@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,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
+ }