agentbox-app 0.1.12 → 0.1.13

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 (214) hide show
  1. package/main/acp.mjs +296 -0
  2. package/main/agent-homes.mjs +158 -0
  3. package/main/agent-sessions.mjs +57 -20
  4. package/main/agent-updates.mjs +9 -22
  5. package/main/analytics.mjs +30 -0
  6. package/main/claude-bin.mjs +45 -311
  7. package/main/claude-input.mjs +9 -3
  8. package/main/claude-launch-env.mjs +54 -0
  9. package/main/claude-models.mjs +39 -7
  10. package/main/claude-plan.mjs +16 -16
  11. package/main/claude-usage.mjs +82 -25
  12. package/main/codex-bin.mjs +51 -249
  13. package/main/codex-session.mjs +38 -3
  14. package/main/codex-threads.mjs +41 -8
  15. package/main/codex-watch.mjs +67 -3
  16. package/main/config.mjs +26 -4
  17. package/main/corner-tag.mjs +489 -0
  18. package/main/crash-report.mjs +26 -3
  19. package/main/engine-bin.mjs +595 -0
  20. package/main/engine-setup.mjs +4 -15
  21. package/main/feedback.mjs +11 -6
  22. package/main/harnesses/claude-stream.mjs +208 -0
  23. package/main/harnesses/claude.mjs +84 -0
  24. package/main/harnesses/codex.mjs +65 -0
  25. package/main/harnesses/common.mjs +78 -0
  26. package/main/harnesses/instructions-file.mjs +116 -0
  27. package/main/harnesses/opencode.mjs +234 -0
  28. package/main/harnesses.mjs +65 -0
  29. package/main/importable-threads.mjs +38 -0
  30. package/main/ipc.mjs +165 -31
  31. package/main/local-app-frames.mjs +82 -0
  32. package/main/main.mjs +100 -4
  33. package/main/memory-gate-history.mjs +51 -12
  34. package/main/memory-gate-server.mjs +67 -11
  35. package/main/memory-gate.mjs +49 -8
  36. package/main/memory-log.mjs +124 -0
  37. package/main/notify.mjs +47 -4
  38. package/main/opencode-server.mjs +271 -0
  39. package/main/opencode.mjs +287 -0
  40. package/main/phone-link.mjs +551 -0
  41. package/main/relay-address.mjs +28 -0
  42. package/main/relay-client.mjs +246 -0
  43. package/main/row-label.mjs +6 -6
  44. package/main/runs-on.mjs +32 -35
  45. package/main/serve.mjs +113 -9
  46. package/main/settings.mjs +148 -10
  47. package/main/shell-path.mjs +86 -0
  48. package/main/ship-queue.mjs +24 -6
  49. package/main/sign-in-files.mjs +3 -7
  50. package/main/source-updater.mjs +27 -1
  51. package/main/start-hold.mjs +73 -0
  52. package/main/store.mjs +1 -1
  53. package/main/supervisor.mjs +832 -428
  54. package/main/task-commands.mjs +13 -17
  55. package/main/task-folders-offthread.mjs +6 -1
  56. package/main/task-folders.mjs +490 -98
  57. package/main/task-terminals.mjs +37 -7
  58. package/main/updater.mjs +71 -9
  59. package/main/worktree-include.mjs +494 -0
  60. package/package.json +10 -1
  61. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
  62. package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
  63. package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
  64. package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
  65. package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
  66. package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
  67. package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
  68. package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
  69. package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
  70. package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
  71. package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
  72. package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
  73. package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
  74. package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
  75. package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
  76. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
  77. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
  78. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
  79. package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
  80. package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
  81. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
  82. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  83. package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
  84. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  85. package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
  86. package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
  87. package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
  88. package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
  89. package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
  90. package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
  91. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
  92. package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
  93. package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
  94. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
  95. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
  96. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
  97. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  98. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
  99. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  100. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
  101. package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
  102. package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
  103. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  104. package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
  105. package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
  106. package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
  107. package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
  108. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
  109. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  110. package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
  111. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
  112. package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
  113. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
  114. package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
  115. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
  116. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
  117. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
  118. package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
  119. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
  120. package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
  121. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
  122. package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
  123. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
  124. package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
  125. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
  126. package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
  127. package/renderer/dist/corner-tag.html +13 -0
  128. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  129. package/renderer/dist/emulator/data/compression/README.md +11 -0
  130. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  131. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  132. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  133. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  134. package/renderer/dist/emulator/data/cores/README.md +22 -0
  135. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  136. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  137. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  138. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  139. package/renderer/dist/emulator/data/cores/package.json +65 -0
  140. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  141. package/renderer/dist/emulator/data/emulator.css +1614 -0
  142. package/renderer/dist/emulator/data/loader.js +170 -0
  143. package/renderer/dist/emulator/data/localization/README.md +63 -0
  144. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  145. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  146. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  147. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  148. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  149. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  150. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  151. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  152. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  153. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  154. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  155. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  156. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  157. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  158. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  159. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  160. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  161. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  162. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  163. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  164. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  165. package/renderer/dist/emulator/data/src/compression.js +142 -0
  166. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  167. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  168. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  169. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  170. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  171. package/renderer/dist/emulator/data/src/storage.js +125 -0
  172. package/renderer/dist/emulator/data/version.json +5 -0
  173. package/renderer/dist/emulator/host.js +297 -0
  174. package/renderer/dist/emulator/index.html +28 -0
  175. package/renderer/dist/emulator/provenance.json +6 -0
  176. package/renderer/dist/index.html +16 -3
  177. package/scripts/approval-server.sh +24 -5
  178. package/scripts/before-publish.mjs +12 -10
  179. package/shared/agent-import.mjs +2 -0
  180. package/shared/agents.d.mts +1 -0
  181. package/shared/bridge-map.mjs +18 -0
  182. package/shared/claude-commands.generated.mjs +6 -4
  183. package/shared/claude-models.generated.mjs +6 -4
  184. package/shared/corner-tag.d.mts +14 -0
  185. package/shared/corner-tag.mjs +141 -0
  186. package/shared/engines.mjs +20 -5
  187. package/shared/harness-definitions.mjs +103 -0
  188. package/shared/notify-rules.d.mts +25 -0
  189. package/shared/notify-rules.mjs +19 -0
  190. package/shared/provider-commands.mjs +10 -2
  191. package/shared/rank.d.mts +2 -0
  192. package/shared/rank.mjs +50 -0
  193. package/shared/relay-address.mjs +6 -0
  194. package/shared/relay-crypto.mjs +165 -0
  195. package/shared/runs-on.d.mts +0 -10
  196. package/shared/runs-on.mjs +25 -49
  197. package/shared/shortcuts.mjs +10 -4
  198. package/shared/spawn-trouble.mjs +57 -6
  199. package/shared/terminal-input.d.mts +1 -0
  200. package/shared/terminal-input.mjs +30 -0
  201. package/shared/terminal-keys.d.mts +1 -0
  202. package/shared/terminal-keys.mjs +16 -0
  203. package/shared/terminal-size.d.mts +4 -0
  204. package/shared/terminal-size.mjs +29 -0
  205. package/shared/usage.d.mts +3 -0
  206. package/shared/usage.mjs +7 -0
  207. package/shared/where-it-runs-from.d.mts +43 -0
  208. package/shared/where-it-runs-from.mjs +155 -0
  209. package/shared/work-items.mjs +25 -1
  210. package/shared/work-lines.mjs +1 -1
  211. package/shared/xterm-invalid-range.d.mts +2 -0
  212. package/shared/xterm-invalid-range.mjs +58 -0
  213. package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
  214. package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
@@ -0,0 +1,6 @@
1
+ // THE RELAY EVERY COPY OF THE APP USES (relay/worker.mjs), deployed to the
2
+ // founder's Cloudflare account on 2026-10-12. With an address here every
3
+ // computer offers "Anywhere" in Settings, then Phone, and starts new people on
4
+ // it; AGENTBOX_RELAY_URL or relay/relay.local.json still take precedence
5
+ // (main/relay-address.mjs). Redeploy with `cd relay && npx wrangler deploy`.
6
+ export const RELAY_URL = 'https://agentbox-relay.agentbox-app.workers.dev';
@@ -0,0 +1,165 @@
1
+ // THE RELAY'S LOCKS (relay/worker.mjs, main/relay-client.mjs, relay/phone/).
2
+ //
3
+ // A phone away from home reaches the Mac through a relay we run. Everything
4
+ // that crosses it is sealed on one end and opened on the other with keys made
5
+ // from the phone's key, which is in the QR code and nowhere else: it never
6
+ // leaves the Mac except into the code, and the phone keeps it in the address's
7
+ // fragment, which a browser never sends. So the relay can pass messages and
8
+ // cannot read, alter or forge one.
9
+ //
10
+ // What the relay does see: a room name, and the Mac's proof that it may hold
11
+ // that room. The room is a hash of the proof, so the relay can check a Mac
12
+ // against it, and neither one tells it the key.
13
+ //
14
+ // Plain WebCrypto, so this one file runs on the Mac (Node), on Cloudflare, and
15
+ // in the phone's service worker.
16
+
17
+ const enc = new TextEncoder();
18
+ const subtle = () => globalThis.crypto.subtle;
19
+ const SALT = enc.encode('agentbox-relay-v1');
20
+
21
+ export function b64u(bytes) {
22
+ let s = '';
23
+ for (const b of bytes) s += String.fromCharCode(b);
24
+ return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
25
+ }
26
+
27
+ async function bits(key, info, length = 256) {
28
+ const ikm = await subtle().importKey('raw', enc.encode(String(key)), 'HKDF', false, ['deriveBits']);
29
+ return new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SALT, info: enc.encode(info) }, ikm, length));
30
+ }
31
+
32
+ async function roomOf(proof) {
33
+ return b64u(new Uint8Array(await subtle().digest('SHA-256', enc.encode(`agentbox-room:${proof}`))));
34
+ }
35
+
36
+ /** The room the relay knows this pair by, and the Mac's proof that it is the Mac's. */
37
+ export async function relayIds(key) {
38
+ const proof = b64u(await bits(key, 'mac-proof'));
39
+ return { room: await roomOf(proof), proof };
40
+ }
41
+
42
+ /** Is `proof` the proof for `room`? The relay asks this of every Mac. */
43
+ export async function macMayJoin(room, proof) {
44
+ if (typeof room !== 'string' || typeof proof !== 'string' || !proof) return false;
45
+ const want = await roomOf(proof);
46
+ if (want.length !== room.length) return false;
47
+ let diff = 0;
48
+ for (let i = 0; i < want.length; i += 1) diff |= want.charCodeAt(i) ^ room.charCodeAt(i);
49
+ return diff === 0;
50
+ }
51
+
52
+ const aes = async (raw) => subtle().importKey('raw', raw, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
53
+
54
+ /** One key for each direction, so nothing sealed one way opens the other. */
55
+ export async function relayKeys(key) {
56
+ return { up: await aes(await bits(key, 'phone-to-mac')), down: await aes(await bits(key, 'mac-to-phone')) };
57
+ }
58
+
59
+ /**
60
+ * Seal `bytes` for the other end. `aad` names what it is (a request, or the
61
+ * n-th frame of the answer to one request), so a frame cannot be passed off
62
+ * as another. Out: 12 bytes of nonce, then the sealed bytes.
63
+ */
64
+ export async function seal(key, bytes, aad) {
65
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
66
+ const ct = new Uint8Array(await subtle().encrypt({ name: 'AES-GCM', iv, additionalData: enc.encode(aad) }, key, bytes));
67
+ const out = new Uint8Array(12 + ct.length);
68
+ out.set(iv, 0);
69
+ out.set(ct, 12);
70
+ return out;
71
+ }
72
+
73
+ /** Open what `seal` made, or throw if a single bit of it was changed. */
74
+ export async function open(key, sealed, aad) {
75
+ if (!(sealed instanceof Uint8Array) || sealed.length < 28) throw new Error('Too short to be sealed.');
76
+ const pt = await subtle().decrypt({ name: 'AES-GCM', iv: sealed.subarray(0, 12), additionalData: enc.encode(aad) }, key, sealed.subarray(12));
77
+ return new Uint8Array(pt);
78
+ }
79
+
80
+ /* ------------------------------------------------------------- the wire */
81
+
82
+ // A request, phone to Mac, before it is sealed: four bytes giving the length of
83
+ // a json head, the head, then the body. The head carries a time and a random
84
+ // nonce, so the Mac can turn away one it has already answered.
85
+ export function packRequest(head, body = new Uint8Array(0)) {
86
+ const h = enc.encode(JSON.stringify(head));
87
+ const out = new Uint8Array(4 + h.length + body.length);
88
+ new DataView(out.buffer).setUint32(0, h.length);
89
+ out.set(h, 4);
90
+ out.set(body, 4 + h.length);
91
+ return out;
92
+ }
93
+
94
+ export function unpackRequest(bytes) {
95
+ const n = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);
96
+ const head = JSON.parse(new TextDecoder().decode(bytes.subarray(4, 4 + n)));
97
+ return { head, body: bytes.subarray(4 + n) };
98
+ }
99
+
100
+ // An answer, Mac to phone, is a run of frames, each sealed on its own: a head
101
+ // (status and headers), the body in pieces, and an end. The end is sealed too,
102
+ // so an answer cut short by anyone in between is seen to be cut short.
103
+ //
104
+ // WAIT comes before the head and says only "still working". A reply to an
105
+ // agent mid-step is answered when the agent takes it, which can be minutes,
106
+ // and the room gives a Mac 30 seconds to start answering. A phone that asks
107
+ // for these (`wait` in its request's head) skips them; one that does not
108
+ // never gets one.
109
+ export const FRAME = Object.freeze({ HEAD: 0, DATA: 1, END: 2, WAIT: 3 });
110
+
111
+ export const frameAad = (nonce, seq) => `res:${nonce}:${seq}`;
112
+
113
+ // ON THE MAC'S SOCKET TO THE RELAY one byte says what a message is, then eight
114
+ // bytes of request number, then whatever it carries:
115
+ // to the Mac 0x01 a request (the sealed bytes) 0x02 the phone gave up
116
+ // 0x03 a piece of a request too big for one message; the
117
+ // 0x01 that follows carries the last piece
118
+ // from the Mac 0x10 one sealed frame of the answer 0x11 done
119
+ // 0x12 refused: it would not open, or it was a replay
120
+ export const TO_MAC = Object.freeze({ REQUEST: 0x01, CANCEL: 0x02, PART: 0x03 });
121
+ /** The most the relay puts in one socket message to the Mac. Cloudflare's limit is 1 MB. */
122
+ export const PART_BYTES = 512 * 1024;
123
+ export const FROM_MAC = Object.freeze({ FRAME: 0x10, DONE: 0x11, REFUSED: 0x12 });
124
+ export const ID_BYTES = 8;
125
+
126
+ export function tag(type, id, rest = new Uint8Array(0)) {
127
+ const out = new Uint8Array(1 + ID_BYTES + rest.length);
128
+ out[0] = type;
129
+ out.set(id, 1);
130
+ out.set(rest, 1 + ID_BYTES);
131
+ return out;
132
+ }
133
+
134
+ // Between the relay and either end each sealed piece goes with its length in
135
+ // front, so one stream of bytes can carry many.
136
+ export function lengthPrefixed(bytes) {
137
+ const out = new Uint8Array(4 + bytes.length);
138
+ new DataView(out.buffer).setUint32(0, bytes.length);
139
+ out.set(bytes, 4);
140
+ return out;
141
+ }
142
+
143
+ /** Split a stream (or a reader of one) of length-prefixed pieces back into the pieces. */
144
+ export async function* pieces(source) {
145
+ const reader = typeof source.getReader === 'function' ? source.getReader() : source;
146
+ let buf = new Uint8Array(0);
147
+ try {
148
+ for (;;) {
149
+ while (buf.length >= 4) {
150
+ const n = new DataView(buf.buffer, buf.byteOffset, buf.byteLength).getUint32(0);
151
+ if (buf.length < 4 + n) break;
152
+ yield buf.slice(4, 4 + n);
153
+ buf = buf.subarray(4 + n);
154
+ }
155
+ const { value, done } = await reader.read();
156
+ if (done) return;
157
+ const next = new Uint8Array(buf.length + value.length);
158
+ next.set(buf, 0);
159
+ next.set(value, buf.length);
160
+ buf = next;
161
+ }
162
+ } finally {
163
+ try { reader.releaseLock(); } catch { /* cancelled under us */ }
164
+ }
165
+ }
@@ -1,20 +1,10 @@
1
1
  export interface RunsOn {
2
2
  /** 'claude' or 'codex', or null on a Mac where nothing is signed in. */
3
3
  engine?: string | null;
4
- /**
5
- * The plan in words, as the tool itself reports it: 'Max 20x', 'Pro'. Null
6
- * where the file could not be read, and then only the name is drawn. */
7
- plan?: string | null;
8
4
  }
9
5
 
10
6
  /** The word for the plan behind an engine, or null for one we cannot name. */
11
7
  export declare function runsOnName(engine?: string | null): string | null;
12
8
 
13
- /** The glance line: 'Claude · Max 20x', or 'Claude' with no plan. Null for null. */
14
- export declare function runsOnLine(facts?: RunsOn): string | null;
15
-
16
- /** The whole sentence, for a hover and for a screen reader. Null for null. */
17
- export declare function runsOnTitle(facts?: RunsOn): string | null;
18
-
19
9
  /** The one line the walk says on a Mac that was already set up. Null for null. */
20
10
  export declare function foundOnThisMac(facts?: RunsOn): string | null;
@@ -1,30 +1,39 @@
1
- // WHAT THE AGENTS ARE RUNNING ON, IN WORDS.
1
+ // THE ONE LINE SAYING IT FOUND AN ACCOUNT ALREADY ON THIS MAC.
2
2
  //
3
- // From a session with a new user, 2026-10-07: "He didn't realize it
4
- // auto-connected to Claude/Codex; he wasn't sure how it was even running. Would
5
- // be nice to have some small indication."
3
+ // From a session with a new user, 2026-10-07, in her words: a new user came in,
4
+ // started running an agent, and was "like, 'Oh my God, where are the tokens
5
+ // coming from? I didn't even connect my account.'"
6
6
  //
7
7
  // He was right that nothing said. The app finds whichever coding agent is
8
8
  // already signed in on the Mac and runs every worker on that subscription
9
9
  // (main/claude-plan.mjs, main/codex-account.mjs), and the walk is SILENT about
10
10
  // it by design: `needsPlan` in renderer/src/plan-setup.ts skips the plan
11
11
  // question entirely when one tool is found and signed in, which is the right
12
- // call and leaves nobody told. The only screen that has ever named the account
13
- // is Settings, two clicks in, and you have to suspect there is something to
14
- // look for before you go.
12
+ // call and leaves nobody told.
13
+ //
14
+ // IT IS A ONE-TIME SURPRISE, SO IT GETS A ONE-TIME ANSWER. A row saying this
15
+ // permanently, in the sidebar's foot, is what this file held first; it shipped
16
+ // on 2026-10-07 and came out the same day. Her words: "we have to get rid of
17
+ // this. It should never say that on the sidebar." Somebody asks where the
18
+ // tokens are coming from ONCE, on their first day, so the place for the answer
19
+ // is the walk they are standing in.
20
+ //
21
+ // AND IT NAMES NO PLAN. The row said "Claude · Max 20x" and that was the half
22
+ // she wanted gone most: "very critically, I want to get rid of that Claude Max
23
+ // 20x plan." The question being answered is "did it connect to something of
24
+ // mine", not "what am I paying for", and the second one is already answered
25
+ // properly on each agent's own page in Settings, where somebody goes when they
26
+ // want it.
15
27
  //
16
28
  // THE PLAN'S VOCABULARY, NOT THE TOOL'S. "Claude" and "ChatGPT" here, never
17
29
  // "Claude Code" and "Codex", which is the rule the walk already follows
18
30
  // (renderer/src/plan-setup.ts: words people know, "never the names of the tools
19
- // underneath"). The fact being shown is WHOSE SUBSCRIPTION IS BEING SPENT, and
20
- // nobody pays for a CLI. The tool names stay where they belong: on a byline
21
- // about which agent is working a row (`engineWordFor`, renderer/src/byline.ts),
22
- // which is a different question with a different answer.
31
+ // underneath"). The tool names stay where they belong: on a byline about which
32
+ // agent is working a row (`engineWordFor`, renderer/src/byline.ts).
23
33
  //
24
- // NOTHING IS SAID UNTIL SOMETHING IS KNOWN. Every function here answers null on
25
- // an engine it cannot name, and the screens draw nothing at all for null rather
26
- // than "checking…" or a dash. A corner that says it does not know is worse than
27
- // a corner that says nothing: it spends the glance and answers no question.
34
+ // NOTHING IS SAID UNTIL SOMETHING IS KNOWN. Both functions answer null on an
35
+ // engine they cannot name, and the walk draws nothing at all for null rather
36
+ // than "checking…" or a dash.
28
37
 
29
38
  /** The word for the plan behind an engine. Null for an engine we cannot name. */
30
39
  export function runsOnName(engine) {
@@ -33,47 +42,14 @@ export function runsOnName(engine) {
33
42
  return null;
34
43
  }
35
44
 
36
- /**
37
- * THE LINE ON A GLANCE SURFACE. "Claude · Max 20x", or just "Claude" where the
38
- * plan cannot be read.
39
- *
40
- * The plan is the raw label the tool itself reports, already turned into words
41
- * by whoever read it (`planLabel` in main/claude-plan.mjs for Claude, the
42
- * named-plan table in main/codex-account.mjs for ChatGPT). It is never
43
- * invented here: a plan nobody measured, printed confidently, is the app
44
- * lying about what somebody is paying for.
45
- */
46
- export function runsOnLine({ engine = null, plan = null } = {}) {
47
- const name = runsOnName(engine);
48
- if (!name) return null;
49
- const words = String(plan ?? '').trim();
50
- return words ? `${name} · ${words}` : name;
51
- }
52
-
53
- /**
54
- * THE WHOLE SENTENCE, for a hover and for whatever reads the screen aloud. The
55
- * glance surface carries two words and a plan; this is where the rest of the
56
- * answer lives, and it is the half that tells a new person what they are
57
- * looking at.
58
- */
59
- export function runsOnTitle({ engine = null, plan = null } = {}) {
60
- const name = runsOnName(engine);
61
- if (!name) return null;
62
- const words = String(plan ?? '').trim();
63
- const plansentence = words ? ` Your ${words} plan.` : '';
64
- return `Your agents run on the ${name} account already signed in on this Mac.${plansentence}`;
65
- }
66
-
67
45
  /**
68
46
  * THE ONE LINE DURING THE WALK, on the Mac where there was nothing to ask.
69
47
  *
70
48
  * It replaces silence, not a question: somebody with no coding agent set up
71
49
  * still gets the plan question and the setup card, and never sees this.
72
50
  */
73
- export function foundOnThisMac({ engine = null, plan = null } = {}) {
51
+ export function foundOnThisMac({ engine = null } = {}) {
74
52
  const name = runsOnName(engine);
75
53
  if (!name) return null;
76
- const words = String(plan ?? '').trim();
77
- if (words) return `Your agents will run on the ${name} ${words} plan already signed in on this Mac.`;
78
54
  return `Your agents will run on the ${name} account already signed in on this Mac.`;
79
55
  }
@@ -166,8 +166,8 @@ export const SHORTCUTS = [
166
166
  ],
167
167
  },
168
168
  {
169
- // The one group that cannot be named by WHERE she is, because all four of
170
- // its keys work both in the list and inside an open task. So it is named by
169
+ // The one group that cannot be named by WHERE she is, because all of its
170
+ // keys work both in the list and inside an open task. So it is named by
171
171
  // what the keys do, literally and in the order they are drawn.
172
172
  label: 'Closing, scheduling and undoing',
173
173
  keys: [
@@ -179,10 +179,16 @@ export const SHORTCUTS = [
179
179
  // the summary inside a thread and still meant the schedule picker in the
180
180
  // list, so this page taught one letter for two different things and an
181
181
  // office manager testing the app met both inside a minute. One meaning
182
- // per letter: S is the summary, L is later.
183
- { keys: ['L'], what: 'Put the task off until a time you pick.' },
182
+ // per letter: S is the summary, L is later. And the page says the word
183
+ // the key stands for, because that word is the action's name now
184
+ // (w-66e3ece93d): the ⌘K row, the picker and the toast all say Later.
185
+ { keys: ['L'], what: 'Later: put the task off until a time you pick.' },
184
186
  // focused branch and list switch: 'z' → undo
185
187
  { keys: ['Z'], what: 'Undo the last thing you did.' },
188
+ // opensTheToast(e.key, toast) above the focused branch → openToastRow.
189
+ // Z's other half: Z takes back what the toast announced, O goes into it.
190
+ // Only while a toast with somewhere to go is up (w-f0bfe32859).
191
+ { keys: ['O'], what: 'Open the task you just sent or answered, while its note is on screen.' },
186
192
  // (meta||ctrl) && 'a' && !inInput && !modal → tick the whole list
187
193
  { keys: ['⌘A'], what: 'Tick every task in the list. Press it again to untick.' },
188
194
  ],
@@ -133,7 +133,11 @@ const INTERRUPTED = [
133
133
  /connection (closed|lost|dropped)/i,
134
134
  /response stopped arriving/i,
135
135
  /may be incomplete/i,
136
- /econnreset|enotfound|etimedout|econnrefused/i,
136
+ /econnreset|enotfound|etimedout|econnrefused|enetunreach|ehostunreach|eai_again/i,
137
+ // Codex also prefixes non-network failures with "stream disconnected".
138
+ // Match the request failure itself so incomplete responses keep their limit.
139
+ /error sending request for url/i,
140
+ /failed to send websocket request/i,
137
141
  /unable to connect/i,
138
142
  /can.{0,3}t reach the api/i,
139
143
  /timed out/i,
@@ -164,6 +168,18 @@ const WORKSPACE = [
164
168
  /skip-git-repo-check/i,
165
169
  ];
166
170
 
171
+ // A MODEL THE LOGIN'S PLAN DOES NOT INCLUDE. Not the account being out and not
172
+ // the model being unknown: the same login runs every other model, and another
173
+ // login on a bigger plan runs this one. Codex's own words, off two dead runs on
174
+ // 2026-10-09 and 2026-10-11 (codex-cli 0.160.0, a free-plan login handed
175
+ // gpt-6.1-sol): "The 'gpt-6.1-sol' model is not supported when using Codex with
176
+ // a ChatGPT account." The second line is ours, thrown before the thread starts
177
+ // by `codexModelRefusal` when the login's own list already says so.
178
+ const MODEL_NOT_ON_PLAN = [
179
+ /model is not supported when using codex with a chatgpt account/i,
180
+ /not a model the plan of the codex account/i,
181
+ ];
182
+
167
183
  // WHICH OF THE THREE THINGS WENT WRONG, from whatever the tool printed.
168
184
  // 'unknown' is a first-class answer, not a failure: an honest generic sentence
169
185
  // beats a confident wrong one.
@@ -173,6 +189,8 @@ export function troubleCause(raw) {
173
189
  // FIRST, because an organization's refusal mentions the subscription and
174
190
  // reads as a signed-out login to the list below.
175
191
  if (ORG_BLOCKED.some((re) => re.test(text))) return 'org-blocked';
192
+ // BEFORE SIGNED-OUT, because Codex's sentence says "ChatGPT account".
193
+ if (MODEL_NOT_ON_PLAN.some((re) => re.test(text))) return 'model-not-on-plan';
176
194
  if (SIGNED_OUT.some((re) => re.test(text))) return 'signed-out';
177
195
  if (AT_LIMIT.some((re) => re.test(text))) return 'at-limit';
178
196
  if (WORKSPACE.some((re) => re.test(text))) return 'workspace';
@@ -184,6 +202,16 @@ export function troubleCause(raw) {
184
202
  return 'unknown';
185
203
  }
186
204
 
205
+ // A SUBSCRIPTION THAT HAS ENDED IS REFUSED IN AN ADMIN'S WORDS. Claude Code
206
+ // 2.1.293 told a lapsed personal Max login "Your organization has disabled
207
+ // Claude subscription access for Claude Code" (2026-10-11), the same line a
208
+ // workspace policy prints, so the text alone cannot tell them apart. The
209
+ // login's own file can: `plan` is main/claude-plan.mjs's reading of it, and
210
+ // `ended` is its billing gone to "none". Anything else keeps its cause.
211
+ export function refineCause(cause, plan) {
212
+ return cause === 'org-blocked' && plan?.ended ? 'subscription-ended' : cause;
213
+ }
214
+
187
215
  // WHEN THE LIMIT LETS GO, straight off the line the CLI printed.
188
216
  //
189
217
  // carries the one thing she actually wants to know, and we were throwing it
@@ -237,7 +265,7 @@ export function needsHerHands(cause) {
237
265
  // same person: one is her, in a terminal; one is an admin of a workspace she
238
266
  // may not run. What they share is the only thing this question asks — trying
239
267
  // again in thirty minutes cannot possibly help.
240
- return cause === 'signed-out' || cause === 'org-blocked';
268
+ return cause === 'signed-out' || cause === 'org-blocked' || cause === 'subscription-ended';
241
269
  }
242
270
 
243
271
  // WHAT SHE READS WHEN NOT ONE AGENT CAN START ANYWHERE.
@@ -246,6 +274,7 @@ export function needsHerHands(cause) {
246
274
  export function troubleSentence(cause) {
247
275
  if (cause === 'signed-out') return 'No agents can start. Your Claude login has run out.';
248
276
  if (cause === 'org-blocked') return 'No agents can start. Your organization has turned Claude Code off.';
277
+ if (cause === 'subscription-ended') return 'No agents can start. Your Claude subscription has ended.';
249
278
  if (cause === 'at-limit') return 'No agents can start. Your Claude plan is at its limit.';
250
279
  if (cause === 'workspace') return 'No agents can start. Claude will not run in this folder.';
251
280
  if (cause === 'interrupted') return 'No agents can start. The connection to Claude keeps dropping.';
@@ -260,6 +289,7 @@ export function troubleRemedy(cause) {
260
289
  // thing to type.
261
290
  if (cause === 'signed-out') return 'Open Claude in a terminal, type /login, and they pick straight back up.';
262
291
  if (cause === 'org-blocked') return 'Ask an admin of that Claude workspace to turn Claude Code back on.';
292
+ if (cause === 'subscription-ended') return 'Renew it at claude.ai, or pick another account in Settings.';
263
293
  if (cause === 'at-limit') return 'They start again on their own when it resets.';
264
294
  if (cause === 'workspace') return 'Open the folder in Claude once and say yes to trusting it.';
265
295
  if (cause === 'interrupted') return `Nothing to do. ${Name} tries them again on its own.`;
@@ -289,10 +319,12 @@ export function accountSentence(cause, dir) {
289
319
  if (cause === 'org-blocked') {
290
320
  return 'Signed in, but its organization has turned Claude Code off. Only an admin of that workspace can turn it back on.';
291
321
  }
322
+ if (cause === 'subscription-ended') return 'Signed in, but its Claude subscription has ended. Renew it at claude.ai to use this account again.';
292
323
  if (cause === 'at-limit') return 'At its limit. It rejoins on its own when the limit resets.';
293
324
  if (cause === 'workspace') return 'Claude would not run in the folder this was asked to work in.';
294
325
  if (cause === 'interrupted') return `Its runs keep getting cut off. ${Name} is trying them again.`;
295
- return `Its sessions are dying on arrival. ${Name} is keeping the other one busy.`;
326
+ // A cause we cannot name points at the account, not at the app (2026-10-11).
327
+ return 'Claude Code would not start on this account. Check that its plan is active at claude.ai.';
296
328
  }
297
329
 
298
330
  /* ------------------ the same fact about a whole ENGINE -------------------- */
@@ -437,8 +469,10 @@ const DEAD_RUN_OPENERS = {
437
469
  workspace: "would not start in this project's folder, so nothing has been done here.",
438
470
  'signed-out': 'could not sign in, so nothing has been done on this task.',
439
471
  'org-blocked': 'is switched off for that account by its organization, so nothing has been done.',
472
+ 'subscription-ended': "cannot run because that account's Claude subscription has ended, so nothing has been done.",
440
473
  'at-limit': 'is at its usage limit, so nothing has been done on this task.',
441
474
  interrupted: 'was cut off in the middle, so nothing has been done on this task.',
475
+ 'model-not-on-plan': "could not use this task's model on that account's plan, so nothing has been done.",
442
476
  unknown: 'stopped before it did anything, so this task has not been started.',
443
477
  };
444
478
 
@@ -453,10 +487,13 @@ const DEAD_RUN_OPENERS = {
453
487
  */
454
488
  const RUN_STOPPED = {
455
489
  'signed-out': 'This run stopped because the account was signed out.',
456
- 'org-blocked': 'This run stopped because that account is switched off by its organization.',
490
+ // Same refusal text for an admin's block and an ended subscription, and this
491
+ // line has only the text, so it says what is true of both.
492
+ 'org-blocked': 'This run stopped because Claude Code will not run on that account.',
457
493
  'at-limit': 'This run stopped because the account hit its usage limit.',
458
494
  workspace: "This run stopped because it would not start in this project's folder.",
459
495
  interrupted: 'This run was cut off in the middle.',
496
+ 'model-not-on-plan': "This run stopped because that account's plan does not include the model.",
460
497
  unknown: 'This run stopped before it finished.',
461
498
  };
462
499
 
@@ -464,7 +501,9 @@ export function runStoppedLine(raw) {
464
501
  return RUN_STOPPED[troubleCause(raw)] ?? RUN_STOPPED.unknown;
465
502
  }
466
503
 
467
- export function deadRunSentence({ engineWord = 'The agent', cause = 'unknown', runs = 1, resetsAt = null } = {}) {
504
+ // `elsewhere` is only read for 'model-not-on-plan': whether another signed-in
505
+ // login's plan has the model, so the task moves there rather than waiting on her.
506
+ export function deadRunSentence({ engineWord = 'The agent', cause = 'unknown', runs = 1, resetsAt = null, elsewhere = false } = {}) {
468
507
  const opener = `${engineWord} ${DEAD_RUN_OPENERS[cause] ?? DEAD_RUN_OPENERS.unknown}`;
469
508
 
470
509
  const again = runs > 1 ? ` This has happened ${runs} times on this task.` : '';
@@ -476,10 +515,16 @@ export function deadRunSentence({ engineWord = 'The agent', cause = 'unknown', r
476
515
  ? ' Sign that account back in and it picks straight up.'
477
516
  : cause === 'org-blocked'
478
517
  ? ' Only an admin of that Claude workspace can turn it back on.'
518
+ : cause === 'subscription-ended'
519
+ ? ' Renew it at claude.ai, or pick another account in Settings.'
479
520
  : cause === 'at-limit'
480
521
  ? (resetsAt ? ` It starts again on its own at ${resetsAt}.` : ' It starts again on its own when the limit resets.')
481
522
  : cause === 'interrupted'
482
523
  ? ` ${Name} runs it again on its own.`
524
+ : cause === 'model-not-on-plan'
525
+ ? (elsewhere
526
+ ? ` ${Name} runs it again on a Codex account whose plan has it.`
527
+ : ' Pick another model for this task, or sign in a Codex account whose plan has it.')
483
528
  : ` ${Name} keeps trying, and it will keep failing the same way until this is fixed.`;
484
529
 
485
530
  return `${opener}${again}${next}`;
@@ -559,9 +604,11 @@ export function deadRunCause(text) {
559
604
  export function causeWord(cause) {
560
605
  if (cause === 'signed-out') return 'a signed-out login';
561
606
  if (cause === 'org-blocked') return 'an organization that blocks Claude Code';
607
+ if (cause === 'subscription-ended') return 'an ended Claude subscription';
562
608
  if (cause === 'at-limit') return 'your Claude limit';
563
609
  if (cause === 'workspace') return 'a folder Claude will not run in';
564
610
  if (cause === 'interrupted') return 'runs getting cut off';
611
+ if (cause === 'model-not-on-plan') return "a model the account's plan does not include";
565
612
  return 'something we cannot name';
566
613
  }
567
614
 
@@ -572,9 +619,11 @@ export function causeWhat(cause, count) {
572
619
  const n = Math.max(0, Math.trunc(count ?? 0));
573
620
  if (cause === 'signed-out') return `${n} stopped by a signed-out Claude login`;
574
621
  if (cause === 'org-blocked') return `${n} stopped by an organization that blocks Claude Code`;
622
+ if (cause === 'subscription-ended') return `${n} stopped by an ended Claude subscription`;
575
623
  if (cause === 'at-limit') return `${n} waiting on your Claude limit`;
576
624
  if (cause === 'workspace') return `${n} in a folder Claude will not run in`;
577
625
  if (cause === 'interrupted') return `${n} cut off mid-run`;
626
+ if (cause === 'model-not-on-plan') return `${n} asking for a model no signed-in Codex plan includes`;
578
627
  return `${n} stopped for a reason we cannot name`;
579
628
  }
580
629
 
@@ -583,9 +632,11 @@ export function causeWhat(cause, count) {
583
632
  export function causeNext(cause, resetsAt = null) {
584
633
  if (cause === 'signed-out') return 'Type /login in a terminal and they pick straight up.';
585
634
  if (cause === 'org-blocked') return 'Ask an admin of that Claude workspace to turn Claude Code back on.';
635
+ if (cause === 'subscription-ended') return 'Renew it at claude.ai, or pick another account in Settings.';
586
636
  if (cause === 'at-limit') return resetsAt ? `They start again on their own at ${resetsAt}.` : 'They start again on their own when it resets.';
587
637
  if (cause === 'workspace') return 'Open the folder in Claude once and say yes to trusting it.';
588
638
  if (cause === 'interrupted') return `Nothing to do. ${Name} is trying them again.`;
639
+ if (cause === 'model-not-on-plan') return 'Pick another model on each, or sign in a Codex account whose plan has it.';
589
640
  return `${Name} keeps trying on its own.`;
590
641
  }
591
642
 
@@ -614,7 +665,7 @@ export function causeBreakdown(rows = [], resetsAt = null) {
614
665
  what: causeWhat(cause, count),
615
666
  next: causeNext(cause, cause === 'at-limit' ? resetsAt : null),
616
667
  act: causeAct(cause),
617
- hers: needsHerHands(cause) || cause === 'workspace',
668
+ hers: needsHerHands(cause) || cause === 'workspace' || cause === 'model-not-on-plan',
618
669
  }))
619
670
  .sort((a, b) => (b.hers - a.hers) || (b.count - a.count) || a.cause.localeCompare(b.cause));
620
671
  }
@@ -0,0 +1 @@
1
+ export function inputQueue(send: (data: string) => Promise<unknown>, options?: { chunk?: number; onError?: (e: unknown) => void }): { push(data: string): void };
@@ -0,0 +1,30 @@
1
+ // KEYS TYPED WHILE ONE WRITE IS ON ITS WAY GO OUT TOGETHER, as the next write.
2
+ //
3
+ // The panel used to send every key as its own round trip to main, each one
4
+ // waiting for the last, so holding Delete built a line of trips that drained
5
+ // after she let go. Now the first key goes at once, and whatever she types
6
+ // while it travels is one write behind it, in order. A paste still goes in
7
+ // pieces main accepts (64 KB is its limit; `chunk` stays well under it).
8
+ //
9
+ // A write that fails (the shell exited) drops what was waiting behind it: those
10
+ // keys were for a shell that is gone, and the failure is reported once.
11
+ export function inputQueue(send, { chunk = 16384, onError } = {}) {
12
+ let queued = '', sending = false;
13
+ async function drain() {
14
+ if (sending) return;
15
+ sending = true;
16
+ try {
17
+ while (queued) {
18
+ const data = queued.slice(0, chunk);
19
+ queued = queued.slice(data.length);
20
+ await send(data);
21
+ }
22
+ } catch (e) {
23
+ queued = '';
24
+ onError?.(e);
25
+ } finally {
26
+ sending = false;
27
+ }
28
+ }
29
+ return { push(data) { queued += data; void drain(); } };
30
+ }
@@ -0,0 +1 @@
1
+ export function optionWordKey(event: { type: string; key: string; altKey: boolean; ctrlKey: boolean; metaKey: boolean; shiftKey: boolean }, isMac: boolean): string | undefined;
@@ -0,0 +1,16 @@
1
+ // OPTION+LEFT AND OPTION+RIGHT JUMP A WORD, as they do in Mac Terminal.
2
+ // xterm 6 sends them as ESC[1;3D and ESC[1;3C, which zsh and bash do not bind,
3
+ // so a shell typed `;3D` into the line instead. Mac Terminal sends ESC b and
4
+ // ESC f, the backward-word and forward-word every shell already knows.
5
+ //
6
+ // Returns what to send for this key, '' to swallow the keyup of one we
7
+ // handled, or undefined to leave the key to xterm. Off a Mac nothing changes:
8
+ // Linux terminals send the same ESC[1;3D xterm does.
9
+ const WORD = { ArrowLeft: '\x1bb', ArrowRight: '\x1bf' };
10
+
11
+ export function optionWordKey(event, isMac) {
12
+ if (!isMac || !event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return undefined;
13
+ const sequence = WORD[event.key];
14
+ if (!sequence) return undefined;
15
+ return event.type === 'keydown' ? sequence : '';
16
+ }
@@ -0,0 +1,4 @@
1
+ export const SIZE_KEY: string;
2
+ export const TERMINAL_MIN: { height: number; width: number };
3
+ export function dragSize(dimension: 'height' | 'width', start: number, delta: number, room: number): number;
4
+ export function readSavedSize(raw: string | null | undefined): { height?: number; width?: number };
@@ -0,0 +1,29 @@
1
+ // HOW BIG THE TERMINAL MAY BE DRAGGED (renderer/src/components/TaskTerminal.tsx).
2
+ //
3
+ // Small enough still shows a few lines; large enough still leaves the page it
4
+ // sits in a strip to read (the thread's title and a line or two above it, or a
5
+ // column of the conversation beside it). The size is one for the whole app,
6
+ // remembered under SIZE_KEY, the way an editor remembers its panel.
7
+ export const SIZE_KEY = 'zero.terminal.size';
8
+ export const TERMINAL_MIN = { height: 120, width: 280 };
9
+ const LEAVE = { height: 120, width: 320 };
10
+ const LARGEST = 4000;
11
+
12
+ /** `start` px, the edge moved `delta` px toward growing, in a pane `room` px across. */
13
+ export function dragSize(dimension, start, delta, room) {
14
+ const min = TERMINAL_MIN[dimension];
15
+ const max = Math.max(min, room - LEAVE[dimension]);
16
+ return Math.round(Math.min(max, Math.max(min, start + delta)));
17
+ }
18
+
19
+ export function readSavedSize(raw) {
20
+ let saved;
21
+ try { saved = JSON.parse(raw ?? ''); } catch { return {}; }
22
+ if (!saved || typeof saved !== 'object' || Array.isArray(saved)) return {};
23
+ const out = {};
24
+ for (const dimension of ['height', 'width']) {
25
+ const v = saved[dimension];
26
+ if (Number.isFinite(v) && v >= TERMINAL_MIN[dimension] && v <= LARGEST) out[dimension] = v;
27
+ }
28
+ return out;
29
+ }
@@ -3,6 +3,9 @@
3
3
  // rule about which coding agent the corner is about, the zone primitives the
4
4
  // Claude reader is built on, and the words the panel prints.
5
5
 
6
+ /** What a usage check that got nothing readable says, for any cause we have not named. */
7
+ export const USAGE_UNREADABLE: string;
8
+
6
9
  export interface UsageLimit {
7
10
  span: 'session' | 'week';
8
11
  qualifier: string | null;
package/shared/usage.mjs CHANGED
@@ -33,6 +33,13 @@ import { DEFAULT_ENGINE } from './engines.mjs';
33
33
 
34
34
  /* ------------------ WHICH CODING AGENT THE CORNER IS ABOUT ---------------- */
35
35
 
36
+ // WHAT A USAGE CHECK THAT GOT NOTHING READABLE SAYS, for every reason we have
37
+ // not named. It used to be "Could not refresh usage. Try again.", which reads
38
+ // as the app having broken; the measured case (2026-10-11) was an account whose
39
+ // subscription had ended, and Claude Code simply printed no limits for it. So
40
+ // it points at the account, which is where an unnamed cause almost always is.
41
+ export const USAGE_UNREADABLE = "Claude Code didn't report usage for this account. Check that its plan is active at claude.ai.";
42
+
36
43
  /**
37
44
  * THE AGENT MOST OF HER LIVE WORK IS ON, or the workspace's own when nothing is
38
45
  * running and when the fleet is level.