mixdog 0.9.1 → 0.9.3

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 (301) hide show
  1. package/package.json +9 -1
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  6. package/scripts/background-task-meta-smoke.mjs +1 -1
  7. package/scripts/bench-run.mjs +262 -0
  8. package/scripts/build-tui.mjs +13 -1
  9. package/scripts/compact-smoke.mjs +12 -0
  10. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  11. package/scripts/explore-bench.mjs +124 -0
  12. package/scripts/hook-bus-test.mjs +191 -0
  13. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  14. package/scripts/internal-comms-bench.mjs +727 -0
  15. package/scripts/internal-comms-smoke.mjs +75 -0
  16. package/scripts/lead-workflow-smoke.mjs +4 -4
  17. package/scripts/live-worker-smoke.mjs +9 -9
  18. package/scripts/output-style-bench.mjs +285 -0
  19. package/scripts/output-style-smoke.mjs +13 -10
  20. package/scripts/patch-replay.mjs +90 -0
  21. package/scripts/path-suffix-test.mjs +57 -0
  22. package/scripts/provider-stream-stall-test.mjs +276 -0
  23. package/scripts/provider-toolcall-test.mjs +599 -1
  24. package/scripts/recall-bench.mjs +207 -0
  25. package/scripts/routing-corpus.mjs +281 -0
  26. package/scripts/session-bench.mjs +1526 -0
  27. package/scripts/session-diag.mjs +595 -0
  28. package/scripts/task-bench.mjs +207 -0
  29. package/scripts/tool-failures.mjs +6 -6
  30. package/scripts/tool-smoke.mjs +310 -67
  31. package/scripts/toolcall-args-test.mjs +81 -0
  32. package/src/agents/debugger/AGENT.md +4 -4
  33. package/src/agents/heavy-worker/AGENT.md +20 -9
  34. package/src/agents/reviewer/AGENT.md +4 -4
  35. package/src/agents/worker/AGENT.md +17 -9
  36. package/src/app.mjs +10 -6
  37. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  38. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  39. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  40. package/src/headless-role.mjs +14 -14
  41. package/src/help.mjs +1 -0
  42. package/src/lib/rules-builder.cjs +32 -54
  43. package/src/mixdog-session-runtime.mjs +1040 -2036
  44. package/src/output-styles/default.md +12 -7
  45. package/src/output-styles/minimal.md +25 -0
  46. package/src/output-styles/oneline.md +21 -0
  47. package/src/output-styles/simple.md +10 -9
  48. package/src/repl.mjs +17 -7
  49. package/src/rules/agent/00-common.md +7 -5
  50. package/src/rules/agent/30-explorer.md +8 -12
  51. package/src/rules/lead/01-general.md +3 -1
  52. package/src/rules/lead/lead-tool.md +13 -2
  53. package/src/rules/shared/01-tool.md +23 -12
  54. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  55. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  59. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  60. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  61. package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
  62. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  63. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
  65. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  66. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
  70. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
  71. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
  72. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  73. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  74. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  75. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  76. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
  78. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
  79. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
  80. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  82. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
  83. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
  85. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  86. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  87. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  88. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
  89. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  90. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  91. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
  93. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  94. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  97. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  98. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  99. package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
  100. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
  101. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  102. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
  103. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  104. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  105. package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
  106. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  107. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  108. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  109. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  110. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  113. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  116. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  118. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  119. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  121. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
  123. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  124. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  125. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
  126. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
  127. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  129. package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
  130. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  131. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  132. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  135. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  136. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  137. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  138. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  139. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
  140. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  141. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  142. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
  148. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
  149. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  150. package/src/runtime/channels/backends/discord.mjs +99 -9
  151. package/src/runtime/channels/backends/telegram.mjs +501 -0
  152. package/src/runtime/channels/index.mjs +437 -1439
  153. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  154. package/src/runtime/channels/lib/config.mjs +54 -2
  155. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  156. package/src/runtime/channels/lib/format.mjs +4 -2
  157. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  158. package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
  159. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  160. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  161. package/src/runtime/channels/lib/telegram-format.mjs +280 -0
  162. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  164. package/src/runtime/channels/lib/webhook.mjs +59 -31
  165. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  166. package/src/runtime/channels/tool-defs.mjs +1 -1
  167. package/src/runtime/memory/index.mjs +465 -345
  168. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  169. package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
  170. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  171. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  172. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  173. package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
  174. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  175. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  176. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  177. package/src/runtime/memory/lib/memory.mjs +121 -4
  178. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  179. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  180. package/src/runtime/memory/lib/recall-format.mjs +183 -0
  181. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  182. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  183. package/src/runtime/memory/tool-defs.mjs +10 -7
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/background-tasks.mjs +2 -3
  186. package/src/runtime/shared/buffered-appender.mjs +149 -0
  187. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  188. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  189. package/src/runtime/shared/config.mjs +9 -0
  190. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  191. package/src/runtime/shared/schedules-store.mjs +21 -19
  192. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  193. package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
  194. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  195. package/src/runtime/shared/tool-surface.mjs +98 -13
  196. package/src/runtime/shared/transcript-writer.mjs +156 -0
  197. package/src/runtime/shared/update-checker.mjs +214 -0
  198. package/src/session-runtime/config-helpers.mjs +209 -0
  199. package/src/session-runtime/effort.mjs +128 -0
  200. package/src/session-runtime/fs-utils.mjs +10 -0
  201. package/src/session-runtime/model-capabilities.mjs +130 -0
  202. package/src/session-runtime/output-styles.mjs +124 -0
  203. package/src/session-runtime/plugin-mcp.mjs +114 -0
  204. package/src/session-runtime/session-text.mjs +100 -0
  205. package/src/session-runtime/statusline-route.mjs +35 -0
  206. package/src/session-runtime/tool-catalog.mjs +720 -0
  207. package/src/session-runtime/workflow.mjs +358 -0
  208. package/src/standalone/agent-tool.mjs +302 -117
  209. package/src/standalone/channel-admin.mjs +133 -40
  210. package/src/standalone/channel-worker.mjs +10 -292
  211. package/src/standalone/explore-tool.mjs +12 -5
  212. package/src/standalone/hook-bus.mjs +165 -8
  213. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  214. package/src/standalone/opencode-go-login.mjs +121 -0
  215. package/src/standalone/provider-admin.mjs +25 -3
  216. package/src/standalone/usage-dashboard.mjs +1 -1
  217. package/src/tui/App.jsx +2883 -778
  218. package/src/tui/components/ConfirmBar.jsx +47 -0
  219. package/src/tui/components/ContextPanel.jsx +5 -3
  220. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  221. package/src/tui/components/Markdown.jsx +22 -98
  222. package/src/tui/components/Message.jsx +14 -35
  223. package/src/tui/components/Picker.jsx +87 -12
  224. package/src/tui/components/PromptInput.jsx +355 -22
  225. package/src/tui/components/QueuedCommands.jsx +1 -1
  226. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  227. package/src/tui/components/Spinner.jsx +7 -7
  228. package/src/tui/components/StatusLine.jsx +40 -21
  229. package/src/tui/components/TextEntryPanel.jsx +51 -7
  230. package/src/tui/components/ToolExecution.jsx +183 -101
  231. package/src/tui/components/TurnDone.jsx +4 -4
  232. package/src/tui/components/UsagePanel.jsx +1 -1
  233. package/src/tui/components/tool-output-format.mjs +161 -23
  234. package/src/tui/components/tool-output-format.test.mjs +87 -0
  235. package/src/tui/display-width.mjs +69 -0
  236. package/src/tui/display-width.test.mjs +35 -0
  237. package/src/tui/dist/index.mjs +6731 -2333
  238. package/src/tui/engine/agent-envelope.mjs +296 -0
  239. package/src/tui/engine/boot-profile.mjs +21 -0
  240. package/src/tui/engine/labels.mjs +67 -0
  241. package/src/tui/engine/notice-text.mjs +112 -0
  242. package/src/tui/engine/queue-helpers.mjs +161 -0
  243. package/src/tui/engine/session-stats.mjs +46 -0
  244. package/src/tui/engine/tool-call-fields.mjs +23 -0
  245. package/src/tui/engine/tool-result-text.mjs +126 -0
  246. package/src/tui/engine.mjs +569 -948
  247. package/src/tui/index.jsx +117 -7
  248. package/src/tui/input-editing.mjs +58 -8
  249. package/src/tui/input-editing.selection.test.mjs +75 -0
  250. package/src/tui/keyboard-protocol.mjs +42 -0
  251. package/src/tui/lib/voice-recorder.mjs +469 -0
  252. package/src/tui/markdown/format-token.mjs +128 -76
  253. package/src/tui/markdown/format-token.test.mjs +61 -19
  254. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  255. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  256. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  257. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  258. package/src/tui/markdown/table-layout.mjs +9 -9
  259. package/src/tui/paste-attachments.mjs +36 -9
  260. package/src/tui/paste-fix.test.mjs +119 -0
  261. package/src/tui/prompt-history-store.mjs +129 -0
  262. package/src/tui/prompt-history-store.test.mjs +52 -0
  263. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  264. package/src/tui/theme.mjs +41 -657
  265. package/src/tui/themes/base.mjs +86 -0
  266. package/src/tui/themes/basic.mjs +85 -0
  267. package/src/tui/themes/catppuccin.mjs +72 -0
  268. package/src/tui/themes/dracula.mjs +70 -0
  269. package/src/tui/themes/everforest.mjs +71 -0
  270. package/src/tui/themes/gruvbox.mjs +71 -0
  271. package/src/tui/themes/index.mjs +71 -0
  272. package/src/tui/themes/indigo.mjs +78 -0
  273. package/src/tui/themes/kanagawa.mjs +80 -0
  274. package/src/tui/themes/light.mjs +81 -0
  275. package/src/tui/themes/nord.mjs +72 -0
  276. package/src/tui/themes/onedark.mjs +16 -0
  277. package/src/tui/themes/rosepine.mjs +70 -0
  278. package/src/tui/themes/teal.mjs +80 -0
  279. package/src/tui/themes/tokyonight.mjs +79 -0
  280. package/src/tui/themes/utils.mjs +106 -0
  281. package/src/tui/themes/warm.mjs +79 -0
  282. package/src/tui/transcript-tool-failures.mjs +13 -2
  283. package/src/ui/markdown.mjs +1 -1
  284. package/src/ui/model-display.mjs +2 -2
  285. package/src/ui/statusline.mjs +75 -27
  286. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  287. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  288. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  289. package/src/workflows/default/WORKFLOW.md +46 -12
  290. package/src/workflows/sequential/WORKFLOW.md +51 -0
  291. package/src/workflows/solo/WORKFLOW.md +12 -1
  292. package/vendor/ink/build/display-width.js +62 -0
  293. package/vendor/ink/build/ink.js +100 -12
  294. package/vendor/ink/build/measure-text.js +4 -1
  295. package/vendor/ink/build/output.js +115 -9
  296. package/vendor/ink/build/render-node-to-output.js +4 -1
  297. package/vendor/ink/build/render.js +4 -0
  298. package/src/output-styles/extreme-simple.md +0 -20
  299. package/src/rules/lead/04-workflow.md +0 -51
  300. package/src/workflows/default/workflow.json +0 -13
  301. package/src/workflows/solo/workflow.json +0 -7
@@ -12,13 +12,13 @@
12
12
  * - Hyperlinks/issue-ref linkify are dropped (no OSC-8 dependency); link text
13
13
  * is shown plainly with its URL.
14
14
  */
15
+ import { createRequire } from 'node:module';
15
16
  import { Chalk } from 'chalk';
16
- import { highlight, supportsLanguage } from 'cli-highlight';
17
17
  import stripAnsi from 'strip-ansi';
18
- import stringWidth from 'string-width';
19
18
  import wrapAnsi from 'wrap-ansi';
20
19
  import { theme, getThemeVersion } from '../theme.mjs';
21
20
  import { BLOCKQUOTE_BAR, HR_LINE } from '../figures.mjs';
21
+ import { displayWidth } from '../display-width.mjs';
22
22
 
23
23
  // Force truecolor so chalk emits 24-bit SGR even when the ambient level is 0.
24
24
  // ink's <Text> passes these escapes through verbatim.
@@ -27,26 +27,12 @@ const chalk = new Chalk({ level: 3 });
27
27
  // Use \n unconditionally (os.EOL's \r breaks segment mapping).
28
28
  const EOL = '\n';
29
29
 
30
- /** Parse an `rgb(r,g,b)` theme string into a chalk colorizer. */
31
30
  function rgbColor(str) {
32
31
  const m = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/.exec(String(str || ''));
33
32
  if (!m) return (s) => s;
34
33
  return chalk.rgb(Number(m[1]), Number(m[2]), Number(m[3]));
35
34
  }
36
35
 
37
- /**
38
- * Parse an `rgb(r,g,b)` theme string into a TRUECOLOR BACKGROUND wrapper. Emits
39
- * `48;2;R;G;B` … `49` (bg reset) around the string so AnsiText (case 48) maps it
40
- * to an ink backgroundColor, giving a code line a tinted band. Returns identity
41
- * when the string is not a valid rgb() so a missing key never corrupts output.
42
- */
43
- function rgbBg(str) {
44
- const m = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/.exec(String(str || ''));
45
- if (!m) return (s) => s;
46
- const open = `\x1b[48;2;${+m[1]};${+m[2]};${+m[3]}m`;
47
- return (s) => `${open}${s}\x1b[49m`;
48
- }
49
-
50
36
  // Colorizers are derived from the active theme's md* keys. They are cached and
51
37
  // rebuilt only when the theme version changes, so a `/theme` switch takes
52
38
  // effect on the next render without recomputing a chalk fn per token.
@@ -81,8 +67,6 @@ export function extraColorizers() {
81
67
  fenceBorder: rgbColor(theme.mdCodeBlockBorder ?? theme.mdHr),
82
68
  link: rgbColor(theme.mdLink ?? theme.code),
83
69
  linkText: rgbColor(theme.mdLinkText ?? theme.mdCode),
84
- strong: rgbColor(theme.mdStrong ?? theme.mdHeading),
85
- emph: rgbColor(theme.mdEmph ?? theme.mdHeading),
86
70
  // diff/patch
87
71
  diffAdded: rgbColor(theme.mdDiffAdded ?? theme.success),
88
72
  diffRemoved: rgbColor(theme.mdDiffRemoved ?? theme.error),
@@ -100,9 +84,6 @@ export function extraColorizers() {
100
84
  synOperator: rgbColor(theme.syntaxOperator ?? theme.mdCodeBlock),
101
85
  synPunct: rgbColor(theme.syntaxPunctuation ?? theme.mdCodeBlock),
102
86
  body: fallbackBody,
103
- // Truecolor background band for fenced code blocks (per-line wrap).
104
- codeBg: rgbBg(theme.mdCodeBlockBg ?? theme.background),
105
- codeSpanBg: rgbBg(theme.mdCodeSpanBg ?? theme.mdCodeBlockBg ?? theme.background),
106
87
  };
107
88
  return _extra;
108
89
  }
@@ -126,12 +107,65 @@ function decodeEntities(s) {
126
107
  }
127
108
 
128
109
  // ── Fenced code block rendering ─────────────────────────────────────────────
129
- // Flush-left wrapped body with per-line background band (no ``` fences / lang row).
130
- // Diff/patch languages (and bodies that look like unified diffs) keep the diff
131
- // highlighter; other languages use cli-highlight (highlight.js) themed from our
132
- // syntax* palette; unknown languages fall back to flat `mdCodeBlock` body color.
110
+ // Gutter-indented, syntax-highlighted body with NO background band and no ```
111
+ // fences. Diff/patch languages (and bodies that look like unified diffs) keep
112
+ // the diff highlighter; other languages use
113
+ // cli-highlight (highlight.js) themed from our syntax* palette; unknown
114
+ // languages fall back to flat `mdCodeBlock` body color.
133
115
  const DIFF_LANGS = new Set(['diff', 'patch', 'udiff', 'git-diff', 'gitdiff']);
134
116
 
117
+ // Fixed tab size for fenced-code rendering. The exact value is cosmetic; what
118
+ // matters for correctness is that NO raw \t byte survives into the rendered
119
+ // string. string-width counts a tab as ZERO cells ("Tabs are ignored by
120
+ // design"; \p{Control} is a zero-width cluster), so our wrap/row math believes
121
+ // a tab-bearing code line is narrower than it is. ink clips the transcript
122
+ // viewport at the measured height, but the terminal then EXPANDS the surviving
123
+ // tab to the next tab stop — adding physical columns/rows AFTER the clip, which
124
+ // makes a code line near the bottom edge bleed THROUGH into the prompt box on
125
+ // scroll-up. Expanding tabs to literal spaces here makes the measurement and
126
+ // the terminal agree, so the clip holds.
127
+ const CODE_TAB_SIZE = 2;
128
+
129
+ /**
130
+ * Normalize raw fenced-code source for terminal rendering:
131
+ * - CRLF / lone CR → LF (so wrap sees real logical lines);
132
+ * - TAB → spaces, column-aware to the next CODE_TAB_SIZE stop, so width math
133
+ * matches what the terminal draws and the viewport clip is not bypassed;
134
+ * - other C0 control chars (and DEL) except LF → single space, so stray
135
+ * control bytes cannot trigger terminal-side cursor moves / extra rows.
136
+ */
137
+ function normalizeCodeText(text) {
138
+ const input = String(text ?? '');
139
+ if (input.length === 0) return input;
140
+ const normalizedEol = input.replace(/\r\n?/g, '\n');
141
+ let out = '';
142
+ let col = 0;
143
+ for (const ch of normalizedEol) {
144
+ if (ch === '\n') {
145
+ out += ch;
146
+ col = 0;
147
+ continue;
148
+ }
149
+ if (ch === '\t') {
150
+ const advance = CODE_TAB_SIZE - (col % CODE_TAB_SIZE);
151
+ out += ' '.repeat(advance);
152
+ col += advance;
153
+ continue;
154
+ }
155
+ const cp = ch.codePointAt(0);
156
+ if (cp != null && (cp <= 0x1f || cp === 0x7f)) {
157
+ // Remaining C0 control / DEL: replace with a space so it cannot move the
158
+ // terminal cursor after ink has already accounted for the row.
159
+ out += ' ';
160
+ col += 1;
161
+ continue;
162
+ }
163
+ out += ch;
164
+ col += displayWidth(ch);
165
+ }
166
+ return out;
167
+ }
168
+
135
169
  /** Wrap text to width, ANSI-aware (lockstep with table-layout hard wrap). */
136
170
  function wrapTextToWidth(text, width, options) {
137
171
  if (width <= 0) return [text];
@@ -153,10 +187,10 @@ function hardWrapAnsiLines(text, width) {
153
187
  const out = [];
154
188
  for (const softLine of wrapTextToWidth(input, max, { hard: true })) {
155
189
  let rest = softLine;
156
- while (rest.length > 0 && stringWidth(rest) > max) {
190
+ while (rest.length > 0 && displayWidth(rest) > max) {
157
191
  let take = 1;
158
192
  for (let i = 1; i <= rest.length; i++) {
159
- if (stringWidth(rest.slice(0, i)) <= max) take = i;
193
+ if (displayWidth(rest.slice(0, i)) <= max) take = i;
160
194
  else break;
161
195
  }
162
196
  out.push(rest.slice(0, take));
@@ -173,37 +207,11 @@ function wrapCodeLine(ansiContent, maxLineWidth) {
173
207
  return hardWrapAnsiLines(ansiContent, contentMax);
174
208
  }
175
209
 
176
- /** Visible terminal width of an ANSI-colored line (codes excluded). */
177
- function visibleLineWidth(line) {
178
- return stringWidth(stripAnsi(String(line ?? '')));
179
- }
180
-
181
- /** Max visible width across lang label + body lines (content-based, not terminal). */
182
- function codeBlockContentWidth(langLine, bodyLines) {
183
- const all = [...(langLine ? [langLine] : []), ...bodyLines];
184
- if (all.length === 0) return 0;
185
- return Math.max(1, ...all.map(visibleLineWidth));
186
- }
187
-
188
- /** Apply code-block bg to one line, padded to a shared compact block width. */
189
- function tintCodeLineToBlockWidth(line, codeBg, blockWidth) {
190
- const w = visibleLineWidth(line);
191
- const pad = Math.max(0, blockWidth - w);
192
- return codeBg(`${line ?? ''}${' '.repeat(pad)}`);
193
- }
194
-
195
- function tintCodeBlockBody(langLine, bodyLines, codeBg) {
196
- const blockWidth = codeBlockContentWidth(langLine, bodyLines);
197
- const langOut = langLine ? tintCodeLineToBlockWidth(langLine, codeBg, blockWidth) : '';
198
- const bodyOut = bodyLines.map((line) => tintCodeLineToBlockWidth(line, codeBg, blockWidth)).join(EOL);
199
- return { langOut, bodyOut };
200
- }
201
-
202
210
  /** Reduce render width by a visible prefix (list marker, blockquote bar, etc.). */
203
211
  function contentWidthAfterPrefix(width, prefix) {
204
212
  const base = Number(width) || 0;
205
213
  if (base <= 0) return 0;
206
- return Math.max(8, base - stringWidth(String(prefix ?? '')));
214
+ return Math.max(8, base - displayWidth(String(prefix ?? '')));
207
215
  }
208
216
 
209
217
  /** Normalize a fenced info-string to a bare lowercase language token. */
@@ -295,6 +303,27 @@ function collectDiffBodyLines(text, c, bandWidth) {
295
303
  }
296
304
 
297
305
  // ── cli-highlight (highlight.js) ────────────────────────────────────────────
306
+ const requireCliHighlight = createRequire(import.meta.url);
307
+ /** @type {{ highlight: typeof import('cli-highlight').highlight, supportsLanguage: typeof import('cli-highlight').supportsLanguage } | null} */
308
+ let cliHighlightModule = null;
309
+
310
+ function getCliHighlight() {
311
+ if (cliHighlightModule) return cliHighlightModule;
312
+ try {
313
+ const clihl = requireCliHighlight('cli-highlight');
314
+ cliHighlightModule = {
315
+ highlight: clihl.highlight,
316
+ supportsLanguage: clihl.supportsLanguage,
317
+ };
318
+ } catch {
319
+ cliHighlightModule = {
320
+ highlight: null,
321
+ supportsLanguage: () => false,
322
+ };
323
+ }
324
+ return cliHighlightModule;
325
+ }
326
+
298
327
  /** Internal family → highlight.js language id (when alias alone is insufficient). */
299
328
  const FAMILY_TO_HLJS = {
300
329
  js: 'javascript',
@@ -439,6 +468,7 @@ function getCliHighlightTheme() {
439
468
 
440
469
  function hljsLanguageFromFamily(family) {
441
470
  if (!family || family === 'md') return null;
471
+ const { supportsLanguage } = getCliHighlight();
442
472
  const mapped = FAMILY_TO_HLJS[family];
443
473
  if (mapped && supportsLanguage(mapped)) return mapped;
444
474
  if (supportsLanguage(family)) return family;
@@ -451,6 +481,7 @@ function resolveHljsLanguage(lang) {
451
481
  if (!normalized) return null;
452
482
  const viaFamily = hljsLanguageFromFamily(LANG_FAMILY[normalized]);
453
483
  if (viaFamily) return viaFamily;
484
+ const { supportsLanguage } = getCliHighlight();
454
485
  return supportsLanguage(normalized) ? normalized : null;
455
486
  }
456
487
 
@@ -459,7 +490,8 @@ function resolveHljsLanguage(lang) {
459
490
  * or highlighting fails (caller falls back to flat body color).
460
491
  */
461
492
  function highlightCodeText(text, hljsLang) {
462
- if (!hljsLang || !supportsLanguage(hljsLang)) return null;
493
+ const { highlight, supportsLanguage } = getCliHighlight();
494
+ if (!hljsLang || !supportsLanguage(hljsLang) || typeof highlight !== 'function') return null;
463
495
  const src = String(text ?? '');
464
496
  if (!src.trim()) return null;
465
497
  if (!/[A-Za-z0-9]/.test(src)) return null;
@@ -531,31 +563,45 @@ function collectHighlightedBodyLines(text, hljsLang, bandWidth) {
531
563
  return lines;
532
564
  }
533
565
 
566
+ /** Left gutter that keeps code visually distinct from prose (no bg band). */
567
+ const CODE_GUTTER = ' ';
568
+
534
569
  /**
535
- * Render a fenced `code` token: flush-left wrapped body with background band
536
- * and language-aware coloring (no visible ``` fence lines or lang label).
570
+ * Render a fenced `code` token: an optional
571
+ * subtle language label row, then the wrapped, syntax/diff/flat-highlighted
572
+ * body. Every row is left-indented by a 2-col gutter. NO background band, no
573
+ * full-width padding, and no visible ``` fence lines.
537
574
  */
538
575
  function renderCodeBlock(token, width = 0) {
539
576
  const { codeBlock } = colorizers();
540
577
  const c = extraColorizers();
541
578
  const lang = normalizeLang(token.lang);
542
- const text = decodeEntities(token.text ?? '');
543
- const bandWidth = Math.max(8, Number(width) || 80);
579
+ const text = normalizeCodeText(decodeEntities(token.text ?? ''));
580
+ const renderWidth = Math.max(8, Number(width) || 80);
581
+ // Wrap content to the render width minus the left gutter so `gutter + content`
582
+ // never overruns the available width.
583
+ const contentWidth = Math.max(1, renderWidth - CODE_GUTTER.length);
544
584
 
545
585
  let bodyLines;
546
586
  if (DIFF_LANGS.has(lang) || (!lang && looksLikeUnifiedDiff(text))) {
547
- bodyLines = collectDiffBodyLines(text, c, bandWidth);
587
+ bodyLines = collectDiffBodyLines(text, c, contentWidth);
548
588
  } else {
549
589
  const hljsLang = resolveHljsLanguage(lang);
550
590
  const family = LANG_FAMILY[lang];
551
591
  if (hljsLang && family !== 'md') {
552
- bodyLines = collectHighlightedBodyLines(text, hljsLang, bandWidth);
592
+ bodyLines = collectHighlightedBodyLines(text, hljsLang, contentWidth);
553
593
  } else {
554
- bodyLines = collectFlatBodyLines(text, codeBlock, bandWidth);
594
+ bodyLines = collectFlatBodyLines(text, codeBlock, contentWidth);
555
595
  }
556
596
  }
557
- const { bodyOut } = tintCodeBlockBody(null, bodyLines, c.codeBg);
558
- return `${bodyOut}${bodyOut ? EOL : ''}`;
597
+ // Indent every body row by the gutter (no bg band).
598
+ const indentedBody = bodyLines.map((line) => `${CODE_GUTTER}${line ?? ''}`);
599
+ // Language label: a subtle (syntax-comment colored) metadata row above the
600
+ // body. Its trimmed visible text is exactly the bare lang token, so consumers
601
+ // can detect the label by trimmed equality.
602
+ const langLine = lang ? `${CODE_GUTTER}${c.synComment(lang)}` : null;
603
+ const rows = [...(langLine ? [langLine] : []), ...indentedBody].join(EOL);
604
+ return `${rows}${rows ? EOL : ''}`;
559
605
  }
560
606
 
561
607
  function numberToLetter(n) {
@@ -603,11 +649,13 @@ function getListNumber(depth, orderedListNumber) {
603
649
  * marked token switch (minus table / hyperlink deps).
604
650
  */
605
651
  export function formatToken(token, listBaseIndent = 0, orderedListNumber = null, parent = null, width = 0, depth = 0) {
606
- const { accent, codeBlock, headingAccent, quoteBorder, quoteText, hrLine } = colorizers();
652
+ const { accent, codeBlock, hrLine } = colorizers();
607
653
  const ex = extraColorizers();
608
654
  switch (token.type) {
609
655
  case 'blockquote': {
610
- const bar = quoteBorder(BLOCKQUOTE_BAR);
656
+ // Block structure carries no color: a dim bar plus
657
+ // body-colored italic text. Color is reserved for inline codespan/link.
658
+ const bar = chalk.dim(BLOCKQUOTE_BAR);
611
659
  const quotePrefix = `${BLOCKQUOTE_BAR} `;
612
660
  const innerWidth = contentWidthAfterPrefix(width, quotePrefix);
613
661
  const inner = (token.tokens ?? []).map((t) => formatToken(t, 0, null, null, innerWidth)).join('');
@@ -615,8 +663,8 @@ export function formatToken(token, listBaseIndent = 0, orderedListNumber = null,
615
663
  .split(EOL)
616
664
  .map((line) => {
617
665
  // Padded fenced-code blank rows are space-only; trim() would drop the quote bar.
618
- if (stringWidth(stripAnsi(line)) === 0) return line;
619
- return `${bar} ${quoteText(chalk.italic(line))}`;
666
+ if (displayWidth(stripAnsi(line)) === 0) return line;
667
+ return `${bar} ${chalk.italic(line)}`;
620
668
  })
621
669
  .join(EOL);
622
670
  }
@@ -624,20 +672,24 @@ export function formatToken(token, listBaseIndent = 0, orderedListNumber = null,
624
672
  // Fenced block: flush-left wrapped body with syntax highlighting.
625
673
  return renderCodeBlock(token, width);
626
674
  case 'codespan':
627
- // inline code
628
- return ex.codeSpanBg(accent(decodeEntities(token.text)));
675
+ // inline code — accent color only (no background tint; a bg box behind
676
+ // inline spans reads as awkward against body text).
677
+ return accent(decodeEntities(token.text));
629
678
  case 'em':
630
- return ex.emph(chalk.italic((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join('')));
679
+ // Italic only no color tint (a colored em clashes per-theme and
680
+ // reads loud against body prose).
681
+ return chalk.italic((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join(''));
631
682
  case 'strong':
632
- return ex.strong(chalk.bold((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join('')));
683
+ // Bold only no color tint (stays body-colored, just heavier weight).
684
+ return chalk.bold((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join(''));
633
685
  case 'heading':
634
686
  switch (token.depth) {
635
687
  case 1:
636
688
  return (
637
- chalk.bold.italic.underline(headingAccent((token.tokens ?? []).map((t) => formatToken(t)).join(''))) + EOL + EOL
689
+ chalk.bold.italic.underline((token.tokens ?? []).map((t) => formatToken(t)).join('')) + EOL + EOL
638
690
  );
639
691
  default: // h2+
640
- return chalk.bold(headingAccent((token.tokens ?? []).map((t) => formatToken(t)).join(''))) + EOL + EOL;
692
+ return chalk.bold((token.tokens ?? []).map((t) => formatToken(t)).join('')) + EOL + EOL;
641
693
  }
642
694
  case 'hr': {
643
695
  // Span the available content width with a box-drawing rule. width is only
@@ -662,12 +714,12 @@ export function formatToken(token, listBaseIndent = 0, orderedListNumber = null,
662
714
  const OSC8_OPEN = (url) => `\x1b]8;;${url}\x07`;
663
715
  const OSC8_CLOSE = '\x1b]8;;\x07';
664
716
  if (plain && plain !== href) {
665
- const styledLabel = ex.linkText(chalk.underline(linkText));
717
+ const styledLabel = ex.linkText(linkText);
666
718
  return `${OSC8_OPEN(href)}${styledLabel}${OSC8_CLOSE}`;
667
719
  }
668
720
  // No distinct label (empty or equal to href): show the URL itself as the
669
721
  // clickable, visible text.
670
- return `${OSC8_OPEN(href)}${ex.link(chalk.underline(href))}${OSC8_CLOSE}`;
722
+ return `${OSC8_OPEN(href)}${ex.link(href)}${OSC8_CLOSE}`;
671
723
  }
672
724
  case 'list':
673
725
  return token.items
@@ -11,7 +11,7 @@ import {
11
11
  } from './format-token.mjs';
12
12
  import { setThemeSetting, theme } from '../theme.mjs';
13
13
 
14
- setThemeSetting('mixdog', { persist: false });
14
+ setThemeSetting('basic', { persist: false });
15
15
 
16
16
  function rgbSgr(value) {
17
17
  const m = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/.exec(String(value || ''));
@@ -23,31 +23,42 @@ function lexFirst(md, type) {
23
23
  return tokens.find((t) => t.type === type) ?? tokens[0];
24
24
  }
25
25
 
26
- test('fenced code block has no ``` fences and flush-left body', () => {
26
+ test('fenced code block has no ``` fences and shows a language label row', () => {
27
27
  const token = lexFirst('```js\nconst x = 1;\n```\n', 'code');
28
- const out = formatToken(token);
28
+ const out = formatToken(token, 0, null, null, 60);
29
29
  const plain = stripAnsi(out);
30
30
  assert.ok(!plain.includes('```'), 'no literal fence markers');
31
- assert.ok(plain.includes('const x = 1;'), 'body is flush-left');
32
- assert.ok(!plain.trimStart().startsWith('js'), 'no language label row');
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');
33
36
  });
34
37
 
35
- test('short code line has no terminal-width trailing padding band', () => {
38
+ test('short code line is gutter-indented with no background band', () => {
39
+ const width = 60;
36
40
  const token = lexFirst('```js\nshort\n```\n', 'code');
37
- const out = formatToken(token, 0, null, null, 60);
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');
38
44
  const bodyLine = stripAnsi(out).split('\n').find((l) => l.includes('short'));
39
45
  assert.ok(bodyLine, 'body line present');
40
- assert.equal(bodyLine, 'short', 'body line is only text');
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');
41
49
  });
42
50
 
43
- test('code block body lines share one compact background width', () => {
51
+ test('code block body lines have no background band and follow content width', () => {
52
+ const width = 80;
44
53
  const token = lexFirst('```js\na\n\nlonger line\n```\n', 'code');
45
- const out = formatToken(token, 0, null, null, 80);
54
+ const out = formatToken(token, 0, null, null, width);
55
+ assert.ok(!out.includes('48;2;'), 'no background SGR anywhere');
46
56
  const plainLines = stripAnsi(out).split('\n').filter((l) => l.length > 0);
47
57
  const widths = plainLines.map((l) => stringWidth(l));
48
- assert.ok(widths.length >= 3, 'body lines rendered');
49
- assert.equal(new Set(widths).size, 1, 'every row uses the same content width');
50
- assert.equal(Math.max(...widths), stringWidth('longer line'), 'width follows longest rendered line');
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');
51
62
  });
52
63
 
53
64
  test('long code line wraps within requested body width', () => {
@@ -63,6 +74,33 @@ test('long code line wraps within requested body width', () => {
63
74
  assert.equal(bodyLines.join('').replace(/\s/g, ''), long, 'wrapped segments preserve content');
64
75
  });
65
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
+
66
104
  test('list item fenced code block respects narrowed width and has no fences', () => {
67
105
  const long = 'b'.repeat(36);
68
106
  const codeTok = lexFirst(`\`\`\`js\n${long}\n\`\`\`\n`, 'code');
@@ -163,12 +201,14 @@ test('highlight cache serves repeat block highlight without changing output', ()
163
201
  assert.ok(first[0].includes('38;2;'), 'highlighted ANSI present');
164
202
  });
165
203
 
166
- test('inline codespan emits truecolor background band', () => {
204
+ test('inline codespan is accent-colored with no background band', () => {
167
205
  const tokens = marked.lexer('`hello`');
168
206
  const codespan = tokens[0].tokens[0];
169
207
  assert.equal(codespan.type, 'codespan');
170
208
  const out = formatToken(codespan);
171
- assert.ok(out.includes('48;2;'), 'background SGR present');
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');
172
212
  assert.ok(stripAnsi(out).includes('hello'));
173
213
  assert.ok(out.includes(rgbSgr(theme.mdCode)), 'inline code accent');
174
214
  });
@@ -265,10 +305,12 @@ test('strong and em use distinct mdStrong / mdEmph colors', () => {
265
305
  const emTok = lexFirst('*ital*', 'paragraph');
266
306
  const strongOut = formatToken(strongTok);
267
307
  const emOut = formatToken(emTok);
268
- assert.ok(strongOut.includes(rgbSgr(theme.mdStrong)), 'strong colored');
269
- assert.ok(emOut.includes(rgbSgr(theme.mdEmph)), 'em colored');
308
+ // Bold/italic carry weight/style only — NO color tint (a colored
309
+ // strong/em clashes per-theme and reads too loud).
270
310
  assert.ok(strongOut.includes('\x1b[1m'), 'strong is bold');
271
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');
272
314
  });
273
315
 
274
316
  test('h1 heading is bold, italic, and underlined', () => {
@@ -302,11 +344,11 @@ test('every palette defines the full extended key set', () => {
302
344
  'syntaxComment', 'syntaxKeyword', 'syntaxFunction', 'syntaxVariable',
303
345
  'syntaxString', 'syntaxNumber', 'syntaxType', 'syntaxOperator', 'syntaxPunctuation',
304
346
  ];
305
- for (const id of ['mixdog', 'pi-dark', 'claude-dark', 'dracula', 'tokyonight', 'nord', 'gruvbox', 'catppuccin', 'everforest']) {
347
+ for (const id of ['basic', 'indigo', 'warm', 'light', 'teal', 'onedark', 'tokyonight', 'kanagawa', 'catppuccin', 'dracula', 'rosepine', 'nord', 'gruvbox', 'everforest']) {
306
348
  setThemeSetting(id, { persist: false });
307
349
  for (const key of required) {
308
350
  assert.ok(/^rgb\(\d+,\s*\d+,\s*\d+\)$/.test(String(theme[key])), `${id}.${key} is an rgb() string`);
309
351
  }
310
352
  }
311
- setThemeSetting('mixdog', { persist: false });
353
+ setThemeSetting('basic', { persist: false });
312
354
  });
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Row-height measurement lockstep with Markdown.jsx / StreamingMarkdown.jsx.
3
+ */
4
+ import stripAnsi from 'strip-ansi';
5
+ import { displayWidth } from '../display-width.mjs';
6
+ import { assistantBodyWidth, measureMarkdownTableRows } from './table-layout.mjs';
7
+ import { renderTokenAnsiSegments } from './render-ansi.mjs';
8
+ import { resolveStreamingMarkdownParts } from './streaming-markdown.mjs';
9
+
10
+ function wrappedLineRows(line, width) {
11
+ const text = String(line);
12
+ const full = displayWidth(text);
13
+ if (full === 0) return 1;
14
+ if (full <= width) return 1;
15
+ let rows = 1;
16
+ let col = 0;
17
+ for (const token of text.split(/(\s+)/)) {
18
+ if (!token) continue;
19
+ const tw = displayWidth(token);
20
+ if (tw === 0) continue;
21
+ if (tw > width) {
22
+ if (col > 0) { rows++; col = 0; }
23
+ rows += Math.ceil(tw / width) - 1;
24
+ col = tw % width || width;
25
+ continue;
26
+ }
27
+ if (col + tw > width) { rows++; col = tw; }
28
+ else { col += tw; }
29
+ }
30
+ return Math.max(1, rows);
31
+ }
32
+
33
+ function estimateWrappedRowsFallback(text, columns, reserve = 3) {
34
+ const width = Math.max(8, Number(columns || 80) - reserve);
35
+ const lines = String(text ?? '').split('\n');
36
+ return Math.max(1, lines.reduce((sum, line) => sum + wrappedLineRows(line, width), 0));
37
+ }
38
+
39
+ export function measureMarkdownRenderedRows(text, columns, { trimPartialFences = false } = {}) {
40
+ const value = String(text ?? '');
41
+ if (!value) return 1;
42
+ const bodyWidth = assistantBodyWidth(columns);
43
+ let segments;
44
+ try {
45
+ segments = renderTokenAnsiSegments(value, { width: bodyWidth, trimPartialFences });
46
+ } catch {
47
+ return Math.max(1, estimateWrappedRowsFallback(value, columns, 3));
48
+ }
49
+ if (!segments.length) return 1;
50
+ let rows = 0;
51
+ for (const seg of segments) {
52
+ if (seg.type === 'table') {
53
+ rows += Math.max(1, measureMarkdownTableRows(seg.token, bodyWidth));
54
+ continue;
55
+ }
56
+ const plain = stripAnsi(String(seg.ansi ?? ''));
57
+ for (const line of plain.split('\n')) {
58
+ rows += wrappedLineRows(line, bodyWidth);
59
+ }
60
+ }
61
+ rows += segments.length - 1;
62
+ return Math.max(1, rows);
63
+ }
64
+
65
+ export function measureStreamingMarkdownRenderedRows(text, columns, streamKey) {
66
+ const value = String(text ?? '');
67
+ if (!value) return 1;
68
+ const parts = resolveStreamingMarkdownParts(value, streamKey);
69
+ if (parts.plain) {
70
+ return measureMarkdownRenderedRows(value, columns, { trimPartialFences: false });
71
+ }
72
+ let rows = 0;
73
+ let childCount = 0;
74
+ if (parts.stablePrefix) {
75
+ rows += measureMarkdownRenderedRows(parts.stablePrefix, columns, { trimPartialFences: false });
76
+ childCount += 1;
77
+ }
78
+ if (parts.unstableSuffix) {
79
+ rows += measureMarkdownRenderedRows(parts.unstableForRender, columns, { trimPartialFences: true });
80
+ childCount += 1;
81
+ }
82
+ if (childCount === 2) rows += 1;
83
+ if (childCount === 0) return 1;
84
+ return Math.max(1, rows);
85
+ }
@@ -5,7 +5,7 @@ import { assistantBodyWidth } from './table-layout.mjs';
5
5
  import { renderTokenAnsiSegments, lexMarkdown, hasMarkdownSyntax } from './render-ansi.mjs';
6
6
  import { setThemeSetting } from '../theme.mjs';
7
7
 
8
- setThemeSetting('mixdog', { persist: false });
8
+ setThemeSetting('basic', { persist: false });
9
9
 
10
10
  // Mirrors the streaming suffix the <Markdown trimPartialFences> path renders.
11
11
  const STREAMING = '```js\nconst x = 1;\n`';