mixdog 0.8.1 → 0.9.1
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 +34 -28
- 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 +205 -33
- 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 +1222 -90
- 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 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -446
- 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 +108 -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 +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -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 +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- 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 +289 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- 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 +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- 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 +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- 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.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 +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- 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 +519 -118
- 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/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
- 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 +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -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 +104 -473
- 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 +44 -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 +139 -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 -95
- 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 +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- 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 +428 -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 +597 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- 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 +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -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 +756 -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 +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
- 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/ink.js +54 -8
- 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/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/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/channels/lib/hook-pipe-server.mjs +0 -671
- 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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Pinned into the transcript right after a turn's output (an `item.kind ===
|
|
8
8
|
* 'turndone'` entry), so it scrolls up with the answer and stays in the
|
|
9
|
-
* scrollback
|
|
9
|
+
* scrollback as a post-think summary line. It leaves a
|
|
10
10
|
* quiet, dim record of how long the turn took next to the answer it belongs to.
|
|
11
11
|
*/
|
|
12
12
|
import React from 'react';
|
|
@@ -45,7 +45,7 @@ export function TurnDone({ elapsedMs = 0, status = 'done', verb = 'Thought', rig
|
|
|
45
45
|
</Text>
|
|
46
46
|
</Box>
|
|
47
47
|
{rightText ? (
|
|
48
|
-
<Box flexShrink={0} width={rightWidth} marginLeft={1} justifyContent="flex-end" overflow="hidden">
|
|
48
|
+
<Box flexShrink={0} width={rightWidth} marginLeft={1} marginRight={1} justifyContent="flex-end" overflow="hidden">
|
|
49
49
|
<Text color={statusMessageColor(rightTone)} wrap="truncate">{rightText}</Text>
|
|
50
50
|
</Box>
|
|
51
51
|
) : null}
|
|
@@ -69,7 +69,7 @@ export function StatusDone({ label = 'Complete', detail = '', rightMessage = '',
|
|
|
69
69
|
</Text>
|
|
70
70
|
</Box>
|
|
71
71
|
{rightText ? (
|
|
72
|
-
<Box flexShrink={0} width={rightWidth} marginLeft={1} justifyContent="flex-end" overflow="hidden">
|
|
72
|
+
<Box flexShrink={0} width={rightWidth} marginLeft={1} marginRight={1} justifyContent="flex-end" overflow="hidden">
|
|
73
73
|
<Text color={statusMessageColor(rightTone)} wrap="truncate">{rightText}</Text>
|
|
74
74
|
</Box>
|
|
75
75
|
) : null}
|
|
@@ -148,10 +148,11 @@ function windowSegmentParts(w) {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
function creditSegmentParts(value) {
|
|
151
|
+
const creditColor = Number(value) === 0 ? theme.text : remainingUsdColor(value);
|
|
151
152
|
return [
|
|
152
153
|
{ text: CREDIT_LABEL, color: theme.subtle },
|
|
153
154
|
{ text: ' ', color: theme.inactive },
|
|
154
|
-
{ text: money(value), color:
|
|
155
|
+
{ text: money(value), color: creditColor },
|
|
155
156
|
];
|
|
156
157
|
}
|
|
157
158
|
|
|
@@ -185,13 +186,10 @@ function rowStatusParts(row, columns = 80, statusWidth = 0) {
|
|
|
185
186
|
case 'checking':
|
|
186
187
|
return [{ text: 'checking...', color: theme.inactive }];
|
|
187
188
|
case 'hidden':
|
|
188
|
-
return row?.detail ? [{ text: row.detail, color: theme.inactive }] : [];
|
|
189
189
|
case 'missing':
|
|
190
|
-
return row?.primary && row.primary !== 'not configured' ? [{ text: row.primary, color: theme.inactive }] : [];
|
|
191
190
|
case 'local':
|
|
192
|
-
return row?.primary ? [{ text: row.primary, color: theme.inactive }] : [];
|
|
193
191
|
case 'error':
|
|
194
|
-
return
|
|
192
|
+
return [];
|
|
195
193
|
default:
|
|
196
194
|
return row?.primary ? [{ text: row.primary, color: theme.text }] : [];
|
|
197
195
|
}
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* components/tool-output-format.mjs — shared post-processing for EXPANDED tool
|
|
3
|
+
* result bodies (ctrl+o). Mirrors the Claude Code output pipeline so every tool
|
|
4
|
+
* surface renders the same way instead of a flat run of plain lines:
|
|
5
|
+
*
|
|
6
|
+
* - language inference from the read/grep path argument (file extension)
|
|
7
|
+
* - JSON auto pretty-print (precision-safe, size-capped)
|
|
8
|
+
* - URL → OSC 8 hyperlink
|
|
9
|
+
* - stray underline-ANSI removal (shell output leaks these)
|
|
10
|
+
* - oversized-output guard (never wrap/scan a 64 MB dump line-by-line)
|
|
11
|
+
* - per-line render: split the `<n>→` / `<file>:<n>:` line-number gutter into a
|
|
12
|
+
* dim column, then syntax-highlight the body with the shared markdown
|
|
13
|
+
* code-block highlighter so colors track the active theme.
|
|
14
|
+
*
|
|
15
|
+
* This module returns ANSI STRINGS (one per logical line). ToolExecution
|
|
16
|
+
* runs `wrapExpandedResultLines` so each physical terminal row maps 1:1 to the
|
|
17
|
+
* left result rail before ink draws the body (no ink `wrap` on expanded rows).
|
|
18
|
+
*/
|
|
19
|
+
import stringWidth from 'string-width';
|
|
20
|
+
import stripAnsi from 'strip-ansi';
|
|
21
|
+
import {
|
|
22
|
+
extraColorizers,
|
|
23
|
+
highlightCodeLine,
|
|
24
|
+
highlightCodeBlockToLines,
|
|
25
|
+
colorizeDiffLine,
|
|
26
|
+
looksLikeUnifiedDiff,
|
|
27
|
+
LANG_FAMILY,
|
|
28
|
+
} from '../markdown/format-token.mjs';
|
|
29
|
+
import { wrapText } from '../markdown/table-layout.mjs';
|
|
30
|
+
import { RESULT_GUTTER } from '../theme.mjs';
|
|
31
|
+
import { hasMarkdownSyntax, renderTokenAnsiSegments } from '../markdown/render-ansi.mjs';
|
|
32
|
+
import { buildTableRender } from '../markdown/table-layout.mjs';
|
|
33
|
+
|
|
34
|
+
const DEFAULT_MARKDOWN_WIDTH = 80;
|
|
35
|
+
|
|
36
|
+
// Hard ceilings so a pathological tool result can never lock the render loop.
|
|
37
|
+
// CC uses ~MAX_LINES*width*4 for the collapsed fold; for the EXPANDED body we
|
|
38
|
+
// cap total characters processed and total lines kept, with an explicit marker.
|
|
39
|
+
export const MAX_EXPANDED_CHARS = 256 * 1024; // 256 KB of text gets per-line processing
|
|
40
|
+
const MAX_EXPANDED_LINES = 4000; // logical-line safety ceiling; physical mount cap is separate
|
|
41
|
+
const MAX_JSON_FORMAT_LENGTH = 10_000; // mirror CC's tryJsonFormatContent cap
|
|
42
|
+
const MAX_HIGHLIGHT_LINE_CHARS = 2000; // skip token-scan on absurdly long lines
|
|
43
|
+
// Lockstep with ToolExecution collapsed fit budget (MIN_RESULT_LINE_CHARS).
|
|
44
|
+
const MIN_EXPANDED_BODY_COLS = 24;
|
|
45
|
+
|
|
46
|
+
// Hard cap on the number of PHYSICAL (wrapped) rows a single expanded tool /
|
|
47
|
+
// script body may mount. MAX_EXPANDED_LINES bounds LOGICAL lines, but a single
|
|
48
|
+
// very long line with no newlines (minified JSON, a 256 KB single-line blob)
|
|
49
|
+
// wraps into thousands of physical rows — and EACH physical row is a mounted
|
|
50
|
+
// Ink <Text> node that ink re-serializes every frame even while clipped off-
|
|
51
|
+
// screen (clipping only trims write coords, not the serialize pass). So a lone
|
|
52
|
+
// huge expanded item could mount thousands of nodes and stall typing/drag even
|
|
53
|
+
// though it is the only visible transcript item. This cap bounds the mounted
|
|
54
|
+
// node count regardless of line shape; the omitted tail gets a clear marker.
|
|
55
|
+
// It is set generously above what any viewport needs so normal multi-line
|
|
56
|
+
// output (already capped at MAX_EXPANDED_LINES logical lines) is byte-for-byte
|
|
57
|
+
// unchanged — only pathological wide/unwrapped blobs are trimmed. Env-tunable
|
|
58
|
+
// via MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES (0 disables); legacy
|
|
59
|
+
// MIXDOG_TUI_EXPANDED_MAX_ROWS is still honored when the primary var is unset.
|
|
60
|
+
export function resolveToolOutputMaxRenderLines() {
|
|
61
|
+
const raw = process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES;
|
|
62
|
+
if (raw !== undefined && String(raw).trim() !== '') {
|
|
63
|
+
const v = Number(raw);
|
|
64
|
+
if (Number.isFinite(v) && v <= 0) return 0;
|
|
65
|
+
if (Number.isFinite(v) && v > 0) return Math.floor(v);
|
|
66
|
+
}
|
|
67
|
+
const legacy = process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS;
|
|
68
|
+
if (legacy !== undefined && String(legacy).trim() !== '') {
|
|
69
|
+
const v = Number(legacy);
|
|
70
|
+
if (Number.isFinite(v) && v > 0) return Math.floor(v);
|
|
71
|
+
}
|
|
72
|
+
return 600;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function omittedPhysicalRowsMarker(omitted, isShell) {
|
|
76
|
+
if (isShell) {
|
|
77
|
+
const n = Math.max(1, Math.floor(Number(omitted) || 0));
|
|
78
|
+
return `\u2026 [${n} line${n === 1 ? '' : 's'} omitted above \u2014 showing newest output below]`;
|
|
79
|
+
}
|
|
80
|
+
return '\u2026 [output truncated for display \u2014 collapse (ctrl+o) or re-read a narrower range]';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function shellLogicalOmittedMarker({ omittedLines = 0, omittedChars = false }, c) {
|
|
84
|
+
if (omittedLines > 0) {
|
|
85
|
+
return c.synComment(omittedPhysicalRowsMarker(omittedLines, true));
|
|
86
|
+
}
|
|
87
|
+
if (omittedChars) {
|
|
88
|
+
return c.synComment('\u2026 [earlier output omitted above \u2014 showing newest output below]');
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function finalizeShellPhysicalCap(buffer, omitted, maxRows) {
|
|
94
|
+
const bodySlots = Math.max(0, maxRows - 1);
|
|
95
|
+
if (omitted <= 0) {
|
|
96
|
+
return buffer.length > 0 ? buffer : [' '];
|
|
97
|
+
}
|
|
98
|
+
const tail = bodySlots > 0 ? buffer.slice(-bodySlots) : [];
|
|
99
|
+
const extraHidden = Math.max(0, buffer.length - bodySlots);
|
|
100
|
+
const totalOmitted = omitted + extraHidden;
|
|
101
|
+
const out = [omittedPhysicalRowsMarker(totalOmitted, true), ...tail];
|
|
102
|
+
return out.length > 0 ? out.slice(0, maxRows) : [' '];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// `<n>→<content>` (read) OR `<n>:<content>` / `<path>:<n>:<content>` (grep).
|
|
106
|
+
const READ_LINE_RE = /^(\s*)(\d+)(\u2192)(.*)$/;
|
|
107
|
+
// Gutter ends with `:<line>:`. Windows absolute paths use `X:\...` so the drive
|
|
108
|
+
// colon must not terminate the path prefix (unlike the old `[^:\n]*:` rule).
|
|
109
|
+
const GREP_LINE_RE = /^(\s*)((?:[A-Za-z]:[\\/](?:[^\n:])*|[^\n:]*):\d+:|\d+:)(\s?)(.*)$/;
|
|
110
|
+
|
|
111
|
+
// http(s) URLs not wrapped in quotes/brackets/whitespace (conservative).
|
|
112
|
+
const URL_RE = /https?:\/\/[^\s"'<>\x1b\\)\]]+/g;
|
|
113
|
+
// CSI SGR and OSC sequences (BEL- or ST-terminated) — linkify plain text only.
|
|
114
|
+
// eslint-disable-next-line no-control-regex
|
|
115
|
+
const ANSI_ESCAPE_RE = /\x1b(?:\[[0-9;]*m|\][\s\S]*?(?:\x07|\x1b\\))/g;
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
/** Infer a highlighter family from a read/grep path arg's extension. */
|
|
119
|
+
export function inferLangFamily(pathArg) {
|
|
120
|
+
const p = String(pathArg || '').trim().toLowerCase();
|
|
121
|
+
if (!p) return null;
|
|
122
|
+
const m = /\.([a-z0-9]+)$/.exec(p);
|
|
123
|
+
if (!m) return null;
|
|
124
|
+
return LANG_FAMILY[m[1]] || null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Strip ONLY underline SGR (4 / 24) — other styles/colors are preserved. */
|
|
128
|
+
export function stripUnderlineAnsi(text) {
|
|
129
|
+
return String(text ?? '').replace(
|
|
130
|
+
// eslint-disable-next-line no-control-regex
|
|
131
|
+
/\x1b\[([0-9;]*)m/g,
|
|
132
|
+
(seq, params) => {
|
|
133
|
+
if (!params) return seq;
|
|
134
|
+
const kept = params.split(';').filter((p) => p !== '' && p !== '4' && p !== '24');
|
|
135
|
+
if (kept.length === 0) return '';
|
|
136
|
+
return `\x1b[${kept.join(';')}m`;
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Wrap bare URLs in OSC 8 hyperlinks (terminals that ignore it show the URL). */
|
|
142
|
+
export function linkifyUrls(text) {
|
|
143
|
+
const src = String(text ?? '');
|
|
144
|
+
if (!/https?:\/\//.test(src)) return src;
|
|
145
|
+
const chunks = [];
|
|
146
|
+
let last = 0;
|
|
147
|
+
for (const match of src.matchAll(ANSI_ESCAPE_RE)) {
|
|
148
|
+
if (match.index > last) chunks.push(src.slice(last, match.index));
|
|
149
|
+
chunks.push(match[0]);
|
|
150
|
+
last = match.index + match[0].length;
|
|
151
|
+
}
|
|
152
|
+
if (last < src.length) chunks.push(src.slice(last));
|
|
153
|
+
return chunks
|
|
154
|
+
.map((part) => (part.startsWith('\x1b')
|
|
155
|
+
? part
|
|
156
|
+
: part.replace(URL_RE, (url) => `\x1b]8;;${url}\x07${url}\x1b]8;;\x07`)))
|
|
157
|
+
.join('');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Precision-safe JSON pretty-print for a single line; returns input on miss. */
|
|
161
|
+
function tryFormatJsonLine(line) {
|
|
162
|
+
const t = String(line ?? '').trim();
|
|
163
|
+
if (!t || (t[0] !== '{' && t[0] !== '[')) return line;
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(t);
|
|
166
|
+
if (parsed === null || typeof parsed !== 'object') return line;
|
|
167
|
+
const pretty = JSON.stringify(parsed, null, 2);
|
|
168
|
+
// Bail if a big-int lost precision on the round-trip.
|
|
169
|
+
const a = t.replace(/\\\//g, '/').replace(/\s+/g, '');
|
|
170
|
+
const b = JSON.stringify(parsed).replace(/\s+/g, '');
|
|
171
|
+
return a === b ? pretty : line;
|
|
172
|
+
} catch {
|
|
173
|
+
return line;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Auto pretty-print whole-result JSON (size-capped, per-line). */
|
|
178
|
+
export function tryFormatJson(text) {
|
|
179
|
+
const src = String(text ?? '');
|
|
180
|
+
if (src.length > MAX_JSON_FORMAT_LENGTH) return src;
|
|
181
|
+
return src.split('\n').map(tryFormatJsonLine).join('\n');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** True when the whole body is a JSON object/array (not markdown prose). */
|
|
185
|
+
function isJsonDocument(text) {
|
|
186
|
+
const t = String(text ?? '').trim();
|
|
187
|
+
if (!t || (t[0] !== '{' && t[0] !== '[')) return false;
|
|
188
|
+
try {
|
|
189
|
+
const parsed = JSON.parse(t);
|
|
190
|
+
return parsed !== null && typeof parsed === 'object';
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** True when text already carries SGR escapes (e.g. shell color output). */
|
|
197
|
+
function hasAnsi(text) {
|
|
198
|
+
return /\x1b\[/.test(String(text ?? ''));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** True when a line carries a read/grep tool line-number gutter. */
|
|
202
|
+
function lineHasToolGutter(line) {
|
|
203
|
+
return READ_LINE_RE.test(line) || GREP_LINE_RE.test(line);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Read/grep bodies are source lines — never whole-document markdown. */
|
|
207
|
+
function contentHasToolGutters(lines) {
|
|
208
|
+
for (const line of lines) {
|
|
209
|
+
if (lineHasToolGutter(line)) return true;
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Whole-result markdown render for prose-y tool output (headings, emphasis,
|
|
216
|
+
* fences). Skipped for shell/ANSI/diff, read/grep gutters, and non-md path
|
|
217
|
+
* language inference (those stay per-line syntax highlight).
|
|
218
|
+
*/
|
|
219
|
+
function shouldRenderExpandedMarkdown({ src, lines, carriesAnsi, isShell, diffMode, family }) {
|
|
220
|
+
if (carriesAnsi || isShell || diffMode) return false;
|
|
221
|
+
if (isJsonDocument(src)) return false;
|
|
222
|
+
if (contentHasToolGutters(lines)) return false;
|
|
223
|
+
if (family && family !== 'md') return false;
|
|
224
|
+
if (family === 'md') return true;
|
|
225
|
+
return hasMarkdownSyntax(src);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Markdown lexer → one ANSI string per terminal row (tables via table-layout). */
|
|
229
|
+
function formatExpandedMarkdownLines(src, { width = DEFAULT_MARKDOWN_WIDTH } = {}) {
|
|
230
|
+
const segments = renderTokenAnsiSegments(src, { width });
|
|
231
|
+
const out = [];
|
|
232
|
+
for (const seg of segments) {
|
|
233
|
+
if (seg.type === 'table') {
|
|
234
|
+
const { lines } = buildTableRender(seg.token, width);
|
|
235
|
+
for (const line of lines) out.push(linkifyUrls(line));
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (seg.type !== 'ansi' || !seg.ansi) continue;
|
|
239
|
+
for (const line of String(seg.ansi).split('\n')) {
|
|
240
|
+
out.push(linkifyUrls(line));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Process an expanded tool-result body into an array of ANSI line strings.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} text raw tool result
|
|
250
|
+
* @param {object} opts
|
|
251
|
+
* @param {string} [opts.pathArg] read/grep path for language inference
|
|
252
|
+
* @param {boolean} [opts.isShell] shell surface (preserve existing ANSI, no highlight)
|
|
253
|
+
* @returns {string[]} one ANSI string per visible line
|
|
254
|
+
*/
|
|
255
|
+
export function formatExpandedResult(text, { pathArg = '', isShell = false } = {}) {
|
|
256
|
+
let src = String(text ?? '');
|
|
257
|
+
if (!src) return [];
|
|
258
|
+
|
|
259
|
+
// Oversized guard: slice before any O(n) per-line work, append a marker.
|
|
260
|
+
let truncatedChars = false;
|
|
261
|
+
let shellOmittedLines = 0;
|
|
262
|
+
let shellOmittedChars = false;
|
|
263
|
+
if (src.length > MAX_EXPANDED_CHARS) {
|
|
264
|
+
src = isShell ? src.slice(-MAX_EXPANDED_CHARS) : src.slice(0, MAX_EXPANDED_CHARS);
|
|
265
|
+
if (isShell) shellOmittedChars = true;
|
|
266
|
+
else truncatedChars = true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const carriesAnsi = hasAnsi(src);
|
|
270
|
+
// JSON pretty only when the text is not already colored (don't reflow ANSI).
|
|
271
|
+
if (!carriesAnsi) src = tryFormatJson(src);
|
|
272
|
+
|
|
273
|
+
let lines = src.split('\n');
|
|
274
|
+
let truncatedLines = false;
|
|
275
|
+
if (lines.length > MAX_EXPANDED_LINES) {
|
|
276
|
+
if (isShell) shellOmittedLines = lines.length - MAX_EXPANDED_LINES;
|
|
277
|
+
lines = isShell ? lines.slice(-MAX_EXPANDED_LINES) : lines.slice(0, MAX_EXPANDED_LINES);
|
|
278
|
+
if (!isShell) truncatedLines = true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const c = extraColorizers();
|
|
282
|
+
// Diff bodies get diff coloring regardless of file family.
|
|
283
|
+
const diffMode = !carriesAnsi && !isShell && looksLikeUnifiedDiff(src);
|
|
284
|
+
const family = isShell || carriesAnsi ? null : inferLangFamily(pathArg);
|
|
285
|
+
|
|
286
|
+
let out;
|
|
287
|
+
if (shouldRenderExpandedMarkdown({ src, lines, carriesAnsi, isShell, diffMode, family })) {
|
|
288
|
+
out = formatExpandedMarkdownLines(src);
|
|
289
|
+
if (out.length > MAX_EXPANDED_LINES) {
|
|
290
|
+
if (isShell) shellOmittedLines += out.length - MAX_EXPANDED_LINES;
|
|
291
|
+
out = isShell ? out.slice(-MAX_EXPANDED_LINES) : out.slice(0, MAX_EXPANDED_LINES);
|
|
292
|
+
if (!isShell) truncatedLines = true;
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
if (family && family !== 'md' && !diffMode && !carriesAnsi && !isShell) {
|
|
296
|
+
out = formatSyntaxBlock(lines, { c, family });
|
|
297
|
+
} else {
|
|
298
|
+
out = lines.map((line) => formatLine(line, { c, family, diffMode, carriesAnsi, isShell }));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (isShell) {
|
|
303
|
+
const marker = shellLogicalOmittedMarker({ omittedLines: shellOmittedLines, omittedChars: shellOmittedChars }, c);
|
|
304
|
+
if (marker) return [marker, ...out];
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
if (truncatedChars || truncatedLines) {
|
|
308
|
+
out.push(c.synComment('… [output truncated for display — re-read a narrower range]'));
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Split read/grep line-number gutter from body (shared by per-line and block highlight). */
|
|
314
|
+
function splitGutter(line) {
|
|
315
|
+
let indent = '';
|
|
316
|
+
let gutter = '';
|
|
317
|
+
let body = line;
|
|
318
|
+
const rm = READ_LINE_RE.exec(line);
|
|
319
|
+
if (rm) {
|
|
320
|
+
indent = rm[1];
|
|
321
|
+
gutter = `${rm[2]}${rm[3]}`;
|
|
322
|
+
body = rm[4];
|
|
323
|
+
} else {
|
|
324
|
+
const gm = GREP_LINE_RE.exec(line);
|
|
325
|
+
if (gm) {
|
|
326
|
+
indent = gm[1];
|
|
327
|
+
gutter = `${gm[2]}${gm[3]}`;
|
|
328
|
+
body = gm[4];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { indent, gutter, body };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Block syntax highlight for non-diff code bodies (multi-line tokens color correctly). */
|
|
335
|
+
function formatSyntaxBlock(lines, { c, family }) {
|
|
336
|
+
const parts = lines.map(splitGutter);
|
|
337
|
+
const bodies = parts.map((p) =>
|
|
338
|
+
p.body.length > MAX_HIGHLIGHT_LINE_CHARS ? '' : p.body,
|
|
339
|
+
);
|
|
340
|
+
const highlighted = highlightCodeBlockToLines(bodies.join('\n'), family, c);
|
|
341
|
+
return parts.map((p, i) => {
|
|
342
|
+
const rawBody = p.body;
|
|
343
|
+
const bodyOut = rawBody.length > MAX_HIGHLIGHT_LINE_CHARS
|
|
344
|
+
? c.body(rawBody)
|
|
345
|
+
: (highlighted[i] ?? '');
|
|
346
|
+
const linked = linkifyUrls(bodyOut);
|
|
347
|
+
return p.gutter ? `${p.indent}${c.synComment(p.gutter)}${linked}` : linked;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Format ONE line: split line-number gutter, then highlight/linkify the body. */
|
|
352
|
+
function formatLine(line, { c, family, diffMode, carriesAnsi, isShell }) {
|
|
353
|
+
// Shell / already-colored output: keep ANSI verbatim, only fix underline leaks
|
|
354
|
+
// and linkify URLs. No gutter split (shell has no <n>→ prefix).
|
|
355
|
+
if (carriesAnsi || isShell) {
|
|
356
|
+
return linkifyUrls(stripUnderlineAnsi(line));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const { indent, gutter, body } = splitGutter(line);
|
|
360
|
+
|
|
361
|
+
const coloredBody = highlightBody(body, { c, family, diffMode });
|
|
362
|
+
const linked = linkifyUrls(coloredBody);
|
|
363
|
+
return gutter ? `${indent}${c.synComment(gutter)}${linked}` : linked;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Color a body string per the active mode (diff / language / plain). */
|
|
367
|
+
function highlightBody(body, { c, family, diffMode }) {
|
|
368
|
+
if (!body) return '';
|
|
369
|
+
if (body.length > MAX_HIGHLIGHT_LINE_CHARS) return c.body(body);
|
|
370
|
+
if (diffMode) return colorizeDiffLine(body, c);
|
|
371
|
+
if (family && family !== 'md') return highlightCodeLine(body, family, c);
|
|
372
|
+
return c.body(body);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Body text width for expanded tool results (terminal cols minus the ⎿ rail). */
|
|
376
|
+
export function expandedResultBodyWidth(columns = 80) {
|
|
377
|
+
const cols = Math.max(1, Number(columns) || 80);
|
|
378
|
+
const budget = cols - stringWidth(RESULT_GUTTER);
|
|
379
|
+
return Math.max(MIN_EXPANDED_BODY_COLS, budget);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function padDisplaySpaces(width) {
|
|
383
|
+
const w = Math.max(0, Math.floor(Number(width) || 0));
|
|
384
|
+
return w ? ' '.repeat(w) : '';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Split an ANSI string after `plainTarget` display columns of visible text. */
|
|
388
|
+
function splitAnsiByPlainWidth(text, plainTarget) {
|
|
389
|
+
const src = String(text ?? '');
|
|
390
|
+
const target = Math.max(0, Math.floor(Number(plainTarget) || 0));
|
|
391
|
+
if (!src) return ['', ''];
|
|
392
|
+
if (target <= 0) return ['', src];
|
|
393
|
+
let i = 0;
|
|
394
|
+
let plain = 0;
|
|
395
|
+
while (i < src.length) {
|
|
396
|
+
if (plain >= target) break;
|
|
397
|
+
if (src[i] === '\x1b') {
|
|
398
|
+
const rest = src.slice(i);
|
|
399
|
+
const sgr = rest.match(/^\x1b\[[0-9;]*m/);
|
|
400
|
+
if (sgr) {
|
|
401
|
+
i += sgr[0].length;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const osc = rest.match(/^\x1b\]8;;[^\x07]*\x07/);
|
|
405
|
+
if (osc) {
|
|
406
|
+
i += osc[0].length;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const cp = src.codePointAt(i);
|
|
411
|
+
const ch = String.fromCodePoint(cp);
|
|
412
|
+
plain += stringWidth(ch);
|
|
413
|
+
i += cp > 0xffff ? 2 : 1;
|
|
414
|
+
}
|
|
415
|
+
return [src.slice(0, i), src.slice(i)];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function leadingPrefixPlainWidth(plainLine) {
|
|
419
|
+
const rm = READ_LINE_RE.exec(plainLine);
|
|
420
|
+
if (rm) return stringWidth(rm[1] + rm[2] + rm[3]);
|
|
421
|
+
const gm = GREP_LINE_RE.exec(plainLine);
|
|
422
|
+
if (gm) return stringWidth(gm[1] + gm[2] + gm[3]);
|
|
423
|
+
return 0;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function wrapOneExpandedLogicalLine(line, maxWidth) {
|
|
427
|
+
const src = String(line ?? '');
|
|
428
|
+
if (!src) return [' '];
|
|
429
|
+
if (stringWidth(src) <= maxWidth) return [src];
|
|
430
|
+
|
|
431
|
+
const prefixPlainW = leadingPrefixPlainWidth(stripAnsi(src));
|
|
432
|
+
const [prefix, body] = prefixPlainW > 0
|
|
433
|
+
? splitAnsiByPlainWidth(src, prefixPlainW)
|
|
434
|
+
: ['', src];
|
|
435
|
+
const prefixW = stringWidth(prefix);
|
|
436
|
+
const bodyBudget = Math.max(1, maxWidth - prefixW);
|
|
437
|
+
const bodyPieces = wrapText(body, bodyBudget, { hard: true });
|
|
438
|
+
if (bodyPieces.length <= 1) return [src];
|
|
439
|
+
|
|
440
|
+
const out = [];
|
|
441
|
+
for (let i = 0; i < bodyPieces.length; i++) {
|
|
442
|
+
if (i === 0) out.push(`${prefix}${bodyPieces[i]}`);
|
|
443
|
+
else out.push(`${padDisplaySpaces(prefixW)}${bodyPieces[i]}`);
|
|
444
|
+
}
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Turn logical expanded lines into physical rows that fit the body column.
|
|
450
|
+
* One output row per left-rail row in ToolExecution (lockstep with App row est.).
|
|
451
|
+
*/
|
|
452
|
+
export function wrapExpandedResultLines(logicalLines, columns = 80, { isShell = false } = {}) {
|
|
453
|
+
const maxRows = resolveToolOutputMaxRenderLines();
|
|
454
|
+
const capOn = maxRows > 0;
|
|
455
|
+
const maxWidth = expandedResultBodyWidth(columns);
|
|
456
|
+
const lines = Array.isArray(logicalLines) ? logicalLines : [];
|
|
457
|
+
const out = [];
|
|
458
|
+
|
|
459
|
+
if (!capOn) {
|
|
460
|
+
for (const line of lines) {
|
|
461
|
+
for (const row of wrapOneExpandedLogicalLine(line, maxWidth)) out.push(row);
|
|
462
|
+
}
|
|
463
|
+
return out.length > 0 ? out : [' '];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (isShell) {
|
|
467
|
+
let omitted = 0;
|
|
468
|
+
for (const line of lines) {
|
|
469
|
+
for (const row of wrapOneExpandedLogicalLine(line, maxWidth)) {
|
|
470
|
+
out.push(row);
|
|
471
|
+
if (out.length > maxRows) {
|
|
472
|
+
out.shift();
|
|
473
|
+
omitted += 1;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return finalizeShellPhysicalCap(out, omitted, maxRows);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Non-shell surfaces keep the head of the expanded body (read/grep/json).
|
|
481
|
+
let truncated = false;
|
|
482
|
+
outer: for (const line of lines) {
|
|
483
|
+
for (const row of wrapOneExpandedLogicalLine(line, maxWidth)) {
|
|
484
|
+
if (out.length < maxRows) {
|
|
485
|
+
out.push(row);
|
|
486
|
+
} else {
|
|
487
|
+
truncated = true;
|
|
488
|
+
break outer;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (truncated) {
|
|
493
|
+
const bodySlots = Math.max(0, maxRows - 1);
|
|
494
|
+
if (out.length > bodySlots) out.length = bodySlots;
|
|
495
|
+
if (maxRows > 0) out.push(omittedPhysicalRowsMarker(1, false));
|
|
496
|
+
return out.length > 0 ? out.slice(0, maxRows) : [' '];
|
|
497
|
+
}
|
|
498
|
+
return out.length > 0 ? out : [' '];
|
|
499
|
+
}
|