@deepseek-harness-tui/dsh-tui 0.8.2 → 0.8.4

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 (39) hide show
  1. package/README.md +37 -12
  2. package/bin/dsh-tui.js +93 -18
  3. package/lib/types/commands.d.ts.map +1 -1
  4. package/lib/types/commands.js +1 -0
  5. package/lib/types/components/LogoV2.d.ts +4 -1
  6. package/lib/types/components/LogoV2.d.ts.map +1 -1
  7. package/lib/types/components/LogoV2.js +8 -3
  8. package/lib/types/components/PromptInput.d.ts.map +1 -1
  9. package/lib/types/components/PromptInput.js +1 -3
  10. package/lib/types/components/TipsPanel.d.ts +14 -0
  11. package/lib/types/components/TipsPanel.d.ts.map +1 -0
  12. package/lib/types/components/TipsPanel.js +37 -0
  13. package/lib/types/components/activityFrames.d.ts.map +1 -1
  14. package/lib/types/components/activityFrames.js +10 -0
  15. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  16. package/lib/types/dsh-adapter/channel.js +8 -14
  17. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  18. package/lib/types/dsh-adapter/plugin.js +17 -0
  19. package/lib/types/effortPrefs.d.ts +7 -6
  20. package/lib/types/effortPrefs.d.ts.map +1 -1
  21. package/lib/types/effortPrefs.js +7 -6
  22. package/lib/types/i18n.d.ts +50 -14
  23. package/lib/types/i18n.d.ts.map +1 -1
  24. package/lib/types/i18n.js +21 -4
  25. package/lib/types/ink/ink.d.ts +1 -0
  26. package/lib/types/ink/ink.d.ts.map +1 -1
  27. package/lib/types/ink/ink.js +11 -0
  28. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  29. package/lib/types/ink/parse-keypress.js +15 -0
  30. package/lib/types/screens/Chat.d.ts.map +1 -1
  31. package/lib/types/screens/Chat.js +10 -3
  32. package/lib/types/screens/StatusLine.js +3 -3
  33. package/lib/types/tips.d.ts +30 -0
  34. package/lib/types/tips.d.ts.map +1 -0
  35. package/lib/types/tips.js +575 -0
  36. package/package.json +5 -3
  37. package/presets/liangshen/.dsh-tui-managed.json +1 -1
  38. package/presets/liangshen/agent.cordis.yml +7 -5
  39. package/presets/liangshen/custom-bash.mjs +184 -9
@@ -13,9 +13,26 @@
13
13
  * the ordinary (cross-platform) subprocess seam keeps the schema anchor
14
14
  * without the PTY dependency.
15
15
  *
16
- * Executable resolution (config `bashPath`):
17
- * - explicit absolute path (e.g. `C:\Program Files\Git\bin\bash.exe`), or
18
- * - `bash` resolved through `ctx.subprocess.resolveExecutable` (PATH lookup).
16
+ * Executable resolution, in order (an explicit entry is the ONLY candidate:
17
+ * a miss fails loudly instead of silently substituting a guess):
18
+ * 1. explicit `config.bashPath` or the `DSH_TUI_LIANGSHEN_BASH_PATH`
19
+ * environment variable (absolute path to a Git Bash bash.exe);
20
+ * 2. a `git` on PATH followed to its installation tree — every Windows Git
21
+ * install ships bash next to git, wherever it was installed, so this
22
+ * covers installer, portable, and Scoop layouts (a Scoop `.shim` sidecar
23
+ * is followed to the real target) — yielding `<root>\bin\bash.exe` and
24
+ * `<root>\usr\bin\bash.exe`;
25
+ * 3. conventional install roots (`ProgramFiles`, `ProgramFiles(x86)`,
26
+ * `%LOCALAPPDATA%\Programs\Git`);
27
+ * 4. Scoop's conventional roots (`%SCOOP%`, else `%USERPROFILE%\scoop`)
28
+ * under `apps\git\current\` — Scoop does not always export `SCOOP`;
29
+ * 5. bare `bash` on PATH.
30
+ * `C:\Windows\System32\bash.exe` (and Sysnative) is the WSL launcher, not
31
+ * the Git Bash executor: it is ALWAYS rejected, so a PATH lookup can never
32
+ * silently hand the model a WSL shell. Resolution runs ONCE at apply time —
33
+ * a total miss warns and SKIPS registration, so `tool-bootstrap`'s
34
+ * missing-tool path fails open to the full catalog instead of registering a
35
+ * `bash` tool whose every call would fail mid-session.
19
36
  *
20
37
  * Semantics mirror the official bash tool: `bash -c <command>` in a fresh
21
38
  * process, bounded output, non-zero exit reported not thrown. No sandbox
@@ -24,6 +41,8 @@
24
41
  * `str_replace_editor` (Minimal's two tools).
25
42
  */
26
43
 
44
+ import { readFileSync } from 'node:fs'
45
+
27
46
  /** Cordis plugin name used by loader diagnostics. */
28
47
  export const name = 'custom-bash'
29
48
 
@@ -50,11 +69,167 @@ const commandSchema = {
50
69
  additionalProperties: false,
51
70
  }
52
71
 
72
+ function addCandidate(candidates, seen, candidate) {
73
+ if (typeof candidate !== 'string' || candidate.length === 0) return
74
+ const key = candidate.toLowerCase()
75
+ if (seen.has(key)) return
76
+ seen.add(key)
77
+ candidates.push(candidate)
78
+ }
79
+
80
+ /** The explicit override, if any: `config.bashPath`, else the environment variable. */
81
+ function explicitBashPath(config, environment) {
82
+ for (const value of [config.bashPath, environment.DSH_TUI_LIANGSHEN_BASH_PATH]) {
83
+ if (typeof value === 'string' && value.length > 0) return value
84
+ }
85
+ return undefined
86
+ }
87
+
88
+ /** Return conventional Git Bash locations in deterministic order (explicit config first). */
89
+ export function windowsBashCandidates(config = {}, environment = process.env) {
90
+ const explicit = explicitBashPath(config, environment)
91
+ if (explicit !== undefined) return [explicit]
92
+
93
+ const candidates = []
94
+ const seen = new Set()
95
+ for (const root of [
96
+ environment.ProgramFiles,
97
+ environment['ProgramFiles(x86)'],
98
+ ]) {
99
+ if (typeof root !== 'string' || root.length === 0) continue
100
+ addCandidate(candidates, seen, `${root}\\Git\\bin\\bash.exe`)
101
+ addCandidate(candidates, seen, `${root}\\Git\\usr\\bin\\bash.exe`)
102
+ }
103
+ if (typeof environment.LOCALAPPDATA === 'string' && environment.LOCALAPPDATA.length > 0) {
104
+ addCandidate(candidates, seen, `${environment.LOCALAPPDATA}\\Programs\\Git\\bin\\bash.exe`)
105
+ addCandidate(candidates, seen, `${environment.LOCALAPPDATA}\\Programs\\Git\\usr\\bin\\bash.exe`)
106
+ }
107
+ // Scoop keeps Git under its own root; SCOOP is exported by some setups, but
108
+ // the per-user default location works without it.
109
+ const scoopRoots = []
110
+ if (typeof environment.SCOOP === 'string' && environment.SCOOP.length > 0) {
111
+ scoopRoots.push(environment.SCOOP)
112
+ }
113
+ if (typeof environment.USERPROFILE === 'string' && environment.USERPROFILE.length > 0) {
114
+ scoopRoots.push(`${environment.USERPROFILE}\\scoop`)
115
+ }
116
+ for (const root of scoopRoots) {
117
+ addCandidate(candidates, seen, `${root}\\apps\\git\\current\\bin\\bash.exe`)
118
+ addCandidate(candidates, seen, `${root}\\apps\\git\\current\\usr\\bin\\bash.exe`)
119
+ }
120
+ addCandidate(candidates, seen, 'bash')
121
+ return candidates
122
+ }
123
+
124
+ /** Windows' System32 bash.exe is a WSL launcher, not the Git Bash executor. */
125
+ export function isWindowsSubsystemLauncher(path) {
126
+ return /[\\/]windows[\\/](?:system32|sysnative)[\\/]bash\.exe$/i.test(path)
127
+ }
128
+
129
+ /**
130
+ * Scoop-style shim launchers keep the real target in a sibling `.shim` text
131
+ * file (`path = "..."`); follow it so a PATH-resolved git.exe still yields
132
+ * its installation tree.
133
+ */
134
+ export function resolveShimTarget(exePath, reader = readFileSync) {
135
+ for (const sidecar of [exePath.replace(/\.exe$/i, '') + '.shim', `${exePath}.shim`]) {
136
+ try {
137
+ const text = reader(sidecar, 'utf8')
138
+ const match = text.match(/^\s*path\s*=\s*"?(.+?)"?\s*$/mi)
139
+ if (match !== null) return match[1]
140
+ } catch { /* not a shim launcher */ }
141
+ }
142
+ return exePath
143
+ }
144
+
145
+ /** Bash candidates derived from a git.exe inside a Git for Windows tree. */
146
+ export function bashCandidatesFromGit(gitPath) {
147
+ if (typeof gitPath !== 'string' || gitPath.length === 0) return []
148
+ const parts = gitPath.replace(/\//g, '\\').split('\\').filter(part => part.length > 0)
149
+ if (parts.at(-1)?.toLowerCase() !== 'git.exe') return []
150
+ const roots = []
151
+ const push = root => {
152
+ if (root.length > 0 && !roots.includes(root)) roots.push(root)
153
+ }
154
+ // <root>\cmd\git.exe and <root>\bin\git.exe -> <root>
155
+ if (parts.length >= 3) push(parts.slice(0, -2).join('\\'))
156
+ // <root>\mingw64\bin\git.exe -> <root>; a plain <root>\cmd layout must not
157
+ // also strip its real root.
158
+ if (parts.length >= 5
159
+ && parts.at(-2)?.toLowerCase() === 'bin'
160
+ && /^mingw(32|64)$/.test(parts.at(-3) ?? '')) {
161
+ push(parts.slice(0, -3).join('\\'))
162
+ }
163
+ const candidates = []
164
+ for (const root of roots) {
165
+ candidates.push(`${root}\\bin\\bash.exe`, `${root}\\usr\\bin\\bash.exe`)
166
+ }
167
+ return candidates
168
+ }
169
+
170
+ /**
171
+ * Follow the PATH-visible git executable to its installation tree: every
172
+ * Windows Git install ships git-bash next to git, wherever it was installed,
173
+ * so this covers installer, portable, and Scoop layouts without hardcoding.
174
+ */
175
+ async function gitTreeCandidates(subprocess, failures) {
176
+ let gitPath
177
+ try {
178
+ gitPath = await subprocess.resolveExecutable('git')
179
+ } catch (error) {
180
+ failures.push(`git: ${error instanceof Error ? error.message : String(error)}`)
181
+ return []
182
+ }
183
+ return bashCandidatesFromGit(resolveShimTarget(gitPath))
184
+ }
185
+
186
+ /** Resolve Git Bash without accidentally accepting the WSL compatibility shim. */
187
+ export async function resolveWindowsBash(subprocess, config = {}, environment = process.env) {
188
+ const failures = []
189
+ const explicit = explicitBashPath(config, environment)
190
+ const candidates = explicit !== undefined
191
+ // An explicit path is honored as-is: never silently substitute a guess.
192
+ ? [explicit]
193
+ : [...await gitTreeCandidates(subprocess, failures), ...windowsBashCandidates(config, environment)]
194
+ for (const candidate of candidates) {
195
+ try {
196
+ const resolved = await subprocess.resolveExecutable(candidate)
197
+ if (isWindowsSubsystemLauncher(resolved)) {
198
+ failures.push(`${candidate}: resolved to the Windows Subsystem for Linux launcher`)
199
+ continue
200
+ }
201
+ return resolved
202
+ } catch (error) {
203
+ failures.push(`${candidate}: ${error instanceof Error ? error.message : String(error)}`)
204
+ }
205
+ }
206
+ throw new Error(`Git Bash executable unavailable (${failures.join('; ')})`)
207
+ }
208
+
209
+ function warn(ctx, message) {
210
+ try {
211
+ ctx.logger.warn(message)
212
+ } catch {
213
+ // Logger unavailable — registration is skipped either way.
214
+ }
215
+ }
216
+
53
217
  /** Register the model-facing `bash` tool. */
54
- export function apply(ctx, config) {
55
- const bashPath = typeof config?.bashPath === 'string' && config.bashPath.length > 0 ? config.bashPath : 'bash'
56
- const timeoutMs = Number.isSafeInteger(config?.timeoutMs) && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS
57
- const maxOutputBytes = Number.isSafeInteger(config?.maxOutputBytes) && config.maxOutputBytes > 0 ? config.maxOutputBytes : DEFAULT_MAX_OUTPUT_BYTES
218
+ export async function apply(ctx, config) {
219
+ const source = config === undefined ? {} : config
220
+ const timeoutMs = Number.isSafeInteger(source.timeoutMs) && source.timeoutMs > 0 ? source.timeoutMs : DEFAULT_TIMEOUT_MS
221
+ const maxOutputBytes = Number.isSafeInteger(source.maxOutputBytes) && source.maxOutputBytes > 0 ? source.maxOutputBytes : DEFAULT_MAX_OUTPUT_BYTES
222
+
223
+ let bashPath
224
+ try {
225
+ bashPath = await resolveWindowsBash(ctx.subprocess, source)
226
+ } catch (error) {
227
+ // Skip registration on a miss: an absent `bash` in the assembled catalog
228
+ // makes tool-bootstrap fail open to the full catalog, while a registered
229
+ // tool that cannot spawn would fail on every call mid-session.
230
+ warn(ctx, `custom-bash: ${error instanceof Error ? error.message : String(error)} — the Windows bootstrap bash tool is not registered; the bootstrap filter will expose the full catalog`)
231
+ return
232
+ }
58
233
 
59
234
  ctx.tools.register({
60
235
  name: 'bash',
@@ -69,6 +244,7 @@ export function apply(ctx, config) {
69
244
  '* NOTE: runs without OS sandbox confinement on Windows (no landlock); treat output as untrusted.',
70
245
  ].join('\n'),
71
246
  parameters: commandSchema,
247
+ timeoutMs,
72
248
  output: {
73
249
  schema: {
74
250
  type: 'object',
@@ -81,13 +257,12 @@ export function apply(ctx, config) {
81
257
  render: (_args, value) => [{ type: 'text', text: value.text }],
82
258
  },
83
259
  async execute(args, exec) {
84
- const shell = await ctx.subprocess.resolveExecutable(bashPath, undefined, exec?.signal)
85
260
  const workdir = typeof args.workdir === 'string' && args.workdir.length > 0
86
261
  ? args.workdir
87
262
  : exec?.agent?.session?.header?.cwd
88
263
  const signal = exec?.signal
89
264
  const handle = ctx.subprocess.spawn({
90
- argv: [shell, '-c', args.command],
265
+ argv: [bashPath, '-c', args.command],
91
266
  ...workdir !== undefined ? { cwd: workdir } : {},
92
267
  stdio: {
93
268
  stdin: 'ignore',