mixdog 0.8.0 → 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 +344 -0
- 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 +1223 -95
- 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 +2325 -450
- 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 +118 -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 +77 -29
- 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/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- 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 +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- 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/advisory-lock.mjs +1 -1
- 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/snapshot-store.mjs +1 -1
- 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/tool-format.mjs +0 -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 +635 -107
- 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 +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- 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 +15 -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 -98
- 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 +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- 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 +617 -368
- 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 +8915 -2418
- 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 +3 -16
- 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
package/src/tui/time-format.mjs
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript visibility rules for fully-failed tool batches.
|
|
3
|
+
* Hook / approval denials are user-actionable and must stay visible in the TUI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parseToolArgs } from '../runtime/shared/tool-surface.mjs';
|
|
7
|
+
|
|
8
|
+
const HOOK_DENIAL_RE = /denied by hook/i;
|
|
9
|
+
const APPROVAL_UNAVAILABLE_RE = /approval required but no approval UI is available/i;
|
|
10
|
+
|
|
11
|
+
export function toolItemResultText(item) {
|
|
12
|
+
if (!item) return '';
|
|
13
|
+
const chunks = [];
|
|
14
|
+
if (item.result != null) chunks.push(String(item.result));
|
|
15
|
+
if (item.rawResult != null && item.rawResult !== item.result) chunks.push(String(item.rawResult));
|
|
16
|
+
return chunks.join('\n').trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isHookApprovalDenialToolResult(text) {
|
|
20
|
+
const value = String(text ?? '').trim();
|
|
21
|
+
if (!value) return false;
|
|
22
|
+
if (HOOK_DENIAL_RE.test(value)) return true;
|
|
23
|
+
if (APPROVAL_UNAVAILABLE_RE.test(value)) return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isHookApprovalDenialToolItem(item) {
|
|
28
|
+
if (!item || item.kind !== 'tool') return false;
|
|
29
|
+
return isHookApprovalDenialToolResult(toolItemResultText(item));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Strip the orchestrator's `Error: tool "name"` prefix for a compact TUI line. */
|
|
33
|
+
export function formatHookDenialDetail(text) {
|
|
34
|
+
const raw = String(text ?? '').trim();
|
|
35
|
+
if (!raw) return '';
|
|
36
|
+
const stripped = raw.replace(/^Error:\s*tool\s*"[^"]*"\s*/i, '').trim();
|
|
37
|
+
return stripped || raw;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isFullyFailedToolBatch(item) {
|
|
41
|
+
if (!item || item.kind !== 'tool') return false;
|
|
42
|
+
const args = parseToolArgs(item.args);
|
|
43
|
+
const hasTaskId = Boolean(args.task_id || args.taskId);
|
|
44
|
+
const status = String(args.status || '').toLowerCase();
|
|
45
|
+
if (hasTaskId && /^(failed|error|timeout|cancelled|canceled|killed)$/.test(status)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const count = Math.max(1, Number(item.count || 1));
|
|
49
|
+
const done = Math.max(0, Math.min(count, Number(item.completedCount || (item.result == null ? 0 : count))));
|
|
50
|
+
const explicit = Number(item.errorCount);
|
|
51
|
+
const failed = Number.isFinite(explicit)
|
|
52
|
+
? Math.max(0, Math.min(count, Math.floor(explicit)))
|
|
53
|
+
: item.isError ? count : 0;
|
|
54
|
+
return done >= count && failed >= count;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Hide noisy all-failed internal tool cards, except hook/approval denials. */
|
|
58
|
+
export function shouldSuppressFullyFailedToolItem(item) {
|
|
59
|
+
if (!isFullyFailedToolBatch(item)) return false;
|
|
60
|
+
if (isHookApprovalDenialToolItem(item)) return false;
|
|
61
|
+
return !hasUsefulFailedToolResultBody(item);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Non-empty result/rawResult text the user can expand and inspect in the transcript. */
|
|
65
|
+
export function hasUsefulFailedToolResultBody(item) {
|
|
66
|
+
return Boolean(toolItemResultText(item));
|
|
67
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
formatHookDenialDetail,
|
|
5
|
+
hasUsefulFailedToolResultBody,
|
|
6
|
+
isFullyFailedToolBatch,
|
|
7
|
+
isHookApprovalDenialToolItem,
|
|
8
|
+
isHookApprovalDenialToolResult,
|
|
9
|
+
shouldSuppressFullyFailedToolItem,
|
|
10
|
+
} from './transcript-tool-failures.mjs';
|
|
11
|
+
|
|
12
|
+
const hookDeniedItem = {
|
|
13
|
+
kind: 'tool',
|
|
14
|
+
name: 'shell',
|
|
15
|
+
args: { command: 'rm -rf /' },
|
|
16
|
+
result: 'Error: tool "shell" denied by hook: approval required but no approval UI is available (policy)',
|
|
17
|
+
isError: true,
|
|
18
|
+
count: 1,
|
|
19
|
+
completedCount: 1,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const noisyFailedItem = {
|
|
23
|
+
kind: 'tool',
|
|
24
|
+
name: 'grep',
|
|
25
|
+
args: { pattern: 'foo' },
|
|
26
|
+
result: 'no matches',
|
|
27
|
+
isError: true,
|
|
28
|
+
count: 1,
|
|
29
|
+
completedCount: 1,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const emptyFailedItem = {
|
|
33
|
+
kind: 'tool',
|
|
34
|
+
name: 'grep',
|
|
35
|
+
args: { pattern: 'foo' },
|
|
36
|
+
result: null,
|
|
37
|
+
rawResult: null,
|
|
38
|
+
isError: true,
|
|
39
|
+
count: 1,
|
|
40
|
+
completedCount: 1,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const failedTaskJsonStringArgs = {
|
|
44
|
+
kind: 'tool',
|
|
45
|
+
name: 'shell',
|
|
46
|
+
args: '{"task_id":"t1","status":"failed","error":"boom"}',
|
|
47
|
+
result: null,
|
|
48
|
+
rawResult: null,
|
|
49
|
+
isError: true,
|
|
50
|
+
count: 1,
|
|
51
|
+
completedCount: 1,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const failedTaskInputWrapperArgs = {
|
|
55
|
+
kind: 'tool',
|
|
56
|
+
name: 'shell',
|
|
57
|
+
args: { input: { task_id: 't2', status: 'timeout' } },
|
|
58
|
+
result: null,
|
|
59
|
+
rawResult: null,
|
|
60
|
+
isError: true,
|
|
61
|
+
count: 1,
|
|
62
|
+
completedCount: 1,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
test('detects hook denial result text', () => {
|
|
66
|
+
assert.equal(
|
|
67
|
+
isHookApprovalDenialToolResult('Error: tool "read" denied by hook: blocked'),
|
|
68
|
+
true,
|
|
69
|
+
);
|
|
70
|
+
assert.equal(
|
|
71
|
+
isHookApprovalDenialToolResult('Error: tool "shell" denied by hook: approval required but no approval UI is available'),
|
|
72
|
+
true,
|
|
73
|
+
);
|
|
74
|
+
assert.equal(isHookApprovalDenialToolResult('grep: no matches'), false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('fully-failed hook denials are not suppressed', () => {
|
|
78
|
+
assert.equal(isFullyFailedToolBatch(hookDeniedItem), true);
|
|
79
|
+
assert.equal(isHookApprovalDenialToolItem(hookDeniedItem), true);
|
|
80
|
+
assert.equal(shouldSuppressFullyFailedToolItem(hookDeniedItem), false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('fully-failed non-hook errors with visible body stay visible', () => {
|
|
84
|
+
assert.equal(isFullyFailedToolBatch(noisyFailedItem), true);
|
|
85
|
+
assert.equal(hasUsefulFailedToolResultBody(noisyFailedItem), true);
|
|
86
|
+
assert.equal(shouldSuppressFullyFailedToolItem(noisyFailedItem), false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('fully-failed non-hook errors without useful body stay suppressed', () => {
|
|
90
|
+
assert.equal(isFullyFailedToolBatch(emptyFailedItem), true);
|
|
91
|
+
assert.equal(hasUsefulFailedToolResultBody(emptyFailedItem), false);
|
|
92
|
+
assert.equal(shouldSuppressFullyFailedToolItem(emptyFailedItem), true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('failed background task with JSON-string args is not suppressed', () => {
|
|
96
|
+
assert.equal(isFullyFailedToolBatch(failedTaskJsonStringArgs), false);
|
|
97
|
+
assert.equal(shouldSuppressFullyFailedToolItem(failedTaskJsonStringArgs), false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('failed background task with input-wrapped args is not suppressed', () => {
|
|
101
|
+
assert.equal(isFullyFailedToolBatch(failedTaskInputWrapperArgs), false);
|
|
102
|
+
assert.equal(shouldSuppressFullyFailedToolItem(failedTaskInputWrapperArgs), false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('formatHookDenialDetail strips tool prefix', () => {
|
|
106
|
+
assert.equal(
|
|
107
|
+
formatHookDenialDetail(hookDeniedItem.result),
|
|
108
|
+
'denied by hook: approval required but no approval UI is available (policy)',
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
package/src/ui/ansi.mjs
CHANGED
|
@@ -85,8 +85,7 @@ export const bgBlack = sgr('40');
|
|
|
85
85
|
export const bgBlue = sgr('44');
|
|
86
86
|
|
|
87
87
|
// --- Truecolor (24-bit) -----------------------------------------------------
|
|
88
|
-
//
|
|
89
|
-
// src/utils/theme.ts darkTheme). 16-color SGR can't reproduce them, so we emit
|
|
88
|
+
// The TUI palette uses explicit rgb() values. 16-color SGR can't reproduce them, so we emit
|
|
90
89
|
// 24-bit SGR. Honors NO_COLOR / TTY exactly like the named helpers above.
|
|
91
90
|
|
|
92
91
|
/** Foreground truecolor wrapper: `rgb(215,119,87)('x')`. */
|
package/src/ui/markdown.mjs
CHANGED
|
@@ -20,17 +20,29 @@ import {
|
|
|
20
20
|
bold,
|
|
21
21
|
italic,
|
|
22
22
|
dim,
|
|
23
|
-
cyan,
|
|
24
|
-
gray,
|
|
25
|
-
yellow,
|
|
26
|
-
green,
|
|
27
23
|
underline,
|
|
28
24
|
strike as strikeStyle,
|
|
29
|
-
brightWhite,
|
|
30
25
|
colorEnabled,
|
|
31
26
|
visibleWidth,
|
|
27
|
+
rgb,
|
|
28
|
+
compose,
|
|
32
29
|
} from './ansi.mjs';
|
|
33
30
|
|
|
31
|
+
// Default Mixdog dark markdown semantics (mirrors src/tui/theme.mjs mixdogPalette md* keys).
|
|
32
|
+
const PALETTE = {
|
|
33
|
+
heading1: rgb(215, 119, 87),
|
|
34
|
+
heading: rgb(240, 198, 116),
|
|
35
|
+
inlineCode: rgb(138, 190, 183),
|
|
36
|
+
link: rgb(47, 127, 255),
|
|
37
|
+
codeBlock: rgb(181, 189, 104),
|
|
38
|
+
fenceLabel: rgb(138, 190, 183),
|
|
39
|
+
listBullet: rgb(138, 190, 183),
|
|
40
|
+
quoteText: rgb(128, 128, 128),
|
|
41
|
+
diffAdd: rgb(0, 170, 75),
|
|
42
|
+
diffDel: rgb(220, 70, 88),
|
|
43
|
+
diffHunk: rgb(204, 157, 44),
|
|
44
|
+
};
|
|
45
|
+
|
|
34
46
|
/**
|
|
35
47
|
* Render a markdown string to a terminal-styled string.
|
|
36
48
|
* @param {string} src
|
|
@@ -54,6 +66,8 @@ function renderUnsafe(src, opts) {
|
|
|
54
66
|
let inFence = false;
|
|
55
67
|
let fenceLang = '';
|
|
56
68
|
let fenceBuf = [];
|
|
69
|
+
let fenceMarker = '';
|
|
70
|
+
let fenceMarkerLen = 0;
|
|
57
71
|
|
|
58
72
|
for (let i = 0; i < lines.length; i++) {
|
|
59
73
|
const line = lines[i];
|
|
@@ -62,16 +76,24 @@ function renderUnsafe(src, opts) {
|
|
|
62
76
|
if (fenceMatch && !inFence) {
|
|
63
77
|
inFence = true;
|
|
64
78
|
fenceLang = fenceMatch[3].trim();
|
|
79
|
+
fenceMarker = fenceMatch[2][0];
|
|
80
|
+
fenceMarkerLen = fenceMatch[2].length;
|
|
65
81
|
fenceBuf = [];
|
|
66
82
|
continue;
|
|
67
83
|
}
|
|
68
84
|
if (inFence) {
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
const closeMatch = /^(\s*)([`~]+)\s*$/.exec(line);
|
|
86
|
+
const markerRun = closeMatch?.[2] ?? '';
|
|
87
|
+
const closes =
|
|
88
|
+
markerRun.length >= fenceMarkerLen &&
|
|
89
|
+
[...markerRun].every((ch) => ch === fenceMarker);
|
|
90
|
+
if (closes) {
|
|
71
91
|
out.push(renderCodeBlock(fenceBuf, fenceLang, width));
|
|
72
92
|
inFence = false;
|
|
73
93
|
fenceLang = '';
|
|
74
94
|
fenceBuf = [];
|
|
95
|
+
fenceMarker = '';
|
|
96
|
+
fenceMarkerLen = 0;
|
|
75
97
|
} else {
|
|
76
98
|
fenceBuf.push(line);
|
|
77
99
|
}
|
|
@@ -98,7 +120,7 @@ function clampWidth(w) {
|
|
|
98
120
|
function renderLine(line, width) {
|
|
99
121
|
// Horizontal rule.
|
|
100
122
|
if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
|
|
101
|
-
return dim('─'.repeat(Math.min(width, 60)));
|
|
123
|
+
return dim(rgb(128, 128, 128)('─'.repeat(Math.min(width, 60))));
|
|
102
124
|
}
|
|
103
125
|
|
|
104
126
|
// ATX heading.
|
|
@@ -106,30 +128,30 @@ function renderLine(line, width) {
|
|
|
106
128
|
if (h) {
|
|
107
129
|
const level = h[1].length;
|
|
108
130
|
const text = renderInline(h[2]);
|
|
109
|
-
if (level === 1) return '\n' + bold(
|
|
110
|
-
if (level === 2) return '\n' + bold(
|
|
111
|
-
if (level === 3) return bold(text);
|
|
112
|
-
return bold
|
|
131
|
+
if (level === 1) return '\n' + compose(bold, PALETTE.heading1)('▌ ' + text);
|
|
132
|
+
if (level === 2) return '\n' + compose(bold, PALETTE.heading)(text);
|
|
133
|
+
if (level === 3) return compose(bold, PALETTE.heading)(text);
|
|
134
|
+
return compose(bold, dim)(text);
|
|
113
135
|
}
|
|
114
136
|
|
|
115
137
|
// Blockquote (possibly nested).
|
|
116
138
|
const q = /^(\s*>+)\s?(.*)$/.exec(line);
|
|
117
139
|
if (q) {
|
|
118
|
-
return dim('│ ') +
|
|
140
|
+
return dim(PALETTE.quoteText('│ ')) + italic(PALETTE.quoteText(renderInline(q[2])));
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
// Bullet list.
|
|
122
144
|
const b = /^(\s*)([-*+])\s+(.*)$/.exec(line);
|
|
123
145
|
if (b) {
|
|
124
146
|
const indent = b[1].replace(/\t/g, ' ');
|
|
125
|
-
return indent +
|
|
147
|
+
return indent + PALETTE.listBullet('•') + ' ' + renderInline(b[3]);
|
|
126
148
|
}
|
|
127
149
|
|
|
128
150
|
// Numbered list.
|
|
129
151
|
const n = /^(\s*)(\d+)([.)])\s+(.*)$/.exec(line);
|
|
130
152
|
if (n) {
|
|
131
153
|
const indent = n[1].replace(/\t/g, ' ');
|
|
132
|
-
return indent +
|
|
154
|
+
return indent + PALETTE.listBullet(n[2] + '.') + ' ' + renderInline(n[4]);
|
|
133
155
|
}
|
|
134
156
|
|
|
135
157
|
// Plain paragraph line.
|
|
@@ -138,40 +160,77 @@ function renderLine(line, width) {
|
|
|
138
160
|
|
|
139
161
|
function renderCodeBlock(bufLines, lang, width) {
|
|
140
162
|
const inner = bufLines.length ? bufLines : [''];
|
|
163
|
+
const isDiff = isDiffFence(lang, inner);
|
|
141
164
|
const contentWidth = Math.max(
|
|
142
165
|
20,
|
|
143
166
|
Math.min(width, inner.reduce((m, l) => Math.max(m, l.length), 0) + 2),
|
|
144
167
|
);
|
|
145
|
-
const
|
|
146
|
-
const
|
|
168
|
+
const labelPlain = lang ? ` ${lang} ` : '';
|
|
169
|
+
const labelStyled = labelPlain ? PALETTE.fenceLabel(labelPlain) : '';
|
|
170
|
+
const ruleLen = Math.max(0, contentWidth - visibleWidth(labelPlain));
|
|
171
|
+
const top = dim('┌') + labelStyled + dim('─'.repeat(ruleLen) + '┐');
|
|
147
172
|
const bottom = dim('└' + '─'.repeat(contentWidth) + '┘');
|
|
148
173
|
const body = inner.map((l) => {
|
|
149
|
-
const text =
|
|
150
|
-
|
|
174
|
+
const text = colorFenceLine(l, isDiff);
|
|
175
|
+
const padTarget = Math.max(0, contentWidth - 1);
|
|
176
|
+
const padded = padVisible(text, padTarget);
|
|
177
|
+
return dim('│ ') + padded + dim('│');
|
|
151
178
|
});
|
|
152
179
|
return [top, ...body, bottom].join('\n');
|
|
153
180
|
}
|
|
154
181
|
|
|
182
|
+
function padVisible(text, targetWidth) {
|
|
183
|
+
const w = visibleWidth(text);
|
|
184
|
+
if (w >= targetWidth) return text;
|
|
185
|
+
return String(text ?? '') + ' '.repeat(targetWidth - w);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isDiffFence(lang, lines) {
|
|
189
|
+
const tag = String(lang ?? '').trim().toLowerCase();
|
|
190
|
+
if (/^(diff|patch|udiff)$/.test(tag)) return true;
|
|
191
|
+
let hunk = false;
|
|
192
|
+
let delta = false;
|
|
193
|
+
for (const line of lines) {
|
|
194
|
+
const s = String(line ?? '');
|
|
195
|
+
if (/^@@/.test(s)) hunk = true;
|
|
196
|
+
if (/^\+/.test(s) && !/^\+\+\+/.test(s)) delta = true;
|
|
197
|
+
if (/^-/.test(s) && !/^---/.test(s)) delta = true;
|
|
198
|
+
}
|
|
199
|
+
return hunk && delta;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function colorFenceLine(line, isDiff) {
|
|
203
|
+
const s = String(line ?? '');
|
|
204
|
+
if (!colorEnabled()) return s;
|
|
205
|
+
if (!isDiff) return PALETTE.codeBlock(s);
|
|
206
|
+
if (/^@@/.test(s)) return PALETTE.diffHunk(s);
|
|
207
|
+
if (/^(\+\+\+|---)/.test(s)) return PALETTE.diffHunk(s);
|
|
208
|
+
if (/^\+/.test(s)) return PALETTE.diffAdd(s);
|
|
209
|
+
if (/^-/.test(s)) return PALETTE.diffDel(s);
|
|
210
|
+
return PALETTE.codeBlock(s);
|
|
211
|
+
}
|
|
212
|
+
|
|
155
213
|
// --- Inline ------------------------------------------------------------------
|
|
156
214
|
|
|
157
215
|
/**
|
|
158
216
|
* Render inline markdown spans. Order matters: we extract code spans first
|
|
159
217
|
* (their contents are literal), then apply emphasis/link transforms to the rest.
|
|
160
218
|
*/
|
|
161
|
-
function renderInline(text) {
|
|
219
|
+
function renderInline(text, state) {
|
|
220
|
+
const st = state ?? { codeSpans: [] };
|
|
162
221
|
let s = String(text ?? '');
|
|
163
222
|
|
|
164
223
|
// Protect inline code spans from further formatting.
|
|
165
|
-
const codeSpans = [];
|
|
166
224
|
s = s.replace(/`([^`]+)`/g, (_m, code) => {
|
|
167
|
-
const token = `\u0000C${codeSpans.length}\u0000`;
|
|
168
|
-
codeSpans.push(code);
|
|
225
|
+
const token = `\u0000C${st.codeSpans.length}\u0000`;
|
|
226
|
+
st.codeSpans.push(code);
|
|
169
227
|
return token;
|
|
170
228
|
});
|
|
171
229
|
|
|
172
230
|
// Links: [text](url) -> text (url)
|
|
173
231
|
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_m, label, url) => {
|
|
174
|
-
|
|
232
|
+
const linkLabel = compose(underline, PALETTE.link)(renderInline(label, st));
|
|
233
|
+
return linkLabel + ' ' + dim('(' + url + ')');
|
|
175
234
|
});
|
|
176
235
|
|
|
177
236
|
// Bold: **x** or __x__
|
|
@@ -187,8 +246,8 @@ function renderInline(text) {
|
|
|
187
246
|
|
|
188
247
|
// Restore code spans, styled.
|
|
189
248
|
s = s.replace(/\u0000C(\d+)\u0000/g, (_m, idx) => {
|
|
190
|
-
const code = codeSpans[Number(idx)] ?? '';
|
|
191
|
-
return colorEnabled() ?
|
|
249
|
+
const code = st.codeSpans[Number(idx)] ?? '';
|
|
250
|
+
return colorEnabled() ? PALETTE.inlineCode(code) : '`' + code + '`';
|
|
192
251
|
});
|
|
193
252
|
|
|
194
253
|
return s;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { env } from 'node:process';
|
|
4
|
+
import { renderMarkdown } from './markdown.mjs';
|
|
5
|
+
import { refreshColorSupport, stripAnsi } from './ansi.mjs';
|
|
6
|
+
|
|
7
|
+
test.before(() => {
|
|
8
|
+
env.FORCE_COLOR = '1';
|
|
9
|
+
refreshColorSupport();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('renderMarkdown never throws on garbage input', () => {
|
|
13
|
+
assert.doesNotThrow(() => renderMarkdown('**unclosed `fence\n> quote\n'));
|
|
14
|
+
assert.equal(renderMarkdown(null), '');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('diff fenced blocks color hunks and +/- lines', () => {
|
|
18
|
+
const src = [
|
|
19
|
+
'```diff',
|
|
20
|
+
'--- a/file.mjs',
|
|
21
|
+
'+++ b/file.mjs',
|
|
22
|
+
'@@ -1,2 +1,2 @@',
|
|
23
|
+
'-old',
|
|
24
|
+
'+new',
|
|
25
|
+
'```',
|
|
26
|
+
].join('\n');
|
|
27
|
+
const out = renderMarkdown(src);
|
|
28
|
+
assert.match(out, /\x1b\[38;2;204;157;44/);
|
|
29
|
+
assert.match(out, /\x1b\[38;2;0;170;75/);
|
|
30
|
+
assert.match(out, /\x1b\[38;2;220;70;88/);
|
|
31
|
+
assert.ok(stripAnsi(out).includes('diff'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('inline code and headings use Mixdog accent colors', () => {
|
|
35
|
+
const out = renderMarkdown('# Title\nUse `code` here.');
|
|
36
|
+
assert.match(out, /\x1b\[38;2;215;119;87/);
|
|
37
|
+
assert.match(out, /\x1b\[38;2;138;190;183/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('longer fence opener does not close on shorter backtick line inside block', () => {
|
|
41
|
+
const src = ['````', '```', 'still inside', '````'].join('\n');
|
|
42
|
+
const plain = stripAnsi(renderMarkdown(src));
|
|
43
|
+
assert.ok(plain.includes('still inside'));
|
|
44
|
+
assert.ok(!plain.includes('```\nstill'));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('mixed backtick-tilde closer does not end backtick fence', () => {
|
|
48
|
+
const src = ['```', '``~', 'still inside', '```'].join('\n');
|
|
49
|
+
const plain = stripAnsi(renderMarkdown(src));
|
|
50
|
+
assert.ok(plain.includes('still inside'));
|
|
51
|
+
assert.ok(plain.includes('``~'));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('fenced code block body rows include right border', () => {
|
|
55
|
+
const out = stripAnsi(renderMarkdown('```js\nx\n```'));
|
|
56
|
+
const rows = out.split('\n').filter((line) => line.startsWith('│ '));
|
|
57
|
+
assert.ok(rows.length >= 1);
|
|
58
|
+
for (const row of rows) {
|
|
59
|
+
assert.match(row, /│\s*$/);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('inline code inside link labels is preserved', () => {
|
|
64
|
+
const out = renderMarkdown('[see `foo`](https://example.com)');
|
|
65
|
+
const plain = stripAnsi(out);
|
|
66
|
+
assert.ok(plain.includes('foo'));
|
|
67
|
+
assert.ok(plain.includes('https://example.com'));
|
|
68
|
+
assert.match(out, /\x1b\[38;2;138;190;183/);
|
|
69
|
+
});
|
|
70
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure model display-name helpers shared by the TUI snap path and statusline.
|
|
3
|
+
* No I/O, catalog, or gateway imports — safe for static TUI bundling.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function titleModelPart(part) {
|
|
7
|
+
const text = String(part || '').trim();
|
|
8
|
+
if (!text) return '';
|
|
9
|
+
const lower = text.toLowerCase();
|
|
10
|
+
if (lower === 'gpt') return 'GPT';
|
|
11
|
+
if (lower === 'api') return 'API';
|
|
12
|
+
if (lower === 'v4') return 'V4';
|
|
13
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stripModelId(model) {
|
|
17
|
+
const text = String(model || '').trim();
|
|
18
|
+
if (!text) return '';
|
|
19
|
+
return text.includes('/') ? (text.split('/').filter(Boolean).at(-1) || text) : text;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeDisplayHint(displayHint) {
|
|
23
|
+
if (displayHint == null || displayHint === '') return '';
|
|
24
|
+
if (typeof displayHint === 'object') {
|
|
25
|
+
return String(displayHint.displayName || displayHint.display || displayHint.name || '').trim();
|
|
26
|
+
}
|
|
27
|
+
return String(displayHint).trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function canonicalModelDisplay(model, provider) {
|
|
31
|
+
void provider;
|
|
32
|
+
const raw = String(model || '')
|
|
33
|
+
.trim()
|
|
34
|
+
.replace(/-\d{4}-\d{2}-\d{2}$/, '')
|
|
35
|
+
.replace(/-\d{8}$/, '');
|
|
36
|
+
if (!raw) return '';
|
|
37
|
+
|
|
38
|
+
const gpt = raw.match(/^gpt-(\d+(?:\.\d+)?)(?:-(.+))?$/i);
|
|
39
|
+
if (gpt) {
|
|
40
|
+
const suffix = gpt[2]
|
|
41
|
+
? '-' + gpt[2].split('-').map(titleModelPart).filter(Boolean).join('-')
|
|
42
|
+
: '';
|
|
43
|
+
return `GPT-${gpt[1]}${suffix}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (/^gpt-/i.test(raw)) {
|
|
47
|
+
return raw
|
|
48
|
+
.split('-')
|
|
49
|
+
.map((part, index) => (index === 0 ? part.toUpperCase() : titleModelPart(part)))
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.join('-');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const openaiO = raw.match(/^o(\d+(?:\.\d+)?)(?:-(.+))?$/i);
|
|
55
|
+
if (openaiO) {
|
|
56
|
+
const tail = openaiO[2]
|
|
57
|
+
? ' ' + openaiO[2].split('-').map(titleModelPart).filter(Boolean).join(' ')
|
|
58
|
+
: '';
|
|
59
|
+
return `O${openaiO[1]}${tail}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const codex = raw.match(/^codex-(.+)$/i);
|
|
63
|
+
if (codex) {
|
|
64
|
+
return `Codex ${codex[1].split('-').map(titleModelPart).filter(Boolean).join(' ')}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const deepseek = raw.match(/^deepseek-(.+)$/i);
|
|
68
|
+
if (deepseek) {
|
|
69
|
+
return `DeepSeek ${deepseek[1].split('-').map(titleModelPart).filter(Boolean).join(' ')}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const grok = raw.match(/^grok-(.+)$/i);
|
|
73
|
+
if (grok) {
|
|
74
|
+
return `Grok ${grok[1].split('-').map(titleModelPart).filter(Boolean).join(' ')}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const claudeLegacy = raw.match(/^claude-(\d+)(?:-(\d+))?-(opus|sonnet|haiku)(?:-|$)/i);
|
|
78
|
+
if (claudeLegacy) {
|
|
79
|
+
const version = `${claudeLegacy[1]}${claudeLegacy[2] ? `.${claudeLegacy[2]}` : ''}`;
|
|
80
|
+
return `Claude ${titleModelPart(claudeLegacy[3])} ${version}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const claude = raw.match(/^claude-(opus|sonnet|haiku)-(.+)$/i);
|
|
84
|
+
if (claude) {
|
|
85
|
+
return `Claude ${titleModelPart(claude[1])} ${claude[2].replace(/-/g, '.')}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const gemini = raw.match(/^gemini-(\d+(?:\.\d+)?)-(.+)$/i);
|
|
89
|
+
if (gemini) {
|
|
90
|
+
return `Gemini ${gemini[1]} ${gemini[2].split('-').map(titleModelPart).filter(Boolean).join(' ')}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const geminiLoose = raw.match(/^gemini-(.+)$/i);
|
|
94
|
+
if (geminiLoose) {
|
|
95
|
+
return `Gemini ${geminiLoose[1].split('-').map(titleModelPart).filter(Boolean).join(' ')}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return raw;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function displayModelName(model, provider = '', displayHint = '') {
|
|
102
|
+
const id = stripModelId(model);
|
|
103
|
+
const hint = normalizeDisplayHint(displayHint);
|
|
104
|
+
|
|
105
|
+
if (id) {
|
|
106
|
+
const canonical = canonicalModelDisplay(id, provider);
|
|
107
|
+
if (canonical && canonical !== id) return canonical;
|
|
108
|
+
}
|
|
109
|
+
if (hint) return hint;
|
|
110
|
+
if (id) return canonicalModelDisplay(id, provider) || id;
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function shortenModelName(name, cols) {
|
|
115
|
+
let out = String(name || 'model').replace(/\s*\(1M context\)/i, ' (1M)');
|
|
116
|
+
out = out.replace(/^Claude\s+/i, '');
|
|
117
|
+
out = out.replace(/^OpenAI\s+/i, '');
|
|
118
|
+
if (cols < 80 && out.length > 18) return out.slice(0, 17) + '…';
|
|
119
|
+
if (cols < 120 && out.length > 28) return out.slice(0, 27) + '…';
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Create a mutable session-usage accumulator. */
|
|
2
|
+
export function createSessionStats() {
|
|
3
|
+
return {
|
|
4
|
+
inputTokens: 0,
|
|
5
|
+
outputTokens: 0,
|
|
6
|
+
cachedTokens: 0,
|
|
7
|
+
cacheWriteTokens: 0,
|
|
8
|
+
promptTokens: 0,
|
|
9
|
+
latestInputTokens: 0,
|
|
10
|
+
latestOutputTokens: 0,
|
|
11
|
+
latestCachedTokens: 0,
|
|
12
|
+
latestCacheWriteTokens: 0,
|
|
13
|
+
latestPromptTokens: 0,
|
|
14
|
+
costUsd: 0,
|
|
15
|
+
turns: 0,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fold one `onUsageDelta` event into the accumulator.
|
|
21
|
+
* @param {object} stats — from createSessionStats()
|
|
22
|
+
* @param {object} delta — { deltaInput, deltaOutput, deltaCachedRead, deltaCacheWrite, costUsd }
|
|
23
|
+
*/
|
|
24
|
+
export function applyUsageDelta(stats, delta = {}) {
|
|
25
|
+
if (!stats || !delta) return stats;
|
|
26
|
+
stats.inputTokens += num(delta.deltaInput);
|
|
27
|
+
stats.outputTokens += num(delta.deltaOutput);
|
|
28
|
+
stats.cachedTokens += num(delta.deltaCachedRead);
|
|
29
|
+
stats.cacheWriteTokens += num(delta.deltaCacheWrite);
|
|
30
|
+
stats.promptTokens += num(delta.deltaPrompt);
|
|
31
|
+
stats.latestInputTokens = num(delta.deltaInput);
|
|
32
|
+
stats.latestOutputTokens = num(delta.deltaOutput);
|
|
33
|
+
stats.latestCachedTokens = num(delta.deltaCachedRead);
|
|
34
|
+
stats.latestCacheWriteTokens = num(delta.deltaCacheWrite);
|
|
35
|
+
stats.latestPromptTokens = num(delta.deltaPrompt);
|
|
36
|
+
// costUsd from the engine is cumulative-per-call; we sum the per-turn deltas.
|
|
37
|
+
stats.costUsd += num(delta.costUsd);
|
|
38
|
+
return stats;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function num(v) {
|
|
42
|
+
const n = Number(v);
|
|
43
|
+
return Number.isFinite(n) ? n : 0;
|
|
44
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { contextPctDisplayLabel } from './statusline.mjs';
|
|
4
|
+
|
|
5
|
+
test('contextPctDisplayLabel clamps displayed percent at 100', () => {
|
|
6
|
+
assert.equal(contextPctDisplayLabel(105), '100');
|
|
7
|
+
assert.equal(contextPctDisplayLabel(100.9), '100');
|
|
8
|
+
assert.equal(contextPctDisplayLabel(99.2), '99');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('contextPctDisplayLabel keeps sub-1% decimal display', () => {
|
|
12
|
+
assert.equal(contextPctDisplayLabel(0.45), '0.5');
|
|
13
|
+
assert.equal(contextPctDisplayLabel(0.04), '0');
|
|
14
|
+
});
|
|
15
|
+
|