mixdog 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,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
- const STATUS = ansiRgb(theme.statusText, '\x1b[38;2;198;198;198m');
34
- const SUBTLE = ansiRgb(theme.statusSubtle, '\x1b[38;2;136;136;136m');
35
- const SUCCESS = ansiRgb(theme.success, '\x1b[38;2;0;170;75m');
36
- const WARNING = ansiRgb(theme.warning, '\x1b[38;2;255;193;7m');
37
- const ERROR = ansiRgb(theme.error, '\x1b[38;2;220;70;88m');
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 String(text || '')
41
- .replace(/\n+$/, '')
42
- .replace(/\x1b\[1m/g, STATUS)
43
- .replace(/\x1b\[2m/g, SUBTLE)
44
- .replace(/\x1b\[31m/g, ERROR)
45
- .replace(/\x1b\[32m/g, SUCCESS)
46
- .replace(/\x1b\[33m/g, WARNING)
47
- .replace(/\x1b\[36m/g, SUBTLE)
48
- .replace(/\x1b\[90m/g, SUBTLE)
49
- .replace(/^(?:\x1b\[[0-9;]*m)*◆(?:\x1b\[[0-9;]*m)*\s?/, STATUS)
50
- .replace(/(\x1b\[0m )(\d+(?:\.\d+)?%)(?= |$)/g, `$1${STATUS}$2${RESET}`)
51
- .replaceAll(`${RESET} ${SUBTLE}│${RESET} `, ` ${SUBTLE}│${RESET} `);
52
- }
53
-
54
- function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fast, cwd, stats, contextWindow, rawContextWindow, resizeEpoch, bridgeRevision = '', bridgeWorkers = [], bridgeJobs = [], initialLine = '' }) {
55
- const [line, setLine] = useState(() => normalizeStatusLine(initialLine));
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) => m.renderStatusline({ sessionId, clientHostPid, provider, model, effort, fast, cwd, stats, contextWindow, rawContextWindow, bridgeWorkers, bridgeJobs }))
603
+ .then((m) => {
604
+ if (!isCurrentEffect()) return null;
605
+ return m.renderStatusline(args);
606
+ })
62
607
  .then((s) => {
63
- if (!alive) return;
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 (alive) setLine((prev) => (prev === '' ? prev : ''));
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, bridgeRevision]);
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={2} overflow="hidden" paddingLeft={2} marginBottom={1} backgroundColor={theme.background}>
80
- {(line ? line.split('\n').slice(0, 2) : [' ', ' ']).map((l, i) => (
81
- <Text key={i} wrap="truncate">{l || ' '}</Text>
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
  }