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.
- package/README.en.md +28 -5
- package/README.md +76 -6
- package/lib/attach.js +156 -0
- package/lib/attach.js.map +1 -0
- package/lib/commands.js +73 -0
- package/lib/commands.js.map +1 -0
- package/lib/diag.js +292 -0
- package/lib/diag.js.map +1 -0
- package/lib/dialogs.js +82 -0
- package/lib/dialogs.js.map +1 -0
- package/lib/display-sock.js +304 -86
- package/lib/display-sock.js.map +1 -1
- package/lib/dsh-compat.js +25 -4
- package/lib/dsh-compat.js.map +1 -1
- package/lib/i18n/en.js +55 -0
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/index.js +2 -0
- package/lib/i18n/index.js.map +1 -1
- package/lib/i18n/zh.js +55 -0
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +75 -73
- package/lib/index.js.map +1 -1
- package/lib/paint.js +20 -38
- package/lib/paint.js.map +1 -1
- package/lib/picker.js +152 -37
- package/lib/picker.js.map +1 -1
- package/lib/rows.js +120 -0
- package/lib/rows.js.map +1 -0
- package/lib/session-index.js +4 -1
- package/lib/session-index.js.map +1 -1
- package/lib/session-list.js +372 -167
- package/lib/session-list.js.map +1 -1
- package/lib/session-lock.js +20 -9
- package/lib/session-lock.js.map +1 -1
- package/lib/stats.js +136 -0
- package/lib/stats.js.map +1 -0
- package/lib/terminal-input.js +470 -0
- package/lib/terminal-input.js.map +1 -0
- package/lib/tui.js +257 -311
- package/lib/tui.js.map +1 -1
- package/lib/types/attach.d.ts +106 -0
- package/lib/types/commands.d.ts +103 -0
- package/lib/types/diag.d.ts +78 -0
- package/lib/types/dialogs.d.ts +79 -0
- package/lib/types/display-sock.d.ts +59 -3
- package/lib/types/dsh-compat.d.ts +7 -0
- package/lib/types/i18n/index.d.ts +4 -0
- package/lib/types/index.d.ts +8 -4
- package/lib/types/paint.d.ts +6 -3
- package/lib/types/picker.d.ts +27 -7
- package/lib/types/rows.d.ts +69 -0
- package/lib/types/session-list.d.ts +43 -3
- package/lib/types/stats.d.ts +98 -0
- package/lib/types/terminal-input.d.ts +164 -0
- package/lib/types/tui.d.ts +33 -8
- 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
|
|
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
|
|
65
|
-
* after each later inspect batch. Unchanged logs reuse
|
|
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
|
+
}
|
package/lib/types/tui.d.ts
CHANGED
|
@@ -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
|
-
|
|
183
|
-
private
|
|
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.
|
|
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": [
|
|
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
|
}
|