mixdog 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -3
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +215 -29
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/recall-usecase-probe.json +6 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +23 -63
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +4 -1
- package/src/agents/heavy-worker/AGENT.md +6 -5
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/worker/AGENT.md +8 -4
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +632 -2042
- package/src/output-styles/default.md +34 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +4 -1
- package/src/output-styles/simple.md +22 -7
- package/src/rules/agent/00-common.md +2 -0
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +0 -11
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +12 -292
- package/src/runtime/channels/index.mjs +229 -663
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
- package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
- package/src/runtime/channels/lib/event-queue.mjs +63 -4
- package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +36 -570
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +201 -1948
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +780 -0
- package/src/runtime/memory/lib/recall-format.mjs +55 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +5 -13
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +23 -0
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +84 -2
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +11 -9
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +170 -0
- package/src/session-runtime/quick-search-models.mjs +46 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +319 -0
- package/src/session-runtime/tool-catalog.mjs +29 -29
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +110 -671
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +30 -9
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +77 -870
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +5 -1
- package/src/standalone/provider-admin.mjs +1 -16
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +945 -8094
- package/src/tui/app/app-format.mjs +206 -0
- package/src/tui/app/channel-pickers.mjs +510 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +324 -0
- package/src/tui/app/model-options.mjs +330 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +490 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +671 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +510 -0
- package/src/tui/app/use-transcript-window.mjs +589 -0
- package/src/tui/components/ConfirmBar.jsx +1 -1
- package/src/tui/components/Picker.jsx +32 -4
- package/src/tui/components/PromptInput.jsx +23 -101
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +63 -12
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +52 -594
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +19652 -18630
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine.mjs +122 -562
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- package/src/tui/input-editing.mjs +2 -2
- package/src/tui/markdown/format-token.mjs +4 -1
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +67 -501
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +1 -1
- package/src/workflows/sequential/WORKFLOW.md +1 -1
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +103 -6
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/input-editing.selection.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/paste-fix.test.mjs +0 -119
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import {
|
|
4
|
-
isInternalRuntimeNotificationText,
|
|
5
|
-
isModelVisibleToolCompletionWrapper,
|
|
6
|
-
modelVisibleToolCompletionMessage,
|
|
7
|
-
shouldPersistModelVisibleToolCompletion,
|
|
8
|
-
toolCompletionMeta,
|
|
9
|
-
toolCompletionInstruction,
|
|
10
|
-
} from './tool-execution-contract.mjs';
|
|
11
|
-
import { _isInternalRuntimeNotificationText } from '../agent/orchestrator/session/manager.mjs';
|
|
12
|
-
|
|
13
|
-
const completedBackgroundTask = [
|
|
14
|
-
'background task',
|
|
15
|
-
'task_id: task_agent_abc123',
|
|
16
|
-
'surface: agent',
|
|
17
|
-
'operation: run',
|
|
18
|
-
'status: completed',
|
|
19
|
-
'started: 2026-03-28T00:00:00.000Z',
|
|
20
|
-
'finished: 2026-03-28T00:00:05.000Z',
|
|
21
|
-
'',
|
|
22
|
-
'worker finished successfully',
|
|
23
|
-
].join('\n');
|
|
24
|
-
|
|
25
|
-
const runningBackgroundTask = [
|
|
26
|
-
'background task',
|
|
27
|
-
'task_id: task_agent_abc123',
|
|
28
|
-
'surface: agent',
|
|
29
|
-
'operation: run',
|
|
30
|
-
'status: running',
|
|
31
|
-
'started: 2026-03-28T00:00:00.000Z',
|
|
32
|
-
'notification: completion will be delivered to the owner session; use status/read only for manual recovery.',
|
|
33
|
-
].join('\n');
|
|
34
|
-
|
|
35
|
-
test('terminal background task with body should persist as model-visible wrapper', () => {
|
|
36
|
-
const meta = toolCompletionMeta({
|
|
37
|
-
surface: 'agent',
|
|
38
|
-
id: 'task_agent_abc123',
|
|
39
|
-
status: 'completed',
|
|
40
|
-
context: { callerSessionId: 'sess_owner' },
|
|
41
|
-
});
|
|
42
|
-
assert.equal(shouldPersistModelVisibleToolCompletion(completedBackgroundTask, meta), true);
|
|
43
|
-
const visible = modelVisibleToolCompletionMessage(completedBackgroundTask, meta);
|
|
44
|
-
assert.ok(visible);
|
|
45
|
-
assert.equal(isInternalRuntimeNotificationText(visible), false);
|
|
46
|
-
assert.equal(_isInternalRuntimeNotificationText(visible), false);
|
|
47
|
-
assert.equal(isModelVisibleToolCompletionWrapper(visible), true);
|
|
48
|
-
assert.match(visible, /worker finished successfully/);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('model-visible completion wrapper is not a human transcript item', () => {
|
|
52
|
-
const meta = toolCompletionMeta({
|
|
53
|
-
surface: 'agent',
|
|
54
|
-
id: 'task_agent_abc123',
|
|
55
|
-
status: 'completed',
|
|
56
|
-
instruction: toolCompletionInstruction({
|
|
57
|
-
surface: 'agent',
|
|
58
|
-
id: 'task_agent_abc123',
|
|
59
|
-
status: 'completed',
|
|
60
|
-
}),
|
|
61
|
-
context: { callerSessionId: 'sess_owner' },
|
|
62
|
-
});
|
|
63
|
-
const visible = modelVisibleToolCompletionMessage(completedBackgroundTask, meta);
|
|
64
|
-
assert.ok(visible);
|
|
65
|
-
assert.match(visible, /^The async agent task/);
|
|
66
|
-
assert.match(visible, /\n\nResult:\n/);
|
|
67
|
-
assert.equal(isModelVisibleToolCompletionWrapper(visible), true);
|
|
68
|
-
assert.equal(isModelVisibleToolCompletionWrapper('please fix the leak in engine.mjs'), false);
|
|
69
|
-
assert.equal(isModelVisibleToolCompletionWrapper(completedBackgroundTask), false);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('running background task stays hidden from model-visible pending drain', () => {
|
|
73
|
-
const meta = toolCompletionMeta({
|
|
74
|
-
surface: 'agent',
|
|
75
|
-
id: 'task_agent_abc123',
|
|
76
|
-
status: 'running',
|
|
77
|
-
context: { callerSessionId: 'sess_owner' },
|
|
78
|
-
});
|
|
79
|
-
assert.equal(shouldPersistModelVisibleToolCompletion(runningBackgroundTask, meta), false);
|
|
80
|
-
assert.equal(modelVisibleToolCompletionMessage(runningBackgroundTask, meta), '');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test('terminal background task persists when result body mentions task-notification', () => {
|
|
84
|
-
const taskWithTagInBody = [
|
|
85
|
-
...completedBackgroundTask.split('\n').slice(0, -1),
|
|
86
|
-
'log line: saw <task-notification status="completed"> in worker output',
|
|
87
|
-
].join('\n');
|
|
88
|
-
const meta = toolCompletionMeta({
|
|
89
|
-
surface: 'agent',
|
|
90
|
-
id: 'task_agent_abc123',
|
|
91
|
-
status: 'completed',
|
|
92
|
-
context: { callerSessionId: 'sess_owner' },
|
|
93
|
-
});
|
|
94
|
-
assert.equal(shouldPersistModelVisibleToolCompletion(taskWithTagInBody, meta), true);
|
|
95
|
-
const visible = modelVisibleToolCompletionMessage(taskWithTagInBody, meta);
|
|
96
|
-
assert.ok(visible);
|
|
97
|
-
assert.match(visible, /<task-notification/);
|
|
98
|
-
assert.equal(isInternalRuntimeNotificationText(visible), false);
|
|
99
|
-
assert.equal(_isInternalRuntimeNotificationText(visible), false);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('internal task-notification envelope does not persist', () => {
|
|
103
|
-
const internalEnvelope = [
|
|
104
|
-
'<task-notification>',
|
|
105
|
-
'<status>completed</status>',
|
|
106
|
-
'<summary>Agent completed</summary>',
|
|
107
|
-
'<task-id>task_agent_internal</task-id>',
|
|
108
|
-
'</task-notification>',
|
|
109
|
-
].join('\n');
|
|
110
|
-
const meta = toolCompletionMeta({
|
|
111
|
-
surface: 'agent',
|
|
112
|
-
id: 'task_agent_internal',
|
|
113
|
-
status: 'completed',
|
|
114
|
-
context: { callerSessionId: 'sess_owner' },
|
|
115
|
-
});
|
|
116
|
-
assert.equal(shouldPersistModelVisibleToolCompletion(internalEnvelope, meta), false);
|
|
117
|
-
assert.equal(modelVisibleToolCompletionMessage(internalEnvelope, meta), '');
|
|
118
|
-
assert.equal(isInternalRuntimeNotificationText(internalEnvelope), true);
|
|
119
|
-
assert.equal(_isInternalRuntimeNotificationText(internalEnvelope), true);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('raw running background task remains internal for manager pending drain', () => {
|
|
123
|
-
assert.equal(isInternalRuntimeNotificationText(runningBackgroundTask), true);
|
|
124
|
-
assert.equal(_isInternalRuntimeNotificationText(runningBackgroundTask), true);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const bracketedShellCompleted = [
|
|
130
|
-
"[task_id: shell_job_1]",
|
|
131
|
-
"[status: completed]",
|
|
132
|
-
"[exit: 0]",
|
|
133
|
-
"",
|
|
134
|
-
"done output",
|
|
135
|
-
].join("\n");
|
|
136
|
-
|
|
137
|
-
const bracketedShellRunning = [
|
|
138
|
-
"[task_id: shell_job_1]",
|
|
139
|
-
"[status: running]",
|
|
140
|
-
"[command: npm test]",
|
|
141
|
-
].join("\n");
|
|
142
|
-
|
|
143
|
-
test("bracketed shell completed raw envelope is internal", () => {
|
|
144
|
-
assert.equal(isInternalRuntimeNotificationText(bracketedShellCompleted), true);
|
|
145
|
-
assert.equal(_isInternalRuntimeNotificationText(bracketedShellCompleted), true);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("bracketed shell running raw envelope is internal", () => {
|
|
149
|
-
assert.equal(isInternalRuntimeNotificationText(bracketedShellRunning), true);
|
|
150
|
-
assert.equal(_isInternalRuntimeNotificationText(bracketedShellRunning), true);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test("wrapped shell completion stays model-visible and not internal", () => {
|
|
154
|
-
const meta = toolCompletionMeta({
|
|
155
|
-
surface: "shell",
|
|
156
|
-
id: "shell_job_1",
|
|
157
|
-
status: "completed",
|
|
158
|
-
context: { callerSessionId: "sess_owner" },
|
|
159
|
-
});
|
|
160
|
-
const visible = modelVisibleToolCompletionMessage(bracketedShellCompleted, meta);
|
|
161
|
-
assert.ok(visible);
|
|
162
|
-
assert.match(visible, /done output/);
|
|
163
|
-
assert.equal(isInternalRuntimeNotificationText(visible), false);
|
|
164
|
-
assert.equal(_isInternalRuntimeNotificationText(visible), false);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("model-visible wrapper bounds large completion bodies", () => {
|
|
168
|
-
const hugeBody = "x".repeat(20_000);
|
|
169
|
-
const hugeTask = [
|
|
170
|
-
...completedBackgroundTask.split("\n").slice(0, -1),
|
|
171
|
-
hugeBody,
|
|
172
|
-
].join("\n");
|
|
173
|
-
const meta = toolCompletionMeta({
|
|
174
|
-
surface: "agent",
|
|
175
|
-
id: "task_agent_abc123",
|
|
176
|
-
status: "completed",
|
|
177
|
-
context: { callerSessionId: "sess_owner" },
|
|
178
|
-
});
|
|
179
|
-
const visible = modelVisibleToolCompletionMessage(hugeTask, meta);
|
|
180
|
-
assert.ok(visible);
|
|
181
|
-
assert.match(visible, /\[result truncated for model context\]/);
|
|
182
|
-
assert.ok(visible.length < hugeBody.length);
|
|
183
|
-
});
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import {
|
|
4
|
-
appendAgentProgressKv,
|
|
5
|
-
buildAgentTaskProgressFields,
|
|
6
|
-
resolveSilentForSeconds,
|
|
7
|
-
} from './agent-task-status.mjs';
|
|
8
|
-
|
|
9
|
-
test('waiting for first activity explains silence', () => {
|
|
10
|
-
const now = 1_000_000;
|
|
11
|
-
const fields = buildAgentTaskProgressFields({
|
|
12
|
-
now,
|
|
13
|
-
sessionStatus: 'running',
|
|
14
|
-
runtimeStage: 'requesting',
|
|
15
|
-
snapshot: {
|
|
16
|
-
stage: 'requesting',
|
|
17
|
-
waitingForFirstActivity: true,
|
|
18
|
-
modelRequestStartedAt: now - 12_000,
|
|
19
|
-
askStartedAt: now - 12_000,
|
|
20
|
-
lastProgressAt: now - 12_000,
|
|
21
|
-
},
|
|
22
|
-
policy: { firstResponseMs: 120_000, idleStaleMs: 1_800_000, toolRunningMs: 600_000 },
|
|
23
|
-
taskStatus: 'running',
|
|
24
|
-
});
|
|
25
|
-
assert.equal(fields.last_progress, 'awaiting first model response');
|
|
26
|
-
assert.match(fields.diagnostic, /waiting for first response/);
|
|
27
|
-
assert.match(fields.watchdog, /first=120s/);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('streaming without recent deltas is called out', () => {
|
|
31
|
-
const now = 2_000_000;
|
|
32
|
-
const fields = buildAgentTaskProgressFields({
|
|
33
|
-
now,
|
|
34
|
-
sessionStatus: 'running',
|
|
35
|
-
runtimeStage: 'streaming',
|
|
36
|
-
snapshot: {
|
|
37
|
-
stage: 'streaming',
|
|
38
|
-
waitingForFirstActivity: false,
|
|
39
|
-
lastStreamDeltaAt: now - 23_000,
|
|
40
|
-
lastProgressAt: now - 23_000,
|
|
41
|
-
},
|
|
42
|
-
taskStatus: 'running',
|
|
43
|
-
});
|
|
44
|
-
assert.match(fields.last_progress, /no stream delta for 23s/);
|
|
45
|
-
assert.match(fields.diagnostic, /no visible output yet \(23s\)/);
|
|
46
|
-
assert.equal(fields.silent_for, 23);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('queued follow-ups surface on idle worker', () => {
|
|
50
|
-
const fields = buildAgentTaskProgressFields({
|
|
51
|
-
sessionStatus: 'idle',
|
|
52
|
-
runtimeStage: 'streaming',
|
|
53
|
-
queuedFollowups: 2,
|
|
54
|
-
taskStatus: 'running',
|
|
55
|
-
});
|
|
56
|
-
assert.equal(fields.queued_followups, 2);
|
|
57
|
-
assert.match(fields.diagnostic, /2 follow-ups queued/);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test('resolveSilentForSeconds uses latest progress signal', () => {
|
|
61
|
-
const now = 10_000;
|
|
62
|
-
const silent = resolveSilentForSeconds(now, { lastProgressAt: now - 5_000 }, { lastStreamDeltaAt: now - 9_000 });
|
|
63
|
-
assert.equal(silent, 5);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('appendAgentProgressKv keeps base line first', () => {
|
|
67
|
-
const line = appendAgentProgressKv('- job1 spawn running', {
|
|
68
|
-
worker_stage: 'streaming',
|
|
69
|
-
silent_for: 4,
|
|
70
|
-
diagnostic: 'streaming',
|
|
71
|
-
});
|
|
72
|
-
assert.match(line, /^- job1 spawn running /);
|
|
73
|
-
assert.match(line, /stage=streaming/);
|
|
74
|
-
assert.match(line, /hint=streaming/);
|
|
75
|
-
});
|
|
76
|
-
|
|
@@ -1,399 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import {
|
|
4
|
-
formatExpandedResult,
|
|
5
|
-
wrapExpandedResultLines,
|
|
6
|
-
expandedResultBodyWidth,
|
|
7
|
-
inferLangFamily,
|
|
8
|
-
stripUnderlineAnsi,
|
|
9
|
-
linkifyUrls,
|
|
10
|
-
tryFormatJson,
|
|
11
|
-
} from './tool-output-format.mjs';
|
|
12
|
-
import stringWidth from 'string-width';
|
|
13
|
-
import { displayWidthWith } from '../display-width.mjs';
|
|
14
|
-
|
|
15
|
-
const stripAnsi = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, '').replace(/\x1b\]8;;[^\x07]*\x07/g, '');
|
|
16
|
-
|
|
17
|
-
function withRenderLineCap(cap, fn) {
|
|
18
|
-
const prev = process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES;
|
|
19
|
-
const legacy = process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS;
|
|
20
|
-
process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES = String(cap);
|
|
21
|
-
delete process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS;
|
|
22
|
-
try {
|
|
23
|
-
return fn();
|
|
24
|
-
} finally {
|
|
25
|
-
if (prev === undefined) delete process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES;
|
|
26
|
-
else process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES = prev;
|
|
27
|
-
if (legacy === undefined) delete process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS;
|
|
28
|
-
else process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS = legacy;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
test('inferLangFamily maps known extensions, null otherwise', () => {
|
|
33
|
-
assert.equal(inferLangFamily('src/x.mjs'), 'js');
|
|
34
|
-
assert.equal(inferLangFamily('a.py'), 'py');
|
|
35
|
-
assert.equal(inferLangFamily('x.json'), 'json');
|
|
36
|
-
assert.equal(inferLangFamily('run.sh'), 'sh');
|
|
37
|
-
assert.equal(inferLangFamily('readme'), null);
|
|
38
|
-
assert.equal(inferLangFamily(''), null);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('read line-number gutter is split into a dim column and body is highlighted', () => {
|
|
42
|
-
const read = '900\u2192function f() {\n901\u2192 return 1;\n902\u2192}';
|
|
43
|
-
const out = formatExpandedResult(read, { pathArg: 'a.mjs' });
|
|
44
|
-
assert.equal(out.length, 3);
|
|
45
|
-
// gutter text survives, body keyword colored (output carries SGR escapes).
|
|
46
|
-
assert.ok(stripAnsi(out[0]).startsWith('900\u2192function f() {'));
|
|
47
|
-
assert.ok(out[0].includes('\x1b['), 'first line carries color escapes');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('non-ANSI tool output expands tabs so width math matches the terminal', () => {
|
|
51
|
-
// A raw \t is ZERO-width to string-width, so wrapExpandedResultLines would
|
|
52
|
-
// under-wrap a tab-bearing line and the terminal-expanded tab would bleed
|
|
53
|
-
// the row through the bottom prompt box on scroll. formatExpandedResult must
|
|
54
|
-
// normalize tabs to spaces for non-ANSI output.
|
|
55
|
-
const out = formatExpandedResult('\tindented line', { pathArg: 'a.txt' });
|
|
56
|
-
const plain = stripAnsi(out.join('\n'));
|
|
57
|
-
assert.ok(!plain.includes('\t'), 'no raw tab survives non-ANSI tool output');
|
|
58
|
-
const body = plain.split('\n').find((l) => l.includes('indented line'));
|
|
59
|
-
assert.ok(body, 'body line present');
|
|
60
|
-
assert.equal(stringWidth(body), body.length, 'visible width equals char length');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('shell ANSI output keeps raw control bytes (not normalized)', () => {
|
|
64
|
-
// ANSI/shell output legitimately carries control bytes in escape sequences;
|
|
65
|
-
// normalization must be skipped so those survive verbatim.
|
|
66
|
-
const sh = '\x1b[31mred\x1b[0m';
|
|
67
|
-
const out = formatExpandedResult(sh, { isShell: true });
|
|
68
|
-
assert.ok(out.join('').includes('\x1b['), 'escape sequences preserved');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('colored shell output still expands visible tabs (ANSI-aware normalize)', () => {
|
|
72
|
-
// A colored line with a raw \t in its VISIBLE text: the SGR escapes must be
|
|
73
|
-
// preserved verbatim while the tab is expanded, so the visible width matches
|
|
74
|
-
// the terminal and the row cannot bleed through the prompt box on scroll.
|
|
75
|
-
const sh = '\x1b[32m\tgreen tabbed\x1b[0m';
|
|
76
|
-
const out = formatExpandedResult(sh, { isShell: true });
|
|
77
|
-
const joined = out.join('\n');
|
|
78
|
-
assert.ok(joined.includes('\x1b['), 'color escapes preserved');
|
|
79
|
-
assert.ok(!joined.includes('\t'), 'visible tab expanded even in colored output');
|
|
80
|
-
const body = stripAnsi(joined).split('\n').find((l) => l.includes('green tabbed'));
|
|
81
|
-
assert.ok(body, 'body line present');
|
|
82
|
-
assert.equal(stringWidth(body), body.length, 'visible width equals char length');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('block syntax highlight skips tokenizer for over-long lines (index aligned)', () => {
|
|
86
|
-
// MAX_HIGHLIGHT_LINE_CHARS is 2000 in tool-output-format.mjs
|
|
87
|
-
const giant = 'x'.repeat(2001);
|
|
88
|
-
const read = `1\u2192const a = 1;\n2\u2192${giant}\n3\u2192const b = 2;`;
|
|
89
|
-
const out = formatExpandedResult(read, { pathArg: 'a.mjs' });
|
|
90
|
-
assert.equal(out.length, 3);
|
|
91
|
-
assert.ok(out[0].includes('\x1b['), 'short line before giant is highlighted');
|
|
92
|
-
assert.ok(out[2].includes('\x1b['), 'short line after giant is highlighted');
|
|
93
|
-
const plainMid = stripAnsi(out[1]);
|
|
94
|
-
assert.ok(plainMid.startsWith('2\u2192'), 'gutter preserved on giant line');
|
|
95
|
-
assert.ok(plainMid.endsWith(giant), 'giant body preserved');
|
|
96
|
-
const hljsSpans = (out[1].match(/\x1b\[[0-9;]*m/g) || []).length;
|
|
97
|
-
const shortSpans = (out[0].match(/\x1b\[[0-9;]*m/g) || []).length;
|
|
98
|
-
assert.ok(hljsSpans <= shortSpans + 2, 'giant line not fully tokenized like normal code');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('grep file:line: gutter is split too', () => {
|
|
102
|
-
const grep = 'src/a.mjs:5581: value: foo,';
|
|
103
|
-
const out = formatExpandedResult(grep, { pathArg: 'src/a.mjs' });
|
|
104
|
-
assert.equal(out.length, 1);
|
|
105
|
-
assert.ok(stripAnsi(out[0]).startsWith('src/a.mjs:5581:'));
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('grep Windows absolute path:line: gutter is split', () => {
|
|
109
|
-
const grep = 'C:\\Project\\mixdog\\src\\App.jsx:12: const x = 1;';
|
|
110
|
-
const out = formatExpandedResult(grep, { pathArg: 'src/App.jsx' });
|
|
111
|
-
assert.equal(out.length, 1);
|
|
112
|
-
const plain = stripAnsi(out[0]);
|
|
113
|
-
assert.ok(plain.startsWith('C:\\Project\\mixdog\\src\\App.jsx:12:'));
|
|
114
|
-
assert.ok(plain.includes('const x = 1;'), 'body preserved after gutter');
|
|
115
|
-
assert.ok(out[0].includes('\x1b['), 'body carries highlight escapes');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('grep bare line-number gutter still works', () => {
|
|
119
|
-
const grep = '12: return null;';
|
|
120
|
-
const out = formatExpandedResult(grep, { pathArg: 'a.mjs' });
|
|
121
|
-
assert.equal(out.length, 1);
|
|
122
|
-
assert.ok(stripAnsi(out[0]).startsWith('12:'));
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('https URLs are not treated as grep path:line: gutters', () => {
|
|
126
|
-
const line = 'see https://example.com:443/path for docs';
|
|
127
|
-
const out = formatExpandedResult(line, {});
|
|
128
|
-
assert.equal(out.length, 1);
|
|
129
|
-
assert.equal(stripAnsi(out[0]), line);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('JSON result is auto pretty-printed (size-capped, precision-safe)', () => {
|
|
133
|
-
const out = formatExpandedResult('{"a":1,"b":[2,3]}', {});
|
|
134
|
-
assert.ok(out.length > 1, 'single-line JSON expands to multiple rows');
|
|
135
|
-
assert.equal(stripAnsi(out[0]), '{');
|
|
136
|
-
// precision loss → keep original line
|
|
137
|
-
const big = '{"id":123456789012345678901234567890}';
|
|
138
|
-
assert.equal(tryFormatJson(big), big);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test('JSON with markdown-like string values stays JSON (no markdown lexer)', () => {
|
|
142
|
-
const raw = '["**literal**","`code`"]';
|
|
143
|
-
const out = formatExpandedResult(raw, {});
|
|
144
|
-
const visible = out.map(stripAnsi).join('\n');
|
|
145
|
-
assert.ok(visible.includes('**literal**'), 'array string keeps ** delimiters');
|
|
146
|
-
assert.ok(visible.includes('`code`'), 'array string keeps backticks');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test('JSON object with markdown-like values is not rendered as markdown', () => {
|
|
150
|
-
const raw = '{"msg":"**bold**","x":"`y`"}';
|
|
151
|
-
const out = formatExpandedResult(raw, {});
|
|
152
|
-
const visible = out.map(stripAnsi).join('\n');
|
|
153
|
-
assert.ok(visible.includes('**bold**'));
|
|
154
|
-
assert.ok(visible.includes('`y`'));
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test('shell output keeps color ANSI but strips underline and never highlights', () => {
|
|
158
|
-
const sh = 'PASS \x1b[32mok\x1b[0m\n\x1b[4mfile.js\x1b[24m done';
|
|
159
|
-
const out = formatExpandedResult(sh, { isShell: true });
|
|
160
|
-
assert.ok(out[0].includes('\x1b[32m'), 'color ANSI preserved');
|
|
161
|
-
assert.ok(!out[1].includes('\x1b[4m'), 'underline stripped');
|
|
162
|
-
assert.equal(stripAnsi(out[1]), 'file.js done');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('unified diff is colored by line class', () => {
|
|
166
|
-
const diff = 'diff --git a/x b/x\n@@ -1 +1 @@\n-old\n+new\n ctx';
|
|
167
|
-
const out = formatExpandedResult(diff, {});
|
|
168
|
-
assert.equal(out.length, 5);
|
|
169
|
-
assert.ok(out.every((l) => l.includes('\x1b[')), 'every diff line colored');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test('oversize output is capped with a truncation marker', () => {
|
|
173
|
-
const many = Array.from({ length: 5000 }, (_, i) => `line${i}`).join('\n');
|
|
174
|
-
const out = formatExpandedResult(many, {});
|
|
175
|
-
assert.ok(out.length <= 4001, 'logical cap keeps at most MAX_EXPANDED_LINES plus marker');
|
|
176
|
-
assert.ok(/truncated/.test(stripAnsi(out[out.length - 1])));
|
|
177
|
-
|
|
178
|
-
const sixHundred = Array.from({ length: 600 }, (_, i) => `row${i}`).join('\n');
|
|
179
|
-
const out600 = formatExpandedResult(sixHundred, {});
|
|
180
|
-
assert.equal(out600.length, 600, 'normal expanded output is not truncated at 80 logical lines');
|
|
181
|
-
|
|
182
|
-
const huge = 'x'.repeat(300 * 1024);
|
|
183
|
-
const out2 = formatExpandedResult(huge, {});
|
|
184
|
-
assert.ok(/truncated/.test(stripAnsi(out2[out2.length - 1])));
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test('stripUnderlineAnsi removes only underline SGR', () => {
|
|
188
|
-
assert.equal(stripUnderlineAnsi('a\x1b[4mu\x1b[24mb'), 'aub');
|
|
189
|
-
assert.equal(stripUnderlineAnsi('a\x1b[32mg\x1b[0mb'), 'a\x1b[32mg\x1b[0mb');
|
|
190
|
-
assert.equal(stripUnderlineAnsi('x\x1b[4;31my'), 'x\x1b[31my');
|
|
191
|
-
assert.equal(stripUnderlineAnsi('x\x1b[42my'), 'x\x1b[42my');
|
|
192
|
-
assert.equal(stripUnderlineAnsi('x\x1b[31;4my'), 'x\x1b[31my');
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test('linkifyUrls wraps bare URLs in OSC 8', () => {
|
|
196
|
-
const out = linkifyUrls('see https://x.com/y here');
|
|
197
|
-
assert.ok(out.includes('\x1b]8;;https://x.com/y\x07'));
|
|
198
|
-
assert.equal(linkifyUrls('no url here'), 'no url here');
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test('linkifyUrls keeps ANSI out of OSC 8 URL targets', () => {
|
|
202
|
-
const colored = 'go \x1b[31mhttps://example.com/a\x1b[0m end';
|
|
203
|
-
const out = linkifyUrls(colored);
|
|
204
|
-
const m = /\x1b]8;;([^\x07]+)\x07/.exec(out);
|
|
205
|
-
assert.ok(m, 'OSC 8 opener present');
|
|
206
|
-
assert.equal(m[1], 'https://example.com/a');
|
|
207
|
-
assert.ok(!m[1].includes('\x1b'), 'target URL has no escape bytes');
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
function osc8UrlTargets(text) {
|
|
211
|
-
const targets = [];
|
|
212
|
-
const bel = /\x1b]8;;([^\x07]+)\x07/g;
|
|
213
|
-
const st = /\x1b]8;;((?:[^\x1b]|\x1b(?![\\]))+)\x1b\\/g;
|
|
214
|
-
let m;
|
|
215
|
-
while ((m = bel.exec(text)) !== null) targets.push(m[1]);
|
|
216
|
-
while ((m = st.exec(text)) !== null) targets.push(m[1]);
|
|
217
|
-
return targets;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
test('linkifyUrls does not re-linkify existing BEL OSC 8 hyperlinks', () => {
|
|
221
|
-
const belLink = '\x1b]8;;https://a.test\x07label\x1b]8;;\x07';
|
|
222
|
-
const out = linkifyUrls(belLink);
|
|
223
|
-
assert.equal(out, belLink);
|
|
224
|
-
assert.equal((out.match(/\x1b]8;;/g) || []).length, 2, 'single open/close pair');
|
|
225
|
-
for (const target of osc8UrlTargets(out)) {
|
|
226
|
-
assert.ok(!target.includes('\x1b'), 'target has no escape bytes');
|
|
227
|
-
assert.ok(!target.includes('\x1b]8;;'), 'no nested OSC in target');
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test('linkifyUrls does not re-linkify existing ST OSC 8 hyperlinks', () => {
|
|
232
|
-
const stLink = '\x1b]8;;https://a.test\x1b\\label\x1b]8;;\x1b\\';
|
|
233
|
-
const out = linkifyUrls(stLink);
|
|
234
|
-
assert.equal(out, stLink);
|
|
235
|
-
assert.equal((out.match(/\x1b]8;;/g) || []).length, 2, 'single open/close pair');
|
|
236
|
-
for (const target of osc8UrlTargets(out)) {
|
|
237
|
-
assert.ok(!target.includes('\x1b'), 'target has no escape bytes');
|
|
238
|
-
assert.ok(!target.includes('\x1b]8;;'), 'no nested OSC in target');
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test('empty input yields no lines', () => {
|
|
243
|
-
assert.deepEqual(formatExpandedResult('', {}), []);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test('wrapExpandedResultLines splits long logical rows to body width', () => {
|
|
247
|
-
const columns = 48;
|
|
248
|
-
const maxW = expandedResultBodyWidth(columns);
|
|
249
|
-
const longBody = 'alpha_beta_gamma_delta '.repeat(6).trimEnd();
|
|
250
|
-
const logical = formatExpandedResult(`900\u2192${longBody}`, { pathArg: 'a.mjs' });
|
|
251
|
-
assert.equal(logical.length, 1);
|
|
252
|
-
const physical = wrapExpandedResultLines(logical, columns);
|
|
253
|
-
assert.ok(physical.length > 1, 'long read line becomes multiple physical rows');
|
|
254
|
-
for (const row of physical) {
|
|
255
|
-
assert.ok(
|
|
256
|
-
stringWidth(stripAnsi(row)) <= maxW,
|
|
257
|
-
`row width ${stringWidth(stripAnsi(row))} exceeds budget ${maxW}`,
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
for (let i = 1; i < physical.length; i++) {
|
|
261
|
-
assert.ok(/^\s+/.test(stripAnsi(physical[i])), 'wrapped continuations are indented');
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test('expanded markdown prose renders headings and emphasis (not raw #/**)', () => {
|
|
266
|
-
const md = '# Title\n\nSome **bold** and `inline`.\n';
|
|
267
|
-
const out = formatExpandedResult(md, {});
|
|
268
|
-
const visible = out.map(stripAnsi).join('\n');
|
|
269
|
-
assert.ok(!visible.includes('# Title'), 'ATX heading marker not shown raw');
|
|
270
|
-
assert.ok(visible.includes('Title'), 'heading text preserved');
|
|
271
|
-
assert.ok(visible.includes('bold'), 'strong text preserved');
|
|
272
|
-
assert.ok(!visible.includes('**'), 'emphasis delimiters stripped');
|
|
273
|
-
assert.ok(out.some((l) => l.includes('\x1b[')), 'markdown path applies theme colors');
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test('expanded fenced code in markdown path is highlighted', () => {
|
|
277
|
-
const md = '```js\nconst x = 1;\n```\n';
|
|
278
|
-
const out = formatExpandedResult(md, {});
|
|
279
|
-
const visible = out.map(stripAnsi).join('\n');
|
|
280
|
-
assert.ok(visible.includes('const x = 1;'), 'fence body preserved');
|
|
281
|
-
assert.ok(out.some((l) => l.includes('\x1b[')), 'fenced block carries ANSI');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test('read gutter lines skip markdown mode and keep syntax highlight', () => {
|
|
285
|
-
const read = '1\u2192# not a heading\n2\u2192const y = 2;\n';
|
|
286
|
-
const out = formatExpandedResult(read, { pathArg: 'a.mjs' });
|
|
287
|
-
const visible = out.map(stripAnsi);
|
|
288
|
-
assert.ok(visible.some((l) => l.includes('1\u2192# not a heading')), 'source # kept on read lines');
|
|
289
|
-
assert.ok(visible.some((l) => l.includes('const y = 2;')), 'read body preserved');
|
|
290
|
-
assert.ok(out.some((l) => stripAnsi(l).includes('const y = 2;') && l.includes('\x1b[')), 'code line still highlighted');
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
test('shell physical cap keeps newest rows across logical lines', () => {
|
|
294
|
-
withRenderLineCap(5, () => {
|
|
295
|
-
const long = 'W'.repeat(240);
|
|
296
|
-
const logical = formatExpandedResult(`${long}\nNEWEST_TAIL_LINE`, { isShell: true });
|
|
297
|
-
const physical = wrapExpandedResultLines(logical, 32, { isShell: true });
|
|
298
|
-
const visible = physical.map(stripAnsi);
|
|
299
|
-
assert.ok(visible.some((l) => l.includes('NEWEST_TAIL_LINE')), 'newest logical line survives rolling cap');
|
|
300
|
-
assert.ok(physical.length <= 5);
|
|
301
|
-
assert.ok(visible.some((l) => /omitted above/i.test(l)));
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
test('exact physical cap row count is not truncated', () => {
|
|
306
|
-
withRenderLineCap(10, () => {
|
|
307
|
-
const logical = Array.from({ length: 10 }, (_, i) => `row-${i}`).map((l) => formatExpandedResult(l, { isShell: true })[0]);
|
|
308
|
-
const physical = wrapExpandedResultLines(logical, 80, { isShell: true });
|
|
309
|
-
assert.equal(physical.length, 10);
|
|
310
|
-
assert.ok(!physical.some((l) => /omitted above/i.test(stripAnsi(l))));
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
test('physical cap of 1 never returns more than one row', () => {
|
|
315
|
-
withRenderLineCap(1, () => {
|
|
316
|
-
const physical = wrapExpandedResultLines(formatExpandedResult('a\nb\nc', { isShell: true }), 80, { isShell: true });
|
|
317
|
-
assert.ok(physical.length <= 1);
|
|
318
|
-
const physicalNonShell = wrapExpandedResultLines(formatExpandedResult('a\nb\nc', {}), 80, { isShell: false });
|
|
319
|
-
assert.ok(physicalNonShell.length <= 1);
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
test('shell physical cap omitted count includes marker slot', () => {
|
|
324
|
-
withRenderLineCap(5, () => {
|
|
325
|
-
const logical = ['r0', 'r1', 'r2', 'r3', 'r4', 'r5'].map((l) => formatExpandedResult(l, { isShell: true })[0]);
|
|
326
|
-
const physical = wrapExpandedResultLines(logical, 80, { isShell: true });
|
|
327
|
-
const visible = physical.map(stripAnsi);
|
|
328
|
-
assert.equal(physical.length, 5);
|
|
329
|
-
assert.match(visible[0], /2 lines omitted above/);
|
|
330
|
-
assert.deepEqual(visible.slice(1), ['r2', 'r3', 'r4', 'r5']);
|
|
331
|
-
});
|
|
332
|
-
withRenderLineCap(1, () => {
|
|
333
|
-
const logical = Array.from({ length: 6 }, (_, i) => `row${i}`).map((l) => formatExpandedResult(l, { isShell: true })[0]);
|
|
334
|
-
const physical = wrapExpandedResultLines(logical, 80, { isShell: true });
|
|
335
|
-
assert.equal(physical.length, 1);
|
|
336
|
-
assert.match(stripAnsi(physical[0]), /6 lines omitted above/);
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test('shell logical truncation marker precedes retained tail', () => {
|
|
341
|
-
const many = Array.from({ length: 5000 }, (_, i) => `line${i}`).join('\n');
|
|
342
|
-
const out = formatExpandedResult(many, { isShell: true });
|
|
343
|
-
const visible = out.map(stripAnsi);
|
|
344
|
-
assert.ok(visible.length <= 4001);
|
|
345
|
-
assert.ok(/omitted above/i.test(visible[0]), 'shell marker is first row');
|
|
346
|
-
assert.ok(visible[visible.length - 1].includes('line4999'), 'newest logical line is last');
|
|
347
|
-
assert.ok(!visible[visible.length - 1].includes('re-read a narrower range'));
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
test('wide policy OFF: displayWidth is byte-for-byte identical to string-width for arrow lines', () => {
|
|
351
|
-
// Guards the "non-WT terminals unchanged" invariant: with the policy OFF
|
|
352
|
-
// (default in this test process — no WT_SESSION, no override), the arrow is
|
|
353
|
-
// still 1 cell, so wrap output must be identical to the string-width world.
|
|
354
|
-
const arrowLine = ' 12\u2192const x = 1; \u2190 done';
|
|
355
|
-
assert.equal(displayWidthWith(arrowLine, false), stringWidth(arrowLine),
|
|
356
|
-
'policy OFF must equal plain string-width');
|
|
357
|
-
const logical = formatExpandedResult(arrowLine, { pathArg: 'a.mjs' });
|
|
358
|
-
const physical = wrapExpandedResultLines(logical, 40);
|
|
359
|
-
const maxW = expandedResultBodyWidth(40);
|
|
360
|
-
for (const row of physical) {
|
|
361
|
-
assert.ok(stringWidth(stripAnsi(row)) <= maxW,
|
|
362
|
-
`row width ${stringWidth(stripAnsi(row))} exceeds ${maxW} (policy OFF)`);
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
test('wide policy ON (forced): every emitted arrow row fits the display-width budget', async () => {
|
|
367
|
-
// The policy is resolved ONCE at module load, so forcing it requires a fresh
|
|
368
|
-
// process. Spawn a child with MIXDOG_TUI_AMBIGUOUS_WIDE=1 and assert that
|
|
369
|
-
// every wrapped read row (containing U+2192) measures <= expandedResultBodyWidth
|
|
370
|
-
// under the SAME wide policy the terminal will use. Without the clamp,
|
|
371
|
-
// string-width accepts arrow rows as fitting while the terminal renders them
|
|
372
|
-
// one cell wider, bleeding into the prompt box.
|
|
373
|
-
const { execFileSync } = await import('node:child_process');
|
|
374
|
-
const { fileURLToPath } = await import('node:url');
|
|
375
|
-
const modUrl = new URL('./tool-output-format.mjs', import.meta.url).href;
|
|
376
|
-
const dwUrl = new URL('../display-width.mjs', import.meta.url).href;
|
|
377
|
-
const script = `
|
|
378
|
-
import { formatExpandedResult, wrapExpandedResultLines, expandedResultBodyWidth } from ${JSON.stringify(modUrl)};
|
|
379
|
-
import { displayWidth, AMBIGUOUS_WIDE } from ${JSON.stringify(dwUrl)};
|
|
380
|
-
const strip = (s) => String(s).replace(/\\x1b\\[[0-9;]*m/g, '').replace(/\\x1b\\]8;;[^\\x07]*\\x07/g, '');
|
|
381
|
-
if (AMBIGUOUS_WIDE !== true) { console.log('POLICY_OFF'); process.exit(2); }
|
|
382
|
-
const columns = 30;
|
|
383
|
-
// Many arrows so a naive string-width fit accepts an over-wide row.
|
|
384
|
-
const body = 'x'.repeat(24) + ' ' + '\\u2192'.repeat(12) + ' tail';
|
|
385
|
-
const logical = formatExpandedResult('42\\u2192' + body, { pathArg: 'a.mjs' });
|
|
386
|
-
const physical = wrapExpandedResultLines(logical, columns);
|
|
387
|
-
const maxW = expandedResultBodyWidth(columns);
|
|
388
|
-
let bad = 0;
|
|
389
|
-
for (const row of physical) {
|
|
390
|
-
if (displayWidth(strip(row)) > maxW) bad++;
|
|
391
|
-
}
|
|
392
|
-
console.log(bad === 0 ? 'OK ' + physical.length : 'BAD ' + bad);
|
|
393
|
-
`;
|
|
394
|
-
const out = execFileSync(process.execPath, ['--input-type=module', '-e', script], {
|
|
395
|
-
env: { ...process.env, MIXDOG_TUI_AMBIGUOUS_WIDE: '1', WT_SESSION: '' },
|
|
396
|
-
encoding: 'utf8',
|
|
397
|
-
}).trim();
|
|
398
|
-
assert.ok(out.startsWith('OK'), `expected all rows within budget, got: ${out}`);
|
|
399
|
-
});
|