dsh-ssh-tui 0.8.1 → 0.8.2

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 (93) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.en.md +118 -15
  3. package/README.md +78 -13
  4. package/docs/release-notes-0.7.4.md +91 -0
  5. package/docs/release-notes-0.8.0.md +56 -0
  6. package/docs/release-notes-0.8.1.md +79 -0
  7. package/docs/release-notes-0.8.2-rc.1.md +159 -0
  8. package/docs/release-notes-0.8.2.md +62 -0
  9. package/docs/screenshots/reconnect.gif +0 -0
  10. package/docs/terminals.md +23 -0
  11. package/docs/windows.md +13 -0
  12. package/lib/approval-state.js +148 -0
  13. package/lib/approval-state.js.map +1 -0
  14. package/lib/auth-failure.js +38 -0
  15. package/lib/auth-failure.js.map +1 -1
  16. package/lib/color-depth.js +26 -0
  17. package/lib/color-depth.js.map +1 -1
  18. package/lib/copy-text.js +30 -8
  19. package/lib/copy-text.js.map +1 -1
  20. package/lib/dialogs.js +81 -0
  21. package/lib/dialogs.js.map +1 -1
  22. package/lib/display-sock.js +138 -23
  23. package/lib/display-sock.js.map +1 -1
  24. package/lib/footer-accents.js +28 -0
  25. package/lib/footer-accents.js.map +1 -0
  26. package/lib/footer-budget.js +328 -0
  27. package/lib/footer-budget.js.map +1 -0
  28. package/lib/footer-format.js +433 -0
  29. package/lib/footer-format.js.map +1 -0
  30. package/lib/footer-layout.js +407 -0
  31. package/lib/footer-layout.js.map +1 -0
  32. package/lib/footer-meters.js +185 -0
  33. package/lib/footer-meters.js.map +1 -0
  34. package/lib/footer.js +19 -558
  35. package/lib/footer.js.map +1 -1
  36. package/lib/i18n/en.js +65 -5
  37. package/lib/i18n/en.js.map +1 -1
  38. package/lib/i18n/zh.js +67 -5
  39. package/lib/i18n/zh.js.map +1 -1
  40. package/lib/paint.js +53 -8
  41. package/lib/paint.js.map +1 -1
  42. package/lib/plan-projection.js +291 -0
  43. package/lib/plan-projection.js.map +1 -0
  44. package/lib/plan.js +50 -0
  45. package/lib/plan.js.map +1 -1
  46. package/lib/question-state.js +83 -0
  47. package/lib/question-state.js.map +1 -0
  48. package/lib/representation.js +283 -0
  49. package/lib/representation.js.map +1 -0
  50. package/lib/screen.js +105 -0
  51. package/lib/screen.js.map +1 -0
  52. package/lib/selection.js +5 -3
  53. package/lib/selection.js.map +1 -1
  54. package/lib/startup.js +49 -0
  55. package/lib/startup.js.map +1 -1
  56. package/lib/stats.js +101 -3
  57. package/lib/stats.js.map +1 -1
  58. package/lib/term-text.js +154 -55
  59. package/lib/term-text.js.map +1 -1
  60. package/lib/theme.js +20 -3
  61. package/lib/theme.js.map +1 -1
  62. package/lib/throughput.js +305 -0
  63. package/lib/throughput.js.map +1 -0
  64. package/lib/tool-present.js +57 -7
  65. package/lib/tool-present.js.map +1 -1
  66. package/lib/tui.js +3776 -1105
  67. package/lib/tui.js.map +1 -1
  68. package/lib/types/approval-state.d.ts +75 -0
  69. package/lib/types/auth-failure.d.ts +25 -0
  70. package/lib/types/color-depth.d.ts +21 -0
  71. package/lib/types/copy-text.d.ts +18 -1
  72. package/lib/types/dialogs.d.ts +111 -1
  73. package/lib/types/display-sock.d.ts +15 -0
  74. package/lib/types/footer-accents.d.ts +22 -0
  75. package/lib/types/footer-budget.d.ts +204 -0
  76. package/lib/types/footer-format.d.ts +221 -0
  77. package/lib/types/footer-layout.d.ts +292 -0
  78. package/lib/types/footer-meters.d.ts +151 -0
  79. package/lib/types/footer.d.ts +19 -269
  80. package/lib/types/paint.d.ts +48 -0
  81. package/lib/types/plan-projection.d.ts +162 -0
  82. package/lib/types/plan.d.ts +13 -0
  83. package/lib/types/question-state.d.ts +96 -0
  84. package/lib/types/representation.d.ts +498 -0
  85. package/lib/types/screen.d.ts +126 -0
  86. package/lib/types/startup.d.ts +24 -0
  87. package/lib/types/stats.d.ts +64 -1
  88. package/lib/types/term-text.d.ts +45 -1
  89. package/lib/types/throughput.d.ts +163 -0
  90. package/lib/types/tool-present.d.ts +41 -0
  91. package/lib/types/transcript-types.d.ts +63 -2
  92. package/lib/types/tui.d.ts +862 -11
  93. package/package.json +16 -6
@@ -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 footer's row shape. */
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. */
@@ -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
  *
@@ -112,6 +122,28 @@ export declare function ambiguousWidthMeasured(): boolean | undefined;
112
122
  * @returns true when those glyphs should be budgeted two cells.
113
123
  */
114
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;
115
147
  export declare function displayWidth(text: string): number;
116
148
  /**
117
149
  * Make a painted row spend the two cells {@link displayWidth} budgets for every
@@ -216,7 +248,19 @@ export declare function hrefAtColumn(hits: readonly PaintedLinkHit[], col: numbe
216
248
  * headings, quotes, lists, rules, links and inline spans keep their own ANSI.
217
249
  */
218
250
  export declare function renderMarkdownLines(text: string, width: number, color: boolean, hyperlinks?: boolean): string[];
219
- /** Cut one line to fit a width, appending an ellipsis when truncated. */
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
+ */
220
264
  export declare function truncateToWidth(text: string, width: number): string;
221
265
  /**
222
266
  * Clip an already-styled ANSI line to `width` terminal cells without dropping
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Live decode throughput for the status row.
3
+ *
4
+ * The harness never publishes tokens per second. What it does publish, per
5
+ * step, is authoritative: `assistant/message` carries the step's output-token
6
+ * count and the packed stream's first-token time, so `decodeTokens / decodeMs`
7
+ * over settled steps is a real measurement. What it does *not* publish is any
8
+ * per-token count while a step is still streaming — adapters emit text deltas
9
+ * and only hand over `usage` as the stream closes (verified against
10
+ * `dsh-llm-deepseek` and `dsh-llm-pi-ai`).
11
+ *
12
+ * So there are exactly two honest numbers, and this module keeps them apart:
13
+ *
14
+ * 1. **Settled** — tokens and milliseconds of a completed step. Exact, and the
15
+ * only number that is ever comparable across steps or sessions.
16
+ * 2. **Live** — characters per second while a step streams, converted with a
17
+ * ratio calibrated from this session's own settled steps. It is an estimate,
18
+ * it is labelled with `~`, and it exists so a long reply shows *something*
19
+ * that moves instead of a number frozen at the previous step.
20
+ *
21
+ * There is deliberately no health grading here. Tokens per second is a property
22
+ * of the model, the provider, the reasoning effort and the prompt, not a signal
23
+ * that something is wrong: 20 tok/s from a reasoning model and 20 tok/s from a
24
+ * small fast one mean different things. The row paints it neutral.
25
+ *
26
+ * @module dsh-ssh-tui/throughput
27
+ */
28
+ /** How much of the recent past the live rate is measured over. */
29
+ export declare const THROUGHPUT_WINDOW_MS = 4000;
30
+ /** Minimum span between the two samples a rate is taken from. */
31
+ export declare const THROUGHPUT_MIN_SPAN_MS = 400;
32
+ /** A live rate older than this describes a stream that stopped, not one running. */
33
+ export declare const THROUGHPUT_FRESH_MS = 2500;
34
+ /**
35
+ * Tokens per character, for the two ends of the script spectrum.
36
+ *
37
+ * Latin text averages roughly a third of a token per character; CJK is close to
38
+ * one, because a Chinese character is usually its own token and most words are one
39
+ * or two characters. Between those ends the estimate is interpolated by the
40
+ * *measured* script mix of the stream being estimated, continuously: a threshold
41
+ * ("30% CJK means Chinese") would put a mostly-English reply with one Chinese
42
+ * paragraph on the wrong side of a cliff, and the number it produces is shown to
43
+ * the reader.
44
+ */
45
+ export declare const DEFAULT_TOKENS_PER_CHAR_LATIN = 0.32;
46
+ export declare const DEFAULT_TOKENS_PER_CHAR_CJK = 0.85;
47
+ /** The Latin end under its original name: the fallback for a stream with no text. */
48
+ export declare const DEFAULT_TOKENS_PER_CHAR = 0.32;
49
+ /** What the status row paints, and what `/status` reports. */
50
+ export interface ThroughputView {
51
+ /** Exact tokens per second of the most recent settled step, when it had usage. */
52
+ settledRate?: number;
53
+ /** Characters the live estimate was built from, for the calibration report. */
54
+ liveChars: number;
55
+ /** Estimated tokens per second over the streaming window. */
56
+ liveRate?: number;
57
+ /** True when `liveRate` was measured recently enough to call it current. */
58
+ fresh: boolean;
59
+ }
60
+ /**
61
+ * Characters to tokens, bounded and finite.
62
+ *
63
+ * The default is an average over mixed prose and code; every settled step
64
+ * replaces half of it with this session's own measurement, which is what makes
65
+ * the live estimate usable on CJK output (where one character is close to one
66
+ * token) without hardcoding a language.
67
+ */
68
+ export declare function boundTokensPerChar(value: number, fallback?: number): number;
69
+ /**
70
+ * The median of up to the last three rates, or the newest one alone.
71
+ *
72
+ * A single frame in which the terminal coalesced a burst of tokens into one
73
+ * delta reads as a spike; a median of three ignores it, while a real change
74
+ * survives because two agreeing samples decide it. Same rule, and the same
75
+ * reasoning, as the link chip's RTT median.
76
+ */
77
+ export declare function medianRate(values: readonly number[]): number | undefined;
78
+ /**
79
+ * Tracks one step's live decode rate from character deltas.
80
+ *
81
+ * Sampling is the caller's job (`note` per fragment): the window's own
82
+ * minimum span is what keeps the number stable, so a burst of tiny deltas and
83
+ * one coalesced delta produce the same answer.
84
+ */
85
+ /**
86
+ * Whether one code point is written in a script whose characters are close to one
87
+ * token each: CJK ideographs, kana, Hangul syllables, and the fullwidth or CJK
88
+ * punctuation that travels with them.
89
+ *
90
+ * Deliberately a property of the *text* rather than of the reader's locale: a
91
+ * Chinese reader's English reply is Latin text and costs Latin tokens per
92
+ * character, and `~tok/s` has to describe the tokens, not the person.
93
+ */
94
+ export declare function isCjkCodePoint(codePoint: number): boolean;
95
+ /**
96
+ * The starting ratio for a stream of a given script mix: `cjkShare` of the
97
+ * characters read as CJK, the rest as Latin. Continuous by construction, so a
98
+ * reply that is 70% English and 30% Chinese lands 30% of the way up the range
99
+ * instead of being classified as one or the other.
100
+ */
101
+ export declare function scriptTokensPerChar(cjkShare: number): number;
102
+ export declare class ThroughputTracker {
103
+ /** The session's own ratio, once a step has settled. */
104
+ private calibrated;
105
+ /** Characters and CJK characters seen in the step being estimated. */
106
+ private observedChars;
107
+ private observedCjk;
108
+ private samples;
109
+ private chars;
110
+ private baseAt;
111
+ private startedAt;
112
+ private lastDeltaAt;
113
+ private lastStepChars;
114
+ /** Rates already computed this step, kept for the median. */
115
+ private recentRates;
116
+ private lastRate;
117
+ private settledRate;
118
+ /**
119
+ * The ratio the estimate is using right now: this session's own measurement once
120
+ * a step has settled, and the script mix of the stream being estimated before
121
+ * that. `/status` and the tests read it.
122
+ */
123
+ tokensPerChar(): number;
124
+ /** Share of this step's characters that were CJK, 0 when none have arrived. */
125
+ cjkShare(): number;
126
+ /** Whether the ratio is a settled measurement rather than a script estimate. */
127
+ calibratedRatio(): boolean;
128
+ /** Drop a finished step's stream state, keeping calibration and last settle. */
129
+ reset(): void;
130
+ /**
131
+ * One observed fragment of model output.
132
+ *
133
+ * The fragment itself, not a count: the estimate is tokens per character, and
134
+ * how many tokens a character is worth depends on which characters they are, so
135
+ * the script mix has to be measured here rather than assumed once for the
136
+ * session. Code points rather than UTF-16 units, so an emoji or a rare ideograph
137
+ * outside the BMP counts as the one character the reader sees.
138
+ * @param text - the fragment; empty is ignored.
139
+ * @param time - wall clock the fragment arrived at, for tests and replay.
140
+ */
141
+ note(text: string, time?: number): void;
142
+ /**
143
+ * Settle a step: fold its exact decode rate into the calibration and into the
144
+ * number an idle row shows.
145
+ * @param outputTokens - the step's authoritative output tokens.
146
+ * @param decodeMs - milliseconds from first token to the settled message.
147
+ */
148
+ settle(outputTokens: number, decodeMs: number): void;
149
+ /** What the status row paints, and what `/status` reports. */
150
+ view(now?: number): ThroughputView;
151
+ /** Drop samples that fell out of the window, and keep the newest few. */
152
+ private trim;
153
+ /** Recompute the window's rate and fold it into the median buffer. */
154
+ private sampleRate;
155
+ }
156
+ /**
157
+ * A rate as the row paints it: whole numbers, `<1` rather than nothing.
158
+ *
159
+ * A slow step that decoded 3 tokens over 30 seconds is still decoding; rounding
160
+ * it to `0` reads as a stalled model. Above 1000 the number is compacted, since
161
+ * four digits is cells the quota and context meters want more.
162
+ */
163
+ export declare function formatRate(tokensPerSecond: number): string;
@@ -4,6 +4,7 @@
4
4
  import type { AskUserQuestionItem } from '@deepseek-ai/dsh-user-questions';
5
5
  import { type TextSegment } from './term-text.js';
6
6
  import type { DiffDisplayLine, Row, ToolDiffHunk } from './transcript-types.js';
7
+ import { type ToolApproval } from './approval-state.js';
7
8
  export declare const SHELL_TOOL_NAMES: Set<string>;
8
9
  export declare const DIFF_TOOL_NAMES: Set<string>;
9
10
  export declare const JOB_TOOL_NAMES: Set<string>;
@@ -104,6 +105,26 @@ export declare const SUBAGENT_TOOL_NAMES: Set<string>;
104
105
  * just duplicates chrome.
105
106
  */
106
107
  export declare const HIDDEN_TOOL_NAMES: Set<string>;
108
+ /**
109
+ * Tools whose card is the *question* they asked, not a generic tool card (B2.4).
110
+ *
111
+ * `ask_user_question` is the only one: its durable representation is the question
112
+ * card built from the call's arguments, and drawing both was the duplicate this
113
+ * round removes. It is a separate set from {@link HIDDEN_TOOL_NAMES} on purpose —
114
+ * hidden means "no representation at all" (`get_goal`), while this means "the
115
+ * representation is drawn by the question projection". A tool card that never
116
+ * appears is a *different* decision from one whose content is shown elsewhere, and
117
+ * the audit has to be able to tell them apart.
118
+ */
119
+ export declare const QUESTION_TOOL_NAMES: Set<string>;
120
+ /**
121
+ * Tools whose card is the *artifact* they act on, not a tool card of their own.
122
+ *
123
+ * `exit_plan_mode` is the plan review: the plan artifact carries the reviewed body,
124
+ * the review's outcome and the lifecycle lines, so a generic card beside all of that
125
+ * was a third representation of one event (B2.5 §15).
126
+ */
127
+ export declare const PLAN_TOOL_NAMES: Set<string>;
107
128
  export declare function toolTitle(name: string): string;
108
129
  export declare function planReviewOf(question: AskUserQuestionItem): boolean;
109
130
  /** Derive the intended file change from a mutation tool's arguments. */
@@ -142,10 +163,30 @@ export declare function buildToolHeader(input: {
142
163
  add: number;
143
164
  del: number;
144
165
  };
166
+ /**
167
+ * The approval that guarded this call (B2.4), when one did.
168
+ *
169
+ * It rides on the title — right after the tool's own name — rather than at the
170
+ * end of the row: the tail is what a narrow terminal truncates first, and a card
171
+ * whose whole point is "this needed a decision" must not be the one card that
172
+ * loses it. It is also the only place the state is shown, since approval no
173
+ * longer writes a row of its own.
174
+ */
175
+ approval?: ToolApproval;
145
176
  }): {
146
177
  plain: string;
147
178
  segments: TextSegment[];
148
179
  };
180
+ /**
181
+ * The title chip: the state, and — when the reader is being told something
182
+ * weaker — why they can trust it less.
183
+ *
184
+ * `live` and `durable` are the same fact at two moments (a person decided, and
185
+ * the log still says so), so they print the state alone. `policy` adds `auto`,
186
+ * and `inferred` says so: a resume that found only the Harness's refusal sentence
187
+ * in a tool result is a reading, and the card has to admit it.
188
+ */
189
+ export declare function approvalChipText(approval: ToolApproval): string;
149
190
  /** How many terminal rows a tool body occupies after wrapping. */
150
191
  export declare function wrappedToolBodyLineCount(lines: readonly {
151
192
  text: string;
@@ -3,6 +3,10 @@
3
3
  * Kept free of the SshTui class so leaf modules can import them.
4
4
  */
5
5
  export type DisconnectPolicyName = 'pause' | 'continue';
6
+ import type { AskUserQuestionOption } from '@deepseek-ai/dsh-user-questions';
7
+ import type { ToolApproval } from './approval-state.js';
8
+ import type { PlanProvenance, PlanState } from './plan-projection.js';
9
+ import type { RepresentationMeta } from './representation.js';
6
10
  export type SubagentLogKind = 'user' | 'assistant' | 'tool' | 'result' | 'turn' | 'approval' | 'team' | 'system';
7
11
  /** One child-session event folded into a parent-side subagent card. */
8
12
  export interface SubagentLogEntry {
@@ -23,7 +27,19 @@ export interface PlanTodoItem {
23
27
  */
24
28
  status: 'pending' | 'in_progress' | 'completed' | 'failed' | 'skipped';
25
29
  }
26
- export type Row = {
30
+ /**
31
+ * A transcript line, plus the policy that produced it.
32
+ *
33
+ * The metadata is attached by `pushRow` (the only entry), which is why it is
34
+ * optional in the *shape*: a creation site writes a plain row and names its source,
35
+ * and the audit reads the classification back off the row afterwards. It is
36
+ * deliberately not a field each site fills in by hand — one owner, one place to get
37
+ * it wrong.
38
+ */
39
+ export type Row = RowShape & {
40
+ representation?: RepresentationMeta;
41
+ };
42
+ type RowShape = {
27
43
  kind: 'user';
28
44
  text: string;
29
45
  } | {
@@ -63,6 +79,14 @@ export type Row = {
63
79
  totalLines?: number;
64
80
  /** Flip-card animation until this timestamp (ms since epoch). */
65
81
  flipUntil?: number;
82
+ /**
83
+ * The approval that guarded this call, when one did (B2.4).
84
+ *
85
+ * A field of the tool card, not a row of its own: approval is a state of
86
+ * the call. `provenance` says how well it is known, so a resume can never
87
+ * dress an inference up as a decision — see `approval-state.ts`.
88
+ */
89
+ approval?: ToolApproval;
66
90
  } | {
67
91
  kind: 'subagent';
68
92
  sessionId: string;
@@ -96,6 +120,23 @@ export type Row = {
96
120
  expanded: boolean;
97
121
  /** When true the plan stays in the scrolling transcript, not the dock. */
98
122
  archived?: boolean;
123
+ /**
124
+ * The plan artifact this row stands for (B2.5).
125
+ *
126
+ * The row is a *reference*: its state, its steps and its lifecycle come from the
127
+ * projection of durable events, and the id is derived from the sequence that
128
+ * opened the artifact — so a replay finds the same row again instead of
129
+ * rebuilding "the newest live plan" from display order.
130
+ */
131
+ artifactId?: string;
132
+ /** The artifact's folded state. */
133
+ state?: PlanState;
134
+ /** How that state is known: an event said it, or it was read off the log. */
135
+ provenance?: PlanProvenance;
136
+ /** The reader's words when they refused a review with feedback. */
137
+ reviewFeedback?: string;
138
+ /** The revision the two display flags below were last computed for. */
139
+ lastRevisionId?: string;
99
140
  /**
100
141
  * Display-only: the last turn ended while todos were still open.
101
142
  * Does not rewrite the session log.
@@ -112,11 +153,30 @@ export type Row = {
112
153
  } | {
113
154
  kind: 'question';
114
155
  questionId: string;
156
+ /**
157
+ * The `ask_user_question` call this question came from, when the Session
158
+ * recorded one. It is what ties the card to the durable projection, so a
159
+ * timed question answered *after* its window closed can be settled from the
160
+ * log instead of from a request that no longer exists.
161
+ */
162
+ callId?: string;
115
163
  title: string;
116
164
  header?: string;
117
165
  detail?: string;
118
166
  intent: 'ask' | 'plan-review';
167
+ /**
168
+ * The choices the call offered, kept so a question the Session still holds
169
+ * can be answered *as the question it was*: without them a second answer is
170
+ * a free-text box, and `ctx.userQuestions.answer` is handed a batch that does
171
+ * not match the call's own shape.
172
+ */
173
+ options?: AskUserQuestionOption[];
174
+ multiSelect?: boolean;
119
175
  status: 'waiting' | 'answered' | 'cancelled';
176
+ /** True when the Session still accepts an answer for this call. */
177
+ continued?: boolean;
178
+ /** True once the durable projection, not a live request, owns this card. */
179
+ durable?: boolean;
120
180
  summary: string;
121
181
  expanded: boolean;
122
182
  } | {
@@ -197,7 +257,7 @@ export type CollapsibleBlock = Extract<Row, {
197
257
  kind: 'streaming-reasoning';
198
258
  expanded: boolean;
199
259
  };
200
- export type DisplayKind = Row['kind'] | 'tool-result' | 'diff-add' | 'diff-del' | 'diff-path' | 'todo-done' | 'todo-active' | 'todo-pending' | 'todo-failed' | 'todo-skipped' | 'plan-dock' | 'subagent-header';
260
+ export type DisplayKind = Row['kind'] | 'tool-result' | 'diff-add' | 'diff-del' | 'diff-path' | 'todo-done' | 'todo-active' | 'todo-pending' | 'todo-failed' | 'todo-skipped' | 'plan-dock' | 'subagent-header' | 'setup-step' | 'setup-choice' | 'setup-field' | 'setup-step' | 'setup-choice' | 'setup-field';
201
261
  /** One rendered diff/inspect body line with its display role. */
202
262
  export interface DiffDisplayLine {
203
263
  kind: DisplayKind;
@@ -233,3 +293,4 @@ export interface ToolDiffHunk {
233
293
  oldText: string | null;
234
294
  newText: string;
235
295
  }
296
+ export {};