返回 DeepSeek-Reasonix
SettingsShellSupport.tsx
根目录 / desktop / frontend / src / components / SettingsShellSupport.tsx
1 import { SettingsSelect } from "./SettingsSelect";
2 import { CircleAlert, CircleCheck, ExternalLink, RefreshCw, SquareTerminal, GitBranch, Info } from "lucide-react";
3 import { openExternal } from "../lib/bridge";
4 import { useT } from "../lib/i18n";
5 import { asArray } from "../lib/array";
6 import type { SandboxView, ShellCapabilityView } from "../lib/types";
7 import { CopyButton } from "./CopyButton";
8
9 // The Sandbox settings section's shell surface: interpreter preference, the
10 // current session's bound shell vs what a reload would pick, and diagnostics.
11 // Windows supports explicit Git Bash selection alongside native PowerShell.
12 // Automatic selection keeps its PowerShell preference.
13
14 function effectiveShellLabel(value: string, t: ReturnType<typeof useT>): string {
15 switch (value) {
16 case "git-bash": return t("settings.effectiveShellGitBash");
17 case "pwsh": return t("settings.effectiveShellPwsh");
18 case "powershell": return t("settings.effectiveShellPowershell");
19 case "bash": return t("settings.effectiveShellBash");
20 case "zsh": return t("settings.effectiveShellZsh");
21 case "sh": return t("settings.effectiveShellSh");
22 case "auto": return t("common.auto");
23 default: return value.trim() || t("common.none");
24 }
25 }
26
27 function capabilityLabel(id: string, t: ReturnType<typeof useT>): string {
28 switch (id) {
29 case "git-bash": return t("settings.effectiveShellGitBash");
30 case "powershell": return t("settings.effectiveShellPowershell");
31 case "pwsh": return t("settings.effectiveShellPwsh");
32 case "zsh": return t("settings.shellCapabilityZsh");
33 case "sh": return t("settings.shellCapabilitySh");
34 case "git": return t("settings.gitCapability");
35 default: return t("settings.effectiveShellBash");
36 }
37 }
38
39 function visibleCapabilities(capabilities: ShellCapabilityView[], windows: boolean): ShellCapabilityView[] {
40 return capabilities.filter(({ id }) => windows
41 ? id === "git-bash" || id === "pwsh" || id === "powershell"
42 : id === "bash" || id === "zsh" || id === "sh");
43 }
44
45 function selectedPreference(preference: string, windows: boolean): string {
46 const normalized = preference.trim().toLowerCase();
47 if (windows) return normalized === "bash" || normalized === "pwsh" || normalized === "powershell" ? normalized : "auto";
48 return normalized === "bash" ? "bash" : "auto";
49 }
50
51 function RepairCard({ message, guidance, busy, reloadSession }: {
52 message: string;
53 guidance?: { manager: string; command?: string } | null;
54 busy: boolean;
55 reloadSession: () => void;
56 }) {
57 const t = useT();
58 return (
59 <div className="shell-support__card">
60 <div className="shell-support__hint">{message}</div>
61 {guidance?.command && (
62 <>
63 <div className="shell-support__repair-command">
64 <code>{guidance.command}</code>
65 <CopyButton text={guidance.command} className="btn btn--small" label={t("settings.shellCopyCommand")} />
66 </div>
67 <div className="shell-support__repair-safety">{t("settings.shellRepairCommandHint")}</div>
68 </>
69 )}
70 <div className="shell-support__actions">
71 <button type="button" className="btn btn--small" disabled={busy} onClick={reloadSession}>
72 <RefreshCw size={13} aria-hidden="true" />
73 <span>{t("settings.shellRepairReload")}</span>
74 </button>
75 </div>
76 </div>
77 );
78 }
79
80 function field(label: string, control: React.ReactNode, stacked = false) {
81 return (
82 <div className={`settings-field${stacked ? " settings-field--stacked" : ""}`}>
83 <div className="settings-field__copy">
84 <div className="settings-field__copy-body">
85 <div className="settings-field__label">{label}</div>
86 </div>
87 </div>
88 <div className="settings-field__control">{control}</div>
89 </div>
90 );
91 }
92
93 function DetectionRow({ cap, t }: { cap: ShellCapabilityView; t: ReturnType<typeof useT> }) {
94 return (
95 <tr className={`shell-capability__row${cap.available ? " shell-capability__row--available" : ""}`}>
96 <th scope="row"><span className="shell-capability__name">{cap.id === "git" ? <GitBranch size={16} aria-hidden="true" /> : <SquareTerminal size={16} aria-hidden="true" />}<span>{capabilityLabel(cap.id, t)}{cap.id === "git" && <small> · {t("settings.shellDependency")}</small>}</span></span></th>
97 <td><span className="shell-capability__status">{cap.available ? <CircleCheck size={16} aria-hidden="true" /> : <CircleAlert size={16} aria-hidden="true" />}{t(cap.available ? "settings.shellDetected" : "settings.shellNotDetected")}</span></td>
98 <td><div className="shell-capability__path"><span className="shell-capability__detail" title={cap.path}>{cap.path || "—"}</span>
99 {cap.path && <CopyButton text={cap.path} label={t("settings.copyEnvironmentPath", { name: capabilityLabel(cap.id, t) })} showInlineLabel={false} />}</div></td>
100 </tr>
101 );
102 }
103
104 export function ShellInterpreterFields({
105 sb,
106 windows,
107 busy,
108 setShell,
109 reloadSession,
110 }: {
111 sb: SandboxView;
112 windows: boolean;
113 busy: boolean;
114 setShell: (prefer: string) => void;
115 reloadSession: () => void;
116 }) {
117 const t = useT();
118 const capabilities = visibleCapabilities(asArray(sb.shellCapabilities), windows);
119 if (windows) capabilities.sort((a, b) => ["git-bash", "pwsh", "powershell"].indexOf(a.id) - ["git-bash", "pwsh", "powershell"].indexOf(b.id));
120 const preference = (sb.shell || "auto").trim().toLowerCase();
121 const selected = selectedPreference(preference, windows);
122 const currentShell = String(sb.effectiveShell || selected);
123 const resolvedShell = String(sb.resolvedShell || selected);
124 const autoLabel = windows ? t("settings.shellAutoWindows") : t("settings.shellAuto");
125 const git = sb.gitCapability ?? null;
126 const bashMissing = !windows && !capabilities.some((capability) => capability.id === "bash" && capability.available);
127 const nativeFallback = sb.resolvedShell === "zsh" || sb.resolvedShell === "sh";
128 const gitBashMissing = windows && !capabilities.some((capability) => capability.id === "git-bash" && capability.available);
129
130 return (
131 <>
132 <div className="sandbox-group sandbox-group--shell">
133 <h3>{t("settings.shellInterpreter")}</h3>
134 <div className="sandbox-shell-choice">
135 <span className="sandbox-shell-current">{t("settings.shellCurrent")}<strong>{effectiveShellLabel(currentShell, t)}</strong></span>
136 <SettingsSelect className="mem-select set-grow" value={preference} selectedLabel={preference === selected ? undefined : autoLabel} disabled={busy} onValueChange={(value) => setShell(value)}>
137 <option value="auto">{autoLabel}</option>
138 {windows ? (
139 <>
140 <option value="bash">{t("settings.effectiveShellGitBash")}</option>
141 <option value="pwsh">{t("settings.shellPwsh")}</option>
142 <option value="powershell">{t("settings.shellPowershell")}</option>
143 </>
144 ) : <option value="bash">{t("settings.shellBash")}</option>}
145 </SettingsSelect>
146 </div>
147 {sb.shellReloadRequired && field(t("settings.resolvedShell"),
148 <div className="settings-readonly-field">
149 {effectiveShellLabel(resolvedShell, t)}
150 {sb.shellReloadRequired && <button type="button" className="btn btn--small set-shell-reload" disabled={busy} onClick={reloadSession}>
151 <RefreshCw size={13} aria-hidden="true" />
152 <span>{t("settings.shellReloadNow")}</span>
153 </button>}
154 </div>)}
155 </div>
156 <div className="sandbox-group sandbox-group--environment">
157 <h3>{t("settings.shellDetection")}</h3>
158 <p className="sandbox-group__hint">{t("settings.shellDetectionHint")}</p>
159 <div className="shell-support">
160 <div className="shell-support__detection">
161 <table className="shell-capability-table" aria-label={t("settings.shellDetection")}>
162 <thead><tr><th scope="col">{t("settings.environmentName")}</th><th scope="col">{t("settings.environmentStatus")}</th><th scope="col">{t("settings.environmentPath")}</th></tr></thead>
163 <tbody>
164 {capabilities.map((capability) => <DetectionRow key={capability.id} cap={capability} t={t} />)}
165 {git && <DetectionRow cap={git} t={t} />}
166 </tbody>
167 </table>
168 </div>
169 {gitBashMissing && (
170 <div className="shell-support__card">
171 <div className="shell-support__hint">{t("settings.shellInstallManualNotice")}</div>
172 <div className="shell-support__actions">
173 <button type="button" className="btn btn--small" onClick={() => void openExternal("https://git-scm.com/download/win")}>
174 <ExternalLink size={14} aria-hidden="true" />
175 <span>{t("settings.shellInstallManualLink")}</span>
176 </button>
177 <button type="button" className="btn btn--small" disabled={busy} onClick={reloadSession}>
178 <RefreshCw size={13} aria-hidden="true" />
179 <span>{t("settings.shellRepairReload")}</span>
180 </button>
181 </div>
182 </div>
183 )}
184 {!windows && bashMissing && !nativeFallback && (
185 <RepairCard message={t("settings.shellBashManualRepair")} guidance={sb.shellRepairGuidance ?? null} busy={busy} reloadSession={reloadSession} />
186 )}
187 {git && !git.available && !windows && (
188 <RepairCard message={t("settings.gitManualRepair")} guidance={sb.gitRepairGuidance ?? null} busy={busy} reloadSession={reloadSession} />
189 )}
190 </div>
191 <p className="sandbox-boundary"><Info size={16} aria-hidden="true" /><span>{t(windows ? "settings.sandboxBoundaryHintWindows" : "settings.sandboxBoundaryHint")}</span></p>
192 </div>
193 </>
194 );
195 }
196
196 lines Plain Text