@bubblebrain-ai/bubble 0.0.7 → 0.0.9

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 (119) hide show
  1. package/dist/agent/categories.d.ts +34 -0
  2. package/dist/agent/categories.js +98 -0
  3. package/dist/agent/profiles.d.ts +4 -0
  4. package/dist/agent/profiles.js +2 -3
  5. package/dist/agent/subagent-control.d.ts +5 -0
  6. package/dist/agent/subagent-control.js +4 -0
  7. package/dist/agent/subagent-lifecycle-reminder.d.ts +3 -0
  8. package/dist/agent/subagent-lifecycle-reminder.js +102 -0
  9. package/dist/agent/subagent-route-format.d.ts +8 -0
  10. package/dist/agent/subagent-route-format.js +18 -0
  11. package/dist/agent/subtask-policy.d.ts +0 -1
  12. package/dist/agent/subtask-policy.js +0 -4
  13. package/dist/agent.d.ts +18 -0
  14. package/dist/agent.js +188 -16
  15. package/dist/config.d.ts +23 -3
  16. package/dist/config.js +59 -6
  17. package/dist/context/budget.d.ts +3 -2
  18. package/dist/context/budget.js +29 -15
  19. package/dist/context/compact.d.ts +23 -0
  20. package/dist/context/compact.js +129 -0
  21. package/dist/context/llm-compactor.d.ts +19 -0
  22. package/dist/context/llm-compactor.js +200 -0
  23. package/dist/context/projector.js +28 -12
  24. package/dist/context/token-estimator.d.ts +14 -0
  25. package/dist/context/token-estimator.js +106 -0
  26. package/dist/context/tool-output-truncate.d.ts +8 -0
  27. package/dist/context/tool-output-truncate.js +59 -0
  28. package/dist/context/usage.d.ts +34 -0
  29. package/dist/context/usage.js +213 -0
  30. package/dist/diff-stats.d.ts +5 -0
  31. package/dist/diff-stats.js +21 -0
  32. package/dist/main.js +68 -7
  33. package/dist/mcp/transports.d.ts +1 -0
  34. package/dist/mcp/transports.js +8 -0
  35. package/dist/model-catalog.d.ts +9 -0
  36. package/dist/model-catalog.js +17 -1
  37. package/dist/orchestrator/default-hooks.js +24 -18
  38. package/dist/prompt/compose.js +2 -1
  39. package/dist/prompt/provider-prompts/kimi.js +3 -1
  40. package/dist/provider-openai-codex.d.ts +13 -2
  41. package/dist/provider-openai-codex.js +81 -32
  42. package/dist/provider-registry.js +22 -6
  43. package/dist/provider-transform.d.ts +3 -1
  44. package/dist/provider-transform.js +15 -0
  45. package/dist/provider.d.ts +4 -1
  46. package/dist/provider.js +89 -4
  47. package/dist/reasoning-debug.d.ts +7 -0
  48. package/dist/reasoning-debug.js +30 -0
  49. package/dist/session-log.js +13 -2
  50. package/dist/session-types.d.ts +1 -1
  51. package/dist/slash-commands/commands.js +60 -2
  52. package/dist/slash-commands/types.d.ts +7 -0
  53. package/dist/tools/agent-lifecycle.js +22 -4
  54. package/dist/tools/edit.js +7 -2
  55. package/dist/tools/file-state.d.ts +19 -0
  56. package/dist/tools/file-state.js +15 -0
  57. package/dist/tools/glob.js +2 -1
  58. package/dist/tools/grep.js +2 -2
  59. package/dist/tools/lsp.js +2 -2
  60. package/dist/tools/path-utils.d.ts +2 -0
  61. package/dist/tools/path-utils.js +16 -0
  62. package/dist/tools/read.d.ts +1 -1
  63. package/dist/tools/read.js +207 -14
  64. package/dist/tools/write.js +3 -2
  65. package/dist/tui/escape-confirmation.d.ts +15 -0
  66. package/dist/tui/escape-confirmation.js +30 -0
  67. package/dist/tui/run.js +93 -23
  68. package/dist/tui-ink/app.d.ts +52 -0
  69. package/dist/tui-ink/app.js +1129 -0
  70. package/dist/tui-ink/approval/approval-dialog.d.ts +13 -0
  71. package/dist/tui-ink/approval/approval-dialog.js +132 -0
  72. package/dist/tui-ink/approval/diff-view.d.ts +7 -0
  73. package/dist/tui-ink/approval/diff-view.js +44 -0
  74. package/dist/tui-ink/approval/select.d.ts +35 -0
  75. package/dist/tui-ink/approval/select.js +88 -0
  76. package/dist/tui-ink/code-highlight.d.ts +8 -0
  77. package/dist/tui-ink/code-highlight.js +122 -0
  78. package/dist/tui-ink/detect-theme.d.ts +19 -0
  79. package/dist/tui-ink/detect-theme.js +123 -0
  80. package/dist/tui-ink/display-history.d.ts +38 -0
  81. package/dist/tui-ink/display-history.js +130 -0
  82. package/dist/tui-ink/edit-diff.d.ts +11 -0
  83. package/dist/tui-ink/edit-diff.js +52 -0
  84. package/dist/tui-ink/file-mentions.d.ts +29 -0
  85. package/dist/tui-ink/file-mentions.js +174 -0
  86. package/dist/tui-ink/footer.d.ts +19 -0
  87. package/dist/tui-ink/footer.js +45 -0
  88. package/dist/tui-ink/image-paste.d.ts +54 -0
  89. package/dist/tui-ink/image-paste.js +288 -0
  90. package/dist/tui-ink/input-box.d.ts +41 -0
  91. package/dist/tui-ink/input-box.js +694 -0
  92. package/dist/tui-ink/input-history.d.ts +16 -0
  93. package/dist/tui-ink/input-history.js +81 -0
  94. package/dist/tui-ink/markdown.d.ts +38 -0
  95. package/dist/tui-ink/markdown.js +394 -0
  96. package/dist/tui-ink/message-list.d.ts +33 -0
  97. package/dist/tui-ink/message-list.js +667 -0
  98. package/dist/tui-ink/model-picker.d.ts +43 -0
  99. package/dist/tui-ink/model-picker.js +331 -0
  100. package/dist/tui-ink/plan-confirm.d.ts +7 -0
  101. package/dist/tui-ink/plan-confirm.js +105 -0
  102. package/dist/tui-ink/question-dialog.d.ts +8 -0
  103. package/dist/tui-ink/question-dialog.js +99 -0
  104. package/dist/tui-ink/recent-activity.d.ts +8 -0
  105. package/dist/tui-ink/recent-activity.js +71 -0
  106. package/dist/tui-ink/run.d.ts +37 -0
  107. package/dist/tui-ink/run.js +53 -0
  108. package/dist/tui-ink/theme.d.ts +66 -0
  109. package/dist/tui-ink/theme.js +115 -0
  110. package/dist/tui-ink/todos.d.ts +7 -0
  111. package/dist/tui-ink/todos.js +46 -0
  112. package/dist/tui-ink/trace-groups.d.ts +27 -0
  113. package/dist/tui-ink/trace-groups.js +389 -0
  114. package/dist/tui-ink/use-terminal-size.d.ts +4 -0
  115. package/dist/tui-ink/use-terminal-size.js +21 -0
  116. package/dist/tui-ink/welcome.d.ts +18 -0
  117. package/dist/tui-ink/welcome.js +138 -0
  118. package/dist/types.d.ts +10 -0
  119. package/package.json +7 -1
@@ -0,0 +1,16 @@
1
+ export declare function defaultHistoryFilePath(): string;
2
+ export declare function loadHistorySync(filePath?: string): string[];
3
+ export declare function appendHistoryEntry(entry: string, filePath?: string): void;
4
+ export interface HistoryNavState {
5
+ history: string[];
6
+ index: number | null;
7
+ draft: string;
8
+ }
9
+ export interface HistoryNavResult {
10
+ text: string;
11
+ index: number | null;
12
+ draft: string;
13
+ changed: boolean;
14
+ }
15
+ export declare function stepHistory(state: HistoryNavState, direction: "up" | "down", currentText: string): HistoryNavResult;
16
+ export declare function pushHistoryEntry(history: string[], entry: string): string[];
@@ -0,0 +1,81 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { getBubbleHome } from "../bubble-home.js";
4
+ const MAX_HISTORY_ENTRIES = 1000;
5
+ export function defaultHistoryFilePath() {
6
+ return join(getBubbleHome(), "input-history.jsonl");
7
+ }
8
+ // JSONL on disk: each line is a JSON-encoded string. JSON encoding handles
9
+ // embedded newlines and quotes so multi-line composer entries round-trip safely.
10
+ export function loadHistorySync(filePath = defaultHistoryFilePath()) {
11
+ try {
12
+ if (!existsSync(filePath))
13
+ return [];
14
+ const raw = readFileSync(filePath, "utf8");
15
+ const out = [];
16
+ for (const line of raw.split("\n")) {
17
+ if (!line)
18
+ continue;
19
+ try {
20
+ const parsed = JSON.parse(line);
21
+ if (typeof parsed === "string" && parsed.length > 0)
22
+ out.push(parsed);
23
+ }
24
+ catch {
25
+ // Malformed line — skip rather than fail the whole load.
26
+ }
27
+ }
28
+ return out.length > MAX_HISTORY_ENTRIES ? out.slice(-MAX_HISTORY_ENTRIES) : out;
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ export function appendHistoryEntry(entry, filePath = defaultHistoryFilePath()) {
35
+ if (!entry || entry.trim().length === 0)
36
+ return;
37
+ try {
38
+ mkdirSync(dirname(filePath), { recursive: true });
39
+ appendFileSync(filePath, JSON.stringify(entry) + "\n", "utf8");
40
+ }
41
+ catch {
42
+ // Persistence is best-effort; never crash the composer over disk IO.
43
+ }
44
+ }
45
+ // Pure transition for ↑/↓ navigation. `index === null` means the user is
46
+ // editing a fresh draft; otherwise it points at history[index]. When stepping
47
+ // from the draft into history we snapshot the current text so ↓ past the
48
+ // newest entry can restore it.
49
+ export function stepHistory(state, direction, currentText) {
50
+ const { history, index, draft } = state;
51
+ const noChange = { text: currentText, index, draft, changed: false };
52
+ if (direction === "up") {
53
+ if (history.length === 0)
54
+ return noChange;
55
+ if (index === null) {
56
+ const newIdx = history.length - 1;
57
+ return { text: history[newIdx], index: newIdx, draft: currentText, changed: true };
58
+ }
59
+ if (index > 0) {
60
+ return { text: history[index - 1], index: index - 1, draft, changed: true };
61
+ }
62
+ return noChange;
63
+ }
64
+ // down
65
+ if (index === null)
66
+ return noChange;
67
+ if (index < history.length - 1) {
68
+ return { text: history[index + 1], index: index + 1, draft, changed: true };
69
+ }
70
+ // Past the newest entry: restore the saved draft and clear it.
71
+ return { text: draft, index: null, draft: "", changed: true };
72
+ }
73
+ // Push to in-memory history with last-entry dedupe so repeated identical
74
+ // submissions don't spam the stack.
75
+ export function pushHistoryEntry(history, entry) {
76
+ if (!entry || entry.trim().length === 0)
77
+ return history;
78
+ if (history.length > 0 && history[history.length - 1] === entry)
79
+ return history;
80
+ return [...history, entry];
81
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Lightweight Markdown renderer for Ink TUI.
3
+ * Supports code blocks, inline formatting, and tables.
4
+ */
5
+ export type MarkdownBlock = {
6
+ type: "paragraph";
7
+ lines: string[];
8
+ } | {
9
+ type: "heading";
10
+ level: number;
11
+ text: string;
12
+ } | {
13
+ type: "code";
14
+ lang: string;
15
+ lines: string[];
16
+ } | {
17
+ type: "table";
18
+ headers: string[];
19
+ rows: string[][];
20
+ };
21
+ export interface MarkdownInlineSegment {
22
+ text: string;
23
+ bold?: boolean;
24
+ italic?: boolean;
25
+ code?: boolean;
26
+ }
27
+ interface InlineStyle {
28
+ bold?: boolean;
29
+ italic?: boolean;
30
+ code?: boolean;
31
+ }
32
+ export declare function parseMarkdownBlocks(text: string): MarkdownBlock[];
33
+ export declare function parseMarkdownInlineSegments(text: string, style?: InlineStyle): MarkdownInlineSegment[];
34
+ export declare function MarkdownContent({ content, maxWidth, }: {
35
+ content: string;
36
+ maxWidth?: number;
37
+ }): import("react/jsx-runtime").JSX.Element;
38
+ export {};
@@ -0,0 +1,394 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Lightweight Markdown renderer for Ink TUI.
4
+ * Supports code blocks, inline formatting, and tables.
5
+ */
6
+ import React from "react";
7
+ import { Box, Text } from "ink";
8
+ import stringWidth from "string-width";
9
+ import { useTerminalSize } from "./use-terminal-size.js";
10
+ import { useTheme } from "./theme.js";
11
+ import { highlightCode, highlightCodeSync } from "./code-highlight.js";
12
+ const graphemeSegmenter = typeof Intl !== "undefined" && typeof Intl.Segmenter === "function"
13
+ ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
14
+ : null;
15
+ function splitGraphemes(text) {
16
+ if (!text)
17
+ return [];
18
+ if (graphemeSegmenter) {
19
+ const out = [];
20
+ for (const { segment } of graphemeSegmenter.segment(text))
21
+ out.push(segment);
22
+ return out;
23
+ }
24
+ return Array.from(text);
25
+ }
26
+ export function parseMarkdownBlocks(text) {
27
+ const lines = text.split("\n");
28
+ const blocks = [];
29
+ let i = 0;
30
+ while (i < lines.length) {
31
+ const line = lines[i];
32
+ // Code block
33
+ if (line.startsWith("```")) {
34
+ const lang = line.slice(3).trim();
35
+ i++;
36
+ const codeLines = [];
37
+ while (i < lines.length && !lines[i].startsWith("```")) {
38
+ codeLines.push(lines[i]);
39
+ i++;
40
+ }
41
+ blocks.push({ type: "code", lang, lines: codeLines });
42
+ i++;
43
+ continue;
44
+ }
45
+ // Table
46
+ if (line.trim().startsWith("|")) {
47
+ const tableLines = [];
48
+ while (i < lines.length && lines[i].trim().startsWith("|")) {
49
+ tableLines.push(lines[i]);
50
+ i++;
51
+ }
52
+ if (tableLines.length >= 2) {
53
+ const headers = parseTableRow(tableLines[0]);
54
+ if (headers.length > 0 && isTableSeparatorRow(tableLines[1], headers.length)) {
55
+ const rows = tableLines.slice(2).map((rowLine) => normalizeTableRow(parseTableRow(rowLine), headers.length));
56
+ blocks.push({ type: "table", headers, rows });
57
+ }
58
+ else {
59
+ blocks.push({ type: "paragraph", lines: tableLines });
60
+ }
61
+ }
62
+ else {
63
+ blocks.push({ type: "paragraph", lines: tableLines });
64
+ }
65
+ continue;
66
+ }
67
+ // Heading
68
+ const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
69
+ if (headingMatch) {
70
+ blocks.push({ type: "heading", level: headingMatch[1].length, text: headingMatch[2].trim() });
71
+ i++;
72
+ continue;
73
+ }
74
+ // Empty line -> skip
75
+ if (line.trim() === "") {
76
+ i++;
77
+ continue;
78
+ }
79
+ // Paragraph
80
+ const paraLines = [];
81
+ while (i < lines.length &&
82
+ lines[i].trim() !== "" &&
83
+ !lines[i].startsWith("```") &&
84
+ !lines[i].trim().startsWith("|")) {
85
+ paraLines.push(lines[i]);
86
+ i++;
87
+ }
88
+ blocks.push({ type: "paragraph", lines: paraLines });
89
+ }
90
+ return blocks;
91
+ }
92
+ function parseTableRow(line) {
93
+ let body = line.trim();
94
+ if (body.startsWith("|"))
95
+ body = body.slice(1);
96
+ if (endsWithUnescapedPipe(body))
97
+ body = body.slice(0, -1);
98
+ const cells = [];
99
+ let current = "";
100
+ let inCode = false;
101
+ for (let i = 0; i < body.length; i++) {
102
+ const char = body[i];
103
+ if (char === "\\" && i + 1 < body.length) {
104
+ current += body[i + 1];
105
+ i++;
106
+ continue;
107
+ }
108
+ if (char === "`") {
109
+ inCode = !inCode;
110
+ current += char;
111
+ continue;
112
+ }
113
+ if (char === "|" && !inCode) {
114
+ cells.push(current.trim());
115
+ current = "";
116
+ continue;
117
+ }
118
+ current += char;
119
+ }
120
+ cells.push(current.trim());
121
+ return cells;
122
+ }
123
+ function endsWithUnescapedPipe(text) {
124
+ if (!text.endsWith("|"))
125
+ return false;
126
+ let slashCount = 0;
127
+ for (let i = text.length - 2; i >= 0 && text[i] === "\\"; i--)
128
+ slashCount++;
129
+ return slashCount % 2 === 0;
130
+ }
131
+ function isTableSeparatorRow(line, expectedColumns) {
132
+ const cells = parseTableRow(line);
133
+ if (cells.length !== expectedColumns)
134
+ return false;
135
+ return cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\s+/g, "")));
136
+ }
137
+ function normalizeTableRow(row, colCount) {
138
+ const normalized = row.slice(0, colCount);
139
+ while (normalized.length < colCount)
140
+ normalized.push("");
141
+ return normalized;
142
+ }
143
+ function visualWidth(str) {
144
+ if (!str)
145
+ return 0;
146
+ return stringWidth(str);
147
+ }
148
+ function graphemeWidth(grapheme) {
149
+ if (!grapheme)
150
+ return 0;
151
+ return stringWidth(grapheme);
152
+ }
153
+ // Inline formatting: bold, italic, inline code
154
+ export function parseMarkdownInlineSegments(text, style = {}) {
155
+ const segments = [];
156
+ let buffer = "";
157
+ let i = 0;
158
+ const flush = () => {
159
+ appendInlineSegment(segments, buffer, style);
160
+ buffer = "";
161
+ };
162
+ while (i < text.length) {
163
+ const char = text[i];
164
+ if (char === "\\" && i + 1 < text.length) {
165
+ buffer += text[i + 1];
166
+ i += 2;
167
+ continue;
168
+ }
169
+ if (char === "`") {
170
+ const close = findClosingMarker(text, "`", i + 1);
171
+ if (close !== -1) {
172
+ flush();
173
+ appendInlineSegment(segments, text.slice(i + 1, close), { ...style, code: true });
174
+ i = close + 1;
175
+ continue;
176
+ }
177
+ }
178
+ const marker = inlineMarkerAt(text, i);
179
+ if (marker) {
180
+ const close = findClosingMarker(text, marker, i + marker.length);
181
+ if (close !== -1 && close > i + marker.length) {
182
+ flush();
183
+ const inner = text.slice(i + marker.length, close);
184
+ const nextStyle = marker === "***"
185
+ ? { ...style, bold: true, italic: true }
186
+ : marker === "**" || marker === "__"
187
+ ? { ...style, bold: true }
188
+ : { ...style, italic: true };
189
+ for (const segment of parseMarkdownInlineSegments(inner, nextStyle)) {
190
+ appendInlineSegment(segments, segment.text, segment);
191
+ }
192
+ i = close + marker.length;
193
+ continue;
194
+ }
195
+ }
196
+ buffer += char;
197
+ i++;
198
+ }
199
+ flush();
200
+ return segments.length > 0 ? segments : [{ text, ...style }];
201
+ }
202
+ function inlineMarkerAt(text, index) {
203
+ for (const marker of ["***", "**", "__", "*", "_"]) {
204
+ if (!text.startsWith(marker, index))
205
+ continue;
206
+ if (marker.includes("_") && isIntraWordUnderscore(text, index, marker.length))
207
+ continue;
208
+ return marker;
209
+ }
210
+ return null;
211
+ }
212
+ function findClosingMarker(text, marker, start) {
213
+ for (let i = start; i <= text.length - marker.length; i++) {
214
+ if (text[i] === "\\") {
215
+ i++;
216
+ continue;
217
+ }
218
+ if (!text.startsWith(marker, i))
219
+ continue;
220
+ if (marker.includes("_") && isIntraWordUnderscore(text, i, marker.length))
221
+ continue;
222
+ return i;
223
+ }
224
+ return -1;
225
+ }
226
+ function isIntraWordUnderscore(text, index, markerLength) {
227
+ const before = text[index - 1];
228
+ const after = text[index + markerLength];
229
+ return isWordChar(before) && isWordChar(after);
230
+ }
231
+ function isWordChar(char) {
232
+ return !!char && /[A-Za-z0-9]/.test(char);
233
+ }
234
+ function appendInlineSegment(segments, text, style) {
235
+ if (!text)
236
+ return;
237
+ const previous = segments[segments.length - 1];
238
+ const next = {
239
+ text,
240
+ bold: style.bold || undefined,
241
+ italic: style.italic || undefined,
242
+ code: style.code || undefined,
243
+ };
244
+ if (previous &&
245
+ previous.bold === next.bold &&
246
+ previous.italic === next.italic &&
247
+ previous.code === next.code) {
248
+ previous.text += text;
249
+ }
250
+ else {
251
+ segments.push(next);
252
+ }
253
+ }
254
+ function renderInlineSegments(text, keyPrefix, style = {}) {
255
+ return parseMarkdownInlineSegments(text, style).map((segment, index) => (_jsx(Text, { bold: segment.bold, italic: segment.italic, color: segment.code ? "#a78bfa" : undefined, children: segment.text }, `${keyPrefix}-${index}`)));
256
+ }
257
+ function inlinePlainText(text) {
258
+ return parseMarkdownInlineSegments(text).map((segment) => segment.text).join("");
259
+ }
260
+ function InlineText({ text }) {
261
+ return _jsx(Text, { children: renderInlineSegments(text, "inline") });
262
+ }
263
+ function CodeBlock({ lang, lines }) {
264
+ const theme = useTheme();
265
+ // Lazy init: try sync highlight when shiki is already warm so the very first
266
+ // paint carries highlighted output. This matters because MessageList renders
267
+ // committed messages inside Ink's <Static>, which only paints each item once
268
+ // — anything we ship via setState in useEffect lands too late to appear in
269
+ // scrollback. Fall back to raw lines if shiki hasn't loaded yet.
270
+ const [highlighted, setHighlighted] = React.useState(() => {
271
+ const code = lines.join("\n");
272
+ if (!code)
273
+ return lines;
274
+ const sync = highlightCodeSync(code, lang || "text");
275
+ return sync ? sync.split("\n") : lines;
276
+ });
277
+ const upgraded = React.useRef(highlighted !== lines);
278
+ React.useEffect(() => {
279
+ if (upgraded.current)
280
+ return;
281
+ let cancelled = false;
282
+ const code = lines.join("\n");
283
+ if (!code)
284
+ return;
285
+ highlightCode(code, lang || "text")
286
+ .then((ansi) => {
287
+ if (cancelled)
288
+ return;
289
+ upgraded.current = true;
290
+ setHighlighted(ansi.split("\n"));
291
+ })
292
+ .catch(() => { });
293
+ return () => {
294
+ cancelled = true;
295
+ };
296
+ }, [lang, lines]);
297
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [lang && _jsx(Text, { color: theme.muted, children: lang }), _jsx(Box, { flexDirection: "column", children: highlighted.map((line, i) => (_jsx(Text, { children: line || " " }, i))) })] }));
298
+ }
299
+ function TableBlock({ headers, rows, maxWidth, }) {
300
+ const { columns: termWidth } = useTerminalSize();
301
+ const colCount = headers.length;
302
+ // Reserve a buffer so the table fits even when wrapped inside an indented
303
+ // box (e.g. the timeline gutter contributes marginLeft + "⛬ " = 5 cells).
304
+ const budget = Math.max(20, (maxWidth ?? termWidth) - 8);
305
+ const maxWidths = headers.map((h, i) => {
306
+ let max = visualWidth(inlinePlainText(h));
307
+ for (const row of rows) {
308
+ const cell = row[i] || "";
309
+ max = Math.max(max, visualWidth(inlinePlainText(cell)));
310
+ }
311
+ return max;
312
+ });
313
+ const totalInnerWidth = maxWidths.reduce((a, b) => a + b, 0);
314
+ const separatorsWidth = colCount * 3 + 1; // " │ " separators + outer edges
315
+ const totalWidth = totalInnerWidth + separatorsWidth;
316
+ let widths = [...maxWidths];
317
+ if (totalWidth > budget) {
318
+ const available = Math.max(budget - separatorsWidth, colCount * 4);
319
+ const ratio = totalInnerWidth > 0 ? available / totalInnerWidth : 1;
320
+ widths = maxWidths.map((w) => Math.max(4, Math.floor(w * ratio)));
321
+ }
322
+ const top = "┌" + widths.map((w) => "─".repeat(w + 2)).join("┬") + "┐";
323
+ const mid = "├" + widths.map((w) => "─".repeat(w + 2)).join("┼") + "┤";
324
+ const bot = "└" + widths.map((w) => "─".repeat(w + 2)).join("┴") + "┘";
325
+ const renderRow = (cells, keyPrefix, isHeader = false) => (_jsxs(Text, { children: ["│ ", cells.map((c, i) => (_jsxs(React.Fragment, { children: [renderTableCell(c, widths[i] ?? 4, isHeader, `${keyPrefix}-cell-${i}`), i < colCount - 1 ? " │ " : " │"] }, i)))] }, keyPrefix));
326
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: top }), renderRow(headers, "header", true), _jsx(Text, { children: mid }), rows.map((row, ri) => renderRow(row, `row-${ri}`)), _jsx(Text, { children: bot })] }));
327
+ }
328
+ function renderTableCell(cell, width, isHeader, keyPrefix) {
329
+ const segments = truncateInlineSegments(parseMarkdownInlineSegments(cell, { bold: isHeader }), width);
330
+ const padding = " ".repeat(Math.max(0, width - inlineSegmentsWidth(segments)));
331
+ return [
332
+ ...segments.map((segment, index) => (_jsx(Text, { bold: segment.bold, italic: segment.italic, color: segment.code ? "#a78bfa" : undefined, children: segment.text }, `${keyPrefix}-${index}`))),
333
+ _jsx(Text, { children: padding }, `${keyPrefix}-pad`),
334
+ ];
335
+ }
336
+ function truncateInlineSegments(segments, width) {
337
+ if (inlineSegmentsWidth(segments) <= width)
338
+ return segments;
339
+ if (width <= 1)
340
+ return [{ text: "…" }];
341
+ const target = width - 1;
342
+ const output = [];
343
+ let used = 0;
344
+ for (const segment of segments) {
345
+ let text = "";
346
+ for (const grapheme of splitGraphemes(segment.text)) {
347
+ const gWidth = graphemeWidth(grapheme);
348
+ if (used + gWidth > target) {
349
+ if (text)
350
+ appendInlineSegment(output, text, segment);
351
+ appendInlineSegment(output, "…", {});
352
+ return output;
353
+ }
354
+ text += grapheme;
355
+ used += gWidth;
356
+ }
357
+ appendInlineSegment(output, text, segment);
358
+ }
359
+ appendInlineSegment(output, "…", {});
360
+ return output;
361
+ }
362
+ function inlineSegmentsWidth(segments) {
363
+ return segments.reduce((sum, segment) => sum + visualWidth(segment.text), 0);
364
+ }
365
+ function HeadingBlock({ level, text }) {
366
+ const theme = useTheme();
367
+ const props = { bold: true };
368
+ if (level === 1) {
369
+ props.underline = true;
370
+ props.color = theme.accent;
371
+ }
372
+ else if (level === 2) {
373
+ props.color = theme.accent;
374
+ }
375
+ else if (level === 3) {
376
+ props.color = theme.warning;
377
+ }
378
+ return (_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { ...props, children: text }) }));
379
+ }
380
+ export function MarkdownContent({ content, maxWidth, }) {
381
+ const blocks = React.useMemo(() => parseMarkdownBlocks(content), [content]);
382
+ return (_jsx(Box, { flexDirection: "column", children: blocks.map((block, i) => {
383
+ if (block.type === "code") {
384
+ return _jsx(CodeBlock, { lang: block.lang, lines: block.lines }, i);
385
+ }
386
+ if (block.type === "table") {
387
+ return (_jsx(TableBlock, { headers: block.headers, rows: block.rows, maxWidth: maxWidth }, i));
388
+ }
389
+ if (block.type === "heading") {
390
+ return _jsx(HeadingBlock, { level: block.level, text: block.text }, i);
391
+ }
392
+ return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: block.lines.map((line, li) => (_jsx(InlineText, { text: line }, li))) }, i));
393
+ }) }));
394
+ }
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import type { DisplayMessage, DisplayMessagePart, DisplayToolCall } from "./display-history.js";
3
+ /**
4
+ * Hint surfaced when the user can interrupt the currently-running pending tool
5
+ * via the approval dialog. The match is loose (by request type → tool name),
6
+ * since ApprovalRequest does not carry a toolCallId today.
7
+ */
8
+ export interface PendingApprovalHint {
9
+ toolName: "edit" | "write" | "bash";
10
+ path?: string;
11
+ command?: string;
12
+ }
13
+ interface MessageListProps {
14
+ messages: DisplayMessage[];
15
+ streamingContent: string;
16
+ streamingReasoning: string;
17
+ streamingTools: DisplayToolCall[];
18
+ streamingParts: DisplayMessagePart[];
19
+ terminalColumns: number;
20
+ verboseTrace: boolean;
21
+ pendingApproval?: PendingApprovalHint | null;
22
+ /** Animation tick used to refresh in-progress elapsed counters. */
23
+ nowTick?: number;
24
+ /**
25
+ * Optional banner rendered as the first item of the scrollback Static
26
+ * stream. Committed to scrollback once on initial mount so it doesn't
27
+ * float between older messages and the live tail as the conversation
28
+ * progresses.
29
+ */
30
+ welcomeBanner?: React.ReactNode;
31
+ }
32
+ export declare function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, verboseTrace, pendingApproval, nowTick, welcomeBanner, }: MessageListProps): import("react/jsx-runtime").JSX.Element;
33
+ export {};