opencode-dux 1.0.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 (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +452 -0
  3. package/dist/agents/descriptions.d.ts +6 -0
  4. package/dist/agents/designer.d.ts +2 -0
  5. package/dist/agents/explorer.d.ts +2 -0
  6. package/dist/agents/fixer.d.ts +2 -0
  7. package/dist/agents/index.d.ts +22 -0
  8. package/dist/agents/interpreter.d.ts +2 -0
  9. package/dist/agents/librarian.d.ts +2 -0
  10. package/dist/agents/oracle.d.ts +2 -0
  11. package/dist/agents/orchestrator.d.ts +27 -0
  12. package/dist/agents/overrides.d.ts +18 -0
  13. package/dist/agents/prompt-blocks.d.ts +97 -0
  14. package/dist/agents/steward.d.ts +3 -0
  15. package/dist/cli/config-io.d.ts +24 -0
  16. package/dist/cli/config-manager.d.ts +4 -0
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +1006 -0
  19. package/dist/cli/install.d.ts +2 -0
  20. package/dist/cli/mcps.d.ts +13 -0
  21. package/dist/cli/model-key-normalization.d.ts +1 -0
  22. package/dist/cli/paths.d.ts +35 -0
  23. package/dist/cli/providers.d.ts +137 -0
  24. package/dist/cli/skills.d.ts +22 -0
  25. package/dist/cli/system.d.ts +5 -0
  26. package/dist/cli/types.d.ts +38 -0
  27. package/dist/config/constants.d.ts +12 -0
  28. package/dist/config/index.d.ts +4 -0
  29. package/dist/config/loader.d.ts +40 -0
  30. package/dist/config/runtime-preset.d.ts +12 -0
  31. package/dist/config/schema.d.ts +281 -0
  32. package/dist/config/utils.d.ts +10 -0
  33. package/dist/discovery/local/types.d.ts +79 -0
  34. package/dist/discovery/local.d.ts +73 -0
  35. package/dist/discovery/mcp-servers.d.ts +88 -0
  36. package/dist/discovery/skills.d.ts +94 -0
  37. package/dist/hooks/apply-patch/codec.d.ts +7 -0
  38. package/dist/hooks/apply-patch/errors.d.ts +25 -0
  39. package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
  40. package/dist/hooks/apply-patch/index.d.ts +15 -0
  41. package/dist/hooks/apply-patch/matching.d.ts +26 -0
  42. package/dist/hooks/apply-patch/operations.d.ts +3 -0
  43. package/dist/hooks/apply-patch/patch.d.ts +2 -0
  44. package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
  45. package/dist/hooks/apply-patch/resolution.d.ts +19 -0
  46. package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
  47. package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
  48. package/dist/hooks/apply-patch/types.d.ts +80 -0
  49. package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
  50. package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
  51. package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
  52. package/dist/hooks/auto-update-checker/index.d.ts +18 -0
  53. package/dist/hooks/auto-update-checker/types.d.ts +22 -0
  54. package/dist/hooks/chat-headers.d.ts +16 -0
  55. package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
  56. package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
  57. package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
  58. package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
  59. package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
  60. package/dist/hooks/filter-available-skills/index.d.ts +32 -0
  61. package/dist/hooks/foreground-fallback/index.d.ts +72 -0
  62. package/dist/hooks/image-hook.d.ts +5 -0
  63. package/dist/hooks/index.d.ts +14 -0
  64. package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
  65. package/dist/hooks/json-error-recovery/index.d.ts +1 -0
  66. package/dist/hooks/phase-reminder/index.d.ts +26 -0
  67. package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
  68. package/dist/hooks/task-session-manager/index.d.ts +52 -0
  69. package/dist/hooks/todo-continuation/index.d.ts +53 -0
  70. package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
  71. package/dist/index.d.ts +5 -0
  72. package/dist/index.js +31782 -0
  73. package/dist/mcp/context7.d.ts +6 -0
  74. package/dist/mcp/grep-app.d.ts +6 -0
  75. package/dist/mcp/index.d.ts +13 -0
  76. package/dist/mcp/types.d.ts +12 -0
  77. package/dist/mcp/websearch.d.ts +9 -0
  78. package/dist/skills/registry.d.ts +29 -0
  79. package/dist/subscriptions/accounts-store.d.ts +57 -0
  80. package/dist/subscriptions/index.d.ts +13 -0
  81. package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
  82. package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
  83. package/dist/subscriptions/types.d.ts +115 -0
  84. package/dist/subscriptions/usage-service.d.ts +74 -0
  85. package/dist/tools/ast-grep/cli.d.ts +15 -0
  86. package/dist/tools/ast-grep/constants.d.ts +25 -0
  87. package/dist/tools/ast-grep/downloader.d.ts +5 -0
  88. package/dist/tools/ast-grep/index.d.ts +10 -0
  89. package/dist/tools/ast-grep/tools.d.ts +3 -0
  90. package/dist/tools/ast-grep/types.d.ts +30 -0
  91. package/dist/tools/ast-grep/utils.d.ts +4 -0
  92. package/dist/tools/delegate.d.ts +14 -0
  93. package/dist/tools/index.d.ts +5 -0
  94. package/dist/tools/preset-manager.d.ts +27 -0
  95. package/dist/tools/smartfetch/binary.d.ts +3 -0
  96. package/dist/tools/smartfetch/cache.d.ts +6 -0
  97. package/dist/tools/smartfetch/constants.d.ts +12 -0
  98. package/dist/tools/smartfetch/index.d.ts +3 -0
  99. package/dist/tools/smartfetch/network.d.ts +38 -0
  100. package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
  101. package/dist/tools/smartfetch/tool.d.ts +3 -0
  102. package/dist/tools/smartfetch/types.d.ts +122 -0
  103. package/dist/tools/smartfetch/utils.d.ts +18 -0
  104. package/dist/tui-state.d.ts +168 -0
  105. package/dist/tui.d.ts +37 -0
  106. package/dist/tui.js +1896 -0
  107. package/dist/utils/agent-variant.d.ts +63 -0
  108. package/dist/utils/compat.d.ts +30 -0
  109. package/dist/utils/env.d.ts +1 -0
  110. package/dist/utils/index.d.ts +9 -0
  111. package/dist/utils/internal-initiator.d.ts +6 -0
  112. package/dist/utils/logger.d.ts +8 -0
  113. package/dist/utils/polling.d.ts +21 -0
  114. package/dist/utils/session-manager.d.ts +55 -0
  115. package/dist/utils/session.d.ts +90 -0
  116. package/dist/utils/subagent-depth.d.ts +35 -0
  117. package/dist/utils/system-collapse.d.ts +6 -0
  118. package/dist/utils/task.d.ts +4 -0
  119. package/dist/utils/zip-extractor.d.ts +1 -0
  120. package/index.ts +1 -0
  121. package/opencode-dux.schema.json +634 -0
  122. package/package.json +103 -0
  123. package/src/agents/descriptions.ts +55 -0
  124. package/src/agents/designer.test.ts +86 -0
  125. package/src/agents/designer.ts +154 -0
  126. package/src/agents/display-name.test.ts +186 -0
  127. package/src/agents/explorer.test.ts +79 -0
  128. package/src/agents/explorer.ts +144 -0
  129. package/src/agents/fixer.test.ts +79 -0
  130. package/src/agents/fixer.ts +145 -0
  131. package/src/agents/index.test.ts +472 -0
  132. package/src/agents/index.ts +248 -0
  133. package/src/agents/interpreter.ts +136 -0
  134. package/src/agents/librarian.test.ts +80 -0
  135. package/src/agents/librarian.ts +145 -0
  136. package/src/agents/oracle.test.ts +89 -0
  137. package/src/agents/oracle.ts +184 -0
  138. package/src/agents/orchestrator.test.ts +116 -0
  139. package/src/agents/orchestrator.ts +574 -0
  140. package/src/agents/overrides.ts +95 -0
  141. package/src/agents/prompt-blocks.test.ts +114 -0
  142. package/src/agents/prompt-blocks.ts +640 -0
  143. package/src/agents/steward.ts +146 -0
  144. package/src/cli/config-io.test.ts +536 -0
  145. package/src/cli/config-io.ts +473 -0
  146. package/src/cli/config-manager.test.ts +141 -0
  147. package/src/cli/config-manager.ts +4 -0
  148. package/src/cli/index.ts +88 -0
  149. package/src/cli/install.ts +282 -0
  150. package/src/cli/mcps.test.ts +62 -0
  151. package/src/cli/mcps.ts +39 -0
  152. package/src/cli/model-key-normalization.test.ts +21 -0
  153. package/src/cli/model-key-normalization.ts +60 -0
  154. package/src/cli/paths.test.ts +167 -0
  155. package/src/cli/paths.ts +144 -0
  156. package/src/cli/providers.test.ts +118 -0
  157. package/src/cli/providers.ts +141 -0
  158. package/src/cli/skills.test.ts +111 -0
  159. package/src/cli/skills.ts +103 -0
  160. package/src/cli/system.test.ts +91 -0
  161. package/src/cli/system.ts +180 -0
  162. package/src/cli/types.ts +43 -0
  163. package/src/config/constants.ts +58 -0
  164. package/src/config/index.ts +4 -0
  165. package/src/config/loader.test.ts +1194 -0
  166. package/src/config/loader.ts +269 -0
  167. package/src/config/model-resolution.test.ts +176 -0
  168. package/src/config/runtime-preset.test.ts +61 -0
  169. package/src/config/runtime-preset.ts +37 -0
  170. package/src/config/schema.ts +248 -0
  171. package/src/config/utils.test.ts +41 -0
  172. package/src/config/utils.ts +23 -0
  173. package/src/discovery/local/types.ts +85 -0
  174. package/src/discovery/local.ts +322 -0
  175. package/src/discovery/mcp-servers.ts +804 -0
  176. package/src/discovery/skills.ts +959 -0
  177. package/src/hooks/apply-patch/codec.test.ts +184 -0
  178. package/src/hooks/apply-patch/codec.ts +352 -0
  179. package/src/hooks/apply-patch/errors.ts +117 -0
  180. package/src/hooks/apply-patch/execution-context.ts +432 -0
  181. package/src/hooks/apply-patch/hook.test.ts +768 -0
  182. package/src/hooks/apply-patch/index.ts +126 -0
  183. package/src/hooks/apply-patch/matching.test.ts +215 -0
  184. package/src/hooks/apply-patch/matching.ts +586 -0
  185. package/src/hooks/apply-patch/operations.test.ts +1535 -0
  186. package/src/hooks/apply-patch/operations.ts +3 -0
  187. package/src/hooks/apply-patch/patch.ts +9 -0
  188. package/src/hooks/apply-patch/prepared-changes.ts +400 -0
  189. package/src/hooks/apply-patch/resolution.test.ts +420 -0
  190. package/src/hooks/apply-patch/resolution.ts +437 -0
  191. package/src/hooks/apply-patch/rewrite.ts +496 -0
  192. package/src/hooks/apply-patch/test-helpers.ts +52 -0
  193. package/src/hooks/apply-patch/types.ts +111 -0
  194. package/src/hooks/auto-update-checker/cache.test.ts +179 -0
  195. package/src/hooks/auto-update-checker/cache.ts +188 -0
  196. package/src/hooks/auto-update-checker/checker.test.ts +159 -0
  197. package/src/hooks/auto-update-checker/checker.ts +308 -0
  198. package/src/hooks/auto-update-checker/constants.ts +33 -0
  199. package/src/hooks/auto-update-checker/index.test.ts +282 -0
  200. package/src/hooks/auto-update-checker/index.ts +225 -0
  201. package/src/hooks/auto-update-checker/types.ts +26 -0
  202. package/src/hooks/chat-headers.test.ts +236 -0
  203. package/src/hooks/chat-headers.ts +97 -0
  204. package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
  205. package/src/hooks/context-pressure-reminder/index.ts +137 -0
  206. package/src/hooks/delegate-task-retry/guidance.ts +41 -0
  207. package/src/hooks/delegate-task-retry/hook.ts +23 -0
  208. package/src/hooks/delegate-task-retry/index.test.ts +38 -0
  209. package/src/hooks/delegate-task-retry/index.ts +7 -0
  210. package/src/hooks/delegate-task-retry/patterns.ts +79 -0
  211. package/src/hooks/filter-available-skills/index.test.ts +297 -0
  212. package/src/hooks/filter-available-skills/index.ts +160 -0
  213. package/src/hooks/foreground-fallback/index.test.ts +624 -0
  214. package/src/hooks/foreground-fallback/index.ts +374 -0
  215. package/src/hooks/image-hook.ts +6 -0
  216. package/src/hooks/index.ts +17 -0
  217. package/src/hooks/json-error-recovery/hook.ts +73 -0
  218. package/src/hooks/json-error-recovery/index.test.ts +111 -0
  219. package/src/hooks/json-error-recovery/index.ts +6 -0
  220. package/src/hooks/phase-reminder/index.test.ts +74 -0
  221. package/src/hooks/phase-reminder/index.ts +85 -0
  222. package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
  223. package/src/hooks/post-file-tool-nudge/index.ts +63 -0
  224. package/src/hooks/task-session-manager/index.test.ts +833 -0
  225. package/src/hooks/task-session-manager/index.ts +434 -0
  226. package/src/hooks/todo-continuation/index.test.ts +3026 -0
  227. package/src/hooks/todo-continuation/index.ts +878 -0
  228. package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
  229. package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
  230. package/src/index.ts +1672 -0
  231. package/src/mcp/context7.ts +14 -0
  232. package/src/mcp/grep-app.ts +11 -0
  233. package/src/mcp/index.test.ts +96 -0
  234. package/src/mcp/index.ts +66 -0
  235. package/src/mcp/types.ts +16 -0
  236. package/src/mcp/websearch.ts +47 -0
  237. package/src/skills/codemap/README.md +60 -0
  238. package/src/skills/codemap/SKILL.md +174 -0
  239. package/src/skills/codemap/scripts/codemap.mjs +483 -0
  240. package/src/skills/codemap/scripts/codemap.test.ts +129 -0
  241. package/src/skills/registry.ts +218 -0
  242. package/src/skills/simplify/README.md +19 -0
  243. package/src/skills/simplify/SKILL.md +138 -0
  244. package/src/subscriptions/accounts-store.test.ts +236 -0
  245. package/src/subscriptions/accounts-store.ts +184 -0
  246. package/src/subscriptions/index.ts +30 -0
  247. package/src/subscriptions/neuralwatt-scraper.ts +108 -0
  248. package/src/subscriptions/opencode-go-scraper.ts +301 -0
  249. package/src/subscriptions/types.ts +145 -0
  250. package/src/subscriptions/usage-service.test.ts +202 -0
  251. package/src/subscriptions/usage-service.ts +651 -0
  252. package/src/tools/ast-grep/cli.ts +257 -0
  253. package/src/tools/ast-grep/constants.ts +214 -0
  254. package/src/tools/ast-grep/downloader.ts +131 -0
  255. package/src/tools/ast-grep/index.ts +24 -0
  256. package/src/tools/ast-grep/tools.ts +117 -0
  257. package/src/tools/ast-grep/types.ts +51 -0
  258. package/src/tools/ast-grep/utils.ts +126 -0
  259. package/src/tools/delegate-handoff.test.ts +18 -0
  260. package/src/tools/delegate.ts +508 -0
  261. package/src/tools/index.ts +8 -0
  262. package/src/tools/preset-manager.test.ts +795 -0
  263. package/src/tools/preset-manager.ts +332 -0
  264. package/src/tools/smartfetch/binary.ts +58 -0
  265. package/src/tools/smartfetch/cache.test.ts +34 -0
  266. package/src/tools/smartfetch/cache.ts +112 -0
  267. package/src/tools/smartfetch/constants.ts +29 -0
  268. package/src/tools/smartfetch/index.ts +8 -0
  269. package/src/tools/smartfetch/network.test.ts +178 -0
  270. package/src/tools/smartfetch/network.ts +614 -0
  271. package/src/tools/smartfetch/secondary-model.test.ts +85 -0
  272. package/src/tools/smartfetch/secondary-model.ts +276 -0
  273. package/src/tools/smartfetch/tool.test.ts +60 -0
  274. package/src/tools/smartfetch/tool.ts +832 -0
  275. package/src/tools/smartfetch/types.ts +135 -0
  276. package/src/tools/smartfetch/utils.test.ts +24 -0
  277. package/src/tools/smartfetch/utils.ts +456 -0
  278. package/src/tui-state.test.ts +867 -0
  279. package/src/tui-state.ts +1255 -0
  280. package/src/tui.test.ts +336 -0
  281. package/src/tui.ts +1539 -0
  282. package/src/utils/agent-variant.test.ts +244 -0
  283. package/src/utils/agent-variant.ts +187 -0
  284. package/src/utils/compat.ts +91 -0
  285. package/src/utils/env.ts +12 -0
  286. package/src/utils/index.ts +9 -0
  287. package/src/utils/internal-initiator.ts +28 -0
  288. package/src/utils/logger.test.ts +220 -0
  289. package/src/utils/logger.ts +136 -0
  290. package/src/utils/polling.test.ts +191 -0
  291. package/src/utils/polling.ts +67 -0
  292. package/src/utils/session-manager.test.ts +173 -0
  293. package/src/utils/session-manager.ts +356 -0
  294. package/src/utils/session.test.ts +110 -0
  295. package/src/utils/session.ts +389 -0
  296. package/src/utils/subagent-depth.test.ts +170 -0
  297. package/src/utils/subagent-depth.ts +75 -0
  298. package/src/utils/system-collapse.test.ts +86 -0
  299. package/src/utils/system-collapse.ts +24 -0
  300. package/src/utils/task.test.ts +24 -0
  301. package/src/utils/task.ts +20 -0
  302. package/src/utils/zip-extractor.ts +102 -0
package/dist/tui.js ADDED
@@ -0,0 +1,1896 @@
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
12
+ var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
20
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
21
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
22
+ for (let key of __getOwnPropNames(mod))
23
+ if (!__hasOwnProp.call(to, key))
24
+ __defProp(to, key, {
25
+ get: __accessProp.bind(mod, key),
26
+ enumerable: true
27
+ });
28
+ if (canCache)
29
+ cache.set(mod, to);
30
+ return to;
31
+ };
32
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
33
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
34
+
35
+ // src/tui.ts
36
+ import { createElement, insert, setProp } from "@opentui/solid";
37
+ import { createSignal } from "solid-js";
38
+
39
+ // src/agents/descriptions.ts
40
+ var AGENT_DESCRIPTIONS = {
41
+ explorer: `<agent name="@explorer">
42
+ - Role: codebase search specialist
43
+ - Delegate when: locate files, usages, symbols, tests, config links
44
+ - Do not use when: exact file is already known and must be read in full
45
+ </agent>`,
46
+ librarian: `<agent name="@librarian">
47
+ - Role: external docs and API reference specialist
48
+ - Delegate when: library behavior, version details, official examples, upstream GitHub issues/PRs/releases
49
+ - Do not use when: pure language fundamentals or local code discovery
50
+ </agent>`,
51
+ oracle: `<agent name="@oracle">
52
+ - Role: technical analysis and code review; uses orchestrator \`model\` + \`variant\` matrix
53
+ - Delegate when: debugging, architecture, tradeoffs, risk, any review depth
54
+ - Do not use when: pure local discovery (@explorer) or docs-only (@librarian)
55
+ </agent>`,
56
+ designer: `<agent name="@designer">
57
+ - Role: UI/UX specialist for ALL user-facing UI — new pages, existing component changes, layout updates, styling, visual polish, a11y
58
+ - Delegate when: ANY change to TSX/JSX files, components, pages, layouts, styling, or user-facing visual elements — BEFORE @oracle or @fixer
59
+ - Do not use when: backend-only, non-visual work, or pure logic changes (hooks/utils) that don't affect rendering
60
+ </agent>`,
61
+ fixer: `<agent name="@fixer">
62
+ - Role: implementation specialist
63
+ - Delegate when: edits, tests, scoped commands-after gates in <first_gate> when applicable
64
+ - Do not use when: strategy/conventions/UI design still unresolved-delegate upward first
65
+ </agent>`,
66
+ steward: `<agent name="@steward">
67
+ - Role: rules citation from steward_paths — verbatim excerpts only; does NOT analyze, evaluate, or compare rules
68
+ - Delegate when: repo conventions needed before oracle/fixer (see <first_gate> 1)
69
+ - Do not use when: pure symbol search (@explorer); rules analysis (@oracle).
70
+ </agent>`,
71
+ interpreter: `<agent name="@interpreter">
72
+ - Role: screenshot / attached-image analyst
73
+ - Delegate when: user message has images and task is not redesign-only
74
+ - Do not use when: redesign-only-use @designer; text-only prompts
75
+ </agent>`
76
+ };
77
+ var AGENT_SIDEBAR_DESCRIPTIONS = {
78
+ orchestrator: "Orchestrates",
79
+ explorer: "File Search",
80
+ librarian: "Doc Search",
81
+ oracle: "Architecture",
82
+ designer: "Design",
83
+ fixer: "Implement",
84
+ steward: "Repo rules",
85
+ interpreter: "Vision"
86
+ };
87
+
88
+ // src/tui-state.ts
89
+ import * as fs from "node:fs";
90
+ import * as os from "node:os";
91
+ import * as path from "node:path";
92
+ var sessionTreeStore = {};
93
+ var SESSION_BUNDLE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
94
+ function emptyBundle(rootSessionId) {
95
+ return {
96
+ rootSessionId,
97
+ lastActivityAt: Date.now(),
98
+ tree: {},
99
+ orchestrationUsageLastSeen: {}
100
+ };
101
+ }
102
+ function normalizeProjectDirectory(raw) {
103
+ return path.normalize(path.resolve(raw));
104
+ }
105
+ function mergedSessionTree(snapshot) {
106
+ const out = {};
107
+ for (const bundle of Object.values(snapshot.sessions)) {
108
+ Object.assign(out, bundle.tree);
109
+ }
110
+ return out;
111
+ }
112
+ function deriveSessionContextPct(used, limit) {
113
+ if (!(limit > 0))
114
+ return 0;
115
+ if (!(Number.isFinite(used) && Number.isFinite(limit)))
116
+ return 0;
117
+ const safeUsed = Math.max(0, used);
118
+ return Math.max(0, Math.min(100, safeUsed / limit * 100));
119
+ }
120
+ function coerceSessionUsageEntry(raw) {
121
+ if (!raw || typeof raw !== "object")
122
+ return;
123
+ return {
124
+ contextUsed: typeof raw.contextUsed === "number" ? Math.max(0, raw.contextUsed) : 0,
125
+ contextLimit: typeof raw.contextLimit === "number" ? Math.max(0, raw.contextLimit) : 0,
126
+ contextPct: typeof raw.contextPct === "number" ? Math.max(0, Math.min(100, raw.contextPct)) : 0,
127
+ input: typeof raw.input === "number" ? Math.max(0, raw.input) : 0,
128
+ output: typeof raw.output === "number" ? Math.max(0, raw.output) : 0,
129
+ reasoning: typeof raw.reasoning === "number" ? Math.max(0, raw.reasoning) : 0,
130
+ cacheRead: typeof raw.cacheRead === "number" ? Math.max(0, raw.cacheRead) : 0,
131
+ cacheWrite: typeof raw.cacheWrite === "number" ? Math.max(0, raw.cacheWrite) : 0,
132
+ updatedAt: typeof raw.updatedAt === "number" ? Math.max(0, raw.updatedAt) : 0
133
+ };
134
+ }
135
+ function mergedSessionUsage(snapshot) {
136
+ const out = {};
137
+ for (const bundle of Object.values(snapshot.sessions)) {
138
+ for (const [sid, node] of Object.entries(bundle.tree)) {
139
+ if (node.usage === undefined)
140
+ continue;
141
+ const usage = coerceSessionUsageEntry(node.usage);
142
+ if (usage)
143
+ out[sid] = usage;
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+ function mergedSessionModels(snapshot) {
149
+ const out = {};
150
+ const tree = mergedSessionTree(snapshot);
151
+ for (const [sid, node] of Object.entries(tree)) {
152
+ if (node.model)
153
+ out[sid] = node.model;
154
+ }
155
+ return out;
156
+ }
157
+ function mergedOrchestrationSigmaAccum(snapshot) {
158
+ const out = {};
159
+ for (const [rootId, bundle] of Object.entries(snapshot.sessions)) {
160
+ if (bundle.orchestrationSigmaAccum) {
161
+ out[rootId] = bundle.orchestrationSigmaAccum;
162
+ }
163
+ }
164
+ return out;
165
+ }
166
+ function touchBundle(bundle) {
167
+ bundle.lastActivityAt = Date.now();
168
+ }
169
+ function locateBundleForSession(snapshot, sessionID) {
170
+ for (const [rootId, bundle] of Object.entries(snapshot.sessions)) {
171
+ if (bundle.tree[sessionID])
172
+ return { rootId, bundle };
173
+ }
174
+ return;
175
+ }
176
+ function mapOpenCodeStatusToTreeStatus(raw) {
177
+ const t = raw.trim().toLowerCase();
178
+ if (t === "idle")
179
+ return "idle";
180
+ if (t === "retry")
181
+ return "retry";
182
+ if (t === "busy")
183
+ return "busy";
184
+ return "busy";
185
+ }
186
+ function applyOpenCodeSessionStatus(snapshot, sessionID, rawType) {
187
+ const mapped = mapOpenCodeStatusToTreeStatus(rawType);
188
+ const hit = locateBundleForSession(snapshot, sessionID);
189
+ if (hit) {
190
+ hit.bundle.tree[sessionID].status = mapped;
191
+ touchBundle(hit.bundle);
192
+ sessionTreeStore[sessionID] = hit.bundle.tree[sessionID];
193
+ return;
194
+ }
195
+ const store = sessionTreeStore[sessionID];
196
+ if (store)
197
+ store.status = mapped;
198
+ }
199
+ function syncOpenCodeStatusesIntoSessionTree(snapshot, statuses) {
200
+ for (const [sid, row] of Object.entries(statuses)) {
201
+ applyOpenCodeSessionStatus(snapshot, sid, row.type);
202
+ }
203
+ }
204
+ function upwardRootFrom(mergedTree, startSessionId) {
205
+ let cur = startSessionId;
206
+ const visited = new Set;
207
+ while (!visited.has(cur)) {
208
+ visited.add(cur);
209
+ const parent = mergedTree[cur]?.parentId;
210
+ if (!parent)
211
+ break;
212
+ cur = parent;
213
+ }
214
+ return cur;
215
+ }
216
+ function resolveBundleRootForSession(snapshot, sessionID, explicitParentId) {
217
+ const merged = mergedSessionTree(snapshot);
218
+ if (!explicitParentId) {
219
+ if (merged[sessionID])
220
+ return upwardRootFrom(merged, sessionID);
221
+ return sessionID;
222
+ }
223
+ return upwardRootFrom(merged, explicitParentId);
224
+ }
225
+ function ensureBundle(snapshot, rootSessionId) {
226
+ let bundle = snapshot.sessions[rootSessionId];
227
+ if (!bundle) {
228
+ bundle = emptyBundle(rootSessionId);
229
+ snapshot.sessions[rootSessionId] = bundle;
230
+ touchBundle(bundle);
231
+ }
232
+ return bundle;
233
+ }
234
+ function getOrCreateTreeNode(bundle, sessionID) {
235
+ const merged = sessionTreeStore[sessionID] ?? bundle.tree[sessionID] ?? {
236
+ title: "",
237
+ agent: "",
238
+ model: "",
239
+ childIds: [],
240
+ status: "busy",
241
+ createdAt: Date.now()
242
+ };
243
+ bundle.tree[sessionID] = merged;
244
+ sessionTreeStore[sessionID] = merged;
245
+ return merged;
246
+ }
247
+ function deleteBundleCascade(snapshot, rootSessionId) {
248
+ const bundle = snapshot.sessions[rootSessionId];
249
+ if (!bundle)
250
+ return new Set;
251
+ const removedIds = new Set(Object.keys(bundle.tree));
252
+ delete snapshot.sessions[rootSessionId];
253
+ return removedIds;
254
+ }
255
+ function pruneSessionSidDataInBundle(bundle, sid) {
256
+ const node = bundle.tree[sid];
257
+ if (node) {
258
+ const needsFlashStart = node.status !== "idle" || node.finishedAt === undefined;
259
+ node.status = "idle";
260
+ if (needsFlashStart) {
261
+ node.finishedAt = Date.now();
262
+ }
263
+ delete node.usage;
264
+ }
265
+ delete bundle.orchestrationUsageLastSeen[sid];
266
+ }
267
+ function normalizedBundleProjectForSession(snapshot, sessionID) {
268
+ const hit = locateBundleForSession(snapshot, sessionID);
269
+ if (!hit?.bundle.projectPath)
270
+ return;
271
+ return normalizeProjectDirectory(hit.bundle.projectPath);
272
+ }
273
+ function expandMissingSessionCascade(mergedTree, seeds) {
274
+ const ids = new Set(seeds);
275
+ let added = true;
276
+ while (added) {
277
+ added = false;
278
+ for (const [sid, node] of Object.entries(mergedTree)) {
279
+ if (ids.has(sid))
280
+ continue;
281
+ const parentId = node.parentId;
282
+ if (parentId && ids.has(parentId)) {
283
+ ids.add(sid);
284
+ added = true;
285
+ }
286
+ }
287
+ }
288
+ return ids;
289
+ }
290
+ function isStrictDescendantInMergedTree(mergedTree, ancestorId, descendantCandidate) {
291
+ if (ancestorId === descendantCandidate)
292
+ return false;
293
+ let cur = descendantCandidate;
294
+ const visited = new Set;
295
+ while (cur && !visited.has(cur)) {
296
+ visited.add(cur);
297
+ if (cur === ancestorId)
298
+ return true;
299
+ cur = mergedTree[cur]?.parentId;
300
+ }
301
+ return false;
302
+ }
303
+ function softPruneTargetHasPollDescendant(mergedTree, targetSid, opencodeIds) {
304
+ for (const pollId of opencodeIds) {
305
+ if (isStrictDescendantInMergedTree(mergedTree, targetSid, pollId)) {
306
+ return true;
307
+ }
308
+ }
309
+ return false;
310
+ }
311
+ function pruneStaleTuiSessionBundles(snapshot, input) {
312
+ const strippedFromFile = new Set;
313
+ for (const rootId of Object.keys(snapshot.sessions)) {
314
+ const bundle = snapshot.sessions[rootId];
315
+ if (!bundle)
316
+ continue;
317
+ if (bundle.lastActivityAt > 0 && input.now - bundle.lastActivityAt >= SESSION_BUNDLE_RETENTION_MS) {
318
+ for (const id of deleteBundleCascade(snapshot, rootId)) {
319
+ strippedFromFile.add(id);
320
+ }
321
+ }
322
+ }
323
+ const projectMatched = normalizeProjectDirectory(input.currentProjectDir);
324
+ if (input.opencodeIds.size > 0) {
325
+ for (const rootId of [...Object.keys(snapshot.sessions)]) {
326
+ const bundle = snapshot.sessions[rootId];
327
+ if (!bundle?.projectPath)
328
+ continue;
329
+ if (normalizeProjectDirectory(bundle.projectPath) !== projectMatched) {
330
+ continue;
331
+ }
332
+ const treeIds = Object.keys(bundle.tree);
333
+ if (treeIds.length === 0)
334
+ continue;
335
+ if (treeIds.every((id) => !input.opencodeIds.has(id))) {
336
+ for (const id of deleteBundleCascade(snapshot, rootId)) {
337
+ strippedFromFile.add(id);
338
+ }
339
+ }
340
+ }
341
+ }
342
+ const merged = mergedSessionTree(snapshot);
343
+ const missingSeeds = input.opencodeIds.size > 0 ? Object.keys(merged).filter((id) => !input.opencodeIds.has(id)) : [];
344
+ const expandedMissing = expandMissingSessionCascade(merged, missingSeeds);
345
+ for (const sid of expandedMissing) {
346
+ if (input.opencodeIds.has(sid))
347
+ continue;
348
+ if (softPruneTargetHasPollDescendant(merged, sid, input.opencodeIds)) {
349
+ continue;
350
+ }
351
+ const projected = normalizedBundleProjectForSession(snapshot, sid);
352
+ if (projected === undefined || projected !== projectMatched)
353
+ continue;
354
+ const located = locateBundleForSession(snapshot, sid);
355
+ if (!located)
356
+ continue;
357
+ const { bundle, rootId } = located;
358
+ pruneSessionSidDataInBundle(bundle, sid);
359
+ strippedFromFile.add(sid);
360
+ if (located.bundle.tree[sid]?.agent === "orchestrator" && sid === rootId) {
361
+ delete bundle.orchestrationSigmaAccum;
362
+ }
363
+ touchBundle(bundle);
364
+ }
365
+ return strippedFromFile;
366
+ }
367
+ var STATE_DIR = "opencode-dux";
368
+ var STATE_FILE = "tui-state.json";
369
+ function dataDir() {
370
+ return process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share");
371
+ }
372
+ function getTuiStatePath() {
373
+ return path.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
374
+ }
375
+ function emptySnapshot() {
376
+ return {
377
+ version: 6,
378
+ updatedAt: Date.now(),
379
+ sessions: {},
380
+ subscriptionUsage: {},
381
+ activeSubscriptionByProvider: {}
382
+ };
383
+ }
384
+ function normalizeSubscriptionUsage(usage) {
385
+ return usage;
386
+ }
387
+ function normalizeSigmaAccum(value) {
388
+ if (!value)
389
+ return;
390
+ return {
391
+ contextUsed: typeof value.contextUsed === "number" ? Math.max(0, value.contextUsed) : 0,
392
+ input: typeof value.input === "number" ? Math.max(0, value.input) : 0,
393
+ output: typeof value.output === "number" ? Math.max(0, value.output) : 0,
394
+ cacheRead: typeof value.cacheRead === "number" ? Math.max(0, value.cacheRead) : 0,
395
+ cacheWrite: typeof value.cacheWrite === "number" ? Math.max(0, value.cacheWrite) : 0
396
+ };
397
+ }
398
+ function normalizeUsageLastSeen(value) {
399
+ const result = {};
400
+ for (const [sessionID, entry] of Object.entries(value)) {
401
+ if (!entry)
402
+ continue;
403
+ result[sessionID] = {
404
+ contextUsed: typeof entry.contextUsed === "number" ? Math.max(0, entry.contextUsed) : 0,
405
+ input: typeof entry.input === "number" ? Math.max(0, entry.input) : 0,
406
+ output: typeof entry.output === "number" ? Math.max(0, entry.output) : 0,
407
+ cacheRead: typeof entry.cacheRead === "number" ? Math.max(0, entry.cacheRead) : 0,
408
+ cacheWrite: typeof entry.cacheWrite === "number" ? Math.max(0, entry.cacheWrite) : 0
409
+ };
410
+ }
411
+ return result;
412
+ }
413
+ function hydrateTreeUsages(tree) {
414
+ for (const node of Object.values(tree)) {
415
+ if (node.usage === undefined || node.usage === null)
416
+ continue;
417
+ const u = coerceSessionUsageEntry(node.usage);
418
+ if (u)
419
+ node.usage = u;
420
+ else
421
+ delete node.usage;
422
+ }
423
+ }
424
+ function parseSessionBundles(raw) {
425
+ const out = {};
426
+ if (!raw || typeof raw !== "object")
427
+ return out;
428
+ const entries = Object.entries(raw);
429
+ for (const [rootId, value] of entries) {
430
+ if (!value || typeof value !== "object")
431
+ continue;
432
+ const v = value;
433
+ const tree = v.tree && typeof v.tree === "object" ? v.tree : {};
434
+ hydrateTreeUsages(tree);
435
+ const lastActivityAt = typeof v.lastActivityAt === "number" ? v.lastActivityAt : Date.now();
436
+ const projectPath = typeof v.projectPath === "string" && v.projectPath.length > 0 ? normalizeProjectDirectory(v.projectPath) : undefined;
437
+ const bundle = {
438
+ rootSessionId: typeof v.rootSessionId === "string" && v.rootSessionId.length > 0 ? v.rootSessionId : rootId,
439
+ lastActivityAt,
440
+ projectPath,
441
+ tree,
442
+ orchestrationSigmaAccum: normalizeSigmaAccum(v.orchestrationSigmaAccum && typeof v.orchestrationSigmaAccum === "object" ? v.orchestrationSigmaAccum : undefined),
443
+ orchestrationUsageLastSeen: normalizeUsageLastSeen(typeof v.orchestrationUsageLastSeen === "object" && v.orchestrationUsageLastSeen ? v.orchestrationUsageLastSeen : {})
444
+ };
445
+ out[rootId] = bundle;
446
+ }
447
+ return out;
448
+ }
449
+ function parseSnapshot(value) {
450
+ try {
451
+ const parsed = JSON.parse(value);
452
+ if (parsed?.version !== 6)
453
+ return null;
454
+ const activeSubscriptionByProvider = {};
455
+ if (parsed.activeSubscriptionByProvider) {
456
+ for (const provider of ["opencode-go", "neuralwatt"]) {
457
+ const name = parsed.activeSubscriptionByProvider[provider];
458
+ if (typeof name === "string" && name.length > 0) {
459
+ activeSubscriptionByProvider[provider] = name;
460
+ }
461
+ }
462
+ }
463
+ return {
464
+ version: 6,
465
+ updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
466
+ sessions: parseSessionBundles(parsed.sessions ?? {}),
467
+ subscriptionUsage: normalizeSubscriptionUsage(typeof parsed.subscriptionUsage === "object" && parsed.subscriptionUsage ? parsed.subscriptionUsage : {}),
468
+ activeSubscriptionByProvider
469
+ };
470
+ } catch {
471
+ return null;
472
+ }
473
+ }
474
+ function tryReadSnapshot() {
475
+ const filePath = getTuiStatePath();
476
+ try {
477
+ const parsed = parseSnapshot(fs.readFileSync(filePath, "utf8"));
478
+ if (parsed) {
479
+ return { snapshot: parsed, okForMutation: true };
480
+ }
481
+ return { snapshot: emptySnapshot(), okForMutation: false };
482
+ } catch (error) {
483
+ if (error.code === "ENOENT") {
484
+ return { snapshot: emptySnapshot(), okForMutation: true };
485
+ }
486
+ return { snapshot: emptySnapshot(), okForMutation: false };
487
+ }
488
+ }
489
+ function readTuiSnapshot() {
490
+ return tryReadSnapshot().snapshot;
491
+ }
492
+ async function readTuiSnapshotAsync() {
493
+ try {
494
+ const parsed = parseSnapshot(await fs.promises.readFile(getTuiStatePath(), "utf8"));
495
+ return parsed ?? emptySnapshot();
496
+ } catch {
497
+ return emptySnapshot();
498
+ }
499
+ }
500
+ function writeTuiSnapshot(snapshot) {
501
+ try {
502
+ const filePath = getTuiStatePath();
503
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
504
+ fs.writeFileSync(filePath, `${JSON.stringify(snapshot)}
505
+ `);
506
+ } catch {}
507
+ }
508
+ var isDrainingSnapshot = false;
509
+ var snapshotMutatorQueue = [];
510
+ function updateSnapshot(mutator) {
511
+ snapshotMutatorQueue.push(mutator);
512
+ if (isDrainingSnapshot) {
513
+ return;
514
+ }
515
+ isDrainingSnapshot = true;
516
+ try {
517
+ while (snapshotMutatorQueue.length > 0) {
518
+ try {
519
+ const { snapshot, okForMutation } = tryReadSnapshot();
520
+ if (!okForMutation) {
521
+ snapshotMutatorQueue.length = 0;
522
+ break;
523
+ }
524
+ while (snapshotMutatorQueue.length > 0) {
525
+ const m = snapshotMutatorQueue.shift();
526
+ if (m === undefined) {
527
+ break;
528
+ }
529
+ m(snapshot);
530
+ }
531
+ snapshot.updatedAt = Date.now();
532
+ writeTuiSnapshot(snapshot);
533
+ } catch {
534
+ snapshotMutatorQueue.length = 0;
535
+ break;
536
+ }
537
+ }
538
+ } finally {
539
+ isDrainingSnapshot = false;
540
+ }
541
+ }
542
+ function applyRecordSessionUsageToSnapshot(snapshot, input) {
543
+ let bundle;
544
+ const located = locateBundleForSession(snapshot, input.sessionID);
545
+ if (located)
546
+ bundle = located.bundle;
547
+ else {
548
+ const rootFallback = resolveBundleRootForSession(snapshot, input.sessionID);
549
+ bundle = ensureBundle(snapshot, rootFallback);
550
+ }
551
+ const node = getOrCreateTreeNode(bundle, input.sessionID);
552
+ const prev = coerceSessionUsageEntry(node.usage);
553
+ const nextContextUsed = input.contextUsed !== undefined ? Math.max(0, input.contextUsed) : prev?.contextUsed ?? 0;
554
+ const nextContextLimit = input.contextLimit != null && input.contextLimit > 0 ? input.contextLimit : prev?.contextLimit ?? 0;
555
+ const next = {
556
+ contextUsed: nextContextUsed,
557
+ contextLimit: nextContextLimit,
558
+ contextPct: deriveSessionContextPct(nextContextUsed, nextContextLimit),
559
+ input: input.input !== undefined ? Math.max(prev?.input ?? 0, input.input) : prev?.input ?? 0,
560
+ output: input.output !== undefined ? Math.max(prev?.output ?? 0, input.output) : prev?.output ?? 0,
561
+ reasoning: input.reasoning !== undefined ? Math.max(prev?.reasoning ?? 0, input.reasoning) : prev?.reasoning ?? 0,
562
+ cacheRead: input.cacheRead !== undefined ? Math.max(prev?.cacheRead ?? 0, input.cacheRead) : prev?.cacheRead ?? 0,
563
+ cacheWrite: input.cacheWrite !== undefined ? Math.max(prev?.cacheWrite ?? 0, input.cacheWrite) : prev?.cacheWrite ?? 0,
564
+ updatedAt: Date.now()
565
+ };
566
+ node.usage = next;
567
+ touchBundle(bundle);
568
+ const rootSessionID = resolveOrchestrationRootSessionID(snapshot, input.sessionID);
569
+ if (!rootSessionID)
570
+ return;
571
+ const orchBundle = locateBundleForSession(snapshot, rootSessionID);
572
+ if (!orchBundle)
573
+ return;
574
+ const previousSeen = orchBundle.bundle.orchestrationUsageLastSeen[input.sessionID] ?? {
575
+ contextUsed: 0,
576
+ input: 0,
577
+ output: 0,
578
+ cacheRead: 0,
579
+ cacheWrite: 0
580
+ };
581
+ const nextSeen = {
582
+ contextUsed: next.contextUsed,
583
+ input: next.input,
584
+ output: next.output,
585
+ cacheRead: next.cacheRead,
586
+ cacheWrite: next.cacheWrite
587
+ };
588
+ const deltaContextUsed = Math.max(0, nextSeen.contextUsed - previousSeen.contextUsed);
589
+ const deltaInput = Math.max(0, nextSeen.input - previousSeen.input);
590
+ const deltaOutput = Math.max(0, nextSeen.output - previousSeen.output);
591
+ const deltaCacheRead = Math.max(0, nextSeen.cacheRead - previousSeen.cacheRead);
592
+ const deltaCacheWrite = Math.max(0, nextSeen.cacheWrite - previousSeen.cacheWrite);
593
+ const prevAccum = orchBundle.bundle.orchestrationSigmaAccum ?? {
594
+ contextUsed: 0,
595
+ input: 0,
596
+ output: 0,
597
+ cacheRead: 0,
598
+ cacheWrite: 0
599
+ };
600
+ orchBundle.bundle.orchestrationSigmaAccum = {
601
+ contextUsed: prevAccum.contextUsed + deltaContextUsed,
602
+ input: prevAccum.input + deltaInput,
603
+ output: prevAccum.output + deltaOutput,
604
+ cacheRead: prevAccum.cacheRead + deltaCacheRead,
605
+ cacheWrite: prevAccum.cacheWrite + deltaCacheWrite
606
+ };
607
+ orchBundle.bundle.orchestrationUsageLastSeen[input.sessionID] = nextSeen;
608
+ touchBundle(orchBundle.bundle);
609
+ }
610
+ function recordSessionUsagesBatch(inputs) {
611
+ if (inputs.length === 0)
612
+ return;
613
+ updateSnapshot((snapshot) => {
614
+ for (const input of inputs) {
615
+ applyRecordSessionUsageToSnapshot(snapshot, input);
616
+ }
617
+ });
618
+ }
619
+ function recordDelegatedSubagentSession(input) {
620
+ updateSnapshot((snapshot) => {
621
+ const rootId = resolveBundleRootForSession(snapshot, input.sessionID, input.parentSessionId);
622
+ const bundle = ensureBundle(snapshot, rootId);
623
+ const existing = sessionTreeStore[input.sessionID] ?? bundle.tree[input.sessionID] ?? {
624
+ title: "",
625
+ agent: "",
626
+ model: "",
627
+ childIds: [],
628
+ status: "busy",
629
+ createdAt: Date.now()
630
+ };
631
+ const node = {
632
+ ...existing,
633
+ title: existing.title,
634
+ agent: input.agent || existing.agent,
635
+ model: existing.model,
636
+ variant: input.variant !== undefined ? input.variant : existing.variant,
637
+ parentId: input.parentSessionId,
638
+ mode: input.mode !== undefined ? input.mode : existing.mode,
639
+ status: existing.status,
640
+ createdAt: existing.createdAt
641
+ };
642
+ bundle.tree[input.sessionID] = node;
643
+ sessionTreeStore[input.sessionID] = node;
644
+ touchBundle(bundle);
645
+ for (const b of Object.values(snapshot.sessions)) {
646
+ const parent = b.tree[input.parentSessionId];
647
+ if (!parent)
648
+ continue;
649
+ if (!parent.childIds.includes(input.sessionID)) {
650
+ parent.childIds.push(input.sessionID);
651
+ }
652
+ b.lastActivityAt = Date.now();
653
+ }
654
+ const storeParent = sessionTreeStore[input.parentSessionId];
655
+ if (storeParent && !storeParent.childIds.includes(input.sessionID)) {
656
+ storeParent.childIds.push(input.sessionID);
657
+ }
658
+ });
659
+ }
660
+ function recordChildSessionSnapshot(input) {
661
+ updateSnapshot((snapshot) => {
662
+ const rootId = resolveBundleRootForSession(snapshot, input.sessionID, input.parentSessionId);
663
+ const bundle = ensureBundle(snapshot, rootId);
664
+ const existing = sessionTreeStore[input.sessionID] ?? bundle.tree[input.sessionID] ?? {
665
+ title: "",
666
+ agent: "",
667
+ model: "",
668
+ childIds: [],
669
+ status: "busy",
670
+ createdAt: Date.now()
671
+ };
672
+ const node = {
673
+ ...existing,
674
+ title: input.title ?? existing.title,
675
+ agent: existing.agent,
676
+ model: existing.model,
677
+ variant: existing.variant,
678
+ parentId: input.parentSessionId !== undefined ? input.parentSessionId : existing.parentId,
679
+ mode: existing.mode,
680
+ status: existing.status,
681
+ createdAt: existing.createdAt
682
+ };
683
+ bundle.tree[input.sessionID] = node;
684
+ sessionTreeStore[input.sessionID] = node;
685
+ touchBundle(bundle);
686
+ if (input.parentSessionId) {
687
+ for (const b of Object.values(snapshot.sessions)) {
688
+ const parent = b.tree[input.parentSessionId];
689
+ if (!parent)
690
+ continue;
691
+ if (!parent.childIds.includes(input.sessionID)) {
692
+ parent.childIds.push(input.sessionID);
693
+ }
694
+ b.lastActivityAt = Date.now();
695
+ }
696
+ const storeParent = sessionTreeStore[input.parentSessionId];
697
+ if (storeParent && !storeParent.childIds.includes(input.sessionID)) {
698
+ storeParent.childIds.push(input.sessionID);
699
+ }
700
+ }
701
+ if (input.projectPath !== undefined && input.projectPath.length > 0) {
702
+ const normalized = normalizeProjectDirectory(input.projectPath);
703
+ const rootForProject = resolveBundleRootForSession(snapshot, input.sessionID);
704
+ const projectBundle = ensureBundle(snapshot, rootForProject);
705
+ projectBundle.projectPath = normalized;
706
+ touchBundle(projectBundle);
707
+ }
708
+ });
709
+ }
710
+ function patchSessionTreeStatusFromOpenCode(sessionID, rawType) {
711
+ updateSnapshot((snapshot) => {
712
+ applyOpenCodeSessionStatus(snapshot, sessionID, rawType);
713
+ });
714
+ }
715
+ function recordSessionEnd(sessionID) {
716
+ updateSnapshot((snapshot) => {
717
+ const located = locateBundleForSession(snapshot, sessionID);
718
+ const node = located?.bundle.tree[sessionID] ?? sessionTreeStore[sessionID];
719
+ if (node)
720
+ delete node.usage;
721
+ if (located)
722
+ touchBundle(located.bundle);
723
+ });
724
+ }
725
+ function recordSessionModel(input) {
726
+ updateSnapshot((snapshot) => {
727
+ const rootId = resolveBundleRootForSession(snapshot, input.sessionID);
728
+ const bundle = ensureBundle(snapshot, rootId);
729
+ const node = getOrCreateTreeNode(bundle, input.sessionID);
730
+ node.model = input.model;
731
+ touchBundle(bundle);
732
+ });
733
+ }
734
+ function recordSessionVariant(input) {
735
+ updateSnapshot((snapshot) => {
736
+ const rootId = resolveBundleRootForSession(snapshot, input.sessionID);
737
+ const bundle = ensureBundle(snapshot, rootId);
738
+ const node = getOrCreateTreeNode(bundle, input.sessionID);
739
+ node.variant = input.variant;
740
+ touchBundle(bundle);
741
+ });
742
+ }
743
+ function recordSessionNode(input) {
744
+ updateSnapshot((snapshot) => {
745
+ const rootId = resolveBundleRootForSession(snapshot, input.sessionID, input.parentId);
746
+ const bundle = ensureBundle(snapshot, rootId);
747
+ const existing = sessionTreeStore[input.sessionID] ?? bundle.tree[input.sessionID] ?? {
748
+ title: "",
749
+ agent: "",
750
+ model: "",
751
+ childIds: [],
752
+ status: "busy",
753
+ createdAt: Date.now()
754
+ };
755
+ const node = {
756
+ ...existing,
757
+ title: input.title !== undefined ? input.title : existing.title,
758
+ agent: input.agent || existing.agent,
759
+ model: input.model ?? existing.model,
760
+ variant: input.variant !== undefined ? input.variant : existing.variant,
761
+ parentId: input.parentId !== undefined ? input.parentId : existing.parentId,
762
+ mode: input.mode !== undefined ? input.mode : existing.mode,
763
+ status: input.status ?? existing.status,
764
+ createdAt: existing.createdAt
765
+ };
766
+ bundle.tree[input.sessionID] = node;
767
+ sessionTreeStore[input.sessionID] = node;
768
+ touchBundle(bundle);
769
+ });
770
+ }
771
+ function recordSessionTitle(input) {
772
+ const trimmed = input.title.trim();
773
+ if (!trimmed)
774
+ return;
775
+ updateSnapshot((snapshot) => {
776
+ const hit = locateBundleForSession(snapshot, input.sessionID);
777
+ if (hit) {
778
+ const node2 = hit.bundle.tree[input.sessionID];
779
+ if (node2) {
780
+ node2.title = trimmed;
781
+ sessionTreeStore[input.sessionID] = node2;
782
+ touchBundle(hit.bundle);
783
+ }
784
+ return;
785
+ }
786
+ const rootId = resolveBundleRootForSession(snapshot, input.sessionID);
787
+ const bundle = ensureBundle(snapshot, rootId);
788
+ const node = getOrCreateTreeNode(bundle, input.sessionID);
789
+ node.title = trimmed;
790
+ touchBundle(bundle);
791
+ });
792
+ }
793
+ function recordSessionDone(sessionID) {
794
+ updateSnapshot((snapshot) => {
795
+ const hit = locateBundleForSession(snapshot, sessionID);
796
+ if (hit) {
797
+ const node = hit.bundle.tree[sessionID];
798
+ if (node) {
799
+ node.status = "idle";
800
+ node.finishedAt = Date.now();
801
+ }
802
+ touchBundle(hit.bundle);
803
+ }
804
+ const storeNode = sessionTreeStore[sessionID];
805
+ if (storeNode) {
806
+ storeNode.status = "idle";
807
+ storeNode.finishedAt = Date.now();
808
+ }
809
+ });
810
+ }
811
+ function resolveOrchestrationRootSessionID(snapshot, sessionID) {
812
+ const merged = mergedSessionTree(snapshot);
813
+ let currentID = sessionID;
814
+ const visited = new Set;
815
+ while (currentID && !visited.has(currentID)) {
816
+ visited.add(currentID);
817
+ const treeNode = merged[currentID];
818
+ if (!treeNode)
819
+ return null;
820
+ if (treeNode.agent === "orchestrator")
821
+ return currentID;
822
+ currentID = treeNode.parentId;
823
+ }
824
+ return null;
825
+ }
826
+ function recordSessionUsage(input) {
827
+ updateSnapshot((snapshot) => {
828
+ applyRecordSessionUsageToSnapshot(snapshot, input);
829
+ });
830
+ }
831
+ function subscriptionUsageKey(provider, accountName) {
832
+ return `${provider}\x00${accountName}`;
833
+ }
834
+ function recordSubscriptionUsage(usage) {
835
+ updateSnapshot((snapshot) => {
836
+ snapshot.subscriptionUsage = {};
837
+ for (const entry of usage) {
838
+ if (entry.accountName) {
839
+ snapshot.subscriptionUsage[subscriptionUsageKey(entry.provider, entry.accountName)] = entry;
840
+ }
841
+ }
842
+ });
843
+ }
844
+ function removeSubscriptionUsageEntry(provider, name) {
845
+ updateSnapshot((snapshot) => {
846
+ delete snapshot.subscriptionUsage[subscriptionUsageKey(provider, name)];
847
+ });
848
+ }
849
+ function recordSessionProject(input) {
850
+ const normalized = normalizeProjectDirectory(input.projectPath);
851
+ updateSnapshot((snapshot) => {
852
+ const rootId = resolveBundleRootForSession(snapshot, input.sessionID);
853
+ const bundle = ensureBundle(snapshot, rootId);
854
+ bundle.projectPath = normalized;
855
+ touchBundle(bundle);
856
+ });
857
+ }
858
+ function deleteSessionEntries(sessionID) {
859
+ updateSnapshot((snapshot) => {
860
+ const located = locateBundleForSession(snapshot, sessionID);
861
+ if (!located)
862
+ return;
863
+ const { bundle, rootId } = located;
864
+ delete bundle.orchestrationUsageLastSeen[sessionID];
865
+ if (sessionID === rootId) {
866
+ for (const id of deleteBundleCascade(snapshot, rootId)) {
867
+ delete sessionTreeStore[id];
868
+ }
869
+ return;
870
+ }
871
+ const node = bundle.tree[sessionID];
872
+ const parentId = node?.parentId;
873
+ delete bundle.tree[sessionID];
874
+ delete sessionTreeStore[sessionID];
875
+ if (parentId) {
876
+ const parent = bundle.tree[parentId];
877
+ if (parent) {
878
+ parent.childIds = parent.childIds.filter((c) => c !== sessionID);
879
+ }
880
+ const storeParent = sessionTreeStore[parentId];
881
+ if (storeParent?.childIds) {
882
+ storeParent.childIds = storeParent.childIds.filter((c) => c !== sessionID);
883
+ }
884
+ }
885
+ if (Object.keys(bundle.tree).length === 0) {
886
+ delete snapshot.sessions[rootId];
887
+ return;
888
+ }
889
+ touchBundle(bundle);
890
+ });
891
+ }
892
+ function recordActiveSubscriptionForProvider(provider, name) {
893
+ updateSnapshot((snapshot) => {
894
+ if (name) {
895
+ snapshot.activeSubscriptionByProvider[provider] = name;
896
+ } else {
897
+ delete snapshot.activeSubscriptionByProvider[provider];
898
+ }
899
+ });
900
+ }
901
+
902
+ // src/tui.ts
903
+ var PLUGIN_NAME = "opencode-dux";
904
+ var BORDER = { type: "single" };
905
+ var SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
906
+ var AGENT_SORT_PRIORITY = {
907
+ orchestrator: 0,
908
+ explorer: 1,
909
+ librarian: 2,
910
+ steward: 3,
911
+ fixer: 4,
912
+ oracle: 5,
913
+ designer: 6,
914
+ interpreter: 7
915
+ };
916
+ var SIDEBAR_MODEL_DISPLAY_MAX = 20;
917
+ var ORCH_ROOT_TITLE_DISPLAY_MAX = 22;
918
+ var ORCH_ROOT_SESSION_ID_DISPLAY_MAX = 27;
919
+ var ORCH_ROOT_MODEL_DISPLAY_MAX = 28;
920
+ var ORCH_CHILD_MODEL_DISPLAY_MAX = 22;
921
+ var ORCH_DEFAULT_TITLE_LABEL = "New session";
922
+ function element(tag, props, children = []) {
923
+ const node = createElement(tag);
924
+ for (const [key, value] of Object.entries(props)) {
925
+ if (value !== undefined)
926
+ setProp(node, key, value);
927
+ }
928
+ for (const child of children) {
929
+ if (child === null || child === undefined || child === false)
930
+ continue;
931
+ insert(node, child);
932
+ }
933
+ return node;
934
+ }
935
+ function text(props, children) {
936
+ return element("text", props, children);
937
+ }
938
+ function box(props, children = []) {
939
+ return element("box", props, children);
940
+ }
941
+ function truncate(value, max = 24) {
942
+ return value.length > max ? `${value.slice(0, max - 1)}…` : value;
943
+ }
944
+ function formatTokenAbbrev(value) {
945
+ if (!Number.isFinite(value) || value <= 0)
946
+ return "0";
947
+ if (value < 1000)
948
+ return Math.round(value).toString();
949
+ if (value < 1e6) {
950
+ const k = Math.round(value / 1000);
951
+ if (k >= 1000)
952
+ return `${Math.round(value / 1e6)}M`;
953
+ return `${k}K`;
954
+ }
955
+ return `${Math.round(value / 1e6)}M`;
956
+ }
957
+ function formatTokenAbbrevDecimal(value) {
958
+ if (!Number.isFinite(value) || value <= 0)
959
+ return "0";
960
+ if (value < 1000)
961
+ return Math.round(value).toString();
962
+ if (value < 1e6) {
963
+ return `${(value / 1000).toFixed(1)}K`;
964
+ }
965
+ return `${(value / 1e6).toFixed(1)}M`;
966
+ }
967
+ function formatTokenExact(value) {
968
+ if (!Number.isFinite(value) || value <= 0)
969
+ return "0";
970
+ return new Intl.NumberFormat("en-US").format(Math.round(value));
971
+ }
972
+ function formatSidebarModelName(model) {
973
+ const lastSlash = model.lastIndexOf("/");
974
+ return lastSlash === -1 ? model : model.slice(lastSlash + 1);
975
+ }
976
+ var ELLIPSIS_CHAR = "…";
977
+ function truncateModelBasenameByHyphenSegments(name, maxTotalLen) {
978
+ if (name.length <= maxTotalLen)
979
+ return name;
980
+ const budget = maxTotalLen - ELLIPSIS_CHAR.length;
981
+ if (budget <= 0)
982
+ return truncate(name, maxTotalLen);
983
+ const parts = name.split("-").filter((p) => p.length > 0);
984
+ if (parts.length === 0)
985
+ return truncate(name, maxTotalLen);
986
+ const head = parts[0];
987
+ if (parts.length === 1) {
988
+ return head ? truncate(head, maxTotalLen) : truncate(name, maxTotalLen);
989
+ }
990
+ if (!head)
991
+ return truncate(name, maxTotalLen);
992
+ if (head.length > budget)
993
+ return truncate(head, maxTotalLen);
994
+ let acc = head;
995
+ for (let i = 1;i < parts.length; i++) {
996
+ const piece = parts[i];
997
+ if (!piece)
998
+ continue;
999
+ const next = `${acc}-${piece}`;
1000
+ if (next.length > budget)
1001
+ break;
1002
+ acc = next;
1003
+ }
1004
+ if (acc.length >= name.length)
1005
+ return name;
1006
+ return `${acc}${ELLIPSIS_CHAR}`;
1007
+ }
1008
+ function formatSidebarModelAndVariant(rawModel, variant, maxModelDisplayLen = SIDEBAR_MODEL_DISPLAY_MAX) {
1009
+ const name = rawModel ? formatSidebarModelName(rawModel) : "";
1010
+ const extraVariant = variant?.trim() ?? "";
1011
+ if (!name)
1012
+ return extraVariant;
1013
+ const modelShown = truncateModelBasenameByHyphenSegments(name, maxModelDisplayLen);
1014
+ if (!extraVariant)
1015
+ return modelShown;
1016
+ return `${modelShown} - ${extraVariant}`;
1017
+ }
1018
+ function formatAgentName(name) {
1019
+ if (name.length <= 16)
1020
+ return name;
1021
+ return `${name.slice(0, 13)}...`;
1022
+ }
1023
+ function formatDuration(ms) {
1024
+ if (!Number.isFinite(ms) || ms < 0)
1025
+ return "0:00";
1026
+ const totalSeconds = Math.floor(ms / 1000);
1027
+ const hours = Math.floor(totalSeconds / 3600);
1028
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
1029
+ const seconds = totalSeconds % 60;
1030
+ if (hours > 0) {
1031
+ return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
1032
+ }
1033
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
1034
+ }
1035
+ function formatSessionUsageRows(snapshot, sessionID, options) {
1036
+ const abbreviateLeft = options?.abbreviateLeft ?? false;
1037
+ const usage = mergedSessionUsage(snapshot)[sessionID];
1038
+ const contextUsed = usage?.contextUsed ?? 0;
1039
+ const contextLimit = usage?.contextLimit ?? 0;
1040
+ const contextPct = Math.round(deriveSessionContextPct(contextUsed, contextLimit));
1041
+ const inputTotal = usage?.input ?? 0;
1042
+ const outputTotal = usage?.output ?? 0;
1043
+ const cacheRead = usage?.cacheRead ?? 0;
1044
+ const cacheWrite = usage?.cacheWrite ?? 0;
1045
+ const cacheTotal = cacheRead + cacheWrite;
1046
+ return {
1047
+ contextPct,
1048
+ ctxLabel: "CTX",
1049
+ ctxValue: `${abbreviateLeft ? formatTokenAbbrevDecimal(contextUsed) : formatTokenExact(contextUsed)}/${abbreviateLeft ? formatTokenAbbrev(contextLimit) : formatTokenExact(contextLimit)} (${contextPct}%)`,
1050
+ ioInputAbbrev: formatTokenAbbrev(inputTotal),
1051
+ ioOutputAbbrev: formatTokenAbbrev(outputTotal),
1052
+ cacheLabel: "CACHE",
1053
+ cacheValue: formatTokenExact(cacheTotal),
1054
+ cacheReadAbbrev: formatTokenExact(cacheRead),
1055
+ cacheWriteAbbrev: formatTokenExact(cacheWrite)
1056
+ };
1057
+ }
1058
+ function aggregateOrchestrationUsage(snapshot, rootSessionID) {
1059
+ const accum = mergedOrchestrationSigmaAccum(snapshot)[rootSessionID];
1060
+ if (!accum) {
1061
+ return {
1062
+ inputTotal: 0,
1063
+ outputTotal: 0,
1064
+ cacheRead: 0,
1065
+ cacheWrite: 0,
1066
+ contextUsed: 0
1067
+ };
1068
+ }
1069
+ return {
1070
+ inputTotal: accum.input,
1071
+ outputTotal: accum.output,
1072
+ cacheRead: accum.cacheRead,
1073
+ cacheWrite: accum.cacheWrite,
1074
+ contextUsed: accum.contextUsed
1075
+ };
1076
+ }
1077
+ function getSidebarAgentNames(snapshot) {
1078
+ const names = Object.keys(AGENT_SIDEBAR_DESCRIPTIONS);
1079
+ return names.sort((a, b) => {
1080
+ const pa = AGENT_SORT_PRIORITY[a] ?? 99;
1081
+ const pb = AGENT_SORT_PRIORITY[b] ?? 99;
1082
+ if (pa !== pb)
1083
+ return pa - pb;
1084
+ return a.localeCompare(b);
1085
+ });
1086
+ }
1087
+ function formatUsageTime(iso) {
1088
+ const diff = new Date(iso).getTime() - Date.now();
1089
+ if (diff <= 0)
1090
+ return "now";
1091
+ const totalMin = Math.ceil(diff / 60000);
1092
+ const hours = Math.floor(totalMin / 60);
1093
+ const minutes = totalMin % 60;
1094
+ if (hours > 24) {
1095
+ const days = Math.floor(hours / 24);
1096
+ return `${days}d ${hours % 24}h`;
1097
+ }
1098
+ if (hours > 0)
1099
+ return `${hours}h ${minutes}m`;
1100
+ return `${minutes}m`;
1101
+ }
1102
+ function neuralwattTokensFormatted(tokens) {
1103
+ if (!Number.isFinite(tokens))
1104
+ return "0";
1105
+ return Math.trunc(tokens).toLocaleString("en-US");
1106
+ }
1107
+ function pushNeuralwattMonthlyTokensRow(rows, theme, u) {
1108
+ rows.push(box({ width: "100%", flexDirection: "row" }, [
1109
+ text({ fg: theme.textMuted }, [
1110
+ ` ${neuralwattTokensFormatted(u.current_month.tokens)} Tokens this month`
1111
+ ])
1112
+ ]));
1113
+ }
1114
+ var BAR_WIDTH = 18;
1115
+ var SIGMA_TOTAL_COLOR = "#F5B041";
1116
+ var METRIC_PAIR_GAP = " ";
1117
+ function renderMetricPairRight(leftIcon, leftValue, rightIcon, rightValue, colors) {
1118
+ return box({ flexDirection: "row", flexShrink: 0 }, [
1119
+ text({ fg: colors.leftFg }, [`${leftIcon} ${leftValue}`]),
1120
+ text({ fg: colors.gapFg }, [METRIC_PAIR_GAP]),
1121
+ text({ fg: colors.rightFg }, [`${rightIcon} ${rightValue}`])
1122
+ ]);
1123
+ }
1124
+ function renderUsageBar(percent) {
1125
+ const filled = Math.round(percent / 100 * BAR_WIDTH);
1126
+ const empty = BAR_WIDTH - filled;
1127
+ return "█".repeat(filled) + "░".repeat(empty);
1128
+ }
1129
+ function getUsageColor(percentRemaining) {
1130
+ if (percentRemaining < 25)
1131
+ return "#E74C3C";
1132
+ if (percentRemaining < 50)
1133
+ return "#F39C12";
1134
+ return "";
1135
+ }
1136
+ function renderOpenCodeGoBars(entry, rows, theme) {
1137
+ const windows = [];
1138
+ if (entry.rolling)
1139
+ windows.push({ label: "R", w: entry.rolling });
1140
+ if (entry.weekly)
1141
+ windows.push({ label: "W", w: entry.weekly });
1142
+ if (entry.monthly)
1143
+ windows.push({ label: "M", w: entry.monthly });
1144
+ for (let i = 0;i < windows.length; i++) {
1145
+ const { label, w } = windows[i];
1146
+ if (!w)
1147
+ continue;
1148
+ const usageColor = getUsageColor(w.percentRemaining);
1149
+ const bar = renderUsageBar(w.percentRemaining);
1150
+ const pct = w.percentRemaining.toFixed(0).padStart(3);
1151
+ const timeLeft = formatUsageTime(w.resetTimeIso);
1152
+ rows.push(box({
1153
+ width: "100%",
1154
+ flexDirection: "row",
1155
+ justifyContent: "space-between"
1156
+ }, [
1157
+ box({ flexDirection: "row" }, [
1158
+ text({ fg: theme.accent }, [`${label} `]),
1159
+ text({ fg: usageColor || theme.text }, [bar]),
1160
+ text({ fg: usageColor || theme.textMuted }, [` ${pct}%`])
1161
+ ]),
1162
+ text({ fg: theme.textMuted }, [timeLeft])
1163
+ ]));
1164
+ }
1165
+ }
1166
+ function renderNeuralwattUsage(entry, rows, theme) {
1167
+ const { subscription, balance, usage: u } = entry;
1168
+ if (subscription && subscription.status === "active") {
1169
+ const kwhIncluded = subscription.kwh_included ?? 0;
1170
+ const kwhUsed = subscription.kwh_used ?? 0;
1171
+ const kwhRemaining = subscription.kwh_remaining ?? 0;
1172
+ if (kwhIncluded > 0) {
1173
+ const kwhPct = Math.min(kwhUsed / kwhIncluded * 100, 100);
1174
+ const bar = renderUsageBar(100 - kwhPct);
1175
+ const remaining = kwhRemaining.toFixed(1);
1176
+ const resetTime = subscription.current_period_end ? formatUsageTime(subscription.current_period_end) : "";
1177
+ const color = kwhPct > 90 ? "#E74C3C" : kwhPct > 75 ? "#F39C12" : "";
1178
+ rows.push(box({
1179
+ width: "100%",
1180
+ flexDirection: "row",
1181
+ justifyContent: "space-between"
1182
+ }, [
1183
+ box({ flexDirection: "row" }, [
1184
+ text({ fg: theme.accent }, ["⚡ "]),
1185
+ text({ fg: color || theme.text }, [bar]),
1186
+ text({ fg: color || theme.textMuted }, [` ${remaining}kWh`])
1187
+ ]),
1188
+ text({ fg: theme.textMuted }, [resetTime])
1189
+ ]));
1190
+ }
1191
+ rows.push(box({
1192
+ width: "100%",
1193
+ flexDirection: "row",
1194
+ justifyContent: "space-between"
1195
+ }, [
1196
+ text({ fg: theme.textMuted }, [
1197
+ ` $${u.current_month.cost_usd.toFixed(2)} this month`
1198
+ ]),
1199
+ text({ fg: theme.textMuted }, [
1200
+ `⚡ ${u.current_month.energy_kwh.toFixed(1)} kWh`
1201
+ ])
1202
+ ]));
1203
+ pushNeuralwattMonthlyTokensRow(rows, theme, u);
1204
+ } else if (subscription && subscription.status !== "active") {
1205
+ const statusColor = subscription.status === "past_due" || subscription.status === "canceling" ? "#E74C3C" : "#F39C12";
1206
+ rows.push(box({
1207
+ width: "100%",
1208
+ flexDirection: "row",
1209
+ justifyContent: "space-between"
1210
+ }, [
1211
+ text({ fg: statusColor }, [` Status: ${subscription.status}`]),
1212
+ text({ fg: theme.textMuted }, [
1213
+ `⚡ ${u.current_month.energy_kwh.toFixed(1)} kWh`
1214
+ ])
1215
+ ]));
1216
+ const kwhIncluded = subscription.kwh_included ?? 0;
1217
+ const kwhUsed = subscription.kwh_used ?? 0;
1218
+ const kwhRemaining = subscription.kwh_remaining ?? 0;
1219
+ if (kwhIncluded > 0) {
1220
+ const kwhPct = Math.min(kwhUsed / kwhIncluded * 100, 100);
1221
+ const bar = renderUsageBar(100 - kwhPct);
1222
+ const remaining = kwhRemaining.toFixed(1);
1223
+ const resetTime = subscription.current_period_end ? formatUsageTime(subscription.current_period_end) : "";
1224
+ const color = kwhPct > 90 ? "#E74C3C" : kwhPct > 75 ? "#F39C12" : "";
1225
+ rows.push(box({
1226
+ width: "100%",
1227
+ flexDirection: "row",
1228
+ justifyContent: "space-between"
1229
+ }, [
1230
+ box({ flexDirection: "row" }, [
1231
+ text({ fg: theme.accent }, ["⚡ "]),
1232
+ text({ fg: color || theme.text }, [bar]),
1233
+ text({ fg: color || theme.textMuted }, [` ${remaining}kWh`])
1234
+ ]),
1235
+ text({ fg: theme.textMuted }, [resetTime])
1236
+ ]));
1237
+ }
1238
+ if (balance.credits_remaining_usd > 0) {
1239
+ rows.push(box({
1240
+ width: "100%",
1241
+ flexDirection: "row",
1242
+ justifyContent: "space-between"
1243
+ }, [
1244
+ text({ fg: theme.textMuted }, [
1245
+ ` \uD83D\uDCB0 $${balance.credits_remaining_usd.toFixed(2)} remaining`
1246
+ ]),
1247
+ text({ fg: theme.textMuted }, [
1248
+ `$${u.current_month.cost_usd.toFixed(2)}/mo`
1249
+ ])
1250
+ ]));
1251
+ }
1252
+ pushNeuralwattMonthlyTokensRow(rows, theme, u);
1253
+ } else {
1254
+ rows.push(box({
1255
+ width: "100%",
1256
+ flexDirection: "row",
1257
+ justifyContent: "space-between"
1258
+ }, [
1259
+ text({ fg: theme.text }, [
1260
+ `\uD83D\uDCB0 $${balance.credits_remaining_usd.toFixed(2)} remaining`
1261
+ ]),
1262
+ text({ fg: theme.textMuted }, [
1263
+ `⚡ ${u.current_month.energy_kwh.toFixed(3)} kWh/mo`
1264
+ ])
1265
+ ]));
1266
+ rows.push(box({
1267
+ width: "100%",
1268
+ flexDirection: "row",
1269
+ justifyContent: "space-between"
1270
+ }, [
1271
+ text({ fg: theme.textMuted }, [
1272
+ ` $${u.current_month.cost_usd.toFixed(2)} this month`
1273
+ ])
1274
+ ]));
1275
+ pushNeuralwattMonthlyTokensRow(rows, theme, u);
1276
+ }
1277
+ }
1278
+ function renderSubscriptionPanel(snapshot, theme) {
1279
+ const usage = snapshot.subscriptionUsage ?? {};
1280
+ const usageEntries = Object.entries(usage).sort(([, a], [, b]) => {
1281
+ if (a.provider !== b.provider)
1282
+ return a.provider.localeCompare(b.provider);
1283
+ return a.accountName.localeCompare(b.accountName);
1284
+ });
1285
+ if (usageEntries.length === 0)
1286
+ return [];
1287
+ const rows = [];
1288
+ let isFirstAccount = true;
1289
+ for (const [, entry] of usageEntries) {
1290
+ const name = entry.accountName;
1291
+ const activeName = snapshot.activeSubscriptionByProvider?.[entry.provider];
1292
+ const isActive = activeName === name;
1293
+ const providerLabel = entry.provider === "neuralwatt" ? " [nw]" : " [go]";
1294
+ if (!isFirstAccount) {
1295
+ rows.push(box({ width: "100%", height: 1 }));
1296
+ }
1297
+ isFirstAccount = false;
1298
+ if (entry.error) {
1299
+ rows.push(box({ width: "100%", flexDirection: "row" }, [
1300
+ text(isActive ? { fg: theme.accent } : { fg: theme.text }, [
1301
+ isActive ? `★ ${truncate(name, 18)}${providerLabel}` : `${truncate(name, 16)}${providerLabel}`
1302
+ ]),
1303
+ text({ fg: theme.textMuted }, [" ⚠️"])
1304
+ ]));
1305
+ rows.push(text({ fg: theme.textMuted }, [` ${truncate(entry.error, 56)}`]));
1306
+ continue;
1307
+ }
1308
+ const displayName = isActive ? `★ ${truncate(name, 18)}${providerLabel}` : `${truncate(name, 16)}${providerLabel}`;
1309
+ rows.push(box({ width: "100%", flexDirection: "row" }, [
1310
+ text(isActive ? { fg: theme.accent } : { fg: theme.text }, [
1311
+ displayName
1312
+ ])
1313
+ ]));
1314
+ if (entry.provider === "opencode-go") {
1315
+ renderOpenCodeGoBars(entry, rows, theme);
1316
+ } else if (entry.provider === "neuralwatt") {
1317
+ renderNeuralwattUsage(entry, rows, theme);
1318
+ } else {
1319
+ rows.push(text({ fg: "#F39C12" }, [
1320
+ " ⚠️ Provider field missing - re-add account with /subscriptions"
1321
+ ]));
1322
+ }
1323
+ }
1324
+ return rows;
1325
+ }
1326
+ var FLASH_DURATION_MS = 2000;
1327
+ function getStatusText(snapshot, sessionID) {
1328
+ return mergedSessionTree(snapshot)[sessionID]?.status ?? "-";
1329
+ }
1330
+ function getStatusWithDuration(snapshot, sessionID, node, now) {
1331
+ const status = getStatusText(snapshot, sessionID);
1332
+ if (node.status === "busy" || node.status === "retry") {
1333
+ const elapsed = now - node.createdAt;
1334
+ return `${status} (${formatDuration(elapsed)})`;
1335
+ }
1336
+ return status;
1337
+ }
1338
+ function getSpinnerChar(now) {
1339
+ return SPINNER_FRAMES[Math.floor(now / 80) % SPINNER_FRAMES.length];
1340
+ }
1341
+ function getStatusColor(status, theme) {
1342
+ const normalized = status.trim();
1343
+ if (normalized === "busy" || normalized.startsWith("busy "))
1344
+ return theme.accent;
1345
+ if (normalized === "retry" || normalized.startsWith("retry "))
1346
+ return theme.error ?? "#EF4444";
1347
+ if (status === "idle")
1348
+ return theme.textMuted;
1349
+ return theme.text;
1350
+ }
1351
+ function splitStatusAndTimer(full) {
1352
+ const m = full.match(/^(\S+)\s+(\([^)]+\))$/);
1353
+ if (!m)
1354
+ return null;
1355
+ return { status: m[1], timer: full.slice(m[1].length) };
1356
+ }
1357
+ function renderStatusLineWithOptionalTimer(full, theme) {
1358
+ const split = splitStatusAndTimer(full);
1359
+ if (!split) {
1360
+ return text({ fg: getStatusColor(full, theme) }, [full]);
1361
+ }
1362
+ return box({ flexDirection: "row", flexShrink: 0 }, [
1363
+ text({ fg: getStatusColor(split.status, theme) }, [split.status]),
1364
+ text({ fg: theme.text }, [split.timer])
1365
+ ]);
1366
+ }
1367
+ function buildOrchestratingRows(snapshot, now, theme) {
1368
+ const tree = mergedSessionTree(snapshot);
1369
+ const usageBySession = mergedSessionUsage(snapshot);
1370
+ const spinner = getSpinnerChar(now);
1371
+ const isVisibleSession = (node) => {
1372
+ if (node.status === "busy" || node.status === "retry")
1373
+ return true;
1374
+ if (node.status !== "idle" || !node.finishedAt)
1375
+ return false;
1376
+ return now - node.finishedAt < FLASH_DURATION_MS + 1000;
1377
+ };
1378
+ const getVisibleChildren = (parentID) => Object.entries(tree).filter(([, child]) => child.parentId === parentID && isVisibleSession(child));
1379
+ const pushUsageRows = (rows2, sessionID, prefix, abbreviateLeft) => {
1380
+ const metrics = formatSessionUsageRows(snapshot, sessionID, {
1381
+ abbreviateLeft
1382
+ });
1383
+ const isChild = !!tree[sessionID]?.parentId;
1384
+ if (isChild) {
1385
+ rows2.push(box({ width: "100%", flexDirection: "row" }, [
1386
+ text({ fg: theme.textMuted }, [prefix]),
1387
+ text({ fg: theme.accent }, [`${metrics.ctxLabel} `]),
1388
+ text({ fg: theme.text }, [metrics.ctxValue])
1389
+ ]));
1390
+ const cacheTotalForRow = (usageBySession[sessionID]?.cacheRead ?? 0) + (usageBySession[sessionID]?.cacheWrite ?? 0);
1391
+ rows2.push(box({ width: "100%", flexDirection: "row" }, [
1392
+ text({ fg: theme.textMuted }, [prefix]),
1393
+ text({ fg: theme.accent }, [`${metrics.cacheLabel} `]),
1394
+ text({ fg: theme.text }, [formatTokenExact(cacheTotalForRow)])
1395
+ ]));
1396
+ rows2.push(box({ width: "100%", flexDirection: "row" }, [
1397
+ text({ fg: theme.textMuted }, [prefix]),
1398
+ renderMetricPairRight("↓", `Input ${metrics.ioInputAbbrev}`, "↑", `Output ${metrics.ioOutputAbbrev}`, {
1399
+ leftFg: "#5DADE2",
1400
+ rightFg: "#58D68D",
1401
+ gapFg: theme.textMuted
1402
+ })
1403
+ ]));
1404
+ rows2.push(box({ width: "100%", flexDirection: "row" }, [
1405
+ text({ fg: theme.textMuted }, [prefix]),
1406
+ renderMetricPairRight("\uD83D\uDCD6", `Read ${metrics.cacheReadAbbrev}`, "\uD83D\uDCDD", `Write ${metrics.cacheWriteAbbrev}`, {
1407
+ leftFg: "#5DADE2",
1408
+ rightFg: "#AF7AC5",
1409
+ gapFg: theme.textMuted
1410
+ })
1411
+ ]));
1412
+ } else {
1413
+ rows2.push(box({
1414
+ width: "100%",
1415
+ flexDirection: "row",
1416
+ justifyContent: "space-between"
1417
+ }, [
1418
+ box({ flexDirection: "row" }, [
1419
+ text({ fg: theme.textMuted }, [prefix]),
1420
+ text({ fg: theme.accent }, [`${metrics.ctxLabel} `]),
1421
+ text({ fg: theme.text }, [metrics.ctxValue])
1422
+ ]),
1423
+ renderMetricPairRight("↓", metrics.ioInputAbbrev, "↑", metrics.ioOutputAbbrev, {
1424
+ leftFg: "#5DADE2",
1425
+ rightFg: "#58D68D",
1426
+ gapFg: theme.textMuted
1427
+ })
1428
+ ]));
1429
+ rows2.push(box({
1430
+ width: "100%",
1431
+ flexDirection: "row",
1432
+ justifyContent: "space-between"
1433
+ }, [
1434
+ box({ flexDirection: "row" }, [
1435
+ text({ fg: theme.textMuted }, [prefix]),
1436
+ text({ fg: theme.accent }, [`${metrics.cacheLabel} `]),
1437
+ text({ fg: theme.text }, [metrics.cacheValue])
1438
+ ]),
1439
+ renderMetricPairRight("\uD83D\uDCD6", metrics.cacheReadAbbrev, "\uD83D\uDCDD", metrics.cacheWriteAbbrev, {
1440
+ leftFg: "#5DADE2",
1441
+ rightFg: "#AF7AC5",
1442
+ gapFg: theme.textMuted
1443
+ })
1444
+ ]));
1445
+ }
1446
+ };
1447
+ const pushAggregateRows = (rows2, sessionID, prefix) => {
1448
+ const totals = aggregateOrchestrationUsage(snapshot, sessionID);
1449
+ const totalIo = totals.contextUsed;
1450
+ const totalCache = totals.cacheRead + totals.cacheWrite;
1451
+ const isChild = !!tree[sessionID]?.parentId;
1452
+ if (isChild) {
1453
+ rows2.push(box({ width: "100%", flexDirection: "row" }, [
1454
+ text({ fg: theme.textMuted }, [prefix]),
1455
+ text({ fg: SIGMA_TOTAL_COLOR }, ["Σ TOTAL "]),
1456
+ text({ fg: theme.text }, [formatTokenExact(totalIo)])
1457
+ ]));
1458
+ rows2.push(box({ width: "100%", flexDirection: "row" }, [
1459
+ text({ fg: theme.textMuted }, [prefix]),
1460
+ text({ fg: SIGMA_TOTAL_COLOR }, ["Σ CACHE "]),
1461
+ text({ fg: theme.text }, [formatTokenAbbrev(totalCache)])
1462
+ ]));
1463
+ rows2.push(box({ width: "100%", flexDirection: "row" }, [
1464
+ text({ fg: theme.textMuted }, [prefix]),
1465
+ renderMetricPairRight("↓", `Input ${formatTokenAbbrev(totals.inputTotal)}`, "↑", `Output ${formatTokenAbbrev(totals.outputTotal)}`, {
1466
+ leftFg: "#5DADE2",
1467
+ rightFg: "#58D68D",
1468
+ gapFg: theme.textMuted
1469
+ })
1470
+ ]));
1471
+ rows2.push(box({ width: "100%", flexDirection: "row" }, [
1472
+ text({ fg: theme.textMuted }, [prefix]),
1473
+ renderMetricPairRight("\uD83D\uDCD6", `Read ${formatTokenAbbrev(totals.cacheRead)}`, "\uD83D\uDCDD", `Write ${formatTokenAbbrev(totals.cacheWrite)}`, {
1474
+ leftFg: "#5DADE2",
1475
+ rightFg: "#AF7AC5",
1476
+ gapFg: theme.textMuted
1477
+ })
1478
+ ]));
1479
+ } else {
1480
+ rows2.push(box({
1481
+ width: "100%",
1482
+ flexDirection: "row",
1483
+ justifyContent: "space-between"
1484
+ }, [
1485
+ box({ flexDirection: "row" }, [
1486
+ text({ fg: theme.textMuted }, [prefix]),
1487
+ text({ fg: SIGMA_TOTAL_COLOR }, ["Σ TOTAL "]),
1488
+ text({ fg: theme.text }, [formatTokenExact(totalIo)])
1489
+ ]),
1490
+ renderMetricPairRight("↓", formatTokenAbbrev(totals.inputTotal), "↑", formatTokenAbbrev(totals.outputTotal), {
1491
+ leftFg: "#5DADE2",
1492
+ rightFg: "#58D68D",
1493
+ gapFg: theme.textMuted
1494
+ })
1495
+ ]));
1496
+ rows2.push(box({
1497
+ width: "100%",
1498
+ flexDirection: "row",
1499
+ justifyContent: "space-between"
1500
+ }, [
1501
+ box({ flexDirection: "row" }, [
1502
+ text({ fg: theme.textMuted }, [prefix]),
1503
+ text({ fg: SIGMA_TOTAL_COLOR }, ["Σ CACHE "]),
1504
+ text({ fg: theme.text }, [formatTokenExact(totalCache)])
1505
+ ]),
1506
+ renderMetricPairRight("\uD83D\uDCD6", formatTokenAbbrev(totals.cacheRead), "\uD83D\uDCDD", formatTokenAbbrev(totals.cacheWrite), {
1507
+ leftFg: "#5DADE2",
1508
+ rightFg: "#AF7AC5",
1509
+ gapFg: theme.textMuted
1510
+ })
1511
+ ]));
1512
+ }
1513
+ };
1514
+ const visibleOrchSessions = [];
1515
+ for (const [id, node] of Object.entries(tree)) {
1516
+ if (node.agent !== "orchestrator")
1517
+ continue;
1518
+ if (node.status === "busy" || node.status === "retry") {
1519
+ visibleOrchSessions.push([id, node]);
1520
+ } else if (node.status === "idle") {
1521
+ const hasVisibleChildren = getVisibleChildren(id).length > 0;
1522
+ if (hasVisibleChildren) {
1523
+ visibleOrchSessions.push([id, node]);
1524
+ } else if (node.finishedAt) {
1525
+ const elapsed = now - node.finishedAt;
1526
+ if (elapsed < FLASH_DURATION_MS + 1000) {
1527
+ visibleOrchSessions.push([id, node]);
1528
+ }
1529
+ } else {
1530
+ visibleOrchSessions.push([id, node]);
1531
+ }
1532
+ }
1533
+ }
1534
+ const countLabel = `${visibleOrchSessions.length} active`;
1535
+ if (visibleOrchSessions.length === 0) {
1536
+ return [
1537
+ countLabel,
1538
+ text({ fg: theme.textMuted }, ["No active orchestrations"])
1539
+ ];
1540
+ }
1541
+ const rows = [];
1542
+ const renderChildren = (parentID, indentPrefix) => {
1543
+ const visibleChildren = getVisibleChildren(parentID);
1544
+ for (let i = 0;i < visibleChildren.length; i++) {
1545
+ const [childId, child] = visibleChildren[i];
1546
+ const isLast = i === visibleChildren.length - 1;
1547
+ const branchChar = isLast ? "└" : "├";
1548
+ const pipeChar = isLast ? " " : "│";
1549
+ const childFlash = child.status === "idle" && child.finishedAt && Math.floor((now - child.finishedAt) / 200) % 2 === 0;
1550
+ const indicator = child.status === "busy" || child.status === "retry" ? spinner : childFlash ? "·" : " ";
1551
+ const childStatusText = getStatusWithDuration(snapshot, childId, child, now);
1552
+ const childVariant = child.variant;
1553
+ const detailPrefix = `${indentPrefix}${pipeChar} `;
1554
+ rows.push(box({
1555
+ width: "100%",
1556
+ flexDirection: "row",
1557
+ justifyContent: "space-between"
1558
+ }, [
1559
+ box({ flexDirection: "row", flexShrink: 0 }, [
1560
+ text({ fg: theme.textMuted }, [`${indentPrefix}${branchChar}─ `]),
1561
+ text({ fg: theme.text }, [`${indicator} ${child.agent}`])
1562
+ ]),
1563
+ renderStatusLineWithOptionalTimer(childStatusText, theme)
1564
+ ]));
1565
+ rows.push(box({ width: "100%", flexDirection: "row" }, [
1566
+ text({ fg: theme.textMuted }, [detailPrefix]),
1567
+ text({ fg: theme.textMuted }, [
1568
+ formatSidebarModelAndVariant(child.model, childVariant, ORCH_CHILD_MODEL_DISPLAY_MAX)
1569
+ ])
1570
+ ]));
1571
+ pushUsageRows(rows, childId, detailPrefix, false);
1572
+ renderChildren(childId, `${indentPrefix}${pipeChar} `);
1573
+ }
1574
+ };
1575
+ for (const [orchId, orchNode] of visibleOrchSessions) {
1576
+ const visibleChildren = getVisibleChildren(orchId);
1577
+ const orchShowSpinner = orchNode.status === "busy" || orchNode.status === "retry" || orchNode.status === "idle" && visibleChildren.length > 0;
1578
+ const orchFlash = orchNode.status === "idle" && !orchShowSpinner && orchNode.finishedAt && now >= orchNode.finishedAt && Math.floor((now - orchNode.finishedAt) / 200) % 2 === 0;
1579
+ const orchDot = orchShowSpinner ? spinner : orchFlash ? "·" : " ";
1580
+ const row1Title = orchNode.title?.trim() ? truncate(orchNode.title, ORCH_ROOT_TITLE_DISPLAY_MAX) : ORCH_DEFAULT_TITLE_LABEL;
1581
+ rows.push(box({
1582
+ flexDirection: "row",
1583
+ justifyContent: "space-between"
1584
+ }, [
1585
+ box({ flexDirection: "row" }, [
1586
+ text({ fg: theme.accent }, [`${orchDot} `]),
1587
+ text({ fg: theme.text }, [row1Title])
1588
+ ]),
1589
+ text({ fg: theme.text }, [
1590
+ orchNode.status === "busy" || orchNode.status === "retry" ? `(${formatDuration(now - orchNode.createdAt)})` : ""
1591
+ ])
1592
+ ]));
1593
+ const orchStatusText = getStatusText(snapshot, orchId);
1594
+ rows.push(box({
1595
+ width: "100%",
1596
+ flexDirection: "row",
1597
+ justifyContent: "space-between"
1598
+ }, [
1599
+ box({ flexDirection: "row", flexShrink: 0 }, [
1600
+ text({ fg: theme.textMuted }, [" "]),
1601
+ text({ fg: theme.text }, [
1602
+ truncate(orchId, ORCH_ROOT_SESSION_ID_DISPLAY_MAX)
1603
+ ])
1604
+ ]),
1605
+ renderStatusLineWithOptionalTimer(orchStatusText, theme)
1606
+ ]));
1607
+ const modelLine = formatSidebarModelAndVariant(orchNode.model, orchNode.variant, ORCH_ROOT_MODEL_DISPLAY_MAX);
1608
+ rows.push(box({ width: "100%", flexDirection: "row" }, [
1609
+ text({ fg: theme.textMuted }, [" "]),
1610
+ text({ fg: theme.textMuted }, [
1611
+ modelLine.length > 0 ? modelLine : "pending"
1612
+ ])
1613
+ ]));
1614
+ pushUsageRows(rows, orchId, " ", true);
1615
+ pushAggregateRows(rows, orchId, " ");
1616
+ renderChildren(orchId, " ");
1617
+ rows.push(box({ width: "100%", height: 1 }));
1618
+ }
1619
+ return [countLabel, ...rows];
1620
+ }
1621
+ function getActiveSessions(snapshot, now) {
1622
+ const entries = [];
1623
+ const tree = mergedSessionTree(snapshot);
1624
+ for (const [sessionID, node] of Object.entries(tree)) {
1625
+ const agentName = node.agent;
1626
+ if (!agentName)
1627
+ continue;
1628
+ if (node.status === "busy" || node.status === "retry") {
1629
+ entries.push({ sessionID, agentName, running: true, finished: false });
1630
+ } else if (node.status === "idle" && node.finishedAt) {
1631
+ let running = false;
1632
+ if (agentName === "orchestrator") {
1633
+ const hasVisibleChildren = Object.entries(tree).some(([_cid, cnode]) => cnode.parentId === sessionID && (cnode.status === "busy" || cnode.status === "retry" || cnode.status === "idle" && cnode.finishedAt && now - cnode.finishedAt < FLASH_DURATION_MS + 1000));
1634
+ if (hasVisibleChildren)
1635
+ running = true;
1636
+ }
1637
+ if (now - node.finishedAt < FLASH_DURATION_MS + 1000) {
1638
+ entries.push({
1639
+ sessionID,
1640
+ agentName,
1641
+ running,
1642
+ finished: !running
1643
+ });
1644
+ }
1645
+ }
1646
+ }
1647
+ return entries;
1648
+ }
1649
+ function renderSidebar(snapshot, theme) {
1650
+ const now = Date.now();
1651
+ const mergedTreeSidebar = mergedSessionTree(snapshot);
1652
+ const sessions = getActiveSessions(snapshot, now);
1653
+ const totalActive = sessions.filter((s) => s.running).length;
1654
+ const spinner = getSpinnerChar(now);
1655
+ const ourSessions = sessions.filter((s) => (s.agentName in AGENT_SORT_PRIORITY)).sort((a, b) => {
1656
+ const pa = AGENT_SORT_PRIORITY[a.agentName] ?? 99;
1657
+ const pb = AGENT_SORT_PRIORITY[b.agentName] ?? 99;
1658
+ if (pa !== pb)
1659
+ return pa - pb;
1660
+ return a.agentName.localeCompare(b.agentName);
1661
+ });
1662
+ const customSessions = sessions.filter((s) => !(s.agentName in AGENT_SORT_PRIORITY)).sort((a, b) => a.agentName.localeCompare(b.agentName));
1663
+ const agentRows = [];
1664
+ const ourGroups = new Map;
1665
+ for (const entry of ourSessions) {
1666
+ const { sessionID, agentName, running, finished } = entry;
1667
+ const rawModel = mergedTreeSidebar[sessionID]?.model;
1668
+ const model = rawModel ? formatSidebarModelName(rawModel) : "pending";
1669
+ const variant = mergedTreeSidebar[sessionID]?.variant;
1670
+ const key = `${agentName}\x00${model}\x00${variant ?? ""}`;
1671
+ const group = ourGroups.get(key);
1672
+ if (group) {
1673
+ group.count++;
1674
+ group.running = group.running || running;
1675
+ group.finished = group.finished || finished;
1676
+ } else {
1677
+ ourGroups.set(key, {
1678
+ sessionID,
1679
+ agentName,
1680
+ running,
1681
+ finished,
1682
+ count: 1,
1683
+ model,
1684
+ variant
1685
+ });
1686
+ }
1687
+ }
1688
+ for (const entry of ourGroups.values()) {
1689
+ const { sessionID, agentName, running, finished, count, variant } = entry;
1690
+ const elapsed = finished ? now - (mergedTreeSidebar[sessionID]?.finishedAt ?? 0) : 0;
1691
+ const flashDot = finished && Math.floor(elapsed / 200) % 2 === 0;
1692
+ const indicator = running ? spinner : flashDot ? "·" : " ";
1693
+ const desc = AGENT_SIDEBAR_DESCRIPTIONS[agentName] ?? agentName;
1694
+ const indicatorColor = theme.accent;
1695
+ const nameStr = formatAgentName(agentName);
1696
+ const descStr = truncate(desc, 10);
1697
+ agentRows.push(box({
1698
+ width: "100%",
1699
+ flexDirection: "row",
1700
+ justifyContent: "space-between"
1701
+ }, [
1702
+ box({ flexDirection: "row" }, [
1703
+ text({ fg: indicatorColor }, [`${indicator} `]),
1704
+ text({ fg: theme.text }, [nameStr]),
1705
+ text({ fg: theme.accent }, [` x${count}`])
1706
+ ]),
1707
+ box({ flexDirection: "row" }, [text({ fg: theme.text }, [descStr])])
1708
+ ]));
1709
+ const rawModel = mergedTreeSidebar[sessionID]?.model;
1710
+ const modelVariantLine = formatSidebarModelAndVariant(rawModel, variant);
1711
+ const statusText = getStatusText(snapshot, sessionID);
1712
+ agentRows.push(box({
1713
+ width: "100%",
1714
+ flexDirection: "row",
1715
+ justifyContent: "space-between"
1716
+ }, [
1717
+ text({ fg: theme.textMuted }, [
1718
+ modelVariantLine.length > 0 ? ` ${modelVariantLine}` : " pending"
1719
+ ]),
1720
+ text({
1721
+ fg: getStatusColor(statusText, theme)
1722
+ }, [statusText])
1723
+ ]));
1724
+ }
1725
+ if (customSessions.length > 0) {
1726
+ agentRows.push(box({ width: "100%" }));
1727
+ const customGroups = new Map;
1728
+ for (const entry of customSessions) {
1729
+ const { sessionID, agentName, running, finished } = entry;
1730
+ const rawModel = mergedTreeSidebar[sessionID]?.model;
1731
+ const model = rawModel ? formatSidebarModelName(rawModel) : "pending";
1732
+ const variant = mergedTreeSidebar[sessionID]?.variant;
1733
+ const key = `${agentName}\x00${model}\x00${variant ?? ""}`;
1734
+ const group = customGroups.get(key);
1735
+ if (group) {
1736
+ group.count++;
1737
+ group.running = group.running || running;
1738
+ group.finished = group.finished || finished;
1739
+ } else {
1740
+ customGroups.set(key, {
1741
+ sessionID,
1742
+ agentName,
1743
+ running,
1744
+ finished,
1745
+ count: 1,
1746
+ model,
1747
+ variant
1748
+ });
1749
+ }
1750
+ }
1751
+ for (const entry of customGroups.values()) {
1752
+ const { sessionID, agentName, running, finished, count, variant } = entry;
1753
+ const elapsed = finished ? now - (mergedTreeSidebar[sessionID]?.finishedAt ?? 0) : 0;
1754
+ const flashDot = finished && Math.floor(elapsed / 200) % 2 === 0;
1755
+ const indicator = running ? spinner : flashDot ? "·" : " ";
1756
+ const nameStr = formatAgentName(agentName);
1757
+ const rawModelChild = mergedTreeSidebar[sessionID]?.model;
1758
+ const modelVariantLineCustom = formatSidebarModelAndVariant(rawModelChild, variant);
1759
+ const customStatusText = getStatusText(snapshot, sessionID);
1760
+ agentRows.push(box({
1761
+ width: "100%",
1762
+ flexDirection: "row",
1763
+ justifyContent: "space-between"
1764
+ }, [
1765
+ box({ flexDirection: "row" }, [
1766
+ text({ fg: theme.accent }, [`${indicator} `]),
1767
+ text({ fg: theme.text }, [nameStr]),
1768
+ text({ fg: theme.accent }, [` x${count}`])
1769
+ ])
1770
+ ]));
1771
+ agentRows.push(box({
1772
+ width: "100%",
1773
+ flexDirection: "row",
1774
+ justifyContent: "space-between"
1775
+ }, [
1776
+ text({ fg: theme.textMuted }, [
1777
+ modelVariantLineCustom.length > 0 ? ` ${modelVariantLineCustom}` : " pending"
1778
+ ]),
1779
+ text({ fg: getStatusColor(customStatusText, theme) }, [
1780
+ customStatusText
1781
+ ])
1782
+ ]));
1783
+ }
1784
+ }
1785
+ if (agentRows.length === 0) {
1786
+ agentRows.push(text({ fg: theme.textMuted }, ["No active agents"]));
1787
+ }
1788
+ const orchestratingRows = buildOrchestratingRows(snapshot, now, theme);
1789
+ const usageRows = renderSubscriptionPanel(snapshot, theme);
1790
+ return box({
1791
+ width: "100%",
1792
+ flexDirection: "column",
1793
+ border: BORDER,
1794
+ borderColor: theme.borderActive,
1795
+ paddingTop: 0,
1796
+ paddingBottom: 0,
1797
+ paddingLeft: 0,
1798
+ paddingRight: 0
1799
+ }, [
1800
+ box({
1801
+ width: "100%",
1802
+ flexDirection: "row",
1803
+ justifyContent: "space-between"
1804
+ }, [
1805
+ text({ fg: theme.text }, ["Agents"]),
1806
+ text({ fg: theme.textMuted }, [`[${totalActive} active]`])
1807
+ ]),
1808
+ ...agentRows,
1809
+ ...orchestratingRows.length > 0 ? [
1810
+ box({ width: "100%", height: 1 }),
1811
+ box({
1812
+ width: "100%",
1813
+ flexDirection: "column",
1814
+ border: BORDER,
1815
+ borderColor: theme.borderActive,
1816
+ paddingTop: 0,
1817
+ paddingBottom: 0,
1818
+ paddingLeft: 0,
1819
+ paddingRight: 0
1820
+ }, [
1821
+ box({
1822
+ width: "100%",
1823
+ flexDirection: "row",
1824
+ justifyContent: "space-between"
1825
+ }, [
1826
+ text({ fg: theme.text }, ["Orchestrating"]),
1827
+ text({ fg: theme.textMuted }, [
1828
+ `[${orchestratingRows[0]}]`
1829
+ ])
1830
+ ]),
1831
+ ...orchestratingRows.slice(1)
1832
+ ])
1833
+ ] : [],
1834
+ ...usageRows.length > 0 ? [
1835
+ box({ width: "100%", height: 1 }),
1836
+ box({
1837
+ width: "100%",
1838
+ flexDirection: "column",
1839
+ border: BORDER,
1840
+ borderColor: theme.borderActive,
1841
+ paddingTop: 0,
1842
+ paddingBottom: 0,
1843
+ paddingLeft: 0,
1844
+ paddingRight: 0
1845
+ }, [
1846
+ box({
1847
+ width: "100%",
1848
+ flexDirection: "row",
1849
+ justifyContent: "space-between"
1850
+ }, [text({ fg: theme.text }, ["API Usage"])]),
1851
+ ...usageRows
1852
+ ])
1853
+ ] : []
1854
+ ]);
1855
+ }
1856
+ var plugin = {
1857
+ id: `${PLUGIN_NAME}:tui`,
1858
+ tui: async (api, _options, _meta) => {
1859
+ const [snapshot, setSnapshot] = createSignal(readTuiSnapshot());
1860
+ const [tick, setTick] = createSignal(0);
1861
+ const dataTimer = setInterval(async () => {
1862
+ try {
1863
+ setSnapshot(await readTuiSnapshotAsync());
1864
+ } catch {}
1865
+ }, 1000);
1866
+ const animTimer = setInterval(() => {
1867
+ setTick(tick() + 1);
1868
+ }, 50);
1869
+ api.lifecycle.onDispose(() => {
1870
+ clearInterval(dataTimer);
1871
+ clearInterval(animTimer);
1872
+ });
1873
+ api.slots.register({
1874
+ order: 150,
1875
+ slots: {
1876
+ sidebar_content() {
1877
+ tick();
1878
+ return renderSidebar(snapshot(), api.theme.current);
1879
+ }
1880
+ }
1881
+ });
1882
+ }
1883
+ };
1884
+ var tui_default = plugin;
1885
+ export {
1886
+ getSidebarAgentNames,
1887
+ formatTokenAbbrevDecimal,
1888
+ formatTokenAbbrev,
1889
+ formatSidebarModelName,
1890
+ formatSidebarModelAndVariant,
1891
+ formatSessionUsageRows,
1892
+ formatDuration,
1893
+ formatAgentName,
1894
+ tui_default as default,
1895
+ aggregateOrchestrationUsage
1896
+ };