@bubblebrain-ai/bubble 0.0.12 → 0.0.14

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 (180) hide show
  1. package/dist/agent/execution-governor.js +1 -1
  2. package/dist/agent/input-controller.d.ts +11 -0
  3. package/dist/agent/input-controller.js +30 -0
  4. package/dist/agent/tool-intent.js +1 -0
  5. package/dist/agent.d.ts +8 -4
  6. package/dist/agent.js +623 -312
  7. package/dist/approval/controller.d.ts +1 -0
  8. package/dist/approval/controller.js +20 -3
  9. package/dist/approval/tool-helper.js +2 -0
  10. package/dist/approval/types.d.ts +14 -1
  11. package/dist/context/compact.js +9 -3
  12. package/dist/context/projector.js +27 -12
  13. package/dist/debug-trace.d.ts +27 -0
  14. package/dist/debug-trace.js +385 -0
  15. package/dist/feishu/agent-host/approval-card.js +9 -0
  16. package/dist/feishu/serve.js +7 -1
  17. package/dist/main.js +86 -9
  18. package/dist/model-catalog.js +1 -0
  19. package/dist/orchestrator/default-hooks.js +19 -8
  20. package/dist/orchestrator/hooks.d.ts +1 -0
  21. package/dist/prompt/environment.js +2 -0
  22. package/dist/prompt/reminders.d.ts +5 -6
  23. package/dist/prompt/reminders.js +8 -9
  24. package/dist/prompt/runtime.js +2 -2
  25. package/dist/provider-openai-codex.d.ts +7 -0
  26. package/dist/provider-openai-codex.js +265 -124
  27. package/dist/provider-registry.d.ts +2 -0
  28. package/dist/provider-registry.js +58 -9
  29. package/dist/provider.d.ts +3 -0
  30. package/dist/provider.js +5 -1
  31. package/dist/session-log.js +13 -1
  32. package/dist/slash-commands/commands.js +39 -0
  33. package/dist/slash-commands/types.d.ts +12 -0
  34. package/dist/stats/usage.d.ts +52 -0
  35. package/dist/stats/usage.js +414 -0
  36. package/dist/tools/apply-patch.d.ts +9 -0
  37. package/dist/tools/apply-patch.js +330 -0
  38. package/dist/tools/bash.js +205 -44
  39. package/dist/tools/edit-apply.d.ts +5 -2
  40. package/dist/tools/edit-apply.js +221 -31
  41. package/dist/tools/edit.js +12 -3
  42. package/dist/tools/file-mutation-queue.d.ts +1 -0
  43. package/dist/tools/file-mutation-queue.js +12 -1
  44. package/dist/tools/index.d.ts +2 -0
  45. package/dist/tools/index.js +7 -1
  46. package/dist/tools/patch-apply.d.ts +41 -0
  47. package/dist/tools/patch-apply.js +312 -0
  48. package/dist/tools/server-manager.d.ts +36 -0
  49. package/dist/tools/server-manager.js +234 -0
  50. package/dist/tools/server.d.ts +6 -0
  51. package/dist/tools/server.js +245 -0
  52. package/dist/tools/write.d.ts +3 -6
  53. package/dist/tools/write.js +26 -46
  54. package/dist/tui/clipboard.d.ts +1 -0
  55. package/dist/tui/clipboard.js +53 -0
  56. package/dist/tui/detect-theme.d.ts +2 -0
  57. package/dist/tui/detect-theme.js +87 -0
  58. package/dist/tui/display-history.d.ts +63 -0
  59. package/dist/tui/display-history.js +306 -0
  60. package/dist/tui/edit-diff.d.ts +11 -0
  61. package/dist/tui/edit-diff.js +57 -0
  62. package/dist/tui/escape-confirmation.d.ts +15 -0
  63. package/dist/tui/escape-confirmation.js +30 -0
  64. package/dist/tui/file-mentions.d.ts +29 -0
  65. package/dist/tui/file-mentions.js +174 -0
  66. package/dist/tui/global-key-router.d.ts +3 -0
  67. package/dist/tui/global-key-router.js +87 -0
  68. package/dist/tui/image-paste.d.ts +95 -0
  69. package/dist/tui/image-paste.js +505 -0
  70. package/dist/tui/input-history.d.ts +16 -0
  71. package/dist/tui/input-history.js +79 -0
  72. package/dist/tui/markdown-inline.d.ts +22 -0
  73. package/dist/tui/markdown-inline.js +68 -0
  74. package/dist/tui/markdown-theme-rules.d.ts +23 -0
  75. package/dist/tui/markdown-theme-rules.js +164 -0
  76. package/dist/tui/markdown-theme.d.ts +5 -0
  77. package/dist/tui/markdown-theme.js +27 -0
  78. package/dist/tui/model-picker-data.d.ts +10 -0
  79. package/dist/tui/model-picker-data.js +32 -0
  80. package/dist/tui/opencode-spinner.d.ts +22 -0
  81. package/dist/tui/opencode-spinner.js +216 -0
  82. package/dist/tui/prompt-keybindings.d.ts +42 -0
  83. package/dist/tui/prompt-keybindings.js +35 -0
  84. package/dist/tui/recent-activity.d.ts +8 -0
  85. package/dist/tui/recent-activity.js +71 -0
  86. package/dist/tui/render-signature.d.ts +1 -0
  87. package/dist/tui/render-signature.js +7 -0
  88. package/dist/tui/run.d.ts +45 -0
  89. package/dist/tui/run.js +9359 -0
  90. package/dist/tui/session-display.d.ts +6 -0
  91. package/dist/tui/session-display.js +12 -0
  92. package/dist/tui/sidebar-mcp.d.ts +31 -0
  93. package/dist/tui/sidebar-mcp.js +62 -0
  94. package/dist/tui/sidebar-state.d.ts +12 -0
  95. package/dist/tui/sidebar-state.js +69 -0
  96. package/dist/tui/streaming-tool-args.d.ts +15 -0
  97. package/dist/tui/streaming-tool-args.js +30 -0
  98. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  99. package/dist/tui/tool-renderers/fallback.js +75 -0
  100. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  101. package/dist/tui/tool-renderers/registry.js +11 -0
  102. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  103. package/dist/tui/tool-renderers/subagent.js +135 -0
  104. package/dist/tui/tool-renderers/types.d.ts +36 -0
  105. package/dist/tui/tool-renderers/types.js +1 -0
  106. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  107. package/dist/tui/tool-renderers/write-preview.js +32 -0
  108. package/dist/tui/tool-renderers/write.d.ts +6 -0
  109. package/dist/tui/tool-renderers/write.js +88 -0
  110. package/dist/tui/trace-groups.d.ts +27 -0
  111. package/dist/tui/trace-groups.js +419 -0
  112. package/dist/tui/wordmark.d.ts +15 -0
  113. package/dist/tui/wordmark.js +179 -0
  114. package/dist/tui-ink/app.js +45 -9
  115. package/dist/tui-ink/approval/approval-dialog.js +7 -1
  116. package/dist/tui-ink/display-history.d.ts +1 -0
  117. package/dist/tui-ink/display-history.js +5 -4
  118. package/dist/tui-ink/message-list.js +23 -9
  119. package/dist/tui-ink/theme.d.ts +3 -9
  120. package/dist/tui-ink/theme.js +39 -45
  121. package/dist/tui-ink/trace-groups.js +1 -1
  122. package/dist/tui-ink/welcome.js +22 -78
  123. package/dist/tui-opentui/app.d.ts +54 -0
  124. package/dist/tui-opentui/app.js +1365 -0
  125. package/dist/tui-opentui/approval/approval-dialog.d.ts +15 -0
  126. package/dist/tui-opentui/approval/approval-dialog.js +145 -0
  127. package/dist/tui-opentui/approval/diff-view.d.ts +9 -0
  128. package/dist/tui-opentui/approval/diff-view.js +43 -0
  129. package/dist/tui-opentui/approval/select.d.ts +37 -0
  130. package/dist/tui-opentui/approval/select.js +91 -0
  131. package/dist/tui-opentui/detect-theme.d.ts +2 -0
  132. package/dist/tui-opentui/detect-theme.js +87 -0
  133. package/dist/tui-opentui/display-history.d.ts +56 -0
  134. package/dist/tui-opentui/display-history.js +130 -0
  135. package/dist/tui-opentui/edit-diff.d.ts +11 -0
  136. package/dist/tui-opentui/edit-diff.js +57 -0
  137. package/dist/tui-opentui/feedback-dialog.d.ts +21 -0
  138. package/dist/tui-opentui/feedback-dialog.js +164 -0
  139. package/dist/tui-opentui/feishu-setup-picker.d.ts +7 -0
  140. package/dist/tui-opentui/feishu-setup-picker.js +272 -0
  141. package/dist/tui-opentui/file-mentions.d.ts +29 -0
  142. package/dist/tui-opentui/file-mentions.js +174 -0
  143. package/dist/tui-opentui/footer.d.ts +26 -0
  144. package/dist/tui-opentui/footer.js +40 -0
  145. package/dist/tui-opentui/image-paste.d.ts +54 -0
  146. package/dist/tui-opentui/image-paste.js +288 -0
  147. package/dist/tui-opentui/input-box.d.ts +34 -0
  148. package/dist/tui-opentui/input-box.js +471 -0
  149. package/dist/tui-opentui/input-history.d.ts +16 -0
  150. package/dist/tui-opentui/input-history.js +79 -0
  151. package/dist/tui-opentui/markdown.d.ts +66 -0
  152. package/dist/tui-opentui/markdown.js +127 -0
  153. package/dist/tui-opentui/message-list.d.ts +31 -0
  154. package/dist/tui-opentui/message-list.js +128 -0
  155. package/dist/tui-opentui/model-picker.d.ts +63 -0
  156. package/dist/tui-opentui/model-picker.js +450 -0
  157. package/dist/tui-opentui/plan-confirm.d.ts +9 -0
  158. package/dist/tui-opentui/plan-confirm.js +124 -0
  159. package/dist/tui-opentui/question-dialog.d.ts +10 -0
  160. package/dist/tui-opentui/question-dialog.js +110 -0
  161. package/dist/tui-opentui/recent-activity.d.ts +8 -0
  162. package/dist/tui-opentui/recent-activity.js +71 -0
  163. package/dist/tui-opentui/run-session-picker.d.ts +10 -0
  164. package/dist/tui-opentui/run-session-picker.js +28 -0
  165. package/dist/tui-opentui/run.d.ts +38 -0
  166. package/dist/tui-opentui/run.js +48 -0
  167. package/dist/tui-opentui/session-picker.d.ts +12 -0
  168. package/dist/tui-opentui/session-picker.js +120 -0
  169. package/dist/tui-opentui/theme.d.ts +89 -0
  170. package/dist/tui-opentui/theme.js +157 -0
  171. package/dist/tui-opentui/todos.d.ts +9 -0
  172. package/dist/tui-opentui/todos.js +45 -0
  173. package/dist/tui-opentui/trace-groups.d.ts +27 -0
  174. package/dist/tui-opentui/trace-groups.js +419 -0
  175. package/dist/tui-opentui/use-terminal-size.d.ts +4 -0
  176. package/dist/tui-opentui/use-terminal-size.js +5 -0
  177. package/dist/tui-opentui/welcome.d.ts +25 -0
  178. package/dist/tui-opentui/welcome.js +77 -0
  179. package/dist/types.d.ts +36 -2
  180. package/package.json +5 -1
@@ -180,9 +180,40 @@ function withMessageKey(message) {
180
180
  const prefix = message.role === "user" ? "user" : message.role === "error" ? "err" : "asst";
181
181
  return { ...message, key: nextDisplayMessageKey(prefix) };
182
182
  }
183
- const STREAMING_STATIC_FLUSH_MIN_CHARS = 5000;
184
- const STREAMING_STATIC_FLUSH_TARGET_CHARS = 3600;
185
- const STREAMING_STATIC_FLUSH_MIN_TAIL = 700;
183
+ // Keep the live (non-Static) region small so non-GPU terminals (xterm.js DOM
184
+ // renderer, ssh into a basic terminal, tmux without GPU) don't flicker when
185
+ // Ink re-reconciles the streaming block on every token. Flushing earlier and
186
+ // in smaller chunks shifts most of the answer into terminal scrollback, where
187
+ // it's a one-time write that doesn't get re-rendered.
188
+ const STREAMING_STATIC_FLUSH_MIN_CHARS = 600;
189
+ const STREAMING_STATIC_FLUSH_TARGET_CHARS = 400;
190
+ const STREAMING_STATIC_FLUSH_MIN_TAIL = 120;
191
+ /**
192
+ * True iff `prefix` ends inside an open ```/~~~ fenced code block. Splitting
193
+ * the streaming buffer at such a point would let the flushed half render
194
+ * without its closing fence — `MarkdownContent` would then treat the body as
195
+ * plain prose and the trailing half would render as an isolated code block
196
+ * with no opener. Fence delimiters of different families don't close each
197
+ * other (a `~~~` inside a ``` block is just text). We use a permissive
198
+ * "line starts with three or more of the same char" rule, ignoring the info
199
+ * string — that's enough to spot when we're mid-block.
200
+ */
201
+ function endsInsideUnclosedCodeFence(prefix) {
202
+ let openMarker = null;
203
+ for (const rawLine of prefix.split("\n")) {
204
+ const line = rawLine.replace(/^ {0,3}/, "");
205
+ if (openMarker === null) {
206
+ if (line.startsWith("```"))
207
+ openMarker = "`";
208
+ else if (line.startsWith("~~~"))
209
+ openMarker = "~";
210
+ }
211
+ else if (line.startsWith(openMarker.repeat(3))) {
212
+ openMarker = null;
213
+ }
214
+ }
215
+ return openMarker !== null;
216
+ }
186
217
  function findStreamingStaticFlushIndex(content) {
187
218
  if (content.length < STREAMING_STATIC_FLUSH_MIN_CHARS)
188
219
  return -1;
@@ -192,12 +223,20 @@ function findStreamingStaticFlushIndex(content) {
192
223
  const search = content.slice(0, upper);
193
224
  const paragraphBreak = search.lastIndexOf("\n\n");
194
225
  if (paragraphBreak >= STREAMING_STATIC_FLUSH_TARGET_CHARS / 2) {
195
- return paragraphBreak + 2;
226
+ const splitIndex = paragraphBreak + 2;
227
+ if (!endsInsideUnclosedCodeFence(content.slice(0, splitIndex))) {
228
+ return splitIndex;
229
+ }
196
230
  }
197
231
  const lineBreak = search.lastIndexOf("\n");
198
232
  if (lineBreak >= STREAMING_STATIC_FLUSH_TARGET_CHARS / 2) {
199
- return lineBreak + 1;
233
+ const splitIndex = lineBreak + 1;
234
+ if (!endsInsideUnclosedCodeFence(content.slice(0, splitIndex))) {
235
+ return splitIndex;
236
+ }
200
237
  }
238
+ // Inside an open code fence: hold off flushing until the closing fence
239
+ // arrives. The live region grows a bit, but Markdown rendering stays correct.
201
240
  return -1;
202
241
  }
203
242
  function cloneDisplayPart(part) {
@@ -957,10 +996,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
957
996
  catch (err) {
958
997
  commitAssistantMessage();
959
998
  if (err instanceof AgentAbortError || err?.name === "AbortError") {
960
- updateDisplayMessages((prev) => [
961
- ...prev,
962
- withMessageKey({ role: "assistant", content: "Cancelled." }),
963
- ]);
999
+ updateDisplayMessages(() => reconstructDisplayMessages(agent.messages));
964
1000
  }
965
1001
  else {
966
1002
  updateDisplayMessages((prev) => [
@@ -53,7 +53,7 @@ function buildOptions(request) {
53
53
  },
54
54
  ];
55
55
  }
56
- // edit / write
56
+ // edit / write / patch
57
57
  return [
58
58
  { id: "yes", label: "Yes", allowAmend: true, amendPlaceholder: "and tell Claude what to do next" },
59
59
  {
@@ -69,6 +69,8 @@ function dialogTitle(req) {
69
69
  switch (req.type) {
70
70
  case "edit":
71
71
  return "Edit file";
72
+ case "patch":
73
+ return "Apply patch";
72
74
  case "write":
73
75
  return req.fileExists ? "Overwrite file" : "Create file";
74
76
  case "bash":
@@ -81,6 +83,8 @@ function dialogQuestion(req) {
81
83
  switch (req.type) {
82
84
  case "edit":
83
85
  return `Do you want to make this edit to ${basename(req.path)}?`;
86
+ case "patch":
87
+ return `Do you want to apply this patch to ${req.paths.length} file${req.paths.length === 1 ? "" : "s"}?`;
84
88
  case "write":
85
89
  return `Do you want to ${req.fileExists ? "overwrite" : "create"} ${basename(req.path)}?`;
86
90
  case "bash":
@@ -99,6 +103,8 @@ function RequestPreview({ request }) {
99
103
  return _jsx(BashPreview, { command: request.command, cwd: request.cwd });
100
104
  case "edit":
101
105
  return _jsx(DiffView, { diff: request.diff });
106
+ case "patch":
107
+ return _jsx(DiffView, { diff: request.diff });
102
108
  case "write":
103
109
  return _jsx(WritePreview, { path: request.path, content: request.content });
104
110
  }
@@ -33,6 +33,7 @@ export interface DisplayToolCall {
33
33
  */
34
34
  rawArguments?: string;
35
35
  result?: string;
36
+ resultCollapsed?: boolean;
36
37
  isError?: boolean;
37
38
  metadata?: ToolResultMetadata;
38
39
  /** Set when the tool_start event was received. Used to render elapsed time. */
@@ -46,7 +46,6 @@ export function toolCallsFromParts(parts) {
46
46
  const FULL_DETAIL_WINDOW = 24;
47
47
  const MAX_OLD_CONTENT_CHARS = 1200;
48
48
  const MAX_OLD_REASONING_CHARS = 600;
49
- const MAX_OLD_TOOL_RESULT_CHARS = 800;
50
49
  export function compactDisplayMessages(messages) {
51
50
  if (messages.length === 0) {
52
51
  return messages;
@@ -97,11 +96,13 @@ function compactDisplayPart(part) {
97
96
  };
98
97
  }
99
98
  function compactToolCall(toolCall) {
99
+ if (toolCall.result === undefined) {
100
+ return toolCall;
101
+ }
100
102
  return {
101
103
  ...toolCall,
102
- result: toolCall.result
103
- ? truncateText(toolCall.result, MAX_OLD_TOOL_RESULT_CHARS)
104
- : toolCall.result,
104
+ result: undefined,
105
+ resultCollapsed: true,
105
106
  };
106
107
  }
107
108
  const PREVIOUS_SUMMARY_PREFIX = /^Previous conversation summary:\s*\n?([\s\S]*)$/;
@@ -59,7 +59,15 @@ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, v
59
59
  const visibleParts = deferredParts.length > 0
60
60
  ? deferredParts
61
61
  : fallbackStreamingParts(deferredContent, tools);
62
- 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, streaming: true }) }))] }));
62
+ return (_jsxs(Box, { flexDirection: "column", children: [deferredReasoning && verboseTrace && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(ReasoningTraceBlock, { reasoning: deferredReasoning }) })), visibleParts.length > 0 && (
63
+ // marginTop intentionally 0: this Box only mounts on the first non-empty
64
+ // streaming frame, so a marginTop=1 here would visibly insert a blank
65
+ // line under the user message right at that moment (the "spinner sits
66
+ // close, then content appears with a sudden gap, then spinner slides
67
+ // down" effect users perceive as flicker on the DOM xterm renderer).
68
+ // marginBottom=1 stays so streamed text doesn't collide with the
69
+ // WaitingIndicator rendered below.
70
+ _jsx(Box, { marginTop: 0, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: true, nowTick: nowTick, showActivity: true, streaming: true }) }))] }));
63
71
  }
64
72
  function MessageParts({ parts, terminalColumns, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, streaming = false, }) {
65
73
  const lastToolsPartIndex = findLastToolsPartIndex(parts);
@@ -97,8 +105,8 @@ function ToolsPart({ toolCalls, terminalColumns, verboseTrace, pendingApproval,
97
105
  }
98
106
  const lastIdx = toolCalls.length - 1;
99
107
  return (_jsx(Box, { flexDirection: "column", children: toolCalls.map((tc, idx) => {
100
- const isWaitingApproval = tc.result === undefined && !!pendingApproval && approvalMatchesTool(pendingApproval, tc);
101
- 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));
108
+ const isWaitingApproval = isToolPending(tc) && !!pendingApproval && approvalMatchesTool(pendingApproval, tc);
109
+ return (_jsx(ToolCallDisplay, { toolCall: tc, isStreaming: isToolPending(tc), verbose: verboseTrace, terminalColumns: terminalColumns, showExpandHint: showExpandHint && idx === lastIdx, waitingApproval: isWaitingApproval, compactTop: idx === 0 && compactTop, nowTick: nowTick }, tc.id));
102
110
  }) }));
103
111
  }
104
112
  function fallbackStreamingParts(content, tools) {
@@ -184,7 +192,7 @@ function findActiveTraceGroup(groups, pendingApproval) {
184
192
  return undefined;
185
193
  }
186
194
  function isTraceGroupWaitingForApproval(group, pendingApproval) {
187
- return !!pendingApproval && group.raw.some((tool) => tool.result === undefined && approvalMatchesTool(pendingApproval, tool));
195
+ return !!pendingApproval && group.raw.some((tool) => isToolPending(tool) && approvalMatchesTool(pendingApproval, tool));
188
196
  }
189
197
  function approvalMatchesTool(hint, tc) {
190
198
  if (hint.toolName !== tc.name)
@@ -194,6 +202,9 @@ function approvalMatchesTool(hint, tc) {
194
202
  }
195
203
  return !hint.path || hint.path === tc.args.path;
196
204
  }
205
+ function isToolPending(tool) {
206
+ return tool.result === undefined && tool.resultCollapsed !== true;
207
+ }
197
208
  function ReasoningTraceBlock({ reasoning }) {
198
209
  const theme = useTheme();
199
210
  const lines = React.useMemo(() => reasoning.split("\n").filter((l) => l.trim() !== ""), [reasoning]);
@@ -272,6 +283,8 @@ function getToolHeader(toolCall) {
272
283
  }
273
284
  }
274
285
  function summarizeToolResult(tc) {
286
+ if (tc.resultCollapsed)
287
+ return tc.isError ? "error output collapsed" : "result collapsed";
275
288
  if (tc.result === undefined)
276
289
  return "pending";
277
290
  const raw = tc.result.replace(/\r\n/g, "\n");
@@ -433,7 +446,7 @@ function ToolCallDisplay({ toolCall, isStreaming, verbose, terminalColumns, show
433
446
  summary = "⏸ waiting for approval";
434
447
  summaryColor = theme.warning;
435
448
  }
436
- else if (toolCall.result === undefined && toolCall.startedAt) {
449
+ else if (isToolPending(toolCall) && toolCall.startedAt) {
437
450
  void nowTick;
438
451
  summary = "running";
439
452
  summaryColor = theme.toolPending;
@@ -446,18 +459,19 @@ function ToolCallDisplay({ toolCall, isStreaming, verbose, terminalColumns, show
446
459
  summaryColor = theme.success;
447
460
  }
448
461
  const editDetails = getEditDiffDetails(toolCall);
449
- const isEditDiff = editDetails !== null && toolCall.result !== undefined;
462
+ const isEditDiff = editDetails !== null && toolCall.result !== undefined && !toolCall.resultCollapsed;
463
+ const showSummary = !toolCall.resultCollapsed || waitingApproval;
450
464
  // Only show the file preview once the tool actually executed. During the
451
465
  // streaming-args phase, args.content is incomplete and re-rendering the
452
466
  // entire body per delta both looks chaotic and breaks on partial escapes.
453
- const isWritePreview = toolCall.name === "write" && !toolCall.isError && toolCall.result !== undefined;
454
- 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 }))] }));
467
+ const isWritePreview = toolCall.name === "write" && !toolCall.isError && toolCall.result !== undefined && !toolCall.resultCollapsed;
468
+ 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, ")"] })] }), showSummary && (_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 }))] }));
455
469
  }
456
470
  function SubagentToolDisplay({ toolCall, verbose, terminalColumns, compactTop, }) {
457
471
  const theme = useTheme();
458
472
  const subagents = subagentsFrom(toolCall);
459
473
  const hasError = toolCall.isError || subagents.some((subagent) => (subagent.status === "failed" || subagent.status === "blocked" || subagent.status === "cancelled"));
460
- const bulletColor = hasError ? theme.error : toolCall.result === undefined ? theme.toolPending : theme.user;
474
+ const bulletColor = hasError ? theme.error : isToolPending(toolCall) ? theme.toolPending : theme.user;
461
475
  const detailWidth = Math.max(24, terminalColumns - 10);
462
476
  const rows = verbose ? sortSubagents(subagents) : sortSubagents(subagents).slice(0, 4);
463
477
  const omitted = Math.max(0, subagents.length - rows.length);
@@ -49,15 +49,9 @@ export interface Theme {
49
49
  }
50
50
  export declare const darkTheme: Theme;
51
51
  /**
52
- * Light palette. Two ground rules drove the color choices:
53
- * 1. Named ANSI colors that render OK on both backgrounds (red/green/blue)
54
- * are kept by name so the user's terminal palette overrides remain
55
- * effective.
56
- * 2. Specific hex values are used wherever the dark palette assumed a dark
57
- * background (notably accent/code/trace colors and message bubbles).
58
- * Each hex was picked to clear WCAG AA contrast (4.5:1) against a near-
59
- * white background (#fafafa) or, when applicable, against the explicit
60
- * surface color in the same palette (e.g. diffAddFg vs diffAdd).
52
+ * Light palette aligned with the restored OpenTUI runtime: paper-neutral
53
+ * surfaces, blue focus/user rails, warm command accent, and semantic tool
54
+ * colors tuned for readable contrast on a light terminal background.
61
55
  */
62
56
  export declare const lightTheme: Theme;
63
57
  export declare const ThemeProvider: import("react").Provider<Theme>;
@@ -47,53 +47,47 @@ export const darkTheme = {
47
47
  diffRemoveFg: "#F48771",
48
48
  };
49
49
  /**
50
- * Light palette. Two ground rules drove the color choices:
51
- * 1. Named ANSI colors that render OK on both backgrounds (red/green/blue)
52
- * are kept by name so the user's terminal palette overrides remain
53
- * effective.
54
- * 2. Specific hex values are used wherever the dark palette assumed a dark
55
- * background (notably accent/code/trace colors and message bubbles).
56
- * Each hex was picked to clear WCAG AA contrast (4.5:1) against a near-
57
- * white background (#fafafa) or, when applicable, against the explicit
58
- * surface color in the same palette (e.g. diffAddFg vs diffAdd).
50
+ * Light palette aligned with the restored OpenTUI runtime: paper-neutral
51
+ * surfaces, blue focus/user rails, warm command accent, and semantic tool
52
+ * colors tuned for readable contrast on a light terminal background.
59
53
  */
60
54
  export const lightTheme = {
61
- user: "green",
62
- agent: "blue",
63
- error: "red",
64
- warning: "#9A6500", // ANSI yellow is invisible on white — go to dark amber.
65
- success: "green",
66
- accent: "#0E5A85", // dark teal — replaces "cyan" which washes out on white.
67
- border: "gray",
68
- borderActive: "#0E5A85",
69
- inputBorder: "#6B5FB8",
70
- inputBorderDisabled: "#c5c3d0",
71
- inputBg: "#eeeef6",
72
- inputBgDisabled: "#e2e2ec",
73
- inputText: "#1c1c24",
74
- inputPlaceholder: "#7a7886",
75
- muted: "gray",
76
- dim: "gray",
77
- thinking: "magenta",
78
- thinkingDim: "gray",
79
- toolName: "#0E5A85",
80
- toolResult: "gray",
81
- toolError: "red",
82
- toolPending: "#9A6500",
83
- code: "#9A6500",
84
- traceAction: "#B85A20",
85
- traceCount: "#5a5a5a",
86
- traceDetail: "gray",
87
- traceCommand: "#1A5FA0",
88
- tracePending: "#9A6500",
89
- userMessageBorder: "#6B5FB8",
90
- userMessageBg: "#e8e6f4",
91
- userMessageText: "#1c1c24",
92
- userRail: "#6B5FB8",
93
- diffAdd: "#d4f4d4",
94
- diffRemove: "#f4d4d4",
95
- diffAddFg: "#1c1c24",
96
- diffRemoveFg: "#1c1c24",
55
+ user: "#356FD2",
56
+ agent: "#171717",
57
+ error: "#B62633",
58
+ warning: "#8B4A00",
59
+ success: "#2F7D4A",
60
+ accent: "#8B4A00",
61
+ border: "#B9BDB8",
62
+ borderActive: "#356FD2",
63
+ inputBorder: "#356FD2",
64
+ inputBorderDisabled: "#D7DAD4",
65
+ inputBg: "#F1F3F0",
66
+ inputBgDisabled: "#E6E8E3",
67
+ inputText: "#171717",
68
+ inputPlaceholder: "#6F7377",
69
+ muted: "#6F7377",
70
+ dim: "#8B9094",
71
+ thinking: "#5F666D",
72
+ thinkingDim: "#8B9094",
73
+ toolName: "#495057",
74
+ toolResult: "#171717",
75
+ toolError: "#B62633",
76
+ toolPending: "#8B4A00",
77
+ code: "#2F7D4A",
78
+ traceAction: "#8B4A00",
79
+ traceCount: "#6F7377",
80
+ traceDetail: "#8B9094",
81
+ traceCommand: "#257E8A",
82
+ tracePending: "#8B4A00",
83
+ userMessageBorder: "#356FD2",
84
+ userMessageBg: "#F1F3F0",
85
+ userMessageText: "#234B93",
86
+ userRail: "#356FD2",
87
+ diffAdd: "#D7E8D8",
88
+ diffRemove: "#F7DADC",
89
+ diffAddFg: "#173D2D",
90
+ diffRemoveFg: "#5D1922",
97
91
  };
98
92
  const ThemeContext = createContext(darkTheme);
99
93
  export const ThemeProvider = ThemeContext.Provider;
@@ -323,7 +323,7 @@ function isFailedSubagent(subagent) {
323
323
  return subagent.status === "failed" || subagent.status === "blocked" || subagent.status === "cancelled";
324
324
  }
325
325
  function isToolPending(tool) {
326
- return tool.result === undefined;
326
+ return tool.result === undefined && tool.resultCollapsed !== true;
327
327
  }
328
328
  function isDirectoryLikeGlob(pattern) {
329
329
  const normalized = pattern.trim();
@@ -3,80 +3,10 @@ import React from "react";
3
3
  import { Box, Text } from "ink";
4
4
  import { createRequire } from "node:module";
5
5
  import { useTheme } from "./theme.js";
6
+ import { BUBBLE_COMPACT_WORDMARK, BUBBLE_WORDMARK, bubbleWordmarkLineText, bubbleWordmarkMaxWidth, } from "../tui/wordmark.js";
6
7
  const require = createRequire(import.meta.url);
7
8
  const PACKAGE_VERSION = readPackageVersion();
8
- const BUBBLE_LOGO_LETTERS = [
9
- [
10
- "██████ ",
11
- "██ ██",
12
- "██ ██",
13
- "██████ ",
14
- "██ ██",
15
- "██ ██",
16
- "██████ ",
17
- ],
18
- [
19
- "██ ██",
20
- "██ ██",
21
- "██ ██",
22
- "██ ██",
23
- "██ ██",
24
- "██ ██",
25
- " █████ ",
26
- ],
27
- [
28
- "██████ ",
29
- "██ ██",
30
- "██ ██",
31
- "██████ ",
32
- "██ ██",
33
- "██ ██",
34
- "██████ ",
35
- ],
36
- [
37
- "██████ ",
38
- "██ ██",
39
- "██ ██",
40
- "██████ ",
41
- "██ ██",
42
- "██ ██",
43
- "██████ ",
44
- ],
45
- [
46
- "██ ",
47
- "██ ",
48
- "██ ",
49
- "██ ",
50
- "██ ",
51
- "██ ",
52
- "███████",
53
- ],
54
- [
55
- "███████",
56
- "██ ",
57
- "██ ",
58
- "██████ ",
59
- "██ ",
60
- "██ ",
61
- "███████",
62
- ],
63
- ];
64
- /**
65
- * Derive a 6-step logo gradient from the active theme tokens so the banner
66
- * stays readable on both dark and light backgrounds.
67
- */
68
- function logoColors(theme) {
69
- return [
70
- theme.userMessageText,
71
- theme.userMessageText,
72
- theme.inputBorder,
73
- theme.inputBorder,
74
- theme.traceCommand,
75
- theme.traceCommand,
76
- ];
77
- }
78
- const COMPACT_LOGO = ["B", "U", "B", "B", "L", "E"];
79
- const WIDE_LOGO_MIN_WIDTH = 52;
9
+ const WIDE_LOGO_MIN_WIDTH = bubbleWordmarkMaxWidth(BUBBLE_WORDMARK) + 4;
80
10
  export function shouldShowWelcomeBanner({ startedWithVisibleHistory, }) {
81
11
  // Keep banner visibility tied to the initial history, not transient overlays,
82
12
  // so opening and closing a picker does not move it in the transcript.
@@ -96,18 +26,32 @@ export function WelcomeBanner({ terminalColumns, modelLabel, cwd, tips, skillsCo
96
26
  : "Type / for commands and @ to reference files";
97
27
  const modelLine = modelLabel ? `${modelLabel}${cwd ? ` · ${cwd}` : ""}` : cwd;
98
28
  return (_jsxs(Box, { width: effectiveWidth, flexDirection: "column", alignItems: "center", marginBottom: 1, children: [_jsx(Box, { flexDirection: "column", alignItems: "center", children: useWideLogo
99
- ? BUBBLE_LOGO_LETTERS[0].map((_, rowIndex) => (_jsx(LogoRow, { rowIndex: rowIndex }, `logo-row-${rowIndex}`)))
29
+ ? BUBBLE_WORDMARK.map((line, rowIndex) => (_jsx(LogoRow, { line: line }, `logo-row-${rowIndex}`)))
100
30
  : _jsx(CompactLogo, {}) }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { bold: true, color: theme.muted, children: PACKAGE_VERSION }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, color: theme.userMessageText, children: "TIP: " }), _jsx(Text, { bold: true, color: theme.userMessageText, children: tip })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "shift+tab to cycle modes \u00B7 ctrl+r for reasoning \u00B7 ctrl+o for trace" }) }), modelLine && (_jsx(Box, { children: _jsx(Text, { color: theme.muted, children: truncateToWidth(modelLine, effectiveWidth - 4) }) })), _jsxs(Box, { marginTop: 1, children: [_jsx(StatusItem, { label: "Skills", count: skillsCount, ok: skillsCount > 0 }), _jsx(Text, { color: theme.muted, children: " " }), _jsx(StatusItem, { label: "MCPs", count: mcpConnectedCount, total: mcpTotalCount, ok: mcpTotalCount === 0 || mcpConnectedCount === mcpTotalCount }), _jsx(Text, { color: theme.muted, children: " " }), _jsx(StatusItem, { label: "AGENTS.md", ok: hasAgentsFile })] })] }));
101
31
  }
102
- function LogoRow({ rowIndex }) {
32
+ function LogoRow({ line }) {
103
33
  const theme = useTheme();
104
- const colors = logoColors(theme);
105
- return (_jsx(Box, { children: BUBBLE_LOGO_LETTERS.map((letter, index) => (_jsxs(React.Fragment, { children: [_jsx(Text, { bold: true, color: colors[index], children: letter[rowIndex] }), index < BUBBLE_LOGO_LETTERS.length - 1 && _jsx(Text, { children: " " })] }, `${index}-${rowIndex}`))) }));
34
+ if (!line.segments) {
35
+ return _jsx(Text, { bold: true, color: logoColor(theme, line.tone ?? "caption"), children: line.text ?? "" });
36
+ }
37
+ return (_jsx(Box, { children: line.segments.map((segment, index) => (_jsx(React.Fragment, { children: _jsx(Text, { bold: true, color: logoColor(theme, segment.tone), children: segment.text }) }, `${index}-${segment.text}`))) }));
106
38
  }
107
39
  function CompactLogo() {
108
40
  const theme = useTheme();
109
- const colors = logoColors(theme);
110
- return (_jsx(Box, { children: COMPACT_LOGO.map((letter, index) => (_jsx(Text, { bold: true, color: colors[index], children: letter }, `${letter}-${index}`))) }));
41
+ const line = BUBBLE_COMPACT_WORDMARK[0];
42
+ if (!line?.segments) {
43
+ return _jsx(Text, { bold: true, color: theme.warning, children: bubbleWordmarkLineText(line ?? { text: "" }) });
44
+ }
45
+ return (_jsx(Box, { children: line.segments.map((segment, index) => (_jsx(Text, { bold: true, color: logoColor(theme, segment.tone), children: segment.text }, `${segment.text}-${index}`))) }));
46
+ }
47
+ function logoColor(theme, tone) {
48
+ switch (tone) {
49
+ case "brand": return theme.warning;
50
+ case "ink": return theme.userMessageText;
51
+ case "stone": return theme.muted;
52
+ case "soft": return theme.dim;
53
+ case "caption": return theme.muted;
54
+ }
111
55
  }
112
56
  function StatusItem({ label, count, total, ok, }) {
113
57
  const theme = useTheme();
@@ -0,0 +1,54 @@
1
+ /** @jsxImportSource @opentui/react */
2
+ import React from "react";
3
+ import { type Agent } from "../agent.js";
4
+ import type { CliArgs } from "../cli.js";
5
+ import type { SessionManager } from "../session.js";
6
+ import type { PlanDecision, Provider } from "../types.js";
7
+ import { type ResolvedTheme, type ThemeMode } from "./theme.js";
8
+ import { ProviderRegistry } from "../provider-registry.js";
9
+ import { SkillRegistry } from "../skills/registry.js";
10
+ import type { ApprovalDecision, ApprovalRequest } from "../approval/types.js";
11
+ import type { BashAllowlist } from "../approval/session-cache.js";
12
+ import type { SettingsManager } from "../permissions/settings.js";
13
+ import type { McpManager } from "../mcp/manager.js";
14
+ import type { LspService } from "../lsp/index.js";
15
+ import type { QuestionController } from "../question/index.js";
16
+ import type { MemoryScope } from "../memory/index.js";
17
+ export interface PlanHandlerRef {
18
+ current?: (plan: string) => Promise<PlanDecision>;
19
+ }
20
+ export interface ApprovalHandlerRef {
21
+ current?: (req: ApprovalRequest) => Promise<ApprovalDecision>;
22
+ }
23
+ interface AppProps {
24
+ agent: Agent;
25
+ args: CliArgs;
26
+ sessionManager?: SessionManager;
27
+ createProvider?: (providerId: string, apiKey: string, baseURL: string) => Provider;
28
+ registry?: ProviderRegistry;
29
+ skillRegistry?: SkillRegistry;
30
+ planHandlerRef?: PlanHandlerRef;
31
+ approvalHandlerRef?: ApprovalHandlerRef;
32
+ questionController?: QuestionController;
33
+ bashAllowlist?: BashAllowlist;
34
+ settingsManager?: SettingsManager;
35
+ lspService?: LspService;
36
+ mcpManager?: McpManager;
37
+ themeMode?: ThemeMode;
38
+ themeOverrides?: Record<string, string>;
39
+ detectedTheme?: ResolvedTheme;
40
+ onThemeModeChange?: (mode: ThemeMode) => void;
41
+ flushMemory?: () => Promise<void>;
42
+ runMemoryCompaction?: () => Promise<string>;
43
+ runMemorySummary?: (scope?: MemoryScope) => Promise<string>;
44
+ runMemoryRefresh?: (scope?: MemoryScope) => Promise<string>;
45
+ /** Whether the bypassPermissions mode is reachable via Shift+Tab cycling. */
46
+ bypassEnabled?: boolean;
47
+ onExit?: (summary: ExitSummary) => void;
48
+ }
49
+ export interface ExitSummary {
50
+ /** Wall-clock duration of the session, in milliseconds. */
51
+ wallMs: number;
52
+ }
53
+ export declare function App({ agent, args, sessionManager, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, bypassEnabled, onExit }: AppProps): React.ReactNode;
54
+ export {};