mixdog 0.8.1 → 0.9.0
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/README.md +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +205 -33
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1222 -90
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2212 -446
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +110 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +76 -28
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +520 -98
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -95
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4788 -2151
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +592 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8826 -2390
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* components/AnsiText.jsx — render ANSI SGR text as ink <Text> spans.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Parses ANSI into styled Text nodes instead of relying on raw
|
|
5
5
|
* escape passthrough. This keeps markdown emphasis/code colors stable when an
|
|
6
6
|
* inner span resets color back to the default.
|
|
7
7
|
*/
|
|
8
8
|
import React from 'react';
|
|
9
9
|
import { Text } from 'ink';
|
|
10
|
-
import { theme } from '../theme.mjs';
|
|
10
|
+
import { theme, getThemeVersion } from '../theme.mjs';
|
|
11
11
|
|
|
12
12
|
const ANSI_RE = /\x1b\[([0-9;]*)m/g;
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
14
|
+
// Resolve the named SGR (30-37/90-97) → theme color map at call time so a
|
|
15
|
+
// live `/theme` switch is honored. `theme` is mutated in-place on switch, so
|
|
16
|
+
// reading the keys per parse keeps colors in sync without a module reload.
|
|
17
|
+
function ansiColorMap() {
|
|
18
|
+
return {
|
|
19
|
+
30: theme.subtle,
|
|
20
|
+
31: theme.error,
|
|
21
|
+
32: theme.success,
|
|
22
|
+
33: theme.warning,
|
|
23
|
+
34: theme.code,
|
|
24
|
+
35: 'ansi:magenta',
|
|
25
|
+
36: 'ansi:cyan',
|
|
26
|
+
37: theme.text,
|
|
27
|
+
90: theme.subtle,
|
|
28
|
+
91: theme.error,
|
|
29
|
+
92: theme.success,
|
|
30
|
+
93: theme.warning,
|
|
31
|
+
94: theme.code,
|
|
32
|
+
95: 'ansi:magentaBright',
|
|
33
|
+
96: 'ansi:cyanBright',
|
|
34
|
+
97: theme.statusText,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
32
37
|
|
|
33
38
|
const ANSI_BG_COLORS = {
|
|
34
39
|
40: 'ansi:black',
|
|
@@ -74,7 +79,7 @@ function rgbCode(codes, index) {
|
|
|
74
79
|
return `rgb(${r},${g},${b})`;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
function applySgr(state, codes, defaultColor) {
|
|
82
|
+
function applySgr(state, codes, defaultColor, ansiColors) {
|
|
78
83
|
if (!codes.length) codes = [0];
|
|
79
84
|
for (let i = 0; i < codes.length; i++) {
|
|
80
85
|
const code = codes[i];
|
|
@@ -133,8 +138,8 @@ function applySgr(state, codes, defaultColor) {
|
|
|
133
138
|
state.backgroundColor = undefined;
|
|
134
139
|
break;
|
|
135
140
|
default:
|
|
136
|
-
if (
|
|
137
|
-
state.color =
|
|
141
|
+
if (ansiColors[code]) {
|
|
142
|
+
state.color = ansiColors[code];
|
|
138
143
|
} else if (ANSI_BG_COLORS[code]) {
|
|
139
144
|
state.backgroundColor = ANSI_BG_COLORS[code];
|
|
140
145
|
}
|
|
@@ -147,6 +152,7 @@ function parseAnsi(text, defaultColor) {
|
|
|
147
152
|
const source = String(text ?? '');
|
|
148
153
|
const spans = [];
|
|
149
154
|
const state = defaultState(defaultColor);
|
|
155
|
+
const ansiColors = ansiColorMap();
|
|
150
156
|
let lastIndex = 0;
|
|
151
157
|
let match;
|
|
152
158
|
|
|
@@ -159,7 +165,7 @@ function parseAnsi(text, defaultColor) {
|
|
|
159
165
|
.split(';')
|
|
160
166
|
.filter(Boolean)
|
|
161
167
|
.map((n) => Number(n));
|
|
162
|
-
applySgr(state, codes, defaultColor);
|
|
168
|
+
applySgr(state, codes, defaultColor, ansiColors);
|
|
163
169
|
lastIndex = ANSI_RE.lastIndex;
|
|
164
170
|
}
|
|
165
171
|
|
|
@@ -171,9 +177,13 @@ function parseAnsi(text, defaultColor) {
|
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
export function AnsiText({ children, defaultColor, wrap }) {
|
|
180
|
+
// ansiColorMap() reads live theme.* keys, so the parsed spans depend on the
|
|
181
|
+
// active theme. Include the theme version in the memo deps so a /theme switch
|
|
182
|
+
// re-parses with the new palette instead of reusing stale span colors.
|
|
183
|
+
const themeVersion = getThemeVersion();
|
|
174
184
|
const spans = React.useMemo(
|
|
175
185
|
() => parseAnsi(children, defaultColor),
|
|
176
|
-
[children, defaultColor],
|
|
186
|
+
[children, defaultColor, themeVersion],
|
|
177
187
|
);
|
|
178
188
|
|
|
179
189
|
return (
|
|
@@ -183,9 +193,10 @@ export function AnsiText({ children, defaultColor, wrap }) {
|
|
|
183
193
|
key={index}
|
|
184
194
|
color={span.style.color}
|
|
185
195
|
backgroundColor={span.style.backgroundColor}
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
bold
|
|
196
|
+
// Honor SGR bold only on spans that chalk/markdown set (e.g. **strong**,
|
|
197
|
+
// headings). Do not force bold globally — avoids fuzzy Korean body text
|
|
198
|
+
// when models emit no bold codes.
|
|
199
|
+
bold={span.style.bold}
|
|
189
200
|
dimColor={span.style.dimColor}
|
|
190
201
|
italic={span.style.italic}
|
|
191
202
|
underline={span.style.underline}
|
|
@@ -171,11 +171,13 @@ function ContextUsageView({ detail, columns }) {
|
|
|
171
171
|
const barWidth = Math.max(12, Math.min(34, innerWidth - stringWidth(summaryText) - stringWidth(pctText) - 5));
|
|
172
172
|
const builtInToolTokens = bucketTokens(schema, ['code', 'web', 'mutation', 'channels', 'setup', 'other']);
|
|
173
173
|
const builtInToolCount = bucketCount(schema, ['code', 'web', 'mutation', 'channels', 'setup', 'other']);
|
|
174
|
-
const
|
|
174
|
+
const sessionTokens = semanticTokens(semantic, ['workspace', 'environment', 'other']);
|
|
175
175
|
const compactionLine = metricValue([
|
|
176
176
|
compaction.stage && compaction.stage !== 'pending' ? compaction.stage : '',
|
|
177
177
|
compaction.state,
|
|
178
|
+
compaction.type ? `type ${compaction.type}` : '',
|
|
178
179
|
compaction.triggerTokens ? `trigger ${formatTokens(compaction.triggerTokens)}` : '',
|
|
180
|
+
compaction.boundaryTokens ? `boundary ${formatTokens(compaction.boundaryTokens)}` : '',
|
|
179
181
|
]);
|
|
180
182
|
const sourceLine = metricValue([
|
|
181
183
|
usage.effective ? `effective ${formatTokens(windowTokens)}` : `window ${formatTokens(windowTokens)}`,
|
|
@@ -192,7 +194,7 @@ function ContextUsageView({ detail, columns }) {
|
|
|
192
194
|
{ label: 'MCP', tokens: bucketTokens(schema, ['mcp']), meta: `${mcp.connected || 0}/${mcp.configured || 0} servers` },
|
|
193
195
|
{ label: 'Skills', tokens: bucketTokens(schema, ['skills']), meta: `${extensions.skills || 0} skills` },
|
|
194
196
|
{ label: 'Memory', tokens: semanticTokens(semantic, ['memory']) + bucketTokens(schema, ['memory']), meta: 'core + recall tools' },
|
|
195
|
-
{ label: '
|
|
197
|
+
{ label: 'Session', tokens: sessionTokens, meta: 'workspace · environment' },
|
|
196
198
|
{ label: 'Workflow', tokens: semanticTokens(semantic, ['workflow']) + bucketTokens(schema, ['agents']), meta: 'workflow · agents' },
|
|
197
199
|
{ label: 'System', tokens: semanticTokens(semantic, ['system']), meta: 'rules · role catalog' },
|
|
198
200
|
{ label: 'Overhead', tokens: finiteNumber(request.overheadTokens), meta: 'request frame' },
|
|
@@ -223,7 +225,7 @@ function ContextUsageView({ detail, columns }) {
|
|
|
223
225
|
);
|
|
224
226
|
}
|
|
225
227
|
|
|
226
|
-
export function ContextPanel({ rows, title = 'Context Usage', columns = 80, fillHeight = false, detail = null }) {
|
|
228
|
+
export function ContextPanel({ rows, title = 'Context Usage', columns = 80, fillHeight = false, detail = null, description = '' }) {
|
|
227
229
|
const safeRows = Array.isArray(rows) ? rows : [];
|
|
228
230
|
const labelWidth = Math.min(
|
|
229
231
|
safeRows.reduce((w, row) => Math.max(w, String(row.label || '').length), 0),
|
|
@@ -231,6 +233,11 @@ export function ContextPanel({ rows, title = 'Context Usage', columns = 80, fill
|
|
|
231
233
|
);
|
|
232
234
|
const valueWidth = Math.max(0, columns - labelWidth - 8);
|
|
233
235
|
const isContextUsage = detail?.type === 'context';
|
|
236
|
+
// Standard panel rhythm: title row, blank, description/hint row, blank, content.
|
|
237
|
+
const panelDescription = truncateText(
|
|
238
|
+
String(description || (isContextUsage ? 'Live context window usage by category.' : '')).replace(/\s+/g, ' ').trim(),
|
|
239
|
+
Math.max(0, columns - 4),
|
|
240
|
+
);
|
|
234
241
|
|
|
235
242
|
return (
|
|
236
243
|
<Box flexDirection="column" flexShrink={0} width="100%" height={fillHeight ? '100%' : undefined}>
|
|
@@ -242,10 +249,13 @@ export function ContextPanel({ rows, title = 'Context Usage', columns = 80, fill
|
|
|
242
249
|
width="100%"
|
|
243
250
|
height={fillHeight ? '100%' : undefined}
|
|
244
251
|
>
|
|
245
|
-
<Box flexDirection="row" justifyContent="space-between"
|
|
252
|
+
<Box flexDirection="row" justifyContent="space-between">
|
|
246
253
|
<Text color={theme.panelTitle}>{title}</Text>
|
|
247
254
|
<Text color={theme.subtle}>Esc back</Text>
|
|
248
255
|
</Box>
|
|
256
|
+
<Text> </Text>
|
|
257
|
+
<Text color={theme.subtle}>{panelDescription || ' '}</Text>
|
|
258
|
+
<Text> </Text>
|
|
249
259
|
{isContextUsage ? (
|
|
250
260
|
<ContextUsageView detail={detail} columns={columns} />
|
|
251
261
|
) : (
|
|
@@ -1,93 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* components/Markdown.jsx — markdown → ink, hybrid renderer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - marked.lexer() produces the token stream
|
|
4
|
+
* Markdown → ink hybrid renderer:
|
|
5
|
+
* - marked.lexer() produces the token stream.
|
|
6
6
|
* - Non-table tokens are rendered to ANSI strings via formatToken and emitted
|
|
7
|
-
* through <AnsiText
|
|
7
|
+
* through <AnsiText> as styled spans.
|
|
8
8
|
* - Tables are rendered by the MarkdownTable component (proper Box layout).
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
9
|
+
* - Block tokens are emitted as separate Ink children so lists, code fences,
|
|
10
|
+
* and tables keep their markdown block boundaries.
|
|
11
11
|
*
|
|
12
|
-
* Syntax highlighting
|
|
12
|
+
* Syntax highlighting is omitted, but token cache + streaming-split are
|
|
13
13
|
* kept so partial markdown does not repaint stable text on every delta.
|
|
14
14
|
*/
|
|
15
15
|
import React, { useRef } from 'react';
|
|
16
16
|
import { Box, Text } from 'ink';
|
|
17
17
|
import { marked } from 'marked';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
configureMarked,
|
|
20
|
+
hasMarkdownSyntax,
|
|
21
|
+
renderTokenAnsiSegments,
|
|
22
|
+
} from '../markdown/render-ansi.mjs';
|
|
23
|
+
import { trimPartialClosingFences } from '../markdown/stream-fence.mjs';
|
|
19
24
|
import { AnsiText } from './AnsiText.jsx';
|
|
20
25
|
import { MarkdownTable } from './MarkdownTable.jsx';
|
|
21
26
|
import { theme } from '../theme.mjs';
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// Disable strikethrough: models use ~ for "approximate" (~100), not <del>.
|
|
28
|
-
marked.use({ tokenizer: { del() { return undefined; } } });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const TOKEN_CACHE_MAX = 500;
|
|
32
|
-
const tokenCache = new Map();
|
|
33
|
-
const MD_SYNTAX_RE = /[#*`|[>\-_~]|\n\n|^\d+\. |\n\d+\. /;
|
|
34
|
-
|
|
35
|
-
function hasMarkdownSyntax(text) {
|
|
36
|
-
const sample = text.length > 500 ? text.slice(0, 500) : text;
|
|
37
|
-
return MD_SYNTAX_RE.test(sample);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function cachedLexer(content) {
|
|
41
|
-
const text = String(content ?? '');
|
|
42
|
-
if (!hasMarkdownSyntax(text)) {
|
|
43
|
-
return [{
|
|
44
|
-
type: 'paragraph',
|
|
45
|
-
raw: text,
|
|
46
|
-
text,
|
|
47
|
-
tokens: [{ type: 'text', raw: text, text }],
|
|
48
|
-
}];
|
|
49
|
-
}
|
|
50
|
-
const hit = tokenCache.get(text);
|
|
51
|
-
if (hit) {
|
|
52
|
-
tokenCache.delete(text);
|
|
53
|
-
tokenCache.set(text, hit);
|
|
54
|
-
return hit;
|
|
55
|
-
}
|
|
56
|
-
const tokens = marked.lexer(text);
|
|
57
|
-
if (tokenCache.size >= TOKEN_CACHE_MAX) {
|
|
58
|
-
const first = tokenCache.keys().next().value;
|
|
59
|
-
if (first !== undefined) tokenCache.delete(first);
|
|
60
|
-
}
|
|
61
|
-
tokenCache.set(text, tokens);
|
|
62
|
-
return tokens;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function renderMarkdownElements(content) {
|
|
66
|
-
configureMarked();
|
|
67
|
-
const tokens = cachedLexer(String(content ?? ''));
|
|
28
|
+
function renderMarkdownElements(content, trimPartialFences = false, tableWidth) {
|
|
29
|
+
// `tableWidth` is the App's body/content width; it doubles as the hr fill
|
|
30
|
+
// width. Fall back to 80 when not provided so an hr still spans a sane rule.
|
|
31
|
+
const segments = renderTokenAnsiSegments(content, { trimPartialFences, width: tableWidth || 80 });
|
|
68
32
|
const result = [];
|
|
69
|
-
let buffer = '';
|
|
70
33
|
let idx = 0;
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
result.push(<
|
|
79
|
-
buffer = '';
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
for (const token of tokens) {
|
|
83
|
-
if (token.type === 'table') {
|
|
84
|
-
flush();
|
|
85
|
-
result.push(<MarkdownTable key={`md_${idx++}`} token={token} />);
|
|
34
|
+
for (const segment of segments) {
|
|
35
|
+
if (segment.type === 'table') {
|
|
36
|
+
// Pass the App's body width as forceWidth so the table is laid out at the
|
|
37
|
+
// SAME width the row-height estimator measures (measureMarkdownTableRows).
|
|
38
|
+
// Without it MarkdownTable falls back to useStdout().columns, which on
|
|
39
|
+
// win32 is frameColumns+1 — a 1-col gap that can flip the table between
|
|
40
|
+
// horizontal and vertical layout and top-clip streaming output.
|
|
41
|
+
result.push(<MarkdownTable key={`md_${idx++}`} token={segment.token} forceWidth={tableWidth} />);
|
|
86
42
|
} else {
|
|
87
|
-
|
|
43
|
+
// defaultColor={theme.text} keeps ANSI resets on the same dark-theme
|
|
44
|
+
// foreground instead of the terminal profile's default foreground.
|
|
45
|
+
result.push(
|
|
46
|
+
<AnsiText key={`md_${idx++}`} defaultColor={theme.text}>{segment.ansi}</AnsiText>,
|
|
47
|
+
);
|
|
88
48
|
}
|
|
89
49
|
}
|
|
90
|
-
flush();
|
|
91
50
|
return result;
|
|
92
51
|
}
|
|
93
52
|
|
|
@@ -145,15 +104,18 @@ function balanceStreamingMarkdown(text) {
|
|
|
145
104
|
return rendered;
|
|
146
105
|
}
|
|
147
106
|
|
|
148
|
-
export function Markdown({ children }) {
|
|
107
|
+
export function Markdown({ children, themeEpoch = 0, trimPartialFences = false, columns }) {
|
|
149
108
|
const elements = React.useMemo(() => {
|
|
150
109
|
try {
|
|
151
|
-
return renderMarkdownElements(children);
|
|
110
|
+
return renderMarkdownElements(children, trimPartialFences, columns);
|
|
152
111
|
} catch {
|
|
153
112
|
// Never throw into the render tree — fall back to raw text.
|
|
154
113
|
return [<Text key="md_0" color={theme.text}>{String(children ?? '')}</Text>];
|
|
155
114
|
}
|
|
156
|
-
|
|
115
|
+
// themeEpoch is a memo dep so a /theme switch re-renders to ANSI with the new
|
|
116
|
+
// md* colors (formatToken re-resolves its colorizers on the active theme).
|
|
117
|
+
// columns is a dep so a resize re-lays-out tables at the new forceWidth.
|
|
118
|
+
}, [children, themeEpoch, trimPartialFences, columns]);
|
|
157
119
|
|
|
158
120
|
return (
|
|
159
121
|
<Box flexDirection="column" gap={1}>
|
|
@@ -162,13 +124,13 @@ export function Markdown({ children }) {
|
|
|
162
124
|
);
|
|
163
125
|
}
|
|
164
126
|
|
|
165
|
-
export function StreamingMarkdown({ children }) {
|
|
127
|
+
export function StreamingMarkdown({ children, themeEpoch = 0, columns }) {
|
|
166
128
|
const stablePrefixRef = useRef('');
|
|
167
129
|
const text = String(children ?? '');
|
|
168
130
|
|
|
169
131
|
if (!hasMarkdownSyntax(text)) {
|
|
170
132
|
stablePrefixRef.current = '';
|
|
171
|
-
return <Markdown>{text}</Markdown>;
|
|
133
|
+
return <Markdown themeEpoch={themeEpoch} columns={columns}>{text}</Markdown>;
|
|
172
134
|
}
|
|
173
135
|
|
|
174
136
|
if (!text.startsWith(stablePrefixRef.current)) {
|
|
@@ -179,7 +141,11 @@ export function StreamingMarkdown({ children }) {
|
|
|
179
141
|
try {
|
|
180
142
|
configureMarked();
|
|
181
143
|
const boundary = stablePrefix.length;
|
|
144
|
+
// Lex the still-streaming suffix and trim any partial closing fence so an
|
|
145
|
+
// open code block does not grow-then-shrink as the final backtick(s)
|
|
146
|
+
// stream in. Operates on a fresh lex so the shared tokenCache is untouched.
|
|
182
147
|
const tokens = marked.lexer(text.substring(boundary));
|
|
148
|
+
trimPartialClosingFences(tokens);
|
|
183
149
|
let lastContentIdx = tokens.length - 1;
|
|
184
150
|
while (lastContentIdx >= 0 && tokens[lastContentIdx]?.type === 'space') lastContentIdx--;
|
|
185
151
|
let advance = 0;
|
|
@@ -198,8 +164,8 @@ export function StreamingMarkdown({ children }) {
|
|
|
198
164
|
const unstableSuffix = text.substring(stablePrefix.length);
|
|
199
165
|
return (
|
|
200
166
|
<Box flexDirection="column" gap={1}>
|
|
201
|
-
{stablePrefix ? <Markdown>{stablePrefix}</Markdown> : null}
|
|
202
|
-
{unstableSuffix ? <Markdown>{balanceStreamingMarkdown(unstableSuffix)}</Markdown> : null}
|
|
167
|
+
{stablePrefix ? <Markdown themeEpoch={themeEpoch} columns={columns}>{stablePrefix}</Markdown> : null}
|
|
168
|
+
{unstableSuffix ? <Markdown themeEpoch={themeEpoch} columns={columns} trimPartialFences>{balanceStreamingMarkdown(unstableSuffix)}</Markdown> : null}
|
|
203
169
|
</Box>
|
|
204
170
|
);
|
|
205
171
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* components/MarkdownTable.jsx — GFM table → ink Box layout.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* adapted for this CLI:
|
|
4
|
+
* GFM table layout for ink:
|
|
6
5
|
* - <Ansi> → <Text> (ink 7 has no <Ansi>; <Text> passes ANSI escapes through).
|
|
7
6
|
* - useTerminalSize() → ink useStdout().stdout.columns.
|
|
8
|
-
* - stringWidth / wrapAnsi from
|
|
7
|
+
* - stringWidth / wrapAnsi from npm packages.
|
|
9
8
|
* - formatCell uses our format-token.formatToken (no highlight arg).
|
|
10
9
|
*
|
|
11
10
|
* The width-fitting algorithm (ideal vs min widths, proportional shrink, hard
|
|
@@ -13,29 +12,7 @@
|
|
|
13
12
|
*/
|
|
14
13
|
import React from 'react';
|
|
15
14
|
import { Text, useStdout } from 'ink';
|
|
16
|
-
import
|
|
17
|
-
import stringWidth from 'string-width';
|
|
18
|
-
import wrapAnsi from 'wrap-ansi';
|
|
19
|
-
import { formatToken, padAligned } from '../markdown/format-token.mjs';
|
|
20
|
-
|
|
21
|
-
const SAFETY_MARGIN = 4;
|
|
22
|
-
const MIN_COLUMN_WIDTH = 3;
|
|
23
|
-
const MAX_ROW_LINES = 4;
|
|
24
|
-
const ANSI_BOLD_START = '\x1b[1m';
|
|
25
|
-
const ANSI_BOLD_END = '\x1b[22m';
|
|
26
|
-
|
|
27
|
-
/** Wrap text to width, ANSI-aware, returning lines (CC wrapText). */
|
|
28
|
-
function wrapText(text, width, options) {
|
|
29
|
-
if (width <= 0) return [text];
|
|
30
|
-
const trimmedText = String(text).trimEnd();
|
|
31
|
-
const wrapped = wrapAnsi(trimmedText, width, {
|
|
32
|
-
hard: options?.hard ?? false,
|
|
33
|
-
trim: false,
|
|
34
|
-
wordWrap: true,
|
|
35
|
-
});
|
|
36
|
-
const lines = wrapped.split('\n').filter((line) => line.length > 0);
|
|
37
|
-
return lines.length > 0 ? lines : [''];
|
|
38
|
-
}
|
|
15
|
+
import { buildTableRender } from '../markdown/table-layout.mjs';
|
|
39
16
|
|
|
40
17
|
export function MarkdownTable({ token, forceWidth }) {
|
|
41
18
|
// App owns resize reflow and there is no <Static> transcript anymore, so table
|
|
@@ -43,162 +20,10 @@ export function MarkdownTable({ token, forceWidth }) {
|
|
|
43
20
|
const actualTerminalWidth = useStdout()?.stdout?.columns ?? 80;
|
|
44
21
|
const terminalWidth = forceWidth ?? actualTerminalWidth;
|
|
45
22
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (words.length === 0) return MIN_COLUMN_WIDTH;
|
|
53
|
-
return Math.max(...words.map((w) => stringWidth(w)), MIN_COLUMN_WIDTH);
|
|
54
|
-
};
|
|
55
|
-
const getIdealWidth = (tokens) =>
|
|
56
|
-
Math.max(stringWidth(getPlainText(tokens)), MIN_COLUMN_WIDTH);
|
|
57
|
-
|
|
58
|
-
// Step 1: min (longest word) and ideal (full content) widths per column.
|
|
59
|
-
const minWidths = token.header.map((header, colIndex) => {
|
|
60
|
-
let maxMinWidth = getMinWidth(header.tokens);
|
|
61
|
-
for (const row of token.rows) maxMinWidth = Math.max(maxMinWidth, getMinWidth(row[colIndex]?.tokens));
|
|
62
|
-
return maxMinWidth;
|
|
63
|
-
});
|
|
64
|
-
const idealWidths = token.header.map((header, colIndex) => {
|
|
65
|
-
let maxIdeal = getIdealWidth(header.tokens);
|
|
66
|
-
for (const row of token.rows) maxIdeal = Math.max(maxIdeal, getIdealWidth(row[colIndex]?.tokens));
|
|
67
|
-
return maxIdeal;
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// Step 2: available space.
|
|
71
|
-
const numCols = token.header.length;
|
|
72
|
-
const borderOverhead = 1 + numCols * 3;
|
|
73
|
-
const availableWidth = Math.max(terminalWidth - borderOverhead - SAFETY_MARGIN, numCols * MIN_COLUMN_WIDTH);
|
|
74
|
-
|
|
75
|
-
// Step 3: fit column widths into available space.
|
|
76
|
-
const totalMin = minWidths.reduce((s, w) => s + w, 0);
|
|
77
|
-
const totalIdeal = idealWidths.reduce((s, w) => s + w, 0);
|
|
78
|
-
let needsHardWrap = false;
|
|
79
|
-
let columnWidths;
|
|
80
|
-
if (totalIdeal <= availableWidth) {
|
|
81
|
-
columnWidths = idealWidths;
|
|
82
|
-
} else if (totalMin <= availableWidth) {
|
|
83
|
-
const extraSpace = availableWidth - totalMin;
|
|
84
|
-
const overflows = idealWidths.map((ideal, i) => ideal - minWidths[i]);
|
|
85
|
-
const totalOverflow = overflows.reduce((s, o) => s + o, 0);
|
|
86
|
-
columnWidths = minWidths.map((min, i) => {
|
|
87
|
-
if (totalOverflow === 0) return min;
|
|
88
|
-
return min + Math.floor((overflows[i] / totalOverflow) * extraSpace);
|
|
89
|
-
});
|
|
90
|
-
} else {
|
|
91
|
-
needsHardWrap = true;
|
|
92
|
-
const scaleFactor = availableWidth / totalMin;
|
|
93
|
-
columnWidths = minWidths.map((w) => Math.max(Math.floor(w * scaleFactor), MIN_COLUMN_WIDTH));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Step 4: max row lines → decide vertical fallback.
|
|
97
|
-
const calculateMaxRowLines = () => {
|
|
98
|
-
let maxLines = 1;
|
|
99
|
-
for (let i = 0; i < token.header.length; i++) {
|
|
100
|
-
const wrapped = wrapText(formatCell(token.header[i].tokens), columnWidths[i], { hard: needsHardWrap });
|
|
101
|
-
maxLines = Math.max(maxLines, wrapped.length);
|
|
102
|
-
}
|
|
103
|
-
for (const row of token.rows) {
|
|
104
|
-
for (let i = 0; i < row.length; i++) {
|
|
105
|
-
const wrapped = wrapText(formatCell(row[i]?.tokens), columnWidths[i], { hard: needsHardWrap });
|
|
106
|
-
maxLines = Math.max(maxLines, wrapped.length);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return maxLines;
|
|
110
|
-
};
|
|
111
|
-
const useVerticalFormat = calculateMaxRowLines() > MAX_ROW_LINES;
|
|
112
|
-
|
|
113
|
-
const renderRowLines = (cells, isHeader) => {
|
|
114
|
-
const cellLines = cells.map((cell, colIndex) =>
|
|
115
|
-
wrapText(formatCell(cell.tokens), columnWidths[colIndex], { hard: needsHardWrap }),
|
|
116
|
-
);
|
|
117
|
-
const maxLines = Math.max(...cellLines.map((l) => l.length), 1);
|
|
118
|
-
const verticalOffsets = cellLines.map((l) => Math.floor((maxLines - l.length) / 2));
|
|
119
|
-
const result = [];
|
|
120
|
-
for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
|
|
121
|
-
let line = '│';
|
|
122
|
-
for (let colIndex = 0; colIndex < cells.length; colIndex++) {
|
|
123
|
-
const lines = cellLines[colIndex];
|
|
124
|
-
const offset = verticalOffsets[colIndex];
|
|
125
|
-
const contentLineIdx = lineIdx - offset;
|
|
126
|
-
const lineText = contentLineIdx >= 0 && contentLineIdx < lines.length ? lines[contentLineIdx] : '';
|
|
127
|
-
const width = columnWidths[colIndex];
|
|
128
|
-
const align = isHeader ? 'center' : token.align?.[colIndex] ?? 'left';
|
|
129
|
-
line += ' ' + padAligned(lineText, stringWidth(lineText), width, align) + ' │';
|
|
130
|
-
}
|
|
131
|
-
result.push(line);
|
|
132
|
-
}
|
|
133
|
-
return result;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const renderBorderLine = (type) => {
|
|
137
|
-
const [left, mid, cross, right] = {
|
|
138
|
-
top: ['┌', '─', '┬', '┐'],
|
|
139
|
-
middle: ['├', '─', '┼', '┤'],
|
|
140
|
-
bottom: ['└', '─', '┴', '┘'],
|
|
141
|
-
}[type];
|
|
142
|
-
let line = left;
|
|
143
|
-
columnWidths.forEach((width, colIndex) => {
|
|
144
|
-
line += mid.repeat(width + 2);
|
|
145
|
-
line += colIndex < columnWidths.length - 1 ? cross : right;
|
|
146
|
-
});
|
|
147
|
-
return line;
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const renderVerticalFormat = () => {
|
|
151
|
-
const lines = [];
|
|
152
|
-
const headers = token.header.map((h) => getPlainText(h.tokens));
|
|
153
|
-
const separatorWidth = Math.min(terminalWidth - 1, 40);
|
|
154
|
-
const separator = '─'.repeat(separatorWidth);
|
|
155
|
-
const wrapIndent = ' ';
|
|
156
|
-
token.rows.forEach((row, rowIndex) => {
|
|
157
|
-
if (rowIndex > 0) lines.push(separator);
|
|
158
|
-
row.forEach((cell, colIndex) => {
|
|
159
|
-
const label = headers[colIndex] || `Column ${colIndex + 1}`;
|
|
160
|
-
const rawValue = formatCell(cell.tokens).trimEnd();
|
|
161
|
-
const value = rawValue.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
162
|
-
const firstLineWidth = terminalWidth - stringWidth(label) - 3;
|
|
163
|
-
const subsequentLineWidth = terminalWidth - wrapIndent.length - 1;
|
|
164
|
-
const firstPassLines = wrapText(value, Math.max(firstLineWidth, 10));
|
|
165
|
-
const firstLine = firstPassLines[0] || '';
|
|
166
|
-
let wrappedValue;
|
|
167
|
-
if (firstPassLines.length <= 1 || subsequentLineWidth <= firstLineWidth) {
|
|
168
|
-
wrappedValue = firstPassLines;
|
|
169
|
-
} else {
|
|
170
|
-
const remainingText = firstPassLines.slice(1).map((l) => l.trim()).join(' ');
|
|
171
|
-
wrappedValue = [firstLine, ...wrapText(remainingText, subsequentLineWidth)];
|
|
172
|
-
}
|
|
173
|
-
lines.push(`${ANSI_BOLD_START}${label}:${ANSI_BOLD_END} ${wrappedValue[0] || ''}`);
|
|
174
|
-
for (let i = 1; i < wrappedValue.length; i++) {
|
|
175
|
-
if (!wrappedValue[i].trim()) continue;
|
|
176
|
-
lines.push(`${wrapIndent}${wrappedValue[i]}`);
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
return lines.join('\n');
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
if (useVerticalFormat) {
|
|
184
|
-
return <Text>{renderVerticalFormat()}</Text>;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const tableLines = [];
|
|
188
|
-
tableLines.push(renderBorderLine('top'));
|
|
189
|
-
tableLines.push(...renderRowLines(token.header, true));
|
|
190
|
-
tableLines.push(renderBorderLine('middle'));
|
|
191
|
-
token.rows.forEach((row, rowIndex) => {
|
|
192
|
-
tableLines.push(...renderRowLines(row, false));
|
|
193
|
-
if (rowIndex < token.rows.length - 1) tableLines.push(renderBorderLine('middle'));
|
|
194
|
-
});
|
|
195
|
-
tableLines.push(renderBorderLine('bottom'));
|
|
196
|
-
|
|
197
|
-
// Safety: if any line would overflow (resize race), fall back to vertical.
|
|
198
|
-
const maxLineWidth = Math.max(...tableLines.map((l) => stringWidth(stripAnsi(l))));
|
|
199
|
-
if (maxLineWidth > terminalWidth - SAFETY_MARGIN) {
|
|
200
|
-
return <Text>{renderVerticalFormat()}</Text>;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return <Text>{tableLines.join('\n')}</Text>;
|
|
23
|
+
// The entire layout (column fit, vertical fallback, bordered box, overflow
|
|
24
|
+
// re-fallback) lives in the pure markdown/table-layout.mjs module so the
|
|
25
|
+
// renderer and the App.jsx row-height estimator share one source of truth and
|
|
26
|
+
// can never drift. The component just draws the lines it returns verbatim.
|
|
27
|
+
const { lines } = buildTableRender(token, terminalWidth);
|
|
28
|
+
return <Text>{lines.join('\n')}</Text>;
|
|
204
29
|
}
|
|
@@ -1,42 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* components/Message.jsx — transcript message rows.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* in the `text` color (
|
|
4
|
+
* Transcript message rows:
|
|
5
|
+
* - Assistant: `● <markdown>` where the dot is `BLACK_CIRCLE`
|
|
6
|
+
* in the `text` color (not the orange title accent), in a 2-wide gutter, and
|
|
7
7
|
* the markdown sits in a column box beside it (flexDirection row).
|
|
8
|
-
* -
|
|
9
|
-
* `paddingRight={1}` (no prompt glyph —
|
|
8
|
+
* - User: text on a `userMessageBackground` background with
|
|
9
|
+
* `paddingRight={1}` (no prompt glyph — the bg distinguishes input
|
|
10
10
|
* from assistant/tool rows).
|
|
11
11
|
*
|
|
12
12
|
* The dot uses `minWidth={2}` so wrapped markdown lines align under the text,
|
|
13
|
-
* not under the dot (
|
|
13
|
+
* not under the dot (2-wide gutter alignment).
|
|
14
14
|
*/
|
|
15
15
|
import React from 'react';
|
|
16
16
|
import { Box, Text, useAnimation } from 'ink';
|
|
17
17
|
import { theme, TURN_MARKER } from '../theme.mjs';
|
|
18
18
|
import { Markdown, StreamingMarkdown } from './Markdown.jsx';
|
|
19
|
+
import { assistantBodyWidth } from '../markdown/table-layout.mjs';
|
|
19
20
|
import { THEREFORE } from '../figures.mjs';
|
|
20
21
|
import { formatDuration } from '../time-format.mjs';
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
// `themeEpoch` is a memo-busting prop (threaded from App): the active theme
|
|
24
|
+
// mutates `theme` in-place, so a /theme switch must re-render this memoized row
|
|
25
|
+
// and recompute its markdown colors. It is forwarded into Markdown so the
|
|
26
|
+
// token/AnsiText caches include it as a dep.
|
|
27
|
+
export const AssistantMessage = React.memo(function AssistantMessage({ text, streaming = false, columns = 80, themeEpoch = 0 }) {
|
|
28
|
+
// The body column needs an EXPLICIT numeric width. Without it, ink/Yoga
|
|
29
|
+
// measures the wrapped markdown body before the row's available width is
|
|
30
|
+
// resolved and caches its height as a single row — so a multi-line assistant
|
|
31
|
+
// message renders only its LAST wrapped line (the head lines are clipped) and
|
|
32
|
+
// streaming height jitters as the measure flips. Reserve the 2-col gutter
|
|
33
|
+
// (BLACK CIRCLE in minWidth={2}) plus one right-edge safety cell. Letting an
|
|
34
|
+
// assistant line reach the terminal's last column can auto-wrap/scroll on
|
|
35
|
+
// Windows Terminal/conhost and make the next redraw appear to lose leading
|
|
36
|
+
// CJK characters even though the backing transcript is intact.
|
|
37
|
+
const bodyWidth = assistantBodyWidth(columns);
|
|
23
38
|
return (
|
|
24
39
|
<Box flexDirection="row" marginTop={1}>
|
|
25
40
|
<Box flexShrink={0} minWidth={2}>
|
|
26
41
|
<Text color={theme.text}>{TURN_MARKER}</Text>
|
|
27
42
|
</Box>
|
|
28
|
-
<Box flexDirection="column"
|
|
29
|
-
{streaming
|
|
43
|
+
<Box flexDirection="column" flexShrink={0} width={bodyWidth}>
|
|
44
|
+
{streaming
|
|
45
|
+
? <StreamingMarkdown themeEpoch={themeEpoch} columns={bodyWidth}>{text}</StreamingMarkdown>
|
|
46
|
+
: <Markdown themeEpoch={themeEpoch} columns={bodyWidth}>{text}</Markdown>}
|
|
30
47
|
</Box>
|
|
31
48
|
</Box>
|
|
32
49
|
);
|
|
33
50
|
});
|
|
34
51
|
|
|
35
|
-
export const UserMessage = React.memo(function UserMessage({ text, attached = false, columns }) {
|
|
52
|
+
export const UserMessage = React.memo(function UserMessage({ text, attached = false, columns, themeEpoch = 0 }) {
|
|
36
53
|
// `attached` = the previous transcript row is also a user message (consecutive
|
|
37
54
|
// steering prompts). Those stack flush together; a user message that follows
|
|
38
55
|
// an assistant/tool row gets a one-row gap above it.
|
|
39
|
-
// The background band fills the row edge-to-edge (
|
|
56
|
+
// The background band fills the row edge-to-edge (user bubble reads as a
|
|
40
57
|
// full-width band, not a text-hugging tag). An explicit numeric width is more
|
|
41
58
|
// robust than "100%" here — it guarantees the band even if a parent's width
|
|
42
59
|
// context is ambiguous. paddingLeft aligns the body under the 2-col gutter.
|