dsh-coding-sidebar 1.0.7 → 1.0.8
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/README.md +2 -2
- package/lib/client-editor.js +436 -227
- package/lib/client-registry.js +627 -7391
- package/lib/client-terminal.js +234 -182
- package/lib/client.js +634 -7398
- package/lib/index.js +367 -67
- package/lib/types/agent-pty.d.ts +62 -4
- package/lib/types/bundle-route.d.ts +1 -1
- package/lib/types/client/EditorHost.d.ts +1 -1
- package/lib/types/client/FileTree.d.ts +2 -2
- package/lib/types/client/TreePanel.d.ts +1 -1
- package/lib/types/client/chunk-loader.d.ts +1 -1
- package/lib/types/client/chunks/locale.d.ts +3 -0
- package/lib/types/client/conversation-draft.d.ts +85 -4
- package/lib/types/client/locales.d.ts +1 -20
- package/lib/types/client/selection-popup.d.ts +58 -0
- package/lib/types/client/service.d.ts +1 -1
- package/lib/types/client/terminal-font.d.ts +25 -2
- package/lib/types/context-types.d.ts +3 -1
- package/lib/types/index.d.ts +9 -0
- package/lib/types/prefs-shared.d.ts +7 -5
- package/lib/types/pty-manager.d.ts +53 -0
- package/lib/types/wire.d.ts +6 -2
- package/package.json +1 -1
- package/src/agent-pty.ts +221 -33
- package/src/bundle-route.ts +1 -1
- package/src/client/EditorHost.tsx +1 -1
- package/src/client/FileTree.tsx +4 -4
- package/src/client/Sidebar.tsx +41 -22
- package/src/client/TerminalView.tsx +13 -0
- package/src/client/TextEditor.tsx +27 -45
- package/src/client/TreePanel.tsx +16 -1
- package/src/client/chunk-loader.ts +1 -1
- package/src/client/chunks/locale.tsx +60 -0
- package/src/client/conversation-draft.ts +233 -7
- package/src/client/index.tsx +19 -8
- package/src/client/locales-ar.ts +5 -4
- package/src/client/locales-de.ts +5 -4
- package/src/client/locales-fr.ts +5 -4
- package/src/client/locales-hi.ts +5 -4
- package/src/client/locales-id.ts +5 -4
- package/src/client/locales-it.ts +5 -4
- package/src/client/locales-ja.ts +5 -4
- package/src/client/locales-ko.ts +5 -4
- package/src/client/locales-nl.ts +5 -4
- package/src/client/locales-pl.ts +5 -4
- package/src/client/locales-pt.ts +5 -4
- package/src/client/locales-ru.ts +5 -4
- package/src/client/locales-sv.ts +5 -4
- package/src/client/locales-th.ts +5 -4
- package/src/client/locales-tr.ts +5 -4
- package/src/client/locales-vi.ts +5 -4
- package/src/client/locales-zh-HK.ts +5 -4
- package/src/client/locales-zh-MO.ts +5 -4
- package/src/client/locales-zh-TW.ts +5 -4
- package/src/client/locales.ts +17 -51
- package/src/client/selection-popup.ts +155 -0
- package/src/client/service.ts +1 -1
- package/src/client/state.ts +14 -7
- package/src/client/terminal-font.ts +34 -3
- package/src/context-types.ts +3 -2
- package/src/git.ts +17 -1
- package/src/index.ts +39 -6
- package/src/open-external.ts +3 -4
- package/src/prefs-shared.ts +7 -5
- package/src/pty-manager.ts +176 -5
- package/src/sidechat-routes.ts +13 -1
- package/src/tools.ts +24 -6
- package/src/wire.ts +3 -0
package/src/pty-manager.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* tab is closed or the plugin tears down.
|
|
8
8
|
*/
|
|
9
9
|
import { chmodSync, existsSync } from 'node:fs'
|
|
10
|
-
import { dirname, join } from 'node:path'
|
|
10
|
+
import { dirname, join, win32 as win32Path } from 'node:path'
|
|
11
11
|
import { createRequire } from 'node:module'
|
|
12
12
|
import { userInfo } from 'node:os'
|
|
13
13
|
import type { IPty } from 'node-pty'
|
|
@@ -153,7 +153,7 @@ export class PtyManager {
|
|
|
153
153
|
sessionId,
|
|
154
154
|
tabId,
|
|
155
155
|
cwd,
|
|
156
|
-
pty: this.nodePty.spawn(shell ?? this.shell, shellSpawnArgs(shellArgs ?? this.shellArgs), {
|
|
156
|
+
pty: this.nodePty.spawn(resolveShellExecutable(shell ?? this.shell), shellSpawnArgs(shellArgs ?? this.shellArgs), {
|
|
157
157
|
name: 'xterm-256color',
|
|
158
158
|
cols: Math.max(2, Math.floor(cols)),
|
|
159
159
|
rows: Math.max(2, Math.floor(rows)),
|
|
@@ -270,6 +270,32 @@ export interface ShellResolutionOptions {
|
|
|
270
270
|
exists?: (path: string) => boolean
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/** Inputs for resolving one configured shell into the executable path passed
|
|
274
|
+
* to node-pty. Injectable so the Windows-only search semantics stay covered
|
|
275
|
+
* on POSIX CI runners. */
|
|
276
|
+
export interface ShellExecutableResolutionOptions {
|
|
277
|
+
/** Platform override (defaults to `process.platform`). */
|
|
278
|
+
platform?: NodeJS.Platform
|
|
279
|
+
/** Environment override; Windows reads PATH/PATHEXT/SystemRoot plus the
|
|
280
|
+
* PowerShell well-known-location variables. */
|
|
281
|
+
env?: NodeJS.ProcessEnv
|
|
282
|
+
/** File-existence probe override (defaults to `existsSync`). */
|
|
283
|
+
exists?: (path: string) => boolean
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Read one Windows environment value case-insensitively. Real
|
|
287
|
+
* `process.env` has case-insensitive lookup on Windows, but injected objects
|
|
288
|
+
* and some embedders do not preserve that behavior. */
|
|
289
|
+
function windowsEnv(env: NodeJS.ProcessEnv, name: string): string | undefined {
|
|
290
|
+
const direct = env[name]
|
|
291
|
+
if (direct !== undefined) return direct
|
|
292
|
+
const lowered = name.toLowerCase()
|
|
293
|
+
for (const [key, value] of Object.entries(env)) {
|
|
294
|
+
if (key.toLowerCase() === lowered) return value
|
|
295
|
+
}
|
|
296
|
+
return undefined
|
|
297
|
+
}
|
|
298
|
+
|
|
273
299
|
/**
|
|
274
300
|
* Candidate directories that may contain a `pwsh.exe` on Windows: PATH
|
|
275
301
|
* entries first, then the well-known machine/user install locations
|
|
@@ -281,7 +307,7 @@ export interface ShellResolutionOptions {
|
|
|
281
307
|
*/
|
|
282
308
|
function windowsPwshCandidateDirs(env: NodeJS.ProcessEnv): string[] {
|
|
283
309
|
const dirs: string[] = []
|
|
284
|
-
const pathEntries = env
|
|
310
|
+
const pathEntries = windowsEnv(env, 'PATH')
|
|
285
311
|
if (pathEntries !== undefined) {
|
|
286
312
|
// The win32 branch always uses the Windows PATH separator; hardcoding it
|
|
287
313
|
// keeps the function testable from POSIX runners without a delimiter
|
|
@@ -291,12 +317,12 @@ function windowsPwshCandidateDirs(env: NodeJS.ProcessEnv): string[] {
|
|
|
291
317
|
if (trimmed !== '') dirs.push(trimmed)
|
|
292
318
|
}
|
|
293
319
|
}
|
|
294
|
-
for (const programFiles of [env
|
|
320
|
+
for (const programFiles of [windowsEnv(env, 'ProgramW6432'), windowsEnv(env, 'ProgramFiles')]) {
|
|
295
321
|
if (programFiles === undefined || programFiles.trim() === '') continue
|
|
296
322
|
dirs.push(join(programFiles, 'PowerShell', '7'))
|
|
297
323
|
dirs.push(join(programFiles, 'PowerShell', '7-preview'))
|
|
298
324
|
}
|
|
299
|
-
const localAppData = env
|
|
325
|
+
const localAppData = windowsEnv(env, 'LOCALAPPDATA')
|
|
300
326
|
if (localAppData !== undefined && localAppData.trim() !== '') {
|
|
301
327
|
dirs.push(join(localAppData, 'Microsoft', 'PowerShell', '7'))
|
|
302
328
|
dirs.push(join(localAppData, 'Microsoft', 'PowerShell', '7-preview'))
|
|
@@ -306,6 +332,95 @@ function windowsPwshCandidateDirs(env: NodeJS.ProcessEnv): string[] {
|
|
|
306
332
|
return [...new Set(dirs)]
|
|
307
333
|
}
|
|
308
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Resolve the configured shell executable before handing it to node-pty.
|
|
337
|
+
*
|
|
338
|
+
* Windows' native backend does not consistently apply the shell's PATHEXT
|
|
339
|
+
* lookup to a bare value (`pwsh` / `cmd` can fail with the opaque
|
|
340
|
+
* `File not found:` error), so perform the lookup ourselves: an explicit
|
|
341
|
+
* path is accepted as-is when it exists (or with a PATHEXT suffix when the
|
|
342
|
+
* user omitted `.exe`), and a bare name is searched through PATH, System32,
|
|
343
|
+
* and PowerShell's known install directories.
|
|
344
|
+
*
|
|
345
|
+
* POSIX node-pty uses `execvp`, so a bare name would already follow PATH —
|
|
346
|
+
* but a wrong name made the pty die with a bare
|
|
347
|
+
* `[process exited with code N]`, so probe like Windows anyway: a path with
|
|
348
|
+
* a separator must exist; a bare name is searched along PATH (the colon
|
|
349
|
+
* form is fixed by the platform). A miss is a clear, actionable
|
|
350
|
+
* `shell-not-found` error instead of a cryptic exit code.
|
|
351
|
+
*
|
|
352
|
+
* @param shell - the configured shell (settings page or yaml `config.shell`).
|
|
353
|
+
* @param options - platform/env/exists injection points for tests.
|
|
354
|
+
* @returns the executable path passed to node-pty.
|
|
355
|
+
* @throws {SidebarError} `shell-not-found` when no candidate exists.
|
|
356
|
+
*/
|
|
357
|
+
export function resolveShellExecutable(
|
|
358
|
+
shell: string,
|
|
359
|
+
options: ShellExecutableResolutionOptions = {},
|
|
360
|
+
): string {
|
|
361
|
+
const configured = unquotePath(shell.trim())
|
|
362
|
+
if (configured === '') return configured
|
|
363
|
+
|
|
364
|
+
const platform = options.platform ?? process.platform
|
|
365
|
+
const env = options.env ?? process.env
|
|
366
|
+
const exists = options.exists ?? existsSync
|
|
367
|
+
const notFound = (): SidebarError =>
|
|
368
|
+
new SidebarError('shell-not-found', `shell executable not found: "${configured}"`, 400, { shell: configured })
|
|
369
|
+
|
|
370
|
+
if (platform === 'win32') {
|
|
371
|
+
const rawPathext = windowsEnv(env, 'PATHEXT')
|
|
372
|
+
const executableExts = (rawPathext ?? '.COM;.EXE')
|
|
373
|
+
.split(';')
|
|
374
|
+
.map(extension => extension.trim())
|
|
375
|
+
// node-pty ultimately calls CreateProcess; batch files need an
|
|
376
|
+
// intermediate cmd.exe and therefore are not valid shell executables.
|
|
377
|
+
.filter(extension => /^\.(?:com|exe)$/i.test(extension))
|
|
378
|
+
if (executableExts.length === 0) executableExts.push('.EXE', '.COM')
|
|
379
|
+
|
|
380
|
+
const hasExtension = win32Path.extname(configured) !== ''
|
|
381
|
+
const names = hasExtension
|
|
382
|
+
? [configured]
|
|
383
|
+
: executableExts.map(extension => configured + extension.toLowerCase())
|
|
384
|
+
const hasPath = win32Path.isAbsolute(configured) || /[\\/]/.test(configured)
|
|
385
|
+
const candidates: string[] = []
|
|
386
|
+
if (hasPath) {
|
|
387
|
+
candidates.push(...names)
|
|
388
|
+
} else {
|
|
389
|
+
const path = windowsEnv(env, 'PATH')
|
|
390
|
+
if (path !== undefined) {
|
|
391
|
+
for (const dir of path.split(';').map(entry => entry.trim()).filter(Boolean)) {
|
|
392
|
+
for (const name of names) candidates.push(win32Path.join(dir, name))
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const systemRoot = windowsEnv(env, 'SystemRoot')
|
|
396
|
+
if (systemRoot !== undefined && systemRoot.trim() !== '') {
|
|
397
|
+
for (const name of names) candidates.push(win32Path.join(systemRoot, 'System32', name))
|
|
398
|
+
}
|
|
399
|
+
if (/^pwsh(?:\.exe)?$/i.test(configured)) {
|
|
400
|
+
for (const dir of windowsPwshCandidateDirs(env)) {
|
|
401
|
+
candidates.push(win32Path.join(dir, 'pwsh.exe'))
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
407
|
+
if (exists(candidate)) return candidate
|
|
408
|
+
}
|
|
409
|
+
throw notFound()
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (configured.includes('/')) {
|
|
413
|
+
if (!exists(configured)) throw notFound()
|
|
414
|
+
return configured
|
|
415
|
+
}
|
|
416
|
+
const path = env.PATH ?? '/usr/bin:/bin'
|
|
417
|
+
for (const dir of path.split(':').map(entry => entry.trim()).filter(Boolean)) {
|
|
418
|
+
const candidate = join(dir, configured)
|
|
419
|
+
if (exists(candidate)) return candidate
|
|
420
|
+
}
|
|
421
|
+
throw notFound()
|
|
422
|
+
}
|
|
423
|
+
|
|
309
424
|
/**
|
|
310
425
|
* The interactive shell for this platform, resolved like a terminal
|
|
311
426
|
* emulator: an explicitly configured shell (the `shell` config field) wins,
|
|
@@ -374,3 +489,59 @@ export function shellSpawnArgs(configured: string[] = []): string[] {
|
|
|
374
489
|
if (configured.length > 0) return [...configured]
|
|
375
490
|
return process.platform === 'win32' ? [] : ['-l']
|
|
376
491
|
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Strip ONE pair of surrounding quotes from a configured shell path. Users
|
|
495
|
+
* paste Windows paths with spaces pre-quoted (`"C:\Program Files\…"`); the
|
|
496
|
+
* quotes are shell-input syntax, not part of the path. Unpaired quotes and
|
|
497
|
+
* shorter values stay verbatim.
|
|
498
|
+
*/
|
|
499
|
+
export function unquotePath(value: string): string {
|
|
500
|
+
if (value.length >= 2) {
|
|
501
|
+
const first = value[0]
|
|
502
|
+
const last = value[value.length - 1]
|
|
503
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) return value.slice(1, -1)
|
|
504
|
+
}
|
|
505
|
+
return value
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Split a settings-page shell-arguments string into argv with quote-aware
|
|
510
|
+
* grouping: `'…'` / `"…"` group whitespace, and characters inside quotes are
|
|
511
|
+
* LITERAL — a backslash is never an escape, so Windows paths survive intact
|
|
512
|
+
* (`-File "C:\my init\init.ps1"` → three tokens, the last containing spaces).
|
|
513
|
+
* The price is that an argument containing a literal quote character cannot
|
|
514
|
+
* be expressed; shell startup arguments never need one. An unclosed quote
|
|
515
|
+
* folds the remainder into the current token (settings input stays
|
|
516
|
+
* forgiving); an empty quote pair yields no argument.
|
|
517
|
+
*/
|
|
518
|
+
export function splitShellArgs(input: string): string[] {
|
|
519
|
+
const args: string[] = []
|
|
520
|
+
let current = ''
|
|
521
|
+
let quote: '"' | "'" | null = null
|
|
522
|
+
let started = false
|
|
523
|
+
for (const ch of input) {
|
|
524
|
+
if (quote !== null) {
|
|
525
|
+
if (ch === quote) quote = null
|
|
526
|
+
else current += ch
|
|
527
|
+
continue
|
|
528
|
+
}
|
|
529
|
+
if (ch === '"' || ch === "'") {
|
|
530
|
+
quote = ch
|
|
531
|
+
started = true
|
|
532
|
+
continue
|
|
533
|
+
}
|
|
534
|
+
if (/\s/.test(ch)) {
|
|
535
|
+
if (started) {
|
|
536
|
+
args.push(current)
|
|
537
|
+
current = ''
|
|
538
|
+
started = false
|
|
539
|
+
}
|
|
540
|
+
continue
|
|
541
|
+
}
|
|
542
|
+
current += ch
|
|
543
|
+
started = true
|
|
544
|
+
}
|
|
545
|
+
if (started) args.push(current)
|
|
546
|
+
return args.filter(arg => arg !== '')
|
|
547
|
+
}
|
package/src/sidechat-routes.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type { Agent, AgentSetup, CreateAgentOptions, ResumeAgentOptions } from '
|
|
|
26
26
|
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
|
|
27
27
|
import type { Context as CordisContext } from '@deepseek-ai/cordis'
|
|
28
28
|
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
|
29
|
+
import { SessionLogOffset } from '@deepseek-ai/dsh-session'
|
|
29
30
|
import type { SidebarSessionEvent } from './context-types.ts'
|
|
30
31
|
import type {
|
|
31
32
|
Context,
|
|
@@ -196,17 +197,28 @@ export function buildSidechatApi(ctx: Context): SidechatRoutes {
|
|
|
196
197
|
data: descriptor as unknown as Record<string, unknown>,
|
|
197
198
|
}
|
|
198
199
|
const seed = [...inheritance.seed, descriptorEvent]
|
|
200
|
+
// Fork-marker fields (the exact shape the host's own session.fork uses):
|
|
201
|
+
// without `isSeeded` + `inheritedEventCount` the session treats the whole
|
|
202
|
+
// seed as the child's OWN events, so the child's Inbox constructor
|
|
203
|
+
// replays the parent's `agent/inbox/spliced` events and inherits
|
|
204
|
+
// whatever input sat UNCLAIMED in the parent at the click moment (a
|
|
205
|
+
// queued follow-up, or a tool-result context spliced into next-step
|
|
206
|
+
// between step boundaries of a long-running turn). The first side
|
|
207
|
+
// prompt would then claim and send that stale message BEFORE the
|
|
208
|
+
// boundary + question. The marker keeps `ownEvents()` at the end-seed
|
|
209
|
+
// boundary, so the inherited inbox replays to empty.
|
|
199
210
|
const options: CreateAgentOptions = {
|
|
200
211
|
sessionId: childId,
|
|
201
212
|
meta: {
|
|
202
213
|
...(parentSession.header.cwd === undefined ? {} : { cwd: parentSession.header.cwd }),
|
|
203
214
|
parentSession: parentSession.id,
|
|
204
|
-
isSeeded:
|
|
215
|
+
isSeeded: true,
|
|
205
216
|
origin: 'subagent',
|
|
206
217
|
delegationDepth: (parentSession.header.delegationDepth ?? 0) + 1,
|
|
207
218
|
...(agentPreset === undefined ? {} : { agentPreset }),
|
|
208
219
|
},
|
|
209
220
|
seed: seed as unknown as readonly SessionEvent[],
|
|
221
|
+
inheritedEventCount: SessionLogOffset(seed.length),
|
|
210
222
|
agentOptions: { ...parent.options },
|
|
211
223
|
setup,
|
|
212
224
|
signal: AbortSignal.timeout(CREATE_TIMEOUT_MS),
|
package/src/tools.ts
CHANGED
|
@@ -287,13 +287,17 @@ export function registerTools(
|
|
|
287
287
|
register(defineTool({
|
|
288
288
|
name: 'terminal_wait_for',
|
|
289
289
|
description:
|
|
290
|
-
'Block until a
|
|
290
|
+
'Block until a pattern appears in a terminal\'s retained transcript, or until the timeout elapses, or until the terminal exits — whichever happens first. '
|
|
291
291
|
+ 'Use this to synchronize on command completion cues ( e.g. a shell prompt, "done", "Listening on", "Build successful" ) '
|
|
292
292
|
+ 'without busy-polling terminal_read. '
|
|
293
|
+
+ 'The needle is a JavaScript regular expression ( a pattern that fails to compile falls back to verbatim substring matching ). '
|
|
294
|
+
+ 'One needle may cover MULTIPLE outcomes — e.g. wait on `(BUILD_OK|BUILD_FAIL)` or `Build (succeeded|failed)` returns as soon as EITHER marker appears, '
|
|
295
|
+
+ 'and the found result\'s `match` field tells which alternative hit ( build success vs failure ). '
|
|
293
296
|
+ 'The wait scans the FULL retained transcript (up to ~1 MiB) on every poll, so a needle that scrolled past the most recent chunk is still a match. '
|
|
294
|
-
+ 'Returns `found` with the line/column
|
|
297
|
+
+ 'Returns `found` with the line/column and the matched text, `timeout` if the needle did not appear in time, or `exited` if the terminal process died before the needle appeared. '
|
|
295
298
|
+ 'Default timeout is 10 seconds; raise it for long-running commands ( dev servers, test suites ). '
|
|
296
|
-
+ 'The wait is cooperative: a tool-call cancel ( or agent turn end ) aborts it immediately.'
|
|
299
|
+
+ 'The wait is cooperative: a tool-call cancel ( or agent turn end ) aborts it immediately. '
|
|
300
|
+
+ 'The user can skip the wait from the sidebar ( a banner on the terminal\'s tab shows the needle and a skip button ) — the tool then returns `skipped`.',
|
|
297
301
|
parameters: {
|
|
298
302
|
uuid: {
|
|
299
303
|
type: 'string',
|
|
@@ -303,7 +307,8 @@ export function registerTools(
|
|
|
303
307
|
needle: {
|
|
304
308
|
type: 'string',
|
|
305
309
|
required: true,
|
|
306
|
-
description: '
|
|
310
|
+
description: 'JavaScript regular expression to wait for (case-sensitive); a pattern that fails to compile falls back to verbatim substring matching. '
|
|
311
|
+
+ 'May cover several outcomes in one wait ( e.g. `(BUILD_OK|BUILD_FAIL)` for build success/failure ) — check `match` in the found result to see which one hit. Must be non-empty.',
|
|
307
312
|
},
|
|
308
313
|
timeout_ms: {
|
|
309
314
|
type: 'number',
|
|
@@ -321,6 +326,7 @@ export function registerTools(
|
|
|
321
326
|
needle: { type: 'string', required: true },
|
|
322
327
|
line: { type: 'integer', required: true, description: '0-based line index in the retained transcript where the needle first appeared.' },
|
|
323
328
|
column: { type: 'integer', required: true, description: '0-based column index within that line where the match starts.' },
|
|
329
|
+
match: { type: 'string', required: true, description: 'The text that actually matched — for multi-outcome patterns ( e.g. `(BUILD_OK|BUILD_FAIL)` ) this tells which alternative matched.' },
|
|
324
330
|
elapsedMs: { type: 'integer', required: true, description: 'Wall-clock milliseconds from wait start to match.' },
|
|
325
331
|
},
|
|
326
332
|
},
|
|
@@ -344,16 +350,28 @@ export function registerTools(
|
|
|
344
350
|
exitSignal: { oneOf: [{ type: 'string' }, { type: 'null' }], description: 'Exit signal name, if killed by a signal.' },
|
|
345
351
|
},
|
|
346
352
|
},
|
|
353
|
+
{
|
|
354
|
+
type: 'object',
|
|
355
|
+
additionalProperties: false,
|
|
356
|
+
properties: {
|
|
357
|
+
kind: { type: 'string', required: true, const: 'skipped' },
|
|
358
|
+
needle: { type: 'string', required: true },
|
|
359
|
+
},
|
|
360
|
+
},
|
|
347
361
|
],
|
|
348
362
|
},
|
|
349
363
|
render: (_args, value) => {
|
|
350
|
-
const v = value as { kind: 'found' | 'timeout' | 'exited'; needle: string; elapsedMs?: number; timeoutMs?: number; line?: number; column?: number; exitCode?: number | null; exitSignal?: string | null }
|
|
364
|
+
const v = value as { kind: 'found' | 'timeout' | 'exited' | 'skipped'; needle: string; elapsedMs?: number; timeoutMs?: number; line?: number; column?: number; match?: string; exitCode?: number | null; exitSignal?: string | null }
|
|
351
365
|
if (v.kind === 'found') {
|
|
352
|
-
|
|
366
|
+
const matched = v.match !== undefined && v.match !== '' ? `, matched "${v.match}"` : ''
|
|
367
|
+
return [{ type: 'text', text: `Found "${v.needle}" at line ${v.line}, column ${v.column}${matched} (after ${v.elapsedMs}ms).` }]
|
|
353
368
|
}
|
|
354
369
|
if (v.kind === 'timeout') {
|
|
355
370
|
return [{ type: 'text', text: `Timed out after ${v.timeoutMs}ms waiting for "${v.needle}". Call terminal_read to inspect the transcript.` }]
|
|
356
371
|
}
|
|
372
|
+
if (v.kind === 'skipped') {
|
|
373
|
+
return [{ type: 'text', text: `Skipped by user while waiting for "${v.needle}" — the wait ended early. Call terminal_read to inspect the transcript and decide how to proceed.` }]
|
|
374
|
+
}
|
|
357
375
|
const exitInfo = v.exitCode !== undefined && v.exitCode !== null ? ` (exit code ${v.exitCode})` : ''
|
|
358
376
|
return [{ type: 'text', text: `Terminal exited before "${v.needle}" appeared${exitInfo}.` }]
|
|
359
377
|
},
|
package/src/wire.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type SidebarErrorCode =
|
|
|
17
17
|
| 'git-error'
|
|
18
18
|
| 'pty-error'
|
|
19
19
|
| 'pty-deps-missing'
|
|
20
|
+
| 'shell-not-found'
|
|
20
21
|
| 'job-error'
|
|
21
22
|
| 'cdp-down'
|
|
22
23
|
| 'sidechat-error'
|
|
@@ -31,6 +32,8 @@ export class SidebarError extends Error {
|
|
|
31
32
|
readonly code: SidebarErrorCode,
|
|
32
33
|
message: string,
|
|
33
34
|
readonly status = 400,
|
|
35
|
+
/** Optional structured context (e.g. `{ shell }` for shell-not-found). */
|
|
36
|
+
readonly meta?: Record<string, string>,
|
|
34
37
|
) {
|
|
35
38
|
super(message)
|
|
36
39
|
}
|