agentbox-app 0.1.6

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 +674 -0
  2. package/README.md +176 -0
  3. package/bin/agentbox.mjs +103 -0
  4. package/briefs/adhd-mode.md +5 -0
  5. package/briefs/message-rules.md +95 -0
  6. package/briefs/worker.md +230 -0
  7. package/briefs/writing-rules.md +336 -0
  8. package/main/account-discovery.mjs +145 -0
  9. package/main/account-tooling.mjs +261 -0
  10. package/main/agent-activity.mjs +56 -0
  11. package/main/agent-files.mjs +422 -0
  12. package/main/agent-schedule.mjs +254 -0
  13. package/main/agent-sessions.mjs +435 -0
  14. package/main/agents.mjs +1095 -0
  15. package/main/analytics.mjs +206 -0
  16. package/main/answer-modes.mjs +206 -0
  17. package/main/approval-prompt-server.mjs +160 -0
  18. package/main/approvals.mjs +487 -0
  19. package/main/artifact-path.mjs +144 -0
  20. package/main/claude-bin.mjs +311 -0
  21. package/main/claude-input.mjs +117 -0
  22. package/main/claude-models.mjs +244 -0
  23. package/main/claude-plan.mjs +148 -0
  24. package/main/claude-usage.mjs +149 -0
  25. package/main/code-change.mjs +383 -0
  26. package/main/code-file.mjs +116 -0
  27. package/main/codex-account.mjs +146 -0
  28. package/main/codex-app-server.mjs +796 -0
  29. package/main/codex-approvals.mjs +712 -0
  30. package/main/codex-bin.mjs +261 -0
  31. package/main/codex-compaction.mjs +49 -0
  32. package/main/codex-models.mjs +194 -0
  33. package/main/codex-session.mjs +797 -0
  34. package/main/codex-threads.mjs +268 -0
  35. package/main/codex-usage-file.mjs +192 -0
  36. package/main/codex-usage.mjs +149 -0
  37. package/main/codex-watch.mjs +35 -0
  38. package/main/codex.mjs +757 -0
  39. package/main/config.mjs +345 -0
  40. package/main/copy-selection.mjs +55 -0
  41. package/main/crash-report.mjs +158 -0
  42. package/main/demo.mjs +254 -0
  43. package/main/dev-window.mjs +103 -0
  44. package/main/doc-file.mjs +99 -0
  45. package/main/doc-scheme.mjs +126 -0
  46. package/main/first-run.mjs +190 -0
  47. package/main/folders.mjs +91 -0
  48. package/main/fresh-user.mjs +120 -0
  49. package/main/git-change.mjs +323 -0
  50. package/main/img-scheme.mjs +90 -0
  51. package/main/instruction-settings.mjs +205 -0
  52. package/main/ipc.mjs +1558 -0
  53. package/main/live-replies.mjs +77 -0
  54. package/main/machine.mjs +113 -0
  55. package/main/main.mjs +972 -0
  56. package/main/menu.mjs +177 -0
  57. package/main/notify.mjs +155 -0
  58. package/main/open-source-file.mjs +8 -0
  59. package/main/project-identity.mjs +149 -0
  60. package/main/provider-commands.mjs +32 -0
  61. package/main/rail-note.mjs +74 -0
  62. package/main/repeats.mjs +261 -0
  63. package/main/row-label.mjs +185 -0
  64. package/main/screen-detail.mjs +44 -0
  65. package/main/serve.mjs +262 -0
  66. package/main/settings.mjs +912 -0
  67. package/main/staleness.mjs +76 -0
  68. package/main/store/chats.mjs +188 -0
  69. package/main/store/home.mjs +383 -0
  70. package/main/store/migrate.mjs +40 -0
  71. package/main/store/paths.mjs +82 -0
  72. package/main/store/pending-writes.mjs +122 -0
  73. package/main/store/project-lock.mjs +200 -0
  74. package/main/store/project.mjs +523 -0
  75. package/main/store/queues.mjs +31 -0
  76. package/main/store/work-items.mjs +299 -0
  77. package/main/store/write-queue.mjs +14 -0
  78. package/main/store-modules.mjs +23 -0
  79. package/main/store.mjs +1269 -0
  80. package/main/supervisor.mjs +6875 -0
  81. package/main/task-commands.mjs +161 -0
  82. package/main/task-folders-offthread.mjs +84 -0
  83. package/main/task-folders-thread.mjs +13 -0
  84. package/main/task-folders.mjs +409 -0
  85. package/main/task-remote-control.mjs +65 -0
  86. package/main/task-terminals.mjs +169 -0
  87. package/main/terminal-access.mjs +6 -0
  88. package/main/updater.mjs +240 -0
  89. package/main/zoom-keys.mjs +54 -0
  90. package/main/zoom-readout.mjs +20 -0
  91. package/mcp/agentbox-mcp.sh +34 -0
  92. package/mcp/core/account.mjs +77 -0
  93. package/mcp/core/chat.mjs +61 -0
  94. package/mcp/core/documents.mjs +238 -0
  95. package/mcp/core/page.mjs +152 -0
  96. package/mcp/core/products.mjs +110 -0
  97. package/mcp/core/skills.mjs +126 -0
  98. package/mcp/core/work.mjs +377 -0
  99. package/mcp/stdio.mjs +70 -0
  100. package/mcp/tools.mjs +257 -0
  101. package/package.json +186 -0
  102. package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
  103. package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
  104. package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
  105. package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
  106. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  107. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  108. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  109. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  110. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  111. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  112. package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
  113. package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
  114. package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
  115. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  116. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  117. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  118. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  119. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  120. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  121. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  122. package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
  123. package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
  124. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  125. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  126. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  127. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  128. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  129. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  130. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  131. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  132. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  133. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  134. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  135. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  136. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  137. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  138. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  139. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  140. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  141. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  142. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  143. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  144. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  145. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  146. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  147. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  148. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  149. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  150. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  151. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  152. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  153. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  154. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  155. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  156. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  157. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  158. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  159. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  160. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  161. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  162. package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
  163. package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
  164. package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
  165. package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
  166. package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
  167. package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
  168. package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
  169. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  170. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  171. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  172. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  173. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  174. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  175. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  176. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  177. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  178. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  179. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  180. package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
  181. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  182. package/renderer/dist/emulator/data/compression/README.md +11 -0
  183. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  184. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  185. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  186. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  187. package/renderer/dist/emulator/data/cores/README.md +22 -0
  188. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  189. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  190. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  191. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  192. package/renderer/dist/emulator/data/cores/package.json +65 -0
  193. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  194. package/renderer/dist/emulator/data/emulator.css +1614 -0
  195. package/renderer/dist/emulator/data/loader.js +170 -0
  196. package/renderer/dist/emulator/data/localization/README.md +63 -0
  197. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  198. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  199. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  200. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  201. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  202. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  203. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  204. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  205. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  206. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  207. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  208. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  209. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  210. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  211. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  212. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  213. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  214. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  215. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  216. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  217. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  218. package/renderer/dist/emulator/data/src/compression.js +142 -0
  219. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  220. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  221. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  222. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  223. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  224. package/renderer/dist/emulator/data/src/storage.js +125 -0
  225. package/renderer/dist/emulator/data/version.json +5 -0
  226. package/renderer/dist/emulator/host.js +299 -0
  227. package/renderer/dist/emulator/index.html +28 -0
  228. package/renderer/dist/emulator/provenance.json +6 -0
  229. package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
  230. package/renderer/dist/index.html +13 -0
  231. package/scripts/after-install.mjs +59 -0
  232. package/scripts/approval-server.sh +43 -0
  233. package/scripts/before-publish.mjs +133 -0
  234. package/shared/agent-import.d.mts +13 -0
  235. package/shared/agent-import.mjs +252 -0
  236. package/shared/agents.d.mts +190 -0
  237. package/shared/agents.mjs +937 -0
  238. package/shared/analytics-events.mjs +63 -0
  239. package/shared/answers.d.mts +12 -0
  240. package/shared/answers.mjs +80 -0
  241. package/shared/artifact-keys.d.mts +28 -0
  242. package/shared/artifact-keys.mjs +140 -0
  243. package/shared/bridge-map.mjs +128 -0
  244. package/shared/claude-commands.d.mts +20 -0
  245. package/shared/claude-commands.generated.mjs +149 -0
  246. package/shared/claude-commands.mjs +131 -0
  247. package/shared/claude-models.generated.d.mts +15 -0
  248. package/shared/claude-models.generated.mjs +26 -0
  249. package/shared/claude-usage.d.mts +16 -0
  250. package/shared/claude-usage.mjs +172 -0
  251. package/shared/codex-commands.d.mts +4 -0
  252. package/shared/codex-commands.mjs +17 -0
  253. package/shared/codex-import.d.mts +38 -0
  254. package/shared/codex-import.mjs +232 -0
  255. package/shared/codex-modes.mjs +159 -0
  256. package/shared/codex-usage.mjs +180 -0
  257. package/shared/contracts.mjs +412 -0
  258. package/shared/crash-scrub.mjs +182 -0
  259. package/shared/dashboard.mjs +481 -0
  260. package/shared/dead-run-trace.mjs +127 -0
  261. package/shared/demo-world.mjs +415 -0
  262. package/shared/effort-levels.d.mts +15 -0
  263. package/shared/effort-levels.mjs +76 -0
  264. package/shared/engines.d.mts +62 -0
  265. package/shared/engines.mjs +389 -0
  266. package/shared/first-run-examples.mjs +60 -0
  267. package/shared/first-run-line.mjs +149 -0
  268. package/shared/first-run-practice.d.mts +32 -0
  269. package/shared/first-run-practice.mjs +254 -0
  270. package/shared/first-run-shape.mjs +100 -0
  271. package/shared/fresh-user-home.mjs +189 -0
  272. package/shared/hook-failure.mjs +22 -0
  273. package/shared/install-id.mjs +39 -0
  274. package/shared/instruction-defaults.json +5 -0
  275. package/shared/library-folder.mjs +28 -0
  276. package/shared/notify-rules.mjs +120 -0
  277. package/shared/product-name.d.mts +6 -0
  278. package/shared/product-name.mjs +144 -0
  279. package/shared/project-folder-check.d.mts +22 -0
  280. package/shared/project-folder-check.mjs +152 -0
  281. package/shared/provider-commands.d.mts +8 -0
  282. package/shared/provider-commands.mjs +58 -0
  283. package/shared/rank.d.mts +19 -0
  284. package/shared/rank.mjs +146 -0
  285. package/shared/recovery.mjs +26 -0
  286. package/shared/referenced-files.d.mts +11 -0
  287. package/shared/referenced-files.mjs +122 -0
  288. package/shared/repeats.d.mts +45 -0
  289. package/shared/repeats.mjs +193 -0
  290. package/shared/schemes.mjs +33 -0
  291. package/shared/settings-terminal.d.mts +1 -0
  292. package/shared/settings-terminal.mjs +14 -0
  293. package/shared/side-build.mjs +57 -0
  294. package/shared/spawn-trouble.d.mts +8 -0
  295. package/shared/spawn-trouble.mjs +599 -0
  296. package/shared/terminal-state.d.mts +1 -0
  297. package/shared/terminal-state.mjs +58 -0
  298. package/shared/usage.d.mts +53 -0
  299. package/shared/usage.mjs +319 -0
  300. package/shared/work-items.mjs +561 -0
  301. package/shared/work-lines.mjs +360 -0
  302. package/worker-permissions.json +6 -0
@@ -0,0 +1,53 @@
1
+ // Types for shared/usage.mjs, which the renderer imports and which is plain
2
+ // JavaScript because main imports it too. Hand-written and short: it is one
3
+ // rule about which coding agent the corner is about, the zone primitives the
4
+ // Claude reader is built on, and the words the panel prints.
5
+
6
+ export interface UsageLimit {
7
+ span: 'session' | 'week';
8
+ qualifier: string | null;
9
+ /** What the panel calls it, in her words, stamped on by whichever reader
10
+ * produced it. See renderer/src/types.ts for why it is not derived here. */
11
+ name: string;
12
+ percent: number;
13
+ resetsText: string | null;
14
+ resetsOn: string | null;
15
+ resetsAt: number | null;
16
+ zone: string | null;
17
+ }
18
+
19
+ /** One live session as `Supervisor#status` reports it. Only the engine is read;
20
+ * it is already one of exactly two words by the time it arrives. */
21
+ export interface RunningSession {
22
+ engine?: string | null;
23
+ }
24
+
25
+ export declare function usageEngine(facts?: {
26
+ workspace?: string;
27
+ running?: RunningSession[] | null;
28
+ }): string;
29
+
30
+ export declare function localZone(): string | null;
31
+ export declare function wallIn(ms: number, zone: string | null): {
32
+ year: number; month: number; day: number; hour: number; minute: number; second: number;
33
+ } | null;
34
+ export declare function instantIn(
35
+ year: number, monthIndex: number, day: number, hour: number, minute: number, zone: string | null,
36
+ ): number;
37
+
38
+ export declare function sessionLimit(limits: UsageLimit[] | null | undefined): UsageLimit | null;
39
+ export declare function headlineLimit(limits: UsageLimit[] | null | undefined): UsageLimit | null;
40
+ export declare const ANY_MOMENT: string;
41
+ export declare function spanLeft(ms: number): string;
42
+ export declare function untilReset(ms: number): string;
43
+
44
+ export declare function limitRows(
45
+ limits: UsageLimit[] | null | undefined,
46
+ now?: number,
47
+ ): { key: string; name: string; used: number; when: string | null }[];
48
+
49
+ export declare function usageSentence(
50
+ limits: UsageLimit[] | null | undefined,
51
+ now?: number,
52
+ engineWord?: string | null,
53
+ ): string | null;
@@ -0,0 +1,319 @@
1
+ // WHAT IS LEFT OF A LIMIT, WHICHEVER CODING AGENT REPORTED IT.
2
+ //
3
+ // The corner meter and its dropdown were built against `claude -p /usage` and
4
+ // nothing else. They are drawn on every screen, unlabelled, and they describe a
5
+ // Claude Code subscription whether or not the work in front of her is billing
6
+ // one. That is the same class of defect he had already caught on the Settings
7
+ // Model row: a control that looks as though it belongs to the coding agent named
8
+ // elsewhere on the screen, and does not.
9
+ //
10
+ // SO THE PANEL'S WORDS LEFT THE CLAUDE FILE, and that move is the point rather
11
+ // than tidying. A Codex reading flowing through a module called `claude-usage`
12
+ // is the same defect written in source: the next person to add a Claude-shaped
13
+ // assumption to it would have no reason not to. What is Claude Code's stays
14
+ // there -- the text format, the timezone stamp it prints, `readUsage` --
15
+ // and what belongs to a LIMIT, any limit, is here.
16
+ //
17
+ // TWO THINGS DID NOT CARRY OVER, AND SAYING SO IS THE POINT OF THIS PARAGRAPH.
18
+ // Claude Code prints a clock time in a named zone and no instant, so the whole
19
+ // of `resetAt` / `wallIn` / `instantIn` exists to turn "Sep 3 at 3:59pm
20
+ // (America/Los_Angeles)" into a moment, and to REFUSE when it cannot be done
21
+ // honestly. Codex hands over an absolute instant and no text at all. Those are
22
+ // different problems and neither reader may be forced into the other's shape.
23
+ // What is genuinely shared is everything after the reading: the countdown, the
24
+ // rows, the sentence, and the rule for which agent the corner is about.
25
+ //
26
+ // AND A LIMIT NOW CARRIES ITS OWN NAME. `limitName` used to derive one from
27
+ // Claude Code's three spans and its qualifier, which is how "This week, Fable"
28
+ // -- a Claude model -- could have appeared over a Codex reading. Each reader
29
+ // names its own limits, in her words, and this file prints the name it is
30
+ // given.
31
+
32
+ import { DEFAULT_ENGINE } from './engines.mjs';
33
+
34
+ /* ------------------ WHICH CODING AGENT THE CORNER IS ABOUT ---------------- */
35
+
36
+ /**
37
+ * THE AGENT MOST OF HER LIVE WORK IS ON, or the workspace's own when nothing is
38
+ * running and when the fleet is level.
39
+ *
40
+ * IT IS THE RULE THE BYLINE ALREADY FOLLOWS, one level up. renderer/src/byline.ts
41
+ * settled this for a row a slice ago -- "WHAT IS RUNNING BEATS WHAT WOULD RUN" --
42
+ * and the corner asks the same question about the whole app. Two rules for one
43
+ * question is how "Medium" and "medium" ended up on adjacent screens.
44
+ *
45
+ * WHY NOT THE WORKSPACE AGENT ALWAYS. It is stabler, and it is wrong in exactly
46
+ * the case that was reported: four Codex workers under a workspace defaulting to
47
+ * Claude Code would draw Claude Code's meter at 12% while the thing about to
48
+ * stop the fleet sits at 96% on the other subscription. A meter that is stable
49
+ * and describes the wrong account is the defect, not the fix.
50
+ *
51
+ * WHY IT COUNTS RATHER THAN ASKING "IS ANY CODEX RUNNING". A rule that flipped
52
+ * the moment one worker of the other kind started would change the SUBJECT of
53
+ * the corner several times a minute on a mixed fleet, and she would be reading a
54
+ * history of two subscriptions in one bar. Counting moves it only when the
55
+ * balance of the fleet really moves. The level case has no answer of its own, so
56
+ * it falls back to the one answer that is always available.
57
+ *
58
+ * The engine on a session is already one of exactly two words: `Supervisor#status`
59
+ * puts every record through `engineOf` first, and a record written before the
60
+ * second engine existed comes out as Claude Code. A session with no engine on it
61
+ * at all is not counted here rather than counted as a third thing.
62
+ */
63
+ export function usageEngine({ workspace = DEFAULT_ENGINE, running = [] } = {}) {
64
+ const tally = new Map();
65
+ for (const session of running ?? []) {
66
+ const id = session?.engine;
67
+ if (typeof id !== 'string' || !id) continue;
68
+ tally.set(id, (tally.get(id) ?? 0) + 1);
69
+ }
70
+ let best = null;
71
+ let most = 0;
72
+ let level = false;
73
+ for (const [id, count] of tally) {
74
+ if (count > most) { best = id; most = count; level = false; } else if (count === most) level = true;
75
+ }
76
+ return best === null || level ? workspace : best;
77
+ }
78
+
79
+ /* --------------------- reading a clock in a named zone -------------------- */
80
+ // The two primitives the zone-honesty of shared/claude-usage.mjs rests on, and
81
+ // the one question this file asks about a day. They exist because
82
+ // `new Date(y, m, d, h, min)` and `Date#getDate` are the PROCESS's zone and
83
+ // nothing else, and every question about a reset is about hers.
84
+ //
85
+ // A ZONE WE CANNOT USE FALLS BACK TO THE PROCESS'S, DELIBERATELY. `zone` is null
86
+ // when `Intl` would not answer at all, and a name Intl rejects throws; in both
87
+ // cases the old behaviour is the best guess left and is exactly right on the only
88
+ // machine that matters, hers. It is a fallback and never a first choice: when we
89
+ // are told the zone we use the zone.
90
+
91
+ export function localZone() {
92
+ try { return Intl.DateTimeFormat().resolvedOptions().timeZone || null; } catch { return null; }
93
+ }
94
+
95
+ const WALL = {
96
+ year: 'numeric', month: '2-digit', day: '2-digit',
97
+ hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
98
+ };
99
+
100
+ /** What a clock in `zone` reads at instant `ms`, or null if it cannot be asked. */
101
+ export function wallIn(ms, zone) {
102
+ if (!zone) return null;
103
+ try {
104
+ const parts = new Intl.DateTimeFormat('en-US', { timeZone: zone, ...WALL }).formatToParts(new Date(ms));
105
+ const at = (type) => Number(parts.find((p) => p.type === type)?.value);
106
+ // `hour12: false` prints midnight as 24 in some engines and 0 in others.
107
+ const read = {
108
+ year: at('year'), month: at('month'), day: at('day'),
109
+ hour: at('hour') % 24, minute: at('minute'), second: at('second'),
110
+ };
111
+ return Object.values(read).every(Number.isFinite) ? read : null;
112
+ } catch { return null; }
113
+ }
114
+
115
+ /**
116
+ * THE INSTANT AT WHICH A CLOCK IN `zone` READS THIS WALL TIME.
117
+ *
118
+ * Two passes and not one, and the second is the DST correction rather than
119
+ * defensiveness: the offset has to be sampled at an instant, the first sample
120
+ * is taken at the wall time pretending to be UTC, and on the two days a year
121
+ * that lands on the far side of a transition the sample is the wrong offset by
122
+ * an hour. Sampling again at the instant the first pass produced puts it inside
123
+ * the same offset as the answer. A wall time that does not exist (the spring
124
+ * gap) or happens twice (the autumn overlap) settles on one of the two, which is
125
+ * what every other reader of a bare clock time does and is a whole hour better
126
+ * than being in the wrong zone.
127
+ */
128
+ export function instantIn(year, monthIndex, day, hour, minute, zone) {
129
+ const local = () => new Date(year, monthIndex, day, hour, minute, 0, 0).getTime();
130
+ if (!zone) return local();
131
+ const wanted = Date.UTC(year, monthIndex, day, hour, minute, 0);
132
+ let ms = wanted;
133
+ for (let pass = 0; pass < 2; pass += 1) {
134
+ const w = wallIn(ms, zone);
135
+ if (!w) return local();
136
+ ms = wanted - (Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second) - ms);
137
+ }
138
+ return ms;
139
+ }
140
+
141
+ /**
142
+ * WHETHER TWO MOMENTS ARE THE SAME DAY WHERE SHE IS, which is what decides
143
+ * whether a reset needs its date said out loud. By calendar date rather than by
144
+ * arithmetic on hours, for the same reason the repeating tasks are: DST and a
145
+ * clock correction have to be boring.
146
+ *
147
+ * IN A NAMED ZONE, for the same reason `resetAt` builds its instant in one
148
+ * (2026-09-04). "Is this reset today" is a question about her calendar, and
149
+ * reading it off the process's clock answered it about the machine's instead:
150
+ * the same instant is Sep 1 in Los Angeles and Sep 2 in Berlin, so the panel's
151
+ * "Resets 4pm" became "Resets Sep 1 at 4pm" purely by moving desks.
152
+ */
153
+ function sameDay(a, b, zone) {
154
+ const x = wallIn(a, zone), y = wallIn(b, zone);
155
+ if (x && y) return x.year === y.year && x.month === y.month && x.day === y.day;
156
+ const p = new Date(a), q = new Date(b);
157
+ return p.getFullYear() === q.getFullYear() && p.getMonth() === q.getMonth() && p.getDate() === q.getDate();
158
+ }
159
+
160
+ /* ------------------------------ the countdown ----------------------------- */
161
+
162
+ /**
163
+ * The five hour limit, which is the one that stops her today. Claude Code
164
+ * prints it as "Current session"; Codex reports it as the `primary` window,
165
+ * measured at 300 minutes. Both readers mark it `span: 'session'`. */
166
+ export function sessionLimit(limits) {
167
+ return (limits ?? []).find((l) => l.span === 'session') ?? null;
168
+ }
169
+
170
+ /**
171
+ * THE ONE LIMIT THE CORNER IS ABOUT, on an account that may not have a five hour
172
+ * window at all.
173
+ *
174
+ * The bar used to be the session limit or nothing, and on 2026-09-18 that turned
175
+ * out to mean NOTHING on the founder's own Codex plan: measured across the 1,140
176
+ * rate-limit records in her twenty-five most recent Codex sessions, every window
177
+ * she has is a seven day one and `secondary` is never populated. Her weekly limit
178
+ * was at 100% used that day, which is exactly the moment a person needs the
179
+ * corner, and the corner would have been blank.
180
+ *
181
+ * The session window still wins whenever there is one, because it is the one that
182
+ * stops her today. When there is not, the fullest window she does have is drawn
183
+ * instead: a limit at 100% decides whether she can start six agents, whatever
184
+ * span it runs over. Null only when there is truly nothing, which stays the
185
+ * honest empty corner it has always been.
186
+ */
187
+ export function headlineLimit(limits) {
188
+ const session = sessionLimit(limits);
189
+ if (session) return session;
190
+ let best = null;
191
+ for (const limit of limits ?? []) {
192
+ if (!Number.isFinite(limit?.percent)) continue;
193
+ if (!best || limit.percent > best.percent) best = limit;
194
+ }
195
+ return best;
196
+ }
197
+
198
+ /**
199
+ * HOW LONG UNTIL IT STARTS OVER, in her house style: whole minutes, then hours
200
+ * and minutes, and never a seconds counter.
201
+ */
202
+ export function untilReset(ms) {
203
+ const span = spanLeft(ms);
204
+ return span === ANY_MOMENT ? span : `${span} left`;
205
+ }
206
+
207
+ /**
208
+ * THE SAME COUNTDOWN WITHOUT THE WORD "left" ON THE END, for the callers that
209
+ * put a preposition in front of it instead. It is the same rule and the same
210
+ * rounding; `untilReset` is this plus a suffix, so the two cannot drift into
211
+ * disagreeing about what 90 minutes is. */
212
+ export const ANY_MOMENT = 'any moment';
213
+ export function spanLeft(ms) {
214
+ const minutes = Math.max(0, Math.floor(ms / 60_000));
215
+ if (minutes < 1) return ANY_MOMENT;
216
+ if (minutes < 60) return `${minutes}m`;
217
+ const hours = Math.floor(minutes / 60);
218
+ // AND DAYS PAST A DAY, which the session limit never needs and the WEEKLY ones
219
+ // always do. The first version of this stopped at hours and read "71h 12m
220
+ // left" on her week, which is a number a person has to do arithmetic on before
221
+ // it means anything. The minutes come off at that range too: nobody reads a
222
+ // weekly allowance to the minute.
223
+ if (hours >= 24) {
224
+ const days = Math.floor(hours / 24);
225
+ const rest = hours % 24;
226
+ return rest ? `${days}d ${rest}h` : `${days}d`;
227
+ }
228
+ return `${hours}h ${minutes % 60}m`;
229
+ }
230
+
231
+ /* -------------------------------- the panel ------------------------------- */
232
+
233
+ /**
234
+ * THE PANEL'S ROWS, ONE PER LIMIT, COUNTING WHAT HAS BEEN SPENT.
235
+ *
236
+ * IT COUNTED WHAT WAS LEFT FOR ABOUT AN HOUR AND THAT WAS WRONG.
237
+ *
238
+ * That is the QUESTION, and "45% used" is a fine answer to it. A bar is not a
239
+ * sentence, and every bar anybody has used fills as the thing is consumed.
240
+ *
241
+ * SO `used`, STRAIGHT FROM THE READING, and no `left` anywhere for a caller to
242
+ * reach for by mistake.
243
+ *
244
+ * THE ORDER IS THE READER'S OWN, which is the five hour one first and the one
245
+ * that stops her today.
246
+ *
247
+ * AND `when` IS A CLOCK TIME, WITH THE DAY WHEN THE DAY IS NOT TODAY. Claude
248
+ * Code prints "Resets 9:59pm" for the session and "Resets Sep 3 at 3:59pm" for
249
+ * the week, and that solves the ambiguity a countdown was invented to dodge on
250
+ * the round before: the week's stamp is "4pm" and reads as today when it is
251
+ * Thursday's. Saying the day is the better answer and it is the one she is
252
+ * pointing at.
253
+ *
254
+ * A READING THAT CARRIES AN INSTANT AND NO CLOCK TEXT COUNTS DOWN INSTEAD,
255
+ * which is the Codex shape and is unreachable for Claude Code: `resetsAt` is
256
+ * only ever non-null there when the same regex already produced `resetsText`.
257
+ * Formatting Codex's instant into a clock time would mean inventing a second
258
+ * copy of the zone machinery next door, to print a fact we already hold exactly
259
+ * -- so the fact we hold is what gets printed, in the countdown wording this
260
+ * file already owns.
261
+ *
262
+ * `when` stays null rather than invented when there is no reset at all, which is
263
+ * what Claude Code gives for a limit at 0% and what Codex's nullable `resetsAt`
264
+ * gives. And when a Claude stamp was computed in another timezone we cannot say
265
+ * which local day it lands on, so the whole stamp goes out as the command wrote
266
+ * it: true either way, and still refusing to guess.
267
+ *
268
+ * WHICH DAY "TODAY" IS gets asked in the limit's OWN zone, which is the zone the
269
+ * CLI printed beside the stamp. That is not a third source of truth: `resetsAt`
270
+ * is null unless that zone is the one `readUsage` was told this machine is in
271
+ * (`resetAt`'s standing refusal), so by the time this line is reached the two
272
+ * are the same zone and the stamp's copy of it is the one in hand. A reading
273
+ * with no zone on it at all falls back to the process's, which is what it always
274
+ * was, and is every Codex reading.
275
+ */
276
+ export function limitRows(limits, now = Date.now()) {
277
+ const here = localZone();
278
+ return (limits ?? []).map((l) => ({
279
+ // Stable across redraws, and it has to carry the qualifier: Claude Code's
280
+ // two week rows are otherwise the same string.
281
+ key: `${l.span}:${l.qualifier ?? ''}`,
282
+ name: l.name,
283
+ used: l.percent,
284
+ when: whenItStartsOver(l, now, here),
285
+ }));
286
+ }
287
+
288
+ function whenItStartsOver(l, now, here) {
289
+ if (l.resetsText) {
290
+ if (l.resetsAt && sameDay(l.resetsAt, now, l.zone ?? here)) return `Resets ${l.resetsText}`;
291
+ return l.resetsOn ? `Resets ${l.resetsOn} at ${l.resetsText}` : `Resets ${l.resetsText}`;
292
+ }
293
+ if (!l.resetsAt) return null;
294
+ const soon = spanLeft(l.resetsAt - now);
295
+ return soon === ANY_MOMENT ? `Resets ${soon}` : `Resets in ${soon}`;
296
+ }
297
+
298
+ /**
299
+ * THE WHOLE SENTENCE, for whatever reads the corner aloud and for the tooltip.
300
+ * It says every limit, because the corner itself is a bar with no words in it at
301
+ * all and a summary has to expand into the thing it summarised.
302
+ *
303
+ * AND IT NAMES THE CODING AGENT WHERE THERE IS ONE TO NAME (2026-09-05). The
304
+ * bar has no words in it, so this sentence IS the corner for anybody who cannot
305
+ * see it; labelling the dropdown and leaving this unlabelled would answer the
306
+ * tester's ask for one reader and not the other. `engineWord` is null on every
307
+ * Mac with one coding agent -- `engineWordFor` in renderer/src/byline.ts is the
308
+ * one rule for that, and it is the same rule the panel's heading follows -- and
309
+ * then this is byte for byte the sentence she has been reading since
310
+ * 2026-09-01.
311
+ */
312
+ export function usageSentence(limits, now = Date.now(), engineWord = null) {
313
+ if (!limits?.length) return null;
314
+ const said = limits.map((l) => {
315
+ const when = l.resetsAt ? `, ${untilReset(l.resetsAt - now)}` : l.resetsText ? `, resets ${l.resetsText}` : '';
316
+ return `${l.name}: ${l.percent}% used${when}.`;
317
+ }).join(' ');
318
+ return engineWord ? `${engineWord}. ${said}` : said;
319
+ }