mixdog 0.8.0 → 0.9.0
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/README.md +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +344 -0
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1223 -95
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +118 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -98
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -1,198 +1,63 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* components/ToolExecution.jsx — a tool call + its result.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
1
|
+
/**
|
|
2
|
+
* components/ToolExecution.jsx — a tool call + its result.
|
|
3
|
+
*
|
|
4
|
+
* Tool call + result layout:
|
|
5
5
|
* - The call line: `● Tool Name(summary)` where the dot is BLACK_CIRCLE
|
|
6
6
|
* (2-wide gutter), the tool name is the user-facing label and the argument
|
|
7
7
|
* summary sits in muted parentheses. NOT raw MCP/internal names.
|
|
8
|
-
* - The result hangs under a single dim ` ⎿ ` gutter — the gutter is placed
|
|
9
|
-
* once, not repeated per wrapped line
|
|
10
|
-
*/
|
|
11
|
-
import React, { useEffect,
|
|
8
|
+
* - The result hangs under a single dim ` ⎿ ` gutter — the gutter is placed
|
|
9
|
+
* once, not repeated per wrapped line.
|
|
10
|
+
*/
|
|
11
|
+
import React, { useEffect, useState } from 'react';
|
|
12
12
|
import { Box, Text } from 'ink';
|
|
13
13
|
import stringWidth from 'string-width';
|
|
14
|
-
import { theme, TURN_MARKER, RESULT_GUTTER } from '../theme.mjs';
|
|
14
|
+
import { theme, TURN_MARKER, RESULT_GUTTER, RESULT_GUTTER_CONT } from '../theme.mjs';
|
|
15
15
|
import { formatElapsed } from '../time-format.mjs';
|
|
16
16
|
import { BULLET_OPERATOR } from '../figures.mjs';
|
|
17
17
|
import {
|
|
18
18
|
displayToolName as surfaceDisplayToolName,
|
|
19
19
|
formatToolSurface,
|
|
20
|
-
summarizeToolArgs as surfaceSummarizeToolArgs,
|
|
21
20
|
summarizeToolResult as surfaceSummarizeToolResult,
|
|
22
21
|
formatAggregateHeader,
|
|
22
|
+
formatToolActionHeader,
|
|
23
|
+
displayModelName,
|
|
24
|
+
summarizeAgentSurfaceBrief,
|
|
25
|
+
AGENT_SURFACE_BRIEF_MAX,
|
|
23
26
|
} from '../../runtime/shared/tool-surface.mjs';
|
|
27
|
+
import { backgroundTaskFailureStatusLabel, isBackgroundErrorOnlyBody } from '../../runtime/shared/err-text.mjs';
|
|
28
|
+
import { formatExpandedResult, wrapExpandedResultLines } from './tool-output-format.mjs';
|
|
24
29
|
|
|
25
30
|
const MIN_RESULT_LINE_CHARS = 24;
|
|
26
31
|
// Hard cap for the parenthesized header arg summary so a long path/query does
|
|
27
32
|
// not eat the whole header line; anything longer is truncated with an ellipsis.
|
|
28
33
|
const SUMMARY_MAX_CHARS = 48;
|
|
34
|
+
const HEADER_FAILURE_STATUS_MAX = 32;
|
|
29
35
|
|
|
30
36
|
export function displayToolName(name, args) {
|
|
31
37
|
return surfaceDisplayToolName(name, args);
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
/** Claude Code-style one-line renderToolUseMessage summary. */
|
|
35
|
-
export function summarizeArgs(name, args) {
|
|
36
|
-
return surfaceSummarizeToolArgs(name, args);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const MAX_RESULT_LINES = 8;
|
|
40
40
|
const TOOL_BLINK_MS = 500;
|
|
41
|
+
const TOOL_BLINK_LIMIT_MS = 3000;
|
|
41
42
|
const TOOL_PENDING_SHOW_DELAY_MS = 1000;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const COUNT_TWEEN_FRAME_MS = 70;
|
|
43
|
+
// Read `theme.subtle` at use-time (not captured here) so a live `/theme`
|
|
44
|
+
// switch re-tones the tool hints. `theme` is mutated in-place on switch.
|
|
45
45
|
|
|
46
46
|
function normalizeCount(value) {
|
|
47
47
|
const n = Number(value || 0);
|
|
48
48
|
return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function baselineCount(value) {
|
|
52
|
-
return normalizeCount(value) > 0 ? 1 : 0;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function easeOutCubic(t) {
|
|
56
|
-
const clamped = Math.max(0, Math.min(1, Number(t) || 0));
|
|
57
|
-
return 1 - Math.pow(1 - clamped, 3);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function tweenCount(from, to, progress) {
|
|
61
|
-
const start = normalizeCount(from);
|
|
62
|
-
const end = normalizeCount(to);
|
|
63
|
-
if (end <= start) return end;
|
|
64
|
-
const clamped = Math.max(0, Math.min(1, Number(progress) || 0));
|
|
65
|
-
if (clamped >= 1) return end;
|
|
66
|
-
return Math.min(end - 1, Math.max(start, Math.floor(start + ((end - start) * easeOutCubic(clamped)))));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function useCountUp(target, enabled = true) {
|
|
70
|
-
const normalized = normalizeCount(target);
|
|
71
|
-
const initial = enabled ? normalized : baselineCount(normalized);
|
|
72
|
-
const [display, setDisplay] = useState(initial);
|
|
73
|
-
const displayRef = useRef(initial);
|
|
74
|
-
const timerRef = useRef(null);
|
|
75
|
-
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
if (timerRef.current) {
|
|
78
|
-
clearInterval(timerRef.current);
|
|
79
|
-
timerRef.current = null;
|
|
80
|
-
}
|
|
81
|
-
if (!enabled) {
|
|
82
|
-
const baseline = baselineCount(normalized);
|
|
83
|
-
displayRef.current = baseline;
|
|
84
|
-
setDisplay(baseline);
|
|
85
|
-
return undefined;
|
|
86
|
-
}
|
|
87
|
-
const from = normalizeCount(displayRef.current);
|
|
88
|
-
const to = normalized;
|
|
89
|
-
if (to <= from) {
|
|
90
|
-
displayRef.current = to;
|
|
91
|
-
setDisplay(to);
|
|
92
|
-
return undefined;
|
|
93
|
-
}
|
|
94
|
-
const started = Date.now();
|
|
95
|
-
const tick = () => {
|
|
96
|
-
const progress = Math.min(1, (Date.now() - started) / COUNT_TWEEN_MS);
|
|
97
|
-
const next = tweenCount(from, to, progress);
|
|
98
|
-
displayRef.current = next;
|
|
99
|
-
setDisplay(next);
|
|
100
|
-
if (progress >= 1 && timerRef.current) {
|
|
101
|
-
clearInterval(timerRef.current);
|
|
102
|
-
timerRef.current = null;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
displayRef.current = from;
|
|
106
|
-
setDisplay(from);
|
|
107
|
-
const timer = setInterval(tick, COUNT_TWEEN_FRAME_MS);
|
|
108
|
-
timerRef.current = timer;
|
|
109
|
-
timer.unref?.();
|
|
110
|
-
return () => {
|
|
111
|
-
clearInterval(timer);
|
|
112
|
-
if (timerRef.current === timer) timerRef.current = null;
|
|
113
|
-
};
|
|
114
|
-
}, [normalized, enabled]);
|
|
115
|
-
|
|
116
|
-
return display;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
51
|
function normalizeCountMap(value = {}) {
|
|
120
52
|
const out = {};
|
|
121
|
-
for (const [key, raw] of Object.entries(value || {}))
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const out = {};
|
|
127
|
-
for (const [key, raw] of Object.entries(value || {})) out[key] = baselineCount(raw);
|
|
128
|
-
return out;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function countMapSignature(value = {}) {
|
|
132
|
-
return Object.keys(value || {})
|
|
133
|
-
.sort()
|
|
134
|
-
.map((key) => `${key}:${normalizeCount(value[key])}`)
|
|
135
|
-
.join('|');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function useCountUpMap(targets = {}, enabled = true) {
|
|
139
|
-
const normalizedTargets = normalizeCountMap(targets);
|
|
140
|
-
const signature = countMapSignature(normalizedTargets);
|
|
141
|
-
const initial = enabled ? normalizedTargets : baselineCountMap(normalizedTargets);
|
|
142
|
-
const [display, setDisplay] = useState(initial);
|
|
143
|
-
const displayRef = useRef(initial);
|
|
144
|
-
const timerRef = useRef(null);
|
|
145
|
-
|
|
146
|
-
useEffect(() => {
|
|
147
|
-
if (timerRef.current) {
|
|
148
|
-
clearInterval(timerRef.current);
|
|
149
|
-
timerRef.current = null;
|
|
150
|
-
}
|
|
151
|
-
if (!enabled) {
|
|
152
|
-
const baseline = baselineCountMap(normalizedTargets);
|
|
153
|
-
displayRef.current = baseline;
|
|
154
|
-
setDisplay(baseline);
|
|
155
|
-
return undefined;
|
|
156
|
-
}
|
|
157
|
-
const from = {};
|
|
158
|
-
let needsTween = false;
|
|
159
|
-
for (const [key, to] of Object.entries(normalizedTargets)) {
|
|
160
|
-
const current = normalizeCount(displayRef.current?.[key]);
|
|
161
|
-
from[key] = current;
|
|
162
|
-
if (to > current) needsTween = true;
|
|
163
|
-
}
|
|
164
|
-
if (!needsTween) {
|
|
165
|
-
displayRef.current = normalizedTargets;
|
|
166
|
-
setDisplay(normalizedTargets);
|
|
167
|
-
return undefined;
|
|
53
|
+
for (const [key, raw] of Object.entries(value || {})) {
|
|
54
|
+
if (raw && typeof raw === 'object') {
|
|
55
|
+
out[key] = { ...raw, count: normalizeCount(raw.count) };
|
|
56
|
+
} else {
|
|
57
|
+
out[key] = normalizeCount(raw);
|
|
168
58
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const progress = Math.min(1, (Date.now() - started) / COUNT_TWEEN_MS);
|
|
172
|
-
const next = {};
|
|
173
|
-
for (const [key, to] of Object.entries(normalizedTargets)) {
|
|
174
|
-
const start = normalizeCount(from[key]);
|
|
175
|
-
next[key] = tweenCount(start, to, progress);
|
|
176
|
-
}
|
|
177
|
-
displayRef.current = next;
|
|
178
|
-
setDisplay(next);
|
|
179
|
-
if (progress >= 1 && timerRef.current) {
|
|
180
|
-
clearInterval(timerRef.current);
|
|
181
|
-
timerRef.current = null;
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
displayRef.current = from;
|
|
185
|
-
setDisplay(from);
|
|
186
|
-
const timer = setInterval(tick, COUNT_TWEEN_FRAME_MS);
|
|
187
|
-
timerRef.current = timer;
|
|
188
|
-
timer.unref?.();
|
|
189
|
-
return () => {
|
|
190
|
-
clearInterval(timer);
|
|
191
|
-
if (timerRef.current === timer) timerRef.current = null;
|
|
192
|
-
};
|
|
193
|
-
}, [signature, enabled]);
|
|
194
|
-
|
|
195
|
-
return display;
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
196
61
|
}
|
|
197
62
|
|
|
198
63
|
function deltaColor(token) {
|
|
@@ -224,91 +89,126 @@ function renderDeltaText(text) {
|
|
|
224
89
|
));
|
|
225
90
|
}
|
|
226
91
|
|
|
92
|
+
// Shared multi-line result body: `└` on the first row, `│` continuation rail on
|
|
93
|
+
// every following row, body text in one flex column so wrapping stays aligned
|
|
94
|
+
// under the head gutter.
|
|
95
|
+
//
|
|
96
|
+
// Two render paths:
|
|
97
|
+
// - COLLAPSED (raw=false): a single fitted summary line, diff(+/-) colored via
|
|
98
|
+
// renderDeltaText.
|
|
99
|
+
// - EXPANDED (raw=true): formatExpandedResult then wrapExpandedResultLines so
|
|
100
|
+
// each physical row fits the body width before render (rail rows stay 1:1;
|
|
101
|
+
// ink does not re-wrap).
|
|
102
|
+
function ResultBody({ lines, rawText, pathArg = '', isShell = false, columns, color, raw }) {
|
|
103
|
+
const renderLines = raw
|
|
104
|
+
? wrapExpandedResultLines(formatExpandedResult(rawText, { pathArg, isShell }), columns)
|
|
105
|
+
: (lines || []);
|
|
106
|
+
if (!renderLines || renderLines.length === 0) return null;
|
|
107
|
+
return (
|
|
108
|
+
<Box flexDirection="row">
|
|
109
|
+
<Box flexShrink={0} flexDirection="column">
|
|
110
|
+
{renderLines.map((_, i) => (
|
|
111
|
+
<Text key={i} color={theme.subtle}>{i === 0 ? RESULT_GUTTER : RESULT_GUTTER_CONT}</Text>
|
|
112
|
+
))}
|
|
113
|
+
</Box>
|
|
114
|
+
<Box flexDirection="column" flexShrink={1} flexGrow={1}>
|
|
115
|
+
{renderLines.map((line, i) => (
|
|
116
|
+
<Text key={i} color={raw ? undefined : color} wrap="truncate">
|
|
117
|
+
{raw
|
|
118
|
+
? (line || ' ')
|
|
119
|
+
: renderDeltaText(fitResultLine(line || ' ', columns))}
|
|
120
|
+
</Text>
|
|
121
|
+
))}
|
|
122
|
+
</Box>
|
|
123
|
+
</Box>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
227
127
|
function plural(count, singular, pluralText = `${singular}s`) {
|
|
228
128
|
return count === 1 ? singular : pluralText;
|
|
229
129
|
}
|
|
230
130
|
|
|
231
|
-
function
|
|
131
|
+
function isShellTool(normalizedName, label = '') {
|
|
232
132
|
const n = String(normalizedName || '').toLowerCase();
|
|
233
133
|
const l = String(label || '').toLowerCase();
|
|
134
|
+
return n === 'shell' || n === 'bash' || n === 'bash_session' || n === 'shell_command' || n === 'job_wait' || l === 'run';
|
|
135
|
+
}
|
|
234
136
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
};
|
|
137
|
+
function shellResultStatus(value) {
|
|
138
|
+
const match = String(value || '').match(/(?:^|\b)status:\s*(running|pending|queued|completed|failed|cancelled|canceled)\b/im);
|
|
139
|
+
return match ? String(match[1] || '').toLowerCase() : '';
|
|
140
|
+
}
|
|
240
141
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
142
|
+
function normalizeTerminalStatus(value) {
|
|
143
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
144
|
+
if (!raw) return '';
|
|
145
|
+
if (/^(running|pending|queued|in_progress|in-progress)$/.test(raw)) return 'running';
|
|
146
|
+
if (/^(completed|complete|done|success|succeeded|ok)$/.test(raw)) return 'completed';
|
|
147
|
+
if (/^(failed|fail|error|errored|timeout|timed_out|killed)$/.test(raw)) return 'failed';
|
|
148
|
+
if (/^(cancelled|canceled|cancel)$/.test(raw)) return 'cancelled';
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
246
151
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (l === 'setup') return copy('Setting Up', 'Set Up', 'item');
|
|
302
|
-
if (l === 'memory') return copyTarget('Checking', 'Checked', 'memory', 'memories');
|
|
303
|
-
if (l === 'agent') return copyTarget('Calling', 'Called', 'agent');
|
|
304
|
-
return copy('Calling', 'Called', 'tool');
|
|
305
|
-
}
|
|
152
|
+
function displayTerminalStatus(value) {
|
|
153
|
+
const status = normalizeTerminalStatus(value);
|
|
154
|
+
if (status === 'running') return 'Running';
|
|
155
|
+
if (status === 'completed') return 'Finished';
|
|
156
|
+
if (status === 'failed') return 'Failed';
|
|
157
|
+
if (status === 'cancelled') return 'Cancelled';
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function resultTerminalStatus(value) {
|
|
162
|
+
const text = String(value || '');
|
|
163
|
+
const tagged = text.match(/<status[^>]*>([\s\S]*?)<\/status>/i)?.[1]?.trim();
|
|
164
|
+
if (tagged) return normalizeTerminalStatus(tagged);
|
|
165
|
+
const bracketed = text.match(/^\[status:\s*([^\]]*)\]/mi)?.[1]?.trim();
|
|
166
|
+
if (bracketed) return normalizeTerminalStatus(bracketed);
|
|
167
|
+
const inline = text.match(/^(?:status|state):\s*([^\s·,;]+)/mi)?.[1]?.trim();
|
|
168
|
+
return normalizeTerminalStatus(inline);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function shellDisplayStatus({ pending = false, failedCount = 0, isError = false, result = '' } = {}) {
|
|
172
|
+
const status = shellResultStatus(result);
|
|
173
|
+
if (pending || /^(running|pending|queued)$/.test(status)) return 'running';
|
|
174
|
+
if (/^cancel/.test(status)) return 'cancelled';
|
|
175
|
+
if (/^(failed|error|killed|timeout)$/.test(status) || isError || failedCount > 0) return 'failed';
|
|
176
|
+
return 'completed';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function shellHeader(status, count = 1) {
|
|
180
|
+
const n = Math.max(1, Number(count) || 1);
|
|
181
|
+
const object = `${n} ${plural(n, 'command')}`;
|
|
182
|
+
if (status === 'running') return `Running ${object}`;
|
|
183
|
+
return `Ran ${object}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function shellDetail(status, elapsed = '') {
|
|
187
|
+
const label = displayTerminalStatus(status) || status;
|
|
188
|
+
return elapsed ? `${elapsed} · ${label}` : label;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function shellResultElapsed(value) {
|
|
192
|
+
const match = String(value || '').match(/^\[elapsed:\s*(\d+)\s*ms\]/mi);
|
|
193
|
+
if (!match) return '';
|
|
194
|
+
const elapsedMs = Number(match[1]);
|
|
195
|
+
return Number.isFinite(elapsedMs) && elapsedMs >= 1000 ? formatElapsed(elapsedMs) : '';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function statusCopy(name, label, count, doneCount, pending, isError, args = {}) {
|
|
199
|
+
// No stableVerbWidth padding: it padded the done verb to the active ("-ing")
|
|
200
|
+
// width, which Ink trims at the line END (vendor output trimEnd) so it never
|
|
201
|
+
// stabilized the pending→done flip — it only left an UGLY mid-header gap
|
|
202
|
+
// ("Searched 1 pattern", "Read 1 file"). The header is wrap="truncate"
|
|
203
|
+
// behind a fixed gutter and the fullscreen full-clear repaints the row, so
|
|
204
|
+
// dropping the pad just normalizes the spacing.
|
|
205
|
+
return formatToolActionHeader(name, args, { pending, count });
|
|
306
206
|
}
|
|
307
207
|
|
|
308
208
|
function fitResultLine(line, columns) {
|
|
309
209
|
const max = Math.max(MIN_RESULT_LINE_CHARS, Number(columns || 80) - 7);
|
|
310
210
|
const text = String(line ?? '');
|
|
311
|
-
return text
|
|
211
|
+
return stringWidth(text) > max ? truncateToWidth(text, max) : text;
|
|
312
212
|
}
|
|
313
213
|
|
|
314
214
|
/** Trim text from the end (by display width) so it fits maxWidth, appending '…'. */
|
|
@@ -326,16 +226,19 @@ function truncateToWidth(text, maxWidth) {
|
|
|
326
226
|
}
|
|
327
227
|
|
|
328
228
|
function isAgentTool(normalizedName) {
|
|
329
|
-
return normalizedName === '
|
|
229
|
+
return normalizedName === 'agent';
|
|
330
230
|
}
|
|
331
231
|
|
|
232
|
+
const SKILL_SURFACE_NAMES = new Set([
|
|
233
|
+
'skill', 'skill_execute', 'skill_view', 'skills_list', 'use_skill',
|
|
234
|
+
]);
|
|
235
|
+
|
|
332
236
|
function isBackgroundTaskTool(normalizedName) {
|
|
333
237
|
return new Set(['explore', 'search', 'shell', 'bash', 'bash_session', 'shell_command', 'task']).has(String(normalizedName || '').toLowerCase());
|
|
334
238
|
}
|
|
335
239
|
|
|
336
240
|
const AGENT_DISPLAY_NAMES = new Map([
|
|
337
241
|
['explore', 'Explore'],
|
|
338
|
-
['web-researcher', 'Web Researcher'],
|
|
339
242
|
['maintainer', 'Maintainer'],
|
|
340
243
|
['worker', 'Worker'],
|
|
341
244
|
['heavy-worker', 'Heavy Worker'],
|
|
@@ -356,19 +259,60 @@ function titleizeAgentName(value) {
|
|
|
356
259
|
.join(' ');
|
|
357
260
|
}
|
|
358
261
|
|
|
262
|
+
function agentModelLabel(args) {
|
|
263
|
+
const a = args && typeof args === 'object' ? args : {};
|
|
264
|
+
const provider = String(a.provider || a.providerId || a.provider_id || '').trim();
|
|
265
|
+
const model = String(a.model || '').trim();
|
|
266
|
+
const displayHint = String(a.modelDisplay || a.model_display || a.displayModel || '').trim();
|
|
267
|
+
return displayModelName(model, provider, displayHint);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function withModel(label, args) {
|
|
271
|
+
const model = agentModelLabel(args);
|
|
272
|
+
return model ? `${label} (${model})` : label;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function agentTagLabel(args) {
|
|
276
|
+
// The real spawn tag (engine fills parsedArgs.tag from the envelope target).
|
|
277
|
+
// Never fall back to task_id — only the human-meaningful spawn tag belongs in
|
|
278
|
+
// the header parentheses.
|
|
279
|
+
return String(args?.tag || '').trim();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function withModelAndTag(label, args) {
|
|
283
|
+
const model = agentModelLabel(args);
|
|
284
|
+
const tag = agentTagLabel(args);
|
|
285
|
+
const inner = [model, tag].filter(Boolean).join(', ');
|
|
286
|
+
return inner ? `${label} (${inner})` : label;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Append a role name to a base action word without leaving a trailing space
|
|
290
|
+
// when the role is unknown (no generic "Agent" fallback).
|
|
291
|
+
function joinActionRole(action, role) {
|
|
292
|
+
return role ? `${action} ${role}` : action;
|
|
293
|
+
}
|
|
294
|
+
|
|
359
295
|
function agentResponseTitle(args) {
|
|
360
296
|
const name = titleizeAgentName(args?.agent || args?.role || args?.subagent_type || args?.name || '');
|
|
361
|
-
|
|
297
|
+
// The agent role + model identify the responder; the response summary itself
|
|
298
|
+
// is hidden in the collapsed card (ctrl+o expand still shows the full body).
|
|
299
|
+
// No generic "Agent" fallback — render just "Response" when the role is empty.
|
|
300
|
+
return withModelAndTag(joinActionRole('Response', name), args);
|
|
362
301
|
}
|
|
363
302
|
|
|
364
303
|
function agentActionTitle(args) {
|
|
365
304
|
const name = titleizeAgentName(args?.agent || args?.role || args?.subagent_type || args?.name || '');
|
|
366
|
-
const agent = name || 'Agent';
|
|
367
305
|
const action = String(args?.type || args?.action || '').toLowerCase();
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (action === '
|
|
306
|
+
// Fixed action verbs regardless of running/completed status. No generic
|
|
307
|
+
// "Agent" fallback for the role: when the role is unknown render the action
|
|
308
|
+
// word alone ("Spawn") instead of "Spawn Agent".
|
|
309
|
+
if (action === 'spawn') return withModelAndTag(joinActionRole('Spawn', name), args);
|
|
310
|
+
if (action === 'send') return withModelAndTag(joinActionRole('Send', name), args);
|
|
311
|
+
if (action === 'list') return 'Agent status';
|
|
312
|
+
if (action === 'cancel') return withModelAndTag(joinActionRole('Cancel', name), args);
|
|
313
|
+
if (action === 'close') return withModelAndTag(joinActionRole('Close', name), args);
|
|
314
|
+
if (action === 'cleanup') return withModelAndTag(joinActionRole('Cleanup', name), args);
|
|
315
|
+
if (action === 'read' || action === 'status') return withModelAndTag(joinActionRole('Status', name), args);
|
|
372
316
|
return '';
|
|
373
317
|
}
|
|
374
318
|
|
|
@@ -377,15 +321,26 @@ function agentActionSummary(args, summary) {
|
|
|
377
321
|
if (!text) return '';
|
|
378
322
|
const name = titleizeAgentName(args?.agent || args?.role || args?.subagent_type || args?.name || '');
|
|
379
323
|
if (name && text === name) return '';
|
|
380
|
-
|
|
381
|
-
|
|
324
|
+
let rest = name && text.startsWith(`${name} · `) ? text.slice(name.length + 3).trim() : text;
|
|
325
|
+
// The role/model/tag surface summary ("Heavy Worker · Opus 4.8") is now folded
|
|
326
|
+
// into the header label itself ("Spawn Heavy Worker (Opus 4.8, tag)"), so drop
|
|
327
|
+
// the model and tag tokens from the parenthesized summary to avoid showing
|
|
328
|
+
// them twice.
|
|
329
|
+
const model = agentModelLabel(args);
|
|
330
|
+
if (model && rest === model) return '';
|
|
331
|
+
const tag = agentTagLabel(args);
|
|
332
|
+
if (tag && rest === tag) return '';
|
|
333
|
+
return rest;
|
|
382
334
|
}
|
|
383
335
|
|
|
384
336
|
function hasAgentResponseResult(value) {
|
|
385
337
|
const text = String(value || '').trim();
|
|
386
338
|
if (!text) return false;
|
|
339
|
+
if (/^(?:undefined|null)$/i.test(text)) return false;
|
|
387
340
|
if (/^status:\s*(?:running|pending|queued|completed|failed|cancelled|canceled)(?:\s*·\s*task_id:\s*\S+)?$/i.test(text)) return false;
|
|
388
|
-
const isBridgeEnvelope = /^(?:
|
|
341
|
+
const isBridgeEnvelope = /^(?:agent task:|background task\b|agent message queued\b|agent close:)/i.test(text)
|
|
342
|
+
|| /^(?:agents|tasks):\s*\d/i.test(text)
|
|
343
|
+
|| /^\(no agents or tasks\)$/i.test(text)
|
|
389
344
|
|| (/^task_id:\s*\S+/mi.test(text) && /^(?:surface|operation|status):\s*/mi.test(text));
|
|
390
345
|
if (!isBridgeEnvelope) return true;
|
|
391
346
|
let sawBlank = false;
|
|
@@ -395,10 +350,12 @@ function hasAgentResponseResult(value) {
|
|
|
395
350
|
sawBlank = true;
|
|
396
351
|
continue;
|
|
397
352
|
}
|
|
398
|
-
if (/^
|
|
353
|
+
if (/^agent result\b/i.test(trimmed)) continue;
|
|
354
|
+
if (/^(?:undefined|null)$/i.test(trimmed)) continue;
|
|
399
355
|
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;
|
|
400
|
-
if (!sawBlank && /^(?:
|
|
401
|
-
if (!sawBlank && /^(?:
|
|
356
|
+
if (!sawBlank && /^(?:agent task|background task|agent message queued\b|agent close:|task_id|surface|operation|label|status|type|target|role|agent|preset|model|effort|fast|limits|started|finished|error|notification|queueDepth):?\s*/i.test(trimmed)) continue;
|
|
357
|
+
if (!sawBlank && /^(?:agents|tasks):\s*/i.test(trimmed)) continue;
|
|
358
|
+
if (/^\(no agents or tasks\)$/i.test(trimmed)) continue;
|
|
402
359
|
if (!sawBlank && /^-\s+\S+/i.test(trimmed)) continue;
|
|
403
360
|
return true;
|
|
404
361
|
}
|
|
@@ -421,17 +378,82 @@ function parseBackgroundTaskResult(value) {
|
|
|
421
378
|
if (match) fields[match[1].toLowerCase()] = match[2].trim();
|
|
422
379
|
}
|
|
423
380
|
const status = String(fields.status || '').toLowerCase();
|
|
381
|
+
const error = fields.error || '';
|
|
424
382
|
return {
|
|
425
383
|
taskId: fields.task_id || fields.taskid || '',
|
|
426
384
|
surface: fields.surface || '',
|
|
427
385
|
operation: fields.operation || '',
|
|
428
386
|
label: fields.label || '',
|
|
429
387
|
status,
|
|
388
|
+
startedAt: fields.started || fields.startedat || '',
|
|
389
|
+
finishedAt: fields.finished || fields.finishedat || '',
|
|
430
390
|
body,
|
|
431
|
-
|
|
391
|
+
error,
|
|
392
|
+
hasResponse: Boolean(body) && !isBackgroundErrorOnlyBody(body, error) && !/^(running|pending|queued)$/i.test(status),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function backgroundTaskMetaFromArgs(args = {}) {
|
|
397
|
+
const taskId = String(args.task_id || args.taskId || '').trim();
|
|
398
|
+
if (!taskId) return null;
|
|
399
|
+
return {
|
|
400
|
+
taskId,
|
|
401
|
+
surface: args.surface || '',
|
|
402
|
+
operation: args.operation || '',
|
|
403
|
+
label: args.label || '',
|
|
404
|
+
status: String(args.status || '').toLowerCase(),
|
|
405
|
+
startedAt: args.startedAt || args.started || '',
|
|
406
|
+
finishedAt: args.finishedAt || args.finished || '',
|
|
407
|
+
error: args.error || '',
|
|
408
|
+
body: '',
|
|
409
|
+
hasResponse: false,
|
|
432
410
|
};
|
|
433
411
|
}
|
|
434
412
|
|
|
413
|
+
function resolveBackgroundTaskMeta(parsedArgs = {}, resultText = '') {
|
|
414
|
+
const parsed = parseBackgroundTaskResult(resultText);
|
|
415
|
+
if (parsed) {
|
|
416
|
+
if (!parsed.error && parsedArgs?.error) parsed.error = parsedArgs.error;
|
|
417
|
+
if (!parsed.status && parsedArgs?.status) parsed.status = String(parsedArgs.status).toLowerCase();
|
|
418
|
+
if (!parsed.surface && parsedArgs?.surface) parsed.surface = parsedArgs.surface;
|
|
419
|
+
return parsed;
|
|
420
|
+
}
|
|
421
|
+
return backgroundTaskMetaFromArgs(parsedArgs);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function backgroundTaskElapsed(meta = {}, fallback = '') {
|
|
425
|
+
const startedMs = Date.parse(meta.startedAt || '');
|
|
426
|
+
const finishedMs = Date.parse(meta.finishedAt || '');
|
|
427
|
+
if (Number.isFinite(startedMs) && Number.isFinite(finishedMs) && finishedMs >= startedMs) {
|
|
428
|
+
const elapsedMs = finishedMs - startedMs;
|
|
429
|
+
return elapsedMs >= 1000 ? formatElapsed(elapsedMs) : '';
|
|
430
|
+
}
|
|
431
|
+
return fallback || '';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function prefixElapsed(detail, elapsed = '') {
|
|
435
|
+
const text = String(detail || '').trim();
|
|
436
|
+
const time = String(elapsed || '').trim();
|
|
437
|
+
if (!time) return text;
|
|
438
|
+
return text ? `${time} · ${text}` : time;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function mergeTerminalDetail(status, detail = '') {
|
|
442
|
+
const label = displayTerminalStatus(status);
|
|
443
|
+
const text = String(detail || '').trim();
|
|
444
|
+
if (!label) return text;
|
|
445
|
+
if (label === 'Finished' && text) return text;
|
|
446
|
+
if (!text) return label;
|
|
447
|
+
if (text.toLowerCase().startsWith(label.toLowerCase())) return text;
|
|
448
|
+
return `${label} · ${text}`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function shouldPrefixSyncElapsed(normalizedName, label) {
|
|
452
|
+
const n = String(normalizedName || '').toLowerCase();
|
|
453
|
+
const l = String(label || '').toLowerCase();
|
|
454
|
+
return n === 'explore' || l === 'explore' || n === 'search' || l === 'search' || l === 'web search';
|
|
455
|
+
}
|
|
456
|
+
|
|
435
457
|
function backgroundTaskDisplayName(normalizedName, meta = {}) {
|
|
436
458
|
const surface = String(meta.surface || normalizedName || '').toLowerCase();
|
|
437
459
|
if (surface === 'explore') return 'Explore';
|
|
@@ -454,20 +476,29 @@ function backgroundTaskActionTitle(normalizedName, meta = {}) {
|
|
|
454
476
|
return `${display} status`;
|
|
455
477
|
}
|
|
456
478
|
|
|
457
|
-
function
|
|
479
|
+
function backgroundTaskFailureDetail(meta = {}, parsedArgs = {}) {
|
|
480
|
+
const status = meta.status || parsedArgs?.status;
|
|
481
|
+
const error = meta.error || parsedArgs?.error;
|
|
482
|
+
if (!error) return '';
|
|
483
|
+
const surface = meta.surface || parsedArgs?.surface || '';
|
|
484
|
+
return backgroundTaskFailureStatusLabel(status, error, { surface });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function backgroundTaskDetail(meta = {}, elapsed = '', parsedArgs = {}) {
|
|
458
488
|
const parts = [];
|
|
459
|
-
|
|
489
|
+
const status = displayTerminalStatus(meta.status);
|
|
490
|
+
if (status) parts.push(status);
|
|
460
491
|
if (meta.taskId) parts.push(`task_id: ${meta.taskId}`);
|
|
461
492
|
const firstBodyLine = String(meta.body || '').split('\n').map((line) => line.trim()).find(Boolean) || '';
|
|
462
493
|
if (firstBodyLine && /^(running|pending|queued)$/i.test(meta.status || '')) parts.push(firstBodyLine);
|
|
463
|
-
return parts.join(' · ');
|
|
494
|
+
return prefixElapsed(parts.join(' · '), elapsed);
|
|
464
495
|
}
|
|
465
496
|
|
|
466
497
|
function isBackgroundTaskResponseArgs(normalizedName, args = {}) {
|
|
467
498
|
if (!isBackgroundTaskTool(normalizedName)) return false;
|
|
468
499
|
const type = String(args?.type || args?.action || '').toLowerCase();
|
|
469
500
|
const status = String(args?.status || '').toLowerCase();
|
|
470
|
-
return type === 'result' || type === 'completion' || (/^(completed|
|
|
501
|
+
return type === 'result' || type === 'completion' || (/^(completed|cancelled|canceled)$/i.test(status) && Boolean(args?.task_id));
|
|
471
502
|
}
|
|
472
503
|
|
|
473
504
|
function isOutputDetailTool(normalizedName, label) {
|
|
@@ -476,26 +507,14 @@ function isOutputDetailTool(normalizedName, label) {
|
|
|
476
507
|
return new Set([
|
|
477
508
|
'shell', 'bash', 'bash_session', 'shell_command', 'job_wait',
|
|
478
509
|
'read', 'view_image', 'read_mcp_resource',
|
|
479
|
-
'grep', 'glob', 'search', 'search_query', 'image_query', 'web_search', 'web_search_call', '
|
|
510
|
+
'grep', 'glob', 'search', 'search_query', 'image_query', 'web_search', 'web_search_call', 'explore', 'web_fetch', 'fetch', 'download_attachment',
|
|
480
511
|
'list', 'ls', 'code_graph',
|
|
481
512
|
'recall', 'recall_memory', 'search_memories', 'remember', 'save_memory', 'update_memory',
|
|
482
513
|
]).has(n) || l === 'read' || l === 'search' || l === 'web search' || l === 'run';
|
|
483
514
|
}
|
|
484
515
|
|
|
485
516
|
function progressDetail({ normalizedName, label, elapsed }) {
|
|
486
|
-
|
|
487
|
-
const l = String(label || '').toLowerCase();
|
|
488
|
-
const suffix = elapsed ? ` - ${elapsed}` : '';
|
|
489
|
-
if (isAgentTool(n) || l === 'agent') return `Calling Agent${suffix}`;
|
|
490
|
-
if (n === 'shell' || n === 'bash' || n === 'bash_session' || n === 'shell_command' || n === 'job_wait' || l === 'run') return `Running${suffix}`;
|
|
491
|
-
if (n === 'search' || n === 'search_query' || n === 'image_query' || n === 'web_search' || n === 'web_search_call' || n === 'firecrawl_search' || n === 'web_fetch' || n === 'fetch' || n === 'download_attachment' || l === 'web search') return `Researching Web${suffix}`;
|
|
492
|
-
if (n === 'explore' || l === 'explore') return `Exploring${suffix}`;
|
|
493
|
-
if (n === 'grep' || n === 'glob' || n === 'list' || n === 'ls' || l === 'search') return `Searching${suffix}`;
|
|
494
|
-
if (n === 'read' || n === 'view_image' || n === 'read_mcp_resource' || l === 'read') return `Reading${suffix}`;
|
|
495
|
-
if (n === 'write' || n === 'edit' || n === 'apply_patch' || l === 'update') return `Editing${suffix}`;
|
|
496
|
-
if (n === 'recall' || n === 'recall_memory' || n === 'search_memories' || l === 'memory') return `Checking Memory${suffix}`;
|
|
497
|
-
if (l === 'setup') return `Setting Up${suffix}`;
|
|
498
|
-
return `Working${suffix}`;
|
|
517
|
+
return elapsed ? `${elapsed} elapsed` : '';
|
|
499
518
|
}
|
|
500
519
|
|
|
501
520
|
function genericCompletedDetail({ normalizedName, label, hasResult, firstResultLine, isError }) {
|
|
@@ -503,7 +522,7 @@ function genericCompletedDetail({ normalizedName, label, hasResult, firstResultL
|
|
|
503
522
|
const l = String(label || '').toLowerCase();
|
|
504
523
|
if (isError) return hasResult ? firstResultLine : 'Failed';
|
|
505
524
|
if (n === 'shell' || n === 'bash' || n === 'bash_session' || n === 'shell_command' || n === 'job_wait') {
|
|
506
|
-
return
|
|
525
|
+
return '';
|
|
507
526
|
}
|
|
508
527
|
if (isOutputDetailTool(n, l)) {
|
|
509
528
|
return hasResult ? firstResultLine : '';
|
|
@@ -511,7 +530,29 @@ function genericCompletedDetail({ normalizedName, label, hasResult, firstResultL
|
|
|
511
530
|
return '';
|
|
512
531
|
}
|
|
513
532
|
|
|
514
|
-
function
|
|
533
|
+
function toolSearchLoadedSummary(resultText) {
|
|
534
|
+
let parsed;
|
|
535
|
+
try {
|
|
536
|
+
parsed = JSON.parse(String(resultText || ''));
|
|
537
|
+
} catch {
|
|
538
|
+
return '';
|
|
539
|
+
}
|
|
540
|
+
const tools = parsed?.selected?.tools;
|
|
541
|
+
if (!tools || typeof tools !== 'object') return '';
|
|
542
|
+
const names = [
|
|
543
|
+
...(Array.isArray(tools.added) ? tools.added : []),
|
|
544
|
+
...(Array.isArray(tools.already) ? tools.already : []),
|
|
545
|
+
]
|
|
546
|
+
.map((name) => String(name || '').trim())
|
|
547
|
+
.filter(Boolean);
|
|
548
|
+
return [...new Set(names)].join(', ');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function agentTerminalDetail(status, isError, elapsed, error = '') {
|
|
552
|
+
const failureDetail = isError && error
|
|
553
|
+
? backgroundTaskFailureStatusLabel(status, error, { surface: 'agent' })
|
|
554
|
+
: '';
|
|
555
|
+
if (failureDetail) return failureDetail;
|
|
515
556
|
const s = String(status || '').toLowerCase();
|
|
516
557
|
const word = /cancel/.test(s)
|
|
517
558
|
? 'Cancelled'
|
|
@@ -529,16 +570,21 @@ function clampFailureCount(errorCount, groupCount, isError) {
|
|
|
529
570
|
return isError ? groupCount : 0;
|
|
530
571
|
}
|
|
531
572
|
|
|
532
|
-
function toolStatusColor({ pending, groupCount, failedCount }) {
|
|
533
|
-
if (pending) return theme.
|
|
573
|
+
function toolStatusColor({ pending, groupCount, failedCount, terminalStatus = '' }) {
|
|
574
|
+
if (pending) return theme.success;
|
|
575
|
+
const status = normalizeTerminalStatus(terminalStatus);
|
|
576
|
+
if (status === 'failed') return theme.error;
|
|
577
|
+
if (status === 'cancelled') return theme.warning || theme.mixdogOrange || theme.subtle;
|
|
534
578
|
if (failedCount <= 0) return theme.success;
|
|
535
579
|
if (groupCount > 1 && failedCount < groupCount) return theme.mixdogOrange || theme.warning;
|
|
536
580
|
return theme.error;
|
|
537
581
|
}
|
|
538
582
|
|
|
539
|
-
export function ToolExecution({ name, args, result, rawResult, isError, errorCount, expanded, globalExpanded = false, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, headerFinalized = true }) {
|
|
583
|
+
export function ToolExecution({ name, args, result, rawResult, isError, errorCount, expanded, globalExpanded = false, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, headerFinalized = true, deferredDisplayReady = false }) {
|
|
540
584
|
const [blinkOn, setBlinkOn] = useState(true);
|
|
585
|
+
const [blinkExpired, setBlinkExpired] = useState(false);
|
|
541
586
|
const [pendingDelayElapsed, setPendingDelayElapsed] = useState(false);
|
|
587
|
+
const [, setElapsedTick] = useState(0);
|
|
542
588
|
const groupCount = Math.max(1, Number(count || 1));
|
|
543
589
|
const doneCount = Math.max(0, Math.min(groupCount, Number(completedCount || (result == null ? 0 : groupCount))));
|
|
544
590
|
const rt = result == null ? null : String(result).replace(/\s+$/, '');
|
|
@@ -547,18 +593,30 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
547
593
|
const startedAtMs = Number(startedAt || 0);
|
|
548
594
|
const completedAtMs = Number(completedAt || 0);
|
|
549
595
|
const pendingAgeMs = pending && startedAtMs ? Math.max(0, Date.now() - startedAtMs) : 0;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
596
|
+
// A card that is still pending but already has something to paint (a result
|
|
597
|
+
// landed, or at least one of an aggregate's parallel calls completed) must
|
|
598
|
+
// SKIP the blank placeholder: it was pushed early (engine ensureVisible on a
|
|
599
|
+
// result before the push-delay) so its startedAt is recent and pendingAgeMs <
|
|
600
|
+
// delay, but it has real header counts + a summary to show. Rendering the
|
|
601
|
+
// placeholder instead made an empty card scroll up first and only fill in as
|
|
602
|
+
// each parallel result arrived. Treating "has visible content" as ready lets
|
|
603
|
+
// the card appear already populated and simply grow taller as more results
|
|
604
|
+
// land — no empty band.
|
|
605
|
+
const hasVisibleProgress = doneCount > 0 || Boolean(String(rt || '').trim());
|
|
606
|
+
const pendingDisplayReady = !pending || !startedAtMs || pendingDelayElapsed || pendingAgeMs >= TOOL_PENDING_SHOW_DELAY_MS || hasVisibleProgress || deferredDisplayReady;
|
|
607
|
+
// Keep the action verb in its active form until the engine explicitly seals
|
|
608
|
+
// the tool block. Fast tool batches often complete before the next provider
|
|
609
|
+
// iteration decides whether to call more tools or emit assistant text; flipping
|
|
610
|
+
// "Finding" -> "Found" -> "Finding" during that gap makes the transcript jump.
|
|
611
|
+
const headerPending = pending || headerFinalized === false;
|
|
553
612
|
const hasResult = result != null && Boolean(String(rt || '').trim());
|
|
554
613
|
const hasRawResult = rawResult != null && Boolean(String(rawRt || '').trim());
|
|
555
614
|
const elapsedMs = startedAtMs ? Math.max(0, (pending ? Date.now() : (completedAtMs || Date.now())) - startedAtMs) : 0;
|
|
556
615
|
const elapsed = elapsedMs >= 1000 ? formatElapsed(elapsedMs) : '';
|
|
557
616
|
const failedCount = clampFailureCount(errorCount, groupCount, isError);
|
|
558
617
|
const statusColor = toolStatusColor({ pending, groupCount, failedCount });
|
|
559
|
-
const
|
|
560
|
-
const
|
|
561
|
-
const displayCategories = useCountUpMap(categories || {}, aggregate === true && countsVisible);
|
|
618
|
+
const displayGroupCount = groupCount;
|
|
619
|
+
const displayCategories = normalizeCountMap(categories || {});
|
|
562
620
|
|
|
563
621
|
useEffect(() => {
|
|
564
622
|
if (!pending) {
|
|
@@ -581,12 +639,60 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
581
639
|
}, [pending, startedAt]);
|
|
582
640
|
|
|
583
641
|
useEffect(() => {
|
|
584
|
-
if (!pending || !pendingDisplayReady)
|
|
642
|
+
if (!pending || !pendingDisplayReady || blinkExpired) {
|
|
643
|
+
setBlinkOn(true);
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
585
646
|
const timer = setInterval(() => setBlinkOn((on) => !on), TOOL_BLINK_MS);
|
|
586
647
|
return () => clearInterval(timer);
|
|
587
|
-
}, [pending, pendingDisplayReady]);
|
|
648
|
+
}, [pending, pendingDisplayReady, blinkExpired]);
|
|
649
|
+
|
|
650
|
+
useEffect(() => {
|
|
651
|
+
if (!pending || !pendingDisplayReady) {
|
|
652
|
+
setBlinkExpired(false);
|
|
653
|
+
return undefined;
|
|
654
|
+
}
|
|
655
|
+
const started = Number(startedAt || 0);
|
|
656
|
+
const remaining = TOOL_BLINK_LIMIT_MS - (started ? Math.max(0, Date.now() - started) : 0);
|
|
657
|
+
if (remaining <= 0) {
|
|
658
|
+
setBlinkExpired(true);
|
|
659
|
+
return undefined;
|
|
660
|
+
}
|
|
661
|
+
setBlinkExpired(false);
|
|
662
|
+
const timer = setTimeout(() => setBlinkExpired(true), remaining);
|
|
663
|
+
return () => clearTimeout(timer);
|
|
664
|
+
}, [pending, pendingDisplayReady, startedAt]);
|
|
588
665
|
|
|
589
|
-
|
|
666
|
+
useEffect(() => {
|
|
667
|
+
if (!pending || !pendingDisplayReady || !startedAtMs) return undefined;
|
|
668
|
+
const timer = setInterval(() => setElapsedTick((tick) => (tick + 1) % 1000000), 1000);
|
|
669
|
+
return () => clearInterval(timer);
|
|
670
|
+
}, [pending, pendingDisplayReady, startedAtMs]);
|
|
671
|
+
|
|
672
|
+
// While a freshly-started tool is still inside its pending-show delay we used
|
|
673
|
+
// to `return null` (0 rendered rows). But estimateTranscriptItemRows() in
|
|
674
|
+
// App.jsx counts a collapsed tool item from the moment it is pushed (1 row for
|
|
675
|
+
// a skill surface, 2 rows otherwise), so the scroll/window math reserved that
|
|
676
|
+
// height while the component painted 0. The moment the delay elapsed (or the
|
|
677
|
+
// tool completed) the real card popped in, the rendered transcript grew and
|
|
678
|
+
// shoved the content above it — the "new tool card jumps up/down as it
|
|
679
|
+
// settles" bug. Reserve the SAME height the estimator predicts with blank
|
|
680
|
+
// content instead, so the card occupies a constant height for its whole
|
|
681
|
+
// lifecycle and nothing reflows when the real header/detail fill in place.
|
|
682
|
+
if (pending && !pendingDisplayReady) {
|
|
683
|
+
// Mirror estimateTranscriptItemRows: a non-aggregate skill surface collapses
|
|
684
|
+
// to a single header row; everything else reserves header + one detail row.
|
|
685
|
+
const placeholderNormalizedName = String(formatToolSurface(name, args)?.normalizedName || '').toLowerCase();
|
|
686
|
+
// Skill surfaces collapse to a single header row; agent surfaces reserve
|
|
687
|
+
// header + one brief detail row (see estimateTranscriptItemRows).
|
|
688
|
+
const placeholderSingleRow = !aggregate && SKILL_SURFACE_NAMES.has(placeholderNormalizedName);
|
|
689
|
+
return (
|
|
690
|
+
<Box flexDirection="column" marginTop={attached ? 0 : 1}>
|
|
691
|
+
<Text> </Text>
|
|
692
|
+
{placeholderSingleRow ? null : <Text> </Text>}
|
|
693
|
+
</Box>
|
|
694
|
+
);
|
|
695
|
+
}
|
|
590
696
|
|
|
591
697
|
// ── Aggregate card ──────────────────────────────────────────────
|
|
592
698
|
if (aggregate) {
|
|
@@ -594,28 +700,64 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
594
700
|
// bounce between "Reading 1 item" and "Reading 4 items". Final counts and
|
|
595
701
|
// result summaries appear only after completion.
|
|
596
702
|
const headerOrder = Array.isArray(args?.categoryOrder) ? args.categoryOrder : null;
|
|
703
|
+
// No stableVerbWidth: see statusCopy — the padding only left a mid-header
|
|
704
|
+
// gap ("Searched 1 pattern, Read 1 file") since Ink trims trailing
|
|
705
|
+
// spaces and never stabilized the flip.
|
|
597
706
|
const headerText = formatAggregateHeader(displayCategories || {}, { pending: headerPending, order: headerOrder });
|
|
598
707
|
let detailText;
|
|
599
708
|
if (hasResult) {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
709
|
+
// The aggregate card reserves EXACTLY ONE detail row when it is not
|
|
710
|
+
// expanded-with-raw (App.jsx estimateTranscriptItemRows counts
|
|
711
|
+
// margin + header + 1 detail row for the no-raw aggregate case). The
|
|
712
|
+
// summary `rt` can be multiline; a single <Text> containing '\n' renders
|
|
713
|
+
// MULTIPLE terminal rows, which desyncs the estimate and makes the card
|
|
714
|
+
// "settle" taller than reserved. Collapse to a single logical line
|
|
715
|
+
// (whitespace-normalized); fitResultLine below trims it to the column
|
|
716
|
+
// width so it can never exceed one terminal row.
|
|
717
|
+
detailText = String(rt).replace(/\s+/g, ' ').trim();
|
|
603
718
|
} else {
|
|
604
719
|
detailText = '';
|
|
605
720
|
}
|
|
606
721
|
|
|
607
|
-
const dotColor = statusColor;
|
|
608
|
-
const dotText = pending && !blinkOn ? ' ' : TURN_MARKER;
|
|
722
|
+
const dotColor = !hasResult && !pending ? theme.subtle : statusColor;
|
|
723
|
+
const dotText = pending && !blinkExpired && !blinkOn ? ' ' : TURN_MARKER;
|
|
609
724
|
const gutter = 2;
|
|
610
725
|
const showHeaderExpandHint = hasRawResult;
|
|
611
|
-
const hintLabel =
|
|
612
|
-
const hintText =
|
|
613
|
-
const
|
|
726
|
+
const hintLabel = `ctrl+o ${expanded ? 'collapse' : 'expand'}`;
|
|
727
|
+
const hintText = ` ${BULLET_OPERATOR} ${hintLabel}`;
|
|
728
|
+
const headerMetaText = pending && elapsed ? ` (${elapsed} elapsed)` : '';
|
|
729
|
+
// Reserve the expand-hint slot for the card's whole lifecycle so the header
|
|
730
|
+
// body never reflows when the hint appears on completion. During pending the
|
|
731
|
+
// elapsed meta shares this same right region; reserving the wider of the two
|
|
732
|
+
// keeps `avail` (and the header clip point) fixed, so nothing "appears then
|
|
733
|
+
// shrinks back" on the right edge as counts/results land.
|
|
734
|
+
const rightReserve = Math.max(stringWidth(hintText), stringWidth(headerMetaText));
|
|
735
|
+
const avail = Math.max(1, (Number(columns) || 80) - 1 - gutter - rightReserve);
|
|
736
|
+
const trailingText = headerMetaText || (showHeaderExpandHint ? hintText : '');
|
|
737
|
+
const trailingColor = theme.subtle;
|
|
614
738
|
const clippedHeader = stringWidth(headerText) > avail
|
|
615
739
|
? truncateToWidth(headerText, avail)
|
|
616
740
|
: headerText;
|
|
617
|
-
|
|
618
|
-
|
|
741
|
+
// Trailing content (elapsed while pending, ctrl+o hint when done) always
|
|
742
|
+
// sits immediately after the header body — no fixed right-edge pin — so it
|
|
743
|
+
// never jumps to the right edge and snaps back on the pending→done flip.
|
|
744
|
+
// Keep the aggregate card at a fixed height (header + one detail row) for
|
|
745
|
+
// its whole lifecycle. Pending cards have no result yet, so reserve the
|
|
746
|
+
// detail row up front instead of growing from 1→2 rows when the summary
|
|
747
|
+
// lands on completion — that late row push is the "줄 튐" jump. The empty
|
|
748
|
+
// placeholder renders as a blank line under the ⎿ gutter; the final summary
|
|
749
|
+
// simply fills it in place. This matches estimateTranscriptItemRows (always
|
|
750
|
+
// 2 + resultRows), so windowing/scroll stay in lockstep too.
|
|
751
|
+
// When there is no summary yet (pending) or none could be derived, fill the
|
|
752
|
+
// reserved detail row with a status word instead of a blank line so the area
|
|
753
|
+
// under the ⎿ gutter never looks empty. Real summaries keep the normal text
|
|
754
|
+
// color; the status placeholder is rendered dim.
|
|
755
|
+
const isPlaceholderDetail = !(expanded && hasRawResult) && !detailText;
|
|
756
|
+
const showRawAggregate = expanded && hasRawResult;
|
|
757
|
+
const detailLines = showRawAggregate
|
|
758
|
+
? rawRt.split('\n')
|
|
759
|
+
: (detailText ? [detailText] : [headerPending ? 'Running' : 'Finished']);
|
|
760
|
+
const aggregateDetailColor = isPlaceholderDetail ? theme.subtle : theme.text;
|
|
619
761
|
return (
|
|
620
762
|
<Box flexDirection="column" marginTop={attached ? 0 : 1}>
|
|
621
763
|
<Box flexDirection="row">
|
|
@@ -624,93 +766,180 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
624
766
|
</Box>
|
|
625
767
|
<Text wrap="truncate">
|
|
626
768
|
<Text bold color={theme.text}>{clippedHeader}</Text>
|
|
627
|
-
{
|
|
769
|
+
{trailingText ? <Text color={trailingColor}>{trailingText}</Text> : null}
|
|
628
770
|
</Text>
|
|
629
771
|
</Box>
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
<Text key={i} color={aggregateDetailColor}>
|
|
638
|
-
{renderDeltaText(fitResultLine(line || ' ', columns))}
|
|
639
|
-
</Text>
|
|
640
|
-
))}
|
|
641
|
-
</Box>
|
|
642
|
-
</Box>
|
|
643
|
-
) : null}
|
|
772
|
+
<ResultBody
|
|
773
|
+
lines={detailLines}
|
|
774
|
+
rawText={rawRt || ''}
|
|
775
|
+
columns={columns}
|
|
776
|
+
color={aggregateDetailColor}
|
|
777
|
+
raw={showRawAggregate}
|
|
778
|
+
/>
|
|
644
779
|
</Box>
|
|
645
780
|
);
|
|
646
781
|
}
|
|
647
782
|
|
|
648
783
|
// ── Normal (non-aggregate) tool card ────────────────────────────
|
|
649
784
|
const { label, summary, normalizedName, args: parsedArgs } = formatToolSurface(name, args);
|
|
650
|
-
const
|
|
651
|
-
|
|
785
|
+
const isShellSurface = isShellTool(normalizedName, label);
|
|
786
|
+
const isSkillSurface = SKILL_SURFACE_NAMES.has(String(normalizedName || '').toLowerCase());
|
|
787
|
+
const backgroundMeta = !pending && isBackgroundTaskTool(normalizedName)
|
|
788
|
+
? resolveBackgroundTaskMeta(parsedArgs, rt || '')
|
|
652
789
|
: null;
|
|
790
|
+
const backgroundError = backgroundMeta?.error || parsedArgs?.error || '';
|
|
791
|
+
const errorOnlyResult = Boolean(rt) && isBackgroundErrorOnlyBody(rt, backgroundError);
|
|
653
792
|
const backgroundResultText = backgroundMeta?.hasResponse ? backgroundMeta.body : '';
|
|
654
|
-
const displayedResultText = backgroundResultText || rt;
|
|
793
|
+
const displayedResultText = backgroundResultText || (errorOnlyResult ? '' : (rt || ''));
|
|
794
|
+
const hasDisplayResult = Boolean(String(displayedResultText || '').trim());
|
|
655
795
|
const lines = displayedResultText ? displayedResultText.split('\n') : [];
|
|
656
796
|
const totalLines = lines.length;
|
|
657
797
|
// Semantic one-line summary derived purely from name/args/result text.
|
|
658
798
|
// Shown in the collapsed, non-error view in place of the raw result block.
|
|
659
799
|
// Grouped cards ("Searched N files" / "Read N files") get the same treatment
|
|
660
800
|
// as single calls: a one-line semantic summary stands in for the raw block.
|
|
661
|
-
const resultSummary = !pending &&
|
|
801
|
+
const resultSummary = !pending && hasDisplayResult
|
|
662
802
|
? surfaceSummarizeToolResult(name, args, displayedResultText, isError)
|
|
663
803
|
: null;
|
|
664
804
|
// Same fit budget fitResultLine() uses, to detect a line that will be clipped.
|
|
665
805
|
const maxResultChars = Math.max(MIN_RESULT_LINE_CHARS, Number(columns || 80) - 7);
|
|
666
806
|
const resultColor = theme.text;
|
|
667
|
-
const firstResultLine =
|
|
668
|
-
const firstResultLineClipped =
|
|
669
|
-
const hasHiddenDetail = !pending &&
|
|
807
|
+
const firstResultLine = hasDisplayResult ? String(lines[0] ?? '') : '';
|
|
808
|
+
const firstResultLineClipped = hasDisplayResult && stringWidth(firstResultLine) > maxResultChars;
|
|
809
|
+
const hasHiddenDetail = !pending && hasDisplayResult && (totalLines > 1 || firstResultLineClipped || Boolean(resultSummary));
|
|
810
|
+
const shellStatus = isShellSurface ? shellDisplayStatus({ pending, failedCount, isError, result: displayedResultText }) : '';
|
|
811
|
+
const shellElapsed = isShellSurface ? (shellResultElapsed(displayedResultText) || elapsed) : '';
|
|
812
|
+
const shellStatusDetail = isShellSurface ? shellDetail(shellStatus, shellElapsed) : '';
|
|
813
|
+
const backgroundElapsed = backgroundMeta
|
|
814
|
+
? backgroundTaskElapsed(backgroundMeta, elapsed)
|
|
815
|
+
: (isBackgroundTaskTool(normalizedName) ? backgroundTaskElapsed(parsedArgs, elapsed) : '');
|
|
670
816
|
|
|
671
817
|
const toolArgPath = parsedArgs?.path ?? parsedArgs?.file_path ?? parsedArgs?.file ?? '';
|
|
672
818
|
const imageDetail = normalizedName === 'view_image' && toolArgPath ? String(toolArgPath) : '';
|
|
673
|
-
const
|
|
674
|
-
|
|
819
|
+
const isBackgroundResult = !pending && isBackgroundTaskTool(normalizedName) && Boolean(backgroundMeta);
|
|
820
|
+
const isBackgroundResponse = isBackgroundResult && (backgroundMeta?.hasResponse || isBackgroundTaskResponseArgs(normalizedName, parsedArgs));
|
|
821
|
+
const isBackgroundMetadataResult = isBackgroundResult && !isBackgroundResponse && Boolean(backgroundMeta);
|
|
822
|
+
const backgroundMetadataFailureLabel = isBackgroundMetadataResult
|
|
823
|
+
? backgroundTaskFailureDetail(backgroundMeta, parsedArgs)
|
|
675
824
|
: '';
|
|
676
|
-
const
|
|
677
|
-
?
|
|
825
|
+
const backgroundMetadataHeaderFailure = Boolean(backgroundMetadataFailureLabel) && !hasDisplayResult
|
|
826
|
+
? backgroundMetadataFailureLabel
|
|
678
827
|
: '';
|
|
679
|
-
const
|
|
680
|
-
?
|
|
828
|
+
const agentHeaderFailure = !pending && isAgentTool(normalizedName) && isError && parsedArgs?.error && !hasDisplayResult
|
|
829
|
+
? backgroundTaskFailureStatusLabel(parsedArgs?.status, parsedArgs?.error, { surface: 'agent' })
|
|
681
830
|
: '';
|
|
682
|
-
const
|
|
831
|
+
const headerFailureStatus = backgroundMetadataHeaderFailure || agentHeaderFailure || '';
|
|
832
|
+
const agentCompletionDetail = !pending && isAgentTool(normalizedName) && !agentHeaderFailure
|
|
833
|
+
? agentTerminalDetail(parsedArgs?.status, isError, elapsed, parsedArgs?.error)
|
|
834
|
+
: '';
|
|
835
|
+
const agentDetail = !pending && isAgentTool(normalizedName) && !hasDisplayResult
|
|
836
|
+
? agentCompletionDetail
|
|
837
|
+
: '';
|
|
838
|
+
const genericDetail = !pending && !isShellSurface && !agentDetail && !imageDetail && !resultSummary
|
|
683
839
|
? genericCompletedDetail({ normalizedName, label, hasResult, firstResultLine, isError })
|
|
684
840
|
: '';
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const backgroundMetadataDetail = isBackgroundMetadataResult
|
|
841
|
+
const terminalStatus = pending
|
|
842
|
+
? 'running'
|
|
843
|
+
: (shellStatus || normalizeTerminalStatus(backgroundMeta?.status) || normalizeTerminalStatus(parsedArgs?.status) || resultTerminalStatus(displayedResultText) || (isError || failedCount > 0 ? 'failed' : 'completed'));
|
|
844
|
+
const backgroundMetadataDetail = isBackgroundMetadataResult && !backgroundMetadataHeaderFailure
|
|
845
|
+
? backgroundTaskDetail(backgroundMeta, backgroundElapsed, parsedArgs)
|
|
846
|
+
: '';
|
|
847
|
+
const backgroundResponseDetail = isBackgroundResponse && resultSummary
|
|
848
|
+
? prefixElapsed(resultSummary, backgroundElapsed)
|
|
849
|
+
: resultSummary;
|
|
850
|
+
const syncElapsedDetail = !isBackgroundResponse && shouldPrefixSyncElapsed(normalizedName, label)
|
|
851
|
+
? prefixElapsed(backgroundResponseDetail, elapsed)
|
|
852
|
+
: backgroundResponseDetail;
|
|
853
|
+
const nonShellDetail = backgroundMetadataDetail || (/^(Cancelled|Failed|Finished)$/i.test(resultSummary || '') && agentCompletionDetail
|
|
854
|
+
? agentCompletionDetail
|
|
855
|
+
: syncElapsedDetail) || agentDetail || imageDetail || genericDetail;
|
|
856
|
+
// A pending non-aggregate tool used to drop its detail row entirely
|
|
857
|
+
// (collapsedDetail = ''), so the card rendered as a single header row. But
|
|
858
|
+
// estimateTranscriptItemRows() in App.jsx reserves 2 rows for a collapsed
|
|
859
|
+
// non-aggregate tool (1 only for skill surfaces). That left a 1-row gap that
|
|
860
|
+
// closed the instant the result landed — the surviving "튐". Reserve the same
|
|
861
|
+
// dim placeholder detail row the aggregate card uses (`Running`) for the whole
|
|
862
|
+
// pending lifecycle so the height stays fixed at header + one detail row and
|
|
863
|
+
// the final summary just fills in place. Skill surfaces collapse to a single
|
|
864
|
+
// row in BOTH the estimate and the render (visibleDetailLines drops the row
|
|
865
|
+
// for isSkillSurface below), so they get no placeholder.
|
|
866
|
+
const pendingDetailPlaceholder = pending && !isSkillSurface ? 'Running' : '';
|
|
867
|
+
const shellCollapsedSummary = isShellSurface && !pending && hasDisplayResult
|
|
868
|
+
? (resultSummary || truncateToWidth(firstResultLine, Math.min(120, maxResultChars)))
|
|
869
|
+
: resultSummary;
|
|
689
870
|
const collapsedDetail = pending
|
|
690
|
-
?
|
|
691
|
-
:
|
|
692
|
-
?
|
|
693
|
-
:
|
|
694
|
-
const
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
871
|
+
? pendingDetailPlaceholder
|
|
872
|
+
: isShellSurface
|
|
873
|
+
? mergeTerminalDetail(shellStatus, shellCollapsedSummary)
|
|
874
|
+
: mergeTerminalDetail(terminalStatus, nonShellDetail);
|
|
875
|
+
const backgroundMetadataExpandable = isBackgroundMetadataResult && hasRawResult && !pending;
|
|
876
|
+
const showRawResult = expanded && (hasDisplayResult || hasRawResult)
|
|
877
|
+
&& (!isBackgroundMetadataResult || hasRawResult);
|
|
878
|
+
const detailLines = showRawResult
|
|
879
|
+
? (hasDisplayResult ? lines : (rawRt ? rawRt.split('\n') : []))
|
|
880
|
+
: (collapsedDetail ? [collapsedDetail] : []);
|
|
881
|
+
const isPendingPlaceholderDetail = !showRawResult && Boolean(pendingDetailPlaceholder);
|
|
882
|
+
const detailColor = isPendingPlaceholderDetail ? theme.subtle : theme.text;
|
|
883
|
+
|
|
884
|
+
const isAgentResult = !isBackgroundResult && !pending && isAgentTool(normalizedName) && hasDisplayResult;
|
|
700
885
|
const isAgentResponse = isAgentResult && hasAgentResponseResult(rt);
|
|
701
|
-
const
|
|
702
|
-
const
|
|
703
|
-
|
|
886
|
+
const isAgentSurfaceCard = isAgentTool(normalizedName);
|
|
887
|
+
const agentSurfaceBriefRaw = isAgentSurfaceCard && !showRawResult
|
|
888
|
+
? summarizeAgentSurfaceBrief(name, parsedArgs, displayedResultText || '', { isError, isResponse: isAgentResponse })
|
|
889
|
+
: '';
|
|
890
|
+
const agentSurfaceBrief = agentSurfaceBriefRaw
|
|
891
|
+
? truncateToWidth(agentSurfaceBriefRaw, Math.min(AGENT_SURFACE_BRIEF_MAX, maxResultChars))
|
|
892
|
+
: '';
|
|
893
|
+
// Skill loads carry the skill name in the header already
|
|
894
|
+
// ("Loaded 1 skill (name)"); the collapsed detail row just repeats it, so
|
|
895
|
+
// drop it and keep the card a single line. Expanding (ctrl+o) still shows the
|
|
896
|
+
// full skill body via the raw-result path.
|
|
897
|
+
// Agent spawn/send/response cards show a tight brief under the ⎿ gutter when
|
|
898
|
+
// collapsed; ctrl+o expand still surfaces the full body.
|
|
899
|
+
let visibleDetailLines = detailLines;
|
|
900
|
+
if (isSkillSurface && !showRawResult) {
|
|
901
|
+
visibleDetailLines = [];
|
|
902
|
+
} else if (isBackgroundMetadataResult && backgroundMetadataHeaderFailure && !showRawResult) {
|
|
903
|
+
visibleDetailLines = [];
|
|
904
|
+
} else if (isAgentSurfaceCard && !showRawResult) {
|
|
905
|
+
const agentDetailFallback = collapsedDetail
|
|
906
|
+
|| (pending ? (pendingDetailPlaceholder || 'Running') : 'Finished');
|
|
907
|
+
const agentDetailLine = agentSurfaceBrief
|
|
908
|
+
|| truncateToWidth(String(agentDetailFallback), Math.min(AGENT_SURFACE_BRIEF_MAX, maxResultChars));
|
|
909
|
+
visibleDetailLines = agentHeaderFailure && !agentSurfaceBrief ? [] : [agentDetailLine];
|
|
910
|
+
}
|
|
911
|
+
const finalStatusColor = toolStatusColor({ pending, groupCount, failedCount, terminalStatus });
|
|
912
|
+
const dotColor = finalStatusColor;
|
|
913
|
+
const dotText = pending && !blinkExpired && !blinkOn ? ' ' : TURN_MARKER;
|
|
704
914
|
let labelText;
|
|
705
915
|
if (isAgentResponse) labelText = agentResponseTitle(parsedArgs);
|
|
706
916
|
else if (isBackgroundResponse) labelText = backgroundTaskResultTitle(normalizedName, backgroundMeta || parsedArgs);
|
|
707
917
|
else if (isBackgroundMetadataResult) labelText = backgroundTaskActionTitle(normalizedName, backgroundMeta);
|
|
708
|
-
else
|
|
918
|
+
else if (isShellSurface) labelText = shellHeader(shellStatus, displayGroupCount);
|
|
919
|
+
else labelText = (isAgentTool(normalizedName) ? agentActionTitle(parsedArgs) : '') || statusCopy(name, label, displayGroupCount, doneCount, headerPending, isError, parsedArgs);
|
|
709
920
|
// Show the parenthesized arg summary for grouped cards too, matching single
|
|
710
921
|
// calls so the header carries the same context.
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
922
|
+
const toolSearchSummary = !pending && normalizedName === 'tool_search' && hasResult
|
|
923
|
+
? toolSearchLoadedSummary(displayedResultText)
|
|
924
|
+
: '';
|
|
925
|
+
const summaryText = isAgentResponse || isBackgroundResponse
|
|
926
|
+
? ''
|
|
927
|
+
: toolSearchSummary || (isAgentTool(normalizedName) ? agentActionSummary(parsedArgs, summary) : summary);
|
|
928
|
+
// Agent cards hide their collapsed body but still expose ctrl+o expand only
|
|
929
|
+
// when expanding would actually reveal something: an agent response body, or a
|
|
930
|
+
// multiline / clipped raw result (e.g. the "agents: N …" worker list). A
|
|
931
|
+
// status-only single-line metadata result has nothing extra to show, so it
|
|
932
|
+
// gets no hint.
|
|
933
|
+
const agentHasExpandableBody = isAgentSurfaceCard && !pending && hasResult
|
|
934
|
+
&& (isAgentResponse || totalLines > 1 || firstResultLineClipped);
|
|
935
|
+
// Agent cards gate the hint solely on agentHasExpandableBody — never on
|
|
936
|
+
// hasHiddenDetail, which goes true for any single-line resultSummary and would
|
|
937
|
+
// wrongly show ctrl+o on a status-only one-liner that has nothing to expand.
|
|
938
|
+
const shellHasExpandableBody = isShellSurface && !pending && hasDisplayResult
|
|
939
|
+
&& (totalLines > 1 || firstResultLineClipped || Boolean(shellCollapsedSummary && shellCollapsedSummary !== firstResultLine));
|
|
940
|
+
const showHeaderExpandHint = (isShellSurface ? shellHasExpandableBody : (isAgentSurfaceCard ? agentHasExpandableBody : (hasHiddenDetail || backgroundMetadataExpandable)))
|
|
941
|
+
&& normalizedName !== 'tool_search';
|
|
942
|
+
const expandHintColor = theme.subtle;
|
|
714
943
|
|
|
715
944
|
// Build a single-line header that never wraps: reserve width for the fixed
|
|
716
945
|
// trailing expand hint plus the dot gutter and a 1-col Windows last-column
|
|
@@ -720,10 +949,25 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
720
949
|
const gutter = 2;
|
|
721
950
|
const hintLabel = showHeaderExpandHint ? `ctrl+o ${expanded ? 'collapse' : 'expand'}` : '';
|
|
722
951
|
const hintText = hintLabel ? ` ${BULLET_OPERATOR} ${hintLabel}` : '';
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
952
|
+
const headerMetaText = pending && elapsed ? ` (${elapsed} elapsed)` : '';
|
|
953
|
+
// The expand hint (post-completion) and the elapsed meta (pending) occupy the
|
|
954
|
+
// SAME trailing region but never at the same time. Subtracting both widths
|
|
955
|
+
// made `avail` shrink/grow across the pending→completed transition, so the
|
|
956
|
+
// label/summary clip point shifted and the header text "jumped out then
|
|
957
|
+
// snapped back" right as a tool finished or cards merged. Reserve the wider of
|
|
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.
|
|
961
|
+
const hintReserveLabel = `ctrl+o ${expanded ? 'collapse' : 'expand'}`;
|
|
962
|
+
const hintReserveText = ` ${BULLET_OPERATOR} ${hintReserveLabel}`;
|
|
963
|
+
const headerFailureText = headerFailureStatus
|
|
964
|
+
? truncateToWidth(headerFailureStatus, HEADER_FAILURE_STATUS_MAX)
|
|
965
|
+
: '';
|
|
966
|
+
const inlineFailureText = headerFailureText ? ` ${BULLET_OPERATOR} ${headerFailureText}` : '';
|
|
967
|
+
const rightReserve = Math.max(stringWidth(hintReserveText), stringWidth(headerMetaText)) + stringWidth(inlineFailureText);
|
|
968
|
+
const avail = Math.max(1, (Number(columns) || 80) - 1 - gutter - rightReserve);
|
|
969
|
+
const trailingText = headerMetaText || (showHeaderExpandHint ? hintText : '');
|
|
970
|
+
const trailingColor = headerMetaText ? theme.subtle : expandHintColor;
|
|
727
971
|
let labelOut;
|
|
728
972
|
let summaryOut;
|
|
729
973
|
if (stringWidth(labelText) >= avail) {
|
|
@@ -740,33 +984,38 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
740
984
|
: '';
|
|
741
985
|
summaryOut = truncatedSummary ? ` (${truncatedSummary})` : '';
|
|
742
986
|
}
|
|
987
|
+
// Keep trailing content (pending elapsed / completed ctrl+o hint) attached
|
|
988
|
+
// directly after the body for the whole lifecycle. The fixed-column pin
|
|
989
|
+
// previously used for elapsed is what made the trailing text jump to the right
|
|
990
|
+
// edge and snap back on the pending→done flip, so there is no pad. `avail`
|
|
991
|
+
// stays reserved (rightReserve) so the body clip point never reflows.
|
|
743
992
|
return (
|
|
744
993
|
<Box flexDirection="column" marginTop={attached ? 0 : 1}>
|
|
745
|
-
<Box flexDirection="row">
|
|
746
|
-
<Box flexShrink={
|
|
747
|
-
<
|
|
994
|
+
<Box flexDirection="row" width="100%">
|
|
995
|
+
<Box flexShrink={1} flexGrow={1} overflow="hidden" minWidth={0}>
|
|
996
|
+
<Box flexDirection="row">
|
|
997
|
+
<Box flexShrink={0} minWidth={2}>
|
|
998
|
+
<Text color={dotColor}>{dotText}</Text>
|
|
999
|
+
</Box>
|
|
1000
|
+
<Text wrap="truncate">
|
|
1001
|
+
<Text bold color={theme.text}>{labelOut}</Text>
|
|
1002
|
+
{summaryOut ? <Text color={theme.text}>{summaryOut}</Text> : null}
|
|
1003
|
+
{inlineFailureText ? <Text color={theme.error}>{inlineFailureText}</Text> : null}
|
|
1004
|
+
{trailingText ? <Text color={trailingColor}>{trailingText}</Text> : null}
|
|
1005
|
+
</Text>
|
|
1006
|
+
</Box>
|
|
748
1007
|
</Box>
|
|
749
|
-
<Text wrap="truncate">
|
|
750
|
-
<Text bold color={theme.text}>{labelOut}</Text>
|
|
751
|
-
{summaryOut ? <Text color={theme.text}>{summaryOut}</Text> : null}
|
|
752
|
-
{showHeaderExpandHint ? <Text color={expandHintColor}>{hintText}</Text> : null}
|
|
753
|
-
</Text>
|
|
754
1008
|
</Box>
|
|
755
1009
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
</Box>
|
|
769
|
-
) : null}
|
|
770
|
-
</Box>
|
|
771
|
-
);
|
|
772
|
-
}
|
|
1010
|
+
<ResultBody
|
|
1011
|
+
lines={visibleDetailLines}
|
|
1012
|
+
rawText={hasDisplayResult ? displayedResultText : (rawRt || '')}
|
|
1013
|
+
pathArg={toolArgPath}
|
|
1014
|
+
isShell={isShellSurface}
|
|
1015
|
+
columns={columns}
|
|
1016
|
+
color={showRawResult ? resultColor : detailColor}
|
|
1017
|
+
raw={showRawResult}
|
|
1018
|
+
/>
|
|
1019
|
+
</Box>
|
|
1020
|
+
);
|
|
1021
|
+
}
|