dsh-code 0.7.0 → 0.9.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 (49) hide show
  1. package/README.en.md +30 -7
  2. package/README.md +30 -7
  3. package/lib/index.mjs +3791 -853
  4. package/lib/types/app.d.ts +90 -1
  5. package/lib/types/approval.d.ts +3 -1
  6. package/lib/types/history.d.ts +15 -4
  7. package/lib/types/index.d.ts +48 -0
  8. package/lib/types/kernel-panels.d.ts +65 -8
  9. package/lib/types/models.d.ts +15 -1
  10. package/lib/types/permissions.d.ts +37 -0
  11. package/lib/types/presets.d.ts +2 -0
  12. package/lib/types/provider-settings.d.ts +144 -0
  13. package/lib/types/questions.d.ts +2 -0
  14. package/lib/types/render/animations.d.ts +8 -6
  15. package/lib/types/render/lines.d.ts +6 -0
  16. package/lib/types/render/markdown.d.ts +3 -3
  17. package/lib/types/render/projection.d.ts +97 -3
  18. package/lib/types/render/status.d.ts +26 -36
  19. package/lib/types/render/text.d.ts +14 -7
  20. package/lib/types/render/tool-detail.d.ts +3 -1
  21. package/lib/types/render/tool-preview.d.ts +14 -1
  22. package/lib/types/session-directory.d.ts +61 -2
  23. package/lib/types/store.d.ts +13 -2
  24. package/lib/types/subagents.d.ts +60 -0
  25. package/lib/types/version.d.ts +5 -0
  26. package/package.json +1 -1
  27. package/src/app.ts +1200 -219
  28. package/src/approval.ts +161 -126
  29. package/src/history.ts +20 -5
  30. package/src/index.ts +577 -167
  31. package/src/kernel-panels.ts +354 -37
  32. package/src/models.ts +26 -0
  33. package/src/permissions.ts +85 -0
  34. package/src/presets.ts +12 -0
  35. package/src/provider-settings.ts +520 -0
  36. package/src/questions.ts +15 -5
  37. package/src/render/animations.ts +32 -18
  38. package/src/render/lines.ts +236 -218
  39. package/src/render/markdown.ts +302 -4
  40. package/src/render/projection.ts +670 -11
  41. package/src/render/status.ts +68 -162
  42. package/src/render/text.ts +28 -9
  43. package/src/render/tool-detail.ts +81 -40
  44. package/src/render/tool-preview.ts +77 -34
  45. package/src/session-directory.ts +171 -10
  46. package/src/skills.ts +8 -4
  47. package/src/store.ts +26 -8
  48. package/src/subagents.ts +165 -0
  49. package/src/version.ts +16 -0
@@ -3,13 +3,13 @@
3
3
  * block/inline parser producing styled line segments the Ink renderer maps
4
4
  * to colored text. No ANSI here — the app owns color mapping, tests own the
5
5
  * structure. The subset mirrors what agent replies actually emit: headings,
6
- * emphasis, inline/fenced code, flat lists, blockquotes, links, rules, and
7
- * wrapped paragraphs. Unknown syntax degrades to plain text (never throws).
6
+ * emphasis, inline/fenced code, flat lists, blockquotes, links, rules, GFM
7
+ * tables, and wrapped paragraphs. Unknown syntax degrades to plain text.
8
8
  *
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' | 'dim' | 'strike';
12
+ export type MdStyle = 'plain' | 'bold' | 'italic' | 'boldItalic' | 'code' | 'accent' | 'accentBold' | 'dim' | 'strike';
13
13
  /** One styled run of text. */
14
14
  export interface MdSegment {
15
15
  /** Visible text (no ANSI). */
@@ -47,6 +47,8 @@ export interface ToolEntry {
47
47
  arguments: string;
48
48
  /** Bounded human-meaningful arguments preview for the tool card. */
49
49
  preview: string;
50
+ /** Bounded delegation prompt (subagent cards' second row), '' when none. */
51
+ prompt: string;
50
52
  /** Execution state; `running` until the paired result lands. */
51
53
  state: 'running' | 'done' | 'error';
52
54
  /** Bounded first text block of the result, empty until it lands. */
@@ -236,7 +238,10 @@ export interface TranscriptView {
236
238
  /**
237
239
  * Fold-internal timing anchors, never rendered: open step and tool-call
238
240
  * start timestamps the next `assistant/message` / `tool/result` resolves
239
- * against. Keyed `turn:step` and by call id.
241
+ * against. Keyed `turn:step` and by call id. `turnSteps`/`turnTools`
242
+ * track which step/tool anchors still belong to the open turn so
243
+ * `turn/end` (and a superseding `step/start`) can sweep anchors an
244
+ * interruption left behind; `turnFiles` keys mutated paths by turn.
240
245
  */
241
246
  readonly anchors: {
242
247
  stepStart: Map<string, number>;
@@ -245,6 +250,8 @@ export interface TranscriptView {
245
250
  compactionTokens: Map<string, number>;
246
251
  lastPruneTokens: number;
247
252
  turnFiles: Map<number, Set<string>>;
253
+ turnSteps: Map<number, string>;
254
+ turnTools: Map<number, Set<string>>;
248
255
  };
249
256
  }
250
257
  /** A fresh, empty transcript view. */
@@ -256,8 +263,92 @@ export declare function createTranscriptView(): TranscriptView;
256
263
  * @returns the view after the event; the input view is never mutated.
257
264
  */
258
265
  export declare function projectEvent(view: TranscriptView, event: SessionEvent): TranscriptView;
266
+ /**
267
+ * Mutable replay accumulator: folds a persisted log into the identical view
268
+ * `projectEvent` would produce, but in near-linear time. Where `projectEvent`
269
+ * is copy-on-write — every append/scan rebuilds the whole `entries` array, so
270
+ * folding a full log costs O(N²) — the accumulator appends by push, resolves
271
+ * id-keyed updates (tool/result, command/done, retry-started) through index
272
+ * maps, and tombstones retired pending rows, so the whole log folds in O(N)
273
+ * plus one compaction pass when tombstones exist.
274
+ *
275
+ * Index maps never delete: every appended row registers its index, so an id
276
+ * lookup miss provably means no matching row exists and the update is an O(1)
277
+ * no-op (a malicious/orphan-heavy log cannot force per-orphan full-array
278
+ * scans). Each id maps to ALL of its indices, so a duplicate id updates every
279
+ * matching row exactly like the copy-on-write reducer.
280
+ *
281
+ * @internal Exported only so tests can (a) prove replay ≡ sequential
282
+ * `projectEvent` folds and (b) assert the linear complexity deterministically
283
+ * via {@link ReplayAccumulator.ops}, which counts entry-level container work
284
+ * instead of relying on wall-clock thresholds. No public consumer.
285
+ */
286
+ export interface ReplayAccumulator {
287
+ /** Working entry list; `undefined` marks a retired pending row (tombstone). */
288
+ entries: (TranscriptEntry | undefined)[];
289
+ /** callId → every index into `entries` holding a `tool` row with that id. */
290
+ toolIndex: Map<string, number[]>;
291
+ /** commandId → every index into `entries` holding a `command` row with that id. */
292
+ commandIndex: Map<string, number[]>;
293
+ /** retryId → every index into `entries` holding a `retry` row with that id. */
294
+ retryIndex: Map<string, number[]>;
295
+ /** messageId → every index into `entries` holding a `pending` row with that id. */
296
+ pendingIndex: Map<string, number[]>;
297
+ /** Tombstone count; zero means `entries` is already the final array. */
298
+ removedCount: number;
299
+ /** Mutable inbox id lists, mirroring `view.pending` order per target. */
300
+ pendingTurn: string[];
301
+ pendingStep: string[];
302
+ streaming: string;
303
+ streamingReasoning: string;
304
+ todos: readonly TodoItem[];
305
+ busy: boolean;
306
+ busySince: number;
307
+ model: string;
308
+ plan: boolean;
309
+ permission: string;
310
+ title: string;
311
+ sandbox: string;
312
+ goal: GoalFold | undefined;
313
+ stats: TranscriptStats;
314
+ stepStart: Map<string, number>;
315
+ toolStart: Map<string, number>;
316
+ firstChunkAt: Map<string, number>;
317
+ compactionTokens: Map<string, number>;
318
+ lastPruneTokens: number;
319
+ turnFiles: Map<number, Set<string>>;
320
+ turnSteps: Map<number, string>;
321
+ turnTools: Map<number, Set<string>>;
322
+ /** Entry-level container operations performed so far (test instrumentation). */
323
+ ops: number;
324
+ }
325
+ /** @internal A fresh replay accumulator whose state mirrors `createTranscriptView()`. */
326
+ export declare function createReplayAccumulator(): ReplayAccumulator;
327
+ /**
328
+ * Fold one session event into a replay accumulator. This mirrors
329
+ * {@link projectEvent} case for case — same stats arithmetic, same anchor
330
+ * set/delete behavior, same entry shapes — so the finished view is identical
331
+ * to a sequential fold; only the `entries` container operations are mutable.
332
+ *
333
+ * @internal Test-instrumentation path; `projectEvents` is the public entry.
334
+ */
335
+ export declare function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent): void;
336
+ /**
337
+ * Materialize the accumulated fold as a `TranscriptView`, compacting any
338
+ * retired tombstones. The anchors maps are handed through as-is (their
339
+ * content is identical to a sequential fold's).
340
+ *
341
+ * @internal Test-instrumentation path; `projectEvents` is the public entry.
342
+ */
343
+ export declare function finishReplay(acc: ReplayAccumulator): TranscriptView;
259
344
  /**
260
345
  * Fold a replayed event history into one view.
346
+ *
347
+ * Folding is near-linear in the log size: the mutable replay accumulator
348
+ * appends in place and resolves id-keyed updates through index maps, so a
349
+ * long persisted session replays without the O(N²) copy-on-write rebuilds a
350
+ * naive sequential fold would incur. The result is identical to folding
351
+ * {@link projectEvent} per event in order.
261
352
  * @param events - events in `seq` order.
262
353
  * @returns the folded view.
263
354
  */
@@ -271,8 +362,11 @@ export declare function projectEvents(events: readonly SessionEvent[]): Transcri
271
362
  * the inbox claims or cancels them durably (`agent/inbox/spliced` removals,
272
363
  * `user/message` retirement), and an append-only `<Static>` flush cannot
273
364
  * erase a row that vanishes from the view — the retired row would ghost on
274
- * screen until the next source-backed replay. Everything else (including a
275
- * completed tail) is final: later events only APPEND new rows.
365
+ * screen until the next source-backed replay. Running commands join the
366
+ * mutable boundary for the same reason in reverse: `command/done` mutates the
367
+ * row's state/summary, so a flushed row would keep its stale running mark
368
+ * until a resize-triggered replay. Everything else (including a completed
369
+ * tail) is final: later events only APPEND new rows.
276
370
  * @param entries - the view's transcript entries in order.
277
371
  * @returns the count of entries safe to flush (0 for an empty transcript).
278
372
  */
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @module @deepseek-ai/dsh-tui/render/status
11
11
  */
12
- import type { ContextSegments, TranscriptStats } from './projection.ts';
12
+ import type { TranscriptStats } from './projection.ts';
13
13
  /**
14
14
  * Compact token count: 517 / 12.2K / 517K / 1.2M (one decimal under three
15
15
  * digits), mirroring the web composer's StatsLine format.
@@ -40,7 +40,7 @@ export declare function cacheHitPercent(usage: TranscriptStats['usage']): number
40
40
  * Presentation tones for status spans; the footer maps each to a theme color
41
41
  * (Codex status-line accents: model/path/branch/state/usage categories).
42
42
  */
43
- export type StatusTone = 'model' | 'live' | 'path' | 'branch' | 'value' | 'label' | 'meta' | 'accent' | 'success' | 'warn' | 'error' | 'ctxSystem' | 'ctxPrompt' | 'ctxAssistant' | 'ctxThinking' | 'ctxTools';
43
+ export type StatusTone = 'model' | 'live' | 'path' | 'branch' | 'value' | 'label' | 'meta' | 'accent' | 'success' | 'warn' | 'error' | 'ctxFill';
44
44
  /** One colored run inside the status bar. */
45
45
  export interface StatusSpan {
46
46
  text: string;
@@ -64,11 +64,9 @@ export interface StatusRow {
64
64
  hint: boolean;
65
65
  }
66
66
  /**
67
- * The footer layout: two stacked physical rows. Row 1 is the identity/state
68
- * row (busy dot, model, cwd, branch, plan, turns, tokens, title; goal,
69
- * sandbox, and permission badges). Row 2 is the run-meters row (mode, the
70
- * context progress bar, cache, and duration figures) and degrades to empty
71
- * before any row-1 content is touched.
67
+ * The footer layout: two stacked physical rows. Row 1 keeps the primary
68
+ * controls in model, cwd, mode, branch, context, permission order. Row 2
69
+ * carries every secondary session/run figure and degrades independently.
72
70
  */
73
71
  export interface StatusLayout {
74
72
  row1: StatusRow;
@@ -89,33 +87,21 @@ export declare const STATUS_CYCLE_HINT = " (shift+tab to cycle)";
89
87
  export declare const CONTEXT_BAR_WIDTH = 24;
90
88
  /** Occupancy at which the usage readout flips from brand blue to amber. */
91
89
  export declare const CONTEXT_WARN_PERCENT = 90;
92
- /** One content-type segment of the context bar (pure data; colors live in app.ts). */
93
- export interface ContextSegmentSpec {
94
- key: keyof ContextSegments;
95
- /** Tone the footer maps to a DeepSeek blue shade. */
96
- tone: StatusTone;
97
- /** Labels longest → shortest; the first one fitting the segment width wins. */
98
- labels: readonly string[];
99
- }
100
- /** The five content types in conversation order, dark light blue. */
101
- export declare const CONTEXT_SEGMENTS: readonly ContextSegmentSpec[];
102
- /**
103
- * Render context occupancy as a segmented bar: one DeepSeek-blue run per
104
- * content type (system/prompt/assistant/thinking/tools), column widths
105
- * proportional to their estimated token share, each with a centered label
106
- * that shortens to fit (system→sys→s). The remaining free tail is a dim
107
- * track whose right edge carries the usage readout (`12.3K/1.0M 25%`,
108
- * shrinking to the bare percent as the tail narrows). The readout flips to
109
- * amber once occupancy reaches the warning threshold; the segment blues stay
110
- * untouched so the composition remains readable at full context. The used
111
- * total comes from the reported `lastPromptTokens`, never from the estimates.
112
- * @param segments - estimated used tokens per content type.
90
+ /**
91
+ * Render context occupancy as ONE stepless bar: a solid DeepSeek-blue fill
92
+ * run, a dim dotted free track, and the usage readout riding the track's
93
+ * right edge (`12.3K/1.0M 25%`, shrinking to the bare percent as the track
94
+ * narrows). No per-content-type segmentation. Column split is deterministic:
95
+ * the free share is `Math.round(free/window*width)` clamped to at least
96
+ * CONTEXT_MIN_FREE columns and at most the full width; the fill takes every
97
+ * remaining column, so a given occupancy always renders the identical bar.
98
+ * The readout flips to amber once occupancy reaches the warning threshold.
113
99
  * @param usedTokens - reported used tokens (drives the readout and percent).
114
100
  * @param contextWindow - route capacity.
115
101
  * @param width - total bar interior columns.
116
102
  * @returns tone-split spans for the footer to paint.
117
103
  */
118
- export declare function contextBar(segments: ContextSegments, usedTokens: number, contextWindow: number, width: number): readonly StatusSpan[];
104
+ export declare function contextBar(usedTokens: number, contextWindow: number, width: number): readonly StatusSpan[];
119
105
  /**
120
106
  * One customizable status item (the Codex /statusline picker contract).
121
107
  * 'left' items render as pipe-separated clusters after the identity dot;
@@ -149,6 +135,12 @@ export declare const DEFAULT_STATUSLINE_ITEMS: readonly StatusItemId[];
149
135
  * @returns the normalized ordered item list.
150
136
  */
151
137
  export declare function parseStatuslineItems(value: unknown): readonly StatusItemId[];
138
+ /**
139
+ * Extra left padding on the secondary row so its content aligns with the
140
+ * model name's left edge on the primary row (padding 2 + busy dot 2). The
141
+ * layout subtracts it from row 2's budget so the indent can never wrap it.
142
+ */
143
+ export declare const STATUS_ROW2_INDENT = 2;
152
144
  /** Identity facts the runner resolves once at mount; empty strings drop out. */
153
145
  export interface StatusFacts {
154
146
  /** 'provider/model' selection serving this session. */
@@ -173,7 +165,7 @@ export interface StatusFacts {
173
165
  } | undefined;
174
166
  /** Whether plan mode is active (folded from 'plan/mode'). */
175
167
  plan: boolean;
176
- /** Active permission preset (folded from 'permission/preset'), empty when unknown. */
168
+ /** Active or pending permission preset; empty only when the service is unavailable. */
177
169
  permission: string;
178
170
  }
179
171
  /**
@@ -185,12 +177,10 @@ export interface StatusFacts {
185
177
  */
186
178
  export declare function permissionTone(permission: string): StatusTone;
187
179
  /**
188
- * Compose the two-row footer layout under a column budget. Row 1 (identity
189
- * and state badges) degrades in a fixed order cycle hint, then title, token
190
- * figures, turn/step counts, goal, divergent sandbox, permission badge and
191
- * only then ellipsizes the identity cluster, so the row never wraps. Row 2
192
- * (mode, context bar, cache, duration figures) fits its own budget and
193
- * degrades to empty before any row-1 content is touched.
180
+ * Compose the two-row footer layout under a column budget. Row 1 keeps model,
181
+ * cwd, mode, branch, context, then the right-pinned permission badge and cycle
182
+ * hint. It drops hint, context, and permission before ellipsizing identity.
183
+ * Row 2 fits all secondary figures and state within its own budget.
194
184
  * @param facts - identity facts resolved by the runner.
195
185
  * @param stats - session figures folded from the durable log.
196
186
  * @param columns - usable columns for each row (before their left padding).
@@ -3,17 +3,21 @@
3
3
  * tool payloads, skill descriptions). Control characters — including ANSI
4
4
  * CSI/OSC escape sequences — would otherwise pass through Ink into the
5
5
  * terminal, letting output rewrite the screen or inject prompts. Newlines
6
- * and tabs survive; everything else in C0/C1 plus DEL becomes a visible
7
- * `\xNN` escape.
6
+ * survive; everything else in C0/C1 plus DEL becomes a visible `\xNN`
7
+ * escape, and bidi overrides / invisible format controls / Unicode line and
8
+ * paragraph separators become a visible `\uXXXX` escape (terminal emulators
9
+ * that render bidirectional text would otherwise reorder the displayed
10
+ * glyphs and let a command read as something it is not).
8
11
  *
9
12
  * @module @deepseek-ai/dsh-code/render/text
10
13
  */
11
14
  /**
12
- * Escape control characters so externally sourced text cannot drive the
13
- * terminal.
15
+ * Escape control and deceptive characters so externally sourced text cannot
16
+ * drive the terminal. C0/C1/DEL render as a literal `\xNN` escape; bidi,
17
+ * invisible-format, and separator controls render as a literal `\uXXXX`
18
+ * escape. Newlines and tabs survive (budgeted callers normalize tabs).
14
19
  * @param text - raw text from a session event, tool payload, or catalog.
15
- * @returns text with every control character (except `\n`, `\t`) rendered
16
- * as a literal `\xNN` escape.
20
+ * @returns display-safe text with every injectable character made visible.
17
21
  */
18
22
  export declare function displayText(text: string): string;
19
23
  /** Collapse external text to one terminal-safe logical row. */
@@ -36,7 +40,10 @@ export interface DisplayTail {
36
40
  * Keep only the newest display-safe text that fits a terminal rectangle.
37
41
  * The scan walks backward and stops as soon as the suffix is full, so a long
38
42
  * reasoning stream does not rescan its entire accumulated prefix per chunk.
39
- * Explicit newlines and terminal wrapping both consume rows.
43
+ * Explicit newlines and terminal wrapping both consume rows; tabs expand to
44
+ * two spaces so terminal tab stops (which render at contextual column 8
45
+ * boundaries, not at the budgeted cell count) cannot inflate the physical
46
+ * row count of the live region.
40
47
  * @param text - raw externally sourced text.
41
48
  * @param columns - available terminal columns.
42
49
  * @param rows - available terminal rows.
@@ -70,7 +70,9 @@ export type ToolDetail = {
70
70
  * Render one change as removed-then-added rows, hunked by common prefix and
71
71
  * suffix. A null before-image (file create) renders as pure additions. The
72
72
  * budget caps emitted rows and reports the cut, so a whole-file overwrite
73
- * never floods the transcript.
73
+ * never floods the transcript. Inputs are hard-capped before line splitting
74
+ * and the row list is built incrementally up to the budget — a crafted or
75
+ * replayed giant diff cannot force a full intermediate rows array.
74
76
  * @param oldText - prior content, or null for a create.
75
77
  * @param newText - content after the change.
76
78
  * @param budget - maximum rows to emit.
@@ -2,7 +2,10 @@
2
2
  * Bounded preview line for a tool invocation's raw JSON arguments: the first
3
3
  * human-meaningful string among the well-known keys (command, path, query, …)
4
4
  * with a fallback to the bounded raw JSON. Shared by the tool card in the
5
- * transcript and the approval bar's command preview.
5
+ * transcript and the approval bar's command preview. Arguments longer than
6
+ * {@link MAX_PARSE_CHARS} are never parsed: the preview is a display concern,
7
+ * and a synchronous `JSON.parse` plus string copies of an unbounded model
8
+ * payload must not run on the approval or projection paths.
6
9
  *
7
10
  * @module @deepseek-ai/dsh-code/render/tool-preview
8
11
  */
@@ -13,3 +16,13 @@
13
16
  * @returns the preview line; empty when nothing useful resolves.
14
17
  */
15
18
  export declare function toolArgumentsPreview(args: string, toolName: string): string;
19
+ /**
20
+ * Bounded prompt preview for delegation-style tools (`subagent`): the
21
+ * `prompt` argument rendered as the card's second row, so the transcript
22
+ * shows what the child agent was asked — not just its description label —
23
+ * while it runs (Codex's SpawnAgent card preview). Anything else returns ''.
24
+ * @param toolName - the tool the arguments belong to.
25
+ * @param args - raw JSON arguments string as the model produced it.
26
+ * @returns the one-line prompt preview, or '' when none applies.
27
+ */
28
+ export declare function toolPromptPreview(toolName: string, args: string): string;
@@ -38,6 +38,8 @@ export interface SessionDirectoryOptions {
38
38
  export interface SessionRow {
39
39
  readonly id: string;
40
40
  readonly createdAt: number;
41
+ /** Last-activity timestamp: artifact mtime when known, else createdAt. */
42
+ readonly updatedAt: number;
41
43
  readonly cwd: string;
42
44
  readonly workspace: string;
43
45
  readonly parent?: string;
@@ -48,7 +50,64 @@ export interface SessionRow {
48
50
  readonly preset: string;
49
51
  readonly title?: string;
50
52
  }
51
- /** Filter/sort header-only records. No session log is loaded here. */
52
- export declare function projectSessionRows(records: readonly SessionRecord[], options: SessionDirectoryOptions): SessionRow[];
53
+ /** True when the header describes a subagent conversation (durable lineage). */
54
+ export declare function isSubagentSession(header: SessionHeader): boolean;
55
+ /** Platform-consistent path equality for session cwd comparisons. */
56
+ export declare function samePath(left: string | undefined, right: string): boolean;
57
+ /**
58
+ * Unique header match by exact id or unique id prefix (root and subagent
59
+ * headers alike); the caller applies any lineage gate.
60
+ * @param headers - the persisted headers.
61
+ * @param wanted - the id or id prefix.
62
+ * @returns the uniquely matched header.
63
+ * @throws when nothing matches or the prefix is ambiguous.
64
+ */
65
+ export declare function matchSessionId(headers: readonly SessionHeader[], wanted: string): SessionHeader;
66
+ /** The newest persisted ROOT session pinned to this cwd, or undefined. */
67
+ export declare function newestRootForCwd(headers: readonly SessionHeader[], cwd: string): SessionHeader | undefined;
68
+ /**
69
+ * Filter/sort header-only records. No session log is loaded here. Sorting is
70
+ * by LAST ACTIVITY (`updated` — artifact mtime when the caller resolved one,
71
+ * else createdAt), matching the codex resume picker's default UpdatedAt
72
+ * ordering: a session you kept talking in outranks one created later but idle.
73
+ * @param records - the header-only records.
74
+ * @param options - filter/sort options.
75
+ * @param updated - per-session last-activity timestamps, when resolved.
76
+ */
77
+ export declare function projectSessionRows(records: readonly SessionRecord[], options: SessionDirectoryOptions, updated?: ReadonlyMap<string, number>): SessionRow[];
53
78
  /** Merge page-local title observations without disturbing directory order. */
54
79
  export declare function mergeSessionTitles(rows: readonly SessionRow[], observations: readonly TitleObservationResult[]): SessionRow[];
80
+ /**
81
+ * Encode a session id the way the JSONL backend does for its on-disk layout
82
+ * (`encodeSegment`: safe units literal, everything else `~XXXX`). Used ONLY to
83
+ * validate that a `locate()` path really is this session's directory before
84
+ * any deletion touches the filesystem — a local copy of the pure upstream
85
+ * contract, kept in sync with `session-persistence-jsonl/src/format.ts`.
86
+ */
87
+ export declare function encodeSessionSegment(raw: string): string;
88
+ /** The session-log artifact names the JSONL backend may create. */
89
+ export declare const SESSION_ARTIFACT_NAMES: readonly string[];
90
+ /**
91
+ * Guard one `locate()` artifact path before deletion (codex's scoped-path
92
+ * check, adapted to the JSONL layout): the file must be a `session.jsonl`
93
+ * artifact sitting in the directory named exactly `encodeSegment(id)`.
94
+ * @param artifact - the path the persistence backend located.
95
+ * @param id - the session id the artifact claims to belong to.
96
+ * @returns the owning session directory, or undefined when the layout is unexpected.
97
+ */
98
+ export declare function sessionArtifactDirectory(artifact: string, id: string): string | undefined;
99
+ /**
100
+ * Collect one session's deletion subtree: the id plus every record whose
101
+ * parent chain leads to it (codex deletes subagent threads with their root).
102
+ * @param records - the full directory listing.
103
+ * @param id - the root session id to delete.
104
+ * @returns the ids to delete, root first.
105
+ */
106
+ export declare function collectDeletionSubtree(records: readonly SessionRecord[], id: string): string[];
107
+ /**
108
+ * Codex-style relative time for session rows ("now", "5m ago", "3h ago",
109
+ * "2d ago"; older than a week falls back to the local date).
110
+ * @param timestamp - epoch milliseconds of the last activity.
111
+ * @param now - the pinned reference clock (one value per list render).
112
+ */
113
+ export declare function formatRelativeTime(timestamp: number, now: number): string;
@@ -1,8 +1,19 @@
1
1
  /**
2
2
  * Observable transcript store: folds session events into the projection view
3
3
  * and notifies subscribers. The renderer subscribes through
4
- * `useSyncExternalStore`; the runner owns event feeding. The store owns no
5
- * timing — listeners fire synchronously after each applied event.
4
+ * `useSyncExternalStore`; the runner owns event feeding.
5
+ *
6
+ * Notification coalescing: the fold stays synchronous — `getView()` always
7
+ * returns the latest state the moment `apply` returns — but listener
8
+ * notification is scheduled on a microtask and deduplicated, so N events
9
+ * delivered inside one synchronous drain (the zai/GLM adapter drains its
10
+ * token buffer in sub-millisecond bursts) produce ONE React re-render.
11
+ * Synchronous per-event notification instead cascades one
12
+ * `useSyncExternalStore` force-update per token inside a single flush; the
13
+ * reconciler counts those as nested passive updates and floods React's
14
+ * "Maximum update depth exceeded" warning past 50 events, besides rendering
15
+ * the whole live tree once per token. A microtask keeps latency within the
16
+ * same macrotask, before Ink's throttled paint.
6
17
  *
7
18
  * @module @deepseek-ai/dsh-tui/store
8
19
  */
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Live subagent activity feed: a bounded, display-only projection of CHILD
3
+ * session events. The transcript store folds only the root session (the
4
+ * durable truth this TUI renders); subagent conversations are their own
5
+ * sessions, and before this module their events were dropped entirely —
6
+ * a running subagent was invisible until its parent tool call settled.
7
+ *
8
+ * This is NOT a second transcript: each child folds to ONE row (label,
9
+ * running state, bounded last-activity text), capped at
10
+ * {@link MAX_SUBAGENT_ROWS}. Rows are advisory display state, rebuilt from
11
+ * live events; nothing here persists or replays. Notification is coalesced
12
+ * to one microtask per delivery burst, mirroring the transcript store's
13
+ * contract (per-token synchronous notify once cascaded past React's nested
14
+ * update limit on the GLM thinking path).
15
+ *
16
+ * @module @deepseek-ai/dsh-code/subagents
17
+ */
18
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
19
+ /** Hard row cap: a fan-out larger than this stays summarized by the head. */
20
+ export declare const MAX_SUBAGENT_ROWS = 8;
21
+ /** One live subagent row in the feed. */
22
+ export interface SubagentRow {
23
+ /** Child session id. */
24
+ readonly id: string;
25
+ /** Display label (session title when observed, else a short id form). */
26
+ readonly label: string;
27
+ /** Coarse lifecycle state folded from the child's events. */
28
+ readonly state: 'running' | 'idle' | 'done';
29
+ /** Bounded last-activity text for the status line. */
30
+ readonly activity: string;
31
+ /** Last fold time (ms, event clock) — newest-first ordering key. */
32
+ readonly updatedAt: number;
33
+ }
34
+ /** The read-only snapshot surface the renderer subscribes to. */
35
+ export interface SubagentFeedView {
36
+ /** Subscribe to feed changes; returns the unsubscribe function. */
37
+ subscribe(listener: () => void): () => void;
38
+ /** Read the current rows (identity-stable between changes). */
39
+ getSnapshot(): readonly SubagentRow[];
40
+ }
41
+ /**
42
+ * Fold one child-session event into its feed row (pure).
43
+ * Unknown event kinds leave the row untouched.
44
+ * @param previous - the row's current state, when any.
45
+ * @param sessionId - the child session id.
46
+ * @param event - the child session event.
47
+ * @returns the next row state.
48
+ */
49
+ export declare function foldSubagentRow(previous: SubagentRow | undefined, sessionId: string, event: SessionEvent): SubagentRow;
50
+ /**
51
+ * Create one subagent feed. `apply` folds a child event (the caller gates
52
+ * which sessions are children); `reset` clears on a session switch. Row
53
+ * order is first-seen; the snapshot array is frozen and only replaced when
54
+ * a row actually changed.
55
+ * @returns the mutable feed handle plus its `SubagentFeedView`.
56
+ */
57
+ export declare function createSubagentFeed(): SubagentFeedView & {
58
+ apply(sessionId: string, event: SessionEvent): void;
59
+ reset(): void;
60
+ };
@@ -0,0 +1,5 @@
1
+ /** Installed dsh-code version exposed by the terminal header. */
2
+ /** Read one package manifest version without making terminal startup depend on it. */
3
+ export declare function readPackageVersion(manifest?: import("url").URL): string;
4
+ /** Version of the installed dsh-code package. */
5
+ export declare const DSH_CODE_VERSION: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-code",
3
3
  "description": "Claude-Code-style interactive TUI bundle for DeepSeek Harness (dsh): DeepSeek-blue whale banner, live session transcript, and a blended status line",
4
- "version": "0.7.0",
4
+ "version": "0.9.0",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "deepseek": "./bin/deepseek.mjs",