mixdog 0.8.1 → 0.9.1

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 (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,28 +1,30 @@
1
1
  /**
2
2
  * markdown/format-token.mjs — token → ANSI string renderer.
3
3
  *
4
- * Ported from Claude Code (refs/claude-code/src/utils/markdown.ts: formatToken
5
- * + helpers), adapted for this CLI:
4
+ * Token ANSI string renderer:
6
5
  * - chalk is forced to truecolor (level 3) so colors render regardless of the
7
6
  * ambient TTY detection (we control the surface).
8
7
  * - The `permission`/code accent and blockquote bar come from our theme.mjs.
9
- * - `code` (fenced) is emitted as plain text + EOL (no syntax highlighter
10
- * dependency); `codespan` (inline) gets the accent color, like CC.
8
+ * - `code` (fenced) uses cli-highlight + theme syntax palette; `codespan`
9
+ * (inline) gets the accent color.
11
10
  * - `table` is NOT handled here — the React component (MarkdownTable.jsx)
12
- * renders tables with proper ink Box layout, mirroring CC's hybrid split.
11
+ * renders tables with proper ink Box layout (hybrid split).
13
12
  * - Hyperlinks/issue-ref linkify are dropped (no OSC-8 dependency); link text
14
13
  * is shown plainly with its URL.
15
14
  */
16
15
  import { Chalk } from 'chalk';
16
+ import { highlight, supportsLanguage } from 'cli-highlight';
17
17
  import stripAnsi from 'strip-ansi';
18
- import { theme } from '../theme.mjs';
19
- import { BLOCKQUOTE_BAR } from '../figures.mjs';
18
+ import stringWidth from 'string-width';
19
+ import wrapAnsi from 'wrap-ansi';
20
+ import { theme, getThemeVersion } from '../theme.mjs';
21
+ import { BLOCKQUOTE_BAR, HR_LINE } from '../figures.mjs';
20
22
 
21
23
  // Force truecolor so chalk emits 24-bit SGR even when the ambient level is 0.
22
24
  // ink's <Text> passes these escapes through verbatim.
23
25
  const chalk = new Chalk({ level: 3 });
24
26
 
25
- // Use \n unconditionally (CC note: os.EOL's \r breaks segment mapping).
27
+ // Use \n unconditionally (os.EOL's \r breaks segment mapping).
26
28
  const EOL = '\n';
27
29
 
28
30
  /** Parse an `rgb(r,g,b)` theme string into a chalk colorizer. */
@@ -32,8 +34,78 @@ function rgbColor(str) {
32
34
  return chalk.rgb(Number(m[1]), Number(m[2]), Number(m[3]));
33
35
  }
34
36
 
35
- const accent = rgbColor(theme.code); // inline code / codespan accent (CC permission)
36
- const dim = rgbColor(theme.subtle);
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
+ // Colorizers are derived from the active theme's md* keys. They are cached and
51
+ // rebuilt only when the theme version changes, so a `/theme` switch takes
52
+ // effect on the next render without recomputing a chalk fn per token.
53
+ let _colorizerVersion = -1;
54
+ let _colorizers = null;
55
+ function colorizers() {
56
+ const version = getThemeVersion();
57
+ if (_colorizers && _colorizerVersion === version) return _colorizers;
58
+ _colorizerVersion = version;
59
+ _colorizers = {
60
+ accent: rgbColor(theme.mdCode), // inline codespan
61
+ codeBlock: rgbColor(theme.mdCodeBlock), // fenced code block body
62
+ headingAccent: rgbColor(theme.mdHeading),
63
+ quoteBorder: rgbColor(theme.mdQuoteBorder),
64
+ quoteText: rgbColor(theme.mdQuote),
65
+ hrLine: rgbColor(theme.mdHr),
66
+ listBullet: rgbColor(theme.mdListBullet),
67
+ };
68
+ return _colorizers;
69
+ }
70
+
71
+ // Extended colorizers (fence border, link, emphasis, diff + syntax classes).
72
+ // Cached on the same theme-version key so a /theme switch rebuilds both maps.
73
+ let _extraVersion = -1;
74
+ let _extra = null;
75
+ export function extraColorizers() {
76
+ const version = getThemeVersion();
77
+ if (_extra && _extraVersion === version) return _extra;
78
+ _extraVersion = version;
79
+ const fallbackBody = rgbColor(theme.mdCodeBlock);
80
+ _extra = {
81
+ fenceBorder: rgbColor(theme.mdCodeBlockBorder ?? theme.mdHr),
82
+ link: rgbColor(theme.mdLink ?? theme.code),
83
+ linkText: rgbColor(theme.mdLinkText ?? theme.mdCode),
84
+ strong: rgbColor(theme.mdStrong ?? theme.mdHeading),
85
+ emph: rgbColor(theme.mdEmph ?? theme.mdHeading),
86
+ // diff/patch
87
+ diffAdded: rgbColor(theme.mdDiffAdded ?? theme.success),
88
+ diffRemoved: rgbColor(theme.mdDiffRemoved ?? theme.error),
89
+ diffHunk: rgbColor(theme.mdDiffHunk ?? theme.code),
90
+ diffHeader: rgbColor(theme.mdDiffHeader ?? theme.mdHeading),
91
+ diffContext: rgbColor(theme.mdDiffContext ?? theme.subtle),
92
+ // syntax classes (fall back to plain code-block body when key missing)
93
+ synComment: rgbColor(theme.syntaxComment ?? theme.subtle),
94
+ synKeyword: rgbColor(theme.syntaxKeyword ?? theme.mdCodeBlock),
95
+ synFunction: rgbColor(theme.syntaxFunction ?? theme.mdCodeBlock),
96
+ synVariable: rgbColor(theme.syntaxVariable ?? theme.mdCodeBlock),
97
+ synString: rgbColor(theme.syntaxString ?? theme.mdCodeBlock),
98
+ synNumber: rgbColor(theme.syntaxNumber ?? theme.mdCodeBlock),
99
+ synType: rgbColor(theme.syntaxType ?? theme.mdCodeBlock),
100
+ synOperator: rgbColor(theme.syntaxOperator ?? theme.mdCodeBlock),
101
+ synPunct: rgbColor(theme.syntaxPunctuation ?? theme.mdCodeBlock),
102
+ 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
+ };
107
+ return _extra;
108
+ }
37
109
 
38
110
  // marked 14 HTML-encodes token.text / codespan.text (`"` → `&quot;`, `&` →
39
111
  // `&amp;`, `<`→`&lt;`, `>`→`&gt;`, `'`→`&#39;`), but we render to a terminal,
@@ -53,80 +125,575 @@ function decodeEntities(s) {
53
125
  .replace(/&amp;/g, '&');
54
126
  }
55
127
 
128
+ // ── 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.
133
+ const DIFF_LANGS = new Set(['diff', 'patch', 'udiff', 'git-diff', 'gitdiff']);
134
+
135
+ /** Wrap text to width, ANSI-aware (lockstep with table-layout hard wrap). */
136
+ function wrapTextToWidth(text, width, options) {
137
+ if (width <= 0) return [text];
138
+ const trimmedText = String(text).trimEnd();
139
+ const wrapped = wrapAnsi(trimmedText, width, {
140
+ hard: options?.hard ?? false,
141
+ trim: false,
142
+ wordWrap: true,
143
+ });
144
+ const lines = wrapped.split('\n').filter((line) => line.length > 0);
145
+ return lines.length > 0 ? lines : [''];
146
+ }
147
+
148
+ /** Hard-wrap so every line satisfies stringWidth(line) <= width. */
149
+ function hardWrapAnsiLines(text, width) {
150
+ const max = Math.max(1, Math.floor(Number(width) || 1));
151
+ const input = String(text ?? '');
152
+ if (!input) return [''];
153
+ const out = [];
154
+ for (const softLine of wrapTextToWidth(input, max, { hard: true })) {
155
+ let rest = softLine;
156
+ while (rest.length > 0 && stringWidth(rest) > max) {
157
+ let take = 1;
158
+ for (let i = 1; i <= rest.length; i++) {
159
+ if (stringWidth(rest.slice(0, i)) <= max) take = i;
160
+ else break;
161
+ }
162
+ out.push(rest.slice(0, take));
163
+ rest = rest.slice(take);
164
+ }
165
+ if (rest.length > 0) out.push(rest);
166
+ }
167
+ return out.length > 0 ? out : [''];
168
+ }
169
+
170
+ /** Wrap one logical code line (ANSI content) to max visible width. */
171
+ function wrapCodeLine(ansiContent, maxLineWidth) {
172
+ const contentMax = Math.max(1, maxLineWidth);
173
+ return hardWrapAnsiLines(ansiContent, contentMax);
174
+ }
175
+
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
+ /** Reduce render width by a visible prefix (list marker, blockquote bar, etc.). */
203
+ function contentWidthAfterPrefix(width, prefix) {
204
+ const base = Number(width) || 0;
205
+ if (base <= 0) return 0;
206
+ return Math.max(8, base - stringWidth(String(prefix ?? '')));
207
+ }
208
+
209
+ /** Normalize a fenced info-string to a bare lowercase language token. */
210
+ function normalizeLang(lang) {
211
+ return String(lang || '')
212
+ .trim()
213
+ .split(/\s+/)[0]
214
+ .toLowerCase();
215
+ }
216
+
217
+ /**
218
+ * Heuristic: does this code body look like a unified diff/patch? Requires at
219
+ * least one +/- body line AND a hunk header or file header, so ordinary code
220
+ * with leading +/- (rare) is not misclassified.
221
+ */
222
+ export function looksLikeUnifiedDiff(text) {
223
+ const lines = String(text ?? '').split(EOL);
224
+ let hasHunk = false;
225
+ let hasFileHeader = false;
226
+ let hasSign = false;
227
+ let hasStat = false;
228
+ for (const line of lines) {
229
+ if (/^@@ .* @@/.test(line) || /^@@ /.test(line)) hasHunk = true;
230
+ if (/^(\+\+\+ |--- |diff --git |index [0-9a-f]+)/.test(line)) hasFileHeader = true;
231
+ if (/^[+-](?![+-])/.test(line)) hasSign = true;
232
+ // `git diff --stat` summary rows: `path | 4 +-` and the trailer
233
+ // `N files changed, M insertions(+), K deletions(-)`.
234
+ if (DIFF_STAT_FILE_RE.test(line) || DIFF_STAT_SUMMARY_RE.test(line)) hasStat = true;
235
+ }
236
+ return ((hasHunk || hasFileHeader) && hasSign) || hasStat;
237
+ }
238
+
239
+ // `git diff --stat` rows. FILE: `<path> | <count> <+/-/ graph>` (count may be
240
+ // `Bin`); SUMMARY: `N file(s) changed[, M insertion…][, K deletion…]`.
241
+ const DIFF_STAT_FILE_RE = /^(\s*)(.+?)(\s+\|\s+)(Bin\b.*|\d+\s*[+\-]*)\s*$/;
242
+ const DIFF_STAT_SUMMARY_RE = /^\s*\d+\s+files?\s+changed\b/;
243
+
244
+ /** Color a `git diff --stat` file row: dim path/sep, green `+`, red `-`. */
245
+ function colorizeDiffStatLine(line, c) {
246
+ const m = DIFF_STAT_FILE_RE.exec(line);
247
+ if (m) {
248
+ const [, indent, path, sep, tail] = m;
249
+ // The tail is either `Bin …` or `<count> <graph>` where graph is +/-.
250
+ const countMatch = /^(\d+)(\s*)([+\-]*)\s*$/.exec(tail);
251
+ let coloredTail;
252
+ if (countMatch) {
253
+ const [, count, gap, bars] = countMatch;
254
+ const pluses = c.diffAdded('+'.repeat((bars.match(/\+/g) || []).length));
255
+ const minuses = c.diffRemoved('-'.repeat((bars.match(/-/g) || []).length));
256
+ coloredTail = `${c.diffContext(count)}${gap}${pluses}${minuses}`;
257
+ } else {
258
+ coloredTail = c.diffContext(tail);
259
+ }
260
+ return `${indent}${c.diffContext(path)}${c.diffContext(sep)}${coloredTail}`;
261
+ }
262
+ return c.diffContext(line);
263
+ }
264
+
265
+ /** Classify and color a single unified-diff line. */
266
+ export function colorizeDiffLine(line, c) {
267
+ // File/section headers first so +++/--- are never treated as add/remove.
268
+ if (/^(\+\+\+|---)(\s|$)/.test(line)) return c.diffHeader(line);
269
+ if (/^(diff --git |index [0-9a-f]|new file|deleted file|rename |similarity |old mode|new mode)/.test(line)) {
270
+ return c.diffHeader(line);
271
+ }
272
+ if (/^@@/.test(line)) return c.diffHunk(line);
273
+ if (/^\+/.test(line)) return c.diffAdded(line);
274
+ if (/^-/.test(line)) return c.diffRemoved(line);
275
+ if (/^\\ No newline/.test(line)) return c.diffContext(line);
276
+ // `git diff --stat` summary trailer + per-file rows.
277
+ if (DIFF_STAT_SUMMARY_RE.test(line)) return colorizeDiffStatTrailer(line, c);
278
+ if (DIFF_STAT_FILE_RE.test(line)) return colorizeDiffStatLine(line, c);
279
+ return c.diffContext(line);
280
+ }
281
+
282
+ /** Color the `N files changed, M insertions(+), K deletions(-)` trailer. */
283
+ function colorizeDiffStatTrailer(line, c) {
284
+ return line.replace(/(\d+)(\s+insertions?\(\+\))/g, (_, n, rest) => `${c.diffAdded(n)}${c.diffContext(rest)}`)
285
+ .replace(/(\d+)(\s+deletions?\(-\))/g, (_, n, rest) => `${c.diffRemoved(n)}${c.diffContext(rest)}`);
286
+ }
287
+
288
+ function collectDiffBodyLines(text, c, bandWidth) {
289
+ const lines = [];
290
+ for (const line of String(text ?? '').split(EOL)) {
291
+ const colored = colorizeDiffLine(line, c);
292
+ lines.push(...wrapCodeLine(colored, bandWidth));
293
+ }
294
+ return lines;
295
+ }
296
+
297
+ // ── cli-highlight (highlight.js) ────────────────────────────────────────────
298
+ /** Internal family → highlight.js language id (when alias alone is insufficient). */
299
+ const FAMILY_TO_HLJS = {
300
+ js: 'javascript',
301
+ ts: 'typescript',
302
+ tsx: 'tsx',
303
+ jsx: 'jsx',
304
+ json: 'json',
305
+ sh: 'bash',
306
+ py: 'python',
307
+ css: 'css',
308
+ html: 'xml',
309
+ go: 'go',
310
+ rust: 'rust',
311
+ java: 'java',
312
+ c: 'cpp',
313
+ ruby: 'ruby',
314
+ sql: 'sql',
315
+ yaml: 'yaml',
316
+ toml: 'ini',
317
+ kotlin: 'kotlin',
318
+ swift: 'swift',
319
+ php: 'php',
320
+ csharp: 'csharp',
321
+ dockerfile: 'dockerfile',
322
+ protobuf: 'protobuf',
323
+ scala: 'scala',
324
+ dart: 'dart',
325
+ lua: 'lua',
326
+ perl: 'perl',
327
+ r: 'r',
328
+ objc: 'objectivec',
329
+ powershell: 'powershell',
330
+ makefile: 'makefile',
331
+ nginx: 'nginx',
332
+ ini: 'ini',
333
+ vim: 'vim',
334
+ haskell: 'haskell',
335
+ elixir: 'elixir',
336
+ clojure: 'clojure',
337
+ };
338
+
339
+ export const LANG_FAMILY = {
340
+ js: 'js', javascript: 'js', mjs: 'js', cjs: 'js',
341
+ ts: 'ts', typescript: 'ts', jsx: 'jsx', tsx: 'tsx',
342
+ json: 'json', json5: 'json',
343
+ bash: 'sh', sh: 'sh', shell: 'sh', zsh: 'sh',
344
+ python: 'py', py: 'py',
345
+ css: 'css', scss: 'css', less: 'css',
346
+ html: 'html', xml: 'html',
347
+ md: 'md', markdown: 'md',
348
+ go: 'go', golang: 'go',
349
+ rust: 'rust', rs: 'rust',
350
+ java: 'java',
351
+ c: 'c', cpp: 'c', 'c++': 'c', cc: 'c', h: 'c', hpp: 'c',
352
+ ruby: 'ruby', rb: 'ruby',
353
+ sql: 'sql',
354
+ yaml: 'yaml', yml: 'yaml',
355
+ toml: 'toml',
356
+ kotlin: 'kotlin', kt: 'kotlin', kts: 'kotlin',
357
+ swift: 'swift',
358
+ php: 'php',
359
+ csharp: 'csharp', cs: 'csharp', 'c#': 'csharp',
360
+ dockerfile: 'dockerfile', docker: 'dockerfile',
361
+ graphql: 'graphql', gql: 'graphql',
362
+ protobuf: 'protobuf', proto: 'protobuf',
363
+ scala: 'scala',
364
+ dart: 'dart',
365
+ lua: 'lua',
366
+ perl: 'perl', pl: 'perl',
367
+ r: 'r', rl: 'r',
368
+ objc: 'objc', 'objective-c': 'objc', 'obj-c': 'objc',
369
+ powershell: 'powershell', ps1: 'powershell', ps: 'powershell', pwsh: 'powershell',
370
+ makefile: 'makefile', make: 'makefile',
371
+ nginx: 'nginx',
372
+ ini: 'ini',
373
+ vim: 'vim',
374
+ haskell: 'haskell', hs: 'haskell',
375
+ elixir: 'elixir', ex: 'elixir', exs: 'elixir',
376
+ clojure: 'clojure', clj: 'clojure',
377
+ };
378
+
379
+ const HIGHLIGHT_CACHE_MAX = 300;
380
+ const highlightCache = new Map();
381
+
382
+ /** @internal Test-only introspection for highlight LRU cache. */
383
+ export function _highlightCacheSizeForTests() {
384
+ return highlightCache.size;
385
+ }
386
+
387
+ let _hljsThemeVersion = -1;
388
+ let _hljsTheme = null;
389
+
390
+ /** Map highlight.js token classes to chalk truecolor fns from the active theme. */
391
+ function buildCliHighlightTheme(c) {
392
+ const plain = (s) => c.body(s);
393
+ return {
394
+ default: plain,
395
+ keyword: c.synKeyword,
396
+ built_in: c.synType,
397
+ type: c.synType,
398
+ literal: c.synKeyword,
399
+ number: c.synNumber,
400
+ regexp: c.synString,
401
+ string: c.synString,
402
+ subst: plain,
403
+ symbol: plain,
404
+ class: c.synType,
405
+ function: c.synFunction,
406
+ title: c.synFunction,
407
+ params: c.synVariable,
408
+ comment: c.synComment,
409
+ doctag: c.synComment,
410
+ meta: c.synComment,
411
+ section: c.synType,
412
+ tag: c.synPunct,
413
+ name: c.synFunction,
414
+ builtin: c.synType,
415
+ attr: c.synType,
416
+ attribute: c.synType,
417
+ variable: c.synVariable,
418
+ selector: c.synKeyword,
419
+ template: c.synVariable,
420
+ bullet: plain,
421
+ code: plain,
422
+ emphasis: plain,
423
+ strong: plain,
424
+ formula: plain,
425
+ link: plain,
426
+ quote: plain,
427
+ addition: c.synString,
428
+ deletion: c.synString,
429
+ };
430
+ }
431
+
432
+ function getCliHighlightTheme() {
433
+ const version = getThemeVersion();
434
+ if (_hljsTheme && _hljsThemeVersion === version) return _hljsTheme;
435
+ _hljsThemeVersion = version;
436
+ _hljsTheme = buildCliHighlightTheme(extraColorizers());
437
+ return _hljsTheme;
438
+ }
439
+
440
+ function hljsLanguageFromFamily(family) {
441
+ if (!family || family === 'md') return null;
442
+ const mapped = FAMILY_TO_HLJS[family];
443
+ if (mapped && supportsLanguage(mapped)) return mapped;
444
+ if (supportsLanguage(family)) return family;
445
+ return null;
446
+ }
447
+
448
+ /** Resolve a fenced info-string to a highlight.js language id. */
449
+ function resolveHljsLanguage(lang) {
450
+ const normalized = normalizeLang(lang);
451
+ if (!normalized) return null;
452
+ const viaFamily = hljsLanguageFromFamily(LANG_FAMILY[normalized]);
453
+ if (viaFamily) return viaFamily;
454
+ return supportsLanguage(normalized) ? normalized : null;
455
+ }
456
+
457
+ /**
458
+ * Highlight source with cli-highlight; returns null when language is unsupported
459
+ * or highlighting fails (caller falls back to flat body color).
460
+ */
461
+ function highlightCodeText(text, hljsLang) {
462
+ if (!hljsLang || !supportsLanguage(hljsLang)) return null;
463
+ const src = String(text ?? '');
464
+ if (!src.trim()) return null;
465
+ if (!/[A-Za-z0-9]/.test(src)) return null;
466
+ const cacheKey = `${hljsLang}|${getThemeVersion()}|${src}`;
467
+ const cached = highlightCache.get(cacheKey);
468
+ if (cached !== undefined) {
469
+ highlightCache.delete(cacheKey);
470
+ highlightCache.set(cacheKey, cached);
471
+ return cached;
472
+ }
473
+ try {
474
+ const out = highlight(src, {
475
+ language: hljsLang,
476
+ theme: getCliHighlightTheme(),
477
+ ignoreIllegals: true,
478
+ });
479
+ if (highlightCache.size >= HIGHLIGHT_CACHE_MAX) {
480
+ const first = highlightCache.keys().next().value;
481
+ if (first !== undefined) highlightCache.delete(first);
482
+ }
483
+ highlightCache.set(cacheKey, out);
484
+ return out;
485
+ } catch {
486
+ return null;
487
+ }
488
+ }
489
+
490
+ /** Highlight a single line (tool expanded output); uses the same theme map. */
491
+ export function highlightCodeLine(line, family, c) {
492
+ const hljsLang = hljsLanguageFromFamily(family);
493
+ const highlighted = highlightCodeText(line, hljsLang);
494
+ if (highlighted != null) return highlighted;
495
+ return line ? c.body(line) : '';
496
+ }
497
+
498
+ /**
499
+ * Highlight a multi-line block in one cli-highlight call; returns one ANSI string per line.
500
+ * On miss, each line is flat-colored with `c.body`.
501
+ */
502
+ export function highlightCodeBlockToLines(text, family, c) {
503
+ const hljsLang = hljsLanguageFromFamily(family);
504
+ const raw = String(text ?? '');
505
+ const highlighted = hljsLang ? highlightCodeText(raw, hljsLang) : null;
506
+ if (highlighted != null) {
507
+ return highlighted.split(EOL);
508
+ }
509
+ return raw.split(EOL).map((line) => (line ? c.body(line) : ''));
510
+ }
511
+
512
+ function collectFlatBodyLines(text, codeBlock, bandWidth) {
513
+ const lines = [];
514
+ for (const line of String(text ?? '').split(EOL)) {
515
+ const colored = codeBlock(line);
516
+ lines.push(...wrapCodeLine(colored, bandWidth));
517
+ }
518
+ return lines;
519
+ }
520
+
521
+ function collectHighlightedBodyLines(text, hljsLang, bandWidth) {
522
+ const highlighted = highlightCodeText(text, hljsLang);
523
+ const { codeBlock } = colorizers();
524
+ const source = highlighted != null
525
+ ? highlighted
526
+ : String(text ?? '').split(EOL).map(codeBlock).join(EOL);
527
+ const lines = [];
528
+ for (const line of source.split(EOL)) {
529
+ lines.push(...wrapCodeLine(line, bandWidth));
530
+ }
531
+ return lines;
532
+ }
533
+
534
+ /**
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).
537
+ */
538
+ function renderCodeBlock(token, width = 0) {
539
+ const { codeBlock } = colorizers();
540
+ const c = extraColorizers();
541
+ const lang = normalizeLang(token.lang);
542
+ const text = decodeEntities(token.text ?? '');
543
+ const bandWidth = Math.max(8, Number(width) || 80);
544
+
545
+ let bodyLines;
546
+ if (DIFF_LANGS.has(lang) || (!lang && looksLikeUnifiedDiff(text))) {
547
+ bodyLines = collectDiffBodyLines(text, c, bandWidth);
548
+ } else {
549
+ const hljsLang = resolveHljsLanguage(lang);
550
+ const family = LANG_FAMILY[lang];
551
+ if (hljsLang && family !== 'md') {
552
+ bodyLines = collectHighlightedBodyLines(text, hljsLang, bandWidth);
553
+ } else {
554
+ bodyLines = collectFlatBodyLines(text, codeBlock, bandWidth);
555
+ }
556
+ }
557
+ const { bodyOut } = tintCodeBlockBody(null, bodyLines, c.codeBg);
558
+ return `${bodyOut}${bodyOut ? EOL : ''}`;
559
+ }
560
+
561
+ function numberToLetter(n) {
562
+ let result = '';
563
+ while (n > 0) {
564
+ n--;
565
+ result = String.fromCharCode(97 + (n % 26)) + result;
566
+ n = Math.floor(n / 26);
567
+ }
568
+ return result;
569
+ }
570
+
571
+ const ROMAN_VALUES = [
572
+ [1000, 'm'], [900, 'cm'], [500, 'd'], [400, 'cd'], [100, 'c'], [90, 'xc'],
573
+ [50, 'l'], [40, 'xl'], [10, 'x'], [9, 'ix'], [5, 'v'], [4, 'iv'], [1, 'i'],
574
+ ];
575
+
576
+ function numberToRoman(n) {
577
+ let result = '';
578
+ for (const [value, numeral] of ROMAN_VALUES) {
579
+ while (n >= value) {
580
+ result += numeral;
581
+ n -= value;
582
+ }
583
+ }
584
+ return result;
585
+ }
586
+
587
+ function getListNumber(depth, orderedListNumber) {
588
+ switch (depth) {
589
+ case 0:
590
+ case 1:
591
+ return String(orderedListNumber);
592
+ case 2:
593
+ return numberToLetter(orderedListNumber);
594
+ case 3:
595
+ return numberToRoman(orderedListNumber);
596
+ default:
597
+ return String(orderedListNumber);
598
+ }
599
+ }
600
+
56
601
  /**
57
602
  * Render a single marked token to an ANSI string.
58
- * Mirrors CC's formatToken switch verbatim (minus table / hyperlink deps).
603
+ * marked token switch (minus table / hyperlink deps).
59
604
  */
60
- export function formatToken(token, listDepth = 0, orderedListNumber = null, parent = null) {
605
+ export function formatToken(token, listBaseIndent = 0, orderedListNumber = null, parent = null, width = 0, depth = 0) {
606
+ const { accent, codeBlock, headingAccent, quoteBorder, quoteText, hrLine } = colorizers();
607
+ const ex = extraColorizers();
61
608
  switch (token.type) {
62
609
  case 'blockquote': {
63
- const inner = (token.tokens ?? []).map((t) => formatToken(t)).join('');
64
- const bar = dim(BLOCKQUOTE_BAR);
610
+ const bar = quoteBorder(BLOCKQUOTE_BAR);
611
+ const quotePrefix = `${BLOCKQUOTE_BAR} `;
612
+ const innerWidth = contentWidthAfterPrefix(width, quotePrefix);
613
+ const inner = (token.tokens ?? []).map((t) => formatToken(t, 0, null, null, innerWidth)).join('');
65
614
  return inner
66
615
  .split(EOL)
67
- .map((line) => (stripAnsi(line).trim() ? `${bar} ${chalk.italic(line)}` : line))
616
+ .map((line) => {
617
+ // 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))}`;
620
+ })
68
621
  .join(EOL);
69
622
  }
70
623
  case 'code':
71
- // No syntax highlighter emit the code text as-is.
72
- return token.text + EOL;
624
+ // Fenced block: flush-left wrapped body with syntax highlighting.
625
+ return renderCodeBlock(token, width);
73
626
  case 'codespan':
74
627
  // inline code
75
- return accent(decodeEntities(token.text));
628
+ return ex.codeSpanBg(accent(decodeEntities(token.text)));
76
629
  case 'em':
77
- return chalk.italic((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join(''));
630
+ return ex.emph(chalk.italic((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join('')));
78
631
  case 'strong':
79
- return chalk.bold((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join(''));
632
+ return ex.strong(chalk.bold((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join('')));
80
633
  case 'heading':
81
634
  switch (token.depth) {
82
635
  case 1:
83
636
  return (
84
- chalk.bold.italic.underline(
85
- (token.tokens ?? []).map((t) => formatToken(t)).join(''),
86
- ) + EOL + EOL
637
+ chalk.bold.italic.underline(headingAccent((token.tokens ?? []).map((t) => formatToken(t)).join(''))) + EOL + EOL
87
638
  );
88
639
  default: // h2+
89
- return chalk.bold((token.tokens ?? []).map((t) => formatToken(t)).join('')) + EOL + EOL;
640
+ return chalk.bold(headingAccent((token.tokens ?? []).map((t) => formatToken(t)).join(''))) + EOL + EOL;
90
641
  }
91
- case 'hr':
92
- return '---';
642
+ case 'hr': {
643
+ // Span the available content width with a box-drawing rule. width is only
644
+ // threaded from the top-level token loop; recursive calls pass 0 and fall
645
+ // back to a sane 80-col rule.
646
+ const w = Math.max(3, Number(width) || 80);
647
+ return hrLine(HR_LINE.repeat(w)) + EOL;
648
+ }
93
649
  case 'image':
94
650
  return token.href;
95
651
  case 'link': {
96
652
  if (token.href.startsWith('mailto:')) {
97
- return token.href.replace(/^mailto:/, '');
653
+ return ex.linkText(token.href.replace(/^mailto:/, ''));
98
654
  }
99
655
  const linkText = (token.tokens ?? []).map((t) => formatToken(t, 0, null, token)).join('');
100
656
  const plain = stripAnsi(linkText);
101
- if (plain && plain !== token.href) {
102
- return `${linkText} (${token.href})`;
657
+ const href = token.href;
658
+ // OSC 8 hyperlink: clickable label, URL hidden in supporting terminals
659
+ // (Windows Terminal / iTerm); other terminals show the label text as-is.
660
+ // AnsiText only strips `\x1b[...m` SGR, so the OSC 8 `\x1b]8;;…\x07`
661
+ // sequences pass through untouched for the terminal to interpret.
662
+ const OSC8_OPEN = (url) => `\x1b]8;;${url}\x07`;
663
+ const OSC8_CLOSE = '\x1b]8;;\x07';
664
+ if (plain && plain !== href) {
665
+ const styledLabel = ex.linkText(chalk.underline(linkText));
666
+ return `${OSC8_OPEN(href)}${styledLabel}${OSC8_CLOSE}`;
103
667
  }
104
- return token.href;
668
+ // No distinct label (empty or equal to href): show the URL itself as the
669
+ // clickable, visible text.
670
+ return `${OSC8_OPEN(href)}${ex.link(chalk.underline(href))}${OSC8_CLOSE}`;
105
671
  }
106
672
  case 'list':
107
673
  return token.items
108
674
  .map((item, index) =>
109
- formatToken(item, listDepth, token.ordered ? token.start + index : null, token),
675
+ formatToken(
676
+ item,
677
+ listBaseIndent,
678
+ token.ordered ? Number(token.start || 1) + index : null,
679
+ token,
680
+ width,
681
+ depth,
682
+ ),
110
683
  )
111
684
  .join('');
112
685
  case 'list_item':
113
- return (token.tokens ?? [])
114
- .map((t) => `${' '.repeat(listDepth)}${formatToken(t, listDepth + 1, orderedListNumber, token)}`)
115
- .join('');
686
+ return formatListItem(token, listBaseIndent, orderedListNumber, parent, depth, width);
116
687
  case 'paragraph':
117
- return (token.tokens ?? []).map((t) => formatToken(t)).join('') + EOL;
688
+ return (token.tokens ?? []).map((t) => formatToken(t, 0, null, null, width)).join('') + EOL;
118
689
  case 'space':
119
690
  return EOL;
120
691
  case 'br':
121
692
  return EOL;
122
693
  case 'text':
123
694
  if (parent?.type === 'link') return decodeEntities(token.text);
124
- if (parent?.type === 'list_item') {
125
- const marker = orderedListNumber === null ? '-' : `${getListNumber(listDepth, orderedListNumber)}.`;
126
- const body = token.tokens
127
- ? token.tokens.map((t) => formatToken(t, listDepth, orderedListNumber, token)).join('')
128
- : decodeEntities(token.text);
129
- return `${marker} ${body}${EOL}`;
695
+ if (token.tokens) {
696
+ return token.tokens.map((t) => formatToken(t, listBaseIndent, orderedListNumber, token, width, depth)).join('');
130
697
  }
131
698
  return decodeEntities(token.text);
132
699
  case 'escape':
@@ -140,44 +707,79 @@ export function formatToken(token, listDepth = 0, orderedListNumber = null, pare
140
707
  }
141
708
  }
142
709
 
143
- /* --- ordered-list numbering (CC: numberToLetter / numberToRoman) ---------- */
144
-
145
- function numberToLetter(n) {
146
- let result = '';
147
- while (n > 0) {
148
- n--;
149
- result = String.fromCharCode(97 + (n % 26)) + result;
150
- n = Math.floor(n / 26);
151
- }
152
- return result;
710
+ function trimTrailingEol(value) {
711
+ return String(value ?? '').replace(/\n+$/g, '');
153
712
  }
154
713
 
155
- const ROMAN_VALUES = [
156
- [1000, 'm'], [900, 'cm'], [500, 'd'], [400, 'cd'], [100, 'c'], [90, 'xc'],
157
- [50, 'l'], [40, 'xl'], [10, 'x'], [9, 'ix'], [5, 'v'], [4, 'iv'], [1, 'i'],
158
- ];
714
+ function prefixLines(value, prefix) {
715
+ return String(value ?? '')
716
+ .split(EOL)
717
+ .map((line) => `${prefix}${line}`)
718
+ .join(EOL);
719
+ }
159
720
 
160
- function numberToRoman(n) {
161
- let result = '';
162
- for (const [value, numeral] of ROMAN_VALUES) {
163
- while (n >= value) { result += numeral; n -= value; }
164
- }
165
- return result;
721
+ function prefixFirstAndRest(value, firstPrefix, restPrefix) {
722
+ const lines = String(value ?? '').split(EOL);
723
+ if (lines.length === 0) return '';
724
+ return [
725
+ `${firstPrefix}${lines[0] ?? ''}`,
726
+ ...lines.slice(1).map((line) => `${restPrefix}${line}`),
727
+ ].join(EOL);
166
728
  }
167
729
 
168
- function getListNumber(listDepth, orderedListNumber) {
169
- switch (listDepth) {
170
- case 0:
171
- case 1: return String(orderedListNumber);
172
- case 2: return numberToLetter(orderedListNumber);
173
- case 3: return numberToRoman(orderedListNumber);
174
- default: return String(orderedListNumber);
730
+ function formatListItem(token, listBaseIndent, orderedListNumber, parent, depth = 0, width = 0) {
731
+ const { listBullet } = colorizers();
732
+ const markerPlain = orderedListNumber === null
733
+ ? '-'
734
+ : `${getListNumber(depth, orderedListNumber)}.`;
735
+ const marker = listBullet(markerPlain);
736
+ const markerPrefix = `${' '.repeat(listBaseIndent)}${marker} `;
737
+ const continuationPrefix = ' '.repeat(stripAnsi(markerPrefix).length);
738
+ const nestedListIndent = continuationPrefix.length;
739
+ const childWidth = contentWidthAfterPrefix(width, continuationPrefix);
740
+ const children = token.tokens ?? [];
741
+ let out = '';
742
+ let firstBlock = true;
743
+
744
+ for (const child of children) {
745
+ if (!child) continue;
746
+
747
+ if (child.type === 'space') {
748
+ if (!firstBlock) {
749
+ out += `${continuationPrefix}${EOL}`;
750
+ }
751
+ continue;
752
+ }
753
+
754
+ if (child.type === 'list') {
755
+ if (firstBlock) {
756
+ out += `${markerPrefix.trimEnd()}${EOL}`;
757
+ firstBlock = false;
758
+ }
759
+ // Pass outer `width` so the nested list subtracts only its own marker prefix once.
760
+ out += formatToken(child, nestedListIndent, null, token, width, depth + 1);
761
+ continue;
762
+ }
763
+
764
+ const rendered = formatToken(child, listBaseIndent, orderedListNumber, token, childWidth, depth);
765
+ const body = trimTrailingEol(rendered);
766
+ if (!body) continue;
767
+
768
+ if (firstBlock) {
769
+ out += `${prefixFirstAndRest(body, markerPrefix, continuationPrefix)}${EOL}`;
770
+ firstBlock = false;
771
+ } else {
772
+ out += `${prefixLines(body, continuationPrefix)}${EOL}`;
773
+ }
175
774
  }
775
+
776
+ if (firstBlock) return `${markerPrefix.trimEnd()}${EOL}`;
777
+ return out;
176
778
  }
177
779
 
178
780
  /**
179
781
  * Pad `content` to `targetWidth` per alignment. `displayWidth` is the visible
180
- * width of `content` (ANSI codes excluded). Ported verbatim from CC padAligned.
782
+ * width of `content` (ANSI codes excluded).
181
783
  */
182
784
  export function padAligned(content, displayWidth, targetWidth, align) {
183
785
  const padding = Math.max(0, targetWidth - displayWidth);