dsh-code 0.3.0 → 0.4.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.
@@ -59,31 +59,14 @@ export interface AppProps {
59
59
  selectModel(row: ModelRow): string;
60
60
  /** Cycle to the next permission preset (Shift+Tab); returns the new label. */
61
61
  cyclePermission(): string;
62
+ /** Export the transcript to a markdown file (/export [path]); reports via notices. */
63
+ exportTranscript(argument: string): Promise<void>;
64
+ /** Rename the session (/title <text>); returns the outcome line for the notice. */
65
+ renameTitle(argument: string): string;
62
66
  /** Registers the app's notice channel with the runner (called once on mount). */
63
67
  onBridgeReady(bridge: {
64
68
  notify(text: string): void;
65
69
  }): void;
66
70
  }
67
- /** One completion candidate row. */
68
- interface CompletionCandidate {
69
- /** Insertion text for the command name (with leading slash). */
70
- label: string;
71
- /** Human-readable description shown beside the label. */
72
- description: string;
73
- /** Candidate origin; skills land the same literal text but route through the prompt. */
74
- origin: 'command' | 'skill' | 'mention';
75
- }
76
- /** The completion menu snapshot the input editor publishes to the app. */
77
- export interface MenuState {
78
- /** Whether the menu is on screen (slash or @mention). */
79
- active: boolean;
80
- /** Whether the menu is driven by an @mention token. */
81
- mention: boolean;
82
- /** Highlighted candidate index (wraps by row count). */
83
- index: number;
84
- /** Rendered rows in display order. */
85
- rows: readonly CompletionCandidate[];
86
- }
87
71
  /** The whole terminal app; state arrives via the store, output via Ink. */
88
72
  export declare function App(props: AppProps): ReactElement;
89
- export {};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Markdown export of one transcript view: the /export command's pure
3
+ * formatter. Deterministic and side-effect free — the runner owns the file
4
+ * write, so tests drive the builder with folded views directly.
5
+ *
6
+ * @module @deepseek-ai/dsh-code/render/export
7
+ */
8
+ import type { TranscriptView } from './projection.ts';
9
+ /**
10
+ * Render the transcript as a standalone markdown document.
11
+ * @param view - the folded transcript view to export.
12
+ * @param sessionId - the full session identity for the header.
13
+ * @returns the complete markdown text.
14
+ */
15
+ export declare function buildExportMarkdown(view: TranscriptView, sessionId: string): string;
@@ -0,0 +1,30 @@
1
+ /** Pure viewport, selection, and scrolling rules for exclusive TUI panels. */
2
+ /** Terminal-space allocation for the inspector's one dynamic screen. */
3
+ export interface InspectorViewport {
4
+ /** Maximum dynamic rows, kept strictly below the terminal height. */
5
+ maxHeight: number;
6
+ /** Rows available to the selected entry after border, title, and footer. */
7
+ bodyRows: number;
8
+ /** Columns available inside the horizontal border and padding. */
9
+ contentColumns: number;
10
+ /** Tiny terminals use a borderless one-line close hint. */
11
+ compact: boolean;
12
+ }
13
+ /**
14
+ * Keep the inspector plus its persistent status/composer chrome below
15
+ * `stdout.rows`: at equality Ink clears the terminal and rewrites all
16
+ * accumulated `<Static>` output on every frame.
17
+ */
18
+ export declare function panelViewport(columns: number, rows: number): InspectorViewport;
19
+ /** Backward-compatible name for the Ctrl+O-specific caller and tests. */
20
+ export declare function inspectorViewport(columns: number, rows: number): InspectorViewport;
21
+ /** Clamp a first-visible row to the range representable by one viewport. */
22
+ export declare function clampScroll(offset: number, totalRows: number, visibleRows: number): number;
23
+ /** Move a viewport by a signed row delta without escaping its content. */
24
+ export declare function moveScroll(offset: number, delta: number, totalRows: number, visibleRows: number): number;
25
+ /** Keep one focused row visible while preserving the current window when possible. */
26
+ export declare function revealRow(offset: number, row: number, totalRows: number, visibleRows: number): number;
27
+ /** Center a selected list row where possible, clamped at both ends. */
28
+ export declare function selectionWindow(cursor: number, totalRows: number, visibleRows: number): number;
29
+ /** Follow appended history only while the inspector cursor was at the tail. */
30
+ export declare function followInspectorCursor(cursor: number, previousLength: number, nextLength: number): number;
@@ -0,0 +1,31 @@
1
+ /** Width-safe styled physical rows for bounded terminal panels. */
2
+ import type { TranscriptEntry } from './projection.ts';
3
+ import { type MdStyle } from './markdown.ts';
4
+ /** Presentation classes mapped to Ink colors by the app boundary. */
5
+ export type LineStyle = MdStyle | 'brand' | 'success' | 'error' | 'warn' | 'dimItalic';
6
+ /** One styled run within a physical terminal row. */
7
+ export interface StyledSegment {
8
+ text: string;
9
+ style: LineStyle;
10
+ }
11
+ /** One row guaranteed not to exceed the requested terminal width. */
12
+ export interface StyledLine {
13
+ segments: readonly StyledSegment[];
14
+ }
15
+ /** Construct one segment without leaking mutable objects into cached rows. */
16
+ export declare function lineSegment(text: string, style?: LineStyle): StyledSegment;
17
+ /**
18
+ * Sanitize and hard-wrap styled content into exact physical rows.
19
+ * Tabs become two visible spaces because terminal tab stops are contextual
20
+ * and therefore cannot participate in a deterministic row budget.
21
+ */
22
+ export declare function styledLines(segments: readonly StyledSegment[], columns: number): readonly StyledLine[];
23
+ /** Plain/dim text convenience over {@link styledLines}. */
24
+ export declare function textLines(text: string, columns: number, style?: LineStyle): readonly StyledLine[];
25
+ /** Markdown rows re-hardened so a single long word cannot escape the budget. */
26
+ export declare function markdownLines(text: string, columns: number): readonly StyledLine[];
27
+ /**
28
+ * Convert one durable transcript entry to its complete scrollable row model.
29
+ * The source entry stays intact; only the caller's visible slice is rendered.
30
+ */
31
+ export declare function transcriptEntryLines(entry: TranscriptEntry, columns: number): readonly StyledLine[];
@@ -7,6 +7,7 @@
7
7
  * @module @deepseek-ai/dsh-tui/render/projection
8
8
  */
9
9
  import type { SessionEvent, TodoItem } from '@deepseek-ai/dsh-session';
10
+ import { type ToolDetail } from './tool-detail.ts';
10
11
  /** One user prompt line. */
11
12
  export interface UserEntry {
12
13
  kind: 'user';
@@ -39,6 +40,12 @@ export interface ToolEntry {
39
40
  state: 'running' | 'done' | 'error';
40
41
  /** Bounded first text block of the result, empty until it lands. */
41
42
  summary: string;
43
+ /**
44
+ * Bounded expansion payload for the verbose transcript (Ctrl+O), derived
45
+ * from the tool's persisted presentation metadata; undefined until the
46
+ * result lands and only when something renderable exists.
47
+ */
48
+ detail: ToolDetail | undefined;
42
49
  }
43
50
  /** One slash-command execution dispatched through `ctx.commands`. */
44
51
  export interface CommandEntry {
@@ -60,8 +67,57 @@ export interface ErrorEntry {
60
67
  /** `code: message` of the failure. */
61
68
  text: string;
62
69
  }
70
+ /** One non-error turn outcome surfaced from `turn/end`. */
71
+ export interface TurnMarkerEntry {
72
+ kind: 'turn-marker';
73
+ /** Human-readable outcome line, dim-rendered. */
74
+ text: string;
75
+ }
76
+ /** One completed compaction lifecycle surfaced from `compaction/end`. */
77
+ export interface CompactionEntry {
78
+ kind: 'compaction';
79
+ /** True when the compaction completed, false when it failed. */
80
+ ok: boolean;
81
+ /** Heuristic tokens shadowed by the compaction (summary or prune price). */
82
+ tokens: number;
83
+ /** Failure text when `ok` is false, empty otherwise. */
84
+ error: string;
85
+ }
86
+ /** One provider-routed model-request retry (the `llm/retry` pair). */
87
+ export interface RetryEntry {
88
+ kind: 'retry';
89
+ /** Correlation id shared with the matching `llm/retry-started`. */
90
+ retryId: string;
91
+ /** Attempt ordinal and its cap. */
92
+ attempt: number;
93
+ max: number;
94
+ /** Failure code that triggered the retry. */
95
+ code: string;
96
+ /** Backoff wait before the next attempt, in ms. */
97
+ delayMs: number;
98
+ /** `running` while the backoff waits, `done` once the attempt started. */
99
+ state: 'running' | 'done';
100
+ }
101
+ /** Turn-tail deliverables: files mutated by the turn's diff-bearing tools. */
102
+ export interface FilesEntry {
103
+ kind: 'files';
104
+ /** Unique mutated paths in call order, bounded. */
105
+ paths: readonly string[];
106
+ }
63
107
  /** Ordered transcript items the renderer draws. */
64
- export type TranscriptEntry = UserEntry | AssistantEntry | ToolEntry | CommandEntry | ErrorEntry;
108
+ export type TranscriptEntry = UserEntry | AssistantEntry | ToolEntry | CommandEntry | ErrorEntry | TurnMarkerEntry | CompactionEntry | RetryEntry | FilesEntry;
109
+ /** The live goal the status line badges, folded from `goal/change`. */
110
+ export interface GoalFold {
111
+ /** Human-requested completion objective. */
112
+ objective: string;
113
+ /** Durable lifecycle phase. */
114
+ phase: 'active' | 'paused' | 'blocked' | 'complete';
115
+ /** Highest admitted continuation round and its cap. */
116
+ rounds: number;
117
+ max: number;
118
+ /** Blocked explanation, empty outside the blocked phase. */
119
+ blocked: string;
120
+ }
65
121
  /** Cumulative token accounting folded from `assistant/message` usage reports. */
66
122
  export interface UsageTotals {
67
123
  /** Prompt-side billed tokens: `inputTokens` plus both cache buckets. */
@@ -83,19 +139,33 @@ export interface TranscriptStats {
83
139
  toolMs: number;
84
140
  /** Cumulative token accounting; input stays 0 until a report lands. */
85
141
  usage: UsageTotals;
142
+ /** Prompt-side size of the most recent reported request (context pressure). */
143
+ lastPromptTokens: number;
144
+ /** Newest advertised route capacity, 0 when no adapter ever advertised one. */
145
+ contextWindow: number;
146
+ /** Summed first-token waits: `step/start` → first non-empty chunk, in ms. */
147
+ ttftMs: number;
148
+ /** Steps that produced a first chunk (the TTFT average's denominator). */
149
+ ttftSteps: number;
150
+ /** Summed decode spans: first chunk → `assistant/message`, in ms. */
151
+ decodeMs: number;
152
+ /** Completion tokens over timed decode spans (the tok/s numerator). */
153
+ decodeTokens: number;
86
154
  }
87
155
  /** The complete TUI transcript view for one session. */
88
156
  export interface TranscriptView {
89
157
  /** Settled entries in log order. */
90
158
  entries: readonly TranscriptEntry[];
91
- /** Text accumulated from `assistant/chunk` deltas since the last flush. */
159
+ /** Bounded text tail accumulated from `assistant/chunk` deltas since the last flush. */
92
160
  streaming: string;
93
- /** Thinking accumulated from `assistant/chunk` reasoning deltas since the last flush. */
161
+ /** Bounded thinking tail accumulated from reasoning deltas since the last flush. */
94
162
  streamingReasoning: string;
95
163
  /** Latest whole-list todo snapshot from `todo/write`, empty when none. */
96
164
  todos: readonly TodoItem[];
97
165
  /** True while a durable turn is open (`turn/start` … `turn/end`). */
98
166
  busy: boolean;
167
+ /** `turn/start` time of the open turn (0 while idle) — the web TurnStatus clock anchor. */
168
+ busySince: number;
99
169
  /** Figures the status line renders. */
100
170
  stats: TranscriptStats;
101
171
  /**
@@ -109,6 +179,12 @@ export interface TranscriptView {
109
179
  plan: boolean;
110
180
  /** Active permission preset folded from the last `permission/preset` event, empty before one. */
111
181
  permission: string;
182
+ /** Latest session title folded from the last `session/title` event, empty before one. */
183
+ title: string;
184
+ /** Sandbox-mode override folded from the last `sandbox/mode` event, empty when never switched. */
185
+ sandbox: string;
186
+ /** Current long-running goal folded from the last `goal/change`, undefined when cleared. */
187
+ goal: GoalFold | undefined;
112
188
  /**
113
189
  * Fold-internal timing anchors, never rendered: open step and tool-call
114
190
  * start timestamps the next `assistant/message` / `tool/result` resolves
@@ -117,6 +193,10 @@ export interface TranscriptView {
117
193
  readonly anchors: {
118
194
  stepStart: Map<string, number>;
119
195
  toolStart: Map<string, number>;
196
+ firstChunkAt: Map<string, number>;
197
+ compactionTokens: Map<string, number>;
198
+ lastPruneTokens: number;
199
+ turnFiles: Map<number, Set<string>>;
120
200
  };
121
201
  }
122
202
  /** A fresh, empty transcript view. */
@@ -134,3 +214,15 @@ export declare function projectEvent(view: TranscriptView, event: SessionEvent):
134
214
  * @returns the folded view.
135
215
  */
136
216
  export declare function projectEvents(events: readonly SessionEvent[]): TranscriptView;
217
+ /**
218
+ * How many leading transcript entries can never change again: only a
219
+ * `running` tool or retry can still mutate in place — everything before the
220
+ * first one (including a completed tail: later events only APPEND new rows)
221
+ * is final. The renderer currently draws the whole transcript dynamically
222
+ * (a `<Static>` flush proved unstable with CJK wrapping on real terminals);
223
+ * this boundary stays as the append-only contract for when flushing is
224
+ * reintroduced.
225
+ * @param entries - the view's transcript entries in order.
226
+ * @returns the count of entries safe to flush (0 for an empty transcript).
227
+ */
228
+ export declare function settledEntryCount(entries: readonly TranscriptEntry[]): number;
@@ -21,6 +21,13 @@ export declare function formatTokens(n: number): string;
21
21
  * @returns display string.
22
22
  */
23
23
  export declare function formatDuration(ms: number): string;
24
+ /**
25
+ * Compact decode rate: one decimal under a hundred, whole below a thousand,
26
+ * then thousands (15.3 / 124 / 1.2K).
27
+ * @param n - tokens per second.
28
+ * @returns display string.
29
+ */
30
+ export declare function formatRate(n: number): string;
24
31
  /**
25
32
  * Cache-hit share of billed prompt-side input.
26
33
  * @param usage - cumulative token totals.
@@ -37,6 +44,16 @@ export interface StatusFacts {
37
44
  branch: string;
38
45
  /** Short session identifier (last dash-separated segment or tail). */
39
46
  sessionId: string;
47
+ /** Latest session title (folded from `session/title`); shown in place of the id. */
48
+ title: string;
49
+ /** Sandbox-mode override (folded from `sandbox/mode`), empty when never switched. */
50
+ sandbox: string;
51
+ /** Live goal summary (folded from `goal/change`), undefined when none. */
52
+ goal: {
53
+ phase: string;
54
+ rounds: number;
55
+ max: number;
56
+ } | undefined;
40
57
  /** Whether plan mode is active (folded from `plan/mode`). */
41
58
  plan: boolean;
42
59
  /** Active permission preset (folded from `permission/preset`), empty when unknown. */
@@ -16,3 +16,21 @@
16
16
  * as a literal `\xNN` escape.
17
17
  */
18
18
  export declare function displayText(text: string): string;
19
+ /** A display-safe suffix bounded by terminal rows and columns. */
20
+ export interface DisplayTail {
21
+ /** Sanitized suffix suitable for direct terminal rendering. */
22
+ text: string;
23
+ /** Whether content before the returned suffix was omitted. */
24
+ truncated: boolean;
25
+ }
26
+ /**
27
+ * Keep only the newest display-safe text that fits a terminal rectangle.
28
+ * The scan walks backward and stops as soon as the suffix is full, so a long
29
+ * reasoning stream does not rescan its entire accumulated prefix per chunk.
30
+ * Explicit newlines and terminal wrapping both consume rows.
31
+ * @param text - raw externally sourced text.
32
+ * @param columns - available terminal columns.
33
+ * @param rows - available terminal rows.
34
+ * @returns a sanitized bounded suffix and whether an earlier prefix was cut.
35
+ */
36
+ export declare function displayTail(text: string, columns: number, rows: number): DisplayTail;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Expansion payloads for tool cards (the Ctrl+O verbose transcript): the
3
+ * TUI-side consumption of the harness presentation contract. Mutation and
4
+ * read tools persist a structured `tool/result.meta` (`diffs`, read
5
+ * windows, web sources) exactly so a capable UI can replay richer cards than
6
+ * the model-facing text; this module narrows that opaque JSON defensively —
7
+ * mirroring the upstream validators — and pre-formats bounded, render-ready
8
+ * rows. Malformed or absent metadata always degrades to the bounded raw
9
+ * result text, never throws during replay.
10
+ *
11
+ * @module @deepseek-ai/dsh-code/render/tool-detail
12
+ */
13
+ /** One rendered diff row: removed, added, or shared context. */
14
+ export interface DiffLine {
15
+ /** '-' removed, '+' added, ' ' context. */
16
+ mark: '-' | '+' | ' ';
17
+ /** The line text, truncated to the column budget. */
18
+ text: string;
19
+ }
20
+ /** One file's bounded inline diff. */
21
+ export interface ToolDiff {
22
+ /** File path the change belongs to. */
23
+ path: string;
24
+ /** Rendered rows in order; '-' block before the '+' block. */
25
+ lines: readonly DiffLine[];
26
+ /** True when the line budget cut the hunk. */
27
+ truncated: boolean;
28
+ }
29
+ /** One numbered line of a read window. */
30
+ export interface ToolReadLine {
31
+ /** 1-based file line number. */
32
+ number: number;
33
+ /** The line text, truncated to the column budget. */
34
+ text: string;
35
+ }
36
+ /** One web-search source row. */
37
+ export interface ToolWebSource {
38
+ /** Source URL. */
39
+ url: string;
40
+ /** Source title, when the provider returned one. */
41
+ title: string | undefined;
42
+ /** Short excerpt, truncated to the column budget. */
43
+ snippet: string;
44
+ }
45
+ /** The expansion payload a verbose tool card renders; a discriminated union. */
46
+ export type ToolDetail = {
47
+ kind: 'diff';
48
+ diffs: readonly ToolDiff[];
49
+ } | {
50
+ kind: 'read';
51
+ path: string;
52
+ offset: number;
53
+ lines: readonly ToolReadLine[];
54
+ totalLines: number;
55
+ truncated: boolean;
56
+ } | {
57
+ kind: 'web-search';
58
+ sources: readonly ToolWebSource[];
59
+ truncated: boolean;
60
+ } | {
61
+ kind: 'web-fetch';
62
+ url: string;
63
+ statusCode: number;
64
+ } | {
65
+ kind: 'raw';
66
+ text: string;
67
+ truncated: boolean;
68
+ };
69
+ /**
70
+ * Render one change as removed-then-added rows, hunked by common prefix and
71
+ * suffix. A null before-image (file create) renders as pure additions. The
72
+ * budget caps emitted rows and reports the cut, so a whole-file overwrite
73
+ * never floods the transcript.
74
+ * @param oldText - prior content, or null for a create.
75
+ * @param newText - content after the change.
76
+ * @param budget - maximum rows to emit.
77
+ * @returns the bounded rows and whether they were cut.
78
+ */
79
+ export declare function diffRows(oldText: string | null, newText: string, budget: number): {
80
+ lines: readonly DiffLine[];
81
+ truncated: boolean;
82
+ };
83
+ /**
84
+ * Narrow the opaque `tool/result.meta` into one bounded expansion payload,
85
+ * mirroring the upstream presenters' degradation ladder: diffs (write/edit),
86
+ * read windows (read), sources (web_search), fetch summaries (web_fetch), and
87
+ * the bounded raw result text as the universal fallback.
88
+ * @param meta - the persisted presentation metadata, when the tool attached one.
89
+ * @param rawText - the joined text blocks of the result message.
90
+ * @returns the expansion payload, or undefined when nothing renderable exists.
91
+ */
92
+ export declare function toolResultDetail(meta: unknown, rawText: string): ToolDetail | undefined;
@@ -16,6 +16,8 @@ export interface TranscriptStore {
16
16
  subscribe(listener: () => void): () => void;
17
17
  /** Fold one session event; ignored events change nothing and notify nobody. */
18
18
  apply(event: SessionEvent): void;
19
+ /** Drop the folded view entirely (/clear): the next event starts a fresh one. */
20
+ reset(): void;
19
21
  }
20
22
  /**
21
23
  * Create one transcript store, optionally seeded with replayed history. The
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.3.0",
4
+ "version": "0.4.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.mjs",
7
7
  "types": "lib/types/index.d.ts",
@@ -72,23 +72,33 @@
72
72
  "@deepseek-ai/dsh-cmdline": "^0.1.0-rc.6",
73
73
  "@deepseek-ai/dsh-code-runtime-worker-thread": "^0.1.0-rc.6",
74
74
  "@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
75
+ "@deepseek-ai/dsh-compaction": "^0.1.0-rc.6",
76
+ "@deepseek-ai/dsh-goal": "^0.1.0-rc.6",
75
77
  "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
76
78
  "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
79
+ "@deepseek-ai/dsh-llm-retry": "^0.1.0-rc.6",
77
80
  "@deepseek-ai/dsh-permission-presets": "^0.1.0-rc.6",
78
81
  "@deepseek-ai/dsh-plan-mode": "^0.1.0-rc.6",
82
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.6",
79
83
  "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
80
84
  "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6",
81
85
  "@deepseek-ai/dsh-session-reference": "^0.1.0-rc.6",
86
+ "@deepseek-ai/dsh-session-title": "^0.1.0-rc.6",
82
87
  "@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
83
88
  "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.6",
84
89
  "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.6"
85
90
  },
86
91
  "devDependencies": {
92
+ "@deepseek-ai/dsh-compaction": "^0.1.0-rc.6",
87
93
  "@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
94
+ "@deepseek-ai/dsh-goal": "^0.1.0-rc.6",
95
+ "@deepseek-ai/dsh-llm-retry": "^0.1.0-rc.6",
88
96
  "@deepseek-ai/dsh-permission-presets": "^0.1.0-rc.6",
89
97
  "@deepseek-ai/dsh-plan-mode": "^0.1.0-rc.6",
98
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.6",
90
99
  "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6",
91
100
  "@deepseek-ai/dsh-session-reference": "^0.1.0-rc.6",
101
+ "@deepseek-ai/dsh-session-title": "^0.1.0-rc.6",
92
102
  "@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
93
103
  "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.6",
94
104
  "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.6",