dsh-ssh-tui 0.5.2 → 0.5.4

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 +36 -6
  2. package/README.md +24 -13
  3. package/lib/approval-reviewer.js +102 -17
  4. package/lib/approval-reviewer.js.map +1 -1
  5. package/lib/auto-approval.js +312 -29
  6. package/lib/auto-approval.js.map +1 -1
  7. package/lib/footer.js +337 -0
  8. package/lib/footer.js.map +1 -0
  9. package/lib/i18n/en.js +83 -8
  10. package/lib/i18n/en.js.map +1 -1
  11. package/lib/i18n/index.js +1 -0
  12. package/lib/i18n/index.js.map +1 -1
  13. package/lib/i18n/zh.js +83 -8
  14. package/lib/i18n/zh.js.map +1 -1
  15. package/lib/json-args.js +30 -0
  16. package/lib/json-args.js.map +1 -0
  17. package/lib/paint.js +262 -0
  18. package/lib/paint.js.map +1 -0
  19. package/lib/picker.js +407 -56
  20. package/lib/picker.js.map +1 -1
  21. package/lib/plan.js +369 -0
  22. package/lib/plan.js.map +1 -0
  23. package/lib/quota.js +408 -0
  24. package/lib/quota.js.map +1 -0
  25. package/lib/session-list.js +18 -21
  26. package/lib/session-list.js.map +1 -1
  27. package/lib/term-text.js +827 -0
  28. package/lib/term-text.js.map +1 -0
  29. package/lib/tool-present.js +744 -0
  30. package/lib/tool-present.js.map +1 -0
  31. package/lib/transcript-types.js +6 -0
  32. package/lib/transcript-types.js.map +1 -0
  33. package/lib/tui.js +747 -3077
  34. package/lib/tui.js.map +1 -1
  35. package/lib/types/approval-reviewer.d.ts +18 -4
  36. package/lib/types/auto-approval.d.ts +53 -2
  37. package/lib/types/footer.d.ts +155 -0
  38. package/lib/types/i18n/index.d.ts +2 -0
  39. package/lib/types/json-args.d.ts +7 -0
  40. package/lib/types/paint.d.ts +78 -0
  41. package/lib/types/picker.d.ts +99 -7
  42. package/lib/types/plan.d.ts +80 -0
  43. package/lib/types/quota.d.ts +94 -0
  44. package/lib/types/session-list.d.ts +13 -0
  45. package/lib/types/term-text.d.ts +130 -0
  46. package/lib/types/tool-present.d.ts +165 -0
  47. package/lib/types/transcript-types.d.ts +152 -0
  48. package/lib/types/tui.d.ts +39 -665
  49. package/package.json +1 -1
@@ -0,0 +1,94 @@
1
+ /**
2
+ * OpenCode / SuperGrok / DeepSeek quota and prepaid-balance parsers.
3
+ */
4
+ /** A recognized OpenCode provider route, used by /usage and /quota. */
5
+ export type OpenCodeFlavor = 'zen' | 'go';
6
+ export interface OpenCodeSource {
7
+ provider: string;
8
+ flavor: OpenCodeFlavor;
9
+ label: string;
10
+ apiKeyEnv: string;
11
+ baseURL?: string;
12
+ }
13
+ export interface LlmPiAiProviderProfile {
14
+ displayName?: unknown;
15
+ apiKeyEnv?: unknown;
16
+ baseURL?: unknown;
17
+ api?: unknown;
18
+ models?: unknown;
19
+ reasoning?: unknown;
20
+ }
21
+ export interface LlmPiAiSection {
22
+ providers?: Record<string, LlmPiAiProviderProfile>;
23
+ }
24
+ /** Build per-model reasoningEfforts from a provider-level reasoning default. */
25
+ export declare function reasoningEffortsForDefault(reasoning: unknown): Record<string, string | null> | undefined;
26
+ export declare const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
27
+ export declare const OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1";
28
+ export declare const SUPERGROK_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
29
+ export declare const DEEPSEEK_PUBLIC_BASE_URL = "https://api.deepseek.com";
30
+ /** OpenAI-completions gateways: probe these relative to the configured base URL. */
31
+ export declare const OPENAI_COMPAT_BALANCE_PATHS: readonly ["/user/balance", "/dashboard/billing/credit_grants", "/v1/dashboard/billing/credit_grants", "/v1/dashboard/billing/subscription"];
32
+ /**
33
+ * Classify the currently selected provider as an OpenCode route. Built-in
34
+ * `opencode`/`opencode-go` ids are recognized directly, and custom llm-pi-ai
35
+ * routes are recognized by their `opencode.ai` base URL.
36
+ */
37
+ export declare function openCodeSourceFor(provider: string, llmPiAiSection: unknown): OpenCodeSource | null;
38
+ export type QuotaPeriod = 'hourly' | 'weekly' | 'monthly' | 'unknown';
39
+ export interface QuotaWindow {
40
+ label: string;
41
+ period: QuotaPeriod;
42
+ /** Remaining percent of the window (100 = unused). */
43
+ remainingPercent: number;
44
+ resetsAt?: string;
45
+ }
46
+ export interface QuotaSnapshot {
47
+ provider: string;
48
+ plan: string;
49
+ windows: QuotaWindow[];
50
+ }
51
+ export declare function remainingPercentFromUsed(usedPercent: number): number;
52
+ /** Cross a remaining-percent threshold from above (50 / 25 / 10 / 5).
53
+ * Only the tightest (lowest) crossed threshold is returned, so one drop
54
+ * never paints 50/25/10 as three identical warnings. */
55
+ export declare function crossedQuotaThresholds(previousRemaining: number | undefined, remaining: number): number[];
56
+ export declare function quotaAlertText(snapshot: QuotaSnapshot, window: QuotaWindow): string;
57
+ /**
58
+ * How often to re-fetch quota or prepaid balance, counted in model steps.
59
+ * Default is every 10 steps. Near a remaining-percent threshold, hourly
60
+ * windows refresh every 4 steps.
61
+ */
62
+ export declare function quotaRefreshEverySteps(window: QuotaWindow | undefined): number;
63
+ /** @deprecated Same cadence as {@link quotaRefreshEverySteps}; the name predates step accounting. */
64
+ export declare const quotaRefreshEveryTurns: typeof quotaRefreshEverySteps;
65
+ export declare function parseSuperGrokBilling(payload: unknown): QuotaSnapshot;
66
+ export declare function parseOpenCodeGoQuota(payload: unknown, provider: string): QuotaSnapshot;
67
+ export declare function formatQuotaSnapshot(snapshot: QuotaSnapshot): string;
68
+ /** Compact `/status` quota line: tightest window first, then the rest. */
69
+ export declare function formatQuotaStatusLine(snapshot: QuotaSnapshot | undefined): string;
70
+ /** Tightest remaining window — used for threshold alerts. */
71
+ export declare function tightestQuotaWindow(snapshot: QuotaSnapshot): QuotaWindow | undefined;
72
+ /** Render the OpenCode Go quota payload as a transcript block. */
73
+ export declare function formatOpenCodeGoUsage(payload: unknown, source: OpenCodeSource): string;
74
+ export interface AccountBalanceLine {
75
+ label: string;
76
+ amount: string;
77
+ currency?: string;
78
+ }
79
+ export interface AccountBalanceSnapshot {
80
+ provider: string;
81
+ plan: string;
82
+ available?: boolean;
83
+ lines: AccountBalanceLine[];
84
+ sourcePath?: string;
85
+ }
86
+ export declare function joinUrl(base: string, path: string): string;
87
+ export declare function parseDeepSeekBalance(payload: unknown, provider?: string): AccountBalanceSnapshot;
88
+ /** Best-effort parse of OpenAI-compatible credit/balance JSON. */
89
+ export declare function parseOpenAiCompatibleBalance(payload: unknown, provider: string, path: string): AccountBalanceSnapshot | undefined;
90
+ /** Compact footer chip: `余额 86.42 CNY`. Prefers remaining/available lines. */
91
+ export declare function formatFooterBalance(snapshot: AccountBalanceSnapshot): string | undefined;
92
+ export declare function formatAccountBalance(snapshot: AccountBalanceSnapshot): string;
93
+ /** Extract a safe human-readable message from an OpenCode error payload. */
94
+ export declare function openCodeApiErrorMessage(payload: unknown): string;
@@ -38,4 +38,17 @@ export interface ResumableSession {
38
38
  }
39
39
  /** `MM-DD HH:mm` local-time label for session lists. */
40
40
  export declare function formatSessionTime(timestamp: number): string;
41
+ /**
42
+ * List resumable top-level sessions, newest first.
43
+ *
44
+ * Subagent-owned sessions and the current session are excluded. Sessions
45
+ * whose event log cannot be inspected are kept (marked `unreadable`) instead
46
+ * of silently disappearing from history; readable sessions with user input
47
+ * sort first. The label is the persisted title, then the user's first input,
48
+ * then the id.
49
+ * @param persistence - the session persistence service.
50
+ * @param currentId - the live session to exclude (empty at launch).
51
+ * @returns every resumable candidate in display order (attachable live
52
+ * hosts first, then readable logs, then unreadable).
53
+ */
41
54
  export declare function listResumableSessions(persistence: SessionPersistence, currentId: string, listHosts?: typeof listAttachableHosts): Promise<ResumableSession[]>;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Terminal cell metrics, wrapping, markdown, and input folding.
3
+ *
4
+ * Isolated so the launch session picker can clip labels without loading
5
+ * the interactive TUI class.
6
+ */
7
+ /**
8
+ * Codex-style compact elapsed: `0s`, `1m 05s`, `1h 01m 01s`.
9
+ * Used by the workspace wait card while the model has not streamed yet.
10
+ */
11
+ export declare function fmtElapsedCompact(elapsedSecs: number): string;
12
+ /**
13
+ * Sweep highlight across `text` (Codex `shimmer.rs`). Truecolor blends a
14
+ * highlight band; otherwise DIM / default / BOLD. Process-start based so
15
+ * every paint of the same frame stays in phase.
16
+ */
17
+ export declare function shimmerText(text: string, nowMs: number, color: boolean): string;
18
+ /**
19
+ * Codex `extract_first_bold`: the first **closed** `**bold**` in the thinking
20
+ * stream, else the first markdown heading. An unclosed `**` means the title
21
+ * has not arrived yet, so return undefined and keep the default header —
22
+ * never fall back to hard-truncated reasoning, reply, or prompt text.
23
+ */
24
+ export declare function waitSummaryFromReasoning(text: string): string | undefined;
25
+ /** Wait-card header + optional detail. Header tracks model work when known. */
26
+ export declare function waitCardCopy(input: {
27
+ toolTitle?: string;
28
+ toolSummary?: string;
29
+ reasoning?: string;
30
+ }): {
31
+ header: string;
32
+ detail?: string;
33
+ };
34
+ /**
35
+ * Codex `wrapped_details_lines`: word-wrap the wait-card detail under the
36
+ * ` └ ` prefix, continue wrapped rows at the prefix width, cap at 3 rows and
37
+ * end the last one with an ellipsis when the text does not fit.
38
+ */
39
+ export declare function wrapWaitDetails(detail: string, width: number, maxLines?: number): string[];
40
+ /**
41
+ * Terminal cell width for one string.
42
+ *
43
+ * Match glibc wcwidth / typical UTF-8 SSH terminals: CJK ideographs and
44
+ * fullwidth forms occupy two cells; East-Asian Ambiguous box-drawing and
45
+ * ornaments (`─`, `●`, `·`, `▸`, `❯`, Braille spinners) occupy one. Counting
46
+ * those ambiguous glyphs as two made `repeatToWidth('─', cols)` paint a
47
+ * half-width rule and parked the input cursor half a cell past the text.
48
+ *
49
+ * Overflow into the input box is handled by clipping/padding painted rows to
50
+ * the measured column count, not by inflating glyph width.
51
+ */
52
+ export declare function displayWidth(text: string): number;
53
+ /** Pad or clip one already-sanitized line so it occupies exactly `width` cells. */
54
+ export declare function padToWidth(text: string, width: number): string;
55
+ /**
56
+ * Pad an already-styled ANSI line to `width` cells without resetting SGR.
57
+ * Diff add/del rows keep their background across the whole terminal row
58
+ * instead of only the glyphs.
59
+ */
60
+ export declare function padAnsiToWidth(text: string, width: number): string;
61
+ /** Visible width of an ANSI-styled line, ignoring CSI / OSC sequences. */
62
+ export declare function visibleWidth(text: string): number;
63
+ /** Repeat a glyph until it occupies exactly `width` cells. */
64
+ export declare function repeatToWidth(glyph: string, width: number): string;
65
+ /** Strip terminal control sequences and expand tabs for display output. */
66
+ export declare function sanitizeTerminalText(text: string): string;
67
+ /** UTF-16 length of the first code point, so fallback cuts never split a surrogate pair. */
68
+ export declare function firstCodePointLength(text: string): number;
69
+ export declare function wrap(text: string, width: number): string[];
70
+ /** One colored span inside a tool-card header line. Offsets are UTF-16 char indices. */
71
+ export interface TextSegment {
72
+ start: number;
73
+ end: number;
74
+ sgr: string;
75
+ }
76
+ /** Wrap plain text and report each output line's char range in the source. */
77
+ export declare function wrapTracked(text: string, width: number): {
78
+ line: string;
79
+ start: number;
80
+ end: number;
81
+ }[];
82
+ /** Paint one already-wrapped output line by the segments overlapping its range. */
83
+ export declare function paintSegmentedLine(line: string, start: number, end: number, segments: readonly TextSegment[]): string;
84
+ /** Wrap `text` and color each output line by overlapping `segments`. */
85
+ export declare function wrapSegmented(text: string, width: number, segments: readonly TextSegment[]): string[];
86
+ export declare function truncate(text: string, maxLines: number): string;
87
+ /**
88
+ * Render workspace markdown into width-bounded terminal rows. Assistant
89
+ * replies get a bold-white base; code blocks, headings, quotes, lists, rules,
90
+ * links and inline spans keep their own ANSI treatment.
91
+ */
92
+ export declare function renderMarkdownLines(text: string, width: number, color: boolean): string[];
93
+ /** Cut one line to fit a width, appending an ellipsis when truncated. */
94
+ export declare function truncateToWidth(text: string, width: number): string;
95
+ /**
96
+ * Clip an already-styled ANSI line to `width` terminal cells without dropping
97
+ * the reset/SGR sequences. Used by the incremental painter so a leftover wide
98
+ * glyph cannot wrap into the next row.
99
+ */
100
+ export declare function clipAnsiToWidth(text: string, width: number): string;
101
+ /** One renderable view of the input line: text plus the cursor's visual offset. */
102
+ export interface InputView {
103
+ text: string;
104
+ cursorOffset: number;
105
+ folded: boolean;
106
+ }
107
+ /**
108
+ * Fold a long input into one terminal row around the cursor.
109
+ *
110
+ * Newlines from a paste are display-only: they do not occupy cells, so a
111
+ * naive `displayWidth(input)` under-counts a multi-line paste and parks the
112
+ * caret in the middle of later text. Fold the *current line* (between the
113
+ * surrounding newlines) and keep `\n` out of the visible slice.
114
+ */
115
+ export declare function foldInputView(input: string, cursor: number, maxWidth: number): InputView;
116
+ /**
117
+ * Map a character index in the input text to its visual (row, col) after the
118
+ * same width wrapping `wrap()` applies to the rendered input. `row` is the
119
+ * 0-based input display line, `col` the 0-based column within that line
120
+ * (before any prompt prefix). This keeps the cursor on the correct line/column
121
+ * when the input contains literal newlines from multi-line pastes.
122
+ */
123
+ export declare function cursorVisualPosition(text: string, cursor: number, width: number): {
124
+ row: number;
125
+ col: number;
126
+ };
127
+ /** Take the first `max` code points of a string without splitting surrogates. */
128
+ export declare function sliceCodePoints(text: string, max: number): string;
129
+ /** Take the last `max` code points of a string without splitting surrogates. */
130
+ export declare function lastCodePoints(text: string, max: number): string;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Tool-card presentation: headers, diffs, compact bursts, JSON bodies.
3
+ */
4
+ import type { AskUserQuestionItem } from '@deepseek-ai/dsh-user-questions';
5
+ import { type TextSegment } from './term-text.js';
6
+ import type { DisplayKind, Row, ToolDiffHunk } from './transcript-types.js';
7
+ export declare const SHELL_TOOL_NAMES: Set<string>;
8
+ export declare const DIFF_TOOL_NAMES: Set<string>;
9
+ /** Format a model list compactly: show the first few entries and an ellipsis. */
10
+ export declare function formatModelList(models: readonly string[], max?: number): string;
11
+ /** Prefer the fields a human scans for; fall back to the first scalar pairs. */
12
+ export declare function friendlyArgsSummary(name: string, args: string): string;
13
+ export declare function countDiffLines(hunks: readonly ToolDiffHunk[] | undefined): number;
14
+ /** Added / removed line counts for a diff (`oldText: null` means a new file). */
15
+ export declare function countDiffAddDel(hunks: readonly ToolDiffHunk[] | undefined): {
16
+ add: number;
17
+ del: number;
18
+ };
19
+ /**
20
+ * Git diffstat token, deletions first like `-13 +24`. Zero parts drop out
21
+ * (a new file shows only `+24`); empty when the diff has no counted lines.
22
+ */
23
+ export declare function diffStatToken(add: number, del: number): string;
24
+ export declare const READ_TOOL_NAMES: Set<string>;
25
+ export declare const TOOL_FLIP_MS = 280;
26
+ export declare function toolTargetPath(name: string, args: string, fallback?: string): string;
27
+ /** Path shown on a compact single-file edit summary. */
28
+ export declare function compactEditPath(item: {
29
+ name: string;
30
+ args: string;
31
+ summary?: string;
32
+ diff?: readonly {
33
+ path?: string;
34
+ }[];
35
+ }): string;
36
+ export declare function sameToolPath(left: string, right: string): boolean;
37
+ export declare function countOutputLines(text: string): number;
38
+ export declare function mergeableToolKind(name: string): 'read' | 'edit' | undefined;
39
+ /**
40
+ * Consecutive same-path reads (or edits) collapse onto one card.
41
+ * A → B → C → A becomes four cards; A ×5 stays one card with repeats=5.
42
+ */
43
+ export declare function canMergeToolCall(previous: Extract<Row, {
44
+ kind: 'tool';
45
+ }> | undefined, next: {
46
+ name: string;
47
+ args: string;
48
+ }): previous is Extract<Row, {
49
+ kind: 'tool';
50
+ }>;
51
+ export declare function compactToolGroups(tools: readonly Extract<Row, {
52
+ kind: 'tool';
53
+ }>[]): {
54
+ edits: Extract<Row, {
55
+ kind: 'tool';
56
+ }>[];
57
+ calls: Extract<Row, {
58
+ kind: 'tool';
59
+ }>[];
60
+ failedCalls: number;
61
+ };
62
+ /**
63
+ * Split compact-view tools into the bursts that belong with each assistant
64
+ * reply: tools after reply N sit with that reply, until the next reply.
65
+ */
66
+ export declare function compactToolBursts(rows: readonly Row[]): Array<{
67
+ after: Extract<Row, {
68
+ kind: 'assistant';
69
+ }> | undefined;
70
+ groups: ReturnType<typeof compactToolGroups>;
71
+ }>;
72
+ export declare const SUBAGENT_TOOL_NAMES: Set<string>;
73
+ /**
74
+ * Tool calls that already have a dedicated transcript card (goal/change,
75
+ * plan dock, question dialog). Showing them again as raw `get_goal` cards
76
+ * just duplicates chrome.
77
+ */
78
+ export declare const HIDDEN_TOOL_NAMES: Set<string>;
79
+ export declare function toolTitle(name: string): string;
80
+ export declare function planReviewOf(question: AskUserQuestionItem): boolean;
81
+ /** Derive the intended file change from a mutation tool's arguments. */
82
+ export declare function diffHunksFromArgs(name: string, argsRaw: string): ToolDiffHunk[] | null;
83
+ /** One-line friendly tool-call presentation (command / path / arg summary). */
84
+ export declare function presentToolCall(name: string, args: string): {
85
+ title: string;
86
+ summary: string;
87
+ command?: string;
88
+ cwd?: string;
89
+ diff?: ToolDiffHunk[];
90
+ };
91
+ /** Validate a tool/result meta payload's structured diff, mirroring the web card. */
92
+ export declare function diffMetaDiffs(meta: unknown): ToolDiffHunk[] | null;
93
+ /** Split one diff side into content lines (trailing newline is a terminator). */
94
+ export declare function diffContentLines(text: string): string[];
95
+ /** One rendered diff body line with its display role. */
96
+ export interface DiffDisplayLine {
97
+ kind: DisplayKind;
98
+ text: string;
99
+ }
100
+ /** Cap one flat diff/body row list to `maxLines` while preserving the final line. */
101
+ export declare function capDisplayLines(lines: readonly DiffDisplayLine[], maxLines: number): DiffDisplayLine[];
102
+ /** Running / ok / error → ANSI color for the status dot and status word only. */
103
+ export declare function toolStateColor(status: 'running' | 'ok' | 'error' | undefined): '33' | '32' | '31';
104
+ export declare function toolStateLabel(status: 'running' | 'ok' | 'error' | undefined): string;
105
+ /** Header + SGR spans: default title, dim operand, colored ●. `[ok]` is omitted — the green dot is enough. */
106
+ export declare function buildToolHeader(input: {
107
+ focused: boolean;
108
+ expanded: boolean;
109
+ title: string;
110
+ summary: string;
111
+ status?: 'running' | 'ok' | 'error';
112
+ command?: string;
113
+ signal?: string;
114
+ exitCode?: number;
115
+ spinner?: string;
116
+ flipping?: boolean;
117
+ diffStat?: {
118
+ add: number;
119
+ del: number;
120
+ };
121
+ }): {
122
+ plain: string;
123
+ segments: TextSegment[];
124
+ };
125
+ /** How many terminal rows a tool body occupies after wrapping. */
126
+ export declare function wrappedToolBodyLineCount(lines: readonly {
127
+ text: string;
128
+ }[], width: number): number;
129
+ /**
130
+ * True when the full tool body plus a one-line header fits in the workspace
131
+ * (the rows between the title bar and the input chrome). Oversized bodies
132
+ * open a dedicated inspect overlay instead of dumping into the transcript.
133
+ */
134
+ export declare function toolBodyFitsWorkspace(bodyLines: number, workspaceRows: number): boolean;
135
+ /** Flatten hunks into git-style `-`/`+` lines plus the web-compatible footer. */
136
+ export declare function renderToolDiff(diffs: ToolDiffHunk[], maxLines: number): DiffDisplayLine[];
137
+ /** Convert any parsed JSON value into readable indented display lines. */
138
+ export declare function friendlyJsonLines(value: unknown, depth?: number): string[];
139
+ /** Minimal tool-row shape the expanded-body renderer reads. */
140
+ export interface ToolBodySource {
141
+ name?: string;
142
+ diff?: ToolDiffHunk[];
143
+ command?: string;
144
+ status?: 'running' | 'ok' | 'error';
145
+ output: string;
146
+ args: string;
147
+ }
148
+ /** Try to parse a result body as one JSON document, when it looks like one. */
149
+ export declare function parseJsonBody(text: string): unknown | null;
150
+ /**
151
+ * The expanded body of one tool card: diffs and shell output keep their
152
+ * dedicated views; every other tool's JSON arguments and JSON result are
153
+ * converted into readable indented content instead of raw JSON text.
154
+ */
155
+ export declare function toolBodyLines(row: ToolBodySource, maxLines: number): DiffDisplayLine[];
156
+ export interface NamedToolBodySource extends ToolBodySource {
157
+ name?: string;
158
+ }
159
+ export declare function specializedToolBody(row: NamedToolBodySource, maxLines?: number): DiffDisplayLine[] | null;
160
+ /** Recover the shell tools' exit marker, mirroring @deepseek-ai/dsh-shell/render. */
161
+ export declare function parseExitStatus(text: string): {
162
+ body: string;
163
+ exitCode?: number;
164
+ signal?: string;
165
+ };
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Shared transcript row shapes used by plan / tool presentation helpers.
3
+ * Kept free of the SshTui class so leaf modules can import them.
4
+ */
5
+ export type DisconnectPolicyName = 'pause' | 'continue';
6
+ export type SubagentLogKind = 'user' | 'assistant' | 'tool' | 'result' | 'turn' | 'approval' | 'team' | 'system';
7
+ /** One child-session event folded into a parent-side subagent card. */
8
+ export interface SubagentLogEntry {
9
+ kind: SubagentLogKind;
10
+ text: string;
11
+ }
12
+ /** One todo-list item as the plan card renders it. */
13
+ export interface PlanTodoItem {
14
+ content: string;
15
+ status: 'pending' | 'in_progress' | 'completed';
16
+ }
17
+ export type Row = {
18
+ kind: 'user';
19
+ text: string;
20
+ } | {
21
+ kind: 'assistant';
22
+ text: string;
23
+ } | {
24
+ kind: 'reasoning';
25
+ text: string;
26
+ expanded: boolean;
27
+ } | {
28
+ kind: 'brand';
29
+ text: string;
30
+ } | {
31
+ kind: 'brand-logo';
32
+ } | {
33
+ kind: 'tool';
34
+ callId: string;
35
+ name: string;
36
+ args: string;
37
+ status?: 'running' | 'ok' | 'error';
38
+ output: string;
39
+ title: string;
40
+ summary: string;
41
+ command?: string;
42
+ cwd?: string;
43
+ diff?: ToolDiffHunk[];
44
+ exitCode?: number;
45
+ signal?: string;
46
+ expanded: boolean;
47
+ /** Consecutive same-path reads/edits folded into this card. */
48
+ repeats?: number;
49
+ /** Call ids folded into this card; results still match after merge. */
50
+ mergedCallIds?: string[];
51
+ /** Sum of output characters across folded reads. */
52
+ totalChars?: number;
53
+ /** Sum of output lines across folded reads. */
54
+ totalLines?: number;
55
+ /** Flip-card animation until this timestamp (ms since epoch). */
56
+ flipUntil?: number;
57
+ } | {
58
+ kind: 'subagent';
59
+ sessionId: string;
60
+ runId: string;
61
+ provider: string;
62
+ local: boolean;
63
+ label: string;
64
+ status: 'running' | 'ok' | 'error' | 'aborted';
65
+ startedAt: number;
66
+ endedAt?: number;
67
+ stopReason?: string;
68
+ lastActivity: string;
69
+ logs: SubagentLogEntry[];
70
+ expanded: boolean;
71
+ } | {
72
+ kind: 'plan';
73
+ active: boolean;
74
+ pending: boolean;
75
+ todos: PlanTodoItem[];
76
+ planMarkdown?: string;
77
+ expanded: boolean;
78
+ /** When true the plan stays in the scrolling transcript, not the dock. */
79
+ archived?: boolean;
80
+ /**
81
+ * Display-only: the last turn ended while todos were still open.
82
+ * Does not rewrite the session log.
83
+ */
84
+ turnLeftOpen?: boolean;
85
+ } | {
86
+ kind: 'question';
87
+ questionId: string;
88
+ title: string;
89
+ header?: string;
90
+ detail?: string;
91
+ intent: 'ask' | 'plan-review';
92
+ status: 'waiting' | 'answered' | 'cancelled';
93
+ summary: string;
94
+ expanded: boolean;
95
+ } | {
96
+ kind: 'goal';
97
+ objective: string;
98
+ phase: 'active' | 'paused' | 'blocked' | 'complete' | 'cleared';
99
+ blockedReason?: string;
100
+ expanded: boolean;
101
+ } | {
102
+ kind: 'compaction';
103
+ compactionId: string;
104
+ status: 'running' | 'ok' | 'error';
105
+ startedAt: number;
106
+ endedAt?: number;
107
+ pruneCount: number;
108
+ prunedTokens: number;
109
+ summary?: string;
110
+ error?: string;
111
+ expanded: boolean;
112
+ } | {
113
+ kind: 'prompt';
114
+ sources: string[];
115
+ text: string;
116
+ plugin?: string;
117
+ expanded: boolean;
118
+ } | {
119
+ kind: 'system';
120
+ text: string;
121
+ } | {
122
+ kind: 'error';
123
+ text: string;
124
+ };
125
+ /** A reasoning/tool/subagent/plan row or the live streaming-reasoning block. */
126
+ export type CollapsibleBlock = Extract<Row, {
127
+ kind: 'reasoning';
128
+ } | {
129
+ kind: 'tool';
130
+ } | {
131
+ kind: 'subagent';
132
+ } | {
133
+ kind: 'plan';
134
+ } | {
135
+ kind: 'question';
136
+ } | {
137
+ kind: 'goal';
138
+ } | {
139
+ kind: 'compaction';
140
+ } | {
141
+ kind: 'prompt';
142
+ }> | {
143
+ kind: 'streaming-reasoning';
144
+ expanded: boolean;
145
+ };
146
+ export type DisplayKind = Row['kind'] | 'tool-result' | 'diff-add' | 'diff-del' | 'diff-path' | 'todo-done' | 'todo-active' | 'todo-pending' | 'plan-dock';
147
+ /** One file's change, matching the web diff-card contract (`card: 'diff'`). */
148
+ export interface ToolDiffHunk {
149
+ path: string;
150
+ oldText: string | null;
151
+ newText: string;
152
+ }