dsh-bash-terminal-ts 0.7.1 → 0.7.2-compat.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.core.js +127 -95
- package/dist/client.js +127 -95
- package/dsh.plugin.json +22 -22
- package/lib/client.js +127 -95
- package/lib/index.js +101 -0
- package/package.json +121 -125
- package/src/client.tsx +153 -147
- package/src/index.ts +105 -0
package/src/client.tsx
CHANGED
|
@@ -1,35 +1,26 @@
|
|
|
1
|
-
// dsh-bash-terminal-ts client plugin: a "Default terminal"
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// dsh-bash-terminal-ts client plugin (compat-legacy): a "Default terminal"
|
|
2
|
+
// preference row contributed to the family settings tab (dsh-family.tab,
|
|
3
|
+
// hosted by dsh-thinking-levels), with a shell.overlay floating-gear fallback
|
|
4
|
+
// for hosts without any settings surface (0.1.0/0.1.1).
|
|
4
5
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
6
|
+
// compat-legacy rewiring vs upstream 0.7.1:
|
|
7
|
+
// - no @deepseek-ai/dsh-client-store / dsh-client-ui-primitives requires:
|
|
8
|
+
// 0.1.0/0.1.1 have no browser module table and 0.1.2 is unverified; the
|
|
9
|
+
// row reads/writes defaultShell through the host config bridge
|
|
10
|
+
// (GET/POST /api/bash-terminal/config) which works on every line.
|
|
11
|
+
// - ui-primitives Menu/Icon replaced by a native select (same capsule CSS).
|
|
12
|
+
// - every surface registration is independently guarded (one unknown slot
|
|
13
|
+
// name on one line must not take down the rest).
|
|
13
14
|
|
|
14
|
-
import { useState } from "react";
|
|
15
|
-
import { defineStore } from "@deepseek-ai/dsh-client-store";
|
|
16
|
-
import { IconChevronDownOutlineMedium, Menu } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
15
|
+
import { useEffect, useState } from "react";
|
|
17
16
|
|
|
18
17
|
const SETTINGS_NS = "settings.bash-terminal";
|
|
19
|
-
/** Entry id of this plugin in the active profile (cordis.patch.yml). */
|
|
20
|
-
const ENTRY_ID = "tool-bash-terminal";
|
|
21
18
|
const SHELLS = ["powershell", "gitbash", "msys2", "wsl"] as const;
|
|
22
19
|
|
|
23
20
|
// Injected once when the browser loads the bundle (node tests guard on document).
|
|
24
21
|
const ROW_CSS =
|
|
25
|
-
".btRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}"
|
|
26
|
-
|
|
27
|
-
".btTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}" +
|
|
28
|
-
".btDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}" +
|
|
29
|
-
".btSelector{background:var(--dsw-alias-bg-module-platform);height:36px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:18px;align-items:center;gap:12px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}" +
|
|
30
|
-
".btSelector:hover{background:var(--dsw-alias-interactive-bg-hover)}" +
|
|
31
|
-
".btChevron{flex:none}";
|
|
32
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"bash-terminal-row\"]") === null) {
|
|
22
|
+
".btRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}.btRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}.btTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.btDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.btSelector{background:var(--dsw-alias-bg-module-platform);height:36px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:18px;align-items:center;gap:12px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.btSelector:hover{background:var(--dsw-alias-interactive-bg-hover)}.btGear{position:fixed;right:16px;bottom:110px;z-index:10000;width:40px;height:40px;border-radius:50%;border:1px solid rgba(127,127,127,.4);background:rgba(30,30,32,.85);color:inherit;cursor:pointer;font-size:17px;box-shadow:0 4px 16px rgba(0,0,0,.35)}.btOverlay{position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,.45)}.btPanel{position:fixed;top:6vh;left:50%;transform:translateX(-50%);width:min(760px,92vw);max-height:84vh;overflow-y:auto;z-index:10001;background:var(--dsw-alias-bg-module-platform,rgba(30,30,32,.92));border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.3));border-radius:14px;padding:20px;color:var(--dsw-alias-label-primary)}";
|
|
23
|
+
if (typeof document !== "undefined" && document.querySelector('style[data-plugin-css="bash-terminal-row"]') === null) {
|
|
33
24
|
const tag = document.createElement("style");
|
|
34
25
|
tag.dataset.plugin = "dsh-bash-terminal-ts";
|
|
35
26
|
tag.dataset.pluginCss = "bash-terminal-row";
|
|
@@ -44,7 +35,7 @@ const zh: Record<string, string> = {
|
|
|
44
35
|
"shell.gitbash": "Git Bash",
|
|
45
36
|
"shell.msys2": "MSYS2",
|
|
46
37
|
"shell.wsl": "WSL",
|
|
47
|
-
"shell.unavailable": "
|
|
38
|
+
"shell.unavailable": "设置面不可用:宿主配置桥未响应(检查宿主是否为 compat 版)"
|
|
48
39
|
};
|
|
49
40
|
const en: Record<string, string> = {
|
|
50
41
|
"shell.title": "Default terminal",
|
|
@@ -53,7 +44,7 @@ const en: Record<string, string> = {
|
|
|
53
44
|
"shell.gitbash": "Git Bash",
|
|
54
45
|
"shell.msys2": "MSYS2",
|
|
55
46
|
"shell.wsl": "WSL",
|
|
56
|
-
"shell.unavailable": "Settings unavailable: the host
|
|
47
|
+
"shell.unavailable": "Settings unavailable: the host config bridge did not respond (is the host a compat build?)"
|
|
57
48
|
};
|
|
58
49
|
const fr: Record<string, string> = {
|
|
59
50
|
"shell.title": "Terminal par défaut",
|
|
@@ -62,73 +53,104 @@ const fr: Record<string, string> = {
|
|
|
62
53
|
"shell.gitbash": "Git Bash",
|
|
63
54
|
"shell.msys2": "MSYS2",
|
|
64
55
|
"shell.wsl": "WSL",
|
|
65
|
-
"shell.unavailable": "Paramètres indisponibles
|
|
56
|
+
"shell.unavailable": "Paramètres indisponibles: le pont de configuration de l'hôte n'a pas répondu"
|
|
66
57
|
};
|
|
67
58
|
const de: Record<string, string> = {
|
|
68
|
-
"shell.title": "
|
|
69
|
-
"shell.description": "Terminal
|
|
59
|
+
"shell.title": "Standardterminal",
|
|
60
|
+
"shell.description": "Terminal für den Shell-Befehl",
|
|
70
61
|
"shell.powershell": "PowerShell",
|
|
71
62
|
"shell.gitbash": "Git Bash",
|
|
72
63
|
"shell.msys2": "MSYS2",
|
|
73
64
|
"shell.wsl": "WSL",
|
|
74
|
-
"shell.unavailable": "Einstellungen nicht verfügbar:
|
|
65
|
+
"shell.unavailable": "Einstellungen nicht verfügbar: Die Konfigurationsbrücke des Hosts hat nicht geantwortet"
|
|
75
66
|
};
|
|
76
67
|
const it: Record<string, string> = {
|
|
77
68
|
"shell.title": "Terminale predefinito",
|
|
78
|
-
"shell.description": "Terminale usato
|
|
69
|
+
"shell.description": "Terminale usato dal comando shell",
|
|
79
70
|
"shell.powershell": "PowerShell",
|
|
80
71
|
"shell.gitbash": "Git Bash",
|
|
81
72
|
"shell.msys2": "MSYS2",
|
|
82
73
|
"shell.wsl": "WSL",
|
|
83
|
-
"shell.unavailable": "Impostazioni non disponibili:
|
|
74
|
+
"shell.unavailable": "Impostazioni non disponibili: il bridge di configurazione dell'host non ha risposto"
|
|
84
75
|
};
|
|
85
76
|
const ru: Record<string, string> = {
|
|
86
77
|
"shell.title": "Терминал по умолчанию",
|
|
87
|
-
"shell.description": "
|
|
78
|
+
"shell.description": "Терминал для команд оболочки",
|
|
88
79
|
"shell.powershell": "PowerShell",
|
|
89
80
|
"shell.gitbash": "Git Bash",
|
|
90
81
|
"shell.msys2": "MSYS2",
|
|
91
82
|
"shell.wsl": "WSL",
|
|
92
|
-
"shell.unavailable": "Настройки недоступны:
|
|
83
|
+
"shell.unavailable": "Настройки недоступны: мост конфигурации хоста не ответил"
|
|
93
84
|
};
|
|
94
85
|
const es: Record<string, string> = {
|
|
95
|
-
"shell.title": "Terminal
|
|
96
|
-
"shell.description": "Terminal
|
|
86
|
+
"shell.title": "Terminal predeterminada",
|
|
87
|
+
"shell.description": "Terminal usada por el comando shell",
|
|
97
88
|
"shell.powershell": "PowerShell",
|
|
98
89
|
"shell.gitbash": "Git Bash",
|
|
99
90
|
"shell.msys2": "MSYS2",
|
|
100
91
|
"shell.wsl": "WSL",
|
|
101
|
-
"shell.unavailable": "Configuración no disponible: el
|
|
92
|
+
"shell.unavailable": "Configuración no disponible: el puente de configuración del host no respondió"
|
|
102
93
|
};
|
|
103
94
|
|
|
104
|
-
|
|
95
|
+
const zhDictTitle = (): string => zh["shell.title"] ?? "Terminal";
|
|
105
96
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
97
|
+
async function bridgeConfig(): Promise<{ defaultShell?: string } | null> {
|
|
98
|
+
try {
|
|
99
|
+
const r = await fetch("/api/bash-terminal/config", {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "content-type": "application/json" },
|
|
102
|
+
body: "{}",
|
|
103
|
+
});
|
|
104
|
+
const d = await r.json();
|
|
105
|
+
return d && d.ok ? (d.value as { defaultShell?: string }) : null;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
112
109
|
}
|
|
113
110
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
async function bridgeWrite(value: string): Promise<boolean> {
|
|
112
|
+
try {
|
|
113
|
+
const r = await fetch("/api/bash-terminal/config", {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: { "content-type": "application/json" },
|
|
116
|
+
body: JSON.stringify({ defaultShell: value }),
|
|
117
|
+
});
|
|
118
|
+
const d = await r.json();
|
|
119
|
+
return !!(d && d.ok);
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
const status =
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
/** 默认终端选择行:配置读写全部走宿主 config bridge(六线统一)。 */
|
|
126
|
+
function ShellPreferenceRow() {
|
|
127
|
+
const [shell, setShell] = useState("powershell");
|
|
128
|
+
const [status, setStatus] = useState<"loading" | "ready" | "unavailable">("loading");
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
let alive = true;
|
|
131
|
+
bridgeConfig().then((v) => {
|
|
132
|
+
if (!alive) return;
|
|
133
|
+
if (v && v.defaultShell) {
|
|
134
|
+
setShell(v.defaultShell);
|
|
135
|
+
setStatus("ready");
|
|
136
|
+
} else {
|
|
137
|
+
setStatus("unavailable");
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return () => {
|
|
141
|
+
alive = false;
|
|
142
|
+
};
|
|
143
|
+
}, []);
|
|
144
|
+
const change = (value: string): void => {
|
|
145
|
+
setShell(value);
|
|
146
|
+
void bridgeWrite(value);
|
|
147
|
+
};
|
|
126
148
|
if (status === "unavailable") {
|
|
127
149
|
return (
|
|
128
150
|
<div className="btRow">
|
|
129
151
|
<div className="btRowText">
|
|
130
|
-
<div className="btTitle">{
|
|
131
|
-
<div className="btDesc">{
|
|
152
|
+
<div className="btTitle">{zh["shell.title"]}</div>
|
|
153
|
+
<div className="btDesc">{zh["shell.unavailable"]}</div>
|
|
132
154
|
</div>
|
|
133
155
|
</div>
|
|
134
156
|
);
|
|
@@ -136,114 +158,98 @@ function ShellPreferenceRow({ t, useStore, setShell }: ShellPreferenceRowProps)
|
|
|
136
158
|
return (
|
|
137
159
|
<div className="btRow">
|
|
138
160
|
<div className="btRowText">
|
|
139
|
-
<div className="btTitle">{
|
|
140
|
-
<div className="btDesc">{
|
|
161
|
+
<div className="btTitle">{zh["shell.title"]}</div>
|
|
162
|
+
<div className="btDesc">{zh["shell.description"]}</div>
|
|
141
163
|
</div>
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
anchor={
|
|
154
|
-
<button
|
|
155
|
-
type="button"
|
|
156
|
-
className="btSelector"
|
|
157
|
-
aria-haspopup="menu"
|
|
158
|
-
aria-expanded={open}
|
|
159
|
-
disabled={!writable}
|
|
160
|
-
onClick={() => setOpen(!open)}
|
|
161
|
-
style={!writable ? { opacity: 0.5, cursor: "not-allowed" } : undefined}
|
|
162
|
-
>
|
|
163
|
-
{t("shell." + shell)}
|
|
164
|
-
<IconChevronDownOutlineMedium className="btChevron" />
|
|
165
|
-
</button>
|
|
166
|
-
}
|
|
167
|
-
/>
|
|
164
|
+
<select
|
|
165
|
+
className="btSelector"
|
|
166
|
+
value={shell}
|
|
167
|
+
onChange={(e) => change((e.target as HTMLSelectElement).value)}
|
|
168
|
+
>
|
|
169
|
+
{SHELLS.map((id) => (
|
|
170
|
+
<option key={id} value={id}>
|
|
171
|
+
{zh["shell." + id] ?? id}
|
|
172
|
+
</option>
|
|
173
|
+
))}
|
|
174
|
+
</select>
|
|
168
175
|
</div>
|
|
169
176
|
);
|
|
170
177
|
}
|
|
171
178
|
|
|
172
|
-
interface SettingsSnapshot {
|
|
173
|
-
status: "loading" | "ready" | "unavailable";
|
|
174
|
-
value?: { defaultShell?: string };
|
|
175
|
-
revision: number | undefined;
|
|
176
|
-
writable: boolean;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
interface ConfigFormBinding {
|
|
180
|
-
getSnapshot(): SettingsSnapshot;
|
|
181
|
-
subscribe(fn: () => void): () => void;
|
|
182
|
-
set(field: string, value: string): Promise<boolean>;
|
|
183
|
-
unset(field: string): Promise<boolean>;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
179
|
interface ClientContext {
|
|
187
180
|
effect(fn: () => unknown, name?: string): unknown;
|
|
188
181
|
locale: { register(ns: string, dicts: Record<string, Record<string, string>>): unknown };
|
|
189
|
-
configForms: { get(entryId: string): ConfigFormBinding };
|
|
190
182
|
slots: {
|
|
191
183
|
inject(slot: string, fn: () => unknown, name?: string): void;
|
|
192
184
|
register(options: Record<string, unknown>, Component: unknown): Record<string, unknown>;
|
|
193
185
|
};
|
|
194
186
|
}
|
|
195
187
|
|
|
196
|
-
|
|
197
|
-
|
|
188
|
+
/** 无设置页宿主(0.1.0/0.1.1)兜底:浮窗齿轮 + 固定定位面板承载设置行。 */
|
|
189
|
+
function SettingsOverlayEntry() {
|
|
190
|
+
const [open, setOpen] = useState(false);
|
|
191
|
+
return (
|
|
192
|
+
<div style={{ position: "fixed", right: "16px", bottom: "110px", zIndex: 10000 }}>
|
|
193
|
+
{open ? <div className="btOverlay" onClick={() => setOpen(false)} /> : null}
|
|
194
|
+
{open ? (
|
|
195
|
+
<div className="btPanel">
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
className="btSelector"
|
|
199
|
+
style={{ float: "right", height: "30px", fontSize: "12px", marginBottom: "8px" }}
|
|
200
|
+
onClick={() => setOpen(false)}
|
|
201
|
+
>
|
|
202
|
+
✕
|
|
203
|
+
</button>
|
|
204
|
+
<ShellPreferenceRow />
|
|
205
|
+
</div>
|
|
206
|
+
) : null}
|
|
207
|
+
<button type="button" className="btGear" title={zhDictTitle()} onClick={() => setOpen(!open)}>
|
|
208
|
+
⚙
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
198
212
|
}
|
|
199
213
|
|
|
200
|
-
/** 账本 label:从 zh/en 字典取「默认终端」标题(无宿主 locale 依赖)。 */
|
|
201
|
-
const zhDictTitle = (): string => (zh as Record<string, string>)["shell.title"] ?? "终端";
|
|
202
|
-
|
|
203
214
|
export function apply(ctx: ClientContext): void {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const store = defineStore<RowState, { sync: (draft: RowState, shell?: string, revision?: number, writable?: boolean, status?: RowState["status"]) => void }>({
|
|
210
|
-
init: (): RowState => ({ shell: "powershell", revision: -1, writable: false, status: "loading" }),
|
|
211
|
-
actions: {
|
|
212
|
-
sync: (d, shell, revision, writable, status) => {
|
|
213
|
-
if (revision !== undefined && revision <= d.revision) return;
|
|
214
|
-
if (shell !== undefined) d.shell = shell;
|
|
215
|
-
if (revision !== undefined) d.revision = revision;
|
|
216
|
-
if (writable !== undefined) d.writable = writable;
|
|
217
|
-
if (status !== undefined) d.status = status;
|
|
218
|
-
}
|
|
215
|
+
const safe = (tag: string, fn: () => void): void => {
|
|
216
|
+
try {
|
|
217
|
+
fn();
|
|
218
|
+
} catch (e) {
|
|
219
|
+
console.warn(`[dsh-bash-terminal-ts] client surface '${tag}' failed:`, e);
|
|
219
220
|
}
|
|
220
|
-
});
|
|
221
|
-
let bound: RowStoreActions | undefined;
|
|
222
|
-
const push = (snap: SettingsSnapshot): void => {
|
|
223
|
-
bound?.sync(snap.value?.defaultShell, snap.revision, snap.writable, snap.status);
|
|
224
221
|
};
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
222
|
+
safe("locale", () => {
|
|
223
|
+
ctx.effect(
|
|
224
|
+
() => ctx.locale.register(SETTINGS_NS, { zh, en, fr, de, it, ru, es }),
|
|
225
|
+
"bash-terminal: settings dictionaries",
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
safe("dsh-family.tab", () => {
|
|
229
|
+
ctx.slots.inject(
|
|
230
|
+
"dsh-family.tab",
|
|
231
|
+
() =>
|
|
232
|
+
ctx.slots.register(
|
|
233
|
+
{
|
|
234
|
+
name: "dsh-family.tab",
|
|
235
|
+
id: "bash-terminal-shell",
|
|
236
|
+
order: 60,
|
|
237
|
+
label: zhDictTitle,
|
|
238
|
+
locale: SETTINGS_NS,
|
|
239
|
+
inject: () => ({}),
|
|
240
|
+
},
|
|
241
|
+
() => <ShellPreferenceRow />,
|
|
242
|
+
),
|
|
243
|
+
"bash-terminal: settings row",
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
safe("shell.overlay", () => {
|
|
247
|
+
ctx.slots.inject(
|
|
248
|
+
"shell.overlay",
|
|
249
|
+
() =>
|
|
250
|
+
ctx.slots.register({ name: "shell.overlay", id: "bash-terminal-settings" }, () => (
|
|
251
|
+
<SettingsOverlayEntry />
|
|
252
|
+
)),
|
|
253
|
+
);
|
|
254
|
+
});
|
|
249
255
|
}
|
package/src/index.ts
CHANGED
|
@@ -54,6 +54,8 @@ import { createTerminalRegistry, terminalTool } from "./terminal.js";
|
|
|
54
54
|
|
|
55
55
|
/** Stable Cordis plugin name. */
|
|
56
56
|
export const name = "bash-terminal";
|
|
57
|
+
/** Composition entry id (the `tool-bash-terminal` row the bundle patch declares). */
|
|
58
|
+
const ENTRY_ID = "tool-bash-terminal";
|
|
57
59
|
/** Services required before the tool can register. */
|
|
58
60
|
export const inject = ["tools", "systemPrompt", "shellEnv", "subprocess", "settings", "sandbox", "sandboxPolicy"];
|
|
59
61
|
|
|
@@ -677,7 +679,13 @@ export function apply(ctx: BashTerminalContext, config: Partial<ConfigValues> =
|
|
|
677
679
|
// 0.1.7: `defaultShell` is a `.volatile()` config field — the loader hands
|
|
678
680
|
// `apply` a live ref and the latest value is resolved on every read; there
|
|
679
681
|
// is no settings registration call anymore (removed with the 0.1.7 host).
|
|
682
|
+
// compat-legacy: ≤0.1.6 的宿主没有配置热重载,bridge 写入走这个覆盖值
|
|
683
|
+
// (rc.1+ 上 volatile 与覆盖值同源同写,二者一致)。
|
|
684
|
+
let shellOverride: string | undefined;
|
|
680
685
|
const currentShell = (): string => {
|
|
686
|
+
if (typeof shellOverride === "string" && (SHELLS as readonly string[]).includes(shellOverride)) {
|
|
687
|
+
return shellOverride;
|
|
688
|
+
}
|
|
681
689
|
const v = config.defaultShell as string | VolatileRef<string> | undefined;
|
|
682
690
|
if (typeof v === "string") return v;
|
|
683
691
|
const got = typeof v?.get === "function" ? v.get() : undefined;
|
|
@@ -715,6 +723,103 @@ export function apply(ctx: BashTerminalContext, config: Partial<ConfigValues> =
|
|
|
715
723
|
text: "Use the shell tool for terminal commands: it runs in the terminal the user chose in Settings -> General -> Default terminal (PowerShell, Git Bash, MSYS2, or WSL) and honors the DSH sandbox. Prefer it over the pwsh tool for everyday commands; keep the pwsh tool for cases that specifically need the sandboxed PowerShell surface."
|
|
716
724
|
});
|
|
717
725
|
|
|
726
|
+
// compat-legacy: 六线统一配置通道(≤0.1.6 无 client-store 模块表,client 走本
|
|
727
|
+
// bridge 读写 defaultShell)。写路径 = settings.mutate;≤0.1.6 的 loader 不会自动
|
|
728
|
+
// 注册命名空间,复用 perm-gate 自门控(not registered → register → 重试);
|
|
729
|
+
// rc.1+ 命名空间由 loader 占用,该分支自然不命中。GET 带回当前值与可选集。
|
|
730
|
+
// 宿主 context 类型未收录 inject/局部服务形状,compat 桥按宽化面调用。
|
|
731
|
+
const compatCtx = ctx as unknown as { inject: (deps: string[], fn: (svcCtx: any) => void) => void };
|
|
732
|
+
compatCtx.inject(["webServer", "settings"], (svcCtx: any) => {
|
|
733
|
+
const svc = svcCtx.settings;
|
|
734
|
+
const writeShell = async (value: string): Promise<void> => {
|
|
735
|
+
if (typeof svc.mutate !== "function") {
|
|
736
|
+
// 显式降级:该线 settings 服务无 mutate(理论仅最老线),覆盖值仅本进程
|
|
737
|
+
// 生效;持久化请改 cordis.patch.yml 的 defaultShell。
|
|
738
|
+
throw new Error(
|
|
739
|
+
"settings service exposes no mutate on this host line - defaultShell applies for this process only; persist via cordis.patch.yml"
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
shellOverride = value;
|
|
743
|
+
try {
|
|
744
|
+
await svc.mutate(ENTRY_ID, [{ op: "set", path: ["defaultShell"], value }]);
|
|
745
|
+
} catch (e) {
|
|
746
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
747
|
+
if (!/not registered/i.test(message) || typeof (svc as Record<string, unknown>).register !== "function") {
|
|
748
|
+
throw e;
|
|
749
|
+
}
|
|
750
|
+
console.warn(`[dsh-bash-terminal-ts] legacy line: registering settings namespace ${ENTRY_ID}`);
|
|
751
|
+
(svc as { register: (ns: string, schema: unknown, opts?: unknown) => unknown }).register(
|
|
752
|
+
ENTRY_ID,
|
|
753
|
+
Config,
|
|
754
|
+
{ base: { defaultShell: value } }
|
|
755
|
+
);
|
|
756
|
+
await svc.mutate(ENTRY_ID, [{ op: "set", path: ["defaultShell"], value }]);
|
|
757
|
+
console.warn("[dsh-bash-terminal-ts] legacy settings namespace registered");
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
svcCtx.effect(
|
|
761
|
+
() => {
|
|
762
|
+
const json = (res: unknown, status: number, body: unknown): void => {
|
|
763
|
+
(res as { writeHead: (s: number, h: Record<string, string>) => void; end: (b: string) => void }).writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
764
|
+
(res as { end: (b: string) => void }).end(JSON.stringify(body));
|
|
765
|
+
};
|
|
766
|
+
const handler = (req: any, res: any): void => {
|
|
767
|
+
const addr = req.socket?.remoteAddress ?? "";
|
|
768
|
+
if (addr !== "127.0.0.1" && addr !== "::1" && addr !== "::ffff:127.0.0.1") {
|
|
769
|
+
json(res, 403, { ok: false, code: "loopback-only" });
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (req.method === "GET") {
|
|
773
|
+
// 缝内省:compat 验证用——当前线 ctx.subprocess 的真实能力面
|
|
774
|
+
const seam = ctx as unknown as { subprocess?: Record<string, unknown> };
|
|
775
|
+
const sp = seam.subprocess as { spawn?: unknown; spawnTerminal?: unknown } | undefined;
|
|
776
|
+
json(res, 200, {
|
|
777
|
+
ok: true,
|
|
778
|
+
value: {
|
|
779
|
+
defaultShell: currentShell(),
|
|
780
|
+
shells: SHELLS,
|
|
781
|
+
seam: { spawn: typeof sp?.spawn, spawnTerminal: typeof sp?.spawnTerminal },
|
|
782
|
+
},
|
|
783
|
+
});
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (req.method !== "POST") {
|
|
787
|
+
json(res, 405, { ok: false, code: "method-not-allowed" });
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const chunks: Buffer[] = [];
|
|
791
|
+
let size = 0;
|
|
792
|
+
req.on("data", (c: Buffer) => {
|
|
793
|
+
size += c.length;
|
|
794
|
+
if (size <= 65536) chunks.push(c);
|
|
795
|
+
});
|
|
796
|
+
req.on("end", () => {
|
|
797
|
+
let body: { defaultShell?: unknown } = {};
|
|
798
|
+
try {
|
|
799
|
+
body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
800
|
+
} catch {
|
|
801
|
+
json(res, 400, { ok: false, code: "bad-json" });
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const value = typeof body.defaultShell === "string" ? body.defaultShell : "";
|
|
805
|
+
if (!(SHELLS as readonly string[]).includes(value)) {
|
|
806
|
+
json(res, 400, { ok: false, code: "invalid-shell" });
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
void writeShell(value)
|
|
810
|
+
.then(() => json(res, 200, { ok: true, value: { defaultShell: value } }))
|
|
811
|
+
.catch((e: unknown) => json(res, 500, { ok: false, code: "write-failed", message: e instanceof Error ? e.message : String(e) }));
|
|
812
|
+
});
|
|
813
|
+
};
|
|
814
|
+
const registered = [
|
|
815
|
+
svcCtx.webServer.register({ kind: "exact", path: "/api/bash-terminal/config", handler }),
|
|
816
|
+
];
|
|
817
|
+
return () => {
|
|
818
|
+
for (const dispose of registered) dispose();
|
|
819
|
+
};
|
|
820
|
+
}, "bash-terminal: compat config bridge");
|
|
821
|
+
});
|
|
822
|
+
|
|
718
823
|
const toolName = "shell";
|
|
719
824
|
const terminalRegistry = createTerminalRegistry(ctx);
|
|
720
825
|
ctx.tools.register(terminalTool(ctx, terminalRegistry, paths, () => currentShell()));
|