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.
Files changed (69) hide show
  1. package/README.md +2 -2
  2. package/lib/client-editor.js +436 -227
  3. package/lib/client-registry.js +627 -7391
  4. package/lib/client-terminal.js +234 -182
  5. package/lib/client.js +634 -7398
  6. package/lib/index.js +367 -67
  7. package/lib/types/agent-pty.d.ts +62 -4
  8. package/lib/types/bundle-route.d.ts +1 -1
  9. package/lib/types/client/EditorHost.d.ts +1 -1
  10. package/lib/types/client/FileTree.d.ts +2 -2
  11. package/lib/types/client/TreePanel.d.ts +1 -1
  12. package/lib/types/client/chunk-loader.d.ts +1 -1
  13. package/lib/types/client/chunks/locale.d.ts +3 -0
  14. package/lib/types/client/conversation-draft.d.ts +85 -4
  15. package/lib/types/client/locales.d.ts +1 -20
  16. package/lib/types/client/selection-popup.d.ts +58 -0
  17. package/lib/types/client/service.d.ts +1 -1
  18. package/lib/types/client/terminal-font.d.ts +25 -2
  19. package/lib/types/context-types.d.ts +3 -1
  20. package/lib/types/index.d.ts +9 -0
  21. package/lib/types/prefs-shared.d.ts +7 -5
  22. package/lib/types/pty-manager.d.ts +53 -0
  23. package/lib/types/wire.d.ts +6 -2
  24. package/package.json +1 -1
  25. package/src/agent-pty.ts +221 -33
  26. package/src/bundle-route.ts +1 -1
  27. package/src/client/EditorHost.tsx +1 -1
  28. package/src/client/FileTree.tsx +4 -4
  29. package/src/client/Sidebar.tsx +41 -22
  30. package/src/client/TerminalView.tsx +13 -0
  31. package/src/client/TextEditor.tsx +27 -45
  32. package/src/client/TreePanel.tsx +16 -1
  33. package/src/client/chunk-loader.ts +1 -1
  34. package/src/client/chunks/locale.tsx +60 -0
  35. package/src/client/conversation-draft.ts +233 -7
  36. package/src/client/index.tsx +19 -8
  37. package/src/client/locales-ar.ts +5 -4
  38. package/src/client/locales-de.ts +5 -4
  39. package/src/client/locales-fr.ts +5 -4
  40. package/src/client/locales-hi.ts +5 -4
  41. package/src/client/locales-id.ts +5 -4
  42. package/src/client/locales-it.ts +5 -4
  43. package/src/client/locales-ja.ts +5 -4
  44. package/src/client/locales-ko.ts +5 -4
  45. package/src/client/locales-nl.ts +5 -4
  46. package/src/client/locales-pl.ts +5 -4
  47. package/src/client/locales-pt.ts +5 -4
  48. package/src/client/locales-ru.ts +5 -4
  49. package/src/client/locales-sv.ts +5 -4
  50. package/src/client/locales-th.ts +5 -4
  51. package/src/client/locales-tr.ts +5 -4
  52. package/src/client/locales-vi.ts +5 -4
  53. package/src/client/locales-zh-HK.ts +5 -4
  54. package/src/client/locales-zh-MO.ts +5 -4
  55. package/src/client/locales-zh-TW.ts +5 -4
  56. package/src/client/locales.ts +17 -51
  57. package/src/client/selection-popup.ts +155 -0
  58. package/src/client/service.ts +1 -1
  59. package/src/client/state.ts +14 -7
  60. package/src/client/terminal-font.ts +34 -3
  61. package/src/context-types.ts +3 -2
  62. package/src/git.ts +17 -1
  63. package/src/index.ts +39 -6
  64. package/src/open-external.ts +3 -4
  65. package/src/prefs-shared.ts +7 -5
  66. package/src/pty-manager.ts +176 -5
  67. package/src/sidechat-routes.ts +13 -1
  68. package/src/tools.ts +24 -6
  69. package/src/wire.ts +3 -0
@@ -15,6 +15,20 @@ export declare function clampDims(cols: number, rows: number): {
15
15
  cols: number;
16
16
  rows: number;
17
17
  };
18
+ /**
19
+ * Arm the Windows pre-ready resize gate for one freshly spawned pty.
20
+ * No-op on POSIX and for injected ptys without `onData`.
21
+ */
22
+ export declare function armPtyResizeGate(pty: IPty): void;
23
+ /**
24
+ * Best-effort resize for WebSocket-driven terminal views. Layout animation
25
+ * can briefly produce unusable dimensions, and node-pty can reject a resize
26
+ * after the socket setup's outer try/catch has returned. Ignore that one
27
+ * frame so the host stays alive and a later valid measurement can retry.
28
+ * Returns whether node-pty accepted the resize (or parked it for replay on
29
+ * the first output — the Windows pre-ready window).
30
+ */
31
+ export declare function tryResizePty(pty: Pick<IPty, 'resize'>, cols: number, rows: number): boolean;
18
32
  /**
19
33
  * Serializable snapshot of one agent terminal — the shape the model sees
20
34
  * through `terminal_list` and the sidebar sees through the push endpoint.
@@ -36,6 +50,15 @@ export interface AgentTerminalSnapshot {
36
50
  exitCode?: number | null;
37
51
  /** Exit signal name if the process was killed by a signal; null otherwise. */
38
52
  exitSignal?: string | null;
53
+ /**
54
+ * The model's active `terminal_wait_for` on this terminal (the sidebar
55
+ * renders the wait banner from it). Present only while a wait is
56
+ * registered; carries the LATEST wait when several overlap.
57
+ */
58
+ waiting?: {
59
+ needle: string;
60
+ since: number;
61
+ };
39
62
  }
40
63
  /** One live agent terminal. */
41
64
  export interface AgentTerminalHandle {
@@ -59,6 +82,17 @@ export interface AgentTerminalHandle {
59
82
  exitCode?: number | null;
60
83
  /** Exit signal number once known (POSIX only; undefined on Windows). */
61
84
  exitSignal?: number | null;
85
+ /** Active wait_for registrations (skip bookkeeping; empty while idle). */
86
+ waits: AgentTerminalActiveWait[];
87
+ }
88
+ /** One active wait_for registration on a handle (banner + skip bookkeeping). */
89
+ export interface AgentTerminalActiveWait {
90
+ /** The needle being awaited (shown on the sidebar wait banner). */
91
+ needle: string;
92
+ /** Epoch ms when the wait registered (age display / debugging). */
93
+ since: number;
94
+ /** Flipped by `skipWait()`; the waiting poll returns `skipped` within one tick. */
95
+ skipped: boolean;
62
96
  }
63
97
  /** Read result shape (mirrors the official tool-pty terminal_read contract). */
64
98
  export interface AgentTerminalReadResult {
@@ -73,14 +107,18 @@ export interface AgentTerminalReadResult {
73
107
  }
74
108
  /** Outcome of {@link AgentPtyRegistry.waitFor}. */
75
109
  export type AgentTerminalWaitResult = {
76
- /** The needle was found in the transcript. */
110
+ /** The pattern that was awaited. */
77
111
  kind: 'found';
78
- /** The matched substring. */
79
112
  needle: string;
80
113
  /** 0-based line index (in the retained transcript) where the needle first appeared. */
81
114
  line: number;
82
115
  /** 0-based column index within that line where the match starts. */
83
116
  column: number;
117
+ /**
118
+ * The text that actually matched — for multi-outcome patterns
119
+ * (e.g. `(BUILD_OK|BUILD_FAIL)`) this tells which alternative matched.
120
+ */
121
+ match: string;
84
122
  /** Elapsed wall-clock milliseconds from the wait start to the match. */
85
123
  elapsedMs: number;
86
124
  } | {
@@ -101,6 +139,11 @@ export type AgentTerminalWaitResult = {
101
139
  exitCode?: number | null;
102
140
  /** The exit signal name, if the process was killed by a signal. */
103
141
  exitSignal?: string | null;
142
+ } | {
143
+ /** The user skipped the wait from the sidebar banner. */
144
+ kind: 'skipped';
145
+ /** The needle that was awaited. */
146
+ needle: string;
104
147
  };
105
148
  /** Snapshot projection of a handle (drops the pty reference and transcript). */
106
149
  export declare function snapshotOf(handle: AgentTerminalHandle): AgentTerminalSnapshot;
@@ -181,12 +224,27 @@ export declare class AgentPtyRegistry {
181
224
  * make event-driven wakeups unreliable. A 50ms poll is fast enough for
182
225
  * interactive use and simple enough to be obviously correct.
183
226
  * @param uuid - terminal to watch.
184
- * @param needle - substring to search for (case-sensitive, verbatim).
227
+ * @param needle - JavaScript regular expression to search for
228
+ * (case-sensitive); a pattern that fails to compile falls back to
229
+ * verbatim substring matching. May cover several outcomes at once
230
+ * (e.g. `(BUILD_OK|BUILD_FAIL)` for build success vs failure) — the
231
+ * returned `match` reports the text that actually matched, so callers
232
+ * can tell which outcome hit.
185
233
  * @param timeoutMs - max wait; default 10000 (10s). Clamped to ≥100ms.
186
234
  * @param signal - caller-owned cancellation; aborts the wait re-throwing.
187
- * @returns one of `found` / `timeout` / `exited`.
235
+ * A wait can also be skipped by the user from the sidebar banner
236
+ * (`skipWait`), which resolves it with `{kind:'skipped'}`.
237
+ * @returns one of `found` / `timeout` / `exited` / `skipped`.
188
238
  */
189
239
  waitFor(uuid: string, needle: string, timeoutMs?: number, signal?: AbortSignal): Promise<AgentTerminalWaitResult>;
240
+ /**
241
+ * Mark every active wait on one terminal as skipped (the sidebar banner's
242
+ * skip button). Each waiting poll loop observes its record's flag within
243
+ * one 50ms tick and returns `{kind:'skipped'}`. Idempotent: 0 when nothing
244
+ * is waiting (a stale banner racing a wait that already resolved).
245
+ * @returns the number of waits that transitioned to skipped.
246
+ */
247
+ skipWait(uuid: string): number;
190
248
  /**
191
249
  * Send a POSIX signal to a terminal's foreground process.
192
250
  *
@@ -1,6 +1,6 @@
1
1
  import type { Context, SidebarHttpRequest, SidebarHttpResponse } from './context-types.ts';
2
2
  /** The chunk names the client may request (mirror of src/client/chunk-loader.ts). */
3
- export declare const CHUNK_NAMES: readonly ["terminal", "editor"];
3
+ export declare const CHUNK_NAMES: readonly ["terminal", "editor", "locale"];
4
4
  export type ChunkName = (typeof CHUNK_NAMES)[number];
5
5
  /**
6
6
  * Build the /sidebar/bundle route handler. `fence` is the shared browser-
@@ -9,5 +9,5 @@ export declare function EditorHost(props: {
9
9
  expanded: string[];
10
10
  revealed: string[];
11
11
  onToggleDir: (path: string) => void;
12
- onReferenceFile: (path: string) => void;
12
+ onReferenceFile: (path: string, isDir: boolean) => void;
13
13
  }): import("react").JSX.Element;
@@ -27,8 +27,8 @@ export declare function FileTree(props: {
27
27
  onOpenWith?: (targetId: string, path: string) => void;
28
28
  /** Toggle one target's pinned state (the submenu row's pushpin). */
29
29
  onToggleOpenWithPin?: (targetId: string) => void;
30
- /** Insert `@<relative path>` into the composer draft. */
31
- onReferenceFile: (path: string) => void;
30
+ /** Insert `@<relative path>` into the composer draft (file vs directory). */
31
+ onReferenceFile: (path: string, isDir: boolean) => void;
32
32
  /** Bump to wipe the level cache and reload the visible set. */
33
33
  refreshTick: number;
34
34
  /** Upload into `dir` (absolute, inside the workspace); runs in the caller. */
@@ -17,7 +17,7 @@ export declare function TreePanel(props: {
17
17
  openWithSsh?: boolean;
18
18
  onOpenWith?: (targetId: string, path: string) => void;
19
19
  onToggleOpenWithPin?: (targetId: string) => void;
20
- onReferenceFile: (path: string) => void;
20
+ onReferenceFile: (path: string, isDir: boolean) => void;
21
21
  /** Full-window presentation: the panel fills its host instead of docking
22
22
  * at a fixed width. */
23
23
  full?: boolean;
@@ -48,7 +48,7 @@
48
48
  * client.js); an edit that does land while a core HMR happens is caught by
49
49
  * the ETag comparison on the next activation.
50
50
  */
51
- export type ChunkName = 'terminal' | 'editor';
51
+ export type ChunkName = 'terminal' | 'editor' | 'locale';
52
52
  /** The module exports a chunk factory provides (namespace-ish record). */
53
53
  export type ChunkExports = Record<string, unknown>;
54
54
  /**
@@ -0,0 +1,3 @@
1
+ /** Every non-zh/en dictionary keyed by its override id, ready for the
2
+ * better-locale store's `register(ns, dicts)`. */
3
+ export declare const localeDicts: Record<string, Record<string, string>>;
@@ -1,14 +1,95 @@
1
1
  /**
2
- * Append text to the current session's composer draft through the
2
+ * Insert text into the current session's composer draft through the
3
3
  * conversation service — the shared path behind the explorer's @-reference
4
4
  * button and the viewer selection popup. The service is resolved lazily
5
5
  * through `ctx.get` (the inject-free read the app's own plugins use); a
6
6
  * missing service or scope degrades to a logged no-op, never a crash.
7
+ *
8
+ * File references additionally use DSH's own structured insert event
9
+ * (`slash/input-insert-reference`, see `insertFileReference`) instead of
10
+ * plain draft text: the native `@file` picker emits this event, and the
11
+ * conversation input machine mints one occurrence whose chip covers the
12
+ * whole reference. Plain text `@folder/file.ts` only ever gets DSH's
13
+ * folder-ref decoration (`@folder/`) — the file name stays undecorated — so
14
+ * plain append is the fallback, not the primary path, for files.
15
+ *
16
+ * Insert position (upstream issue #425): the draft store only exposes the
17
+ * whole string (`getSnapshot().draft` + `setDraft(text)`) — there is no
18
+ * caret API on the conversation service. The composer's `<textarea>` keeps
19
+ * its last selection even while unfocused, so the live caret is probed from
20
+ * the DOM (guarded by a value-sync check), and the text is spliced at that
21
+ * position, replacing any live selection — with whitespace-aware joins, an
22
+ * insert into the middle of a sentence keeps single-space separation like
23
+ * the append path. An unknown/stale caret falls back to appending at the
24
+ * end (the pre-fix behavior).
25
+ *
26
+ * The caret is also *restored* after the insert: committing a programmatic
27
+ * draft change resets the controlled textarea's caret (observed landing at
28
+ * the start of the value), which would make every later insert probe the
29
+ * reset position and drift the stack (A|B + C + D ended up as |DACB). The
30
+ * placement (`placeComposerCaretAfterInsert`) puts the caret right after the
31
+ * inserted text once the value commit lands — the index accounts for the
32
+ * separating space on the left, so stacked inserts stay at their running
33
+ * position (A|B + C + D → ACD|B).
7
34
  */
8
35
  import type { Context } from '../context-types.ts';
36
+ /** A resolved composer caret/selection in draft coordinates. */
37
+ export interface DraftCaret {
38
+ start: number;
39
+ end: number;
40
+ }
9
41
  /**
10
- * Append `text` to the session's composer draft (space-separated, like the
11
- * @-mentions). Returns false — and logs — when the conversation service or
12
- * the session scope is unavailable.
42
+ * The spliced draft string (see {@link spliceInsert}); pure string math.
43
+ */
44
+ export declare function insertAtCaret(draft: string, text: string, caret: DraftCaret | null): string;
45
+ /**
46
+ * Resolve the composer's live caret from its DOM `<textarea>`. The draft
47
+ * store has no caret API, so the sidebar reads the composed input's selection
48
+ * directly; the value-sync check (`el.value === draft`) discards stale or
49
+ * wrong-composer reads — a caret must never be applied against a draft it
50
+ * was not measured on.
51
+ *
52
+ * Returns null when the composer is missing, disabled/read-only, out of
53
+ * sync with the store draft, or has no measurable selection (odd hosts
54
+ * report null selectionStart/End).
55
+ */
56
+ export declare function probeComposerCaret(draft: string): DraftCaret | null;
57
+ /**
58
+ * Restore the composer caret to `caretIndex` after a programmatic
59
+ * `setDraft` commit. A controlled textarea update resets the caret (React
60
+ * commits the value asynchronously and the browser moves the caret to the
61
+ * start/end), so the placement is scheduled and retried across at most two
62
+ * animation frames (setTimeout fallback), and only applied when the textarea
63
+ * still matches `expectedDraft` — a newer edit or a different composer wins
64
+ * the race untouched. The caret is clamped into the value bounds, mirroring
65
+ * how browsers clamp type-in positions.
66
+ */
67
+ export declare function placeComposerCaretAfterInsert(expectedDraft: string, caretIndex: number): void;
68
+ /**
69
+ * Insert `text` into the session's composer draft at the composer's live
70
+ * caret (see {@link probeComposerCaret}), falling back to appending at the
71
+ * end when the caret cannot be resolved. Returns false — and logs — when the
72
+ * conversation service or the session scope is unavailable.
13
73
  */
14
74
  export declare function appendToDraft(ctx: Context, sessionId: string, text: string): boolean;
75
+ /**
76
+ * The DSH `@file` spelling for one relative path, mirroring the host grammar
77
+ * (`formatFileMention` in `@deepseek-ai/dsh-file-reference`): plain when
78
+ * there is no whitespace, quoted when there is, and `undefined` when the
79
+ * path contains a control character or an embedded quote the editor grammar
80
+ * cannot represent.
81
+ */
82
+ export declare function fileMention(relativePath: string): {
83
+ mention: string;
84
+ label: string;
85
+ } | undefined;
86
+ /**
87
+ * Insert one FILE reference as a structured chip (like DSH's own `@` picker).
88
+ * The chip displays `@<basename>` but serializes to `@<relative path>` on
89
+ * send, so the reference stays a single link from trigger to basename.
90
+ *
91
+ * Directories are NOT handled here: DSH's folder grammar wants the trailing
92
+ * slash as plain text (`@dir/`) so completion can descend, which
93
+ * `appendToDraft` already covers.
94
+ */
95
+ export declare function insertFileReference(ctx: Context, sessionId: string, relativePath: string): boolean;
@@ -94,6 +94,7 @@ export declare const zh: {
94
94
  terminalDepsFailed: string;
95
95
  terminalDepsHint: string;
96
96
  terminalDepsProfile: string;
97
+ terminalShellNotFound: string;
97
98
  refresh: string;
98
99
  refreshUnsavedConfirm: string;
99
100
  save: string;
@@ -362,26 +363,6 @@ export declare const en: Record<keyof typeof zh, string>;
362
363
  * (`'sidebar'` is taken by DSH's own ui-sidebar, hence this distinct name).
363
364
  */
364
365
  export declare const LOCALE_NS = "betterSidebar";
365
- /** The ja dictionary (key-set-equal to zh, enforced by the type annotation). */
366
- export declare const ja: Record<keyof typeof zh, string>;
367
- export declare const de: Record<keyof typeof zh, string>;
368
- export declare const fr: Record<keyof typeof zh, string>;
369
- export declare const pt: Record<keyof typeof zh, string>;
370
- export declare const ko: Record<keyof typeof zh, string>;
371
- export declare const ar: Record<keyof typeof zh, string>;
372
- export declare const hi: Record<keyof typeof zh, string>;
373
- export declare const id: Record<keyof typeof zh, string>;
374
- export declare const tr: Record<keyof typeof zh, string>;
375
- export declare const vi: Record<keyof typeof zh, string>;
376
- export declare const th: Record<keyof typeof zh, string>;
377
- export declare const ru: Record<keyof typeof zh, string>;
378
- export declare const it: Record<keyof typeof zh, string>;
379
- export declare const nl: Record<keyof typeof zh, string>;
380
- export declare const sv: Record<keyof typeof zh, string>;
381
- export declare const pl: Record<keyof typeof zh, string>;
382
- export declare const zhHK: Record<keyof typeof zh, string>;
383
- export declare const zhTW: Record<keyof typeof zh, string>;
384
- export declare const zhMO: Record<keyof typeof zh, string>;
385
366
  /**
386
367
  * The better-locale override store attached by the client apply
387
368
  * (absent → no override; the zh/en chain runs). The store's `active`
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The floating "add selection to conversation" popup shared by the text
3
+ * viewer: a viewport-anchored button portaled to `document.body`, kept alive
4
+ * across the selection gesture and committed on click.
5
+ *
6
+ * Dismissal contract (upstream issue #425): the popup must never outlive its
7
+ * editor surface. The sidebar keeps every tab MOUNTED — switching tabs only
8
+ * flips the pane cell to `display:none` and collapsing the panel translates
9
+ * it off-screen — while the portaled `position:fixed` button stays pinned to
10
+ * its viewport anchor, ignoring both. The caller already hides on surface
11
+ * scrolls, selection collapse and content swaps; this hook adds the global
12
+ * dismissal that covers everything else:
13
+ *
14
+ * - any `mousedown` outside the button (tab bar, composer, another pane,
15
+ * the collapse toggle, …) closes it;
16
+ * - `Escape` closes it;
17
+ * - the document going hidden (`visibilitychange`) or the window losing
18
+ * focus closes it;
19
+ * - an `IntersectionObserver` on the editor surface closes it as soon as the
20
+ * surface leaves the viewport — the tab-switch (`display:none`) and
21
+ * panel-collapse (translated off-screen) paths have no DOM events of their
22
+ * own, so the geometry signal is the only reliable one.
23
+ *
24
+ * The button's own `mousedown` is never treated as an outside click: the
25
+ * caller preventDefaults it to keep the selection/caret alive until the
26
+ * click commits (the hook's capture-phase listener runs first and must not
27
+ * hide for it).
28
+ */
29
+ import { type RefObject } from 'react';
30
+ /** The floating "add to conversation" action: payload + viewport anchor. */
31
+ export interface SelectionPopup {
32
+ insert: string;
33
+ left: number;
34
+ top: number;
35
+ }
36
+ export interface SelectionPopupOptions {
37
+ /** Commit the payload into the composer draft (button click). */
38
+ onCommit(insert: string): void;
39
+ /**
40
+ * The DOM surface that must stay on screen for the popup to live: the
41
+ * CodeMirror host element. Called lazily (refs are null until the content
42
+ * loads).
43
+ */
44
+ getSurface(): HTMLElement | null;
45
+ }
46
+ export interface SelectionPopupControls {
47
+ /** The current popup (null = hidden). */
48
+ popup: SelectionPopup | null;
49
+ /** Attach to the portaled button element. */
50
+ buttonRef: RefObject<HTMLButtonElement>;
51
+ /** Anchor the popup above a selection (viewport-clamped). */
52
+ show(insert: string, left: number, top: number): void;
53
+ /** Hide the popup (idempotent). */
54
+ hide(): void;
55
+ /** The button's click: commit the stored payload, then hide. */
56
+ commit(): void;
57
+ }
58
+ export declare function useSelectionPopup(options: SelectionPopupOptions): SelectionPopupControls;
@@ -132,7 +132,7 @@ export interface TabComponentProps {
132
132
  /** The explorer's reveal-highlight set (ExplorerView; "Show in folder" targets). */
133
133
  revealed?: string[];
134
134
  onToggleDir?: (path: string) => void;
135
- onReferenceFile?: (path: string) => void;
135
+ onReferenceFile?: (path: string, isDir: boolean) => void;
136
136
  onOpenFile?: (path: string) => void;
137
137
  onOpenDiff?: (tab: SidebarTab) => void;
138
138
  onSubagentJump?: (childSessionId: string) => void;
@@ -60,12 +60,35 @@ export declare const ICON_FONT_FALLBACKS: readonly string[];
60
60
  * @returns the stack with icon fallbacks merged in.
61
61
  */
62
62
  export declare function withIconFontFallbacks(stack: string): string;
63
+ /**
64
+ * Guarantee the stack ends in a generic family, so a font the browser cannot
65
+ * resolve degrades to a monospace one.
66
+ *
67
+ * Without this a stack of unresolvable names (a misspelled family, or one only
68
+ * installed on the machine running the shell rather than the one running the
69
+ * browser) falls through to the browser's *standard* font, which is
70
+ * proportional — xterm then measures its cell from a proportional advance and
71
+ * the whole grid breaks, rather than merely losing the requested typeface.
72
+ *
73
+ * This does not sniff whether the named families exist (an explicit non-goal
74
+ * of the terminal font design): it only terminates the stack. A stack that
75
+ * already names a generic family is returned untouched, so a user who
76
+ * deliberately wrote one keeps it. Generics are matched unquoted only — a
77
+ * quoted `"monospace"` is a family *name*, not the keyword.
78
+ *
79
+ * @param stack - the resolved font-family value.
80
+ * @returns the stack, ending in a generic family.
81
+ */
82
+ export declare function withMonospaceFallback(stack: string): string;
63
83
  /**
64
84
  * Resolve the xterm font options for the given prefs.
65
85
  *
66
86
  * The base family keeps its existing precedence — user pref > theme code
67
- * font > built-in stack — and then {@link withIconFontFallbacks} tops it up
68
- * so prompt icons resolve regardless of which base won.
87
+ * font > built-in stack. {@link withMonospaceFallback} then terminates the
88
+ * stack with a generic family so an unresolvable name degrades to a
89
+ * monospace instead of the proportional standard font, and finally
90
+ * {@link withIconFontFallbacks} tops it up so prompt icons resolve
91
+ * regardless of which base won.
69
92
  *
70
93
  * @param prefs - the current side card preferences.
71
94
  * @param themeFontFamily - the app's theme code font (`--ds-font-family-code`
@@ -430,10 +430,12 @@ export interface SidebarLocaleService {
430
430
  }
431
431
  /** The composer draft face the sidebar reaches through `ctx.conversation.input`. */
432
432
  export interface SidebarSessionInput {
433
- /** The live input store (draft read for append). */
433
+ /** The live input store (draft read for append). `draftRev` is the machine's
434
+ * span-CAS revision — required to mint a structured file-reference chip. */
434
435
  state: {
435
436
  getSnapshot(): {
436
437
  draft: string;
438
+ draftRev?: number;
437
439
  };
438
440
  };
439
441
  /** Replace the draft text (the input machine's single public write path). */
@@ -45,3 +45,12 @@ export interface SidebarSettingsFace {
45
45
  * {@link resolveSidebarConfig}.
46
46
  */
47
47
  export declare function apply(ctx: Context, config?: SidebarConfig): void;
48
+ /**
49
+ * The WS close reason for a failed terminal attach. A missing configured
50
+ * shell gets a SHORT machine-readable marker (`shell-not-found:<name>`,
51
+ * capped by BYTES — a WS close reason allows at most 123 bytes, which `ws`
52
+ * validates with `Buffer.byteLength`) that the client maps to a localized,
53
+ * actionable banner; every other failure keeps the raw message (the
54
+ * model-side tool errors read it verbatim).
55
+ */
56
+ export declare function wsCloseReasonOf(error: unknown): string;
@@ -14,14 +14,16 @@ export interface SidebarPrefs {
14
14
  /** Default panel width as a percent of the window width (20–60). */
15
15
  defaultWidthPercent: number;
16
16
  /**
17
- * Whether the sidebar auto-activates (opens the panel) and expands the
18
- * Subagent page when the current conversation spawns a new subagent.
17
+ * Whether the sidebar auto-activates the Subagent page when the current
18
+ * conversation spawns a new subagent. Wide viewports also open the panel;
19
+ * narrow viewports prepare the tab without opening the full-screen drawer.
19
20
  */
20
21
  autoOpenSubagent: boolean;
21
22
  /**
22
- * Whether the sidebar auto-activates (opens the panel) and expands the
23
- * Jobs page when a NEW background job appears for the current
24
- * conversation (any new job id, not just the first one).
23
+ * Whether the sidebar auto-activates the Jobs page when a NEW background
24
+ * job appears for the current conversation (any new job id, not just the
25
+ * first one). Wide viewports also open the panel; narrow viewports prepare
26
+ * the tab without opening the full-screen drawer.
25
27
  */
26
28
  autoOpenJobs: boolean;
27
29
  /**
@@ -134,6 +134,41 @@ export interface ShellResolutionOptions {
134
134
  /** File-existence probe override (defaults to `existsSync`). */
135
135
  exists?: (path: string) => boolean;
136
136
  }
137
+ /** Inputs for resolving one configured shell into the executable path passed
138
+ * to node-pty. Injectable so the Windows-only search semantics stay covered
139
+ * on POSIX CI runners. */
140
+ export interface ShellExecutableResolutionOptions {
141
+ /** Platform override (defaults to `process.platform`). */
142
+ platform?: NodeJS.Platform;
143
+ /** Environment override; Windows reads PATH/PATHEXT/SystemRoot plus the
144
+ * PowerShell well-known-location variables. */
145
+ env?: NodeJS.ProcessEnv;
146
+ /** File-existence probe override (defaults to `existsSync`). */
147
+ exists?: (path: string) => boolean;
148
+ }
149
+ /**
150
+ * Resolve the configured shell executable before handing it to node-pty.
151
+ *
152
+ * Windows' native backend does not consistently apply the shell's PATHEXT
153
+ * lookup to a bare value (`pwsh` / `cmd` can fail with the opaque
154
+ * `File not found:` error), so perform the lookup ourselves: an explicit
155
+ * path is accepted as-is when it exists (or with a PATHEXT suffix when the
156
+ * user omitted `.exe`), and a bare name is searched through PATH, System32,
157
+ * and PowerShell's known install directories.
158
+ *
159
+ * POSIX node-pty uses `execvp`, so a bare name would already follow PATH —
160
+ * but a wrong name made the pty die with a bare
161
+ * `[process exited with code N]`, so probe like Windows anyway: a path with
162
+ * a separator must exist; a bare name is searched along PATH (the colon
163
+ * form is fixed by the platform). A miss is a clear, actionable
164
+ * `shell-not-found` error instead of a cryptic exit code.
165
+ *
166
+ * @param shell - the configured shell (settings page or yaml `config.shell`).
167
+ * @param options - platform/env/exists injection points for tests.
168
+ * @returns the executable path passed to node-pty.
169
+ * @throws {SidebarError} `shell-not-found` when no candidate exists.
170
+ */
171
+ export declare function resolveShellExecutable(shell: string, options?: ShellExecutableResolutionOptions): string;
137
172
  /**
138
173
  * The interactive shell for this platform, resolved like a terminal
139
174
  * emulator: an explicitly configured shell (the `shell` config field) wins,
@@ -166,3 +201,21 @@ export declare function shellDisplayName(shell: string): string;
166
201
  * defaults entirely, giving deployments full control over shell startup.
167
202
  */
168
203
  export declare function shellSpawnArgs(configured?: string[]): string[];
204
+ /**
205
+ * Strip ONE pair of surrounding quotes from a configured shell path. Users
206
+ * paste Windows paths with spaces pre-quoted (`"C:\Program Files\…"`); the
207
+ * quotes are shell-input syntax, not part of the path. Unpaired quotes and
208
+ * shorter values stay verbatim.
209
+ */
210
+ export declare function unquotePath(value: string): string;
211
+ /**
212
+ * Split a settings-page shell-arguments string into argv with quote-aware
213
+ * grouping: `'…'` / `"…"` group whitespace, and characters inside quotes are
214
+ * LITERAL — a backslash is never an escape, so Windows paths survive intact
215
+ * (`-File "C:\my init\init.ps1"` → three tokens, the last containing spaces).
216
+ * The price is that an argument containing a literal quote character cannot
217
+ * be expressed; shell startup arguments never need one. An unclosed quote
218
+ * folds the remainder into the current token (settings input stays
219
+ * forgiving); an empty quote pair yields no argument.
220
+ */
221
+ export declare function splitShellArgs(input: string): string[];
@@ -6,12 +6,16 @@
6
6
  */
7
7
  import type { SidebarHttpRequest, SidebarHttpResponse } from './context-types.ts';
8
8
  /** Machine-readable error codes of the sidebar API. */
9
- export type SidebarErrorCode = 'bad-request' | 'not-found' | 'forbidden' | 'method-error' | 'too-large' | 'fs-error' | 'git-error' | 'pty-error' | 'pty-deps-missing' | 'job-error' | 'cdp-down' | 'sidechat-error' | 'subagents-unavailable' | 'settings-rejected' | 'settings-conflict' | 'internal';
9
+ export type SidebarErrorCode = 'bad-request' | 'not-found' | 'forbidden' | 'method-error' | 'too-large' | 'fs-error' | 'git-error' | 'pty-error' | 'pty-deps-missing' | 'shell-not-found' | 'job-error' | 'cdp-down' | 'sidechat-error' | 'subagents-unavailable' | 'settings-rejected' | 'settings-conflict' | 'internal';
10
10
  /** One API failure with its wire code and HTTP status. */
11
11
  export declare class SidebarError extends Error {
12
12
  readonly code: SidebarErrorCode;
13
13
  readonly status: number;
14
- constructor(code: SidebarErrorCode, message: string, status?: number);
14
+ /** Optional structured context (e.g. `{ shell }` for shell-not-found). */
15
+ readonly meta?: Record<string, string> | undefined;
16
+ constructor(code: SidebarErrorCode, message: string, status?: number,
17
+ /** Optional structured context (e.g. `{ shell }` for shell-not-found). */
18
+ meta?: Record<string, string> | undefined);
15
19
  }
16
20
  /** Success envelope of one API method. */
17
21
  export interface SidebarOk<T> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-coding-sidebar",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes the betterSidebar service for other plugins to register sidebar tabs and file viewers. KCoder-maintained fork of DSH-better-sidebar 0.17.2 (bottom panel removed, upstream-decoupled release line).",
5
5
  "type": "module",
6
6
  "repository": {