@bubblebrain-ai/bubble 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/budget-ledger.d.ts +20 -0
- package/dist/agent/budget-ledger.js +51 -0
- package/dist/agent/execution-governor.js +1 -1
- package/dist/agent/profiles.d.ts +59 -0
- package/dist/agent/profiles.js +460 -0
- package/dist/agent/subagent-control.d.ts +52 -0
- package/dist/agent/subagent-control.js +38 -0
- package/dist/agent/task-size.d.ts +9 -0
- package/dist/agent/task-size.js +33 -0
- package/dist/agent/tool-intent.d.ts +1 -0
- package/dist/agent/tool-intent.js +1 -1
- package/dist/agent.d.ts +60 -1
- package/dist/agent.js +648 -55
- package/dist/context/budget.js +1 -0
- package/dist/context/compact-llm.js +7 -6
- package/dist/context/compact.js +6 -6
- package/dist/context/projector.d.ts +3 -3
- package/dist/context/projector.js +32 -18
- package/dist/context/prune.d.ts +2 -2
- package/dist/context/prune.js +1 -4
- package/dist/main.js +12 -5
- package/dist/mcp/manager.js +1 -0
- package/dist/orchestrator/default-hooks.js +85 -35
- package/dist/orchestrator/hooks.d.ts +5 -3
- package/dist/prompt/compose.d.ts +1 -0
- package/dist/prompt/compose.js +11 -1
- package/dist/prompt/environment.js +23 -2
- package/dist/prompt/provider-prompts/deepseek.js +1 -2
- package/dist/prompt/provider-prompts/kimi.js +1 -2
- package/dist/prompt/reminders.d.ts +21 -2
- package/dist/prompt/reminders.js +53 -8
- package/dist/prompt/runtime.d.ts +1 -1
- package/dist/prompt/runtime.js +17 -23
- package/dist/provider-artifacts.d.ts +7 -0
- package/dist/provider-artifacts.js +60 -0
- package/dist/provider.d.ts +16 -8
- package/dist/provider.js +149 -34
- package/dist/session-log.js +3 -1
- package/dist/system-prompt.d.ts +2 -0
- package/dist/tools/agent-lifecycle.d.ts +6 -0
- package/dist/tools/agent-lifecycle.js +355 -0
- package/dist/tools/bash.d.ts +2 -1
- package/dist/tools/bash.js +3 -1
- package/dist/tools/edit-apply.d.ts +25 -0
- package/dist/tools/edit-apply.js +228 -0
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +75 -56
- package/dist/tools/exit-plan-mode.js +3 -1
- package/dist/tools/file-mutation-queue.d.ts +1 -0
- package/dist/tools/file-mutation-queue.js +32 -0
- package/dist/tools/file-state.d.ts +25 -0
- package/dist/tools/file-state.js +52 -0
- package/dist/tools/glob.js +1 -0
- package/dist/tools/grep.js +1 -0
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.js +9 -7
- package/dist/tools/lsp.js +2 -0
- package/dist/tools/memory.js +2 -0
- package/dist/tools/question.js +2 -0
- package/dist/tools/read.d.ts +2 -1
- package/dist/tools/read.js +6 -1
- package/dist/tools/skill.js +1 -0
- package/dist/tools/task.js +1 -0
- package/dist/tools/todo.js +1 -0
- package/dist/tools/tool-search.js +2 -1
- package/dist/tools/web-fetch.js +1 -0
- package/dist/tools/web-search.js +1 -0
- package/dist/tools/write.d.ts +4 -3
- package/dist/tools/write.js +135 -54
- package/dist/tui/display-history.d.ts +10 -1
- package/dist/tui/markdown-inline.d.ts +22 -0
- package/dist/tui/markdown-inline.js +68 -0
- package/dist/tui/render-signature.d.ts +1 -0
- package/dist/tui/render-signature.js +7 -0
- package/dist/tui/run.js +811 -274
- package/dist/tui/streaming-tool-args.d.ts +15 -0
- package/dist/tui/streaming-tool-args.js +30 -0
- package/dist/tui/tool-renderers/fallback.d.ts +2 -0
- package/dist/tui/tool-renderers/fallback.js +75 -0
- package/dist/tui/tool-renderers/registry.d.ts +3 -0
- package/dist/tui/tool-renderers/registry.js +11 -0
- package/dist/tui/tool-renderers/subagent.d.ts +2 -0
- package/dist/tui/tool-renderers/subagent.js +114 -0
- package/dist/tui/tool-renderers/types.d.ts +36 -0
- package/dist/tui/tool-renderers/types.js +1 -0
- package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
- package/dist/tui/tool-renderers/write-preview.js +30 -0
- package/dist/tui/tool-renderers/write.d.ts +6 -0
- package/dist/tui/tool-renderers/write.js +88 -0
- package/dist/types.d.ts +105 -10
- 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,12 @@ 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";
|
|
21
|
+
import { extractStreamingArgsHint } from "./streaming-tool-args.js";
|
|
16
22
|
import { getNextPermissionMode, PERMISSION_MODE_INFO } from "../permission/mode.js";
|
|
17
23
|
import { getContextBudget } from "../context/budget.js";
|
|
18
24
|
import { getLspService } from "../lsp/index.js";
|
|
@@ -55,6 +61,7 @@ const DEFAULT_THEME = {
|
|
|
55
61
|
messageAssistantText: "#eeeeee",
|
|
56
62
|
messageAssistantAccent: "#fab283",
|
|
57
63
|
messageThinkingText: "#8b949e",
|
|
64
|
+
messageThinkingContentText: "#6e7681",
|
|
58
65
|
messageThinkingBorder: "#282828",
|
|
59
66
|
toolText: "#a6acb8",
|
|
60
67
|
toolPending: "#fab283",
|
|
@@ -103,10 +110,31 @@ const QUESTION_MAX_OPTIONS = 10;
|
|
|
103
110
|
const QUESTION_MAX_CONFIRM_ROWS = 3;
|
|
104
111
|
const QUESTION_PANEL_MIN_HEIGHT = 9;
|
|
105
112
|
const HOME_LOGO = [
|
|
106
|
-
" /\\
|
|
107
|
-
"(
|
|
108
|
-
"
|
|
113
|
+
{ text: " /\\___/\\ ", tone: "primary" },
|
|
114
|
+
{ text: "( ◕ ◕ )", tone: "primary" },
|
|
115
|
+
{ text: "( ◡ ω ◡ )", tone: "warning" },
|
|
116
|
+
{ text: " (\")_(\") ", tone: "warning" },
|
|
117
|
+
{ text: "", tone: "primary" },
|
|
118
|
+
{ text: "· ◌ ○ ◯ ·", tone: "textMuted" },
|
|
119
|
+
{ text: "", tone: "primary" },
|
|
120
|
+
{ text: "██████╗ ██╗ ██╗██████╗ ██████╗ ██╗ ███████╗", tone: "primary" },
|
|
121
|
+
{ text: "██╔══██╗██║ ██║██╔══██╗██╔══██╗██║ ██╔════╝", tone: "primary" },
|
|
122
|
+
{ text: "██████╔╝██║ ██║██████╔╝██████╔╝██║ █████╗ ", tone: "warning" },
|
|
123
|
+
{ text: "██╔══██╗██║ ██║██╔══██╗██╔══██╗██║ ██╔══╝ ", tone: "warning" },
|
|
124
|
+
{ text: "██████╔╝╚██████╔╝██████╔╝██████╔╝███████╗███████╗", tone: "accent" },
|
|
125
|
+
{ text: "╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝", tone: "accent" },
|
|
126
|
+
{ text: "", tone: "primary" },
|
|
127
|
+
{ text: "── your bubble coding companion ──", tone: "secondary" },
|
|
109
128
|
];
|
|
129
|
+
function homeLogoColor(tone) {
|
|
130
|
+
switch (tone) {
|
|
131
|
+
case "primary": return theme.primary;
|
|
132
|
+
case "warning": return theme.warning;
|
|
133
|
+
case "accent": return theme.accent;
|
|
134
|
+
case "secondary": return theme.secondary;
|
|
135
|
+
case "textMuted": return theme.textMuted;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
110
138
|
const HOME_TIPS = [
|
|
111
139
|
"Type @ followed by a filename to attach file context",
|
|
112
140
|
"Press Shift+Tab to cycle Build, Plan, and Bypass modes",
|
|
@@ -318,20 +346,16 @@ function OpenTuiApp(props) {
|
|
|
318
346
|
let rootBox;
|
|
319
347
|
let sidebarShell;
|
|
320
348
|
let transcriptHost;
|
|
321
|
-
const transcriptState = {
|
|
349
|
+
const transcriptState = {
|
|
350
|
+
entries: [],
|
|
351
|
+
expandedCompactions: new Set(),
|
|
352
|
+
expandedWrites: new Set(),
|
|
353
|
+
defaultWritesExpanded: false,
|
|
354
|
+
};
|
|
322
355
|
let dock;
|
|
323
356
|
let homeComposerShell;
|
|
324
357
|
let sessionComposerShell;
|
|
325
358
|
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
359
|
let approvalRoot;
|
|
336
360
|
let approvalHeaderTitle;
|
|
337
361
|
let approvalMetaIcon;
|
|
@@ -1524,7 +1548,6 @@ function OpenTuiApp(props) {
|
|
|
1524
1548
|
for (const timer of questionSyncTimers)
|
|
1525
1549
|
clearTimeout(timer);
|
|
1526
1550
|
questionSyncTimers.clear();
|
|
1527
|
-
stopThinkingSpinner();
|
|
1528
1551
|
if (props.options.planHandlerRef)
|
|
1529
1552
|
props.options.planHandlerRef.current = undefined;
|
|
1530
1553
|
if (props.options.approvalHandlerRef)
|
|
@@ -1668,6 +1691,16 @@ function OpenTuiApp(props) {
|
|
|
1668
1691
|
event.preventDefault?.();
|
|
1669
1692
|
return true;
|
|
1670
1693
|
}
|
|
1694
|
+
if (event.ctrl && name === "t" && !picker) {
|
|
1695
|
+
toggleThinkingVisibility();
|
|
1696
|
+
event.preventDefault?.();
|
|
1697
|
+
return true;
|
|
1698
|
+
}
|
|
1699
|
+
if (event.ctrl && name === "o" && !picker) {
|
|
1700
|
+
toggleVisibleWriteBlocks();
|
|
1701
|
+
event.preventDefault?.();
|
|
1702
|
+
return true;
|
|
1703
|
+
}
|
|
1671
1704
|
if (routeModalKey(event))
|
|
1672
1705
|
return true;
|
|
1673
1706
|
if (cycleModeFromKey(event))
|
|
@@ -1688,12 +1721,12 @@ function OpenTuiApp(props) {
|
|
|
1688
1721
|
plan: pendingPlan()?.plan,
|
|
1689
1722
|
selectedOption: approvalOptionIdx(),
|
|
1690
1723
|
showThinking: showThinking(),
|
|
1691
|
-
|
|
1692
|
-
if (transcriptState.
|
|
1693
|
-
transcriptState.
|
|
1724
|
+
onToggleWrite: (key) => {
|
|
1725
|
+
if (transcriptState.expandedWrites.has(key)) {
|
|
1726
|
+
transcriptState.expandedWrites.delete(key);
|
|
1694
1727
|
}
|
|
1695
1728
|
else {
|
|
1696
|
-
transcriptState.
|
|
1729
|
+
transcriptState.expandedWrites.add(key);
|
|
1697
1730
|
}
|
|
1698
1731
|
syncSessionMessages();
|
|
1699
1732
|
},
|
|
@@ -1708,52 +1741,61 @@ function OpenTuiApp(props) {
|
|
|
1708
1741
|
},
|
|
1709
1742
|
};
|
|
1710
1743
|
}
|
|
1711
|
-
function
|
|
1712
|
-
if (!
|
|
1744
|
+
function toggleThinkingVisibility() {
|
|
1745
|
+
if (!currentTranscriptMessages(streamingDisplay).some((message) => !!message.reasoning?.trim())) {
|
|
1746
|
+
setNotice("No thinking blocks to toggle");
|
|
1713
1747
|
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
|
-
}
|
|
1729
|
-
if (rendered) {
|
|
1730
|
-
transcriptHost?.requestRender();
|
|
1731
|
-
rootBox?.requestRender();
|
|
1732
1748
|
}
|
|
1749
|
+
setShowThinking((prev) => {
|
|
1750
|
+
const next = !prev;
|
|
1751
|
+
setNotice(next ? "Thinking blocks visible" : "Thinking blocks hidden");
|
|
1752
|
+
return next;
|
|
1753
|
+
});
|
|
1754
|
+
redrawTranscript();
|
|
1733
1755
|
}
|
|
1734
|
-
function
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
thinkingSpinnerFrameIndex = 0;
|
|
1739
|
-
}
|
|
1740
|
-
function syncThinkingSpinner() {
|
|
1741
|
-
const hasStreamingThinking = transcriptState.entries.some((entry) => !!entry.refs.reasoningToggleText && entry.refs.reasoningStreaming === true);
|
|
1742
|
-
if (!hasStreamingThinking) {
|
|
1743
|
-
stopThinkingSpinner();
|
|
1756
|
+
function toggleVisibleWriteBlocks() {
|
|
1757
|
+
const keys = collectVisibleWriteKeys();
|
|
1758
|
+
if (!keys.length) {
|
|
1759
|
+
setNotice("No write previews to toggle");
|
|
1744
1760
|
return;
|
|
1745
1761
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1762
|
+
const shouldExpand = keys.some((key) => !transcriptState.expandedWrites.has(key));
|
|
1763
|
+
transcriptState.defaultWritesExpanded = shouldExpand;
|
|
1764
|
+
for (const key of keys) {
|
|
1765
|
+
if (shouldExpand)
|
|
1766
|
+
transcriptState.expandedWrites.add(key);
|
|
1767
|
+
else
|
|
1768
|
+
transcriptState.expandedWrites.delete(key);
|
|
1769
|
+
}
|
|
1770
|
+
setNotice(shouldExpand ? "Write previews expanded" : "Write previews collapsed");
|
|
1771
|
+
syncSessionMessages();
|
|
1772
|
+
}
|
|
1773
|
+
function collectVisibleWriteKeys() {
|
|
1774
|
+
const messages = currentTranscriptMessages(streamingDisplay)
|
|
1775
|
+
.filter((message) => hasRenderableMessage(message, showThinking()));
|
|
1776
|
+
const keys = [];
|
|
1777
|
+
for (const [index, message] of messages.entries()) {
|
|
1778
|
+
const messageKey = transcriptMessageKey(message, index);
|
|
1779
|
+
for (const tool of message.toolCalls ?? []) {
|
|
1780
|
+
if (isWritePreviewTool(tool)) {
|
|
1781
|
+
keys.push(writeToolKey(messageKey, tool));
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return keys;
|
|
1786
|
+
}
|
|
1787
|
+
function syncSessionMessages(messages = currentTranscriptMessages(streamingDisplay)) {
|
|
1788
|
+
if (!transcriptHost)
|
|
1748
1789
|
return;
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
renderThinkingSpinnerFrame();
|
|
1752
|
-
}, PROMPT_SCANNER_INTERVAL_MS);
|
|
1790
|
+
updateTranscriptHost(transcriptHost, transcriptState, messages, transcriptOptions(), props.syntaxStyle, props.subtleSyntaxStyle);
|
|
1791
|
+
syncPromptSurfaces();
|
|
1753
1792
|
}
|
|
1754
1793
|
function redrawTranscript(extra, baseMessages = displayMessages) {
|
|
1755
|
-
const shouldFollow = shouldFollowTranscriptBeforeUpdate();
|
|
1756
1794
|
streamingDisplay = extra;
|
|
1795
|
+
renderTranscriptNow(streamingDisplay, baseMessages);
|
|
1796
|
+
}
|
|
1797
|
+
function renderTranscriptNow(extra, baseMessages = displayMessages) {
|
|
1798
|
+
const shouldFollow = shouldFollowTranscriptBeforeUpdate();
|
|
1757
1799
|
const nextMessages = compactDisplayMessages(extra ? [...baseMessages, extra] : baseMessages);
|
|
1758
1800
|
syncSessionMessages(nextMessages);
|
|
1759
1801
|
rootBox?.requestRender();
|
|
@@ -3078,12 +3120,7 @@ function OpenTuiApp(props) {
|
|
|
3078
3120
|
}
|
|
3079
3121
|
async function executeSlash(input) {
|
|
3080
3122
|
if (/^\/(?:thinking|toggle-thinking)(?:\s|$)/.test(input.trim())) {
|
|
3081
|
-
|
|
3082
|
-
const next = !prev;
|
|
3083
|
-
setNotice(next ? "Thinking blocks visible" : "Thinking blocks hidden");
|
|
3084
|
-
return next;
|
|
3085
|
-
});
|
|
3086
|
-
redrawTranscript();
|
|
3123
|
+
toggleThinkingVisibility();
|
|
3087
3124
|
return true;
|
|
3088
3125
|
}
|
|
3089
3126
|
const wasHomeSurfaceActive = isHomeSurfaceActive();
|
|
@@ -3525,54 +3562,128 @@ function OpenTuiApp(props) {
|
|
|
3525
3562
|
let assistantContent = "";
|
|
3526
3563
|
let assistantReasoning = "";
|
|
3527
3564
|
const toolCalls = [];
|
|
3565
|
+
let currentTurnHasToolCall = false;
|
|
3566
|
+
let turnStartedAt;
|
|
3528
3567
|
let runError;
|
|
3529
3568
|
let runCancelled = false;
|
|
3569
|
+
// Throttle redraws driven by per-token streaming events (reasoning_delta
|
|
3570
|
+
// and tool_call_delta). Both can fire hundreds of times per second on a
|
|
3571
|
+
// long reply; coalescing into ~16fps keeps the transcript alive without
|
|
3572
|
+
// thrashing OpenTUI's layout or re-parsing markdown per token.
|
|
3573
|
+
let pendingStreamingRedrawTimer;
|
|
3574
|
+
const STREAMING_REDRAW_INTERVAL_MS = 60;
|
|
3575
|
+
const buildStreamingDisplay = (status) => ({
|
|
3576
|
+
role: "assistant",
|
|
3577
|
+
content: "",
|
|
3578
|
+
reasoning: assistantReasoning || undefined,
|
|
3579
|
+
toolCalls: toolCalls.length ? [...toolCalls] : undefined,
|
|
3580
|
+
status,
|
|
3581
|
+
streaming: true,
|
|
3582
|
+
turnStartedAt,
|
|
3583
|
+
});
|
|
3584
|
+
const flushStreamingRedraw = () => {
|
|
3585
|
+
if (pendingStreamingRedrawTimer === undefined)
|
|
3586
|
+
return;
|
|
3587
|
+
clearTimeout(pendingStreamingRedrawTimer);
|
|
3588
|
+
pendingStreamingRedrawTimer = undefined;
|
|
3589
|
+
redrawTranscript(buildStreamingDisplay(toolCalls.length ? undefined : "thinking"));
|
|
3590
|
+
};
|
|
3591
|
+
const scheduleStreamingRedraw = () => {
|
|
3592
|
+
if (pendingStreamingRedrawTimer !== undefined)
|
|
3593
|
+
return;
|
|
3594
|
+
pendingStreamingRedrawTimer = setTimeout(flushStreamingRedraw, STREAMING_REDRAW_INTERVAL_MS);
|
|
3595
|
+
};
|
|
3530
3596
|
try {
|
|
3531
3597
|
for await (const event of props.agent.run(actualInput, props.args.cwd, { abortSignal: run.abortController.signal })) {
|
|
3532
3598
|
if (event.type === "turn_start") {
|
|
3533
3599
|
assistantContent = "";
|
|
3534
3600
|
assistantReasoning = "";
|
|
3535
3601
|
toolCalls.length = 0;
|
|
3602
|
+
currentTurnHasToolCall = false;
|
|
3603
|
+
turnStartedAt = Date.now();
|
|
3536
3604
|
redrawTranscript({
|
|
3537
3605
|
role: "assistant",
|
|
3538
3606
|
content: "",
|
|
3539
3607
|
status: "thinking",
|
|
3540
3608
|
streaming: true,
|
|
3609
|
+
turnStartedAt,
|
|
3541
3610
|
});
|
|
3542
3611
|
}
|
|
3543
3612
|
else if (event.type === "text_delta") {
|
|
3544
3613
|
assistantContent += event.content;
|
|
3545
|
-
redrawTranscript({
|
|
3546
|
-
role: "assistant",
|
|
3547
|
-
content: assistantContent,
|
|
3548
|
-
reasoning: assistantReasoning || undefined,
|
|
3549
|
-
toolCalls: toolCalls.length ? [...toolCalls] : undefined,
|
|
3550
|
-
status: "responding",
|
|
3551
|
-
streaming: true,
|
|
3552
|
-
});
|
|
3553
3614
|
}
|
|
3554
3615
|
else if (event.type === "reasoning_delta") {
|
|
3555
3616
|
assistantReasoning += event.content;
|
|
3617
|
+
scheduleStreamingRedraw();
|
|
3618
|
+
}
|
|
3619
|
+
else if (event.type === "tool_call_start") {
|
|
3620
|
+
currentTurnHasToolCall = true;
|
|
3621
|
+
// Insert a streaming placeholder so the user sees feedback the moment
|
|
3622
|
+
// the model commits to a tool call, instead of waiting for the args
|
|
3623
|
+
// JSON to fully stream + parse.
|
|
3624
|
+
if (!toolCalls.find((item) => item.id === event.id)) {
|
|
3625
|
+
toolCalls.push({
|
|
3626
|
+
id: event.id,
|
|
3627
|
+
name: event.name,
|
|
3628
|
+
args: {},
|
|
3629
|
+
rawArguments: "",
|
|
3630
|
+
streamingArgs: true,
|
|
3631
|
+
status: "pending",
|
|
3632
|
+
});
|
|
3633
|
+
}
|
|
3556
3634
|
redrawTranscript({
|
|
3557
3635
|
role: "assistant",
|
|
3558
|
-
content:
|
|
3636
|
+
content: "",
|
|
3559
3637
|
reasoning: assistantReasoning || undefined,
|
|
3560
|
-
toolCalls:
|
|
3561
|
-
status: "thinking",
|
|
3638
|
+
toolCalls: [...toolCalls],
|
|
3562
3639
|
streaming: true,
|
|
3640
|
+
turnStartedAt,
|
|
3563
3641
|
});
|
|
3564
3642
|
}
|
|
3643
|
+
else if (event.type === "tool_call_delta") {
|
|
3644
|
+
currentTurnHasToolCall = true;
|
|
3645
|
+
const existing = toolCalls.find((item) => item.id === event.id);
|
|
3646
|
+
if (existing) {
|
|
3647
|
+
existing.name = event.name || existing.name;
|
|
3648
|
+
existing.rawArguments = event.arguments;
|
|
3649
|
+
existing.streamingArgs = true;
|
|
3650
|
+
const hint = extractStreamingArgsHint(event.arguments);
|
|
3651
|
+
if (hint.path && existing.args.path !== hint.path) {
|
|
3652
|
+
existing.args = { ...existing.args, path: hint.path };
|
|
3653
|
+
}
|
|
3654
|
+
existing.streamingNewlineCount = hint.newlineCount;
|
|
3655
|
+
scheduleStreamingRedraw();
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
else if (event.type === "tool_call_end") {
|
|
3659
|
+
currentTurnHasToolCall = true;
|
|
3660
|
+
}
|
|
3565
3661
|
else if (event.type === "tool_start") {
|
|
3566
|
-
|
|
3662
|
+
currentTurnHasToolCall = true;
|
|
3663
|
+
flushStreamingRedraw();
|
|
3664
|
+
const now = Date.now();
|
|
3665
|
+
const existing = toolCalls.find((item) => item.id === event.id);
|
|
3666
|
+
if (existing) {
|
|
3667
|
+
existing.args = event.args;
|
|
3668
|
+
existing.streamingArgs = false;
|
|
3669
|
+
existing.streamingNewlineCount = undefined;
|
|
3670
|
+
existing.rawArguments = undefined;
|
|
3671
|
+
existing.status = "running";
|
|
3672
|
+
existing.startedAt = existing.startedAt ?? now;
|
|
3673
|
+
}
|
|
3674
|
+
else {
|
|
3675
|
+
toolCalls.push({ id: event.id, name: event.name, args: event.args, status: "running", startedAt: now });
|
|
3676
|
+
}
|
|
3567
3677
|
if (event.name === "question") {
|
|
3568
3678
|
scheduleQuestionSync();
|
|
3569
3679
|
}
|
|
3570
3680
|
redrawTranscript({
|
|
3571
3681
|
role: "assistant",
|
|
3572
|
-
content:
|
|
3682
|
+
content: "",
|
|
3573
3683
|
reasoning: assistantReasoning || undefined,
|
|
3574
3684
|
toolCalls: [...toolCalls],
|
|
3575
3685
|
streaming: true,
|
|
3686
|
+
turnStartedAt,
|
|
3576
3687
|
});
|
|
3577
3688
|
}
|
|
3578
3689
|
else if (event.type === "tool_end") {
|
|
@@ -3582,12 +3693,14 @@ function OpenTuiApp(props) {
|
|
|
3582
3693
|
call.isError = event.result.isError;
|
|
3583
3694
|
call.metadata = event.result.metadata;
|
|
3584
3695
|
call.status = event.result.isError ? "error" : "completed";
|
|
3696
|
+
call.completedAt = Date.now();
|
|
3585
3697
|
redrawTranscript({
|
|
3586
3698
|
role: "assistant",
|
|
3587
|
-
content: assistantContent,
|
|
3699
|
+
content: currentTurnHasToolCall ? "" : assistantContent,
|
|
3588
3700
|
reasoning: assistantReasoning || undefined,
|
|
3589
3701
|
toolCalls: [...toolCalls],
|
|
3590
3702
|
streaming: true,
|
|
3703
|
+
turnStartedAt,
|
|
3591
3704
|
});
|
|
3592
3705
|
}
|
|
3593
3706
|
if (event.name === "question") {
|
|
@@ -3596,6 +3709,30 @@ function OpenTuiApp(props) {
|
|
|
3596
3709
|
refreshGitSidebar();
|
|
3597
3710
|
syncSidebarLsp();
|
|
3598
3711
|
}
|
|
3712
|
+
else if (event.type === "tool_update") {
|
|
3713
|
+
const call = toolCalls.find((item) => item.id === event.id);
|
|
3714
|
+
if (call) {
|
|
3715
|
+
call.metadata = mergeToolMetadata(call.metadata, event.update.metadata);
|
|
3716
|
+
call.result = event.update.message ?? call.result;
|
|
3717
|
+
const finished = event.update.status === "failed" || event.update.status === "blocked" || event.update.status === "cancelled" || event.update.status === "completed";
|
|
3718
|
+
call.status = event.update.status === "failed" || event.update.status === "blocked" || event.update.status === "cancelled"
|
|
3719
|
+
? "error"
|
|
3720
|
+
: event.update.status === "completed"
|
|
3721
|
+
? "completed"
|
|
3722
|
+
: "running";
|
|
3723
|
+
call.isError = call.status === "error";
|
|
3724
|
+
if (finished && call.completedAt === undefined)
|
|
3725
|
+
call.completedAt = Date.now();
|
|
3726
|
+
redrawTranscript({
|
|
3727
|
+
role: "assistant",
|
|
3728
|
+
content: currentTurnHasToolCall ? "" : assistantContent,
|
|
3729
|
+
reasoning: assistantReasoning || undefined,
|
|
3730
|
+
toolCalls: [...toolCalls],
|
|
3731
|
+
streaming: true,
|
|
3732
|
+
turnStartedAt,
|
|
3733
|
+
});
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3599
3736
|
else if (event.type === "todos_updated") {
|
|
3600
3737
|
setTodos(event.todos);
|
|
3601
3738
|
syncSidebarTodos(event.todos);
|
|
@@ -3607,6 +3744,10 @@ function OpenTuiApp(props) {
|
|
|
3607
3744
|
bumpSidebar();
|
|
3608
3745
|
}
|
|
3609
3746
|
else if (event.type === "turn_end") {
|
|
3747
|
+
if (pendingStreamingRedrawTimer !== undefined) {
|
|
3748
|
+
clearTimeout(pendingStreamingRedrawTimer);
|
|
3749
|
+
pendingStreamingRedrawTimer = undefined;
|
|
3750
|
+
}
|
|
3610
3751
|
if (event.usage) {
|
|
3611
3752
|
setSidebarUsage((current) => ({
|
|
3612
3753
|
contextTokens: event.usage.promptTokens || current.contextTokens,
|
|
@@ -3622,9 +3763,12 @@ function OpenTuiApp(props) {
|
|
|
3622
3763
|
bumpSidebar();
|
|
3623
3764
|
const assistantMessage = {
|
|
3624
3765
|
role: "assistant",
|
|
3625
|
-
content: assistantContent,
|
|
3766
|
+
content: currentTurnHasToolCall ? "" : assistantContent,
|
|
3626
3767
|
reasoning: assistantReasoning || undefined,
|
|
3627
3768
|
toolCalls: toolCalls.length ? [...toolCalls] : undefined,
|
|
3769
|
+
turnStartedAt,
|
|
3770
|
+
turnCompletedAt: Date.now(),
|
|
3771
|
+
turnUsage: event.usage,
|
|
3628
3772
|
};
|
|
3629
3773
|
const nextMessages = hasRenderableMessage(assistantMessage)
|
|
3630
3774
|
? [...displayMessages, assistantMessage]
|
|
@@ -3634,6 +3778,7 @@ function OpenTuiApp(props) {
|
|
|
3634
3778
|
assistantContent = "";
|
|
3635
3779
|
assistantReasoning = "";
|
|
3636
3780
|
toolCalls.length = 0;
|
|
3781
|
+
turnStartedAt = undefined;
|
|
3637
3782
|
streamingDisplay = undefined;
|
|
3638
3783
|
}
|
|
3639
3784
|
}
|
|
@@ -3645,6 +3790,10 @@ function OpenTuiApp(props) {
|
|
|
3645
3790
|
}
|
|
3646
3791
|
}
|
|
3647
3792
|
finally {
|
|
3793
|
+
if (pendingStreamingRedrawTimer !== undefined) {
|
|
3794
|
+
clearTimeout(pendingStreamingRedrawTimer);
|
|
3795
|
+
pendingStreamingRedrawTimer = undefined;
|
|
3796
|
+
}
|
|
3648
3797
|
pendingApprovalRef = undefined;
|
|
3649
3798
|
setPendingApproval(undefined);
|
|
3650
3799
|
setApprovalOptionIdx(0);
|
|
@@ -3736,7 +3885,7 @@ function OpenTuiApp(props) {
|
|
|
3736
3885
|
paddingLeft: 2,
|
|
3737
3886
|
paddingRight: 2,
|
|
3738
3887
|
}, [
|
|
3739
|
-
h("box", { flexShrink: 0, flexDirection: "column" }, ...HOME_LOGO.map((line) => h("text", { fg:
|
|
3888
|
+
h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center" }, ...HOME_LOGO.map((line) => h("text", { fg: homeLogoColor(line.tone) }, line.text || " "))),
|
|
3740
3889
|
h("box", { height: 1, minHeight: 0, flexShrink: 1 }),
|
|
3741
3890
|
h("box", {
|
|
3742
3891
|
ref: (ref) => {
|
|
@@ -4481,7 +4630,6 @@ function OpenTuiApp(props) {
|
|
|
4481
4630
|
if (isNewHost)
|
|
4482
4631
|
transcriptState.entries = [];
|
|
4483
4632
|
updateTranscriptHost(ref, transcriptState, currentTranscriptMessages(streamingDisplay), transcriptOptions(), props.syntaxStyle, props.subtleSyntaxStyle);
|
|
4484
|
-
syncThinkingSpinner();
|
|
4485
4633
|
syncPromptSurfaces(isNewHost);
|
|
4486
4634
|
if (isNewHost)
|
|
4487
4635
|
scheduleTranscriptScrollAfterUpdate(transcriptScrollFollowing, 0);
|
|
@@ -4820,19 +4968,31 @@ function renderAssistantMessage(message, syntaxStyle, subtleSyntaxStyle, showThi
|
|
|
4820
4968
|
borderColor: theme.messageThinkingBorder,
|
|
4821
4969
|
flexDirection: "column",
|
|
4822
4970
|
flexShrink: 0,
|
|
4823
|
-
}, renderMarkdownContent(formatThinkingMarkdown(visibleReasoning), subtleSyntaxStyle, {
|
|
4971
|
+
}, h("text", { content: thinkingLabelContent(message.streaming === true, reasoningElapsedMs(message)), fg: theme.messageThinkingText, wrapMode: "none" }), renderMarkdownContent(formatThinkingMarkdown(visibleReasoning), subtleSyntaxStyle, {
|
|
4824
4972
|
streaming: message.streaming === true,
|
|
4825
|
-
fg: theme.
|
|
4973
|
+
fg: theme.messageThinkingContentText,
|
|
4826
4974
|
})));
|
|
4827
4975
|
}
|
|
4828
|
-
|
|
4976
|
+
const toolCalls = message.toolCalls ?? [];
|
|
4977
|
+
for (const tool of toolCalls)
|
|
4829
4978
|
children.push(renderTool(tool, syntaxStyle, width));
|
|
4830
|
-
|
|
4831
|
-
|
|
4979
|
+
const trimmedContent = message.content.trim();
|
|
4980
|
+
if (trimmedContent && toolCalls.length > 0) {
|
|
4981
|
+
children.push(h("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0 }, h("text", { content: answerDividerStyledText(), wrapMode: "none" })));
|
|
4982
|
+
}
|
|
4983
|
+
if (trimmedContent) {
|
|
4984
|
+
children.push(h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, renderMarkdownContent(trimmedContent, syntaxStyle, {
|
|
4832
4985
|
streaming: message.streaming === true,
|
|
4833
4986
|
fg: theme.messageAssistantText,
|
|
4834
4987
|
})));
|
|
4835
4988
|
}
|
|
4989
|
+
if (message.streaming === true && trimmedContent) {
|
|
4990
|
+
children.push(h("box", { paddingLeft: 3, flexShrink: 0 }, h("text", { fg: theme.primary, wrapMode: "none" }, "▌")));
|
|
4991
|
+
}
|
|
4992
|
+
const summaryString = formatTurnSummary(message);
|
|
4993
|
+
if (summaryString) {
|
|
4994
|
+
children.push(h("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0 }, h("text", { fg: theme.textMuted, wrapMode: "none" }, summaryString)));
|
|
4995
|
+
}
|
|
4836
4996
|
if (!children.length)
|
|
4837
4997
|
return null;
|
|
4838
4998
|
return h("box", { flexDirection: "column", flexShrink: 0 }, children);
|
|
@@ -4911,12 +5071,23 @@ function updateTranscriptHost(host, state, messages, options, syntaxStyle, subtl
|
|
|
4911
5071
|
}
|
|
4912
5072
|
for (const [index, message] of visibleMessages.entries()) {
|
|
4913
5073
|
const key = transcriptMessageKey(message, index);
|
|
4914
|
-
|
|
5074
|
+
if (state.defaultWritesExpanded) {
|
|
5075
|
+
for (const tool of message.toolCalls ?? []) {
|
|
5076
|
+
if (isWritePreviewTool(tool)) {
|
|
5077
|
+
state.expandedWrites.add(writeToolKey(key, tool));
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
4915
5081
|
const compactionExpanded = state.expandedCompactions.has(key);
|
|
4916
|
-
const signature = transcriptMessageSignature(message,
|
|
5082
|
+
const signature = transcriptMessageSignature(message, compactionExpanded);
|
|
4917
5083
|
const previous = state.entries[index];
|
|
4918
5084
|
if (previous?.key === key && previous.signature === signature) {
|
|
4919
|
-
updateMessageEntry(previous, message, showThinking,
|
|
5085
|
+
updateMessageEntry(previous, message, showThinking, compactionExpanded, {
|
|
5086
|
+
syntaxStyle,
|
|
5087
|
+
expandedWrites: state.expandedWrites,
|
|
5088
|
+
width: options?.width ?? 80,
|
|
5089
|
+
onToggleWrite: options?.onToggleWrite,
|
|
5090
|
+
});
|
|
4920
5091
|
nextEntries.push(previous);
|
|
4921
5092
|
continue;
|
|
4922
5093
|
}
|
|
@@ -4924,7 +5095,7 @@ function updateTranscriptHost(host, state, messages, options, syntaxStyle, subtl
|
|
|
4924
5095
|
host.remove(previous.node.id);
|
|
4925
5096
|
previous.node.destroyRecursively();
|
|
4926
5097
|
}
|
|
4927
|
-
const entry = createMessageEntry(ctx, message, index, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking, options?.width ?? 80,
|
|
5098
|
+
const entry = createMessageEntry(ctx, message, index, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking, options?.width ?? 80, compactionExpanded, state.expandedWrites, options?.onToggleCompaction, options?.onToggleWrite);
|
|
4928
5099
|
if (entry) {
|
|
4929
5100
|
host.add(entry.node, index);
|
|
4930
5101
|
nextEntries.push(entry);
|
|
@@ -4967,34 +5138,23 @@ function clearTranscriptEntries(host, state) {
|
|
|
4967
5138
|
function transcriptMessageKey(message, index) {
|
|
4968
5139
|
return `${index}:${message.role}`;
|
|
4969
5140
|
}
|
|
4970
|
-
function transcriptMessageSignature(message,
|
|
5141
|
+
function transcriptMessageSignature(message, compactionExpanded = false) {
|
|
4971
5142
|
if (message.role !== "assistant")
|
|
4972
5143
|
return message.role;
|
|
4973
5144
|
if (message.syntheticKind === "ui_compact_card") {
|
|
4974
5145
|
return `compaction:${compactionExpanded ? "expanded" : "collapsed"}:${message.compactionMeta?.turns ?? 0}`;
|
|
4975
5146
|
}
|
|
4976
5147
|
const modelSwitch = parseModelSwitchMessage(message.content);
|
|
4977
|
-
const
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
5148
|
+
const pureModelSwitch = modelSwitch && !message.reasoning?.trim() && !(message.toolCalls?.length);
|
|
5149
|
+
if (pureModelSwitch) {
|
|
5150
|
+
return `assistant:model-switch:${hashString(modelSwitch)}`;
|
|
5151
|
+
}
|
|
4981
5152
|
return [
|
|
4982
5153
|
message.role,
|
|
4983
|
-
|
|
4984
|
-
message.status ?? "idle",
|
|
4985
|
-
visibleReasoning ? (thinkingExpanded ? "reasoning-expanded" : "reasoning-collapsed") : "no-reasoning",
|
|
4986
|
-
message.content.trim() ? "content" : "no-content",
|
|
4987
|
-
tools,
|
|
5154
|
+
"standard",
|
|
4988
5155
|
].join(":");
|
|
4989
5156
|
}
|
|
4990
|
-
function
|
|
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) {
|
|
5157
|
+
function updateMessageEntry(entry, message, showThinking = true, compactionExpanded = false, assistantOptions) {
|
|
4998
5158
|
if (message.role === "user") {
|
|
4999
5159
|
if (entry.refs.userText)
|
|
5000
5160
|
entry.refs.userText.content = message.content || " ";
|
|
@@ -5018,21 +5178,163 @@ function updateMessageEntry(entry, message, showThinking = true, thinkingExpande
|
|
|
5018
5178
|
}
|
|
5019
5179
|
return;
|
|
5020
5180
|
}
|
|
5181
|
+
if (assistantOptions) {
|
|
5182
|
+
updateAssistantEntry(entry, message, showThinking, assistantOptions);
|
|
5183
|
+
return;
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
function updateAssistantEntry(entry, message, showThinking, options) {
|
|
5187
|
+
const content = message.content.trim();
|
|
5188
|
+
const visibleReasoning = showThinking ? message.reasoning?.trim() ?? "" : "";
|
|
5189
|
+
const tools = message.toolCalls ?? [];
|
|
5190
|
+
const showStatus = !!message.status && !visibleReasoning && !content && tools.length === 0;
|
|
5021
5191
|
if (entry.refs.statusText) {
|
|
5022
|
-
entry.refs.statusText.content = assistantStatusLabel(message);
|
|
5192
|
+
entry.refs.statusText.content = showStatus ? assistantStatusLabel(message) : "";
|
|
5193
|
+
}
|
|
5194
|
+
if (entry.refs.statusBox) {
|
|
5195
|
+
entry.refs.statusBox.visible = showStatus;
|
|
5023
5196
|
}
|
|
5197
|
+
const streamingReasoning = message.streaming === true;
|
|
5024
5198
|
if (entry.refs.reasoningToggleText) {
|
|
5025
|
-
entry.refs.
|
|
5026
|
-
entry.refs.
|
|
5027
|
-
|
|
5199
|
+
entry.refs.reasoningStreaming = streamingReasoning;
|
|
5200
|
+
entry.refs.reasoningToggleText.content = visibleReasoning
|
|
5201
|
+
? thinkingLabelContent(streamingReasoning, reasoningElapsedMs(message))
|
|
5202
|
+
: new StyledText([fg(theme.messageThinkingText)("")]);
|
|
5203
|
+
}
|
|
5204
|
+
// During streaming we update only the plain text node — cheap per-delta. The
|
|
5205
|
+
// markdown node stays hidden + stale. Once streaming ends (turn_end), we
|
|
5206
|
+
// pay the parse cost exactly once and swap visibility.
|
|
5207
|
+
if (entry.refs.reasoningPlainText) {
|
|
5208
|
+
if (streamingReasoning) {
|
|
5209
|
+
entry.refs.reasoningPlainText.content = formatThinkingMarkdown(visibleReasoning);
|
|
5210
|
+
}
|
|
5211
|
+
entry.refs.reasoningPlainText.visible = streamingReasoning && !!visibleReasoning;
|
|
5028
5212
|
}
|
|
5029
5213
|
if (entry.refs.reasoningMarkdown) {
|
|
5030
|
-
|
|
5031
|
-
|
|
5214
|
+
if (!streamingReasoning) {
|
|
5215
|
+
syncMarkdownRenderable(entry.refs.reasoningMarkdown, formatThinkingMarkdown(visibleReasoning), false);
|
|
5216
|
+
}
|
|
5217
|
+
entry.refs.reasoningMarkdown.visible = !streamingReasoning && !!visibleReasoning;
|
|
5218
|
+
}
|
|
5219
|
+
if (entry.refs.reasoningBox) {
|
|
5220
|
+
entry.refs.reasoningBox.visible = !!visibleReasoning;
|
|
5221
|
+
}
|
|
5222
|
+
updateAssistantToolEntries(entry, tools, options);
|
|
5223
|
+
if (entry.refs.answerDividerBox) {
|
|
5224
|
+
const showDivider = tools.length > 0 && !!content;
|
|
5225
|
+
entry.refs.answerDividerBox.visible = showDivider;
|
|
5226
|
+
if (entry.refs.answerDividerText) {
|
|
5227
|
+
entry.refs.answerDividerText.content = showDivider
|
|
5228
|
+
? answerDividerStyledText()
|
|
5229
|
+
: new StyledText([fg(theme.textMuted)("")]);
|
|
5230
|
+
}
|
|
5032
5231
|
}
|
|
5033
5232
|
if (entry.refs.contentMarkdown) {
|
|
5034
|
-
entry.refs.contentMarkdown
|
|
5035
|
-
|
|
5233
|
+
syncMarkdownRenderable(entry.refs.contentMarkdown, content, message.streaming === true);
|
|
5234
|
+
}
|
|
5235
|
+
if (entry.refs.contentBox) {
|
|
5236
|
+
entry.refs.contentBox.visible = !!content;
|
|
5237
|
+
}
|
|
5238
|
+
if (entry.refs.contentCursorBox) {
|
|
5239
|
+
const cursorActive = message.streaming === true && !!content;
|
|
5240
|
+
entry.refs.contentCursorBox.visible = cursorActive;
|
|
5241
|
+
if (entry.refs.contentCursorText)
|
|
5242
|
+
entry.refs.contentCursorText.content = cursorActive ? "▌" : "";
|
|
5243
|
+
}
|
|
5244
|
+
const summaryString = formatTurnSummary(message);
|
|
5245
|
+
if (entry.refs.turnSummaryText) {
|
|
5246
|
+
entry.refs.turnSummaryText.content = summaryString ?? "";
|
|
5247
|
+
}
|
|
5248
|
+
if (entry.refs.turnSummaryBox) {
|
|
5249
|
+
entry.refs.turnSummaryBox.visible = !!summaryString;
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5252
|
+
function syncMarkdownRenderable(markdown, content, streaming) {
|
|
5253
|
+
if (markdown.content === content && markdown.streaming === streaming)
|
|
5254
|
+
return;
|
|
5255
|
+
markdown.content = content;
|
|
5256
|
+
markdown.streaming = streaming;
|
|
5257
|
+
markdown.clearCache();
|
|
5258
|
+
}
|
|
5259
|
+
function updateAssistantToolEntries(entry, tools, options) {
|
|
5260
|
+
const toolsBox = entry.refs.toolsBox;
|
|
5261
|
+
if (!toolsBox)
|
|
5262
|
+
return;
|
|
5263
|
+
toolsBox.visible = tools.length > 0;
|
|
5264
|
+
const previousEntries = entry.refs.toolEntries ?? new Map();
|
|
5265
|
+
const nextEntries = new Map();
|
|
5266
|
+
tools.forEach((tool, index) => {
|
|
5267
|
+
const toolKey = writeToolKey(entry.key, tool);
|
|
5268
|
+
const writeExpanded = options.expandedWrites.has(toolKey);
|
|
5269
|
+
const signature = toolRenderableSignature(tool, writeExpanded);
|
|
5270
|
+
const previous = previousEntries.get(tool.id);
|
|
5271
|
+
if (previous?.signature === signature) {
|
|
5272
|
+
nextEntries.set(tool.id, previous);
|
|
5273
|
+
return;
|
|
5274
|
+
}
|
|
5275
|
+
if (previous) {
|
|
5276
|
+
toolsBox.remove(previous.node.id);
|
|
5277
|
+
previous.node.destroyRecursively();
|
|
5278
|
+
}
|
|
5279
|
+
const node = createToolRenderable(toolsBox.ctx, tool, options.syntaxStyle, options.width, writeExpanded, isWritePreviewTool(tool) ? () => options.onToggleWrite?.(toolKey) : undefined);
|
|
5280
|
+
toolsBox.add(node, index);
|
|
5281
|
+
nextEntries.set(tool.id, { signature, node });
|
|
5282
|
+
});
|
|
5283
|
+
for (const [id, previous] of previousEntries.entries()) {
|
|
5284
|
+
if (nextEntries.has(id))
|
|
5285
|
+
continue;
|
|
5286
|
+
toolsBox.remove(previous.node.id);
|
|
5287
|
+
previous.node.destroyRecursively();
|
|
5288
|
+
}
|
|
5289
|
+
entry.refs.toolEntries = nextEntries;
|
|
5290
|
+
}
|
|
5291
|
+
function toolRenderableSignature(tool, writeExpanded) {
|
|
5292
|
+
return [
|
|
5293
|
+
tool.id,
|
|
5294
|
+
tool.name,
|
|
5295
|
+
tool.status ?? (tool.result === undefined ? "pending" : "completed"),
|
|
5296
|
+
tool.isError ? "error" : "ok",
|
|
5297
|
+
tool.streamingArgs ? "streaming-args" : "args-complete",
|
|
5298
|
+
writeExpanded ? "expanded" : "collapsed",
|
|
5299
|
+
hashString(stableStringify(tool.args)),
|
|
5300
|
+
hashString(tool.rawArguments ?? ""),
|
|
5301
|
+
hashString(tool.result ?? ""),
|
|
5302
|
+
hashString(stableStringify(tool.metadata ?? null)),
|
|
5303
|
+
].join(":");
|
|
5304
|
+
}
|
|
5305
|
+
function mergeToolMetadata(current, incoming) {
|
|
5306
|
+
if (!incoming)
|
|
5307
|
+
return current;
|
|
5308
|
+
if (current?.kind !== "subagent" || incoming.kind !== "subagent") {
|
|
5309
|
+
return incoming;
|
|
5310
|
+
}
|
|
5311
|
+
const currentSubagents = Array.isArray(current.subagents) ? current.subagents : [];
|
|
5312
|
+
const incomingSubagents = Array.isArray(incoming.subagents) ? incoming.subagents : [];
|
|
5313
|
+
const byId = new Map();
|
|
5314
|
+
for (const item of currentSubagents) {
|
|
5315
|
+
const subAgentId = typeof item === "object" && item !== null && "subAgentId" in item
|
|
5316
|
+
? String(item.subAgentId)
|
|
5317
|
+
: "";
|
|
5318
|
+
byId.set(subAgentId || `current:${byId.size}`, item);
|
|
5319
|
+
}
|
|
5320
|
+
for (const item of incomingSubagents) {
|
|
5321
|
+
const subAgentId = typeof item === "object" && item !== null && "subAgentId" in item
|
|
5322
|
+
? String(item.subAgentId)
|
|
5323
|
+
: "";
|
|
5324
|
+
byId.set(subAgentId || `incoming:${byId.size}`, item);
|
|
5325
|
+
}
|
|
5326
|
+
return {
|
|
5327
|
+
...current,
|
|
5328
|
+
...incoming,
|
|
5329
|
+
subagents: [...byId.values()],
|
|
5330
|
+
};
|
|
5331
|
+
}
|
|
5332
|
+
function stableStringify(value) {
|
|
5333
|
+
try {
|
|
5334
|
+
return JSON.stringify(value) ?? "";
|
|
5335
|
+
}
|
|
5336
|
+
catch {
|
|
5337
|
+
return String(value);
|
|
5036
5338
|
}
|
|
5037
5339
|
}
|
|
5038
5340
|
function createBox(ctx, options, children = []) {
|
|
@@ -5056,6 +5358,7 @@ function createMarkdown(ctx, content, syntaxStyle, options) {
|
|
|
5056
5358
|
content,
|
|
5057
5359
|
syntaxStyle,
|
|
5058
5360
|
treeSitterClient,
|
|
5361
|
+
renderNode: createSemanticMarkdownRenderNode(ctx, options?.fg ?? theme.messageAssistantText),
|
|
5059
5362
|
streaming: options?.streaming === true,
|
|
5060
5363
|
conceal: true,
|
|
5061
5364
|
concealCode: false,
|
|
@@ -5075,6 +5378,87 @@ function createMarkdown(ctx, content, syntaxStyle, options) {
|
|
|
5075
5378
|
},
|
|
5076
5379
|
});
|
|
5077
5380
|
}
|
|
5381
|
+
function createSemanticMarkdownRenderNode(ctx, defaultFg) {
|
|
5382
|
+
const palette = semanticMarkdownPalette(defaultFg);
|
|
5383
|
+
return (token, context) => {
|
|
5384
|
+
switch (token?.type) {
|
|
5385
|
+
case "hr":
|
|
5386
|
+
return createText(ctx, new StyledText([
|
|
5387
|
+
fg(theme.borderSubtle)("─".repeat(48)),
|
|
5388
|
+
]), {
|
|
5389
|
+
fg: theme.borderSubtle,
|
|
5390
|
+
wrapMode: "none",
|
|
5391
|
+
flexShrink: 0,
|
|
5392
|
+
});
|
|
5393
|
+
case "heading":
|
|
5394
|
+
return createText(ctx, markdownInlineToStyledText(markdownTokenInlineTokens(token), palette, token.text ?? "", { bold: true }), {
|
|
5395
|
+
fg: defaultFg,
|
|
5396
|
+
wrapMode: "word",
|
|
5397
|
+
flexShrink: 0,
|
|
5398
|
+
});
|
|
5399
|
+
case "paragraph":
|
|
5400
|
+
return createText(ctx, markdownInlineToStyledText(markdownTokenInlineTokens(token), palette, token.text ?? ""), {
|
|
5401
|
+
fg: defaultFg,
|
|
5402
|
+
wrapMode: "word",
|
|
5403
|
+
flexShrink: 0,
|
|
5404
|
+
});
|
|
5405
|
+
case "list":
|
|
5406
|
+
return createMarkdownList(ctx, token, palette, defaultFg);
|
|
5407
|
+
default:
|
|
5408
|
+
return context.defaultRender();
|
|
5409
|
+
}
|
|
5410
|
+
};
|
|
5411
|
+
}
|
|
5412
|
+
function createMarkdownList(ctx, token, palette, defaultFg) {
|
|
5413
|
+
const ordered = token?.ordered === true;
|
|
5414
|
+
const start = typeof token?.start === "number" ? token.start : 1;
|
|
5415
|
+
const items = Array.isArray(token?.items) ? token.items : [];
|
|
5416
|
+
if (items.length === 0)
|
|
5417
|
+
return null;
|
|
5418
|
+
return createBox(ctx, {
|
|
5419
|
+
flexDirection: "column",
|
|
5420
|
+
flexShrink: 0,
|
|
5421
|
+
}, items.map((item, index) => {
|
|
5422
|
+
const marker = ordered ? `${start + index}. ` : "• ";
|
|
5423
|
+
return createText(ctx, new StyledText([
|
|
5424
|
+
fg(theme.textMuted)(marker),
|
|
5425
|
+
...markdownInlineToStyledText(markdownTokenInlineTokens(item), palette, item.text ?? "").chunks,
|
|
5426
|
+
]), {
|
|
5427
|
+
fg: defaultFg,
|
|
5428
|
+
wrapMode: "word",
|
|
5429
|
+
flexShrink: 0,
|
|
5430
|
+
});
|
|
5431
|
+
}));
|
|
5432
|
+
}
|
|
5433
|
+
function markdownTokenInlineTokens(token) {
|
|
5434
|
+
if (Array.isArray(token?.tokens))
|
|
5435
|
+
return token.tokens;
|
|
5436
|
+
if (typeof token?.text === "string")
|
|
5437
|
+
return [{ type: "text", text: token.text }];
|
|
5438
|
+
return undefined;
|
|
5439
|
+
}
|
|
5440
|
+
function semanticMarkdownPalette(defaultFg) {
|
|
5441
|
+
return {
|
|
5442
|
+
text: defaultFg,
|
|
5443
|
+
textMuted: theme.textMuted,
|
|
5444
|
+
success: theme.success,
|
|
5445
|
+
warning: theme.warning,
|
|
5446
|
+
secondary: theme.secondary,
|
|
5447
|
+
};
|
|
5448
|
+
}
|
|
5449
|
+
function markdownInlineToStyledText(tokens, palette, fallback = "", style = {}) {
|
|
5450
|
+
const chunks = markdownInlineSegments(tokens, fallback, style).map((segment) => {
|
|
5451
|
+
let chunk = fg(palette[segment.color ?? "text"])(segment.text);
|
|
5452
|
+
if (segment.bold)
|
|
5453
|
+
chunk = bold(chunk);
|
|
5454
|
+
if (segment.italic)
|
|
5455
|
+
chunk = italic(chunk);
|
|
5456
|
+
if (segment.dim)
|
|
5457
|
+
chunk = dim(chunk);
|
|
5458
|
+
return chunk;
|
|
5459
|
+
});
|
|
5460
|
+
return new StyledText(chunks);
|
|
5461
|
+
}
|
|
5078
5462
|
function createDiffRenderable(ctx, diff, filePath, syntaxStyle, width = 80) {
|
|
5079
5463
|
return new DiffRenderable(ctx, {
|
|
5080
5464
|
diff,
|
|
@@ -5137,6 +5521,24 @@ function createCodeBlockRenderable(ctx, content, filePath, syntaxStyle) {
|
|
|
5137
5521
|
lineNumbers.add(code);
|
|
5138
5522
|
return lineNumbers;
|
|
5139
5523
|
}
|
|
5524
|
+
function createToolRenderHelpers() {
|
|
5525
|
+
return {
|
|
5526
|
+
theme,
|
|
5527
|
+
createBox: (ctx, options, children) => createBox(ctx, options, children),
|
|
5528
|
+
createText: (ctx, content, options) => createText(ctx, content, (options ?? {})),
|
|
5529
|
+
createCodeBlockRenderable,
|
|
5530
|
+
createDiffRenderable,
|
|
5531
|
+
toolColor,
|
|
5532
|
+
displayToolName,
|
|
5533
|
+
toolHeader,
|
|
5534
|
+
toolPath,
|
|
5535
|
+
extractToolDiff,
|
|
5536
|
+
summarizeToolResult,
|
|
5537
|
+
isToolFinished,
|
|
5538
|
+
toolPreview,
|
|
5539
|
+
toolStateIcon,
|
|
5540
|
+
};
|
|
5541
|
+
}
|
|
5140
5542
|
function renderCodeBlockContent(content, filePath, syntaxStyle) {
|
|
5141
5543
|
return h("line_number", { fg: theme.textMuted, minWidth: 3, paddingRight: 1 }, h("code", {
|
|
5142
5544
|
content,
|
|
@@ -5148,14 +5550,14 @@ function renderCodeBlockContent(content, filePath, syntaxStyle) {
|
|
|
5148
5550
|
width: "100%",
|
|
5149
5551
|
}));
|
|
5150
5552
|
}
|
|
5151
|
-
function createMessageEntry(ctx, message, index, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking = true, width = 80,
|
|
5553
|
+
function createMessageEntry(ctx, message, index, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking = true, width = 80, compactionExpanded = false, expandedWrites = new Set(), onToggleCompaction, onToggleWrite) {
|
|
5152
5554
|
if (message.role === "user")
|
|
5153
5555
|
return createUserEntry(ctx, message, index, key, signature);
|
|
5154
5556
|
if (message.role === "error")
|
|
5155
5557
|
return createErrorEntry(ctx, message, key, signature);
|
|
5156
5558
|
if (message.syntheticKind === "ui_compact_card")
|
|
5157
5559
|
return createCompactionCardEntry(ctx, message, key, signature, compactionExpanded, onToggleCompaction);
|
|
5158
|
-
return createAssistantEntry(ctx, message, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking, width,
|
|
5560
|
+
return createAssistantEntry(ctx, message, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking, width, expandedWrites, onToggleWrite);
|
|
5159
5561
|
}
|
|
5160
5562
|
function createUserEntry(ctx, message, index, key, signature) {
|
|
5161
5563
|
const refs = {};
|
|
@@ -5200,7 +5602,7 @@ function createErrorEntry(ctx, message, key, signature) {
|
|
|
5200
5602
|
}, [text]);
|
|
5201
5603
|
return { key, signature, node, refs };
|
|
5202
5604
|
}
|
|
5203
|
-
function createAssistantEntry(ctx, message, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking = true, width = 80,
|
|
5605
|
+
function createAssistantEntry(ctx, message, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking = true, width = 80, expandedWrites = new Set(), onToggleWrite) {
|
|
5204
5606
|
const modelSwitch = parseModelSwitchMessage(message.content);
|
|
5205
5607
|
if (modelSwitch && !message.reasoning?.trim() && !(message.toolCalls?.length)) {
|
|
5206
5608
|
return createModelSwitchEntry(ctx, modelSwitch, key, signature);
|
|
@@ -5208,70 +5610,135 @@ function createAssistantEntry(ctx, message, syntaxStyle, subtleSyntaxStyle, key,
|
|
|
5208
5610
|
const children = [];
|
|
5209
5611
|
const refs = {};
|
|
5210
5612
|
const visibleReasoning = showThinking ? message.reasoning?.trim() : "";
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
children.push(createBox(ctx, {
|
|
5261
|
-
paddingLeft: 3,
|
|
5262
|
-
marginTop: 1,
|
|
5263
|
-
flexDirection: "column",
|
|
5613
|
+
const content = message.content.trim();
|
|
5614
|
+
const tools = message.toolCalls ?? [];
|
|
5615
|
+
const showStatus = !!message.status && !visibleReasoning && !content && tools.length === 0;
|
|
5616
|
+
const status = createText(ctx, assistantStatusLabel(message), {
|
|
5617
|
+
fg: theme.messageThinkingText,
|
|
5618
|
+
});
|
|
5619
|
+
refs.statusText = status;
|
|
5620
|
+
const statusBox = createBox(ctx, {
|
|
5621
|
+
paddingLeft: 3,
|
|
5622
|
+
marginTop: 1,
|
|
5623
|
+
flexShrink: 0,
|
|
5624
|
+
visible: showStatus,
|
|
5625
|
+
}, [status]);
|
|
5626
|
+
refs.statusBox = statusBox;
|
|
5627
|
+
children.push(statusBox);
|
|
5628
|
+
const labelText = createText(ctx, thinkingLabelContent(message.streaming === true, reasoningElapsedMs(message)), {
|
|
5629
|
+
fg: theme.messageThinkingText,
|
|
5630
|
+
wrapMode: "none",
|
|
5631
|
+
});
|
|
5632
|
+
refs.reasoningToggleText = labelText;
|
|
5633
|
+
const streamingReasoning = message.streaming === true;
|
|
5634
|
+
refs.reasoningStreaming = streamingReasoning;
|
|
5635
|
+
// While the model is still streaming we render reasoning as plain text — a
|
|
5636
|
+
// single TextRenderable.content update is cheap, whereas re-parsing markdown
|
|
5637
|
+
// (treesitter + cache clear) per token grows to O(N²) and freezes the TUI.
|
|
5638
|
+
// The markdown variant is parsed once at turn_end and only then becomes
|
|
5639
|
+
// visible.
|
|
5640
|
+
const plainText = createText(ctx, formatThinkingMarkdown(visibleReasoning ?? ""), {
|
|
5641
|
+
fg: theme.messageThinkingContentText,
|
|
5642
|
+
wrapMode: "word",
|
|
5643
|
+
visible: streamingReasoning && !!visibleReasoning,
|
|
5644
|
+
});
|
|
5645
|
+
refs.reasoningPlainText = plainText;
|
|
5646
|
+
const markdown = createMarkdown(ctx, streamingReasoning ? "" : formatThinkingMarkdown(visibleReasoning ?? ""), subtleSyntaxStyle, {
|
|
5647
|
+
streaming: false,
|
|
5648
|
+
fg: theme.messageThinkingContentText,
|
|
5649
|
+
});
|
|
5650
|
+
markdown.visible = !streamingReasoning && !!visibleReasoning;
|
|
5651
|
+
refs.reasoningMarkdown = markdown;
|
|
5652
|
+
const reasoningBox = createBox(ctx, {
|
|
5653
|
+
paddingLeft: 2,
|
|
5654
|
+
marginTop: 1,
|
|
5655
|
+
border: ["left"],
|
|
5656
|
+
borderColor: theme.messageThinkingBorder,
|
|
5657
|
+
flexDirection: "column",
|
|
5658
|
+
flexShrink: 0,
|
|
5659
|
+
visible: !!visibleReasoning,
|
|
5660
|
+
}, [
|
|
5661
|
+
createBox(ctx, {
|
|
5264
5662
|
flexShrink: 0,
|
|
5265
|
-
}, [
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5663
|
+
}, [labelText]),
|
|
5664
|
+
plainText,
|
|
5665
|
+
markdown,
|
|
5666
|
+
]);
|
|
5667
|
+
refs.reasoningBox = reasoningBox;
|
|
5668
|
+
children.push(reasoningBox);
|
|
5669
|
+
const toolsBox = createBox(ctx, {
|
|
5670
|
+
flexDirection: "column",
|
|
5671
|
+
flexShrink: 0,
|
|
5672
|
+
visible: tools.length > 0,
|
|
5673
|
+
});
|
|
5674
|
+
refs.toolsBox = toolsBox;
|
|
5675
|
+
refs.toolEntries = new Map();
|
|
5676
|
+
children.push(toolsBox);
|
|
5677
|
+
const showAnswerDivider = tools.length > 0 && !!content;
|
|
5678
|
+
const answerDividerText = createText(ctx, showAnswerDivider ? answerDividerStyledText() : new StyledText([fg(theme.textMuted)("")]), { wrapMode: "none" });
|
|
5679
|
+
refs.answerDividerText = answerDividerText;
|
|
5680
|
+
const answerDividerBox = createBox(ctx, {
|
|
5681
|
+
paddingLeft: 3,
|
|
5682
|
+
marginTop: 1,
|
|
5683
|
+
flexShrink: 0,
|
|
5684
|
+
visible: showAnswerDivider,
|
|
5685
|
+
}, [answerDividerText]);
|
|
5686
|
+
refs.answerDividerBox = answerDividerBox;
|
|
5687
|
+
children.push(answerDividerBox);
|
|
5688
|
+
const contentMarkdown = createMarkdown(ctx, content, syntaxStyle, {
|
|
5689
|
+
streaming: message.streaming === true,
|
|
5690
|
+
fg: theme.messageAssistantText,
|
|
5691
|
+
});
|
|
5692
|
+
refs.contentMarkdown = contentMarkdown;
|
|
5693
|
+
const contentBox = createBox(ctx, {
|
|
5694
|
+
paddingLeft: 3,
|
|
5695
|
+
marginTop: 1,
|
|
5696
|
+
flexDirection: "column",
|
|
5697
|
+
flexShrink: 0,
|
|
5698
|
+
visible: !!content,
|
|
5699
|
+
}, [contentMarkdown]);
|
|
5700
|
+
refs.contentBox = contentBox;
|
|
5701
|
+
children.push(contentBox);
|
|
5702
|
+
const cursorActive = message.streaming === true && !!content;
|
|
5703
|
+
const contentCursorText = createText(ctx, "▌", { fg: theme.primary, wrapMode: "none" });
|
|
5704
|
+
refs.contentCursorText = contentCursorText;
|
|
5705
|
+
const contentCursorBox = createBox(ctx, {
|
|
5706
|
+
paddingLeft: 3,
|
|
5707
|
+
flexShrink: 0,
|
|
5708
|
+
visible: cursorActive,
|
|
5709
|
+
}, [contentCursorText]);
|
|
5710
|
+
refs.contentCursorBox = contentCursorBox;
|
|
5711
|
+
children.push(contentCursorBox);
|
|
5712
|
+
const summaryString = formatTurnSummary(message);
|
|
5713
|
+
const turnSummaryText = createText(ctx, summaryString ?? "", { fg: theme.textMuted, wrapMode: "none" });
|
|
5714
|
+
refs.turnSummaryText = turnSummaryText;
|
|
5715
|
+
const turnSummaryBox = createBox(ctx, {
|
|
5716
|
+
paddingLeft: 3,
|
|
5717
|
+
marginTop: 1,
|
|
5718
|
+
flexShrink: 0,
|
|
5719
|
+
visible: !!summaryString,
|
|
5720
|
+
}, [turnSummaryText]);
|
|
5721
|
+
refs.turnSummaryBox = turnSummaryBox;
|
|
5722
|
+
children.push(turnSummaryBox);
|
|
5723
|
+
const entry = {
|
|
5270
5724
|
key,
|
|
5271
5725
|
signature,
|
|
5272
5726
|
node: createBox(ctx, { flexDirection: "column", flexShrink: 0 }, children),
|
|
5273
5727
|
refs,
|
|
5274
5728
|
};
|
|
5729
|
+
updateAssistantToolEntries(entry, tools, {
|
|
5730
|
+
syntaxStyle,
|
|
5731
|
+
expandedWrites,
|
|
5732
|
+
width,
|
|
5733
|
+
onToggleWrite,
|
|
5734
|
+
});
|
|
5735
|
+
return entry;
|
|
5736
|
+
}
|
|
5737
|
+
function answerDividerStyledText() {
|
|
5738
|
+
return new StyledText([
|
|
5739
|
+
fg(theme.accent)("◆ "),
|
|
5740
|
+
fg(theme.textMuted)(italic("Answer")),
|
|
5741
|
+
]);
|
|
5275
5742
|
}
|
|
5276
5743
|
function createCompactionCardEntry(ctx, message, key, signature, expanded, onToggle) {
|
|
5277
5744
|
const refs = {};
|
|
@@ -5389,7 +5856,7 @@ function createTodoWriteRenderable(ctx, tool) {
|
|
|
5389
5856
|
flexDirection: "column",
|
|
5390
5857
|
flexShrink: 0,
|
|
5391
5858
|
}, [
|
|
5392
|
-
createText(ctx,
|
|
5859
|
+
createText(ctx, `→ Planning tasks...`, { fg: toolColor(tool) }),
|
|
5393
5860
|
]);
|
|
5394
5861
|
}
|
|
5395
5862
|
return createBox(ctx, {
|
|
@@ -5416,90 +5883,26 @@ function createTodoWriteRenderable(ctx, tool) {
|
|
|
5416
5883
|
}),
|
|
5417
5884
|
]);
|
|
5418
5885
|
}
|
|
5419
|
-
function createToolRenderable(ctx, tool, syntaxStyle, width = 80) {
|
|
5886
|
+
function createToolRenderable(ctx, tool, syntaxStyle, width = 80, writeExpanded = false, onToggleWrite) {
|
|
5420
5887
|
if (tool.name === "question") {
|
|
5421
5888
|
return createQuestionToolRenderable(ctx, tool);
|
|
5422
5889
|
}
|
|
5423
5890
|
if (tool.name === "todo_write") {
|
|
5424
5891
|
return createTodoWriteRenderable(ctx, tool);
|
|
5425
5892
|
}
|
|
5426
|
-
const
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
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
|
-
}
|
|
5893
|
+
const renderer = findToolRenderer(tool);
|
|
5894
|
+
if (renderer) {
|
|
5895
|
+
return renderer.render({
|
|
5896
|
+
ctx,
|
|
5897
|
+
tool,
|
|
5898
|
+
syntaxStyle,
|
|
5899
|
+
width,
|
|
5900
|
+
writeExpanded,
|
|
5901
|
+
onToggleWrite,
|
|
5902
|
+
helpers: createToolRenderHelpers(),
|
|
5903
|
+
});
|
|
5494
5904
|
}
|
|
5495
|
-
|
|
5496
|
-
paddingLeft: 3,
|
|
5497
|
-
marginTop: 1,
|
|
5498
|
-
flexDirection: "column",
|
|
5499
|
-
flexShrink: 0,
|
|
5500
|
-
}, [
|
|
5501
|
-
createText(ctx, new StyledText(chunks), { wrapMode: "word" }),
|
|
5502
|
-
]);
|
|
5905
|
+
throw new Error(`No renderer for tool '${tool.name}'`);
|
|
5503
5906
|
}
|
|
5504
5907
|
function createQuestionToolRenderable(ctx, tool) {
|
|
5505
5908
|
const questions = questionToolQuestions(tool);
|
|
@@ -5512,7 +5915,7 @@ function createQuestionToolRenderable(ctx, tool) {
|
|
|
5512
5915
|
flexDirection: "column",
|
|
5513
5916
|
flexShrink: 0,
|
|
5514
5917
|
}, [
|
|
5515
|
-
createText(ctx,
|
|
5918
|
+
createText(ctx, `→ ${rejected ? "Asked" : "Asking"} questions...`, {
|
|
5516
5919
|
fg: rejected ? theme.textMuted : toolColor(tool),
|
|
5517
5920
|
attributes: rejected ? TextAttributes.STRIKETHROUGH : undefined,
|
|
5518
5921
|
}),
|
|
@@ -5558,16 +5961,27 @@ function renderTool(tool, syntaxStyle, width = 80) {
|
|
|
5558
5961
|
if (tool.name === "question") {
|
|
5559
5962
|
return renderQuestionTool(tool);
|
|
5560
5963
|
}
|
|
5561
|
-
const icon = tool
|
|
5964
|
+
const icon = toolStateIcon(tool);
|
|
5562
5965
|
const color = toolColor(tool);
|
|
5563
5966
|
const diff = extractToolDiff(tool);
|
|
5564
5967
|
if (diff && !tool.isError && tool.name === "edit") {
|
|
5565
5968
|
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
5969
|
}
|
|
5567
|
-
if (
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5970
|
+
if (isWritePreviewTool(tool)) {
|
|
5971
|
+
const hasContent = typeof tool.args.content === "string";
|
|
5972
|
+
const contentStr = hasContent ? String(tool.args.content) : "";
|
|
5973
|
+
const preview = hasContent ? formatWritePreview(contentStr, false) : null;
|
|
5974
|
+
const lineCount = hasContent
|
|
5975
|
+
? contentStr.split(/\r?\n/).length
|
|
5976
|
+
: (tool.streamingNewlineCount ?? 0) + 1;
|
|
5977
|
+
const summary = tool.result ?? `${isToolFinished(tool) ? "Prepared" : "Writing"} ${lineCount} lines to ${toolPath(tool) ?? "file"}`;
|
|
5978
|
+
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}`), preview ? renderCodeBlockContent(preview.content, toolPath(tool), syntaxStyle) : null, preview && preview.omittedLines > 0
|
|
5979
|
+
? h("text", { fg: theme.textMuted }, `... +${preview.omittedLines} lines (ctrl+o to expand)`)
|
|
5980
|
+
: preview && preview.omittedChars > 0
|
|
5981
|
+
? h("text", { fg: theme.textMuted }, `... +${preview.omittedChars} chars (ctrl+o to expand)`)
|
|
5982
|
+
: null));
|
|
5983
|
+
}
|
|
5984
|
+
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
5985
|
}
|
|
5572
5986
|
function renderQuestionTool(tool) {
|
|
5573
5987
|
const questions = questionToolQuestions(tool);
|
|
@@ -5577,7 +5991,7 @@ function renderQuestionTool(tool) {
|
|
|
5577
5991
|
return h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, h("text", {
|
|
5578
5992
|
fg: rejected ? theme.textMuted : toolColor(tool),
|
|
5579
5993
|
attributes: rejected ? TextAttributes.STRIKETHROUGH : undefined,
|
|
5580
|
-
},
|
|
5994
|
+
}, `→ ${rejected ? "Asked" : "Asking"} questions...`));
|
|
5581
5995
|
}
|
|
5582
5996
|
return h("box", {
|
|
5583
5997
|
border: ["left"],
|
|
@@ -5842,11 +6256,9 @@ function formatDisplayContentParts(content, labelStart) {
|
|
|
5842
6256
|
function reconstructDisplayMessages(agentMessages) {
|
|
5843
6257
|
const result = [];
|
|
5844
6258
|
for (const message of agentMessages) {
|
|
5845
|
-
if (message.role === "system" || message.role === "tool")
|
|
6259
|
+
if (message.role === "system" || message.role === "meta" || message.role === "tool")
|
|
5846
6260
|
continue;
|
|
5847
6261
|
if (message.role === "user") {
|
|
5848
|
-
if (message.isMeta)
|
|
5849
|
-
continue;
|
|
5850
6262
|
result.push({
|
|
5851
6263
|
role: "user",
|
|
5852
6264
|
content: typeof message.content === "string"
|
|
@@ -5935,7 +6347,9 @@ function formatTranscript(messages, options) {
|
|
|
5935
6347
|
if (visibleReasoning) {
|
|
5936
6348
|
appendBlank();
|
|
5937
6349
|
append("│ ", theme.messageThinkingBorder);
|
|
5938
|
-
|
|
6350
|
+
chunks.push(fg(theme.messageThinkingText)(italic("Thinking\n")));
|
|
6351
|
+
append("│ ", theme.messageThinkingBorder);
|
|
6352
|
+
appendLine(truncate(formatThinkingMarkdown(visibleReasoning), 500), theme.messageThinkingContentText);
|
|
5939
6353
|
}
|
|
5940
6354
|
if (message.status && !visibleReasoning && !message.content.trim() && !(message.toolCalls?.length)) {
|
|
5941
6355
|
appendBlank();
|
|
@@ -5946,7 +6360,7 @@ function formatTranscript(messages, options) {
|
|
|
5946
6360
|
appendBlank();
|
|
5947
6361
|
const icon = tool.name === "bash" ? "$" : tool.name === "edit" || tool.name === "write" ? "✎" : "●";
|
|
5948
6362
|
const color = toolColor(tool);
|
|
5949
|
-
append(` ${
|
|
6363
|
+
append(` ${icon} `, color);
|
|
5950
6364
|
append(displayToolName(tool.name), color);
|
|
5951
6365
|
const header = toolHeader(tool);
|
|
5952
6366
|
if (header)
|
|
@@ -5996,7 +6410,7 @@ function renderHomeState(input) {
|
|
|
5996
6410
|
flexDirection: "column",
|
|
5997
6411
|
alignItems: "center",
|
|
5998
6412
|
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:
|
|
6413
|
+
}, 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
6414
|
}
|
|
6001
6415
|
function hasRenderableMessage(message, showThinking = true) {
|
|
6002
6416
|
if (message.role === "error")
|
|
@@ -6172,6 +6586,11 @@ function displayToolName(name) {
|
|
|
6172
6586
|
glob: "Glob",
|
|
6173
6587
|
web_fetch: "WebFetch",
|
|
6174
6588
|
web_search: "WebSearch",
|
|
6589
|
+
subagent: "Subagent",
|
|
6590
|
+
spawn_agent: "SpawnAgent",
|
|
6591
|
+
wait_agent: "WaitAgent",
|
|
6592
|
+
send_input: "SendInput",
|
|
6593
|
+
close_agent: "CloseAgent",
|
|
6175
6594
|
task: "Task",
|
|
6176
6595
|
todo: "Todo",
|
|
6177
6596
|
question: "Questions",
|
|
@@ -6180,6 +6599,20 @@ function displayToolName(name) {
|
|
|
6180
6599
|
}
|
|
6181
6600
|
function toolHeader(tool) {
|
|
6182
6601
|
const args = tool.args || {};
|
|
6602
|
+
if (tool.name === "subagent") {
|
|
6603
|
+
if (typeof args.agent === "string")
|
|
6604
|
+
return `(${args.agent})`;
|
|
6605
|
+
if (Array.isArray(args.tasks))
|
|
6606
|
+
return `(${args.tasks.length} tasks)`;
|
|
6607
|
+
}
|
|
6608
|
+
if (tool.name === "spawn_agent") {
|
|
6609
|
+
const agent = args.agent_type ?? args.agent ?? "default";
|
|
6610
|
+
return `(${agent})`;
|
|
6611
|
+
}
|
|
6612
|
+
if (tool.name === "wait_agent" || tool.name === "send_input" || tool.name === "close_agent") {
|
|
6613
|
+
const agentId = args.agent_id ?? (Array.isArray(args.agent_ids) ? `${args.agent_ids.length} agents` : undefined);
|
|
6614
|
+
return agentId ? `(${truncate(String(agentId), 64)})` : "";
|
|
6615
|
+
}
|
|
6183
6616
|
const value = args.path ?? args.command ?? args.pattern ?? args.url ?? args.query;
|
|
6184
6617
|
return value ? `(${truncate(String(value).replace(/\n/g, " "), 64)})` : "";
|
|
6185
6618
|
}
|
|
@@ -6225,8 +6658,11 @@ function filetype(filePath) {
|
|
|
6225
6658
|
return ext ? map[ext] : undefined;
|
|
6226
6659
|
}
|
|
6227
6660
|
function summarizeToolResult(tool) {
|
|
6228
|
-
if (!isToolFinished(tool))
|
|
6229
|
-
|
|
6661
|
+
if (!isToolFinished(tool)) {
|
|
6662
|
+
if (tool.status === "running")
|
|
6663
|
+
return "running";
|
|
6664
|
+
return tool.streamingArgs ? "preparing" : "pending";
|
|
6665
|
+
}
|
|
6230
6666
|
if (tool.name === "question") {
|
|
6231
6667
|
if (isQuestionRejected(tool))
|
|
6232
6668
|
return "dismissed";
|
|
@@ -6237,14 +6673,47 @@ function summarizeToolResult(tool) {
|
|
|
6237
6673
|
if (tool.isError)
|
|
6238
6674
|
return truncate(result.split("\n").find(Boolean) || "error", 120);
|
|
6239
6675
|
const lines = result.replace(/\r\n/g, "\n").split("\n").filter((line) => line.trim()).length;
|
|
6676
|
+
const matches = typeof tool.metadata?.matches === "number" ? tool.metadata.matches : undefined;
|
|
6677
|
+
if (tool.name === "read")
|
|
6678
|
+
return "";
|
|
6240
6679
|
if (tool.name === "edit")
|
|
6241
6680
|
return "patched file";
|
|
6242
6681
|
if (tool.name === "write")
|
|
6243
6682
|
return "wrote file";
|
|
6244
|
-
if (tool.name === "
|
|
6683
|
+
if (tool.name === "grep" || tool.name === "glob") {
|
|
6684
|
+
if (matches !== undefined)
|
|
6685
|
+
return `${matches} match${matches === 1 ? "" : "es"}`;
|
|
6686
|
+
return lines ? `${lines} line${lines === 1 ? "" : "s"}` : "no matches";
|
|
6687
|
+
}
|
|
6688
|
+
if (tool.name === "bash") {
|
|
6689
|
+
if (matches !== undefined)
|
|
6690
|
+
return `${matches} match${matches === 1 ? "" : "es"}`;
|
|
6245
6691
|
return lines ? `${lines} line${lines === 1 ? "" : "s"} output` : "done";
|
|
6692
|
+
}
|
|
6246
6693
|
return lines ? `${lines} line${lines === 1 ? "" : "s"}` : "done";
|
|
6247
6694
|
}
|
|
6695
|
+
function toolStateIcon(tool) {
|
|
6696
|
+
if (tool.isError || tool.status === "error")
|
|
6697
|
+
return "✗";
|
|
6698
|
+
if (!isToolFinished(tool)) {
|
|
6699
|
+
if (tool.status === "running")
|
|
6700
|
+
return "◐";
|
|
6701
|
+
return "◌";
|
|
6702
|
+
}
|
|
6703
|
+
if (tool.name === "bash")
|
|
6704
|
+
return "$";
|
|
6705
|
+
if (tool.name === "edit")
|
|
6706
|
+
return "✎";
|
|
6707
|
+
if (tool.name === "write")
|
|
6708
|
+
return "✎";
|
|
6709
|
+
if (tool.name === "read")
|
|
6710
|
+
return "▤";
|
|
6711
|
+
if (tool.name === "grep" || tool.name === "glob")
|
|
6712
|
+
return "⌕";
|
|
6713
|
+
if (tool.name === "web_fetch" || tool.name === "web_search")
|
|
6714
|
+
return "⌖";
|
|
6715
|
+
return "●";
|
|
6716
|
+
}
|
|
6248
6717
|
function toolSummaryWithPreview(tool) {
|
|
6249
6718
|
const summary = ` ${summarizeToolResult(tool)}`;
|
|
6250
6719
|
const preview = toolPreview(tool);
|
|
@@ -6259,7 +6728,7 @@ function toolSummaryWithPreview(tool) {
|
|
|
6259
6728
|
function toolPreview(tool) {
|
|
6260
6729
|
if (!isToolFinished(tool) || tool.isError || !tool.result)
|
|
6261
6730
|
return undefined;
|
|
6262
|
-
if (tool.name !== "
|
|
6731
|
+
if (tool.name !== "glob")
|
|
6263
6732
|
return undefined;
|
|
6264
6733
|
const lines = tool.result
|
|
6265
6734
|
.replace(/\r\n/g, "\n")
|
|
@@ -6315,7 +6784,10 @@ function isToolFinished(tool) {
|
|
|
6315
6784
|
function assistantStatusLabel(message) {
|
|
6316
6785
|
if (message.status === "responding")
|
|
6317
6786
|
return "Responding...";
|
|
6318
|
-
|
|
6787
|
+
const elapsed = formatDuration(reasoningElapsedMs(message));
|
|
6788
|
+
if (message.streaming)
|
|
6789
|
+
return elapsed ? `Thinking ${elapsed}...` : "Thinking...";
|
|
6790
|
+
return elapsed ? `Thought for ${elapsed}` : "Thinking";
|
|
6319
6791
|
}
|
|
6320
6792
|
function buildContextGauge(percent, barWidth) {
|
|
6321
6793
|
const clamped = Math.max(0, Math.min(100, percent));
|
|
@@ -6346,9 +6818,74 @@ function formatContextRemaining(value) {
|
|
|
6346
6818
|
return `${(value / 1_000).toFixed(1)}K`;
|
|
6347
6819
|
return String(value);
|
|
6348
6820
|
}
|
|
6349
|
-
function
|
|
6350
|
-
const
|
|
6351
|
-
|
|
6821
|
+
function thinkingLabelContent(streaming = false, elapsedMs) {
|
|
6822
|
+
const elapsed = formatDuration(elapsedMs);
|
|
6823
|
+
const label = streaming
|
|
6824
|
+
? (elapsed ? `Thinking ${elapsed}...` : "Thinking...")
|
|
6825
|
+
: (elapsed ? `Thought for ${elapsed}` : "Thought");
|
|
6826
|
+
return new StyledText([
|
|
6827
|
+
fg(theme.messageThinkingText)(italic(label)),
|
|
6828
|
+
]);
|
|
6829
|
+
}
|
|
6830
|
+
function formatDuration(ms) {
|
|
6831
|
+
if (ms === undefined || !Number.isFinite(ms) || ms <= 0)
|
|
6832
|
+
return "";
|
|
6833
|
+
if (ms < 1000)
|
|
6834
|
+
return `${Math.max(1, Math.round(ms))}ms`;
|
|
6835
|
+
const seconds = ms / 1000;
|
|
6836
|
+
if (seconds < 10)
|
|
6837
|
+
return `${seconds.toFixed(1)}s`;
|
|
6838
|
+
if (seconds < 60)
|
|
6839
|
+
return `${Math.round(seconds)}s`;
|
|
6840
|
+
let minutes = Math.floor(seconds / 60);
|
|
6841
|
+
let remSec = Math.round(seconds - minutes * 60);
|
|
6842
|
+
// Math.round can lift remSec to exactly 60 (e.g. 239.6s → 3m60s). Carry into minutes.
|
|
6843
|
+
if (remSec >= 60) {
|
|
6844
|
+
minutes += Math.floor(remSec / 60);
|
|
6845
|
+
remSec = remSec % 60;
|
|
6846
|
+
}
|
|
6847
|
+
return remSec === 0 ? `${minutes}m` : `${minutes}m${remSec}s`;
|
|
6848
|
+
}
|
|
6849
|
+
function reasoningElapsedMs(message) {
|
|
6850
|
+
if (message.turnStartedAt === undefined)
|
|
6851
|
+
return undefined;
|
|
6852
|
+
const end = !message.streaming ? (message.turnCompletedAt ?? Date.now()) : Date.now();
|
|
6853
|
+
const diff = end - message.turnStartedAt;
|
|
6854
|
+
return diff > 0 ? diff : undefined;
|
|
6855
|
+
}
|
|
6856
|
+
function toolElapsedMs(tool) {
|
|
6857
|
+
if (tool.startedAt === undefined)
|
|
6858
|
+
return undefined;
|
|
6859
|
+
const end = tool.completedAt ?? (tool.status === "completed" || tool.status === "error" ? Date.now() : undefined);
|
|
6860
|
+
if (end === undefined)
|
|
6861
|
+
return undefined;
|
|
6862
|
+
const diff = end - tool.startedAt;
|
|
6863
|
+
return diff > 0 ? diff : undefined;
|
|
6864
|
+
}
|
|
6865
|
+
function turnElapsedMs(message) {
|
|
6866
|
+
if (message.turnStartedAt === undefined)
|
|
6867
|
+
return undefined;
|
|
6868
|
+
const end = message.turnCompletedAt ?? Date.now();
|
|
6869
|
+
const diff = end - message.turnStartedAt;
|
|
6870
|
+
return diff > 0 ? diff : undefined;
|
|
6871
|
+
}
|
|
6872
|
+
function formatTurnSummary(message) {
|
|
6873
|
+
const parts = [];
|
|
6874
|
+
const elapsed = turnElapsedMs(message);
|
|
6875
|
+
if (elapsed !== undefined)
|
|
6876
|
+
parts.push(formatDuration(elapsed));
|
|
6877
|
+
const usage = message.turnUsage;
|
|
6878
|
+
if (usage) {
|
|
6879
|
+
if (usage.promptTokens)
|
|
6880
|
+
parts.push(`${formatCompactNumber(usage.promptTokens)}↑`);
|
|
6881
|
+
if (usage.completionTokens)
|
|
6882
|
+
parts.push(`${formatCompactNumber(usage.completionTokens)}↓`);
|
|
6883
|
+
if (usage.reasoningTokens)
|
|
6884
|
+
parts.push(`${formatCompactNumber(usage.reasoningTokens)}◇`);
|
|
6885
|
+
}
|
|
6886
|
+
if (!parts.length)
|
|
6887
|
+
return undefined;
|
|
6888
|
+
return `· ${parts.join(" · ")}`;
|
|
6352
6889
|
}
|
|
6353
6890
|
function truncate(value, max) {
|
|
6354
6891
|
return value.length > max ? value.slice(0, Math.max(1, max - 1)).trimEnd() + "…" : value;
|