dsh-ssh-tui 0.7.2 → 0.7.3

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 (76) hide show
  1. package/README.en.md +16 -1
  2. package/README.md +14 -1
  3. package/cordis.patch.yml +15 -0
  4. package/docs/terminals.md +113 -0
  5. package/lib/diag.js +35 -0
  6. package/lib/diag.js.map +1 -1
  7. package/lib/display-sock.js +259 -42
  8. package/lib/display-sock.js.map +1 -1
  9. package/lib/doctor.js +71 -37
  10. package/lib/doctor.js.map +1 -1
  11. package/lib/dsh-compat.js +206 -88
  12. package/lib/dsh-compat.js.map +1 -1
  13. package/lib/footer.js +4 -2
  14. package/lib/footer.js.map +1 -1
  15. package/lib/i18n/en.js +18 -0
  16. package/lib/i18n/en.js.map +1 -1
  17. package/lib/i18n/index.js +13 -7
  18. package/lib/i18n/index.js.map +1 -1
  19. package/lib/i18n/zh.js +18 -0
  20. package/lib/i18n/zh.js.map +1 -1
  21. package/lib/index.js +36 -11
  22. package/lib/index.js.map +1 -1
  23. package/lib/paint.js +5 -2
  24. package/lib/paint.js.map +1 -1
  25. package/lib/picker.js +5 -3
  26. package/lib/picker.js.map +1 -1
  27. package/lib/platform.js +296 -11
  28. package/lib/platform.js.map +1 -1
  29. package/lib/preset-authoring.js +10 -14
  30. package/lib/preset-authoring.js.map +1 -1
  31. package/lib/preset-compat.js +100 -0
  32. package/lib/preset-compat.js.map +1 -0
  33. package/lib/preset-picker.js +5 -1
  34. package/lib/preset-picker.js.map +1 -1
  35. package/lib/preset-rows.js +83 -13
  36. package/lib/preset-rows.js.map +1 -1
  37. package/lib/provider-catalog.js +4 -4
  38. package/lib/route-memory.js +3 -3
  39. package/lib/route-memory.js.map +1 -1
  40. package/lib/session-index.js +5 -0
  41. package/lib/session-index.js.map +1 -1
  42. package/lib/session-lock.js +4 -1
  43. package/lib/session-lock.js.map +1 -1
  44. package/lib/session-route.js +3 -0
  45. package/lib/session-route.js.map +1 -1
  46. package/lib/settings-routes.js +10 -0
  47. package/lib/settings-routes.js.map +1 -0
  48. package/lib/settings-subagent.js +10 -0
  49. package/lib/settings-subagent.js.map +1 -0
  50. package/lib/subagent-model.js +5 -5
  51. package/lib/subagent-model.js.map +1 -1
  52. package/lib/supergrok-token.js +4 -0
  53. package/lib/supergrok-token.js.map +1 -1
  54. package/lib/terminal-caps.js +358 -0
  55. package/lib/terminal-caps.js.map +1 -0
  56. package/lib/tui.js +184 -120
  57. package/lib/tui.js.map +1 -1
  58. package/lib/types/diag.d.ts +18 -1
  59. package/lib/types/display-sock.d.ts +63 -22
  60. package/lib/types/doctor.d.ts +8 -1
  61. package/lib/types/dsh-compat.d.ts +141 -44
  62. package/lib/types/footer.d.ts +3 -1
  63. package/lib/types/i18n/index.d.ts +18 -12
  64. package/lib/types/index.d.ts +45 -0
  65. package/lib/types/picker.d.ts +3 -0
  66. package/lib/types/platform.d.ts +175 -10
  67. package/lib/types/preset-authoring.d.ts +8 -14
  68. package/lib/types/preset-compat.d.ts +58 -0
  69. package/lib/types/preset-picker.d.ts +1 -1
  70. package/lib/types/preset-rows.d.ts +51 -9
  71. package/lib/types/settings-routes.d.ts +16 -0
  72. package/lib/types/settings-subagent.d.ts +24 -0
  73. package/lib/types/subagent-model.d.ts +7 -7
  74. package/lib/types/terminal-caps.d.ts +105 -0
  75. package/lib/types/tui.d.ts +41 -7
  76. package/package.json +82 -55
@@ -21,6 +21,24 @@ export interface DiagSnapshot {
21
21
  hostVersion: string;
22
22
  nodeVersion: string;
23
23
  platform: string;
24
+ /**
25
+ * Which terminal we classified, and the capabilities we then claimed. A
26
+ * Windows report that says "the mouse does nothing" or "copy did nothing" is
27
+ * otherwise guesswork: conhost and Windows Terminal look identical from the
28
+ * transcript, and the claimed flags are what the TUI actually acted on.
29
+ */
30
+ terminal?: {
31
+ family: string;
32
+ label: string;
33
+ mouse: boolean;
34
+ bracketedPaste: boolean;
35
+ alternateScreen: boolean;
36
+ osc52: boolean;
37
+ osc8: boolean;
38
+ title: boolean;
39
+ /** `DSH_TUI_TERM_CAPS` tokens that were rejected (typos), for the row. */
40
+ ignoredOverrides: readonly string[];
41
+ };
24
42
  /**
25
43
  * What the palette resolved to and which hints decided it. A "no colour on
26
44
  * Windows" report is otherwise guesswork: `TERM` is unset there by default.
@@ -72,7 +90,6 @@ export declare function readErrTail(sessionId: string, dshHome?: string): Promis
72
90
  * The first entry is the actionable one; the rest is supporting context.
73
91
  */
74
92
  export declare function diagVerdicts(snapshot: DiagSnapshot): string[];
75
- /** The whole report as transcript lines. Pure. */
76
93
  export declare function formatDiag(snapshot: DiagSnapshot): string[];
77
94
  /** Gather everything the report needs. Every probe is best-effort. */
78
95
  export declare function collectDiag(options: {
@@ -71,6 +71,14 @@ export declare function sessionSockLookupPaths(sessionId: string, dshHome?: stri
71
71
  * device names (`CON`, `NUL`, …) from becoming the file stem.
72
72
  */
73
73
  export declare function sessionErrPath(sessionId: string, dshHome?: string, platform?: NodeJS.Platform): string;
74
+ /**
75
+ * Where the hidden-console bootstrap writes the Host's pid.
76
+ *
77
+ * On `\\.\pipe\` Windows there is no socket file to derive a name from, and the
78
+ * state directory already holds the per-session lock and stderr log, so the pid
79
+ * file lives beside them. It is removed as soon as it has been read.
80
+ */
81
+ export declare function sessionBootstrapPidPath(sessionId: string, dshHome?: string): string;
74
82
  /** Pre-digest Host stderr log next to the 0.7.1 socket, when that name differs. */
75
83
  export declare function legacySessionErrPath(sessionId: string, dshHome?: string, platform?: NodeJS.Platform): string | undefined;
76
84
  export declare function encodeFrame(type: number, payload?: Buffer): Buffer;
@@ -141,6 +149,8 @@ export interface HostExitWatch {
141
149
  /** Stop watching; call once the channel is confirmed up. */
142
150
  dispose(): void;
143
151
  }
152
+ /** Exported for the test that pins its loop-ref behaviour; not public API. */
153
+ export declare function watchHostPid(pid: number): HostExitWatch;
144
154
  /**
145
155
  * How long a dead-pid report waits for the child's `exit` event before giving
146
156
  * up on its code. The event is normally delivered within a tick; the bound only
@@ -191,33 +201,64 @@ export declare function quietTerminalInput(stdin?: NodeJS.ReadStream): number;
191
201
  * written into a link that may already be dead.
192
202
  */
193
203
  export declare function restoreTerminalInput(stdin?: NodeJS.ReadStream): void;
204
+ /** Test seam for {@link spawnDetachedHost}; production passes nothing. */
205
+ export interface SpawnHostOptions {
206
+ /**
207
+ * Start the Host through this command instead of resolving the real one, or
208
+ * `null` to spawn it directly even where a bootstrap exists.
209
+ *
210
+ * `null` is for the tests that are about the *direct* path — the fixture Hosts
211
+ * in `tests/display-host-e2e.test.mjs` assert on the child's exit code, which
212
+ * only a real child handle can report (a pid watch resolves `null`). The
213
+ * bootstrap has its own coverage: the builders on every platform, and the real
214
+ * Windows probes end to end.
215
+ */
216
+ bootstrap?: {
217
+ command: string;
218
+ args: string[];
219
+ } | null | undefined;
220
+ /** How long the bootstrap may take to report a pid (Windows PowerShell start). */
221
+ bootstrapTimeoutMs?: number;
222
+ }
194
223
  /**
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.
224
+ * Start the Host through the hidden-console bootstrap and return its pid, or
225
+ * `undefined` when the bootstrap could not report one.
197
226
  *
198
- * POSIX wants `detached: true` (setsid) so the Host survives the launcher and a
199
- * hung-up terminal.
227
+ * `spawnSync` on purpose. The pid has to be in hand before this function
228
+ * returns (the caller watches it, and the fallback must never leave two Hosts
229
+ * for one session), and the cost is one bounded wait while the boot splash is
230
+ * already on screen. PowerShell exits as soon as `Start-Process` has created the
231
+ * Host, so the wait is its own start-up, not the Host's.
200
232
  *
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.
233
+ * Falling back is safe exactly when nothing was printed: `Start-Process -PassThru`
234
+ * either starts the Host and prints its id, or throws before starting anything
235
+ * (`$ErrorActionPreference = 'Stop'`). A *timeout* is the one case where a Host
236
+ * might exist and the pid was lost, so it does not fall back — it reports.
237
+ */
238
+ export declare function spawnHostThroughBootstrap(bootstrap: {
239
+ command: string;
240
+ args: string[];
241
+ }, options: {
242
+ env: NodeJS.ProcessEnv;
243
+ platform: NodeJS.Platform;
244
+ timeoutMs: number;
245
+ /** File the bootstrap writes the Host's pid to. */
246
+ pidFile: string;
247
+ }): {
248
+ pid: number;
249
+ } | undefined;
250
+ /**
251
+ * Spawn a detached Host copy of this `dsh` invocation and return its sock path.
211
252
  *
212
- * Pure and platform-parameterised so the Windows branch can be asserted from
213
- * Linux (see docs/platform.md).
253
+ * On Windows the Host goes through {@link hostBootstrapCommand} when the OS
254
+ * PowerShell is available: a direct spawn there cannot both survive the
255
+ * launcher (libuv's `KILL_ON_JOB_CLOSE` job takes a non-detached child with it)
256
+ * and avoid flashing console windows (`detached` is DETACHED_PROCESS, which makes
257
+ * Windows ignore `CREATE_NO_WINDOW`). The bootstrap gives the Host a console of
258
+ * its own, hidden — see `docs/platform.md`. Without it, the direct spawn below
259
+ * is still what runs, with the old semantics.
214
260
  */
215
- export declare function hostSpawnOptions(platform?: NodeJS.Platform): {
216
- detached: boolean;
217
- windowsHide: boolean;
218
- };
219
- /** Spawn a detached Host copy of this `dsh` invocation and return its sock path. */
220
- export declare function spawnDetachedHost(sessionId: string, platform?: NodeJS.Platform): SpawnedHost;
261
+ export declare function spawnDetachedHost(sessionId: string, platform?: NodeJS.Platform, options?: SpawnHostOptions): SpawnedHost;
221
262
  export declare function probeDisplaySock(path: string, timeoutMs?: number): Promise<boolean>;
222
263
  export interface RelayResult {
223
264
  /** Host sent goodbye — user exited from the attached session. */
@@ -14,7 +14,7 @@
14
14
  * `collectDoctor` and every probe is best-effort. Nothing leaves the machine.
15
15
  * @module dsh-ssh-tui/doctor
16
16
  */
17
- import { type PatchAnalysis, type PatchRowRef, type RosterRow } from './preset-rows.js';
17
+ import { type HostGeneration, type PatchAnalysis, type PatchRowRef, type RosterRow } from './preset-rows.js';
18
18
  export type DoctorStatus = 'ok' | 'warn' | 'fail';
19
19
  export interface DoctorCheck {
20
20
  id: string;
@@ -64,6 +64,12 @@ export interface DoctorFacts {
64
64
  range?: string;
65
65
  releases: Record<string, string>;
66
66
  };
67
+ /**
68
+ * Which settings protocol the host speaks; decides which rows a terminal
69
+ * profile has to mount itself. Absent means the 0.1.5 line, so a snapshot
70
+ * built before this field existed keeps its verdicts.
71
+ */
72
+ generation?: HostGeneration;
67
73
  /** Distinct installs of `@deepseek-ai/dsh-scope` found from the anchors. */
68
74
  scopeCopies: readonly string[];
69
75
  /** Absent when routing could not be read (no settings service). */
@@ -101,6 +107,7 @@ export declare function collectDoctor(options: {
101
107
  services: DoctorFacts['services'];
102
108
  anchors: ReadonlyArray<string | undefined>;
103
109
  routing?: DoctorRouting;
110
+ generation?: HostGeneration;
104
111
  }): Promise<DoctorFacts>;
105
112
  /** Rows `/doctor --fix` would mount, given the services the composition registered. */
106
113
  export declare function rowsToRepair(facts: DoctorFacts): RosterRow[];
@@ -1,35 +1,136 @@
1
1
  /**
2
- * Dual-stack shims for dsh 0.1.2-rc.1 and 0.1.5-rc.1 (and the 0.1.5-alpha
3
- * handle API that landed with it).
2
+ * Dual-stack shims for the dsh 0.1.5-rc and 0.1.7-rc lines (including the
3
+ * 0.1.5-alpha handle API that landed with the former).
4
4
  *
5
- * 0.1.2 turned settings free functions into `SettingsProvider` methods and
6
- * replaced `Session.events` with on-demand readers. 0.1.5 replaced
7
- * `SessionPersistence.list`/`inspect`/`locate` with snapshot `list` plus
8
- * per-session `open` handles, and moved live tokens from durable
9
- * `assistant/chunk` events to process-local `agent/assistant-stream`.
10
- * Every shim here picks the API that is actually present so one build
11
- * runs on either host.
5
+ * 0.1.5 resolves settings through `SettingsProvider.get`/`installSection` and
6
+ * exposes session persistence as snapshot `list` plus per-session `open`
7
+ * handles; live tokens arrive on the process-local `agent/assistant-stream`.
8
+ * 0.1.7 replaced the settings service with schema-projected forms. Every shim
9
+ * here picks the API that is actually present so one build runs on either
10
+ * host line.
12
11
  */
13
12
  import type { Context } from '@deepseek-ai/cordis';
13
+ import type { ContextFormed } from '@deepseek-ai/dsh-llm';
14
14
  import type { SessionEvent } from '@deepseek-ai/dsh-session';
15
- import type { SettingsNamespace, SettingsSectionHooks } from '@deepseek-ai/dsh-settings';
15
+ import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
16
16
  import type z from '@deepseek-ai/schemastery';
17
+ declare module '@deepseek-ai/dsh-llm' {
18
+ /**
19
+ * The TUI's own notice/steering messages, which it commits to the durable log
20
+ * with `source.kind === 'plugin'`.
21
+ *
22
+ * 0.1.5 shipped this member; 0.1.7 removed the catch-all and documents the
23
+ * intended pattern instead — "each producer declares its own `kind` in its
24
+ * own module". This is that declaration, and it keeps the committed log shape
25
+ * identical on both lines.
26
+ */
27
+ interface MessageSourceMap {
28
+ plugin: {
29
+ kind: 'plugin';
30
+ plugin: string;
31
+ } & ContextFormed;
32
+ }
33
+ }
17
34
  /**
18
- * 0.1.1-rc.2 wraps namespaces via `settingsNamespace()`; 0.1.2+ brands them at
19
- * the type level and takes the plain string at runtime. A cast covers both.
35
+ * Settings namespaces are branded strings at the type level on both supported
36
+ * lines; this cast supplies the brand from a plain literal.
20
37
  */
21
38
  export declare function settingsNamespace(value: string): SettingsNamespace;
22
39
  /**
23
- * Register a settings section: 0.1.2-rc.1 moved the free function onto the
24
- * `settings` service as `installSection`, callable only once that service is
25
- * injected (plugins apply before it, so `ctx.inject` must defer — same
26
- * pattern the harness's own packages use); 0.1.1-rc.2 keeps the free
27
- * function, which defers internally and is safe at apply time.
40
+ * Hooks a settings consumer hands to {@link installSettingsSection}.
41
+ *
42
+ * Spelled out locally instead of imported: 0.1.7 deleted the
43
+ * `SettingsSectionHooks` export, and this shape is the whole contract the three
44
+ * call sites use.
45
+ */
46
+ export interface SettingsSectionHooks<T> {
47
+ /**
48
+ * Receive the active configuration source: the resolved settings value while
49
+ * a section is attached. Called at attach and again after every change.
50
+ * @param current - thunk returning the currently authoritative value.
51
+ */
52
+ setSource(current: () => T): void;
53
+ /**
54
+ * Re-judge anything derived from the source — registration-level facts,
55
+ * memoized resolutions — after an attach or a committed change.
56
+ */
57
+ onChange(): void;
58
+ /** Reject a resolved section this consumer could not act on. */
59
+ validate?(value: T): void;
60
+ }
61
+ /** Which settings protocol the running host speaks. */
62
+ export type SettingsGeneration = 'legacy' | 'forms';
63
+ /**
64
+ * The host's settings generation, as feature detection rather than a version.
65
+ *
66
+ * `legacy` (0.1.5) resolves a namespace through `settings.get`; `forms` (0.1.7)
67
+ * has no `get` and projects a form per loader entry. Callers that differ by
68
+ * generation — which profile rows a terminal profile must mount, for one — read
69
+ * it here instead of sniffing package versions.
70
+ *
71
+ * Before the service exists (a plugin applies before it is mounted) the same
72
+ * split shows up as the roster package's absence, and the answer is memoized as
73
+ * soon as the service is seen so a later call cannot disagree with an earlier
74
+ * one.
75
+ */
76
+ export declare function hostSettingsGeneration(ctx: Context): SettingsGeneration;
77
+ /** Forget the memoized descriptors so the next read re-walks the forms. */
78
+ export declare function invalidateSettingsCache(ctx: Context): void;
79
+ /**
80
+ * Read one settings section.
81
+ *
82
+ * 0.1.5 resolves it through `get(ns)`. 0.1.7 has no `get`, so the value comes
83
+ * from the entry's descriptor, which projects the live config — volatile fields
84
+ * only, i.e. exactly the fields its form exposes — and returns `undefined` when
85
+ * no entry carries that id.
86
+ */
87
+ export declare function readSettingsSection(ctx: Context, ns: SettingsNamespace): unknown;
88
+ /**
89
+ * The user's own settings document, keyed by namespace, on either line.
90
+ *
91
+ * 0.1.5 publishes it as `settings.document`. 0.1.7 dropped the property — the
92
+ * document is the profile patch now — but each descriptor still carries the
93
+ * user layer it was built from, so the same view is reconstructible. Callers
94
+ * that ask "did the user configure this?" must use this, not
95
+ * {@link readSettingsSection}: a resolved read also carries the composition
96
+ * base and schema defaults, which is exactly what such a caller must not
97
+ * mistake for a user choice.
98
+ */
99
+ export declare function settingsDocument(ctx: Context): Record<string, unknown> | undefined;
100
+ /**
101
+ * Mark one schema field as form-writable on the 0.1.7 line.
102
+ *
103
+ * 0.1.7 projects only fields whose schema node carries the `volatile` meta, and
104
+ * refuses a settings write to any other path. The 0.1.5 schemastery (3.18.2)
105
+ * has no such builder, so the call is feature-detected rather than typed: on
106
+ * that line the marker means nothing and the schema is returned untouched.
107
+ */
108
+ export declare function liveField<T>(schema: z<T>): z<T>;
109
+ /**
110
+ * Register a settings section.
111
+ *
112
+ * 0.1.5 publishes the `settings` service with `installSection`, callable only
113
+ * once that service is injected (plugins apply before it, so `ctx.inject` must
114
+ * defer — same pattern the harness's own packages use).
115
+ *
116
+ * 0.1.7 removed it. The section is now the loader entry's own `Config`
117
+ * schema — this plugin's is `ssh-tui`, and the two auxiliary namespaces are
118
+ * carried by the `dsh-ssh-tui/settings-*` rows in `cordis.patch.yml` — so the
119
+ * only thing left for a consumer to wire is the live read (`setSource`) and the
120
+ * change notification (`onChange`).
28
121
  */
29
122
  export declare function installSettingsSection<T>(ctx: Context, ns: SettingsNamespace, schema: z<T>, entry: T, hooks: SettingsSectionHooks<T>): void;
30
123
  /**
31
- * Read the full durable event log: 0.1.2-rc.1 replaced the `Session.events`
32
- * property with on-demand readers; 0.1.1-rc.2 still exposes the property.
124
+ * Whether a tool result reports failure.
125
+ *
126
+ * 0.1.5 carries `isError` on the `tool-result` content block; 0.1.7 removed
127
+ * that block from `ContentBlockMap` and moved the flag onto the message
128
+ * itself. Both are read, so one build understands either host.
129
+ */
130
+ export declare function toolResultFailed(message: unknown): boolean;
131
+ /**
132
+ * Read the full durable event log. Both supported lines read it on demand
133
+ * through `snapshotEvents()`.
33
134
  */
34
135
  export declare function sessionEvents(session: object): readonly SessionEvent[];
35
136
  /**
@@ -60,7 +161,6 @@ export interface SessionHeaderLike {
60
161
  /** Logical log plus the header it belongs to. */
61
162
  export interface SessionInspectionLike {
62
163
  events: readonly unknown[];
63
- meta?: SessionHeaderLike;
64
164
  header?: SessionHeaderLike;
65
165
  /**
66
166
  * Backend state for the slice. `detached` means the backend never
@@ -71,27 +171,28 @@ export interface SessionInspectionLike {
71
171
  eventState?: string;
72
172
  }
73
173
  /**
74
- * 0.1.2 `list()` returns headers; 0.1.5 returns `{ header, revision, … }`
75
- * snapshots. Normalize to headers so the picker does not care which host
174
+ * Both supported lines return `{ header, revision, … }` snapshots from
175
+ * `list()`; normalize to the header so the picker does not care which host
76
176
  * it is talking to.
77
177
  */
78
178
  export declare function listPersistenceHeaders(persistence: object): Promise<SessionHeaderLike[]>;
79
179
  /**
80
- * 0.1.2 `inspect(id)` returns `{ meta, events }`. 0.1.5 dropped inspect in
81
- * favour of `open(id, 'read')` + `handle.read()`. Close the handle so a
82
- * listing pass does not pin write ownership.
180
+ * Read one session through `open(id, 'read')` + `handle.read()`, the access
181
+ * both supported lines expose. Close the handle so a listing pass does not pin
182
+ * write ownership.
83
183
  */
84
184
  export declare function inspectPersistenceSession(persistence: object, id: unknown): Promise<SessionInspectionLike>;
85
- /** 0.1.2 owns `locate(header)`; 0.1.5 hid it on the JSONL backend. */
185
+ /**
186
+ * A session's artifact path from its header. Both supported lines still
187
+ * implement `locate()` on the JSONL backend, but their typings keep it
188
+ * private, so the call stays feature-detected.
189
+ */
86
190
  export declare function persistenceLocate(persistence: object, meta: object): {
87
191
  path?: string;
88
192
  } | undefined;
89
- /**
90
- * 0.1.2 command input advertised `images`; 0.1.5 renamed the flag to
91
- * `attachments`. Either true means the slash command accepts composer files.
92
- */
193
+ /** Whether a command's input admits the composer's attachments. */
93
194
  export declare function commandAcceptsAttachments(input: unknown): boolean;
94
- /** One model stream chunk, from either `assistant/chunk` or `agent/assistant-stream`. */
195
+ /** One chunk from a live `agent/assistant-stream` frame. */
95
196
  export interface StreamChunkLike {
96
197
  type: string;
97
198
  text?: string;
@@ -133,12 +234,12 @@ export declare function streamFrameAttemptId(frame: unknown): unknown;
133
234
  */
134
235
  export declare function streamFirstTokenTime(stream: unknown): number | undefined;
135
236
  /**
136
- * Durable `assistant/chunk` payload, or a live stream frame's inner chunk.
237
+ * A live `agent/assistant-stream` chunk frame's inner chunk plus its framing.
137
238
  *
138
- * `fallback` supplies the turn/step for live chunk frames, which do not carry
139
- * them (see {@link streamFrameOwner}).
239
+ * `fallback` supplies the turn/step, which chunk frames do not carry (see
240
+ * {@link streamFrameOwner}).
140
241
  */
141
- export declare function streamChunkOf(eventOrFrame: unknown, fallback?: {
242
+ export declare function streamChunkOf(frame: unknown, fallback?: {
142
243
  turn: number;
143
244
  step: number;
144
245
  }): {
@@ -147,20 +248,16 @@ export declare function streamChunkOf(eventOrFrame: unknown, fallback?: {
147
248
  step: number;
148
249
  time: number;
149
250
  /**
150
- * False when neither the source nor a fallback carried a real turn/step.
251
+ * False when neither the frame nor a fallback carried a real turn/step.
151
252
  * Usage folded under such a chunk would be filed under a bogus key (0:0)
152
253
  * that `step/end` never clears, inflating the session totals forever.
153
254
  */
154
255
  stepKnown: boolean;
155
256
  } | undefined;
156
- /** Durable event type as a plain string so 0.1.5 hosts can omit `assistant/chunk`. */
157
- export declare function sessionEventType(event: unknown): string;
158
- /** True when this event is a live-or-durable assistant token that replay should skip. */
159
- export declare function isAssistantStreamEvent(event: unknown): boolean;
160
- /**
161
- * Subscribe to a host event that may not exist on the compile-time Events
162
- * map. 0.1.5 emits `agent/assistant-stream`; 0.1.2 never does. Cordis
163
- * still accepts the string; the listener is simply never called on 0.1.2.
257
+ /**
258
+ * Subscribe to a host event whose scoped payload type does not match this
259
+ * build's `Events` map. Both supported lines emit `agent/assistant-stream`,
260
+ * and Cordis accepts the plain event name at runtime.
164
261
  */
165
262
  export declare function listenHostEvent(ctx: {
166
263
  on: (event: never, handler: never) => unknown;
@@ -105,7 +105,9 @@ export declare function fitFooterChips(chips: readonly FooterChip[], width: numb
105
105
  * the preset-owned tools are missing. It leads the strip and keeps its glyph
106
106
  * longest, because it is the one group that reports a broken install.
107
107
  */
108
- export declare function footerHealthChip(missing: boolean, color?: boolean): FooterChip | undefined;
108
+ export declare function footerHealthChip(missing: boolean, color?: boolean,
109
+ /** Which rows are missing: the 0.1.5 roster, or the 0.1.7 agent plane. */
110
+ kind?: 'roster' | 'agent-plane'): FooterChip | undefined;
109
111
  export declare function fitFooterStatsLine(chip: string, groups: readonly string[], width: number): string;
110
112
  export type FooterActivityKind = 'plan-review' | 'waiting' | 'compacting' | 'retry' | 'subagents' | 'tools' | 'plan-open' | 'plan-pending' | 'goal' | 'waiting-llm' | 'idle';
111
113
  export interface FooterStatusInput {
@@ -10,22 +10,28 @@ import type { Context } from '@deepseek-ai/cordis';
10
10
  export type Locale = 'zh' | 'en';
11
11
  export type MessageVars = Record<string, string | number>;
12
12
  export declare const UI_LOCALE_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
13
+ /**
14
+ * Fields `/language`, `/view`, `/disconnect` and `/autoapproval` persist.
15
+ *
16
+ * Every field is live: on 0.1.7 the section *is* this form, and only volatile
17
+ * paths may be written (see {@link liveField}).
18
+ */
13
19
  export declare const UI_LOCALE_SCHEMA: z<Schemastery.ObjectS<{
14
- language: z<string, string>;
15
- skipUpdate: z<string, string>;
16
- view: z<string, string>;
17
- disconnect: z<string, string>;
18
- autoApproval: z<string, string>;
20
+ language: z<string>;
21
+ skipUpdate: z<string>;
22
+ view: z<string>;
23
+ disconnect: z<string>;
24
+ autoApproval: z<string>;
19
25
  /** Milliseconds a leftover, finished Host waits before exiting; 0 = never. */
20
- idleExit: z<number, number>;
26
+ idleExit: z<number>;
21
27
  }>, Schemastery.ObjectT<{
22
- language: z<string, string>;
23
- skipUpdate: z<string, string>;
24
- view: z<string, string>;
25
- disconnect: z<string, string>;
26
- autoApproval: z<string, string>;
28
+ language: z<string>;
29
+ skipUpdate: z<string>;
30
+ view: z<string>;
31
+ disconnect: z<string>;
32
+ autoApproval: z<string>;
27
33
  /** Milliseconds a leftover, finished Host waits before exiting; 0 = never. */
28
- idleExit: z<number, number>;
34
+ idleExit: z<number>;
29
35
  }>>;
30
36
  export declare function localeFromTag(tag: string): Locale | undefined;
31
37
  /** Pick zh/en from env, optionally after a saved settings value. */
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { Context } from '@deepseek-ai/cordis';
8
8
  export { ATTACH_RECOVERY_WINDOW_MS, attachPeerVanished } from './attach.js';
9
+ import z from '@deepseek-ai/schemastery';
9
10
  export declare const name = "ssh-tui";
10
11
  /** Core services required before the terminal channel can drive an agent. */
11
12
  export declare const inject: string[];
@@ -27,7 +28,51 @@ export interface Config {
27
28
  model?: string;
28
29
  /** Minimum milliseconds between paints; see DSH_TUI_PAINT_MS. */
29
30
  paintIntervalMs?: number;
31
+ /**
32
+ * The fields below are the TUI's live settings, i.e. the `ssh-tui` section.
33
+ * They ride on this entry's schema so 0.1.7 can project a form for it — and so
34
+ * a pre-0.1.7 `$DSH_HOME/settings.yaml` `ssh-tui:` section is imported into
35
+ * this entry rather than left behind.
36
+ */
37
+ /** UI language (`/language`); zh unless the environment says otherwise. */
38
+ language?: string;
39
+ /** Newest plugin version whose update notice was dismissed. */
40
+ skipUpdate?: string;
41
+ /** Workspace pane layout (`/view`). */
42
+ view?: string;
43
+ /** What a dropped display does (`/disconnect`). */
44
+ disconnect?: string;
45
+ /** Auto-approval mode (`/autoapproval`). */
46
+ autoApproval?: string;
47
+ /** Milliseconds a leftover finished Host waits before exiting; 0 = never. */
48
+ idleExit?: number;
30
49
  }
50
+ /** Every field above, as schemastery resolves them (all optional). */
51
+ interface ConfigFields {
52
+ sessionId?: string;
53
+ showReasoning?: boolean;
54
+ maxToolOutputLines?: number;
55
+ color?: boolean;
56
+ welcome?: string;
57
+ resume?: boolean;
58
+ resumePicker?: boolean;
59
+ provider?: string;
60
+ model?: string;
61
+ paintIntervalMs?: number;
62
+ language?: string;
63
+ skipUpdate?: string;
64
+ view?: string;
65
+ disconnect?: string;
66
+ autoApproval?: string;
67
+ idleExit?: number;
68
+ }
69
+ /**
70
+ * The entry's schema. Everything a launch supplies (`config:` in
71
+ * `cordis.patch.yml`, including its `!!js` expressions) stays ordinary,
72
+ * non-live configuration; the TUI's own settings are the live fields, which is
73
+ * what makes them visible to, and writable through, the 0.1.7 settings service.
74
+ */
75
+ export declare const Config: z<ConfigFields>;
31
76
  /**
32
77
  * Mount the SSH TUI. The `main` agent is created here after the loader
33
78
  * settles, reading the saved default provider/model from
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import type { Context } from '@deepseek-ai/cordis';
13
13
  import { openResumableSessionPager, type ResumableSession } from './session-list.js';
14
+ import { type TerminalCapabilities } from './terminal-caps.js';
14
15
  /** What the launch picker decided. */
15
16
  export type SessionPickerResult = {
16
17
  kind: 'resume';
@@ -144,5 +145,7 @@ export interface SessionPickerOptions {
144
145
  stdin?: NodeJS.ReadStream;
145
146
  stdout?: NodeJS.WriteStream;
146
147
  openPager?: typeof openResumableSessionPager;
148
+ /** The terminal to act on; defaults to reading the environment (tests inject). */
149
+ terminalCaps?: TerminalCapabilities;
147
150
  }
148
151
  export declare function showSessionPicker(ctx: Context, color: boolean, signal?: AbortSignal, options?: SessionPickerOptions): Promise<SessionPickerResult>;