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,108 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import stripAnsi from 'strip-ansi';
|
|
4
|
-
import { assistantBodyWidth } from './table-layout.mjs';
|
|
5
|
-
import { renderTokenAnsiSegments, lexMarkdown, hasMarkdownSyntax } from './render-ansi.mjs';
|
|
6
|
-
import { setThemeSetting } from '../theme.mjs';
|
|
7
|
-
|
|
8
|
-
setThemeSetting('basic', { persist: false });
|
|
9
|
-
|
|
10
|
-
// Mirrors the streaming suffix the <Markdown trimPartialFences> path renders.
|
|
11
|
-
const STREAMING = '```js\nconst x = 1;\n`';
|
|
12
|
-
|
|
13
|
-
function codeBlockVisibleLines(segments) {
|
|
14
|
-
const codeSeg = segments.find((s) => s.type === 'ansi' && s.token?.type === 'code');
|
|
15
|
-
assert.ok(codeSeg, 'a fenced code segment is rendered');
|
|
16
|
-
return stripAnsi(codeSeg.ansi).split('\n');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
test('streaming render path drops the partial trailing backtick body line', () => {
|
|
20
|
-
const segments = renderTokenAnsiSegments(STREAMING, { trimPartialFences: true });
|
|
21
|
-
const lines = codeBlockVisibleLines(segments);
|
|
22
|
-
const bodyLines = lines.filter((l) => !/^js$/.test(l.trim()));
|
|
23
|
-
for (const l of bodyLines) {
|
|
24
|
-
assert.notEqual(l.trim(), '`', 'no body line is a lone partial fence');
|
|
25
|
-
}
|
|
26
|
-
// The real code body is still present.
|
|
27
|
-
assert.ok(lines.some((l) => l.includes('const x = 1;')), 'code body preserved');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('without trimPartialFences the partial backtick survives as a body line', () => {
|
|
31
|
-
// Proves the trim flag is what removes it on the render path (not formatToken).
|
|
32
|
-
const segments = renderTokenAnsiSegments(STREAMING, { trimPartialFences: false });
|
|
33
|
-
const lines = codeBlockVisibleLines(segments);
|
|
34
|
-
const bodyLines = lines.filter((l) => !/^js$/.test(l.trim()));
|
|
35
|
-
assert.ok(
|
|
36
|
-
bodyLines.some((l) => l.trim() === '`'),
|
|
37
|
-
'untrimmed path keeps the partial fence as a body line',
|
|
38
|
-
);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('trimPartialFences tokens are not cached (stable text unaffected)', () => {
|
|
42
|
-
const stable = '```js\nconst y = 2;\n```\n';
|
|
43
|
-
const a = lexMarkdown(stable, { trimPartialFences: false });
|
|
44
|
-
const b = lexMarkdown(stable, { trimPartialFences: false });
|
|
45
|
-
assert.strictEqual(a, b, 'stable content returns the cached token array');
|
|
46
|
-
// Streaming lex must be a fresh (non-cached) array.
|
|
47
|
-
const c = lexMarkdown(STREAMING, { trimPartialFences: true });
|
|
48
|
-
const d = lexMarkdown(STREAMING, { trimPartialFences: true });
|
|
49
|
-
assert.notStrictEqual(c, d, 'streaming lex is never cached');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('closed code block renders lang label and body without fence markers', () => {
|
|
53
|
-
const segments = renderTokenAnsiSegments('```js\nconst z = 3;\n```\n', { trimPartialFences: true });
|
|
54
|
-
const lines = codeBlockVisibleLines(segments);
|
|
55
|
-
assert.ok(lines.some((l) => l.includes('const z = 3;')), 'body present');
|
|
56
|
-
assert.ok(lines.some((l) => l.trim() === 'js'), 'language label present');
|
|
57
|
-
assert.ok(!lines.some((l) => l.includes('```')), 'no literal fence markers');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test('renderTokenAnsiSegments width matches assistant body width for code pre-wrap', () => {
|
|
61
|
-
const long = 'z'.repeat(60);
|
|
62
|
-
const md = `\`\`\`js\n${long}\n\`\`\``;
|
|
63
|
-
const bodyWidth = assistantBodyWidth(40);
|
|
64
|
-
const atBody = renderTokenAnsiSegments(md, { width: bodyWidth });
|
|
65
|
-
const atDefault = renderTokenAnsiSegments(md, { width: 80 });
|
|
66
|
-
const bodyLines = (seg) => stripAnsi(seg.find((s) => s.token?.type === 'code').ansi)
|
|
67
|
-
.split('\n')
|
|
68
|
-
.filter((l) => l.startsWith(' '));
|
|
69
|
-
assert.ok(bodyLines(atBody).length > bodyLines(atDefault).length,
|
|
70
|
-
'narrow body width produces more wrapped code rows than width 80');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const PLAIN_PREFIX = 'x'.repeat(501);
|
|
74
|
-
|
|
75
|
-
test('hasMarkdownSyntax detects strong markers after a 500+ char plain prefix', () => {
|
|
76
|
-
assert.ok(hasMarkdownSyntax(`${PLAIN_PREFIX}**late bold**`));
|
|
77
|
-
});
|
|
78
|
-
test('hasMarkdownSyntax detects inline code after a 500+ char plain prefix', () => {
|
|
79
|
-
assert.ok(hasMarkdownSyntax(`${PLAIN_PREFIX}\`late code\``));
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('render path parses fenced code after a 500+ char plain prefix', () => {
|
|
83
|
-
const input = `${PLAIN_PREFIX}\n\`\`\`js\nconst late = true;\n\`\`\``;
|
|
84
|
-
const tokens = lexMarkdown(input);
|
|
85
|
-
assert.ok(
|
|
86
|
-
tokens.some((t) => t.type === 'code' && String(t.text).includes('const late = true;')),
|
|
87
|
-
'lexer emits a code token with the fenced body',
|
|
88
|
-
);
|
|
89
|
-
const segments = renderTokenAnsiSegments(input);
|
|
90
|
-
const lines = codeBlockVisibleLines(segments);
|
|
91
|
-
assert.ok(lines.some((l) => l.includes('const late = true;')), 'code block body is rendered');
|
|
92
|
-
const visible = segments.map((s) => stripAnsi(s.ansi ?? '')).join('\n');
|
|
93
|
-
assert.ok(!visible.includes(`${PLAIN_PREFIX}\`\`\`js`), 'prefix is not glued to raw fence markers');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('render path parses strong after a 500+ char plain prefix (no raw **)', () => {
|
|
97
|
-
const segments = renderTokenAnsiSegments(`${PLAIN_PREFIX}**rendered**`);
|
|
98
|
-
const visible = segments.map((s) => stripAnsi(s.ansi ?? '')).join('');
|
|
99
|
-
assert.ok(visible.includes('rendered'), 'bold text is present');
|
|
100
|
-
assert.ok(!visible.includes('**'), 'markdown strong markers are not leaked');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('render path parses inline code after a 500+ char plain prefix (no raw backticks)', () => {
|
|
104
|
-
const segments = renderTokenAnsiSegments(`${PLAIN_PREFIX}\`snippet\``);
|
|
105
|
-
const visible = segments.map((s) => stripAnsi(s.ansi ?? '')).join('');
|
|
106
|
-
assert.ok(visible.includes('snippet'), 'codespan text is present');
|
|
107
|
-
assert.ok(!visible.includes('`'), 'inline code backticks are not leaked');
|
|
108
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { marked } from 'marked';
|
|
4
|
-
import { trimPartialClosingFences } from './stream-fence.mjs';
|
|
5
|
-
|
|
6
|
-
test('trims a partial closing fence (single backtick) from open code block', () => {
|
|
7
|
-
const tokens = marked.lexer('```js\nconst x = 1;\n`');
|
|
8
|
-
trimPartialClosingFences(tokens);
|
|
9
|
-
const code = tokens.find((t) => t.type === 'code');
|
|
10
|
-
assert.ok(code, 'code token exists');
|
|
11
|
-
assert.ok(!code.text.endsWith('`'), 'partial fence trimmed from text');
|
|
12
|
-
assert.ok(code.text.includes('const x = 1;'), 'real body preserved');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test('leaves a complete closing fence intact', () => {
|
|
16
|
-
const tokens = marked.lexer('```js\nconst x = 1;\n```\n');
|
|
17
|
-
const code = tokens.find((t) => t.type === 'code');
|
|
18
|
-
const before = code.text;
|
|
19
|
-
trimPartialClosingFences(tokens);
|
|
20
|
-
assert.equal(code.text, before, 'closed block unchanged');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test('no-op on non-code trailing token', () => {
|
|
24
|
-
const tokens = marked.lexer('hello world\n');
|
|
25
|
-
assert.doesNotThrow(() => trimPartialClosingFences(tokens));
|
|
26
|
-
});
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { assistantBodyWidth } from './table-layout.mjs';
|
|
4
|
-
import {
|
|
5
|
-
measureMarkdownRenderedRows,
|
|
6
|
-
measureStreamingMarkdownRenderedRows,
|
|
7
|
-
} from './measure-rendered-rows.mjs';
|
|
8
|
-
import { resolveStreamingMarkdownParts, streamingLayoutText } from './streaming-markdown.mjs';
|
|
9
|
-
import { setThemeSetting } from '../theme.mjs';
|
|
10
|
-
|
|
11
|
-
setThemeSetting('basic', { persist: false });
|
|
12
|
-
|
|
13
|
-
function measureStreamingLayoutFromParts(text, columns, streamKey) {
|
|
14
|
-
const parts = resolveStreamingMarkdownParts(text, streamKey);
|
|
15
|
-
if (parts.plain) {
|
|
16
|
-
return measureMarkdownRenderedRows(text, columns);
|
|
17
|
-
}
|
|
18
|
-
let rows = 0;
|
|
19
|
-
let childCount = 0;
|
|
20
|
-
if (parts.stablePrefix) {
|
|
21
|
-
rows += measureMarkdownRenderedRows(parts.stablePrefix, columns);
|
|
22
|
-
childCount += 1;
|
|
23
|
-
}
|
|
24
|
-
if (parts.unstableSuffix) {
|
|
25
|
-
rows += measureMarkdownRenderedRows(parts.unstableForRender, columns, { trimPartialFences: true });
|
|
26
|
-
childCount += 1;
|
|
27
|
-
}
|
|
28
|
-
if (childCount === 2) rows += 1;
|
|
29
|
-
return Math.max(1, rows);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
test('streaming row estimate matches shared layout parts at body width', () => {
|
|
33
|
-
const columns = 40;
|
|
34
|
-
const bodyWidth = assistantBodyWidth(columns);
|
|
35
|
-
const cases = [
|
|
36
|
-
{ key: 'bold-wrap', text: `**${'m'.repeat(bodyWidth + 12)}` },
|
|
37
|
-
{ key: 'partial-fence', text: `\`\`\`js\n${'w'.repeat(bodyWidth + 6)}\n\`` },
|
|
38
|
-
];
|
|
39
|
-
for (const { key, text } of cases) {
|
|
40
|
-
const estimate = measureStreamingMarkdownRenderedRows(text, columns, key);
|
|
41
|
-
const layout = measureStreamingLayoutFromParts(text, columns, key);
|
|
42
|
-
assert.strictEqual(estimate, layout, `${key}: estimate must match render layout parts`);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('partial code fence streaming measure uses trimPartialFences on unstable suffix', () => {
|
|
47
|
-
const columns = 48;
|
|
48
|
-
const bodyWidth = assistantBodyWidth(columns);
|
|
49
|
-
const streamKey = 'test-stream-fence';
|
|
50
|
-
const text = `\`\`\`js\n${'n'.repeat(bodyWidth + 8)}\n\``;
|
|
51
|
-
const estimate = measureStreamingMarkdownRenderedRows(text, columns, streamKey);
|
|
52
|
-
const parts = resolveStreamingMarkdownParts(text, streamKey);
|
|
53
|
-
const manual = measureMarkdownRenderedRows(parts.unstableForRender, columns, { trimPartialFences: true });
|
|
54
|
-
assert.strictEqual(estimate, Math.max(1, manual));
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('leading blank lines do not create an empty stable child or extra gap row', () => {
|
|
58
|
-
const columns = 40;
|
|
59
|
-
const streamKey = 'test-leading-blank';
|
|
60
|
-
const raw = '\n\n**streaming body**';
|
|
61
|
-
const trimmed = streamingLayoutText(raw);
|
|
62
|
-
const parts = resolveStreamingMarkdownParts(raw, streamKey);
|
|
63
|
-
assert.strictEqual(parts.stablePrefix, '', 'leading newlines must not promote whitespace-only stable');
|
|
64
|
-
assert.ok(parts.unstableSuffix.includes('streaming body'));
|
|
65
|
-
const estimate = measureStreamingMarkdownRenderedRows(raw, columns, streamKey);
|
|
66
|
-
const trimmedEstimate = measureStreamingMarkdownRenderedRows(trimmed, columns, streamKey);
|
|
67
|
-
assert.strictEqual(estimate, trimmedEstimate, 'raw vs trimmed leading newlines must match');
|
|
68
|
-
const layout = measureStreamingLayoutFromParts(raw, columns, streamKey);
|
|
69
|
-
assert.strictEqual(estimate, layout);
|
|
70
|
-
});
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import stringWidth from 'string-width';
|
|
4
|
-
import {
|
|
5
|
-
shouldFoldPastedText,
|
|
6
|
-
formatPastedTextRef,
|
|
7
|
-
pastedTextReferenceIds,
|
|
8
|
-
expandPastedTextTokens,
|
|
9
|
-
} from './paste-attachments.mjs';
|
|
10
|
-
import { caretPosition } from './input-editing.mjs';
|
|
11
|
-
|
|
12
|
-
// --- Part 1: pasted-text token folding ----------------------------------
|
|
13
|
-
|
|
14
|
-
test('shouldFoldPastedText fires on >=3 lines or >200 chars', () => {
|
|
15
|
-
assert.equal(shouldFoldPastedText('a\nb\nc'), true); // 3 lines
|
|
16
|
-
assert.equal(shouldFoldPastedText('a\nb'), false); // 2 lines, short
|
|
17
|
-
assert.equal(shouldFoldPastedText('x'.repeat(201)), true); // > 200 chars
|
|
18
|
-
assert.equal(shouldFoldPastedText('x'.repeat(200)), false); // == 200, not over
|
|
19
|
-
assert.equal(shouldFoldPastedText(''), false);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test('token round-trips through format + expand', () => {
|
|
23
|
-
const original = 'line1\nline2\nline3';
|
|
24
|
-
const token = formatPastedTextRef(7, original);
|
|
25
|
-
assert.equal(token, '[Pasted text #7 +3 lines]');
|
|
26
|
-
const refs = pastedTextReferenceIds(`before ${token} after`);
|
|
27
|
-
assert.deepEqual([...refs], [7]);
|
|
28
|
-
const map = { 7: { id: 7, text: original } };
|
|
29
|
-
assert.equal(
|
|
30
|
-
expandPastedTextTokens(`before ${token} after`, map),
|
|
31
|
-
`before ${original} after`,
|
|
32
|
-
);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('broken / partially-deleted tokens do not expand', () => {
|
|
36
|
-
const map = { 3: { id: 3, text: 'HIDDEN' } };
|
|
37
|
-
// Missing the "+N lines" tail — not a valid token, left verbatim.
|
|
38
|
-
assert.equal(expandPastedTextTokens('[Pasted text #3]', map), '[Pasted text #3]');
|
|
39
|
-
// Truncated bracket.
|
|
40
|
-
assert.equal(expandPastedTextTokens('[Pasted text #3 +2 lines', map), '[Pasted text #3 +2 lines');
|
|
41
|
-
// Unknown id stays as-is.
|
|
42
|
-
assert.equal(expandPastedTextTokens('[Pasted text #9 +2 lines]', map), '[Pasted text #9 +2 lines]');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// --- Part 2: caret row math vs ink wrap-ansi (hard, wordWrap:false) -------
|
|
46
|
-
|
|
47
|
-
// Faithful re-implementation of wrap-ansi's wrapWord loop for a single
|
|
48
|
-
// space-free line (the prompt caret math walks graphemes the same way). ink
|
|
49
|
-
// renders <Text wrap="hard"> which calls wrapAnsi(text, w, {hard,wordWrap:false}).
|
|
50
|
-
// For a run with no spaces the whole line goes through wrapWord, so the visual
|
|
51
|
-
// row count is what this produces.
|
|
52
|
-
function inkWrapWordRows(line, columns) {
|
|
53
|
-
const chars = Array.from(new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(line), (s) => s.segment);
|
|
54
|
-
let rows = 1;
|
|
55
|
-
let visible = 0;
|
|
56
|
-
for (let i = 0; i < chars.length; i += 1) {
|
|
57
|
-
const cw = stringWidth(chars[i]);
|
|
58
|
-
if (visible + cw <= columns) {
|
|
59
|
-
// stays on current row
|
|
60
|
-
} else {
|
|
61
|
-
rows += 1;
|
|
62
|
-
visible = 0;
|
|
63
|
-
}
|
|
64
|
-
visible += cw;
|
|
65
|
-
if (visible === columns && i < chars.length - 1) {
|
|
66
|
-
rows += 1;
|
|
67
|
-
visible = 0;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return rows;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// caretPosition row for the END of the string == (visual row count - 1).
|
|
74
|
-
function caretRowsForWholeString(text, w) {
|
|
75
|
-
return caretPosition(text, text.length, w).row + 1;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
test('caret rows agree with ink wrap for wide-char lines at odd widths', () => {
|
|
79
|
-
// A width where a width-2 glyph cannot sit flush on the last column exposes
|
|
80
|
-
// the mismatch: caretPosition must NOT advance an extra row when the char is
|
|
81
|
-
// simply pushed to the next line (ink does the same — it never leaves a
|
|
82
|
-
// half-column then over-counts).
|
|
83
|
-
const cases = [
|
|
84
|
-
{ text: '가'.repeat(30), w: 41 },
|
|
85
|
-
{ text: '가'.repeat(30), w: 40 },
|
|
86
|
-
{ text: '가나다'.repeat(10) + 'ascii', w: 13 },
|
|
87
|
-
{ text: '한글'.repeat(8), w: 7 },
|
|
88
|
-
{ text: '가'.repeat(5), w: 2 },
|
|
89
|
-
];
|
|
90
|
-
for (const { text, w } of cases) {
|
|
91
|
-
assert.equal(
|
|
92
|
-
caretRowsForWholeString(text, w),
|
|
93
|
-
inkWrapWordRows(text, w),
|
|
94
|
-
`width ${w} on ${JSON.stringify(text.slice(0, 6))}…`,
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// Cursor exactly at a soft-wrap boundary MID-text: ink renders the following
|
|
100
|
-
// glyph on the next row, so the caret must report {row N+1, col 0}, never
|
|
101
|
-
// {row N, col == w} (which would sit outside the box).
|
|
102
|
-
test('caret at mid-text wrap boundary rolls to next row (col 0)', () => {
|
|
103
|
-
const value = '가나다라마'; // 5 wide glyphs, width 2 each
|
|
104
|
-
const w = 4; // 2 glyphs per row
|
|
105
|
-
// offset 2 = after '나' (each glyph is ONE UTF-16 unit) = end of visual
|
|
106
|
-
// row 0; content ('다라마') follows, so the caret rolls to the next row.
|
|
107
|
-
const at = caretPosition(value, 2, w);
|
|
108
|
-
assert.deepEqual(at, { row: 1, col: 0 }, 'boundary caret must sit on next row');
|
|
109
|
-
// offset 4 = after '라' = end of visual row 1 (rows 0-1 are full).
|
|
110
|
-
assert.deepEqual(caretPosition(value, 4, w), { row: 2, col: 0 }, 'second boundary rolls too');
|
|
111
|
-
// Cursor at the very end of a FLUSH-ending text ('가나다라' fills rows 0-1
|
|
112
|
-
// exactly) with a trailing cell (PromptInput appends one) also rolls.
|
|
113
|
-
const flush = '가나다라';
|
|
114
|
-
const end = caretPosition(flush, flush.length, w, true);
|
|
115
|
-
assert.deepEqual(end, { row: 2, col: 0 }, 'flush end with trailing cell rolls to next row');
|
|
116
|
-
// Non-flush end ('마' leaves col 2) must NOT roll even with a trailing cell.
|
|
117
|
-
const nonFlush = caretPosition(value, value.length, w, true);
|
|
118
|
-
assert.deepEqual(nonFlush, { row: 2, col: 2 }, 'non-flush end stays on its row');
|
|
119
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import {
|
|
7
|
-
appendPromptHistory,
|
|
8
|
-
buildMergedPromptHistory,
|
|
9
|
-
loadPromptHistory,
|
|
10
|
-
pushSessionPromptHistory,
|
|
11
|
-
promptHistoryKey,
|
|
12
|
-
} from './prompt-history-store.mjs';
|
|
13
|
-
|
|
14
|
-
test('promptHistoryKey normalizes whitespace', () => {
|
|
15
|
-
assert.equal(promptHistoryKey(' hello world '), 'hello world');
|
|
16
|
-
assert.equal(promptHistoryKey(' '), '');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('buildMergedPromptHistory prefers session order and dedupes', () => {
|
|
20
|
-
const merged = buildMergedPromptHistory(
|
|
21
|
-
['newest', 'older'],
|
|
22
|
-
['stale', 'newest', 'from-disk'],
|
|
23
|
-
50,
|
|
24
|
-
);
|
|
25
|
-
assert.deepEqual(merged, ['newest', 'older', 'from-disk', 'stale']);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('appendPromptHistory persists per cwd across reload', () => {
|
|
29
|
-
const dataDir = mkdtempSync(join(tmpdir(), 'mixdog-prompt-history-'));
|
|
30
|
-
const previous = process.env.MIXDOG_DATA_DIR;
|
|
31
|
-
process.env.MIXDOG_DATA_DIR = dataDir;
|
|
32
|
-
try {
|
|
33
|
-
const cwd = join(dataDir, 'project-a');
|
|
34
|
-
assert.deepEqual(appendPromptHistory(cwd, 'first prompt'), ['first prompt']);
|
|
35
|
-
assert.deepEqual(appendPromptHistory(cwd, 'second prompt'), ['first prompt', 'second prompt']);
|
|
36
|
-
assert.deepEqual(appendPromptHistory(cwd, 'first prompt'), ['second prompt', 'first prompt']);
|
|
37
|
-
const loaded = loadPromptHistory(cwd);
|
|
38
|
-
assert.deepEqual(loaded, ['second prompt', 'first prompt']);
|
|
39
|
-
} finally {
|
|
40
|
-
if (previous === undefined) delete process.env.MIXDOG_DATA_DIR;
|
|
41
|
-
else process.env.MIXDOG_DATA_DIR = previous;
|
|
42
|
-
rmSync(dataDir, { recursive: true, force: true });
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('pushSessionPromptHistory is cwd-session newest-first with dedupe', () => {
|
|
47
|
-
let session = pushSessionPromptHistory([], 'alpha');
|
|
48
|
-
session = pushSessionPromptHistory(session, 'beta');
|
|
49
|
-
session = pushSessionPromptHistory(session, 'alpha');
|
|
50
|
-
assert.deepEqual(session, ['alpha', 'beta']);
|
|
51
|
-
});
|
|
52
|
-
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import {
|
|
4
|
-
remapCanonicalStatuslineTruecolor,
|
|
5
|
-
normalizeStatuslineAnsi,
|
|
6
|
-
statuslineFooterCacheKey,
|
|
7
|
-
statuslineFooterIdentityChanged,
|
|
8
|
-
isResetStatsState,
|
|
9
|
-
STATUSLINE_CANONICAL_TRUECOLOR,
|
|
10
|
-
} from './statusline-ansi-bridge.mjs';
|
|
11
|
-
import { theme, setThemeSetting } from './theme.mjs';
|
|
12
|
-
|
|
13
|
-
function sgr38(r, g, b) {
|
|
14
|
-
return `\x1b[38;2;${r};${g};${b}m`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function statusColorsFromTheme() {
|
|
18
|
-
const ansiRgb = (value, fallback) => {
|
|
19
|
-
const match = /^rgb\((\d+),(\d+),(\d+)\)$/.exec(String(value || '').replace(/\s+/g, ''));
|
|
20
|
-
if (!match) return fallback;
|
|
21
|
-
return sgr38(match[1], match[2], match[3]);
|
|
22
|
-
};
|
|
23
|
-
return {
|
|
24
|
-
STATUS: ansiRgb(theme.statusText, sgr38(198, 198, 198)),
|
|
25
|
-
SUBTLE: ansiRgb(theme.statusSubtle, sgr38(136, 136, 136)),
|
|
26
|
-
SUCCESS: ansiRgb(theme.success, sgr38(0, 170, 75)),
|
|
27
|
-
WARNING: ansiRgb(theme.warning, sgr38(255, 193, 7)),
|
|
28
|
-
ERROR: ansiRgb(theme.error, sgr38(220, 70, 88)),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
test('remaps canonical truecolor sequences from ui/statusline.mjs', () => {
|
|
33
|
-
const colors = statusColorsFromTheme();
|
|
34
|
-
const subtle = sgr38(...STATUSLINE_CANONICAL_TRUECOLOR.subtle);
|
|
35
|
-
const success = sgr38(...STATUSLINE_CANONICAL_TRUECOLOR.success);
|
|
36
|
-
const raw = `${subtle}│\x1b[0m ${success}42%`;
|
|
37
|
-
const out = remapCanonicalStatuslineTruecolor(raw, colors);
|
|
38
|
-
assert.ok(!out.includes('38;2;136;136;136'), 'subtle canonical RGB should be replaced');
|
|
39
|
-
assert.ok(!out.includes('38;2;0;170;75'), 'success canonical RGB should be replaced');
|
|
40
|
-
assert.ok(out.includes(colors.SUBTLE));
|
|
41
|
-
assert.ok(out.includes(colors.SUCCESS));
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('normalizeStatuslineAnsi maps bold and legacy SGR plus truecolor', () => {
|
|
45
|
-
const colors = statusColorsFromTheme();
|
|
46
|
-
const raw = `\x1b[1mmodel\x1b[0m \x1b[33m5H\x1b[0m ${sgr38(...STATUSLINE_CANONICAL_TRUECOLOR.error)}quota`;
|
|
47
|
-
const out = normalizeStatuslineAnsi(raw, colors);
|
|
48
|
-
assert.ok(out.includes(colors.STATUS));
|
|
49
|
-
assert.ok(out.includes(colors.WARNING));
|
|
50
|
-
assert.ok(out.includes(colors.ERROR));
|
|
51
|
-
assert.ok(!out.includes('\x1b[33m'));
|
|
52
|
-
assert.ok(!out.includes('38;2;220;70;88'));
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('normalizeStatuslineAnsi preserves leading diamond glyph', () => {
|
|
56
|
-
const colors = statusColorsFromTheme();
|
|
57
|
-
const subtle = sgr38(...STATUSLINE_CANONICAL_TRUECOLOR.subtle);
|
|
58
|
-
const raw = `${subtle}◆\x1b[0m \x1b[1mgpt-4o\x1b[0m`;
|
|
59
|
-
const out = normalizeStatuslineAnsi(raw, colors);
|
|
60
|
-
assert.ok(out.includes('◆'), 'visible ◆ must remain');
|
|
61
|
-
assert.ok(out.includes('gpt-4o'));
|
|
62
|
-
assert.ok(out.indexOf('◆') < out.indexOf('gpt-4o'));
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('retimes bare values after reset without clobbering explicit segment colors', () => {
|
|
66
|
-
const colors = statusColorsFromTheme();
|
|
67
|
-
const subtle = colors.SUBTLE;
|
|
68
|
-
const success = colors.SUCCESS;
|
|
69
|
-
const warning = colors.WARNING;
|
|
70
|
-
const reset = '\x1b[0m';
|
|
71
|
-
const creditSeg = `${subtle}Credit${reset} $12.34`;
|
|
72
|
-
const outCredit = normalizeStatuslineAnsi(creditSeg, colors, { reset });
|
|
73
|
-
assert.ok(outCredit.includes('$12.34'));
|
|
74
|
-
const moneyIdx = outCredit.indexOf('$12.34');
|
|
75
|
-
assert.ok(moneyIdx > 0);
|
|
76
|
-
assert.ok(outCredit.slice(0, moneyIdx).includes(colors.STATUS), 'bare credit amount uses status text');
|
|
77
|
-
|
|
78
|
-
const usedLimit = `${subtle}5H${reset} $3.00/$10.00`;
|
|
79
|
-
const outUsed = normalizeStatuslineAnsi(usedLimit, colors, { reset });
|
|
80
|
-
assert.ok(outUsed.includes('$3.00/$10.00'));
|
|
81
|
-
assert.ok(outUsed.slice(0, outUsed.indexOf('$3.00')).includes(colors.STATUS));
|
|
82
|
-
|
|
83
|
-
const colored = `${subtle}5H${reset} ${warning}est $5.00${reset}`;
|
|
84
|
-
const outColored = normalizeStatuslineAnsi(colored, colors, { reset });
|
|
85
|
-
assert.ok(outColored.includes(warning), 'warning color on value must remain');
|
|
86
|
-
const estIdx = outColored.indexOf('est');
|
|
87
|
-
assert.ok(estIdx > 0);
|
|
88
|
-
assert.ok(outColored.slice(0, estIdx).includes(warning));
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('statuslineFooterCacheKey changes when session or route identity changes', () => {
|
|
92
|
-
const a = statuslineFooterCacheKey({ sessionId: 's1', provider: 'p', model: 'm1' });
|
|
93
|
-
const b = statuslineFooterCacheKey({ sessionId: 's2', provider: 'p', model: 'm1' });
|
|
94
|
-
const c = statuslineFooterCacheKey({ sessionId: 's1', provider: 'p', model: 'm2' });
|
|
95
|
-
assert.notEqual(a, b);
|
|
96
|
-
assert.notEqual(a, c);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test('statusline footer identity tracks context display boundary fields', () => {
|
|
100
|
-
const base = {
|
|
101
|
-
sessionId: 's1',
|
|
102
|
-
provider: 'p',
|
|
103
|
-
model: 'm',
|
|
104
|
-
contextWindow: 1_000_000,
|
|
105
|
-
displayContextWindow: 900_000,
|
|
106
|
-
rawContextWindow: 1_000_000,
|
|
107
|
-
compactBoundaryTokens: 900_000,
|
|
108
|
-
autoCompactTokenLimit: 810_000,
|
|
109
|
-
stats: { currentEstimatedContextTokens: 10_000 },
|
|
110
|
-
};
|
|
111
|
-
const changedDisplay = { ...base, displayContextWindow: 800_000 };
|
|
112
|
-
const changedBoundary = { ...base, compactBoundaryTokens: 800_000 };
|
|
113
|
-
const changedTrigger = { ...base, autoCompactTokenLimit: 720_000 };
|
|
114
|
-
|
|
115
|
-
assert.notEqual(statuslineFooterCacheKey(base), statuslineFooterCacheKey(changedDisplay));
|
|
116
|
-
assert.notEqual(statuslineFooterCacheKey(base), statuslineFooterCacheKey(changedBoundary));
|
|
117
|
-
assert.notEqual(statuslineFooterCacheKey(base), statuslineFooterCacheKey(changedTrigger));
|
|
118
|
-
assert.equal(statuslineFooterIdentityChanged(changedDisplay, base), true);
|
|
119
|
-
assert.equal(statuslineFooterIdentityChanged(changedBoundary, base), true);
|
|
120
|
-
assert.equal(statuslineFooterIdentityChanged(changedTrigger, base), true);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test('stats reset transition is footer identity change (cache must not reuse on /theme)', () => {
|
|
124
|
-
const activeStats = {
|
|
125
|
-
currentContextTokens: 1200,
|
|
126
|
-
inputTokens: 800,
|
|
127
|
-
turns: 2,
|
|
128
|
-
};
|
|
129
|
-
const resetStats = {
|
|
130
|
-
currentContextTokens: 0,
|
|
131
|
-
currentEstimatedContextTokens: 0,
|
|
132
|
-
inputTokens: 0,
|
|
133
|
-
latestInputTokens: 0,
|
|
134
|
-
promptTokens: 0,
|
|
135
|
-
turns: 0,
|
|
136
|
-
};
|
|
137
|
-
assert.equal(isResetStatsState(resetStats), true);
|
|
138
|
-
assert.equal(isResetStatsState(activeStats), false);
|
|
139
|
-
const sameRoute = { sessionId: 's1', provider: 'p', model: 'm', stats: resetStats };
|
|
140
|
-
const lastRoute = { sessionId: 's1', provider: 'p', model: 'm', stats: activeStats };
|
|
141
|
-
assert.equal(statuslineFooterCacheKey(sameRoute), statuslineFooterCacheKey(lastRoute));
|
|
142
|
-
assert.equal(statuslineFooterIdentityChanged(sameRoute, lastRoute), true);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test('theme switch changes normalized output when raw line is canonical', () => {
|
|
146
|
-
setThemeSetting('basic', { persist: false });
|
|
147
|
-
const colorsA = statusColorsFromTheme();
|
|
148
|
-
const raw = `${sgr38(...STATUSLINE_CANONICAL_TRUECOLOR.success)}▓▓\x1b[0m ${sgr38(...STATUSLINE_CANONICAL_TRUECOLOR.subtle)}░░`;
|
|
149
|
-
const tonedA = normalizeStatuslineAnsi(raw, colorsA);
|
|
150
|
-
|
|
151
|
-
setThemeSetting('teal', { persist: false });
|
|
152
|
-
const colorsB = statusColorsFromTheme();
|
|
153
|
-
const tonedB = normalizeStatuslineAnsi(raw, colorsB);
|
|
154
|
-
|
|
155
|
-
assert.notEqual(tonedA, tonedB);
|
|
156
|
-
assert.ok(tonedB.includes(colorsB.SUCCESS));
|
|
157
|
-
assert.ok(tonedB.includes(colorsB.SUBTLE));
|
|
158
|
-
setThemeSetting('basic', { persist: false });
|
|
159
|
-
});
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import {
|
|
4
|
-
formatHookDenialDetail,
|
|
5
|
-
hasUsefulFailedToolResultBody,
|
|
6
|
-
isFullyFailedToolBatch,
|
|
7
|
-
isHookApprovalDenialToolItem,
|
|
8
|
-
isHookApprovalDenialToolResult,
|
|
9
|
-
shouldSuppressFullyFailedToolItem,
|
|
10
|
-
} from './transcript-tool-failures.mjs';
|
|
11
|
-
|
|
12
|
-
const hookDeniedItem = {
|
|
13
|
-
kind: 'tool',
|
|
14
|
-
name: 'shell',
|
|
15
|
-
args: { command: 'rm -rf /' },
|
|
16
|
-
result: 'Error: tool "shell" denied by hook: approval required but no approval UI is available (policy)',
|
|
17
|
-
isError: true,
|
|
18
|
-
count: 1,
|
|
19
|
-
completedCount: 1,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const noisyFailedItem = {
|
|
23
|
-
kind: 'tool',
|
|
24
|
-
name: 'grep',
|
|
25
|
-
args: { pattern: 'foo' },
|
|
26
|
-
result: 'no matches',
|
|
27
|
-
isError: true,
|
|
28
|
-
count: 1,
|
|
29
|
-
completedCount: 1,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const emptyFailedItem = {
|
|
33
|
-
kind: 'tool',
|
|
34
|
-
name: 'grep',
|
|
35
|
-
args: { pattern: 'foo' },
|
|
36
|
-
result: null,
|
|
37
|
-
rawResult: null,
|
|
38
|
-
isError: true,
|
|
39
|
-
count: 1,
|
|
40
|
-
completedCount: 1,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const failedTaskJsonStringArgs = {
|
|
44
|
-
kind: 'tool',
|
|
45
|
-
name: 'shell',
|
|
46
|
-
args: '{"task_id":"t1","status":"failed","error":"boom"}',
|
|
47
|
-
result: null,
|
|
48
|
-
rawResult: null,
|
|
49
|
-
isError: true,
|
|
50
|
-
count: 1,
|
|
51
|
-
completedCount: 1,
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const failedTaskInputWrapperArgs = {
|
|
55
|
-
kind: 'tool',
|
|
56
|
-
name: 'shell',
|
|
57
|
-
args: { input: { task_id: 't2', status: 'timeout' } },
|
|
58
|
-
result: null,
|
|
59
|
-
rawResult: null,
|
|
60
|
-
isError: true,
|
|
61
|
-
count: 1,
|
|
62
|
-
completedCount: 1,
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
test('detects hook denial result text', () => {
|
|
66
|
-
assert.equal(
|
|
67
|
-
isHookApprovalDenialToolResult('Error: tool "read" denied by hook: blocked'),
|
|
68
|
-
true,
|
|
69
|
-
);
|
|
70
|
-
assert.equal(
|
|
71
|
-
isHookApprovalDenialToolResult('Error: tool "shell" denied by hook: approval required but no approval UI is available'),
|
|
72
|
-
true,
|
|
73
|
-
);
|
|
74
|
-
assert.equal(isHookApprovalDenialToolResult('grep: no matches'), false);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('fully-failed hook denials are not suppressed', () => {
|
|
78
|
-
assert.equal(isFullyFailedToolBatch(hookDeniedItem), true);
|
|
79
|
-
assert.equal(isHookApprovalDenialToolItem(hookDeniedItem), true);
|
|
80
|
-
assert.equal(shouldSuppressFullyFailedToolItem(hookDeniedItem), false);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test('fully-failed non-hook errors with visible body stay visible', () => {
|
|
84
|
-
assert.equal(isFullyFailedToolBatch(noisyFailedItem), true);
|
|
85
|
-
assert.equal(hasUsefulFailedToolResultBody(noisyFailedItem), true);
|
|
86
|
-
assert.equal(shouldSuppressFullyFailedToolItem(noisyFailedItem), false);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('fully-failed non-hook errors without useful body stay suppressed', () => {
|
|
90
|
-
assert.equal(isFullyFailedToolBatch(emptyFailedItem), true);
|
|
91
|
-
assert.equal(hasUsefulFailedToolResultBody(emptyFailedItem), false);
|
|
92
|
-
assert.equal(shouldSuppressFullyFailedToolItem(emptyFailedItem), true);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('failed background task with JSON-string args is not suppressed', () => {
|
|
96
|
-
assert.equal(isFullyFailedToolBatch(failedTaskJsonStringArgs), false);
|
|
97
|
-
assert.equal(shouldSuppressFullyFailedToolItem(failedTaskJsonStringArgs), false);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('failed background task with input-wrapped args is not suppressed', () => {
|
|
101
|
-
assert.equal(isFullyFailedToolBatch(failedTaskInputWrapperArgs), false);
|
|
102
|
-
assert.equal(shouldSuppressFullyFailedToolItem(failedTaskInputWrapperArgs), false);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test('formatHookDenialDetail strips tool prefix', () => {
|
|
106
|
-
assert.equal(
|
|
107
|
-
formatHookDenialDetail(hookDeniedItem.result),
|
|
108
|
-
'denied by hook: approval required but no approval UI is available (policy)',
|
|
109
|
-
);
|
|
110
|
-
});
|
|
111
|
-
|