mixdog 0.9.1 → 0.9.2
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/package.json +8 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +306 -66
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +4 -2
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +4 -2
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +710 -318
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +12 -2
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +7 -8
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +7 -0
- package/src/rules/shared/01-tool.md +17 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +332 -57
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +59 -32
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +78 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +202 -98
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +183 -20
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -9
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
- package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
- package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +441 -1224
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +80 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +184 -19
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory.mjs +101 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +129 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/standalone/agent-tool.mjs +255 -109
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +8 -291
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/provider-admin.mjs +11 -0
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2096 -732
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +83 -7
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +170 -98
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +159 -21
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6965 -2391
- package/src/tui/engine.mjs +287 -126
- package/src/tui/index.jsx +117 -7
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +453 -0
- package/src/tui/markdown/format-token.mjs +129 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +0 -11
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +81 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +26 -27
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +39 -12
- package/src/workflows/sequential/WORKFLOW.md +46 -0
- package/src/workflows/solo/WORKFLOW.md +7 -0
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import React, { useEffect, useState } from 'react';
|
|
12
12
|
import { Box, Text } from 'ink';
|
|
13
13
|
import stringWidth from 'string-width';
|
|
14
|
+
import stripAnsi from 'strip-ansi';
|
|
15
|
+
import { displayWidth } from '../display-width.mjs';
|
|
14
16
|
import { theme, TURN_MARKER, RESULT_GUTTER, RESULT_GUTTER_CONT } from '../theme.mjs';
|
|
15
17
|
import { formatElapsed } from '../time-format.mjs';
|
|
16
18
|
import { BULLET_OPERATOR } from '../figures.mjs';
|
|
@@ -28,6 +30,11 @@ import { backgroundTaskFailureStatusLabel, isBackgroundErrorOnlyBody } from '../
|
|
|
28
30
|
import { formatExpandedResult, wrapExpandedResultLines } from './tool-output-format.mjs';
|
|
29
31
|
|
|
30
32
|
const MIN_RESULT_LINE_CHARS = 24;
|
|
33
|
+
// Hard cap for the collapsed result detail row (the second line under the ⎿
|
|
34
|
+
// gutter). Independent of terminal width so a wide terminal never lets a long
|
|
35
|
+
// line (e.g. an agent response brief) stretch the whole row — anything past
|
|
36
|
+
// this is truncated with an ellipsis. ctrl+o expand still shows the full body.
|
|
37
|
+
const RESULT_LINE_HARD_MAX = 80;
|
|
31
38
|
// Hard cap for the parenthesized header arg summary so a long path/query does
|
|
32
39
|
// not eat the whole header line; anything longer is truncated with an ellipsis.
|
|
33
40
|
const SUMMARY_MAX_CHARS = 48;
|
|
@@ -42,6 +49,21 @@ const TOOL_BLINK_LIMIT_MS = 3000;
|
|
|
42
49
|
const TOOL_PENDING_SHOW_DELAY_MS = 1000;
|
|
43
50
|
// Read `theme.subtle` at use-time (not captured here) so a live `/theme`
|
|
44
51
|
// switch re-tones the tool hints. `theme` is mutated in-place on switch.
|
|
52
|
+
// Collapsed tool headers/details are laid out as single terminal rows. Never let
|
|
53
|
+
// raw C0/control bytes (CR, tabs, cursor escapes, etc.) reach those rows: a
|
|
54
|
+
// terminal can apply them after Ink has already clipped/measured the row, which
|
|
55
|
+
// makes a scrolled tool card appear to write through the prompt/statusline.
|
|
56
|
+
const INLINE_CONTROL_RE = /[\u0000-\u001F\u007F]/g;
|
|
57
|
+
|
|
58
|
+
function safeInlineText(value) {
|
|
59
|
+
return stripAnsi(String(value ?? ''))
|
|
60
|
+
.replace(/\r\n?/g, '\n')
|
|
61
|
+
.replace(/\t/g, ' ')
|
|
62
|
+
.replace(/\n+/g, ' ')
|
|
63
|
+
.replace(INLINE_CONTROL_RE, ' ')
|
|
64
|
+
.replace(/\s+/g, ' ')
|
|
65
|
+
.trim();
|
|
66
|
+
}
|
|
45
67
|
|
|
46
68
|
function normalizeCount(value) {
|
|
47
69
|
const n = Number(value || 0);
|
|
@@ -169,10 +191,27 @@ function resultTerminalStatus(value) {
|
|
|
169
191
|
if (tagged) return normalizeTerminalStatus(tagged);
|
|
170
192
|
const bracketed = text.match(/^\[status:\s*([^\]]*)\]/mi)?.[1]?.trim();
|
|
171
193
|
if (bracketed) return normalizeTerminalStatus(bracketed);
|
|
194
|
+
// Loose inline `status: x` / `state: x` matches are a last-resort fallback —
|
|
195
|
+
// prefer the engine-controlled `<status>` tag or `[status: …]` marker above.
|
|
196
|
+
// A loose match can false-positive on prose that happens to start with
|
|
197
|
+
// "status:" (rare, but shellResultStatus below already owns real shell
|
|
198
|
+
// output parsing; this fallback stays narrow and unchanged in behavior).
|
|
172
199
|
const inline = text.match(/^(?:status|state):\s*([^\s·,;]+)/mi)?.[1]?.trim();
|
|
173
200
|
return normalizeTerminalStatus(inline);
|
|
174
201
|
}
|
|
175
202
|
|
|
203
|
+
const LEADING_STATUS_MARKER_LINE_RE = /^\[status:\s*[^\]]*\]\s*$/i;
|
|
204
|
+
|
|
205
|
+
function stripLeadingStatusMarkerLines(lines) {
|
|
206
|
+
const out = Array.isArray(lines) ? lines.slice() : [];
|
|
207
|
+
if (out.length > 0 && LEADING_STATUS_MARKER_LINE_RE.test(String(out[0] ?? '').trim())) out.shift();
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function stripLeadingStatusMarkerFromText(text) {
|
|
212
|
+
return stripLeadingStatusMarkerLines(String(text || '').split('\n')).join('\n');
|
|
213
|
+
}
|
|
214
|
+
|
|
176
215
|
function shellDisplayStatus({ pending = false, failedCount = 0, isError = false, result = '' } = {}) {
|
|
177
216
|
const status = shellResultStatus(result);
|
|
178
217
|
if (pending || /^(running|pending|queued)$/.test(status)) return 'running';
|
|
@@ -188,11 +227,6 @@ function shellHeader(status, count = 1) {
|
|
|
188
227
|
return `Ran ${object}`;
|
|
189
228
|
}
|
|
190
229
|
|
|
191
|
-
function shellDetail(status, elapsed = '') {
|
|
192
|
-
const label = displayTerminalStatus(status) || status;
|
|
193
|
-
return elapsed ? `${elapsed} · ${label}` : label;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
230
|
function shellResultElapsed(value) {
|
|
197
231
|
const match = String(value || '').match(/^\[elapsed:\s*(\d+)\s*ms\]/mi);
|
|
198
232
|
if (!match) return '';
|
|
@@ -211,20 +245,20 @@ function statusCopy(name, label, count, doneCount, pending, isError, args = {})
|
|
|
211
245
|
}
|
|
212
246
|
|
|
213
247
|
function fitResultLine(line, columns) {
|
|
214
|
-
const max = Math.max(MIN_RESULT_LINE_CHARS, Number(columns || 80) - 7);
|
|
215
|
-
const text =
|
|
216
|
-
return
|
|
248
|
+
const max = Math.min(RESULT_LINE_HARD_MAX, Math.max(MIN_RESULT_LINE_CHARS, Number(columns || 80) - 7));
|
|
249
|
+
const text = safeInlineText(line);
|
|
250
|
+
return displayWidth(text) > max ? truncateToWidth(text, max) : text;
|
|
217
251
|
}
|
|
218
252
|
|
|
219
253
|
/** Trim text from the end (by display width) so it fits maxWidth, appending '…'. */
|
|
220
254
|
function truncateToWidth(text, maxWidth) {
|
|
221
|
-
const str =
|
|
255
|
+
const str = safeInlineText(text);
|
|
222
256
|
if (maxWidth < 1) return '';
|
|
223
|
-
if (
|
|
257
|
+
if (displayWidth(str) <= maxWidth) return str;
|
|
224
258
|
const chars = Array.from(str);
|
|
225
259
|
let out = '';
|
|
226
260
|
for (const ch of chars) {
|
|
227
|
-
if (
|
|
261
|
+
if (displayWidth(out + ch + '…') > maxWidth) break;
|
|
228
262
|
out += ch;
|
|
229
263
|
}
|
|
230
264
|
return `${out}…`;
|
|
@@ -272,11 +306,6 @@ function agentModelLabel(args) {
|
|
|
272
306
|
return displayModelName(model, provider, displayHint);
|
|
273
307
|
}
|
|
274
308
|
|
|
275
|
-
function withModel(label, args) {
|
|
276
|
-
const model = agentModelLabel(args);
|
|
277
|
-
return model ? `${label} (${model})` : label;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
309
|
function agentTagLabel(args) {
|
|
281
310
|
// The real spawn tag (engine fills parsedArgs.tag from the envelope target).
|
|
282
311
|
// Never fall back to task_id — only the human-meaningful spawn tag belongs in
|
|
@@ -291,43 +320,46 @@ function withModelAndTag(label, args) {
|
|
|
291
320
|
return inner ? `${label} (${inner})` : label;
|
|
292
321
|
}
|
|
293
322
|
|
|
294
|
-
// Append
|
|
295
|
-
// when the
|
|
296
|
-
function
|
|
297
|
-
return
|
|
323
|
+
// Append an agent name to a base action word without leaving a trailing space
|
|
324
|
+
// when the agent is unknown (no generic "Agent" fallback).
|
|
325
|
+
function joinActionAgent(action, agent) {
|
|
326
|
+
return agent ? `${action} ${agent}` : action;
|
|
298
327
|
}
|
|
299
328
|
|
|
300
329
|
function agentResponseTitle(args) {
|
|
301
|
-
const name = titleizeAgentName(args?.agent || args?.
|
|
302
|
-
// The agent
|
|
330
|
+
const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '');
|
|
331
|
+
// The agent + model identify the responder; the response summary itself
|
|
303
332
|
// is hidden in the collapsed card (ctrl+o expand still shows the full body).
|
|
304
|
-
// No generic "Agent" fallback — render just "Response" when the
|
|
305
|
-
return withModelAndTag(
|
|
333
|
+
// No generic "Agent" fallback — render just "Response" when the agent is empty.
|
|
334
|
+
return withModelAndTag(joinActionAgent('Response', name), args);
|
|
306
335
|
}
|
|
307
336
|
|
|
308
337
|
function agentActionTitle(args) {
|
|
309
|
-
const name = titleizeAgentName(args?.agent || args?.
|
|
310
|
-
|
|
338
|
+
const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '');
|
|
339
|
+
// Runtime treats an omitted type/action as "spawn" (see agent-tool.mjs default),
|
|
340
|
+
// so mirror that contract here instead of falling through to the generic
|
|
341
|
+
// "Called agent" status copy.
|
|
342
|
+
const action = String(args?.type || args?.action || 'spawn').toLowerCase();
|
|
311
343
|
// Fixed action verbs regardless of running/completed status. No generic
|
|
312
|
-
// "Agent" fallback for the
|
|
344
|
+
// "Agent" fallback for the agent: when the agent is unknown render the action
|
|
313
345
|
// word alone ("Spawn") instead of "Spawn Agent".
|
|
314
|
-
if (action === 'spawn') return withModelAndTag(
|
|
315
|
-
if (action === 'send') return withModelAndTag(
|
|
346
|
+
if (action === 'spawn') return withModelAndTag(joinActionAgent('Spawn', name), args);
|
|
347
|
+
if (action === 'send') return withModelAndTag(joinActionAgent('Send', name), args);
|
|
316
348
|
if (action === 'list') return 'Agent status';
|
|
317
|
-
if (action === 'cancel') return withModelAndTag(
|
|
318
|
-
if (action === 'close') return withModelAndTag(
|
|
319
|
-
if (action === 'cleanup') return withModelAndTag(
|
|
320
|
-
if (action === 'read' || action === 'status') return withModelAndTag(
|
|
349
|
+
if (action === 'cancel') return withModelAndTag(joinActionAgent('Cancel', name), args);
|
|
350
|
+
if (action === 'close') return withModelAndTag(joinActionAgent('Close', name), args);
|
|
351
|
+
if (action === 'cleanup') return withModelAndTag(joinActionAgent('Cleanup', name), args);
|
|
352
|
+
if (action === 'read' || action === 'status') return withModelAndTag(joinActionAgent('Status', name), args);
|
|
321
353
|
return '';
|
|
322
354
|
}
|
|
323
355
|
|
|
324
356
|
function agentActionSummary(args, summary) {
|
|
325
357
|
const text = String(summary || '').trim();
|
|
326
358
|
if (!text) return '';
|
|
327
|
-
const name = titleizeAgentName(args?.agent || args?.
|
|
359
|
+
const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '');
|
|
328
360
|
if (name && text === name) return '';
|
|
329
361
|
let rest = name && text.startsWith(`${name} · `) ? text.slice(name.length + 3).trim() : text;
|
|
330
|
-
// The
|
|
362
|
+
// The agent/model/tag surface summary ("Heavy Worker · Opus 4.8") is now folded
|
|
331
363
|
// into the header label itself ("Spawn Heavy Worker (Opus 4.8, tag)"), so drop
|
|
332
364
|
// the model and tag tokens from the parenthesized summary to avoid showing
|
|
333
365
|
// them twice.
|
|
@@ -358,7 +390,7 @@ function hasAgentResponseResult(value) {
|
|
|
358
390
|
if (/^agent result\b/i.test(trimmed)) continue;
|
|
359
391
|
if (/^(?:undefined|null)$/i.test(trimmed)) continue;
|
|
360
392
|
if (/^<\/?(?:final-answer|task-notification|task-id|tool-use-id|output-file|result|status|summary|usage|total_tokens|tool_uses|duration_ms|worktree|worktreePath|worktreeBranch)[^>]*>$/i.test(trimmed)) continue;
|
|
361
|
-
if (!sawBlank && /^(?:agent task|background task|agent message queued\b|agent close:|task_id|surface|operation|label|status|type|target|
|
|
393
|
+
if (!sawBlank && /^(?:agent task|background task|agent message queued\b|agent close:|task_id|surface|operation|label|status|type|target|agent|preset|model|effort|fast|limits|started|finished|error|notification|queueDepth):?\s*/i.test(trimmed)) continue;
|
|
362
394
|
if (!sawBlank && /^(?:agents|tasks):\s*/i.test(trimmed)) continue;
|
|
363
395
|
if (/^\(no agents or tasks\)$/i.test(trimmed)) continue;
|
|
364
396
|
if (!sawBlank && /^-\s+\S+/i.test(trimmed)) continue;
|
|
@@ -440,7 +472,10 @@ function prefixElapsed(detail, elapsed = '') {
|
|
|
440
472
|
const text = String(detail || '').trim();
|
|
441
473
|
const time = String(elapsed || '').trim();
|
|
442
474
|
if (!time) return text;
|
|
443
|
-
|
|
475
|
+
// Unified convention: the elapsed time ALWAYS goes at the END, ` · ` separated.
|
|
476
|
+
// Guard against a double-append when the text already ends with the same time.
|
|
477
|
+
if (text && text.endsWith(`· ${time}`)) return text;
|
|
478
|
+
return text ? `${text} · ${time}` : time;
|
|
444
479
|
}
|
|
445
480
|
|
|
446
481
|
function mergeTerminalDetail(status, detail = '') {
|
|
@@ -518,10 +553,6 @@ function isOutputDetailTool(normalizedName, label) {
|
|
|
518
553
|
]).has(n) || l === 'read' || l === 'search' || l === 'web search' || l === 'run';
|
|
519
554
|
}
|
|
520
555
|
|
|
521
|
-
function progressDetail({ normalizedName, label, elapsed }) {
|
|
522
|
-
return elapsed ? `${elapsed} elapsed` : '';
|
|
523
|
-
}
|
|
524
|
-
|
|
525
556
|
function genericCompletedDetail({ normalizedName, label, hasResult, firstResultLine, isError }) {
|
|
526
557
|
const n = String(normalizedName || '').toLowerCase();
|
|
527
558
|
const l = String(label || '').toLowerCase();
|
|
@@ -566,7 +597,8 @@ function agentTerminalDetail(status, isError, elapsed, error = '') {
|
|
|
566
597
|
: /done|success|complete|closed/.test(s)
|
|
567
598
|
? 'Finished'
|
|
568
599
|
: '';
|
|
569
|
-
|
|
600
|
+
// Unified ` · <time>` convention (previously "Finished after 12s").
|
|
601
|
+
return word ? `${word}${elapsed ? ` · ${elapsed}` : ''}` : '';
|
|
570
602
|
}
|
|
571
603
|
|
|
572
604
|
function clampFailureCount(errorCount, groupCount, isError) {
|
|
@@ -575,17 +607,26 @@ function clampFailureCount(errorCount, groupCount, isError) {
|
|
|
575
607
|
return isError ? groupCount : 0;
|
|
576
608
|
}
|
|
577
609
|
|
|
610
|
+
// Single source of truth for the tool-card dot (●) color. Both the aggregate
|
|
611
|
+
// and normal (single-tool) render paths must call this with a resolved
|
|
612
|
+
// `terminalStatus` — do not recompute color inline elsewhere.
|
|
613
|
+
// running/pending -> mixdogOrange || warning (blink handled by caller)
|
|
614
|
+
// success -> theme.success
|
|
615
|
+
// partial failure -> mixdogOrange (some, not all, of the group failed)
|
|
616
|
+
// all failed -> theme.error
|
|
617
|
+
// cancelled -> theme.warning
|
|
578
618
|
function toolStatusColor({ pending, groupCount, failedCount, terminalStatus = '' }) {
|
|
579
|
-
if (pending) return theme.
|
|
619
|
+
if (pending) return theme.mixdogOrange || theme.warning;
|
|
580
620
|
const status = normalizeTerminalStatus(terminalStatus);
|
|
621
|
+
if (status === 'cancelled') return theme.warning;
|
|
581
622
|
if (status === 'failed') return theme.error;
|
|
582
|
-
if (status === 'cancelled') return theme.warning || theme.mixdogOrange || theme.subtle;
|
|
583
623
|
if (failedCount <= 0) return theme.success;
|
|
584
624
|
if (groupCount > 1 && failedCount < groupCount) return theme.mixdogOrange || theme.warning;
|
|
585
625
|
return theme.error;
|
|
586
626
|
}
|
|
587
627
|
|
|
588
|
-
export function ToolExecution({ name, args, result, rawResult, isError, errorCount, expanded,
|
|
628
|
+
export function ToolExecution({ name, args, result, rawResult, isError, errorCount, expanded, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, headerFinalized = true, deferredDisplayReady = false }) {
|
|
629
|
+
const rowWidth = Math.max(1, Number(columns || 80));
|
|
589
630
|
const [blinkOn, setBlinkOn] = useState(true);
|
|
590
631
|
const [blinkExpired, setBlinkExpired] = useState(false);
|
|
591
632
|
const [pendingDelayElapsed, setPendingDelayElapsed] = useState(false);
|
|
@@ -619,7 +660,6 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
619
660
|
const elapsedMs = startedAtMs ? Math.max(0, (pending ? Date.now() : (completedAtMs || Date.now())) - startedAtMs) : 0;
|
|
620
661
|
const elapsed = elapsedMs >= 1000 ? formatElapsed(elapsedMs) : '';
|
|
621
662
|
const failedCount = clampFailureCount(errorCount, groupCount, isError);
|
|
622
|
-
const statusColor = toolStatusColor({ pending, groupCount, failedCount });
|
|
623
663
|
const displayGroupCount = groupCount;
|
|
624
664
|
const displayCategories = normalizeCountMap(categories || {});
|
|
625
665
|
|
|
@@ -692,7 +732,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
692
732
|
// header + one brief detail row (see estimateTranscriptItemRows).
|
|
693
733
|
const placeholderSingleRow = !aggregate && SKILL_SURFACE_NAMES.has(placeholderNormalizedName);
|
|
694
734
|
return (
|
|
695
|
-
<Box flexDirection="column" marginTop={attached ? 0 : 1}>
|
|
735
|
+
<Box flexDirection="column" marginTop={attached ? 0 : 1} width={rowWidth} overflow="hidden">
|
|
696
736
|
<Text> </Text>
|
|
697
737
|
{placeholderSingleRow ? null : <Text> </Text>}
|
|
698
738
|
</Box>
|
|
@@ -708,7 +748,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
708
748
|
// No stableVerbWidth: see statusCopy — the padding only left a mid-header
|
|
709
749
|
// gap ("Searched 1 pattern, Read 1 file") since Ink trims trailing
|
|
710
750
|
// spaces and never stabilized the flip.
|
|
711
|
-
const headerText = formatAggregateHeader(displayCategories || {}, { pending: headerPending, order: headerOrder });
|
|
751
|
+
const headerText = safeInlineText(formatAggregateHeader(displayCategories || {}, { pending: headerPending, order: headerOrder }));
|
|
712
752
|
let detailText;
|
|
713
753
|
if (hasResult) {
|
|
714
754
|
// The aggregate card reserves EXACTLY ONE detail row when it is not
|
|
@@ -719,33 +759,40 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
719
759
|
// "settle" taller than reserved. Collapse to a single logical line
|
|
720
760
|
// (whitespace-normalized); fitResultLine below trims it to the column
|
|
721
761
|
// width so it can never exceed one terminal row.
|
|
722
|
-
detailText =
|
|
762
|
+
detailText = safeInlineText(rt);
|
|
723
763
|
} else {
|
|
724
764
|
detailText = '';
|
|
725
765
|
}
|
|
726
766
|
|
|
727
|
-
|
|
767
|
+
// Resolve the aggregate's terminalStatus from the collapsed detail `rt`
|
|
768
|
+
// (which carries a `[status: cancelled]`/`<status>` marker when the
|
|
769
|
+
// aggregate was cancelled) plus isError/failedCount for failures. Pending
|
|
770
|
+
// stays running; a clean completion stays success. toolStatusColor is the
|
|
771
|
+
// single source of dot color for both aggregate and normal cards.
|
|
772
|
+
const aggregateTerminalStatus = pending
|
|
773
|
+
? 'running'
|
|
774
|
+
: (resultTerminalStatus(rt) || (isError || failedCount > 0 ? 'failed' : 'completed'));
|
|
775
|
+
const dotColor = toolStatusColor({ pending, groupCount, failedCount, terminalStatus: aggregateTerminalStatus });
|
|
728
776
|
const dotText = pending && !blinkExpired && !blinkOn ? ' ' : TURN_MARKER;
|
|
729
777
|
const gutter = 2;
|
|
730
778
|
const showHeaderExpandHint = hasRawResult;
|
|
731
779
|
const hintLabel = `ctrl+o ${expanded ? 'collapse' : 'expand'}`;
|
|
732
780
|
const hintText = ` ${BULLET_OPERATOR} ${hintLabel}`;
|
|
733
|
-
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
|
|
739
|
-
const rightReserve = Math.max(stringWidth(hintText), stringWidth(headerMetaText));
|
|
781
|
+
// The header right-side trailing slot only ever shows the ctrl+o hint. The
|
|
782
|
+
// pending elapsed meta was removed from the header — it lives on the detail
|
|
783
|
+
// row now (`Running · 12s`) so a per-second digit change never reflows the
|
|
784
|
+
// header. Still reserve the hint slot for the whole lifecycle so the body
|
|
785
|
+
// clip point stays fixed when the hint appears on completion.
|
|
786
|
+
const rightReserve = stringWidth(hintText);
|
|
740
787
|
const avail = Math.max(1, (Number(columns) || 80) - 1 - gutter - rightReserve);
|
|
741
|
-
const trailingText =
|
|
788
|
+
const trailingText = showHeaderExpandHint ? hintText : '';
|
|
742
789
|
const trailingColor = theme.subtle;
|
|
743
790
|
const clippedHeader = stringWidth(headerText) > avail
|
|
744
791
|
? truncateToWidth(headerText, avail)
|
|
745
792
|
: headerText;
|
|
746
|
-
// Trailing content (
|
|
747
|
-
// sits immediately after the header body — no fixed right-edge pin — so
|
|
748
|
-
// never jumps to the right edge and snaps back on the pending→done flip.
|
|
793
|
+
// Trailing content (ctrl+o hint only; pending elapsed lives on the detail
|
|
794
|
+
// row) sits immediately after the header body — no fixed right-edge pin — so
|
|
795
|
+
// it never jumps to the right edge and snaps back on the pending→done flip.
|
|
749
796
|
// Keep the aggregate card at a fixed height (header + one detail row) for
|
|
750
797
|
// its whole lifecycle. Pending cards have no result yet, so reserve the
|
|
751
798
|
// detail row up front instead of growing from 1→2 rows when the summary
|
|
@@ -759,13 +806,19 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
759
806
|
// color; the status placeholder is rendered dim.
|
|
760
807
|
const isPlaceholderDetail = !(expanded && hasRawResult) && !detailText;
|
|
761
808
|
const showRawAggregate = expanded && hasRawResult;
|
|
809
|
+
// Aggregate cards intentionally omit elapsed time once grouped. A brief
|
|
810
|
+
// `Running · 1s` tick during the grouped→finished handoff reads as visual
|
|
811
|
+
// noise, and the grouped header already communicates that work is active.
|
|
812
|
+
const pendingPlaceholder = headerPending
|
|
813
|
+
? 'Running'
|
|
814
|
+
: 'Finished';
|
|
762
815
|
const detailLines = showRawAggregate
|
|
763
816
|
? rawRt.split('\n')
|
|
764
|
-
: (detailText ? [detailText] : [
|
|
817
|
+
: (detailText ? [detailText] : [pendingPlaceholder]);
|
|
765
818
|
const aggregateDetailColor = isPlaceholderDetail ? theme.subtle : theme.text;
|
|
766
819
|
return (
|
|
767
|
-
<Box flexDirection="column" marginTop={attached ? 0 : 1}>
|
|
768
|
-
<Box flexDirection="row">
|
|
820
|
+
<Box flexDirection="column" marginTop={attached ? 0 : 1} width={rowWidth} overflow="hidden">
|
|
821
|
+
<Box flexDirection="row" width={rowWidth} overflow="hidden">
|
|
769
822
|
<Box flexShrink={0} minWidth={2}>
|
|
770
823
|
<Text color={dotColor}>{dotText}</Text>
|
|
771
824
|
</Box>
|
|
@@ -797,30 +850,35 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
797
850
|
const backgroundResultText = backgroundMeta?.hasResponse ? backgroundMeta.body : '';
|
|
798
851
|
const displayedResultText = backgroundResultText || (errorOnlyResult ? '' : (rt || ''));
|
|
799
852
|
const hasDisplayResult = Boolean(String(displayedResultText || '').trim());
|
|
800
|
-
const
|
|
853
|
+
const displayedResultBodyText = stripLeadingStatusMarkerFromText(displayedResultText);
|
|
854
|
+
const hasDisplayBody = Boolean(String(displayedResultBodyText || '').trim());
|
|
855
|
+
const lines = displayedResultBodyText ? displayedResultBodyText.split('\n') : [];
|
|
801
856
|
const totalLines = lines.length;
|
|
802
857
|
// Semantic one-line summary derived purely from name/args/result text.
|
|
803
858
|
// Shown in the collapsed, non-error view in place of the raw result block.
|
|
804
859
|
// Grouped cards ("Searched N files" / "Read N files") get the same treatment
|
|
805
860
|
// as single calls: a one-line semantic summary stands in for the raw block.
|
|
806
|
-
const resultSummary = !pending &&
|
|
807
|
-
? surfaceSummarizeToolResult(name, args,
|
|
861
|
+
const resultSummary = !pending && hasDisplayBody
|
|
862
|
+
? surfaceSummarizeToolResult(name, args, displayedResultBodyText, isError)
|
|
808
863
|
: null;
|
|
809
864
|
// Same fit budget fitResultLine() uses, to detect a line that will be clipped.
|
|
810
|
-
const maxResultChars = Math.max(MIN_RESULT_LINE_CHARS, Number(columns || 80) - 7);
|
|
865
|
+
const maxResultChars = Math.min(RESULT_LINE_HARD_MAX, Math.max(MIN_RESULT_LINE_CHARS, Number(columns || 80) - 7));
|
|
811
866
|
const resultColor = theme.text;
|
|
812
867
|
const firstResultLine = hasDisplayResult ? String(lines[0] ?? '') : '';
|
|
813
|
-
const firstResultLineClipped =
|
|
814
|
-
const hasHiddenDetail = !pending &&
|
|
868
|
+
const firstResultLineClipped = hasDisplayBody && stringWidth(firstResultLine) > maxResultChars;
|
|
869
|
+
const hasHiddenDetail = !pending && hasDisplayBody && (totalLines > 1 || firstResultLineClipped || Boolean(resultSummary));
|
|
815
870
|
const shellStatus = isShellSurface ? shellDisplayStatus({ pending, failedCount, isError, result: displayedResultText }) : '';
|
|
816
871
|
const shellElapsed = isShellSurface ? (shellResultElapsed(displayedResultText) || elapsed) : '';
|
|
817
|
-
const shellStatusDetail = isShellSurface ? shellDetail(shellStatus, shellElapsed) : '';
|
|
818
872
|
const backgroundElapsed = backgroundMeta
|
|
819
873
|
? backgroundTaskElapsed(backgroundMeta, elapsed)
|
|
820
874
|
: (isBackgroundTaskTool(normalizedName) ? backgroundTaskElapsed(parsedArgs, elapsed) : '');
|
|
821
875
|
|
|
822
876
|
const toolArgPath = parsedArgs?.path ?? parsedArgs?.file_path ?? parsedArgs?.file ?? '';
|
|
823
|
-
|
|
877
|
+
// Audit HIGH: on a FAILED view_image the path detail used to win over the
|
|
878
|
+
// error cause (nonShellDetail order puts imageDetail before genericDetail),
|
|
879
|
+
// so the card showed the filename instead of why it failed. Suppress the
|
|
880
|
+
// path detail on error; the error-cause summary/first line takes the row.
|
|
881
|
+
const imageDetail = normalizedName === 'view_image' && toolArgPath && !isError ? String(toolArgPath) : '';
|
|
824
882
|
const isBackgroundResult = !pending && isBackgroundTaskTool(normalizedName) && Boolean(backgroundMeta);
|
|
825
883
|
const isBackgroundResponse = isBackgroundResult && (backgroundMeta?.hasResponse || isBackgroundTaskResponseArgs(normalizedName, parsedArgs));
|
|
826
884
|
const isBackgroundMetadataResult = isBackgroundResult && !isBackgroundResponse && Boolean(backgroundMeta);
|
|
@@ -868,20 +926,22 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
868
926
|
// the final summary just fills in place. Skill surfaces collapse to a single
|
|
869
927
|
// row in BOTH the estimate and the render (visibleDetailLines drops the row
|
|
870
928
|
// for isSkillSurface below), so they get no placeholder.
|
|
871
|
-
const pendingDetailPlaceholder = pending && !isSkillSurface
|
|
929
|
+
const pendingDetailPlaceholder = pending && !isSkillSurface
|
|
930
|
+
? (elapsed ? `Running · ${elapsed}` : 'Running')
|
|
931
|
+
: '';
|
|
872
932
|
const shellCollapsedSummary = isShellSurface && !pending && hasDisplayResult
|
|
873
933
|
? (resultSummary || truncateToWidth(firstResultLine, Math.min(120, maxResultChars)))
|
|
874
934
|
: resultSummary;
|
|
875
935
|
const collapsedDetail = pending
|
|
876
936
|
? pendingDetailPlaceholder
|
|
877
937
|
: isShellSurface
|
|
878
|
-
? mergeTerminalDetail(shellStatus, shellCollapsedSummary)
|
|
938
|
+
? prefixElapsed(mergeTerminalDetail(shellStatus, shellCollapsedSummary), shellElapsed)
|
|
879
939
|
: mergeTerminalDetail(terminalStatus, nonShellDetail);
|
|
880
940
|
const backgroundMetadataExpandable = isBackgroundMetadataResult && hasRawResult && !pending;
|
|
881
|
-
const showRawResult = expanded && (
|
|
941
|
+
const showRawResult = expanded && (hasDisplayBody || hasRawResult)
|
|
882
942
|
&& (!isBackgroundMetadataResult || hasRawResult);
|
|
883
943
|
const detailLines = showRawResult
|
|
884
|
-
? (
|
|
944
|
+
? (hasDisplayBody ? lines : (rawRt ? stripLeadingStatusMarkerLines(rawRt.split('\n')) : []))
|
|
885
945
|
: (collapsedDetail ? [collapsedDetail] : []);
|
|
886
946
|
const isPendingPlaceholderDetail = !showRawResult && Boolean(pendingDetailPlaceholder);
|
|
887
947
|
const detailColor = isPendingPlaceholderDetail ? theme.subtle : theme.text;
|
|
@@ -903,7 +963,10 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
903
963
|
// collapsed; ctrl+o expand still surfaces the full body.
|
|
904
964
|
let visibleDetailLines = detailLines;
|
|
905
965
|
if (isSkillSurface && !showRawResult) {
|
|
906
|
-
|
|
966
|
+
// Audit HIGH: a FAILED skill load used to drop its detail row with the
|
|
967
|
+
// success path, hiding the one-line cause entirely. Keep the detail row
|
|
968
|
+
// when the call errored; only the redundant success repeat is dropped.
|
|
969
|
+
visibleDetailLines = isError && collapsedDetail ? [collapsedDetail] : [];
|
|
907
970
|
} else if (isBackgroundMetadataResult && backgroundMetadataHeaderFailure && !showRawResult) {
|
|
908
971
|
visibleDetailLines = [];
|
|
909
972
|
} else if (isAgentSurfaceCard && !showRawResult) {
|
|
@@ -922,14 +985,25 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
922
985
|
else if (isBackgroundMetadataResult) labelText = backgroundTaskActionTitle(normalizedName, backgroundMeta);
|
|
923
986
|
else if (isShellSurface) labelText = shellHeader(shellStatus, displayGroupCount);
|
|
924
987
|
else labelText = (isAgentTool(normalizedName) ? agentActionTitle(parsedArgs) : '') || statusCopy(name, label, displayGroupCount, doneCount, headerPending, isError, parsedArgs);
|
|
988
|
+
labelText = safeInlineText(labelText);
|
|
925
989
|
// Show the parenthesized arg summary for grouped cards too, matching single
|
|
926
990
|
// calls so the header carries the same context.
|
|
927
991
|
const toolSearchSummary = !pending && normalizedName === 'tool_search' && hasResult
|
|
928
992
|
? toolSearchLoadedSummary(displayedResultText)
|
|
929
993
|
: '';
|
|
930
|
-
const
|
|
994
|
+
const rawSummaryText = safeInlineText(isAgentResponse || isBackgroundResponse
|
|
931
995
|
? ''
|
|
932
|
-
: toolSearchSummary || (isAgentTool(normalizedName) ? agentActionSummary(parsedArgs, summary) : summary);
|
|
996
|
+
: toolSearchSummary || (isAgentTool(normalizedName) ? agentActionSummary(parsedArgs, summary) : summary));
|
|
997
|
+
// Drop the parenthesized arg summary when it is a bare "<n> <unit>" count
|
|
998
|
+
// that the header verb already spells out (e.g. header "Searching 6 patterns"
|
|
999
|
+
// + summary "6 patterns"). Multi-arg array calls hit this; single calls keep
|
|
1000
|
+
// their descriptive summary ("pattern: \"foo\"") since it never matches the
|
|
1001
|
+
// header tail. Channel surfaces are unaffected — they build the summary from
|
|
1002
|
+
// summarizeToolArgs directly and never render this header verb.
|
|
1003
|
+
const summaryIsHeaderCount = rawSummaryText
|
|
1004
|
+
&& /^\d+\s+\S+$/.test(rawSummaryText)
|
|
1005
|
+
&& labelText.endsWith(rawSummaryText);
|
|
1006
|
+
const summaryText = summaryIsHeaderCount ? '' : rawSummaryText;
|
|
933
1007
|
// Agent cards hide their collapsed body but still expose ctrl+o expand only
|
|
934
1008
|
// when expanding would actually reveal something: an agent response body, or a
|
|
935
1009
|
// multiline / clipped raw result (e.g. the "agents: N …" worker list). A
|
|
@@ -941,6 +1015,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
941
1015
|
// hasHiddenDetail, which goes true for any single-line resultSummary and would
|
|
942
1016
|
// wrongly show ctrl+o on a status-only one-liner that has nothing to expand.
|
|
943
1017
|
const shellHasExpandableBody = isShellSurface && !pending && hasDisplayResult
|
|
1018
|
+
&& hasDisplayBody
|
|
944
1019
|
&& (totalLines > 1 || firstResultLineClipped || Boolean(shellCollapsedSummary && shellCollapsedSummary !== firstResultLine));
|
|
945
1020
|
const showHeaderExpandHint = (isShellSurface ? shellHasExpandableBody : (isAgentSurfaceCard ? agentHasExpandableBody : (hasHiddenDetail || backgroundMetadataExpandable)))
|
|
946
1021
|
&& normalizedName !== 'tool_search';
|
|
@@ -954,25 +1029,22 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
954
1029
|
const gutter = 2;
|
|
955
1030
|
const hintLabel = showHeaderExpandHint ? `ctrl+o ${expanded ? 'collapse' : 'expand'}` : '';
|
|
956
1031
|
const hintText = hintLabel ? ` ${BULLET_OPERATOR} ${hintLabel}` : '';
|
|
957
|
-
|
|
958
|
-
//
|
|
959
|
-
//
|
|
960
|
-
//
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
// the two for the whole lifecycle (matching the aggregate card's rightReserve)
|
|
964
|
-
// so `avail` stays fixed and nothing reflows. The hint slot is reserved even
|
|
965
|
-
// while pending so its later appearance does not push the body either.
|
|
1032
|
+
// The header right-side trailing slot only ever shows the ctrl+o hint. The
|
|
1033
|
+
// pending elapsed meta was removed from the header — it lives on the detail
|
|
1034
|
+
// row now (`Running · 12s`) so a per-second digit change (9s→10s) or the
|
|
1035
|
+
// pending→done swap never reflows the header. The hint slot is reserved for
|
|
1036
|
+
// the whole lifecycle (even while pending) so its later appearance on
|
|
1037
|
+
// completion does not push the body clip point.
|
|
966
1038
|
const hintReserveLabel = `ctrl+o ${expanded ? 'collapse' : 'expand'}`;
|
|
967
1039
|
const hintReserveText = ` ${BULLET_OPERATOR} ${hintReserveLabel}`;
|
|
968
1040
|
const headerFailureText = headerFailureStatus
|
|
969
1041
|
? truncateToWidth(headerFailureStatus, HEADER_FAILURE_STATUS_MAX)
|
|
970
1042
|
: '';
|
|
971
1043
|
const inlineFailureText = headerFailureText ? ` ${BULLET_OPERATOR} ${headerFailureText}` : '';
|
|
972
|
-
const rightReserve =
|
|
1044
|
+
const rightReserve = stringWidth(hintReserveText) + stringWidth(inlineFailureText);
|
|
973
1045
|
const avail = Math.max(1, (Number(columns) || 80) - 1 - gutter - rightReserve);
|
|
974
|
-
const trailingText =
|
|
975
|
-
const trailingColor =
|
|
1046
|
+
const trailingText = showHeaderExpandHint ? hintText : '';
|
|
1047
|
+
const trailingColor = expandHintColor;
|
|
976
1048
|
let labelOut;
|
|
977
1049
|
let summaryOut;
|
|
978
1050
|
if (stringWidth(labelText) >= avail) {
|
|
@@ -989,13 +1061,13 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
989
1061
|
: '';
|
|
990
1062
|
summaryOut = truncatedSummary ? ` (${truncatedSummary})` : '';
|
|
991
1063
|
}
|
|
992
|
-
// Keep trailing content (pending elapsed
|
|
993
|
-
// directly after the body for the whole lifecycle. The
|
|
994
|
-
// previously used for elapsed is what made the trailing text
|
|
995
|
-
// edge and snap back on the pending→done flip, so there is no
|
|
996
|
-
// stays reserved (rightReserve) so the body clip point never reflows.
|
|
1064
|
+
// Keep trailing content (ctrl+o hint only; pending elapsed lives on the detail
|
|
1065
|
+
// row) attached directly after the body for the whole lifecycle. The
|
|
1066
|
+
// fixed-column pin previously used for elapsed is what made the trailing text
|
|
1067
|
+
// jump to the right edge and snap back on the pending→done flip, so there is no
|
|
1068
|
+
// pad. `avail` stays reserved (rightReserve) so the body clip point never reflows.
|
|
997
1069
|
return (
|
|
998
|
-
<Box flexDirection="column" marginTop={attached ? 0 : 1}>
|
|
1070
|
+
<Box flexDirection="column" marginTop={attached ? 0 : 1} width={rowWidth} overflow="hidden">
|
|
999
1071
|
<Box flexDirection="row" width="100%">
|
|
1000
1072
|
<Box flexShrink={1} flexGrow={1} overflow="hidden" minWidth={0}>
|
|
1001
1073
|
<Box flexDirection="row">
|
|
@@ -1014,7 +1086,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
1014
1086
|
|
|
1015
1087
|
<ResultBody
|
|
1016
1088
|
lines={visibleDetailLines}
|
|
1017
|
-
rawText={
|
|
1089
|
+
rawText={hasDisplayBody ? displayedResultBodyText : stripLeadingStatusMarkerFromText(rawRt || '')}
|
|
1018
1090
|
pathArg={toolArgPath}
|
|
1019
1091
|
isShell={isShellSurface}
|
|
1020
1092
|
columns={columns}
|
|
@@ -26,7 +26,7 @@ function cleanRightMessage(value) {
|
|
|
26
26
|
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export function TurnDone({ elapsedMs = 0, status = 'done', verb = 'Thought', rightMessage = '', rightTone = 'info', rightMessageWidth = 24 }) {
|
|
29
|
+
export function TurnDone({ elapsedMs = 0, status = 'done', verb = 'Thought', rightMessage = '', rightTone = 'info', rightMessageWidth = 24, marginTop = 1 }) {
|
|
30
30
|
const elapsed = formatDuration(elapsedMs);
|
|
31
31
|
const cancelled = status === 'cancelled';
|
|
32
32
|
const doneVerb = String(verb || 'Thought').trim() || 'Thought';
|
|
@@ -37,7 +37,7 @@ export function TurnDone({ elapsedMs = 0, status = 'done', verb = 'Thought', rig
|
|
|
37
37
|
const rightWidth = Math.max(1, Number(rightMessageWidth) || 24);
|
|
38
38
|
|
|
39
39
|
return (
|
|
40
|
-
<Box marginTop={
|
|
40
|
+
<Box marginTop={marginTop} flexDirection="row" width="100%">
|
|
41
41
|
<Box flexGrow={1} flexShrink={1} overflow="hidden">
|
|
42
42
|
<Text wrap="truncate">
|
|
43
43
|
<Text color={theme.spinnerGlyph}>{TURN_DONE_MARKER} </Text>
|
|
@@ -53,14 +53,14 @@ export function TurnDone({ elapsedMs = 0, status = 'done', verb = 'Thought', rig
|
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export function StatusDone({ label = 'Complete', detail = '', rightMessage = '', rightTone = 'info', rightMessageWidth = 24 }) {
|
|
56
|
+
export function StatusDone({ label = 'Complete', detail = '', rightMessage = '', rightTone = 'info', rightMessageWidth = 24, marginTop = 1 }) {
|
|
57
57
|
const copy = String(label || 'Complete').trim() || 'Complete';
|
|
58
58
|
const suffix = String(detail || '').trim();
|
|
59
59
|
const rightText = cleanRightMessage(rightMessage);
|
|
60
60
|
const rightWidth = Math.max(1, Number(rightMessageWidth) || 24);
|
|
61
61
|
|
|
62
62
|
return (
|
|
63
|
-
<Box marginTop={
|
|
63
|
+
<Box marginTop={marginTop} flexDirection="row" width="100%">
|
|
64
64
|
<Box flexGrow={1} flexShrink={1} overflow="hidden">
|
|
65
65
|
<Text wrap="truncate">
|
|
66
66
|
<Text color={theme.spinnerGlyph}>{TURN_DONE_MARKER} </Text>
|
|
@@ -12,7 +12,7 @@ const STATUS_SEPARATOR = ' │ ';
|
|
|
12
12
|
|
|
13
13
|
function money(value) {
|
|
14
14
|
const n = Number(value);
|
|
15
|
-
if (!Number.isFinite(n)) return '
|
|
15
|
+
if (!Number.isFinite(n)) return 'N/A';
|
|
16
16
|
if (n === 0) return '$0';
|
|
17
17
|
if (n >= 10) return `$${n.toFixed(0)}`;
|
|
18
18
|
if (n >= 1) return `$${n.toFixed(2)}`;
|