dsh-ssh-tui 0.7.3 → 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 +98 -27
- package/README.md +77 -27
- package/docs/remote-ops.md +27 -1
- package/docs/terminals.md +17 -4
- 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 +3 -0
- package/lib/diag.js.map +1 -1
- package/lib/dialogs.js.map +1 -1
- package/lib/display-sock.js +313 -2
- package/lib/display-sock.js.map +1 -1
- package/lib/dsh-compat.js +46 -0
- package/lib/dsh-compat.js.map +1 -1
- package/lib/i18n/en.js +40 -14
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/index.js +5 -0
- package/lib/i18n/index.js.map +1 -1
- package/lib/i18n/zh.js +40 -14
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +75 -15
- 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 +32 -1
- package/lib/paint.js.map +1 -1
- package/lib/picker.js +104 -15
- package/lib/picker.js.map +1 -1
- package/lib/plan.js +8 -0
- package/lib/plan.js.map +1 -1
- package/lib/platform.js +81 -0
- package/lib/platform.js.map +1 -1
- package/lib/preset-rows.js +54 -24
- package/lib/preset-rows.js.map +1 -1
- package/lib/question-wait.js +418 -0
- package/lib/question-wait.js.map +1 -0
- package/lib/selection.js +26 -8
- package/lib/selection.js.map +1 -1
- package/lib/term-text.js +79 -8
- package/lib/term-text.js.map +1 -1
- package/lib/terminal-input.js +13 -0
- package/lib/terminal-input.js.map +1 -1
- package/lib/tui.js +597 -51
- 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 +2 -0
- package/lib/types/dialogs.d.ts +16 -0
- package/lib/types/display-sock.d.ts +78 -2
- package/lib/types/dsh-compat.d.ts +44 -12
- package/lib/types/i18n/index.d.ts +13 -3
- package/lib/types/paint.d.ts +21 -0
- package/lib/types/picker.d.ts +22 -0
- package/lib/types/platform.d.ts +22 -0
- package/lib/types/preset-rows.d.ts +18 -10
- package/lib/types/question-wait.d.ts +221 -0
- package/lib/types/selection.d.ts +12 -4
- package/lib/types/settings-subagent.d.ts +3 -3
- package/lib/types/subagent-model.d.ts +3 -3
- package/lib/types/term-text.d.ts +14 -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 +126 -3
- 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 +80 -42
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
|
@@ -38,6 +38,8 @@ export interface DiagSnapshot {
|
|
|
38
38
|
title: boolean;
|
|
39
39
|
/** `DSH_TUI_TERM_CAPS` tokens that were rejected (typos), for the row. */
|
|
40
40
|
ignoredOverrides: readonly string[];
|
|
41
|
+
/** Chrome is being drawn in ASCII because the console cannot decode UTF-8. */
|
|
42
|
+
ascii: boolean;
|
|
41
43
|
};
|
|
42
44
|
/**
|
|
43
45
|
* What the palette resolved to and which hints decided it. A "no colour on
|
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:
|
|
@@ -89,6 +112,12 @@ export declare function decodeResize(payload: Buffer): {
|
|
|
89
112
|
} | undefined;
|
|
90
113
|
export declare function encodeRtt(rttMs: number | undefined): Buffer;
|
|
91
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;
|
|
92
121
|
/** Incremental decoder for one socket. */
|
|
93
122
|
export declare class FrameReader {
|
|
94
123
|
private buffer;
|
|
@@ -113,19 +142,45 @@ export interface DisplayHostHandlers {
|
|
|
113
142
|
export declare class DisplayHost {
|
|
114
143
|
readonly path: string;
|
|
115
144
|
private readonly handlers;
|
|
116
|
-
/** 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. */
|
|
117
147
|
private readonly options;
|
|
118
148
|
private server;
|
|
119
149
|
private socket;
|
|
120
150
|
private reader;
|
|
121
151
|
attached: boolean;
|
|
152
|
+
/** Set while a {@link FRAME_PROBE} round trip is waiting for its answer. */
|
|
153
|
+
private probeSettle;
|
|
154
|
+
private probeInFlight;
|
|
122
155
|
constructor(path: string, handlers: DisplayHostHandlers,
|
|
123
|
-
/** 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. */
|
|
124
158
|
options?: {
|
|
125
159
|
helloGraceMs?: number;
|
|
160
|
+
probeTimeoutMs?: number;
|
|
126
161
|
});
|
|
127
162
|
listen(): Promise<void>;
|
|
128
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;
|
|
129
184
|
sendStdout(bytes: Buffer | string): boolean;
|
|
130
185
|
sendGoodbye(): void;
|
|
131
186
|
close(): Promise<void>;
|
|
@@ -142,6 +197,24 @@ export declare class DisplayHost {
|
|
|
142
197
|
* it without stealing the display.
|
|
143
198
|
*/
|
|
144
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>;
|
|
145
218
|
/** Watches a freshly spawned Host so a crash is reported immediately. */
|
|
146
219
|
export interface HostExitWatch {
|
|
147
220
|
/** Resolves with the exit code (or null when killed) once the Host exits. */
|
|
@@ -271,6 +344,9 @@ export interface DisplayRelayOptions {
|
|
|
271
344
|
signals?: Pick<NodeJS.Process, 'on' | 'off' | 'removeListener'>;
|
|
272
345
|
/** Link kind for the RTT probe; defaults to this process's SSH env. */
|
|
273
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;
|
|
274
350
|
/** Typing captured before this relay existed; sent to the Host after HELLO. */
|
|
275
351
|
seed?: string;
|
|
276
352
|
/**
|
|
@@ -14,23 +14,55 @@ import type { ContextFormed } from '@deepseek-ai/dsh-llm';
|
|
|
14
14
|
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
15
15
|
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
16
16
|
import type z from '@deepseek-ai/schemastery';
|
|
17
|
+
/**
|
|
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";
|
|
17
37
|
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
38
|
interface MessageSourceMap {
|
|
28
|
-
|
|
29
|
-
kind: '
|
|
30
|
-
plugin: string;
|
|
39
|
+
'dsh-ssh-tui': {
|
|
40
|
+
kind: 'dsh-ssh-tui';
|
|
31
41
|
} & ContextFormed;
|
|
32
42
|
}
|
|
33
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;
|
|
34
66
|
/**
|
|
35
67
|
* Settings namespaces are branded strings at the type level on both supported
|
|
36
68
|
* lines; this cast supplies the brand from a plain literal.
|
|
@@ -16,7 +16,7 @@ export declare const UI_LOCALE_NAMESPACE: import("@deepseek-ai/dsh-settings").Se
|
|
|
16
16
|
* Every field is live: on 0.1.7 the section *is* this form, and only volatile
|
|
17
17
|
* paths may be written (see {@link liveField}).
|
|
18
18
|
*/
|
|
19
|
-
export declare const UI_LOCALE_SCHEMA: z<Schemastery.ObjectS<{
|
|
19
|
+
export declare const UI_LOCALE_SCHEMA: z<Schemastery.ObjectS<NoInfer<{
|
|
20
20
|
language: z<string>;
|
|
21
21
|
skipUpdate: z<string>;
|
|
22
22
|
view: z<string>;
|
|
@@ -24,7 +24,12 @@ export declare const UI_LOCALE_SCHEMA: z<Schemastery.ObjectS<{
|
|
|
24
24
|
autoApproval: z<string>;
|
|
25
25
|
/** Milliseconds a leftover, finished Host waits before exiting; 0 = never. */
|
|
26
26
|
idleExit: z<number>;
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Command run once when a question starts waiting and nobody is attached.
|
|
29
|
+
* Empty means off. See `question-wait.ts` for the environment it receives.
|
|
30
|
+
*/
|
|
31
|
+
notify: z<string>;
|
|
32
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
28
33
|
language: z<string>;
|
|
29
34
|
skipUpdate: z<string>;
|
|
30
35
|
view: z<string>;
|
|
@@ -32,7 +37,12 @@ export declare const UI_LOCALE_SCHEMA: z<Schemastery.ObjectS<{
|
|
|
32
37
|
autoApproval: z<string>;
|
|
33
38
|
/** Milliseconds a leftover, finished Host waits before exiting; 0 = never. */
|
|
34
39
|
idleExit: z<number>;
|
|
35
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Command run once when a question starts waiting and nobody is attached.
|
|
42
|
+
* Empty means off. See `question-wait.ts` for the environment it receives.
|
|
43
|
+
*/
|
|
44
|
+
notify: z<string>;
|
|
45
|
+
}>>, "plain">;
|
|
36
46
|
export declare function localeFromTag(tag: string): Locale | undefined;
|
|
37
47
|
/** Pick zh/en from env, optionally after a saved settings value. */
|
|
38
48
|
export declare function resolveLocale(env?: NodeJS.ProcessEnv, saved?: string): Locale;
|
package/lib/types/paint.d.ts
CHANGED
|
@@ -37,6 +37,27 @@ export declare function ignoreFurtherHangupSignals(): void;
|
|
|
37
37
|
export declare function waitUntilIdleOrTimeout(isIdle: () => boolean, timeoutMs: number, now?: () => number, wait?: (ms: number) => Promise<void>): Promise<'idle' | 'timeout'>;
|
|
38
38
|
/** Map a CSI-6n round-trip to a paint cadence. Unknown RTT uses the SSH default. */
|
|
39
39
|
export declare function paintIntervalForRtt(rttMs: number | undefined): number;
|
|
40
|
+
/**
|
|
41
|
+
* How many measurements the link's reported round-trip is taken over.
|
|
42
|
+
*
|
|
43
|
+
* Three is the smallest window where one outlier cannot move the answer and two
|
|
44
|
+
* agreeing measurements can.
|
|
45
|
+
*/
|
|
46
|
+
export declare const RTT_HISTORY = 3;
|
|
47
|
+
/**
|
|
48
|
+
* The link's current round-trip, from the last few measurements.
|
|
49
|
+
*
|
|
50
|
+
* The median — not the latest, not the mean. The probe shares the wire with the
|
|
51
|
+
* paint it is measuring, so a single burst can read like an overloaded link, and
|
|
52
|
+
* because the cadence *and* the per-frame byte budget follow this number, one
|
|
53
|
+
* bad measurement used to sit on the footer chip (and in the paint budget) for
|
|
54
|
+
* the rest of the session. A median ignores one outlier, follows a real change
|
|
55
|
+
* as soon as two measurements agree, and averages the pair in between so the
|
|
56
|
+
* chip does not jump.
|
|
57
|
+
* @param samples - measured round-trips, oldest first; non-finite ones ignored.
|
|
58
|
+
* @returns the median in whole milliseconds, or undefined with nothing to go on.
|
|
59
|
+
*/
|
|
60
|
+
export declare function medianRtt(samples: readonly number[]): number | undefined;
|
|
40
61
|
export declare function paintLinkLabel(kind: PaintLinkKind, intervalMs: number, probed: boolean): string;
|
|
41
62
|
export type LinkQuality = 'local' | 'good' | 'ok' | 'slow' | 'poor' | 'unknown';
|
|
42
63
|
/** Signal-bar quality from a measured SSH round-trip, or local TTY. */
|
package/lib/types/picker.d.ts
CHANGED
|
@@ -44,6 +44,18 @@ export interface SessionPickerState {
|
|
|
44
44
|
loading?: boolean;
|
|
45
45
|
/** Sessions in history that have not been read yet. */
|
|
46
46
|
more?: number;
|
|
47
|
+
/**
|
|
48
|
+
* A row whose session another window is attached to, waiting for the user to
|
|
49
|
+
* confirm. Submitting such a row does not attach on its own: the lock says a
|
|
50
|
+
* window is on that session, and attaching kicks that window. The note under
|
|
51
|
+
* the row cannot prove the window is still alive, so the picker asks once
|
|
52
|
+
* instead of either refusing forever or silently stealing the session.
|
|
53
|
+
*/
|
|
54
|
+
confirmTakeover?: {
|
|
55
|
+
id: string;
|
|
56
|
+
sock: string;
|
|
57
|
+
pid: number;
|
|
58
|
+
};
|
|
47
59
|
}
|
|
48
60
|
/** One key / control action against {@link SessionPickerState}. */
|
|
49
61
|
export type SessionPickerAction = {
|
|
@@ -89,6 +101,16 @@ export type SessionPickerStep = {
|
|
|
89
101
|
export declare function sessionSearchHaystack(session: ResumableSession): string;
|
|
90
102
|
/** Whether one session matches a whitespace-separated query (every token). */
|
|
91
103
|
export declare function sessionMatchesQuery(session: ResumableSession, query: string): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Whether a live Host is holding this session in a window *right now*.
|
|
106
|
+
*
|
|
107
|
+
* The lock's `state` is written by the Host: `attached` while a display relay is
|
|
108
|
+
* connected, `paused` / `running-detached` once the window is gone (a dropped
|
|
109
|
+
* link, a closed window) and the Host stayed behind. Only an explicit
|
|
110
|
+
* `attached` counts — a lock from before the field existed has no state, and
|
|
111
|
+
* treating "unknown" as "in use" would block a resume that used to work.
|
|
112
|
+
*/
|
|
113
|
+
export declare function sessionAttachedElsewhere(session: ResumableSession): boolean;
|
|
92
114
|
/** Sessions still visible under the current filter, in list order. */
|
|
93
115
|
export declare function filterResumableSessions(sessions: readonly ResumableSession[], query: string): ResumableSession[];
|
|
94
116
|
/** Keep `cursor` inside `[0, total)`. Empty lists pin to 0. */
|
package/lib/types/platform.d.ts
CHANGED
|
@@ -213,3 +213,25 @@ export declare function restrictPathToUserSync(path: string, options?: {
|
|
|
213
213
|
mode: number;
|
|
214
214
|
directory?: boolean;
|
|
215
215
|
} & RestrictDeps): boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Whether the terminal in front of us can paint UTF-8 at all.
|
|
218
|
+
*
|
|
219
|
+
* The chrome this TUI draws — rules, status dots, the warning glyph — is
|
|
220
|
+
* Unicode. A console whose output code page is not UTF-8 (a legacy Windows
|
|
221
|
+
* console left on CP936 or CP437) and a POSIX locale that is not UTF-8 both
|
|
222
|
+
* render those bytes as mojibake, which is worse than plain ASCII: the frame
|
|
223
|
+
* still has to line up. This answers only "can it decode UTF-8", never "which
|
|
224
|
+
* terminal", so the capability table stays where it is.
|
|
225
|
+
*
|
|
226
|
+
* The decision is read from the environment, which is all a portable test can
|
|
227
|
+
* assert: `DSH_TUI_ASCII=1` forces the fallback and `=0` forbids it; otherwise
|
|
228
|
+
* a locale naming a non-UTF-8 charset (or the bare `C` / `POSIX`) opts in,
|
|
229
|
+
* and so does a Windows console whose output code page is recorded as anything
|
|
230
|
+
* but 65001. An unset locale is left alone — a UTF-8 SSH session often exports
|
|
231
|
+
* nothing, and guessing "ASCII" there would strip the chrome from the audience
|
|
232
|
+
* this TUI is built for.
|
|
233
|
+
* @param env - the environment to read (tests pass their own).
|
|
234
|
+
* @param platform - the platform the decision is for.
|
|
235
|
+
* @returns true when chrome must be drawn in ASCII.
|
|
236
|
+
*/
|
|
237
|
+
export declare function asciiFallbackEnabled(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* STORE accepts additive rows with plugin-owned ids and no `@deepseek-ai/*`
|
|
8
8
|
* module names. The profile's user layer is the supported home for the row, so
|
|
9
9
|
* both the install script and the running TUI write the same block here — the
|
|
10
|
-
* TUI needs it because `dsh plugin add
|
|
11
|
-
*
|
|
10
|
+
* TUI needs it because the in-app update (`dsh plugin add`, at whatever version)
|
|
11
|
+
* never runs `scripts/`, which npm installs do not ship.
|
|
12
12
|
*
|
|
13
13
|
* The roster is not cosmetic: without it `/mode` cannot switch, and the tools
|
|
14
14
|
* the shipped presets own (`ask_user_question`, `present`, PTC's presentation)
|
|
@@ -42,11 +42,15 @@ export declare const ROSTER_ROWS: readonly RosterRow[];
|
|
|
42
42
|
* per-session declarations a surface mounts (`@deepseek-ai/dsh-agent-preset`
|
|
43
43
|
* rows over the `agent-preset-registry` service), and `dsh-base` keeps the
|
|
44
44
|
* agent-plane rows enabled for the TUI, which is single-session and composes
|
|
45
|
-
* its agent process-wide.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
45
|
+
* its agent process-wide. Beyond the base, upstream's standard preset declares a
|
|
46
|
+
* persona and these two tools. Only the tools are mounted here: a *preset* is an
|
|
47
|
+
* agent scope, but a profile is not one, and `@deepseek-ai/dsh-persona`
|
|
48
|
+
* registers the two prompt sections `dsh-system-prompt` already owns at this
|
|
49
|
+
* layer — the loader rejects the row ("prompt section
|
|
50
|
+
* \"deployment:persona-prefix\" is already registered") and one entry then
|
|
51
|
+
* silently never activates. The deployment persona this line ships with is
|
|
52
|
+
* `dsh-system-prompt`'s own empty default, which is what a profile is meant to
|
|
53
|
+
* use unless it deliberately sets one.
|
|
50
54
|
*/
|
|
51
55
|
export declare const FORMS_ROWS: readonly RosterRow[];
|
|
52
56
|
/** The rows one host line's profile has to mount. */
|
|
@@ -56,11 +60,15 @@ export declare const ALL_ROSTER_ROWS: readonly RosterRow[];
|
|
|
56
60
|
/** The comment header the block introduces itself with, in both writers. */
|
|
57
61
|
export declare const ROSTER_PATCH_HEADER = "# dsh-ssh-tui /mode: the agent-preset roster (standard / minimal / PTC /\n# cordis, plus every preset under $DSH_HOME/.agent-presets) and the two host\n# services the shipped presets need. dsh-base composes no roster in a terminal\n# profile, and a third-party bundle patch may not mount an @deepseek-ai row, so\n# the profile's user layer owns them.\n";
|
|
58
62
|
/** The same header on a host that composes its agent process-wide. */
|
|
59
|
-
export declare const FORMS_PATCH_HEADER = "# dsh-ssh-tui: the agent-plane rows a 0.1.7 terminal profile mounts for itself.\n# That line composes the agent process-wide (presets are a per-session Web\n# feature now), and dsh-base already carries
|
|
63
|
+
export declare const FORMS_PATCH_HEADER = "# dsh-ssh-tui: the agent-plane rows a 0.1.7 terminal profile mounts for itself.\n# That line composes the agent process-wide (presets are a per-session Web\n# feature now), and dsh-base already carries the rest of what the standard\n# preset declares: dsh-system-prompt owns the persona sections at this layer, so\n# only the two tools are left to mount. @deepseek-ai/dsh-persona is deliberately\n# absent \u2014 mounting it here collides with those sections and never activates.\n";
|
|
60
64
|
/** The header for one host line. */
|
|
61
65
|
export declare function rosterPatchHeader(generation: HostGeneration): string;
|
|
62
|
-
/**
|
|
63
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The entries one host line's rows need: a single `- insert:` list.
|
|
68
|
+
* @param rows - the rows to write.
|
|
69
|
+
* @returns the YAML text, ending in a newline.
|
|
70
|
+
*/
|
|
71
|
+
export declare function rosterEntriesText(rows: readonly RosterRow[]): string;
|
|
64
72
|
/**
|
|
65
73
|
* The exact profile patch block that mounts the roster and the two host
|
|
66
74
|
* services the shipped presets need. `scripts/ensure-profile-rows.sh` carries
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a question does while nobody is attached to answer it.
|
|
3
|
+
*
|
|
4
|
+
* A dropped SSH link leaves the question queued in memory, and the only trace of
|
|
5
|
+
* it used to be a transcript line written *after* the user reconnected — which is
|
|
6
|
+
* how a turn spent waiting for an answer went unnoticed until then. Two signals
|
|
7
|
+
* exist before that:
|
|
8
|
+
*
|
|
9
|
+
* - a marker file under `$DSH_HOME/tui-socks/`, so logging back into the jump
|
|
10
|
+
* host shows a session waiting on an answer before the TUI is even open;
|
|
11
|
+
* - one command the user configured, run once, carrying the question in its
|
|
12
|
+
* environment. Mail and WeCom are presets of that command rather than clients
|
|
13
|
+
* of their own: a jump host has whichever of them it already has, and neither
|
|
14
|
+
* belongs in this plugin's dependency tree.
|
|
15
|
+
* @module dsh-ssh-tui/question-wait
|
|
16
|
+
*/
|
|
17
|
+
import { spawn } from 'node:child_process';
|
|
18
|
+
/** One question waiting on a person who is not attached. */
|
|
19
|
+
export interface WaitingQuestion {
|
|
20
|
+
/** The session the question belongs to. */
|
|
21
|
+
sessionId: string;
|
|
22
|
+
/** How many questions are waiting, this one included. */
|
|
23
|
+
count: number;
|
|
24
|
+
/** The question text, already clipped. */
|
|
25
|
+
question: string;
|
|
26
|
+
/** When the wait started, so a later reader can say how long it has been. */
|
|
27
|
+
since: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The marker for one session.
|
|
31
|
+
*
|
|
32
|
+
* Beside the session's stderr log, under the same digested label, so a long id
|
|
33
|
+
* stays inside a Windows path and two ids that sanitize to the same stem do not
|
|
34
|
+
* share a file. `.waiting` rather than a dotfile: the point is that `ls` shows it.
|
|
35
|
+
* @param sessionId - the session that is waiting.
|
|
36
|
+
* @param dshHome - the harness home; defaults to the process one.
|
|
37
|
+
* @returns the marker path.
|
|
38
|
+
*/
|
|
39
|
+
export declare function waitingMarkerPath(sessionId: string, dshHome?: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Write the marker, replacing one already there.
|
|
42
|
+
*
|
|
43
|
+
* The question text is the only sensitive part, so the file is owner-only. A
|
|
44
|
+
* failure to write is reported as `false` and never thrown: the question is
|
|
45
|
+
* still queued, and a marker is not worth failing it over.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately a plain `writeFile`, not a staged sibling renamed into place.
|
|
48
|
+
* The rename is atomic on POSIX, but Windows refuses to replace a file another
|
|
49
|
+
* process has open — and the whole point of this marker is that something else
|
|
50
|
+
* polls it (a jump-host `cat` in a loop), so the "atomic" version stops updating
|
|
51
|
+
* exactly while it is being read. The cost of the plain write is a window
|
|
52
|
+
* between the create and the first byte where a reader sees an empty file; the
|
|
53
|
+
* marker is rewritten on every change, so that window is what a consumer has to
|
|
54
|
+
* tolerate rather than something the writer can hide.
|
|
55
|
+
* @param question - the question that just started waiting.
|
|
56
|
+
* @param dshHome - the harness home; defaults to the process one.
|
|
57
|
+
* @returns whether the marker is on disk.
|
|
58
|
+
*/
|
|
59
|
+
export declare function writeWaitingMarker(question: WaitingQuestion, dshHome?: string): Promise<boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* Remove the marker. Missing is fine: the question may have been answered, or
|
|
62
|
+
* the marker may never have been written.
|
|
63
|
+
* @param sessionId - the session whose wait ended.
|
|
64
|
+
* @param dshHome - the harness home; defaults to the process one.
|
|
65
|
+
*/
|
|
66
|
+
export declare function clearWaitingMarker(sessionId: string, dshHome?: string): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* The command to run when a question starts waiting, if the user configured one.
|
|
69
|
+
*
|
|
70
|
+
* The environment wins over settings, the same order `/disconnect` uses, so a
|
|
71
|
+
* one-off invocation can point somewhere else without editing the profile.
|
|
72
|
+
* Empty and whitespace are "not configured".
|
|
73
|
+
* @param env - the process environment.
|
|
74
|
+
* @param saved - the `ssh-tui.notify` value, when settings has one.
|
|
75
|
+
* @returns the command, or undefined when notifications are off.
|
|
76
|
+
*/
|
|
77
|
+
export declare function notifyCommand(env?: NodeJS.ProcessEnv, saved?: string): string | undefined;
|
|
78
|
+
/** What one notification knows about the question it is announcing. */
|
|
79
|
+
export interface NotifyContext {
|
|
80
|
+
sessionId: string;
|
|
81
|
+
/** How many questions are waiting. */
|
|
82
|
+
count: number;
|
|
83
|
+
/** The question text. */
|
|
84
|
+
question: string;
|
|
85
|
+
/** Milliseconds the question has already been waiting. */
|
|
86
|
+
waitedMs: number;
|
|
87
|
+
/** A command that reattaches to the session. */
|
|
88
|
+
resumeCommand: string;
|
|
89
|
+
/** SMTP username, present only for an authenticated server. */
|
|
90
|
+
smtpUser?: string;
|
|
91
|
+
/** SMTP password, present only for an authenticated server. */
|
|
92
|
+
smtpPassword?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* One notify target, as `/notify` parsed it.
|
|
96
|
+
*
|
|
97
|
+
* `off` clears the command. `mail` is the local mailer. `smtp` is a submission
|
|
98
|
+
* to a server, authenticated when a password is given. `local` is the machine's
|
|
99
|
+
* own SMTP listener, which needs no account.
|
|
100
|
+
*/
|
|
101
|
+
export type NotifyTarget = {
|
|
102
|
+
kind: 'off';
|
|
103
|
+
} | {
|
|
104
|
+
kind: 'mail';
|
|
105
|
+
address: string;
|
|
106
|
+
} | {
|
|
107
|
+
kind: 'smtp';
|
|
108
|
+
host: string;
|
|
109
|
+
port: number;
|
|
110
|
+
from: string;
|
|
111
|
+
to: string;
|
|
112
|
+
user?: string;
|
|
113
|
+
password?: string;
|
|
114
|
+
} | {
|
|
115
|
+
kind: 'local';
|
|
116
|
+
port: number;
|
|
117
|
+
from: string;
|
|
118
|
+
to: string;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Parse one `/notify` argument into a target.
|
|
122
|
+
*
|
|
123
|
+
* Four shapes, deliberately small:
|
|
124
|
+
*
|
|
125
|
+
* - empty, `off`, `none` — turn it off;
|
|
126
|
+
* - `mail you@example.com` — the local mailer;
|
|
127
|
+
* - `smtp host[:port] from to [user [password]]` — an SMTP server, port 587
|
|
128
|
+
* when omitted, authenticated only when a user is given;
|
|
129
|
+
* - `local [port] from to` — the machine's own mailer on 25, or another port.
|
|
130
|
+
*
|
|
131
|
+
* @param raw - everything after `/notify`.
|
|
132
|
+
* @returns the target, or undefined when the shape is not one of those.
|
|
133
|
+
*/
|
|
134
|
+
export declare function parseNotifyTarget(raw: string): NotifyTarget | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* The shell command one target runs.
|
|
137
|
+
*
|
|
138
|
+
* SMTP and the local mailer both go through Python's stdlib `smtplib`, which
|
|
139
|
+
* is present on every jump host this plugin runs on (Node itself is). The
|
|
140
|
+
* password, when there is one, is read from `DSH_TUI_NOTIFY_SMTP_PASSWORD` at
|
|
141
|
+
* send time rather than written into the command, so it never lands in the
|
|
142
|
+
* settings file or the process list. The message text arrives on stdin.
|
|
143
|
+
* @param target - a parsed target other than `off`.
|
|
144
|
+
* @returns the command `/notify` stores.
|
|
145
|
+
*/
|
|
146
|
+
/**
|
|
147
|
+
* What `/notify` confirms back: the target in the user's own words, without the
|
|
148
|
+
* password.
|
|
149
|
+
* @param target - a parsed target other than `off`.
|
|
150
|
+
* @returns a short label.
|
|
151
|
+
*/
|
|
152
|
+
export declare function notifyTargetLabel(target: Exclude<NotifyTarget, {
|
|
153
|
+
kind: 'off';
|
|
154
|
+
}>): string;
|
|
155
|
+
export declare function notifyTargetCommand(target: Exclude<NotifyTarget, {
|
|
156
|
+
kind: 'off';
|
|
157
|
+
}>): string;
|
|
158
|
+
/**
|
|
159
|
+
* A mail command for an address, using whichever sender the host has.
|
|
160
|
+
*
|
|
161
|
+
* `mail` (or `mailx`) is on most jump hosts and speaks SMTP for them; no
|
|
162
|
+
* provider, token, or library joins this plugin. The body arrives on stdin, so
|
|
163
|
+
* the question text is never a shell argument.
|
|
164
|
+
* @param address - where the mail goes.
|
|
165
|
+
* @returns the command.
|
|
166
|
+
*/
|
|
167
|
+
export declare function mailNotifyCommand(address: string): string;
|
|
168
|
+
/**
|
|
169
|
+
* A WeCom group-robot command for one webhook key.
|
|
170
|
+
*
|
|
171
|
+
* The payload is assembled by the caller and sent with `curl`, which a jump
|
|
172
|
+
* host already has. The key is the only secret, and it stays in the command the
|
|
173
|
+
* user configured rather than in this repo.
|
|
174
|
+
* @param key - the robot's webhook key.
|
|
175
|
+
* @returns the command.
|
|
176
|
+
*/
|
|
177
|
+
export declare function wecomNotifyCommand(key: string): string;
|
|
178
|
+
/**
|
|
179
|
+
* The text a notification shows. Short on purpose: a mail subject and a WeCom
|
|
180
|
+
* message both get truncated by their carrier, so the resume command has to fit.
|
|
181
|
+
* @param context - the question being announced.
|
|
182
|
+
* @returns the message, one fact per line.
|
|
183
|
+
*/
|
|
184
|
+
export declare function notifyMessage(context: NotifyContext): string;
|
|
185
|
+
/**
|
|
186
|
+
* The body a WeCom robot expects: a text message whose content is the notice.
|
|
187
|
+
* @param context - the question being announced.
|
|
188
|
+
* @returns JSON.
|
|
189
|
+
*/
|
|
190
|
+
export declare function wecomPayload(context: NotifyContext): string;
|
|
191
|
+
/**
|
|
192
|
+
* Run the user's notify command once.
|
|
193
|
+
*
|
|
194
|
+
* The message goes to stdin and the facts go to the environment, so a command
|
|
195
|
+
* can be either `mail` (reads stdin) or anything that reads the variables. The
|
|
196
|
+
* command runs through a shell because it is a shell command by nature — the
|
|
197
|
+
* user wrote it as one — and the platform's own shell, so a Windows host uses
|
|
198
|
+
* `cmd`. Nothing it prints or fails with comes back: a notification that errors
|
|
199
|
+
* must not surface in the transcript or delay the question.
|
|
200
|
+
* @param command - the configured command.
|
|
201
|
+
* @param context - the question being announced.
|
|
202
|
+
* @param deps - injectable process spawn and clock, for tests.
|
|
203
|
+
* @returns once the command has exited, timed out, or failed to start.
|
|
204
|
+
*/
|
|
205
|
+
export declare function runNotify(command: string, context: NotifyContext, deps?: {
|
|
206
|
+
spawnFn?: typeof spawn;
|
|
207
|
+
platform?: NodeJS.Platform;
|
|
208
|
+
timeoutMs?: number;
|
|
209
|
+
}): Promise<void>;
|
|
210
|
+
/**
|
|
211
|
+
* The shell that runs a notify command, and the flag that precedes it.
|
|
212
|
+
*
|
|
213
|
+
* A jump host means `sh -c`. Windows has no `sh` on PATH as a rule, so there
|
|
214
|
+
* the command runs under `cmd /c` and the user writes it for `cmd`.
|
|
215
|
+
* @param platform - injectable so the Windows shape is testable from POSIX.
|
|
216
|
+
* @returns the executable and its command flag.
|
|
217
|
+
*/
|
|
218
|
+
export declare function notifyShell(platform?: NodeJS.Platform): {
|
|
219
|
+
command: string;
|
|
220
|
+
flag: string;
|
|
221
|
+
};
|