dsh-ssh-tui 0.7.3 → 0.8.0
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 +101 -27
- package/README.md +83 -27
- package/docs/remote-ops.md +80 -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/auth-failure.js +92 -0
- package/lib/auth-failure.js.map +1 -0
- package/lib/commands.js +2 -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 +49 -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 +49 -14
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +76 -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 +713 -51
- package/lib/tui.js.map +1 -1
- package/lib/types/attach.d.ts +8 -0
- package/lib/types/auth-failure.d.ts +48 -0
- package/lib/types/commands.d.ts +6 -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/index.d.ts +7 -0
- 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 +149 -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 +104 -69
|
@@ -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
|
+
};
|
package/lib/types/selection.d.ts
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* OSC 52 (the channel `/copy` already uses).
|
|
10
10
|
*
|
|
11
11
|
* Only **model replies** are freely selectable. Everything here is pure: the
|
|
12
|
-
* caller passes the painted lines (plain text
|
|
13
|
-
* reply
|
|
14
|
-
* reverse video, and the text to copy.
|
|
15
|
-
* stays in `tui.ts`.
|
|
12
|
+
* caller passes the painted lines (plain text, whether each one came from a
|
|
13
|
+
* reply, and how many leading cells are chrome) and gets back the ordered
|
|
14
|
+
* selection, the cell spans to paint in reverse video, and the text to copy.
|
|
15
|
+
* The IO — mouse reports, OSC 52, notices — stays in `tui.ts`.
|
|
16
16
|
*
|
|
17
17
|
* Columns are **cells**, not characters: a CJK glyph is two cells wide, and a
|
|
18
18
|
* selection that touches either half includes the whole glyph. Surrogate pairs
|
|
@@ -24,6 +24,14 @@ export interface SelectableLine {
|
|
|
24
24
|
text: string;
|
|
25
25
|
/** True when this line came from a model reply, the only freely copyable kind. */
|
|
26
26
|
copyable: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Cells at the start of the line that are chrome, not content.
|
|
29
|
+
*
|
|
30
|
+
* The focused card or reply carries a `▶ ` marker, and a reply's lines are
|
|
31
|
+
* copyable, so without this the marker was part of the text a drag copied off
|
|
32
|
+
* its first line. Only the first line of the focused row carries one.
|
|
33
|
+
*/
|
|
34
|
+
gutter?: number;
|
|
27
35
|
}
|
|
28
36
|
/** A point in the transcript: `line` indexes the painted lines, `column` is a cell. */
|
|
29
37
|
export interface SelectionPoint {
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
import type { Context } from '@deepseek-ai/cordis';
|
|
12
12
|
export declare const name = "ssh-tui-settings-subagent";
|
|
13
13
|
/** The subagent selection, as the 0.1.7 settings service wants to see it. */
|
|
14
|
-
export declare const Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
14
|
+
export declare const Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
|
|
15
15
|
provider: import("@deepseek-ai/schemastery").default<string>;
|
|
16
16
|
model: import("@deepseek-ai/schemastery").default<string>;
|
|
17
17
|
reasoningEffort: import("@deepseek-ai/schemastery").default<string>;
|
|
18
|
-
}
|
|
18
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
19
19
|
provider: import("@deepseek-ai/schemastery").default<string>;
|
|
20
20
|
model: import("@deepseek-ai/schemastery").default<string>;
|
|
21
21
|
reasoningEffort: import("@deepseek-ai/schemastery").default<string>;
|
|
22
|
-
}
|
|
22
|
+
}>>, "plain">;
|
|
23
23
|
/** Nothing to mount: the TUI owns every read and write of this section. */
|
|
24
24
|
export declare function apply(_ctx: Context): void;
|
|
@@ -130,15 +130,15 @@ export declare function adoptSessionSubagentSelection(ref: SubagentSelectionRef,
|
|
|
130
130
|
*/
|
|
131
131
|
export declare function releaseSessionSubagentSelection(ref: SubagentSelectionRef, settingsValue: unknown): void;
|
|
132
132
|
/** Settings schema for the subagent selection; every field is form-writable. */
|
|
133
|
-
export declare const SUBAGENT_SETTINGS_SCHEMA: z<Schemastery.ObjectS<{
|
|
133
|
+
export declare const SUBAGENT_SETTINGS_SCHEMA: z<Schemastery.ObjectS<NoInfer<{
|
|
134
134
|
provider: z<string>;
|
|
135
135
|
model: z<string>;
|
|
136
136
|
reasoningEffort: z<string>;
|
|
137
|
-
}
|
|
137
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
138
138
|
provider: z<string>;
|
|
139
139
|
model: z<string>;
|
|
140
140
|
reasoningEffort: z<string>;
|
|
141
|
-
}
|
|
141
|
+
}>>, "plain">;
|
|
142
142
|
/** Normalize a raw settings section into a live typed selection. */
|
|
143
143
|
export declare function normalizeSubagentSelection(value: unknown): SubagentSelection;
|
|
144
144
|
/**
|
package/lib/types/term-text.d.ts
CHANGED
|
@@ -38,6 +38,20 @@ export declare function waitCardCopy(input: {
|
|
|
38
38
|
* end the last one with an ellipsis when the text does not fit.
|
|
39
39
|
*/
|
|
40
40
|
export declare function wrapWaitDetails(detail: string, width: number, maxLines?: number): string[];
|
|
41
|
+
/** Forget the cached decision, so the next read re-consults the environment. */
|
|
42
|
+
export declare function resetAsciiChrome(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Rewrite one string's chrome into ASCII when the terminal cannot decode UTF-8.
|
|
45
|
+
*
|
|
46
|
+
* Only the glyphs in {@link ASCII_CHROME} move; CJK text is left byte for byte,
|
|
47
|
+
* because there is no ASCII for it and a Chinese locale on a non-UTF-8 console
|
|
48
|
+
* needs `/language en`, not a mangled translation. Widths are preserved for the
|
|
49
|
+
* two-cell marks, so a row measured either side of this call occupies the same
|
|
50
|
+
* number of cells.
|
|
51
|
+
* @param text - one row, possibly with ANSI sequences in it.
|
|
52
|
+
* @returns the row unchanged when UTF-8 chrome is safe.
|
|
53
|
+
*/
|
|
54
|
+
export declare function mapAsciiChrome(text: string): string;
|
|
41
55
|
/**
|
|
42
56
|
* Terminal cell width for one string.
|
|
43
57
|
*
|
|
@@ -133,6 +133,16 @@ export declare class TerminalInputPump {
|
|
|
133
133
|
* and 0 ms, two "agreeing" samples, early stop, chip frozen on a wrong value).
|
|
134
134
|
*/
|
|
135
135
|
private quietMs;
|
|
136
|
+
/**
|
|
137
|
+
* One request, one window: what a display-liveness check needs.
|
|
138
|
+
*
|
|
139
|
+
* `measure()` deliberately is not this. It waits out its whole budget between
|
|
140
|
+
* attempts so a late answer cannot be read as a fast one, and a check that
|
|
141
|
+
* costs seconds per question is a picker that hangs. Reading a late answer as
|
|
142
|
+
* this window's is the harmless direction here: it reports "the terminal is
|
|
143
|
+
* there", which is the conservative answer for every caller.
|
|
144
|
+
*/
|
|
145
|
+
measureOnce(timeoutMs?: number): Promise<boolean>;
|
|
136
146
|
/**
|
|
137
147
|
* Wait until the line has been quiet for a full window, measured from the
|
|
138
148
|
* later of the last answer and the last request. An answer to a request we
|
|
@@ -142,6 +142,26 @@ export type Row = {
|
|
|
142
142
|
text: string;
|
|
143
143
|
plugin?: string;
|
|
144
144
|
expanded: boolean;
|
|
145
|
+
} | {
|
|
146
|
+
/**
|
|
147
|
+
* Files one turn changed, from the Host's `workspaceChanges` service.
|
|
148
|
+
* Display only: the summary is not in the session log, and a restarted
|
|
149
|
+
* Host cannot reopen it, so the card is never rebuilt from history.
|
|
150
|
+
*/
|
|
151
|
+
kind: 'changes';
|
|
152
|
+
/** The turn the summary describes. A later event for it replaces this card. */
|
|
153
|
+
turn: number;
|
|
154
|
+
/** Sequence of the `workspace/changes` event the summary was read from. */
|
|
155
|
+
seq: number;
|
|
156
|
+
/** Session the summary belongs to; `diff` is served per session. */
|
|
157
|
+
sessionId: string;
|
|
158
|
+
/** Header text, rendered once so the card does not recompute it per frame. */
|
|
159
|
+
header: string;
|
|
160
|
+
/** One line per listed file, in the service's own order. */
|
|
161
|
+
files: string[];
|
|
162
|
+
/** Files the service's cap left off the list, appended after `files`. */
|
|
163
|
+
more?: string;
|
|
164
|
+
expanded: boolean;
|
|
145
165
|
} | {
|
|
146
166
|
kind: 'system';
|
|
147
167
|
text: string;
|
|
@@ -171,6 +191,8 @@ export type CollapsibleBlock = Extract<Row, {
|
|
|
171
191
|
kind: 'compaction';
|
|
172
192
|
} | {
|
|
173
193
|
kind: 'prompt';
|
|
194
|
+
} | {
|
|
195
|
+
kind: 'changes';
|
|
174
196
|
}> | {
|
|
175
197
|
kind: 'streaming-reasoning';
|
|
176
198
|
expanded: boolean;
|
package/lib/types/tui.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { type SessionRoute } from './session-route.js';
|
|
|
25
25
|
import { type SubagentSelectionRef } from './subagent-model.js';
|
|
26
26
|
import { type AskUserQuestionAnswer, type AskUserQuestionRequest } from '@deepseek-ai/dsh-user-questions';
|
|
27
27
|
import type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval';
|
|
28
|
-
import type { CollapsibleBlock, DisconnectPolicyName } from './transcript-types.js';
|
|
28
|
+
import type { CollapsibleBlock, DisconnectPolicyName, Row } from './transcript-types.js';
|
|
29
29
|
import { paintedLinkHits } from './term-text.js';
|
|
30
30
|
export type { CollapsibleBlock, DisplayKind, DisconnectPolicyName, PlanTodoItem, Row, SubagentLogEntry, ToolDiffHunk, } from './transcript-types.js';
|
|
31
31
|
export { clipAnsiToWidth, cursorVisualPosition, displayWidth, foldInputView, hrefAtColumn, osc52Clipboard, osc8Enabled, paintedLinkHits, fmtElapsedCompact, padAnsiToWidth, padToWidth, renderMarkdownLines, repeatToWidth, shimmerText, truncateToWidth, visibleWidth, waitCardCopy, waitSummaryFromReasoning, wrapWaitDetails, } from './term-text.js';
|
|
@@ -107,6 +107,15 @@ export interface TuiConfig {
|
|
|
107
107
|
onHangup?: () => void | Promise<void>;
|
|
108
108
|
/** Called when a Display relay attaches after hangup. */
|
|
109
109
|
onReattach?: () => void | Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* The display just went away, before the Host has decided whether to stay.
|
|
112
|
+
*
|
|
113
|
+
* The lock stops saying `attached` from here: that field is what a resume (and
|
|
114
|
+
* the launch picker's list) reads to decide whether another window is on the
|
|
115
|
+
* session, and leaving it set for the whole cancel-and-flush below turned a
|
|
116
|
+
* reconnect in that window into "already attached to another window".
|
|
117
|
+
*/
|
|
118
|
+
onDetach?: () => void | Promise<void>;
|
|
110
119
|
/** Host process: no local TTY; paint only through the display socket. */
|
|
111
120
|
headlessDisplay?: boolean;
|
|
112
121
|
/** Append events as plain lines instead of painting (see `line-mode`). */
|
|
@@ -123,6 +132,16 @@ export interface TuiController {
|
|
|
123
132
|
disconnectPolicy(): DisconnectPolicyName;
|
|
124
133
|
}
|
|
125
134
|
export type WorkspaceView = 'detailed' | 'compact';
|
|
135
|
+
/**
|
|
136
|
+
* One row the card cursor can land on.
|
|
137
|
+
*
|
|
138
|
+
* Every collapsible card, plus the model replies. A reply has nothing to expand,
|
|
139
|
+
* which is why it used to be left out of the ring — and that is exactly what
|
|
140
|
+
* made `/copy` unreachable for it: `/copy` takes the focused row, so as soon as
|
|
141
|
+
* the reader selected any card the reply stopped being a copy target, and the
|
|
142
|
+
* newest reply survived only as the no-focus fallback.
|
|
143
|
+
*/
|
|
144
|
+
export type FocusTarget = Row | CollapsibleBlock;
|
|
126
145
|
export declare function parseDisconnectPolicy(raw: string): DisconnectPolicyName | undefined;
|
|
127
146
|
/**
|
|
128
147
|
* The `reasoningEfforts` map `/setup` writes for a hand-declared
|
|
@@ -221,6 +240,7 @@ export declare class SshTui {
|
|
|
221
240
|
private relayRows;
|
|
222
241
|
private readonly onHangup;
|
|
223
242
|
private readonly onReattach;
|
|
243
|
+
private readonly onDetach;
|
|
224
244
|
private renderTimer;
|
|
225
245
|
private readonly decoder;
|
|
226
246
|
private readonly color;
|
|
@@ -254,6 +274,14 @@ export declare class SshTui {
|
|
|
254
274
|
private commandSuggestions;
|
|
255
275
|
private suggestionIndex;
|
|
256
276
|
private focusedRow;
|
|
277
|
+
/**
|
|
278
|
+
* The text of the last prompt the user sent, kept so an opted-in retry can
|
|
279
|
+
* send the same thing again after a provider-side auth failure. Cleared when
|
|
280
|
+
* the retry fires, which is what bounds it to one attempt per user message.
|
|
281
|
+
*/
|
|
282
|
+
private lastUserText;
|
|
283
|
+
/** Set on every turn/start; a retry consumes it. */
|
|
284
|
+
private authRetryArmed;
|
|
257
285
|
private pendingMessages;
|
|
258
286
|
private lastActivity;
|
|
259
287
|
private lastIdleCtrlCAt;
|
|
@@ -317,6 +345,15 @@ export declare class SshTui {
|
|
|
317
345
|
private readonly inputGuard;
|
|
318
346
|
private thinkingStartedAt;
|
|
319
347
|
private waitStartedAt;
|
|
348
|
+
/**
|
|
349
|
+
* Whether the running turn produced anything a person can read: reply text or
|
|
350
|
+
* a tool card. A turn that ends with thinking only is the upstream returning
|
|
351
|
+
* an empty stop, and painting 完成 for it reads as a finished answer — see
|
|
352
|
+
* the hint at `turn/end`.
|
|
353
|
+
*/
|
|
354
|
+
private turnSawOutput;
|
|
355
|
+
/** Whether the turn produced thinking, which the empty-stop hint names. */
|
|
356
|
+
private turnSawReasoning;
|
|
320
357
|
private completionSignaled;
|
|
321
358
|
private replaying;
|
|
322
359
|
/** Display-line budget for the first paint after resume; 0 = full transcript. */
|
|
@@ -370,10 +407,22 @@ export declare class SshTui {
|
|
|
370
407
|
private detachedDeniedCount;
|
|
371
408
|
/** Questions waiting for a display right now — their cards do not exist yet. */
|
|
372
409
|
private queuedQuestions;
|
|
410
|
+
/** When the current detached wait began, so the reconnect can say how long. */
|
|
411
|
+
private questionWaitSince;
|
|
373
412
|
private paintIntervalMs;
|
|
374
413
|
private paintLink;
|
|
414
|
+
/**
|
|
415
|
+
* Whether this process runs inside an SSH session, from the sshd environment.
|
|
416
|
+
*
|
|
417
|
+
* Distinct from `paintLink`, which `applyProbedRtt` pins to `'ssh'` for any
|
|
418
|
+
* relayed frame — a local window included. Only this flag says the capability
|
|
419
|
+
* table described a remote tty rather than the terminal the bytes reach.
|
|
420
|
+
*/
|
|
421
|
+
private sshSession;
|
|
375
422
|
private paintProbed;
|
|
376
423
|
private paintRttMs;
|
|
424
|
+
/** The last few measured round-trips; the chip and the budget use their median. */
|
|
425
|
+
private paintRttHistory;
|
|
377
426
|
private sessionTitle;
|
|
378
427
|
private llmRetry;
|
|
379
428
|
private quotaSnapshot;
|
|
@@ -562,6 +611,8 @@ export declare class SshTui {
|
|
|
562
611
|
/** Re-open DECSET and start painting to an attached Display relay. */
|
|
563
612
|
attachRelayDisplay(): void;
|
|
564
613
|
currentDisconnectPolicy(): DisconnectPolicyName;
|
|
614
|
+
/** The paint cadence in force right now: the link tier the footer shows. */
|
|
615
|
+
currentPaintIntervalMs(): number;
|
|
565
616
|
applyProbedRtt(rttMs: number | undefined): void;
|
|
566
617
|
private markDirty;
|
|
567
618
|
private toolCardSummary;
|
|
@@ -576,6 +627,19 @@ export declare class SshTui {
|
|
|
576
627
|
private flushLineMode;
|
|
577
628
|
/** The transcript rows that support per-row expand/collapse. */
|
|
578
629
|
private collapsibleRows;
|
|
630
|
+
/**
|
|
631
|
+
* Everything the card cursor walks, in transcript order: the collapsible
|
|
632
|
+
* cards plus the model replies.
|
|
633
|
+
*
|
|
634
|
+
* Built from the two lists rather than from the rows alone, because one of the
|
|
635
|
+
* collapsible entries is synthetic: the live thinking block exists only while a
|
|
636
|
+
* turn streams and is never a member of `this.rows`. Filtering the rows (the
|
|
637
|
+
* first version of this) silently dropped it out of the cursor walk — ↑ on a
|
|
638
|
+
* turn that had only streamed thinking did nothing, and Enter could no longer
|
|
639
|
+
* expand the card being written. It belongs at the end, which is where it is
|
|
640
|
+
* painted, so anything not in the rows is appended.
|
|
641
|
+
*/
|
|
642
|
+
private focusRing;
|
|
579
643
|
private spinnerFrame;
|
|
580
644
|
/**
|
|
581
645
|
* Codex wait card: shown while the turn is running. The live thinking
|
|
@@ -630,6 +694,16 @@ export declare class SshTui {
|
|
|
630
694
|
private markSearchRow;
|
|
631
695
|
private openToolInspect;
|
|
632
696
|
private openSubagentInspect;
|
|
697
|
+
/**
|
|
698
|
+
* Read one reply full-screen.
|
|
699
|
+
*
|
|
700
|
+
* What Enter does on a selected reply: a reply has no body to fold away, and
|
|
701
|
+
* the transcript wraps it to the window — which for a long answer with tables
|
|
702
|
+
* or code blocks is not how it reads. The overlay is the same surface the tool
|
|
703
|
+
* cards use, and the copy key works inside it, so "read it, then copy it"
|
|
704
|
+
* needs no trip back through the timeline.
|
|
705
|
+
*/
|
|
706
|
+
private openReplyInspect;
|
|
633
707
|
/**
|
|
634
708
|
* Line mode has no framed scroller, so an inspect body goes into the log.
|
|
635
709
|
*
|
|
@@ -640,8 +714,18 @@ export declare class SshTui {
|
|
|
640
714
|
private echoInspectToLog;
|
|
641
715
|
closeInspect(): void;
|
|
642
716
|
private paintCollapsibleHeader;
|
|
643
|
-
/**
|
|
644
|
-
|
|
717
|
+
/**
|
|
718
|
+
* Which file line of an expanded changes card the screen cursor is on.
|
|
719
|
+
*
|
|
720
|
+
* The card paints its header, then one row per file, and `selectableLines`
|
|
721
|
+
* holds exactly the viewport. The cursor sits on the input row unless the
|
|
722
|
+
* reader scrolled, so the file is how many painted rows above the cursor the
|
|
723
|
+
* card's header is. A cursor on the header, the remainder line, or the prompt
|
|
724
|
+
* falls back to the first file, which is the only defensible guess.
|
|
725
|
+
*/
|
|
726
|
+
private changesFileIndex;
|
|
727
|
+
/** Move the card cursor among the cards and the replies. */
|
|
728
|
+
private moveFocus;
|
|
645
729
|
/** Toggle the focused block; without focus, toggle the most recent one. */
|
|
646
730
|
toggleCollapsible(): void;
|
|
647
731
|
/**
|
|
@@ -807,6 +891,27 @@ export declare class SshTui {
|
|
|
807
891
|
private readonly handleInboxClaimed;
|
|
808
892
|
private readonly handleInboxDiscarded;
|
|
809
893
|
private readonly handleDisposed;
|
|
894
|
+
/**
|
|
895
|
+
* One `workspace/changes` event: the files a turn changed.
|
|
896
|
+
*
|
|
897
|
+
* The event carries only the turn number; the summary stays on the Host and
|
|
898
|
+
* is served by `workspaceChanges` for this event's own sequence, and only
|
|
899
|
+
* while the Session lives. A missing service (every 0.1.5 Host) or a summary
|
|
900
|
+
* that can no longer be opened (a log replayed after a restart) shows
|
|
901
|
+
* nothing — upstream's rule, so a card that cannot be read is never drawn.
|
|
902
|
+
* A later event for the same turn replaces the earlier summary in place.
|
|
903
|
+
*/
|
|
904
|
+
private noteWorkspaceChanges;
|
|
905
|
+
/** Copy one summary onto its card. Shared by the first event and its replacements. */
|
|
906
|
+
private fillChangesCard;
|
|
907
|
+
/**
|
|
908
|
+
* Full view of one file on a changes card: its comparison, as the service
|
|
909
|
+
* computed it. Binary and oversized files have no lines, so the overlay
|
|
910
|
+
* carries the one-line explanation instead of an empty body.
|
|
911
|
+
*/
|
|
912
|
+
private openChangesInspect;
|
|
913
|
+
/** Open (or replace) the changes overlay, or echo it to the log in line mode. */
|
|
914
|
+
private openChangesInspectLines;
|
|
810
915
|
/** Plan-mode / command / team events that plugins merge into SessionEventMap. */
|
|
811
916
|
private handleExtensionEvent;
|
|
812
917
|
private findCompactionRow;
|
|
@@ -851,6 +956,19 @@ export declare class SshTui {
|
|
|
851
956
|
/** Keep an open inspect overlay in sync with the live child log. */
|
|
852
957
|
private refreshOpenSubagentInspect;
|
|
853
958
|
private hasLiveDisplay;
|
|
959
|
+
/**
|
|
960
|
+
* Tell the absent user that a question is waiting.
|
|
961
|
+
*
|
|
962
|
+
* The marker file is the part that needs no configuration: it sits next to the
|
|
963
|
+
* session's stderr log, so logging back into the jump host shows it before the
|
|
964
|
+
* TUI is open. The command is whatever the user pointed `ssh-tui.notify` at,
|
|
965
|
+
* run once; it never blocks the wait and never reports its own failure.
|
|
966
|
+
*/
|
|
967
|
+
private announceWaitingQuestion;
|
|
968
|
+
/** The SMTP account `/notify smtp` saved, when the target is authenticated. */
|
|
969
|
+
private readNotifySmtp;
|
|
970
|
+
/** `DSH_TUI_NOTIFY`, else the saved `ssh-tui.notify`. Empty means off. */
|
|
971
|
+
private readNotifyCommand;
|
|
854
972
|
private waitForLiveDisplay;
|
|
855
973
|
/**
|
|
856
974
|
* Auto mode rides the approval waterfall: when the host approval policy is
|
|
@@ -884,6 +1002,10 @@ export declare class SshTui {
|
|
|
884
1002
|
* `the user rejected tool "bash"`. Steer a plugin notice with the real
|
|
885
1003
|
* classifier/reviewer reason. Summary matches the workspace decision row
|
|
886
1004
|
* so the handler does not paint the body twice.
|
|
1005
|
+
*
|
|
1006
|
+
* This notice is committed to the session, so it is exactly the injection a
|
|
1007
|
+
* V4 log refuses when it wears the released `plugin` wrapper — see
|
|
1008
|
+
* {@link TUI_SOURCE_KIND}.
|
|
887
1009
|
*/
|
|
888
1010
|
private tellModelApprovalDenied;
|
|
889
1011
|
readonly handleApproval: (request: ApprovalRequest, _next: () => Promise<ApprovalOutcome>) => Promise<ApprovalOutcome>;
|
|
@@ -1092,7 +1214,31 @@ export declare class SshTui {
|
|
|
1092
1214
|
private runLanguageCommand;
|
|
1093
1215
|
/** /view: detailed (see the work) vs compact (Codex-like summary). */
|
|
1094
1216
|
private runViewCommand;
|
|
1217
|
+
/**
|
|
1218
|
+
* /notify: where a question waiting on an absent user is announced.
|
|
1219
|
+
*
|
|
1220
|
+
* No argument shows what is configured. `off` clears it. `mail`, `smtp` and
|
|
1221
|
+
* `local` store the command that target needs; the SMTP password is kept
|
|
1222
|
+
* beside it and handed to the command through the environment, so it is never
|
|
1223
|
+
* part of the command string.
|
|
1224
|
+
*/
|
|
1225
|
+
private runNotifyCommand;
|
|
1095
1226
|
/** /disconnect: pause (default) or continue the turn after SSH drop. */
|
|
1227
|
+
/**
|
|
1228
|
+
* Say where an auth failure came from, and retry it if the user asked for that.
|
|
1229
|
+
*
|
|
1230
|
+
* `code: "AUTH"` collapses two opposite situations (see `auth-failure.ts`), and
|
|
1231
|
+
* the host does not retry either of them: a provider-side 401/403 that lasted
|
|
1232
|
+
* 45 seconds killed two turns in a row on 2026-09-29 while the same key, model
|
|
1233
|
+
* and route answered 200 straight afterwards. The hint costs nothing and the
|
|
1234
|
+
* retry is opt-in, because resending a long session re-sends its whole context.
|
|
1235
|
+
*/
|
|
1236
|
+
private reportAuthFailure;
|
|
1237
|
+
private explainAuthFailure;
|
|
1238
|
+
/** Whether the opt-in retry is on (`/retryauth`, the settings form, or the env). */
|
|
1239
|
+
private retryProviderAuthEnabled;
|
|
1240
|
+
/** `/retryauth [on|off]` — the setting that lets one provider 401/403 retry itself. */
|
|
1241
|
+
private runRetryAuthCommand;
|
|
1096
1242
|
private runDisconnectCommand;
|
|
1097
1243
|
/** /mode: pick an agent preset (standard / minimal / ptc / cordis / routing-suite / ...). */
|
|
1098
1244
|
private runModeCommand;
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Best-effort npm latest check. Failures stay quiet. The TUI may offer to
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* install the version it just read, by number: `dsh plugin --profile <name>
|
|
4
|
+
* add dsh-ssh-tui@<version>`. A bare package name lets pnpm keep the lockfile
|
|
5
|
+
* pin (e.g. 0.3.7), and `@latest` is resolved a second time at install — under
|
|
6
|
+
* pnpm's `minimumReleaseAge` that second lookup can quietly land on yesterday's
|
|
7
|
+
* release while still exiting 0.
|
|
5
8
|
*/
|
|
6
9
|
import { spawn } from 'node:child_process';
|
|
7
10
|
export declare const PLUGIN_PACKAGE = "dsh-ssh-tui";
|
|
8
11
|
export declare function compareSemver(a: string, b: string): number;
|
|
9
12
|
export declare function resolvePluginProfileName(env?: NodeJS.ProcessEnv): string;
|
|
10
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The install command for one already-resolved version.
|
|
15
|
+
*
|
|
16
|
+
* The version is the number `checkForPluginUpdate` read from the registry, not
|
|
17
|
+
* the `latest` dist-tag: handing pnpm the tag makes it resolve the release a
|
|
18
|
+
* second time, and a release younger than `minimumReleaseAge` is then skipped
|
|
19
|
+
* without an error. A caller with no resolved version passes `@latest`, which
|
|
20
|
+
* is still better than a bare name.
|
|
21
|
+
* @param profile - the profile the plugin is installed into.
|
|
22
|
+
* @param version - the exact version to install.
|
|
23
|
+
* @returns the command, safe to show the user and to run.
|
|
24
|
+
*/
|
|
25
|
+
export declare function pluginUpgradeCommand(profile?: string, version?: string): string;
|
|
11
26
|
export declare function formatUpdateNotice(current: string, latest: string, profile?: string): string;
|
|
12
27
|
export declare function fetchLatestNpmVersion(fetchImpl?: typeof fetch): Promise<string | undefined>;
|
|
13
28
|
export interface PluginUpdateInfo {
|
|
@@ -50,7 +65,7 @@ export declare function resolveDshInvocation(options?: {
|
|
|
50
65
|
* hold the fallback together.
|
|
51
66
|
*/
|
|
52
67
|
export declare function shellSafeProfile(profile: string): string;
|
|
53
|
-
export declare function installPluginLatest(profile?: string, deps?: {
|
|
68
|
+
export declare function installPluginLatest(profile?: string, version?: string, deps?: {
|
|
54
69
|
invocation?: DshInvocation;
|
|
55
70
|
spawnFn?: typeof spawn;
|
|
56
71
|
}): Promise<{
|