@bubblebrain-ai/bubble 0.0.4 → 0.0.5

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 (77) hide show
  1. package/dist/agent/budget-ledger.d.ts +20 -0
  2. package/dist/agent/budget-ledger.js +51 -0
  3. package/dist/agent/execution-governor.js +1 -1
  4. package/dist/agent/profiles.d.ts +59 -0
  5. package/dist/agent/profiles.js +460 -0
  6. package/dist/agent/subagent-control.d.ts +52 -0
  7. package/dist/agent/subagent-control.js +38 -0
  8. package/dist/agent.d.ts +60 -1
  9. package/dist/agent.js +602 -53
  10. package/dist/context/budget.js +1 -0
  11. package/dist/context/compact-llm.js +7 -6
  12. package/dist/context/compact.js +6 -6
  13. package/dist/context/projector.d.ts +3 -3
  14. package/dist/context/projector.js +32 -18
  15. package/dist/context/prune.d.ts +2 -2
  16. package/dist/context/prune.js +1 -4
  17. package/dist/main.js +12 -5
  18. package/dist/mcp/manager.js +1 -0
  19. package/dist/orchestrator/default-hooks.js +48 -9
  20. package/dist/orchestrator/hooks.d.ts +5 -0
  21. package/dist/prompt/compose.d.ts +1 -0
  22. package/dist/prompt/compose.js +8 -1
  23. package/dist/prompt/environment.js +21 -2
  24. package/dist/prompt/reminders.d.ts +3 -1
  25. package/dist/prompt/reminders.js +23 -4
  26. package/dist/prompt/runtime.d.ts +1 -1
  27. package/dist/prompt/runtime.js +1 -1
  28. package/dist/provider-artifacts.d.ts +7 -0
  29. package/dist/provider-artifacts.js +60 -0
  30. package/dist/provider.d.ts +6 -7
  31. package/dist/provider.js +77 -15
  32. package/dist/session-log.js +3 -1
  33. package/dist/system-prompt.d.ts +2 -0
  34. package/dist/tools/agent-lifecycle.d.ts +6 -0
  35. package/dist/tools/agent-lifecycle.js +355 -0
  36. package/dist/tools/bash.js +2 -0
  37. package/dist/tools/edit-apply.d.ts +25 -0
  38. package/dist/tools/edit-apply.js +197 -0
  39. package/dist/tools/edit.js +63 -56
  40. package/dist/tools/exit-plan-mode.js +3 -1
  41. package/dist/tools/file-mutation-queue.d.ts +1 -0
  42. package/dist/tools/file-mutation-queue.js +32 -0
  43. package/dist/tools/glob.js +1 -0
  44. package/dist/tools/grep.js +1 -0
  45. package/dist/tools/index.d.ts +1 -1
  46. package/dist/tools/index.js +3 -3
  47. package/dist/tools/lsp.js +2 -0
  48. package/dist/tools/memory.js +2 -0
  49. package/dist/tools/question.js +2 -0
  50. package/dist/tools/read.js +1 -0
  51. package/dist/tools/skill.js +1 -0
  52. package/dist/tools/task.js +1 -0
  53. package/dist/tools/todo.js +1 -0
  54. package/dist/tools/tool-search.js +2 -1
  55. package/dist/tools/web-fetch.js +1 -0
  56. package/dist/tools/web-search.js +1 -0
  57. package/dist/tools/write.js +2 -0
  58. package/dist/tui/display-history.d.ts +8 -1
  59. package/dist/tui/markdown-inline.d.ts +22 -0
  60. package/dist/tui/markdown-inline.js +68 -0
  61. package/dist/tui/render-signature.d.ts +1 -0
  62. package/dist/tui/render-signature.js +7 -0
  63. package/dist/tui/run.js +712 -267
  64. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  65. package/dist/tui/tool-renderers/fallback.js +75 -0
  66. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  67. package/dist/tui/tool-renderers/registry.js +11 -0
  68. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  69. package/dist/tui/tool-renderers/subagent.js +114 -0
  70. package/dist/tui/tool-renderers/types.d.ts +36 -0
  71. package/dist/tui/tool-renderers/types.js +1 -0
  72. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  73. package/dist/tui/tool-renderers/write-preview.js +22 -0
  74. package/dist/tui/tool-renderers/write.d.ts +6 -0
  75. package/dist/tui/tool-renderers/write.js +82 -0
  76. package/dist/types.d.ts +90 -10
  77. package/package.json +1 -1
package/dist/tui/run.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BoxRenderable, CodeRenderable, createCliRenderer, DiffRenderable, getTreeSitterClient, MarkdownRenderable, LineNumberRenderable, StyledText, RGBA, fg, bg, bold, dim, TextAttributes, TextRenderable, } from "@opentui/core";
1
+ import { BoxRenderable, CodeRenderable, createCliRenderer, DiffRenderable, getTreeSitterClient, MarkdownRenderable, LineNumberRenderable, StyledText, RGBA, fg, bg, bold, italic, dim, TextAttributes, TextRenderable, } from "@opentui/core";
2
2
  import { createComponent, createElement, insert, render, spread, useKeyboard, useRenderer, useSelectionHandler, useTerminalDimensions, } from "@opentui/solid";
3
3
  import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
4
4
  import { AgentAbortError } from "../agent.js";
@@ -13,6 +13,11 @@ import { sidebarMcpRowsFromStates, renderMcpRowMarker } from "./sidebar-mcp.js";
13
13
  import { expandAtMentions, filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
14
14
  import { compactDisplayMessages } from "./display-history.js";
15
15
  import { createMarkdownSyntaxStyle, createSubtleMarkdownSyntaxStyle } from "./markdown-theme.js";
16
+ import { markdownInlineSegments } from "./markdown-inline.js";
17
+ import { hashString } from "./render-signature.js";
18
+ import { findToolRenderer } from "./tool-renderers/registry.js";
19
+ import { writeToolKey } from "./tool-renderers/write.js";
20
+ import { formatWritePreview, isWritePreviewTool } from "./tool-renderers/write-preview.js";
16
21
  import { getNextPermissionMode, PERMISSION_MODE_INFO } from "../permission/mode.js";
17
22
  import { getContextBudget } from "../context/budget.js";
18
23
  import { getLspService } from "../lsp/index.js";
@@ -55,6 +60,7 @@ const DEFAULT_THEME = {
55
60
  messageAssistantText: "#eeeeee",
56
61
  messageAssistantAccent: "#fab283",
57
62
  messageThinkingText: "#8b949e",
63
+ messageThinkingContentText: "#6e7681",
58
64
  messageThinkingBorder: "#282828",
59
65
  toolText: "#a6acb8",
60
66
  toolPending: "#fab283",
@@ -103,10 +109,31 @@ const QUESTION_MAX_OPTIONS = 10;
103
109
  const QUESTION_MAX_CONFIRM_ROWS = 3;
104
110
  const QUESTION_PANEL_MIN_HEIGHT = 9;
105
111
  const HOME_LOGO = [
106
- " /\\_/\\ █▀▀▄ █ █ █▀▀▄ █▀▀▄ █ █▀▀",
107
- "( o.o ) █▀▀▄ █ █ █▀▀▄ █▀▀▄ █ █▀▀",
108
- " > ^ < ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀▀ ▀▀▀▀",
112
+ { text: " /\\___/\\ ", tone: "primary" },
113
+ { text: "( ◕ ◕ )", tone: "primary" },
114
+ { text: "( ω )", tone: "warning" },
115
+ { text: " (\")_(\") ", tone: "warning" },
116
+ { text: "", tone: "primary" },
117
+ { text: "· ◌ ○ ◯ ·", tone: "textMuted" },
118
+ { text: "", tone: "primary" },
119
+ { text: "██████╗ ██╗ ██╗██████╗ ██████╗ ██╗ ███████╗", tone: "primary" },
120
+ { text: "██╔══██╗██║ ██║██╔══██╗██╔══██╗██║ ██╔════╝", tone: "primary" },
121
+ { text: "██████╔╝██║ ██║██████╔╝██████╔╝██║ █████╗ ", tone: "warning" },
122
+ { text: "██╔══██╗██║ ██║██╔══██╗██╔══██╗██║ ██╔══╝ ", tone: "warning" },
123
+ { text: "██████╔╝╚██████╔╝██████╔╝██████╔╝███████╗███████╗", tone: "accent" },
124
+ { text: "╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝", tone: "accent" },
125
+ { text: "", tone: "primary" },
126
+ { text: "── your bubble coding companion ──", tone: "secondary" },
109
127
  ];
128
+ function homeLogoColor(tone) {
129
+ switch (tone) {
130
+ case "primary": return theme.primary;
131
+ case "warning": return theme.warning;
132
+ case "accent": return theme.accent;
133
+ case "secondary": return theme.secondary;
134
+ case "textMuted": return theme.textMuted;
135
+ }
136
+ }
110
137
  const HOME_TIPS = [
111
138
  "Type @ followed by a filename to attach file context",
112
139
  "Press Shift+Tab to cycle Build, Plan, and Bypass modes",
@@ -318,20 +345,16 @@ function OpenTuiApp(props) {
318
345
  let rootBox;
319
346
  let sidebarShell;
320
347
  let transcriptHost;
321
- const transcriptState = { entries: [], expandedThinking: new Set(), expandedCompactions: new Set() };
348
+ const transcriptState = {
349
+ entries: [],
350
+ expandedCompactions: new Set(),
351
+ expandedWrites: new Set(),
352
+ defaultWritesExpanded: false,
353
+ };
322
354
  let dock;
323
355
  let homeComposerShell;
324
356
  let sessionComposerShell;
325
357
  const promptScannerSyncs = new Set();
326
- const thinkingSpinnerFrames = createFrames({
327
- width: 4,
328
- color: theme.primary,
329
- style: "blocks",
330
- inactiveFactor: 0.45,
331
- minAlpha: 0.25,
332
- });
333
- let thinkingSpinnerFrameIndex = 0;
334
- let thinkingSpinnerTimer;
335
358
  let approvalRoot;
336
359
  let approvalHeaderTitle;
337
360
  let approvalMetaIcon;
@@ -1524,7 +1547,6 @@ function OpenTuiApp(props) {
1524
1547
  for (const timer of questionSyncTimers)
1525
1548
  clearTimeout(timer);
1526
1549
  questionSyncTimers.clear();
1527
- stopThinkingSpinner();
1528
1550
  if (props.options.planHandlerRef)
1529
1551
  props.options.planHandlerRef.current = undefined;
1530
1552
  if (props.options.approvalHandlerRef)
@@ -1668,6 +1690,16 @@ function OpenTuiApp(props) {
1668
1690
  event.preventDefault?.();
1669
1691
  return true;
1670
1692
  }
1693
+ if (event.ctrl && name === "t" && !picker) {
1694
+ toggleThinkingVisibility();
1695
+ event.preventDefault?.();
1696
+ return true;
1697
+ }
1698
+ if (event.ctrl && name === "o" && !picker) {
1699
+ toggleVisibleWriteBlocks();
1700
+ event.preventDefault?.();
1701
+ return true;
1702
+ }
1671
1703
  if (routeModalKey(event))
1672
1704
  return true;
1673
1705
  if (cycleModeFromKey(event))
@@ -1688,12 +1720,12 @@ function OpenTuiApp(props) {
1688
1720
  plan: pendingPlan()?.plan,
1689
1721
  selectedOption: approvalOptionIdx(),
1690
1722
  showThinking: showThinking(),
1691
- onToggleThinking: (key) => {
1692
- if (transcriptState.expandedThinking.has(key)) {
1693
- transcriptState.expandedThinking.delete(key);
1723
+ onToggleWrite: (key) => {
1724
+ if (transcriptState.expandedWrites.has(key)) {
1725
+ transcriptState.expandedWrites.delete(key);
1694
1726
  }
1695
1727
  else {
1696
- transcriptState.expandedThinking.add(key);
1728
+ transcriptState.expandedWrites.add(key);
1697
1729
  }
1698
1730
  syncSessionMessages();
1699
1731
  },
@@ -1708,52 +1740,61 @@ function OpenTuiApp(props) {
1708
1740
  },
1709
1741
  };
1710
1742
  }
1711
- function syncSessionMessages(messages = currentTranscriptMessages(streamingDisplay)) {
1712
- if (!transcriptHost)
1743
+ function toggleThinkingVisibility() {
1744
+ if (!currentTranscriptMessages(streamingDisplay).some((message) => !!message.reasoning?.trim())) {
1745
+ setNotice("No thinking blocks to toggle");
1713
1746
  return;
1714
- updateTranscriptHost(transcriptHost, transcriptState, messages, transcriptOptions(), props.syntaxStyle, props.subtleSyntaxStyle);
1715
- syncThinkingSpinner();
1716
- syncPromptSurfaces();
1717
- }
1718
- function renderThinkingSpinnerFrame() {
1719
- const frame = thinkingSpinnerFrames[thinkingSpinnerFrameIndex % thinkingSpinnerFrames.length] ?? "";
1720
- let rendered = false;
1721
- for (const entry of transcriptState.entries) {
1722
- const ref = entry.refs.reasoningToggleText;
1723
- if (!ref || !entry.refs.reasoningStreaming)
1724
- continue;
1725
- ref.content = thinkingToggleLabel(entry.refs.reasoningExpanded === true, true, frame);
1726
- ref.requestRender();
1727
- rendered = true;
1728
1747
  }
1729
- if (rendered) {
1730
- transcriptHost?.requestRender();
1731
- rootBox?.requestRender();
1732
- }
1733
- }
1734
- function stopThinkingSpinner() {
1735
- if (thinkingSpinnerTimer)
1736
- clearInterval(thinkingSpinnerTimer);
1737
- thinkingSpinnerTimer = undefined;
1738
- thinkingSpinnerFrameIndex = 0;
1748
+ setShowThinking((prev) => {
1749
+ const next = !prev;
1750
+ setNotice(next ? "Thinking blocks visible" : "Thinking blocks hidden");
1751
+ return next;
1752
+ });
1753
+ redrawTranscript();
1739
1754
  }
1740
- function syncThinkingSpinner() {
1741
- const hasStreamingThinking = transcriptState.entries.some((entry) => !!entry.refs.reasoningToggleText && entry.refs.reasoningStreaming === true);
1742
- if (!hasStreamingThinking) {
1743
- stopThinkingSpinner();
1755
+ function toggleVisibleWriteBlocks() {
1756
+ const keys = collectVisibleWriteKeys();
1757
+ if (!keys.length) {
1758
+ setNotice("No write previews to toggle");
1744
1759
  return;
1745
1760
  }
1746
- renderThinkingSpinnerFrame();
1747
- if (thinkingSpinnerTimer)
1761
+ const shouldExpand = keys.some((key) => !transcriptState.expandedWrites.has(key));
1762
+ transcriptState.defaultWritesExpanded = shouldExpand;
1763
+ for (const key of keys) {
1764
+ if (shouldExpand)
1765
+ transcriptState.expandedWrites.add(key);
1766
+ else
1767
+ transcriptState.expandedWrites.delete(key);
1768
+ }
1769
+ setNotice(shouldExpand ? "Write previews expanded" : "Write previews collapsed");
1770
+ syncSessionMessages();
1771
+ }
1772
+ function collectVisibleWriteKeys() {
1773
+ const messages = currentTranscriptMessages(streamingDisplay)
1774
+ .filter((message) => hasRenderableMessage(message, showThinking()));
1775
+ const keys = [];
1776
+ for (const [index, message] of messages.entries()) {
1777
+ const messageKey = transcriptMessageKey(message, index);
1778
+ for (const tool of message.toolCalls ?? []) {
1779
+ if (isWritePreviewTool(tool)) {
1780
+ keys.push(writeToolKey(messageKey, tool));
1781
+ }
1782
+ }
1783
+ }
1784
+ return keys;
1785
+ }
1786
+ function syncSessionMessages(messages = currentTranscriptMessages(streamingDisplay)) {
1787
+ if (!transcriptHost)
1748
1788
  return;
1749
- thinkingSpinnerTimer = setInterval(() => {
1750
- thinkingSpinnerFrameIndex = (thinkingSpinnerFrameIndex + 1) % thinkingSpinnerFrames.length;
1751
- renderThinkingSpinnerFrame();
1752
- }, PROMPT_SCANNER_INTERVAL_MS);
1789
+ updateTranscriptHost(transcriptHost, transcriptState, messages, transcriptOptions(), props.syntaxStyle, props.subtleSyntaxStyle);
1790
+ syncPromptSurfaces();
1753
1791
  }
1754
1792
  function redrawTranscript(extra, baseMessages = displayMessages) {
1755
- const shouldFollow = shouldFollowTranscriptBeforeUpdate();
1756
1793
  streamingDisplay = extra;
1794
+ renderTranscriptNow(streamingDisplay, baseMessages);
1795
+ }
1796
+ function renderTranscriptNow(extra, baseMessages = displayMessages) {
1797
+ const shouldFollow = shouldFollowTranscriptBeforeUpdate();
1757
1798
  const nextMessages = compactDisplayMessages(extra ? [...baseMessages, extra] : baseMessages);
1758
1799
  syncSessionMessages(nextMessages);
1759
1800
  rootBox?.requestRender();
@@ -3078,12 +3119,7 @@ function OpenTuiApp(props) {
3078
3119
  }
3079
3120
  async function executeSlash(input) {
3080
3121
  if (/^\/(?:thinking|toggle-thinking)(?:\s|$)/.test(input.trim())) {
3081
- setShowThinking((prev) => {
3082
- const next = !prev;
3083
- setNotice(next ? "Thinking blocks visible" : "Thinking blocks hidden");
3084
- return next;
3085
- });
3086
- redrawTranscript();
3122
+ toggleThinkingVisibility();
3087
3123
  return true;
3088
3124
  }
3089
3125
  const wasHomeSurfaceActive = isHomeSurfaceActive();
@@ -3525,6 +3561,8 @@ function OpenTuiApp(props) {
3525
3561
  let assistantContent = "";
3526
3562
  let assistantReasoning = "";
3527
3563
  const toolCalls = [];
3564
+ let currentTurnHasToolCall = false;
3565
+ let turnStartedAt;
3528
3566
  let runError;
3529
3567
  let runCancelled = false;
3530
3568
  try {
@@ -3533,46 +3571,72 @@ function OpenTuiApp(props) {
3533
3571
  assistantContent = "";
3534
3572
  assistantReasoning = "";
3535
3573
  toolCalls.length = 0;
3574
+ currentTurnHasToolCall = false;
3575
+ turnStartedAt = Date.now();
3536
3576
  redrawTranscript({
3537
3577
  role: "assistant",
3538
3578
  content: "",
3539
3579
  status: "thinking",
3540
3580
  streaming: true,
3581
+ turnStartedAt,
3541
3582
  });
3542
3583
  }
3543
3584
  else if (event.type === "text_delta") {
3544
3585
  assistantContent += event.content;
3586
+ }
3587
+ else if (event.type === "reasoning_delta") {
3588
+ assistantReasoning += event.content;
3545
3589
  redrawTranscript({
3546
3590
  role: "assistant",
3547
- content: assistantContent,
3591
+ content: "",
3548
3592
  reasoning: assistantReasoning || undefined,
3549
3593
  toolCalls: toolCalls.length ? [...toolCalls] : undefined,
3550
- status: "responding",
3594
+ status: "thinking",
3551
3595
  streaming: true,
3596
+ turnStartedAt,
3552
3597
  });
3553
3598
  }
3554
- else if (event.type === "reasoning_delta") {
3555
- assistantReasoning += event.content;
3599
+ else if (event.type === "tool_call_start") {
3600
+ currentTurnHasToolCall = true;
3556
3601
  redrawTranscript({
3557
3602
  role: "assistant",
3558
- content: assistantContent,
3603
+ content: "",
3559
3604
  reasoning: assistantReasoning || undefined,
3560
3605
  toolCalls: toolCalls.length ? [...toolCalls] : undefined,
3561
- status: "thinking",
3606
+ status: toolCalls.length ? undefined : "thinking",
3562
3607
  streaming: true,
3608
+ turnStartedAt,
3563
3609
  });
3564
3610
  }
3611
+ else if (event.type === "tool_call_delta") {
3612
+ currentTurnHasToolCall = true;
3613
+ }
3614
+ else if (event.type === "tool_call_end") {
3615
+ currentTurnHasToolCall = true;
3616
+ }
3565
3617
  else if (event.type === "tool_start") {
3566
- toolCalls.push({ id: event.id, name: event.name, args: event.args, status: "running" });
3618
+ currentTurnHasToolCall = true;
3619
+ const now = Date.now();
3620
+ const existing = toolCalls.find((item) => item.id === event.id);
3621
+ if (existing) {
3622
+ existing.args = event.args;
3623
+ existing.streamingArgs = false;
3624
+ existing.status = "running";
3625
+ existing.startedAt = existing.startedAt ?? now;
3626
+ }
3627
+ else {
3628
+ toolCalls.push({ id: event.id, name: event.name, args: event.args, status: "running", startedAt: now });
3629
+ }
3567
3630
  if (event.name === "question") {
3568
3631
  scheduleQuestionSync();
3569
3632
  }
3570
3633
  redrawTranscript({
3571
3634
  role: "assistant",
3572
- content: assistantContent,
3635
+ content: "",
3573
3636
  reasoning: assistantReasoning || undefined,
3574
3637
  toolCalls: [...toolCalls],
3575
3638
  streaming: true,
3639
+ turnStartedAt,
3576
3640
  });
3577
3641
  }
3578
3642
  else if (event.type === "tool_end") {
@@ -3582,12 +3646,14 @@ function OpenTuiApp(props) {
3582
3646
  call.isError = event.result.isError;
3583
3647
  call.metadata = event.result.metadata;
3584
3648
  call.status = event.result.isError ? "error" : "completed";
3649
+ call.completedAt = Date.now();
3585
3650
  redrawTranscript({
3586
3651
  role: "assistant",
3587
- content: assistantContent,
3652
+ content: currentTurnHasToolCall ? "" : assistantContent,
3588
3653
  reasoning: assistantReasoning || undefined,
3589
3654
  toolCalls: [...toolCalls],
3590
3655
  streaming: true,
3656
+ turnStartedAt,
3591
3657
  });
3592
3658
  }
3593
3659
  if (event.name === "question") {
@@ -3596,6 +3662,30 @@ function OpenTuiApp(props) {
3596
3662
  refreshGitSidebar();
3597
3663
  syncSidebarLsp();
3598
3664
  }
3665
+ else if (event.type === "tool_update") {
3666
+ const call = toolCalls.find((item) => item.id === event.id);
3667
+ if (call) {
3668
+ call.metadata = mergeToolMetadata(call.metadata, event.update.metadata);
3669
+ call.result = event.update.message ?? call.result;
3670
+ const finished = event.update.status === "failed" || event.update.status === "blocked" || event.update.status === "cancelled" || event.update.status === "completed";
3671
+ call.status = event.update.status === "failed" || event.update.status === "blocked" || event.update.status === "cancelled"
3672
+ ? "error"
3673
+ : event.update.status === "completed"
3674
+ ? "completed"
3675
+ : "running";
3676
+ call.isError = call.status === "error";
3677
+ if (finished && call.completedAt === undefined)
3678
+ call.completedAt = Date.now();
3679
+ redrawTranscript({
3680
+ role: "assistant",
3681
+ content: currentTurnHasToolCall ? "" : assistantContent,
3682
+ reasoning: assistantReasoning || undefined,
3683
+ toolCalls: [...toolCalls],
3684
+ streaming: true,
3685
+ turnStartedAt,
3686
+ });
3687
+ }
3688
+ }
3599
3689
  else if (event.type === "todos_updated") {
3600
3690
  setTodos(event.todos);
3601
3691
  syncSidebarTodos(event.todos);
@@ -3622,9 +3712,12 @@ function OpenTuiApp(props) {
3622
3712
  bumpSidebar();
3623
3713
  const assistantMessage = {
3624
3714
  role: "assistant",
3625
- content: assistantContent,
3715
+ content: currentTurnHasToolCall ? "" : assistantContent,
3626
3716
  reasoning: assistantReasoning || undefined,
3627
3717
  toolCalls: toolCalls.length ? [...toolCalls] : undefined,
3718
+ turnStartedAt,
3719
+ turnCompletedAt: Date.now(),
3720
+ turnUsage: event.usage,
3628
3721
  };
3629
3722
  const nextMessages = hasRenderableMessage(assistantMessage)
3630
3723
  ? [...displayMessages, assistantMessage]
@@ -3634,6 +3727,7 @@ function OpenTuiApp(props) {
3634
3727
  assistantContent = "";
3635
3728
  assistantReasoning = "";
3636
3729
  toolCalls.length = 0;
3730
+ turnStartedAt = undefined;
3637
3731
  streamingDisplay = undefined;
3638
3732
  }
3639
3733
  }
@@ -3736,7 +3830,7 @@ function OpenTuiApp(props) {
3736
3830
  paddingLeft: 2,
3737
3831
  paddingRight: 2,
3738
3832
  }, [
3739
- h("box", { flexShrink: 0, flexDirection: "column" }, ...HOME_LOGO.map((line) => h("text", { fg: theme.primary }, line))),
3833
+ h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center" }, ...HOME_LOGO.map((line) => h("text", { fg: homeLogoColor(line.tone) }, line.text || " "))),
3740
3834
  h("box", { height: 1, minHeight: 0, flexShrink: 1 }),
3741
3835
  h("box", {
3742
3836
  ref: (ref) => {
@@ -4481,7 +4575,6 @@ function OpenTuiApp(props) {
4481
4575
  if (isNewHost)
4482
4576
  transcriptState.entries = [];
4483
4577
  updateTranscriptHost(ref, transcriptState, currentTranscriptMessages(streamingDisplay), transcriptOptions(), props.syntaxStyle, props.subtleSyntaxStyle);
4484
- syncThinkingSpinner();
4485
4578
  syncPromptSurfaces(isNewHost);
4486
4579
  if (isNewHost)
4487
4580
  scheduleTranscriptScrollAfterUpdate(transcriptScrollFollowing, 0);
@@ -4820,19 +4913,31 @@ function renderAssistantMessage(message, syntaxStyle, subtleSyntaxStyle, showThi
4820
4913
  borderColor: theme.messageThinkingBorder,
4821
4914
  flexDirection: "column",
4822
4915
  flexShrink: 0,
4823
- }, renderMarkdownContent(formatThinkingMarkdown(visibleReasoning), subtleSyntaxStyle, {
4916
+ }, h("text", { content: thinkingLabelContent(message.streaming === true, reasoningElapsedMs(message)), fg: theme.messageThinkingText, wrapMode: "none" }), renderMarkdownContent(formatThinkingMarkdown(visibleReasoning), subtleSyntaxStyle, {
4824
4917
  streaming: message.streaming === true,
4825
- fg: theme.messageThinkingText,
4918
+ fg: theme.messageThinkingContentText,
4826
4919
  })));
4827
4920
  }
4828
- for (const tool of message.toolCalls ?? [])
4921
+ const toolCalls = message.toolCalls ?? [];
4922
+ for (const tool of toolCalls)
4829
4923
  children.push(renderTool(tool, syntaxStyle, width));
4830
- if (message.content.trim()) {
4831
- children.push(h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, renderMarkdownContent(message.content.trim(), syntaxStyle, {
4924
+ const trimmedContent = message.content.trim();
4925
+ if (trimmedContent && toolCalls.length > 0) {
4926
+ children.push(h("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0 }, h("text", { content: answerDividerStyledText(), wrapMode: "none" })));
4927
+ }
4928
+ if (trimmedContent) {
4929
+ children.push(h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, renderMarkdownContent(trimmedContent, syntaxStyle, {
4832
4930
  streaming: message.streaming === true,
4833
4931
  fg: theme.messageAssistantText,
4834
4932
  })));
4835
4933
  }
4934
+ if (message.streaming === true && trimmedContent) {
4935
+ children.push(h("box", { paddingLeft: 3, flexShrink: 0 }, h("text", { fg: theme.primary, wrapMode: "none" }, "▌")));
4936
+ }
4937
+ const summaryString = formatTurnSummary(message);
4938
+ if (summaryString) {
4939
+ children.push(h("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0 }, h("text", { fg: theme.textMuted, wrapMode: "none" }, summaryString)));
4940
+ }
4836
4941
  if (!children.length)
4837
4942
  return null;
4838
4943
  return h("box", { flexDirection: "column", flexShrink: 0 }, children);
@@ -4911,12 +5016,23 @@ function updateTranscriptHost(host, state, messages, options, syntaxStyle, subtl
4911
5016
  }
4912
5017
  for (const [index, message] of visibleMessages.entries()) {
4913
5018
  const key = transcriptMessageKey(message, index);
4914
- const thinkingExpanded = state.expandedThinking.has(key);
5019
+ if (state.defaultWritesExpanded) {
5020
+ for (const tool of message.toolCalls ?? []) {
5021
+ if (isWritePreviewTool(tool)) {
5022
+ state.expandedWrites.add(writeToolKey(key, tool));
5023
+ }
5024
+ }
5025
+ }
4915
5026
  const compactionExpanded = state.expandedCompactions.has(key);
4916
- const signature = transcriptMessageSignature(message, showThinking, thinkingExpanded, compactionExpanded);
5027
+ const signature = transcriptMessageSignature(message, compactionExpanded);
4917
5028
  const previous = state.entries[index];
4918
5029
  if (previous?.key === key && previous.signature === signature) {
4919
- updateMessageEntry(previous, message, showThinking, thinkingExpanded, compactionExpanded);
5030
+ updateMessageEntry(previous, message, showThinking, compactionExpanded, {
5031
+ syntaxStyle,
5032
+ expandedWrites: state.expandedWrites,
5033
+ width: options?.width ?? 80,
5034
+ onToggleWrite: options?.onToggleWrite,
5035
+ });
4920
5036
  nextEntries.push(previous);
4921
5037
  continue;
4922
5038
  }
@@ -4924,7 +5040,7 @@ function updateTranscriptHost(host, state, messages, options, syntaxStyle, subtl
4924
5040
  host.remove(previous.node.id);
4925
5041
  previous.node.destroyRecursively();
4926
5042
  }
4927
- const entry = createMessageEntry(ctx, message, index, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking, options?.width ?? 80, thinkingExpanded, compactionExpanded, options?.onToggleThinking, options?.onToggleCompaction);
5043
+ const entry = createMessageEntry(ctx, message, index, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking, options?.width ?? 80, compactionExpanded, state.expandedWrites, options?.onToggleCompaction, options?.onToggleWrite);
4928
5044
  if (entry) {
4929
5045
  host.add(entry.node, index);
4930
5046
  nextEntries.push(entry);
@@ -4967,34 +5083,23 @@ function clearTranscriptEntries(host, state) {
4967
5083
  function transcriptMessageKey(message, index) {
4968
5084
  return `${index}:${message.role}`;
4969
5085
  }
4970
- function transcriptMessageSignature(message, showThinking = true, thinkingExpanded = false, compactionExpanded = false) {
5086
+ function transcriptMessageSignature(message, compactionExpanded = false) {
4971
5087
  if (message.role !== "assistant")
4972
5088
  return message.role;
4973
5089
  if (message.syntheticKind === "ui_compact_card") {
4974
5090
  return `compaction:${compactionExpanded ? "expanded" : "collapsed"}:${message.compactionMeta?.turns ?? 0}`;
4975
5091
  }
4976
5092
  const modelSwitch = parseModelSwitchMessage(message.content);
4977
- const tools = (message.toolCalls ?? [])
4978
- .map((tool) => `${tool.id}:${tool.name}:${tool.status ?? (tool.result === undefined ? "pending" : "completed")}:${tool.isError ? "error" : "ok"}`)
4979
- .join("|");
4980
- const visibleReasoning = showThinking && !!message.reasoning?.trim();
5093
+ const pureModelSwitch = modelSwitch && !message.reasoning?.trim() && !(message.toolCalls?.length);
5094
+ if (pureModelSwitch) {
5095
+ return `assistant:model-switch:${hashString(modelSwitch)}`;
5096
+ }
4981
5097
  return [
4982
5098
  message.role,
4983
- modelSwitch ? "model-switch" : "standard",
4984
- message.status ?? "idle",
4985
- visibleReasoning ? (thinkingExpanded ? "reasoning-expanded" : "reasoning-collapsed") : "no-reasoning",
4986
- message.content.trim() ? "content" : "no-content",
4987
- tools,
5099
+ "standard",
4988
5100
  ].join(":");
4989
5101
  }
4990
- function hashString(value) {
4991
- let hash = 5381;
4992
- for (let index = 0; index < value.length; index++) {
4993
- hash = ((hash << 5) + hash) ^ value.charCodeAt(index);
4994
- }
4995
- return (hash >>> 0).toString(36);
4996
- }
4997
- function updateMessageEntry(entry, message, showThinking = true, thinkingExpanded = false, compactionExpanded = false) {
5102
+ function updateMessageEntry(entry, message, showThinking = true, compactionExpanded = false, assistantOptions) {
4998
5103
  if (message.role === "user") {
4999
5104
  if (entry.refs.userText)
5000
5105
  entry.refs.userText.content = message.content || " ";
@@ -5018,21 +5123,150 @@ function updateMessageEntry(entry, message, showThinking = true, thinkingExpande
5018
5123
  }
5019
5124
  return;
5020
5125
  }
5126
+ if (assistantOptions) {
5127
+ updateAssistantEntry(entry, message, showThinking, assistantOptions);
5128
+ return;
5129
+ }
5130
+ }
5131
+ function updateAssistantEntry(entry, message, showThinking, options) {
5132
+ const content = message.content.trim();
5133
+ const visibleReasoning = showThinking ? message.reasoning?.trim() ?? "" : "";
5134
+ const tools = message.toolCalls ?? [];
5135
+ const showStatus = !!message.status && !visibleReasoning && !content && tools.length === 0;
5021
5136
  if (entry.refs.statusText) {
5022
- entry.refs.statusText.content = assistantStatusLabel(message);
5137
+ entry.refs.statusText.content = showStatus ? assistantStatusLabel(message) : "";
5138
+ }
5139
+ if (entry.refs.statusBox) {
5140
+ entry.refs.statusBox.visible = showStatus;
5023
5141
  }
5024
5142
  if (entry.refs.reasoningToggleText) {
5025
- entry.refs.reasoningExpanded = thinkingExpanded;
5026
5143
  entry.refs.reasoningStreaming = message.streaming === true;
5027
- entry.refs.reasoningToggleText.content = thinkingToggleLabel(thinkingExpanded, message.streaming === true);
5144
+ entry.refs.reasoningToggleText.content = visibleReasoning
5145
+ ? thinkingLabelContent(message.streaming === true, reasoningElapsedMs(message))
5146
+ : new StyledText([fg(theme.messageThinkingText)("")]);
5028
5147
  }
5029
5148
  if (entry.refs.reasoningMarkdown) {
5030
- entry.refs.reasoningMarkdown.content = showThinking ? formatThinkingMarkdown(message.reasoning?.trim() ?? "") : "";
5031
- entry.refs.reasoningMarkdown.streaming = message.streaming === true;
5149
+ syncMarkdownRenderable(entry.refs.reasoningMarkdown, formatThinkingMarkdown(visibleReasoning), message.streaming === true);
5150
+ }
5151
+ if (entry.refs.reasoningBox) {
5152
+ entry.refs.reasoningBox.visible = !!visibleReasoning;
5153
+ }
5154
+ updateAssistantToolEntries(entry, tools, options);
5155
+ if (entry.refs.answerDividerBox) {
5156
+ const showDivider = tools.length > 0 && !!content;
5157
+ entry.refs.answerDividerBox.visible = showDivider;
5158
+ if (entry.refs.answerDividerText) {
5159
+ entry.refs.answerDividerText.content = showDivider
5160
+ ? answerDividerStyledText()
5161
+ : new StyledText([fg(theme.textMuted)("")]);
5162
+ }
5032
5163
  }
5033
5164
  if (entry.refs.contentMarkdown) {
5034
- entry.refs.contentMarkdown.content = message.content.trim();
5035
- entry.refs.contentMarkdown.streaming = message.streaming === true;
5165
+ syncMarkdownRenderable(entry.refs.contentMarkdown, content, message.streaming === true);
5166
+ }
5167
+ if (entry.refs.contentBox) {
5168
+ entry.refs.contentBox.visible = !!content;
5169
+ }
5170
+ if (entry.refs.contentCursorBox) {
5171
+ const cursorActive = message.streaming === true && !!content;
5172
+ entry.refs.contentCursorBox.visible = cursorActive;
5173
+ if (entry.refs.contentCursorText)
5174
+ entry.refs.contentCursorText.content = cursorActive ? "▌" : "";
5175
+ }
5176
+ const summaryString = formatTurnSummary(message);
5177
+ if (entry.refs.turnSummaryText) {
5178
+ entry.refs.turnSummaryText.content = summaryString ?? "";
5179
+ }
5180
+ if (entry.refs.turnSummaryBox) {
5181
+ entry.refs.turnSummaryBox.visible = !!summaryString;
5182
+ }
5183
+ }
5184
+ function syncMarkdownRenderable(markdown, content, streaming) {
5185
+ if (markdown.content === content && markdown.streaming === streaming)
5186
+ return;
5187
+ markdown.content = content;
5188
+ markdown.streaming = streaming;
5189
+ markdown.clearCache();
5190
+ }
5191
+ function updateAssistantToolEntries(entry, tools, options) {
5192
+ const toolsBox = entry.refs.toolsBox;
5193
+ if (!toolsBox)
5194
+ return;
5195
+ toolsBox.visible = tools.length > 0;
5196
+ const previousEntries = entry.refs.toolEntries ?? new Map();
5197
+ const nextEntries = new Map();
5198
+ tools.forEach((tool, index) => {
5199
+ const toolKey = writeToolKey(entry.key, tool);
5200
+ const writeExpanded = options.expandedWrites.has(toolKey);
5201
+ const signature = toolRenderableSignature(tool, writeExpanded);
5202
+ const previous = previousEntries.get(tool.id);
5203
+ if (previous?.signature === signature) {
5204
+ nextEntries.set(tool.id, previous);
5205
+ return;
5206
+ }
5207
+ if (previous) {
5208
+ toolsBox.remove(previous.node.id);
5209
+ previous.node.destroyRecursively();
5210
+ }
5211
+ const node = createToolRenderable(toolsBox.ctx, tool, options.syntaxStyle, options.width, writeExpanded, isWritePreviewTool(tool) ? () => options.onToggleWrite?.(toolKey) : undefined);
5212
+ toolsBox.add(node, index);
5213
+ nextEntries.set(tool.id, { signature, node });
5214
+ });
5215
+ for (const [id, previous] of previousEntries.entries()) {
5216
+ if (nextEntries.has(id))
5217
+ continue;
5218
+ toolsBox.remove(previous.node.id);
5219
+ previous.node.destroyRecursively();
5220
+ }
5221
+ entry.refs.toolEntries = nextEntries;
5222
+ }
5223
+ function toolRenderableSignature(tool, writeExpanded) {
5224
+ return [
5225
+ tool.id,
5226
+ tool.name,
5227
+ tool.status ?? (tool.result === undefined ? "pending" : "completed"),
5228
+ tool.isError ? "error" : "ok",
5229
+ tool.streamingArgs ? "streaming-args" : "args-complete",
5230
+ writeExpanded ? "expanded" : "collapsed",
5231
+ hashString(stableStringify(tool.args)),
5232
+ hashString(tool.rawArguments ?? ""),
5233
+ hashString(tool.result ?? ""),
5234
+ hashString(stableStringify(tool.metadata ?? null)),
5235
+ ].join(":");
5236
+ }
5237
+ function mergeToolMetadata(current, incoming) {
5238
+ if (!incoming)
5239
+ return current;
5240
+ if (current?.kind !== "subagent" || incoming.kind !== "subagent") {
5241
+ return incoming;
5242
+ }
5243
+ const currentSubagents = Array.isArray(current.subagents) ? current.subagents : [];
5244
+ const incomingSubagents = Array.isArray(incoming.subagents) ? incoming.subagents : [];
5245
+ const byId = new Map();
5246
+ for (const item of currentSubagents) {
5247
+ const subAgentId = typeof item === "object" && item !== null && "subAgentId" in item
5248
+ ? String(item.subAgentId)
5249
+ : "";
5250
+ byId.set(subAgentId || `current:${byId.size}`, item);
5251
+ }
5252
+ for (const item of incomingSubagents) {
5253
+ const subAgentId = typeof item === "object" && item !== null && "subAgentId" in item
5254
+ ? String(item.subAgentId)
5255
+ : "";
5256
+ byId.set(subAgentId || `incoming:${byId.size}`, item);
5257
+ }
5258
+ return {
5259
+ ...current,
5260
+ ...incoming,
5261
+ subagents: [...byId.values()],
5262
+ };
5263
+ }
5264
+ function stableStringify(value) {
5265
+ try {
5266
+ return JSON.stringify(value) ?? "";
5267
+ }
5268
+ catch {
5269
+ return String(value);
5036
5270
  }
5037
5271
  }
5038
5272
  function createBox(ctx, options, children = []) {
@@ -5056,6 +5290,7 @@ function createMarkdown(ctx, content, syntaxStyle, options) {
5056
5290
  content,
5057
5291
  syntaxStyle,
5058
5292
  treeSitterClient,
5293
+ renderNode: createSemanticMarkdownRenderNode(ctx, options?.fg ?? theme.messageAssistantText),
5059
5294
  streaming: options?.streaming === true,
5060
5295
  conceal: true,
5061
5296
  concealCode: false,
@@ -5075,6 +5310,87 @@ function createMarkdown(ctx, content, syntaxStyle, options) {
5075
5310
  },
5076
5311
  });
5077
5312
  }
5313
+ function createSemanticMarkdownRenderNode(ctx, defaultFg) {
5314
+ const palette = semanticMarkdownPalette(defaultFg);
5315
+ return (token, context) => {
5316
+ switch (token?.type) {
5317
+ case "hr":
5318
+ return createText(ctx, new StyledText([
5319
+ fg(theme.borderSubtle)("─".repeat(48)),
5320
+ ]), {
5321
+ fg: theme.borderSubtle,
5322
+ wrapMode: "none",
5323
+ flexShrink: 0,
5324
+ });
5325
+ case "heading":
5326
+ return createText(ctx, markdownInlineToStyledText(markdownTokenInlineTokens(token), palette, token.text ?? "", { bold: true }), {
5327
+ fg: defaultFg,
5328
+ wrapMode: "word",
5329
+ flexShrink: 0,
5330
+ });
5331
+ case "paragraph":
5332
+ return createText(ctx, markdownInlineToStyledText(markdownTokenInlineTokens(token), palette, token.text ?? ""), {
5333
+ fg: defaultFg,
5334
+ wrapMode: "word",
5335
+ flexShrink: 0,
5336
+ });
5337
+ case "list":
5338
+ return createMarkdownList(ctx, token, palette, defaultFg);
5339
+ default:
5340
+ return context.defaultRender();
5341
+ }
5342
+ };
5343
+ }
5344
+ function createMarkdownList(ctx, token, palette, defaultFg) {
5345
+ const ordered = token?.ordered === true;
5346
+ const start = typeof token?.start === "number" ? token.start : 1;
5347
+ const items = Array.isArray(token?.items) ? token.items : [];
5348
+ if (items.length === 0)
5349
+ return null;
5350
+ return createBox(ctx, {
5351
+ flexDirection: "column",
5352
+ flexShrink: 0,
5353
+ }, items.map((item, index) => {
5354
+ const marker = ordered ? `${start + index}. ` : "• ";
5355
+ return createText(ctx, new StyledText([
5356
+ fg(theme.textMuted)(marker),
5357
+ ...markdownInlineToStyledText(markdownTokenInlineTokens(item), palette, item.text ?? "").chunks,
5358
+ ]), {
5359
+ fg: defaultFg,
5360
+ wrapMode: "word",
5361
+ flexShrink: 0,
5362
+ });
5363
+ }));
5364
+ }
5365
+ function markdownTokenInlineTokens(token) {
5366
+ if (Array.isArray(token?.tokens))
5367
+ return token.tokens;
5368
+ if (typeof token?.text === "string")
5369
+ return [{ type: "text", text: token.text }];
5370
+ return undefined;
5371
+ }
5372
+ function semanticMarkdownPalette(defaultFg) {
5373
+ return {
5374
+ text: defaultFg,
5375
+ textMuted: theme.textMuted,
5376
+ success: theme.success,
5377
+ warning: theme.warning,
5378
+ secondary: theme.secondary,
5379
+ };
5380
+ }
5381
+ function markdownInlineToStyledText(tokens, palette, fallback = "", style = {}) {
5382
+ const chunks = markdownInlineSegments(tokens, fallback, style).map((segment) => {
5383
+ let chunk = fg(palette[segment.color ?? "text"])(segment.text);
5384
+ if (segment.bold)
5385
+ chunk = bold(chunk);
5386
+ if (segment.italic)
5387
+ chunk = italic(chunk);
5388
+ if (segment.dim)
5389
+ chunk = dim(chunk);
5390
+ return chunk;
5391
+ });
5392
+ return new StyledText(chunks);
5393
+ }
5078
5394
  function createDiffRenderable(ctx, diff, filePath, syntaxStyle, width = 80) {
5079
5395
  return new DiffRenderable(ctx, {
5080
5396
  diff,
@@ -5137,6 +5453,24 @@ function createCodeBlockRenderable(ctx, content, filePath, syntaxStyle) {
5137
5453
  lineNumbers.add(code);
5138
5454
  return lineNumbers;
5139
5455
  }
5456
+ function createToolRenderHelpers() {
5457
+ return {
5458
+ theme,
5459
+ createBox: (ctx, options, children) => createBox(ctx, options, children),
5460
+ createText: (ctx, content, options) => createText(ctx, content, (options ?? {})),
5461
+ createCodeBlockRenderable,
5462
+ createDiffRenderable,
5463
+ toolColor,
5464
+ displayToolName,
5465
+ toolHeader,
5466
+ toolPath,
5467
+ extractToolDiff,
5468
+ summarizeToolResult,
5469
+ isToolFinished,
5470
+ toolPreview,
5471
+ toolStateIcon,
5472
+ };
5473
+ }
5140
5474
  function renderCodeBlockContent(content, filePath, syntaxStyle) {
5141
5475
  return h("line_number", { fg: theme.textMuted, minWidth: 3, paddingRight: 1 }, h("code", {
5142
5476
  content,
@@ -5148,14 +5482,14 @@ function renderCodeBlockContent(content, filePath, syntaxStyle) {
5148
5482
  width: "100%",
5149
5483
  }));
5150
5484
  }
5151
- function createMessageEntry(ctx, message, index, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking = true, width = 80, thinkingExpanded = false, compactionExpanded = false, onToggleThinking, onToggleCompaction) {
5485
+ function createMessageEntry(ctx, message, index, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking = true, width = 80, compactionExpanded = false, expandedWrites = new Set(), onToggleCompaction, onToggleWrite) {
5152
5486
  if (message.role === "user")
5153
5487
  return createUserEntry(ctx, message, index, key, signature);
5154
5488
  if (message.role === "error")
5155
5489
  return createErrorEntry(ctx, message, key, signature);
5156
5490
  if (message.syntheticKind === "ui_compact_card")
5157
5491
  return createCompactionCardEntry(ctx, message, key, signature, compactionExpanded, onToggleCompaction);
5158
- return createAssistantEntry(ctx, message, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking, width, thinkingExpanded, onToggleThinking);
5492
+ return createAssistantEntry(ctx, message, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking, width, expandedWrites, onToggleWrite);
5159
5493
  }
5160
5494
  function createUserEntry(ctx, message, index, key, signature) {
5161
5495
  const refs = {};
@@ -5200,7 +5534,7 @@ function createErrorEntry(ctx, message, key, signature) {
5200
5534
  }, [text]);
5201
5535
  return { key, signature, node, refs };
5202
5536
  }
5203
- function createAssistantEntry(ctx, message, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking = true, width = 80, thinkingExpanded = false, onToggleThinking) {
5537
+ function createAssistantEntry(ctx, message, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking = true, width = 80, expandedWrites = new Set(), onToggleWrite) {
5204
5538
  const modelSwitch = parseModelSwitchMessage(message.content);
5205
5539
  if (modelSwitch && !message.reasoning?.trim() && !(message.toolCalls?.length)) {
5206
5540
  return createModelSwitchEntry(ctx, modelSwitch, key, signature);
@@ -5208,70 +5542,121 @@ function createAssistantEntry(ctx, message, syntaxStyle, subtleSyntaxStyle, key,
5208
5542
  const children = [];
5209
5543
  const refs = {};
5210
5544
  const visibleReasoning = showThinking ? message.reasoning?.trim() : "";
5211
- if (message.status && !visibleReasoning && !message.content.trim() && !(message.toolCalls?.length)) {
5212
- const status = createText(ctx, assistantStatusLabel(message), {
5213
- fg: theme.messageThinkingText,
5214
- });
5215
- refs.statusText = status;
5216
- children.push(createBox(ctx, {
5217
- paddingLeft: 3,
5218
- marginTop: 1,
5219
- flexShrink: 0,
5220
- }, [status]));
5221
- }
5222
- if (visibleReasoning) {
5223
- const reasoningChildren = [];
5224
- const toggleText = createText(ctx, thinkingToggleLabel(thinkingExpanded, message.streaming === true), {
5225
- fg: theme.messageThinkingText,
5226
- wrapMode: "none",
5227
- });
5228
- refs.reasoningToggleText = toggleText;
5229
- refs.reasoningExpanded = thinkingExpanded;
5230
- refs.reasoningStreaming = message.streaming === true;
5231
- reasoningChildren.push(createBox(ctx, {
5232
- flexShrink: 0,
5233
- onMouseUp: () => onToggleThinking?.(key),
5234
- }, [toggleText]));
5235
- if (thinkingExpanded) {
5236
- const markdown = createMarkdown(ctx, formatThinkingMarkdown(visibleReasoning), subtleSyntaxStyle, {
5237
- streaming: message.streaming === true,
5238
- fg: theme.messageThinkingText,
5239
- });
5240
- refs.reasoningMarkdown = markdown;
5241
- reasoningChildren.push(markdown);
5242
- }
5243
- children.push(createBox(ctx, {
5244
- paddingLeft: 2,
5245
- marginTop: 1,
5246
- border: ["left"],
5247
- borderColor: theme.messageThinkingBorder,
5248
- flexDirection: "column",
5249
- flexShrink: 0,
5250
- }, reasoningChildren));
5251
- }
5252
- for (const tool of message.toolCalls ?? [])
5253
- children.push(createToolRenderable(ctx, tool, syntaxStyle, width));
5254
- if (message.content.trim()) {
5255
- const markdown = createMarkdown(ctx, message.content.trim(), syntaxStyle, {
5256
- streaming: message.streaming === true,
5257
- fg: theme.messageAssistantText,
5258
- });
5259
- refs.contentMarkdown = markdown;
5260
- children.push(createBox(ctx, {
5261
- paddingLeft: 3,
5262
- marginTop: 1,
5263
- flexDirection: "column",
5545
+ const content = message.content.trim();
5546
+ const tools = message.toolCalls ?? [];
5547
+ const showStatus = !!message.status && !visibleReasoning && !content && tools.length === 0;
5548
+ const status = createText(ctx, assistantStatusLabel(message), {
5549
+ fg: theme.messageThinkingText,
5550
+ });
5551
+ refs.statusText = status;
5552
+ const statusBox = createBox(ctx, {
5553
+ paddingLeft: 3,
5554
+ marginTop: 1,
5555
+ flexShrink: 0,
5556
+ visible: showStatus,
5557
+ }, [status]);
5558
+ refs.statusBox = statusBox;
5559
+ children.push(statusBox);
5560
+ const labelText = createText(ctx, thinkingLabelContent(message.streaming === true, reasoningElapsedMs(message)), {
5561
+ fg: theme.messageThinkingText,
5562
+ wrapMode: "none",
5563
+ });
5564
+ refs.reasoningToggleText = labelText;
5565
+ refs.reasoningStreaming = message.streaming === true;
5566
+ const markdown = createMarkdown(ctx, formatThinkingMarkdown(visibleReasoning ?? ""), subtleSyntaxStyle, {
5567
+ streaming: message.streaming === true,
5568
+ fg: theme.messageThinkingContentText,
5569
+ });
5570
+ refs.reasoningMarkdown = markdown;
5571
+ const reasoningBox = createBox(ctx, {
5572
+ paddingLeft: 2,
5573
+ marginTop: 1,
5574
+ border: ["left"],
5575
+ borderColor: theme.messageThinkingBorder,
5576
+ flexDirection: "column",
5577
+ flexShrink: 0,
5578
+ visible: !!visibleReasoning,
5579
+ }, [
5580
+ createBox(ctx, {
5264
5581
  flexShrink: 0,
5265
- }, [markdown]));
5266
- }
5267
- if (!children.length)
5268
- return null;
5269
- return {
5582
+ }, [labelText]),
5583
+ markdown,
5584
+ ]);
5585
+ refs.reasoningBox = reasoningBox;
5586
+ children.push(reasoningBox);
5587
+ const toolsBox = createBox(ctx, {
5588
+ flexDirection: "column",
5589
+ flexShrink: 0,
5590
+ visible: tools.length > 0,
5591
+ });
5592
+ refs.toolsBox = toolsBox;
5593
+ refs.toolEntries = new Map();
5594
+ children.push(toolsBox);
5595
+ const showAnswerDivider = tools.length > 0 && !!content;
5596
+ const answerDividerText = createText(ctx, showAnswerDivider ? answerDividerStyledText() : new StyledText([fg(theme.textMuted)("")]), { wrapMode: "none" });
5597
+ refs.answerDividerText = answerDividerText;
5598
+ const answerDividerBox = createBox(ctx, {
5599
+ paddingLeft: 3,
5600
+ marginTop: 1,
5601
+ flexShrink: 0,
5602
+ visible: showAnswerDivider,
5603
+ }, [answerDividerText]);
5604
+ refs.answerDividerBox = answerDividerBox;
5605
+ children.push(answerDividerBox);
5606
+ const contentMarkdown = createMarkdown(ctx, content, syntaxStyle, {
5607
+ streaming: message.streaming === true,
5608
+ fg: theme.messageAssistantText,
5609
+ });
5610
+ refs.contentMarkdown = contentMarkdown;
5611
+ const contentBox = createBox(ctx, {
5612
+ paddingLeft: 3,
5613
+ marginTop: 1,
5614
+ flexDirection: "column",
5615
+ flexShrink: 0,
5616
+ visible: !!content,
5617
+ }, [contentMarkdown]);
5618
+ refs.contentBox = contentBox;
5619
+ children.push(contentBox);
5620
+ const cursorActive = message.streaming === true && !!content;
5621
+ const contentCursorText = createText(ctx, "▌", { fg: theme.primary, wrapMode: "none" });
5622
+ refs.contentCursorText = contentCursorText;
5623
+ const contentCursorBox = createBox(ctx, {
5624
+ paddingLeft: 3,
5625
+ flexShrink: 0,
5626
+ visible: cursorActive,
5627
+ }, [contentCursorText]);
5628
+ refs.contentCursorBox = contentCursorBox;
5629
+ children.push(contentCursorBox);
5630
+ const summaryString = formatTurnSummary(message);
5631
+ const turnSummaryText = createText(ctx, summaryString ?? "", { fg: theme.textMuted, wrapMode: "none" });
5632
+ refs.turnSummaryText = turnSummaryText;
5633
+ const turnSummaryBox = createBox(ctx, {
5634
+ paddingLeft: 3,
5635
+ marginTop: 1,
5636
+ flexShrink: 0,
5637
+ visible: !!summaryString,
5638
+ }, [turnSummaryText]);
5639
+ refs.turnSummaryBox = turnSummaryBox;
5640
+ children.push(turnSummaryBox);
5641
+ const entry = {
5270
5642
  key,
5271
5643
  signature,
5272
5644
  node: createBox(ctx, { flexDirection: "column", flexShrink: 0 }, children),
5273
5645
  refs,
5274
5646
  };
5647
+ updateAssistantToolEntries(entry, tools, {
5648
+ syntaxStyle,
5649
+ expandedWrites,
5650
+ width,
5651
+ onToggleWrite,
5652
+ });
5653
+ return entry;
5654
+ }
5655
+ function answerDividerStyledText() {
5656
+ return new StyledText([
5657
+ fg(theme.accent)("◆ "),
5658
+ fg(theme.textMuted)(italic("Answer")),
5659
+ ]);
5275
5660
  }
5276
5661
  function createCompactionCardEntry(ctx, message, key, signature, expanded, onToggle) {
5277
5662
  const refs = {};
@@ -5389,7 +5774,7 @@ function createTodoWriteRenderable(ctx, tool) {
5389
5774
  flexDirection: "column",
5390
5775
  flexShrink: 0,
5391
5776
  }, [
5392
- createText(ctx, `~ Planning tasks...`, { fg: toolColor(tool) }),
5777
+ createText(ctx, `→ Planning tasks...`, { fg: toolColor(tool) }),
5393
5778
  ]);
5394
5779
  }
5395
5780
  return createBox(ctx, {
@@ -5416,90 +5801,26 @@ function createTodoWriteRenderable(ctx, tool) {
5416
5801
  }),
5417
5802
  ]);
5418
5803
  }
5419
- function createToolRenderable(ctx, tool, syntaxStyle, width = 80) {
5804
+ function createToolRenderable(ctx, tool, syntaxStyle, width = 80, writeExpanded = false, onToggleWrite) {
5420
5805
  if (tool.name === "question") {
5421
5806
  return createQuestionToolRenderable(ctx, tool);
5422
5807
  }
5423
5808
  if (tool.name === "todo_write") {
5424
5809
  return createTodoWriteRenderable(ctx, tool);
5425
5810
  }
5426
- const icon = tool.name === "bash" ? "$" : tool.name === "edit" || tool.name === "write" ? "✎" : "●";
5427
- const color = toolColor(tool);
5428
- const header = toolHeader(tool);
5429
- const diff = extractToolDiff(tool);
5430
- if (diff && !tool.isError && tool.name === "edit") {
5431
- return createBox(ctx, {
5432
- paddingLeft: 3,
5433
- marginTop: 1,
5434
- flexDirection: "column",
5435
- flexShrink: 0,
5436
- }, [
5437
- createText(ctx, new StyledText([
5438
- fg(color)(`${icon} ${displayToolName(tool.name)}`),
5439
- fg(theme.toolText)(header ? ` ${header}` : ""),
5440
- ])),
5441
- createBox(ctx, {
5442
- paddingLeft: 1,
5443
- marginTop: 1,
5444
- border: ["left"],
5445
- borderColor: theme.borderSubtle,
5446
- flexDirection: "column",
5447
- flexShrink: 0,
5448
- }, [createDiffRenderable(ctx, diff, toolPath(tool), syntaxStyle, width)]),
5449
- ]);
5450
- }
5451
- if (!tool.isError && tool.name === "write" && typeof tool.args?.content === "string" && isToolFinished(tool)) {
5452
- return createBox(ctx, {
5453
- paddingLeft: 3,
5454
- marginTop: 1,
5455
- flexDirection: "column",
5456
- flexShrink: 0,
5457
- }, [
5458
- createText(ctx, new StyledText([
5459
- fg(color)(`${icon} ${displayToolName(tool.name)}`),
5460
- fg(theme.toolText)(header ? ` ${header}` : ""),
5461
- ])),
5462
- createBox(ctx, {
5463
- paddingLeft: 1,
5464
- marginTop: 1,
5465
- border: ["left"],
5466
- borderColor: theme.borderSubtle,
5467
- flexDirection: "column",
5468
- flexShrink: 0,
5469
- }, [createCodeBlockRenderable(ctx, tool.args.content, toolPath(tool), syntaxStyle)]),
5470
- ]);
5471
- }
5472
- const chunks = [
5473
- fg(color)(`${isToolFinished(tool) ? "" : "~ "}${icon} ${displayToolName(tool.name)}`),
5474
- ];
5475
- if (header)
5476
- chunks.push(fg(theme.toolText)(` ${header}`));
5477
- if (tool.result) {
5478
- chunks.push(fg(theme.text)("\n"));
5479
- chunks.push(fg(theme.borderSubtle)(" "));
5480
- chunks.push(fg(tool.isError ? theme.toolError : theme.textMuted)(summarizeToolResult(tool)));
5481
- const preview = toolPreview(tool);
5482
- if (preview) {
5483
- for (const line of preview.lines) {
5484
- chunks.push(fg(theme.text)("\n"));
5485
- chunks.push(fg(theme.borderSubtle)(" "));
5486
- chunks.push(fg(theme.toolText)(line));
5487
- }
5488
- if (preview.omitted > 0) {
5489
- chunks.push(fg(theme.text)("\n"));
5490
- chunks.push(fg(theme.borderSubtle)(" "));
5491
- chunks.push(fg(theme.textMuted)(`+ ${preview.omitted} more`));
5492
- }
5493
- }
5811
+ const renderer = findToolRenderer(tool);
5812
+ if (renderer) {
5813
+ return renderer.render({
5814
+ ctx,
5815
+ tool,
5816
+ syntaxStyle,
5817
+ width,
5818
+ writeExpanded,
5819
+ onToggleWrite,
5820
+ helpers: createToolRenderHelpers(),
5821
+ });
5494
5822
  }
5495
- return createBox(ctx, {
5496
- paddingLeft: 3,
5497
- marginTop: 1,
5498
- flexDirection: "column",
5499
- flexShrink: 0,
5500
- }, [
5501
- createText(ctx, new StyledText(chunks), { wrapMode: "word" }),
5502
- ]);
5823
+ throw new Error(`No renderer for tool '${tool.name}'`);
5503
5824
  }
5504
5825
  function createQuestionToolRenderable(ctx, tool) {
5505
5826
  const questions = questionToolQuestions(tool);
@@ -5512,7 +5833,7 @@ function createQuestionToolRenderable(ctx, tool) {
5512
5833
  flexDirection: "column",
5513
5834
  flexShrink: 0,
5514
5835
  }, [
5515
- createText(ctx, `${isToolFinished(tool) ? "" : "~ "}→ ${rejected ? "Asked" : "Asking"} questions...`, {
5836
+ createText(ctx, `→ ${rejected ? "Asked" : "Asking"} questions...`, {
5516
5837
  fg: rejected ? theme.textMuted : toolColor(tool),
5517
5838
  attributes: rejected ? TextAttributes.STRIKETHROUGH : undefined,
5518
5839
  }),
@@ -5558,16 +5879,22 @@ function renderTool(tool, syntaxStyle, width = 80) {
5558
5879
  if (tool.name === "question") {
5559
5880
  return renderQuestionTool(tool);
5560
5881
  }
5561
- const icon = tool.name === "bash" ? "$" : tool.name === "edit" || tool.name === "write" ? "✎" : "●";
5882
+ const icon = toolStateIcon(tool);
5562
5883
  const color = toolColor(tool);
5563
5884
  const diff = extractToolDiff(tool);
5564
5885
  if (diff && !tool.isError && tool.name === "edit") {
5565
5886
  return h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, h("text", { fg: color }, `${icon} ${displayToolName(tool.name)}${toolHeader(tool) ? ` ${toolHeader(tool)}` : ""}`), h("box", { paddingLeft: 1, marginTop: 1, border: ["left"], borderColor: theme.borderSubtle, flexDirection: "column", flexShrink: 0 }, renderDiffContent(diff, toolPath(tool), syntaxStyle, width)));
5566
5887
  }
5567
- if (!tool.isError && tool.name === "write" && typeof tool.args?.content === "string" && isToolFinished(tool)) {
5568
- return h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, h("text", { fg: color }, `${icon} ${displayToolName(tool.name)}${toolHeader(tool) ? ` ${toolHeader(tool)}` : ""}`), h("box", { paddingLeft: 1, marginTop: 1, border: ["left"], borderColor: theme.borderSubtle, flexDirection: "column", flexShrink: 0 }, renderCodeBlockContent(tool.args.content, toolPath(tool), syntaxStyle)));
5888
+ if (isWritePreviewTool(tool)) {
5889
+ const preview = formatWritePreview(tool.args.content, false);
5890
+ const summary = tool.result ?? `${isToolFinished(tool) ? "Prepared" : "Writing"} ${tool.args.content.split(/\r?\n/).length} lines to ${toolPath(tool) ?? "file"}`;
5891
+ return h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, h("text", { fg: color }, `${icon} ${displayToolName(tool.name)}${toolHeader(tool) ? ` ${toolHeader(tool)}` : ""}`), h("box", { paddingLeft: 1, marginTop: 0, border: ["left"], borderColor: theme.borderSubtle, flexDirection: "column", flexShrink: 0 }, h("text", { fg: theme.textMuted }, `└ ${summary}`), renderCodeBlockContent(preview.content, toolPath(tool), syntaxStyle), preview.omittedLines > 0
5892
+ ? h("text", { fg: theme.textMuted }, `... +${preview.omittedLines} lines (ctrl+o to expand)`)
5893
+ : preview.omittedChars > 0
5894
+ ? h("text", { fg: theme.textMuted }, `... +${preview.omittedChars} chars (ctrl+o to expand)`)
5895
+ : null));
5569
5896
  }
5570
- return h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, h("text", { fg: color }, `${isToolFinished(tool) ? "" : "~ "}${icon} ${displayToolName(tool.name)}${toolHeader(tool) ? ` ${toolHeader(tool)}` : ""}`), () => tool.result ? h("text", { fg: tool.isError ? theme.toolError : theme.textMuted, wrapMode: "word" }, toolSummaryWithPreview(tool)) : null);
5897
+ return h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, h("text", { fg: color }, `${icon} ${displayToolName(tool.name)}${toolHeader(tool) ? ` ${toolHeader(tool)}` : ""}`), () => tool.result ? h("text", { fg: tool.isError ? theme.toolError : theme.textMuted, wrapMode: "word" }, toolSummaryWithPreview(tool)) : null);
5571
5898
  }
5572
5899
  function renderQuestionTool(tool) {
5573
5900
  const questions = questionToolQuestions(tool);
@@ -5577,7 +5904,7 @@ function renderQuestionTool(tool) {
5577
5904
  return h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, h("text", {
5578
5905
  fg: rejected ? theme.textMuted : toolColor(tool),
5579
5906
  attributes: rejected ? TextAttributes.STRIKETHROUGH : undefined,
5580
- }, `${isToolFinished(tool) ? "" : "~ "}→ ${rejected ? "Asked" : "Asking"} questions...`));
5907
+ }, `→ ${rejected ? "Asked" : "Asking"} questions...`));
5581
5908
  }
5582
5909
  return h("box", {
5583
5910
  border: ["left"],
@@ -5842,11 +6169,9 @@ function formatDisplayContentParts(content, labelStart) {
5842
6169
  function reconstructDisplayMessages(agentMessages) {
5843
6170
  const result = [];
5844
6171
  for (const message of agentMessages) {
5845
- if (message.role === "system" || message.role === "tool")
6172
+ if (message.role === "system" || message.role === "meta" || message.role === "tool")
5846
6173
  continue;
5847
6174
  if (message.role === "user") {
5848
- if (message.isMeta)
5849
- continue;
5850
6175
  result.push({
5851
6176
  role: "user",
5852
6177
  content: typeof message.content === "string"
@@ -5935,7 +6260,9 @@ function formatTranscript(messages, options) {
5935
6260
  if (visibleReasoning) {
5936
6261
  appendBlank();
5937
6262
  append("│ ", theme.messageThinkingBorder);
5938
- appendLine(truncate(formatThinkingMarkdown(visibleReasoning), 500), theme.messageThinkingText);
6263
+ chunks.push(fg(theme.messageThinkingText)(italic("Thinking\n")));
6264
+ append("│ ", theme.messageThinkingBorder);
6265
+ appendLine(truncate(formatThinkingMarkdown(visibleReasoning), 500), theme.messageThinkingContentText);
5939
6266
  }
5940
6267
  if (message.status && !visibleReasoning && !message.content.trim() && !(message.toolCalls?.length)) {
5941
6268
  appendBlank();
@@ -5946,7 +6273,7 @@ function formatTranscript(messages, options) {
5946
6273
  appendBlank();
5947
6274
  const icon = tool.name === "bash" ? "$" : tool.name === "edit" || tool.name === "write" ? "✎" : "●";
5948
6275
  const color = toolColor(tool);
5949
- append(` ${isToolFinished(tool) ? "" : "~ "}${icon} `, color);
6276
+ append(` ${icon} `, color);
5950
6277
  append(displayToolName(tool.name), color);
5951
6278
  const header = toolHeader(tool);
5952
6279
  if (header)
@@ -5996,7 +6323,7 @@ function renderHomeState(input) {
5996
6323
  flexDirection: "column",
5997
6324
  alignItems: "center",
5998
6325
  justifyContent: "center",
5999
- }, h("box", { flexDirection: "column", flexShrink: 0, width: "100%" }, h("text", { fg: theme.text }, ""), h("text", { fg: theme.text }, ""), ...HOME_LOGO.map((line) => h("text", { fg: theme.primary }, centerLine(line, width))), h("text", { fg: theme.text }, ""), h("text", { fg: theme.warning }, centerLine(`● Tip ${input.tip}`, width)), cwd ? h("text", { fg: theme.textMuted }, centerLine(` ${cwd}`, width)) : null));
6326
+ }, h("box", { flexDirection: "column", flexShrink: 0, width: "100%" }, h("text", { fg: theme.text }, ""), h("text", { fg: theme.text }, ""), ...HOME_LOGO.map((line) => h("text", { fg: homeLogoColor(line.tone) }, centerLine(line.text || " ", width))), h("text", { fg: theme.text }, ""), h("text", { fg: theme.warning }, centerLine(`● Tip ${input.tip}`, width)), cwd ? h("text", { fg: theme.textMuted }, centerLine(` ${cwd}`, width)) : null));
6000
6327
  }
6001
6328
  function hasRenderableMessage(message, showThinking = true) {
6002
6329
  if (message.role === "error")
@@ -6172,6 +6499,11 @@ function displayToolName(name) {
6172
6499
  glob: "Glob",
6173
6500
  web_fetch: "WebFetch",
6174
6501
  web_search: "WebSearch",
6502
+ subagent: "Subagent",
6503
+ spawn_agent: "SpawnAgent",
6504
+ wait_agent: "WaitAgent",
6505
+ send_input: "SendInput",
6506
+ close_agent: "CloseAgent",
6175
6507
  task: "Task",
6176
6508
  todo: "Todo",
6177
6509
  question: "Questions",
@@ -6180,6 +6512,20 @@ function displayToolName(name) {
6180
6512
  }
6181
6513
  function toolHeader(tool) {
6182
6514
  const args = tool.args || {};
6515
+ if (tool.name === "subagent") {
6516
+ if (typeof args.agent === "string")
6517
+ return `(${args.agent})`;
6518
+ if (Array.isArray(args.tasks))
6519
+ return `(${args.tasks.length} tasks)`;
6520
+ }
6521
+ if (tool.name === "spawn_agent") {
6522
+ const agent = args.agent_type ?? args.agent ?? "default";
6523
+ return `(${agent})`;
6524
+ }
6525
+ if (tool.name === "wait_agent" || tool.name === "send_input" || tool.name === "close_agent") {
6526
+ const agentId = args.agent_id ?? (Array.isArray(args.agent_ids) ? `${args.agent_ids.length} agents` : undefined);
6527
+ return agentId ? `(${truncate(String(agentId), 64)})` : "";
6528
+ }
6183
6529
  const value = args.path ?? args.command ?? args.pattern ?? args.url ?? args.query;
6184
6530
  return value ? `(${truncate(String(value).replace(/\n/g, " "), 64)})` : "";
6185
6531
  }
@@ -6225,8 +6571,11 @@ function filetype(filePath) {
6225
6571
  return ext ? map[ext] : undefined;
6226
6572
  }
6227
6573
  function summarizeToolResult(tool) {
6228
- if (!isToolFinished(tool))
6229
- return tool.status === "running" ? "running" : "pending";
6574
+ if (!isToolFinished(tool)) {
6575
+ if (tool.status === "running")
6576
+ return "running";
6577
+ return tool.streamingArgs ? "preparing" : "pending";
6578
+ }
6230
6579
  if (tool.name === "question") {
6231
6580
  if (isQuestionRejected(tool))
6232
6581
  return "dismissed";
@@ -6237,14 +6586,47 @@ function summarizeToolResult(tool) {
6237
6586
  if (tool.isError)
6238
6587
  return truncate(result.split("\n").find(Boolean) || "error", 120);
6239
6588
  const lines = result.replace(/\r\n/g, "\n").split("\n").filter((line) => line.trim()).length;
6589
+ const matches = typeof tool.metadata?.matches === "number" ? tool.metadata.matches : undefined;
6590
+ if (tool.name === "read")
6591
+ return "";
6240
6592
  if (tool.name === "edit")
6241
6593
  return "patched file";
6242
6594
  if (tool.name === "write")
6243
6595
  return "wrote file";
6244
- if (tool.name === "bash")
6596
+ if (tool.name === "grep" || tool.name === "glob") {
6597
+ if (matches !== undefined)
6598
+ return `${matches} match${matches === 1 ? "" : "es"}`;
6599
+ return lines ? `${lines} line${lines === 1 ? "" : "s"}` : "no matches";
6600
+ }
6601
+ if (tool.name === "bash") {
6602
+ if (matches !== undefined)
6603
+ return `${matches} match${matches === 1 ? "" : "es"}`;
6245
6604
  return lines ? `${lines} line${lines === 1 ? "" : "s"} output` : "done";
6605
+ }
6246
6606
  return lines ? `${lines} line${lines === 1 ? "" : "s"}` : "done";
6247
6607
  }
6608
+ function toolStateIcon(tool) {
6609
+ if (tool.isError || tool.status === "error")
6610
+ return "✗";
6611
+ if (!isToolFinished(tool)) {
6612
+ if (tool.status === "running")
6613
+ return "◐";
6614
+ return "◌";
6615
+ }
6616
+ if (tool.name === "bash")
6617
+ return "$";
6618
+ if (tool.name === "edit")
6619
+ return "✎";
6620
+ if (tool.name === "write")
6621
+ return "✎";
6622
+ if (tool.name === "read")
6623
+ return "▤";
6624
+ if (tool.name === "grep" || tool.name === "glob")
6625
+ return "⌕";
6626
+ if (tool.name === "web_fetch" || tool.name === "web_search")
6627
+ return "⌖";
6628
+ return "●";
6629
+ }
6248
6630
  function toolSummaryWithPreview(tool) {
6249
6631
  const summary = ` ${summarizeToolResult(tool)}`;
6250
6632
  const preview = toolPreview(tool);
@@ -6259,7 +6641,7 @@ function toolSummaryWithPreview(tool) {
6259
6641
  function toolPreview(tool) {
6260
6642
  if (!isToolFinished(tool) || tool.isError || !tool.result)
6261
6643
  return undefined;
6262
- if (tool.name !== "read" && tool.name !== "glob")
6644
+ if (tool.name !== "glob")
6263
6645
  return undefined;
6264
6646
  const lines = tool.result
6265
6647
  .replace(/\r\n/g, "\n")
@@ -6315,7 +6697,10 @@ function isToolFinished(tool) {
6315
6697
  function assistantStatusLabel(message) {
6316
6698
  if (message.status === "responding")
6317
6699
  return "Responding...";
6318
- return message.streaming ? "Thinking..." : "Thinking";
6700
+ const elapsed = formatDuration(reasoningElapsedMs(message));
6701
+ if (message.streaming)
6702
+ return elapsed ? `Thinking ${elapsed}...` : "Thinking...";
6703
+ return elapsed ? `Thought for ${elapsed}` : "Thinking";
6319
6704
  }
6320
6705
  function buildContextGauge(percent, barWidth) {
6321
6706
  const clamped = Math.max(0, Math.min(100, percent));
@@ -6346,9 +6731,69 @@ function formatContextRemaining(value) {
6346
6731
  return `${(value / 1_000).toFixed(1)}K`;
6347
6732
  return String(value);
6348
6733
  }
6349
- function thinkingToggleLabel(expanded, streaming = false, spinnerFrame = "") {
6350
- const arrow = expanded ? "▼" : "▶";
6351
- return streaming && spinnerFrame ? `${spinnerFrame} ${arrow} Thinking` : `${arrow} Thinking`;
6734
+ function thinkingLabelContent(streaming = false, elapsedMs) {
6735
+ const elapsed = formatDuration(elapsedMs);
6736
+ const label = streaming
6737
+ ? (elapsed ? `Thinking ${elapsed}...` : "Thinking...")
6738
+ : (elapsed ? `Thought for ${elapsed}` : "Thought");
6739
+ return new StyledText([
6740
+ fg(theme.messageThinkingText)(italic(label)),
6741
+ ]);
6742
+ }
6743
+ function formatDuration(ms) {
6744
+ if (ms === undefined || !Number.isFinite(ms) || ms <= 0)
6745
+ return "";
6746
+ if (ms < 1000)
6747
+ return `${Math.max(1, Math.round(ms))}ms`;
6748
+ const seconds = ms / 1000;
6749
+ if (seconds < 10)
6750
+ return `${seconds.toFixed(1)}s`;
6751
+ if (seconds < 60)
6752
+ return `${Math.round(seconds)}s`;
6753
+ const minutes = Math.floor(seconds / 60);
6754
+ const remSec = Math.round(seconds - minutes * 60);
6755
+ return remSec === 0 ? `${minutes}m` : `${minutes}m${remSec}s`;
6756
+ }
6757
+ function reasoningElapsedMs(message) {
6758
+ if (message.turnStartedAt === undefined)
6759
+ return undefined;
6760
+ const end = !message.streaming ? (message.turnCompletedAt ?? Date.now()) : Date.now();
6761
+ const diff = end - message.turnStartedAt;
6762
+ return diff > 0 ? diff : undefined;
6763
+ }
6764
+ function toolElapsedMs(tool) {
6765
+ if (tool.startedAt === undefined)
6766
+ return undefined;
6767
+ const end = tool.completedAt ?? (tool.status === "completed" || tool.status === "error" ? Date.now() : undefined);
6768
+ if (end === undefined)
6769
+ return undefined;
6770
+ const diff = end - tool.startedAt;
6771
+ return diff > 0 ? diff : undefined;
6772
+ }
6773
+ function turnElapsedMs(message) {
6774
+ if (message.turnStartedAt === undefined)
6775
+ return undefined;
6776
+ const end = message.turnCompletedAt ?? Date.now();
6777
+ const diff = end - message.turnStartedAt;
6778
+ return diff > 0 ? diff : undefined;
6779
+ }
6780
+ function formatTurnSummary(message) {
6781
+ const parts = [];
6782
+ const elapsed = turnElapsedMs(message);
6783
+ if (elapsed !== undefined)
6784
+ parts.push(formatDuration(elapsed));
6785
+ const usage = message.turnUsage;
6786
+ if (usage) {
6787
+ if (usage.promptTokens)
6788
+ parts.push(`${formatCompactNumber(usage.promptTokens)}↑`);
6789
+ if (usage.completionTokens)
6790
+ parts.push(`${formatCompactNumber(usage.completionTokens)}↓`);
6791
+ if (usage.reasoningTokens)
6792
+ parts.push(`${formatCompactNumber(usage.reasoningTokens)}◇`);
6793
+ }
6794
+ if (!parts.length)
6795
+ return undefined;
6796
+ return `· ${parts.join(" · ")}`;
6352
6797
  }
6353
6798
  function truncate(value, max) {
6354
6799
  return value.length > max ? value.slice(0, Math.max(1, max - 1)).trimEnd() + "…" : value;