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