dsh-ssh-tui 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +113 -27
- package/README.md +90 -27
- package/cordis.patch.yml +15 -0
- package/docs/remote-ops.md +27 -1
- package/docs/terminals.md +126 -0
- package/docs/windows.md +128 -0
- package/lib/attach.js +14 -1
- package/lib/attach.js.map +1 -1
- package/lib/commands.js +1 -0
- package/lib/commands.js.map +1 -1
- package/lib/copy-text.js +2 -0
- package/lib/copy-text.js.map +1 -1
- package/lib/diag.js +38 -0
- package/lib/diag.js.map +1 -1
- package/lib/dialogs.js.map +1 -1
- package/lib/display-sock.js +572 -44
- package/lib/display-sock.js.map +1 -1
- package/lib/doctor.js +71 -37
- package/lib/doctor.js.map +1 -1
- package/lib/dsh-compat.js +252 -88
- package/lib/dsh-compat.js.map +1 -1
- package/lib/footer.js +4 -2
- package/lib/footer.js.map +1 -1
- package/lib/i18n/en.js +54 -10
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/index.js +18 -7
- package/lib/i18n/index.js.map +1 -1
- package/lib/i18n/zh.js +54 -10
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +111 -26
- package/lib/index.js.map +1 -1
- package/lib/line-mode.js +4 -0
- package/lib/line-mode.js.map +1 -1
- package/lib/paint.js +37 -3
- package/lib/paint.js.map +1 -1
- package/lib/picker.js +109 -18
- package/lib/picker.js.map +1 -1
- package/lib/plan.js +8 -0
- package/lib/plan.js.map +1 -1
- package/lib/platform.js +377 -11
- package/lib/platform.js.map +1 -1
- package/lib/preset-authoring.js +10 -14
- package/lib/preset-authoring.js.map +1 -1
- package/lib/preset-compat.js +100 -0
- package/lib/preset-compat.js.map +1 -0
- package/lib/preset-picker.js +5 -1
- package/lib/preset-picker.js.map +1 -1
- package/lib/preset-rows.js +119 -19
- package/lib/preset-rows.js.map +1 -1
- package/lib/provider-catalog.js +4 -4
- package/lib/question-wait.js +418 -0
- package/lib/question-wait.js.map +1 -0
- package/lib/route-memory.js +3 -3
- package/lib/route-memory.js.map +1 -1
- package/lib/selection.js +26 -8
- package/lib/selection.js.map +1 -1
- package/lib/session-index.js +5 -0
- package/lib/session-index.js.map +1 -1
- package/lib/session-lock.js +4 -1
- package/lib/session-lock.js.map +1 -1
- package/lib/session-route.js +3 -0
- package/lib/session-route.js.map +1 -1
- package/lib/settings-routes.js +10 -0
- package/lib/settings-routes.js.map +1 -0
- package/lib/settings-subagent.js +10 -0
- package/lib/settings-subagent.js.map +1 -0
- package/lib/subagent-model.js +5 -5
- package/lib/subagent-model.js.map +1 -1
- package/lib/supergrok-token.js +4 -0
- package/lib/supergrok-token.js.map +1 -1
- package/lib/term-text.js +79 -8
- package/lib/term-text.js.map +1 -1
- package/lib/terminal-caps.js +358 -0
- package/lib/terminal-caps.js.map +1 -0
- package/lib/terminal-input.js +13 -0
- package/lib/terminal-input.js.map +1 -1
- package/lib/tui.js +775 -165
- package/lib/tui.js.map +1 -1
- package/lib/types/attach.d.ts +8 -0
- package/lib/types/commands.d.ts +3 -0
- package/lib/types/diag.d.ts +20 -1
- package/lib/types/dialogs.d.ts +16 -0
- package/lib/types/display-sock.d.ts +141 -24
- package/lib/types/doctor.d.ts +8 -1
- package/lib/types/dsh-compat.d.ts +173 -44
- package/lib/types/footer.d.ts +3 -1
- package/lib/types/i18n/index.d.ts +31 -15
- package/lib/types/index.d.ts +45 -0
- package/lib/types/paint.d.ts +21 -0
- package/lib/types/picker.d.ts +25 -0
- package/lib/types/platform.d.ts +197 -10
- package/lib/types/preset-authoring.d.ts +8 -14
- package/lib/types/preset-compat.d.ts +58 -0
- package/lib/types/preset-picker.d.ts +1 -1
- package/lib/types/preset-rows.d.ts +63 -13
- package/lib/types/question-wait.d.ts +221 -0
- package/lib/types/selection.d.ts +12 -4
- package/lib/types/settings-routes.d.ts +16 -0
- package/lib/types/settings-subagent.d.ts +24 -0
- package/lib/types/subagent-model.d.ts +10 -10
- package/lib/types/term-text.d.ts +14 -0
- package/lib/types/terminal-caps.d.ts +105 -0
- package/lib/types/terminal-input.d.ts +10 -0
- package/lib/types/transcript-types.d.ts +22 -0
- package/lib/types/tui.d.ts +167 -10
- package/lib/types/update-check.d.ts +19 -4
- package/lib/types/workspace-changes.d.ts +135 -0
- package/lib/update-check.js +25 -8
- package/lib/update-check.js.map +1 -1
- package/lib/workspace-changes.js +120 -0
- package/lib/workspace-changes.js.map +1 -0
- package/package.json +124 -59
package/lib/types/attach.d.ts
CHANGED
|
@@ -48,6 +48,12 @@ export interface LiveHost {
|
|
|
48
48
|
kind: 'attachable' | 'zombie';
|
|
49
49
|
sock: string;
|
|
50
50
|
pid: number;
|
|
51
|
+
/**
|
|
52
|
+
* The lock's own view of the session: `attached` while a display relay is
|
|
53
|
+
* connected, `paused` / `running-detached` once its window is gone. Absent on
|
|
54
|
+
* a lock written before the field existed, which must not read as "in use".
|
|
55
|
+
*/
|
|
56
|
+
state?: string;
|
|
51
57
|
exitWatch?: {
|
|
52
58
|
dispose(): void;
|
|
53
59
|
exited: Promise<number | null>;
|
|
@@ -97,6 +103,8 @@ export interface AttacherDeps {
|
|
|
97
103
|
replaced(sessionId: string): string;
|
|
98
104
|
flapping(sessionId: string): string;
|
|
99
105
|
zombie(sessionId: string, pid: number): string;
|
|
106
|
+
/** Refused: a window is on this session right now (see `attachOrSpawn`). */
|
|
107
|
+
attached(sessionId: string, pid: number): string;
|
|
100
108
|
};
|
|
101
109
|
locksDisabled?(): boolean;
|
|
102
110
|
now?(): number;
|
package/lib/types/commands.d.ts
CHANGED
|
@@ -39,6 +39,9 @@ export declare const LOCAL_COMMANDS: readonly [{
|
|
|
39
39
|
}, {
|
|
40
40
|
readonly name: "disconnect";
|
|
41
41
|
readonly key: "cmd.disconnect";
|
|
42
|
+
}, {
|
|
43
|
+
readonly name: "notify";
|
|
44
|
+
readonly key: "cmd.notify";
|
|
42
45
|
}, {
|
|
43
46
|
readonly name: "approval";
|
|
44
47
|
readonly key: "cmd.approval";
|
package/lib/types/diag.d.ts
CHANGED
|
@@ -21,6 +21,26 @@ 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
|
+
/** Chrome is being drawn in ASCII because the console cannot decode UTF-8. */
|
|
42
|
+
ascii: boolean;
|
|
43
|
+
};
|
|
24
44
|
/**
|
|
25
45
|
* What the palette resolved to and which hints decided it. A "no colour on
|
|
26
46
|
* Windows" report is otherwise guesswork: `TERM` is unset there by default.
|
|
@@ -72,7 +92,6 @@ export declare function readErrTail(sessionId: string, dshHome?: string): Promis
|
|
|
72
92
|
* The first entry is the actionable one; the rest is supporting context.
|
|
73
93
|
*/
|
|
74
94
|
export declare function diagVerdicts(snapshot: DiagSnapshot): string[];
|
|
75
|
-
/** The whole report as transcript lines. Pure. */
|
|
76
95
|
export declare function formatDiag(snapshot: DiagSnapshot): string[];
|
|
77
96
|
/** Gather everything the report needs. Every probe is best-effort. */
|
|
78
97
|
export declare function collectDiag(options: {
|
package/lib/types/dialogs.d.ts
CHANGED
|
@@ -55,6 +55,22 @@ export interface InspectDialog {
|
|
|
55
55
|
subagentSessionId?: string;
|
|
56
56
|
/** `/find` already scrolled to its hit here; later repaints keep the offset. */
|
|
57
57
|
searchRevealed?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* What the copy key takes while this overlay is up.
|
|
60
|
+
*
|
|
61
|
+
* The reader asked to see this body full-screen, so the body is what they
|
|
62
|
+
* mean by "copy this" — the overlay has no input line, and `/copy` typed at
|
|
63
|
+
* the prompt is covered by it. Undefined falls back to whatever is selected
|
|
64
|
+
* behind the overlay, which is the card the reader opened to get here.
|
|
65
|
+
*/
|
|
66
|
+
copyText?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Confirmation line shown in place of the key hint after an in-overlay copy.
|
|
69
|
+
*
|
|
70
|
+
* The notice row the copy also pushes is behind this screen, and silence after
|
|
71
|
+
* a clipboard write is the one thing that reads as a failure.
|
|
72
|
+
*/
|
|
73
|
+
notice?: string;
|
|
58
74
|
}
|
|
59
75
|
export type Dialog = ConfirmDialog | QuestionDialog | OnboardingDialog | InspectDialog;
|
|
60
76
|
export interface DialogAnswer {
|
|
@@ -5,6 +5,29 @@ export declare const FRAME_HELLO = 4;
|
|
|
5
5
|
export declare const FRAME_GOODBYE = 5;
|
|
6
6
|
export declare const FRAME_RTT = 6;
|
|
7
7
|
export declare const FRAME_REPLACED = 7;
|
|
8
|
+
/** Host → relay: round-trip your terminal now and say what it answered. */
|
|
9
|
+
export declare const FRAME_PROBE = 8;
|
|
10
|
+
/** Relay → Host: the answer to {@link FRAME_PROBE}. */
|
|
11
|
+
export declare const FRAME_PROBE_REPLY = 9;
|
|
12
|
+
/** A prober → Host: is a window *really* on this session? Does not claim it. */
|
|
13
|
+
export declare const FRAME_QUERY = 10;
|
|
14
|
+
/** Host → prober: the answer to {@link FRAME_QUERY}. */
|
|
15
|
+
export declare const FRAME_QUERY_REPLY = 11;
|
|
16
|
+
/**
|
|
17
|
+
* What a relay's own terminal round trip said.
|
|
18
|
+
*
|
|
19
|
+
* `live` — the terminal answered just now, so somebody has a screen in front of
|
|
20
|
+
* them. `silent` — it answered when this relay attached and does not answer
|
|
21
|
+
* now: that window is gone. The distinction is the whole point: a cut SSH link
|
|
22
|
+
* leaves the launcher *connected* (it sees no hangup until sshd does, which
|
|
23
|
+
* can be hours with TCP keepalive), so the socket alone cannot tell a live
|
|
24
|
+
* window from a dead one — only the far end can. `unknown` — it never answered
|
|
25
|
+
* (a pipe, a dumb terminal, or a link that was already dead at attach), and
|
|
26
|
+
* silence must not be read as an answer.
|
|
27
|
+
*/
|
|
28
|
+
export type TerminalVerdict = 'live' | 'silent' | 'unknown';
|
|
29
|
+
/** What a Host tells a prober about the window on its session. */
|
|
30
|
+
export type AttachmentVerdict = 'attached' | 'detached' | 'unknown';
|
|
8
31
|
/**
|
|
9
32
|
* Drop launcher SIGTERM/SIGINT/SIGHUP so closing SSH cannot dispose the tree
|
|
10
33
|
* before hangup handling. Leaving the session with setsid() is best-effort:
|
|
@@ -71,6 +94,14 @@ export declare function sessionSockLookupPaths(sessionId: string, dshHome?: stri
|
|
|
71
94
|
* device names (`CON`, `NUL`, …) from becoming the file stem.
|
|
72
95
|
*/
|
|
73
96
|
export declare function sessionErrPath(sessionId: string, dshHome?: string, platform?: NodeJS.Platform): string;
|
|
97
|
+
/**
|
|
98
|
+
* Where the hidden-console bootstrap writes the Host's pid.
|
|
99
|
+
*
|
|
100
|
+
* On `\\.\pipe\` Windows there is no socket file to derive a name from, and the
|
|
101
|
+
* state directory already holds the per-session lock and stderr log, so the pid
|
|
102
|
+
* file lives beside them. It is removed as soon as it has been read.
|
|
103
|
+
*/
|
|
104
|
+
export declare function sessionBootstrapPidPath(sessionId: string, dshHome?: string): string;
|
|
74
105
|
/** Pre-digest Host stderr log next to the 0.7.1 socket, when that name differs. */
|
|
75
106
|
export declare function legacySessionErrPath(sessionId: string, dshHome?: string, platform?: NodeJS.Platform): string | undefined;
|
|
76
107
|
export declare function encodeFrame(type: number, payload?: Buffer): Buffer;
|
|
@@ -81,6 +112,12 @@ export declare function decodeResize(payload: Buffer): {
|
|
|
81
112
|
} | undefined;
|
|
82
113
|
export declare function encodeRtt(rttMs: number | undefined): Buffer;
|
|
83
114
|
export declare function decodeRtt(payload: Buffer): number | undefined;
|
|
115
|
+
export declare function encodeProbe(): Buffer;
|
|
116
|
+
export declare function encodeProbeReply(verdict: TerminalVerdict): Buffer;
|
|
117
|
+
export declare function decodeProbeReply(payload: Buffer): TerminalVerdict;
|
|
118
|
+
export declare function encodeQuery(): Buffer;
|
|
119
|
+
export declare function encodeQueryReply(verdict: AttachmentVerdict): Buffer;
|
|
120
|
+
export declare function decodeQueryReply(payload: Buffer): AttachmentVerdict;
|
|
84
121
|
/** Incremental decoder for one socket. */
|
|
85
122
|
export declare class FrameReader {
|
|
86
123
|
private buffer;
|
|
@@ -105,19 +142,45 @@ export interface DisplayHostHandlers {
|
|
|
105
142
|
export declare class DisplayHost {
|
|
106
143
|
readonly path: string;
|
|
107
144
|
private readonly handlers;
|
|
108
|
-
/** Test
|
|
145
|
+
/** Test seams: how long a silent connection may wait for its HELLO, and
|
|
146
|
+
* how long a relay may take to report its terminal round trip. */
|
|
109
147
|
private readonly options;
|
|
110
148
|
private server;
|
|
111
149
|
private socket;
|
|
112
150
|
private reader;
|
|
113
151
|
attached: boolean;
|
|
152
|
+
/** Set while a {@link FRAME_PROBE} round trip is waiting for its answer. */
|
|
153
|
+
private probeSettle;
|
|
154
|
+
private probeInFlight;
|
|
114
155
|
constructor(path: string, handlers: DisplayHostHandlers,
|
|
115
|
-
/** Test
|
|
156
|
+
/** Test seams: how long a silent connection may wait for its HELLO, and
|
|
157
|
+
* how long a relay may take to report its terminal round trip. */
|
|
116
158
|
options?: {
|
|
117
159
|
helloGraceMs?: number;
|
|
160
|
+
probeTimeoutMs?: number;
|
|
118
161
|
});
|
|
119
162
|
listen(): Promise<void>;
|
|
120
163
|
private accept;
|
|
164
|
+
/**
|
|
165
|
+
* Whether a window is really on this session — asked of the window itself.
|
|
166
|
+
*
|
|
167
|
+
* "The socket answers" is not evidence. A relay whose SSH link was cut stays
|
|
168
|
+
* connected (its launcher sees no hangup until sshd does), and the Host then
|
|
169
|
+
* truthfully reports a display that no longer has a screen behind it. Only the
|
|
170
|
+
* far end can settle it, so this round-trips the terminal through the relay.
|
|
171
|
+
*/
|
|
172
|
+
attachmentVerdict(): Promise<AttachmentVerdict>;
|
|
173
|
+
/** One answer to one prober; never claims the display, never throws. */
|
|
174
|
+
private answerQuery;
|
|
175
|
+
/**
|
|
176
|
+
* Ask the claimed relay to round-trip its terminal, with a deadline.
|
|
177
|
+
*
|
|
178
|
+
* Sharing one in-flight round trip matters: a second question while the first
|
|
179
|
+
* is unanswered would be settled by the first reply, and two windows asking at
|
|
180
|
+
* once is a normal resume, not an error.
|
|
181
|
+
*/
|
|
182
|
+
private probeTerminal;
|
|
183
|
+
private settleProbe;
|
|
121
184
|
sendStdout(bytes: Buffer | string): boolean;
|
|
122
185
|
sendGoodbye(): void;
|
|
123
186
|
close(): Promise<void>;
|
|
@@ -134,6 +197,24 @@ export declare class DisplayHost {
|
|
|
134
197
|
* it without stealing the display.
|
|
135
198
|
*/
|
|
136
199
|
export declare function displaySockExists(path: string, timeoutMs?: number): Promise<boolean>;
|
|
200
|
+
/**
|
|
201
|
+
* How long a prober waits for the Host's answer to {@link FRAME_QUERY}.
|
|
202
|
+
*
|
|
203
|
+
* It has to cover the relay's own two probe windows (see
|
|
204
|
+
* {@link DISPLAY_PROBE_TIMEOUT_MS}) plus the hop; the answer only takes this
|
|
205
|
+
* long when the display really is silent.
|
|
206
|
+
*/
|
|
207
|
+
export declare const DISPLAY_QUERY_TIMEOUT_MS = 4000;
|
|
208
|
+
/**
|
|
209
|
+
* Ask a live Host whether a window is really on its session.
|
|
210
|
+
*
|
|
211
|
+
* `undefined` means the Host did not answer — an older Host that does not know
|
|
212
|
+
* the frame, or one whose event loop is stuck. Every caller must read that as
|
|
213
|
+
* "cannot tell" and never as "free": the cost of the doubt is one confirmation
|
|
214
|
+
* prompt, and the cost of guessing wrong is taking a session away from a window
|
|
215
|
+
* somebody is still typing in.
|
|
216
|
+
*/
|
|
217
|
+
export declare function queryDisplayAttachment(path: string, timeoutMs?: number): Promise<AttachmentVerdict | undefined>;
|
|
137
218
|
/** Watches a freshly spawned Host so a crash is reported immediately. */
|
|
138
219
|
export interface HostExitWatch {
|
|
139
220
|
/** Resolves with the exit code (or null when killed) once the Host exits. */
|
|
@@ -141,6 +222,8 @@ export interface HostExitWatch {
|
|
|
141
222
|
/** Stop watching; call once the channel is confirmed up. */
|
|
142
223
|
dispose(): void;
|
|
143
224
|
}
|
|
225
|
+
/** Exported for the test that pins its loop-ref behaviour; not public API. */
|
|
226
|
+
export declare function watchHostPid(pid: number): HostExitWatch;
|
|
144
227
|
/**
|
|
145
228
|
* How long a dead-pid report waits for the child's `exit` event before giving
|
|
146
229
|
* up on its code. The event is normally delivered within a tick; the bound only
|
|
@@ -191,33 +274,64 @@ export declare function quietTerminalInput(stdin?: NodeJS.ReadStream): number;
|
|
|
191
274
|
* written into a link that may already be dead.
|
|
192
275
|
*/
|
|
193
276
|
export declare function restoreTerminalInput(stdin?: NodeJS.ReadStream): void;
|
|
277
|
+
/** Test seam for {@link spawnDetachedHost}; production passes nothing. */
|
|
278
|
+
export interface SpawnHostOptions {
|
|
279
|
+
/**
|
|
280
|
+
* Start the Host through this command instead of resolving the real one, or
|
|
281
|
+
* `null` to spawn it directly even where a bootstrap exists.
|
|
282
|
+
*
|
|
283
|
+
* `null` is for the tests that are about the *direct* path — the fixture Hosts
|
|
284
|
+
* in `tests/display-host-e2e.test.mjs` assert on the child's exit code, which
|
|
285
|
+
* only a real child handle can report (a pid watch resolves `null`). The
|
|
286
|
+
* bootstrap has its own coverage: the builders on every platform, and the real
|
|
287
|
+
* Windows probes end to end.
|
|
288
|
+
*/
|
|
289
|
+
bootstrap?: {
|
|
290
|
+
command: string;
|
|
291
|
+
args: string[];
|
|
292
|
+
} | null | undefined;
|
|
293
|
+
/** How long the bootstrap may take to report a pid (Windows PowerShell start). */
|
|
294
|
+
bootstrapTimeoutMs?: number;
|
|
295
|
+
}
|
|
194
296
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
297
|
+
* Start the Host through the hidden-console bootstrap and return its pid, or
|
|
298
|
+
* `undefined` when the bootstrap could not report one.
|
|
197
299
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
300
|
+
* `spawnSync` on purpose. The pid has to be in hand before this function
|
|
301
|
+
* returns (the caller watches it, and the fallback must never leave two Hosts
|
|
302
|
+
* for one session), and the cost is one bounded wait while the boot splash is
|
|
303
|
+
* already on screen. PowerShell exits as soon as `Start-Process` has created the
|
|
304
|
+
* Host, so the wait is its own start-up, not the Host's.
|
|
200
305
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* (
|
|
204
|
-
*
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
306
|
+
* Falling back is safe exactly when nothing was printed: `Start-Process -PassThru`
|
|
307
|
+
* either starts the Host and prints its id, or throws before starting anything
|
|
308
|
+
* (`$ErrorActionPreference = 'Stop'`). A *timeout* is the one case where a Host
|
|
309
|
+
* might exist and the pid was lost, so it does not fall back — it reports.
|
|
310
|
+
*/
|
|
311
|
+
export declare function spawnHostThroughBootstrap(bootstrap: {
|
|
312
|
+
command: string;
|
|
313
|
+
args: string[];
|
|
314
|
+
}, options: {
|
|
315
|
+
env: NodeJS.ProcessEnv;
|
|
316
|
+
platform: NodeJS.Platform;
|
|
317
|
+
timeoutMs: number;
|
|
318
|
+
/** File the bootstrap writes the Host's pid to. */
|
|
319
|
+
pidFile: string;
|
|
320
|
+
}): {
|
|
321
|
+
pid: number;
|
|
322
|
+
} | undefined;
|
|
323
|
+
/**
|
|
324
|
+
* Spawn a detached Host copy of this `dsh` invocation and return its sock path.
|
|
211
325
|
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
326
|
+
* On Windows the Host goes through {@link hostBootstrapCommand} when the OS
|
|
327
|
+
* PowerShell is available: a direct spawn there cannot both survive the
|
|
328
|
+
* launcher (libuv's `KILL_ON_JOB_CLOSE` job takes a non-detached child with it)
|
|
329
|
+
* and avoid flashing console windows (`detached` is DETACHED_PROCESS, which makes
|
|
330
|
+
* Windows ignore `CREATE_NO_WINDOW`). The bootstrap gives the Host a console of
|
|
331
|
+
* its own, hidden — see `docs/platform.md`. Without it, the direct spawn below
|
|
332
|
+
* is still what runs, with the old semantics.
|
|
214
333
|
*/
|
|
215
|
-
export declare function
|
|
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;
|
|
334
|
+
export declare function spawnDetachedHost(sessionId: string, platform?: NodeJS.Platform, options?: SpawnHostOptions): SpawnedHost;
|
|
221
335
|
export declare function probeDisplaySock(path: string, timeoutMs?: number): Promise<boolean>;
|
|
222
336
|
export interface RelayResult {
|
|
223
337
|
/** Host sent goodbye — user exited from the attached session. */
|
|
@@ -230,6 +344,9 @@ export interface DisplayRelayOptions {
|
|
|
230
344
|
signals?: Pick<NodeJS.Process, 'on' | 'off' | 'removeListener'>;
|
|
231
345
|
/** Link kind for the RTT probe; defaults to this process's SSH env. */
|
|
232
346
|
ssh?: boolean;
|
|
347
|
+
/** Re-measure cadences (tests); defaults to {@link RTT_RECHECK_FIRST_MS} / {@link RTT_RECHECK_MS}. */
|
|
348
|
+
rttFirstRecheckMs?: number;
|
|
349
|
+
rttRecheckMs?: number;
|
|
233
350
|
/** Typing captured before this relay existed; sent to the Host after HELLO. */
|
|
234
351
|
seed?: string;
|
|
235
352
|
/**
|
package/lib/types/doctor.d.ts
CHANGED
|
@@ -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,168 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dual-stack shims for dsh 0.1.
|
|
3
|
-
* handle API that landed with
|
|
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.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
15
|
+
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
16
16
|
import type z from '@deepseek-ai/schemastery';
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* The source kind this plugin declares for the messages it commits itself: the
|
|
19
|
+
* plan-close nudge and the approval-denied steering notice.
|
|
20
|
+
*
|
|
21
|
+
* These used to ride the released catch-all wrapper —
|
|
22
|
+
* `<kind: 'plugin', plugin: 'dsh-ssh-tui'>` — which 0.1.5 shipped as a member and
|
|
23
|
+
* 0.1.7 deleted ("each producer declares its own `kind` in its own module; there
|
|
24
|
+
* is no shared catch-all `plugin` kind"). Worse than missing on 0.1.7: its V4
|
|
25
|
+
* native admission *refuses* the wrapper outright, so an append under that
|
|
26
|
+
* spelling throws "format v4 message requires a producer-owned source kind" and
|
|
27
|
+
* takes the whole turn down with it — which is what the plan nudge and the
|
|
28
|
+
* approval-denied notice did to any turn they fired in on a V4 session.
|
|
29
|
+
*
|
|
30
|
+
* The producer-owned spelling is the one shape both supported lines admit: the
|
|
31
|
+
* wrapper was only ever mandatory for `system/message` (`SystemMessage['source']`
|
|
32
|
+
* is the `plugin` member on 0.1.5), while user-role messages have always carried
|
|
33
|
+
* a producer's own kind — the 0.1.5 line's own `goal`, `webhook`, and
|
|
34
|
+
* `agent-instructions` producers all commit `kind: '<their name>'`.
|
|
35
|
+
*/
|
|
36
|
+
export declare const TUI_SOURCE_KIND = "dsh-ssh-tui";
|
|
37
|
+
declare module '@deepseek-ai/dsh-llm' {
|
|
38
|
+
interface MessageSourceMap {
|
|
39
|
+
'dsh-ssh-tui': {
|
|
40
|
+
kind: 'dsh-ssh-tui';
|
|
41
|
+
} & ContextFormed;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The kind the V3-to-V4 lane gives the same messages in a converted log.
|
|
46
|
+
*
|
|
47
|
+
* The conversion lifts a released plugin wrapper to the producer's kind when it
|
|
48
|
+
* knows the plugin, and falls back to `plugin:<name>` when it does not — which is
|
|
49
|
+
* this plugin's case, so a resumed V3 session carries the prefixed spelling.
|
|
50
|
+
*/
|
|
51
|
+
export declare const TUI_CONVERTED_SOURCE_KIND = "plugin:dsh-ssh-tui";
|
|
52
|
+
/**
|
|
53
|
+
* Whether a durable message source came from this plugin.
|
|
54
|
+
*
|
|
55
|
+
* Three spellings reach a reader: the producer-owned kind written now, the
|
|
56
|
+
* `plugin:`-prefixed compatibility kind the V3-to-V4 conversion assigns, and the
|
|
57
|
+
* original wrapper (`kind: 'plugin'` + `plugin: 'dsh-ssh-tui'`) that a log
|
|
58
|
+
* committed before this change — or an unconverted V3 file read on the 0.1.5
|
|
59
|
+
* line — still carries.
|
|
60
|
+
*
|
|
61
|
+
* @param kind - the source's `kind`, when it has one.
|
|
62
|
+
* @param plugin - the source's legacy `plugin` field, when it carries the wrapper.
|
|
63
|
+
* @returns whether this plugin produced the message.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isTuiMessageSource(kind: unknown, plugin?: unknown): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Settings namespaces are branded strings at the type level on both supported
|
|
68
|
+
* lines; this cast supplies the brand from a plain literal.
|
|
20
69
|
*/
|
|
21
70
|
export declare function settingsNamespace(value: string): SettingsNamespace;
|
|
22
71
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
72
|
+
* Hooks a settings consumer hands to {@link installSettingsSection}.
|
|
73
|
+
*
|
|
74
|
+
* Spelled out locally instead of imported: 0.1.7 deleted the
|
|
75
|
+
* `SettingsSectionHooks` export, and this shape is the whole contract the three
|
|
76
|
+
* call sites use.
|
|
77
|
+
*/
|
|
78
|
+
export interface SettingsSectionHooks<T> {
|
|
79
|
+
/**
|
|
80
|
+
* Receive the active configuration source: the resolved settings value while
|
|
81
|
+
* a section is attached. Called at attach and again after every change.
|
|
82
|
+
* @param current - thunk returning the currently authoritative value.
|
|
83
|
+
*/
|
|
84
|
+
setSource(current: () => T): void;
|
|
85
|
+
/**
|
|
86
|
+
* Re-judge anything derived from the source — registration-level facts,
|
|
87
|
+
* memoized resolutions — after an attach or a committed change.
|
|
88
|
+
*/
|
|
89
|
+
onChange(): void;
|
|
90
|
+
/** Reject a resolved section this consumer could not act on. */
|
|
91
|
+
validate?(value: T): void;
|
|
92
|
+
}
|
|
93
|
+
/** Which settings protocol the running host speaks. */
|
|
94
|
+
export type SettingsGeneration = 'legacy' | 'forms';
|
|
95
|
+
/**
|
|
96
|
+
* The host's settings generation, as feature detection rather than a version.
|
|
97
|
+
*
|
|
98
|
+
* `legacy` (0.1.5) resolves a namespace through `settings.get`; `forms` (0.1.7)
|
|
99
|
+
* has no `get` and projects a form per loader entry. Callers that differ by
|
|
100
|
+
* generation — which profile rows a terminal profile must mount, for one — read
|
|
101
|
+
* it here instead of sniffing package versions.
|
|
102
|
+
*
|
|
103
|
+
* Before the service exists (a plugin applies before it is mounted) the same
|
|
104
|
+
* split shows up as the roster package's absence, and the answer is memoized as
|
|
105
|
+
* soon as the service is seen so a later call cannot disagree with an earlier
|
|
106
|
+
* one.
|
|
107
|
+
*/
|
|
108
|
+
export declare function hostSettingsGeneration(ctx: Context): SettingsGeneration;
|
|
109
|
+
/** Forget the memoized descriptors so the next read re-walks the forms. */
|
|
110
|
+
export declare function invalidateSettingsCache(ctx: Context): void;
|
|
111
|
+
/**
|
|
112
|
+
* Read one settings section.
|
|
113
|
+
*
|
|
114
|
+
* 0.1.5 resolves it through `get(ns)`. 0.1.7 has no `get`, so the value comes
|
|
115
|
+
* from the entry's descriptor, which projects the live config — volatile fields
|
|
116
|
+
* only, i.e. exactly the fields its form exposes — and returns `undefined` when
|
|
117
|
+
* no entry carries that id.
|
|
118
|
+
*/
|
|
119
|
+
export declare function readSettingsSection(ctx: Context, ns: SettingsNamespace): unknown;
|
|
120
|
+
/**
|
|
121
|
+
* The user's own settings document, keyed by namespace, on either line.
|
|
122
|
+
*
|
|
123
|
+
* 0.1.5 publishes it as `settings.document`. 0.1.7 dropped the property — the
|
|
124
|
+
* document is the profile patch now — but each descriptor still carries the
|
|
125
|
+
* user layer it was built from, so the same view is reconstructible. Callers
|
|
126
|
+
* that ask "did the user configure this?" must use this, not
|
|
127
|
+
* {@link readSettingsSection}: a resolved read also carries the composition
|
|
128
|
+
* base and schema defaults, which is exactly what such a caller must not
|
|
129
|
+
* mistake for a user choice.
|
|
130
|
+
*/
|
|
131
|
+
export declare function settingsDocument(ctx: Context): Record<string, unknown> | undefined;
|
|
132
|
+
/**
|
|
133
|
+
* Mark one schema field as form-writable on the 0.1.7 line.
|
|
134
|
+
*
|
|
135
|
+
* 0.1.7 projects only fields whose schema node carries the `volatile` meta, and
|
|
136
|
+
* refuses a settings write to any other path. The 0.1.5 schemastery (3.18.2)
|
|
137
|
+
* has no such builder, so the call is feature-detected rather than typed: on
|
|
138
|
+
* that line the marker means nothing and the schema is returned untouched.
|
|
139
|
+
*/
|
|
140
|
+
export declare function liveField<T>(schema: z<T>): z<T>;
|
|
141
|
+
/**
|
|
142
|
+
* Register a settings section.
|
|
143
|
+
*
|
|
144
|
+
* 0.1.5 publishes the `settings` service with `installSection`, callable only
|
|
145
|
+
* once that service is injected (plugins apply before it, so `ctx.inject` must
|
|
146
|
+
* defer — same pattern the harness's own packages use).
|
|
147
|
+
*
|
|
148
|
+
* 0.1.7 removed it. The section is now the loader entry's own `Config`
|
|
149
|
+
* schema — this plugin's is `ssh-tui`, and the two auxiliary namespaces are
|
|
150
|
+
* carried by the `dsh-ssh-tui/settings-*` rows in `cordis.patch.yml` — so the
|
|
151
|
+
* only thing left for a consumer to wire is the live read (`setSource`) and the
|
|
152
|
+
* change notification (`onChange`).
|
|
28
153
|
*/
|
|
29
154
|
export declare function installSettingsSection<T>(ctx: Context, ns: SettingsNamespace, schema: z<T>, entry: T, hooks: SettingsSectionHooks<T>): void;
|
|
30
155
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
156
|
+
* Whether a tool result reports failure.
|
|
157
|
+
*
|
|
158
|
+
* 0.1.5 carries `isError` on the `tool-result` content block; 0.1.7 removed
|
|
159
|
+
* that block from `ContentBlockMap` and moved the flag onto the message
|
|
160
|
+
* itself. Both are read, so one build understands either host.
|
|
161
|
+
*/
|
|
162
|
+
export declare function toolResultFailed(message: unknown): boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Read the full durable event log. Both supported lines read it on demand
|
|
165
|
+
* through `snapshotEvents()`.
|
|
33
166
|
*/
|
|
34
167
|
export declare function sessionEvents(session: object): readonly SessionEvent[];
|
|
35
168
|
/**
|
|
@@ -60,7 +193,6 @@ export interface SessionHeaderLike {
|
|
|
60
193
|
/** Logical log plus the header it belongs to. */
|
|
61
194
|
export interface SessionInspectionLike {
|
|
62
195
|
events: readonly unknown[];
|
|
63
|
-
meta?: SessionHeaderLike;
|
|
64
196
|
header?: SessionHeaderLike;
|
|
65
197
|
/**
|
|
66
198
|
* Backend state for the slice. `detached` means the backend never
|
|
@@ -71,27 +203,28 @@ export interface SessionInspectionLike {
|
|
|
71
203
|
eventState?: string;
|
|
72
204
|
}
|
|
73
205
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
206
|
+
* Both supported lines return `{ header, revision, … }` snapshots from
|
|
207
|
+
* `list()`; normalize to the header so the picker does not care which host
|
|
76
208
|
* it is talking to.
|
|
77
209
|
*/
|
|
78
210
|
export declare function listPersistenceHeaders(persistence: object): Promise<SessionHeaderLike[]>;
|
|
79
211
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
212
|
+
* Read one session through `open(id, 'read')` + `handle.read()`, the access
|
|
213
|
+
* both supported lines expose. Close the handle so a listing pass does not pin
|
|
214
|
+
* write ownership.
|
|
83
215
|
*/
|
|
84
216
|
export declare function inspectPersistenceSession(persistence: object, id: unknown): Promise<SessionInspectionLike>;
|
|
85
|
-
/**
|
|
217
|
+
/**
|
|
218
|
+
* A session's artifact path from its header. Both supported lines still
|
|
219
|
+
* implement `locate()` on the JSONL backend, but their typings keep it
|
|
220
|
+
* private, so the call stays feature-detected.
|
|
221
|
+
*/
|
|
86
222
|
export declare function persistenceLocate(persistence: object, meta: object): {
|
|
87
223
|
path?: string;
|
|
88
224
|
} | 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
|
-
*/
|
|
225
|
+
/** Whether a command's input admits the composer's attachments. */
|
|
93
226
|
export declare function commandAcceptsAttachments(input: unknown): boolean;
|
|
94
|
-
/** One
|
|
227
|
+
/** One chunk from a live `agent/assistant-stream` frame. */
|
|
95
228
|
export interface StreamChunkLike {
|
|
96
229
|
type: string;
|
|
97
230
|
text?: string;
|
|
@@ -133,12 +266,12 @@ export declare function streamFrameAttemptId(frame: unknown): unknown;
|
|
|
133
266
|
*/
|
|
134
267
|
export declare function streamFirstTokenTime(stream: unknown): number | undefined;
|
|
135
268
|
/**
|
|
136
|
-
*
|
|
269
|
+
* A live `agent/assistant-stream` chunk frame's inner chunk plus its framing.
|
|
137
270
|
*
|
|
138
|
-
* `fallback` supplies the turn/step
|
|
139
|
-
*
|
|
271
|
+
* `fallback` supplies the turn/step, which chunk frames do not carry (see
|
|
272
|
+
* {@link streamFrameOwner}).
|
|
140
273
|
*/
|
|
141
|
-
export declare function streamChunkOf(
|
|
274
|
+
export declare function streamChunkOf(frame: unknown, fallback?: {
|
|
142
275
|
turn: number;
|
|
143
276
|
step: number;
|
|
144
277
|
}): {
|
|
@@ -147,20 +280,16 @@ export declare function streamChunkOf(eventOrFrame: unknown, fallback?: {
|
|
|
147
280
|
step: number;
|
|
148
281
|
time: number;
|
|
149
282
|
/**
|
|
150
|
-
* False when neither the
|
|
283
|
+
* False when neither the frame nor a fallback carried a real turn/step.
|
|
151
284
|
* Usage folded under such a chunk would be filed under a bogus key (0:0)
|
|
152
285
|
* that `step/end` never clears, inflating the session totals forever.
|
|
153
286
|
*/
|
|
154
287
|
stepKnown: boolean;
|
|
155
288
|
} | undefined;
|
|
156
|
-
/**
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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.
|
|
289
|
+
/**
|
|
290
|
+
* Subscribe to a host event whose scoped payload type does not match this
|
|
291
|
+
* build's `Events` map. Both supported lines emit `agent/assistant-stream`,
|
|
292
|
+
* and Cordis accepts the plain event name at runtime.
|
|
164
293
|
*/
|
|
165
294
|
export declare function listenHostEvent(ctx: {
|
|
166
295
|
on: (event: never, handler: never) => unknown;
|
package/lib/types/footer.d.ts
CHANGED
|
@@ -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
|
|
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 {
|