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.
Files changed (329) hide show
  1. package/package.json +7 -3
  2. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  3. package/scripts/bench/lead-review-tasks.json +20 -0
  4. package/scripts/bench/r4-mixed-tasks.json +20 -0
  5. package/scripts/bench/review-tasks.json +20 -0
  6. package/scripts/bench/round-codex.json +114 -0
  7. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  8. package/scripts/bench/round-mixdog-lead.json +269 -0
  9. package/scripts/bench/round-mixdog.json +126 -0
  10. package/scripts/bench/round-r10-bigsample.json +679 -0
  11. package/scripts/bench/round-r11-codexalign.json +257 -0
  12. package/scripts/bench/round-r4-codex.json +114 -0
  13. package/scripts/bench/round-r4-mixed.json +225 -0
  14. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  15. package/scripts/bench/round-r6-codex.json +114 -0
  16. package/scripts/bench/round-r6-solo.json +257 -0
  17. package/scripts/bench/round-r7-full.json +254 -0
  18. package/scripts/bench/round-r8-fulldefault.json +255 -0
  19. package/scripts/bench-run.mjs +215 -29
  20. package/scripts/freevar-smoke.mjs +95 -0
  21. package/scripts/internal-comms-bench.mjs +1 -0
  22. package/scripts/internal-comms-smoke.mjs +10 -9
  23. package/scripts/mouse-probe.mjs +45 -0
  24. package/scripts/output-style-bench.mjs +13 -6
  25. package/scripts/output-style-smoke.mjs +4 -4
  26. package/scripts/provider-toolcall-test.mjs +7 -3
  27. package/scripts/recall-usecase-cases.json +18 -0
  28. package/scripts/recall-usecase-probe.json +6 -0
  29. package/scripts/session-bench.mjs +152 -6
  30. package/scripts/tool-smoke.mjs +23 -63
  31. package/scripts/tui-render-smoke.mjs +90 -0
  32. package/scripts/webhook-smoke.mjs +208 -0
  33. package/src/agents/debugger/AGENT.md +4 -1
  34. package/src/agents/heavy-worker/AGENT.md +6 -5
  35. package/src/agents/maintainer/AGENT.md +4 -0
  36. package/src/agents/reviewer/AGENT.md +2 -1
  37. package/src/agents/worker/AGENT.md +8 -4
  38. package/src/lib/rules-builder.cjs +4 -0
  39. package/src/mixdog-session-runtime.mjs +632 -2042
  40. package/src/output-styles/default.md +34 -9
  41. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  42. package/src/output-styles/minimal.md +4 -1
  43. package/src/output-styles/simple.md +22 -7
  44. package/src/rules/agent/00-common.md +2 -0
  45. package/src/rules/lead/lead-brief.md +12 -0
  46. package/src/rules/lead/lead-tool.md +0 -11
  47. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  49. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  50. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  51. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  52. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  53. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  54. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  55. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  56. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  57. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  58. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  59. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  60. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  62. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  63. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  65. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  66. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  67. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  69. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  70. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
  76. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
  77. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  80. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  82. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  83. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  84. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  85. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  87. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  88. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  89. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  90. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  91. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  93. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  94. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  95. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  97. package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
  98. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  99. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  100. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  101. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  102. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  103. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  104. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  105. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  106. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  107. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  108. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  109. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
  110. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  118. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  119. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  120. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  124. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  125. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  126. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  128. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  130. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  131. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  132. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  133. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  134. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  136. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  137. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  138. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  139. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  140. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  141. package/src/runtime/channels/backends/discord.mjs +12 -292
  142. package/src/runtime/channels/index.mjs +229 -663
  143. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  144. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  145. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  146. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  147. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  148. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  149. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  150. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  151. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  152. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  153. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  155. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  156. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  157. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  158. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  159. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  160. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  161. package/src/runtime/channels/lib/webhook.mjs +36 -570
  162. package/src/runtime/channels/tool-defs.mjs +11 -130
  163. package/src/runtime/memory/index.mjs +201 -1948
  164. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  165. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  166. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  167. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  168. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  169. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  170. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  171. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  172. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  173. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  174. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  175. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  177. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  178. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  179. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  180. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  181. package/src/runtime/memory/tool-defs.mjs +5 -13
  182. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  183. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  184. package/src/runtime/search/lib/web-tools.mjs +24 -602
  185. package/src/runtime/shared/atomic-file.mjs +26 -1
  186. package/src/runtime/shared/launcher-control.mjs +2 -2
  187. package/src/runtime/shared/tool-primitives.mjs +308 -0
  188. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  189. package/src/runtime/shared/tool-surface.mjs +80 -898
  190. package/src/runtime/shared/transcript-writer.mjs +23 -0
  191. package/src/runtime/shared/update-checker.mjs +7 -4
  192. package/src/session-runtime/config-helpers.mjs +84 -2
  193. package/src/session-runtime/config-lifecycle.mjs +232 -0
  194. package/src/session-runtime/cwd-plugins.mjs +226 -0
  195. package/src/session-runtime/mcp-glue.mjs +177 -0
  196. package/src/session-runtime/model-recency.mjs +111 -0
  197. package/src/session-runtime/native-search.mjs +247 -0
  198. package/src/session-runtime/output-styles.mjs +11 -9
  199. package/src/session-runtime/prewarm.mjs +142 -0
  200. package/src/session-runtime/provider-models.mjs +278 -0
  201. package/src/session-runtime/provider-usage.mjs +120 -0
  202. package/src/session-runtime/quick-model-rows.mjs +170 -0
  203. package/src/session-runtime/quick-search-models.mjs +46 -0
  204. package/src/session-runtime/session-hooks.mjs +93 -0
  205. package/src/session-runtime/settings-api.mjs +319 -0
  206. package/src/session-runtime/tool-catalog.mjs +29 -29
  207. package/src/session-runtime/tool-defs.mjs +84 -0
  208. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  209. package/src/standalone/agent-tool/helpers.mjs +237 -0
  210. package/src/standalone/agent-tool/notify.mjs +107 -0
  211. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  212. package/src/standalone/agent-tool/render.mjs +152 -0
  213. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  214. package/src/standalone/agent-tool.mjs +110 -671
  215. package/src/standalone/channel-worker.mjs +4 -7
  216. package/src/standalone/explore-tool.mjs +30 -9
  217. package/src/standalone/hook-bus/config.mjs +207 -0
  218. package/src/standalone/hook-bus/constants.mjs +90 -0
  219. package/src/standalone/hook-bus/handlers.mjs +481 -0
  220. package/src/standalone/hook-bus/payload.mjs +31 -0
  221. package/src/standalone/hook-bus/rules.mjs +77 -0
  222. package/src/standalone/hook-bus.mjs +77 -870
  223. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  224. package/src/standalone/opencode-go-login.mjs +5 -1
  225. package/src/standalone/provider-admin.mjs +1 -16
  226. package/src/standalone/usage-dashboard.mjs +3 -1
  227. package/src/tui/App.jsx +945 -8094
  228. package/src/tui/app/app-format.mjs +206 -0
  229. package/src/tui/app/channel-pickers.mjs +510 -0
  230. package/src/tui/app/clipboard.mjs +67 -0
  231. package/src/tui/app/core-memory-picker.mjs +210 -0
  232. package/src/tui/app/extension-pickers.mjs +506 -0
  233. package/src/tui/app/input-parsers.mjs +193 -0
  234. package/src/tui/app/maintenance-pickers.mjs +324 -0
  235. package/src/tui/app/model-options.mjs +330 -0
  236. package/src/tui/app/model-picker.mjs +365 -0
  237. package/src/tui/app/onboarding-steps.mjs +400 -0
  238. package/src/tui/app/project-picker.mjs +247 -0
  239. package/src/tui/app/provider-setup-picker.mjs +580 -0
  240. package/src/tui/app/resume-picker.mjs +55 -0
  241. package/src/tui/app/route-pickers.mjs +419 -0
  242. package/src/tui/app/settings-picker.mjs +490 -0
  243. package/src/tui/app/slash-commands.mjs +101 -0
  244. package/src/tui/app/slash-dispatch.mjs +427 -0
  245. package/src/tui/app/text-layout.mjs +46 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  247. package/src/tui/app/transcript-window.mjs +671 -0
  248. package/src/tui/app/use-mouse-input.mjs +460 -0
  249. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  250. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  251. package/src/tui/app/use-transcript-window.mjs +589 -0
  252. package/src/tui/components/ConfirmBar.jsx +1 -1
  253. package/src/tui/components/Picker.jsx +32 -4
  254. package/src/tui/components/PromptInput.jsx +23 -101
  255. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  256. package/src/tui/components/StatusLine.jsx +63 -12
  257. package/src/tui/components/TextEntryPanel.jsx +11 -0
  258. package/src/tui/components/ToolExecution.jsx +52 -594
  259. package/src/tui/components/TranscriptItem.jsx +105 -0
  260. package/src/tui/components/UsagePanel.jsx +18 -4
  261. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  262. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  263. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  264. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  265. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  266. package/src/tui/display-width.mjs +20 -3
  267. package/src/tui/dist/index.mjs +19652 -18630
  268. package/src/tui/engine/agent-job-feed.mjs +133 -0
  269. package/src/tui/engine/notification-plan.mjs +76 -0
  270. package/src/tui/engine/render-timing.mjs +17 -0
  271. package/src/tui/engine/tool-approval.mjs +94 -0
  272. package/src/tui/engine/tool-card-results.mjs +234 -0
  273. package/src/tui/engine/tool-result-status.mjs +135 -0
  274. package/src/tui/engine.mjs +122 -562
  275. package/src/tui/figures.mjs +5 -0
  276. package/src/tui/index.jsx +105 -0
  277. package/src/tui/input-editing.mjs +2 -2
  278. package/src/tui/markdown/format-token.mjs +4 -1
  279. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  280. package/src/tui/theme.mjs +6 -0
  281. package/src/ui/statusline-agents.mjs +213 -0
  282. package/src/ui/statusline-format.mjs +146 -0
  283. package/src/ui/statusline-segments.mjs +148 -0
  284. package/src/ui/statusline.mjs +67 -501
  285. package/src/ui/tool-card.mjs +0 -1
  286. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  287. package/src/workflows/default/WORKFLOW.md +1 -1
  288. package/src/workflows/sequential/WORKFLOW.md +1 -1
  289. package/vendor/ink/build/display-width.js +19 -3
  290. package/vendor/ink/build/ink.js +103 -6
  291. package/vendor/ink/build/log-update.js +17 -3
  292. package/vendor/ink/build/wrap-text.js +125 -0
  293. package/scripts/_test-folder-dialog.mjs +0 -30
  294. package/scripts/fix-brief-fn.mjs +0 -35
  295. package/scripts/fix-format-tool-surface.mjs +0 -24
  296. package/scripts/fix-tool-exec-visible.mjs +0 -42
  297. package/scripts/patch-agent-brief.mjs +0 -48
  298. package/scripts/patch-app.mjs +0 -21
  299. package/scripts/patch-app2.mjs +0 -18
  300. package/scripts/patch-dist-brief.mjs +0 -96
  301. package/scripts/patch-tool-exec.mjs +0 -70
  302. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  303. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  304. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  305. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  306. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  307. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  308. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  309. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  310. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  311. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  312. package/src/standalone/agent-task-status.test.mjs +0 -76
  313. package/src/tui/components/tool-output-format.test.mjs +0 -399
  314. package/src/tui/display-width.test.mjs +0 -35
  315. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  316. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  317. package/src/tui/input-editing.selection.test.mjs +0 -75
  318. package/src/tui/markdown/format-token.test.mjs +0 -354
  319. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  320. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  321. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  322. package/src/tui/paste-fix.test.mjs +0 -119
  323. package/src/tui/prompt-history-store.test.mjs +0 -52
  324. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  325. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  326. package/src/ui/markdown.test.mjs +0 -70
  327. package/src/ui/statusline-context-label.test.mjs +0 -15
  328. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  329. 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
-