open-multi-agent-kit 0.96.2 → 0.98.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (368) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +43 -9
  3. package/dist/cli/args.d.ts +2 -3
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +13 -189
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/help.d.ts +3 -0
  8. package/dist/cli/help.d.ts.map +1 -0
  9. package/dist/cli/help.js +194 -0
  10. package/dist/cli/help.js.map +1 -0
  11. package/dist/cli/initial-message.d.ts.map +1 -1
  12. package/dist/cli/initial-message.js +2 -1
  13. package/dist/cli/initial-message.js.map +1 -1
  14. package/dist/cli/resource-paths.d.ts +4 -0
  15. package/dist/cli/resource-paths.d.ts.map +1 -0
  16. package/dist/cli/resource-paths.js +59 -0
  17. package/dist/cli/resource-paths.js.map +1 -0
  18. package/dist/commands/adaptorch-doctor-cli.d.ts +13 -0
  19. package/dist/commands/adaptorch-doctor-cli.d.ts.map +1 -0
  20. package/dist/commands/adaptorch-doctor-cli.js +115 -0
  21. package/dist/commands/adaptorch-doctor-cli.js.map +1 -0
  22. package/dist/commands/resource-doctor-cli.d.ts +4 -0
  23. package/dist/commands/resource-doctor-cli.d.ts.map +1 -1
  24. package/dist/commands/resource-doctor-cli.js +35 -5
  25. package/dist/commands/resource-doctor-cli.js.map +1 -1
  26. package/dist/core/adaptorch-bridge.d.ts +11 -13
  27. package/dist/core/adaptorch-bridge.d.ts.map +1 -1
  28. package/dist/core/adaptorch-bridge.js +11 -13
  29. package/dist/core/adaptorch-bridge.js.map +1 -1
  30. package/dist/core/agent-session-snapshot.d.ts +16 -0
  31. package/dist/core/agent-session-snapshot.d.ts.map +1 -0
  32. package/dist/core/agent-session-snapshot.js +111 -0
  33. package/dist/core/agent-session-snapshot.js.map +1 -0
  34. package/dist/core/agent-session.d.ts +14 -10
  35. package/dist/core/agent-session.d.ts.map +1 -1
  36. package/dist/core/agent-session.js +102 -217
  37. package/dist/core/agent-session.js.map +1 -1
  38. package/dist/core/agent-tool-settings.d.ts +10 -0
  39. package/dist/core/agent-tool-settings.d.ts.map +1 -1
  40. package/dist/core/agent-tool-settings.js +10 -1
  41. package/dist/core/agent-tool-settings.js.map +1 -1
  42. package/dist/core/attachment-store.d.ts +39 -0
  43. package/dist/core/attachment-store.d.ts.map +1 -0
  44. package/dist/core/attachment-store.js +81 -0
  45. package/dist/core/attachment-store.js.map +1 -0
  46. package/dist/core/command-safety.d.ts +8 -0
  47. package/dist/core/command-safety.d.ts.map +1 -1
  48. package/dist/core/command-safety.js +140 -14
  49. package/dist/core/command-safety.js.map +1 -1
  50. package/dist/core/compaction/compaction.d.ts +13 -1
  51. package/dist/core/compaction/compaction.d.ts.map +1 -1
  52. package/dist/core/compaction/compaction.js +60 -62
  53. package/dist/core/compaction/compaction.js.map +1 -1
  54. package/dist/core/compaction/index.d.ts +1 -0
  55. package/dist/core/compaction/index.d.ts.map +1 -1
  56. package/dist/core/compaction/index.js +1 -0
  57. package/dist/core/compaction/index.js.map +1 -1
  58. package/dist/core/compaction/knowledge-triage-format.d.ts +9 -0
  59. package/dist/core/compaction/knowledge-triage-format.d.ts.map +1 -0
  60. package/dist/core/compaction/knowledge-triage-format.js +71 -0
  61. package/dist/core/compaction/knowledge-triage-format.js.map +1 -0
  62. package/dist/core/compaction/knowledge-triage-types.d.ts +7 -0
  63. package/dist/core/compaction/knowledge-triage-types.d.ts.map +1 -0
  64. package/dist/core/compaction/knowledge-triage-types.js +2 -0
  65. package/dist/core/compaction/knowledge-triage-types.js.map +1 -0
  66. package/dist/core/compaction/knowledge-triage.d.ts +29 -0
  67. package/dist/core/compaction/knowledge-triage.d.ts.map +1 -0
  68. package/dist/core/compaction/knowledge-triage.js +189 -0
  69. package/dist/core/compaction/knowledge-triage.js.map +1 -0
  70. package/dist/core/compaction/transaction.d.ts.map +1 -1
  71. package/dist/core/compaction/transaction.js +1 -1
  72. package/dist/core/compaction/transaction.js.map +1 -1
  73. package/dist/core/compaction/utils.d.ts +4 -0
  74. package/dist/core/compaction/utils.d.ts.map +1 -1
  75. package/dist/core/compaction/utils.js +37 -0
  76. package/dist/core/compaction/utils.js.map +1 -1
  77. package/dist/core/completion-sound-io.d.ts +14 -0
  78. package/dist/core/completion-sound-io.d.ts.map +1 -0
  79. package/dist/core/completion-sound-io.js +64 -0
  80. package/dist/core/completion-sound-io.js.map +1 -0
  81. package/dist/core/completion-sound.d.ts +4 -10
  82. package/dist/core/completion-sound.d.ts.map +1 -1
  83. package/dist/core/completion-sound.js +28 -58
  84. package/dist/core/completion-sound.js.map +1 -1
  85. package/dist/core/context-budget-governor-v2.d.ts +1 -1
  86. package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
  87. package/dist/core/context-budget-governor-v2.js +1 -1
  88. package/dist/core/context-budget-governor-v2.js.map +1 -1
  89. package/dist/core/context-budget-relevance.d.ts +15 -2
  90. package/dist/core/context-budget-relevance.d.ts.map +1 -1
  91. package/dist/core/context-budget-relevance.js +21 -9
  92. package/dist/core/context-budget-relevance.js.map +1 -1
  93. package/dist/core/context-budget-system-prompt-items.d.ts +1 -1
  94. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  95. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  96. package/dist/core/context-budget-system-prompt.d.ts +1 -1
  97. package/dist/core/context-budget-system-prompt.d.ts.map +1 -1
  98. package/dist/core/context-budget-system-prompt.js.map +1 -1
  99. package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
  100. package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
  101. package/dist/core/context-budget-v2-cache-keys.js +2 -0
  102. package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
  103. package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
  104. package/dist/core/context-budget-v2-plan-cache-keys.js +3 -0
  105. package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
  106. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  107. package/dist/core/context-budget-v2-planner.js +2 -1
  108. package/dist/core/context-budget-v2-planner.js.map +1 -1
  109. package/dist/core/context-budget-v2-scoring.d.ts +17 -0
  110. package/dist/core/context-budget-v2-scoring.d.ts.map +1 -1
  111. package/dist/core/context-budget-v2-scoring.js +25 -8
  112. package/dist/core/context-budget-v2-scoring.js.map +1 -1
  113. package/dist/core/context-budget-v2-selection.d.ts +2 -2
  114. package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
  115. package/dist/core/context-budget-v2-selection.js +18 -1
  116. package/dist/core/context-budget-v2-selection.js.map +1 -1
  117. package/dist/core/context-budget-v2-types.d.ts +17 -0
  118. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  119. package/dist/core/context-budget-v2-types.js +15 -0
  120. package/dist/core/context-budget-v2-types.js.map +1 -1
  121. package/dist/core/context-file.d.ts +23 -0
  122. package/dist/core/context-file.d.ts.map +1 -0
  123. package/dist/core/context-file.js +17 -0
  124. package/dist/core/context-file.js.map +1 -0
  125. package/dist/core/extensions/builtin/command-safety-gate.d.ts +3 -1
  126. package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
  127. package/dist/core/extensions/builtin/command-safety-gate.js +7 -0
  128. package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
  129. package/dist/core/extensions/types.d.ts +12 -1
  130. package/dist/core/extensions/types.d.ts.map +1 -1
  131. package/dist/core/extensions/types.js.map +1 -1
  132. package/dist/core/keybindings.d.ts +1 -1
  133. package/dist/core/keybindings.d.ts.map +1 -1
  134. package/dist/core/keybindings.js +6 -1
  135. package/dist/core/keybindings.js.map +1 -1
  136. package/dist/core/model-prompt-policy.d.ts +1 -1
  137. package/dist/core/model-prompt-policy.d.ts.map +1 -1
  138. package/dist/core/model-prompt-policy.js.map +1 -1
  139. package/dist/core/model-registry.d.ts +8 -3
  140. package/dist/core/model-registry.d.ts.map +1 -1
  141. package/dist/core/model-registry.js +71 -1
  142. package/dist/core/model-registry.js.map +1 -1
  143. package/dist/core/package-doctor-source-scan.d.ts +9 -0
  144. package/dist/core/package-doctor-source-scan.d.ts.map +1 -1
  145. package/dist/core/package-doctor-source-scan.js +77 -4
  146. package/dist/core/package-doctor-source-scan.js.map +1 -1
  147. package/dist/core/prompt-attachment.d.ts +51 -0
  148. package/dist/core/prompt-attachment.d.ts.map +1 -0
  149. package/dist/core/prompt-attachment.js +186 -0
  150. package/dist/core/prompt-attachment.js.map +1 -0
  151. package/dist/core/prompt-settlement.d.ts +6 -3
  152. package/dist/core/prompt-settlement.d.ts.map +1 -1
  153. package/dist/core/prompt-settlement.js +10 -14
  154. package/dist/core/prompt-settlement.js.map +1 -1
  155. package/dist/core/provider-resilience.d.ts +28 -0
  156. package/dist/core/provider-resilience.d.ts.map +1 -1
  157. package/dist/core/provider-resilience.js +50 -0
  158. package/dist/core/provider-resilience.js.map +1 -1
  159. package/dist/core/provider-retry.d.ts +1 -1
  160. package/dist/core/provider-retry.d.ts.map +1 -1
  161. package/dist/core/provider-retry.js +1 -1
  162. package/dist/core/provider-retry.js.map +1 -1
  163. package/dist/core/provider-usage.d.ts.map +1 -1
  164. package/dist/core/provider-usage.js +68 -2
  165. package/dist/core/provider-usage.js.map +1 -1
  166. package/dist/core/reasoning-router-promotion.d.ts +124 -0
  167. package/dist/core/reasoning-router-promotion.d.ts.map +1 -0
  168. package/dist/core/reasoning-router-promotion.js +121 -0
  169. package/dist/core/reasoning-router-promotion.js.map +1 -0
  170. package/dist/core/reasoning-router-replay-stability.d.ts +65 -0
  171. package/dist/core/reasoning-router-replay-stability.d.ts.map +1 -0
  172. package/dist/core/reasoning-router-replay-stability.js +67 -0
  173. package/dist/core/reasoning-router-replay-stability.js.map +1 -0
  174. package/dist/core/reasoning-router-v4-weights.d.ts +6 -6
  175. package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
  176. package/dist/core/reasoning-router-v4-weights.js +3 -3
  177. package/dist/core/reasoning-router-v4-weights.js.map +1 -1
  178. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  179. package/dist/core/reasoning-router-v4.js +6 -6
  180. package/dist/core/reasoning-router-v4.js.map +1 -1
  181. package/dist/core/redaction.d.ts.map +1 -1
  182. package/dist/core/redaction.js +1 -1
  183. package/dist/core/redaction.js.map +1 -1
  184. package/dist/core/resource-admission.d.ts +2 -1
  185. package/dist/core/resource-admission.d.ts.map +1 -1
  186. package/dist/core/resource-admission.js +11 -0
  187. package/dist/core/resource-admission.js.map +1 -1
  188. package/dist/core/resource-governor-format.d.ts +1 -1
  189. package/dist/core/resource-governor-format.d.ts.map +1 -1
  190. package/dist/core/resource-governor-format.js.map +1 -1
  191. package/dist/core/resource-governor-settings.d.ts +6 -12
  192. package/dist/core/resource-governor-settings.d.ts.map +1 -1
  193. package/dist/core/resource-governor-settings.js.map +1 -1
  194. package/dist/core/resource-governor-types.d.ts +24 -0
  195. package/dist/core/resource-governor-types.d.ts.map +1 -0
  196. package/dist/core/resource-governor-types.js +15 -0
  197. package/dist/core/resource-governor-types.js.map +1 -0
  198. package/dist/core/resource-loader.d.ts +1 -7
  199. package/dist/core/resource-loader.d.ts.map +1 -1
  200. package/dist/core/resource-loader.js.map +1 -1
  201. package/dist/core/resource-observation-journal.d.ts +2 -1
  202. package/dist/core/resource-observation-journal.d.ts.map +1 -1
  203. package/dist/core/resource-observation-journal.js +4 -3
  204. package/dist/core/resource-observation-journal.js.map +1 -1
  205. package/dist/core/resource-observation-report-reader.d.ts +7 -0
  206. package/dist/core/resource-observation-report-reader.d.ts.map +1 -0
  207. package/dist/core/resource-observation-report-reader.js +157 -0
  208. package/dist/core/resource-observation-report-reader.js.map +1 -0
  209. package/dist/core/resource-observation-report.d.ts +24 -0
  210. package/dist/core/resource-observation-report.d.ts.map +1 -0
  211. package/dist/core/resource-observation-report.js +101 -0
  212. package/dist/core/resource-observation-report.js.map +1 -0
  213. package/dist/core/router-feedback-collector.d.ts +1 -1
  214. package/dist/core/router-feedback-collector.d.ts.map +1 -1
  215. package/dist/core/router-feedback-collector.js.map +1 -1
  216. package/dist/core/session-bash-service.d.ts.map +1 -1
  217. package/dist/core/session-bash-service.js +4 -3
  218. package/dist/core/session-bash-service.js.map +1 -1
  219. package/dist/core/session-failure-cause.d.ts +33 -0
  220. package/dist/core/session-failure-cause.d.ts.map +1 -0
  221. package/dist/core/session-failure-cause.js +141 -0
  222. package/dist/core/session-failure-cause.js.map +1 -0
  223. package/dist/core/session-system-prompt.d.ts +1 -1
  224. package/dist/core/session-system-prompt.d.ts.map +1 -1
  225. package/dist/core/session-system-prompt.js.map +1 -1
  226. package/dist/core/session-termination.d.ts +1 -1
  227. package/dist/core/session-termination.d.ts.map +1 -1
  228. package/dist/core/session-termination.js +11 -2
  229. package/dist/core/session-termination.js.map +1 -1
  230. package/dist/core/settings-manager.d.ts +1 -1
  231. package/dist/core/settings-manager.d.ts.map +1 -1
  232. package/dist/core/settings-manager.js.map +1 -1
  233. package/dist/core/system-prompt.d.ts +1 -1
  234. package/dist/core/system-prompt.d.ts.map +1 -1
  235. package/dist/core/system-prompt.js.map +1 -1
  236. package/dist/core/tools/bash.d.ts.map +1 -1
  237. package/dist/core/tools/bash.js +10 -14
  238. package/dist/core/tools/bash.js.map +1 -1
  239. package/dist/core/tools/grep.d.ts.map +1 -1
  240. package/dist/core/tools/grep.js +13 -48
  241. package/dist/core/tools/grep.js.map +1 -1
  242. package/dist/core/tools/read.d.ts.map +1 -1
  243. package/dist/core/tools/read.js +1 -34
  244. package/dist/core/tools/read.js.map +1 -1
  245. package/dist/core/workload-classifier.d.ts +2 -10
  246. package/dist/core/workload-classifier.d.ts.map +1 -1
  247. package/dist/core/workload-classifier.js +17 -0
  248. package/dist/core/workload-classifier.js.map +1 -1
  249. package/dist/core/workload-family-matcher.d.ts +1 -1
  250. package/dist/core/workload-family-matcher.d.ts.map +1 -1
  251. package/dist/core/workload-family-matcher.js.map +1 -1
  252. package/dist/core/workload-shard-execution-types.d.ts +40 -0
  253. package/dist/core/workload-shard-execution-types.d.ts.map +1 -0
  254. package/dist/core/workload-shard-execution-types.js +14 -0
  255. package/dist/core/workload-shard-execution-types.js.map +1 -0
  256. package/dist/core/workload-shard-executor.d.ts +3 -27
  257. package/dist/core/workload-shard-executor.d.ts.map +1 -1
  258. package/dist/core/workload-shard-executor.js.map +1 -1
  259. package/dist/core/workload-shard-runner.d.ts +1 -1
  260. package/dist/core/workload-shard-runner.d.ts.map +1 -1
  261. package/dist/core/workload-shard-runner.js.map +1 -1
  262. package/dist/core/workload-types.d.ts +24 -0
  263. package/dist/core/workload-types.d.ts.map +1 -0
  264. package/dist/core/workload-types.js +15 -0
  265. package/dist/core/workload-types.js.map +1 -0
  266. package/dist/index.d.ts +1 -1
  267. package/dist/index.d.ts.map +1 -1
  268. package/dist/index.js +1 -1
  269. package/dist/index.js.map +1 -1
  270. package/dist/main.d.ts +2 -0
  271. package/dist/main.d.ts.map +1 -1
  272. package/dist/main.js +27 -47
  273. package/dist/main.js.map +1 -1
  274. package/dist/modes/interactive/components/attachment-strip.d.ts +21 -0
  275. package/dist/modes/interactive/components/attachment-strip.d.ts.map +1 -0
  276. package/dist/modes/interactive/components/attachment-strip.js +136 -0
  277. package/dist/modes/interactive/components/attachment-strip.js.map +1 -0
  278. package/dist/modes/interactive/components/footer.d.ts +6 -0
  279. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  280. package/dist/modes/interactive/components/footer.js +26 -1
  281. package/dist/modes/interactive/components/footer.js.map +1 -1
  282. package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
  283. package/dist/modes/interactive/components/status-sidebar.js +12 -1
  284. package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
  285. package/dist/modes/interactive/interactive-mode.d.ts +23 -4
  286. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  287. package/dist/modes/interactive/interactive-mode.js +174 -53
  288. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  289. package/dist/modes/interactive/theme/omk-control-panel.json +1 -1
  290. package/dist/utils/clipboard-image.d.ts.map +1 -1
  291. package/dist/utils/clipboard-image.js +42 -33
  292. package/dist/utils/clipboard-image.js.map +1 -1
  293. package/dist/utils/clipboard.d.ts.map +1 -1
  294. package/dist/utils/clipboard.js +14 -2
  295. package/dist/utils/clipboard.js.map +1 -1
  296. package/docs/adaptorch-preview-spec.md +20 -18
  297. package/docs/adaptorch-preview.md +9 -9
  298. package/docs/compaction.md +25 -4
  299. package/docs/correctness-wall.md +10 -10
  300. package/docs/docs.json +12 -0
  301. package/docs/environment-variables.md +9 -2
  302. package/docs/extensions.md +12 -3
  303. package/docs/index.md +7 -1
  304. package/docs/metrics.md +74 -2
  305. package/docs/provider-resilience.md +11 -0
  306. package/docs/runtime-algorithms.md +359 -0
  307. package/docs/sdk.md +2 -2
  308. package/docs/settings.md +11 -10
  309. package/docs/usage.md +19 -3
  310. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  311. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  312. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  313. package/examples/extensions/gondolin/package-lock.json +2 -2
  314. package/examples/extensions/gondolin/package.json +1 -1
  315. package/examples/extensions/sandbox/package-lock.json +2 -2
  316. package/examples/extensions/sandbox/package.json +1 -1
  317. package/examples/extensions/with-deps/package-lock.json +2 -2
  318. package/examples/extensions/with-deps/package.json +1 -1
  319. package/npm-shrinkwrap.json +18 -18
  320. package/package.json +7 -7
  321. package/dist/core/context-budget-governor.d.ts +0 -67
  322. package/dist/core/context-budget-governor.d.ts.map +0 -1
  323. package/dist/core/context-budget-governor.js +0 -224
  324. package/dist/core/context-budget-governor.js.map +0 -1
  325. package/dist/core/lean-ctx.d.ts +0 -99
  326. package/dist/core/lean-ctx.d.ts.map +0 -1
  327. package/dist/core/lean-ctx.js +0 -225
  328. package/dist/core/lean-ctx.js.map +0 -1
  329. package/dist/core/read-anchors.d.ts +0 -110
  330. package/dist/core/read-anchors.d.ts.map +0 -1
  331. package/dist/core/read-anchors.js +0 -193
  332. package/dist/core/read-anchors.js.map +0 -1
  333. package/dist/core/recovery-checkpoint.d.ts +0 -196
  334. package/dist/core/recovery-checkpoint.d.ts.map +0 -1
  335. package/dist/core/recovery-checkpoint.js +0 -382
  336. package/dist/core/recovery-checkpoint.js.map +0 -1
  337. package/dist/core/sandbox-policy-v2.d.ts +0 -49
  338. package/dist/core/sandbox-policy-v2.d.ts.map +0 -1
  339. package/dist/core/sandbox-policy-v2.js +0 -368
  340. package/dist/core/sandbox-policy-v2.js.map +0 -1
  341. package/dist/core/tools/omp-pure-seams.d.ts +0 -41
  342. package/dist/core/tools/omp-pure-seams.d.ts.map +0 -1
  343. package/dist/core/tools/omp-pure-seams.js +0 -142
  344. package/dist/core/tools/omp-pure-seams.js.map +0 -1
  345. package/dist/core/tools/omp-seam-runtime.d.ts +0 -100
  346. package/dist/core/tools/omp-seam-runtime.d.ts.map +0 -1
  347. package/dist/core/tools/omp-seam-runtime.js +0 -40
  348. package/dist/core/tools/omp-seam-runtime.js.map +0 -1
  349. package/dist/guardrails/audit-logger.d.ts +0 -4
  350. package/dist/guardrails/audit-logger.d.ts.map +0 -1
  351. package/dist/guardrails/audit-logger.js +0 -28
  352. package/dist/guardrails/audit-logger.js.map +0 -1
  353. package/dist/guardrails/freedomd-adapter.d.ts +0 -16
  354. package/dist/guardrails/freedomd-adapter.d.ts.map +0 -1
  355. package/dist/guardrails/freedomd-adapter.js +0 -87
  356. package/dist/guardrails/freedomd-adapter.js.map +0 -1
  357. package/dist/orchestration/lane-grant-auditor.d.ts +0 -4
  358. package/dist/orchestration/lane-grant-auditor.d.ts.map +0 -1
  359. package/dist/orchestration/lane-grant-auditor.js +0 -87
  360. package/dist/orchestration/lane-grant-auditor.js.map +0 -1
  361. package/dist/types/guardrails.d.ts +0 -61
  362. package/dist/types/guardrails.d.ts.map +0 -1
  363. package/dist/types/guardrails.js +0 -9
  364. package/dist/types/guardrails.js.map +0 -1
  365. package/dist/types/lane-grant.d.ts +0 -58
  366. package/dist/types/lane-grant.d.ts.map +0 -1
  367. package/dist/types/lane-grant.js +0 -9
  368. package/dist/types/lane-grant.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-strip.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/attachment-strip.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Terminal preview strip for prompt image attachments.\n *\n * Default renderer is ANSI truecolor half-block (\"▀\" = two vertical pixels per\n * cell) — works everywhere, no protocol negotiation. Decoding goes through\n * Photon when available; failures degrade to a metadata-only tile instead of\n * hiding the paste. Previews are cached per (attachment, width).\n */\n\nimport { truncateToWidth } from \"omk-tui\";\nimport { describePromptImageAttachment, type PromptImageAttachment } from \"../../../core/prompt-attachment.ts\";\nimport { loadPhoton } from \"../../../utils/photon.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\nconst PREVIEW_ROWS = 6;\nconst MAX_TILES = 3;\n\nexport interface AttachmentStripHandle {\n\trender(width: number): string[];\n\tinvalidate(): void;\n}\n\ninterface DecodedImage {\n\twidth: number;\n\theight: number;\n\trgba: Uint8Array;\n}\n\nconst decodeCache = new Map<string, DecodedImage | null>();\n\nasync function decodeToRgba(id: string, bytes: Uint8Array | undefined): Promise<DecodedImage | null> {\n\tconst cached = decodeCache.get(id);\n\tif (cached !== undefined) return cached;\n\tlet decoded: DecodedImage | null = null;\n\tif (bytes && bytes.length > 0) {\n\t\ttry {\n\t\t\tconst photon = await loadPhoton();\n\t\t\tif (photon) {\n\t\t\t\tconst image = photon.PhotonImage.new_from_byteslice(bytes);\n\t\t\t\ttry {\n\t\t\t\t\tconst width = image.get_width();\n\t\t\t\t\tconst height = image.get_height();\n\t\t\t\t\tconst raw =\n\t\t\t\t\t\ttypeof (image as { get_image_data?: unknown }).get_image_data === \"function\"\n\t\t\t\t\t\t\t? (image as unknown as { get_image_data: () => Uint8Array }).get_image_data()\n\t\t\t\t\t\t\t: image.get_bytes();\n\t\t\t\t\tif (width > 0 && height > 0 && raw.length >= width * height * 4) {\n\t\t\t\t\t\tdecoded = { width, height, rgba: new Uint8Array(raw.buffer, raw.byteOffset, raw.length) };\n\t\t\t\t\t}\n\t\t\t\t} finally {\n\t\t\t\t\timage.free();\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\tdecoded = null;\n\t\t}\n\t}\n\tdecodeCache.set(id, decoded);\n\treturn decoded;\n}\n\n/** Render one attachment as truecolor half-block lines inside a thin box. */\nfunction renderTile(index: number, attachment: PromptImageAttachment, tileWidth: number): string[] {\n\tconst muted = (text: string) => theme.fg(\"muted\", text);\n\t/** Hard width cap: a wrapped border row drifts the line-count-based viewport. */\n\tconst fitToTileWidth = (text: string) => truncateToWidth(text, tileWidth);\n\tconst title = ` Image ${index} `;\n\tconst top = fitToTileWidth(muted(`┌${title}${\"─\".repeat(Math.max(0, tileWidth - title.length - 1))}┐`));\n\tconst footerText = describePromptImageAttachment(attachment);\n\tconst footer = fitToTileWidth(\n\t\tmuted(`└ ${footerText}${\"─\".repeat(Math.max(0, tileWidth - footerText.length - 3))}┘`),\n\t);\n\n\tconst inner = tileWidth - 2;\n\tconst lines: string[] = [top];\n\n\tif (inner < 8) {\n\t\tlines.push(fitToTileWidth(muted(`│ ${footerText.slice(0, inner)} │`)), footer);\n\t\treturn lines;\n\t}\n\n\tconst decoded = decodeCache.get(attachment.id);\n\tif (!decoded) {\n\t\t// Metadata-only fallback (decode unavailable or still pending).\n\t\tfor (let row = 0; row < PREVIEW_ROWS; row += 1) {\n\t\t\tconst label = row === Math.floor(PREVIEW_ROWS / 2) ? \"🖼 no preview\" : \"\";\n\t\t\tlines.push(fitToTileWidth(muted(`│${(label + \" \".repeat(inner)).slice(0, inner)}│`)));\n\t\t}\n\t\tlines.push(footer);\n\t\treturn lines;\n\t}\n\n\tconst cellRows = PREVIEW_ROWS * 2;\n\tfor (let row = 0; row < PREVIEW_ROWS; row += 1) {\n\t\tlet line = \"\";\n\t\tfor (let col = 0; col < inner; col += 1) {\n\t\t\tconst sy = Math.floor(((row * 2 + 0.5) / cellRows) * decoded.height);\n\t\t\tconst syBottom = Math.min(decoded.height - 1, Math.floor(((row * 2 + 1.5) / cellRows) * decoded.height));\n\t\t\tconst sx = Math.floor(((col + 0.5) / inner) * decoded.width);\n\t\t\tconst top_ = samplePixelAt(decoded, sx, sy);\n\t\t\tconst bottom = samplePixelAt(decoded, sx, syBottom);\n\t\t\tline += `\\x1b[38;2;${top_[0]};${top_[1]};${top_[2]}m\\x1b[48;2;${bottom[0]};${bottom[1]};${bottom[2]}m▀\\x1b[0m`;\n\t\t}\n\t\tlines.push(fitToTileWidth(muted(\"│\") + line + muted(\"│\")));\n\t}\n\tlines.push(footer);\n\treturn lines;\n}\n\nfunction samplePixelAt(decoded: DecodedImage, x: number, y: number): [number, number, number] {\n\tconst clampedX = Math.min(x, decoded.width - 1);\n\tconst clampedY = Math.min(y, decoded.height - 1);\n\tconst offset = (clampedY * decoded.width + clampedX) * 4;\n\treturn [decoded.rgba[offset], decoded.rgba[offset + 1], decoded.rgba[offset + 2]];\n}\n\n/**\n * Build the strip component. `getBytes` supplies raw bytes for decoding;\n * `onHintAction` is reserved for future interactive removal.\n */\nexport function createAttachmentStrip(\n\tattachments: readonly PromptImageAttachment[],\n\tgetBytes: (id: string) => Uint8Array | undefined,\n\t_onHintAction?: () => void,\n): AttachmentStripHandle {\n\tlet cachedWidth: number | undefined;\n\tlet cachedLines: string[] | undefined;\n\n\tvoid (async () => {\n\t\tfor (const attachment of attachments) {\n\t\t\tawait decodeToRgba(attachment.id, getBytes(attachment.id));\n\t\t}\n\t\tcachedWidth = undefined; // force re-render once decodes settle\n\t})();\n\n\treturn {\n\t\trender(width: number): string[] {\n\t\t\tif (cachedLines && cachedWidth === width) return cachedLines;\n\t\t\tconst lines: string[] = [];\n\t\t\tconst shown = attachments.slice(0, MAX_TILES);\n\t\t\tconst tileWidth = Math.min(44, Math.max(16, width - 6));\n\t\t\tshown.forEach((attachment, index) => {\n\t\t\t\tlines.push(...renderTile(index + 1, attachment, tileWidth));\n\t\t\t});\n\t\t\tconst hidden = attachments.length - shown.length;\n\t\t\tif (hidden > 0) {\n\t\t\t\tlines.push(\n\t\t\t\t\ttruncateToWidth(theme.fg(\"muted\", `+${hidden} more attached (${attachments.length} total)`), width),\n\t\t\t\t);\n\t\t\t}\n\t\t\tcachedWidth = width;\n\t\t\tcachedLines = lines;\n\t\t\treturn lines;\n\t\t},\n\t\tinvalidate(): void {\n\t\t\tcachedWidth = undefined;\n\t\t\tcachedLines = undefined;\n\t\t},\n\t};\n}\n\n/** Test hook: drop memoized decodes. */\nexport function resetAttachmentDecodeCache(): void {\n\tdecodeCache.clear();\n}\n"]}
@@ -12,6 +12,12 @@ export declare function formatTokens(count: number): string;
12
12
  export declare function formatBytes(bytes: number): string;
13
13
  export declare function formatPackageIntake(summary: PiPackageIntakeSummary): string;
14
14
  export declare function formatCwdForFooter(cwd: string, home: string | undefined): string;
15
+ /**
16
+ * Extract the provider endpoint host from a model base URL for footer display.
17
+ * Returns undefined when the URL is missing or unparsable so callers can fall
18
+ * back to provider-only display.
19
+ */
20
+ export declare function formatEndpointForFooter(baseUrl: string | undefined): string | undefined;
15
21
  /**
16
22
  * Footer component that shows pwd, token stats, and context usage.
17
23
  * Computes token/context stats from session, gets git branch and extension statuses from provider.
@@ -1 +1 @@
1
- {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,SAAS,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAejF;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKjD;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAK3E;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAYhF;AAED;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,UAAU,CAA6B;IAE/C,YAAY,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,0BAA0B,EAGxE;IAED,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED,2EAA2E;IAC3E,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE3C;IAED;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAiC3B;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA+K9B;CACD","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"omk-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { PiPackageIntakeSummary } from \"../../../core/pi-package-intake.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts for compact footer display.\n */\nexport function formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/**\n * Format byte counts for compact footer display.\n */\nexport function formatBytes(bytes: number): string {\n\tif (bytes < 1024) return `${bytes}B`;\n\tif (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}K`;\n\tif (bytes < 1024 * 1024 * 1024) return `${Math.round(bytes / (1024 * 1024))}M`;\n\treturn `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;\n}\n\nexport function formatPackageIntake(summary: PiPackageIntakeSummary): string {\n\tconst ready =\n\t\tsummary.acceptedNative + summary.acceptedReference + summary.acceptedMeasurement + summary.acceptedAdvisory;\n\tconst review = summary.deferred + summary.reject;\n\treturn `PKG ${ready}/${summary.total} R${review} B${summary.hardForkBlocked}`;\n}\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~${sep}${relativeToHome}`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\tprivate showSystemMetrics = false;\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\n\tconstructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/** Show system-wide CPU/MEM metrics in the stats line (off by default). */\n\tsetShowSystemMetrics(enabled: boolean): void {\n\t\tthis.showSystemMetrics = enabled;\n\t}\n\n\t/**\n\t * Build a compact system-wide CPU/memory metrics segment for the footer.\n\t * Degrades gracefully to shorter forms (or is omitted entirely) when the\n\t * terminal is very narrow so the rest of the footer can still render.\n\t */\n\tprivate buildMetricsSegment(width: number): string | undefined {\n\t\tif (!this.showSystemMetrics) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst cpu = this.footerData.getSystemCpuPercent();\n\t\tconst memUsed = this.footerData.getSystemMemoryUsedBytes();\n\t\tconst memTotal = this.footerData.getSystemMemoryTotalBytes();\n\t\tif (cpu === null || memUsed === null || memTotal === null || memTotal <= 0) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst memPercent = (memUsed / memTotal) * 100;\n\t\tconst color: \"error\" | \"warning\" | \"dim\" =\n\t\t\tcpu >= 90 || memPercent >= 95 ? \"error\" : cpu >= 70 || memPercent >= 85 ? \"warning\" : \"dim\";\n\n\t\tconst cpuStr = `${Math.round(cpu)}%`;\n\t\tconst memStr = `${Math.round(memPercent)}%`;\n\n\t\tif (width >= 56) {\n\t\t\treturn theme.fg(color, `CPU ${cpuStr} MEM ${memStr} (${formatBytes(memUsed)}/${formatBytes(memTotal)})`);\n\t\t}\n\t\tif (width >= 48) {\n\t\t\treturn theme.fg(color, `CPU ${cpuStr} MEM ${memStr}`);\n\t\t}\n\t\tif (width >= 30) {\n\t\t\treturn theme.fg(color, `${cpuStr} ${memStr}`);\n\t\t}\n\t\tif (width >= 18) {\n\t\t\treturn theme.fg(color, cpuStr);\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line. Each part carries a drop priority so narrow terminals\n\t\t// shed low-value segments first instead of truncating the context stats tail.\n\t\tconst statsParts: Array<{ text: string; priority: number }> = [];\n\t\tif (totalInput) statsParts.push({ text: `↑${formatTokens(totalInput)}`, priority: 3 });\n\t\tif (totalOutput) statsParts.push({ text: `↓${formatTokens(totalOutput)}`, priority: 3 });\n\t\tif (totalCacheRead) statsParts.push({ text: `R${formatTokens(totalCacheRead)}`, priority: 2 });\n\t\tif (totalCacheWrite) statsParts.push({ text: `W${formatTokens(totalCacheWrite)}`, priority: 2 });\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push({ text: costStr, priority: 4 });\n\t\t}\n\n\t\t// Add CPU/memory metrics (opt-in); degrade gracefully when horizontal space is tight.\n\t\tconst metricsSegment = this.buildMetricsSegment(width);\n\t\tif (metricsSegment) {\n\t\t\tstatsParts.push({ text: metricsSegment, priority: 1 });\n\t\t}\n\t\t// Package intake status is internal jargon for sessions without packages - hide when empty.\n\t\tconst packageIntake = this.footerData.getPackageIntakeSummary();\n\t\tif (packageIntake.total > 0) {\n\t\t\tstatsParts.push({ text: formatPackageIntake(packageIntake), priority: 2 });\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\t// Context usage is the most important number in the footer - never drop it.\n\t\tstatsParts.push({ text: contextPercentStr, priority: Number.POSITIVE_INFINITY });\n\n\t\t// Drop the lowest-priority parts until the stats line fits.\n\t\tlet activeParts = [...statsParts];\n\t\tlet statsLeft = activeParts.map((part) => part.text).join(\" \");\n\t\twhile (visibleWidth(statsLeft) > width) {\n\t\t\tconst droppable = activeParts.filter((part) => Number.isFinite(part.priority));\n\t\t\tif (droppable.length === 0) break;\n\t\t\tlet lowest = droppable[0]!;\n\t\t\tfor (const part of droppable) {\n\t\t\t\tif (part.priority < lowest.priority) lowest = part;\n\t\t\t}\n\t\t\tactiveParts = activeParts.filter((part) => part !== lowest);\n\t\t\tstatsLeft = activeParts.map((part) => part.text).join(\" \");\n\t\t}\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// Final fallback: truncate if even the undroppable parts overflow\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
1
+ {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,SAAS,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAejF;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKjD;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAK3E;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAYhF;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAUvF;AAED;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,UAAU,CAA6B;IAE/C,YAAY,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,0BAA0B,EAGxE;IAED,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED,2EAA2E;IAC3E,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE3C;IAED;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAiC3B;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAqL9B;CACD","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"omk-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { PiPackageIntakeSummary } from \"../../../core/pi-package-intake.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts for compact footer display.\n */\nexport function formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/**\n * Format byte counts for compact footer display.\n */\nexport function formatBytes(bytes: number): string {\n\tif (bytes < 1024) return `${bytes}B`;\n\tif (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}K`;\n\tif (bytes < 1024 * 1024 * 1024) return `${Math.round(bytes / (1024 * 1024))}M`;\n\treturn `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;\n}\n\nexport function formatPackageIntake(summary: PiPackageIntakeSummary): string {\n\tconst ready =\n\t\tsummary.acceptedNative + summary.acceptedReference + summary.acceptedMeasurement + summary.acceptedAdvisory;\n\tconst review = summary.deferred + summary.reject;\n\treturn `PKG ${ready}/${summary.total} R${review} B${summary.hardForkBlocked}`;\n}\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~${sep}${relativeToHome}`;\n}\n\n/**\n * Extract the provider endpoint host from a model base URL for footer display.\n * Returns undefined when the URL is missing or unparsable so callers can fall\n * back to provider-only display.\n */\nexport function formatEndpointForFooter(baseUrl: string | undefined): string | undefined {\n\tif (!baseUrl) return undefined;\n\ttry {\n\t\tlet hostname = new URL(baseUrl).hostname;\n\t\t// Aliyun hosts carry a long shared suffix that adds no information in a rail.\n\t\tif (hostname.endsWith(\".aliyuncs.com\")) hostname = hostname.slice(0, -\".aliyuncs.com\".length);\n\t\treturn hostname || undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\tprivate showSystemMetrics = false;\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\n\tconstructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/** Show system-wide CPU/MEM metrics in the stats line (off by default). */\n\tsetShowSystemMetrics(enabled: boolean): void {\n\t\tthis.showSystemMetrics = enabled;\n\t}\n\n\t/**\n\t * Build a compact system-wide CPU/memory metrics segment for the footer.\n\t * Degrades gracefully to shorter forms (or is omitted entirely) when the\n\t * terminal is very narrow so the rest of the footer can still render.\n\t */\n\tprivate buildMetricsSegment(width: number): string | undefined {\n\t\tif (!this.showSystemMetrics) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst cpu = this.footerData.getSystemCpuPercent();\n\t\tconst memUsed = this.footerData.getSystemMemoryUsedBytes();\n\t\tconst memTotal = this.footerData.getSystemMemoryTotalBytes();\n\t\tif (cpu === null || memUsed === null || memTotal === null || memTotal <= 0) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst memPercent = (memUsed / memTotal) * 100;\n\t\tconst color: \"error\" | \"warning\" | \"dim\" =\n\t\t\tcpu >= 90 || memPercent >= 95 ? \"error\" : cpu >= 70 || memPercent >= 85 ? \"warning\" : \"dim\";\n\n\t\tconst cpuStr = `${Math.round(cpu)}%`;\n\t\tconst memStr = `${Math.round(memPercent)}%`;\n\n\t\tif (width >= 56) {\n\t\t\treturn theme.fg(color, `CPU ${cpuStr} MEM ${memStr} (${formatBytes(memUsed)}/${formatBytes(memTotal)})`);\n\t\t}\n\t\tif (width >= 48) {\n\t\t\treturn theme.fg(color, `CPU ${cpuStr} MEM ${memStr}`);\n\t\t}\n\t\tif (width >= 30) {\n\t\t\treturn theme.fg(color, `${cpuStr} ${memStr}`);\n\t\t}\n\t\tif (width >= 18) {\n\t\t\treturn theme.fg(color, cpuStr);\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line. Each part carries a drop priority so narrow terminals\n\t\t// shed low-value segments first instead of truncating the context stats tail.\n\t\tconst statsParts: Array<{ text: string; priority: number }> = [];\n\t\tif (totalInput) statsParts.push({ text: `↑${formatTokens(totalInput)}`, priority: 3 });\n\t\tif (totalOutput) statsParts.push({ text: `↓${formatTokens(totalOutput)}`, priority: 3 });\n\t\tif (totalCacheRead) statsParts.push({ text: `R${formatTokens(totalCacheRead)}`, priority: 2 });\n\t\tif (totalCacheWrite) statsParts.push({ text: `W${formatTokens(totalCacheWrite)}`, priority: 2 });\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push({ text: costStr, priority: 4 });\n\t\t}\n\n\t\t// Add CPU/memory metrics (opt-in); degrade gracefully when horizontal space is tight.\n\t\tconst metricsSegment = this.buildMetricsSegment(width);\n\t\tif (metricsSegment) {\n\t\t\tstatsParts.push({ text: metricsSegment, priority: 1 });\n\t\t}\n\t\t// Package intake status is internal jargon for sessions without packages - hide when empty.\n\t\tconst packageIntake = this.footerData.getPackageIntakeSummary();\n\t\tif (packageIntake.total > 0) {\n\t\t\tstatsParts.push({ text: formatPackageIntake(packageIntake), priority: 2 });\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\t// Context usage is the most important number in the footer - never drop it.\n\t\tstatsParts.push({ text: contextPercentStr, priority: Number.POSITIVE_INFINITY });\n\n\t\t// Drop the lowest-priority parts until the stats line fits.\n\t\tlet activeParts = [...statsParts];\n\t\tlet statsLeft = activeParts.map((part) => part.text).join(\" \");\n\t\twhile (visibleWidth(statsLeft) > width) {\n\t\t\tconst droppable = activeParts.filter((part) => Number.isFinite(part.priority));\n\t\t\tif (droppable.length === 0) break;\n\t\t\tlet lowest = droppable[0]!;\n\t\t\tfor (const part of droppable) {\n\t\t\t\tif (part.priority < lowest.priority) lowest = part;\n\t\t\t}\n\t\t\tactiveParts = activeParts.filter((part) => part !== lowest);\n\t\t\tstatsLeft = activeParts.map((part) => part.text).join(\" \");\n\t\t}\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// Final fallback: truncate if even the undroppable parts overflow\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\tconst endpointHost = formatEndpointForFooter(state.model.baseUrl);\n\t\t\tconst providerLabel = endpointHost ? `${state.model.provider} · ${endpointHost}` : state.model.provider;\n\t\t\trightSide = `(${providerLabel}) ${rightSideWithoutProvider}`;\n\t\t\tif (endpointHost && statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide with endpoint host; fall back to provider-only\n\t\t\t\trightSide = `(${state.model.provider}) ${rightSideWithoutProvider}`;\n\t\t\t}\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
@@ -55,6 +55,25 @@ export function formatCwdForFooter(cwd, home) {
55
55
  return cwd;
56
56
  return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
57
57
  }
58
+ /**
59
+ * Extract the provider endpoint host from a model base URL for footer display.
60
+ * Returns undefined when the URL is missing or unparsable so callers can fall
61
+ * back to provider-only display.
62
+ */
63
+ export function formatEndpointForFooter(baseUrl) {
64
+ if (!baseUrl)
65
+ return undefined;
66
+ try {
67
+ let hostname = new URL(baseUrl).hostname;
68
+ // Aliyun hosts carry a long shared suffix that adds no information in a rail.
69
+ if (hostname.endsWith(".aliyuncs.com"))
70
+ hostname = hostname.slice(0, -".aliyuncs.com".length);
71
+ return hostname || undefined;
72
+ }
73
+ catch {
74
+ return undefined;
75
+ }
76
+ }
58
77
  /**
59
78
  * Footer component that shows pwd, token stats, and context usage.
60
79
  * Computes token/context stats from session, gets git branch and extension statuses from provider.
@@ -239,7 +258,13 @@ export class FooterComponent {
239
258
  // Prepend the provider in parentheses if there are multiple providers and there's enough room
240
259
  let rightSide = rightSideWithoutProvider;
241
260
  if (this.footerData.getAvailableProviderCount() > 1 && state.model) {
242
- rightSide = `(${state.model.provider}) ${rightSideWithoutProvider}`;
261
+ const endpointHost = formatEndpointForFooter(state.model.baseUrl);
262
+ const providerLabel = endpointHost ? `${state.model.provider} · ${endpointHost}` : state.model.provider;
263
+ rightSide = `(${providerLabel}) ${rightSideWithoutProvider}`;
264
+ if (endpointHost && statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {
265
+ // Too wide with endpoint host; fall back to provider-only
266
+ rightSide = `(${state.model.provider}) ${rightSideWithoutProvider}`;
267
+ }
243
268
  if (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {
244
269
  // Too wide, fall back
245
270
  rightSide = rightSideWithoutProvider;
@@ -1 +1 @@
1
- {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"omk-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { PiPackageIntakeSummary } from \"../../../core/pi-package-intake.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts for compact footer display.\n */\nexport function formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/**\n * Format byte counts for compact footer display.\n */\nexport function formatBytes(bytes: number): string {\n\tif (bytes < 1024) return `${bytes}B`;\n\tif (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}K`;\n\tif (bytes < 1024 * 1024 * 1024) return `${Math.round(bytes / (1024 * 1024))}M`;\n\treturn `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;\n}\n\nexport function formatPackageIntake(summary: PiPackageIntakeSummary): string {\n\tconst ready =\n\t\tsummary.acceptedNative + summary.acceptedReference + summary.acceptedMeasurement + summary.acceptedAdvisory;\n\tconst review = summary.deferred + summary.reject;\n\treturn `PKG ${ready}/${summary.total} R${review} B${summary.hardForkBlocked}`;\n}\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~${sep}${relativeToHome}`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\tprivate showSystemMetrics = false;\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\n\tconstructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/** Show system-wide CPU/MEM metrics in the stats line (off by default). */\n\tsetShowSystemMetrics(enabled: boolean): void {\n\t\tthis.showSystemMetrics = enabled;\n\t}\n\n\t/**\n\t * Build a compact system-wide CPU/memory metrics segment for the footer.\n\t * Degrades gracefully to shorter forms (or is omitted entirely) when the\n\t * terminal is very narrow so the rest of the footer can still render.\n\t */\n\tprivate buildMetricsSegment(width: number): string | undefined {\n\t\tif (!this.showSystemMetrics) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst cpu = this.footerData.getSystemCpuPercent();\n\t\tconst memUsed = this.footerData.getSystemMemoryUsedBytes();\n\t\tconst memTotal = this.footerData.getSystemMemoryTotalBytes();\n\t\tif (cpu === null || memUsed === null || memTotal === null || memTotal <= 0) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst memPercent = (memUsed / memTotal) * 100;\n\t\tconst color: \"error\" | \"warning\" | \"dim\" =\n\t\t\tcpu >= 90 || memPercent >= 95 ? \"error\" : cpu >= 70 || memPercent >= 85 ? \"warning\" : \"dim\";\n\n\t\tconst cpuStr = `${Math.round(cpu)}%`;\n\t\tconst memStr = `${Math.round(memPercent)}%`;\n\n\t\tif (width >= 56) {\n\t\t\treturn theme.fg(color, `CPU ${cpuStr} MEM ${memStr} (${formatBytes(memUsed)}/${formatBytes(memTotal)})`);\n\t\t}\n\t\tif (width >= 48) {\n\t\t\treturn theme.fg(color, `CPU ${cpuStr} MEM ${memStr}`);\n\t\t}\n\t\tif (width >= 30) {\n\t\t\treturn theme.fg(color, `${cpuStr} ${memStr}`);\n\t\t}\n\t\tif (width >= 18) {\n\t\t\treturn theme.fg(color, cpuStr);\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line. Each part carries a drop priority so narrow terminals\n\t\t// shed low-value segments first instead of truncating the context stats tail.\n\t\tconst statsParts: Array<{ text: string; priority: number }> = [];\n\t\tif (totalInput) statsParts.push({ text: `↑${formatTokens(totalInput)}`, priority: 3 });\n\t\tif (totalOutput) statsParts.push({ text: `↓${formatTokens(totalOutput)}`, priority: 3 });\n\t\tif (totalCacheRead) statsParts.push({ text: `R${formatTokens(totalCacheRead)}`, priority: 2 });\n\t\tif (totalCacheWrite) statsParts.push({ text: `W${formatTokens(totalCacheWrite)}`, priority: 2 });\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push({ text: costStr, priority: 4 });\n\t\t}\n\n\t\t// Add CPU/memory metrics (opt-in); degrade gracefully when horizontal space is tight.\n\t\tconst metricsSegment = this.buildMetricsSegment(width);\n\t\tif (metricsSegment) {\n\t\t\tstatsParts.push({ text: metricsSegment, priority: 1 });\n\t\t}\n\t\t// Package intake status is internal jargon for sessions without packages - hide when empty.\n\t\tconst packageIntake = this.footerData.getPackageIntakeSummary();\n\t\tif (packageIntake.total > 0) {\n\t\t\tstatsParts.push({ text: formatPackageIntake(packageIntake), priority: 2 });\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\t// Context usage is the most important number in the footer - never drop it.\n\t\tstatsParts.push({ text: contextPercentStr, priority: Number.POSITIVE_INFINITY });\n\n\t\t// Drop the lowest-priority parts until the stats line fits.\n\t\tlet activeParts = [...statsParts];\n\t\tlet statsLeft = activeParts.map((part) => part.text).join(\" \");\n\t\twhile (visibleWidth(statsLeft) > width) {\n\t\t\tconst droppable = activeParts.filter((part) => Number.isFinite(part.priority));\n\t\t\tif (droppable.length === 0) break;\n\t\t\tlet lowest = droppable[0]!;\n\t\t\tfor (const part of droppable) {\n\t\t\t\tif (part.priority < lowest.priority) lowest = part;\n\t\t\t}\n\t\t\tactiveParts = activeParts.filter((part) => part !== lowest);\n\t\t\tstatsLeft = activeParts.map((part) => part.text).join(\" \");\n\t\t}\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// Final fallback: truncate if even the undroppable parts overflow\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
1
+ {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"omk-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { PiPackageIntakeSummary } from \"../../../core/pi-package-intake.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts for compact footer display.\n */\nexport function formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/**\n * Format byte counts for compact footer display.\n */\nexport function formatBytes(bytes: number): string {\n\tif (bytes < 1024) return `${bytes}B`;\n\tif (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}K`;\n\tif (bytes < 1024 * 1024 * 1024) return `${Math.round(bytes / (1024 * 1024))}M`;\n\treturn `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;\n}\n\nexport function formatPackageIntake(summary: PiPackageIntakeSummary): string {\n\tconst ready =\n\t\tsummary.acceptedNative + summary.acceptedReference + summary.acceptedMeasurement + summary.acceptedAdvisory;\n\tconst review = summary.deferred + summary.reject;\n\treturn `PKG ${ready}/${summary.total} R${review} B${summary.hardForkBlocked}`;\n}\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~${sep}${relativeToHome}`;\n}\n\n/**\n * Extract the provider endpoint host from a model base URL for footer display.\n * Returns undefined when the URL is missing or unparsable so callers can fall\n * back to provider-only display.\n */\nexport function formatEndpointForFooter(baseUrl: string | undefined): string | undefined {\n\tif (!baseUrl) return undefined;\n\ttry {\n\t\tlet hostname = new URL(baseUrl).hostname;\n\t\t// Aliyun hosts carry a long shared suffix that adds no information in a rail.\n\t\tif (hostname.endsWith(\".aliyuncs.com\")) hostname = hostname.slice(0, -\".aliyuncs.com\".length);\n\t\treturn hostname || undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\tprivate showSystemMetrics = false;\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\n\tconstructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/** Show system-wide CPU/MEM metrics in the stats line (off by default). */\n\tsetShowSystemMetrics(enabled: boolean): void {\n\t\tthis.showSystemMetrics = enabled;\n\t}\n\n\t/**\n\t * Build a compact system-wide CPU/memory metrics segment for the footer.\n\t * Degrades gracefully to shorter forms (or is omitted entirely) when the\n\t * terminal is very narrow so the rest of the footer can still render.\n\t */\n\tprivate buildMetricsSegment(width: number): string | undefined {\n\t\tif (!this.showSystemMetrics) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst cpu = this.footerData.getSystemCpuPercent();\n\t\tconst memUsed = this.footerData.getSystemMemoryUsedBytes();\n\t\tconst memTotal = this.footerData.getSystemMemoryTotalBytes();\n\t\tif (cpu === null || memUsed === null || memTotal === null || memTotal <= 0) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst memPercent = (memUsed / memTotal) * 100;\n\t\tconst color: \"error\" | \"warning\" | \"dim\" =\n\t\t\tcpu >= 90 || memPercent >= 95 ? \"error\" : cpu >= 70 || memPercent >= 85 ? \"warning\" : \"dim\";\n\n\t\tconst cpuStr = `${Math.round(cpu)}%`;\n\t\tconst memStr = `${Math.round(memPercent)}%`;\n\n\t\tif (width >= 56) {\n\t\t\treturn theme.fg(color, `CPU ${cpuStr} MEM ${memStr} (${formatBytes(memUsed)}/${formatBytes(memTotal)})`);\n\t\t}\n\t\tif (width >= 48) {\n\t\t\treturn theme.fg(color, `CPU ${cpuStr} MEM ${memStr}`);\n\t\t}\n\t\tif (width >= 30) {\n\t\t\treturn theme.fg(color, `${cpuStr} ${memStr}`);\n\t\t}\n\t\tif (width >= 18) {\n\t\t\treturn theme.fg(color, cpuStr);\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line. Each part carries a drop priority so narrow terminals\n\t\t// shed low-value segments first instead of truncating the context stats tail.\n\t\tconst statsParts: Array<{ text: string; priority: number }> = [];\n\t\tif (totalInput) statsParts.push({ text: `↑${formatTokens(totalInput)}`, priority: 3 });\n\t\tif (totalOutput) statsParts.push({ text: `↓${formatTokens(totalOutput)}`, priority: 3 });\n\t\tif (totalCacheRead) statsParts.push({ text: `R${formatTokens(totalCacheRead)}`, priority: 2 });\n\t\tif (totalCacheWrite) statsParts.push({ text: `W${formatTokens(totalCacheWrite)}`, priority: 2 });\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push({ text: costStr, priority: 4 });\n\t\t}\n\n\t\t// Add CPU/memory metrics (opt-in); degrade gracefully when horizontal space is tight.\n\t\tconst metricsSegment = this.buildMetricsSegment(width);\n\t\tif (metricsSegment) {\n\t\t\tstatsParts.push({ text: metricsSegment, priority: 1 });\n\t\t}\n\t\t// Package intake status is internal jargon for sessions without packages - hide when empty.\n\t\tconst packageIntake = this.footerData.getPackageIntakeSummary();\n\t\tif (packageIntake.total > 0) {\n\t\t\tstatsParts.push({ text: formatPackageIntake(packageIntake), priority: 2 });\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\t// Context usage is the most important number in the footer - never drop it.\n\t\tstatsParts.push({ text: contextPercentStr, priority: Number.POSITIVE_INFINITY });\n\n\t\t// Drop the lowest-priority parts until the stats line fits.\n\t\tlet activeParts = [...statsParts];\n\t\tlet statsLeft = activeParts.map((part) => part.text).join(\" \");\n\t\twhile (visibleWidth(statsLeft) > width) {\n\t\t\tconst droppable = activeParts.filter((part) => Number.isFinite(part.priority));\n\t\t\tif (droppable.length === 0) break;\n\t\t\tlet lowest = droppable[0]!;\n\t\t\tfor (const part of droppable) {\n\t\t\t\tif (part.priority < lowest.priority) lowest = part;\n\t\t\t}\n\t\t\tactiveParts = activeParts.filter((part) => part !== lowest);\n\t\t\tstatsLeft = activeParts.map((part) => part.text).join(\" \");\n\t\t}\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// Final fallback: truncate if even the undroppable parts overflow\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\tconst endpointHost = formatEndpointForFooter(state.model.baseUrl);\n\t\t\tconst providerLabel = endpointHost ? `${state.model.provider} · ${endpointHost}` : state.model.provider;\n\t\t\trightSide = `(${providerLabel}) ${rightSideWithoutProvider}`;\n\t\t\tif (endpointHost && statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide with endpoint host; fall back to provider-only\n\t\t\t\trightSide = `(${state.model.provider}) ${rightSideWithoutProvider}`;\n\t\t\t}\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"status-sidebar.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/status-sidebar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAGxF,OAAO,EACN,KAAK,kBAAkB,EAKvB,uBAAuB,EACvB,KAAK,yBAAyB,EAE9B,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,uBAAuB,EAAE,CAAC;AASnC,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAC3C,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAC3C,gGAAgG;AAChG,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AAgBD,KAAK,wBAAwB,GAAG,CAC/B,OAAO,EAAE,YAAY,EACrB,QAAQ,CAAC,EAAE,MAAM,KACb,OAAO,CAAC,yBAAyB,GAAG,SAAS,CAAC,CAAC;AACpD,KAAK,iBAAiB,GAAG,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC;AAC5F,KAAK,oBAAoB,GAAG,QAAQ,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,sBAAsB,CAAC,EAAE,wBAAwB,CAAC;IAClD,+EAA+E;IAC/E,eAAe,CAAC,EAAE,iBAAiB,CAAC;CACpC,CAAC,CAAC;AAEH;;;;;;;GAOG;AACH,qBAAa,sBAAuB,YAAW,SAAS;IACvD,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,qBAAqB,CAAgB;IAC7C,OAAO,CAAC,eAAe,CAAe;IACtC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,sBAAsB,CAA2B;IAGzD,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,QAAQ,CAAqE;IACrF,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,iBAAiB,CAAgD;IACzE,OAAO,CAAC,wBAAwB,CAA2B;IAC3D,OAAO,CAAC,0BAA0B,CAAyB;IAC3D,OAAO,CAAC,0BAA0B,CAA6B;IAC/D,OAAO,CAAC,yBAAyB,CAAmC;IACpE,OAAO,CAAC,yBAAyB,CAA6B;IAE9D,YACC,UAAU,EAAE,MAAM,YAAY,EAC9B,UAAU,EAAE,0BAA0B,EACtC,qBAAqB,EAAE,MAAM,OAAO,EACpC,eAAe,GAAE,MAAM,MAAiB,EACxC,OAAO,GAAE,oBAAyB,EAgBlC;IAED,UAAU,IAAI,IAAI,CAGjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAwH9B;IAED,OAAO,CAAC,wBAAwB;IA2DhC,OAAO,CAAC,wBAAwB;IA+BhC;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAuClB;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAmBxB,OAAO,CAAC,cAAc;IAetB,0EAAwE;IACxE,OAAO,CAAC,SAAS;CAwBjB","sourcesContent":["import type { Component } from \"omk-tui\";\nimport { truncateToWidth, visibleWidth } from \"omk-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { McpServerStatus } from \"../../../core/mcp/manager.ts\";\nimport { loadMcpInventory, type McpServerEntry } from \"../../../core/mcp-inventory.ts\";\nimport {\n\ttype CodexUsageSnapshot,\n\tgetConfiguredSubscriptionUsageProviders,\n\tgetSubscriptionUsageRevision,\n\tgetSubscriptionUsageSource,\n\tloadSubscriptionUsage,\n\tparseCodexUsageSnapshot,\n\ttype SubscriptionUsageSnapshot,\n\ttype SubscriptionUsageWindow,\n} from \"../../../core/provider-usage.ts\";\n\nexport { parseCodexUsageSnapshot };\n\nimport { stripAnsi } from \"../../../utils/ansi.ts\";\nimport { type ThemeColor, theme } from \"../theme/theme.ts\";\nimport { boxBottom, boxTextLine, boxTop, sidebarRule } from \"./control-panel-box.ts\";\nimport { classifyMcpStability } from \"./control-panel-runtime-status.ts\";\nimport { formatBytes, formatCwdForFooter, formatPackageIntake, formatTokens } from \"./footer.ts\";\nimport { keyText } from \"./keybinding-hints.ts\";\n\nexport const STATUS_SIDEBAR_WIDTH = 34;\nexport const STATUS_SIDEBAR_MAX_WIDTH = 48;\nexport const STATUS_SIDEBAR_MIN_WIDTH = 96;\n/** Blank columns between the main content and the pinned rail (part of the reserved gutter). */\nexport const STATUS_SIDEBAR_GUTTER_GAP = 1;\n\n/**\n * Responsive rail width: ~26% of the terminal, clamped to [34, 48] so the rail\n * grows on fullscreen terminals while the content column stays dominant.\n * The pinned overlay and the reserved gutter must both use this function.\n */\nexport function statusSidebarWidth(termWidth: number): number {\n\treturn Math.max(STATUS_SIDEBAR_WIDTH, Math.min(STATUS_SIDEBAR_MAX_WIDTH, Math.floor(termWidth * 0.26)));\n}\n\n/**\n * Responsive MCP roster rows: taller terminals list more servers before the\n * \"+N more\" collapse. ~24 rows of the rail are fixed chrome (header, model,\n * context, tokens, system), the rest is available to the roster.\n */\nexport function mcpMaxRows(termRows: number): number {\n\treturn Math.max(4, Math.min(18, termRows - 24));\n}\n\nconst METER_CELLS = 12;\nconst METER_MAX_CELLS = 24;\n/** Rolling window of CPU samples feeding the activity sparkline (wide rails show more history). */\nconst SPARK_WINDOW = 44;\n/** Minimum interval between sparkline samples so the rail animates, not jitters. */\nconst SPARK_SAMPLE_MS = 1000;\n/** MCP inventory is read from disk; cache it so per-frame renders stay cheap. */\nconst MCP_CACHE_TTL_MS = 5000;\n/** Protocol-ping cadence for live MCP connectivity (render-triggered, fire-and-forget). */\nconst MCP_HEALTH_INTERVAL_MS = 15_000;\n/** Failed servers are re-attempted on this slower cadence, never on every probe. */\nconst MCP_RECONNECT_INTERVAL_MS = 60_000;\nconst SPARK_CHARS = [\"▁\", \"▂\", \"▃\", \"▄\", \"▅\", \"▆\", \"▇\", \"█\"] as const;\n\ntype SubscriptionUsageFetcher = (\n\tsession: AgentSession,\n\tprovider?: string,\n) => Promise<SubscriptionUsageSnapshot | undefined>;\ntype CodexUsageFetcher = (session: AgentSession) => Promise<CodexUsageSnapshot | undefined>;\ntype StatusSidebarOptions = Readonly<{\n\trequestRender?: () => void;\n\tfetchSubscriptionUsage?: SubscriptionUsageFetcher;\n\t/** Compatibility for callers that injected the original Codex-only fetcher. */\n\tfetchCodexUsage?: CodexUsageFetcher;\n}>;\n\n/**\n * Fixed right-rail sidebar that shows the bottom-bar (footer) status vertically,\n * styled after opencode's pinned side rails plus a live MCP server roster.\n *\n * Pinned via the `pinStatusSidebar` setting (or OMK_PIN_STATUS_SIDEBAR=1) and\n * toggled with app.sidebar.toggle (Ctrl+Q). While pinned, the bottom footer is\n * removed so the same data is not rendered twice.\n */\nexport class StatusSidebarComponent implements Component {\n\tprivate getSession: () => AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate getAutoCompactEnabled: () => boolean;\n\tprivate getTerminalRows: () => number;\n\tprivate requestRender: () => void;\n\tprivate fetchSubscriptionUsage: SubscriptionUsageFetcher;\n\n\t// --- living elements: rolling CPU sparkline + cached MCP/usage data ---\n\tprivate cpuHistory: number[] = [];\n\tprivate lastSparkSampleAt = 0;\n\tprivate mcpCache: { at: number; cwd: string; entries: McpServerEntry[] } | undefined;\n\tprivate lastMcpProbeAt = 0;\n\tprivate lastMcpReconnectAt = 0;\n\tprivate mcpProbeInFlight = false;\n\tprivate subscriptionUsage = new Map<string, SubscriptionUsageSnapshot>();\n\tprivate subscriptionUsageSession: AgentSession | undefined;\n\tprivate subscriptionUsageProviders: readonly string[] = [];\n\tprivate subscriptionUsageFetchedAt = new Map<string, number>();\n\tprivate subscriptionUsageInFlight = new Map<string, AgentSession>();\n\tprivate subscriptionUsageRevision = new Map<string, number>();\n\n\tconstructor(\n\t\tgetSession: () => AgentSession,\n\t\tfooterData: ReadonlyFooterDataProvider,\n\t\tgetAutoCompactEnabled: () => boolean,\n\t\tgetTerminalRows: () => number = () => 32,\n\t\toptions: StatusSidebarOptions = {},\n\t) {\n\t\tthis.getSession = getSession;\n\t\tthis.footerData = footerData;\n\t\tthis.getAutoCompactEnabled = getAutoCompactEnabled;\n\t\tthis.getTerminalRows = getTerminalRows;\n\t\tthis.requestRender = options.requestRender ?? (() => {});\n\t\tconst legacyFetcher = options.fetchCodexUsage;\n\t\tthis.fetchSubscriptionUsage =\n\t\t\toptions.fetchSubscriptionUsage ??\n\t\t\t(legacyFetcher\n\t\t\t\t? async (session, provider) =>\n\t\t\t\t\t\tgetSubscriptionUsageSource(provider)?.kind === \"codex\"\n\t\t\t\t\t\t\t? codexSubscriptionSnapshot(await legacyFetcher(session))\n\t\t\t\t\t\t\t: loadSubscriptionUsage(session, undefined, provider)\n\t\t\t\t: (session, provider) => loadSubscriptionUsage(session, undefined, provider));\n\t}\n\n\tinvalidate(): void {\n\t\t// Mostly stateless: status recomputes from the live session each render.\n\t\t// Only the sparkline history and MCP cache persist across frames.\n\t}\n\n\trender(width: number): string[] {\n\t\tconst session = this.getSession();\n\t\tconst state = session.state;\n\t\tthis.refreshMcpHealth(session);\n\t\tthis.refreshSubscriptionUsage(session);\n\t\tconst lines: string[] = [boxTop(width, \"STATUS RAIL\")];\n\n\t\t// --- Live header: session uptime + CPU activity sparkline ---\n\t\tlines.push(\n\t\t\tboxTextLine(width, `${theme.fg(\"muted\", \"up \")}${theme.fg(\"accent\", formatUptime(process.uptime()))}`),\n\t\t);\n\t\tlines.push(boxTextLine(width, this.sparkline(width)));\n\n\t\t// --- Location ---\n\t\tconst cwd = formatCwdForFooter(session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\t\tlines.push(boxTextLine(width, `${theme.fg(\"muted\", \"cwd \")}${theme.fg(\"text\", cwd)}`));\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tlines.push(boxTextLine(width, `${theme.fg(\"muted\", \"git \")}${theme.fg(\"accent\", branch)}`));\n\t\t}\n\t\tconst sessionName = session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tlines.push(boxTextLine(width, `${theme.fg(\"muted\", \"sess \")}${theme.fg(\"text\", sessionName)}`));\n\t\t}\n\n\t\t// --- Model ---\n\t\tlines.push(sidebarRule(width, \"MODEL\"));\n\t\tlines.push(\n\t\t\tboxTextLine(width, `${theme.fg(\"muted\", \"id \")}${theme.fg(\"accent\", state.model?.id ?? \"no-model\")}`),\n\t\t);\n\t\tif (state.model?.reasoning) {\n\t\t\tlines.push(\n\t\t\t\tboxTextLine(width, `${theme.fg(\"muted\", \"think \")}${theme.fg(\"mdCode\", state.thinkingLevel || \"off\")}`),\n\t\t\t);\n\t\t}\n\n\t\t// --- Active provider subscription quota ---\n\t\tconst usageLines = this.subscriptionUsageSection(width);\n\t\tlines.push(...usageLines);\n\n\t\t// --- Context ---\n\t\tlines.push(sidebarRule(width, \"CONTEXT\"));\n\t\tconst contextUsage = session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst percent = contextUsage?.percent ?? null;\n\t\tconst auto = this.getAutoCompactEnabled() ? \" (auto)\" : \"\";\n\t\tconst percentLabel =\n\t\t\tpercent === null\n\t\t\t\t? `?/${formatTokens(contextWindow)}${auto}`\n\t\t\t\t: `${percent.toFixed(1)}%/${formatTokens(contextWindow)}${auto}`;\n\t\tconst percentColor: ThemeColor =\n\t\t\tpercent !== null && percent > 90 ? \"error\" : percent !== null && percent > 70 ? \"warning\" : \"success\";\n\t\tlines.push(boxTextLine(width, `${theme.fg(\"muted\", \"ctx \")}${theme.fg(percentColor, percentLabel)}`));\n\t\tlines.push(boxTextLine(width, meter(percent, width)));\n\n\t\t// --- Tokens (cumulative across all session entries, mirrors the footer) ---\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\t\tfor (const entry of session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\t\tlines.push(sidebarRule(width, \"TOKENS\"));\n\t\tlines.push(\n\t\t\tboxTextLine(\n\t\t\t\twidth,\n\t\t\t\t`${theme.fg(\"success\", `↑${formatTokens(totalInput)}`)} ${theme.fg(\"accent\", `↓${formatTokens(totalOutput)}`)}`,\n\t\t\t),\n\t\t);\n\t\tlines.push(\n\t\t\tboxTextLine(\n\t\t\t\twidth,\n\t\t\t\t`${theme.fg(\"muted\", `R${formatTokens(totalCacheRead)}`)} ${theme.fg(\"muted\", `W${formatTokens(totalCacheWrite)}`)}`,\n\t\t\t),\n\t\t);\n\t\tconst usingSubscription = state.model ? session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tlines.push(\n\t\t\t\tboxTextLine(\n\t\t\t\t\twidth,\n\t\t\t\t\t`${theme.fg(\"muted\", \"cost \")}${theme.fg(\"warning\", `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`)}`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// --- MCP roster (live connectivity + opencode-style stability dots) ---\n\t\tlines.push(...this.mcpSection(width, session, usageLines.length));\n\n\t\t// --- System ---\n\t\tlines.push(sidebarRule(width, \"SYSTEM\"));\n\t\tconst cpu = this.footerData.getCpuPercent();\n\t\tconst mem = this.footerData.getMemoryRssBytes();\n\t\tconst sysParts: string[] = [];\n\t\tif (cpu !== null) sysParts.push(`cpu ${cpu.toFixed(0)}%`);\n\t\tif (mem !== null) sysParts.push(`mem ${formatBytes(mem)}`);\n\t\tif (sysParts.length > 0) {\n\t\t\tlines.push(boxTextLine(width, theme.fg(\"muted\", sysParts.join(\" \"))));\n\t\t}\n\t\tlines.push(boxTextLine(width, theme.fg(\"dim\", formatPackageIntake(this.footerData.getPackageIntakeSummary()))));\n\n\t\t// --- Extension statuses ---\n\t\tconst statuses = this.footerData.getExtensionStatuses();\n\t\tif (statuses.size > 0) {\n\t\t\tlines.push(sidebarRule(width, \"EXT\"));\n\t\t\tfor (const [key, value] of statuses) {\n\t\t\t\tlines.push(boxTextLine(width, `${theme.fg(\"mdCode\", key)} ${theme.fg(\"muted\", value)}`));\n\t\t\t}\n\t\t}\n\n\t\tlines.push(boxTextLine(width, theme.fg(\"dim\", `${keyText(\"app.sidebar.toggle\")} unpin`)));\n\t\tlines.push(boxBottom(width));\n\t\treturn lines;\n\t}\n\n\tprivate refreshSubscriptionUsage(session: AgentSession): void {\n\t\tif (this.subscriptionUsageSession !== session) {\n\t\t\tthis.subscriptionUsage.clear();\n\t\t\tthis.subscriptionUsageFetchedAt.clear();\n\t\t\tthis.subscriptionUsageInFlight.clear();\n\t\t\tthis.subscriptionUsageRevision.clear();\n\t\t\tthis.subscriptionUsageSession = session;\n\t\t}\n\n\t\tconst providers = getConfiguredSubscriptionUsageProviders(session);\n\t\tthis.subscriptionUsageProviders = providers;\n\t\tconst configured = new Set(providers);\n\t\tfor (const provider of this.subscriptionUsage.keys()) {\n\t\t\tif (!configured.has(provider)) {\n\t\t\t\tthis.subscriptionUsage.delete(provider);\n\t\t\t\tthis.subscriptionUsageFetchedAt.delete(provider);\n\t\t\t\tthis.subscriptionUsageRevision.delete(provider);\n\t\t\t}\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tfor (const provider of providers) {\n\t\t\tconst source = getSubscriptionUsageSource(provider);\n\t\t\tconst revision = getSubscriptionUsageRevision(provider);\n\t\t\tconst revisionChanged = revision !== (this.subscriptionUsageRevision.get(provider) ?? 0);\n\t\t\tif (\n\t\t\t\t!source ||\n\t\t\t\tthis.subscriptionUsageInFlight.has(provider) ||\n\t\t\t\t(!revisionChanged && now - (this.subscriptionUsageFetchedAt.get(provider) ?? 0) < source.ttlMs)\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.subscriptionUsageFetchedAt.set(provider, now);\n\t\t\tthis.subscriptionUsageRevision.set(provider, revision);\n\t\t\tthis.subscriptionUsageInFlight.set(provider, session);\n\t\t\tvoid this.fetchSubscriptionUsage(session, provider)\n\t\t\t\t.then((snapshot) => {\n\t\t\t\t\tif (snapshot && this.getSession() === session && this.subscriptionUsageProviders.includes(provider)) {\n\t\t\t\t\t\tthis.subscriptionUsage.set(provider, snapshot);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch(() => {\n\t\t\t\t\tif (this.getSession() === session && this.subscriptionUsageProviders.includes(provider)) {\n\t\t\t\t\t\tthis.subscriptionUsage.set(provider, {\n\t\t\t\t\t\t\tlabel: source.label,\n\t\t\t\t\t\t\twindows: [],\n\t\t\t\t\t\t\tmessage: \"usage unavailable\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\tif (this.subscriptionUsageInFlight.get(provider) === session) {\n\t\t\t\t\t\tthis.subscriptionUsageInFlight.delete(provider);\n\t\t\t\t\t}\n\t\t\t\t\tthis.requestRender();\n\t\t\t\t});\n\t\t}\n\t}\n\n\tprivate subscriptionUsageSection(width: number): string[] {\n\t\tif (this.subscriptionUsageProviders.length === 0) return [];\n\t\tconst lines = [railRule(width, \"USAGE\", theme.fg(\"accent\", String(this.subscriptionUsageProviders.length)))];\n\t\tfor (const provider of this.subscriptionUsageProviders) {\n\t\t\tconst source = getSubscriptionUsageSource(provider);\n\t\t\tconst snapshot = this.subscriptionUsage.get(provider);\n\t\t\tconst label = snapshot?.label ?? source?.label;\n\t\t\tif (!label) continue;\n\t\t\tif (!snapshot) {\n\t\t\t\tif (this.subscriptionUsageInFlight.has(provider)) {\n\t\t\t\t\tlines.push(boxTextLine(width, `${theme.fg(\"accent\", label)} ${theme.fg(\"dim\", \"loading…\")}`));\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (snapshot.windows.length === 0) {\n\t\t\t\tlines.push(boxTextLine(width, theme.fg(\"accent\", label)));\n\t\t\t\tlines.push(boxTextLine(width, theme.fg(\"dim\", snapshot.message ?? \"usage unavailable\")));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tlines.push(boxTextLine(width, theme.fg(\"accent\", label)));\n\t\t\tfor (const window of snapshot.windows) {\n\t\t\t\tlines.push(boxTextLine(width, usageMeter(window.label, window, width)));\n\t\t\t}\n\t\t\tconst resets = snapshot.windows.flatMap((window) =>\n\t\t\t\twindow.resetsAt === undefined ? [] : [`${window.label} ${formatReset(window.resetsAt)}`],\n\t\t\t);\n\t\t\tif (resets.length > 0) lines.push(boxTextLine(width, theme.fg(\"dim\", `reset ${resets.join(\" · \")}`)));\n\t\t}\n\t\treturn lines.length === 1 ? [] : lines;\n\t}\n\n\t/**\n\t * MCP server roster with a live-connected/total counter in the section\n\t * rule and a per-server state badge. Config inventory reads are cached;\n\t * live status comes from the session's MCP manager each frame.\n\t */\n\tprivate mcpSection(width: number, session: AgentSession, reservedRows = 0): string[] {\n\t\tconst cwd = session.sessionManager.getCwd();\n\t\tconst entries = this.loadMcpEntries(cwd);\n\t\tconst live = new Map(session.mcpServerStatus().map((status) => [status.name, status]));\n\t\tconst ready = [...live.values()].filter((status) => status.state === \"ready\").length;\n\t\tconst anyFailed = [...live.values()].some((status) => status.state === \"failed\");\n\t\tconst stable = entries.filter((entry) => classifyMcpStability(entry) === \"stable\").length;\n\t\tconst countColor: ThemeColor =\n\t\t\tentries.length === 0\n\t\t\t\t? \"dim\"\n\t\t\t\t: live.size > 0\n\t\t\t\t\t? anyFailed\n\t\t\t\t\t\t? \"error\"\n\t\t\t\t\t\t: ready === live.size\n\t\t\t\t\t\t\t? \"success\"\n\t\t\t\t\t\t\t: \"warning\"\n\t\t\t\t\t: stable === entries.length\n\t\t\t\t\t\t? \"success\"\n\t\t\t\t\t\t: \"warning\";\n\t\tconst count =\n\t\t\tentries.length === 0 ? \"0\" : live.size > 0 ? `${ready}/${live.size}` : `${stable}/${entries.length}`;\n\n\t\tconst lines: string[] = [railRule(width, \"MCP\", theme.fg(countColor, count))];\n\t\tif (entries.length === 0) {\n\t\t\tlines.push(boxTextLine(width, theme.fg(\"dim\", \"none configured\")));\n\t\t\treturn lines;\n\t\t}\n\n\t\tconst shown = entries.slice(0, mcpMaxRows(this.getTerminalRows() - reservedRows));\n\t\tfor (const entry of shown) {\n\t\t\tlines.push(boxTextLine(width, mcpRow(entry, width, live.get(entry.name))));\n\t\t}\n\t\tconst hidden = entries.length - shown.length;\n\t\tif (hidden > 0) {\n\t\t\tlines.push(boxTextLine(width, theme.fg(\"dim\", `+${hidden} more…`)));\n\t\t}\n\t\treturn lines;\n\t}\n\n\t/**\n\t * Keep the rail's MCP rows truthful: ping connected servers on a slow\n\t * cadence and retry failed ones even slower. Fire-and-forget from render —\n\t * the 2s metrics repaint supplies the ticks, so no extra timer exists to\n\t * leak. Probing never spawns connections: idle servers stay idle until a\n\t * tool call attaches them (manager lazy-connect design).\n\t */\n\tprivate refreshMcpHealth(session: AgentSession): void {\n\t\tconst now = Date.now();\n\t\tif (this.mcpProbeInFlight) return;\n\t\tconst probeDue = now - this.lastMcpProbeAt >= MCP_HEALTH_INTERVAL_MS;\n\t\tconst reconnectDue = now - this.lastMcpReconnectAt >= MCP_RECONNECT_INTERVAL_MS;\n\t\tif (!probeDue && !reconnectDue) return;\n\t\tif (session.mcpServerStatus().length === 0) return; // no MCP manager attached\n\t\tthis.mcpProbeInFlight = true;\n\t\tthis.lastMcpProbeAt = now;\n\t\tif (reconnectDue) this.lastMcpReconnectAt = now;\n\t\tvoid session\n\t\t\t.mcpCheckHealth({ reconnectFailed: reconnectDue })\n\t\t\t.catch(() => {})\n\t\t\t.finally(() => {\n\t\t\t\tthis.mcpProbeInFlight = false;\n\t\t\t\tthis.requestRender();\n\t\t\t});\n\t}\n\n\tprivate loadMcpEntries(cwd: string): McpServerEntry[] {\n\t\tconst now = Date.now();\n\t\tif (this.mcpCache && this.mcpCache.cwd === cwd && now - this.mcpCache.at < MCP_CACHE_TTL_MS) {\n\t\t\treturn this.mcpCache.entries;\n\t\t}\n\t\tlet entries: McpServerEntry[] = [];\n\t\ttry {\n\t\t\tentries = loadMcpInventory(cwd).entries;\n\t\t} catch {\n\t\t\tentries = [];\n\t\t}\n\t\tthis.mcpCache = { at: now, cwd, entries };\n\t\treturn entries;\n\t}\n\n\t/** Rolling CPU sparkline — a living pulse along the top of the rail. */\n\tprivate sparkline(width: number): string {\n\t\tconst now = Date.now();\n\t\tconst cpu = this.footerData.getCpuPercent();\n\t\tif (cpu !== null && now - this.lastSparkSampleAt >= SPARK_SAMPLE_MS) {\n\t\t\tthis.lastSparkSampleAt = now;\n\t\t\tthis.cpuHistory.push(Math.max(0, Math.min(100, cpu)));\n\t\t\tif (this.cpuHistory.length > SPARK_WINDOW) {\n\t\t\t\tthis.cpuHistory.shift();\n\t\t\t}\n\t\t}\n\n\t\tconst inner = Math.max(0, width - 4);\n\t\tconst label = theme.fg(\"muted\", \"act \");\n\t\tconst cells = Math.max(0, inner - visibleWidth(label));\n\t\tif (cells === 0 || this.cpuHistory.length === 0) {\n\t\t\treturn `${label}${theme.fg(\"borderMuted\", \"▁\".repeat(Math.max(0, cells)))}`;\n\t\t}\n\n\t\t// Right-align the window so the sparkline grows in from the left edge.\n\t\tconst window = this.cpuHistory.slice(-cells);\n\t\tconst bars = window.map((sample) => SPARK_CHARS[Math.min(7, Math.floor(sample / 12.6))]).join(\"\");\n\t\tconst pad = \"▁\".repeat(Math.max(0, cells - window.length));\n\t\treturn `${label}${theme.fg(\"borderMuted\", pad)}${theme.fg(\"accent\", bars)}`;\n\t}\n}\n\n/** Section rule with a right-aligned counter, e.g. `│─ MCP ─────── 22/24 │`. */\nfunction railRule(width: number, label: string, right: string): string {\n\tconst left = `${theme.fg(\"borderMuted\", \"─\")}${theme.fg(\"accent\", ` ${label} `)}`;\n\tconst rightText = ` ${right} `;\n\tconst fill = Math.max(0, width - 2 - visibleWidth(left) - visibleWidth(rightText));\n\tconst line = `${theme.fg(\"borderMuted\", \"│\")}${left}${theme.fg(\"borderMuted\", \"─\".repeat(fill))}${rightText}${theme.fg(\"borderMuted\", \"│\")}`;\n\treturn visibleWidth(line) <= width ? line : truncateToWidth(line, width, \"\");\n}\n\n/** One MCP server row: live-state badge + name + right-aligned detail, truncated to the rail width. */\nfunction mcpRow(entry: McpServerEntry, width: number, live?: McpServerStatus): string {\n\tconst badge = live ? liveMcpBadge(live) : configMcpBadge(entry);\n\tconst safeName = stripAnsi(entry.name)\n\t\t.replace(/[\\t\\r\\n]+/g, \" \")\n\t\t.replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f-\\u009f\\u200b-\\u200f\\u202a-\\u202e\\u2060-\\u206f]/g, \"\")\n\t\t.replace(/\\s+/g, \" \")\n\t\t.trim();\n\tconst detail = live ? liveMcpDetail(live) : \"\";\n\t// frame (4) + dot + space (2); detail is right-aligned with one gap column.\n\tconst detailText = detail ? ` ${detail}` : \"\";\n\tconst nameWidth = Math.max(1, width - 4 - 2 - visibleWidth(detailText));\n\tconst name = truncateToWidth(safeName || \"<unnamed>\", nameWidth, \"…\");\n\tconst pad = Math.max(0, width - 4 - 2 - visibleWidth(name) - visibleWidth(detailText));\n\treturn `${badge.dot} ${theme.fg(badge.nameColor, name)}${\" \".repeat(pad)}${detail ? theme.fg(\"dim\", detailText) : \"\"}`;\n}\n\nfunction configMcpBadge(entry: McpServerEntry): { dot: string; nameColor: ThemeColor } {\n\tconst stability = classifyMcpStability(entry);\n\treturn {\n\t\tdot:\n\t\t\tstability === \"stable\"\n\t\t\t\t? theme.fg(\"success\", \"●\")\n\t\t\t\t: stability === \"overridden\"\n\t\t\t\t\t? theme.fg(\"dim\", \"◐\")\n\t\t\t\t\t: theme.fg(\"warning\", \"○\"),\n\t\tnameColor: stability === \"stable\" ? \"text\" : stability === \"overridden\" ? \"dim\" : \"warning\",\n\t};\n}\n\nfunction liveMcpBadge(status: McpServerStatus): { dot: string; nameColor: ThemeColor } {\n\tswitch (status.state) {\n\t\tcase \"ready\":\n\t\t\treturn { dot: theme.fg(\"success\", \"●\"), nameColor: \"text\" };\n\t\tcase \"connecting\":\n\t\t\treturn { dot: theme.fg(\"warning\", \"◐\"), nameColor: \"text\" };\n\t\tcase \"failed\":\n\t\t\treturn status.error === \"disabled by configuration\"\n\t\t\t\t? { dot: theme.fg(\"dim\", \"○\"), nameColor: \"dim\" }\n\t\t\t\t: { dot: theme.fg(\"error\", \"✕\"), nameColor: \"error\" };\n\t\tdefault:\n\t\t\treturn { dot: theme.fg(\"dim\", \"○\"), nameColor: \"dim\" };\n\t}\n}\n\nfunction liveMcpDetail(status: McpServerStatus): string {\n\tswitch (status.state) {\n\t\tcase \"ready\":\n\t\t\treturn `${status.toolCount}t`;\n\t\tcase \"connecting\":\n\t\t\treturn \"…\";\n\t\tcase \"failed\":\n\t\t\treturn status.error === \"disabled by configuration\" ? \"off\" : \"failed\";\n\t\tdefault:\n\t\t\treturn \"idle\";\n\t}\n}\n\nfunction formatUptime(seconds: number): string {\n\tconst total = Math.max(0, Math.floor(seconds));\n\tconst h = Math.floor(total / 3600);\n\tconst m = Math.floor((total % 3600) / 60);\n\tconst s = total % 60;\n\tconst pad = (n: number) => n.toString().padStart(2, \"0\");\n\treturn h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;\n}\n\nfunction usageMeter(label: string, window: SubscriptionUsageWindow, width: number): string {\n\tconst percent = Math.max(0, Math.min(100, window.usedPercent));\n\tconst percentText = `${Math.round(percent)}%`;\n\tconst labelText = `${label} `;\n\tconst innerWidth = Math.max(0, width - 4);\n\tconst cells = Math.max(4, Math.min(METER_MAX_CELLS, innerWidth - labelText.length - percentText.length - 1));\n\tconst filled = Math.round((percent / 100) * cells);\n\tconst color = usageColor(percent);\n\treturn `${theme.fg(\"muted\", labelText)}${theme.fg(color, \"█\".repeat(filled))}${theme.fg(\"borderMuted\", \"░\".repeat(cells - filled))} ${theme.fg(color, percentText)}`;\n}\n\nfunction usageColor(percent: number): ThemeColor {\n\tif (percent >= 90) return \"error\";\n\tif (percent >= 75) return \"warning\";\n\treturn \"success\";\n}\n\nfunction codexSubscriptionSnapshot(snapshot: CodexUsageSnapshot | undefined): SubscriptionUsageSnapshot | undefined {\n\tif (!snapshot) return undefined;\n\treturn {\n\t\tlabel: \"CODEX\",\n\t\twindows: [\n\t\t\tsnapshot.fiveHour ? { label: \"5H\", ...snapshot.fiveHour } : undefined,\n\t\t\tsnapshot.sevenDay ? { label: \"7D\", ...snapshot.sevenDay } : undefined,\n\t\t].filter((window): window is SubscriptionUsageWindow => window !== undefined),\n\t};\n}\n\nfunction formatReset(resetsAt: number): string {\n\tconst seconds = Math.max(0, Math.ceil(resetsAt - Date.now() / 1000));\n\tif (seconds < 60) return \"now\";\n\tconst totalMinutes = Math.ceil(seconds / 60);\n\tif (totalMinutes < 60) return `${totalMinutes}m`;\n\tif (totalMinutes < 24 * 60) {\n\t\tconst hours = Math.floor(totalMinutes / 60);\n\t\tconst minutes = totalMinutes % 60;\n\t\treturn `${hours}h${minutes > 0 ? `${minutes}m` : \"\"}`;\n\t}\n\tconst totalHours = Math.ceil(totalMinutes / 60);\n\tconst days = Math.floor(totalHours / 24);\n\tconst hours = totalHours % 24;\n\treturn `${days}d${hours > 0 ? `${hours}h` : \"\"}`;\n}\n\nfunction meter(percent: number | null, width: number): string {\n\t// Fill the rail: frame (4) + space + up to \"100%\" (4) stay reserved.\n\tconst cells = Math.max(METER_CELLS, Math.min(METER_MAX_CELLS, width - 9));\n\tif (percent === null) {\n\t\treturn `${theme.fg(\"borderMuted\", \"░\".repeat(cells))} ${theme.fg(\"muted\", \"??%\")}`;\n\t}\n\tconst clamped = Math.max(0, Math.min(100, percent));\n\tconst filled = Math.round((clamped / 100) * cells);\n\tconst color: ThemeColor = clamped >= 85 ? \"warning\" : clamped >= 65 ? \"mdCode\" : \"success\";\n\treturn `${theme.fg(color, \"█\".repeat(filled))}${theme.fg(\"borderMuted\", \"░\".repeat(cells - filled))} ${theme.fg(color, `${Math.round(clamped)}%`)}`;\n}\n"]}
1
+ {"version":3,"file":"status-sidebar.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/status-sidebar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAGxF,OAAO,EACN,KAAK,kBAAkB,EAKvB,uBAAuB,EACvB,KAAK,yBAAyB,EAE9B,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,uBAAuB,EAAE,CAAC;AAenC,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAC3C,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAC3C,gGAAgG;AAChG,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AAgBD,KAAK,wBAAwB,GAAG,CAC/B,OAAO,EAAE,YAAY,EACrB,QAAQ,CAAC,EAAE,MAAM,KACb,OAAO,CAAC,yBAAyB,GAAG,SAAS,CAAC,CAAC;AACpD,KAAK,iBAAiB,GAAG,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC;AAC5F,KAAK,oBAAoB,GAAG,QAAQ,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,sBAAsB,CAAC,EAAE,wBAAwB,CAAC;IAClD,+EAA+E;IAC/E,eAAe,CAAC,EAAE,iBAAiB,CAAC;CACpC,CAAC,CAAC;AAEH;;;;;;;GAOG;AACH,qBAAa,sBAAuB,YAAW,SAAS;IACvD,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,qBAAqB,CAAgB;IAC7C,OAAO,CAAC,eAAe,CAAe;IACtC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,sBAAsB,CAA2B;IAGzD,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,QAAQ,CAAqE;IACrF,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,iBAAiB,CAAgD;IACzE,OAAO,CAAC,wBAAwB,CAA2B;IAC3D,OAAO,CAAC,0BAA0B,CAAyB;IAC3D,OAAO,CAAC,0BAA0B,CAA6B;IAC/D,OAAO,CAAC,yBAAyB,CAAmC;IACpE,OAAO,CAAC,yBAAyB,CAA6B;IAE9D,YACC,UAAU,EAAE,MAAM,YAAY,EAC9B,UAAU,EAAE,0BAA0B,EACtC,qBAAqB,EAAE,MAAM,OAAO,EACpC,eAAe,GAAE,MAAM,MAAiB,EACxC,OAAO,GAAE,oBAAyB,EAgBlC;IAED,UAAU,IAAI,IAAI,CAGjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA4H9B;IAED,OAAO,CAAC,wBAAwB;IA2DhC,OAAO,CAAC,wBAAwB;IAqChC;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAuClB;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAmBxB,OAAO,CAAC,cAAc;IAetB,0EAAwE;IACxE,OAAO,CAAC,SAAS;CAwBjB","sourcesContent":["import type { Component } from \"omk-tui\";\nimport { truncateToWidth, visibleWidth } from \"omk-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { McpServerStatus } from \"../../../core/mcp/manager.ts\";\nimport { loadMcpInventory, type McpServerEntry } from \"../../../core/mcp-inventory.ts\";\nimport {\n\ttype CodexUsageSnapshot,\n\tgetConfiguredSubscriptionUsageProviders,\n\tgetSubscriptionUsageRevision,\n\tgetSubscriptionUsageSource,\n\tloadSubscriptionUsage,\n\tparseCodexUsageSnapshot,\n\ttype SubscriptionUsageSnapshot,\n\ttype SubscriptionUsageWindow,\n} from \"../../../core/provider-usage.ts\";\n\nexport { parseCodexUsageSnapshot };\n\nimport { stripAnsi } from \"../../../utils/ansi.ts\";\nimport { type ThemeColor, theme } from \"../theme/theme.ts\";\nimport { boxBottom, boxTextLine, boxTop, sidebarRule } from \"./control-panel-box.ts\";\nimport { classifyMcpStability } from \"./control-panel-runtime-status.ts\";\nimport {\n\tformatBytes,\n\tformatCwdForFooter,\n\tformatEndpointForFooter,\n\tformatPackageIntake,\n\tformatTokens,\n} from \"./footer.ts\";\nimport { keyText } from \"./keybinding-hints.ts\";\n\nexport const STATUS_SIDEBAR_WIDTH = 34;\nexport const STATUS_SIDEBAR_MAX_WIDTH = 48;\nexport const STATUS_SIDEBAR_MIN_WIDTH = 96;\n/** Blank columns between the main content and the pinned rail (part of the reserved gutter). */\nexport const STATUS_SIDEBAR_GUTTER_GAP = 1;\n\n/**\n * Responsive rail width: ~26% of the terminal, clamped to [34, 48] so the rail\n * grows on fullscreen terminals while the content column stays dominant.\n * The pinned overlay and the reserved gutter must both use this function.\n */\nexport function statusSidebarWidth(termWidth: number): number {\n\treturn Math.max(STATUS_SIDEBAR_WIDTH, Math.min(STATUS_SIDEBAR_MAX_WIDTH, Math.floor(termWidth * 0.26)));\n}\n\n/**\n * Responsive MCP roster rows: taller terminals list more servers before the\n * \"+N more\" collapse. ~24 rows of the rail are fixed chrome (header, model,\n * context, tokens, system), the rest is available to the roster.\n */\nexport function mcpMaxRows(termRows: number): number {\n\treturn Math.max(4, Math.min(18, termRows - 24));\n}\n\nconst METER_CELLS = 12;\nconst METER_MAX_CELLS = 24;\n/** Rolling window of CPU samples feeding the activity sparkline (wide rails show more history). */\nconst SPARK_WINDOW = 44;\n/** Minimum interval between sparkline samples so the rail animates, not jitters. */\nconst SPARK_SAMPLE_MS = 1000;\n/** MCP inventory is read from disk; cache it so per-frame renders stay cheap. */\nconst MCP_CACHE_TTL_MS = 5000;\n/** Protocol-ping cadence for live MCP connectivity (render-triggered, fire-and-forget). */\nconst MCP_HEALTH_INTERVAL_MS = 15_000;\n/** Failed servers are re-attempted on this slower cadence, never on every probe. */\nconst MCP_RECONNECT_INTERVAL_MS = 60_000;\nconst SPARK_CHARS = [\"▁\", \"▂\", \"▃\", \"▄\", \"▅\", \"▆\", \"▇\", \"█\"] as const;\n\ntype SubscriptionUsageFetcher = (\n\tsession: AgentSession,\n\tprovider?: string,\n) => Promise<SubscriptionUsageSnapshot | undefined>;\ntype CodexUsageFetcher = (session: AgentSession) => Promise<CodexUsageSnapshot | undefined>;\ntype StatusSidebarOptions = Readonly<{\n\trequestRender?: () => void;\n\tfetchSubscriptionUsage?: SubscriptionUsageFetcher;\n\t/** Compatibility for callers that injected the original Codex-only fetcher. */\n\tfetchCodexUsage?: CodexUsageFetcher;\n}>;\n\n/**\n * Fixed right-rail sidebar that shows the bottom-bar (footer) status vertically,\n * styled after opencode's pinned side rails plus a live MCP server roster.\n *\n * Pinned via the `pinStatusSidebar` setting (or OMK_PIN_STATUS_SIDEBAR=1) and\n * toggled with app.sidebar.toggle (Ctrl+Q). While pinned, the bottom footer is\n * removed so the same data is not rendered twice.\n */\nexport class StatusSidebarComponent implements Component {\n\tprivate getSession: () => AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate getAutoCompactEnabled: () => boolean;\n\tprivate getTerminalRows: () => number;\n\tprivate requestRender: () => void;\n\tprivate fetchSubscriptionUsage: SubscriptionUsageFetcher;\n\n\t// --- living elements: rolling CPU sparkline + cached MCP/usage data ---\n\tprivate cpuHistory: number[] = [];\n\tprivate lastSparkSampleAt = 0;\n\tprivate mcpCache: { at: number; cwd: string; entries: McpServerEntry[] } | undefined;\n\tprivate lastMcpProbeAt = 0;\n\tprivate lastMcpReconnectAt = 0;\n\tprivate mcpProbeInFlight = false;\n\tprivate subscriptionUsage = new Map<string, SubscriptionUsageSnapshot>();\n\tprivate subscriptionUsageSession: AgentSession | undefined;\n\tprivate subscriptionUsageProviders: readonly string[] = [];\n\tprivate subscriptionUsageFetchedAt = new Map<string, number>();\n\tprivate subscriptionUsageInFlight = new Map<string, AgentSession>();\n\tprivate subscriptionUsageRevision = new Map<string, number>();\n\n\tconstructor(\n\t\tgetSession: () => AgentSession,\n\t\tfooterData: ReadonlyFooterDataProvider,\n\t\tgetAutoCompactEnabled: () => boolean,\n\t\tgetTerminalRows: () => number = () => 32,\n\t\toptions: StatusSidebarOptions = {},\n\t) {\n\t\tthis.getSession = getSession;\n\t\tthis.footerData = footerData;\n\t\tthis.getAutoCompactEnabled = getAutoCompactEnabled;\n\t\tthis.getTerminalRows = getTerminalRows;\n\t\tthis.requestRender = options.requestRender ?? (() => {});\n\t\tconst legacyFetcher = options.fetchCodexUsage;\n\t\tthis.fetchSubscriptionUsage =\n\t\t\toptions.fetchSubscriptionUsage ??\n\t\t\t(legacyFetcher\n\t\t\t\t? async (session, provider) =>\n\t\t\t\t\t\tgetSubscriptionUsageSource(provider)?.kind === \"codex\"\n\t\t\t\t\t\t\t? codexSubscriptionSnapshot(await legacyFetcher(session))\n\t\t\t\t\t\t\t: loadSubscriptionUsage(session, undefined, provider)\n\t\t\t\t: (session, provider) => loadSubscriptionUsage(session, undefined, provider));\n\t}\n\n\tinvalidate(): void {\n\t\t// Mostly stateless: status recomputes from the live session each render.\n\t\t// Only the sparkline history and MCP cache persist across frames.\n\t}\n\n\trender(width: number): string[] {\n\t\tconst session = this.getSession();\n\t\tconst state = session.state;\n\t\tthis.refreshMcpHealth(session);\n\t\tthis.refreshSubscriptionUsage(session);\n\t\tconst lines: string[] = [boxTop(width, \"STATUS RAIL\")];\n\n\t\t// --- Live header: session uptime + CPU activity sparkline ---\n\t\tlines.push(\n\t\t\tboxTextLine(width, `${theme.fg(\"muted\", \"up \")}${theme.fg(\"accent\", formatUptime(process.uptime()))}`),\n\t\t);\n\t\tlines.push(boxTextLine(width, this.sparkline(width)));\n\n\t\t// --- Location ---\n\t\tconst cwd = formatCwdForFooter(session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\t\tlines.push(boxTextLine(width, `${theme.fg(\"muted\", \"cwd \")}${theme.fg(\"text\", cwd)}`));\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tlines.push(boxTextLine(width, `${theme.fg(\"muted\", \"git \")}${theme.fg(\"accent\", branch)}`));\n\t\t}\n\t\tconst sessionName = session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tlines.push(boxTextLine(width, `${theme.fg(\"muted\", \"sess \")}${theme.fg(\"text\", sessionName)}`));\n\t\t}\n\n\t\t// --- Model ---\n\t\tlines.push(sidebarRule(width, \"MODEL\"));\n\t\tlines.push(\n\t\t\tboxTextLine(width, `${theme.fg(\"muted\", \"id \")}${theme.fg(\"accent\", state.model?.id ?? \"no-model\")}`),\n\t\t);\n\t\tconst endpointHost = formatEndpointForFooter(state.model?.baseUrl);\n\t\tif (endpointHost) {\n\t\t\tlines.push(boxTextLine(width, `${theme.fg(\"muted\", \"endp \")}${theme.fg(\"dim\", endpointHost)}`));\n\t\t}\n\t\tif (state.model?.reasoning) {\n\t\t\tlines.push(\n\t\t\t\tboxTextLine(width, `${theme.fg(\"muted\", \"think \")}${theme.fg(\"mdCode\", state.thinkingLevel || \"off\")}`),\n\t\t\t);\n\t\t}\n\n\t\t// --- Active provider subscription quota ---\n\t\tconst usageLines = this.subscriptionUsageSection(width);\n\t\tlines.push(...usageLines);\n\n\t\t// --- Context ---\n\t\tlines.push(sidebarRule(width, \"CONTEXT\"));\n\t\tconst contextUsage = session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst percent = contextUsage?.percent ?? null;\n\t\tconst auto = this.getAutoCompactEnabled() ? \" (auto)\" : \"\";\n\t\tconst percentLabel =\n\t\t\tpercent === null\n\t\t\t\t? `?/${formatTokens(contextWindow)}${auto}`\n\t\t\t\t: `${percent.toFixed(1)}%/${formatTokens(contextWindow)}${auto}`;\n\t\tconst percentColor: ThemeColor =\n\t\t\tpercent !== null && percent > 90 ? \"error\" : percent !== null && percent > 70 ? \"warning\" : \"success\";\n\t\tlines.push(boxTextLine(width, `${theme.fg(\"muted\", \"ctx \")}${theme.fg(percentColor, percentLabel)}`));\n\t\tlines.push(boxTextLine(width, meter(percent, width)));\n\n\t\t// --- Tokens (cumulative across all session entries, mirrors the footer) ---\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\t\tfor (const entry of session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\t\tlines.push(sidebarRule(width, \"TOKENS\"));\n\t\tlines.push(\n\t\t\tboxTextLine(\n\t\t\t\twidth,\n\t\t\t\t`${theme.fg(\"success\", `↑${formatTokens(totalInput)}`)} ${theme.fg(\"accent\", `↓${formatTokens(totalOutput)}`)}`,\n\t\t\t),\n\t\t);\n\t\tlines.push(\n\t\t\tboxTextLine(\n\t\t\t\twidth,\n\t\t\t\t`${theme.fg(\"muted\", `R${formatTokens(totalCacheRead)}`)} ${theme.fg(\"muted\", `W${formatTokens(totalCacheWrite)}`)}`,\n\t\t\t),\n\t\t);\n\t\tconst usingSubscription = state.model ? session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tlines.push(\n\t\t\t\tboxTextLine(\n\t\t\t\t\twidth,\n\t\t\t\t\t`${theme.fg(\"muted\", \"cost \")}${theme.fg(\"warning\", `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`)}`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// --- MCP roster (live connectivity + opencode-style stability dots) ---\n\t\tlines.push(...this.mcpSection(width, session, usageLines.length));\n\n\t\t// --- System ---\n\t\tlines.push(sidebarRule(width, \"SYSTEM\"));\n\t\tconst cpu = this.footerData.getCpuPercent();\n\t\tconst mem = this.footerData.getMemoryRssBytes();\n\t\tconst sysParts: string[] = [];\n\t\tif (cpu !== null) sysParts.push(`cpu ${cpu.toFixed(0)}%`);\n\t\tif (mem !== null) sysParts.push(`mem ${formatBytes(mem)}`);\n\t\tif (sysParts.length > 0) {\n\t\t\tlines.push(boxTextLine(width, theme.fg(\"muted\", sysParts.join(\" \"))));\n\t\t}\n\t\tlines.push(boxTextLine(width, theme.fg(\"dim\", formatPackageIntake(this.footerData.getPackageIntakeSummary()))));\n\n\t\t// --- Extension statuses ---\n\t\tconst statuses = this.footerData.getExtensionStatuses();\n\t\tif (statuses.size > 0) {\n\t\t\tlines.push(sidebarRule(width, \"EXT\"));\n\t\t\tfor (const [key, value] of statuses) {\n\t\t\t\tlines.push(boxTextLine(width, `${theme.fg(\"mdCode\", key)} ${theme.fg(\"muted\", value)}`));\n\t\t\t}\n\t\t}\n\n\t\tlines.push(boxTextLine(width, theme.fg(\"dim\", `${keyText(\"app.sidebar.toggle\")} unpin`)));\n\t\tlines.push(boxBottom(width));\n\t\treturn lines;\n\t}\n\n\tprivate refreshSubscriptionUsage(session: AgentSession): void {\n\t\tif (this.subscriptionUsageSession !== session) {\n\t\t\tthis.subscriptionUsage.clear();\n\t\t\tthis.subscriptionUsageFetchedAt.clear();\n\t\t\tthis.subscriptionUsageInFlight.clear();\n\t\t\tthis.subscriptionUsageRevision.clear();\n\t\t\tthis.subscriptionUsageSession = session;\n\t\t}\n\n\t\tconst providers = getConfiguredSubscriptionUsageProviders(session);\n\t\tthis.subscriptionUsageProviders = providers;\n\t\tconst configured = new Set(providers);\n\t\tfor (const provider of this.subscriptionUsage.keys()) {\n\t\t\tif (!configured.has(provider)) {\n\t\t\t\tthis.subscriptionUsage.delete(provider);\n\t\t\t\tthis.subscriptionUsageFetchedAt.delete(provider);\n\t\t\t\tthis.subscriptionUsageRevision.delete(provider);\n\t\t\t}\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tfor (const provider of providers) {\n\t\t\tconst source = getSubscriptionUsageSource(provider);\n\t\t\tconst revision = getSubscriptionUsageRevision(provider);\n\t\t\tconst revisionChanged = revision !== (this.subscriptionUsageRevision.get(provider) ?? 0);\n\t\t\tif (\n\t\t\t\t!source ||\n\t\t\t\tthis.subscriptionUsageInFlight.has(provider) ||\n\t\t\t\t(!revisionChanged && now - (this.subscriptionUsageFetchedAt.get(provider) ?? 0) < source.ttlMs)\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.subscriptionUsageFetchedAt.set(provider, now);\n\t\t\tthis.subscriptionUsageRevision.set(provider, revision);\n\t\t\tthis.subscriptionUsageInFlight.set(provider, session);\n\t\t\tvoid this.fetchSubscriptionUsage(session, provider)\n\t\t\t\t.then((snapshot) => {\n\t\t\t\t\tif (snapshot && this.getSession() === session && this.subscriptionUsageProviders.includes(provider)) {\n\t\t\t\t\t\tthis.subscriptionUsage.set(provider, snapshot);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch(() => {\n\t\t\t\t\tif (this.getSession() === session && this.subscriptionUsageProviders.includes(provider)) {\n\t\t\t\t\t\tthis.subscriptionUsage.set(provider, {\n\t\t\t\t\t\t\tlabel: source.label,\n\t\t\t\t\t\t\twindows: [],\n\t\t\t\t\t\t\tmessage: \"usage unavailable\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\tif (this.subscriptionUsageInFlight.get(provider) === session) {\n\t\t\t\t\t\tthis.subscriptionUsageInFlight.delete(provider);\n\t\t\t\t\t}\n\t\t\t\t\tthis.requestRender();\n\t\t\t\t});\n\t\t}\n\t}\n\n\tprivate subscriptionUsageSection(width: number): string[] {\n\t\tif (this.subscriptionUsageProviders.length === 0) return [];\n\t\tconst lines = [railRule(width, \"USAGE\", theme.fg(\"accent\", String(this.subscriptionUsageProviders.length)))];\n\t\tfor (const provider of this.subscriptionUsageProviders) {\n\t\t\tconst source = getSubscriptionUsageSource(provider);\n\t\t\tconst snapshot = this.subscriptionUsage.get(provider);\n\t\t\tconst label = snapshot?.label ?? source?.label;\n\t\t\tif (!label) continue;\n\t\t\tconst activeEndpoint =\n\t\t\t\tprovider === this.getSession().state.model?.provider\n\t\t\t\t\t? formatEndpointForFooter(this.getSession().state.model?.baseUrl)\n\t\t\t\t\t: undefined;\n\t\t\tif (!snapshot) {\n\t\t\t\tif (this.subscriptionUsageInFlight.has(provider)) {\n\t\t\t\t\tlines.push(boxTextLine(width, `${theme.fg(\"accent\", label)} ${theme.fg(\"dim\", \"loading…\")}`));\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (snapshot.windows.length === 0) {\n\t\t\t\tlines.push(boxTextLine(width, theme.fg(\"accent\", label)));\n\t\t\t\tif (activeEndpoint) lines.push(boxTextLine(width, theme.fg(\"dim\", activeEndpoint)));\n\t\t\t\tlines.push(boxTextLine(width, theme.fg(\"dim\", snapshot.message ?? \"usage unavailable\")));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tlines.push(boxTextLine(width, theme.fg(\"accent\", label)));\n\t\t\tif (activeEndpoint) lines.push(boxTextLine(width, theme.fg(\"dim\", activeEndpoint)));\n\t\t\tfor (const window of snapshot.windows) {\n\t\t\t\tlines.push(boxTextLine(width, usageMeter(window.label, window, width)));\n\t\t\t}\n\t\t\tconst resets = snapshot.windows.flatMap((window) =>\n\t\t\t\twindow.resetsAt === undefined ? [] : [`${window.label} ${formatReset(window.resetsAt)}`],\n\t\t\t);\n\t\t\tif (resets.length > 0) lines.push(boxTextLine(width, theme.fg(\"dim\", `reset ${resets.join(\" · \")}`)));\n\t\t}\n\t\treturn lines.length === 1 ? [] : lines;\n\t}\n\n\t/**\n\t * MCP server roster with a live-connected/total counter in the section\n\t * rule and a per-server state badge. Config inventory reads are cached;\n\t * live status comes from the session's MCP manager each frame.\n\t */\n\tprivate mcpSection(width: number, session: AgentSession, reservedRows = 0): string[] {\n\t\tconst cwd = session.sessionManager.getCwd();\n\t\tconst entries = this.loadMcpEntries(cwd);\n\t\tconst live = new Map(session.mcpServerStatus().map((status) => [status.name, status]));\n\t\tconst ready = [...live.values()].filter((status) => status.state === \"ready\").length;\n\t\tconst anyFailed = [...live.values()].some((status) => status.state === \"failed\");\n\t\tconst stable = entries.filter((entry) => classifyMcpStability(entry) === \"stable\").length;\n\t\tconst countColor: ThemeColor =\n\t\t\tentries.length === 0\n\t\t\t\t? \"dim\"\n\t\t\t\t: live.size > 0\n\t\t\t\t\t? anyFailed\n\t\t\t\t\t\t? \"error\"\n\t\t\t\t\t\t: ready === live.size\n\t\t\t\t\t\t\t? \"success\"\n\t\t\t\t\t\t\t: \"warning\"\n\t\t\t\t\t: stable === entries.length\n\t\t\t\t\t\t? \"success\"\n\t\t\t\t\t\t: \"warning\";\n\t\tconst count =\n\t\t\tentries.length === 0 ? \"0\" : live.size > 0 ? `${ready}/${live.size}` : `${stable}/${entries.length}`;\n\n\t\tconst lines: string[] = [railRule(width, \"MCP\", theme.fg(countColor, count))];\n\t\tif (entries.length === 0) {\n\t\t\tlines.push(boxTextLine(width, theme.fg(\"dim\", \"none configured\")));\n\t\t\treturn lines;\n\t\t}\n\n\t\tconst shown = entries.slice(0, mcpMaxRows(this.getTerminalRows() - reservedRows));\n\t\tfor (const entry of shown) {\n\t\t\tlines.push(boxTextLine(width, mcpRow(entry, width, live.get(entry.name))));\n\t\t}\n\t\tconst hidden = entries.length - shown.length;\n\t\tif (hidden > 0) {\n\t\t\tlines.push(boxTextLine(width, theme.fg(\"dim\", `+${hidden} more…`)));\n\t\t}\n\t\treturn lines;\n\t}\n\n\t/**\n\t * Keep the rail's MCP rows truthful: ping connected servers on a slow\n\t * cadence and retry failed ones even slower. Fire-and-forget from render —\n\t * the 2s metrics repaint supplies the ticks, so no extra timer exists to\n\t * leak. Probing never spawns connections: idle servers stay idle until a\n\t * tool call attaches them (manager lazy-connect design).\n\t */\n\tprivate refreshMcpHealth(session: AgentSession): void {\n\t\tconst now = Date.now();\n\t\tif (this.mcpProbeInFlight) return;\n\t\tconst probeDue = now - this.lastMcpProbeAt >= MCP_HEALTH_INTERVAL_MS;\n\t\tconst reconnectDue = now - this.lastMcpReconnectAt >= MCP_RECONNECT_INTERVAL_MS;\n\t\tif (!probeDue && !reconnectDue) return;\n\t\tif (session.mcpServerStatus().length === 0) return; // no MCP manager attached\n\t\tthis.mcpProbeInFlight = true;\n\t\tthis.lastMcpProbeAt = now;\n\t\tif (reconnectDue) this.lastMcpReconnectAt = now;\n\t\tvoid session\n\t\t\t.mcpCheckHealth({ reconnectFailed: reconnectDue })\n\t\t\t.catch(() => {})\n\t\t\t.finally(() => {\n\t\t\t\tthis.mcpProbeInFlight = false;\n\t\t\t\tthis.requestRender();\n\t\t\t});\n\t}\n\n\tprivate loadMcpEntries(cwd: string): McpServerEntry[] {\n\t\tconst now = Date.now();\n\t\tif (this.mcpCache && this.mcpCache.cwd === cwd && now - this.mcpCache.at < MCP_CACHE_TTL_MS) {\n\t\t\treturn this.mcpCache.entries;\n\t\t}\n\t\tlet entries: McpServerEntry[] = [];\n\t\ttry {\n\t\t\tentries = loadMcpInventory(cwd).entries;\n\t\t} catch {\n\t\t\tentries = [];\n\t\t}\n\t\tthis.mcpCache = { at: now, cwd, entries };\n\t\treturn entries;\n\t}\n\n\t/** Rolling CPU sparkline — a living pulse along the top of the rail. */\n\tprivate sparkline(width: number): string {\n\t\tconst now = Date.now();\n\t\tconst cpu = this.footerData.getCpuPercent();\n\t\tif (cpu !== null && now - this.lastSparkSampleAt >= SPARK_SAMPLE_MS) {\n\t\t\tthis.lastSparkSampleAt = now;\n\t\t\tthis.cpuHistory.push(Math.max(0, Math.min(100, cpu)));\n\t\t\tif (this.cpuHistory.length > SPARK_WINDOW) {\n\t\t\t\tthis.cpuHistory.shift();\n\t\t\t}\n\t\t}\n\n\t\tconst inner = Math.max(0, width - 4);\n\t\tconst label = theme.fg(\"muted\", \"act \");\n\t\tconst cells = Math.max(0, inner - visibleWidth(label));\n\t\tif (cells === 0 || this.cpuHistory.length === 0) {\n\t\t\treturn `${label}${theme.fg(\"borderMuted\", \"▁\".repeat(Math.max(0, cells)))}`;\n\t\t}\n\n\t\t// Right-align the window so the sparkline grows in from the left edge.\n\t\tconst window = this.cpuHistory.slice(-cells);\n\t\tconst bars = window.map((sample) => SPARK_CHARS[Math.min(7, Math.floor(sample / 12.6))]).join(\"\");\n\t\tconst pad = \"▁\".repeat(Math.max(0, cells - window.length));\n\t\treturn `${label}${theme.fg(\"borderMuted\", pad)}${theme.fg(\"accent\", bars)}`;\n\t}\n}\n\n/** Section rule with a right-aligned counter, e.g. `│─ MCP ─────── 22/24 │`. */\nfunction railRule(width: number, label: string, right: string): string {\n\tconst left = `${theme.fg(\"borderMuted\", \"─\")}${theme.fg(\"accent\", ` ${label} `)}`;\n\tconst rightText = ` ${right} `;\n\tconst fill = Math.max(0, width - 2 - visibleWidth(left) - visibleWidth(rightText));\n\tconst line = `${theme.fg(\"borderMuted\", \"│\")}${left}${theme.fg(\"borderMuted\", \"─\".repeat(fill))}${rightText}${theme.fg(\"borderMuted\", \"│\")}`;\n\treturn visibleWidth(line) <= width ? line : truncateToWidth(line, width, \"\");\n}\n\n/** One MCP server row: live-state badge + name + right-aligned detail, truncated to the rail width. */\nfunction mcpRow(entry: McpServerEntry, width: number, live?: McpServerStatus): string {\n\tconst badge = live ? liveMcpBadge(live) : configMcpBadge(entry);\n\tconst safeName = stripAnsi(entry.name)\n\t\t.replace(/[\\t\\r\\n]+/g, \" \")\n\t\t.replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f-\\u009f\\u200b-\\u200f\\u202a-\\u202e\\u2060-\\u206f]/g, \"\")\n\t\t.replace(/\\s+/g, \" \")\n\t\t.trim();\n\tconst detail = live ? liveMcpDetail(live) : \"\";\n\t// frame (4) + dot + space (2); detail is right-aligned with one gap column.\n\tconst detailText = detail ? ` ${detail}` : \"\";\n\tconst nameWidth = Math.max(1, width - 4 - 2 - visibleWidth(detailText));\n\tconst name = truncateToWidth(safeName || \"<unnamed>\", nameWidth, \"…\");\n\tconst pad = Math.max(0, width - 4 - 2 - visibleWidth(name) - visibleWidth(detailText));\n\treturn `${badge.dot} ${theme.fg(badge.nameColor, name)}${\" \".repeat(pad)}${detail ? theme.fg(\"dim\", detailText) : \"\"}`;\n}\n\nfunction configMcpBadge(entry: McpServerEntry): { dot: string; nameColor: ThemeColor } {\n\tconst stability = classifyMcpStability(entry);\n\treturn {\n\t\tdot:\n\t\t\tstability === \"stable\"\n\t\t\t\t? theme.fg(\"success\", \"●\")\n\t\t\t\t: stability === \"overridden\"\n\t\t\t\t\t? theme.fg(\"dim\", \"◐\")\n\t\t\t\t\t: theme.fg(\"warning\", \"○\"),\n\t\tnameColor: stability === \"stable\" ? \"text\" : stability === \"overridden\" ? \"dim\" : \"warning\",\n\t};\n}\n\nfunction liveMcpBadge(status: McpServerStatus): { dot: string; nameColor: ThemeColor } {\n\tswitch (status.state) {\n\t\tcase \"ready\":\n\t\t\treturn { dot: theme.fg(\"success\", \"●\"), nameColor: \"text\" };\n\t\tcase \"connecting\":\n\t\t\treturn { dot: theme.fg(\"warning\", \"◐\"), nameColor: \"text\" };\n\t\tcase \"failed\":\n\t\t\treturn status.error === \"disabled by configuration\"\n\t\t\t\t? { dot: theme.fg(\"dim\", \"○\"), nameColor: \"dim\" }\n\t\t\t\t: { dot: theme.fg(\"error\", \"✕\"), nameColor: \"error\" };\n\t\tdefault:\n\t\t\treturn { dot: theme.fg(\"dim\", \"○\"), nameColor: \"dim\" };\n\t}\n}\n\nfunction liveMcpDetail(status: McpServerStatus): string {\n\tswitch (status.state) {\n\t\tcase \"ready\":\n\t\t\treturn `${status.toolCount}t`;\n\t\tcase \"connecting\":\n\t\t\treturn \"…\";\n\t\tcase \"failed\":\n\t\t\treturn status.error === \"disabled by configuration\" ? \"off\" : \"failed\";\n\t\tdefault:\n\t\t\treturn \"idle\";\n\t}\n}\n\nfunction formatUptime(seconds: number): string {\n\tconst total = Math.max(0, Math.floor(seconds));\n\tconst h = Math.floor(total / 3600);\n\tconst m = Math.floor((total % 3600) / 60);\n\tconst s = total % 60;\n\tconst pad = (n: number) => n.toString().padStart(2, \"0\");\n\treturn h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;\n}\n\nfunction usageMeter(label: string, window: SubscriptionUsageWindow, width: number): string {\n\tconst percent = Math.max(0, Math.min(100, window.usedPercent));\n\tconst percentText = `${Math.round(percent)}%`;\n\tconst labelText = `${label} `;\n\tconst innerWidth = Math.max(0, width - 4);\n\tconst cells = Math.max(4, Math.min(METER_MAX_CELLS, innerWidth - labelText.length - percentText.length - 1));\n\tconst filled = Math.round((percent / 100) * cells);\n\tconst color = usageColor(percent);\n\treturn `${theme.fg(\"muted\", labelText)}${theme.fg(color, \"█\".repeat(filled))}${theme.fg(\"borderMuted\", \"░\".repeat(cells - filled))} ${theme.fg(color, percentText)}`;\n}\n\nfunction usageColor(percent: number): ThemeColor {\n\tif (percent >= 90) return \"error\";\n\tif (percent >= 75) return \"warning\";\n\treturn \"success\";\n}\n\nfunction codexSubscriptionSnapshot(snapshot: CodexUsageSnapshot | undefined): SubscriptionUsageSnapshot | undefined {\n\tif (!snapshot) return undefined;\n\treturn {\n\t\tlabel: \"CODEX\",\n\t\twindows: [\n\t\t\tsnapshot.fiveHour ? { label: \"5H\", ...snapshot.fiveHour } : undefined,\n\t\t\tsnapshot.sevenDay ? { label: \"7D\", ...snapshot.sevenDay } : undefined,\n\t\t].filter((window): window is SubscriptionUsageWindow => window !== undefined),\n\t};\n}\n\nfunction formatReset(resetsAt: number): string {\n\tconst seconds = Math.max(0, Math.ceil(resetsAt - Date.now() / 1000));\n\tif (seconds < 60) return \"now\";\n\tconst totalMinutes = Math.ceil(seconds / 60);\n\tif (totalMinutes < 60) return `${totalMinutes}m`;\n\tif (totalMinutes < 24 * 60) {\n\t\tconst hours = Math.floor(totalMinutes / 60);\n\t\tconst minutes = totalMinutes % 60;\n\t\treturn `${hours}h${minutes > 0 ? `${minutes}m` : \"\"}`;\n\t}\n\tconst totalHours = Math.ceil(totalMinutes / 60);\n\tconst days = Math.floor(totalHours / 24);\n\tconst hours = totalHours % 24;\n\treturn `${days}d${hours > 0 ? `${hours}h` : \"\"}`;\n}\n\nfunction meter(percent: number | null, width: number): string {\n\t// Fill the rail: frame (4) + space + up to \"100%\" (4) stay reserved.\n\tconst cells = Math.max(METER_CELLS, Math.min(METER_MAX_CELLS, width - 9));\n\tif (percent === null) {\n\t\treturn `${theme.fg(\"borderMuted\", \"░\".repeat(cells))} ${theme.fg(\"muted\", \"??%\")}`;\n\t}\n\tconst clamped = Math.max(0, Math.min(100, percent));\n\tconst filled = Math.round((clamped / 100) * cells);\n\tconst color: ThemeColor = clamped >= 85 ? \"warning\" : clamped >= 65 ? \"mdCode\" : \"success\";\n\treturn `${theme.fg(color, \"█\".repeat(filled))}${theme.fg(\"borderMuted\", \"░\".repeat(cells - filled))} ${theme.fg(color, `${Math.round(clamped)}%`)}`;\n}\n"]}
@@ -6,7 +6,7 @@ import { stripAnsi } from "../../../utils/ansi.js";
6
6
  import { theme } from "../theme/theme.js";
7
7
  import { boxBottom, boxTextLine, boxTop, sidebarRule } from "./control-panel-box.js";
8
8
  import { classifyMcpStability } from "./control-panel-runtime-status.js";
9
- import { formatBytes, formatCwdForFooter, formatPackageIntake, formatTokens } from "./footer.js";
9
+ import { formatBytes, formatCwdForFooter, formatEndpointForFooter, formatPackageIntake, formatTokens, } from "./footer.js";
10
10
  import { keyText } from "./keybinding-hints.js";
11
11
  export const STATUS_SIDEBAR_WIDTH = 34;
12
12
  export const STATUS_SIDEBAR_MAX_WIDTH = 48;
@@ -112,6 +112,10 @@ export class StatusSidebarComponent {
112
112
  // --- Model ---
113
113
  lines.push(sidebarRule(width, "MODEL"));
114
114
  lines.push(boxTextLine(width, `${theme.fg("muted", "id ")}${theme.fg("accent", state.model?.id ?? "no-model")}`));
115
+ const endpointHost = formatEndpointForFooter(state.model?.baseUrl);
116
+ if (endpointHost) {
117
+ lines.push(boxTextLine(width, `${theme.fg("muted", "endp ")}${theme.fg("dim", endpointHost)}`));
118
+ }
115
119
  if (state.model?.reasoning) {
116
120
  lines.push(boxTextLine(width, `${theme.fg("muted", "think ")}${theme.fg("mdCode", state.thinkingLevel || "off")}`));
117
121
  }
@@ -243,6 +247,9 @@ export class StatusSidebarComponent {
243
247
  const label = snapshot?.label ?? source?.label;
244
248
  if (!label)
245
249
  continue;
250
+ const activeEndpoint = provider === this.getSession().state.model?.provider
251
+ ? formatEndpointForFooter(this.getSession().state.model?.baseUrl)
252
+ : undefined;
246
253
  if (!snapshot) {
247
254
  if (this.subscriptionUsageInFlight.has(provider)) {
248
255
  lines.push(boxTextLine(width, `${theme.fg("accent", label)} ${theme.fg("dim", "loading…")}`));
@@ -251,10 +258,14 @@ export class StatusSidebarComponent {
251
258
  }
252
259
  if (snapshot.windows.length === 0) {
253
260
  lines.push(boxTextLine(width, theme.fg("accent", label)));
261
+ if (activeEndpoint)
262
+ lines.push(boxTextLine(width, theme.fg("dim", activeEndpoint)));
254
263
  lines.push(boxTextLine(width, theme.fg("dim", snapshot.message ?? "usage unavailable")));
255
264
  continue;
256
265
  }
257
266
  lines.push(boxTextLine(width, theme.fg("accent", label)));
267
+ if (activeEndpoint)
268
+ lines.push(boxTextLine(width, theme.fg("dim", activeEndpoint)));
258
269
  for (const window of snapshot.windows) {
259
270
  lines.push(boxTextLine(width, usageMeter(window.label, window, width)));
260
271
  }