dsh-ssh-tui 0.5.9 → 0.6.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.
Files changed (56) hide show
  1. package/README.en.md +28 -5
  2. package/README.md +76 -6
  3. package/lib/attach.js +156 -0
  4. package/lib/attach.js.map +1 -0
  5. package/lib/commands.js +73 -0
  6. package/lib/commands.js.map +1 -0
  7. package/lib/diag.js +292 -0
  8. package/lib/diag.js.map +1 -0
  9. package/lib/dialogs.js +82 -0
  10. package/lib/dialogs.js.map +1 -0
  11. package/lib/display-sock.js +304 -86
  12. package/lib/display-sock.js.map +1 -1
  13. package/lib/dsh-compat.js +25 -4
  14. package/lib/dsh-compat.js.map +1 -1
  15. package/lib/i18n/en.js +55 -0
  16. package/lib/i18n/en.js.map +1 -1
  17. package/lib/i18n/index.js +2 -0
  18. package/lib/i18n/index.js.map +1 -1
  19. package/lib/i18n/zh.js +55 -0
  20. package/lib/i18n/zh.js.map +1 -1
  21. package/lib/index.js +75 -73
  22. package/lib/index.js.map +1 -1
  23. package/lib/paint.js +20 -38
  24. package/lib/paint.js.map +1 -1
  25. package/lib/picker.js +152 -37
  26. package/lib/picker.js.map +1 -1
  27. package/lib/rows.js +120 -0
  28. package/lib/rows.js.map +1 -0
  29. package/lib/session-index.js +4 -1
  30. package/lib/session-index.js.map +1 -1
  31. package/lib/session-list.js +372 -167
  32. package/lib/session-list.js.map +1 -1
  33. package/lib/session-lock.js +20 -9
  34. package/lib/session-lock.js.map +1 -1
  35. package/lib/stats.js +136 -0
  36. package/lib/stats.js.map +1 -0
  37. package/lib/terminal-input.js +470 -0
  38. package/lib/terminal-input.js.map +1 -0
  39. package/lib/tui.js +257 -311
  40. package/lib/tui.js.map +1 -1
  41. package/lib/types/attach.d.ts +106 -0
  42. package/lib/types/commands.d.ts +103 -0
  43. package/lib/types/diag.d.ts +78 -0
  44. package/lib/types/dialogs.d.ts +79 -0
  45. package/lib/types/display-sock.d.ts +59 -3
  46. package/lib/types/dsh-compat.d.ts +7 -0
  47. package/lib/types/i18n/index.d.ts +4 -0
  48. package/lib/types/index.d.ts +8 -4
  49. package/lib/types/paint.d.ts +6 -3
  50. package/lib/types/picker.d.ts +27 -7
  51. package/lib/types/rows.d.ts +69 -0
  52. package/lib/types/session-list.d.ts +43 -3
  53. package/lib/types/stats.d.ts +98 -0
  54. package/lib/types/terminal-input.d.ts +164 -0
  55. package/lib/types/tui.d.ts +33 -8
  56. package/package.json +11 -5
@@ -0,0 +1,69 @@
1
+ import type { PlanTodoItem, Row, ToolDiffHunk } from './transcript-types.js';
2
+ type ToolRow = Extract<Row, {
3
+ kind: 'tool';
4
+ }>;
5
+ type PlanRow = Extract<Row, {
6
+ kind: 'plan';
7
+ }>;
8
+ /** Long sessions keep the newest rows; older ones are dropped from the front. */
9
+ export declare const MAX_TRANSCRIPT_ROWS = 5000;
10
+ /**
11
+ * Drop the oldest rows past `max` and report how many went. The caller owns the
12
+ * things that point at rows (the focused card, clickable maps), so they can be
13
+ * invalidated with that count.
14
+ */
15
+ export declare function boundTranscriptRows(rows: Row[], max?: number): number;
16
+ export declare function findToolRowByCallId(rows: readonly Row[], callId: string): ToolRow | undefined;
17
+ /** The card a repeated call of the same tool should merge into, if any. */
18
+ export declare function findMergeableToolRow(rows: readonly Row[], next: {
19
+ name: string;
20
+ args: string;
21
+ }): ToolRow | undefined;
22
+ /**
23
+ * Fold a repeated call into the card that is already on screen: the card grows
24
+ * a repeat mark and starts over as `running`. `now` is passed in so the flip
25
+ * animation is testable.
26
+ */
27
+ export declare function mergeToolCard(previous: ToolRow, next: {
28
+ callId: string;
29
+ name: string;
30
+ args: string;
31
+ title: string;
32
+ summary: string;
33
+ diff?: ToolDiffHunk[];
34
+ }, now: number, replaying?: boolean): void;
35
+ export declare function findLivePlanRow(rows: readonly Row[]): PlanRow | undefined;
36
+ /** Whether the plan card should open on its own when it has work left. */
37
+ export declare function planShouldDefaultExpand(plan: {
38
+ active?: boolean;
39
+ pending?: boolean;
40
+ todos: readonly PlanTodoItem[];
41
+ }): boolean;
42
+ /** Older / finished plans stay in the scrolling transcript, greyed out. */
43
+ export declare function archiveStalePlans(rows: readonly Row[], keep?: PlanRow): void;
44
+ export interface TranscriptWindow<T> {
45
+ /** Index of the first transcript line on screen; a fresh window means a repaint. */
46
+ start: number;
47
+ /** Clamped scroll offset; the caller stores it back for the next paint. */
48
+ scrollOffset: number;
49
+ visibleLines: string[];
50
+ visibleRefs: (T | undefined)[];
51
+ /** Blank lines added above so a short transcript sits against the chrome. */
52
+ padding: number;
53
+ }
54
+ /**
55
+ * Pick the slice of the transcript the terminal shows.
56
+ *
57
+ * `available` is the row budget the chrome left over; `reveal` is a row a fresh
58
+ * message wants on screen, which wins over the current scroll position. A
59
+ * transcript shorter than the budget is padded at the top so the prompt stays
60
+ * at the bottom instead of floating in the middle of the screen.
61
+ */
62
+ export declare function windowTranscript<T>(input: {
63
+ lines: readonly string[];
64
+ refs: readonly (T | undefined)[];
65
+ available: number;
66
+ scrollOffset: number;
67
+ reveal?: T | undefined;
68
+ }): TranscriptWindow<T>;
69
+ export {};
@@ -34,16 +34,55 @@ export interface ResumableSession {
34
34
  sock: string;
35
35
  state?: string;
36
36
  };
37
+ /**
38
+ * `label` is a placeholder (the raw session id) because the log has not been
39
+ * inspected yet. The picker holds these back instead of painting an id that
40
+ * turns into a title two seconds later.
41
+ */
42
+ labelPending?: boolean;
37
43
  }
38
44
  /** `MM-DD HH:mm` local-time label for session lists. */
39
45
  export declare function formatSessionTime(timestamp: number): string;
40
- /** Incremental listing so the picker can paint before older logs are parsed. */
46
+ /** Incremental listing so a caller can paint before older logs are parsed. */
41
47
  export interface ResumableSessionListing {
42
48
  /** Sessions already inspected (or restored from the disk cache). */
43
49
  sessions: ResumableSession[];
44
50
  /** Whether older logs are still being inspected. */
45
51
  pending: boolean;
46
52
  }
53
+ /** One lazy page: everything read so far, plus what is still uninspected. */
54
+ export interface ResumableSessionPage {
55
+ /** Resolved sessions in display order; a label here is never a placeholder. */
56
+ sessions: ResumableSession[];
57
+ /** Candidates not inspected yet (an upper bound on rows still to come). */
58
+ remaining: number;
59
+ /** No candidates are left to inspect. */
60
+ done: boolean;
61
+ }
62
+ /**
63
+ * A listing that grows on demand.
64
+ *
65
+ * The picker reads one page, paints it, and only reads on when the user reaches
66
+ * for older sessions. A launch used to inspect every log before the first
67
+ * frame, which is what made a large history feel like a hang; the first page
68
+ * alone also has every title resolved, so nothing on screen is ever a raw id
69
+ * that turns into a title a second later.
70
+ */
71
+ export interface ResumableSessionPager {
72
+ /** Inspect onward until `size` more rows exist, or history runs out. */
73
+ page(size?: number): Promise<ResumableSessionPage>;
74
+ /** Read every remaining candidate (the `/resume` flow wants the whole list). */
75
+ complete(): Promise<ResumableSession[]>;
76
+ }
77
+ /** Sessions read before the picker's first paint, and per lazy page after it. */
78
+ export declare const PICKER_PAGE_SIZE = 9;
79
+ /**
80
+ * Open a pager over the store. Nothing is inspected until the first `page()`.
81
+ */
82
+ export declare function openResumableSessionPager(persistence: object, currentId: string, options?: {
83
+ listHosts?: typeof listAttachableHosts;
84
+ indexPath?: string;
85
+ }): Promise<ResumableSessionPager>;
47
86
  /**
48
87
  * List resumable top-level sessions, newest first.
49
88
  *
@@ -61,8 +100,9 @@ export declare function listResumableSessions(persistence: object, currentId: st
61
100
  /**
62
101
  * List resumable sessions, painting the recent page first.
63
102
  *
64
- * `onUpdate` fires after the priority page (cached + newest logs) and again
65
- * after each later inspect batch. Unchanged logs reuse `$DSH_HOME/tui-session-index.json`.
103
+ * `onUpdate` fires after the header sketch, after the first resolved title, and
104
+ * again after each later inspect batch. Unchanged logs reuse
105
+ * `$DSH_HOME/tui-session-index.json`.
66
106
  */
67
107
  export declare function listResumableSessionsProgressive(persistence: object, currentId: string, options?: {
68
108
  listHosts?: typeof listAttachableHosts;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Session statistics: turns/steps, LLM and tool time, TTFT, decode rate, and
3
+ * token usage.
4
+ *
5
+ * `tui.ts` owns the rendering; this owns the arithmetic. The rules that used to
6
+ * live among the event handlers are what make the footer honest, so they live
7
+ * here with tests instead of inside a 7k-line class:
8
+ *
9
+ * - a repeated usage report for the same step replaces its sample (hosts report
10
+ * usage per step more than once, and naive adding double counts),
11
+ * - TTFT latches on the *first* token delta of the open step,
12
+ * - the settled `assistant/message` packed stream wins over that latch (a
13
+ * retried step would otherwise span both attempts and report a rate ~20x off),
14
+ * - decode rate needs a finite, non-negative output token count.
15
+ */
16
+ import type { TokenUsage } from '@deepseek-ai/dsh-llm';
17
+ export interface SessionUsage {
18
+ inputTokens: number;
19
+ outputTokens: number;
20
+ cacheReadTokens: number;
21
+ cacheWriteTokens: number;
22
+ }
23
+ export interface SessionStatsSnapshot {
24
+ turns: number;
25
+ steps: number;
26
+ llmMs: number;
27
+ toolMs: number;
28
+ ttftMs: number;
29
+ ttftSteps: number;
30
+ decodeMs: number;
31
+ decodeTokens: number;
32
+ usage: SessionUsage;
33
+ }
34
+ /** Flat view of the same numbers, for the footer's stats line. */
35
+ export interface SessionStatsRow {
36
+ turns: number;
37
+ steps: number;
38
+ llmMs: number;
39
+ toolMs: number;
40
+ ttftMs: number;
41
+ ttftSteps: number;
42
+ decodeMs: number;
43
+ decodeTokens: number;
44
+ inputTokens: number;
45
+ outputTokens: number;
46
+ cacheReadTokens: number;
47
+ cacheWriteTokens: number;
48
+ }
49
+ export interface OpenStep {
50
+ turn: number;
51
+ step: number;
52
+ }
53
+ export declare function emptySessionUsage(): SessionUsage;
54
+ export declare function emptySessionStats(): SessionStatsSnapshot;
55
+ /** Flatten a snapshot into the footer's row shape. */
56
+ export declare function statsRowOf(stats: SessionStatsSnapshot): SessionStatsRow;
57
+ /**
58
+ * Accumulates the stats the footer and `/status` show as session events arrive.
59
+ * Every method is a no-op for events that do not apply, so callers can forward
60
+ * the raw event stream without checking.
61
+ */
62
+ export declare class SessionStatsTracker {
63
+ private stats;
64
+ private openStep;
65
+ private readonly usageByStep;
66
+ private readonly pendingToolTimes;
67
+ private lastTurn;
68
+ /** The step a live chunk belongs to when the host frame carried none. */
69
+ currentStep(): OpenStep | undefined;
70
+ /** `step/start`: opens the clock LLM time, TTFT and decode are measured from. */
71
+ noteStepStart(turn: number, step: number, time: number): void;
72
+ /** The first token delta of the open step latches its TTFT clock. */
73
+ noteFirstToken(turn: number, step: number, time: number): void;
74
+ /** Replace one step's usage sample so a repeated report never double counts. */
75
+ recordUsage(turn: number, step: number, usage: TokenUsage): void;
76
+ /**
77
+ * `assistant/message`: settle the open step's LLM time, TTFT and decode rate.
78
+ * `firstTokenTime` is the settlement's own packed-stream answer when it has
79
+ * one; `undefined` falls back to the live latch.
80
+ */
81
+ settleMessage(input: {
82
+ turn: number;
83
+ step: number;
84
+ time: number;
85
+ firstTokenTime?: number | undefined;
86
+ outputTokens?: number | undefined;
87
+ }): void;
88
+ /** `tool/call`: start the tool clock for this call. */
89
+ noteToolStart(callId: string, time: number): void;
90
+ /** `tool/result`: add the elapsed tool time, once. */
91
+ noteToolEnd(callId: string, time: number): void;
92
+ /** `step/end`: count the step, and forget the step's usage dedupe key. */
93
+ noteStepEnd(turn: number, step: number): void;
94
+ /** `turn/end`: a tool whose result never arrived contributes nothing. */
95
+ noteTurnEnd(): void;
96
+ /** Current totals. A copy: callers cannot disturb the accumulator. */
97
+ snapshot(): SessionStatsSnapshot;
98
+ }
@@ -0,0 +1,164 @@
1
+ /** True when this process is talking to a terminal through SSH. */
2
+ export declare function detectSshSession(env?: NodeJS.ProcessEnv): boolean;
3
+ /** What a probe writes to ask the terminal where its cursor is. */
4
+ export declare const CURSOR_POSITION_REQUEST = "\u001B[6n";
5
+ /**
6
+ * How long a half-arrived reply is held before it is handed over as typing.
7
+ * A real sequence arrives within one read or the next; anything slower is
8
+ * either a very unlucky split or the start of a user's own escape key.
9
+ */
10
+ export declare const INPUT_HOLD_MS = 30;
11
+ /** A single probe answer that misses this window counts as "no reply". */
12
+ export declare const RTT_SAMPLE_TIMEOUT_MS = 350;
13
+ /** Wider window for the rest of the measurement once one has been missed. */
14
+ export declare const RTT_SLOW_SAMPLE_TIMEOUT_MS = 800;
15
+ /** Answers to collect before one is reported. */
16
+ export declare const RTT_SAMPLE_COUNT = 3;
17
+ /** Two answers this close already describe the link; the third is not needed. */
18
+ export declare const RTT_AGREEMENT_MS = 6;
19
+ /** Wall-clock ceiling for one measurement, however chatty the terminal is. */
20
+ export declare const RTT_MEASURE_BUDGET_MS = 2500;
21
+ /**
22
+ * An SSH answer that came back this much faster than the slowest sample cannot
23
+ * have made the round trip — it was already on the wire when we asked. Relative
24
+ * on purpose: `ssh localhost` honestly answers in about 2 ms.
25
+ */
26
+ export declare const RTT_OUTLIER_RATIO = 0.25;
27
+ /** Remove every complete cursor reply from `text`. */
28
+ export declare function stripCursorReplies(text: string): {
29
+ text: string;
30
+ replies: number;
31
+ };
32
+ /**
33
+ * Drop cursor replies from a byte stream that is otherwise user input.
34
+ *
35
+ * Feed every read to `push()`; the bytes it returns are what the user actually
36
+ * typed. A read that ends in the middle of a possible reply is held back (see
37
+ * `flush()`) so `ESC [ 1` + `7;1R` cannot become visible digits either.
38
+ */
39
+ export declare class TerminalInputFilter {
40
+ private held;
41
+ push(text: string): {
42
+ forward: string;
43
+ replies: number;
44
+ };
45
+ /** Release a held partial (its window passed, so it was not a reply). */
46
+ flush(): string;
47
+ /** Forget a held partial — used when the caller knows it must be stale. */
48
+ reset(): void;
49
+ get pending(): boolean;
50
+ }
51
+ /**
52
+ * The filter with the hold timer attached, for a caller that turns bytes into
53
+ * keystrokes (the TUI and the session picker) instead of forwarding them.
54
+ */
55
+ export declare class TerminalInputGuard {
56
+ private readonly emit;
57
+ private readonly holdMs;
58
+ private readonly filter;
59
+ private timer;
60
+ constructor(emit: (text: string) => void, holdMs?: number);
61
+ /** Feed one decoded read; anything that is not a reply reaches `emit`. */
62
+ push(text: string): void;
63
+ /** Hand a half-arrived sequence over (its window passed; it is typing). */
64
+ release(): void;
65
+ /** Drop the timer and any held bytes (display detached, picker settled). */
66
+ stop(): void;
67
+ }
68
+ export interface TerminalInputPumpOptions {
69
+ stdin: NodeJS.ReadStream;
70
+ stdout: NodeJS.WriteStream;
71
+ /** Receives bytes that are real typing, never probe answers. */
72
+ onInput: (text: string) => void;
73
+ /** Link kind; defaults to the SSH environment of this process. */
74
+ ssh?: boolean;
75
+ holdMs?: number;
76
+ sampleTimeoutMs?: number;
77
+ samples?: number;
78
+ /** Give up sampling after this long; the attach must not wait forever. */
79
+ budgetMs?: number;
80
+ /** One line per probe attempt, for `DSH_TUI_DEBUG=1` troubleshooting. */
81
+ debug?: (message: string) => void;
82
+ }
83
+ /**
84
+ * stdin → replies (for the probe) + real input (for the caller).
85
+ *
86
+ * Long-lived on purpose: the relay keeps it running for the whole attachment,
87
+ * so a reply that arrives seconds late is dropped instead of reaching the Host.
88
+ */
89
+ export declare class TerminalInputPump {
90
+ private readonly options;
91
+ private readonly filter;
92
+ private readonly decoder;
93
+ private readonly waiters;
94
+ /** When the last answer (accepted or not) arrived; drives the quiet wait. */
95
+ private lastAnswerAt;
96
+ /** When the last request went out; an answer may follow it for a window. */
97
+ private lastRequestAt;
98
+ /** Current answer window; widened once when the first one is missed. */
99
+ private windowMs;
100
+ /** Answers seen, used to tell "slow terminal" from "terminal never answers". */
101
+ private answersSeen;
102
+ /** Slowest sample so far; the quiet wait has to clear the worst of them. */
103
+ private slowestSampleMs;
104
+ private holdTimer;
105
+ private listening;
106
+ readonly ssh: boolean;
107
+ constructor(options: TerminalInputPumpOptions);
108
+ start(): void;
109
+ stop(): void;
110
+ /**
111
+ * Ask the terminal for its cursor a few times and report the round-trip that
112
+ * best describes the link. `undefined` means the terminal never answered in
113
+ * time (a dumb pipe), and the caller must not treat that as "0 ms".
114
+ */
115
+ measure(): Promise<number | undefined>;
116
+ /**
117
+ * Drop an answer that came back far too fast to have made the round trip —
118
+ * an answer to somebody else's request, still on the wire when we asked.
119
+ * Relative, not absolute (`ssh localhost` honestly answers in 2 ms), and
120
+ * measured against the *median*: comparing with the slowest sample would
121
+ * throw away three good answers because one request sat behind a repaint.
122
+ */
123
+ private dropFastOutliers;
124
+ /**
125
+ * How long the line must be quiet before the next request may go out.
126
+ *
127
+ * The only safe answer is the full sample window: a request we sent can be
128
+ * answered at any point inside it, so anything shorter can attribute that
129
+ * answer to the request that follows. Deriving it from the samples instead
130
+ * looks cheaper and is exactly what goes wrong — a leftover answer reports
131
+ * 1 ms, the window shrinks to the minimum, and the honest answer to the
132
+ * abandoned request lands in the *next* window (250 ms link reported as 1 ms
133
+ * and 0 ms, two "agreeing" samples, early stop, chip frozen on a wrong value).
134
+ */
135
+ private quietMs;
136
+ /**
137
+ * Wait until the line has been quiet for a full window, measured from the
138
+ * later of the last answer and the last request. An answer to a request we
139
+ * already gave up on lands in here and is thrown away, so it cannot be
140
+ * attributed to the request that follows it.
141
+ */
142
+ private quiet;
143
+ /**
144
+ * Consume what the kernel already holds, so an answer that was on its way
145
+ * before this request cannot be read as its answer.
146
+ *
147
+ * `read()` emits `data` for the chunk it returns whenever a listener is
148
+ * attached, and the listener path would then deliver the same bytes twice
149
+ * (a typed `hi` arrived at the Host as `hihi`). Detach for the duration of
150
+ * the loop: it is synchronous, so nothing can be lost in between.
151
+ */
152
+ private drain;
153
+ private ask;
154
+ private readonly onData;
155
+ private feed;
156
+ /**
157
+ * One reply: hand it to the oldest outstanding request. An answer that
158
+ * belongs to a request we already gave up on is simply dropped here — and
159
+ * its arrival time still restarts the quiet window, which is what keeps it
160
+ * from being attributed to the next request.
161
+ */
162
+ private answer;
163
+ private scheduleHold;
164
+ }
@@ -137,6 +137,9 @@ export declare class SshTui {
137
137
  private readonly headlessDisplay;
138
138
  private disconnectPolicy;
139
139
  private detachedIdleTimer;
140
+ /** Armed once an SSH drop left this Host alive with work still running. */
141
+ private hostKeptAlive;
142
+ private idleExitTimer;
140
143
  private displayDetached;
141
144
  private displayHost;
142
145
  private relayColumns;
@@ -179,17 +182,16 @@ export declare class SshTui {
179
182
  private openToolCalls;
180
183
  /** Survives result settlement so a card-less result can still be labelled. */
181
184
  private toolCallNames;
182
- private readonly stats;
183
- private openStepStats;
185
+ /** Session totals; the tracker owns the arithmetic (see `stats.ts`). */
186
+ private readonly statsTracker;
187
+ /** Snapshot of the session totals, for the footer and `/status`. */
188
+ private get stats();
184
189
  /** Attempt whose live `start` frame opened the current token stream. */
185
190
  private liveStreamOwner;
186
191
  /** Live events parked while the (yielding) history replay holds the floor. */
187
192
  private replayQueue;
188
193
  /** A relay claimed the display while a hangup was still cancelling/flushing. */
189
194
  private reattachedDuringHangup;
190
- private readonly pendingToolTimes;
191
- private readonly usageByStep;
192
- private lastStatsTurn;
193
195
  private scrollOffset;
194
196
  private readonly clickableRows;
195
197
  private readonly paintedLinkHitsByRow;
@@ -201,6 +203,12 @@ export declare class SshTui {
201
203
  private streamingReasoning;
202
204
  private escapeBuffer;
203
205
  private escapeTimer;
206
+ /**
207
+ * Cursor-position replies removed from the relay's stdin stream. A launcher
208
+ * from an older release (or a reply that raced its own probe) would otherwise
209
+ * type `[17;1R` into the prompt or cancel a dialog with a bare ESC.
210
+ */
211
+ private readonly inputGuard;
204
212
  private thinkingStartedAt;
205
213
  private waitStartedAt;
206
214
  private completionSignaled;
@@ -256,6 +264,19 @@ export declare class SshTui {
256
264
  private detachedIdleMs;
257
265
  private clearDetachedIdleTimer;
258
266
  private armDetachedIdleTimer;
267
+ /**
268
+ * How long a leftover, finished Host may sit with no display before it exits
269
+ * and hands the session back. `0` disables the exit (legacy behavior).
270
+ */
271
+ private idleExitMs;
272
+ private clearIdleExitTimer;
273
+ /**
274
+ * Arm the exit for a Host a busy SSH drop left behind. Called when the agent
275
+ * goes idle — the reason the Host was kept is gone, and every second it stays
276
+ * is a second its `session.lock` keeps the browser surface from opening the
277
+ * session (`resume failed for session "…"`). A reattach cancels it.
278
+ */
279
+ private armIdleExitTimer;
259
280
  private isCompactView;
260
281
  /** Test helper: switch the workspace view without going through /view. */
261
282
  setWorkspaceView(view: WorkspaceView): void;
@@ -282,6 +303,12 @@ export declare class SshTui {
282
303
  detachDisplay(): void;
283
304
  /** Restore the terminal and drop event wiring. Does not flush or exit. */
284
305
  dispose(): Promise<void>;
306
+ /**
307
+ * `/diag`: collect the local facts about this session's channel, lock, and
308
+ * Host, then print the decision chain. Everything is local; nothing is sent
309
+ * anywhere (see the privacy note in the README).
310
+ */
311
+ private runDiagCommand;
285
312
  /** Human-facing exit with goodbye and flush; called from key handling. */
286
313
  requestExit(code: number): Promise<void>;
287
314
  /**
@@ -346,7 +373,6 @@ export declare class SshTui {
346
373
  private beginWait;
347
374
  private endWait;
348
375
  private waitCardSource;
349
- private planShouldDefaultExpand;
350
376
  private findSubagentRow;
351
377
  private findLivePlanRow;
352
378
  /** Older / finished plans stay in the scrolling transcript. */
@@ -386,8 +412,6 @@ export declare class SshTui {
386
412
  private suggestionsVisible;
387
413
  private currentProviderId;
388
414
  private currentSelectionLabel;
389
- /** Replace one step's usage sample so a repeated report never double counts. */
390
- private recordUsage;
391
415
  /** Compact session stats groups for the first footer row. */
392
416
  private statsText;
393
417
  /** Refresh the terminal window title (throttled while running). */
@@ -569,6 +593,7 @@ export declare class SshTui {
569
593
  private resolveSuperGrokToken;
570
594
  private fetchJson;
571
595
  private readonly handleData;
596
+ private handleInputText;
572
597
  /** Handle one data chunk that may contain bracketed-paste markers. */
573
598
  private processPasteChunk;
574
599
  /** Insert pasted text into the input buffer; CR/LF are literal newlines. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ssh-tui",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "description": "SSH-friendly interactive terminal TUI plugin for DeepSeek Harness",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -64,9 +64,12 @@
64
64
  "0.1.3-alpha.2": "compatible",
65
65
  "0.1.5-alpha.1": "compatible",
66
66
  "0.1.5-alpha.2": "compatible",
67
- "0.1.5-rc.1": "compatible"
67
+ "0.1.5-rc.1": "compatible",
68
+ "0.1.5-rc.2": "compatible"
68
69
  },
69
- "profiles": ["tui"]
70
+ "profiles": [
71
+ "tui"
72
+ ]
70
73
  }
71
74
  },
72
75
  "scripts": {
@@ -102,11 +105,11 @@
102
105
  "@deepseek-ai/dsh-credentials": ">=0.1.2-rc.1 <0.1.6 || >=0.1.3-alpha.2 <0.1.6 || >=0.1.5-alpha.1 <0.1.6",
103
106
  "@deepseek-ai/dsh-llm": ">=0.1.2-rc.1 <0.1.6 || >=0.1.3-alpha.2 <0.1.6 || >=0.1.5-alpha.1 <0.1.6",
104
107
  "@deepseek-ai/dsh-session": ">=0.1.2-rc.1 <0.1.6 || >=0.1.3-alpha.2 <0.1.6 || >=0.1.5-alpha.1 <0.1.6",
108
+ "@deepseek-ai/dsh-session-persistence": ">=0.1.2-rc.1 <0.1.6 || >=0.1.3-alpha.2 <0.1.6 || >=0.1.5-alpha.1 <0.1.6",
105
109
  "@deepseek-ai/dsh-settings": ">=0.1.2-rc.1 <0.1.6 || >=0.1.3-alpha.2 <0.1.6 || >=0.1.5-alpha.1 <0.1.6",
106
110
  "@deepseek-ai/dsh-subagent": ">=0.1.2-rc.1 <0.1.6 || >=0.1.3-alpha.2 <0.1.6 || >=0.1.5-alpha.1 <0.1.6",
107
111
  "@deepseek-ai/dsh-user-approval": ">=0.1.2-rc.1 <0.1.6 || >=0.1.3-alpha.2 <0.1.6 || >=0.1.5-alpha.1 <0.1.6",
108
- "@deepseek-ai/dsh-user-questions": ">=0.1.2-rc.1 <0.1.6 || >=0.1.3-alpha.2 <0.1.6 || >=0.1.5-alpha.1 <0.1.6",
109
- "@deepseek-ai/dsh-session-persistence": ">=0.1.2-rc.1 <0.1.6 || >=0.1.3-alpha.2 <0.1.6 || >=0.1.5-alpha.1 <0.1.6"
112
+ "@deepseek-ai/dsh-user-questions": ">=0.1.2-rc.1 <0.1.6 || >=0.1.3-alpha.2 <0.1.6 || >=0.1.5-alpha.1 <0.1.6"
110
113
  },
111
114
  "peerDependenciesMeta": {
112
115
  "@deepseek-ai/dsh-user-approval": {
@@ -125,6 +128,7 @@
125
128
  "devDependencies": {
126
129
  "@deepseek-ai/cordis": "^4.0.1",
127
130
  "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
131
+ "@deepseek-ai/dsh": "0.1.5-rc.1",
128
132
  "@deepseek-ai/dsh-agent": "0.1.5-rc.1",
129
133
  "@deepseek-ai/dsh-agent-default-model": "0.1.5-rc.1",
130
134
  "@deepseek-ai/dsh-agent-loop": "0.1.5-rc.1",
@@ -139,6 +143,7 @@
139
143
  "@deepseek-ai/dsh-invariants": "0.1.5-rc.1",
140
144
  "@deepseek-ai/dsh-jobs": "0.1.5-rc.1",
141
145
  "@deepseek-ai/dsh-llm": "0.1.5-rc.1",
146
+ "@deepseek-ai/dsh-llm-mock-server": "^0.1.5-rc.1",
142
147
  "@deepseek-ai/dsh-sandbox": "0.1.5-rc.1",
143
148
  "@deepseek-ai/dsh-sandbox-policy": "0.1.5-rc.1",
144
149
  "@deepseek-ai/dsh-scope": "0.1.5-rc.1",
@@ -155,6 +160,7 @@
155
160
  "@deepseek-ai/dsh-user-approval": "0.1.5-rc.1",
156
161
  "@deepseek-ai/dsh-user-questions": "0.1.5-rc.1",
157
162
  "@types/node": "^24.0.0",
163
+ "@xterm/headless": "^6.0.0",
158
164
  "semver": "^7.8.5",
159
165
  "typescript": "^5.9.0"
160
166
  }