dsh-ssh-tui 0.8.0 → 0.8.2-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +150 -0
- package/README.en.md +74 -8
- package/README.md +451 -577
- package/docs/display-mode.md +122 -0
- package/docs/release-notes-0.7.4.md +91 -0
- package/docs/release-notes-0.8.0.md +56 -0
- package/docs/release-notes-0.8.1.md +79 -0
- package/docs/release-notes-0.8.2-rc.1.md +159 -0
- package/docs/remote-ops.md +51 -0
- package/docs/terminals.md +53 -0
- package/docs/windows.md +13 -0
- package/lib/approval-state.js +148 -0
- package/lib/approval-state.js.map +1 -0
- package/lib/attach.js +4 -4
- package/lib/attach.js.map +1 -1
- package/lib/auth-failure.js +74 -0
- package/lib/auth-failure.js.map +1 -1
- package/lib/color-depth.js +26 -0
- package/lib/color-depth.js.map +1 -1
- package/lib/commands.js +2 -0
- package/lib/commands.js.map +1 -1
- package/lib/copy-text.js +30 -8
- package/lib/copy-text.js.map +1 -1
- package/lib/dialogs.js +124 -0
- package/lib/dialogs.js.map +1 -1
- package/lib/display-mode.js +147 -0
- package/lib/display-mode.js.map +1 -0
- package/lib/display-sock.js +490 -35
- package/lib/display-sock.js.map +1 -1
- package/lib/footer-accents.js +28 -0
- package/lib/footer-accents.js.map +1 -0
- package/lib/footer-budget.js +328 -0
- package/lib/footer-budget.js.map +1 -0
- package/lib/footer-format.js +433 -0
- package/lib/footer-format.js.map +1 -0
- package/lib/footer-layout.js +407 -0
- package/lib/footer-layout.js.map +1 -0
- package/lib/footer-meters.js +185 -0
- package/lib/footer-meters.js.map +1 -0
- package/lib/footer.js +19 -561
- package/lib/footer.js.map +1 -1
- package/lib/glyph-measure.js +92 -0
- package/lib/glyph-measure.js.map +1 -0
- package/lib/i18n/en.js +83 -7
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/zh.js +85 -7
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +73 -5
- package/lib/index.js.map +1 -1
- package/lib/paint.js +75 -17
- package/lib/paint.js.map +1 -1
- package/lib/picker.js +14 -13
- package/lib/picker.js.map +1 -1
- package/lib/plan-projection.js +291 -0
- package/lib/plan-projection.js.map +1 -0
- package/lib/plan.js +61 -11
- package/lib/plan.js.map +1 -1
- package/lib/platform.js +96 -0
- package/lib/platform.js.map +1 -1
- package/lib/question-state.js +83 -0
- package/lib/question-state.js.map +1 -0
- package/lib/representation.js +283 -0
- package/lib/representation.js.map +1 -0
- package/lib/screen.js +105 -0
- package/lib/screen.js.map +1 -0
- package/lib/selection.js +5 -3
- package/lib/selection.js.map +1 -1
- package/lib/session-blank.js +81 -0
- package/lib/session-blank.js.map +1 -0
- package/lib/session-list.js +102 -81
- package/lib/session-list.js.map +1 -1
- package/lib/startup.js +56 -0
- package/lib/startup.js.map +1 -1
- package/lib/stats.js +101 -3
- package/lib/stats.js.map +1 -1
- package/lib/subagent-model.js +8 -7
- package/lib/subagent-model.js.map +1 -1
- package/lib/term-text.js +436 -69
- package/lib/term-text.js.map +1 -1
- package/lib/terminal-input.js +132 -10
- package/lib/terminal-input.js.map +1 -1
- package/lib/theme.js +335 -0
- package/lib/theme.js.map +1 -0
- package/lib/throughput.js +305 -0
- package/lib/throughput.js.map +1 -0
- package/lib/tool-present.js +67 -15
- package/lib/tool-present.js.map +1 -1
- package/lib/tui.js +4186 -1133
- package/lib/tui.js.map +1 -1
- package/lib/types/approval-state.d.ts +75 -0
- package/lib/types/attach.d.ts +6 -2
- package/lib/types/auth-failure.d.ts +55 -0
- package/lib/types/color-depth.d.ts +21 -0
- package/lib/types/commands.d.ts +6 -0
- package/lib/types/copy-text.d.ts +18 -1
- package/lib/types/dialogs.d.ts +147 -1
- package/lib/types/display-mode.d.ts +99 -0
- package/lib/types/display-sock.d.ts +90 -0
- package/lib/types/footer-accents.d.ts +22 -0
- package/lib/types/footer-budget.d.ts +204 -0
- package/lib/types/footer-format.d.ts +221 -0
- package/lib/types/footer-layout.d.ts +292 -0
- package/lib/types/footer-meters.d.ts +151 -0
- package/lib/types/footer.d.ts +19 -269
- package/lib/types/glyph-measure.d.ts +41 -0
- package/lib/types/index.d.ts +13 -0
- package/lib/types/paint.d.ts +48 -0
- package/lib/types/plan-projection.d.ts +162 -0
- package/lib/types/plan.d.ts +18 -2
- package/lib/types/platform.d.ts +83 -0
- package/lib/types/question-state.d.ts +96 -0
- package/lib/types/representation.d.ts +498 -0
- package/lib/types/screen.d.ts +126 -0
- package/lib/types/session-blank.d.ts +51 -0
- package/lib/types/session-list.d.ts +34 -0
- package/lib/types/startup.d.ts +30 -0
- package/lib/types/stats.d.ts +64 -1
- package/lib/types/subagent-model.d.ts +7 -6
- package/lib/types/term-text.d.ts +100 -24
- package/lib/types/terminal-input.d.ts +37 -0
- package/lib/types/theme.d.ts +109 -0
- package/lib/types/throughput.d.ts +163 -0
- package/lib/types/tool-present.d.ts +43 -2
- package/lib/types/transcript-types.d.ts +63 -2
- package/lib/types/tui.d.ts +960 -12
- package/package.json +73 -67
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Screen contract: a whole-view surface with its own navigation state.
|
|
3
|
+
*
|
|
4
|
+
* A **Surface** (ask-user, a picker) borrows a few rows of the workspace and gives
|
|
5
|
+
* them back when the action is done: the transcript behind it never moves, and
|
|
6
|
+
* the reader returns to the same place. A **Screen** is the other thing entirely —
|
|
7
|
+
* it replaces the workspace, owns its own scrolling, and must be able to redraw
|
|
8
|
+
* itself from its state alone (a reattach after an SSH drop is a normal event, not
|
|
9
|
+
* an edge case). See `docs/decisions/b2-architecture-decisions.md` (AD-1 … AD-3).
|
|
10
|
+
*
|
|
11
|
+
* Depth is exactly one: a Screen may hold tabs, sections and detail modes of its
|
|
12
|
+
* own, but it never pushes another Screen. Nothing here allocates a stack, and
|
|
13
|
+
* nothing here renders — this module is the contract, `tui.ts` is the renderer.
|
|
14
|
+
*/
|
|
15
|
+
import type { Dialog, InspectDialog } from './dialogs.js';
|
|
16
|
+
import type { DiffDisplayLine } from './tool-present.js';
|
|
17
|
+
/**
|
|
18
|
+
* Which Screen is up. Adding one here is the only way to add one at all.
|
|
19
|
+
*
|
|
20
|
+
* `setup` is the odd one: it draws no body of its own — the wizard's rows are built
|
|
21
|
+
* from its state machine every frame — but it is a Screen in every other sense. It
|
|
22
|
+
* owns the whole view, it owns its input, it never enters the dialog queue, and it is
|
|
23
|
+
* rebuilt from state after a reattach (B2.6).
|
|
24
|
+
*/
|
|
25
|
+
export type ScreenKind = 'inspect' | 'report' | 'setup';
|
|
26
|
+
/** The report a `report` Screen is showing. */
|
|
27
|
+
export type ReportKind = 'status' | 'usage' | 'diag' | 'doctor' | 'help' | 'subagents';
|
|
28
|
+
/**
|
|
29
|
+
* A Screen's whole state.
|
|
30
|
+
*
|
|
31
|
+
* Everything the renderer needs is here on purpose: nothing about the picture
|
|
32
|
+
* lives only in the paint loop, which is what makes a reattach able to rebuild the
|
|
33
|
+
* same screen, and what makes the screen testable without a terminal. `offset` is
|
|
34
|
+
* the first visible body row — the Screen's own scroll position, unrelated to the
|
|
35
|
+
* transcript's `scrollOffset` behind it.
|
|
36
|
+
*/
|
|
37
|
+
export interface ScreenState {
|
|
38
|
+
kind: ScreenKind;
|
|
39
|
+
/** Title row. */
|
|
40
|
+
title: string;
|
|
41
|
+
/** Body, already rendered for the width it will be shown at. */
|
|
42
|
+
lines: readonly DiffDisplayLine[];
|
|
43
|
+
/** First visible body row. */
|
|
44
|
+
offset: number;
|
|
45
|
+
/** Which report this is, when `kind === 'report'`. */
|
|
46
|
+
report?: ReportKind;
|
|
47
|
+
/** What the copy key takes while this screen is up. */
|
|
48
|
+
copyText?: string;
|
|
49
|
+
/**
|
|
50
|
+
* A confirmation line shown in place of the navigation hint.
|
|
51
|
+
*
|
|
52
|
+
* Used by an in-screen copy ("copied 412 chars") and by a Screen's own action
|
|
53
|
+
* (the doctor repair result): the row the action would otherwise push is behind
|
|
54
|
+
* this screen, and silence after an action reads as a failure.
|
|
55
|
+
*/
|
|
56
|
+
notice?: string;
|
|
57
|
+
/** Child session whose live log refreshes this screen in place. */
|
|
58
|
+
subagentSessionId?: string;
|
|
59
|
+
/** `/find` already scrolled to its hit here; later repaints keep the offset. */
|
|
60
|
+
searchRevealed?: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The one judgement: is this dialog a Screen?
|
|
64
|
+
*
|
|
65
|
+
* A Screen is *not* a dialog — it never enters `dialogQueue`, is never replaced by
|
|
66
|
+
* a Surface and never blocks one. This function exists so that no future call site
|
|
67
|
+
* has to remember that: a screen-shaped dialog handed to the dialog path is
|
|
68
|
+
* converted here and handed to the Screen path instead, and a test pins it.
|
|
69
|
+
*
|
|
70
|
+
* Returns `undefined` for the surfaces that genuinely are dialogs (questions,
|
|
71
|
+
* confirms, the onboarding wizard).
|
|
72
|
+
*/
|
|
73
|
+
export declare function screenFromDialog(dialog: Dialog | InspectDialog): ScreenState | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* The row kind a report falls back to where there is no frame to draw on.
|
|
76
|
+
*
|
|
77
|
+
* Line mode has no Screen (and no way to dismiss one), so `/diag` and `/doctor`
|
|
78
|
+
* keep their own copyable `diag` rows there — which is also where they are read
|
|
79
|
+
* from in a transcript (`/copy error` looks for `error` or `diag`).
|
|
80
|
+
*/
|
|
81
|
+
export declare function reportRowKind(report: ReportKind): 'diag' | 'system';
|
|
82
|
+
/** The rows a Screen lays out, top to bottom. */
|
|
83
|
+
export interface ScreenLayout {
|
|
84
|
+
/** 1 when the title row is drawn. */
|
|
85
|
+
titleRows: number;
|
|
86
|
+
/** 1 when the rule under the title is drawn. */
|
|
87
|
+
dividerRows: number;
|
|
88
|
+
/** Rows available to the body; may be 0 on a terminal that short. */
|
|
89
|
+
bodyRows: number;
|
|
90
|
+
/** 1 when the navigation hint row is drawn. */
|
|
91
|
+
hintRows: number;
|
|
92
|
+
/** 1 when the compact runtime strip is drawn. */
|
|
93
|
+
stripRows: number;
|
|
94
|
+
/** 0-based index of the first body row in the painted frame. */
|
|
95
|
+
contentTop: number;
|
|
96
|
+
/** 0-based index of the hint row, or -1 when it is not drawn. */
|
|
97
|
+
hintTop: number;
|
|
98
|
+
/** 0-based index of the strip row, or -1 when it is not drawn. */
|
|
99
|
+
stripTop: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Lay a Screen out for the height the terminal has.
|
|
103
|
+
*
|
|
104
|
+
* The strip is the one row that never goes: a Screen is a place the reader can be
|
|
105
|
+
* *while the agent keeps working*, and "is it still running, is anything waiting,
|
|
106
|
+
* is the link alive" has to be answerable without leaving. After that the order
|
|
107
|
+
* gives up the least load-bearing row first — the rule under the title, then the
|
|
108
|
+
* title, then body rows — and the navigation hint goes only when there is nothing
|
|
109
|
+
* left to navigate. Measured at 8 / 12 / 20 rows in the Screen contract tests.
|
|
110
|
+
*/
|
|
111
|
+
export declare function screenLayout(height: number): ScreenLayout;
|
|
112
|
+
/**
|
|
113
|
+
* Clamp a Screen's scroll offset to its body.
|
|
114
|
+
*
|
|
115
|
+
* The body can be shorter than the last offset the reader scrolled to (a resize, a
|
|
116
|
+
* shorter report, a live subagent log that was replaced), and an offset past the
|
|
117
|
+
* end paints an empty screen with a position readout that lies.
|
|
118
|
+
*/
|
|
119
|
+
export declare function clampScreenOffset(offset: number, lineCount: number, bodyRows: number): number;
|
|
120
|
+
/**
|
|
121
|
+
* `1–18/64`, the position readout a Screen's footer shows.
|
|
122
|
+
*
|
|
123
|
+
* Zero lines reads `0/0` rather than `1–0/0`: the second looks like a bug in the
|
|
124
|
+
* readout instead of an empty report.
|
|
125
|
+
*/
|
|
126
|
+
export declare function screenPositionText(offset: number, lineCount: number, bodyRows: number): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What makes a session "blank": a launch that never did anything.
|
|
3
|
+
*
|
|
4
|
+
* A fresh TUI start creates a session before the reader has typed anything, so
|
|
5
|
+
* quitting straight away leaves an artifact behind. The picker hides those, but
|
|
6
|
+
* they are real files, and other surfaces (the web session list) read the same
|
|
7
|
+
* artifacts without that filter — which is how a session that was never used
|
|
8
|
+
* still shows up in another profile's menu.
|
|
9
|
+
*
|
|
10
|
+
* The rule lives in its own module, with no host imports, for one reason: three
|
|
11
|
+
* very different callers have to agree on it — the picker (which prunes while
|
|
12
|
+
* listing), the TUI (which drops its own session on exit), and
|
|
13
|
+
* `scripts/prune-blank-sessions.mjs` (which sweeps whatever already accumulated,
|
|
14
|
+
* outside a running host).
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-ssh-tui/session-blank
|
|
17
|
+
*/
|
|
18
|
+
/** Whether one event is a message the reader typed. */
|
|
19
|
+
export declare function isUserMessageEvent(event: unknown): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Whether a turn was opened and never closed.
|
|
22
|
+
*
|
|
23
|
+
* A session with an open turn is mid-work: it must never be treated as empty,
|
|
24
|
+
* however little it looks like it has done.
|
|
25
|
+
* @param events - decoded session events, in order.
|
|
26
|
+
* @returns true when a `turn/start` has no matching `turn/end`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function hasUnfinishedTurn(events: readonly unknown[]): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Whether the session ever produced a reply.
|
|
31
|
+
*
|
|
32
|
+
* A failed request counts: the error text is the only thing the reader has to
|
|
33
|
+
* look at, and deleting it would delete their evidence.
|
|
34
|
+
* @param events - decoded session events, in order.
|
|
35
|
+
* @returns true when an assistant message or a failed turn is present.
|
|
36
|
+
*/
|
|
37
|
+
export declare function sessionHasReply(events: readonly unknown[]): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Whether a session saw the reader's input, a reply, or an open turn.
|
|
40
|
+
* @param events - decoded session events, in order.
|
|
41
|
+
* @returns true when there is something worth keeping.
|
|
42
|
+
*/
|
|
43
|
+
export declare function sessionHasWork(events: readonly unknown[]): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether a session's events describe a launch that never did anything.
|
|
46
|
+
* @param events - the session's decoded events.
|
|
47
|
+
* @returns true when nothing the reader would recognise as work happened. An
|
|
48
|
+
* empty event list is *not* blank: it is an unreadable or detached log, and
|
|
49
|
+
* guessing there once made a listing delete a live session's directory.
|
|
50
|
+
*/
|
|
51
|
+
export declare function sessionEventsAreBlank(events: readonly unknown[]): boolean;
|
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
* labels, and ordering, so both surfaces offer the same sessions.
|
|
5
5
|
*/
|
|
6
6
|
import { listAttachableHosts } from './session-lock.js';
|
|
7
|
+
/** Delete one session's on-disk artifacts (log directory), best effort. */
|
|
8
|
+
/**
|
|
9
|
+
* Delete one session's artifacts by id.
|
|
10
|
+
*
|
|
11
|
+
* `false` means the persistence layer could not locate it (already gone, or a
|
|
12
|
+
* service that does not expose `locate`) — never an error worth surfacing, since
|
|
13
|
+
* the only caller is tidying up on the way out.
|
|
14
|
+
* @param persistence - the session persistence service.
|
|
15
|
+
* @param id - the session id.
|
|
16
|
+
* @returns true when a directory was found and removed.
|
|
17
|
+
*/
|
|
18
|
+
export declare function pruneSessionById(persistence: object | undefined, id: string): Promise<boolean>;
|
|
7
19
|
/** Last path segment for the footer chip (`\root\genshin\srv` → `srv`). */
|
|
8
20
|
export declare function sessionCwdLabel(cwd: string): string;
|
|
9
21
|
export declare function formatFooterCwd(cwd: string): string;
|
|
@@ -88,6 +100,28 @@ export declare function openResumableSessionPager(persistence: object, currentId
|
|
|
88
100
|
listHosts?: typeof listAttachableHosts;
|
|
89
101
|
indexPath?: string;
|
|
90
102
|
}): Promise<ResumableSessionPager>;
|
|
103
|
+
/**
|
|
104
|
+
* Delete every session that never saw user input, and report what happened.
|
|
105
|
+
*
|
|
106
|
+
* The picker prunes these as it resolves them, but only for the surface that
|
|
107
|
+
* looks at them: a launch that died before doing anything kept showing up in
|
|
108
|
+
* *other* profiles' menus (the web session list reads the same artifacts without
|
|
109
|
+
* this filter). So the same rule is offered as an explicit sweep, which is also
|
|
110
|
+
* what `/cleanup` runs.
|
|
111
|
+
* @param persistence - the session persistence service.
|
|
112
|
+
* @param currentId - the live session, never a candidate for deletion.
|
|
113
|
+
* @param options - `listHosts` for test injection.
|
|
114
|
+
* @returns how many were deleted, how many were kept, and how many could not be
|
|
115
|
+
* read (an unreadable log is kept: it might be a live writer).
|
|
116
|
+
*/
|
|
117
|
+
export declare function pruneBlankSessions(persistence: object, currentId: string, options?: {
|
|
118
|
+
listHosts?: typeof listAttachableHosts;
|
|
119
|
+
dryRun?: boolean;
|
|
120
|
+
}): Promise<{
|
|
121
|
+
pruned: number;
|
|
122
|
+
kept: number;
|
|
123
|
+
unreadable: number;
|
|
124
|
+
}>;
|
|
91
125
|
/**
|
|
92
126
|
* List resumable top-level sessions, newest first.
|
|
93
127
|
*
|
package/lib/types/startup.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Context } from '@deepseek-ai/cordis';
|
|
8
8
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
9
|
+
import { type DisplayMode } from './display-mode.js';
|
|
9
10
|
/** Service key under which the parsed TUI launch options are provided. */
|
|
10
11
|
export declare const SSH_TUI_STARTUP_SERVICE = "sshTuiStartup";
|
|
11
12
|
/** Config `id` of the agent-loop entry the TUI drives. */
|
|
@@ -24,6 +25,11 @@ export interface SshTuiStartup {
|
|
|
24
25
|
readonly provider?: string;
|
|
25
26
|
/** ANSI color opt-out supplied at launch. */
|
|
26
27
|
readonly noColor?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Where the bytes go: `stdio` when the parent speaks the display protocol on
|
|
30
|
+
* stdin/stdout (a GUI, a web terminal, a harness) instead of owning a TTY.
|
|
31
|
+
*/
|
|
32
|
+
readonly display?: DisplayMode;
|
|
27
33
|
}
|
|
28
34
|
declare module '@deepseek-ai/cordis' {
|
|
29
35
|
interface Context {
|
|
@@ -32,6 +38,30 @@ declare module '@deepseek-ai/cordis' {
|
|
|
32
38
|
}
|
|
33
39
|
export declare const name = "ssh-tui-startup";
|
|
34
40
|
export declare const inject: string[];
|
|
41
|
+
/**
|
|
42
|
+
* Whether another application already owns this profile's command line.
|
|
43
|
+
*
|
|
44
|
+
* `@deepseek-ai/dsh-cmdline` has **no arbitration between apps**: every app plugin is
|
|
45
|
+
* handed the same argv and parses it with its own grammar, and an unknown flag is
|
|
46
|
+
* `program.error`, which exits the process. A profile that hosts two apps therefore
|
|
47
|
+
* requires the second one to understand the first one's flags — and the desktop
|
|
48
|
+
* application passes `--no-open` (the web app's own flag) to its host, so this row's
|
|
49
|
+
* grammar rejected it: the host exited 1, the application reported
|
|
50
|
+
* `dsh desktop host exited with 1: error: unknown option '--no-open'` and died. Only
|
|
51
|
+
* the *startup* row did that; the TUI row never ran, which is why 0.8.1's inert-when-
|
|
52
|
+
* headless fix could not have caught it.
|
|
53
|
+
*
|
|
54
|
+
* The signals are two, and they overlap on purpose:
|
|
55
|
+
*
|
|
56
|
+
* - the launcher facts (`desktopLauncher`): an Electron host, `DeepSeek Harness.exe`,
|
|
57
|
+
* or `app.asar` in argv — true whatever the profile's row order is;
|
|
58
|
+
* - `webStartup`: the web app provides it as soon as it has parsed, and a `web`
|
|
59
|
+
* profile is not a terminal profile in any case.
|
|
60
|
+
*
|
|
61
|
+
* Stepping aside is not a degraded mode: the TUI row `inject`s `sshTuiStartup`, so
|
|
62
|
+
* nothing of this plugin activates and the host keeps its own grammar and its own app.
|
|
63
|
+
*/
|
|
64
|
+
export declare function anotherAppOwnsCommandLine(ctx: Context): boolean;
|
|
35
65
|
/**
|
|
36
66
|
* Build the TUI command grammar and provide the session identity for the
|
|
37
67
|
* agent-loop row plus the {@link SshTuiStartup} service. On `--help` or a
|
package/lib/types/stats.d.ts
CHANGED
|
@@ -14,12 +14,41 @@
|
|
|
14
14
|
* - decode rate needs a finite, non-negative output token count.
|
|
15
15
|
*/
|
|
16
16
|
import type { TokenUsage } from '@deepseek-ai/dsh-llm';
|
|
17
|
+
import { type ThroughputView } from './throughput.js';
|
|
17
18
|
export interface SessionUsage {
|
|
18
19
|
inputTokens: number;
|
|
19
20
|
outputTokens: number;
|
|
21
|
+
/** Sum of the full-call totals the harness itself reported, when it reported one. */
|
|
22
|
+
reportedTokens: number;
|
|
23
|
+
/** Steps whose usage carried a harness total. */
|
|
24
|
+
reportedSteps: number;
|
|
25
|
+
/** Steps whose usage carried none, so the billed parts had to stand in. */
|
|
26
|
+
unreportedSteps: number;
|
|
20
27
|
cacheReadTokens: number;
|
|
21
28
|
cacheWriteTokens: number;
|
|
22
29
|
}
|
|
30
|
+
/** The session-size number the second row shows, and how it was derived. */
|
|
31
|
+
export interface SessionTokenTotal {
|
|
32
|
+
tokens: number;
|
|
33
|
+
/**
|
|
34
|
+
* `harness` when every usage sample carried the harness's own total; `sum`
|
|
35
|
+
* when some step's billed parts had to be added in instead.
|
|
36
|
+
*/
|
|
37
|
+
basis: 'harness' | 'sum';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* How many tokens this session has moved, on the accounting the harness
|
|
41
|
+
* published.
|
|
42
|
+
*
|
|
43
|
+
* The harness is the authority: a `totalTokens` it reports is a full-call total
|
|
44
|
+
* whose parts are defined provider by provider — pi-ai's own adapters build it
|
|
45
|
+
* as prompt + output + cache read + cache write — and re-deriving one from the
|
|
46
|
+
* three prompt counters plus output is exactly the arithmetic that
|
|
47
|
+
* double-counts on a provider that folds cache into its prompt count. So
|
|
48
|
+
* reported totals are summed, and only a step that reported none contributes
|
|
49
|
+
* its billed parts. The basis says which happened, and `/status` prints it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function sessionTokenTotal(usage: SessionUsage): SessionTokenTotal | undefined;
|
|
23
52
|
export interface SessionStatsSnapshot {
|
|
24
53
|
turns: number;
|
|
25
54
|
steps: number;
|
|
@@ -29,6 +58,12 @@ export interface SessionStatsSnapshot {
|
|
|
29
58
|
ttftSteps: number;
|
|
30
59
|
decodeMs: number;
|
|
31
60
|
decodeTokens: number;
|
|
61
|
+
/** Tokens and milliseconds of the most recent settled step alone. */
|
|
62
|
+
lastDecodeMs: number;
|
|
63
|
+
lastDecodeTokens: number;
|
|
64
|
+
/** Decode totals excluding the step that is currently open. */
|
|
65
|
+
settledDecodeMs: number;
|
|
66
|
+
settledDecodeTokens: number;
|
|
32
67
|
usage: SessionUsage;
|
|
33
68
|
}
|
|
34
69
|
/** Flat view of the same numbers, for the footer's stats line. */
|
|
@@ -45,6 +80,7 @@ export interface SessionStatsRow {
|
|
|
45
80
|
outputTokens: number;
|
|
46
81
|
cacheReadTokens: number;
|
|
47
82
|
cacheWriteTokens: number;
|
|
83
|
+
totalTokens: number;
|
|
48
84
|
}
|
|
49
85
|
export interface OpenStep {
|
|
50
86
|
turn: number;
|
|
@@ -52,7 +88,7 @@ export interface OpenStep {
|
|
|
52
88
|
}
|
|
53
89
|
export declare function emptySessionUsage(): SessionUsage;
|
|
54
90
|
export declare function emptySessionStats(): SessionStatsSnapshot;
|
|
55
|
-
/** Flatten a snapshot into the
|
|
91
|
+
/** Flatten a snapshot into the legacy stats-row shape. */
|
|
56
92
|
export declare function statsRowOf(stats: SessionStatsSnapshot): SessionStatsRow;
|
|
57
93
|
/**
|
|
58
94
|
* Accumulates the stats the footer and `/status` show as session events arrive.
|
|
@@ -65,10 +101,35 @@ export declare class SessionStatsTracker {
|
|
|
65
101
|
private readonly usageByStep;
|
|
66
102
|
private readonly pendingToolTimes;
|
|
67
103
|
private lastTurn;
|
|
104
|
+
/** Live decode estimate for the step currently streaming. */
|
|
105
|
+
private readonly live;
|
|
68
106
|
/** The step a live chunk belongs to when the host frame carried none. */
|
|
69
107
|
currentStep(): OpenStep | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* The open step's two clocks, for the status row's elapsed suffix.
|
|
110
|
+
*
|
|
111
|
+
* Both are host wall clocks, so a caller may subtract them from `Date.now()`
|
|
112
|
+
* without a translation. `firstTokenAt` is absent until the step's first
|
|
113
|
+
* delta, which is exactly the difference between "the request is out" and
|
|
114
|
+
* "the model is talking".
|
|
115
|
+
*/
|
|
116
|
+
stepClocks(): {
|
|
117
|
+
startedAt?: number;
|
|
118
|
+
firstTokenAt?: number;
|
|
119
|
+
};
|
|
70
120
|
/** `step/start`: opens the clock LLM time, TTFT and decode are measured from. */
|
|
71
121
|
noteStepStart(turn: number, step: number, time: number): void;
|
|
122
|
+
/**
|
|
123
|
+
* A live fragment of model output, for the running throughput estimate.
|
|
124
|
+
*
|
|
125
|
+
* The text itself travels: the estimate is tokens per character, and how many
|
|
126
|
+
* tokens a character is worth depends on the script the fragment is written in.
|
|
127
|
+
* @param text - the fragment.
|
|
128
|
+
* @param time - wall clock the fragment arrived at.
|
|
129
|
+
*/
|
|
130
|
+
noteDelta(text: string, time?: number): void;
|
|
131
|
+
/** The running throughput estimate, settled value included. */
|
|
132
|
+
throughput(now?: number): ThroughputView;
|
|
72
133
|
/** The first token delta of the open step latches its TTFT clock. */
|
|
73
134
|
noteFirstToken(turn: number, step: number, time: number): void;
|
|
74
135
|
/** Replace one step's usage sample so a repeated report never double counts. */
|
|
@@ -87,6 +148,8 @@ export declare class SessionStatsTracker {
|
|
|
87
148
|
}): void;
|
|
88
149
|
/** `tool/call`: start the tool clock for this call. */
|
|
89
150
|
noteToolStart(callId: string, time: number): void;
|
|
151
|
+
/** When a still-open tool call started, for the status row's elapsed suffix. */
|
|
152
|
+
toolStartedAt(callId: string): number | undefined;
|
|
90
153
|
/** `tool/result`: add the elapsed tool time, once. */
|
|
91
154
|
noteToolEnd(callId: string, time: number): void;
|
|
92
155
|
/** `step/end`: count the step, and forget the step's usage dedupe key. */
|
|
@@ -44,13 +44,14 @@ export declare function canonicalProviderId(provider: string | undefined): strin
|
|
|
44
44
|
*/
|
|
45
45
|
export declare function subagentProviderDiffers(parent: string | undefined, child: string | undefined): boolean;
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* Which identity role a subagent chip title wears.
|
|
48
|
+
*
|
|
49
|
+
* The colours themselves live in `theme.ts`: the same provider as the parent
|
|
50
|
+
* gets the identity role, a different provider gets the foreign one, and the
|
|
51
|
+
* palette decides what those look like (under `mono` they become an attribute,
|
|
52
|
+
* because the distinction has to survive a terminal without colour).
|
|
50
53
|
*/
|
|
51
|
-
export declare
|
|
52
|
-
export declare const SUBAGENT_FOREIGN_SGR = "38;5;80";
|
|
53
|
-
export declare function subagentIdentitySgr(foreign: boolean): typeof SUBAGENT_IDENTITY_SGR | typeof SUBAGENT_FOREIGN_SGR;
|
|
54
|
+
export declare function subagentIdentityRole(foreign: boolean): 'subagent-self' | 'subagent-foreign';
|
|
54
55
|
/**
|
|
55
56
|
* True when the stored subagent model still belongs to the parent provider
|
|
56
57
|
* family. An explicit leftover DeepSeek flash id after switching to xAI is
|
package/lib/types/term-text.d.ts
CHANGED
|
@@ -40,6 +40,16 @@ export declare function waitCardCopy(input: {
|
|
|
40
40
|
export declare function wrapWaitDetails(detail: string, width: number, maxLines?: number): string[];
|
|
41
41
|
/** Forget the cached decision, so the next read re-consults the environment. */
|
|
42
42
|
export declare function resetAsciiChrome(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Whether chrome is being drawn in ASCII, for renderers that pick the glyph run
|
|
45
|
+
* themselves.
|
|
46
|
+
*
|
|
47
|
+
* `pinEmojiCells` translates them anyway; asking is for the case where the
|
|
48
|
+
* *shape* differs rather than just the character — a metre whose filled cell is
|
|
49
|
+
* `#` reads as `##..`, not as `#..#`, so the renderer wants to know before it
|
|
50
|
+
* counts and pads.
|
|
51
|
+
*/
|
|
52
|
+
export declare function asciiChromeEnabled(): boolean;
|
|
43
53
|
/**
|
|
44
54
|
* Rewrite one string's chrome into ASCII when the terminal cannot decode UTF-8.
|
|
45
55
|
*
|
|
@@ -53,25 +63,87 @@ export declare function resetAsciiChrome(): void;
|
|
|
53
63
|
*/
|
|
54
64
|
export declare function mapAsciiChrome(text: string): string;
|
|
55
65
|
/**
|
|
56
|
-
*
|
|
66
|
+
* Resolve the ambiguous-glyph policy for this process.
|
|
57
67
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
68
|
+
* Cheap enough to call once per line (it compares one string) and far too
|
|
69
|
+
* expensive to call per character, which is the distinction that matters.
|
|
70
|
+
* @param env - the environment to read.
|
|
71
|
+
* @param onTerminal - whether the output is a terminal at all.
|
|
72
|
+
* @returns whether the glyphs are two cells wide, and whether the second cell is
|
|
73
|
+
* ours to reserve.
|
|
74
|
+
*/
|
|
75
|
+
export declare function ambiguousPolicy(env?: NodeJS.ProcessEnv, onTerminal?: boolean): {
|
|
76
|
+
wide: boolean;
|
|
77
|
+
reserve: boolean;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Record what the terminal's own answer said about these glyphs.
|
|
63
81
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
* Called by the launcher after measuring; also the escape hatch for a caller
|
|
83
|
+
* that knows better than the locale (a relay that has already measured for its
|
|
84
|
+
* own accounting, or a test).
|
|
85
|
+
* @param wide - true when the terminal advances two cells, false for one, or
|
|
86
|
+
* undefined to fall back to the locale.
|
|
87
|
+
*/
|
|
88
|
+
export declare function setAmbiguousWidthMeasured(wide: boolean | undefined): void;
|
|
89
|
+
/**
|
|
90
|
+
* Force the reserve behaviour, for a caller that measured the same terminal for
|
|
91
|
+
* its own accounting (a relay) or a test.
|
|
92
|
+
* @param reserve - whether to spend a space after each ambiguous glyph.
|
|
93
|
+
*/
|
|
94
|
+
export declare function setAmbiguousWidthReserve(reserve: boolean): void;
|
|
95
|
+
/** Whether the second cell is currently reserved with a space. */
|
|
96
|
+
export declare function ambiguousWidthReserved(): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Cells one character costs in this TUI's layout.
|
|
71
99
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
100
|
+
* The reserve case still costs two: the glyph advances one cell and the reserving
|
|
101
|
+
* space takes the next, so the layout must budget both or every row holding one
|
|
102
|
+
* comes up short. Non-ambiguous characters are unchanged.
|
|
103
|
+
* @param cp - the code point.
|
|
104
|
+
* @returns 0, 1 or 2 cells.
|
|
74
105
|
*/
|
|
106
|
+
export declare function ambiguousCellCost(cp: number): number;
|
|
107
|
+
/** What the last measurement decided, for diagnostics and tests. */
|
|
108
|
+
export declare function ambiguousWidthMeasured(): boolean | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Whether ambiguous glyphs in {@link AMBIGUOUS_WIDE_RANGES} are drawn two cells
|
|
111
|
+
* wide here.
|
|
112
|
+
*
|
|
113
|
+
* `DSH_TUI_AMBIGUOUS_WIDTH=1|2` answers outright. Otherwise the locale decides,
|
|
114
|
+
* and only when there really is a terminal: a zh/ja/ko locale means the terminal
|
|
115
|
+
* is very likely using a CJK font, where these glyphs are full width. The UI
|
|
116
|
+
* language is deliberately *not* consulted — a Chinese reader on a Western
|
|
117
|
+
* terminal has narrow glyphs, and typing in Chinese does not change the font
|
|
118
|
+
* metrics.
|
|
119
|
+
* @param env - the environment to read (tests pass their own).
|
|
120
|
+
* @param onTerminal - whether the output is a terminal at all; a pipe or a test
|
|
121
|
+
* harness has no font metrics, so there the narrow default applies.
|
|
122
|
+
* @returns true when those glyphs should be budgeted two cells.
|
|
123
|
+
*/
|
|
124
|
+
export declare function ambiguousWidthIsTwo(env?: NodeJS.ProcessEnv, onTerminal?: boolean): boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Cells one *code unit* costs at its position, given a resolved policy.
|
|
127
|
+
*
|
|
128
|
+
* The per-character core of {@link displayWidth}, split out for the
|
|
129
|
+
* callers that walk a string themselves (clipping, truncation, wrapping): they
|
|
130
|
+
* resolve {@link ambiguousPolicy} once per string and then ask this per
|
|
131
|
+
* character, instead of paying `displayWidth`'s policy lookup and ASCII mapping
|
|
132
|
+
* for every character of every row. A profile of a window drag put 56% of the
|
|
133
|
+
* samples in that lookup, because the clipping loops are O(n²) in a way nothing
|
|
134
|
+
* about their shape advertises: `clipAnsiToWidth` calls this for each character,
|
|
135
|
+
* and `truncateToWidth` re-measures the whole remainder inside its loop.
|
|
136
|
+
*
|
|
137
|
+
* ASCII is answered from its code point before any table lookup: it is the
|
|
138
|
+
* overwhelming majority of every row, and it can never be ambiguous or wide.
|
|
139
|
+
* @param cp - the code point.
|
|
140
|
+
* @param policy - the resolved policy, as returned by {@link ambiguousPolicy}.
|
|
141
|
+
* @returns 0, 1 or 2 cells.
|
|
142
|
+
*/
|
|
143
|
+
export declare function charCellWidth(cp: number, policy: {
|
|
144
|
+
wide: boolean;
|
|
145
|
+
reserve: boolean;
|
|
146
|
+
}): number;
|
|
75
147
|
export declare function displayWidth(text: string): number;
|
|
76
148
|
/**
|
|
77
149
|
* Make a painted row spend the two cells {@link displayWidth} budgets for every
|
|
@@ -176,7 +248,19 @@ export declare function hrefAtColumn(hits: readonly PaintedLinkHit[], col: numbe
|
|
|
176
248
|
* headings, quotes, lists, rules, links and inline spans keep their own ANSI.
|
|
177
249
|
*/
|
|
178
250
|
export declare function renderMarkdownLines(text: string, width: number, color: boolean, hyperlinks?: boolean): string[];
|
|
179
|
-
/**
|
|
251
|
+
/**
|
|
252
|
+
* Cut **one line** to fit a width, appending an ellipsis when truncated.
|
|
253
|
+
*
|
|
254
|
+
* A line break is not a character in a row: it moves the terminal to the next row,
|
|
255
|
+
* and a frame that addresses its rows absolutely (`ESC[row;1H`) then writes every
|
|
256
|
+
* following row one line off — content lands on the composer, on the footer, and on
|
|
257
|
+
* top of whatever was already there. `sanitizeTerminalText` keeps LF/CR on purpose
|
|
258
|
+
* (it also sanitizes multi-line bodies), so the collapse happens here, where the
|
|
259
|
+
* contract is "one row": each break becomes a single space, which keeps the string
|
|
260
|
+
* the same *length* — callers that pass this result into `paintSegmentedLine` rely
|
|
261
|
+
* on character offsets into the text they built, and a collapsing replacement would
|
|
262
|
+
* silently move every colour segment.
|
|
263
|
+
*/
|
|
180
264
|
export declare function truncateToWidth(text: string, width: number): string;
|
|
181
265
|
/**
|
|
182
266
|
* Clip an already-styled ANSI line to `width` terminal cells without dropping
|
|
@@ -201,14 +285,6 @@ export interface InputView {
|
|
|
201
285
|
cursorOffset: number;
|
|
202
286
|
folded: boolean;
|
|
203
287
|
}
|
|
204
|
-
/**
|
|
205
|
-
* Fold a long input into one terminal row around the cursor.
|
|
206
|
-
*
|
|
207
|
-
* Newlines from a paste are display-only: they do not occupy cells, so a
|
|
208
|
-
* naive `displayWidth(input)` under-counts a multi-line paste and parks the
|
|
209
|
-
* caret in the middle of later text. Fold the *current line* (between the
|
|
210
|
-
* surrounding newlines) and keep `\n` out of the visible slice.
|
|
211
|
-
*/
|
|
212
288
|
export declare function foldInputView(input: string, cursor: number, maxWidth: number): InputView;
|
|
213
289
|
/**
|
|
214
290
|
* Map a character index in the input text to its visual (row, col) after the
|
|
@@ -28,6 +28,7 @@ export declare const RTT_OUTLIER_RATIO = 0.25;
|
|
|
28
28
|
export declare function stripCursorReplies(text: string): {
|
|
29
29
|
text: string;
|
|
30
30
|
replies: number;
|
|
31
|
+
last?: string;
|
|
31
32
|
};
|
|
32
33
|
/**
|
|
33
34
|
* Drop cursor replies from a byte stream that is otherwise user input.
|
|
@@ -38,6 +39,8 @@ export declare function stripCursorReplies(text: string): {
|
|
|
38
39
|
*/
|
|
39
40
|
export declare class TerminalInputFilter {
|
|
40
41
|
private held;
|
|
42
|
+
/** The most recent cursor reply this filter swallowed, if any. */
|
|
43
|
+
lastReply: string | undefined;
|
|
41
44
|
push(text: string): {
|
|
42
45
|
forward: string;
|
|
43
46
|
replies: number;
|
|
@@ -103,10 +106,26 @@ export declare class TerminalInputPump {
|
|
|
103
106
|
private slowestSampleMs;
|
|
104
107
|
private holdTimer;
|
|
105
108
|
private listening;
|
|
109
|
+
/** Set by {@link quiet}: the held tail is dropped instead of forwarded. */
|
|
110
|
+
private dropping;
|
|
106
111
|
readonly ssh: boolean;
|
|
107
112
|
constructor(options: TerminalInputPumpOptions);
|
|
108
113
|
start(): void;
|
|
109
114
|
stop(): void;
|
|
115
|
+
/**
|
|
116
|
+
* Wait one more round trip before letting go of the terminal.
|
|
117
|
+
*
|
|
118
|
+
* A cursor reply can still be on the wire when a relay stops. The terminal
|
|
119
|
+
* answers a Device Status Report after the round trip, so on a slow SSH link
|
|
120
|
+
* that is hundreds of milliseconds after the request; restoring cooked mode
|
|
121
|
+
* first lets the tty echo the answer as literal `^[[25;1R` text at the user's
|
|
122
|
+
* prompt, and leaves it in the tty queue for the shell to read as typing.
|
|
123
|
+
* Staying in raw mode for one round trip swallows it instead. Typing that
|
|
124
|
+
* lands in this window is forwarded as usual — the window exists to drop
|
|
125
|
+
* answers, not keystrokes.
|
|
126
|
+
* @param graceMs - how long the terminal may still owe us a reply.
|
|
127
|
+
*/
|
|
128
|
+
handBack(graceMs?: number): Promise<void>;
|
|
110
129
|
/**
|
|
111
130
|
* Ask the terminal for its cursor a few times and report the round-trip that
|
|
112
131
|
* best describes the link. `undefined` means the terminal never answered in
|
|
@@ -170,5 +189,23 @@ export declare class TerminalInputPump {
|
|
|
170
189
|
* from being attributed to the next request.
|
|
171
190
|
*/
|
|
172
191
|
private answer;
|
|
192
|
+
/**
|
|
193
|
+
* Ask the terminal for its cursor after printing `probe`, and report where it
|
|
194
|
+
* says the cursor ended up.
|
|
195
|
+
*
|
|
196
|
+
* This is the measurement half of the pump: `measure()` answers "how long did
|
|
197
|
+
* the round trip take", this answers "how far did the cursor move". Replies are
|
|
198
|
+
* swallowed by the same filter that swallows them for the timing probe — which
|
|
199
|
+
* matters, because a reply nobody consumes is echoed on screen as literal
|
|
200
|
+
* `^[[1;5R` text, and that is exactly what a hand-rolled version of this did.
|
|
201
|
+
* @param probe - the text to print before asking (cleared afterwards).
|
|
202
|
+
* @param timeoutMs - how long to wait for the answer.
|
|
203
|
+
* @returns the coordinates the terminal reported, or undefined when it stayed
|
|
204
|
+
* silent.
|
|
205
|
+
*/
|
|
206
|
+
askPosition(probe: string, timeoutMs?: number): Promise<{
|
|
207
|
+
row: number;
|
|
208
|
+
column: number;
|
|
209
|
+
} | undefined>;
|
|
173
210
|
private scheduleHold;
|
|
174
211
|
}
|