mixdog 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -23
- package/package.json +34 -28
- 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 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -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 +108 -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 +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -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 +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- 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 +289 -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 +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- 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 +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +519 -118
- 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/llm-worker-host.mjs +0 -4
- 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 -2
- 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 +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -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 +104 -473
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +44 -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 +139 -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 +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- 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 +428 -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 +597 -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 +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- 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 +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -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 +756 -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 +1 -620
- 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/ink.js +54 -8
- 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/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- 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/channels/lib/hook-pipe-server.mjs +0 -671
- 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,18 +1,24 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* components/StatusLine.jsx — the vendored mixdog L1/L2 statusline footer.
|
|
3
|
-
*
|
|
1
|
+
/**
|
|
2
|
+
* components/StatusLine.jsx — the vendored mixdog L1/L2 statusline footer.
|
|
3
|
+
*
|
|
4
4
|
* renderStatusline() (src/ui/statusline.mjs) is async (it awaits the vendored
|
|
5
5
|
* statusline-lib that may query the gateway). We recompute it whenever the
|
|
6
6
|
* stats/model change and tone-map the vendored ANSI string into the React TUI
|
|
7
7
|
* palette before printing it through ink's <Text>.
|
|
8
8
|
*/
|
|
9
|
-
import React, { useEffect, useState } from 'react';
|
|
9
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
10
10
|
import { Box, Text } from 'ink';
|
|
11
|
+
import { displayModelName, shortenModelName } from '../../ui/model-display.mjs';
|
|
11
12
|
import { theme } from '../theme.mjs';
|
|
13
|
+
import {
|
|
14
|
+
normalizeStatuslineAnsi,
|
|
15
|
+
statuslineFooterCacheKey,
|
|
16
|
+
statuslineFooterIdentityChanged,
|
|
17
|
+
} from '../statusline-ansi-bridge.mjs';
|
|
12
18
|
|
|
13
|
-
// Loaded at RUNTIME (not bundled) so its vendored statusline-lib relative
|
|
14
|
-
// imports resolve from the real src/ui location, not the dist/ bundle dir.
|
|
15
|
-
// esbuild leaves dynamic-import string specifiers alone.
|
|
19
|
+
// Loaded at RUNTIME (not bundled) so its vendored statusline-lib relative
|
|
20
|
+
// imports resolve from the real src/ui location, not the dist/ bundle dir.
|
|
21
|
+
// esbuild leaves dynamic-import string specifiers alone.
|
|
16
22
|
const STATUSLINE_MODULE = '../../ui/statusline.mjs';
|
|
17
23
|
let statuslineModulePromise = null;
|
|
18
24
|
|
|
@@ -21,8 +27,76 @@ function loadStatuslineModule() {
|
|
|
21
27
|
return statuslineModulePromise;
|
|
22
28
|
}
|
|
23
29
|
|
|
30
|
+
function resetStatuslineModuleLoad() {
|
|
31
|
+
statuslineModulePromise = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
const RESET = '\x1b[0m';
|
|
25
35
|
const STATUSLINE_RENDER_DEBOUNCE_MS = 150;
|
|
36
|
+
const STATUSLINE_BOOT_FULL_DELAY_MS = 1000;
|
|
37
|
+
const STATUSLINE_BOOT_FULL_DELAY_ACTIVE_MS = 2200;
|
|
38
|
+
const STATUSLINE_BOOT_FULL_RETRY_MS = 2000;
|
|
39
|
+
const STATUSLINE_BOOT_FULL_RETRY_MAX_MS = 30000;
|
|
40
|
+
const STATUSLINE_REFRESH_MS = 2000;
|
|
41
|
+
const STATUSLINE_ACTIVE_REFRESH_MS = 250;
|
|
42
|
+
|
|
43
|
+
function isTerminalStatus(statusText) {
|
|
44
|
+
return /idle|done|complete|success|closed|error|fail|cancel|killed|timeout/.test(String(statusText || '').toLowerCase());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasRunningStatuslineWorkers(agentWorkers = [], agentJobs = []) {
|
|
48
|
+
for (const worker of Array.isArray(agentWorkers) ? agentWorkers : []) {
|
|
49
|
+
const tag = String(worker?.tag || worker?.role || worker?.name || '').trim();
|
|
50
|
+
if (tag && !isTerminalStatus(worker?.stage || worker?.status)) return true;
|
|
51
|
+
}
|
|
52
|
+
for (const job of Array.isArray(agentJobs) ? agentJobs : []) {
|
|
53
|
+
if (/running/i.test(String(job?.status || job?.stage || ''))) return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stripAnsi(text) {
|
|
59
|
+
return String(text || '').replace(/\x1b\[[0-9;]*m/g, '');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasActiveStatuslineTools(activeTools = null) {
|
|
63
|
+
if (!activeTools || typeof activeTools !== 'object') return false;
|
|
64
|
+
const e = Number(activeTools.explore?.count) > 0;
|
|
65
|
+
const s = Number(activeTools.search?.count) > 0;
|
|
66
|
+
return e || s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hasActiveStatuslineWork(line, agentWorkers = [], agentJobs = [], activeTools = null) {
|
|
70
|
+
return hasRunningStatuslineWorkers(agentWorkers, agentJobs)
|
|
71
|
+
|| hasActiveStatuslineTools(activeTools)
|
|
72
|
+
|| /\bRunning \d+ (?:Agents?|Shells?)\b/.test(stripAnsi(line))
|
|
73
|
+
|| /\b(?:Exploring|Searching)\b/.test(stripAnsi(line));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function bootFullRenderEligible(mountAtMs, line, agentWorkers = [], agentJobs = [], activeTools = null) {
|
|
77
|
+
const elapsed = Date.now() - mountAtMs;
|
|
78
|
+
const active = hasActiveStatuslineWork(line, agentWorkers, agentJobs, activeTools);
|
|
79
|
+
const delay = active ? STATUSLINE_BOOT_FULL_DELAY_ACTIVE_MS : STATUSLINE_BOOT_FULL_DELAY_MS;
|
|
80
|
+
return elapsed >= delay;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function canAttemptBootFullRender(nextAttemptAtMs = 0) {
|
|
84
|
+
return Date.now() >= nextAttemptAtMs;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function shouldSnapLocalStatusline(args, lastArgs) {
|
|
88
|
+
return statuslineFooterIdentityChanged(args, lastArgs);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function scheduleBootFullRetry(backoffMsRef, nextAttemptAtRef) {
|
|
92
|
+
resetStatuslineModuleLoad();
|
|
93
|
+
const nextBackoff = Math.min(
|
|
94
|
+
STATUSLINE_BOOT_FULL_RETRY_MAX_MS,
|
|
95
|
+
Math.max(STATUSLINE_BOOT_FULL_RETRY_MS, backoffMsRef.current * 2),
|
|
96
|
+
);
|
|
97
|
+
backoffMsRef.current = nextBackoff;
|
|
98
|
+
nextAttemptAtRef.current = Date.now() + nextBackoff;
|
|
99
|
+
}
|
|
26
100
|
|
|
27
101
|
function ansiRgb(value, fallback) {
|
|
28
102
|
const match = /^rgb\((\d+),(\d+),(\d+)\)$/.exec(String(value || '').replace(/\s+/g, ''));
|
|
@@ -30,42 +104,526 @@ function ansiRgb(value, fallback) {
|
|
|
30
104
|
return `\x1b[38;2;${match[1]};${match[2]};${match[3]}m`;
|
|
31
105
|
}
|
|
32
106
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
107
|
+
// SGR escapes derived from the active theme. Resolved per call (not captured at
|
|
108
|
+
// module load) so a live `/theme` switch re-tones the statusline on the next
|
|
109
|
+
// render. `theme` is mutated in-place on switch.
|
|
110
|
+
function statusColors() {
|
|
111
|
+
return {
|
|
112
|
+
STATUS: ansiRgb(theme.statusText, '\x1b[38;2;198;198;198m'),
|
|
113
|
+
SUBTLE: ansiRgb(theme.statusSubtle, '\x1b[38;2;136;136;136m'),
|
|
114
|
+
SUCCESS: ansiRgb(theme.success, '\x1b[38;2;0;170;75m'),
|
|
115
|
+
WARNING: ansiRgb(theme.warning, '\x1b[38;2;255;193;7m'),
|
|
116
|
+
ERROR: ansiRgb(theme.error, '\x1b[38;2;220;70;88m'),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function terminalColumns() {
|
|
121
|
+
const cols = Number(process.stdout?.columns);
|
|
122
|
+
return Number.isFinite(cols) && cols > 0 ? Math.floor(cols) : 120;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function localNum(value) {
|
|
126
|
+
const n = Number(value);
|
|
127
|
+
return Number.isFinite(n) ? n : 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function localContextPct({
|
|
131
|
+
provider = '',
|
|
132
|
+
stats = null,
|
|
133
|
+
contextWindow = 0,
|
|
134
|
+
displayContextWindow = 0,
|
|
135
|
+
rawContextWindow = 0,
|
|
136
|
+
compactBoundaryTokens = 0,
|
|
137
|
+
autoCompactTokenLimit = 0,
|
|
138
|
+
} = {}) {
|
|
139
|
+
const baseWindow = localNum(compactBoundaryTokens) > 0
|
|
140
|
+
? localNum(compactBoundaryTokens)
|
|
141
|
+
: (localNum(displayContextWindow) > 0
|
|
142
|
+
? localNum(displayContextWindow)
|
|
143
|
+
: (localNum(contextWindow) > 0
|
|
144
|
+
? localNum(contextWindow)
|
|
145
|
+
: (localNum(rawContextWindow) > 0 ? localNum(rawContextWindow) : 200_000)));
|
|
146
|
+
const compactTrigger = localNum(autoCompactTokenLimit);
|
|
147
|
+
const window = compactTrigger > 0 && compactTrigger < baseWindow ? compactTrigger : baseWindow;
|
|
148
|
+
const s = stats && typeof stats === 'object' ? stats : {};
|
|
149
|
+
const source = String(s.currentContextSource || '').toLowerCase();
|
|
150
|
+
const estimated = localNum(s.currentEstimatedContextTokens);
|
|
151
|
+
if (estimated > 0) {
|
|
152
|
+
return Math.max(0, (estimated / window) * 100);
|
|
153
|
+
}
|
|
154
|
+
if (source === 'estimated') return 0;
|
|
155
|
+
let tokens = localNum(s.currentContextTokens ?? s.contextTokens);
|
|
156
|
+
if (!tokens) return 0;
|
|
157
|
+
return Math.max(0, (tokens / window) * 100);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function localContextPctDisplayLabel(ctxPct) {
|
|
161
|
+
const pct = Number(ctxPct);
|
|
162
|
+
if (!Number.isFinite(pct) || pct <= 0) return '0';
|
|
163
|
+
if (pct > 0 && pct < 1) return String(Math.round(pct * 10) / 10);
|
|
164
|
+
return String(Math.floor(Math.min(100, pct)));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function localContextSegmentFromPct(ctxPct = 0) {
|
|
168
|
+
const { SUBTLE, SUCCESS, WARNING, ERROR } = statusColors();
|
|
169
|
+
const cols = terminalColumns();
|
|
170
|
+
const cells = cols >= 80 ? 14 : 0;
|
|
171
|
+
const raw = Number(ctxPct);
|
|
172
|
+
const pct = Number.isFinite(raw) ? Math.max(0, raw) : 0;
|
|
173
|
+
const barPct = Math.max(0, Math.min(100, pct));
|
|
174
|
+
const fill = pct >= 90 ? ERROR : pct >= 70 ? WARNING : SUCCESS;
|
|
175
|
+
const label = localContextPctDisplayLabel(pct);
|
|
176
|
+
if (!cells) return `${fill}${label}%${RESET}`;
|
|
177
|
+
let filled = Math.floor(barPct * cells / 100);
|
|
178
|
+
if (barPct >= 1 && filled === 0) filled = 1;
|
|
179
|
+
filled = Math.max(0, Math.min(cells, filled));
|
|
180
|
+
const bar = '▓'.repeat(filled) + '░'.repeat(cells - filled);
|
|
181
|
+
const filledBar = bar.replace(/░/g, '');
|
|
182
|
+
const emptyBar = bar.replace(/▓/g, '');
|
|
183
|
+
return `${fill}${filledBar}${RESET}${SUBTLE}${emptyBar}${RESET} ${label}%`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const LOCAL_WORKER_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
187
|
+
// L2 segment spinner reuses the original worker dot glyphs (no separate glyph
|
|
188
|
+
// list) but spins them at 120ms instead of the worker spinner's 160ms. Mirrors
|
|
189
|
+
// statusline.mjs l2SpinnerFrame()/L2_SPINNER_FRAME_MS so instant-local and full
|
|
190
|
+
// render stay in sync (no flicker).
|
|
191
|
+
const LOCAL_L2_SPINNER_FRAME_MS = 120;
|
|
192
|
+
|
|
193
|
+
function localWorkerSpinnerFrame(now = Date.now()) {
|
|
194
|
+
const index = Math.floor(now / 160) % LOCAL_WORKER_SPINNER_FRAMES.length;
|
|
195
|
+
return LOCAL_WORKER_SPINNER_FRAMES[index] || LOCAL_WORKER_SPINNER_FRAMES[0];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function localL2SpinnerFrame(now = Date.now()) {
|
|
199
|
+
const index = Math.floor(now / LOCAL_L2_SPINNER_FRAME_MS) % LOCAL_WORKER_SPINNER_FRAMES.length;
|
|
200
|
+
return LOCAL_WORKER_SPINNER_FRAMES[index] || LOCAL_WORKER_SPINNER_FRAMES[0];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Byte-identical replica of src/tui/time-format.mjs formatDuration() with
|
|
204
|
+
// DEFAULT options (no mostSignificantOnly / hideTrailingZeros), wrapped to drop
|
|
205
|
+
// sub-1s. Output shape: '' (<1s), `Xs`, `Xm Ys`, `Xh Ym Zs`, `Xd Yh Zm`. Mirrors
|
|
206
|
+
// statusline.mjs formatElapsed so instant-local L2 elapsed matches full render.
|
|
207
|
+
function localFormatElapsed(ms) {
|
|
208
|
+
if (!Number.isFinite(Number(ms))) return '';
|
|
209
|
+
const value = Math.max(0, Number(ms) || 0);
|
|
210
|
+
if (value < 60_000) {
|
|
211
|
+
if (value < 1_000) return '';
|
|
212
|
+
return `${Math.floor(value / 1000)}s`;
|
|
213
|
+
}
|
|
214
|
+
const days = Math.floor(value / 86_400_000);
|
|
215
|
+
const hours = Math.floor((value % 86_400_000) / 3_600_000);
|
|
216
|
+
const minutes = Math.floor((value % 3_600_000) / 60_000);
|
|
217
|
+
const seconds = Math.floor((value % 60_000) / 1000);
|
|
218
|
+
if (days > 0) return `${days}d ${hours}h ${minutes}m`;
|
|
219
|
+
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
|
|
220
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
221
|
+
return `${seconds}s`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function localRunningWorkerCount(agentWorkers = [], agentJobs = []) {
|
|
225
|
+
const seen = new Set();
|
|
226
|
+
for (const worker of Array.isArray(agentWorkers) ? agentWorkers : []) {
|
|
227
|
+
const tag = String(worker?.tag || worker?.role || worker?.name || '').trim();
|
|
228
|
+
if (!tag || isTerminalStatus(worker?.stage || worker?.status)) continue;
|
|
229
|
+
seen.add(tag);
|
|
230
|
+
}
|
|
231
|
+
for (const job of Array.isArray(agentJobs) ? agentJobs : []) {
|
|
232
|
+
if (!/running/i.test(String(job?.status || job?.stage || ''))) continue;
|
|
233
|
+
const tag = String(job?.tag || job?.role || job?.type || job?.task_id || job?.taskId || '').trim();
|
|
234
|
+
if (!tag) continue;
|
|
235
|
+
seen.add(tag);
|
|
236
|
+
}
|
|
237
|
+
return seen.size;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function localRunningWorkerTags(agentWorkers = [], agentJobs = [], limit = 3) {
|
|
241
|
+
const tags = [];
|
|
242
|
+
const seen = new Set();
|
|
243
|
+
for (const worker of Array.isArray(agentWorkers) ? agentWorkers : []) {
|
|
244
|
+
const tag = String(worker?.tag || worker?.role || worker?.name || '').trim();
|
|
245
|
+
if (!tag || isTerminalStatus(worker?.stage || worker?.status) || seen.has(tag)) continue;
|
|
246
|
+
seen.add(tag);
|
|
247
|
+
tags.push(tag);
|
|
248
|
+
}
|
|
249
|
+
for (const job of Array.isArray(agentJobs) ? agentJobs : []) {
|
|
250
|
+
if (!/running/i.test(String(job?.status || job?.stage || ''))) continue;
|
|
251
|
+
const tag = String(job?.tag || job?.role || job?.type || job?.task_id || job?.taskId || '').trim();
|
|
252
|
+
if (!tag || seen.has(tag)) continue;
|
|
253
|
+
seen.add(tag);
|
|
254
|
+
tags.push(tag);
|
|
255
|
+
}
|
|
256
|
+
if (tags.length <= limit) return tags.join(', ');
|
|
257
|
+
return `${tags.slice(0, limit).join(', ')}, +${tags.length - limit}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function localTimeMs(value) {
|
|
261
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) return value;
|
|
262
|
+
const n = Date.parse(String(value || ''));
|
|
263
|
+
return Number.isFinite(n) ? n : 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Oldest running worker/job start time, for the Agents segment elapsed. Mirrors
|
|
267
|
+
// the async path which derives elapsed from the oldest running worker.
|
|
268
|
+
function localOldestWorkerStartMs(agentWorkers = [], agentJobs = []) {
|
|
269
|
+
let oldest = Infinity;
|
|
270
|
+
for (const worker of Array.isArray(agentWorkers) ? agentWorkers : []) {
|
|
271
|
+
if (isTerminalStatus(worker?.stage || worker?.status)) continue;
|
|
272
|
+
const t = localTimeMs(worker?.startedAt || worker?.startTime || worker?.createdAt);
|
|
273
|
+
if (t > 0 && t < oldest) oldest = t;
|
|
274
|
+
}
|
|
275
|
+
for (const job of Array.isArray(agentJobs) ? agentJobs : []) {
|
|
276
|
+
if (!/running/i.test(String(job?.status || job?.stage || ''))) continue;
|
|
277
|
+
const t = localTimeMs(job?.startedAt);
|
|
278
|
+
if (t > 0 && t < oldest) oldest = t;
|
|
279
|
+
}
|
|
280
|
+
return Number.isFinite(oldest) ? oldest : 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// L2 assembly only — themed SGR (statusColors SUCCESS/STATUS/SUBTLE), already in
|
|
284
|
+
// the active palette, so this must NOT be passed through normalizeStatusLine.
|
|
285
|
+
// Returns the joined L2 string or '' when no active segment. Order:
|
|
286
|
+
// Agents → Exploring → Searching → Shells. (Shell segment is disk-based and not
|
|
287
|
+
// available to the instant-local path; intentionally omitted here.)
|
|
288
|
+
function localStatusLineL2({
|
|
289
|
+
agentWorkers = [],
|
|
290
|
+
agentJobs = [],
|
|
291
|
+
activeTools = null,
|
|
292
|
+
} = {}, now = Date.now()) {
|
|
293
|
+
const { STATUS, SUBTLE, SUCCESS } = statusColors();
|
|
294
|
+
const spin = `${SUCCESS}${localL2SpinnerFrame(now)}${RESET}`;
|
|
295
|
+
const segSep = ` ${SUBTLE}│${RESET} `;
|
|
296
|
+
const elapsedSuffix = (label) => (label ? ` ${SUBTLE}·${RESET} ${label}` : '');
|
|
297
|
+
const l2Parts = [];
|
|
298
|
+
const runningCount = localRunningWorkerCount(agentWorkers, agentJobs);
|
|
299
|
+
if (runningCount > 0) {
|
|
300
|
+
const label = `Running ${runningCount} Agent${runningCount === 1 ? '' : 's'}`;
|
|
301
|
+
const tagSummary = localRunningWorkerTags(agentWorkers, agentJobs);
|
|
302
|
+
const tags = tagSummary ? ` ${SUBTLE}(${RESET}${STATUS}${tagSummary}${RESET}${SUBTLE})${RESET}` : '';
|
|
303
|
+
const oldestStart = localOldestWorkerStartMs(agentWorkers, agentJobs);
|
|
304
|
+
const elapsed = oldestStart > 0 ? localFormatElapsed(now - oldestStart) : '';
|
|
305
|
+
l2Parts.push(`${spin} ${STATUS}${label}${RESET}${tags}${elapsedSuffix(elapsed)}`);
|
|
306
|
+
}
|
|
307
|
+
const tools = activeTools && typeof activeTools === 'object' ? activeTools : {};
|
|
308
|
+
const exploreInfo = tools.explore || null;
|
|
309
|
+
const searchInfo = tools.search || null;
|
|
310
|
+
if (exploreInfo && localNum(exploreInfo.count) > 0) {
|
|
311
|
+
const elapsed = localNum(exploreInfo.startedAt) > 0 ? localFormatElapsed(now - localNum(exploreInfo.startedAt)) : '';
|
|
312
|
+
l2Parts.push(`${spin} ${STATUS}Exploring${RESET}${elapsedSuffix(elapsed)}`);
|
|
313
|
+
}
|
|
314
|
+
if (searchInfo && localNum(searchInfo.count) > 0) {
|
|
315
|
+
const elapsed = localNum(searchInfo.startedAt) > 0 ? localFormatElapsed(now - localNum(searchInfo.startedAt)) : '';
|
|
316
|
+
l2Parts.push(`${spin} ${STATUS}Searching${RESET}${elapsedSuffix(elapsed)}`);
|
|
317
|
+
}
|
|
318
|
+
return l2Parts.length ? l2Parts.join(segSep) : '';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Invert localFormatElapsed (`Xs`, `Xm Ys`, `Xh Ym Zs`, `Xd Yh Zm`) back to ms so
|
|
322
|
+
// a grafted shell segment's elapsed can ADVANCE in real time. Returns 0 for empty
|
|
323
|
+
// or unparseable input. Each unit has a distinct suffix letter, so order doesn't
|
|
324
|
+
// matter; the `m(?!s)` guard avoids mis-reading a stray `ms` (our format never
|
|
325
|
+
// emits `ms`, but be safe).
|
|
326
|
+
function parseElapsedLabelMs(label = '') {
|
|
327
|
+
const s = String(label).trim();
|
|
328
|
+
if (!s) return 0;
|
|
329
|
+
let ms = 0;
|
|
330
|
+
const d = /(\d+)\s*d/.exec(s); if (d) ms += Number(d[1]) * 86_400_000;
|
|
331
|
+
const h = /(\d+)\s*h/.exec(s); if (h) ms += Number(h[1]) * 3_600_000;
|
|
332
|
+
const m = /(\d+)\s*m(?!s)/.exec(s); if (m) ms += Number(m[1]) * 60_000;
|
|
333
|
+
const sec = /(\d+)\s*s/.exec(s); if (sec) ms += Number(sec[1]) * 1000;
|
|
334
|
+
return ms;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Rebuild the Shell L2 segment(s) from a cached (normalized) L2 line so a graft
|
|
338
|
+
// onto the cached L1 does not drop `Running N Shell(s) · …` for one ~150ms frame.
|
|
339
|
+
// The instant-local path has NO disk access to shell jobs, so we cannot recompute
|
|
340
|
+
// the segment from args; instead we parse the count + elapsed from the cached L2's
|
|
341
|
+
// VISIBLE (ANSI-stripped) text and re-emit the segment in fresh themed SGR (with a
|
|
342
|
+
// current spinner frame so it stays in sync with the fresh Agents/Explore/Search
|
|
343
|
+
// segments). The cached elapsed is ADVANCED by (now - capturedAt) so seconds keep
|
|
344
|
+
// ticking between full renders with zero disk access; if capturedAt is 0 (cache
|
|
345
|
+
// from before this change / wiped) we fall back to the cached text as-is. Returns
|
|
346
|
+
// an array of themed segment strings (usually 0 or 1) in canonical tail order.
|
|
347
|
+
function extractCachedShellSegments(cachedL2 = '', now = Date.now(), capturedAt = 0) {
|
|
348
|
+
const visible = stripAnsi(cachedL2);
|
|
349
|
+
if (!visible) return [];
|
|
350
|
+
const { STATUS, SUBTLE, SUCCESS } = statusColors();
|
|
351
|
+
const spin = `${SUCCESS}${localL2SpinnerFrame(now)}${RESET}`;
|
|
352
|
+
const elapsedSuffix = (label) => (label ? ` ${SUBTLE}·${RESET} ${label}` : '');
|
|
353
|
+
const out = [];
|
|
354
|
+
// Split the visible L2 on the segment separator ` │ ` and keep Shell segments.
|
|
355
|
+
for (const seg of visible.split(' │ ')) {
|
|
356
|
+
// Start-anchored so only a REAL standalone shell segment matches. A visible
|
|
357
|
+
// segment is `<spinner-glyph> Running N … [· elapsed]`, so allow the single
|
|
358
|
+
// leading spinner token via `^(?:\S+\s+)?` then pin `Running N Shell(s)` to
|
|
359
|
+
// that position. An Agents segment is `<glyph> Running N Agents (tags) …` →
|
|
360
|
+
// `Shells?` cannot match `Agents`; a tag-injected `Running 3 Shells · 9s`
|
|
361
|
+
// sits AFTER `Running N Agents (` (not at the pinned start) → no false match.
|
|
362
|
+
const m = /^(?:\S+\s+)?Running (\d+) Shells?\b(?:\s·\s(.+?))?\s*$/.exec(seg.trim());
|
|
363
|
+
if (!m) continue;
|
|
364
|
+
const n = Number(m[1]) || 0;
|
|
365
|
+
if (n <= 0) continue;
|
|
366
|
+
// Advance the cached elapsed by (now - capturedAt) so seconds keep ticking
|
|
367
|
+
// between full renders — pure arithmetic, no disk I/O. Reformatted through
|
|
368
|
+
// localFormatElapsed so it matches the full-render shape (no visual jump).
|
|
369
|
+
// If there was no cached elapsed, keep it empty (don't fabricate one); if
|
|
370
|
+
// capturedAt is 0, fall back to the cached text verbatim (no advance).
|
|
371
|
+
const cachedElapsedText = (m[2] || '').trim();
|
|
372
|
+
const baseMs = parseElapsedLabelMs(cachedElapsedText);
|
|
373
|
+
const advancedMs = baseMs > 0 && capturedAt > 0 ? baseMs + Math.max(0, now - capturedAt) : baseMs;
|
|
374
|
+
const elapsed = advancedMs > 0 ? localFormatElapsed(advancedMs) : cachedElapsedText;
|
|
375
|
+
const label = `Running ${n} Shell${n === 1 ? '' : 's'}`;
|
|
376
|
+
out.push(`${spin} ${STATUS}${label}${RESET}${elapsedSuffix(elapsed)}`);
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function localBootStatusLine(args = {}) {
|
|
382
|
+
const {
|
|
383
|
+
provider = '',
|
|
384
|
+
model = '',
|
|
385
|
+
effort = '',
|
|
386
|
+
fast = false,
|
|
387
|
+
stats = null,
|
|
388
|
+
contextWindow = 0,
|
|
389
|
+
displayContextWindow = 0,
|
|
390
|
+
rawContextWindow = 0,
|
|
391
|
+
compactBoundaryTokens = 0,
|
|
392
|
+
autoCompactTokenLimit = 0,
|
|
393
|
+
} = args;
|
|
394
|
+
const raw = String(model || '').trim();
|
|
395
|
+
const { STATUS, SUBTLE } = statusColors();
|
|
396
|
+
const display = shortenModelName(
|
|
397
|
+
displayModelName(raw, provider, ''),
|
|
398
|
+
terminalColumns(),
|
|
399
|
+
);
|
|
400
|
+
const flags = [effort ? String(effort).toUpperCase() : '', fast === true ? 'FAST' : ''].filter(Boolean);
|
|
401
|
+
const modelBits = [display, ...flags].join(` ${SUBTLE}·${RESET} `);
|
|
402
|
+
const ctxPct = localContextPct({
|
|
403
|
+
provider,
|
|
404
|
+
stats,
|
|
405
|
+
contextWindow,
|
|
406
|
+
displayContextWindow,
|
|
407
|
+
rawContextWindow,
|
|
408
|
+
compactBoundaryTokens,
|
|
409
|
+
autoCompactTokenLimit,
|
|
410
|
+
});
|
|
411
|
+
const l1 = `${STATUS}${modelBits}${RESET} ${SUBTLE}│${RESET} ${localContextSegmentFromPct(ctxPct)}`;
|
|
412
|
+
const l2 = localStatusLineL2(args);
|
|
413
|
+
return l2 ? `${l1}\n${l2}` : l1;
|
|
414
|
+
}
|
|
38
415
|
|
|
39
416
|
export function normalizeStatusLine(text) {
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
417
|
+
return normalizeStatuslineAnsi(text, statusColors(), { reset: RESET });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function workflowModeLabel(workflow = {}) {
|
|
421
|
+
const name = String(workflow?.name || workflow?.id || 'Default').trim() || 'Default';
|
|
422
|
+
return `${name} Mode`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fast, cwd, stats, contextWindow, displayContextWindow = 0, compactBoundaryTokens = 0, autoCompactTokenLimit = 0, rawContextWindow, resizeEpoch, agentRevision = '', agentWorkers = [], agentJobs = [], activeTools = null, initialLine = '', workflow = null, themeEpoch = 0 }) {
|
|
426
|
+
const [line, setLine] = useState(() => normalizeStatusLine(initialLine || localBootStatusLine({
|
|
427
|
+
provider,
|
|
428
|
+
model,
|
|
429
|
+
effort,
|
|
430
|
+
fast,
|
|
431
|
+
stats,
|
|
432
|
+
contextWindow,
|
|
433
|
+
displayContextWindow,
|
|
434
|
+
rawContextWindow,
|
|
435
|
+
compactBoundaryTokens,
|
|
436
|
+
autoCompactTokenLimit,
|
|
437
|
+
agentWorkers,
|
|
438
|
+
agentJobs,
|
|
439
|
+
activeTools,
|
|
440
|
+
})));
|
|
441
|
+
const [refreshTick, setRefreshTick] = useState(0);
|
|
442
|
+
const statuslineArgsRef = useRef(null);
|
|
443
|
+
const bootFullDoneRef = useRef(false);
|
|
444
|
+
const mountAtRef = useRef(Date.now());
|
|
445
|
+
const lineRef = useRef('');
|
|
446
|
+
const bootFullNextAttemptAtRef = useRef(0);
|
|
447
|
+
const bootFullRetryBackoffMsRef = useRef(STATUSLINE_BOOT_FULL_RETRY_MS);
|
|
448
|
+
const renderEffectIdRef = useRef(0);
|
|
449
|
+
const lastImmediateArgsRef = useRef(null);
|
|
450
|
+
const themeEpochRef = useRef(themeEpoch);
|
|
451
|
+
const lastRawFullLineRef = useRef('');
|
|
452
|
+
const lastRawFullLineCacheKeyRef = useRef('');
|
|
453
|
+
// When (Date.now()) the cached full line was captured, so a grafted shell
|
|
454
|
+
// segment can advance its elapsed by (now - capturedAt) without disk access.
|
|
455
|
+
const lastRawFullLineAtRef = useRef(0);
|
|
456
|
+
|
|
457
|
+
const statuslineArgs = {
|
|
458
|
+
sessionId, clientHostPid, provider, model, effort, fast, cwd, stats,
|
|
459
|
+
contextWindow, displayContextWindow, compactBoundaryTokens, autoCompactTokenLimit, rawContextWindow,
|
|
460
|
+
agentWorkers, agentJobs, activeTools,
|
|
461
|
+
};
|
|
462
|
+
statuslineArgsRef.current = statuslineArgs;
|
|
463
|
+
lineRef.current = line;
|
|
464
|
+
// Stable primitive signature for the activeTools object so the render effect
|
|
465
|
+
// re-runs when explore/search counts or start times change (object identity
|
|
466
|
+
// would otherwise be a new ref every render and over-fire the effect).
|
|
467
|
+
const activeToolsSignature = activeTools
|
|
468
|
+
? [
|
|
469
|
+
Number(activeTools.explore?.count) || 0,
|
|
470
|
+
Number(activeTools.explore?.startedAt) || 0,
|
|
471
|
+
Number(activeTools.search?.count) || 0,
|
|
472
|
+
Number(activeTools.search?.startedAt) || 0,
|
|
473
|
+
].join('|')
|
|
474
|
+
: '';
|
|
475
|
+
const refreshMs = hasActiveStatuslineWork(line, agentWorkers, agentJobs, activeTools) ? STATUSLINE_ACTIVE_REFRESH_MS : STATUSLINE_REFRESH_MS;
|
|
476
|
+
|
|
477
|
+
useEffect(() => {
|
|
478
|
+
const timer = setInterval(() => {
|
|
479
|
+
setRefreshTick((tick) => (tick + 1) % 1_000_000);
|
|
480
|
+
}, refreshMs);
|
|
481
|
+
timer.unref?.();
|
|
482
|
+
return () => clearInterval(timer);
|
|
483
|
+
}, [refreshMs]);
|
|
56
484
|
|
|
57
485
|
useEffect(() => {
|
|
58
486
|
let alive = true;
|
|
487
|
+
const effectId = renderEffectIdRef.current + 1;
|
|
488
|
+
renderEffectIdRef.current = effectId;
|
|
489
|
+
const isCurrentEffect = () => alive && renderEffectIdRef.current === effectId;
|
|
490
|
+
const args = statuslineArgsRef.current || statuslineArgs;
|
|
491
|
+
const footerCacheKey = statuslineFooterCacheKey({ ...args, agentRevision });
|
|
492
|
+
const identityChanged = shouldSnapLocalStatusline(
|
|
493
|
+
{ ...args, agentRevision },
|
|
494
|
+
lastImmediateArgsRef.current,
|
|
495
|
+
);
|
|
496
|
+
// ROUTE identity = the subset that actually changes the async full line's L1
|
|
497
|
+
// usage/quota segment (provider/model/session/effort/fast + context windows).
|
|
498
|
+
// agentRevision, compactBoundaryTokens, autoCompactTokenLimit and stats-reset
|
|
499
|
+
// are NON-ROUTE: they churn while several agents run but DON'T invalidate the
|
|
500
|
+
// L1 usage windows. Only a true route/session switch may wipe the cached full
|
|
501
|
+
// line and snap to the usage-less local line; non-route churn must keep the
|
|
502
|
+
// last good full line so `5H …/7D …` never blinks. (lastImmediateArgsRef holds
|
|
503
|
+
// exactly these route fields, captured at the end of the previous effect run.)
|
|
504
|
+
const prevImmediate = lastImmediateArgsRef.current;
|
|
505
|
+
const routeChanged = !prevImmediate
|
|
506
|
+
|| prevImmediate.sessionId !== args.sessionId
|
|
507
|
+
|| prevImmediate.provider !== args.provider
|
|
508
|
+
|| prevImmediate.model !== args.model
|
|
509
|
+
|| prevImmediate.effort !== args.effort
|
|
510
|
+
|| prevImmediate.fast !== args.fast
|
|
511
|
+
|| prevImmediate.contextWindow !== args.contextWindow
|
|
512
|
+
|| prevImmediate.displayContextWindow !== args.displayContextWindow
|
|
513
|
+
|| prevImmediate.rawContextWindow !== args.rawContextWindow;
|
|
514
|
+
// A theme switch must re-tone the footer immediately: the stored `line`
|
|
515
|
+
// holds already-normalized ANSI with the OLD palette, so re-running
|
|
516
|
+
// normalizeStatusLine on it is a no-op. Force a fresh local rebuild (new
|
|
517
|
+
// palette) and reset bootFullDone so the next full render re-normalizes.
|
|
518
|
+
const themeChanged = themeEpochRef.current !== themeEpoch;
|
|
519
|
+
if (themeChanged) {
|
|
520
|
+
themeEpochRef.current = themeEpoch;
|
|
521
|
+
}
|
|
522
|
+
// Only a real route/session switch invalidates the cached full line (and its
|
|
523
|
+
// L1 usage segment). On non-route identity churn (agent stage/status, compact
|
|
524
|
+
// boundary, auto-compact limit, stats reset) KEEP the cache so the usage
|
|
525
|
+
// segment survives; the async full render scheduled below refreshes any stale
|
|
526
|
+
// L2 within ~150ms (and every ~250ms while active).
|
|
527
|
+
if (routeChanged) {
|
|
528
|
+
lastRawFullLineRef.current = '';
|
|
529
|
+
lastRawFullLineCacheKeyRef.current = '';
|
|
530
|
+
lastRawFullLineAtRef.current = 0;
|
|
531
|
+
bootFullDoneRef.current = false;
|
|
532
|
+
}
|
|
533
|
+
const snapLocalNow = themeChanged
|
|
534
|
+
|| bootFullDoneRef.current !== true
|
|
535
|
+
|| identityChanged;
|
|
536
|
+
if (snapLocalNow) {
|
|
537
|
+
// Reuse the last good FULL line (with its L1 usage segment) whenever this is
|
|
538
|
+
// NOT a route switch and a cached full line exists — covers theme re-tone,
|
|
539
|
+
// agent churn, compact/auto-compact changes and stats reset. We intentionally
|
|
540
|
+
// do NOT require `=== footerCacheKey` here: footerCacheKey embeds agentRevision
|
|
541
|
+
// + compact fields, so it differs on exactly the non-route churn we want to
|
|
542
|
+
// ride through, and requiring equality would fall back to the usage-less
|
|
543
|
+
// localBootStatusLine and reintroduce the blink. Route switches (routeChanged)
|
|
544
|
+
// have already wiped the cache above, so they snap local.
|
|
545
|
+
const useCachedRaw = !routeChanged && Boolean(lastRawFullLineRef.current);
|
|
546
|
+
let localNext;
|
|
547
|
+
if (useCachedRaw) {
|
|
548
|
+
// Keep the cached L1 (usage segment preserved, re-toned via normalize) but
|
|
549
|
+
// graft a FRESH L2 from the CURRENT args so agent count/elapsed/explore/
|
|
550
|
+
// search are never stale even for one ~150ms frame. The cached full line
|
|
551
|
+
// is canonical-truecolor → normalizeStatusLine re-tones it to the active
|
|
552
|
+
// palette; the fresh L2 is built with statusColors() (already themed) so it
|
|
553
|
+
// MUST NOT be double-normalized. Fall back to localBootStatusLine if the
|
|
554
|
+
// cached line somehow normalizes to empty (never emit a blank).
|
|
555
|
+
const cachedNormalized = normalizeStatusLine(lastRawFullLineRef.current);
|
|
556
|
+
if (cachedNormalized) {
|
|
557
|
+
const [cachedL1, cachedL2 = ''] = cachedNormalized.split('\n');
|
|
558
|
+
const now = Date.now();
|
|
559
|
+
// Share ONE `now` across fresh + grafted-shell segments so every L2
|
|
560
|
+
// spinner shows the SAME frame even across a 120ms frame boundary.
|
|
561
|
+
const freshL2 = localStatusLineL2(args, now); // already themed — do NOT normalize
|
|
562
|
+
// Preserve the cached L2's Shell segment(s): the local path can't recompute
|
|
563
|
+
// them (disk-based), so carry them as the canonical tail (Agents → Explore →
|
|
564
|
+
// Searching → Shells). Rebuilt in themed SGR with a current spinner frame so
|
|
565
|
+
// they animate in sync with the fresh segments.
|
|
566
|
+
const shellSegments = extractCachedShellSegments(cachedL2, now, lastRawFullLineAtRef.current);
|
|
567
|
+
const { SUBTLE } = statusColors();
|
|
568
|
+
const localSegSep = ` ${SUBTLE}│${RESET} `;
|
|
569
|
+
const grafted = [freshL2, ...shellSegments].filter(Boolean).join(localSegSep);
|
|
570
|
+
localNext = grafted ? `${cachedL1}\n${grafted}` : cachedL1;
|
|
571
|
+
} else {
|
|
572
|
+
localNext = normalizeStatusLine(localBootStatusLine(args));
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
localNext = normalizeStatusLine(localBootStatusLine(args));
|
|
576
|
+
}
|
|
577
|
+
if (localNext) setLine((prev) => (prev === localNext ? prev : localNext));
|
|
578
|
+
}
|
|
579
|
+
lastImmediateArgsRef.current = {
|
|
580
|
+
agentRevision,
|
|
581
|
+
sessionId: args.sessionId,
|
|
582
|
+
provider: args.provider,
|
|
583
|
+
model: args.model,
|
|
584
|
+
effort: args.effort,
|
|
585
|
+
fast: args.fast,
|
|
586
|
+
contextWindow: args.contextWindow,
|
|
587
|
+
displayContextWindow: args.displayContextWindow,
|
|
588
|
+
rawContextWindow: args.rawContextWindow,
|
|
589
|
+
compactBoundaryTokens: args.compactBoundaryTokens,
|
|
590
|
+
autoCompactTokenLimit: args.autoCompactTokenLimit,
|
|
591
|
+
stats: args.stats,
|
|
592
|
+
};
|
|
59
593
|
const timer = setTimeout(() => {
|
|
594
|
+
if (bootFullDoneRef.current !== true) {
|
|
595
|
+
if (!bootFullRenderEligible(mountAtRef.current, lineRef.current, args.agentWorkers, args.agentJobs, args.activeTools)) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (!canAttemptBootFullRender(bootFullNextAttemptAtRef.current)) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
60
602
|
loadStatuslineModule()
|
|
61
|
-
.then((m) =>
|
|
603
|
+
.then((m) => {
|
|
604
|
+
if (!isCurrentEffect()) return null;
|
|
605
|
+
return m.renderStatusline(args);
|
|
606
|
+
})
|
|
62
607
|
.then((s) => {
|
|
63
|
-
if (!
|
|
608
|
+
if (!isCurrentEffect() || s == null) return;
|
|
609
|
+
lastRawFullLineCacheKeyRef.current = footerCacheKey;
|
|
610
|
+
lastRawFullLineRef.current = String(s);
|
|
611
|
+
lastRawFullLineAtRef.current = Date.now();
|
|
612
|
+
bootFullDoneRef.current = true;
|
|
613
|
+
bootFullRetryBackoffMsRef.current = STATUSLINE_BOOT_FULL_RETRY_MS;
|
|
614
|
+
bootFullNextAttemptAtRef.current = 0;
|
|
64
615
|
const next = normalizeStatusLine(s);
|
|
65
|
-
setLine((prev) => (prev === next ? prev : next));
|
|
616
|
+
if (next) setLine((prev) => (prev === next ? prev : next));
|
|
66
617
|
})
|
|
67
618
|
.catch(() => {
|
|
68
|
-
if (
|
|
619
|
+
if (!isCurrentEffect()) return;
|
|
620
|
+
if (bootFullDoneRef.current !== true) {
|
|
621
|
+
scheduleBootFullRetry(bootFullRetryBackoffMsRef, bootFullNextAttemptAtRef);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
resetStatuslineModuleLoad();
|
|
625
|
+
// Keep the previous/minimal line. Boot-time gateway/cache races should
|
|
626
|
+
// never blank the reserved footer and make the statusline flicker.
|
|
69
627
|
});
|
|
70
628
|
}, STATUSLINE_RENDER_DEBOUNCE_MS);
|
|
71
629
|
timer.unref?.();
|
|
@@ -73,13 +631,23 @@ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fas
|
|
|
73
631
|
alive = false;
|
|
74
632
|
clearTimeout(timer);
|
|
75
633
|
};
|
|
76
|
-
}, [sessionId, clientHostPid, provider, model, effort, fast, cwd, stats, contextWindow, rawContextWindow, resizeEpoch,
|
|
77
|
-
|
|
634
|
+
}, [sessionId, clientHostPid, provider, model, effort, fast, cwd, stats, contextWindow, displayContextWindow, compactBoundaryTokens, autoCompactTokenLimit, rawContextWindow, resizeEpoch, agentRevision, agentWorkers, agentJobs, activeToolsSignature, refreshTick, themeEpoch]);
|
|
635
|
+
|
|
636
|
+
const lines = line ? line.split('\n').slice(0, 2) : [' ', ' '];
|
|
637
|
+
const workflowLabel = workflowModeLabel(workflow);
|
|
638
|
+
// Footer footprint stays 3 rows total, but L2 sits directly under L1 without
|
|
639
|
+
// an internal spacer; the remaining row is kept as outer breathing room.
|
|
78
640
|
return (
|
|
79
|
-
<Box flexDirection="column" width="100%" height={
|
|
80
|
-
|
|
81
|
-
<
|
|
82
|
-
|
|
641
|
+
<Box flexDirection="column" width="100%" height={3} overflow="hidden" paddingLeft={2} backgroundColor={theme.background}>
|
|
642
|
+
<Box flexDirection="row" width="100%" overflow="hidden">
|
|
643
|
+
<Box flexGrow={1} flexShrink={1} overflow="hidden">
|
|
644
|
+
<Text wrap="truncate">{lines[0] || ' '}</Text>
|
|
645
|
+
</Box>
|
|
646
|
+
<Box flexShrink={0} marginLeft={1} marginRight={1}>
|
|
647
|
+
<Text color={theme.statusText} wrap="truncate">{workflowLabel}</Text>
|
|
648
|
+
</Box>
|
|
649
|
+
</Box>
|
|
650
|
+
<Text wrap="truncate">{lines[1] || ' '}</Text>
|
|
83
651
|
</Box>
|
|
84
652
|
);
|
|
85
653
|
}
|