@gajae-code/coding-agent 0.11.1 → 0.11.2

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 (404) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/cli/config-cli.d.ts +11 -1
  3. package/dist/types/cli/notify-cli.d.ts +1 -0
  4. package/dist/types/cli/session-picker.d.ts +2 -1
  5. package/dist/types/cli/setup-cli.d.ts +1 -1
  6. package/dist/types/commands/mcp-serve.d.ts +33 -0
  7. package/dist/types/commands/mcp.d.ts +1 -1
  8. package/dist/types/commands/notify.d.ts +0 -2
  9. package/dist/types/commands/sdk.d.ts +1 -1
  10. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  11. package/dist/types/config/file-lock.d.ts +14 -4
  12. package/dist/types/config/keybindings.d.ts +42 -1
  13. package/dist/types/config/model-auth.d.ts +3 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  15. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  16. package/dist/types/config/model-equivalence.d.ts +20 -0
  17. package/dist/types/config/model-registry.d.ts +16 -15
  18. package/dist/types/config/model-resolver.d.ts +28 -18
  19. package/dist/types/config/models-config-schema.d.ts +11 -0
  20. package/dist/types/config/settings-schema.d.ts +56 -1
  21. package/dist/types/config/settings.d.ts +14 -11
  22. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  23. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  24. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  25. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  26. package/dist/types/discovery/helpers.d.ts +0 -6
  27. package/dist/types/edit/index.d.ts +3 -0
  28. package/dist/types/eval/py/env.d.ts +9 -0
  29. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  30. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  31. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  32. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  33. package/dist/types/extensibility/shared-events.d.ts +2 -0
  34. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  35. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  36. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  37. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  38. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  39. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  40. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  41. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  42. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  43. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  44. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  45. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  46. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  47. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  49. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  50. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  51. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  52. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  53. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  54. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  55. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  56. package/dist/types/goals/runtime.d.ts +1 -0
  57. package/dist/types/goals/state.d.ts +8 -0
  58. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  59. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  60. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  61. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  62. package/dist/types/hindsight/state.d.ts +11 -4
  63. package/dist/types/lsp/lspmux.d.ts +5 -3
  64. package/dist/types/lsp/path-trust.d.ts +1 -0
  65. package/dist/types/main.d.ts +3 -2
  66. package/dist/types/memories/storage.d.ts +1 -0
  67. package/dist/types/modes/action-registry.d.ts +29 -0
  68. package/dist/types/modes/components/chrome.d.ts +14 -0
  69. package/dist/types/modes/components/command-palette.d.ts +27 -0
  70. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  71. package/dist/types/modes/components/index.d.ts +2 -1
  72. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  73. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  74. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  75. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  76. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  77. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  78. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  79. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  80. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  81. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  82. package/dist/types/modes/components/status-line.d.ts +1 -94
  83. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  84. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  85. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  86. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  87. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  88. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  89. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  90. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  91. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  92. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  93. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  94. package/dist/types/modes/interactive-mode.d.ts +19 -17
  95. package/dist/types/modes/jobs-observer.d.ts +2 -0
  96. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  97. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
  98. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  99. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  100. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  101. package/dist/types/modes/types.d.ts +20 -13
  102. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  103. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  104. package/dist/types/sdk/broker/broker.d.ts +78 -12
  105. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  106. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  107. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  108. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  109. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  110. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  111. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  112. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  113. package/dist/types/sdk/bus/config.d.ts +11 -0
  114. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  115. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  116. package/dist/types/sdk/bus/index.d.ts +27 -0
  117. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  118. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  119. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  120. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  121. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  122. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  123. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  124. package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
  125. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  126. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  127. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  128. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  129. package/dist/types/sdk/host/host.d.ts +2 -0
  130. package/dist/types/sdk/index.d.ts +1 -0
  131. package/dist/types/sdk/session-directory.d.ts +56 -0
  132. package/dist/types/session/agent-session.d.ts +82 -47
  133. package/dist/types/session/agent-storage.d.ts +7 -0
  134. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  135. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  136. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  137. package/dist/types/session/messages.d.ts +16 -0
  138. package/dist/types/session/session-manager-internal.d.ts +14 -0
  139. package/dist/types/session/session-manager.d.ts +82 -12
  140. package/dist/types/session/session-storage.d.ts +43 -3
  141. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  142. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  143. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  144. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  145. package/dist/types/system-prompt.d.ts +2 -11
  146. package/dist/types/task/executor.d.ts +3 -1
  147. package/dist/types/thinking.d.ts +7 -0
  148. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  149. package/dist/types/tools/ask.d.ts +64 -2
  150. package/dist/types/tools/bash.d.ts +3 -0
  151. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  152. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  153. package/dist/types/tools/cron.d.ts +2 -0
  154. package/dist/types/tools/fetch.d.ts +1 -0
  155. package/dist/types/tools/index.d.ts +30 -15
  156. package/dist/types/tools/output-meta.d.ts +1 -0
  157. package/dist/types/tools/read.d.ts +3 -0
  158. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  159. package/dist/types/utils/event-bus.d.ts +1 -1
  160. package/dist/types/web/search/index.d.ts +3 -1
  161. package/package.json +21 -9
  162. package/scripts/build-sdk-package-smoke.ts +34 -1
  163. package/scripts/generate-hotkeys-docs.ts +74 -0
  164. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  165. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  166. package/src/async/job-manager.ts +49 -6
  167. package/src/cli/config-cli.ts +75 -2
  168. package/src/cli/notify-cli.ts +92 -26
  169. package/src/cli/session-picker.ts +20 -3
  170. package/src/cli/setup-cli.ts +1 -1
  171. package/src/cli.ts +1 -1
  172. package/src/commands/config.ts +1 -1
  173. package/src/commands/harness.ts +34 -106
  174. package/src/commands/launch.ts +6 -4
  175. package/src/commands/mcp-serve.ts +85 -8
  176. package/src/commands/mcp.ts +2 -2
  177. package/src/commands/notify.ts +8 -4
  178. package/src/commands/sdk.ts +61 -40
  179. package/src/commands/ultragoal.ts +4 -1
  180. package/src/config/atomic-yaml-patch.ts +80 -13
  181. package/src/config/file-lock.ts +105 -43
  182. package/src/config/keybindings.ts +64 -20
  183. package/src/config/model-auth.ts +9 -0
  184. package/src/config/model-bindings-applier.ts +139 -0
  185. package/src/config/model-discovery-manager.ts +224 -0
  186. package/src/config/model-equivalence.ts +66 -0
  187. package/src/config/model-registry.ts +169 -302
  188. package/src/config/model-resolver.ts +361 -369
  189. package/src/config/models-config-schema.ts +5 -4
  190. package/src/config/settings-schema.ts +159 -2
  191. package/src/config/settings.ts +458 -482
  192. package/src/coordinator-mcp/policy.ts +76 -11
  193. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  194. package/src/coordinator-mcp/question-state.ts +778 -0
  195. package/src/coordinator-mcp/server.ts +1702 -238
  196. package/src/debug/index.ts +1 -1
  197. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  198. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  199. package/src/discovery/helpers.ts +1 -50
  200. package/src/edit/index.ts +64 -0
  201. package/src/eval/py/env.ts +26 -0
  202. package/src/eval/py/kernel.ts +5 -3
  203. package/src/extensibility/extensions/loader.ts +9 -1
  204. package/src/extensibility/extensions/runner.ts +12 -3
  205. package/src/extensibility/extensions/types.ts +39 -1
  206. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  207. package/src/extensibility/hooks/runner.ts +2 -2
  208. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  209. package/src/extensibility/shared-events.ts +2 -0
  210. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  211. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  212. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  213. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  214. package/src/gjc-runtime/launch-tmux.ts +2 -5
  215. package/src/gjc-runtime/launch-worktree.ts +5 -5
  216. package/src/gjc-runtime/linux-proc.ts +71 -0
  217. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  218. package/src/gjc-runtime/session-resolution.ts +18 -3
  219. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  220. package/src/gjc-runtime/state-migrations.ts +1 -1
  221. package/src/gjc-runtime/state-runtime.ts +320 -249
  222. package/src/gjc-runtime/state-writer.ts +36 -40
  223. package/src/gjc-runtime/team-launch.ts +319 -0
  224. package/src/gjc-runtime/team-notify.ts +326 -0
  225. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  226. package/src/gjc-runtime/team-store.ts +828 -0
  227. package/src/gjc-runtime/team-workers.ts +962 -0
  228. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  229. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  230. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  231. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  232. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  233. package/src/gjc-runtime/ultragoal-guard.ts +29 -5
  234. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  235. package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
  236. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  237. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  238. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  239. package/src/goals/continuation-timeout-guard.ts +79 -0
  240. package/src/goals/runtime.ts +7 -5
  241. package/src/goals/state.ts +18 -0
  242. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  243. package/src/harness-control-plane/phase-rollup.ts +4 -7
  244. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  245. package/src/harness-control-plane/receipt-spool.ts +51 -7
  246. package/src/harness-control-plane/session-lease.ts +12 -130
  247. package/src/hindsight/backend.ts +26 -27
  248. package/src/hindsight/state.ts +101 -32
  249. package/src/hooks/skill-state.ts +17 -11
  250. package/src/internal-urls/docs-index.generated.ts +21 -18
  251. package/src/lsp/client.ts +1 -1
  252. package/src/lsp/config.ts +145 -26
  253. package/src/lsp/index.ts +1 -1
  254. package/src/lsp/lspmux.ts +39 -10
  255. package/src/lsp/path-trust.ts +81 -0
  256. package/src/main.ts +28 -8
  257. package/src/memories/index.ts +1 -0
  258. package/src/memories/storage.ts +6 -2
  259. package/src/modes/action-registry.ts +165 -0
  260. package/src/modes/components/chrome.ts +40 -0
  261. package/src/modes/components/command-palette.ts +168 -0
  262. package/src/modes/components/custom-editor.ts +1 -1
  263. package/src/modes/components/custom-provider-wizard.ts +51 -14
  264. package/src/modes/components/index.ts +2 -1
  265. package/src/modes/components/pet-selector.ts +11 -17
  266. package/src/modes/components/plan-preview-overlay.ts +295 -0
  267. package/src/modes/components/queue-mode-selector.ts +10 -27
  268. package/src/modes/components/queue-pane.ts +79 -0
  269. package/src/modes/components/read-tool-group.ts +13 -0
  270. package/src/modes/components/session-observer-overlay.ts +207 -772
  271. package/src/modes/components/sessions-dashboard.ts +181 -0
  272. package/src/modes/components/settings-selector.ts +1 -1
  273. package/src/modes/components/show-images-selector.ts +10 -24
  274. package/src/modes/components/status-line/types.ts +1 -1
  275. package/src/modes/components/status-line.ts +1 -969
  276. package/src/modes/components/tasks-pane.ts +73 -0
  277. package/src/modes/components/theme-selector.ts +11 -31
  278. package/src/modes/components/tool-execution.ts +22 -0
  279. package/src/modes/components/tool-status-header.ts +1056 -0
  280. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  281. package/src/modes/controllers/btw-controller.ts +1 -0
  282. package/src/modes/controllers/event-controller.ts +3 -2
  283. package/src/modes/controllers/extension-ui-controller.ts +24 -1
  284. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  285. package/src/modes/controllers/input-controller.ts +764 -229
  286. package/src/modes/controllers/mode-gate.ts +24 -0
  287. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  288. package/src/modes/controllers/selector-controller.ts +225 -10
  289. package/src/modes/controllers/stt-controller.ts +125 -0
  290. package/src/modes/interactive-mode.ts +289 -1150
  291. package/src/modes/jobs-observer.ts +3 -0
  292. package/src/modes/runtime-init.ts +8 -0
  293. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  294. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
  295. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  296. package/src/modes/tasks-aggregator.ts +199 -0
  297. package/src/modes/transcript-item-registry.ts +229 -0
  298. package/src/modes/types.ts +45 -18
  299. package/src/modes/utils/ui-helpers.ts +17 -9
  300. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  301. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  302. package/src/prompts/agents/architect.md +2 -8
  303. package/src/prompts/agents/critic.md +2 -8
  304. package/src/prompts/agents/executor.md +3 -0
  305. package/src/prompts/agents/planner.md +4 -12
  306. package/src/prompts/system/custom-system-prompt.md +1 -23
  307. package/src/prompts/system/plan-mode-approved.md +8 -0
  308. package/src/prompts/system/system-prompt.md +17 -96
  309. package/src/prompts/tools/bash.md +4 -4
  310. package/src/prompts/tools/browser.md +15 -52
  311. package/src/prompts/tools/find.md +2 -2
  312. package/src/prompts/tools/hashline.md +13 -55
  313. package/src/prompts/tools/read.md +3 -6
  314. package/src/prompts/tools/search-tool-bm25.md +0 -2
  315. package/src/prompts/tools/search.md +2 -4
  316. package/src/prompts/tools/skill-discovery.md +1 -2
  317. package/src/prompts/tools/task.md +1 -1
  318. package/src/runtime-mcp/manager.ts +1 -1
  319. package/src/sdk/broker/broker.ts +206 -78
  320. package/src/sdk/broker/discovery.ts +1 -1
  321. package/src/sdk/broker/ensure.ts +3 -1
  322. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  323. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  324. package/src/sdk/broker/lifecycle.ts +1399 -142
  325. package/src/sdk/broker/process-incarnation.ts +5 -13
  326. package/src/sdk/broker/session-index.ts +145 -56
  327. package/src/sdk/broker/state-version.ts +21 -0
  328. package/src/sdk/broker/transport.ts +1 -1
  329. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  330. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  331. package/src/sdk/bus/chat-effect-journal.ts +10 -2
  332. package/src/sdk/bus/config-commands.ts +5 -4
  333. package/src/sdk/bus/config.ts +98 -0
  334. package/src/sdk/bus/conversation-store.ts +5 -29
  335. package/src/sdk/bus/daemon-paths.ts +2 -0
  336. package/src/sdk/bus/html-format.ts +20 -3
  337. package/src/sdk/bus/index.ts +778 -93
  338. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  339. package/src/sdk/bus/natives-augment.d.ts +26 -1
  340. package/src/sdk/bus/notification-service.ts +168 -20
  341. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  342. package/src/sdk/bus/recent-activity.ts +188 -74
  343. package/src/sdk/bus/rich-document.ts +258 -0
  344. package/src/sdk/bus/rich-render.ts +62 -70
  345. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  346. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  347. package/src/sdk/bus/telegram-daemon.ts +1389 -237
  348. package/src/sdk/bus/threaded-inbound.ts +8 -3
  349. package/src/sdk/bus/threaded-render.ts +57 -1
  350. package/src/sdk/bus/topic-registry.ts +120 -13
  351. package/src/sdk/host/control/dispatch.ts +14 -2
  352. package/src/sdk/host/control/operations.ts +6 -1
  353. package/src/sdk/host/host.ts +31 -1
  354. package/src/sdk/host/query/handlers.ts +20 -22
  355. package/src/sdk/host/query/revision-store.ts +80 -57
  356. package/src/sdk/index.ts +1 -0
  357. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  358. package/src/sdk/session-directory.ts +141 -0
  359. package/src/sdk/session.ts +458 -479
  360. package/src/session/agent-session.ts +1700 -583
  361. package/src/session/agent-storage.ts +18 -0
  362. package/src/session/cache-economics.ts +25 -8
  363. package/src/session/fallback-chain-controller.ts +53 -3
  364. package/src/session/internal/managed-session-scope.ts +2226 -0
  365. package/src/session/internal/managed-session-storage.ts +453 -0
  366. package/src/session/messages.ts +91 -2
  367. package/src/session/session-manager-internal.ts +33 -0
  368. package/src/session/session-manager.ts +1112 -333
  369. package/src/session/session-storage.ts +444 -39
  370. package/src/session/volatile-context-pruning.ts +78 -0
  371. package/src/setup/credential-auto-import.ts +2 -1
  372. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  373. package/src/setup/provider-onboarding.ts +36 -61
  374. package/src/setup/provider-presets.json +5 -0
  375. package/src/skill-state/active-state.ts +3 -13
  376. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  377. package/src/skill-state/workflow-state-contract.ts +6 -41
  378. package/src/slash-commands/builtin-registry.ts +46 -11
  379. package/src/system-prompt.ts +37 -44
  380. package/src/task/agents.ts +19 -1
  381. package/src/task/executor.ts +90 -6
  382. package/src/task/index.ts +36 -21
  383. package/src/thinking.ts +21 -6
  384. package/src/tool-discovery/tool-index.ts +2 -134
  385. package/src/tools/ask.ts +157 -21
  386. package/src/tools/ast-edit.ts +1 -1
  387. package/src/tools/bash.ts +38 -0
  388. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  389. package/src/tools/browser/tab-supervisor.ts +282 -36
  390. package/src/tools/computer.ts +10 -3
  391. package/src/tools/cron.ts +30 -16
  392. package/src/tools/eval.ts +1 -2
  393. package/src/tools/fetch.ts +11 -4
  394. package/src/tools/index.ts +76 -24
  395. package/src/tools/output-meta.ts +13 -5
  396. package/src/tools/read.ts +37 -1
  397. package/src/tools/resource-gc.ts +2 -2
  398. package/src/tools/search-tool-bm25.ts +11 -74
  399. package/src/utils/event-bus.ts +9 -4
  400. package/src/web/search/index.ts +3 -3
  401. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  402. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  403. package/src/discovery/plugin-dir-roots.ts +0 -28
  404. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -0,0 +1,1056 @@
1
+ import * as fs from "node:fs";
2
+
3
+ import { type Component, truncateToWidth, visibleWidth } from "@gajae-code/tui";
4
+ import { formatCount, getProjectDir } from "@gajae-code/utils";
5
+ import { $ } from "bun";
6
+ import type { AppKeybinding, KeybindingsManager } from "../../config/keybindings";
7
+ import { KEYBINDINGS } from "../../config/keybindings";
8
+ import { settings } from "../../config/settings";
9
+ import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../config/settings-schema";
10
+ import { theme } from "../../modes/theme/theme";
11
+ import type { AgentSession } from "../../session/agent-session";
12
+ import { readVisibleSkillActiveState, type SkillActiveEntry } from "../../skill-state/active-state";
13
+ import * as git from "../../utils/git";
14
+ import { getSessionAccentAnsi, getSessionAccentHex } from "../../utils/session-color";
15
+ import type { ActionRegistry, FocusDomain } from "../action-registry";
16
+ import { EMPTY_JOBS_SNAPSHOT, type JobsSnapshot } from "../jobs-observer";
17
+ import { sanitizeStatusText } from "../shared";
18
+ import { renderSkillHudBar } from "./skill-hud/render";
19
+ import {
20
+ canReuseCachedPr,
21
+ createPrCacheContext,
22
+ isSamePrCacheContext,
23
+ type PrCacheContext,
24
+ resolveCurrentBranch,
25
+ } from "./status-line/git-utils";
26
+ import { getPreset } from "./status-line/presets";
27
+ import { renderSegment, type SegmentContext } from "./status-line/segments";
28
+ import { getSeparator } from "./status-line/separators";
29
+ import { calculateTokensPerSecond } from "./status-line/token-rate";
30
+ import type { SeparatorDef } from "./status-line/types";
31
+
32
+ export interface StatusLineSegmentOptions {
33
+ model?: { showThinkingLevel?: boolean; showContextPercent?: boolean };
34
+ path?: { abbreviate?: boolean; maxLength?: number; stripWorkPrefix?: boolean };
35
+ git?: { showBranch?: boolean; showStaged?: boolean; showUnstaged?: boolean; showUntracked?: boolean };
36
+ time?: { format?: "12h" | "24h"; showSeconds?: boolean };
37
+ usage?: { mode?: "used" | "remaining" };
38
+ }
39
+
40
+ export interface StatusLineSettings {
41
+ preset?: StatusLinePreset;
42
+ leftSegments?: StatusLineSegmentId[];
43
+ rightSegments?: StatusLineSegmentId[];
44
+ separator?: StatusLineSeparatorStyle;
45
+ segmentOptions?: StatusLineSegmentOptions;
46
+ previewHighlightSegment?: StatusLineSegmentId;
47
+ showHookStatus?: boolean;
48
+ showSkillHud?: boolean;
49
+ sessionAccent?: boolean;
50
+ maxRows?: number;
51
+ }
52
+
53
+ export interface StatusLineComponentOptions {
54
+ version?: string;
55
+ actionRegistry?: ActionRegistry<void>;
56
+ getKeybindings?: () => KeybindingsManager;
57
+ focusDomain?: FocusDomain;
58
+ }
59
+
60
+ export interface StatusLineActionHint {
61
+ id: AppKeybinding;
62
+ content: string;
63
+ }
64
+
65
+ const ACTION_HINT_PRIORITY: readonly AppKeybinding[] = [
66
+ "app.message.sendNow",
67
+ "app.message.queue",
68
+ "app.message.followUp",
69
+ "app.message.dequeue",
70
+ "app.commandPalette.open",
71
+ "app.plan.toggle",
72
+ "app.mode.cycle",
73
+ "app.thinking.cycle",
74
+ "app.model.select",
75
+ "app.model.cycleForward",
76
+ "app.history.search",
77
+ "app.session.togglePath",
78
+ "app.session.toggleSort",
79
+ "app.session.rename",
80
+ "app.session.delete",
81
+ "app.tree.foldOrUp",
82
+ "app.tree.unfoldOrDown",
83
+ ];
84
+
85
+ /**
86
+ * Produces whole, bound action hints for the current focus domain. The registry
87
+ * remains the authority for availability; KEYBINDINGS remains the authority for
88
+ * whether an action has a binding and the active manager supplies overrides.
89
+ */
90
+ export function getAvailableActionHints(
91
+ actionRegistry: ActionRegistry<void> | undefined,
92
+ getKeybindings: (() => KeybindingsManager) | undefined,
93
+ width: number,
94
+ domain: FocusDomain = "composer",
95
+ ): StatusLineActionHint[] {
96
+ if (!actionRegistry || !getKeybindings || width <= 0) return [];
97
+ const keybindings = getKeybindings();
98
+ const available = actionRegistry.all().filter(action => actionRegistry.isAvailable(action.id));
99
+
100
+ const byId = new Map(available.map(action => [action.id, action]));
101
+ const candidates = ACTION_HINT_PRIORITY.map(id => byId.get(id))
102
+ .filter((action): action is NonNullable<typeof action> => action !== undefined)
103
+ .filter(action => action.domains.includes(domain));
104
+ const selected: StatusLineActionHint[] = [];
105
+ let used = 0;
106
+ for (const action of candidates) {
107
+ const bindingId = action.bindingId ?? action.id;
108
+ if (!(bindingId in KEYBINDINGS)) continue;
109
+ const keys = keybindings.getKeys(bindingId);
110
+ if (keys.length === 0) continue;
111
+ const content = theme.fg("dim", keybindings.getDisplayString(bindingId)) + theme.fg("muted", ` ${action.title}`);
112
+ const nextWidth = visibleWidth(content) + (selected.length === 0 ? 0 : 3);
113
+ if (used + nextWidth > width) break;
114
+ selected.push({ id: action.id, content });
115
+ used += nextWidth;
116
+ }
117
+ return selected;
118
+ }
119
+
120
+ interface CollectedStatusSegments {
121
+ ctx: SegmentContext;
122
+ separatorDef: SeparatorDef;
123
+ bgAnsi: string;
124
+ fgAnsi: string;
125
+ sepAnsi: string;
126
+ left: string[];
127
+ leftSegIds: StatusLineSegmentId[];
128
+ right: string[];
129
+ previewHighlightSegment: StatusLineSegmentId | undefined;
130
+ sessionAccent: boolean | undefined;
131
+ leftSepWidth: number;
132
+ rightSepWidth: number;
133
+ leftCapWidth: number;
134
+ rightCapWidth: number;
135
+ }
136
+
137
+ // StatusLineComponent
138
+ // ═══════════════════════════════════════════════════════════════════════════
139
+
140
+ export class StatusLineComponent implements Component {
141
+ #settings: StatusLineSettings = {};
142
+ #cachedBranch: string | null | undefined = undefined;
143
+ #cachedBranchRepoId: string | null | undefined = undefined;
144
+ #branchProjectDir: string | undefined;
145
+ #branchLastFetch = 0;
146
+ #branchInFlight = false;
147
+ #gitWatcher: fs.FSWatcher | null = null;
148
+ #onBranchChange: (() => void) | null = null;
149
+ #autoCompactEnabled: boolean = true;
150
+ #hookStatuses: Map<string, string> = new Map();
151
+ #subagentCount: number = 0;
152
+ #jobs: JobsSnapshot = EMPTY_JOBS_SNAPSHOT;
153
+ #sessionStartTime: number = Date.now();
154
+ #planModeStatus: { enabled: boolean; paused: boolean } | null = null;
155
+ #goalModeStatus: { enabled: boolean; paused: boolean } | null = null;
156
+ #skillHudEntries: SkillActiveEntry[] = [];
157
+ #skillHudLastFetch = 0;
158
+ #skillHudInFlight = false;
159
+ #version: string | undefined;
160
+ #actionRegistry: ActionRegistry<void> | undefined;
161
+ #getKeybindings: (() => KeybindingsManager) | undefined;
162
+ #focusDomain: FocusDomain;
163
+
164
+ #resolvedSettingsCache:
165
+ | (Required<Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">> &
166
+ StatusLineSettings)
167
+ | undefined;
168
+ #resolvedSettingsFingerprint: string | undefined;
169
+ #renderedRowsCache: { key: string; rows: string[] } | undefined;
170
+ #renderedRowsCacheHits = 0;
171
+ #renderedRowsCacheMisses = 0;
172
+
173
+ // Git status caching (1s TTL)
174
+ #cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
175
+ #gitStatusLastFetch = 0;
176
+ #gitStatusInFlight = false;
177
+
178
+ // PR lookup caching (invalidated on branch/repo context changes)
179
+ #cachedPr: { number: number; url: string } | null | undefined = undefined;
180
+ #cachedPrContext: PrCacheContext | undefined = undefined;
181
+ #prLookupInFlight = false;
182
+ #defaultBranch?: string;
183
+ #lastTokensPerSecond: number | null = null;
184
+ #lastTokensPerSecondTimestamp: number | null = null;
185
+
186
+ // Provider usage caching (5-min TTL, OAuth/sub only)
187
+ #cachedUsage: SegmentContext["usage"] = null;
188
+ #usageFetchedAt = 0;
189
+ #usageInFlight = false;
190
+
191
+ constructor(
192
+ private readonly session: AgentSession,
193
+ options: StatusLineComponentOptions = {},
194
+ ) {
195
+ this.#settings = {
196
+ preset: settings.get("statusLine.preset"),
197
+ leftSegments: settings.get("statusLine.leftSegments"),
198
+ rightSegments: settings.get("statusLine.rightSegments"),
199
+ separator: settings.get("statusLine.separator"),
200
+ showHookStatus: settings.get("statusLine.showHookStatus"),
201
+ showSkillHud: settings.get("statusLine.showSkillHud"),
202
+ segmentOptions: settings.getGroup("statusLine").segmentOptions,
203
+ sessionAccent: settings.get("statusLine.sessionAccent"),
204
+ maxRows: settings.get("statusLine.maxRows"),
205
+ };
206
+ this.#version = options.version?.trim() || undefined;
207
+ this.#actionRegistry = options.actionRegistry;
208
+ this.#getKeybindings = options.getKeybindings;
209
+ this.#focusDomain = options.focusDomain ?? "composer";
210
+ }
211
+
212
+ updateSettings(settings: StatusLineSettings): void {
213
+ this.#settings = { ...this.#settings, previewHighlightSegment: undefined, ...settings };
214
+ }
215
+
216
+ setActionRegistry(actionRegistry: ActionRegistry<void>, getKeybindings: () => KeybindingsManager): void {
217
+ this.#actionRegistry = actionRegistry;
218
+ this.#getKeybindings = getKeybindings;
219
+ this.#renderedRowsCache = undefined;
220
+ }
221
+
222
+ setFocusDomain(domain: FocusDomain): void {
223
+ if (this.#focusDomain === domain) return;
224
+ this.#focusDomain = domain;
225
+ this.#renderedRowsCache = undefined;
226
+ }
227
+
228
+ setAutoCompactEnabled(enabled: boolean): void {
229
+ this.#autoCompactEnabled = enabled;
230
+ }
231
+
232
+ setSubagentCount(count: number): void {
233
+ this.#subagentCount = count;
234
+ }
235
+
236
+ setJobs(jobs: JobsSnapshot): void {
237
+ this.#jobs = jobs;
238
+ }
239
+
240
+ setSessionStartTime(time: number): void {
241
+ this.#sessionStartTime = time;
242
+ }
243
+
244
+ setPlanModeStatus(status: { enabled: boolean; paused: boolean } | undefined): void {
245
+ this.#planModeStatus = status ?? null;
246
+ }
247
+
248
+ setGoalModeStatus(status: { enabled: boolean; paused: boolean } | undefined): void {
249
+ this.#goalModeStatus = status ?? null;
250
+ }
251
+
252
+ setSkillHudEntriesForTest(entries: SkillActiveEntry[]): void {
253
+ this.#skillHudEntries = entries;
254
+ this.#skillHudLastFetch = Date.now();
255
+ }
256
+
257
+ setHookStatus(key: string, text: string | undefined): void {
258
+ if (text === undefined) {
259
+ this.#hookStatuses.delete(key);
260
+ } else {
261
+ this.#hookStatuses.set(key, text);
262
+ }
263
+ }
264
+
265
+ watchBranch(onBranchChange: () => void): void {
266
+ this.#onBranchChange = onBranchChange;
267
+ this.#setupGitWatcher();
268
+ }
269
+
270
+ #setupGitWatcher(): void {
271
+ if (this.#gitWatcher) {
272
+ this.#gitWatcher.close();
273
+ this.#gitWatcher = null;
274
+ }
275
+
276
+ const gitHeadPath = git.repo.resolveSync(getProjectDir())?.headPath ?? null;
277
+ if (!gitHeadPath) return;
278
+
279
+ try {
280
+ this.#gitWatcher = fs.watch(gitHeadPath, () => {
281
+ this.#invalidateGitCaches();
282
+ if (this.#onBranchChange) {
283
+ this.#onBranchChange();
284
+ }
285
+ });
286
+ } catch {
287
+ this.#invalidateGitCaches();
288
+ }
289
+ }
290
+
291
+ dispose(): void {
292
+ if (this.#gitWatcher) {
293
+ this.#gitWatcher.close();
294
+ this.#gitWatcher = null;
295
+ }
296
+ }
297
+
298
+ invalidate(): void {
299
+ this.#invalidateGitCaches();
300
+ }
301
+
302
+ #invalidateGitCaches(): void {
303
+ this.#cachedBranch = undefined;
304
+ this.#cachedBranchRepoId = undefined;
305
+ this.#branchProjectDir = undefined;
306
+ this.#cachedPrContext = undefined;
307
+ this.#branchLastFetch = 0;
308
+ this.#branchInFlight = false;
309
+ this.#renderedRowsCache = undefined;
310
+ }
311
+ #getCurrentBranch(): string | null {
312
+ const now = Date.now();
313
+ const projectDir = getProjectDir();
314
+ const withinTtl =
315
+ this.#cachedBranch !== undefined &&
316
+ this.#branchProjectDir === projectDir &&
317
+ Date.now() - this.#branchLastFetch < 1000;
318
+ if (withinTtl || this.#branchInFlight) {
319
+ return this.#cachedBranch ?? null;
320
+ }
321
+
322
+ this.#branchInFlight = true;
323
+ try {
324
+ const current = resolveCurrentBranch(projectDir);
325
+ this.#cachedBranchRepoId = current.repoId;
326
+ this.#cachedBranch = current.branch;
327
+ this.#branchProjectDir = projectDir;
328
+ return this.#cachedBranch ?? null;
329
+ } catch {
330
+ this.#cachedBranchRepoId = null;
331
+ this.#cachedBranch = null;
332
+ this.#branchProjectDir = projectDir;
333
+ return null;
334
+ } finally {
335
+ this.#branchLastFetch = now;
336
+ this.#branchInFlight = false;
337
+ }
338
+ }
339
+
340
+ #isDefaultBranch(branch: string): boolean {
341
+ if (this.#defaultBranch === undefined) {
342
+ this.#defaultBranch = "main";
343
+ (async () => {
344
+ const resolved = await git.branch.default(getProjectDir());
345
+ if (resolved) {
346
+ this.#defaultBranch = resolved;
347
+ if (this.#onBranchChange) {
348
+ this.#onBranchChange();
349
+ }
350
+ }
351
+ })();
352
+ }
353
+ return branch === this.#defaultBranch;
354
+ }
355
+
356
+ #getGitStatus(): { staged: number; unstaged: number; untracked: number } | null {
357
+ if (this.#gitStatusInFlight || Date.now() - this.#gitStatusLastFetch < 1000) {
358
+ return this.#cachedGitStatus;
359
+ }
360
+
361
+ this.#gitStatusInFlight = true;
362
+
363
+ (async () => {
364
+ try {
365
+ this.#cachedGitStatus = await git.status.summary(getProjectDir());
366
+ } catch {
367
+ this.#cachedGitStatus = null;
368
+ } finally {
369
+ this.#gitStatusLastFetch = Date.now();
370
+ this.#gitStatusInFlight = false;
371
+ }
372
+ })();
373
+
374
+ return this.#cachedGitStatus;
375
+ }
376
+
377
+ #lookupPr(): { number: number; url: string } | null {
378
+ const branch = this.#getCurrentBranch();
379
+ const currentContext = branch ? createPrCacheContext(branch, this.#cachedBranchRepoId ?? null) : null;
380
+
381
+ if (canReuseCachedPr(this.#cachedPr, this.#cachedPrContext, currentContext)) {
382
+ return this.#cachedPr ?? null;
383
+ }
384
+
385
+ const stalePr = this.#cachedPr;
386
+
387
+ // Don't look up if no branch, detached HEAD, default branch, or already in flight
388
+ if (!branch || branch === "detached" || this.#isDefaultBranch(branch) || this.#prLookupInFlight) {
389
+ return stalePr ?? null;
390
+ }
391
+
392
+ this.#prLookupInFlight = true;
393
+ const lookupContext = currentContext;
394
+
395
+ // Fire async lookup, keep stale value visible until resolved
396
+ (async () => {
397
+ // Helper: only write cache if branch/repo context hasn't changed since launch
398
+ const setCachedPr = (value: { number: number; url: string } | null) => {
399
+ const latestBranch = this.#getCurrentBranch();
400
+ const latestContext = latestBranch
401
+ ? createPrCacheContext(latestBranch, this.#cachedBranchRepoId ?? null)
402
+ : undefined;
403
+ if (lookupContext && isSamePrCacheContext(latestContext, lookupContext)) {
404
+ this.#cachedPr = value;
405
+ this.#cachedPrContext = lookupContext;
406
+ }
407
+ };
408
+ try {
409
+ // Requires `gh repo set-default` to be configured; fails gracefully if not
410
+ const result = await $`gh pr view --json number,url`.quiet().nothrow();
411
+ if (result.exitCode !== 0) {
412
+ setCachedPr(null);
413
+ return;
414
+ }
415
+ const pr = JSON.parse(result.stdout.toString()) as { number: number; url: string };
416
+ if (typeof pr.number === "number") {
417
+ setCachedPr({ number: pr.number, url: pr.url });
418
+ } else {
419
+ setCachedPr(null);
420
+ }
421
+ } catch {
422
+ setCachedPr(null);
423
+ } finally {
424
+ this.#prLookupInFlight = false;
425
+ if (this.#onBranchChange) {
426
+ this.#onBranchChange();
427
+ }
428
+ }
429
+ })();
430
+
431
+ return stalePr ?? null;
432
+ }
433
+
434
+ #getTokensPerSecond(): number | null {
435
+ let lastAssistantTimestamp: number | null = null;
436
+ for (let i = this.session.state.messages.length - 1; i >= 0; i--) {
437
+ const message = this.session.state.messages[i];
438
+ if (message?.role === "assistant") {
439
+ lastAssistantTimestamp = message.timestamp;
440
+ break;
441
+ }
442
+ }
443
+
444
+ if (lastAssistantTimestamp === null) {
445
+ this.#lastTokensPerSecond = null;
446
+ this.#lastTokensPerSecondTimestamp = null;
447
+ return null;
448
+ }
449
+
450
+ const rate = calculateTokensPerSecond(this.session.state.messages, this.session.isStreaming);
451
+ if (rate !== null) {
452
+ this.#lastTokensPerSecond = rate;
453
+ this.#lastTokensPerSecondTimestamp = lastAssistantTimestamp;
454
+ return rate;
455
+ }
456
+
457
+ if (this.#lastTokensPerSecondTimestamp === lastAssistantTimestamp) {
458
+ return this.#lastTokensPerSecond;
459
+ }
460
+
461
+ return null;
462
+ }
463
+
464
+ #refreshSkillHudInBackground(): void {
465
+ if (this.#settings.showSkillHud === false) return;
466
+ const now = Date.now();
467
+ if (this.#skillHudInFlight || now - this.#skillHudLastFetch < 1000) return;
468
+ const getCwd = this.session.sessionManager?.getCwd;
469
+ const getSessionId = this.session.sessionManager?.getSessionId;
470
+ const cwd = typeof getCwd === "function" ? getCwd.call(this.session.sessionManager) : getProjectDir();
471
+ const sessionId = typeof getSessionId === "function" ? getSessionId.call(this.session.sessionManager) : undefined;
472
+ this.#skillHudInFlight = true;
473
+ void readVisibleSkillActiveState(cwd, sessionId, { tier: "hud" })
474
+ .then(state => {
475
+ this.#skillHudEntries = state?.active_skills ?? [];
476
+ })
477
+ .catch(() => {
478
+ this.#skillHudEntries = [];
479
+ })
480
+ .finally(() => {
481
+ this.#skillHudLastFetch = Date.now();
482
+ this.#skillHudInFlight = false;
483
+ });
484
+ }
485
+
486
+ /**
487
+ * Background-refresh the OAuth quota report. Guarded by a 5-min TTL on both
488
+ * success (cache lifetime) and error (backoff). Exposed (non-private) so
489
+ * unit tests can verify the backoff invariant.
490
+ */
491
+ refreshUsageInBackground(): void {
492
+ const now = Date.now();
493
+ if (this.#usageInFlight) return;
494
+ if (this.#usageFetchedAt > 0 && now - this.#usageFetchedAt < 5 * 60_000) return;
495
+ const fetcher = (this.session as { fetchUsageReports?: () => Promise<unknown> }).fetchUsageReports;
496
+ if (typeof fetcher !== "function") return;
497
+ this.#usageInFlight = true;
498
+ void fetcher
499
+ .call(this.session)
500
+ .then(reports => {
501
+ this.#cachedUsage = this.#normalizeUsageReports(reports);
502
+ this.#usageFetchedAt = Date.now();
503
+ if (this.#onBranchChange) {
504
+ this.#onBranchChange();
505
+ }
506
+ })
507
+ .catch(() => {
508
+ // Backoff on error: stamp the fetch time so the 5-min TTL guard
509
+ // also acts as an error budget. Without this, every render
510
+ // kicks off another fetch (gated only by #usageInFlight),
511
+ // which hammers the endpoint during a network outage / 5xx.
512
+ this.#usageFetchedAt = Date.now();
513
+ })
514
+ .finally(() => {
515
+ this.#usageInFlight = false;
516
+ });
517
+ }
518
+
519
+ #normalizeUsageReports(reports: unknown): SegmentContext["usage"] {
520
+ if (!Array.isArray(reports)) return null;
521
+ const windows: NonNullable<SegmentContext["usage"]>["windows"] = [];
522
+ const seen = new Set<string>();
523
+ const now = Date.now();
524
+
525
+ const codexWindowLabel = (windowId: string | undefined, fallback: string): string => {
526
+ if (windowId && /^\d+[hd]$/.test(windowId)) return windowId;
527
+ return fallback;
528
+ };
529
+ const codexResetUnit = (label: string): "m" | "h" => (label.endsWith("d") ? "h" : "m");
530
+
531
+ const pushWindow = (
532
+ key: string,
533
+ label: string,
534
+ fraction: number,
535
+ resetsAt: number | undefined,
536
+ resetUnit: "m" | "h",
537
+ ) => {
538
+ if (seen.has(key)) return;
539
+ seen.add(key);
540
+ windows.push({
541
+ label,
542
+ percent: fraction * 100,
543
+ resetValue:
544
+ typeof resetsAt === "number"
545
+ ? Math.max(0, Math.round((resetsAt - now) / (resetUnit === "m" ? 60_000 : 3_600_000)))
546
+ : undefined,
547
+ resetUnit,
548
+ });
549
+ };
550
+
551
+ for (const report of reports) {
552
+ if (!report || typeof report !== "object") continue;
553
+ const provider = (report as { provider?: unknown }).provider;
554
+ const providerId = typeof provider === "string" ? provider : undefined;
555
+ const limits = (report as { limits?: unknown }).limits;
556
+ if (!Array.isArray(limits)) continue;
557
+ for (const limit of limits) {
558
+ if (!limit || typeof limit !== "object") continue;
559
+ const l = limit as {
560
+ id?: unknown;
561
+ scope?: { windowId?: string; tier?: string; modelId?: string };
562
+ window?: { id?: string; resetsAt?: number };
563
+ amount?: { usedFraction?: number };
564
+ };
565
+ const fraction = l.amount?.usedFraction;
566
+ if (typeof fraction !== "number") continue;
567
+ const id = typeof l.id === "string" ? l.id : "";
568
+ const windowId = l.scope?.windowId ?? l.window?.id;
569
+ const tier = l.scope?.tier;
570
+ const modelId = l.scope?.modelId;
571
+ const resetsAt = l.window?.resetsAt;
572
+
573
+ if (providerId === "openai-codex") {
574
+ if (id === "openai-codex:primary" || (!id && !!windowId && windowId !== "7d" && !modelId)) {
575
+ const label = codexWindowLabel(windowId, "primary");
576
+ pushWindow("codex:primary", label, fraction, resetsAt, codexResetUnit(label));
577
+ } else if (id === "openai-codex:secondary" || (!id && windowId === "7d" && !modelId)) {
578
+ const label = codexWindowLabel(windowId, "secondary");
579
+ pushWindow("codex:secondary", label, fraction, resetsAt, codexResetUnit(label));
580
+ }
581
+ } else if (windowId === "5h" && !tier) {
582
+ pushWindow(`${providerId ?? "provider"}:5h`, "5h", fraction, resetsAt, "m");
583
+ } else if (windowId === "7d" && !tier) {
584
+ pushWindow(`${providerId ?? "provider"}:7d`, "7d", fraction, resetsAt, "h");
585
+ }
586
+ }
587
+ }
588
+
589
+ return windows.length > 0 ? { windows } : null;
590
+ }
591
+
592
+ #buildSegmentContext(
593
+ width: number,
594
+ effectiveSettings: Required<
595
+ Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
596
+ > &
597
+ StatusLineSettings,
598
+ ): SegmentContext {
599
+ const state = this.session.state;
600
+
601
+ // Trigger background fetch (5-min TTL); render uses cached value
602
+ this.refreshUsageInBackground();
603
+
604
+ // Get usage statistics
605
+ const aggregateUsageStats = this.session.sessionManager?.getUsageStatistics() ?? {
606
+ input: 0,
607
+ output: 0,
608
+ cacheRead: 0,
609
+ cacheWrite: 0,
610
+ premiumRequests: 0,
611
+ cost: 0,
612
+ };
613
+ const usageStats = {
614
+ ...aggregateUsageStats,
615
+ tokensPerSecond: this.#getTokensPerSecond(),
616
+ };
617
+
618
+ const contextUsage = this.session.getContextUsage?.();
619
+ const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
620
+ const contextPercent = contextUsage?.percent ?? null;
621
+ // Suppress the inline model percentage when a standalone context_pct
622
+ // segment is also rendered, so the value is not shown twice.
623
+ const contextPctSegmentActive =
624
+ effectiveSettings.leftSegments.includes("context_pct") ||
625
+ effectiveSettings.rightSegments.includes("context_pct");
626
+
627
+ return {
628
+ session: this.session,
629
+ width,
630
+ options: effectiveSettings.segmentOptions ?? {},
631
+ planMode: this.#planModeStatus,
632
+ goalMode: this.#goalModeStatus,
633
+ usageStats,
634
+ contextPercent,
635
+ contextWindow,
636
+ contextPctSegmentActive,
637
+ autoCompactEnabled: this.#autoCompactEnabled,
638
+ subagentCount: this.#subagentCount,
639
+ jobs: this.#jobs,
640
+ sessionStartTime: this.#sessionStartTime,
641
+ git: {
642
+ branch: this.#getCurrentBranch(),
643
+ status: this.#getGitStatus(),
644
+ pr: this.#lookupPr(),
645
+ },
646
+ usage: this.#cachedUsage,
647
+ };
648
+ }
649
+
650
+ #settingsFingerprint(): string {
651
+ return JSON.stringify(this.#settings);
652
+ }
653
+
654
+ #resolveSettings(): Required<
655
+ Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
656
+ > &
657
+ StatusLineSettings {
658
+ const fingerprint = this.#settingsFingerprint();
659
+ if (this.#resolvedSettingsCache && this.#resolvedSettingsFingerprint === fingerprint) {
660
+ return this.#resolvedSettingsCache;
661
+ }
662
+
663
+ const preset = this.#settings.preset ?? "default";
664
+ const presetDef = getPreset(preset);
665
+ const useCustomSegments = preset === "custom";
666
+ const mergedSegmentOptions: StatusLineSettings["segmentOptions"] = {};
667
+
668
+ for (const [segment, options] of Object.entries(presetDef.segmentOptions ?? {})) {
669
+ mergedSegmentOptions[segment as keyof StatusLineSegmentOptions] = { ...(options as Record<string, unknown>) };
670
+ }
671
+
672
+ for (const [segment, options] of Object.entries(this.#settings.segmentOptions ?? {})) {
673
+ const current = mergedSegmentOptions[segment as keyof StatusLineSegmentOptions] ?? {};
674
+ mergedSegmentOptions[segment as keyof StatusLineSegmentOptions] = {
675
+ ...(current as Record<string, unknown>),
676
+ ...(options as Record<string, unknown>),
677
+ };
678
+ }
679
+
680
+ const leftSegments = useCustomSegments
681
+ ? (this.#settings.leftSegments ?? presetDef.leftSegments)
682
+ : presetDef.leftSegments;
683
+ const rightSegments = useCustomSegments
684
+ ? (this.#settings.rightSegments ?? presetDef.rightSegments)
685
+ : presetDef.rightSegments;
686
+
687
+ this.#resolvedSettingsFingerprint = fingerprint;
688
+ this.#resolvedSettingsCache = {
689
+ ...this.#settings,
690
+ leftSegments,
691
+ rightSegments,
692
+ separator: this.#settings.separator ?? presetDef.separator,
693
+ segmentOptions: mergedSegmentOptions,
694
+ };
695
+ return this.#resolvedSettingsCache;
696
+ }
697
+
698
+ #groupWidth(parts: string[], capWidth: number, sepWidth: number): number {
699
+ if (parts.length === 0) return 0;
700
+ const partsWidth = parts.reduce((sum, part) => sum + visibleWidth(part), 0);
701
+ const sepTotal = Math.max(0, parts.length - 1) * (sepWidth + 2);
702
+ return partsWidth + sepTotal + 2 + capWidth;
703
+ }
704
+
705
+ #renderStatusGroup(
706
+ parts: string[],
707
+ direction: "left" | "right",
708
+ separatorDef: SeparatorDef,
709
+ bgAnsi: string,
710
+ fgAnsi: string,
711
+ sepAnsi: string,
712
+ ): string {
713
+ if (parts.length === 0) return "";
714
+ const sep = direction === "left" ? separatorDef.left : separatorDef.right;
715
+ const cap = separatorDef.endCaps
716
+ ? direction === "left"
717
+ ? separatorDef.endCaps.right
718
+ : separatorDef.endCaps.left
719
+ : "";
720
+ const capPrefix = separatorDef.endCaps?.useBgAsFg ? bgAnsi.replace("\x1b[48;", "\x1b[38;") : bgAnsi + sepAnsi;
721
+ const capText = cap ? `${capPrefix}${cap}\x1b[0m` : "";
722
+
723
+ let content = bgAnsi + fgAnsi;
724
+ content += ` ${parts.join(` ${sepAnsi}${sep}${fgAnsi} `)} `;
725
+ content += "\x1b[0m";
726
+
727
+ if (capText) {
728
+ return direction === "right" ? capText + content : content + capText;
729
+ }
730
+ return content;
731
+ }
732
+
733
+ #shrinkPathToWidth(content: string, ctx: SegmentContext, shrinkBy: number): string | null {
734
+ const currentPathVW = visibleWidth(content);
735
+ const minPathVW = 8; // icon + ellipsis + a few chars
736
+ const shrinkable = currentPathVW - minPathVW;
737
+ if (shrinkable <= 0 || shrinkBy <= 0) return null;
738
+ const targetShrink = Math.min(shrinkable, shrinkBy);
739
+ const currentMaxLen = ctx.options.path?.maxLength ?? 40;
740
+ let newMaxLen = Math.max(4, Math.min(currentMaxLen, currentPathVW) - targetShrink);
741
+ const pathCtx = (maxLen: number): SegmentContext => ({
742
+ ...ctx,
743
+ options: { ...ctx.options, path: { ...ctx.options.path, maxLength: maxLen } },
744
+ });
745
+ let reRendered = renderSegment("path", pathCtx(newMaxLen));
746
+ if (!reRendered.visible || !reRendered.content) return null;
747
+ // maxLength governs path text, not icon prefix; iterate to compensate.
748
+ for (let i = 0; i < 8; i++) {
749
+ const saved = currentPathVW - visibleWidth(reRendered.content);
750
+ if (saved >= targetShrink) break;
751
+ const nextMaxLen = Math.max(4, newMaxLen - (targetShrink - saved));
752
+ if (nextMaxLen >= newMaxLen) break; // no progress or hit floor
753
+ newMaxLen = nextMaxLen;
754
+ const adjusted = renderSegment("path", pathCtx(newMaxLen));
755
+ if (!adjusted.visible || !adjusted.content) break;
756
+ reRendered = adjusted;
757
+ }
758
+ return reRendered.content;
759
+ }
760
+
761
+ #collectStatusSegments(
762
+ width: number,
763
+ effectiveSettings: Required<
764
+ Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
765
+ > &
766
+ StatusLineSettings,
767
+ ): CollectedStatusSegments {
768
+ const ctx = this.#buildSegmentContext(width, effectiveSettings);
769
+ const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
770
+
771
+ // Use the subtle surface tone (the same elevated background as user-message
772
+ // bubbles) instead of the heavy `statusLineBg` block, so the rail layers
773
+ // just above the base background as a quiet zone rather than a solid bar.
774
+ // Resolving through a semantic slot keeps it correct across every theme.
775
+ const bgAnsi = theme.getBgAnsi("userMessageBg");
776
+ const fgAnsi = theme.getFgAnsi("text");
777
+ const sepAnsi = theme.getFgAnsi("statusLineSep");
778
+
779
+ const previewHighlightSegment = effectiveSettings.previewHighlightSegment;
780
+ const highlightSegment = (segId: StatusLineSegmentId, content: string): string =>
781
+ previewHighlightSegment === segId ? `\x1b[7m${content}\x1b[27m` : content;
782
+
783
+ const left: string[] = [];
784
+ const leftSegIds: StatusLineSegmentId[] = [];
785
+ for (const segId of effectiveSettings.leftSegments) {
786
+ const rendered = renderSegment(segId, ctx);
787
+ if (rendered.visible && rendered.content) {
788
+ left.push(highlightSegment(segId, rendered.content));
789
+ leftSegIds.push(segId);
790
+ }
791
+ }
792
+
793
+ const right: string[] = [];
794
+ const actionHints = getAvailableActionHints(this.#actionRegistry, this.#getKeybindings, width, this.#focusDomain);
795
+ for (const segId of effectiveSettings.rightSegments) {
796
+ const rendered = renderSegment(segId, ctx);
797
+ if (rendered.visible && rendered.content) {
798
+ right.push(highlightSegment(segId, rendered.content));
799
+ }
800
+ }
801
+ right.push(...actionHints.map(hint => hint.content));
802
+
803
+ const runningBackgroundJobs =
804
+ this.session.getAsyncJobSnapshot()?.running.filter(job => job.metadata?.monitor !== true).length ?? 0;
805
+ if (runningBackgroundJobs > 0) {
806
+ const icon = theme.icon.agents ? `${theme.icon.agents} ` : "";
807
+ const label = `${formatCount("job", runningBackgroundJobs)} running`;
808
+ right.push(theme.fg("statusLineSubagents", `${icon}${label}`));
809
+ }
810
+ if (this.#version) {
811
+ right.push(theme.fg("dim", `v${this.#version}`));
812
+ }
813
+
814
+ return {
815
+ ctx,
816
+ separatorDef,
817
+ bgAnsi,
818
+ fgAnsi,
819
+ sepAnsi,
820
+ left,
821
+ leftSegIds,
822
+ right,
823
+ previewHighlightSegment,
824
+ sessionAccent: effectiveSettings.sessionAccent,
825
+ leftSepWidth: visibleWidth(separatorDef.left),
826
+ rightSepWidth: visibleWidth(separatorDef.right),
827
+ leftCapWidth: separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.right) : 0,
828
+ rightCapWidth: separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.left) : 0,
829
+ };
830
+ }
831
+
832
+ #resolveMaxRows(): number {
833
+ const raw = this.#settings.maxRows ?? 1;
834
+ if (!Number.isFinite(raw)) return 1;
835
+ return Math.max(1, Math.min(3, Math.trunc(raw)));
836
+ }
837
+
838
+ #buildStatusLine(width: number, precollected?: CollectedStatusSegments): string {
839
+ const seg = precollected ?? this.#collectStatusSegments(width, this.#resolveSettings());
840
+ const { ctx, separatorDef, bgAnsi, fgAnsi, sepAnsi, previewHighlightSegment } = seg;
841
+ const { leftSepWidth, rightSepWidth, leftCapWidth, rightCapWidth } = seg;
842
+ const left = [...seg.left];
843
+ const leftIds = [...seg.leftSegIds];
844
+ const right = [...seg.right];
845
+ const topFillWidth = Math.max(0, width);
846
+
847
+ let leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
848
+ let rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
849
+ const totalWidth = () => leftWidth + rightWidth + (left.length > 0 && right.length > 0 ? 1 : 0);
850
+
851
+ if (topFillWidth > 0) {
852
+ // Shrink path before dropping right-side telemetry — path is the only elastic segment,
853
+ // and presets such as default-usage should not hide usage just because cwd is long.
854
+ const pathIdx = leftIds.indexOf("path");
855
+ if (pathIdx >= 0 && totalWidth() > topFillWidth) {
856
+ const overflow = totalWidth() - topFillWidth;
857
+ const shrunk = this.#shrinkPathToWidth(left[pathIdx], ctx, overflow);
858
+ if (shrunk !== null) {
859
+ left[pathIdx] = previewHighlightSegment === "path" ? `\x1b[7m${shrunk}\x1b[27m` : shrunk;
860
+ leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
861
+ }
862
+ }
863
+ while (totalWidth() > topFillWidth && right.length > 0) {
864
+ right.pop();
865
+ rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
866
+ }
867
+ while (totalWidth() > topFillWidth && left.length > 0) {
868
+ left.pop();
869
+ leftIds.pop();
870
+ leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
871
+ }
872
+ }
873
+
874
+ const leftGroup = this.#renderStatusGroup(left, "left", separatorDef, bgAnsi, fgAnsi, sepAnsi);
875
+ const rightGroup = this.#renderStatusGroup(right, "right", separatorDef, bgAnsi, fgAnsi, sepAnsi);
876
+ if (!leftGroup && !rightGroup) return "";
877
+
878
+ if (topFillWidth === 0 || left.length === 0 || right.length === 0) {
879
+ return leftGroup + (leftGroup && rightGroup ? " " : "") + rightGroup;
880
+ }
881
+
882
+ leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
883
+ rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
884
+ const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
885
+ const sessionName = seg.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined;
886
+ const accentHex = sessionName ? getSessionAccentHex(sessionName) : undefined;
887
+ const gapColor = getSessionAccentAnsi(accentHex) ?? theme.getFgAnsi("border");
888
+ const gapFill = `${gapColor}${theme.boxRound.horizontal.repeat(gapWidth)}\x1b[39m`;
889
+ return leftGroup + gapFill + rightGroup;
890
+ }
891
+
892
+ /**
893
+ * Multi-row status line. When `maxRows > 1` and the single-line layout would
894
+ * overflow, segments wrap onto additional left-justified rows instead of
895
+ * being dropped. Falls back to the polished justified single row whenever
896
+ * everything fits on one line.
897
+ */
898
+ #buildStatusRows(width: number, maxRows: number): string[] {
899
+ const effectiveSettings = this.#resolveSettings();
900
+ const seg = this.#collectStatusSegments(width, effectiveSettings);
901
+ const cacheKey = JSON.stringify({
902
+ width,
903
+ maxRows,
904
+ settings: this.#resolvedSettingsFingerprint,
905
+ left: seg.left,
906
+ leftSegIds: seg.leftSegIds,
907
+ right: seg.right,
908
+ separator: effectiveSettings.separator,
909
+ previewHighlightSegment: seg.previewHighlightSegment,
910
+ sessionAccent: seg.sessionAccent,
911
+ theme: [seg.bgAnsi, seg.fgAnsi, seg.sepAnsi],
912
+ rowLayout: {
913
+ separatorLeft: seg.separatorDef.left,
914
+ separatorRight: seg.separatorDef.right,
915
+ separatorEndCapLeft: seg.separatorDef.endCaps?.left,
916
+ separatorEndCapRight: seg.separatorDef.endCaps?.right,
917
+ separatorEndCapUseBgAsFg: seg.separatorDef.endCaps?.useBgAsFg,
918
+ borderFgAnsi: theme.getFgAnsi("border"),
919
+ boxRoundHorizontal: theme.boxRound.horizontal,
920
+ },
921
+ context: [seg.ctx.contextPercent, seg.ctx.contextWindow],
922
+ usageStats: seg.ctx.usageStats,
923
+ usage: seg.ctx.usage,
924
+ git: seg.ctx.git,
925
+ modes: [seg.ctx.planMode, seg.ctx.goalMode, seg.ctx.autoCompactEnabled],
926
+ runtime: [seg.ctx.subagentCount, seg.ctx.jobs, seg.ctx.sessionStartTime],
927
+ sessionName: seg.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined,
928
+ asyncJobs: this.session
929
+ .getAsyncJobSnapshot()
930
+ ?.running.filter(job => job.metadata?.monitor !== true)
931
+ .map(job => job.id ?? job.metadata ?? job)
932
+ .join(","),
933
+ version: this.#version,
934
+ });
935
+ if (this.#renderedRowsCache?.key === cacheKey) {
936
+ this.#renderedRowsCacheHits++;
937
+ return [...this.#renderedRowsCache.rows];
938
+ }
939
+ this.#renderedRowsCacheMisses++;
940
+
941
+ if (seg.left.length === 0 && seg.right.length === 0) {
942
+ this.#renderedRowsCache = { key: cacheKey, rows: [] };
943
+ return [];
944
+ }
945
+
946
+ const topFillWidth = Math.max(1, width);
947
+ const leftWidth = this.#groupWidth(seg.left, seg.leftCapWidth, seg.leftSepWidth);
948
+ const rightWidth = this.#groupWidth(seg.right, seg.rightCapWidth, seg.rightSepWidth);
949
+ const gap = seg.left.length > 0 && seg.right.length > 0 ? 1 : 0;
950
+ const fitsSingleRow = leftWidth + rightWidth + gap <= topFillWidth;
951
+
952
+ let rows: string[];
953
+ if (maxRows <= 1 || fitsSingleRow) {
954
+ const single = this.#buildStatusLine(width, seg);
955
+ rows = single ? [single] : [];
956
+ } else {
957
+ const items: { content: string; isPath: boolean }[] = [
958
+ ...seg.left.map((content, i) => ({ content, isPath: seg.leftSegIds[i] === "path" })),
959
+ ...seg.right.map(content => ({ content, isPath: false })),
960
+ ];
961
+
962
+ for (const item of items) {
963
+ if (!item.isPath) continue;
964
+ const alone = this.#groupWidth([item.content], seg.leftCapWidth, seg.leftSepWidth);
965
+ if (alone > topFillWidth) {
966
+ const shrunk = this.#shrinkPathToWidth(item.content, seg.ctx, alone - topFillWidth);
967
+ if (shrunk !== null) item.content = shrunk;
968
+ }
969
+ }
970
+
971
+ const packedRows: string[][] = [];
972
+ let current: string[] = [];
973
+ for (const item of items) {
974
+ if (current.length === 0) {
975
+ current.push(item.content);
976
+ continue;
977
+ }
978
+ const tentative = [...current, item.content];
979
+ if (this.#groupWidth(tentative, seg.leftCapWidth, seg.leftSepWidth) <= topFillWidth) {
980
+ current = tentative;
981
+ } else {
982
+ packedRows.push(current);
983
+ current = [item.content];
984
+ if (packedRows.length >= maxRows) break;
985
+ }
986
+ }
987
+ if (packedRows.length < maxRows && current.length > 0) {
988
+ packedRows.push(current);
989
+ }
990
+
991
+ rows = packedRows.map(row =>
992
+ this.#renderStatusGroup(row, "left", seg.separatorDef, seg.bgAnsi, seg.fgAnsi, seg.sepAnsi),
993
+ );
994
+ }
995
+
996
+ this.#renderedRowsCache = { key: cacheKey, rows };
997
+ return [...rows];
998
+ }
999
+
1000
+ getTopBorder(width: number): { content: string; width: number } {
1001
+ const content = this.#buildStatusLine(width);
1002
+ return {
1003
+ content,
1004
+ width: visibleWidth(content),
1005
+ };
1006
+ }
1007
+
1008
+ /**
1009
+ * Multi-row-aware content for the settings preview: the wrapped rows joined
1010
+ * with newlines so a single `Text` renders them stacked. Honors the current
1011
+ * `maxRows`; identical to the single status line when `maxRows` is 1 or when
1012
+ * everything fits on one row.
1013
+ */
1014
+ getPreviewContent(width: number): string {
1015
+ return this.#buildStatusRows(width, this.#resolveMaxRows()).join("\n");
1016
+ }
1017
+
1018
+ getCacheStatsForTest(): { rowHits: number; rowMisses: number } {
1019
+ return { rowHits: this.#renderedRowsCacheHits, rowMisses: this.#renderedRowsCacheMisses };
1020
+ }
1021
+
1022
+ invalidateBranchForTest(): void {
1023
+ this.#invalidateGitCaches();
1024
+ }
1025
+
1026
+ setCachedPrForTest(pr: { number: number; url: string } | null): void {
1027
+ const branch = this.#getCurrentBranch();
1028
+ this.#cachedPr = pr;
1029
+ this.#cachedPrContext = branch ? createPrCacheContext(branch, this.#cachedBranchRepoId ?? null) : undefined;
1030
+ }
1031
+
1032
+ render(width: number): string[] {
1033
+ const lines: string[] = [];
1034
+ this.#refreshSkillHudInBackground();
1035
+ const skillHud = this.#settings.showSkillHud === false ? null : renderSkillHudBar(this.#skillHudEntries, width);
1036
+ if (skillHud) {
1037
+ lines.push(skillHud);
1038
+ }
1039
+
1040
+ const statusRows = this.#buildStatusRows(width, this.#resolveMaxRows());
1041
+ for (const statusRow of statusRows) {
1042
+ if (statusRow) lines.push(truncateToWidth(statusRow, width));
1043
+ }
1044
+
1045
+ const showHooks = this.#settings.showHookStatus ?? true;
1046
+ if (showHooks && this.#hookStatuses.size > 0) {
1047
+ const sortedStatuses = Array.from(this.#hookStatuses.entries())
1048
+ .sort(([a], [b]) => a.localeCompare(b))
1049
+ .map(([, text]) => sanitizeStatusText(text));
1050
+ const hookLine = sortedStatuses.join(" ");
1051
+ lines.push(truncateToWidth(hookLine, width));
1052
+ }
1053
+
1054
+ return lines;
1055
+ }
1056
+ }