返回 DeepSeek-Reasonix
SettingsShellSupport.tsx
根目录 / desktop / frontend / src / components / SettingsShellSupport.tsx
1 import { SettingsSelect } from "./SettingsSelect";
2 import { CircleAlert, CircleCheck, RefreshCw } from "lucide-react";
3 import { useT } from "../lib/i18n";
4 import { asArray } from "../lib/array";
5 import type { SandboxView, ShellCapabilityView } from "../lib/types";
6 import { CopyButton } from "./CopyButton";
7
8 // The Sandbox settings section's shell surface: interpreter preference, the
9 // current session's bound shell vs what a reload would pick, and diagnostics.
10 // Windows exposes native PowerShell runtimes only; legacy Bash discovery stays
11 // behind compatibility code and is not presented as a supported Agent runtime.
12
13 function effectiveShellLabel(value: string, t: ReturnType<typeof useT>): string {
14 switch (value) {
15 case "git-bash": return t("settings.effectiveShellGitBash");
16 case "pwsh": return t("settings.effectiveShellPwsh");
17 case "powershell": return t("settings.effectiveShellPowershell");
18 case "bash": return t("settings.effectiveShellBash");
19 case "zsh": return t("settings.effectiveShellZsh");
20 case "sh": return t("settings.effectiveShellSh");
21 case "auto": return t("common.auto");
22 default: return value.trim() || t("common.none");
23 }
24 }
25
26 function capabilityLabel(id: string, t: ReturnType<typeof useT>): string {
27 switch (id) {
28 case "git-bash": return t("settings.effectiveShellGitBash");
29 case "powershell": return t("settings.effectiveShellPowershell");
30 case "pwsh": return t("settings.effectiveShellPwsh");
31 case "zsh": return t("settings.shellCapabilityZsh");
32 case "sh": return t("settings.shellCapabilitySh");
33 case "git": return t("settings.gitCapability");
34 default: return t("settings.effectiveShellBash");
35 }
36 }
37
38 function visibleCapabilities(capabilities: ShellCapabilityView[], windows: boolean): ShellCapabilityView[] {
39 return capabilities.filter(({ id }) => windows
40 ? id === "pwsh" || id === "powershell"
41 : id === "bash" || id === "zsh" || id === "sh");
42 }
43
44 function selectedPreference(preference: string, windows: boolean): string {
45 const normalized = preference.trim().toLowerCase();
46 if (windows) return normalized === "pwsh" || normalized === "powershell" ? normalized : "auto";
47 return normalized === "bash" ? "bash" : "auto";
48 }
49
50 function ShellRuntimeValue({ shell, capabilities, t }: {
51 shell: string;
52 capabilities: ShellCapabilityView[];
53 t: ReturnType<typeof useT>;
54 }) {
55 const capability = capabilities.find(({ id }) => id === shell);
56 return (
57 <div className="shell-runtime">
58 <span>{effectiveShellLabel(shell, t)}</span>
59 {capability?.path && <code>{capability.path}</code>}
60 </div>
61 );
62 }
63
64 function RepairCard({ message, guidance, busy, reloadSession }: {
65 message: string;
66 guidance?: { manager: string; command?: string } | null;
67 busy: boolean;
68 reloadSession: () => void;
69 }) {
70 const t = useT();
71 return (
72 <div className="shell-support__card">
73 <div className="shell-support__hint">{message}</div>
74 {guidance?.command && (
75 <>
76 <div className="shell-support__repair-command">
77 <code>{guidance.command}</code>
78 <CopyButton text={guidance.command} className="btn btn--small" label={t("settings.shellCopyCommand")} />
79 </div>
80 <div className="shell-support__repair-safety">{t("settings.shellRepairCommandHint")}</div>
81 </>
82 )}
83 <div className="shell-support__actions">
84 <button type="button" className="btn btn--small" disabled={busy} onClick={reloadSession}>
85 <RefreshCw size={13} aria-hidden="true" />
86 <span>{t("settings.shellRepairReload")}</span>
87 </button>
88 </div>
89 </div>
90 );
91 }
92
93 function field(label: string, control: React.ReactNode, stacked = false) {
94 return (
95 <div className={`settings-field${stacked ? " settings-field--stacked" : ""}`}>
96 <div className="settings-field__copy">
97 <div className="settings-field__copy-body">
98 <div className="settings-field__label">{label}</div>
99 </div>
100 </div>
101 <div className="settings-field__control">{control}</div>
102 </div>
103 );
104 }
105
106 function DetectionRow({ cap, t }: { cap: ShellCapabilityView; t: ReturnType<typeof useT> }) {
107 return (
108 <div className="shell-capability__row">
109 {cap.available ? <CircleCheck size={14} aria-hidden="true" /> : <CircleAlert size={14} aria-hidden="true" />}
110 <span className="shell-capability__name">{capabilityLabel(cap.id, t)}</span>
111 <span className="shell-capability__detail">
112 {cap.available ? (cap.path ? t("settings.shellDetectedAt", { path: cap.path }) : t("settings.shellDetected")) : t("settings.shellNotDetected")}
113 </span>
114 </div>
115 );
116 }
117
118 export function ShellInterpreterFields({
119 sb,
120 windows,
121 busy,
122 setShell,
123 reloadSession,
124 }: {
125 sb: SandboxView;
126 windows: boolean;
127 busy: boolean;
128 setShell: (prefer: string) => void;
129 reloadSession: () => void;
130 }) {
131 const t = useT();
132 const capabilities = visibleCapabilities(asArray(sb.shellCapabilities), windows);
133 const preference = (sb.shell || "auto").trim().toLowerCase();
134 const selected = selectedPreference(preference, windows);
135 const currentShell = String(sb.effectiveShell || selected);
136 const resolvedShell = String(sb.resolvedShell || selected);
137 const autoLabel = windows ? t("settings.shellAutoWindows") : t("settings.shellAuto");
138
139 return (
140 <>
141 {field(t(windows ? "settings.powershellRuntime" : "settings.shellInterpreter"),
142 <SettingsSelect className="mem-select set-grow" value={preference} selectedLabel={preference === selected ? undefined : autoLabel} disabled={busy} onValueChange={(value) => setShell(value)}>
143 <option value="auto">{autoLabel}</option>
144 {windows ? (
145 <>
146 <option value="pwsh">{t("settings.shellPwsh")}</option>
147 <option value="powershell">{t("settings.shellPowershell")}</option>
148 </>
149 ) : <option value="bash">{t("settings.shellBash")}</option>}
150 </SettingsSelect>)}
151 {field(t("settings.effectiveShell"),
152 <div className="settings-readonly-field"><ShellRuntimeValue shell={currentShell} capabilities={capabilities} t={t} /></div>)}
153 {sb.shellReloadRequired && field(t("settings.resolvedShell"),
154 <div className="settings-readonly-field">
155 <ShellRuntimeValue shell={resolvedShell} capabilities={capabilities} t={t} />
156 <button type="button" className="btn btn--small set-shell-reload" disabled={busy} onClick={reloadSession}>
157 <RefreshCw size={13} aria-hidden="true" />
158 <span>{t("settings.shellReloadNow")}</span>
159 </button>
160 </div>)}
161 </>
162 );
163 }
164
165 export function ShellEnvironmentDetails({
166 sb,
167 windows,
168 busy,
169 effectiveWriteRoots,
170 reloadSession,
171 }: {
172 sb: SandboxView;
173 windows: boolean;
174 busy: boolean;
175 effectiveWriteRoots: string[];
176 reloadSession: () => void;
177 }) {
178 const t = useT();
179 const capabilities = visibleCapabilities(asArray(sb.shellCapabilities), windows);
180 const git = sb.gitCapability ?? null;
181 const bashMissing = !windows && !capabilities.some((capability) => capability.id === "bash" && capability.available);
182 const nativeFallback = sb.resolvedShell === "zsh" || sb.resolvedShell === "sh";
183 const powershellFallback = windows
184 && selectedPreference(sb.shell || "auto", true) === "auto"
185 && sb.resolvedShell === "powershell"
186 && !capabilities.some((capability) => capability.id === "pwsh" && capability.available);
187 const runtimeMissing = windows
188 ? !capabilities.some((capability) => capability.available)
189 : bashMissing && !nativeFallback;
190 const needsAttention = Boolean(sb.shellReloadRequired || runtimeMissing || nativeFallback || powershellFallback);
191
192 return (
193 <details className="runtime-details" open={needsAttention || undefined}>
194 <summary>{t("settings.runtimeDetails")}</summary>
195 <div>
196 {field(t("settings.shellDetection"),
197 <div className="shell-support">
198 <div className="settings-readonly-field shell-support__detection">
199 {capabilities.map((capability) => <DetectionRow key={capability.id} cap={capability} t={t} />)}
200 </div>
201 {!windows && bashMissing && !nativeFallback && (
202 <RepairCard message={t("settings.shellBashManualRepair")} guidance={sb.shellRepairGuidance ?? null} busy={busy} reloadSession={reloadSession} />
203 )}
204 </div>, true)}
205 {git && field(t("settings.dependencyDetection"),
206 <div className="shell-support">
207 <div className="settings-readonly-field shell-support__detection">
208 <DetectionRow cap={git} t={t} />
209 </div>
210 {!git.available && !windows && (
211 <RepairCard message={t("settings.gitManualRepair")} guidance={sb.gitRepairGuidance ?? null} busy={busy} reloadSession={reloadSession} />
212 )}
213 </div>, true)}
214 {field(t("settings.effectiveWriteRoots"),
215 <div className="set-rules set-rules--readonly">
216 <div className="set-rules__chips">
217 {effectiveWriteRoots.length === 0 && <span className="mem-empty">{t("settings.noEffectiveWriteRoots")}</span>}
218 {effectiveWriteRoots.map((path, index) => (
219 <span className="set-rule set-rule--path" key={`${path}-${index}`}>{path}</span>
220 ))}
221 </div>
222 </div>, true)}
223 <footer>
224 <button type="button" className="btn btn--small" disabled={busy} onClick={reloadSession}>
225 <RefreshCw size={13} aria-hidden="true" />
226 <span>{t("settings.reloadSessionConfig")}</span>
227 </button>
228 </footer>
229 </div>
230 </details>
231 );
232 }
233
233 lines Plain Text