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,35 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import stringWidth from 'string-width';
4
- import { displayWidthWith, isProblemCodePoint } from './display-width.mjs';
5
-
6
- // Policy ON: the two problem ranges become 2 cells; everything else unchanged.
7
- test('policy ON widens circled digits and arrows only', () => {
8
- assert.equal(displayWidthWith('\u2460', true), 2); // ① circled digit
9
- assert.equal(displayWidthWith('\u2192', true), 2); // → rightwards arrow
10
- assert.equal(displayWidthWith('a', true), 1); // ASCII unchanged
11
- // Box-drawing / figures must NOT be widened by the policy.
12
- assert.equal(displayWidthWith('\u2514', true), stringWidth('\u2514')); // └
13
- assert.equal(displayWidthWith('\u2502', true), stringWidth('\u2502')); // │
14
- assert.equal(displayWidthWith('\u2500', true), stringWidth('\u2500')); // ─
15
- assert.equal(displayWidthWith('\u25cf', true), stringWidth('\u25cf')); // ●
16
- // Mixed string: base + one +1 per problem code point.
17
- assert.equal(displayWidthWith('a\u2460b\u2192', true), stringWidth('a\u2460b\u2192') + 2);
18
- });
19
-
20
- // Policy OFF: byte-for-byte identical to plain string-width.
21
- test('policy OFF equals string-width', () => {
22
- for (const s of ['\u2460', '\u2192', 'a', '\u2514', 'hello \u2192 world', '']) {
23
- assert.equal(displayWidthWith(s, false), stringWidth(s));
24
- }
25
- });
26
-
27
- test('problem range membership', () => {
28
- assert.equal(isProblemCodePoint(0x2460), true);
29
- assert.equal(isProblemCodePoint(0x24ff), true);
30
- assert.equal(isProblemCodePoint(0x2190), true);
31
- assert.equal(isProblemCodePoint(0x21ff), true);
32
- assert.equal(isProblemCodePoint(0x2500), false); // box-drawing
33
- assert.equal(isProblemCodePoint(0x245f), false); // just below range
34
- assert.equal(isProblemCodePoint(0x2200), false); // just above arrows
35
- });
@@ -1,115 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { resolveTuiRuntimeNotificationDelivery } from "./engine.mjs";
4
- import { toolCompletionMeta } from "../runtime/shared/tool-execution-contract.mjs";
5
-
6
- const completedBackgroundTask = [
7
- "background task",
8
- "task_id: task_agent_abc123",
9
- "surface: agent",
10
- "operation: run",
11
- "status: completed",
12
- "",
13
- "worker finished successfully",
14
- ].join("\n");
15
-
16
- const runningBackgroundTask = [
17
- "background task",
18
- "task_id: task_agent_abc123",
19
- "surface: agent",
20
- "operation: run",
21
- "status: running",
22
- ].join("\n");
23
-
24
- const statusOnlyCompletedBackgroundTask = [
25
- "background task",
26
- "task_id: task_agent_abc123",
27
- "surface: agent",
28
- "operation: run",
29
- "status: completed",
30
- ].join("\n");
31
-
32
- const agentTaskEnvelopeWithBody = [
33
- "agent task: task_agent_xyz",
34
- "status: completed",
35
- "",
36
- "done",
37
- ].join("\n");
38
-
39
- const nestedAgentTaskInBackgroundTask = [
40
- "background task",
41
- "task_id: task_agent_abc123",
42
- "surface: agent",
43
- "operation: run",
44
- "status: completed",
45
- "",
46
- "agent task: task_agent_nested",
47
- "status: completed",
48
- "",
49
- "nested worker result",
50
- ].join("\n");
51
-
52
- test("execution completion uses wrapper model content not raw enqueue text", () => {
53
- const meta = toolCompletionMeta({
54
- surface: "agent",
55
- id: "task_agent_abc123",
56
- status: "completed",
57
- context: { callerSessionId: "sess_owner" },
58
- });
59
- const delivery = resolveTuiRuntimeNotificationDelivery({ meta }, completedBackgroundTask);
60
- assert.equal(delivery.action, "execution-ui");
61
- assert.equal(delivery.displayText, completedBackgroundTask);
62
- assert.ok(delivery.modelContent);
63
- assert.match(delivery.modelContent, /worker finished successfully/);
64
- assert.doesNotMatch(delivery.modelContent, /^background task\b/m);
65
- assert.notEqual(delivery.modelContent, completedBackgroundTask);
66
- });
67
-
68
- test("running execution notification is UI-only with empty model content", () => {
69
- const meta = toolCompletionMeta({
70
- surface: "agent",
71
- id: "task_agent_abc123",
72
- status: "running",
73
- context: { callerSessionId: "sess_owner" },
74
- });
75
- const delivery = resolveTuiRuntimeNotificationDelivery({ meta }, runningBackgroundTask);
76
- assert.equal(delivery.action, "execution-ui");
77
- assert.equal(delivery.modelContent, "");
78
- });
79
-
80
- test("nested agent task body inside completed background task is not status-only", () => {
81
- const meta = toolCompletionMeta({
82
- surface: "agent",
83
- id: "task_agent_abc123",
84
- status: "completed",
85
- context: { callerSessionId: "sess_owner" },
86
- });
87
- const delivery = resolveTuiRuntimeNotificationDelivery({ meta }, nestedAgentTaskInBackgroundTask);
88
- assert.equal(delivery.action, "execution-ui");
89
- assert.ok(String(delivery.modelContent || "").trim());
90
- assert.match(delivery.modelContent, /nested worker result/);
91
- });
92
-
93
- test("agent task envelope with blank-line body is not status-only", () => {
94
- const meta = toolCompletionMeta({
95
- surface: "agent",
96
- id: "task_agent_xyz",
97
- status: "completed",
98
- context: { callerSessionId: "sess_owner" },
99
- });
100
- const delivery = resolveTuiRuntimeNotificationDelivery({ meta }, agentTaskEnvelopeWithBody);
101
- assert.equal(delivery.action, "execution-ui");
102
- assert.ok(String(delivery.modelContent || "").trim());
103
- });
104
-
105
- test("header-only completed background task is status-only", () => {
106
- const meta = toolCompletionMeta({
107
- surface: "agent",
108
- id: "task_agent_abc123",
109
- status: "completed",
110
- context: { callerSessionId: "sess_owner" },
111
- });
112
- const delivery = resolveTuiRuntimeNotificationDelivery({ meta }, statusOnlyCompletedBackgroundTask);
113
- assert.equal(delivery.action, "status-only");
114
- assert.equal(delivery.modelContent, "");
115
- });
@@ -1,75 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import {
4
- toolAggregateDetailFallback,
5
- toolGroupedDisplayFallback,
6
- toolResultText,
7
- } from './engine.mjs';
8
-
9
- test('toolResultText prefers nested content over empty scalar fields', () => {
10
- assert.equal(
11
- toolResultText({ text: '', content: [{ type: 'text', text: 'nested body' }] }),
12
- 'nested body',
13
- );
14
- });
15
-
16
- test('toolResultText unwraps nested tool_result blocks', () => {
17
- assert.equal(
18
- toolResultText({
19
- type: 'tool_result',
20
- content: [{ type: 'tool_result', content: 'inner payload' }],
21
- }),
22
- 'inner payload',
23
- );
24
- });
25
-
26
- test('toolResultText preserves explicit empty tool_result clears', () => {
27
- assert.equal(toolResultText({ type: 'tool_result', content: '' }), '');
28
- });
29
-
30
- test('toolResultText preserves empty tool_result content arrays', () => {
31
- assert.equal(toolResultText({ type: 'tool_result', content: [] }), '');
32
- assert.notEqual(toolResultText({ type: 'tool_result', content: [] }), '{"type":"tool_result","content":[]}');
33
- });
34
-
35
- test('toolResultText preserves empty typed text parts inside tool_result', () => {
36
- assert.equal(
37
- toolResultText({ type: 'tool_result', content: [{ type: 'text', text: '' }] }),
38
- '',
39
- );
40
- });
41
-
42
- test('toolResultText preserves empty object-shaped tool_result inner content', () => {
43
- assert.equal(toolResultText({ type: 'tool_result', content: {} }), '');
44
- assert.equal(
45
- toolResultText({ type: 'tool_result', content: { type: 'text', text: '' } }),
46
- '',
47
- );
48
- });
49
-
50
- test('toolResultText still extracts plain strings and structured bodies', () => {
51
- assert.equal(toolResultText('plain body'), 'plain body');
52
- assert.equal(
53
- toolResultText([{ type: 'text', text: 'line one' }, { type: 'text', text: 'line two' }]),
54
- 'line one\nline two',
55
- );
56
- });
57
-
58
- test('toolResultText maps every top-level array element without using index as depth', () => {
59
- const parts = Array.from({ length: 14 }, (_, i) => ({ type: 'text', text: `line-${i}` }));
60
- const out = toolResultText(parts);
61
- assert.match(out, /^line-0\n/);
62
- assert.match(out, /\nline-13$/);
63
- assert.equal(out.split('\n').length, 14);
64
- });
65
-
66
- test('toolAggregateDetailFallback uses raw body when summary detail is empty', () => {
67
- assert.equal(toolAggregateDetailFallback('', 'file contents here'), 'file contents here');
68
- });
69
-
70
- test('toolGroupedDisplayFallback uses later extracted body when grouped text is empty', () => {
71
- assert.equal(toolGroupedDisplayFallback('', '', 'recovered body'), 'recovered body');
72
- assert.equal(toolGroupedDisplayFallback('group summary', '', 'later body'), 'group summary');
73
- });
74
-
75
-
@@ -1,75 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import {
4
- moveCursor,
5
- previousWordOffset,
6
- nextWordOffset,
7
- selectionRange,
8
- verticalOffset,
9
- wordRangeAt,
10
- lineStart,
11
- lineEnd,
12
- } from './input-editing.mjs';
13
-
14
- // Whole-word extend (Ctrl+Shift+Left/Right) relies on moveCursor(extend:true)
15
- // starting/keeping an anchor while jumping by word.
16
- test('ctrl+shift+right extends selection by whole word', () => {
17
- const draft = { value: 'hello world foo', cursor: 0, selectionAnchor: null };
18
- const next = moveCursor(draft, nextWordOffset(draft.value, draft.cursor), { extend: true });
19
- assert.equal(next.cursor, 'hello'.length);
20
- assert.equal(next.selectionAnchor, 0);
21
- assert.deepEqual(selectionRange(next), { start: 0, end: 5 });
22
- });
23
-
24
- test('ctrl+shift+left extends selection backward by whole word', () => {
25
- const value = 'hello world';
26
- const draft = { value, cursor: value.length, selectionAnchor: null };
27
- const next = moveCursor(draft, previousWordOffset(value, value.length), { extend: true });
28
- assert.equal(next.selectionAnchor, value.length);
29
- assert.deepEqual(selectionRange(next), { start: 'hello '.length, end: value.length });
30
- });
31
-
32
- // Shift+Up on the first line yields no vertical move (targetRow < 0) → callers
33
- // then extend to document start (offset 0).
34
- test('verticalOffset returns same cursor on first line (up)', () => {
35
- const value = 'line1\nline2';
36
- const moved = verticalOffset(value, 2, 80, -1, null);
37
- assert.equal(moved.cursor, 2, 'no vertical move available above first line');
38
- });
39
-
40
- test('shift+up to document start extends selection to offset 0', () => {
41
- const value = 'abc';
42
- const draft = { value, cursor: 2, selectionAnchor: null };
43
- const next = moveCursor(draft, 0, { extend: true });
44
- assert.deepEqual(selectionRange(next), { start: 0, end: 2 });
45
- });
46
-
47
- test('shift+down to document end extends selection to value.length', () => {
48
- const value = 'abc';
49
- const draft = { value, cursor: 1, selectionAnchor: null };
50
- const next = moveCursor(draft, value.length, { extend: true });
51
- assert.deepEqual(selectionRange(next), { start: 1, end: 3 });
52
- });
53
-
54
- // Double-click word select: clicking anywhere inside a word run selects the
55
- // whole run, and a click on punctuation/whitespace selects that run instead.
56
- test('wordRangeAt selects the word run under the offset', () => {
57
- const value = 'hello world foo';
58
- assert.deepEqual(wordRangeAt(value, 2), { start: 0, end: 5 });
59
- assert.deepEqual(wordRangeAt(value, 6), { start: 6, end: 11 });
60
- assert.deepEqual(wordRangeAt(value, 0), { start: 0, end: 5 });
61
- assert.deepEqual(wordRangeAt(value, value.length), { start: 12, end: 15 });
62
- });
63
-
64
- test('wordRangeAt selects a punctuation/whitespace run distinctly from words', () => {
65
- const value = 'foo---bar';
66
- assert.deepEqual(wordRangeAt(value, 4), { start: 3, end: 6 });
67
- });
68
-
69
- // Triple-click line select uses lineStart/lineEnd directly.
70
- test('lineStart/lineEnd bound the current line for triple-click select', () => {
71
- const value = 'line1\nline2\nline3';
72
- const offset = value.indexOf('line2') + 2;
73
- assert.equal(lineStart(value, offset), 6);
74
- assert.equal(lineEnd(value, offset), 11);
75
- });
@@ -1,354 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { marked } from 'marked';
4
- import stripAnsi from 'strip-ansi';
5
- import stringWidth from 'string-width';
6
- import {
7
- formatToken,
8
- highlightCodeBlockToLines,
9
- extraColorizers,
10
- _highlightCacheSizeForTests,
11
- } from './format-token.mjs';
12
- import { setThemeSetting, theme } from '../theme.mjs';
13
-
14
- setThemeSetting('basic', { persist: false });
15
-
16
- function rgbSgr(value) {
17
- const m = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/.exec(String(value || ''));
18
- return m ? `38;2;${m[1]};${m[2]};${m[3]}` : null;
19
- }
20
-
21
- function lexFirst(md, type) {
22
- const tokens = marked.lexer(md);
23
- return tokens.find((t) => t.type === type) ?? tokens[0];
24
- }
25
-
26
- test('fenced code block has no ``` fences and shows a language label row', () => {
27
- const token = lexFirst('```js\nconst x = 1;\n```\n', 'code');
28
- const out = formatToken(token, 0, null, null, 60);
29
- const plain = stripAnsi(out);
30
- assert.ok(!plain.includes('```'), 'no literal fence markers');
31
- assert.ok(plain.includes('const x = 1;'), 'body present');
32
- // The first non-empty row is the subtle language label; its trimmed text is
33
- // exactly the bare lang token (gutter trims away).
34
- const rows = plain.split('\n').filter((l) => l.trim().length > 0);
35
- assert.equal(rows[0].trim(), 'js', 'first row is the language label');
36
- });
37
-
38
- test('short code line is gutter-indented with no background band', () => {
39
- const width = 60;
40
- const token = lexFirst('```js\nshort\n```\n', 'code');
41
- const out = formatToken(token, 0, null, null, width);
42
- // No background band: fenced code renders with no `48;2;` bg SGR.
43
- assert.ok(!out.includes('48;2;'), 'no background SGR');
44
- const bodyLine = stripAnsi(out).split('\n').find((l) => l.includes('short'));
45
- assert.ok(bodyLine, 'body line present');
46
- assert.ok(bodyLine.startsWith(' short'), 'body is gutter-indented');
47
- // Content width only — NOT padded out to the full render width.
48
- assert.equal(stringWidth(bodyLine), ' short'.length, 'body row follows content width');
49
- });
50
-
51
- test('code block body lines have no background band and follow content width', () => {
52
- const width = 80;
53
- const token = lexFirst('```js\na\n\nlonger line\n```\n', 'code');
54
- const out = formatToken(token, 0, null, null, width);
55
- assert.ok(!out.includes('48;2;'), 'no background SGR anywhere');
56
- const plainLines = stripAnsi(out).split('\n').filter((l) => l.length > 0);
57
- const widths = plainLines.map((l) => stringWidth(l));
58
- assert.ok(widths.length >= 2, 'body lines rendered');
59
- // Widths follow content (gutter + visible text), not forced equal to render
60
- // width: the longest row stays well under the 80-col render width.
61
- assert.ok(Math.max(...widths) < width, 'rows are not padded to the render width');
62
- });
63
-
64
- test('long code line wraps within requested body width', () => {
65
- const long = 'a'.repeat(40);
66
- const token = lexFirst(`\`\`\`js\n${long}\n\`\`\`\n`, 'code');
67
- const width = 20;
68
- const out = formatToken(token, 0, null, null, width);
69
- const bodyLines = stripAnsi(out).split('\n').filter((l) => l.includes('a'));
70
- assert.ok(bodyLines.length > 1, 'long line is wrapped into multiple rows');
71
- for (const line of bodyLines) {
72
- assert.ok(line.length <= width, `wrapped line "${line}" fits width ${width}`);
73
- }
74
- assert.equal(bodyLines.join('').replace(/\s/g, ''), long, 'wrapped segments preserve content');
75
- });
76
-
77
- test('fenced code expands tabs so width math matches the terminal', () => {
78
- // A raw \t counts as ZERO cells in string-width ("Tabs are ignored by
79
- // design"), so without normalization the wrap/row math under-counts a
80
- // tab-bearing line and the terminal-expanded line bleeds past the viewport
81
- // clip. After normalization every rendered row must contain no raw \t and
82
- // its visible width must equal its character length (tabs → spaces).
83
- const token = lexFirst('```\n\tindented\n```\n', 'code');
84
- const out = formatToken(token, 0, null, null, 60);
85
- const plain = stripAnsi(out);
86
- assert.ok(!plain.includes('\t'), 'no raw tab survives into the rendered string');
87
- const bodyLine = plain.split('\n').find((l) => l.includes('indented'));
88
- assert.ok(bodyLine, 'body line present');
89
- assert.equal(stringWidth(bodyLine), bodyLine.length, 'visible width equals char length (no zero-width tab)');
90
- });
91
-
92
- test('fenced code strips stray C0 control chars except newline', () => {
93
- // A bare \x07 (BELL) etc. measures as zero width but can move the terminal
94
- // cursor; replace with a space so ink's row accounting holds.
95
- const token = lexFirst('```\na\x07b\n```\n', 'code');
96
- const out = formatToken(token, 0, null, null, 60);
97
- const plain = stripAnsi(out);
98
- assert.ok(!/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(plain), 'no raw C0/DEL control survives');
99
- const bodyLine = plain.split('\n').find((l) => l.includes('a') && l.includes('b'));
100
- assert.ok(bodyLine, 'body line present');
101
- assert.equal(stringWidth(bodyLine), bodyLine.length, 'control char became a real space cell');
102
- });
103
-
104
- test('list item fenced code block respects narrowed width and has no fences', () => {
105
- const long = 'b'.repeat(36);
106
- const codeTok = lexFirst(`\`\`\`js\n${long}\n\`\`\`\n`, 'code');
107
- const listTok = {
108
- type: 'list',
109
- ordered: false,
110
- items: [{ type: 'list_item', tokens: [codeTok] }],
111
- };
112
- const outerWidth = 22;
113
- const out = formatToken(listTok, 0, null, null, outerWidth);
114
- const plain = stripAnsi(out);
115
- assert.ok(!plain.includes('```'), 'no literal fence markers');
116
- const prefixed = plain.split('\n').filter((l) => l.includes('b'));
117
- assert.ok(prefixed.length > 1, 'nested list code wraps under narrow width');
118
- for (const line of prefixed) {
119
- assert.ok(stringWidth(line) <= outerWidth, `line fits outer width: "${line}"`);
120
- }
121
- });
122
-
123
- test('blockquote fenced code block respects narrowed width and has no fences', () => {
124
- const long = 'c'.repeat(36);
125
- const codeTok = lexFirst(`\`\`\`js\n${long}\n\`\`\`\n`, 'code');
126
- const quoteTok = { type: 'blockquote', tokens: [codeTok] };
127
- const outerWidth = 24;
128
- const out = formatToken(quoteTok, 0, null, null, outerWidth);
129
- const plain = stripAnsi(out);
130
- assert.ok(!plain.includes('```'), 'no literal fence markers');
131
- const lines = plain.split('\n').filter((l) => l.includes('c'));
132
- assert.ok(lines.length > 1, 'blockquote code wraps under narrow width');
133
- for (const line of lines) {
134
- assert.ok(stringWidth(line) <= outerWidth, `line fits outer width: "${line}"`);
135
- }
136
- });
137
-
138
- test('blockquote fenced code with blank line keeps quote prefix on every code row', () => {
139
- const codeTok = lexFirst('```js\na\n\nb\n```\n', 'code');
140
- const quoteTok = { type: 'blockquote', tokens: [codeTok] };
141
- const outerWidth = 40;
142
- const out = formatToken(quoteTok, 0, null, null, outerWidth);
143
- const plain = stripAnsi(out);
144
- assert.ok(!plain.includes('```'), 'no literal fence markers');
145
- const bar = '\u258e';
146
- const visibleLines = plain.split('\n').filter((l) => stringWidth(l) > 0);
147
- assert.ok(visibleLines.length >= 3, 'two body lines + blank padded row');
148
- for (const line of visibleLines) {
149
- assert.ok(line.startsWith(bar), `quote bar on every visible row: "${line}"`);
150
- assert.ok(stringWidth(line) <= outerWidth, `line fits outer width: "${line}"`);
151
- }
152
- const blankPadded = visibleLines.find((l) => !l.includes('a') && !l.includes('b'));
153
- assert.ok(blankPadded, 'background-padded blank code row is rendered');
154
- assert.ok(blankPadded.startsWith(bar), 'blank padded code row keeps quote prefix');
155
- });
156
-
157
- test('two-level nested list code block does not double-subtract list prefix width', () => {
158
- const long = 'd'.repeat(40);
159
- const codeTok = lexFirst(`\`\`\`js\n${long}\n\`\`\`\n`, 'code');
160
- const innerList = {
161
- type: 'list',
162
- ordered: false,
163
- items: [{ type: 'list_item', tokens: [codeTok] }],
164
- };
165
- const outerList = {
166
- type: 'list',
167
- ordered: false,
168
- items: [{ type: 'list_item', tokens: [innerList] }],
169
- };
170
- const outerWidth = 30;
171
- const singleLevelNested = formatToken(innerList, 2, null, null, outerWidth);
172
- const twoLevelNested = formatToken(outerList, 0, null, null, outerWidth);
173
- const codeLines = (plain) => plain.split('\n').filter((l) => l.includes('d'));
174
- const singleLines = codeLines(stripAnsi(singleLevelNested));
175
- const nestedLines = codeLines(stripAnsi(twoLevelNested));
176
- assert.equal(
177
- nestedLines.length,
178
- singleLines.length,
179
- 'nested list code wraps like one-level nested list at the same outer width',
180
- );
181
- assert.ok(nestedLines.length > 1, 'code is wrapped under narrow width');
182
- });
183
-
184
- test('js highlighting colors keyword, string, number distinctly', () => {
185
- const token = lexFirst('```js\nconst n = 42;\nlet s = "hi";\n```\n', 'code');
186
- const out = formatToken(token);
187
- assert.ok(out.includes(rgbSgr(theme.syntaxKeyword)), 'keyword colored');
188
- assert.ok(out.includes(rgbSgr(theme.syntaxNumber)), 'number colored');
189
- assert.ok(out.includes(rgbSgr(theme.syntaxString)), 'string colored');
190
- });
191
-
192
- test('highlight cache serves repeat block highlight without changing output', () => {
193
- const c = extraColorizers();
194
- const text = `const cacheProbe_${Date.now()} = 1;\nlet y = 2;\n`;
195
- const sizeBefore = _highlightCacheSizeForTests();
196
- const first = highlightCodeBlockToLines(text, 'js', c);
197
- assert.equal(_highlightCacheSizeForTests(), sizeBefore + 1, 'first highlight populates cache');
198
- const second = highlightCodeBlockToLines(text, 'js', c);
199
- assert.deepEqual(second, first);
200
- assert.equal(_highlightCacheSizeForTests(), sizeBefore + 1, 'cache hit does not grow entries');
201
- assert.ok(first[0].includes('38;2;'), 'highlighted ANSI present');
202
- });
203
-
204
- test('inline codespan is accent-colored with no background band', () => {
205
- const tokens = marked.lexer('`hello`');
206
- const codespan = tokens[0].tokens[0];
207
- assert.equal(codespan.type, 'codespan');
208
- const out = formatToken(codespan);
209
- // Inline code uses accent color only — no bg box behind the span (a tinted
210
- // band behind inline text reads as awkward against body prose).
211
- assert.ok(!out.includes('48;2;'), 'no background SGR');
212
- assert.ok(stripAnsi(out).includes('hello'));
213
- assert.ok(out.includes(rgbSgr(theme.mdCode)), 'inline code accent');
214
- });
215
-
216
- test('kotlin fence resolves and highlights', () => {
217
- const token = lexFirst('```kotlin\nfun main() {}\n```\n', 'code');
218
- const out = formatToken(token);
219
- assert.ok(out.includes(rgbSgr(theme.syntaxKeyword)) || out.includes(rgbSgr(theme.mdCodeBlock)));
220
- assert.ok(stripAnsi(out).includes('fun main'));
221
- });
222
-
223
- test('typescript fence uses typescript highlighter', () => {
224
- const token = lexFirst('```ts\nconst x: number = 1;\n```\n', 'code');
225
- const out = formatToken(token);
226
- assert.ok(out.includes('38;2;'));
227
- assert.ok(stripAnsi(out).includes('const x'));
228
- });
229
-
230
- test('diff fenced block colors add/remove/hunk/header separately', () => {
231
- const diff = [
232
- '```diff',
233
- 'diff --git a/x b/x',
234
- '--- a/x',
235
- '+++ b/x',
236
- '@@ -1,2 +1,2 @@',
237
- '-old line',
238
- '+new line',
239
- ' context line',
240
- '```',
241
- '',
242
- ].join('\n');
243
- const token = lexFirst(diff, 'code');
244
- const out = formatToken(token);
245
- const added = rgbSgr(theme.mdDiffAdded);
246
- const removed = rgbSgr(theme.mdDiffRemoved);
247
- const hunk = rgbSgr(theme.mdDiffHunk);
248
- const header = rgbSgr(theme.mdDiffHeader);
249
- assert.ok(out.includes(added), 'added line color present');
250
- assert.ok(out.includes(removed), 'removed line color present');
251
- assert.ok(out.includes(hunk), 'hunk line color present');
252
- assert.ok(out.includes(header), 'header line color present');
253
- assert.equal(new Set([added, removed, hunk, header]).size, 4);
254
- assert.ok(out.indexOf('+++ b/x') !== -1);
255
- });
256
-
257
- test('plus/minus file headers are not treated as add/remove', () => {
258
- const diff = '```patch\n--- a/f\n+++ b/f\n@@ -0,0 +1 @@\n+x\n```\n';
259
- const token = lexFirst(diff, 'code');
260
- const out = formatToken(token);
261
- const headerSgr = rgbSgr(theme.mdDiffHeader);
262
- const addedSgr = rgbSgr(theme.mdDiffAdded);
263
- const idxHeader = out.indexOf('+++ b/f');
264
- const before = out.slice(0, idxHeader);
265
- const lastHeader = before.lastIndexOf(headerSgr);
266
- const lastAdded = before.lastIndexOf(addedSgr);
267
- assert.ok(lastHeader > lastAdded, 'plus-plus-plus header uses header color, not added');
268
- });
269
-
270
- test('bare unified diff (no lang) is detected and colored', () => {
271
- const token = lexFirst('```\n@@ -1 +1 @@\n-a\n+b\n```\n', 'code');
272
- const out = formatToken(token);
273
- assert.ok(out.includes(rgbSgr(theme.mdDiffHunk)), 'hunk colored on bare diff');
274
- assert.ok(out.includes(rgbSgr(theme.mdDiffAdded)), 'add colored on bare diff');
275
- });
276
-
277
- test('unknown language falls back to flat code block color', () => {
278
- const token = lexFirst('```foobarlang\nsome text\n```\n', 'code');
279
- const out = formatToken(token);
280
- assert.ok(out.includes(rgbSgr(theme.mdCodeBlock)), 'flat code-block color used');
281
- assert.ok(stripAnsi(out).includes('some text'), 'body is flush-left');
282
- });
283
-
284
- test('inline link emits OSC 8 hyperlink with styled label (URL hidden)', () => {
285
- const token = lexFirst('[label](https://example.com)', 'paragraph');
286
- const out = formatToken(token);
287
- // OSC 8 wraps the visible label; the raw URL lives in the escape, not as
288
- // on-screen text, so stripAnsi keeps only the label.
289
- assert.ok(out.includes('\x1b]8;;https://example.com\x07'), 'OSC 8 link target present');
290
- assert.ok(out.includes('\x1b]8;;\x07'), 'OSC 8 close present');
291
- assert.ok(stripAnsi(out).includes('label'), 'link label text visible');
292
- assert.ok(out.includes(rgbSgr(theme.mdLinkText)), 'link label colored');
293
- });
294
-
295
- test('bare-url link emits OSC 8 with the URL as visible clickable text', () => {
296
- const token = lexFirst('<https://example.com>', 'paragraph');
297
- const out = formatToken(token);
298
- assert.ok(out.includes('\x1b]8;;https://example.com\x07'), 'OSC 8 link target present');
299
- assert.ok(stripAnsi(out).includes('https://example.com'), 'URL visible as text');
300
- assert.ok(out.includes(rgbSgr(theme.mdLink)), 'link URL colored');
301
- });
302
-
303
- test('strong and em use distinct mdStrong / mdEmph colors', () => {
304
- const strongTok = lexFirst('**bold**', 'paragraph');
305
- const emTok = lexFirst('*ital*', 'paragraph');
306
- const strongOut = formatToken(strongTok);
307
- const emOut = formatToken(emTok);
308
- // Bold/italic carry weight/style only — NO color tint (a colored
309
- // strong/em clashes per-theme and reads too loud).
310
- assert.ok(strongOut.includes('\x1b[1m'), 'strong is bold');
311
- assert.ok(emOut.includes('\x1b[3m'), 'em is italic');
312
- assert.ok(!strongOut.includes(rgbSgr(theme.mdStrong)), 'strong is not color-tinted');
313
- assert.ok(!emOut.includes(rgbSgr(theme.mdEmph)), 'em is not color-tinted');
314
- });
315
-
316
- test('h1 heading is bold, italic, and underlined', () => {
317
- const token = lexFirst('# Title', 'heading');
318
- const out = formatToken(token);
319
- assert.ok(out.includes('\x1b[1m'), 'h1 is bold');
320
- assert.ok(out.includes('\x1b[3m'), 'h1 is italic');
321
- assert.ok(out.includes('\x1b[4m'), 'h1 is underlined');
322
- });
323
-
324
- test('ordered list markers follow nesting depth (1. / a. / i.)', () => {
325
- const md = [
326
- '1. one',
327
- ' 1. two',
328
- ' 1. three',
329
- ' 1. four',
330
- ].join('\n');
331
- const tokens = marked.lexer(md);
332
- const plain = stripAnsi(tokens.map((t) => formatToken(t)).join(''));
333
- assert.ok(plain.includes('1. one'), 'depth 0 arabic');
334
- assert.ok(plain.includes('1. two'), 'depth 1 arabic');
335
- assert.ok(plain.includes('a. three'), 'depth 2 lowercase letter');
336
- assert.ok(plain.includes('i. four'), 'depth 3 lowercase roman');
337
- });
338
-
339
- test('every palette defines the full extended key set', () => {
340
- const required = [
341
- 'mdCodeBlockBorder', 'mdCodeBlockBg', 'mdCodeSpanBg', 'mdLink', 'mdLinkText', 'mdStrong', 'mdEmph',
342
- 'mdDiffAdded', 'mdDiffRemoved', 'mdDiffHunk', 'mdDiffHeader', 'mdDiffContext',
343
- 'mdDiffAddedBg', 'mdDiffRemovedBg',
344
- 'syntaxComment', 'syntaxKeyword', 'syntaxFunction', 'syntaxVariable',
345
- 'syntaxString', 'syntaxNumber', 'syntaxType', 'syntaxOperator', 'syntaxPunctuation',
346
- ];
347
- for (const id of ['basic', 'indigo', 'warm', 'light', 'teal', 'onedark', 'tokyonight', 'kanagawa', 'catppuccin', 'dracula', 'rosepine', 'nord', 'gruvbox', 'everforest']) {
348
- setThemeSetting(id, { persist: false });
349
- for (const key of required) {
350
- assert.ok(/^rgb\(\d+,\s*\d+,\s*\d+\)$/.test(String(theme[key])), `${id}.${key} is an rgb() string`);
351
- }
352
- }
353
- setThemeSetting('basic', { persist: false });
354
- });