dsh-code 1.0.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.en.md +123 -26
  2. package/README.md +124 -27
  3. package/bin/deepseek.mjs +283 -35
  4. package/cordis.patch.yml +97 -0
  5. package/lib/index.mjs +5008 -881
  6. package/lib/session-query.mjs +150 -0
  7. package/lib/startup.mjs +4 -4
  8. package/lib/{theme-DCT8Y2xf.mjs → theme-7u5Qo3dF.mjs} +657 -20
  9. package/lib/types/app.d.ts +106 -62
  10. package/lib/types/authorization-panel.d.ts +3 -3
  11. package/lib/types/git-workflow.d.ts +91 -2
  12. package/lib/types/i18n.d.ts +39 -0
  13. package/lib/types/index.d.ts +100 -1
  14. package/lib/types/input-split.d.ts +1 -1
  15. package/lib/types/kernel-panels.d.ts +107 -29
  16. package/lib/types/language-panel.d.ts +12 -0
  17. package/lib/types/locales/en.d.ts +450 -0
  18. package/lib/types/locales/zh.d.ts +9 -0
  19. package/lib/types/mentions.d.ts +7 -3
  20. package/lib/types/models.d.ts +14 -0
  21. package/lib/types/panel-accent.d.ts +28 -0
  22. package/lib/types/rainbow.d.ts +69 -0
  23. package/lib/types/render/animations.d.ts +42 -0
  24. package/lib/types/render/editor.d.ts +4 -3
  25. package/lib/types/render/ime-cursor.d.ts +60 -0
  26. package/lib/types/render/inspector.d.ts +26 -0
  27. package/lib/types/render/lines.d.ts +21 -1
  28. package/lib/types/render/markdown.d.ts +1 -1
  29. package/lib/types/render/projection.d.ts +130 -4
  30. package/lib/types/render/status.d.ts +9 -9
  31. package/lib/types/render/text.d.ts +6 -0
  32. package/lib/types/render/usage.d.ts +113 -0
  33. package/lib/types/session-directory.d.ts +17 -0
  34. package/lib/types/session-query.d.ts +92 -0
  35. package/lib/types/startup.d.ts +1 -1
  36. package/lib/types/terminal-title.d.ts +66 -0
  37. package/lib/types/theme-panel.d.ts +2 -2
  38. package/lib/types/theme.d.ts +271 -52
  39. package/lib/types/update-panel.d.ts +49 -0
  40. package/lib/types/update.d.ts +75 -0
  41. package/lib/types/version.d.ts +4 -3
  42. package/package.json +246 -90
  43. package/src/app.ts +1369 -509
  44. package/src/approval.ts +166 -166
  45. package/src/authorization-panel.ts +19 -16
  46. package/src/editor-keys.ts +371 -371
  47. package/src/git-workflow.ts +229 -3
  48. package/src/i18n.ts +68 -0
  49. package/src/index.ts +534 -80
  50. package/src/input-split.ts +3 -3
  51. package/src/internals.ts +5 -0
  52. package/src/kernel-panels.ts +554 -86
  53. package/src/keyboard.ts +5 -4
  54. package/src/language-panel.ts +53 -0
  55. package/src/locales/en.ts +489 -0
  56. package/src/locales/zh.ts +488 -0
  57. package/src/mentions.ts +8 -4
  58. package/src/models.ts +264 -212
  59. package/src/panel-accent.ts +41 -0
  60. package/src/presets.ts +1 -1
  61. package/src/provider-settings.ts +1 -1
  62. package/src/rainbow.ts +208 -0
  63. package/src/render/animations.ts +104 -6
  64. package/src/render/editor.ts +25 -24
  65. package/src/render/export.ts +116 -95
  66. package/src/render/ime-cursor.ts +147 -0
  67. package/src/render/inspector.ts +42 -0
  68. package/src/render/lines.ts +628 -415
  69. package/src/render/markdown.ts +15 -3
  70. package/src/render/projection.ts +572 -21
  71. package/src/render/status.ts +59 -39
  72. package/src/render/text.ts +14 -0
  73. package/src/render/tool-preview.ts +77 -77
  74. package/src/render/usage.ts +430 -0
  75. package/src/render/width.ts +2 -2
  76. package/src/session-directory.ts +8 -6
  77. package/src/session-query.ts +239 -0
  78. package/src/startup.ts +3 -3
  79. package/src/subagents.ts +229 -229
  80. package/src/terminal-title.ts +190 -0
  81. package/src/theme-panel.ts +17 -21
  82. package/src/theme.ts +281 -33
  83. package/src/update-panel.ts +256 -0
  84. package/src/update.ts +126 -0
  85. package/src/version.ts +58 -20
  86. package/src/whale-glyph.ts +23 -23
@@ -54,6 +54,19 @@ export declare function deepDivingGradientColor(index: number, tick: number, gra
54
54
  export declare function deepDivingSparkIntensity(tick: number): number;
55
55
  /** Blue RGB color for the breathing Deep diving sparkle. */
56
56
  export declare function deepDivingSparkColor(tick: number, base: RgbTriple, highlight: RgbTriple): RgbTriple;
57
+ /** One full prismatic flow lap — lively: violet → fuchsia → cyan → violet in 2.4s. */
58
+ export declare const FLOW_PERIOD_MS = 2400;
59
+ /**
60
+ * The prismatic flow color at one elapsed-time sample: a smoothstep walk
61
+ * around the anchor triangle, one full lap per {@link FLOW_PERIOD_MS}. Pure —
62
+ * the Ink layer owns the timer and passes its tick scaled by its own cadence
63
+ * (tick × tickMs). Callers gate on the animations preference and fall back to
64
+ * a static palette color when animation is off.
65
+ * @param elapsedMs - milliseconds since the flow started (any sign or size).
66
+ * @param anchors - the colors to walk, in order (theme.ts FLOW_ANCHORS).
67
+ * @returns the interpolated anchor color at this instant.
68
+ */
69
+ export declare function flowColor(elapsedMs: number, anchors: readonly RgbTriple[]): RgbTriple;
57
70
  /** Caret blink cadence: one blink step (on or off) per tick. */
58
71
  export declare const CARET_BLINK_TICK_MS = 530;
59
72
  /** Caret visibility: half the ticks on, half off (530ms blink). */
@@ -174,6 +187,35 @@ export declare function easeInOut(progress: number): number;
174
187
  * @returns the envelope value in 0..1.
175
188
  */
176
189
  export declare function envelope(elapsed: number, total: number, fadeIn: number, fadeOut: number): number;
190
+ /**
191
+ * The /rainbow celebration on the three-row composer band: a FIXED
192
+ * seven-color spectrum (not the rolled palette) that slides across every
193
+ * row as one ribbon. Triggered when switching to rainbow or rerolling the
194
+ * seed; independent of RAINBOW_SEED so the burst always reads as a prism.
195
+ */
196
+ export declare const RAINBOW_BURST_TICK_MS = 33;
197
+ export declare const RAINBOW_BURST_DURATION_MS = 1800;
198
+ export declare const RAINBOW_BURST_HUES: readonly RgbTriple[];
199
+ /**
200
+ * Smoothstep in 0..1, then wrap-lerp around the seven burst hues.
201
+ * Position 0 is red, wrapping back toward red at 1. Shared by the
202
+ * composer burst and the static rainbow whale header.
203
+ */
204
+ export declare function rainbowSpectrumHue(position: number): RgbTriple;
205
+ /**
206
+ * Background tint for one composer-band cell during the rainbow burst.
207
+ * Every row of a column shares the same hue (a solid ribbon); edge rows
208
+ * are slightly dimmer so the middle editor row reads as the crest. The
209
+ * spectrum slides ~1.2 widths over the burst, then fades to the band base.
210
+ * @param tick - frame index at {@link RAINBOW_BURST_TICK_MS}.
211
+ * @param column - band column (0..width-1).
212
+ * @param width - band width in columns.
213
+ * @param base - the theme's composerBand color to blend toward.
214
+ * @param row - band row (0..rows-1).
215
+ * @param rows - band height (composer is three rows: pad, editor, pad).
216
+ * @returns the blended RGB, or null after the burst (or at zero alpha).
217
+ */
218
+ export declare function rainbowBurstColumnBg(tick: number, column: number, width: number, base: RgbTriple, row?: number, rows?: number): RgbTriple | null;
177
219
  /**
178
220
  * The background color for one composer-band column at a tick — Codex
179
221
  * `paint_bands` + `Canvas::tint` for all three styles. Bands overlap with a
@@ -155,8 +155,9 @@ export declare function replaceRangePreservingCursor(value: string, cursor: numb
155
155
  */
156
156
  export declare function composerMaxRows(terminalRows: number): number;
157
157
  /**
158
- * History navigation starts with Up on an empty draft, or after visual
159
- * movement has reached the directional text edge of an unchanged recalled
160
- * entry. Every other position remains under textarea movement.
158
+ * History navigation starts with Up on an empty draft, or continues from an
159
+ * unchanged recalled entry whenever the caret sits on either text edge
160
+ * (start or end) - moving the caret into the interior returns the keys to
161
+ * ordinary editing until an edge is reached again.
161
162
  */
162
163
  export declare function shouldRecallNavigate(value: string, cursor: number, lastRecalled: string | null, direction: -1 | 1): boolean;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * IME cursor anchoring for the composer.
3
+ *
4
+ * Ink keeps the real terminal cursor hidden and parks it just below the
5
+ * dynamic tree (after the status row). IME composition text and candidate
6
+ * windows anchor to that real cursor cell - VS Code's integrated terminal
7
+ * positions its hidden IME textarea there, and Windows consoles behave the
8
+ * same - so CJK input appeared at the bottom of the screen instead of at the
9
+ * caret. The anchor moves the real cursor onto the caret cell without
10
+ * touching Ink's relative erase ledger:
11
+ *
12
+ * - the displacement is owned: before any foreign write or re-anchor, the
13
+ * wrapper cancels it (cursor down, column 1), so every writer - Ink's
14
+ * log-update rewrites, the resize replay, protocol pushes - keeps seeing
15
+ * the cursor exactly where it was left;
16
+ * - log-update frame chunks (which start with the erase-line sequence) get
17
+ * the anchor re-appended inside the same write, so a repaint can never
18
+ * leave the cursor behind - in particular the caret blink keeps the anchor
19
+ * stable while an IME composition is open, because the terminal parses the
20
+ * rewrite and the re-anchor as one atomic update.
21
+ *
22
+ * @module @deepseek-ai/dsh-code/render/ime-cursor
23
+ */
24
+ /** Cancel `rows` of owned upward displacement and return to column 1. */
25
+ export declare function imeCursorRestore(rows: number): string;
26
+ /** Move onto the caret cell: `rows` up from Ink's parked row, 0-based column. */
27
+ export declare function imeCursorMove(rows: number, column: number): string;
28
+ /**
29
+ * Rows between the caret cell and Ink's parked cursor row: the band's bottom
30
+ * blank row, the editor window rows below the caret, the status footer, and
31
+ * Ink's own below-frame row. Rows above the composer (gutter, live content)
32
+ * never enter this distance.
33
+ */
34
+ export declare function imeCursorRowsUp(input: {
35
+ editorWindowRows: number;
36
+ caretRowInWindow: number;
37
+ rowsBelowComposer: number;
38
+ }): number;
39
+ /** The installed anchor handle. */
40
+ export interface ImeCursorAnchor {
41
+ /** Anchor on the caret cell: `rows` above Ink's parked row at the 0-based
42
+ * `column`; `rows <= 0` releases the anchor. */
43
+ anchor(rows: number, column: number): void;
44
+ /** Cancel the displacement, restore the original write path, and detach. */
45
+ release(): void;
46
+ }
47
+ /**
48
+ * Take over `stream.write` so the anchor displacement stays invisible to every
49
+ * other writer. Idempotent per stream: a second install returns the live
50
+ * handle. Returns `undefined` on non-TTY streams where anchoring is meaningless.
51
+ */
52
+ export declare function installImeCursorAnchor(stream: NodeJS.WriteStream): ImeCursorAnchor | undefined;
53
+ /**
54
+ * Keep the real terminal cursor on the composer's caret cell while `active`
55
+ * (the editable composer). The second effect runs after every commit without
56
+ * a dep list: frame rewrites restore the anchor themselves, but any other
57
+ * write (protocol push, resize replay) leaves the cursor at Ink's parked
58
+ * position, and the next commit re-anchors it.
59
+ */
60
+ export declare function useImeCursorAnchor(active: boolean, rows: number, column: number): void;
@@ -16,6 +16,32 @@ export interface InspectorViewport {
16
16
  }
17
17
  /** One transcript-to-composer gutter, collapsed on short terminals. */
18
18
  export declare function layoutGutterRows(rows: number): 0 | 1;
19
+ /** Measured chrome that sits below (or in) the live region. */
20
+ export interface LiveRegionChrome {
21
+ /** Terminal rows. */
22
+ readonly terminalRows: number;
23
+ /** Editor rows inside the composer band (frozen reports 1). */
24
+ readonly composerRows: number;
25
+ /** Status footer rows. */
26
+ readonly statusBarRows: 1 | 2;
27
+ /** Completion menu rows (0 when closed). */
28
+ readonly menuRows: number;
29
+ /** Transcript-to-composer gutter. */
30
+ readonly gutterRows: 0 | 1;
31
+ /** Notice line present. */
32
+ readonly notice: boolean;
33
+ /** Todo summary line present. */
34
+ readonly todo: boolean;
35
+ /** Agents summary line present. */
36
+ readonly agents: boolean;
37
+ }
38
+ /**
39
+ * Rows left for live transcript and streaming after pinning the composer and
40
+ * status at the bottom. Variable chrome (menu, notice, todos, agents, extra
41
+ * status row, extra editor rows) is deducted here so those rows cover the
42
+ * live region instead of growing the tree and moving the bottom bar.
43
+ */
44
+ export declare function liveRegionBudget(chrome: LiveRegionChrome): number;
19
45
  /**
20
46
  * Keep the inspector plus its persistent status/composer chrome below
21
47
  * `stdout.rows`: at equality Ink clears the terminal and rewrites all
@@ -2,7 +2,7 @@
2
2
  import { type TranscriptEntry } from './projection.ts';
3
3
  import { type MdStyle } from './markdown.ts';
4
4
  /** Presentation classes mapped to Ink colors by the app boundary. */
5
- export type LineStyle = MdStyle | 'brand' | 'success' | 'error' | 'warn' | 'dimItalic';
5
+ export type LineStyle = MdStyle | 'brand' | 'success' | 'error' | 'warn' | 'dimItalic' | 'diffAdd' | 'diffDel' | 'promptRow' | 'promptQueuedRow' | 'promptSteeredRow';
6
6
  /** One styled run within a physical terminal row. */
7
7
  export interface StyledSegment {
8
8
  text: string;
@@ -14,6 +14,18 @@ export interface StyledLine {
14
14
  }
15
15
  /** Construct one segment without leaking mutable objects into cached rows. */
16
16
  export declare function lineSegment(text: string, style?: LineStyle): StyledSegment;
17
+ /**
18
+ * Classify one unified-diff row for coloring: additions and deletions carry
19
+ * the diff tint styles (background on rich terminals), hunk headers and file
20
+ * markers stay brand-blue, and everything else is dim context.
21
+ */
22
+ export declare function diffLineStyle(line: string): LineStyle;
23
+ /**
24
+ * Extend pure diff-tinted rows to the full width with same-styled padding so
25
+ * the tint reads as one unbroken bar (GitHub-style), including wrapped
26
+ * continuation rows; mixed or non-diff rows pass through untouched.
27
+ */
28
+ export declare function fillDiffLineBars(lines: readonly StyledLine[], columns: number): readonly StyledLine[];
17
29
  /**
18
30
  * Sanitize and hard-wrap styled content into exact physical rows.
19
31
  * Tabs become two visible spaces because terminal tab stops are contextual
@@ -30,6 +42,14 @@ export declare function markdownLines(text: string, columns: number): readonly S
30
42
  * reasoning content and assistant Markdown share one left edge.
31
43
  */
32
44
  export declare function reasoningLines(text: string, columns: number): readonly StyledLine[];
45
+ /**
46
+ * Style the lines of one user prompt for display: plain verbatim, except
47
+ * inside ```diff / ```patch fences where added and removed lines take the
48
+ * shared diff tints (the review prompt pastes its diff this way, and the
49
+ * user row does not go through the markdown renderer). The fence markers
50
+ * and file headers stay plain.
51
+ */
52
+ export declare function userPromptSegments(text: string): readonly StyledSegment[];
33
53
  /**
34
54
  * Convert one durable transcript entry to its complete scrollable row model.
35
55
  * The source entry stays intact; only the caller's visible slice is rendered.
@@ -9,7 +9,7 @@
9
9
  * @module @deepseek-ai/dsh-code/render/markdown
10
10
  */
11
11
  /** Style classes the renderer emits; the app maps them to colors/props. */
12
- export type MdStyle = 'plain' | 'bold' | 'italic' | 'boldItalic' | 'code' | 'accent' | 'accentBold' | 'dim' | 'strike';
12
+ export type MdStyle = 'plain' | 'bold' | 'italic' | 'boldItalic' | 'code' | 'accent' | 'accentBold' | 'dim' | 'strike' | 'diffAdd' | 'diffDel';
13
13
  /** One styled run of text. */
14
14
  export interface MdSegment {
15
15
  /** Visible text (no ANSI). */
@@ -23,6 +23,12 @@ export interface UserEntry {
23
23
  images?: readonly ImageBlock['attachment'][];
24
24
  /** Durable file references carried by this prompt (0.1.5 file blocks). */
25
25
  files?: readonly FileAttachmentRef[];
26
+ /**
27
+ * How the prompt reached the agent, when it did not arrive as an ordinary
28
+ * submission: `queued` waited for this turn, `steered` joined it mid-flight.
29
+ * Absent for a prompt typed straight into an idle composer.
30
+ */
31
+ delivery?: 'queued' | 'steered';
26
32
  }
27
33
  /** One user message waiting in the agent inbox (the web's queued-message row). */
28
34
  export interface PendingEntry {
@@ -79,6 +85,68 @@ export interface ToolEntry {
79
85
  * result lands and only when something renderable exists.
80
86
  */
81
87
  detail: ToolDetail | undefined;
88
+ /**
89
+ * Nested PTC sub-dispatches (`run_code`) in start order, bounded to the
90
+ * newest {@link MAX_TOOL_SUB_DISPATCHES} rows.
91
+ */
92
+ subs: readonly ToolSubDispatch[];
93
+ /** Sub-dispatches evicted from the bounded window. */
94
+ subsDropped: number;
95
+ }
96
+ /**
97
+ * One nested PTC sub-dispatch under a `run_code` parent call: the durable
98
+ * `tool/ptc-dispatch-start`/`tool/ptc-dispatch` pair folded to one bounded
99
+ * row (upstream contract: pair by `subCallId`, every start settles, and the
100
+ * settle carries `tool/result`'s own vocabulary).
101
+ */
102
+ export interface ToolSubDispatch {
103
+ /** Opaque sub-call id pairing the start with its settle. */
104
+ subCallId: string;
105
+ /** Sub-call tool name. */
106
+ name: string;
107
+ /** Bounded arguments preview. */
108
+ preview: string;
109
+ /** Lifecycle; `running` until the paired settle lands. */
110
+ state: 'running' | 'done' | 'error';
111
+ /** Bounded first text block of the settle content, '' until it lands. */
112
+ summary: string;
113
+ /** Wall-clock duration (settle − start), 0 while running. */
114
+ durationMs: number;
115
+ }
116
+ /** Bounded sub-dispatch window per tool card (display budget only). */
117
+ export declare const MAX_TOOL_SUB_DISPATCHES = 12;
118
+ /** Bounded member window per workflow run card (display budget only). */
119
+ export declare const MAX_WORKFLOW_MEMBERS = 12;
120
+ /** One workflow member (an `agent()` call inside a `workflow` script). */
121
+ export interface WorkflowMember {
122
+ /** Member sequence within the run (the agent-start/agent-end pairing key). */
123
+ seq: number;
124
+ /** Display label. */
125
+ label: string;
126
+ /** Declared phase title, '' when none. */
127
+ phase: string;
128
+ /** Child session id (cross-links the subagent feed's rows). */
129
+ childId: string;
130
+ /** Settlement; `running` until the paired `tool-workflow/agent-end`. */
131
+ outcome: 'running' | 'completed' | 'failed' | 'cancelled';
132
+ }
133
+ /**
134
+ * One durable workflow run (the `tool-workflow/*` record a `workflow` or
135
+ * `ralph` tool appends to the parent session): run identity plus its
136
+ * bounded member list, live until `tool-workflow/run-end` settles.
137
+ */
138
+ export interface WorkflowEntry {
139
+ kind: 'workflow';
140
+ /** Stable run identity shared by every event of the run. */
141
+ runId: string;
142
+ /** Display name of the run. */
143
+ name: string;
144
+ /** Members in sequence order, bounded to the newest window. */
145
+ members: readonly WorkflowMember[];
146
+ /** Members evicted from the bounded window. */
147
+ membersDropped: number;
148
+ /** Run settlement; `running` until `tool-workflow/run-end`. */
149
+ state: 'running' | 'completed' | 'cancelled' | 'error';
82
150
  }
83
151
  /** One slash-command execution dispatched through `ctx.commands`. */
84
152
  export interface CommandEntry {
@@ -144,7 +212,7 @@ export interface FilesEntry {
144
212
  paths: readonly string[];
145
213
  }
146
214
  /** Ordered transcript items the renderer draws. */
147
- export type TranscriptEntry = UserEntry | PendingEntry | AssistantEntry | ToolEntry | CommandEntry | ErrorEntry | TurnMarkerEntry | CompactionEntry | RetryEntry | FilesEntry;
215
+ export type TranscriptEntry = UserEntry | PendingEntry | AssistantEntry | ToolEntry | CommandEntry | ErrorEntry | TurnMarkerEntry | CompactionEntry | RetryEntry | FilesEntry | WorkflowEntry;
148
216
  /** The live goal the status line badges, folded from `goal/change`. */
149
217
  export interface GoalFold {
150
218
  /** Human-requested completion objective. */
@@ -157,14 +225,21 @@ export interface GoalFold {
157
225
  /** Blocked explanation, empty outside the blocked phase. */
158
226
  blocked: string;
159
227
  }
160
- /** Cumulative token accounting folded from `assistant/message` usage reports. */
228
+ /**
229
+ * Cumulative token accounting folded from `assistant/message` usage reports.
230
+ * The buckets are disjoint and mirror the provider's report, so the prompt
231
+ * side is never double counted: reasoning tokens are already inside
232
+ * `outputTokens`, and cache reads are never folded into the uncached input.
233
+ */
161
234
  export interface UsageTotals {
162
- /** Prompt-side billed tokens: `inputTokens` plus both cache buckets. */
163
- inputTokens: number;
235
+ /** Prompt-side tokens billed outside the cache. */
236
+ uncachedInputTokens: number;
164
237
  /** Completion-side tokens over the whole log. */
165
238
  outputTokens: number;
166
239
  /** Cache-read tokens over the whole log (0 when the adapter reports none). */
167
240
  cacheReadTokens: number;
241
+ /** Cache-write tokens over the whole log (0 when the adapter reports none). */
242
+ cacheWriteTokens: number;
168
243
  }
169
244
  /**
170
245
  * Estimated used tokens per context content type, folded from transcript
@@ -219,6 +294,38 @@ export interface TranscriptStats {
219
294
  */
220
295
  reasoningEffort: string;
221
296
  }
297
+ /** One active reminder folded from durable `schedule/change` events. */
298
+ export interface ScheduleRow {
299
+ readonly id: string;
300
+ readonly kind: 'after' | 'at' | 'every';
301
+ readonly prompt: string;
302
+ /** Next due time (epoch ms); the /schedule panel derives overdue/relative labels. */
303
+ readonly targetAt: number;
304
+ /** Recurrence seconds for 'every' rows, undefined otherwise. */
305
+ readonly everySeconds?: number;
306
+ }
307
+ /**
308
+ * The durable `schedule/change` payload shape this fold consumes. Upstream
309
+ * strict-decodes the whole transition stream before appending, so unknown
310
+ * ids here are corrupt-input edges that degrade to a no-op.
311
+ */
312
+ export interface ScheduleChangeLike {
313
+ readonly operation: 'create' | 'delete' | 'dispatch';
314
+ readonly schedule?: {
315
+ readonly id: string;
316
+ readonly kind: 'after' | 'at' | 'every';
317
+ readonly prompt: string;
318
+ readonly afterSeconds?: number;
319
+ readonly everySeconds?: number;
320
+ readonly scheduledAt: string;
321
+ };
322
+ readonly id?: string;
323
+ readonly acceptedAt?: string;
324
+ }
325
+ /** Fold one `schedule/change` into the active-reminder list (create/delete/dispatch). */
326
+ export declare function applyScheduleChange(rows: readonly ScheduleRow[], data: ScheduleChangeLike): readonly ScheduleRow[];
327
+ /** First anchor-aligned target after `acceptedAt`, stepping from the previous aligned target. */
328
+ export declare function nextEveryTarget(previousTarget: number, acceptedAt: number, everySeconds: number): number;
222
329
  /** The complete TUI transcript view for one session. */
223
330
  export interface TranscriptView {
224
331
  /** Settled entries in log order. */
@@ -265,6 +372,8 @@ export interface TranscriptView {
265
372
  sandbox: string;
266
373
  /** Current long-running goal folded from the last `goal/change`, undefined when cleared. */
267
374
  goal: GoalFold | undefined;
375
+ /** Active reminders folded from `schedule/change` events, oldest target first at render. */
376
+ schedules: readonly ScheduleRow[];
268
377
  /**
269
378
  * Ordered live message ids per inbox target, mirrored from
270
379
  * `agent/inbox/spliced` exactly like the upstream Inbox projection — the
@@ -274,6 +383,14 @@ export interface TranscriptView {
274
383
  'next-turn': readonly string[];
275
384
  'next-step': readonly string[];
276
385
  };
386
+ /**
387
+ * In-flight inbox messages by the list they were inserted into, kept until
388
+ * the durable user message that claims them lands. The claim itself is a
389
+ * plain splice that empties the pending row first, so this map — not
390
+ * {@link pending} — is what lets a settled prompt say it was queued or
391
+ * steered rather than typed into an idle composer.
392
+ */
393
+ claimOrigin: ReadonlyMap<string, 'next-turn' | 'next-step'>;
277
394
  /**
278
395
  * Fold-internal timing anchors, never rendered: open step and tool-call
279
396
  * start timestamps the next `assistant/message` / `tool/result` resolves
@@ -285,6 +402,8 @@ export interface TranscriptView {
285
402
  readonly anchors: {
286
403
  stepStart: Map<string, number>;
287
404
  toolStart: Map<string, number>;
405
+ /** Open PTC sub-dispatch starts by `subCallId` (duration anchors). */
406
+ subStart: Map<string, number>;
288
407
  firstChunkAt: Map<string, number>;
289
408
  compactionTokens: Map<string, number>;
290
409
  lastPruneTokens: number;
@@ -341,11 +460,15 @@ export interface ReplayAccumulator {
341
460
  retryIndex: Map<string, number[]>;
342
461
  /** messageId → every index into `entries` holding a `pending` row with that id. */
343
462
  pendingIndex: Map<string, number[]>;
463
+ /** runId → every index into `entries` holding a `workflow` row with that id. */
464
+ workflowIndex: Map<string, number[]>;
344
465
  /** Tombstone count; zero means `entries` is already the final array. */
345
466
  removedCount: number;
346
467
  /** Mutable inbox id lists, mirroring `view.pending` order per target. */
347
468
  pendingTurn: string[];
348
469
  pendingStep: string[];
470
+ /** Mutable mirror of `view.claimOrigin` (see the reducer's field doc). */
471
+ claimOrigin: Map<string, 'next-turn' | 'next-step'>;
349
472
  streaming: string;
350
473
  streamingReasoning: string;
351
474
  todos: readonly TodoItem[];
@@ -360,9 +483,12 @@ export interface ReplayAccumulator {
360
483
  systemPrompt: string;
361
484
  sandbox: string;
362
485
  goal: GoalFold | undefined;
486
+ schedules: readonly ScheduleRow[];
363
487
  stats: TranscriptStats;
364
488
  stepStart: Map<string, number>;
365
489
  toolStart: Map<string, number>;
490
+ /** Open PTC sub-dispatch starts by `subCallId` (duration anchors). */
491
+ subStart: Map<string, number>;
366
492
  firstChunkAt: Map<string, number>;
367
493
  compactionTokens: Map<string, number>;
368
494
  lastPruneTokens: number;
@@ -10,13 +10,8 @@
10
10
  * @module @deepseek-ai/dsh-tui/render/status
11
11
  */
12
12
  import type { TranscriptStats } from './projection.ts';
13
- /**
14
- * Compact token count: 517 / 12.2K / 517K / 1.2M (one decimal under three
15
- * digits), mirroring the web composer's StatsLine format.
16
- * @param n - token count.
17
- * @returns display string.
18
- */
19
- export declare function formatTokens(n: number): string;
13
+ import { formatTokens } from './text.ts';
14
+ export { formatTokens };
20
15
  /**
21
16
  * Compact duration: 45.2s under a minute, 2m42s from there on.
22
17
  * @param ms - duration in milliseconds.
@@ -24,7 +19,9 @@ export declare function formatTokens(n: number): string;
24
19
  */
25
20
  export declare function formatDuration(ms: number): string;
26
21
  /**
27
- * Cache-hit share of billed prompt-side input.
22
+ * Cache-hit share of billed prompt-side input. The denominator is the same
23
+ * billed total the /usage panel shows (uncached input plus both cache
24
+ * buckets), so the two readouts can never disagree.
28
25
  * @param usage - cumulative token totals.
29
26
  * @returns percent rounded to one decimal place, or null when no input was billed.
30
27
  */
@@ -33,7 +30,7 @@ export declare function cacheHitPercent(usage: TranscriptStats['usage']): number
33
30
  * Presentation tones for status spans; the footer maps each to a theme color
34
31
  * (Codex status-line accents: model/path/branch/state/usage categories).
35
32
  */
36
- export type StatusTone = 'model' | 'live' | 'path' | 'branch' | 'value' | 'label' | 'meta' | 'accent' | 'success' | 'warn' | 'error' | 'ctxFill';
33
+ export type StatusTone = 'model' | 'live' | 'path' | 'branch' | 'value' | 'label' | 'meta' | 'accent' | 'success' | 'plan' | 'warn' | 'error' | 'ctxFill';
37
34
  /** One colored run inside the status bar. */
38
35
  export interface StatusSpan {
39
36
  text: string;
@@ -70,7 +67,10 @@ export declare const STATUS_GROUP_SEPARATOR = " | ";
70
67
  /** Separator between trailing state spans. */
71
68
  export declare const STATUS_ITEM_SEPARATOR = " \u00B7 ";
72
69
  /** The Codex-style mode cycle hint appended to the permission badge. */
70
+ /** English compatibility value for callers that only measure the default layout. */
73
71
  export declare const STATUS_CYCLE_HINT = " (shift+tab to cycle)";
72
+ /** Localized mode-cycle hint used by the live layout. */
73
+ export declare function statusCycleHint(): string;
74
74
  /**
75
75
  * Interior columns of the context bar. The layout starts every bar at this
76
76
  * width so the drop ladder can pre-measure the group, then degrades the
@@ -11,6 +11,12 @@
11
11
  *
12
12
  * @module @deepseek-ai/dsh-code/render/text
13
13
  */
14
+ /**
15
+ * Compact token count: exact below 1K, then one-decimal-ish K/M.
16
+ * @param n - token count.
17
+ * @returns display string.
18
+ */
19
+ export declare function formatTokens(n: number): string;
14
20
  /**
15
21
  * Escape control and deceptive characters so externally sourced text cannot
16
22
  * drive the terminal. C0/C1/DEL render as a literal `\xNN` escape; bidi,
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Session usage for the /usage panel: the provider-reported token totals, the
3
+ * context pressure and composition, and the exact per-turn accounting, folded
4
+ * into the styled rows the panel draws.
5
+ *
6
+ * Every figure comes from the harness token meter. The four token buckets are
7
+ * disjoint, so the prompt side is never double counted; the composition block
8
+ * is a density estimate and is labelled as one.
9
+ *
10
+ * @module @deepseek-ai/dsh-tui/render/usage
11
+ */
12
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
13
+ import type { TokenUsageProjection, TurnTokenUsage } from '@deepseek-ai/dsh-token-meter/client';
14
+ import { type StyledLine } from './lines.ts';
15
+ /** One completed turn's exact provider-reported accounting. */
16
+ export interface UsageTurn {
17
+ /** Durable turn number (`turn/start`). */
18
+ readonly turn: number;
19
+ readonly usage: TurnTokenUsage;
20
+ /**
21
+ * The model that billed this turn, or '' when nothing in the log names one.
22
+ * The meter's own `routes` is preferred; it is absent whenever ONE attempt
23
+ * went unattributed, so the turn's own assistant messages answer instead.
24
+ */
25
+ readonly model: string;
26
+ }
27
+ /**
28
+ * Everything the panel reads. The totals come from the mounted projection and
29
+ * the per-turn rows from the meter's own fold; a missing projection renders as
30
+ * explicitly unavailable rather than as zeros, because an unmounted deployment
31
+ * and a session with no traffic are different facts.
32
+ */
33
+ export interface UsageView {
34
+ /** Provider-reported usage over the whole durable log. */
35
+ readonly totals?: TokenUsageProjection;
36
+ /** Completed turns, oldest first. */
37
+ readonly turns: readonly UsageTurn[];
38
+ }
39
+ /** Prompt-side tokens the provider billed: uncached input plus both cache buckets. */
40
+ export declare function billedInputTokens(totals: TokenUsageProjection): number;
41
+ /** Prompt plus completion tokens over the whole log. */
42
+ export declare function usageTotalTokens(totals: TokenUsageProjection): number;
43
+ /**
44
+ * Cache-hit share of billed prompt-side input.
45
+ * @param totals - cumulative provider-reported buckets.
46
+ * @returns percent rounded to one decimal place, or null when nothing was billed.
47
+ */
48
+ export declare function usageCacheHitPercent(totals: TokenUsageProjection): number | null;
49
+ /** One complete turn's durable events, in log order. */
50
+ export interface TurnSlice {
51
+ readonly turn: number;
52
+ readonly events: readonly SessionEvent[];
53
+ }
54
+ /**
55
+ * Split a durable log into COMPLETE turns. A turn still running has no
56
+ * `turn/end` yet, and an unfinished attempt has no exact accounting, so the
57
+ * trailing slice is dropped rather than guessed at.
58
+ * @param events - the whole durable log, in log order.
59
+ * @returns one slice per `turn/start`…`turn/end` span, oldest first.
60
+ */
61
+ export declare function completedTurns(events: readonly SessionEvent[]): readonly TurnSlice[];
62
+ /**
63
+ * Exact usage for every completed turn that can be proven.
64
+ * @param events - the whole durable log, in log order.
65
+ * @param derive - the meter's per-turn fold, injected so this module stays pure.
66
+ * @returns one row per provable turn, oldest first; turns whose attempts did
67
+ * not all report usage are omitted rather than estimated, and so is a turn
68
+ * that billed nothing at all — an empty row is noise in a usage table.
69
+ */
70
+ export declare function turnUsages(events: readonly SessionEvent[], derive: (events: readonly SessionEvent[]) => TurnTokenUsage | undefined): readonly UsageTurn[];
71
+ /** A bucket some turn of a group did not report, leaving the group sum a floor. */
72
+ export type PartialBucket = 'cacheReadTokens' | 'cacheWriteTokens' | 'reasoningTokens';
73
+ /** One model's merged totals across every turn it billed. */
74
+ export interface ModelUsage {
75
+ /** Model names joined with ` + ` (a turn that switched models lists both). */
76
+ readonly model: string;
77
+ /** Turns attributed to this model. */
78
+ readonly turns: number;
79
+ readonly uncachedInputTokens: number;
80
+ readonly outputTokens: number;
81
+ readonly totalTokens: number;
82
+ /** Summed over the turns that reported it; absent when none did. */
83
+ readonly cacheReadTokens?: number;
84
+ /** Summed over the turns that reported it; absent when none did. */
85
+ readonly cacheWriteTokens?: number;
86
+ /** Summed over the turns that reported it; absent when none did. */
87
+ readonly reasoningTokens?: number;
88
+ /**
89
+ * Buckets some turn of the group left unreported. The corresponding sum (and
90
+ * the hit share derived from it) counts only what WAS reported, so the
91
+ * display marks it as a floor rather than hiding the group's known traffic.
92
+ */
93
+ readonly partial: readonly PartialBucket[];
94
+ }
95
+ /**
96
+ * Merge the per-turn rows by the model that billed them, biggest spender
97
+ * first. A turn lands in exactly one group — the group named by every model it
98
+ * used — so the sums stay additive and a mid-turn model switch never counts
99
+ * the same tokens twice. Each bucket is merged over the turns that reported
100
+ * it, and {@link ModelUsage.partial} records the ones that stayed silent.
101
+ * @param turns - provable per-turn rows, oldest first.
102
+ * @returns one row per model group.
103
+ */
104
+ export declare function modelTotals(turns: readonly UsageTurn[]): readonly ModelUsage[];
105
+ /** The model attribution for one turn: the meter's routes, else its messages. */
106
+ export declare function turnModel(slice: TurnSlice, usage: TurnTokenUsage): string;
107
+ /**
108
+ * Render the panel body.
109
+ * @param view - the projection totals plus the derived per-turn rows.
110
+ * @param columns - usable content columns inside the panel border.
111
+ * @returns bounded, styled rows ready to draw.
112
+ */
113
+ export declare function usageLines(view: UsageView, columns: number): readonly StyledLine[];
@@ -24,6 +24,23 @@ export interface SessionQueryService {
24
24
  listSessions(signal?: AbortSignal): Promise<SessionRecord[]>;
25
25
  readTitleSnapshots(ids: readonly string[], signal?: AbortSignal): Promise<TitleObservationResult[]>;
26
26
  readSession(id: string, signal?: AbortSignal): Promise<SessionLogSnapshot>;
27
+ /** Cross-session full-text search (SQLite FTS engine; openAt may gate it). */
28
+ searchSessions(request: {
29
+ query: string;
30
+ limit?: number;
31
+ }, exec?: {
32
+ signal?: AbortSignal;
33
+ }): Promise<{
34
+ items: readonly {
35
+ header: SessionHeader;
36
+ live: boolean;
37
+ persisted: boolean;
38
+ bestMatch: {
39
+ snippet: string;
40
+ time: number;
41
+ };
42
+ }[];
43
+ }>;
27
44
  }
28
45
  export type SessionScope = 'roots' | 'all';
29
46
  export type CwdScope = 'all' | 'current';