dsh-code 0.3.0 → 0.5.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 (46) hide show
  1. package/README.md +201 -55
  2. package/README.zh.md +204 -61
  3. package/bin/deepseek.mjs +70 -0
  4. package/cordis.patch.yml +62 -6
  5. package/lib/index.mjs +4073 -1802
  6. package/lib/startup.mjs +34 -17
  7. package/lib/types/app.d.ts +23 -22
  8. package/lib/types/commands.d.ts +2 -0
  9. package/lib/types/index.d.ts +5 -5
  10. package/lib/types/internals.d.ts +2 -0
  11. package/lib/types/kernel-panels.d.ts +23 -0
  12. package/lib/types/plugin-inventory.d.ts +11 -0
  13. package/lib/types/presets.d.ts +32 -0
  14. package/lib/types/render/export.d.ts +15 -0
  15. package/lib/types/render/inspector.d.ts +34 -0
  16. package/lib/types/render/lines.d.ts +31 -0
  17. package/lib/types/render/projection.d.ts +95 -3
  18. package/lib/types/render/status.d.ts +19 -0
  19. package/lib/types/render/text.d.ts +27 -0
  20. package/lib/types/render/tool-detail.d.ts +92 -0
  21. package/lib/types/session-directory.d.ts +54 -0
  22. package/lib/types/session-switch.d.ts +17 -0
  23. package/lib/types/skills.d.ts +2 -0
  24. package/lib/types/startup.d.ts +11 -1
  25. package/lib/types/store.d.ts +2 -0
  26. package/package.json +16 -1
  27. package/src/app.ts +1367 -277
  28. package/src/commands.ts +15 -1
  29. package/src/index.ts +373 -128
  30. package/src/internals.ts +5 -0
  31. package/src/kernel-panels.ts +254 -0
  32. package/src/plugin-inventory.ts +47 -0
  33. package/src/presets.ts +64 -0
  34. package/src/render/export.ts +81 -0
  35. package/src/render/inspector.ts +88 -0
  36. package/src/render/lines.ts +207 -0
  37. package/src/render/markdown.ts +15 -1
  38. package/src/render/projection.ts +279 -16
  39. package/src/render/status.ts +51 -1
  40. package/src/render/text.ts +107 -0
  41. package/src/render/tool-detail.ts +197 -0
  42. package/src/session-directory.ts +102 -0
  43. package/src/session-switch.ts +58 -0
  44. package/src/skills.ts +20 -7
  45. package/src/startup.ts +38 -20
  46. package/src/store.ts +8 -0
package/lib/startup.mjs CHANGED
@@ -3,7 +3,7 @@ import { parseCmdline } from "@deepseek-ai/dsh-cmdline";
3
3
  //#region src/startup.ts
4
4
  /**
5
5
  * The interactive terminal app's command-line provider: parses `--resume`,
6
- * `--continue`, `--session`, and `--help`, then publishes
6
+ * `--continue`, `--session`, `--mode`, and `--help`, then publishes
7
7
  * {@link TUI_STARTUP_SERVICE} for the runner to consume lazily. Follows the
8
8
  * headless bundle's startup shape (a commander action publishing a service
9
9
  * through {@link parseCmdline}).
@@ -26,17 +26,41 @@ const name = "tui-startup";
26
26
  const inject = ["cmdlineArgs"];
27
27
  /** Service provided by this plugin and injected by the terminal runner. */
28
28
  const TUI_STARTUP_SERVICE = "tuiStartup";
29
+ /** Pure option policy shared by Commander and tests. */
30
+ function resolveTuiStartup(options) {
31
+ if ([
32
+ options.resume !== void 0,
33
+ options.continue === true,
34
+ options.session !== void 0
35
+ ].filter(Boolean).length > 1) throw new Error("--resume, --continue, and --session are mutually exclusive");
36
+ if (options.session === "") throw new Error("--session needs an id");
37
+ if (options.resume === "") throw new Error("--resume needs a session id or id prefix");
38
+ if (options.mode === "") throw new Error("--mode needs a preset id");
39
+ if (options.mode !== void 0 && (options.resume !== void 0 || options.continue === true)) throw new Error("--mode applies only to a new session; it cannot be combined with --resume or --continue");
40
+ return options.resume !== void 0 ? {
41
+ kind: "resume",
42
+ sessionId: options.resume
43
+ } : options.continue === true ? { kind: "latest" } : options.session !== void 0 ? {
44
+ kind: "named",
45
+ sessionId: options.session,
46
+ ...options.mode === void 0 ? {} : { mode: options.mode }
47
+ } : {
48
+ kind: "fresh",
49
+ ...options.mode === void 0 ? {} : { mode: options.mode }
50
+ };
51
+ }
29
52
  /**
30
53
  * This app's command: the launcher's flags this app owns, its description,
31
54
  * and its help text.
32
55
  * @returns a fresh program, so one process can parse more than once (tests).
33
56
  */
34
57
  function tuiCommand() {
35
- return new Command().name("dsh --profile cli").description("Claude-Code-style interactive terminal for DeepSeek Harness.").helpOption("-h, --help", "show this help").option("-r, --resume <session>", "resume the persisted session with this id (or unique id prefix)").option("-c, --continue", "resume the most recent persisted session for this working directory").option("--session <id>", "create a new session under this explicit id").addHelpText("after", `
58
+ return new Command().name("dsh --profile cli").description("Claude-Code-style interactive terminal for DeepSeek Harness.").helpOption("-h, --help", "show this help").option("-r, --resume <session>", "resume the persisted session with this id (or unique id prefix)").option("-c, --continue", "resume the most recent persisted session for this working directory").option("--session <id>", "create a new session under this explicit id").option("--mode <preset>", "agent preset for a newly created session").addHelpText("after", `
36
59
  Examples:
37
60
  dsh --profile cli fresh session, minted id
38
61
  dsh --profile cli --resume abc123 resume session by id prefix
39
62
  dsh --profile cli --continue resume the latest local session
63
+ dsh --profile cli --mode minimal fresh session using the minimal preset
40
64
  `);
41
65
  }
42
66
  /**
@@ -48,23 +72,16 @@ function apply(ctx) {
48
72
  const program = tuiCommand();
49
73
  program.action(() => {
50
74
  const options = program.opts();
51
- if ([
52
- options.resume !== void 0,
53
- options.continue === true,
54
- options.session !== void 0
55
- ].filter(Boolean).length > 1) program.error("error: --resume, --continue, and --session are mutually exclusive");
56
- if (options.session !== void 0 && options.session === "") program.error("error: --session needs an id");
57
- if (options.resume !== void 0 && options.resume === "") program.error("error: --resume needs a session id or id prefix");
58
- const startup = options.resume !== void 0 ? {
59
- kind: "resume",
60
- sessionId: options.resume
61
- } : options.continue === true ? { kind: "latest" } : options.session !== void 0 ? {
62
- kind: "named",
63
- sessionId: options.session
64
- } : { kind: "fresh" };
75
+ let startup;
76
+ try {
77
+ startup = resolveTuiStartup(options);
78
+ } catch (error) {
79
+ program.error(`error: ${error instanceof Error ? error.message : String(error)}`);
80
+ }
81
+ if (startup === void 0) return;
65
82
  ctx.provide(TUI_STARTUP_SERVICE, { startup });
66
83
  });
67
84
  parseCmdline(ctx, program);
68
85
  }
69
86
  //#endregion
70
- export { TUI_STARTUP_SERVICE, apply, inject, name };
87
+ export { TUI_STARTUP_SERVICE, apply, inject, name, resolveTuiStartup };
@@ -21,6 +21,11 @@ import type { ModelDirectory, ModelRow } from './models.ts';
21
21
  import type { QuestionStore } from './questions.ts';
22
22
  import type { SkillsView } from './skills.ts';
23
23
  import type { MentionCandidate } from './mentions.ts';
24
+ import type { PresetRow } from './presets.ts';
25
+ import type { PluginRow } from './plugin-inventory.ts';
26
+ import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts';
27
+ /** Visual priority for one bounded local notice. */
28
+ export type NoticeTone = 'info' | 'warning' | 'error';
24
29
  /** Props the runner hands the app; callbacks stay owned by the runner. */
25
30
  export interface AppProps {
26
31
  /** Event-fed transcript store for the live session. */
@@ -37,12 +42,16 @@ export interface AppProps {
37
42
  model: string;
38
43
  /** Working-directory basename the session serves. */
39
44
  cwd: string;
45
+ /** Absolute working directory used by session filters and references. */
46
+ workspaceRoot: string;
40
47
  /** Git branch name, empty outside a repository. */
41
48
  branch: string;
42
49
  /** Short session identifier. */
43
50
  sessionId: string;
44
51
  /** Whether this session was resumed from persistence. */
45
52
  resumed: boolean;
53
+ /** Agent preset currently composing the session. */
54
+ mode: string;
46
55
  /** Submit one line: slash commands to the registry, other text to the agent. */
47
56
  dispatch(text: string): void;
48
57
  /** Submit steering: consumed at the running turn's next step boundary. */
@@ -59,31 +68,23 @@ export interface AppProps {
59
68
  selectModel(row: ModelRow): string;
60
69
  /** Cycle to the next permission preset (Shift+Tab); returns the new label. */
61
70
  cyclePermission(): string;
71
+ /** Export the transcript to a markdown file (/export [path]); reports via notices. */
72
+ exportTranscript(argument: string): Promise<void>;
73
+ /** Rename the session (/title <text>); returns the outcome line for the notice. */
74
+ renameTitle(argument: string): string;
75
+ /** Preset/session/plugin kernel operations. */
76
+ loadPresets(): Promise<readonly PresetRow[]>;
77
+ switchMode(id: string): Promise<string>;
78
+ createSession(mode?: string): void;
79
+ loadSessions(options: SessionDirectoryOptions, signal?: AbortSignal): Promise<readonly SessionRow[]>;
80
+ loadSessionTranscript(id: string, signal?: AbortSignal): Promise<string>;
81
+ switchSession(row: SessionRow): void;
82
+ cancelSessionSwitch(): boolean;
83
+ loadPlugins(): readonly PluginRow[];
62
84
  /** Registers the app's notice channel with the runner (called once on mount). */
63
85
  onBridgeReady(bridge: {
64
- notify(text: string): void;
86
+ notify(text: string, tone?: NoticeTone): void;
65
87
  }): void;
66
88
  }
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
89
  /** The whole terminal app; state arrives via the store, output via Ink. */
88
90
  export declare function App(props: AppProps): ReactElement;
89
- export {};
@@ -14,6 +14,8 @@ import type { CommandDescriptor } from '@deepseek-ai/dsh-commands';
14
14
  export interface CommandsView {
15
15
  /** Name-sorted descriptors after scoped shadowing. */
16
16
  readonly descriptors: readonly CommandDescriptor[];
17
+ /** Latest descriptor-read failure; the help panel exposes it in place. */
18
+ readonly error?: string;
17
19
  /** Subscribe to list changes (`commands/change`); returns the unsubscribe function. */
18
20
  subscribe(listener: () => void): () => void;
19
21
  /** Retarget the agent whose scoped view the list is read through. */
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * @deepseek-ai/dsh-code — the interactive terminal driver. The bundle patch
3
3
  * rides over dsh-base without Host, HTTP, or browser plugins; this runner
4
- * creates (or resumes) one Agent through the core registry, mounts the Ink
5
- * app (DeepSeek blue, whale wordmark), folds submitted prompts into the same
6
- * durable session, answers approval asks with a y/n bar, dispatches slash
7
- * commands through the shared registry, and on quit flushes and requests
8
- * process exit.
4
+ * creates or resumes preset-composed Agents through the core registry, keeps
5
+ * one Ink owner while the active session changes, folds submitted prompts
6
+ * into the selected durable session, answers approval asks with a y/n bar,
7
+ * dispatches slash commands, and on quit flushes and requests process exit.
9
8
  *
10
9
  * @module @deepseek-ai/dsh-code
11
10
  */
@@ -21,6 +20,7 @@ export interface Config {
21
20
  startup: {
22
21
  kind: string;
23
22
  sessionId?: string;
23
+ mode?: string;
24
24
  };
25
25
  }
26
26
  export declare const Config: z<Config>;
@@ -8,6 +8,8 @@
8
8
  import type { ReactElement } from 'react';
9
9
  /** A mounted terminal app instance; the runner owns unmount ordering. */
10
10
  export interface TuiMount {
11
+ /** Replace the root element while preserving Ink's single terminal owner. */
12
+ rerender(element: ReactElement): void;
11
13
  /** Tear the terminal app down before flush and exit. */
12
14
  unmount(): void;
13
15
  }
@@ -0,0 +1,23 @@
1
+ /** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
2
+ import { type ReactElement } from 'react';
3
+ import type { PresetRow } from './presets.ts';
4
+ import type { PluginRow } from './plugin-inventory.ts';
5
+ import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts';
6
+ export declare function ModePanel({ current, load, select, close }: {
7
+ current: string;
8
+ load(): Promise<readonly PresetRow[]>;
9
+ select(id: string): void;
10
+ close(): void;
11
+ }): ReactElement;
12
+ export declare function PluginPanel({ load, close, initialQuery }: {
13
+ load(): readonly PluginRow[];
14
+ close(): void;
15
+ initialQuery?: string;
16
+ }): ReactElement;
17
+ export declare function ResumePanel({ currentCwd, load, readTranscript, select, close }: {
18
+ currentCwd: string;
19
+ load(options: SessionDirectoryOptions, signal?: AbortSignal): Promise<readonly SessionRow[]>;
20
+ readTranscript(id: string, signal?: AbortSignal): Promise<string>;
21
+ select(row: SessionRow): void;
22
+ close(): void;
23
+ }): ReactElement;
@@ -0,0 +1,11 @@
1
+ /** Read-only projection of Cordis Loader entries for /plugin. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ export type PluginPhase = 'pending' | 'loading' | 'active' | 'failed' | 'unloading' | null;
4
+ export interface PluginRow {
5
+ readonly entryId: string;
6
+ readonly moduleName: string;
7
+ readonly enabled: boolean;
8
+ readonly phase: PluginPhase;
9
+ }
10
+ /** Snapshot the live Loader; group-only rows are composition containers, not plugins. */
11
+ export declare function listPluginRows(ctx: Context): PluginRow[];
@@ -0,0 +1,32 @@
1
+ /** Agent-preset policy kept independent from the Ink surface. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ import type { Agent, AgentHandle } from '@deepseek-ai/dsh-agent';
4
+ import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
5
+ /** One discoverable agent composition. */
6
+ export interface PresetRow {
7
+ readonly id: string;
8
+ readonly trust: 'system' | 'user';
9
+ readonly name?: string;
10
+ readonly description?: string;
11
+ readonly order?: number;
12
+ readonly broken?: string;
13
+ }
14
+ /** Structural boundary for the optional upstream AgentPresets service. */
15
+ export interface AgentPresetsService {
16
+ readonly defaultId: string;
17
+ list(): Promise<PresetRow[]>;
18
+ resolve(id?: string): Promise<PresetRow>;
19
+ mount(agentCtx: Context, id?: string): Promise<PresetRow>;
20
+ recompose(agentCtx: Context, id: string): Promise<PresetRow>;
21
+ composedPreset(agentCtx: Context): string | undefined;
22
+ }
23
+ /** Read an optional Cordis service without requiring its package at build time. */
24
+ export declare function agentPresetsFrom(ctx: Context): AgentPresetsService | undefined;
25
+ /** A preset may change only before the first durable turn begins. */
26
+ export declare function isBlankSession(events: readonly SessionEvent[]): boolean;
27
+ /** Latest logged selection wins; legacy sessions deliberately fall back to standard. */
28
+ export declare function resolvePreset(session: Pick<Session, 'header' | 'events'>): string;
29
+ /** Recompose atomically from the caller's perspective, logging only success. */
30
+ export declare function switchPreset(service: AgentPresetsService, agent: Agent, presetId: string): Promise<PresetRow>;
31
+ /** Minimal handle shape used by lifecycle tests without exposing Agent internals. */
32
+ export type OwnedAgent = Pick<AgentHandle, 'agent' | 'dispose'>;
@@ -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,34 @@
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
+ /** Optional blank rows separating title/body/footer on roomy terminals. */
9
+ gapRows: 0 | 2;
10
+ /** Columns available inside the horizontal border and padding. */
11
+ contentColumns: number;
12
+ /** Tiny terminals use a borderless one-line close hint. */
13
+ compact: boolean;
14
+ }
15
+ /** One transcript-to-composer gutter, collapsed on short terminals. */
16
+ export declare function layoutGutterRows(rows: number): 0 | 1;
17
+ /**
18
+ * Keep the inspector plus its persistent status/composer chrome below
19
+ * `stdout.rows`: at equality Ink clears the terminal and rewrites all
20
+ * accumulated `<Static>` output on every frame.
21
+ */
22
+ export declare function panelViewport(columns: number, rows: number): InspectorViewport;
23
+ /** Backward-compatible name for the Ctrl+O-specific caller and tests. */
24
+ export declare function inspectorViewport(columns: number, rows: number): InspectorViewport;
25
+ /** Clamp a first-visible row to the range representable by one viewport. */
26
+ export declare function clampScroll(offset: number, totalRows: number, visibleRows: number): number;
27
+ /** Move a viewport by a signed row delta without escaping its content. */
28
+ export declare function moveScroll(offset: number, delta: number, totalRows: number, visibleRows: number): number;
29
+ /** Keep one focused row visible while preserving the current window when possible. */
30
+ export declare function revealRow(offset: number, row: number, totalRows: number, visibleRows: number): number;
31
+ /** Center a selected list row where possible, clamped at both ends. */
32
+ export declare function selectionWindow(cursor: number, totalRows: number, visibleRows: number): number;
33
+ /** Follow appended history only while the inspector cursor was at the tail. */
34
+ 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.
@@ -31,12 +38,24 @@ export declare function cacheHitPercent(usage: TranscriptStats['usage']): number
31
38
  export interface StatusFacts {
32
39
  /** `provider/model` selection serving this session. */
33
40
  model: string;
41
+ /** Agent preset composing this session. */
42
+ mode?: string;
34
43
  /** Working-directory basename the session serves. */
35
44
  cwd: string;
36
45
  /** Git branch name, empty outside a repository or on a detached HEAD file. */
37
46
  branch: string;
38
47
  /** Short session identifier (last dash-separated segment or tail). */
39
48
  sessionId: string;
49
+ /** Latest session title (folded from `session/title`); shown in place of the id. */
50
+ title: string;
51
+ /** Sandbox-mode override (folded from `sandbox/mode`), empty when never switched. */
52
+ sandbox: string;
53
+ /** Live goal summary (folded from `goal/change`), undefined when none. */
54
+ goal: {
55
+ phase: string;
56
+ rounds: number;
57
+ max: number;
58
+ } | undefined;
40
59
  /** Whether plan mode is active (folded from `plan/mode`). */
41
60
  plan: boolean;
42
61
  /** Active permission preset (folded from `permission/preset`), empty when unknown. */
@@ -16,3 +16,30 @@
16
16
  * as a literal `\xNN` escape.
17
17
  */
18
18
  export declare function displayText(text: string): string;
19
+ /** Collapse external text to one terminal-safe logical row. */
20
+ export declare function singleLineText(text: string): string;
21
+ /**
22
+ * Truncate one display-safe row without ever exceeding its physical-column
23
+ * budget. The ellipsis is included inside the budget, matching Codex's popup
24
+ * truncation contract; the previous app-local helper appended it after the
25
+ * row was already full and could force an extra terminal wrap.
26
+ */
27
+ export declare function truncateColumns(text: string, columns: number): string;
28
+ /** A display-safe suffix bounded by terminal rows and columns. */
29
+ export interface DisplayTail {
30
+ /** Sanitized suffix suitable for direct terminal rendering. */
31
+ text: string;
32
+ /** Whether content before the returned suffix was omitted. */
33
+ truncated: boolean;
34
+ }
35
+ /**
36
+ * Keep only the newest display-safe text that fits a terminal rectangle.
37
+ * The scan walks backward and stops as soon as the suffix is full, so a long
38
+ * reasoning stream does not rescan its entire accumulated prefix per chunk.
39
+ * Explicit newlines and terminal wrapping both consume rows.
40
+ * @param text - raw externally sourced text.
41
+ * @param columns - available terminal columns.
42
+ * @param rows - available terminal rows.
43
+ * @returns a sanitized bounded suffix and whether an earlier prefix was cut.
44
+ */
45
+ export declare function displayTail(text: string, columns: number, rows: number): DisplayTail;