@bubblebrain-ai/bubble 0.0.6 → 0.0.8

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 (85) hide show
  1. package/dist/agent/execution-governor.d.ts +5 -13
  2. package/dist/agent/execution-governor.js +33 -142
  3. package/dist/agent.d.ts +6 -0
  4. package/dist/agent.js +36 -3
  5. package/dist/context/budget.d.ts +1 -0
  6. package/dist/context/budget.js +1 -1
  7. package/dist/context/usage.d.ts +34 -0
  8. package/dist/context/usage.js +213 -0
  9. package/dist/diff-stats.d.ts +5 -0
  10. package/dist/diff-stats.js +21 -0
  11. package/dist/main.js +83 -44
  12. package/dist/mcp/transports.d.ts +1 -0
  13. package/dist/mcp/transports.js +8 -0
  14. package/dist/model-catalog.js +1 -1
  15. package/dist/orchestrator/default-hooks.js +9 -33
  16. package/dist/prompt/compose.js +2 -1
  17. package/dist/prompt/provider-prompts/kimi.js +3 -1
  18. package/dist/prompt/reminders.d.ts +2 -1
  19. package/dist/prompt/reminders.js +4 -3
  20. package/dist/provider-registry.js +3 -3
  21. package/dist/provider-transform.d.ts +3 -1
  22. package/dist/provider-transform.js +15 -0
  23. package/dist/provider.d.ts +4 -1
  24. package/dist/provider.js +89 -4
  25. package/dist/reasoning-debug.d.ts +7 -0
  26. package/dist/reasoning-debug.js +30 -0
  27. package/dist/session-log.js +13 -2
  28. package/dist/session-types.d.ts +1 -1
  29. package/dist/slash-commands/commands.js +36 -19
  30. package/dist/tools/edit.js +5 -0
  31. package/dist/tools/file-state.d.ts +19 -0
  32. package/dist/tools/file-state.js +15 -0
  33. package/dist/tools/read.d.ts +1 -1
  34. package/dist/tools/read.js +92 -11
  35. package/dist/tui/escape-confirmation.d.ts +15 -0
  36. package/dist/tui/escape-confirmation.js +30 -0
  37. package/dist/tui/run.js +93 -23
  38. package/dist/tui-ink/app.d.ts +43 -0
  39. package/dist/tui-ink/app.js +1016 -0
  40. package/dist/tui-ink/approval/approval-dialog.d.ts +13 -0
  41. package/dist/tui-ink/approval/approval-dialog.js +129 -0
  42. package/dist/tui-ink/approval/diff-view.d.ts +7 -0
  43. package/dist/tui-ink/approval/diff-view.js +43 -0
  44. package/dist/tui-ink/approval/select.d.ts +35 -0
  45. package/dist/tui-ink/approval/select.js +87 -0
  46. package/dist/tui-ink/code-highlight.d.ts +6 -0
  47. package/dist/tui-ink/code-highlight.js +94 -0
  48. package/dist/tui-ink/display-history.d.ts +38 -0
  49. package/dist/tui-ink/display-history.js +130 -0
  50. package/dist/tui-ink/edit-diff.d.ts +11 -0
  51. package/dist/tui-ink/edit-diff.js +52 -0
  52. package/dist/tui-ink/file-mentions.d.ts +29 -0
  53. package/dist/tui-ink/file-mentions.js +174 -0
  54. package/dist/tui-ink/footer.d.ts +19 -0
  55. package/dist/tui-ink/footer.js +44 -0
  56. package/dist/tui-ink/image-paste.d.ts +54 -0
  57. package/dist/tui-ink/image-paste.js +288 -0
  58. package/dist/tui-ink/input-box.d.ts +41 -0
  59. package/dist/tui-ink/input-box.js +637 -0
  60. package/dist/tui-ink/markdown.d.ts +38 -0
  61. package/dist/tui-ink/markdown.js +384 -0
  62. package/dist/tui-ink/message-list.d.ts +33 -0
  63. package/dist/tui-ink/message-list.js +571 -0
  64. package/dist/tui-ink/model-picker.d.ts +43 -0
  65. package/dist/tui-ink/model-picker.js +326 -0
  66. package/dist/tui-ink/plan-confirm.d.ts +7 -0
  67. package/dist/tui-ink/plan-confirm.js +104 -0
  68. package/dist/tui-ink/question-dialog.d.ts +8 -0
  69. package/dist/tui-ink/question-dialog.js +98 -0
  70. package/dist/tui-ink/recent-activity.d.ts +8 -0
  71. package/dist/tui-ink/recent-activity.js +71 -0
  72. package/dist/tui-ink/run.d.ts +33 -0
  73. package/dist/tui-ink/run.js +25 -0
  74. package/dist/tui-ink/theme.d.ts +37 -0
  75. package/dist/tui-ink/theme.js +42 -0
  76. package/dist/tui-ink/todos.d.ts +7 -0
  77. package/dist/tui-ink/todos.js +44 -0
  78. package/dist/tui-ink/trace-groups.d.ts +25 -0
  79. package/dist/tui-ink/trace-groups.js +310 -0
  80. package/dist/tui-ink/use-terminal-size.d.ts +4 -0
  81. package/dist/tui-ink/use-terminal-size.js +21 -0
  82. package/dist/tui-ink/welcome.d.ts +18 -0
  83. package/dist/tui-ink/welcome.js +119 -0
  84. package/dist/types.d.ts +4 -0
  85. package/package.json +6 -1
@@ -0,0 +1,571 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Static, Text } from "ink";
4
+ import { theme } from "./theme.js";
5
+ import { highlightCode, inferLang } from "./code-highlight.js";
6
+ import { MarkdownContent } from "./markdown.js";
7
+ import { buildTraceGroups, formatElapsed, formatTracePath, traceGroupLabel } from "./trace-groups.js";
8
+ import { EDIT_COLLAPSED_DIFF_LINES, formatEditSuccessSummary, getEditDiffDetails } from "./edit-diff.js";
9
+ export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, verboseTrace, pendingApproval, nowTick, welcomeBanner, }) {
10
+ const hasStreaming = !!(streamingContent ||
11
+ streamingReasoning ||
12
+ streamingTools.length > 0 ||
13
+ streamingParts.length > 0);
14
+ // Committed messages enter ink's <Static> region immediately and never move
15
+ // between a live <Box> and Static. Moving the same message across those
16
+ // regions writes it into terminal scrollback twice. Mutable assistant output
17
+ // stays in StreamingMessage until the agent reports a final turn_end. Keep
18
+ // the Static instance identity stable across terminal resizes; remounting it
19
+ // would replay all previously-written scrollback items.
20
+ const staticItems = [];
21
+ if (welcomeBanner) {
22
+ staticItems.push({ kind: "welcome", key: "welcome" });
23
+ }
24
+ const lastMessageIndex = messages.length - 1;
25
+ for (let i = 0; i < messages.length; i++) {
26
+ const msg = messages[i];
27
+ staticItems.push({
28
+ kind: "message",
29
+ key: msg.key ?? `message-${i}`,
30
+ message: msg,
31
+ showExpandHint: !hasStreaming && i === lastMessageIndex,
32
+ });
33
+ }
34
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticItems, children: (item) => {
35
+ if (item.kind === "welcome") {
36
+ return _jsx(React.Fragment, { children: welcomeBanner }, item.key);
37
+ }
38
+ return (_jsx(MessageItem, { message: item.message, terminalColumns: terminalColumns, verboseTrace: verboseTrace, showExpandHint: item.showExpandHint, nowTick: item.showExpandHint ? nowTick : undefined }, item.key));
39
+ } }), hasStreaming && (_jsx(StreamingMessage, { content: streamingContent, reasoning: streamingReasoning, tools: streamingTools, parts: streamingParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, nowTick: nowTick }))] }));
40
+ }
41
+ function MessageItem({ message, terminalColumns, verboseTrace, showExpandHint, nowTick, }) {
42
+ if (message.role === "user") {
43
+ return _jsx(UserMessageBlock, { content: message.content, terminalColumns: terminalColumns });
44
+ }
45
+ if (message.role === "error") {
46
+ return (_jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsxs(Text, { color: theme.error, children: ["Error: ", message.content] }) }));
47
+ }
48
+ const hasVisibleAssistantContent = !!message.content ||
49
+ (message.toolCalls?.length ?? 0) > 0 ||
50
+ (message.parts?.length ?? 0) > 0 ||
51
+ (!!message.reasoning && verboseTrace);
52
+ if (!hasVisibleAssistantContent)
53
+ return null;
54
+ return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [message.reasoning && verboseTrace && _jsx(ReasoningTraceBlock, { reasoning: message.reasoning }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })) : (_jsxs(_Fragment, { children: [message.toolCalls && (_jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })), message.content && _jsx(MarkdownContent, { content: message.content })] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls }))] }));
55
+ }
56
+ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, verboseTrace, pendingApproval, nowTick, }) {
57
+ const deferredContent = React.useDeferredValue(content);
58
+ const deferredReasoning = React.useDeferredValue(reasoning);
59
+ const deferredParts = React.useDeferredValue(parts);
60
+ const visibleParts = deferredParts.length > 0
61
+ ? deferredParts
62
+ : fallbackStreamingParts(deferredContent, tools);
63
+ return (_jsxs(Box, { flexDirection: "column", children: [deferredReasoning && verboseTrace && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(ReasoningTraceBlock, { reasoning: deferredReasoning }) })), visibleParts.length > 0 && (_jsx(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: true, nowTick: nowTick, showActivity: true }) }))] }));
64
+ }
65
+ function MessageParts({ parts, terminalColumns, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, }) {
66
+ const lastToolsPartIndex = findLastToolsPartIndex(parts);
67
+ return (_jsx(Box, { flexDirection: "column", children: parts.map((part, idx) => {
68
+ if (part.type === "text") {
69
+ return (_jsx(TimelineText, { content: part.content, compactTop: idx === 0, terminalColumns: terminalColumns }, `text-${idx}`));
70
+ }
71
+ return (_jsx(ToolsPart, { toolCalls: part.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: showExpandHint && idx === lastToolsPartIndex, compactTop: idx === 0, nowTick: nowTick, showActivity: showActivity && idx === lastToolsPartIndex }, `tools-${idx}`));
72
+ }) }));
73
+ }
74
+ function TimelineText({ content, compactTop, terminalColumns, }) {
75
+ if (!content.trim())
76
+ return null;
77
+ // marginLeft (2) + "⛬ " glyph (3 visual cells) = 5 cells consumed by the
78
+ // timeline gutter; pass the remaining width so wide blocks like tables size
79
+ // themselves against the actual content area instead of the raw terminal.
80
+ const available = terminalColumns ? Math.max(20, terminalColumns - 5) : undefined;
81
+ return (_jsxs(Box, { marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsx(Text, { color: theme.agent, children: "\u26EC " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(MarkdownContent, { content: content.trim(), maxWidth: available }) })] }));
82
+ }
83
+ function ToolsPart({ toolCalls, terminalColumns, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
84
+ if (toolCalls.length === 0)
85
+ return null;
86
+ if (!verboseTrace) {
87
+ return (_jsx(TraceGroupList, { toolCalls: toolCalls, terminalColumns: terminalColumns, pendingApproval: pendingApproval, nowTick: nowTick, compactTop: compactTop, showActivity: showActivity }));
88
+ }
89
+ const lastIdx = toolCalls.length - 1;
90
+ return (_jsx(Box, { flexDirection: "column", children: toolCalls.map((tc, idx) => {
91
+ const isWaitingApproval = tc.result === undefined && !!pendingApproval && approvalMatchesTool(pendingApproval, tc);
92
+ return (_jsx(ToolCallDisplay, { toolCall: tc, isStreaming: tc.result === undefined, verbose: verboseTrace, terminalColumns: terminalColumns, showExpandHint: showExpandHint && idx === lastIdx, waitingApproval: isWaitingApproval, compactTop: idx === 0 && compactTop, nowTick: nowTick }, tc.id));
93
+ }) }));
94
+ }
95
+ function fallbackStreamingParts(content, tools) {
96
+ const parts = [];
97
+ if (tools.length > 0)
98
+ parts.push({ type: "tools", toolCalls: tools });
99
+ if (content)
100
+ parts.push({ type: "text", content });
101
+ return parts;
102
+ }
103
+ function findLastToolsPartIndex(parts) {
104
+ for (let i = parts.length - 1; i >= 0; i--) {
105
+ if (parts[i]?.type === "tools")
106
+ return i;
107
+ }
108
+ return -1;
109
+ }
110
+ function TraceGroupList({ toolCalls, terminalColumns, pendingApproval, nowTick, compactTop = false, showActivity = false, }) {
111
+ const groups = React.useMemo(() => buildTraceGroups(toolCalls), [toolCalls]);
112
+ const activeGroup = showActivity ? findActiveTraceGroup(groups, pendingApproval) : undefined;
113
+ if (groups.length === 0)
114
+ return null;
115
+ return (_jsxs(Box, { flexDirection: "column", children: [activeGroup && (_jsx(TraceActivityLine, { group: activeGroup, pendingApproval: pendingApproval, nowTick: nowTick, terminalColumns: terminalColumns })), groups.map((group, idx) => (_jsx(TraceGroupBlock, { group: group, terminalColumns: terminalColumns, pendingApproval: pendingApproval, compactTop: idx === 0 && compactTop, nowTick: nowTick }, group.raw.map((tool) => tool.id).join(":"))))] }));
116
+ }
117
+ function TraceActivityLine({ group, pendingApproval, nowTick, terminalColumns, }) {
118
+ const waiting = isTraceGroupWaitingForApproval(group, pendingApproval);
119
+ const elapsed = formatElapsed(group.startedAt, nowTick);
120
+ const labelWidth = Math.max(20, terminalColumns - 26);
121
+ const label = truncateVisual(traceGroupLabel(group), labelWidth);
122
+ return (_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { color: waiting ? theme.warning : theme.tracePending, children: "\u25CF " }), _jsxs(Text, { color: theme.traceDetail, children: [waiting ? "Waiting for approval" : "Working on", " "] }), _jsx(Text, { color: theme.traceAction, children: label }), elapsed && _jsxs(Text, { color: theme.traceDetail, children: [" \u00B7 ", elapsed] })] }));
123
+ }
124
+ function TraceGroupBlock({ group, terminalColumns, pendingApproval, compactTop, nowTick, }) {
125
+ const waiting = isTraceGroupWaitingForApproval(group, pendingApproval);
126
+ const status = traceGroupStatus(group, waiting, nowTick);
127
+ const editTool = group.kind === "edit" && group.raw.length === 1 ? group.raw[0] : undefined;
128
+ const editDetails = editTool && !group.pending && !group.hasError ? getEditDiffDetails(editTool) : null;
129
+ if (editTool && editDetails) {
130
+ return (_jsx(EditTraceBlock, { tool: editTool, details: editDetails, terminalColumns: terminalColumns, compactTop: compactTop, status: status }));
131
+ }
132
+ const titleColor = group.hasError ? theme.error : theme.traceAction;
133
+ const detailColor = group.hasError ? theme.error : theme.traceDetail;
134
+ const commandWidth = Math.max(14, terminalColumns - group.title.length - 16);
135
+ const detailWidth = Math.max(20, terminalColumns - 8);
136
+ const detailLines = group.previewLines.length > 0 ? group.previewLines : group.items;
137
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: titleColor, children: group.title }), group.command ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: theme.traceCommand, children: truncateVisual(group.command, commandWidth) })] })) : group.count !== undefined && group.noun ? (_jsxs(Text, { color: theme.traceCount, children: [" ", group.count, " ", group.noun] })) : null, status && _jsxs(Text, { color: status.color, children: [" ", status.text] })] }), detailLines.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: detailLines.map((line, index) => (_jsxs(Box, { marginLeft: index === 0 ? 0 : 2, children: [index === 0 && _jsx(Text, { color: theme.traceDetail, children: "\u21B3 " }), _jsx(Text, { color: detailColor, children: truncateVisual(line, detailWidth - (index === 0 ? 2 : 0)) })] }, index))) })), group.omitted > 0 && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.traceDetail, children: ["... ", group.omitted, " more, Ctrl+O to view"] }) }))] }));
138
+ }
139
+ function EditTraceBlock({ tool, details, terminalColumns, compactTop, status, }) {
140
+ const path = formatTracePath(details.path ?? tool.args.path ?? "");
141
+ const pathWidth = Math.max(14, terminalColumns - 12);
142
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.traceAction, children: "Edit" }), path && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: theme.traceCommand, children: truncateVisual(path, pathWidth) })] })), status && _jsxs(Text, { color: status.color, children: [" ", status.text] })] }), _jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { color: theme.traceDetail, children: "\u23BF " }), _jsx(Text, { color: theme.success, children: formatEditSuccessSummary(details) })] }), _jsx(DiffBlock, { diff: details.diff, terminalColumns: terminalColumns, maxLines: EDIT_COLLAPSED_DIFF_LINES, verbose: false, showExpandHint: true })] }));
143
+ }
144
+ function traceGroupStatus(group, waitingApproval, nowTick) {
145
+ if (waitingApproval)
146
+ return { text: "waiting for approval", color: theme.warning };
147
+ if (group.pending) {
148
+ const elapsed = formatElapsed(group.startedAt, nowTick);
149
+ return { text: elapsed ? `running · ${elapsed}` : "running", color: theme.tracePending };
150
+ }
151
+ if (group.hasError)
152
+ return { text: "error", color: theme.error };
153
+ return null;
154
+ }
155
+ function findActiveTraceGroup(groups, pendingApproval) {
156
+ for (let i = groups.length - 1; i >= 0; i--) {
157
+ const group = groups[i];
158
+ if (isTraceGroupWaitingForApproval(group, pendingApproval) || group.pending) {
159
+ return group;
160
+ }
161
+ }
162
+ return undefined;
163
+ }
164
+ function isTraceGroupWaitingForApproval(group, pendingApproval) {
165
+ return !!pendingApproval && group.raw.some((tool) => tool.result === undefined && approvalMatchesTool(pendingApproval, tool));
166
+ }
167
+ function approvalMatchesTool(hint, tc) {
168
+ if (hint.toolName !== tc.name)
169
+ return false;
170
+ if (hint.toolName === "bash") {
171
+ return !hint.command || hint.command === tc.args.command;
172
+ }
173
+ return !hint.path || hint.path === tc.args.path;
174
+ }
175
+ function ReasoningTraceBlock({ reasoning }) {
176
+ const lines = React.useMemo(() => reasoning.split("\n").filter((l) => l.trim() !== ""), [reasoning]);
177
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: theme.thinkingDim, dimColor: true, children: ["\u273B Reasoning trace", lines.length > 0 ? ` · ${lines.length} line${lines.length === 1 ? "" : "s"}` : ""] }), lines.map((line, i) => (_jsx(Text, { color: theme.thinkingDim, dimColor: true, italic: true, children: line }, i)))] }));
178
+ }
179
+ function UserMessageBlock({ content, terminalColumns }) {
180
+ const horizontalRoom = Math.max(20, terminalColumns - 4);
181
+ const maxBubbleTextWidth = Math.max(1, horizontalRoom - 4);
182
+ const wrappedLines = content
183
+ .split("\n")
184
+ .flatMap((line) => wrapByVisualWidth(line, maxBubbleTextWidth));
185
+ const bubbleTextWidth = Math.min(maxBubbleTextWidth, Math.max(8, ...wrappedLines.map((line) => visualWidth(line))));
186
+ return (_jsx(Box, { flexDirection: "column", children: wrappedLines.map((line, index) => (_jsxs(Box, { children: [_jsx(Text, { color: theme.userRail, children: "\u258C " }), _jsx(Text, { backgroundColor: theme.userMessageBg, color: theme.userMessageText, children: ` ${padVisual(line || " ", bubbleTextWidth)} ` })] }, index))) }));
187
+ }
188
+ const TOOL_DISPLAY_NAMES = {
189
+ read: "Read",
190
+ write: "Write",
191
+ edit: "Edit",
192
+ bash: "Bash",
193
+ grep: "Grep",
194
+ glob: "Glob",
195
+ web_fetch: "WebFetch",
196
+ web_search: "WebSearch",
197
+ };
198
+ const TOOL_GLYPHS = {
199
+ read: "⏺",
200
+ write: "✎",
201
+ edit: "✎",
202
+ bash: "▶",
203
+ grep: "⌕",
204
+ glob: "⌕",
205
+ web_fetch: "⇲",
206
+ web_search: "⌕",
207
+ task: "↳",
208
+ todo: "✓",
209
+ skill: "★",
210
+ };
211
+ function displayToolName(name) {
212
+ if (TOOL_DISPLAY_NAMES[name])
213
+ return TOOL_DISPLAY_NAMES[name];
214
+ return name.charAt(0).toUpperCase() + name.slice(1);
215
+ }
216
+ function toolGlyph(name) {
217
+ return TOOL_GLYPHS[name] ?? "●";
218
+ }
219
+ function getToolHeader(toolCall) {
220
+ const args = toolCall.args || {};
221
+ const trunc = (s, n = 50) => (s.length > n ? s.slice(0, n) + "..." : s);
222
+ switch (toolCall.name) {
223
+ case "read":
224
+ case "write":
225
+ case "edit":
226
+ return args.path ? trunc(String(args.path), 60) : undefined;
227
+ case "bash":
228
+ return args.command ? trunc(String(args.command).replace(/\n/g, " "), 60) : undefined;
229
+ case "grep":
230
+ return args.pattern ? trunc(String(args.pattern), 60) : undefined;
231
+ case "glob":
232
+ return args.pattern ? trunc(String(args.pattern), 60) : undefined;
233
+ case "web_fetch":
234
+ return args.url ? trunc(String(args.url), 60) : undefined;
235
+ case "web_search":
236
+ return args.query ? trunc(String(args.query), 60) : undefined;
237
+ default:
238
+ return undefined;
239
+ }
240
+ }
241
+ function summarizeToolResult(tc) {
242
+ if (tc.result === undefined)
243
+ return "pending";
244
+ const raw = tc.result.replace(/\r\n/g, "\n");
245
+ if (tc.isError) {
246
+ const firstLine = raw.split("\n").find((l) => l.trim() !== "") || "Error";
247
+ return firstLine.length > 80 ? firstLine.slice(0, 80) + "..." : firstLine;
248
+ }
249
+ const nonEmpty = raw.split("\n").filter((l) => l.trim() !== "");
250
+ const lineCount = nonEmpty.length;
251
+ const p = (n, singular, plural) => `${n} ${n === 1 ? singular : plural}`;
252
+ switch (tc.name) {
253
+ case "read":
254
+ return p(lineCount, "line", "lines");
255
+ case "write": {
256
+ const firstLine = raw.split("\n")[0] || "";
257
+ if (firstLine.startsWith("Wrote ") || firstLine.startsWith("Updated ")) {
258
+ return firstLine;
259
+ }
260
+ return "Wrote file";
261
+ }
262
+ case "edit": {
263
+ return formatEditSuccessSummary(getEditDiffDetails(tc));
264
+ }
265
+ case "bash":
266
+ return lineCount > 0 ? `${p(lineCount, "line", "lines")} output` : "Done";
267
+ case "grep":
268
+ return `Found ${p(lineCount, "match", "matches")}`;
269
+ case "glob":
270
+ return `Found ${p(lineCount, "file", "files")}`;
271
+ case "web_search":
272
+ return `${p(lineCount, "result", "results")}`;
273
+ case "web_fetch":
274
+ return p(lineCount, "line", "lines");
275
+ default:
276
+ return lineCount > 0 ? p(lineCount, "line", "lines") : "Done";
277
+ }
278
+ }
279
+ const COLLAPSED_PREVIEW_LINES = 10;
280
+ const EXPANDED_PREVIEW_LINES = 50;
281
+ function ToolCallDisplay({ toolCall, isStreaming, verbose, terminalColumns, showExpandHint = false, waitingApproval = false, compactTop = false, nowTick, }) {
282
+ // Show raw output immediately, then upgrade to highlighted ANSI when shiki
283
+ // resolves. Avoids a noticeable "flash" where the line jumps from empty/raw
284
+ // to colorized after a tick.
285
+ const initialPreview = React.useMemo(() => {
286
+ if (toolCall.result === undefined || toolCall.isError)
287
+ return null;
288
+ return toolCall.result.replace(/\r\n/g, "\n");
289
+ }, [toolCall.result, toolCall.isError]);
290
+ const [highlighted, setHighlighted] = React.useState(initialPreview);
291
+ const header = getToolHeader(toolCall);
292
+ const maxLines = verbose ? EXPANDED_PREVIEW_LINES : COLLAPSED_PREVIEW_LINES;
293
+ React.useEffect(() => {
294
+ let cancelled = false;
295
+ if (toolCall.result === undefined || toolCall.isError) {
296
+ setHighlighted(null);
297
+ return;
298
+ }
299
+ const raw = toolCall.result.replace(/\r\n/g, "\n");
300
+ let lang = "text";
301
+ if (toolCall.name === "read")
302
+ lang = inferLang(toolCall.args.path);
303
+ else if (toolCall.name === "bash")
304
+ lang = "shell";
305
+ // Always seed with raw so the user sees content immediately.
306
+ setHighlighted(raw);
307
+ if (lang === "text") {
308
+ return;
309
+ }
310
+ highlightCode(raw, lang)
311
+ .then((out) => {
312
+ if (!cancelled)
313
+ setHighlighted(out);
314
+ })
315
+ .catch(() => {
316
+ if (!cancelled)
317
+ setHighlighted(raw);
318
+ });
319
+ return () => {
320
+ cancelled = true;
321
+ };
322
+ }, [toolCall.result, toolCall.name, toolCall.args.path, toolCall.isError]);
323
+ const glyph = toolGlyph(toolCall.name);
324
+ const bulletColor = toolCall.isError
325
+ ? theme.error
326
+ : waitingApproval
327
+ ? theme.warning
328
+ : isStreaming
329
+ ? theme.toolPending
330
+ : theme.user;
331
+ const name = displayToolName(toolCall.name);
332
+ // Compose summary: pending tools get an elapsed counter; waiting-for-approval
333
+ // gets an explicit badge so the trail survives the dialog closing.
334
+ let summary;
335
+ let summaryColor = theme.muted;
336
+ if (waitingApproval) {
337
+ summary = "⏸ waiting for approval";
338
+ summaryColor = theme.warning;
339
+ }
340
+ else if (toolCall.result === undefined && toolCall.startedAt) {
341
+ const elapsedSec = Math.max(0, Math.floor(((nowTick ?? Date.now()) - toolCall.startedAt) / 1000));
342
+ summary = elapsedSec > 0 ? `running · ${elapsedSec}s` : "running";
343
+ summaryColor = theme.toolPending;
344
+ }
345
+ else {
346
+ summary = summarizeToolResult(toolCall);
347
+ if (toolCall.isError)
348
+ summaryColor = theme.error;
349
+ else if (toolCall.name === "edit" && toolCall.result !== undefined)
350
+ summaryColor = theme.success;
351
+ }
352
+ const editDetails = getEditDiffDetails(toolCall);
353
+ const isEditDiff = editDetails !== null && toolCall.result !== undefined;
354
+ // Only show the file preview once the tool actually executed. During the
355
+ // streaming-args phase, args.content is incomplete and re-rendering the
356
+ // entire body per delta both looks chaotic and breaks on partial escapes.
357
+ const isWritePreview = toolCall.name === "write" && !toolCall.isError && toolCall.result !== undefined;
358
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: bulletColor, children: [glyph, " "] }), _jsx(Text, { bold: true, color: theme.toolName, children: name }), header && _jsxs(Text, { color: theme.muted, children: ["(", header, ")"] })] }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: summaryColor, children: ["\u23BF ", summary] }) }), toolCall.isError && toolCall.result && (_jsx(Box, { marginLeft: 4, flexDirection: "column", children: toolCall.result.replace(/\r\n/g, "\n").split("\n").slice(0, 6).map((line, i) => (_jsx(Text, { color: theme.error, children: line }, i))) })), isEditDiff && (_jsx(DiffBlock, { diff: editDetails.diff, terminalColumns: terminalColumns, maxLines: maxLines, verbose: verbose, showExpandHint: showExpandHint })), isWritePreview && (_jsx(WritePreview, { content: String(toolCall.args.content || ""), maxLines: maxLines, verbose: verbose, showExpandHint: showExpandHint })), !toolCall.isError && !isEditDiff && !isWritePreview && highlighted && (_jsx(OutputPreview, { text: highlighted, maxLines: maxLines, verbose: verbose, showExpandHint: showExpandHint }))] }));
359
+ }
360
+ function renderTruncationHint(remaining, verbose, showExpandHint) {
361
+ if (remaining <= 0)
362
+ return null;
363
+ const noun = `line${remaining === 1 ? "" : "s"}`;
364
+ if (verbose) {
365
+ return (_jsxs(Text, { color: theme.muted, children: ["... (", remaining, " more ", noun, ")"] }));
366
+ }
367
+ return (_jsxs(Text, { color: theme.muted, children: ["\u2026 +", remaining, " ", noun, showExpandHint ? " (ctrl+o to expand)" : ""] }));
368
+ }
369
+ function OutputPreview({ text, maxLines, verbose, showExpandHint, }) {
370
+ const lines = text.split("\n");
371
+ const shown = lines.slice(0, maxLines);
372
+ const remaining = Math.max(0, lines.length - maxLines);
373
+ if (shown.length === 0 || (shown.length === 1 && shown[0] === ""))
374
+ return null;
375
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 4, children: [shown.map((line, i) => (_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "\u2502 " }), _jsx(Text, { children: line })] }, i))), renderTruncationHint(remaining, verbose, showExpandHint)] }));
376
+ }
377
+ function WritePreview({ content, maxLines, verbose, showExpandHint, }) {
378
+ const lines = content.split("\n");
379
+ const shown = lines.slice(0, maxLines);
380
+ const remaining = Math.max(0, lines.length - maxLines);
381
+ const numWidth = Math.max(2, String(lines.length).length);
382
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 4, children: [shown.map((line, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: theme.muted, children: [String(i + 1).padStart(numWidth, " "), " "] }), _jsx(Text, { children: line })] }, i))), renderTruncationHint(remaining, verbose, showExpandHint)] }));
383
+ }
384
+ function parseDiffLines(body) {
385
+ const result = [];
386
+ let oldNum = 0;
387
+ let newNum = 0;
388
+ const rawLines = body.split("\n");
389
+ if (rawLines[rawLines.length - 1] === "")
390
+ rawLines.pop();
391
+ for (const raw of rawLines) {
392
+ if (raw.startsWith("+++") ||
393
+ raw.startsWith("---") ||
394
+ raw.startsWith("Index:") ||
395
+ raw.startsWith("==="))
396
+ continue;
397
+ if (raw.startsWith("@@")) {
398
+ const m = raw.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
399
+ if (m) {
400
+ oldNum = parseInt(m[1], 10);
401
+ newNum = parseInt(m[2], 10);
402
+ }
403
+ continue;
404
+ }
405
+ if (raw.startsWith("+")) {
406
+ result.push({ type: "add", num: newNum, content: raw.slice(1) });
407
+ newNum++;
408
+ }
409
+ else if (raw.startsWith("-")) {
410
+ result.push({ type: "remove", num: oldNum, content: raw.slice(1) });
411
+ oldNum++;
412
+ }
413
+ else {
414
+ const content = raw.startsWith(" ") ? raw.slice(1) : raw;
415
+ result.push({ type: "context", num: newNum, content });
416
+ oldNum++;
417
+ newNum++;
418
+ }
419
+ }
420
+ return result;
421
+ }
422
+ function DiffBlock({ diff, terminalColumns, maxLines, verbose, showExpandHint, }) {
423
+ const lines = parseDiffLines(diff);
424
+ const shown = lines.slice(0, maxLines);
425
+ const remaining = Math.max(0, lines.length - maxLines);
426
+ const maxNum = lines.reduce((acc, l) => Math.max(acc, l.num), 0);
427
+ const numWidth = Math.max(2, String(maxNum).length);
428
+ const leftMargin = 2;
429
+ const prefixWidth = numWidth + 4; // " NUM ± "
430
+ // Reserve the full left-margin chain from terminal edge to diff content:
431
+ // app padding (1) + ToolCallDisplay marginLeft (2) + DiffBlock marginLeft (2)
432
+ // + right padding (1) + 1-col safety = 7. Without this, each row overflows
433
+ // by 1 column, the terminal auto-wraps, and every line renders with a blank
434
+ // row beneath it.
435
+ const bandWidth = Math.max(10, terminalColumns - 7);
436
+ const contentWidth = Math.max(1, bandWidth - prefixWidth);
437
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: leftMargin, children: [shown.map((line, i) => {
438
+ const bg = line.type === "add"
439
+ ? theme.diffAdd
440
+ : line.type === "remove"
441
+ ? theme.diffRemove
442
+ : undefined;
443
+ const sign = line.type === "add" ? "+" : line.type === "remove" ? "-" : " ";
444
+ const numStr = String(line.num).padStart(numWidth, " ");
445
+ const truncated = truncateVisual(line.content, contentWidth);
446
+ const padded = padVisual(truncated, contentWidth);
447
+ const lineText = ` ${numStr} ${sign} ${padded}`;
448
+ return (_jsx(Text, { backgroundColor: bg, color: theme.userMessageText, children: lineText }, i));
449
+ }), renderTruncationHint(remaining, verbose, showExpandHint)] }));
450
+ }
451
+ /**
452
+ * "Edited 3 files (+42 -8) — a.ts, b.ts" digest below the assistant turn.
453
+ * Surfaces only when there is at least one file-mutating tool call.
454
+ */
455
+ function TurnDigest({ toolCalls }) {
456
+ const digest = React.useMemo(() => buildDigest(toolCalls), [toolCalls]);
457
+ if (!digest)
458
+ return null;
459
+ return (_jsx(Box, { marginLeft: 2, marginTop: 1, children: _jsx(Text, { color: theme.muted, dimColor: true, children: digest }) }));
460
+ }
461
+ function buildDigest(toolCalls) {
462
+ const paths = new Set();
463
+ let added = 0;
464
+ let removed = 0;
465
+ let writes = 0;
466
+ let edits = 0;
467
+ for (const tc of toolCalls) {
468
+ if (tc.isError || !tc.result)
469
+ continue;
470
+ if (tc.name === "edit") {
471
+ const details = getEditDiffDetails(tc);
472
+ if (details) {
473
+ added += details.added;
474
+ removed += details.removed;
475
+ }
476
+ if (tc.args.path)
477
+ paths.add(String(tc.args.path));
478
+ edits += 1;
479
+ }
480
+ else if (tc.name === "write") {
481
+ if (tc.args.path)
482
+ paths.add(String(tc.args.path));
483
+ writes += 1;
484
+ const content = String(tc.args.content ?? "");
485
+ if (content)
486
+ added += content.split("\n").length;
487
+ }
488
+ }
489
+ const total = edits + writes;
490
+ if (total === 0 || paths.size === 0)
491
+ return null;
492
+ const verb = edits > 0 && writes === 0 ? "Edited" : writes > 0 && edits === 0 ? "Wrote" : "Touched";
493
+ const pathList = Array.from(paths);
494
+ const shownPaths = pathList.slice(0, 4).map((p) => p.split("/").pop() || p);
495
+ const extra = pathList.length - shownPaths.length;
496
+ const pathDisplay = shownPaths.join(", ") + (extra > 0 ? `, +${extra} more` : "");
497
+ const stats = added || removed
498
+ ? ` (${added ? `+${added}` : ""}${added && removed ? " " : ""}${removed ? `-${removed}` : ""})`
499
+ : "";
500
+ return `↳ ${verb} ${paths.size} file${paths.size === 1 ? "" : "s"}${stats} — ${pathDisplay}`;
501
+ }
502
+ function truncateVisual(str, maxWidth) {
503
+ if (maxWidth <= 0)
504
+ return "";
505
+ let out = "";
506
+ let width = 0;
507
+ for (const char of str) {
508
+ const w = charVisualWidth(char);
509
+ if (width + w > maxWidth)
510
+ break;
511
+ out += char;
512
+ width += w;
513
+ }
514
+ return out;
515
+ }
516
+ function visualWidth(str) {
517
+ let width = 0;
518
+ for (const char of str) {
519
+ const code = char.codePointAt(0) || 0;
520
+ if ((code >= 0x4e00 && code <= 0x9fff) ||
521
+ (code >= 0x3000 && code <= 0x303f) ||
522
+ (code >= 0xff00 && code <= 0xffef) ||
523
+ (code >= 0x3040 && code <= 0x309f) ||
524
+ (code >= 0x30a0 && code <= 0x30ff)) {
525
+ width += 2;
526
+ }
527
+ else {
528
+ width += 1;
529
+ }
530
+ }
531
+ return width;
532
+ }
533
+ function padVisual(str, width) {
534
+ const currentWidth = visualWidth(str);
535
+ return str + " ".repeat(Math.max(0, width - currentWidth));
536
+ }
537
+ function charVisualWidth(char) {
538
+ const code = char.codePointAt(0) || 0;
539
+ if ((code >= 0x4e00 && code <= 0x9fff) ||
540
+ (code >= 0x3000 && code <= 0x303f) ||
541
+ (code >= 0xff00 && code <= 0xffef) ||
542
+ (code >= 0x3040 && code <= 0x309f) ||
543
+ (code >= 0x30a0 && code <= 0x30ff)) {
544
+ return 2;
545
+ }
546
+ return 1;
547
+ }
548
+ function wrapByVisualWidth(line, maxWidth) {
549
+ if (maxWidth <= 0)
550
+ return [line];
551
+ if (line === "")
552
+ return [""];
553
+ const result = [];
554
+ let current = "";
555
+ let currentWidth = 0;
556
+ for (const char of line) {
557
+ const w = charVisualWidth(char);
558
+ if (currentWidth + w > maxWidth) {
559
+ result.push(current);
560
+ current = char;
561
+ currentWidth = w;
562
+ }
563
+ else {
564
+ current += char;
565
+ currentWidth += w;
566
+ }
567
+ }
568
+ if (current !== "" || result.length === 0)
569
+ result.push(current);
570
+ return result;
571
+ }
@@ -0,0 +1,43 @@
1
+ import { ProviderRegistry } from "../provider-registry.js";
2
+ export interface ModelPickerOption {
3
+ id: string;
4
+ label: string;
5
+ group: string;
6
+ providerBadge: string;
7
+ }
8
+ export interface ModelPickerProps {
9
+ registry: ProviderRegistry;
10
+ current: string;
11
+ recent: string[];
12
+ onSelect: (model: string) => void;
13
+ onCancel: () => void;
14
+ }
15
+ export declare function ModelPicker({ registry, current, recent, onSelect, onCancel }: ModelPickerProps): import("react/jsx-runtime").JSX.Element;
16
+ export declare function buildLocalModelOptions(registry: ProviderRegistry, current: string, recent: string[]): ModelPickerOption[];
17
+ export interface ProviderPickerProps {
18
+ providers: Array<{
19
+ id: string;
20
+ name: string;
21
+ enabled: boolean;
22
+ }>;
23
+ current?: string;
24
+ onSelect: (providerId: string) => void;
25
+ onCancel: () => void;
26
+ title?: string;
27
+ }
28
+ export declare function ProviderPicker({ providers, current, onSelect, onCancel, title }: ProviderPickerProps): import("react/jsx-runtime").JSX.Element;
29
+ export interface KeyPickerProps {
30
+ providerName: string;
31
+ onSubmit: (key: string) => void;
32
+ onCancel: () => void;
33
+ }
34
+ export declare function KeyPicker({ providerName, onSubmit, onCancel }: KeyPickerProps): import("react/jsx-runtime").JSX.Element;
35
+ export interface SkillPickerProps {
36
+ skills: Array<{
37
+ name: string;
38
+ description: string;
39
+ }>;
40
+ onSelect: (name: string) => void;
41
+ onCancel: () => void;
42
+ }
43
+ export declare function SkillPicker({ skills, onSelect, onCancel }: SkillPickerProps): import("react/jsx-runtime").JSX.Element;