@wzwjustdoit/dsh-plugin-git-bash 0.0.0-stage → 0.4.0

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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -2
  3. package/client.js +736 -0
  4. package/cordis.patch.yml +27 -0
  5. package/lib/agents.d.ts +5 -0
  6. package/lib/agents.d.ts.map +1 -0
  7. package/lib/agents.js +56 -0
  8. package/lib/agents.js.map +1 -0
  9. package/lib/discovery.d.ts +21 -0
  10. package/lib/discovery.d.ts.map +1 -0
  11. package/lib/discovery.js +122 -0
  12. package/lib/discovery.js.map +1 -0
  13. package/lib/index.d.ts +55 -0
  14. package/lib/index.d.ts.map +1 -0
  15. package/lib/index.js +369 -0
  16. package/lib/index.js.map +1 -0
  17. package/native/CMakeLists.txt +58 -0
  18. package/native/bin/win32-x64/msys-token-guard-hook.dll +0 -0
  19. package/native/bin/win32-x64/msys-token-guard.exe +0 -0
  20. package/native/msys-token-guard-hook.cpp +470 -0
  21. package/native/msys-token-guard.cpp +671 -0
  22. package/native/vendor/detours/LICENSE.md +23 -0
  23. package/native/vendor/detours/README.dsh.md +13 -0
  24. package/native/vendor/detours/creatwth.cpp +1577 -0
  25. package/native/vendor/detours/detours.cpp +2364 -0
  26. package/native/vendor/detours/detours.h +1059 -0
  27. package/native/vendor/detours/detver.h +27 -0
  28. package/native/vendor/detours/disasm.cpp +4236 -0
  29. package/native/vendor/detours/image.cpp +2251 -0
  30. package/native/vendor/detours/modules.cpp +929 -0
  31. package/native/vendor/detours/uimports.cpp +269 -0
  32. package/package.json +120 -4
  33. package/scripts/build-native.mjs +35 -0
  34. package/scripts/dsh.desktop.startup/README.md +27 -0
  35. package/scripts/dsh.desktop.startup/dsh.desktop.startup.gitbash.bat +28 -0
  36. package/scripts/dsh.desktop.startup/dsh.desktop.startup.gitbash.sh +46 -0
  37. package/scripts/dsh.desktop.startup/dsh.desktop.startup.gitbash.vbs +0 -0
  38. package/scripts/dsh.web.startup/README.md +33 -0
  39. package/scripts/dsh.web.startup/dsh.web.startup.gitbash.bat +28 -0
  40. package/scripts/dsh.web.startup/dsh.web.startup.gitbash.sh +48 -0
  41. package/scripts/dsh.web.startup/dsh.web.startup.gitbash.vbs +0 -0
package/client.js ADDED
@@ -0,0 +1,736 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@wzwjustdoit/dsh-plugin-git-bash",
3
+ factory: (require) => {
4
+ const module = { exports: {} };
5
+ const {
6
+ createElement,
7
+ useEffect,
8
+ useState,
9
+ useSyncExternalStore,
10
+ } = require("react");
11
+ const {
12
+ IconChevronDownOutline14,
13
+ IconFolderOpenOutline16,
14
+ } = require("@deepseek-ai/dsh-client-ui-primitives");
15
+
16
+ const SLOT = "tool.call.toolview";
17
+ const SETTINGS_SLOT = "settings.plugin.item";
18
+ const SETTINGS_TAB_SLOT = "settings.plugins.tab";
19
+ const SETTINGS_NAMESPACE = "shell";
20
+ const LOCALE_NAMESPACE = "git-bash.settings";
21
+ const PRIORITY = -100;
22
+ const WRAP_ATTR = "data-dsh-git-bash-wrap";
23
+ const STYLE_ID = "dsh-plugin-git-bash-command-wrap";
24
+ const SETTINGS_STYLE_ID = "dsh-plugin-git-bash-settings";
25
+ const STYLE_OWNER = {};
26
+ const COMMAND_WRAP_CSS = `
27
+ [${WRAP_ATTR}] [class*="_command_"] {
28
+ white-space: pre-wrap !important;
29
+ overflow-wrap: anywhere !important;
30
+ word-break: normal !important;
31
+ overflow: visible !important;
32
+ text-overflow: clip !important;
33
+ }
34
+ `;
35
+ const SETTINGS_CSS = `
36
+ [data-dsh-git-bash-settings] {
37
+ list-style: none;
38
+ color: var(--dsw-alias-label-primary);
39
+ background: var(--dsw-alias-bg-layer-3);
40
+ border: 1px solid var(--dsw-alias-border-l2);
41
+ border-radius: 8px;
42
+ transition: border-color 0.16s, background 0.16s;
43
+ }
44
+ [data-dsh-git-bash-settings]:hover,
45
+ [data-dsh-git-bash-settings][data-open="true"] {
46
+ border-color: var(--dsw-alias-label-dimmed);
47
+ }
48
+ [data-dsh-git-bash-settings][data-open="true"] {
49
+ background: var(--dsw-alias-bg-layer-2);
50
+ }
51
+ .dgb-settings-header {
52
+ appearance: none;
53
+ width: 100%;
54
+ min-height: 68px;
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 12px;
58
+ padding: 14px 16px;
59
+ color: inherit;
60
+ font: inherit;
61
+ text-align: left;
62
+ cursor: pointer;
63
+ background: transparent;
64
+ border: 0;
65
+ border-radius: 8px;
66
+ }
67
+ .dgb-settings-header:focus-visible,
68
+ .dgb-settings-button:focus-visible,
69
+ .dgb-settings-reset:focus-visible,
70
+ .dgb-settings-input:focus-visible {
71
+ outline: 2px solid var(--dsw-alias-brand-primary);
72
+ outline-offset: 2px;
73
+ }
74
+ .dgb-settings-head-text {
75
+ min-width: 0;
76
+ display: flex;
77
+ flex: 1;
78
+ flex-direction: column;
79
+ gap: 4px;
80
+ }
81
+ .dgb-settings-title {
82
+ font-size: 15px;
83
+ font-weight: 600;
84
+ line-height: 1.4;
85
+ }
86
+ .dgb-settings-description {
87
+ color: var(--dsw-alias-label-tertiary);
88
+ font-size: 13px;
89
+ line-height: 1.5;
90
+ overflow-wrap: anywhere;
91
+ }
92
+ .dgb-settings-overridden {
93
+ flex: none;
94
+ padding: 1px 8px;
95
+ color: var(--dsw-alias-label-secondary);
96
+ background: var(--dsw-alias-bg-module-platform);
97
+ border-radius: 999px;
98
+ font-size: 11px;
99
+ font-weight: 500;
100
+ line-height: 17px;
101
+ white-space: nowrap;
102
+ }
103
+ .dgb-settings-chevron {
104
+ flex: none;
105
+ color: var(--dsw-alias-label-tertiary);
106
+ transition: transform 0.16s;
107
+ }
108
+ .dgb-settings-chevron-open {
109
+ transform: rotate(180deg);
110
+ }
111
+ .dgb-settings-body {
112
+ margin: 0 16px;
113
+ padding: 0 0 8px;
114
+ border-top: 1px solid var(--dsw-alias-border-l2);
115
+ }
116
+ .dgb-settings-readonly,
117
+ .dgb-settings-message,
118
+ .dgb-settings-hint {
119
+ margin: 0;
120
+ color: var(--dsw-alias-label-tertiary);
121
+ font-size: 12px;
122
+ line-height: 1.5;
123
+ }
124
+ .dgb-settings-readonly {
125
+ padding-top: 12px;
126
+ }
127
+ .dgb-settings-field {
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 6px;
131
+ padding: 12px 0;
132
+ }
133
+ .dgb-settings-field-head {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 8px;
137
+ }
138
+ .dgb-settings-label {
139
+ min-width: 0;
140
+ flex: 1;
141
+ font-size: 13px;
142
+ font-weight: 500;
143
+ line-height: 1.5;
144
+ }
145
+ .dgb-settings-field-actions {
146
+ display: inline-flex;
147
+ align-items: center;
148
+ gap: 8px;
149
+ }
150
+ .dgb-settings-reset {
151
+ padding: 0;
152
+ color: var(--dsw-alias-label-secondary);
153
+ font: inherit;
154
+ font-size: 12px;
155
+ line-height: 1.5;
156
+ cursor: pointer;
157
+ background: transparent;
158
+ border: 0;
159
+ }
160
+ .dgb-settings-reset:hover:not(:disabled) {
161
+ color: var(--dsw-alias-label-primary);
162
+ }
163
+ .dgb-settings-input-row {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 8px;
167
+ }
168
+ .dgb-settings-input {
169
+ min-width: 0;
170
+ height: 34px;
171
+ flex: 1 1 260px;
172
+ padding: 0 12px;
173
+ color: var(--dsw-alias-label-primary);
174
+ font: inherit;
175
+ font-size: 13px;
176
+ line-height: 1.5;
177
+ background: var(--dsw-alias-bg-layer-3);
178
+ border: 1px solid var(--dsw-alias-border-l2);
179
+ border-radius: 8px;
180
+ }
181
+ .dgb-settings-input[aria-invalid="true"] {
182
+ border-color: var(--dsw-alias-state-error-primary);
183
+ }
184
+ .dgb-settings-input:disabled,
185
+ .dgb-settings-button:disabled,
186
+ .dgb-settings-reset:disabled {
187
+ color: var(--dsw-alias-label-tertiary);
188
+ cursor: default;
189
+ }
190
+ .dgb-settings-button {
191
+ min-height: 34px;
192
+ display: inline-flex;
193
+ flex: none;
194
+ align-items: center;
195
+ justify-content: center;
196
+ gap: 7px;
197
+ padding: 5px 12px;
198
+ color: var(--dsw-alias-label-secondary);
199
+ font: inherit;
200
+ font-size: 13px;
201
+ line-height: 1.5;
202
+ cursor: pointer;
203
+ background: transparent;
204
+ border: 1px solid var(--dsw-alias-border-l2);
205
+ border-radius: 8px;
206
+ }
207
+ .dgb-settings-button:hover:not(:disabled) {
208
+ color: var(--dsw-alias-label-primary);
209
+ background: var(--dsw-alias-bg-layer-3);
210
+ }
211
+ .dgb-settings-button-primary {
212
+ color: var(--dsw-alias-label-primary-foreground);
213
+ background: var(--dsw-alias-button-primary-fill);
214
+ border-color: transparent;
215
+ }
216
+ .dgb-settings-button-primary:hover:not(:disabled) {
217
+ color: var(--dsw-alias-label-primary-foreground);
218
+ background: var(--dsw-alias-button-primary-hover);
219
+ }
220
+ .dgb-settings-button-primary:disabled {
221
+ color: var(--dsw-alias-label-tertiary);
222
+ background: var(--dsw-alias-button-primary-dimmed);
223
+ }
224
+ .dgb-settings-error {
225
+ color: var(--dsw-alias-state-error-primary);
226
+ }
227
+ .dgb-settings-field-footer {
228
+ display: flex;
229
+ align-items: center;
230
+ justify-content: flex-end;
231
+ gap: 8px;
232
+ padding: 8px 0 4px;
233
+ }
234
+ .dgb-settings-message {
235
+ min-width: 0;
236
+ flex: 1;
237
+ }
238
+ @media (max-width: 560px) {
239
+ .dgb-settings-header {
240
+ align-items: flex-start;
241
+ }
242
+ .dgb-settings-input-row {
243
+ align-items: stretch;
244
+ flex-direction: column;
245
+ }
246
+ .dgb-settings-input {
247
+ width: 100%;
248
+ flex-basis: 34px;
249
+ }
250
+ .dgb-settings-picker {
251
+ width: 100%;
252
+ }
253
+ }
254
+ `;
255
+
256
+ function installStyle(id, content) {
257
+ if (typeof document === "undefined") return () => {};
258
+ let style = document.getElementById(id);
259
+ if (style === null) {
260
+ style = document.createElement("style");
261
+ style.id = id;
262
+ document.head.appendChild(style);
263
+ }
264
+ style.textContent = content;
265
+ style.__dshGitBashOwner = STYLE_OWNER;
266
+ return () => {
267
+ if (style.__dshGitBashOwner === STYLE_OWNER) style.remove();
268
+ };
269
+ }
270
+
271
+ function installCommandWrapStyle() {
272
+ return installStyle(STYLE_ID, COMMAND_WRAP_CSS);
273
+ }
274
+
275
+ function installSettingsStyle() {
276
+ return installStyle(SETTINGS_STYLE_ID, SETTINGS_CSS);
277
+ }
278
+
279
+ function parseArguments(block) {
280
+ const raw = "kind" in block ? block.call?.argsRaw : block.argsRaw;
281
+ if (typeof raw !== "string" || raw === "") return null;
282
+ try {
283
+ const value = JSON.parse(raw);
284
+ return typeof value === "object" && value !== null ? value : null;
285
+ } catch {
286
+ return null;
287
+ }
288
+ }
289
+
290
+ function flattenContent(content) {
291
+ return content.map((block) => {
292
+ if (block?.type === "text" && typeof block.text === "string") return block.text;
293
+ return JSON.stringify(block, null, 2);
294
+ }).join("\n");
295
+ }
296
+
297
+ function parseTerminalOutput(output) {
298
+ const signal = output.match(/(?:\r?\n)?\[killed by signal: ([^\]\r\n]+)\]\s*$/);
299
+ if (signal !== null) {
300
+ return { output: output.slice(0, signal.index), signal: signal[1] };
301
+ }
302
+ const exit = output.match(/(?:\r?\n)?\[exit code: (-?\d+)\]\s*$/);
303
+ if (exit !== null) {
304
+ return { output: output.slice(0, exit.index), exitCode: Number(exit[1]) };
305
+ }
306
+ return { output };
307
+ }
308
+
309
+ function addNestedTerminalViews(block) {
310
+ const settled = "kind" in block;
311
+ if (block.callView !== null) return block;
312
+ if (settled && (block.resultView !== null || block.isError)) return block;
313
+
314
+ const args = parseArguments(block);
315
+ if (args === null || typeof args.command !== "string" || args.command === "") return block;
316
+ const callView = {
317
+ card: "terminal",
318
+ title: args.command,
319
+ ...(typeof args.description === "string" ? { description: args.description } : {}),
320
+ ...(typeof args.workdir === "string" ? { cwd: args.workdir } : {}),
321
+ };
322
+ if (!settled) return { ...block, callView };
323
+
324
+ return {
325
+ ...block,
326
+ callView,
327
+ resultView: {
328
+ card: "terminal",
329
+ ...parseTerminalOutput(flattenContent(block.content)),
330
+ },
331
+ };
332
+ }
333
+
334
+ const settingsLocales = {
335
+ en: {
336
+ title: "Git Bash",
337
+ description: "Select the Git for Windows used by Bash commands: one for the startup script and danger-full-access, one for the confined modes.",
338
+ unsaved: "Unsaved",
339
+ overridden: "Overridden",
340
+ reset: "Reset to default",
341
+ readOnly: "This deployment stores settings read-only.",
342
+ pathLabel: "Git Bash executable (startup script / danger-full-access)",
343
+ pathHint: "Leave blank to auto-detect Git for Windows. The path must point to bash.exe.",
344
+ confinedPathLabel: "Git Bash executable (read-only / workspace-write)",
345
+ confinedPathHint: "Use a DIFFERENT Git installation than the one above. Leave blank to fall back to the same one.",
346
+ invalidPath: "Enter an absolute Windows path ending in bash.exe, or leave it blank.",
347
+ browse: "Select Git installation folder",
348
+ browsing: "Selecting...",
349
+ browseTitle: "Open the Windows folder picker",
350
+ discard: "Discard",
351
+ save: "Save",
352
+ saving: "Saving...",
353
+ saveFailed: "That bash.exe is unavailable or invalid. Check the path and try again.",
354
+ pickerFailed: "The Windows folder picker could not be opened.",
355
+ },
356
+ zh: {
357
+ title: "Git Bash",
358
+ description: "选择 Bash 命令使用的 Git for Windows:一份给启动脚本与 danger-full-access,另一份给受限模式。",
359
+ unsaved: "未保存",
360
+ overridden: "已覆盖",
361
+ reset: "恢复默认",
362
+ readOnly: "本部署的设置为只读。",
363
+ pathLabel: "Git Bash 可执行文件(启动脚本 / danger-full-access)",
364
+ pathHint: "留空会自动探测 Git for Windows;路径必须指向 bash.exe。",
365
+ confinedPathLabel: "Git Bash 可执行文件(read-only / workspace-write)",
366
+ confinedPathHint: "请使用与上一项【不同】的 Git 安装;留空则回落到同一份。",
367
+ invalidPath: "请输入以 bash.exe 结尾的 Windows 绝对路径,或留空自动探测。",
368
+ browse: "选择 Git 安装目录",
369
+ browsing: "选择中...",
370
+ browseTitle: "打开 Windows 原生目录选择窗口",
371
+ discard: "放弃修改",
372
+ save: "保存",
373
+ saving: "保存中...",
374
+ saveFailed: "该 bash.exe 不可用或路径无效,请检查后重试。",
375
+ pickerFailed: "无法打开 Windows 原生目录选择窗口。",
376
+ },
377
+ };
378
+
379
+ function hasOwn(value, key) {
380
+ return typeof value === "object" && value !== null &&
381
+ Object.prototype.hasOwnProperty.call(value, key);
382
+ }
383
+
384
+ function isGitBashExecutablePath(value) {
385
+ return /^(?:$|(?:[A-Z]:[\\/]|\\\\[^\\/]+[\\/][^\\/]+[\\/]|%[^%]+%[\\/])(?:.*[\\/])?bash\.exe)$/i
386
+ .test(value.trim());
387
+ }
388
+
389
+ function appendWindowsPath(directory, suffix) {
390
+ return directory.endsWith("\\") ? directory + suffix : directory + "\\" + suffix;
391
+ }
392
+
393
+ function gitBashExecutableFromDirectory(directory) {
394
+ let normalized = typeof directory === "string" ? directory.trim().replace(/\//g, "\\") : "";
395
+ normalized = normalized.replace(/\\+$/, "");
396
+ if (/^[A-Za-z]:$/.test(normalized)) normalized += "\\";
397
+ if (normalized === "" || /(?:^|\\)bash\.exe$/i.test(normalized)) return normalized;
398
+ if (/(?:^|\\)bin$/i.test(normalized)) return appendWindowsPath(normalized, "bash.exe");
399
+ if (/(?:^|\\)usr$/i.test(normalized)) return appendWindowsPath(normalized, "bin\\bash.exe");
400
+ return appendWindowsPath(normalized, "bin\\bash.exe");
401
+ }
402
+
403
+ function fieldOf(value, field) {
404
+ return typeof value?.[field] === "string" ? value[field] : "";
405
+ }
406
+
407
+ // One path field: label, input, native folder picker, per-field override
408
+ // badge and reset. Both `executable` and `confinedExecutable` share it, so
409
+ // they stay consistent without duplicating the interaction logic.
410
+ function GitBashPathField(props) {
411
+ const { field, idBase, label, hint, invalidHint, placeholder, scope, pickDirectory, t, busy } = props;
412
+ const snapshot = useSyncExternalStore(
413
+ (listener) => scope.subscribe(listener),
414
+ () => scope.getSnapshot(),
415
+ () => scope.getSnapshot(),
416
+ );
417
+ const effective = fieldOf(snapshot.value, field);
418
+ const inherited = fieldOf(snapshot.base, field);
419
+ const overridden = hasOwn(snapshot.user, field);
420
+ const writable = props.writable;
421
+ const [text, setText] = useState(effective);
422
+ const [dirty, setDirty] = useState(false);
423
+ const [resetPending, setResetPending] = useState(false);
424
+ const [picking, setPicking] = useState(false);
425
+ const [message, setMessage] = useState("");
426
+
427
+ useEffect(() => {
428
+ if (dirty) return;
429
+ setText(effective);
430
+ }, [dirty, effective, snapshot.revision]);
431
+
432
+ const trimmed = text.trim();
433
+ const willUnset = resetPending || trimmed === "";
434
+ const invalid = !willUnset && !isGitBashExecutablePath(trimmed);
435
+ const previewOverridden = dirty ? !willUnset : overridden;
436
+ const fieldBusy = busy || picking;
437
+
438
+ const edit = (value) => {
439
+ setText(value);
440
+ setDirty(value.trim() !== effective.trim());
441
+ setResetPending(false);
442
+ setMessage("");
443
+ };
444
+ const reset = () => {
445
+ setText(inherited);
446
+ setDirty(true);
447
+ setResetPending(true);
448
+ setMessage("");
449
+ };
450
+ const discard = () => {
451
+ setText(effective);
452
+ setDirty(false);
453
+ setResetPending(false);
454
+ setMessage("");
455
+ };
456
+ const browse = async () => {
457
+ setPicking(true);
458
+ setMessage("");
459
+ try {
460
+ const directory = await pickDirectory();
461
+ if (directory !== null) edit(gitBashExecutableFromDirectory(directory));
462
+ } catch {
463
+ setMessage(t("pickerFailed"));
464
+ } finally {
465
+ setPicking(false);
466
+ }
467
+ };
468
+ const save = async () => {
469
+ if (!dirty || invalid || !writable) return;
470
+ setMessage("");
471
+ try {
472
+ if (willUnset) await scope.unset(field);
473
+ else await scope.set(field, trimmed);
474
+
475
+ const latest = scope.getSnapshot();
476
+ const accepted = willUnset
477
+ ? !hasOwn(latest.user, field)
478
+ : hasOwn(latest.user, field) && latest.user[field] === trimmed;
479
+ if (!accepted) {
480
+ setMessage(t("saveFailed"));
481
+ return;
482
+ }
483
+
484
+ setText(fieldOf(latest.value, field));
485
+ setDirty(false);
486
+ setResetPending(false);
487
+ } catch {
488
+ setMessage(t("saveFailed"));
489
+ }
490
+ };
491
+
492
+ const inputId = "plugin-config-git-bash-" + idBase;
493
+ const hintId = inputId + "-hint";
494
+
495
+ return createElement("div", { className: "dgb-settings-field" },
496
+ createElement("div", { className: "dgb-settings-field-head" },
497
+ createElement("label", {
498
+ className: "dgb-settings-label",
499
+ htmlFor: inputId,
500
+ }, t(label)),
501
+ previewOverridden ? createElement("span", { className: "dgb-settings-field-actions" },
502
+ createElement("span", { className: "dgb-settings-overridden" }, t("overridden")),
503
+ createElement("button", {
504
+ type: "button",
505
+ className: "dgb-settings-reset",
506
+ disabled: !writable || fieldBusy,
507
+ onClick: reset,
508
+ }, t("reset")),
509
+ ) : null,
510
+ ),
511
+ createElement("div", { className: "dgb-settings-input-row" },
512
+ createElement("input", {
513
+ id: inputId,
514
+ className: "dgb-settings-input",
515
+ type: "text",
516
+ autoComplete: "off",
517
+ spellCheck: false,
518
+ value: text,
519
+ placeholder,
520
+ disabled: !writable || fieldBusy,
521
+ "aria-describedby": hintId,
522
+ ...(invalid ? {
523
+ "aria-invalid": true,
524
+ "aria-errormessage": hintId,
525
+ } : {}),
526
+ onChange: (event) => edit(event.target.value),
527
+ }),
528
+ createElement("button", {
529
+ type: "button",
530
+ className: "dgb-settings-button dgb-settings-picker",
531
+ title: t("browseTitle"),
532
+ disabled: !writable || fieldBusy,
533
+ onClick: browse,
534
+ },
535
+ createElement(IconFolderOpenOutline16, {}),
536
+ t(picking ? "browsing" : "browse")),
537
+ ),
538
+ createElement("p", {
539
+ id: hintId,
540
+ className: invalid ? "dgb-settings-hint dgb-settings-error" : "dgb-settings-hint",
541
+ ...(invalid ? { role: "alert" } : {}),
542
+ }, t(invalid ? invalidHint : hint)),
543
+ createElement("div", { className: "dgb-settings-field-footer" },
544
+ message ? createElement("p", {
545
+ className: "dgb-settings-message dgb-settings-error",
546
+ role: "status",
547
+ }, message) : null,
548
+ createElement("button", {
549
+ type: "button",
550
+ className: "dgb-settings-button",
551
+ disabled: !dirty || fieldBusy,
552
+ onClick: discard,
553
+ }, t("discard")),
554
+ createElement("button", {
555
+ type: "button",
556
+ className: "dgb-settings-button dgb-settings-button-primary",
557
+ disabled: !dirty || invalid || !writable || fieldBusy,
558
+ onClick: save,
559
+ }, t("save")),
560
+ ),
561
+ );
562
+ }
563
+
564
+ function GitBashSettingsCard(props) {
565
+ const { scope, pickDirectory, t } = props;
566
+ const snapshot = useSyncExternalStore(
567
+ (listener) => scope.subscribe(listener),
568
+ () => scope.getSnapshot(),
569
+ () => scope.getSnapshot(),
570
+ );
571
+ // Gate on `executable` alone: it predates `confinedExecutable`, so an
572
+ // older host schema (without the new field) still renders the first
573
+ // field rather than hiding the whole card.
574
+ const supported = snapshot.status === "ready" && hasOwn(snapshot.value, "executable");
575
+ const writable = supported && snapshot.writable;
576
+ const [open, setOpen] = useState(false);
577
+
578
+ if (!supported) return null;
579
+
580
+ const fieldProps = { scope, pickDirectory, t, writable };
581
+
582
+ return createElement("li", {
583
+ "data-dsh-git-bash-settings": "",
584
+ "data-open": open ? "true" : undefined,
585
+ },
586
+ createElement("button", {
587
+ type: "button",
588
+ className: "dgb-settings-header",
589
+ "aria-expanded": open,
590
+ onClick: () => setOpen(!open),
591
+ },
592
+ createElement("span", { className: "dgb-settings-head-text" },
593
+ createElement("span", { className: "dgb-settings-title" }, t("title")),
594
+ createElement("span", { className: "dgb-settings-description" }, t("description")),
595
+ ),
596
+ createElement(IconChevronDownOutline14, {
597
+ className: open
598
+ ? "dgb-settings-chevron dgb-settings-chevron-open"
599
+ : "dgb-settings-chevron",
600
+ })),
601
+ open ? createElement("div", { className: "dgb-settings-body" },
602
+ !writable ? createElement("p", {
603
+ className: "dgb-settings-readonly",
604
+ role: "status",
605
+ }, t("readOnly")) : null,
606
+ createElement(GitBashPathField, {
607
+ ...fieldProps,
608
+ field: "executable",
609
+ idBase: "executable",
610
+ label: "pathLabel",
611
+ hint: "pathHint",
612
+ invalidHint: "invalidPath",
613
+ placeholder: "C:\\Program Files\\Git\\bin\\bash.exe",
614
+ }),
615
+ createElement(GitBashPathField, {
616
+ ...fieldProps,
617
+ field: "confinedExecutable",
618
+ idBase: "confined-executable",
619
+ label: "confinedPathLabel",
620
+ hint: "confinedPathHint",
621
+ invalidHint: "invalidPath",
622
+ placeholder: "D:\\PortableGit\\usr\\bin\\bash.exe",
623
+ }),
624
+ ) : null);
625
+ }
626
+
627
+ function installSettingsNamespaceDedupe(ctx) {
628
+ let stop = () => {};
629
+ const install = () => {
630
+ const configurableTab = ctx.slots.entries(SETTINGS_TAB_SLOT)
631
+ .find((entry) => entry.options.id === "configurable");
632
+ if (configurableTab === undefined) return false;
633
+ const store = configurableTab.inject().hooks.configurablePlugins;
634
+ if (store === undefined) return false;
635
+ const dedupe = () => {
636
+ const snapshot = store.getSnapshot();
637
+ const namespaces = [...new Set(snapshot.namespaces)];
638
+ if (namespaces.length === snapshot.namespaces.length) return;
639
+ store.set({ ...snapshot, namespaces });
640
+ };
641
+ stop = store.subscribe(dedupe);
642
+ dedupe();
643
+ return true;
644
+ };
645
+ ctx.effect(() => {
646
+ if (install()) return stop;
647
+ const off = ctx.slots.subscribe(SETTINGS_TAB_SLOT, () => {
648
+ if (install()) off();
649
+ });
650
+ return () => {
651
+ off();
652
+ stop();
653
+ };
654
+ }, "git-bash: deduplicate shadowed settings namespaces");
655
+ }
656
+
657
+ function apply(ctx) {
658
+ ctx.effect(() => {
659
+ const disposeCommandStyle = installCommandWrapStyle();
660
+ const disposeSettingsStyle = installSettingsStyle();
661
+ return () => {
662
+ disposeSettingsStyle();
663
+ disposeCommandStyle();
664
+ };
665
+ }, "git-bash: client styles");
666
+
667
+ const t = ctx.locale.bind(LOCALE_NAMESPACE);
668
+ ctx.effect(() => ctx.locale.register(LOCALE_NAMESPACE, settingsLocales),
669
+ "git-bash: settings dictionaries");
670
+ // The configurable settings tab collects namespaces from raw keyed entries, so
671
+ // shadowed cards (same key, different priority) yield duplicate namespaces.
672
+ installSettingsNamespaceDedupe(ctx);
673
+ // DSH 0.2 dropped the `settingsScope` service. Register the configuration
674
+ // card only when the host still exposes it; the Bash terminal row below is
675
+ // independent of settings, so the entry activates either way.
676
+ const settingsScope = ctx.get("settingsScope");
677
+ if (settingsScope !== undefined) {
678
+ const scope = settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
679
+ ctx.slots.inject(SETTINGS_SLOT, () => ctx.slots.register({
680
+ name: SETTINGS_SLOT,
681
+ key: SETTINGS_NAMESPACE,
682
+ priority: PRIORITY,
683
+ locale: LOCALE_NAMESPACE,
684
+ inject: () => ({
685
+ scope,
686
+ pickDirectory: () => ctx.uiWorkspace.pickDirectory(),
687
+ t,
688
+ }),
689
+ }, GitBashSettingsCard));
690
+ }
691
+
692
+ ctx.slots.inject(SLOT, () => {
693
+ const original = ctx.slots.entries(SLOT)
694
+ .filter((entry) => entry.options.key === "bash" && entry.options.priority !== PRIORITY)
695
+ .sort((left, right) => (left.options.priority ?? 0) - (right.options.priority ?? 0))[0];
696
+ if (original === undefined) return undefined;
697
+
698
+ function GitBashRow(props) {
699
+ const block = addNestedTerminalViews(props.block);
700
+ const row = createElement(original.component, { ...props, block });
701
+ if (block === props.block) return row;
702
+ return createElement("div", {
703
+ [WRAP_ATTR]: "",
704
+ style: { display: "contents" },
705
+ }, row);
706
+ }
707
+
708
+ return ctx.slots.register({
709
+ name: SLOT,
710
+ key: "bash",
711
+ priority: PRIORITY,
712
+ locale: "conversation",
713
+ }, GitBashRow);
714
+ });
715
+ }
716
+
717
+ // `settingsScope` is intentionally absent: DSH 0.2 removed that service, and
718
+ // listing it here keeps the whole client entry pending forever (the host
719
+ // reports "1 entry did not activate"). The settings card wiring below probes
720
+ // for it at runtime instead.
721
+ const inject = [
722
+ "slots",
723
+ "locale",
724
+ "connection",
725
+ "remote",
726
+ "uiWorkspace",
727
+ ];
728
+ module.exports = {
729
+ apply,
730
+ inject,
731
+ gitBashExecutableFromDirectory,
732
+ isGitBashExecutablePath,
733
+ };
734
+ return module.exports;
735
+ },
736
+ });