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.
- package/package.json +9 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +310 -67
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +20 -9
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +17 -9
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +1040 -2036
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +17 -7
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +8 -12
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +13 -2
- package/src/rules/shared/01-tool.md +23 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
- package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +437 -1439
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +280 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +465 -345
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +121 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +10 -7
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +156 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +302 -117
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +10 -292
- package/src/standalone/explore-tool.mjs +12 -5
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +25 -3
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2883 -778
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +355 -22
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +183 -101
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +161 -23
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6731 -2333
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +569 -948
- package/src/tui/index.jsx +117 -7
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +469 -0
- package/src/tui/markdown/format-token.mjs +128 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +36 -9
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +80 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +75 -27
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +46 -12
- package/src/workflows/sequential/WORKFLOW.md +51 -0
- package/src/workflows/solo/WORKFLOW.md +12 -1
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- 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
|
-
//
|
|
130
|
-
// Diff/patch languages (and bodies that look like unified diffs) keep
|
|
131
|
-
// highlighter; other languages use
|
|
132
|
-
// syntax* palette; unknown
|
|
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 &&
|
|
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 (
|
|
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 -
|
|
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
|
-
|
|
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:
|
|
536
|
-
*
|
|
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
|
|
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,
|
|
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,
|
|
592
|
+
bodyLines = collectHighlightedBodyLines(text, hljsLang, contentWidth);
|
|
553
593
|
} else {
|
|
554
|
-
bodyLines = collectFlatBodyLines(text, codeBlock,
|
|
594
|
+
bodyLines = collectFlatBodyLines(text, codeBlock, contentWidth);
|
|
555
595
|
}
|
|
556
596
|
}
|
|
557
|
-
|
|
558
|
-
|
|
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,
|
|
652
|
+
const { accent, codeBlock, hrLine } = colorizers();
|
|
607
653
|
const ex = extraColorizers();
|
|
608
654
|
switch (token.type) {
|
|
609
655
|
case 'blockquote': {
|
|
610
|
-
|
|
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 (
|
|
619
|
-
return `${bar} ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
689
|
+
chalk.bold.italic.underline((token.tokens ?? []).map((t) => formatToken(t)).join('')) + EOL + EOL
|
|
638
690
|
);
|
|
639
691
|
default: // h2+
|
|
640
|
-
return chalk.bold(
|
|
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(
|
|
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(
|
|
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('
|
|
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
|
|
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
|
|
32
|
-
|
|
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
|
|
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,
|
|
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.
|
|
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
|
|
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,
|
|
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 >=
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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 ['
|
|
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('
|
|
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('
|
|
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`';
|