mixdog 0.9.0 → 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 +10 -3
- 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/session-ingest-smoke.mjs +2 -2
- 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/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +34 -56
- package/src/mixdog-session-runtime.mjs +710 -319
- 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 -4
- 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 +359 -106
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
- 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 +86 -30
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
- 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 +265 -1
- 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/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- 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 -1254
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +54 -3
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +187 -43
- 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/llm-worker-host.mjs +0 -4
- 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-ops-policy.mjs +0 -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/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +116 -7
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- 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/child-spawn-gate.mjs +0 -6
- 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/seeds.mjs +1 -11
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2137 -750
- 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 +146 -9
- 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 +177 -100
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +312 -40
- package/src/tui/components/tool-output-format.test.mjs +180 -1
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +7324 -2393
- 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 +354 -142
- package/src/tui/markdown/format-token.test.mjs +155 -17
- 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 -647
- 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-lib.mjs +0 -623
- 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 +154 -20
- 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/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- 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);
|
|
@@ -98,10 +120,15 @@ function renderDeltaText(text) {
|
|
|
98
120
|
// renderDeltaText.
|
|
99
121
|
// - EXPANDED (raw=true): formatExpandedResult then wrapExpandedResultLines so
|
|
100
122
|
// each physical row fits the body width before render (rail rows stay 1:1;
|
|
101
|
-
// ink does not re-wrap).
|
|
123
|
+
// ink does not re-wrap). Physical row mount cap: MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES
|
|
124
|
+
// (default 600; 0 disables). Shell/script bodies keep the newest tail when capped.
|
|
102
125
|
function ResultBody({ lines, rawText, pathArg = '', isShell = false, columns, color, raw }) {
|
|
103
126
|
const renderLines = raw
|
|
104
|
-
? wrapExpandedResultLines(
|
|
127
|
+
? wrapExpandedResultLines(
|
|
128
|
+
formatExpandedResult(rawText, { pathArg, isShell }),
|
|
129
|
+
columns,
|
|
130
|
+
{ isShell },
|
|
131
|
+
)
|
|
105
132
|
: (lines || []);
|
|
106
133
|
if (!renderLines || renderLines.length === 0) return null;
|
|
107
134
|
return (
|
|
@@ -164,10 +191,27 @@ function resultTerminalStatus(value) {
|
|
|
164
191
|
if (tagged) return normalizeTerminalStatus(tagged);
|
|
165
192
|
const bracketed = text.match(/^\[status:\s*([^\]]*)\]/mi)?.[1]?.trim();
|
|
166
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).
|
|
167
199
|
const inline = text.match(/^(?:status|state):\s*([^\s·,;]+)/mi)?.[1]?.trim();
|
|
168
200
|
return normalizeTerminalStatus(inline);
|
|
169
201
|
}
|
|
170
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
|
+
|
|
171
215
|
function shellDisplayStatus({ pending = false, failedCount = 0, isError = false, result = '' } = {}) {
|
|
172
216
|
const status = shellResultStatus(result);
|
|
173
217
|
if (pending || /^(running|pending|queued)$/.test(status)) return 'running';
|
|
@@ -183,11 +227,6 @@ function shellHeader(status, count = 1) {
|
|
|
183
227
|
return `Ran ${object}`;
|
|
184
228
|
}
|
|
185
229
|
|
|
186
|
-
function shellDetail(status, elapsed = '') {
|
|
187
|
-
const label = displayTerminalStatus(status) || status;
|
|
188
|
-
return elapsed ? `${elapsed} · ${label}` : label;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
230
|
function shellResultElapsed(value) {
|
|
192
231
|
const match = String(value || '').match(/^\[elapsed:\s*(\d+)\s*ms\]/mi);
|
|
193
232
|
if (!match) return '';
|
|
@@ -206,20 +245,20 @@ function statusCopy(name, label, count, doneCount, pending, isError, args = {})
|
|
|
206
245
|
}
|
|
207
246
|
|
|
208
247
|
function fitResultLine(line, columns) {
|
|
209
|
-
const max = Math.max(MIN_RESULT_LINE_CHARS, Number(columns || 80) - 7);
|
|
210
|
-
const text =
|
|
211
|
-
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;
|
|
212
251
|
}
|
|
213
252
|
|
|
214
253
|
/** Trim text from the end (by display width) so it fits maxWidth, appending '…'. */
|
|
215
254
|
function truncateToWidth(text, maxWidth) {
|
|
216
|
-
const str =
|
|
255
|
+
const str = safeInlineText(text);
|
|
217
256
|
if (maxWidth < 1) return '';
|
|
218
|
-
if (
|
|
257
|
+
if (displayWidth(str) <= maxWidth) return str;
|
|
219
258
|
const chars = Array.from(str);
|
|
220
259
|
let out = '';
|
|
221
260
|
for (const ch of chars) {
|
|
222
|
-
if (
|
|
261
|
+
if (displayWidth(out + ch + '…') > maxWidth) break;
|
|
223
262
|
out += ch;
|
|
224
263
|
}
|
|
225
264
|
return `${out}…`;
|
|
@@ -267,11 +306,6 @@ function agentModelLabel(args) {
|
|
|
267
306
|
return displayModelName(model, provider, displayHint);
|
|
268
307
|
}
|
|
269
308
|
|
|
270
|
-
function withModel(label, args) {
|
|
271
|
-
const model = agentModelLabel(args);
|
|
272
|
-
return model ? `${label} (${model})` : label;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
309
|
function agentTagLabel(args) {
|
|
276
310
|
// The real spawn tag (engine fills parsedArgs.tag from the envelope target).
|
|
277
311
|
// Never fall back to task_id — only the human-meaningful spawn tag belongs in
|
|
@@ -286,43 +320,46 @@ function withModelAndTag(label, args) {
|
|
|
286
320
|
return inner ? `${label} (${inner})` : label;
|
|
287
321
|
}
|
|
288
322
|
|
|
289
|
-
// Append
|
|
290
|
-
// when the
|
|
291
|
-
function
|
|
292
|
-
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;
|
|
293
327
|
}
|
|
294
328
|
|
|
295
329
|
function agentResponseTitle(args) {
|
|
296
|
-
const name = titleizeAgentName(args?.agent || args?.
|
|
297
|
-
// The agent
|
|
330
|
+
const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '');
|
|
331
|
+
// The agent + model identify the responder; the response summary itself
|
|
298
332
|
// is hidden in the collapsed card (ctrl+o expand still shows the full body).
|
|
299
|
-
// No generic "Agent" fallback — render just "Response" when the
|
|
300
|
-
return withModelAndTag(
|
|
333
|
+
// No generic "Agent" fallback — render just "Response" when the agent is empty.
|
|
334
|
+
return withModelAndTag(joinActionAgent('Response', name), args);
|
|
301
335
|
}
|
|
302
336
|
|
|
303
337
|
function agentActionTitle(args) {
|
|
304
|
-
const name = titleizeAgentName(args?.agent || args?.
|
|
305
|
-
|
|
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();
|
|
306
343
|
// Fixed action verbs regardless of running/completed status. No generic
|
|
307
|
-
// "Agent" fallback for the
|
|
344
|
+
// "Agent" fallback for the agent: when the agent is unknown render the action
|
|
308
345
|
// word alone ("Spawn") instead of "Spawn Agent".
|
|
309
|
-
if (action === 'spawn') return withModelAndTag(
|
|
310
|
-
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);
|
|
311
348
|
if (action === 'list') return 'Agent status';
|
|
312
|
-
if (action === 'cancel') return withModelAndTag(
|
|
313
|
-
if (action === 'close') return withModelAndTag(
|
|
314
|
-
if (action === 'cleanup') return withModelAndTag(
|
|
315
|
-
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);
|
|
316
353
|
return '';
|
|
317
354
|
}
|
|
318
355
|
|
|
319
356
|
function agentActionSummary(args, summary) {
|
|
320
357
|
const text = String(summary || '').trim();
|
|
321
358
|
if (!text) return '';
|
|
322
|
-
const name = titleizeAgentName(args?.agent || args?.
|
|
359
|
+
const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '');
|
|
323
360
|
if (name && text === name) return '';
|
|
324
361
|
let rest = name && text.startsWith(`${name} · `) ? text.slice(name.length + 3).trim() : text;
|
|
325
|
-
// The
|
|
362
|
+
// The agent/model/tag surface summary ("Heavy Worker · Opus 4.8") is now folded
|
|
326
363
|
// into the header label itself ("Spawn Heavy Worker (Opus 4.8, tag)"), so drop
|
|
327
364
|
// the model and tag tokens from the parenthesized summary to avoid showing
|
|
328
365
|
// them twice.
|
|
@@ -353,7 +390,7 @@ function hasAgentResponseResult(value) {
|
|
|
353
390
|
if (/^agent result\b/i.test(trimmed)) continue;
|
|
354
391
|
if (/^(?:undefined|null)$/i.test(trimmed)) continue;
|
|
355
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;
|
|
356
|
-
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;
|
|
357
394
|
if (!sawBlank && /^(?:agents|tasks):\s*/i.test(trimmed)) continue;
|
|
358
395
|
if (/^\(no agents or tasks\)$/i.test(trimmed)) continue;
|
|
359
396
|
if (!sawBlank && /^-\s+\S+/i.test(trimmed)) continue;
|
|
@@ -435,7 +472,10 @@ function prefixElapsed(detail, elapsed = '') {
|
|
|
435
472
|
const text = String(detail || '').trim();
|
|
436
473
|
const time = String(elapsed || '').trim();
|
|
437
474
|
if (!time) return text;
|
|
438
|
-
|
|
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;
|
|
439
479
|
}
|
|
440
480
|
|
|
441
481
|
function mergeTerminalDetail(status, detail = '') {
|
|
@@ -513,10 +553,6 @@ function isOutputDetailTool(normalizedName, label) {
|
|
|
513
553
|
]).has(n) || l === 'read' || l === 'search' || l === 'web search' || l === 'run';
|
|
514
554
|
}
|
|
515
555
|
|
|
516
|
-
function progressDetail({ normalizedName, label, elapsed }) {
|
|
517
|
-
return elapsed ? `${elapsed} elapsed` : '';
|
|
518
|
-
}
|
|
519
|
-
|
|
520
556
|
function genericCompletedDetail({ normalizedName, label, hasResult, firstResultLine, isError }) {
|
|
521
557
|
const n = String(normalizedName || '').toLowerCase();
|
|
522
558
|
const l = String(label || '').toLowerCase();
|
|
@@ -561,7 +597,8 @@ function agentTerminalDetail(status, isError, elapsed, error = '') {
|
|
|
561
597
|
: /done|success|complete|closed/.test(s)
|
|
562
598
|
? 'Finished'
|
|
563
599
|
: '';
|
|
564
|
-
|
|
600
|
+
// Unified ` · <time>` convention (previously "Finished after 12s").
|
|
601
|
+
return word ? `${word}${elapsed ? ` · ${elapsed}` : ''}` : '';
|
|
565
602
|
}
|
|
566
603
|
|
|
567
604
|
function clampFailureCount(errorCount, groupCount, isError) {
|
|
@@ -570,17 +607,26 @@ function clampFailureCount(errorCount, groupCount, isError) {
|
|
|
570
607
|
return isError ? groupCount : 0;
|
|
571
608
|
}
|
|
572
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
|
|
573
618
|
function toolStatusColor({ pending, groupCount, failedCount, terminalStatus = '' }) {
|
|
574
|
-
if (pending) return theme.
|
|
619
|
+
if (pending) return theme.mixdogOrange || theme.warning;
|
|
575
620
|
const status = normalizeTerminalStatus(terminalStatus);
|
|
621
|
+
if (status === 'cancelled') return theme.warning;
|
|
576
622
|
if (status === 'failed') return theme.error;
|
|
577
|
-
if (status === 'cancelled') return theme.warning || theme.mixdogOrange || theme.subtle;
|
|
578
623
|
if (failedCount <= 0) return theme.success;
|
|
579
624
|
if (groupCount > 1 && failedCount < groupCount) return theme.mixdogOrange || theme.warning;
|
|
580
625
|
return theme.error;
|
|
581
626
|
}
|
|
582
627
|
|
|
583
|
-
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));
|
|
584
630
|
const [blinkOn, setBlinkOn] = useState(true);
|
|
585
631
|
const [blinkExpired, setBlinkExpired] = useState(false);
|
|
586
632
|
const [pendingDelayElapsed, setPendingDelayElapsed] = useState(false);
|
|
@@ -614,7 +660,6 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
614
660
|
const elapsedMs = startedAtMs ? Math.max(0, (pending ? Date.now() : (completedAtMs || Date.now())) - startedAtMs) : 0;
|
|
615
661
|
const elapsed = elapsedMs >= 1000 ? formatElapsed(elapsedMs) : '';
|
|
616
662
|
const failedCount = clampFailureCount(errorCount, groupCount, isError);
|
|
617
|
-
const statusColor = toolStatusColor({ pending, groupCount, failedCount });
|
|
618
663
|
const displayGroupCount = groupCount;
|
|
619
664
|
const displayCategories = normalizeCountMap(categories || {});
|
|
620
665
|
|
|
@@ -687,7 +732,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
687
732
|
// header + one brief detail row (see estimateTranscriptItemRows).
|
|
688
733
|
const placeholderSingleRow = !aggregate && SKILL_SURFACE_NAMES.has(placeholderNormalizedName);
|
|
689
734
|
return (
|
|
690
|
-
<Box flexDirection="column" marginTop={attached ? 0 : 1}>
|
|
735
|
+
<Box flexDirection="column" marginTop={attached ? 0 : 1} width={rowWidth} overflow="hidden">
|
|
691
736
|
<Text> </Text>
|
|
692
737
|
{placeholderSingleRow ? null : <Text> </Text>}
|
|
693
738
|
</Box>
|
|
@@ -703,7 +748,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
703
748
|
// No stableVerbWidth: see statusCopy — the padding only left a mid-header
|
|
704
749
|
// gap ("Searched 1 pattern, Read 1 file") since Ink trims trailing
|
|
705
750
|
// spaces and never stabilized the flip.
|
|
706
|
-
const headerText = formatAggregateHeader(displayCategories || {}, { pending: headerPending, order: headerOrder });
|
|
751
|
+
const headerText = safeInlineText(formatAggregateHeader(displayCategories || {}, { pending: headerPending, order: headerOrder }));
|
|
707
752
|
let detailText;
|
|
708
753
|
if (hasResult) {
|
|
709
754
|
// The aggregate card reserves EXACTLY ONE detail row when it is not
|
|
@@ -714,33 +759,40 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
714
759
|
// "settle" taller than reserved. Collapse to a single logical line
|
|
715
760
|
// (whitespace-normalized); fitResultLine below trims it to the column
|
|
716
761
|
// width so it can never exceed one terminal row.
|
|
717
|
-
detailText =
|
|
762
|
+
detailText = safeInlineText(rt);
|
|
718
763
|
} else {
|
|
719
764
|
detailText = '';
|
|
720
765
|
}
|
|
721
766
|
|
|
722
|
-
|
|
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 });
|
|
723
776
|
const dotText = pending && !blinkExpired && !blinkOn ? ' ' : TURN_MARKER;
|
|
724
777
|
const gutter = 2;
|
|
725
778
|
const showHeaderExpandHint = hasRawResult;
|
|
726
779
|
const hintLabel = `ctrl+o ${expanded ? 'collapse' : 'expand'}`;
|
|
727
780
|
const hintText = ` ${BULLET_OPERATOR} ${hintLabel}`;
|
|
728
|
-
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
|
|
734
|
-
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);
|
|
735
787
|
const avail = Math.max(1, (Number(columns) || 80) - 1 - gutter - rightReserve);
|
|
736
|
-
const trailingText =
|
|
788
|
+
const trailingText = showHeaderExpandHint ? hintText : '';
|
|
737
789
|
const trailingColor = theme.subtle;
|
|
738
790
|
const clippedHeader = stringWidth(headerText) > avail
|
|
739
791
|
? truncateToWidth(headerText, avail)
|
|
740
792
|
: headerText;
|
|
741
|
-
// Trailing content (
|
|
742
|
-
// sits immediately after the header body — no fixed right-edge pin — so
|
|
743
|
-
// 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.
|
|
744
796
|
// Keep the aggregate card at a fixed height (header + one detail row) for
|
|
745
797
|
// its whole lifecycle. Pending cards have no result yet, so reserve the
|
|
746
798
|
// detail row up front instead of growing from 1→2 rows when the summary
|
|
@@ -754,13 +806,19 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
754
806
|
// color; the status placeholder is rendered dim.
|
|
755
807
|
const isPlaceholderDetail = !(expanded && hasRawResult) && !detailText;
|
|
756
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';
|
|
757
815
|
const detailLines = showRawAggregate
|
|
758
816
|
? rawRt.split('\n')
|
|
759
|
-
: (detailText ? [detailText] : [
|
|
817
|
+
: (detailText ? [detailText] : [pendingPlaceholder]);
|
|
760
818
|
const aggregateDetailColor = isPlaceholderDetail ? theme.subtle : theme.text;
|
|
761
819
|
return (
|
|
762
|
-
<Box flexDirection="column" marginTop={attached ? 0 : 1}>
|
|
763
|
-
<Box flexDirection="row">
|
|
820
|
+
<Box flexDirection="column" marginTop={attached ? 0 : 1} width={rowWidth} overflow="hidden">
|
|
821
|
+
<Box flexDirection="row" width={rowWidth} overflow="hidden">
|
|
764
822
|
<Box flexShrink={0} minWidth={2}>
|
|
765
823
|
<Text color={dotColor}>{dotText}</Text>
|
|
766
824
|
</Box>
|
|
@@ -792,30 +850,35 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
792
850
|
const backgroundResultText = backgroundMeta?.hasResponse ? backgroundMeta.body : '';
|
|
793
851
|
const displayedResultText = backgroundResultText || (errorOnlyResult ? '' : (rt || ''));
|
|
794
852
|
const hasDisplayResult = Boolean(String(displayedResultText || '').trim());
|
|
795
|
-
const
|
|
853
|
+
const displayedResultBodyText = stripLeadingStatusMarkerFromText(displayedResultText);
|
|
854
|
+
const hasDisplayBody = Boolean(String(displayedResultBodyText || '').trim());
|
|
855
|
+
const lines = displayedResultBodyText ? displayedResultBodyText.split('\n') : [];
|
|
796
856
|
const totalLines = lines.length;
|
|
797
857
|
// Semantic one-line summary derived purely from name/args/result text.
|
|
798
858
|
// Shown in the collapsed, non-error view in place of the raw result block.
|
|
799
859
|
// Grouped cards ("Searched N files" / "Read N files") get the same treatment
|
|
800
860
|
// as single calls: a one-line semantic summary stands in for the raw block.
|
|
801
|
-
const resultSummary = !pending &&
|
|
802
|
-
? surfaceSummarizeToolResult(name, args,
|
|
861
|
+
const resultSummary = !pending && hasDisplayBody
|
|
862
|
+
? surfaceSummarizeToolResult(name, args, displayedResultBodyText, isError)
|
|
803
863
|
: null;
|
|
804
864
|
// Same fit budget fitResultLine() uses, to detect a line that will be clipped.
|
|
805
|
-
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));
|
|
806
866
|
const resultColor = theme.text;
|
|
807
867
|
const firstResultLine = hasDisplayResult ? String(lines[0] ?? '') : '';
|
|
808
|
-
const firstResultLineClipped =
|
|
809
|
-
const hasHiddenDetail = !pending &&
|
|
868
|
+
const firstResultLineClipped = hasDisplayBody && stringWidth(firstResultLine) > maxResultChars;
|
|
869
|
+
const hasHiddenDetail = !pending && hasDisplayBody && (totalLines > 1 || firstResultLineClipped || Boolean(resultSummary));
|
|
810
870
|
const shellStatus = isShellSurface ? shellDisplayStatus({ pending, failedCount, isError, result: displayedResultText }) : '';
|
|
811
871
|
const shellElapsed = isShellSurface ? (shellResultElapsed(displayedResultText) || elapsed) : '';
|
|
812
|
-
const shellStatusDetail = isShellSurface ? shellDetail(shellStatus, shellElapsed) : '';
|
|
813
872
|
const backgroundElapsed = backgroundMeta
|
|
814
873
|
? backgroundTaskElapsed(backgroundMeta, elapsed)
|
|
815
874
|
: (isBackgroundTaskTool(normalizedName) ? backgroundTaskElapsed(parsedArgs, elapsed) : '');
|
|
816
875
|
|
|
817
876
|
const toolArgPath = parsedArgs?.path ?? parsedArgs?.file_path ?? parsedArgs?.file ?? '';
|
|
818
|
-
|
|
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) : '';
|
|
819
882
|
const isBackgroundResult = !pending && isBackgroundTaskTool(normalizedName) && Boolean(backgroundMeta);
|
|
820
883
|
const isBackgroundResponse = isBackgroundResult && (backgroundMeta?.hasResponse || isBackgroundTaskResponseArgs(normalizedName, parsedArgs));
|
|
821
884
|
const isBackgroundMetadataResult = isBackgroundResult && !isBackgroundResponse && Boolean(backgroundMeta);
|
|
@@ -863,20 +926,22 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
863
926
|
// the final summary just fills in place. Skill surfaces collapse to a single
|
|
864
927
|
// row in BOTH the estimate and the render (visibleDetailLines drops the row
|
|
865
928
|
// for isSkillSurface below), so they get no placeholder.
|
|
866
|
-
const pendingDetailPlaceholder = pending && !isSkillSurface
|
|
929
|
+
const pendingDetailPlaceholder = pending && !isSkillSurface
|
|
930
|
+
? (elapsed ? `Running · ${elapsed}` : 'Running')
|
|
931
|
+
: '';
|
|
867
932
|
const shellCollapsedSummary = isShellSurface && !pending && hasDisplayResult
|
|
868
933
|
? (resultSummary || truncateToWidth(firstResultLine, Math.min(120, maxResultChars)))
|
|
869
934
|
: resultSummary;
|
|
870
935
|
const collapsedDetail = pending
|
|
871
936
|
? pendingDetailPlaceholder
|
|
872
937
|
: isShellSurface
|
|
873
|
-
? mergeTerminalDetail(shellStatus, shellCollapsedSummary)
|
|
938
|
+
? prefixElapsed(mergeTerminalDetail(shellStatus, shellCollapsedSummary), shellElapsed)
|
|
874
939
|
: mergeTerminalDetail(terminalStatus, nonShellDetail);
|
|
875
940
|
const backgroundMetadataExpandable = isBackgroundMetadataResult && hasRawResult && !pending;
|
|
876
|
-
const showRawResult = expanded && (
|
|
941
|
+
const showRawResult = expanded && (hasDisplayBody || hasRawResult)
|
|
877
942
|
&& (!isBackgroundMetadataResult || hasRawResult);
|
|
878
943
|
const detailLines = showRawResult
|
|
879
|
-
? (
|
|
944
|
+
? (hasDisplayBody ? lines : (rawRt ? stripLeadingStatusMarkerLines(rawRt.split('\n')) : []))
|
|
880
945
|
: (collapsedDetail ? [collapsedDetail] : []);
|
|
881
946
|
const isPendingPlaceholderDetail = !showRawResult && Boolean(pendingDetailPlaceholder);
|
|
882
947
|
const detailColor = isPendingPlaceholderDetail ? theme.subtle : theme.text;
|
|
@@ -898,7 +963,10 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
898
963
|
// collapsed; ctrl+o expand still surfaces the full body.
|
|
899
964
|
let visibleDetailLines = detailLines;
|
|
900
965
|
if (isSkillSurface && !showRawResult) {
|
|
901
|
-
|
|
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] : [];
|
|
902
970
|
} else if (isBackgroundMetadataResult && backgroundMetadataHeaderFailure && !showRawResult) {
|
|
903
971
|
visibleDetailLines = [];
|
|
904
972
|
} else if (isAgentSurfaceCard && !showRawResult) {
|
|
@@ -917,14 +985,25 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
917
985
|
else if (isBackgroundMetadataResult) labelText = backgroundTaskActionTitle(normalizedName, backgroundMeta);
|
|
918
986
|
else if (isShellSurface) labelText = shellHeader(shellStatus, displayGroupCount);
|
|
919
987
|
else labelText = (isAgentTool(normalizedName) ? agentActionTitle(parsedArgs) : '') || statusCopy(name, label, displayGroupCount, doneCount, headerPending, isError, parsedArgs);
|
|
988
|
+
labelText = safeInlineText(labelText);
|
|
920
989
|
// Show the parenthesized arg summary for grouped cards too, matching single
|
|
921
990
|
// calls so the header carries the same context.
|
|
922
991
|
const toolSearchSummary = !pending && normalizedName === 'tool_search' && hasResult
|
|
923
992
|
? toolSearchLoadedSummary(displayedResultText)
|
|
924
993
|
: '';
|
|
925
|
-
const
|
|
994
|
+
const rawSummaryText = safeInlineText(isAgentResponse || isBackgroundResponse
|
|
926
995
|
? ''
|
|
927
|
-
: 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;
|
|
928
1007
|
// Agent cards hide their collapsed body but still expose ctrl+o expand only
|
|
929
1008
|
// when expanding would actually reveal something: an agent response body, or a
|
|
930
1009
|
// multiline / clipped raw result (e.g. the "agents: N …" worker list). A
|
|
@@ -936,6 +1015,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
936
1015
|
// hasHiddenDetail, which goes true for any single-line resultSummary and would
|
|
937
1016
|
// wrongly show ctrl+o on a status-only one-liner that has nothing to expand.
|
|
938
1017
|
const shellHasExpandableBody = isShellSurface && !pending && hasDisplayResult
|
|
1018
|
+
&& hasDisplayBody
|
|
939
1019
|
&& (totalLines > 1 || firstResultLineClipped || Boolean(shellCollapsedSummary && shellCollapsedSummary !== firstResultLine));
|
|
940
1020
|
const showHeaderExpandHint = (isShellSurface ? shellHasExpandableBody : (isAgentSurfaceCard ? agentHasExpandableBody : (hasHiddenDetail || backgroundMetadataExpandable)))
|
|
941
1021
|
&& normalizedName !== 'tool_search';
|
|
@@ -949,25 +1029,22 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
949
1029
|
const gutter = 2;
|
|
950
1030
|
const hintLabel = showHeaderExpandHint ? `ctrl+o ${expanded ? 'collapse' : 'expand'}` : '';
|
|
951
1031
|
const hintText = hintLabel ? ` ${BULLET_OPERATOR} ${hintLabel}` : '';
|
|
952
|
-
|
|
953
|
-
//
|
|
954
|
-
//
|
|
955
|
-
//
|
|
956
|
-
//
|
|
957
|
-
//
|
|
958
|
-
// the two for the whole lifecycle (matching the aggregate card's rightReserve)
|
|
959
|
-
// so `avail` stays fixed and nothing reflows. The hint slot is reserved even
|
|
960
|
-
// 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.
|
|
961
1038
|
const hintReserveLabel = `ctrl+o ${expanded ? 'collapse' : 'expand'}`;
|
|
962
1039
|
const hintReserveText = ` ${BULLET_OPERATOR} ${hintReserveLabel}`;
|
|
963
1040
|
const headerFailureText = headerFailureStatus
|
|
964
1041
|
? truncateToWidth(headerFailureStatus, HEADER_FAILURE_STATUS_MAX)
|
|
965
1042
|
: '';
|
|
966
1043
|
const inlineFailureText = headerFailureText ? ` ${BULLET_OPERATOR} ${headerFailureText}` : '';
|
|
967
|
-
const rightReserve =
|
|
1044
|
+
const rightReserve = stringWidth(hintReserveText) + stringWidth(inlineFailureText);
|
|
968
1045
|
const avail = Math.max(1, (Number(columns) || 80) - 1 - gutter - rightReserve);
|
|
969
|
-
const trailingText =
|
|
970
|
-
const trailingColor =
|
|
1046
|
+
const trailingText = showHeaderExpandHint ? hintText : '';
|
|
1047
|
+
const trailingColor = expandHintColor;
|
|
971
1048
|
let labelOut;
|
|
972
1049
|
let summaryOut;
|
|
973
1050
|
if (stringWidth(labelText) >= avail) {
|
|
@@ -984,13 +1061,13 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
984
1061
|
: '';
|
|
985
1062
|
summaryOut = truncatedSummary ? ` (${truncatedSummary})` : '';
|
|
986
1063
|
}
|
|
987
|
-
// Keep trailing content (pending elapsed
|
|
988
|
-
// directly after the body for the whole lifecycle. The
|
|
989
|
-
// previously used for elapsed is what made the trailing text
|
|
990
|
-
// edge and snap back on the pending→done flip, so there is no
|
|
991
|
-
// 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.
|
|
992
1069
|
return (
|
|
993
|
-
<Box flexDirection="column" marginTop={attached ? 0 : 1}>
|
|
1070
|
+
<Box flexDirection="column" marginTop={attached ? 0 : 1} width={rowWidth} overflow="hidden">
|
|
994
1071
|
<Box flexDirection="row" width="100%">
|
|
995
1072
|
<Box flexShrink={1} flexGrow={1} overflow="hidden" minWidth={0}>
|
|
996
1073
|
<Box flexDirection="row">
|
|
@@ -1009,7 +1086,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
1009
1086
|
|
|
1010
1087
|
<ResultBody
|
|
1011
1088
|
lines={visibleDetailLines}
|
|
1012
|
-
rawText={
|
|
1089
|
+
rawText={hasDisplayBody ? displayedResultBodyText : stripLeadingStatusMarkerFromText(rawRt || '')}
|
|
1013
1090
|
pathArg={toolArgPath}
|
|
1014
1091
|
isShell={isShellSurface}
|
|
1015
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)}`;
|