dsh-ssh-tui 0.7.4 → 0.8.1
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 +37 -0
- package/README.md +436 -572
- package/docs/display-mode.md +122 -0
- package/docs/remote-ops.md +104 -0
- package/docs/terminals.md +53 -0
- package/lib/attach.js +4 -4
- package/lib/attach.js.map +1 -1
- package/lib/auth-failure.js +128 -0
- package/lib/auth-failure.js.map +1 -0
- package/lib/commands.js +3 -0
- package/lib/commands.js.map +1 -1
- package/lib/dialogs.js +43 -0
- package/lib/dialogs.js.map +1 -1
- package/lib/display-mode.js +147 -0
- package/lib/display-mode.js.map +1 -0
- package/lib/display-sock.js +361 -21
- package/lib/display-sock.js.map +1 -1
- package/lib/footer.js +6 -9
- package/lib/footer.js.map +1 -1
- package/lib/glyph-measure.js +92 -0
- package/lib/glyph-measure.js.map +1 -0
- package/lib/i18n/en.js +27 -2
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/zh.js +27 -2
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +74 -5
- package/lib/index.js.map +1 -1
- package/lib/paint.js +22 -9
- package/lib/paint.js.map +1 -1
- package/lib/picker.js +14 -13
- package/lib/picker.js.map +1 -1
- package/lib/plan.js +11 -11
- package/lib/plan.js.map +1 -1
- package/lib/platform.js +96 -0
- package/lib/platform.js.map +1 -1
- package/lib/session-blank.js +81 -0
- package/lib/session-blank.js.map +1 -0
- package/lib/session-list.js +102 -81
- package/lib/session-list.js.map +1 -1
- package/lib/startup.js +7 -0
- package/lib/startup.js.map +1 -1
- package/lib/subagent-model.js +8 -7
- package/lib/subagent-model.js.map +1 -1
- package/lib/term-text.js +296 -28
- package/lib/term-text.js.map +1 -1
- package/lib/terminal-input.js +132 -10
- package/lib/terminal-input.js.map +1 -1
- package/lib/theme.js +318 -0
- package/lib/theme.js.map +1 -0
- package/lib/tool-present.js +11 -9
- package/lib/tool-present.js.map +1 -1
- package/lib/tui.js +535 -37
- package/lib/tui.js.map +1 -1
- package/lib/types/attach.d.ts +6 -2
- package/lib/types/auth-failure.d.ts +78 -0
- package/lib/types/commands.d.ts +9 -0
- package/lib/types/dialogs.d.ts +36 -0
- package/lib/types/display-mode.d.ts +99 -0
- package/lib/types/display-sock.d.ts +75 -0
- package/lib/types/footer.d.ts +1 -1
- package/lib/types/glyph-measure.d.ts +41 -0
- package/lib/types/index.d.ts +20 -0
- package/lib/types/plan.d.ts +5 -2
- package/lib/types/platform.d.ts +83 -0
- package/lib/types/session-blank.d.ts +51 -0
- package/lib/types/session-list.d.ts +34 -0
- package/lib/types/startup.d.ts +6 -0
- package/lib/types/subagent-model.d.ts +7 -6
- package/lib/types/term-text.d.ts +55 -23
- package/lib/types/terminal-input.d.ts +37 -0
- package/lib/types/theme.d.ts +109 -0
- package/lib/types/tool-present.d.ts +2 -2
- package/lib/types/tui.d.ts +121 -1
- package/package.json +92 -93
package/lib/types/attach.d.ts
CHANGED
|
@@ -88,7 +88,9 @@ export interface AttacherDeps {
|
|
|
88
88
|
/** Stop capturing and return the typing ('' when unsupported). */
|
|
89
89
|
endCapture?(): string;
|
|
90
90
|
inspectLiveHost(sessionId: string): Promise<LiveHost | undefined>;
|
|
91
|
-
spawnHost(sessionId: string
|
|
91
|
+
spawnHost(sessionId: string, options?: {
|
|
92
|
+
ambiguousWide?: boolean;
|
|
93
|
+
}): SpawnedDisplayHost;
|
|
92
94
|
waitForDisplaySock(spawned: SpawnedDisplayHost): Promise<void>;
|
|
93
95
|
/**
|
|
94
96
|
* One already-formatted status line for stderr. `transient` marks a line the
|
|
@@ -136,7 +138,9 @@ export interface Attacher {
|
|
|
136
138
|
seed?: string;
|
|
137
139
|
}): Promise<void>;
|
|
138
140
|
/** Attach to a live Host on this session, or spawn a fresh one and attach. */
|
|
139
|
-
attachOrSpawn(sessionId: string, recover?: boolean
|
|
141
|
+
attachOrSpawn(sessionId: string, recover?: boolean, options?: {
|
|
142
|
+
ambiguousWide?: boolean;
|
|
143
|
+
}): Promise<void>;
|
|
140
144
|
/** Recoveries recorded in the current burst window (tests). */
|
|
141
145
|
readonly recoveries: number;
|
|
142
146
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two failure classes that look alike in the transcript and need different fixes.
|
|
3
|
+
*
|
|
4
|
+
* 1. **Authentication** — `code: "AUTH"`, and the message says which side said
|
|
5
|
+
* no (below).
|
|
6
|
+
* 2. **Reasoning replay** — a 400 from a thinking-mode gateway that wants the
|
|
7
|
+
* previous turn's reasoning passed back; the harness drops it when it rebuilds
|
|
8
|
+
* a long multi-turn history. Reported upstream (deepseek-harness #1780, #231),
|
|
9
|
+
* so the advice is a workaround, not a fix.
|
|
10
|
+
*
|
|
11
|
+
* Where an authentication failure came from.
|
|
12
|
+
*
|
|
13
|
+
* A turn can die with `code: "AUTH"` for two very different reasons, and the
|
|
14
|
+
* difference decides what the reader should do:
|
|
15
|
+
*
|
|
16
|
+
* - **provider**: the request left the machine and the provider (or something
|
|
17
|
+
* between it and the model) rejected it. The local credential is present, so
|
|
18
|
+
* changing it is the wrong move — the usual causes are a transient upstream
|
|
19
|
+
* failure, a plan/quota limit, or a model the account may not call. Retrying
|
|
20
|
+
* is reasonable.
|
|
21
|
+
* - **local**: nothing usable is configured on this machine, so every request
|
|
22
|
+
* would fail the same way. Retrying changes nothing; the key has to be set.
|
|
23
|
+
*
|
|
24
|
+
* The host reports both as `AUTH`, and the only place the difference survives is
|
|
25
|
+
* the message body — which is why this is a text classifier and why it is kept
|
|
26
|
+
* pure: the caller resolves the actual credential and decides what to say.
|
|
27
|
+
*
|
|
28
|
+
* Observed bodies (2026-09-29, command-code):
|
|
29
|
+
* provider → `OpenAI API error (403): {"message":"Authentication failed.
|
|
30
|
+
* Please check your credentials.","type":"permission_error"}`
|
|
31
|
+
* provider → `403 {"type":"error","error":{"type":"permission_error",
|
|
32
|
+
* "message":"MODEL_NOT_IN_PLAN: …"}}`
|
|
33
|
+
* local → `dsh-llm: no API key for provider "x" (set X_API_KEY)`
|
|
34
|
+
* A *missing* credential on that gateway answered with a 401 and its own
|
|
35
|
+
* envelope (`{"success":false,"error":{"code":"UNAUTHORIZED"}}`), which is why
|
|
36
|
+
* an HTTP status alone cannot classify this: 401 and 403 both appear on the
|
|
37
|
+
* provider side.
|
|
38
|
+
*
|
|
39
|
+
* @module dsh-ssh-tui/auth-failure
|
|
40
|
+
*/
|
|
41
|
+
/** Which side rejected the request. */
|
|
42
|
+
export type AuthFailureOrigin = 'provider' | 'local';
|
|
43
|
+
export interface AuthFailure {
|
|
44
|
+
origin: AuthFailureOrigin;
|
|
45
|
+
/** The HTTP status, when the message carried one. */
|
|
46
|
+
status?: number;
|
|
47
|
+
}
|
|
48
|
+
/** The HTTP status inside a host-formatted failure, if there is one. */
|
|
49
|
+
export declare function statusOf(message: string): number | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Classify one failure message.
|
|
52
|
+
* @param message - the failure text the host attached to the turn.
|
|
53
|
+
* @returns the origin (and status when known), or undefined when the message
|
|
54
|
+
* says nothing about authentication — an unrelated failure keeps its own row
|
|
55
|
+
* and gets no advice.
|
|
56
|
+
*/
|
|
57
|
+
export declare function classifyAuthFailure(message: string): AuthFailure | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* A thinking-mode gateway refusing a follow-up because the reasoning is missing.
|
|
60
|
+
*
|
|
61
|
+
* Seen on the 0.2.0-rc line with the command-code route and `effort: max`
|
|
62
|
+
* (five times in one long session, always on a *continuation*, never on the
|
|
63
|
+
* first request of a turn):
|
|
64
|
+
*
|
|
65
|
+
* {"type":"invalid_request_error","code":"invalid_request_error",
|
|
66
|
+
* "message":"The `reasoning_text` in the thinking mode must be passed back
|
|
67
|
+
* to the API."}
|
|
68
|
+
*
|
|
69
|
+
* Both a live probe and the upstream reports agree on the shape: the client is
|
|
70
|
+
* expected to send the previous assistant turn's reasoning items back, and the
|
|
71
|
+
* harness does not do that once a session grows long. Nothing the reader can do
|
|
72
|
+
* fixes the client, but two workarounds exist — no thinking on that route, or
|
|
73
|
+
* the same gateway's Anthropic-flavoured route, which carries signed thinking
|
|
74
|
+
* blocks the harness does replay.
|
|
75
|
+
* @param message - the failure text the host attached to the turn.
|
|
76
|
+
* @returns true when this is that failure.
|
|
77
|
+
*/
|
|
78
|
+
export declare function isReasoningReplayFailure(message: string): boolean;
|
package/lib/types/commands.d.ts
CHANGED
|
@@ -39,6 +39,15 @@ export declare const LOCAL_COMMANDS: readonly [{
|
|
|
39
39
|
}, {
|
|
40
40
|
readonly name: "disconnect";
|
|
41
41
|
readonly key: "cmd.disconnect";
|
|
42
|
+
}, {
|
|
43
|
+
readonly name: "retryauth";
|
|
44
|
+
readonly key: "cmd.retryauth";
|
|
45
|
+
}, {
|
|
46
|
+
readonly name: "theme";
|
|
47
|
+
readonly key: "cmd.theme";
|
|
48
|
+
}, {
|
|
49
|
+
readonly name: "cleanup";
|
|
50
|
+
readonly key: "cmd.cleanup";
|
|
42
51
|
}, {
|
|
43
52
|
readonly name: "notify";
|
|
44
53
|
readonly key: "cmd.notify";
|
package/lib/types/dialogs.d.ts
CHANGED
|
@@ -16,6 +16,15 @@ export interface ConfirmDialog {
|
|
|
16
16
|
resolve(value: 'y' | 'n' | 'cancel'): void;
|
|
17
17
|
}
|
|
18
18
|
export interface QuestionDialog {
|
|
19
|
+
/**
|
|
20
|
+
* Called whenever the highlighted option moves.
|
|
21
|
+
*
|
|
22
|
+
* The theme picker uses it to paint the candidate palette immediately, so the
|
|
23
|
+
* choice is a preview rather than a list of names — the reader sees the
|
|
24
|
+
* transcript in the palette before committing to it. Nothing else sets it, and
|
|
25
|
+
* a dialog without it behaves exactly as before.
|
|
26
|
+
*/
|
|
27
|
+
onCursor?: (cursor: number) => void;
|
|
19
28
|
kind: 'questions';
|
|
20
29
|
question: AskUserQuestionItem;
|
|
21
30
|
index: number;
|
|
@@ -103,6 +112,33 @@ export declare function applyQuestionFilter(dialog: QuestionDialog): void;
|
|
|
103
112
|
export declare function clearQuestionFilter(dialog: QuestionDialog): void;
|
|
104
113
|
/** Move the question highlight, clamped to its options. Returns false if not a question dialog. */
|
|
105
114
|
export declare function moveQuestionCursor(dialog: QuestionDialog, delta: number): boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Which options a fresh question dialog starts with.
|
|
117
|
+
*
|
|
118
|
+
* The rule that caused a real mis-answer: a **multi-select** list used to inherit
|
|
119
|
+
* the single-select default of "the first option is already chosen", so a reader
|
|
120
|
+
* who moved the highlight to the row they wanted and pressed Enter answered with
|
|
121
|
+
* the first row instead — the `●` never left it. A multi-select starts empty
|
|
122
|
+
* unless the caller asks for a specific preselection; `questionSubmit` then falls
|
|
123
|
+
* back to the highlighted row, which is what pressing Enter means.
|
|
124
|
+
* @param question - the question being asked.
|
|
125
|
+
* @param preselected - an index the caller wants chosen up front, if any.
|
|
126
|
+
* @returns the indexes to select initially.
|
|
127
|
+
*/
|
|
128
|
+
export declare function initialQuestionSelection(question: AskUserQuestionItem, preselected?: number): number[];
|
|
129
|
+
/**
|
|
130
|
+
* The circle drawn beside one option.
|
|
131
|
+
*
|
|
132
|
+
* `●` marks **what Enter will submit**, which is why it sits on the highlighted
|
|
133
|
+
* row while nothing is ticked — the mark follows the cursor, so what the reader
|
|
134
|
+
* sees is what the answer will be. A ticked row in a multi-select list carries
|
|
135
|
+
* `✓` instead: several rows can be chosen at once, and the circle cannot be in
|
|
136
|
+
* two places without lying about one of them.
|
|
137
|
+
* @param dialog - the open question dialog.
|
|
138
|
+
* @param index - the option index being drawn.
|
|
139
|
+
* @returns the marker for that row.
|
|
140
|
+
*/
|
|
141
|
+
export declare function questionOptionMarker(dialog: QuestionDialog, index: number): '●' | '○' | '✓';
|
|
106
142
|
/** Select option `index`: toggles in a multi-select list, replaces otherwise. */
|
|
107
143
|
export declare function selectQuestionOption(dialog: QuestionDialog, index: number): void;
|
|
108
144
|
/** Handle a hotkey: select its option and report whether it applied. */
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the TUI's bytes go when the parent is not a terminal.
|
|
3
|
+
*
|
|
4
|
+
* The plugin paints through a display relay, and a relay normally *is* a
|
|
5
|
+
* terminal: it writes ANSI to stdout, listens for `resize`, and puts stdin in raw
|
|
6
|
+
* mode. A GUI, a browser terminal or a test harness has none of those, but it can
|
|
7
|
+
* provide the same three things over pipes — bytes in, bytes out, and a size it
|
|
8
|
+
* reports as a protocol frame. This module is the switch that says "the parent is
|
|
9
|
+
* such a relay, do not require a TTY".
|
|
10
|
+
*
|
|
11
|
+
* Two entries, one mechanism and one sugar (as chosen when the mode was
|
|
12
|
+
* designed):
|
|
13
|
+
*
|
|
14
|
+
* - `DSH_TUI_DISPLAY=stdio` in the environment — the mechanism. Setting it *is*
|
|
15
|
+
* the parent's statement that it is a relay, so it is also what the no-TTY
|
|
16
|
+
* guard consults.
|
|
17
|
+
* - `--display stdio` on the command line — the sugar, declared in the profile's
|
|
18
|
+
* own grammar so it shows up in `--help` and a typo is an error rather than a
|
|
19
|
+
* silently inert plugin.
|
|
20
|
+
*
|
|
21
|
+
* @module dsh-ssh-tui/display-mode
|
|
22
|
+
*/
|
|
23
|
+
/** The environment variable that selects the display mode. */
|
|
24
|
+
export declare const DISPLAY_MODE_ENV = "DSH_TUI_DISPLAY";
|
|
25
|
+
/**
|
|
26
|
+
* `tty` is the default: paint to the process's own terminal. `stdio` means the
|
|
27
|
+
* parent speaks the display protocol on stdin/stdout.
|
|
28
|
+
*/
|
|
29
|
+
export type DisplayMode = 'tty' | 'stdio';
|
|
30
|
+
/** A mode request, and what to say when the value was not one of the two. */
|
|
31
|
+
export interface DisplayRequest {
|
|
32
|
+
mode?: DisplayMode;
|
|
33
|
+
/** The unrecognized value, when one was given. */
|
|
34
|
+
invalid?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse one mode word.
|
|
38
|
+
* @param value - a value from the environment or a flag.
|
|
39
|
+
* @returns the mode, or undefined for anything else (the caller reports it).
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseDisplayMode(value: string | undefined): DisplayMode | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* The `--display <mode>` value in an argument vector.
|
|
44
|
+
*
|
|
45
|
+
* Accepts both spellings a person may type (`--display stdio`,
|
|
46
|
+
* `--display=stdio`) and only ever reads the pair it owns — everything else is
|
|
47
|
+
* left where it is, because the same vector is parsed by the profile's own
|
|
48
|
+
* grammar and by the host.
|
|
49
|
+
* @param argv - an argument vector (defaults to this process's).
|
|
50
|
+
* @returns the value found, if any.
|
|
51
|
+
*/
|
|
52
|
+
export declare function displayModeFromArgv(argv?: readonly string[]): string | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the mode request from the environment and the command line.
|
|
55
|
+
*
|
|
56
|
+
* The environment wins when both are present: it is the mechanism, and a parent
|
|
57
|
+
* that sets it is describing the channel it is about to speak on, whereas a flag
|
|
58
|
+
* is a convenience the reader may have left in a wrapper script.
|
|
59
|
+
* @param env - the environment to read.
|
|
60
|
+
* @param argv - the argument vector to read.
|
|
61
|
+
* @returns the requested mode, plus the offending value when one was useless.
|
|
62
|
+
*/
|
|
63
|
+
export declare function requestedDisplayMode(env?: NodeJS.ProcessEnv, argv?: readonly string[]): DisplayRequest;
|
|
64
|
+
/**
|
|
65
|
+
* A window-size report: `CSI 8 ; rows ; cols t`.
|
|
66
|
+
*
|
|
67
|
+
* This is the sequence a terminal sends when it is asked for its size, and it is
|
|
68
|
+
* also how a pipe parent says "the panel is now this big". A pipe has no
|
|
69
|
+
* `resize` event and no SIGWINCH, so without this the embedder's only way to
|
|
70
|
+
* resize the TUI would be to restart it.
|
|
71
|
+
*/
|
|
72
|
+
export declare const WINDOW_SIZE_REPORT: RegExp;
|
|
73
|
+
/** A size a parent declared: columns first, the way the rest of the code wants it. */
|
|
74
|
+
export interface ParentResize {
|
|
75
|
+
columns: number;
|
|
76
|
+
rows: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Split a parent's input into keystrokes and size reports.
|
|
80
|
+
*
|
|
81
|
+
* A report can arrive split across two reads (`ESC [ 8 ; 2` then `4 ; 80 t`),
|
|
82
|
+
* and forwarding half of it as typing would send escape bytes to the model's
|
|
83
|
+
* prompt. The tail is held until the next read, exactly like the cursor-reply
|
|
84
|
+
* filter holds a half-arrived reply — with the same deadline that filter gets:
|
|
85
|
+
* a lone `ESC` matches the prefix, and the byte after it may never come, so a
|
|
86
|
+
* caller that forwards typing **must** release a {@link pending} run when its
|
|
87
|
+
* window passes (`flush()`). Waiting for the next read instead swallows the
|
|
88
|
+
* user's Escape key, and delivers it glued to whatever key follows as an Alt
|
|
89
|
+
* chord. `TerminalInputGuard` is the same filter with that timer attached.
|
|
90
|
+
* @returns a filter for one input stream.
|
|
91
|
+
*/
|
|
92
|
+
export declare function createParentResizeFilter(): {
|
|
93
|
+
push(text: string): {
|
|
94
|
+
forward: string;
|
|
95
|
+
sizes: ParentResize[];
|
|
96
|
+
};
|
|
97
|
+
flush(): string;
|
|
98
|
+
readonly pending: boolean;
|
|
99
|
+
};
|
|
@@ -13,6 +13,15 @@ export declare const FRAME_PROBE_REPLY = 9;
|
|
|
13
13
|
export declare const FRAME_QUERY = 10;
|
|
14
14
|
/** Host → prober: the answer to {@link FRAME_QUERY}. */
|
|
15
15
|
export declare const FRAME_QUERY_REPLY = 11;
|
|
16
|
+
/**
|
|
17
|
+
* Relay → host: what this terminal does with the ambiguous glyphs.
|
|
18
|
+
*
|
|
19
|
+
* Payload: one byte, bit 0 = the terminal advances two cells, bit 1 = the second
|
|
20
|
+
* cell has to be reserved by us with a space. Sent right after HELLO, because the
|
|
21
|
+
* Host decides every width in the session and it is the relay — not the Host —
|
|
22
|
+
* that owns the terminal to measure on.
|
|
23
|
+
*/
|
|
24
|
+
export declare const FRAME_METRICS = 12;
|
|
16
25
|
/**
|
|
17
26
|
* What a relay's own terminal round trip said.
|
|
18
27
|
*
|
|
@@ -105,6 +114,24 @@ export declare function sessionBootstrapPidPath(sessionId: string, dshHome?: str
|
|
|
105
114
|
/** Pre-digest Host stderr log next to the 0.7.1 socket, when that name differs. */
|
|
106
115
|
export declare function legacySessionErrPath(sessionId: string, dshHome?: string, platform?: NodeJS.Platform): string | undefined;
|
|
107
116
|
export declare function encodeFrame(type: number, payload?: Buffer): Buffer;
|
|
117
|
+
/**
|
|
118
|
+
* The size a relay should report.
|
|
119
|
+
*
|
|
120
|
+
* A terminal knows its own size; a parent that speaks the protocol on pipes is
|
|
121
|
+
* not a terminal, so it declares one through `COLUMNS`/`LINES` — the convention
|
|
122
|
+
* every shell and TUI already shares — and corrects it later with a resize frame
|
|
123
|
+
* if the window changes. Without either, the historical 80×24 stands.
|
|
124
|
+
* @param stdout - the stream that may be a terminal.
|
|
125
|
+
* @param env - the environment to read.
|
|
126
|
+
* @returns the columns and rows to send as the first resize.
|
|
127
|
+
*/
|
|
128
|
+
export declare function relayTerminalSize(stdout?: {
|
|
129
|
+
columns?: number;
|
|
130
|
+
rows?: number;
|
|
131
|
+
}, env?: NodeJS.ProcessEnv): {
|
|
132
|
+
columns: number;
|
|
133
|
+
rows: number;
|
|
134
|
+
};
|
|
108
135
|
export declare function encodeResize(columns: number, rows: number): Buffer;
|
|
109
136
|
export declare function decodeResize(payload: Buffer): {
|
|
110
137
|
columns: number;
|
|
@@ -115,6 +142,16 @@ export declare function decodeRtt(payload: Buffer): number | undefined;
|
|
|
115
142
|
export declare function encodeProbe(): Buffer;
|
|
116
143
|
export declare function encodeProbeReply(verdict: TerminalVerdict): Buffer;
|
|
117
144
|
export declare function decodeProbeReply(payload: Buffer): TerminalVerdict;
|
|
145
|
+
/** Encode {@link FRAME_METRICS}. */
|
|
146
|
+
export declare function encodeMetrics(metrics: {
|
|
147
|
+
wide: boolean;
|
|
148
|
+
reserve: boolean;
|
|
149
|
+
}): Buffer;
|
|
150
|
+
/** Decode {@link FRAME_METRICS}; undefined when the payload is not one byte. */
|
|
151
|
+
export declare function decodeMetrics(payload: Buffer): {
|
|
152
|
+
wide: boolean;
|
|
153
|
+
reserve: boolean;
|
|
154
|
+
} | undefined;
|
|
118
155
|
export declare function encodeQuery(): Buffer;
|
|
119
156
|
export declare function encodeQueryReply(verdict: AttachmentVerdict): Buffer;
|
|
120
157
|
export declare function decodeQueryReply(payload: Buffer): AttachmentVerdict;
|
|
@@ -130,6 +167,11 @@ export interface DisplayHostHandlers {
|
|
|
130
167
|
onStdin(bytes: Buffer): void;
|
|
131
168
|
onResize(columns: number, rows: number): void;
|
|
132
169
|
onRtt?(rttMs: number | undefined): void;
|
|
170
|
+
/** The relay measured this terminal's ambiguous glyphs; adopt the verdict. */
|
|
171
|
+
onMetrics?(metrics: {
|
|
172
|
+
wide: boolean;
|
|
173
|
+
reserve: boolean;
|
|
174
|
+
}): void;
|
|
133
175
|
onDetach(info?: {
|
|
134
176
|
replaced?: boolean;
|
|
135
177
|
}): void;
|
|
@@ -183,6 +225,18 @@ export declare class DisplayHost {
|
|
|
183
225
|
private settleProbe;
|
|
184
226
|
sendStdout(bytes: Buffer | string): boolean;
|
|
185
227
|
sendGoodbye(): void;
|
|
228
|
+
/**
|
|
229
|
+
* Say goodbye and let the frame leave before the socket is reaped.
|
|
230
|
+
*
|
|
231
|
+
* The frame is what tells the launcher to exit instead of reading the close as
|
|
232
|
+
* a crash and re-attaching (`attach.ts`) — the replacement path flushes
|
|
233
|
+
* `FRAME_REPLACED` for exactly the same reason. A write only *queues* on libuv
|
|
234
|
+
* until the peer drains it, and reaping the socket cancels what is still
|
|
235
|
+
* queued: on a busy link, with a starting session still painting into that
|
|
236
|
+
* socket, the goodbye was dropped along with those frames and the launcher
|
|
237
|
+
* re-attached over the user's own `/exit` instead of exiting.
|
|
238
|
+
*/
|
|
239
|
+
private farewellAndReap;
|
|
186
240
|
close(): Promise<void>;
|
|
187
241
|
}
|
|
188
242
|
/**
|
|
@@ -274,8 +328,29 @@ export declare function quietTerminalInput(stdin?: NodeJS.ReadStream): number;
|
|
|
274
328
|
* written into a link that may already be dead.
|
|
275
329
|
*/
|
|
276
330
|
export declare function restoreTerminalInput(stdin?: NodeJS.ReadStream): void;
|
|
331
|
+
/**
|
|
332
|
+
* The environment a detached Host is started with.
|
|
333
|
+
*
|
|
334
|
+
* Notable entries: the marker that tells the child it *is* the Host, and the
|
|
335
|
+
* ambiguous-width decision. The Host paints, but its own stdout is the relay
|
|
336
|
+
* socket, so it cannot look at a TTY to know how wide the window's terminal
|
|
337
|
+
* draws `①` or `—`; this process has that terminal, so it decides and hands the
|
|
338
|
+
* answer over. An explicit value in the environment still wins.
|
|
339
|
+
* @param env - the parent environment (a test passes its own).
|
|
340
|
+
* @param onTerminal - whether *this* process owns a terminal; it is the evidence
|
|
341
|
+
* the width table is chosen from, and a test harness is not a terminal.
|
|
342
|
+
* @returns the child environment.
|
|
343
|
+
*/
|
|
344
|
+
export declare function hostChildEnv(env?: NodeJS.ProcessEnv, onTerminal?: boolean, measuredAmbiguousWide?: boolean): NodeJS.ProcessEnv;
|
|
277
345
|
/** Test seam for {@link spawnDetachedHost}; production passes nothing. */
|
|
278
346
|
export interface SpawnHostOptions {
|
|
347
|
+
/**
|
|
348
|
+
* What the launcher measured for the ambiguous glyphs, when it measured.
|
|
349
|
+
*
|
|
350
|
+
* The Host paints every frame with a socket for stdout, so it cannot ask the
|
|
351
|
+
* terminal anything; the answer travels in its environment instead.
|
|
352
|
+
*/
|
|
353
|
+
ambiguousWide?: boolean;
|
|
279
354
|
/**
|
|
280
355
|
* Start the Host through this command instead of resolving the real one, or
|
|
281
356
|
* `null` to spawn it directly even where a bootstrap exists.
|
package/lib/types/footer.d.ts
CHANGED
|
@@ -181,7 +181,7 @@ export declare function subagentRouteLabel(model: string, provider?: string, eff
|
|
|
181
181
|
* The mute (`90`) is reopened after the chip so the rest of the identity row
|
|
182
182
|
* stays dim.
|
|
183
183
|
*/
|
|
184
|
-
export declare function paintFooterSubagentChip(line: string, chip: string, foreign: boolean, muteSgr?: string, depth?: ColorDepth): string;
|
|
184
|
+
export declare function paintFooterSubagentChip(line: string, chip: string, foreign: boolean, muteSgr?: string, depth?: ColorDepth, identitySgr?: string): string;
|
|
185
185
|
export declare function footerSubagentForeign(input: Pick<FooterStatusInput, 'provider' | 'subProvider'>): boolean;
|
|
186
186
|
/**
|
|
187
187
|
* The model as the footer shows it: the model's own name, without a
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { TerminalInputPump } from './terminal-input.js';
|
|
2
|
+
/**
|
|
3
|
+
* The glyphs the probe prints.
|
|
4
|
+
*
|
|
5
|
+
* Drawn from the families {@link AMBIGUOUS_WIDE_RANGES} covers — an enclosed
|
|
6
|
+
* digit, a dash, an ellipsis, a curly quote, a middle dot, a bullet, a roman
|
|
7
|
+
* numeral. Mixing families is deliberate: a font may cover `①` from a fallback
|
|
8
|
+
* face while treating `—` as typographic punctuation, and a single-glyph probe
|
|
9
|
+
* would then decide the whole table from one sample. A terminal that treats the
|
|
10
|
+
* ambiguous block as full width spends two cells on all seven.
|
|
11
|
+
*/
|
|
12
|
+
export declare const GLYPH_PROBE_TEXT = "\u2460\u2014\u2026\u201C\u201D\u00B7\u2022\u2160";
|
|
13
|
+
export interface GlyphWidthMeasurement {
|
|
14
|
+
/** True when the terminal spent two cells per probe glyph. */
|
|
15
|
+
wide: boolean;
|
|
16
|
+
/** Cells the terminal reported for the whole probe string. */
|
|
17
|
+
cells: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Measure the ambiguous-glyph width on a real terminal.
|
|
21
|
+
*
|
|
22
|
+
* Returns `undefined` — never a guess — when there is no terminal, when it does
|
|
23
|
+
* not answer, or when the answer matches neither expectation (a font with mixed
|
|
24
|
+
* metrics: the caller then keeps its locale-based default rather than inventing a
|
|
25
|
+
* table from one sample).
|
|
26
|
+
*
|
|
27
|
+
* The round trip goes through {@link TerminalInputPump.askPosition}, which
|
|
28
|
+
* swallows the reply. That is not an implementation detail: a cursor reply that
|
|
29
|
+
* nobody consumes is echoed on screen as literal `^[[1;5R` text, and a
|
|
30
|
+
* hand-rolled version of this probe did exactly that at boot.
|
|
31
|
+
* @param options - the streams (or a pump already running on them), the probe
|
|
32
|
+
* text, and the reply budget.
|
|
33
|
+
* @returns what the terminal said, or undefined.
|
|
34
|
+
*/
|
|
35
|
+
export declare function measureAmbiguousGlyphWidth(options?: {
|
|
36
|
+
stdin?: NodeJS.ReadStream;
|
|
37
|
+
stdout?: NodeJS.WriteStream;
|
|
38
|
+
probe?: string;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
pump?: TerminalInputPump;
|
|
41
|
+
}): Promise<GlyphWidthMeasurement | undefined>;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface Config {
|
|
|
36
36
|
*/
|
|
37
37
|
/** UI language (`/language`); zh unless the environment says otherwise. */
|
|
38
38
|
language?: string;
|
|
39
|
+
/** Palette (`/theme`): default | catppuccin | gruvbox | mono. */
|
|
40
|
+
theme?: string;
|
|
39
41
|
/** Newest plugin version whose update notice was dismissed. */
|
|
40
42
|
skipUpdate?: string;
|
|
41
43
|
/** Workspace pane layout (`/view`). */
|
|
@@ -46,10 +48,26 @@ export interface Config {
|
|
|
46
48
|
autoApproval?: string;
|
|
47
49
|
/** Milliseconds a leftover finished Host waits before exiting; 0 = never. */
|
|
48
50
|
idleExit?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Retry once when the *provider* rejects a request with 401/403 while a local
|
|
53
|
+
* credential is configured (see `auth-failure.ts`). Off by default: the retry
|
|
54
|
+
* re-sends the whole context, which in a long session is expensive.
|
|
55
|
+
*/
|
|
56
|
+
retryProviderAuth?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Fail loudly instead of staying inert when there is no terminal.
|
|
59
|
+
*
|
|
60
|
+
* The default (`false`) logs one line and mounts nothing — that is what keeps
|
|
61
|
+
* this plugin harmless in a GUI host with no console (the desktop app) and in
|
|
62
|
+
* pipes. Set it when a missing terminal *is* an error for the caller: a script
|
|
63
|
+
* asserting that a terminal profile really starts, for one.
|
|
64
|
+
*/
|
|
65
|
+
requireTerminal?: boolean;
|
|
49
66
|
}
|
|
50
67
|
/** Every field above, as schemastery resolves them (all optional). */
|
|
51
68
|
interface ConfigFields {
|
|
52
69
|
sessionId?: string;
|
|
70
|
+
requireTerminal?: boolean;
|
|
53
71
|
showReasoning?: boolean;
|
|
54
72
|
maxToolOutputLines?: number;
|
|
55
73
|
color?: boolean;
|
|
@@ -60,11 +78,13 @@ interface ConfigFields {
|
|
|
60
78
|
model?: string;
|
|
61
79
|
paintIntervalMs?: number;
|
|
62
80
|
language?: string;
|
|
81
|
+
theme?: string;
|
|
63
82
|
skipUpdate?: string;
|
|
64
83
|
view?: string;
|
|
65
84
|
disconnect?: string;
|
|
66
85
|
autoApproval?: string;
|
|
67
86
|
idleExit?: number;
|
|
87
|
+
retryProviderAuth?: boolean;
|
|
68
88
|
}
|
|
69
89
|
/**
|
|
70
90
|
* The entry's schema. Everything a launch supplies (`config:` in
|
package/lib/types/plan.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Plan dock, todo lists, /find, prompt-injection cards, and compact errors.
|
|
3
3
|
*/
|
|
4
|
+
import { type Theme } from './theme.js';
|
|
4
5
|
import { type TextSegment } from './term-text.js';
|
|
5
6
|
import type { DiffDisplayLine, DisplayKind, PlanTodoItem, Row, SubagentLogEntry } from './transcript-types.js';
|
|
6
7
|
export declare const MAX_SUBAGENT_LOGS = 80;
|
|
@@ -83,10 +84,10 @@ export declare function askSummary(value: unknown): string;
|
|
|
83
84
|
export declare function firstDisplayLine(text: string): string;
|
|
84
85
|
/** Collapse a child-session blob to one short chip/wait-card line. */
|
|
85
86
|
export declare function clipSubagentActivity(text: string, maxChars?: number): string;
|
|
86
|
-
/** Running / ok / aborted / error →
|
|
87
|
+
/** Running / ok / aborted / error → theme token for the status dot and word. */
|
|
87
88
|
export declare function subagentStateColor(status: Extract<Row, {
|
|
88
89
|
kind: 'subagent';
|
|
89
|
-
}>['status']):
|
|
90
|
+
}>['status'], theme?: Theme): string;
|
|
90
91
|
/**
|
|
91
92
|
* Rebuild a subagent chip from the parent spawn tool call that survives in the
|
|
92
93
|
* session log. Live `subagent/start` is not replayed, so resume would otherwise
|
|
@@ -138,6 +139,8 @@ export declare function buildSubagentHeader(input: {
|
|
|
138
139
|
inspectHint?: string;
|
|
139
140
|
/** Different provider from the parent: paint the title cyan, not violet. */
|
|
140
141
|
foreign?: boolean;
|
|
142
|
+
/** Active palette; callers pass the session's theme so `mono` stays monochrome. */
|
|
143
|
+
theme?: Theme;
|
|
141
144
|
}): {
|
|
142
145
|
plain: string;
|
|
143
146
|
segments: TextSegment[];
|
package/lib/types/platform.d.ts
CHANGED
|
@@ -235,3 +235,86 @@ export declare function restrictPathToUserSync(path: string, options?: {
|
|
|
235
235
|
* @returns true when chrome must be drawn in ASCII.
|
|
236
236
|
*/
|
|
237
237
|
export declare function asciiFallbackEnabled(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
238
|
+
/**
|
|
239
|
+
* What this process can serve.
|
|
240
|
+
*
|
|
241
|
+
* The plugin is a *terminal* UI, and the host it is mounted into decides whether
|
|
242
|
+
* there is a terminal at all:
|
|
243
|
+
*
|
|
244
|
+
* - `host-relay`: this is the detached TUI Host itself (see
|
|
245
|
+
* `isTuiHostProcess`); its terminal lives in whichever window attaches to the
|
|
246
|
+
* session, so a missing TTY here is expected and it must serve.
|
|
247
|
+
* - `terminal`: a real TTY on stdin and stdout.
|
|
248
|
+
* - `desktop`: the desktop Harness launcher (Electron-as-Node, GUI subsystem).
|
|
249
|
+
* It never has a console, so no terminal profile can ever run under it.
|
|
250
|
+
* - `no-tty`: anything else without a TTY — a pipe (`… | tee`), cron, a service.
|
|
251
|
+
*/
|
|
252
|
+
export type TerminalAvailability = 'host-relay' | 'terminal' | 'desktop' | 'no-tty';
|
|
253
|
+
/**
|
|
254
|
+
* Decide what to serve, from facts the caller can state.
|
|
255
|
+
*
|
|
256
|
+
* Pure on purpose: this is the one branch that decides whether the plugin mounts
|
|
257
|
+
* anything at all, and every caller-visible behaviour difference (a window, a
|
|
258
|
+
* log line, a hard failure) hangs off it.
|
|
259
|
+
* @param facts - the host process marker, the two TTY answers, and whether the
|
|
260
|
+
* launcher is the desktop's.
|
|
261
|
+
* @returns the availability bucket.
|
|
262
|
+
*/
|
|
263
|
+
export declare function terminalAvailability(facts: {
|
|
264
|
+
hostProcess: boolean;
|
|
265
|
+
stdinIsTty: boolean;
|
|
266
|
+
stdoutIsTty: boolean;
|
|
267
|
+
desktop: boolean;
|
|
268
|
+
}): TerminalAvailability;
|
|
269
|
+
/**
|
|
270
|
+
* Whether this process is the desktop app's launcher rather than a terminal Node.
|
|
271
|
+
*
|
|
272
|
+
* The desktop Harness ships a PATH shim that runs its own GUI-subsystem binary
|
|
273
|
+
* with `ELECTRON_RUN_AS_NODE=1` (`DeepSeek Harness.exe --expose-internals
|
|
274
|
+
* app.asar/dsh/.../cli.js`). A GUI-subsystem process started from a console does
|
|
275
|
+
* not attach to it on Windows, so `process.stdin.isTTY` / `stdout.isTTY` stay
|
|
276
|
+
* undefined and no terminal profile can start under it. That is a property of
|
|
277
|
+
* the launcher, not of the user's terminal, and the two cases deserve different
|
|
278
|
+
* advice — which is all this decides.
|
|
279
|
+
* @param facts - the runtime's own answers (`process.versions`, `execPath`, `argv`).
|
|
280
|
+
* @returns true when the launcher is the desktop app's Electron-as-Node shim.
|
|
281
|
+
*/
|
|
282
|
+
export declare function desktopLauncher(facts: {
|
|
283
|
+
electron?: string | undefined;
|
|
284
|
+
execPath?: string | undefined;
|
|
285
|
+
argv?: readonly string[] | undefined;
|
|
286
|
+
}): boolean;
|
|
287
|
+
/**
|
|
288
|
+
* The message a terminal profile fails with when the launcher has no TTY.
|
|
289
|
+
*
|
|
290
|
+
* The desktop case is worth its own wording: the generic "must be TTYs" line
|
|
291
|
+
* sends a desktop user hunting for a broken terminal, while the real answer is
|
|
292
|
+
* "this launcher never has one — use the npm CLI in a terminal/SSH session, and
|
|
293
|
+
* note the desktop app does not need this plugin at all".
|
|
294
|
+
* @param desktop - whether {@link desktopLauncher} recognised the launcher.
|
|
295
|
+
* @returns the error text for the plugin to throw.
|
|
296
|
+
*/
|
|
297
|
+
export declare function nonTtyErrorMessage(desktop: boolean): string;
|
|
298
|
+
/**
|
|
299
|
+
* What to log when the plugin has nowhere to draw.
|
|
300
|
+
*
|
|
301
|
+
* Deliberately not an error: on the desktop the situation is normal (a GUI app
|
|
302
|
+
* has no console), and a terminal profile is not a thing the user broke. The
|
|
303
|
+
* line has to (a) say nothing is running *from this plugin*, (b) explain why,
|
|
304
|
+
* (c) give the one command that fixes it, and (d) not read as a failure — the
|
|
305
|
+
* plugin used to throw here, which surfaced as "1 entry did not activate" and,
|
|
306
|
+
* on the desktop, took the app down with it.
|
|
307
|
+
* @param availability - `desktop` or `no-tty`.
|
|
308
|
+
* @returns the log line.
|
|
309
|
+
*/
|
|
310
|
+
export declare function inactiveNotice(availability: 'desktop' | 'no-tty'): string;
|
|
311
|
+
/**
|
|
312
|
+
* The message for the opt-in hard failure (`requireTerminal: true`).
|
|
313
|
+
*
|
|
314
|
+
* Same two situations as {@link inactiveNotice}, but for callers who would
|
|
315
|
+
* rather a profile fail loudly than quietly do nothing — a script asserting that
|
|
316
|
+
* a terminal profile really starts, for one.
|
|
317
|
+
* @param desktop - whether the launcher is the desktop's.
|
|
318
|
+
* @returns the error text.
|
|
319
|
+
*/
|
|
320
|
+
export declare function requiredTerminalError(desktop: boolean): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What makes a session "blank": a launch that never did anything.
|
|
3
|
+
*
|
|
4
|
+
* A fresh TUI start creates a session before the reader has typed anything, so
|
|
5
|
+
* quitting straight away leaves an artifact behind. The picker hides those, but
|
|
6
|
+
* they are real files, and other surfaces (the web session list) read the same
|
|
7
|
+
* artifacts without that filter — which is how a session that was never used
|
|
8
|
+
* still shows up in another profile's menu.
|
|
9
|
+
*
|
|
10
|
+
* The rule lives in its own module, with no host imports, for one reason: three
|
|
11
|
+
* very different callers have to agree on it — the picker (which prunes while
|
|
12
|
+
* listing), the TUI (which drops its own session on exit), and
|
|
13
|
+
* `scripts/prune-blank-sessions.mjs` (which sweeps whatever already accumulated,
|
|
14
|
+
* outside a running host).
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-ssh-tui/session-blank
|
|
17
|
+
*/
|
|
18
|
+
/** Whether one event is a message the reader typed. */
|
|
19
|
+
export declare function isUserMessageEvent(event: unknown): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Whether a turn was opened and never closed.
|
|
22
|
+
*
|
|
23
|
+
* A session with an open turn is mid-work: it must never be treated as empty,
|
|
24
|
+
* however little it looks like it has done.
|
|
25
|
+
* @param events - decoded session events, in order.
|
|
26
|
+
* @returns true when a `turn/start` has no matching `turn/end`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function hasUnfinishedTurn(events: readonly unknown[]): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Whether the session ever produced a reply.
|
|
31
|
+
*
|
|
32
|
+
* A failed request counts: the error text is the only thing the reader has to
|
|
33
|
+
* look at, and deleting it would delete their evidence.
|
|
34
|
+
* @param events - decoded session events, in order.
|
|
35
|
+
* @returns true when an assistant message or a failed turn is present.
|
|
36
|
+
*/
|
|
37
|
+
export declare function sessionHasReply(events: readonly unknown[]): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Whether a session saw the reader's input, a reply, or an open turn.
|
|
40
|
+
* @param events - decoded session events, in order.
|
|
41
|
+
* @returns true when there is something worth keeping.
|
|
42
|
+
*/
|
|
43
|
+
export declare function sessionHasWork(events: readonly unknown[]): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether a session's events describe a launch that never did anything.
|
|
46
|
+
* @param events - the session's decoded events.
|
|
47
|
+
* @returns true when nothing the reader would recognise as work happened. An
|
|
48
|
+
* empty event list is *not* blank: it is an unreadable or detached log, and
|
|
49
|
+
* guessing there once made a listing delete a live session's directory.
|
|
50
|
+
*/
|
|
51
|
+
export declare function sessionEventsAreBlank(events: readonly unknown[]): boolean;
|