dsh-ssh-tui 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.en.md +76 -7
  2. package/README.md +50 -7
  3. package/lib/approval-cache.js +12 -9
  4. package/lib/approval-cache.js.map +1 -1
  5. package/lib/color-depth.js +10 -4
  6. package/lib/color-depth.js.map +1 -1
  7. package/lib/diag.js +41 -17
  8. package/lib/diag.js.map +1 -1
  9. package/lib/dialogs.js.map +1 -1
  10. package/lib/display-sock.js +68 -10
  11. package/lib/display-sock.js.map +1 -1
  12. package/lib/footer.js +35 -7
  13. package/lib/footer.js.map +1 -1
  14. package/lib/gateway-protocol.js +202 -0
  15. package/lib/gateway-protocol.js.map +1 -0
  16. package/lib/i18n/en.js +61 -41
  17. package/lib/i18n/en.js.map +1 -1
  18. package/lib/i18n/zh.js +61 -41
  19. package/lib/i18n/zh.js.map +1 -1
  20. package/lib/index.js +99 -23
  21. package/lib/index.js.map +1 -1
  22. package/lib/job-label.js +99 -17
  23. package/lib/job-label.js.map +1 -1
  24. package/lib/plan.js +269 -17
  25. package/lib/plan.js.map +1 -1
  26. package/lib/platform.js +88 -0
  27. package/lib/platform.js.map +1 -0
  28. package/lib/provider-catalog.js +3 -0
  29. package/lib/provider-catalog.js.map +1 -1
  30. package/lib/route-memory.js +8 -1
  31. package/lib/route-memory.js.map +1 -1
  32. package/lib/session-list.js +18 -1
  33. package/lib/session-list.js.map +1 -1
  34. package/lib/session-lock.js +75 -23
  35. package/lib/session-lock.js.map +1 -1
  36. package/lib/session-route.js +331 -0
  37. package/lib/session-route.js.map +1 -0
  38. package/lib/subagent-model.js +79 -1
  39. package/lib/subagent-model.js.map +1 -1
  40. package/lib/term-text.js +7 -4
  41. package/lib/term-text.js.map +1 -1
  42. package/lib/tool-present.js +46 -14
  43. package/lib/tool-present.js.map +1 -1
  44. package/lib/tui.js +1789 -275
  45. package/lib/tui.js.map +1 -1
  46. package/lib/types/color-depth.d.ts +3 -3
  47. package/lib/types/dialogs.d.ts +4 -0
  48. package/lib/types/display-sock.d.ts +52 -1
  49. package/lib/types/footer.d.ts +18 -4
  50. package/lib/types/gateway-protocol.d.ts +88 -0
  51. package/lib/types/job-label.d.ts +38 -7
  52. package/lib/types/plan.d.ts +94 -4
  53. package/lib/types/platform.d.ts +50 -0
  54. package/lib/types/route-memory.d.ts +17 -22
  55. package/lib/types/session-list.d.ts +5 -0
  56. package/lib/types/session-lock.d.ts +9 -0
  57. package/lib/types/session-route.d.ts +191 -0
  58. package/lib/types/subagent-model.d.ts +53 -0
  59. package/lib/types/term-text.d.ts +3 -2
  60. package/lib/types/tool-present.d.ts +2 -17
  61. package/lib/types/transcript-types.d.ts +44 -1
  62. package/lib/types/tui.d.ts +292 -2
  63. package/package.json +1 -1
@@ -18,10 +18,10 @@ export type ColorDepth = 'truecolor' | '256' | '8' | 'none';
18
18
  * Decide the palette from the environment.
19
19
  *
20
20
  * `DSH_TUI_COLOR_DEPTH` wins outright so a user can correct a wrong guess.
21
- * Otherwise: no colour at all when `NO_COLOR` is set or `TERM` names a
22
- * monochrome terminal; truecolor when `COLORTERM` says so; 256 for a
21
+ * Otherwise: no colour at all when `NO_COLOR` is set or `TERM` is `dumb`
22
+ * (or empty on POSIX); truecolor when `COLORTERM` says so; 256 for a
23
23
  * `*256color` terminal and for tmux/screen (their default palette is 256 and
24
- * they translate truecolor poorly); 8 for everything else.
24
+ * they translate truecolor poorly); 8 for Linux/VT consoles and everything else.
25
25
  * @param env - the environment to read (tests pass their own).
26
26
  * @returns the palette to paint with.
27
27
  */
@@ -51,6 +51,10 @@ export interface InspectDialog {
51
51
  title: string;
52
52
  lines: DiffDisplayLine[];
53
53
  offset: number;
54
+ /** Child session whose live log should refresh this overlay in place. */
55
+ subagentSessionId?: string;
56
+ /** `/find` already scrolled to its hit here; later repaints keep the offset. */
57
+ searchRevealed?: boolean;
54
58
  }
55
59
  export type Dialog = ConfirmDialog | QuestionDialog | OnboardingDialog | InspectDialog;
56
60
  export interface DialogAnswer {
@@ -33,12 +33,36 @@ export declare function resolveDshHome(env?: NodeJS.ProcessEnv, home?: string):
33
33
  export declare function sessionSockDir(dshHome?: string): string;
34
34
  /** Filesystem/pipe-safe form of a session id, as used for locks and channels. */
35
35
  export declare function safeSessionId(sessionId: string): string;
36
+ /**
37
+ * Readable, collision-free short label for one session: a sanitized head plus a
38
+ * digest of the raw id. The digest is always appended because sanitizing alone
39
+ * collapses distinct ids (`foo/bar` and `foo_bar`, `abc` and `abc.`) into one
40
+ * name. On Windows the pipe namespace is machine-wide; on POSIX the same
41
+ * collapse would share a socket file and a lock, so a relay could attach to
42
+ * the wrong session.
43
+ */
44
+ export declare function sessionLabel(sessionId: string, maxLength: number): string;
36
45
  /**
37
46
  * Address of the per-session display channel: a filesystem path on POSIX, a
38
47
  * named pipe on Windows. `platform` is injectable so the Windows shape stays
39
48
  * testable from a POSIX test run.
40
49
  */
41
50
  export declare function sessionSockPath(sessionId: string, dshHome?: string, platform?: NodeJS.Platform): string;
51
+ /**
52
+ * Pre-digest POSIX socket path (`tui-socks/<safeId>.sock`).
53
+ *
54
+ * 0.7.1 Hosts still listen here. A newer build's attach must find that channel
55
+ * instead of spawning a second Host that dies on the session write handle.
56
+ * Distinct ids that collapsed under sanitizing (`foo/bar` vs `foo_bar`)
57
+ * share this name — that is why the digest form exists — so a leftover
58
+ * file is only an attach target, never the path a new Host binds.
59
+ */
60
+ export declare function legacySessionSockPath(sessionId: string, dshHome?: string, platform?: NodeJS.Platform): string | undefined;
61
+ /**
62
+ * Channel a leftover Host may still be listening on: the digested path first,
63
+ * then the 0.7.1 name when it is different.
64
+ */
65
+ export declare function sessionSockLookupPaths(sessionId: string, dshHome?: string, platform?: NodeJS.Platform): string[];
42
66
  /**
43
67
  * Host stderr log for one session. POSIX keeps the historical `<sock>.err`
44
68
  * next to the socket; a Windows pipe name is not a file path, so the log lives
@@ -47,6 +71,8 @@ export declare function sessionSockPath(sessionId: string, dshHome?: string, pla
47
71
  * device names (`CON`, `NUL`, …) from becoming the file stem.
48
72
  */
49
73
  export declare function sessionErrPath(sessionId: string, dshHome?: string, platform?: NodeJS.Platform): string;
74
+ /** Pre-digest Host stderr log next to the 0.7.1 socket, when that name differs. */
75
+ export declare function legacySessionErrPath(sessionId: string, dshHome?: string, platform?: NodeJS.Platform): string | undefined;
50
76
  export declare function encodeFrame(type: number, payload?: Buffer): Buffer;
51
77
  export declare function encodeResize(columns: number, rows: number): Buffer;
52
78
  export declare function decodeResize(payload: Buffer): {
@@ -165,8 +191,33 @@ export declare function quietTerminalInput(stdin?: NodeJS.ReadStream): number;
165
191
  * written into a link that may already be dead.
166
192
  */
167
193
  export declare function restoreTerminalInput(stdin?: NodeJS.ReadStream): void;
194
+ /**
195
+ * How to start the background Host so it outlives this process *and* does not
196
+ * make its own children flash console windows on Windows.
197
+ *
198
+ * POSIX wants `detached: true` (setsid) so the Host survives the launcher and a
199
+ * hung-up terminal.
200
+ *
201
+ * Windows is the opposite: `detached: true` maps to DETACHED_PROCESS, which
202
+ * gives the Host **no console at all**, and Windows ignores CREATE_NO_WINDOW
203
+ * (what `windowsHide` sets) when DETACHED_PROCESS is present. Every console
204
+ * child the Host then starts — each tool call, every shell, node, git — has to
205
+ * allocate its own console, which is a visible window flashing over the TUI.
206
+ * Dropping `detached` there lets `windowsHide` do its job: the Host gets its
207
+ * own invisible console, and descendants inherit it instead of creating one.
208
+ * The Host still outlives the launcher: Windows does not kill children with
209
+ * their parent, and its console is its own, so closing the user's terminal does
210
+ * not reach it either.
211
+ *
212
+ * Pure and platform-parameterised so the Windows branch can be asserted from
213
+ * Linux (see docs/platform.md).
214
+ */
215
+ export declare function hostSpawnOptions(platform?: NodeJS.Platform): {
216
+ detached: boolean;
217
+ windowsHide: boolean;
218
+ };
168
219
  /** Spawn a detached Host copy of this `dsh` invocation and return its sock path. */
169
- export declare function spawnDetachedHost(sessionId: string): SpawnedHost;
220
+ export declare function spawnDetachedHost(sessionId: string, platform?: NodeJS.Platform): SpawnedHost;
170
221
  export declare function probeDisplaySock(path: string, timeoutMs?: number): Promise<boolean>;
171
222
  export interface RelayResult {
172
223
  /** Host sent goodbye — user exited from the attached session. */
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Footer stats, status identity, context-pressure chips, and `/status` report.
3
3
  */
4
+ import { type ColorDepth } from './color-depth.js';
4
5
  import { type QuotaPeriod, type QuotaSnapshot, type QuotaSource } from './quota.js';
5
6
  import type { DisconnectPolicyName } from './transcript-types.js';
6
7
  export declare const WAIT_INDICATOR_MS = 8000;
@@ -161,12 +162,25 @@ export declare function formatQuotaBar(remainingPercent: number, width?: number)
161
162
  * `sub:<model>` chip for the identity row: `sub:grok-4.5`, or
162
163
  * `sub:grok-4.5(xhigh)` when an explicit `/subeffort` is set.
163
164
  *
164
- * It shows the model name only, like the parent's own chip: a `provider/model`
165
- * prefix on both chips doubled the same route and pushed the quota badge towards
166
- * the drop edge. The provider is not lost — `/submodel` and `/status` report it,
167
- * and the child's model name is the part that differs from the parent's.
165
+ * It shows the model name only, like the parent's own chip, unless `/submodel`
166
+ * pinned a different provider — then `sub:xai/grok-4.5` so the foreign route
167
+ * is visible on the identity row as well as the recolored chip.
168
168
  */
169
169
  export declare function subagentRouteLabel(model: string, provider?: string, effort?: string): string;
170
+ /**
171
+ * Paint the `sub:` chip on an otherwise muted status line.
172
+ *
173
+ * Only a *foreign* route is accented: cyan when `/submodel` pinned a supplier
174
+ * the parent is not on. A child that follows the parent (or that was pinned
175
+ * back onto the parent's own provider) keeps the identity row's mute — the
176
+ * accent means "this route is not your parent's", and spending it on every
177
+ * child made the following case look pinned.
178
+ *
179
+ * The mute (`90`) is reopened after the chip so the rest of the identity row
180
+ * stays dim.
181
+ */
182
+ export declare function paintFooterSubagentChip(line: string, chip: string, foreign: boolean, muteSgr?: string, depth?: ColorDepth): string;
183
+ export declare function footerSubagentForeign(input: Pick<FooterStatusInput, 'provider' | 'subProvider'>): boolean;
170
184
  /**
171
185
  * The model as the footer shows it: the model's own name, without a
172
186
  * `provider/model` route prefix.
@@ -0,0 +1,88 @@
1
+ /**
2
+ * One gateway, several wire protocols.
3
+ *
4
+ * `llm-pi-ai` puts `api` on the *provider* entry, not on a model entry, so a
5
+ * gateway whose catalogue spans protocols (Command Code: 55 of 71 models answer
6
+ * `/responses`, 8 only `/chat/completions`, the Claude family only `/messages`)
7
+ * cannot be one entry. The wizard therefore keeps one row per gateway and
8
+ * expands it into sibling entries — `command-code`, `command-code-responses`,
9
+ * `command-code-messages` — with each model filed under the protocol it
10
+ * actually speaks. Everything here is pure so the split can be tested without
11
+ * a settings file or a network.
12
+ */
13
+ export type GatewayProtocol = 'openai-responses' | 'openai-completions' | 'anthropic-messages';
14
+ /** Preference order for a model a gateway lists on more than one route. */
15
+ export declare const GATEWAY_PROTOCOL_PREFERENCE: readonly GatewayProtocol[];
16
+ /** Id suffix per protocol; the base entry keeps the plain gateway id. */
17
+ export declare const GATEWAY_PROTOCOL_SUFFIX: Record<GatewayProtocol, string>;
18
+ /** The endpoint path each protocol speaks, for messages and hints. */
19
+ export declare const GATEWAY_PROTOCOL_ENDPOINT: Record<GatewayProtocol, string>;
20
+ /** The endpoint strings gateways publish; `null` for anything unrecognised. */
21
+ export declare function endpointProtocol(endpoint: string): GatewayProtocol | null;
22
+ /** `command-code` + `openai-responses` → `command-code-responses`. */
23
+ export declare function siblingProviderId(baseId: string, protocol: GatewayProtocol): string;
24
+ /** The protocol a sibling id carries, or null when it is not one of ours. */
25
+ export declare function siblingProtocolOf(id: string): GatewayProtocol | null;
26
+ /** True when `id` is a sibling of `baseId` (and not the base itself). */
27
+ export declare function isSiblingOf(baseId: string, id: string): boolean;
28
+ /** The gateway id a sibling row belongs to; a base id comes back unchanged. */
29
+ export declare function baseProviderIdOf(id: string): string;
30
+ /** The protocol a provider entry's `api` names, when it is one of the three we model. */
31
+ export declare function declaredProtocol(value: unknown): GatewayProtocol | undefined;
32
+ /**
33
+ * Whether an entry that speaks `protocol` may serve `model`, according to the
34
+ * gateway's own `supported_endpoints`. A gateway that publishes nothing about
35
+ * the model — or no protocol to check against — never blocks it: silence is not
36
+ * a refusal, only an explicit list that omits the protocol is.
37
+ */
38
+ export declare function gatewayServesModel(endpoints: ReadonlyMap<string, readonly string[]> | undefined, model: string, protocol: GatewayProtocol | undefined): boolean;
39
+ /**
40
+ * The protocols a gateway lists for `model`, in preference order. Empty when the
41
+ * gateway says nothing, which callers read as "no opinion".
42
+ */
43
+ export declare function advertisedProtocols(endpoints: ReadonlyMap<string, readonly string[]> | undefined, model: string): GatewayProtocol[];
44
+ /**
45
+ * The protocol one model should be filed under. The gateway's own
46
+ * `supported_endpoints` decide when it publishes them; a model already
47
+ * configured on a route the gateway still lists keeps that route, and a model
48
+ * the gateway does not describe keeps the route it is already on rather than
49
+ * being relocated by a hand-maintained table. Otherwise the earliest entry in
50
+ * `preference` wins, then the table, then the gateway's own primary protocol.
51
+ */
52
+ export declare function resolveModelProtocol(input: {
53
+ advertised?: readonly string[];
54
+ /** The provider protocol this model is already configured on, if any. */
55
+ existing?: GatewayProtocol;
56
+ table?: GatewayProtocol;
57
+ fallback: GatewayProtocol;
58
+ preference?: readonly GatewayProtocol[];
59
+ }): GatewayProtocol;
60
+ /**
61
+ * Every model of one gateway, filed by protocol. `existing` carries the
62
+ * protocol each model is already configured on, so re-running setup keeps a
63
+ * working placement instead of re-homing it. Models the gateway does not
64
+ * describe, that are not configured yet, and that the table does not know land
65
+ * on `fallback`, which is the gateway's own pinned protocol — the one its
66
+ * default models were verified on.
67
+ */
68
+ export declare function splitModelsByProtocol(input: {
69
+ models: readonly string[];
70
+ advertised?: ReadonlyMap<string, readonly string[]>;
71
+ existing?: ReadonlyMap<string, GatewayProtocol>;
72
+ table?: Readonly<Record<string, GatewayProtocol>>;
73
+ fallback: GatewayProtocol;
74
+ preference?: readonly GatewayProtocol[];
75
+ }): Map<GatewayProtocol, string[]>;
76
+ /**
77
+ * OpenCode Go's published endpoint table (https://opencode.ai/docs/go/), because
78
+ * `GET /zen/go/v1/models` carries no `supported_endpoints` field at all.
79
+ *
80
+ * The whole DeepSeek family answers `/responses` (verified against the live
81
+ * gateway, and what this plugin's pinned template has shipped since 0.7.0) even
82
+ * though the page still lists those models under chat. A route that works is not
83
+ * downgraded because a document lags. Zen's own (non-Go) route publishes no
84
+ * table here — unknown models there take the gateway's primary protocol.
85
+ */
86
+ export declare const ZEN_GO_MODEL_PROTOCOLS: Readonly<Record<string, GatewayProtocol>>;
87
+ /** The built-in table for a gateway base URL, when we have one. */
88
+ export declare function gatewayModelTable(baseURL: string): Readonly<Record<string, GatewayProtocol>> | undefined;
@@ -1,20 +1,51 @@
1
1
  /**
2
- * Friendly display names for background jobs.
2
+ * Friendly display names for background jobs and subagent chips.
3
3
  *
4
- * `job_*` cards otherwise read as the raw tool vocabulary (`job_output`,
5
- * `job_id: bash-1`), which is both untranslated and hard to talk about out
6
- * loud. Each job id instead gets a two-word alias — "蔚蓝水獭", "azure otter" —
7
- * derived from the id itself rather than from a random draw, so the same job
8
- * keeps one name across its call card, its result card, and every later
4
+ * Background `job_*` cards otherwise read as the raw tool vocabulary
5
+ * (`job_output`, `job_id: bash-1`). Each job id gets a two-token alias
6
+ * (hour + weather: 昏风 / twilight wind) derived from the id itself rather
7
+ * than from a random draw, so the same
8
+ * job keeps one name across its call card, its result card, and every later
9
9
  * `job_output` / `job_kill` mention. The model-facing id stays authoritative;
10
10
  * the alias is presentation only.
11
11
  *
12
+ * Subagent chips use a different scheme: a role distilled from the parent
13
+ * spawn description, joined to one of the four directional beasts
14
+ * (青龙 / 白虎 / 朱雀 / 玄武) hashed from the child session id.
15
+ *
12
16
  * @module dsh-ssh-tui/job-label
13
17
  */
18
+ /** FNV-1a over the job id: stable, dependency-free, and well spread for short ids. */
19
+ export declare function hashJobId(id: string): number;
14
20
  /**
15
21
  * One stable alias for a background job, or `undefined` when the id is empty
16
22
  * or the locale carries no vocabulary.
17
23
  * @param jobId - the native job id (`bash-1`, `pwsh-2`, …).
18
- * @returns the locale-formatted alias, e.g. `蔚蓝水獭` / `azure otter`.
24
+ * @returns the locale-formatted alias, e.g. `昏风` / `twilight wind`.
19
25
  */
20
26
  export declare function jobAlias(jobId: string): string | undefined;
27
+ /** The four directional beasts, hashed from the child session id. */
28
+ export declare const SUBAGENT_BEASTS: readonly ["azure-dragon", "white-tiger", "vermilion-bird", "black-tortoise"];
29
+ export type SubagentBeastId = (typeof SUBAGENT_BEASTS)[number];
30
+ export type SubagentRoleId = 'scout' | 'scribe' | 'artisan' | 'envoy' | 'inquirer' | 'sentinel' | 'steward' | 'courier';
31
+ /** Distill a parent spawn description into one role id. */
32
+ export declare function subagentRoleId(task: string): SubagentRoleId;
33
+ /** Hash the child session onto one of the four beasts. */
34
+ export declare function subagentBeastId(sessionId: string): SubagentBeastId;
35
+ /** Localized role label (`探路` / `scout`). */
36
+ export declare function subagentRoleLabel(role: SubagentRoleId): string;
37
+ /** Localized beast label (`青龙` / `Azure Dragon`). */
38
+ export declare function subagentBeastLabel(beast: SubagentBeastId): string;
39
+ /**
40
+ * Chip title: `探路·青龙` / `Scout · Azure Dragon`.
41
+ * Falls back to the spawn-time label when neither task nor id can be named.
42
+ */
43
+ export declare function subagentCourtesyName(input: {
44
+ sessionId: string;
45
+ task?: string;
46
+ fallback: string;
47
+ }): string;
48
+ /** True when a string looks like a session / tool-call id, not a display name. */
49
+ export declare function looksLikeOpaqueId(value: string): boolean;
50
+ /** Prefer a recorded tool name; never surface `call-<uuid>` as a title. */
51
+ export declare function displayToolName(name: string | undefined): string;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Plan dock, todo lists, /find, prompt-injection cards, and compact errors.
3
3
  */
4
- import type { DisplayKind, PlanTodoItem, Row, SubagentLogEntry } from './transcript-types.js';
4
+ import { type TextSegment } from './term-text.js';
5
+ import type { DiffDisplayLine, DisplayKind, PlanTodoItem, Row, SubagentLogEntry } from './transcript-types.js';
5
6
  export declare const MAX_SUBAGENT_LOGS = 80;
6
7
  export declare const TODO_STATUS_MARK: Record<PlanTodoItem['status'], string>;
7
8
  /** True while a plan still belongs in the dock (latest incomplete work). */
@@ -78,10 +79,99 @@ export declare function parsePlanTodos(value: unknown): PlanTodoItem[];
78
79
  export declare function todoSummary(value: unknown): string;
79
80
  /** Compact ask_user_question summary from tool arguments. */
80
81
  export declare function askSummary(value: unknown): string;
81
- /** One-line subagent card header used while collapsed. */
82
- export declare function subagentHeaderText(row: Extract<Row, {
82
+ /** First non-empty line, collapsed to a single scan line. */
83
+ export declare function firstDisplayLine(text: string): string;
84
+ /** Collapse a child-session blob to one short chip/wait-card line. */
85
+ export declare function clipSubagentActivity(text: string, maxChars?: number): string;
86
+ /** Running / ok / aborted / error → ANSI for the status dot and status word. */
87
+ export declare function subagentStateColor(status: Extract<Row, {
83
88
  kind: 'subagent';
84
- }>, now?: number): string;
89
+ }>['status']): '33' | '32' | '31' | '90';
90
+ /**
91
+ * Rebuild a subagent chip from the parent spawn tool call that survives in the
92
+ * session log. Live `subagent/start` is not replayed, so resume would otherwise
93
+ * show a generic tool card titled from the English description ("probe").
94
+ */
95
+ export declare function subagentRowFromSpawnTool(input: {
96
+ callId: string;
97
+ task: string;
98
+ provider?: string;
99
+ /** Model route for the child, when the caller knows it. */
100
+ modelProvider?: string;
101
+ local?: boolean;
102
+ status?: Extract<Row, {
103
+ kind: 'subagent';
104
+ }>['status'];
105
+ startedAt?: number;
106
+ endedAt?: number;
107
+ output?: string;
108
+ }): Extract<Row, {
109
+ kind: 'subagent';
110
+ }>;
111
+ /**
112
+ * Courtesy title: distilled role plus a directional beast.
113
+ *
114
+ * The beast comes from the child's own session id, never the parent call id a
115
+ * replayed chip was built with: the same child must keep its symbol across a
116
+ * `--resume`.
117
+ */
118
+ export declare function subagentDisplayName(row: Extract<Row, {
119
+ kind: 'subagent';
120
+ }>): string;
121
+ /**
122
+ * Short activity for the collapsed chip and wait card: prefer the parent
123
+ * task name, else a clipped last log line. Never the full child transcript.
124
+ */
125
+ export declare function subagentChipSummary(row: Extract<Row, {
126
+ kind: 'subagent';
127
+ }>): string;
128
+ /** Header + SGR spans: identity color, status dot/word, muted summary. */
129
+ export declare function buildSubagentHeader(input: {
130
+ focused: boolean;
131
+ title: string;
132
+ status: Extract<Row, {
133
+ kind: 'subagent';
134
+ }>['status'];
135
+ elapsedLabel: string;
136
+ summary: string;
137
+ spinner?: string;
138
+ inspectHint?: string;
139
+ /** Different provider from the parent: paint the title cyan, not violet. */
140
+ foreign?: boolean;
141
+ }): {
142
+ plain: string;
143
+ segments: TextSegment[];
144
+ };
145
+ /** Map a folded child-session event onto an existing display role. */
146
+ export declare function subagentLogDisplayKind(entry: SubagentLogEntry, status: Extract<Row, {
147
+ kind: 'subagent';
148
+ }>['status']): DisplayKind;
149
+ /**
150
+ * Overlay body: session line, stop reason, then the clipped child log.
151
+ *
152
+ * The child's session id is the one `/subagents kill` takes, so it belongs on
153
+ * this line — the collapsed chip stays free of opaque ids.
154
+ */
155
+ export declare function subagentInspectLines(row: Extract<Row, {
156
+ kind: 'subagent';
157
+ }>): DiffDisplayLine[];
158
+ /**
159
+ * Classify a child-session error so the chip can say *why* it died, not just
160
+ * that it ended. Quota and expired auth get a command that actually helps;
161
+ * everything else keeps a clipped diagnostic.
162
+ */
163
+ export declare function describeSubagentFailure(input: {
164
+ stopReason?: string;
165
+ message?: string;
166
+ provider?: string;
167
+ }): {
168
+ hint: string;
169
+ kind: 'quota' | 'auth' | 'effort' | 'error';
170
+ } | undefined;
85
171
  export declare function appendSubagentLog(row: Extract<Row, {
86
172
  kind: 'subagent';
87
173
  }>, entry: SubagentLogEntry): void;
174
+ /** Fold a child user/plugin blob: reminders become one inject chip, not raw XML. */
175
+ export declare function foldSubagentUserLog(row: Extract<Row, {
176
+ kind: 'subagent';
177
+ }>, text: string, sourceKind?: string, plugin?: string): void;
@@ -0,0 +1,50 @@
1
+ /** This process is running on Windows. */
2
+ export declare const IS_WINDOWS: boolean;
3
+ /** Windows delivers resizes on the stream; everyone else raises SIGWINCH. */
4
+ export declare function usesSigwinch(platform?: NodeJS.Platform): boolean;
5
+ /** The launcher's environment file: a shell fragment or a `cmd` script. */
6
+ export declare function envFileName(platform?: NodeJS.Platform): string;
7
+ /** The shell a Windows user has: PowerShell, where POSIX code would say bash. */
8
+ export declare function shellName(platform?: NodeJS.Platform): string;
9
+ /**
10
+ * Whether lock liveness is decided by asking the OS about the process (Windows:
11
+ * `Get-Process` + creation time) rather than by reading `/proc` (Linux) or the
12
+ * command line (darwin).
13
+ *
14
+ * `lockOwnerIsAlive` returns early into `windowsProcessMatchesLock` when this is
15
+ * true; the POSIX body below it has no meaning there (`/proc` does not exist).
16
+ */
17
+ export declare function usesProcessIdentity(platform?: NodeJS.Platform): boolean;
18
+ /**
19
+ * How to start the background Host so it outlives this process *and* does not
20
+ * make its own children flash console windows on Windows.
21
+ *
22
+ * POSIX wants `detached: true` (setsid) so the Host survives the launcher and a
23
+ * hung-up terminal.
24
+ *
25
+ * Windows is the opposite: `detached: true` maps to DETACHED_PROCESS, which
26
+ * gives the Host **no console at all**, and Windows ignores CREATE_NO_WINDOW
27
+ * (what `windowsHide` sets) when DETACHED_PROCESS is present. Every console
28
+ * child the Host then starts — each tool call, every shell, node, git — has to
29
+ * allocate its own console, which is a visible window flashing over the TUI.
30
+ * Dropping `detached` there lets `windowsHide` do its job: the Host gets its
31
+ * own invisible console, and descendants inherit it instead of creating one.
32
+ * The Host still outlives the launcher: Windows does not kill children with
33
+ * their parent, and its console is its own, so closing the user's terminal does
34
+ * not reach it either.
35
+ */
36
+ export declare function hostSpawnOptions(platform?: NodeJS.Platform): {
37
+ detached: boolean;
38
+ windowsHide: boolean;
39
+ };
40
+ /**
41
+ * A path a human can read, with the platform's own shorthand.
42
+ *
43
+ * `~/.dsh/env.sh` is the POSIX form; Windows users know `%USERPROFILE%`, and a
44
+ * `C:\Users\...` prefix spelled out is noise in a one-line hint.
45
+ */
46
+ export declare function displayHomePath(home: string, file: string, options?: {
47
+ platform?: NodeJS.Platform;
48
+ env?: NodeJS.ProcessEnv;
49
+ userHome?: string;
50
+ }): string;
@@ -5,28 +5,23 @@
5
5
  import z from '@deepseek-ai/schemastery';
6
6
  import type { Context } from '@deepseek-ai/cordis';
7
7
  export declare const ROUTE_MEMORY_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
8
- /** Settings schema for `$DSH_HOME/settings.yaml` under ssh-tui-routes. */
9
- export declare const ROUTE_MEMORY_SCHEMA: z<Schemastery.ObjectS<{
10
- providers: z<import("@deepseek-ai/cosmokit").Dict<{
11
- model?: string | null | undefined;
12
- reasoningEffort?: string | null | undefined;
13
- updatedAt?: number | null | undefined;
14
- } & import("@deepseek-ai/cosmokit").Dict, string>, import("@deepseek-ai/cosmokit").Dict<Schemastery.ObjectT<{
15
- model: z<string, string>;
16
- reasoningEffort: z<string, string>;
17
- updatedAt: z<number, number>;
18
- }>, string>>;
19
- }>, Schemastery.ObjectT<{
20
- providers: z<import("@deepseek-ai/cosmokit").Dict<{
21
- model?: string | null | undefined;
22
- reasoningEffort?: string | null | undefined;
23
- updatedAt?: number | null | undefined;
24
- } & import("@deepseek-ai/cosmokit").Dict, string>, import("@deepseek-ai/cosmokit").Dict<Schemastery.ObjectT<{
25
- model: z<string, string>;
26
- reasoningEffort: z<string, string>;
27
- updatedAt: z<number, number>;
28
- }>, string>>;
29
- }>>;
8
+ /** The shape `ssh-tui-routes` takes in `$DSH_HOME/settings.yaml`. */
9
+ export interface RouteMemorySettings {
10
+ providers: Record<string, {
11
+ model: string;
12
+ reasoningEffort: string;
13
+ updatedAt: number;
14
+ }>;
15
+ }
16
+ /**
17
+ * Settings schema for `$DSH_HOME/settings.yaml` under ssh-tui-routes.
18
+ *
19
+ * The type argument is spelled out rather than inferred: `schemastery`'s
20
+ * builder names its own package internally, so an inferred schema makes `tsc`
21
+ * emit a declaration that references a path inside `node_modules` and fails
22
+ * with TS2742 on any layout that does not match this machine's.
23
+ */
24
+ export declare const ROUTE_MEMORY_SCHEMA: z<RouteMemorySettings>;
30
25
  export interface RememberedRoute {
31
26
  model: string;
32
27
  reasoningEffort?: string;
@@ -10,10 +10,15 @@ export declare function formatFooterCwd(cwd: string): string;
10
10
  /**
11
11
  * Switch the process into a persisted session working directory. Returns the
12
12
  * directory actually used; missing/invalid paths stay put and are reported.
13
+ *
14
+ * The target must be an absolute directory. A regular file that happens to
15
+ * exist at the recorded path used to pass `existsSync` and then fail inside
16
+ * `chdir` with a platform errno; resume now refuses it before moving.
13
17
  */
14
18
  export declare function enterSessionCwd(cwd: string | undefined, options?: {
15
19
  current?: string;
16
20
  exists?: (path: string) => boolean;
21
+ isDirectory?: (path: string) => boolean;
17
22
  chdir?: (path: string) => void;
18
23
  }): {
19
24
  cwd: string;
@@ -21,6 +21,15 @@ export declare class SessionLockHeldError extends Error {
21
21
  constructor(lock: SessionLockInfo, path: string);
22
22
  }
23
23
  export declare function sessionLockPath(sessionId: string, dshHome?: string): string;
24
+ /**
25
+ * Pre-digest lock path (`tui-locks/<safeId>.json`).
26
+ *
27
+ * 0.7.1 Hosts still write here. Lookup must find that file; a new Host must
28
+ * not bind a second lock beside a live one just because the filename changed.
29
+ */
30
+ export declare function legacySessionLockPath(sessionId: string, dshHome?: string): string;
31
+ /** Digested lock first, then the 0.7.1 name when it is different. */
32
+ export declare function sessionLockLookupPaths(sessionId: string, dshHome?: string): string[];
24
33
  export declare function parseSessionLock(raw: string): SessionLockInfo | undefined;
25
34
  /** True when `pid` still exists on this machine (best-effort). */
26
35
  export declare function processIsAlive(pid: number): boolean;