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.
- package/main/acp.mjs +296 -0
- package/main/agent-homes.mjs +158 -0
- package/main/agent-sessions.mjs +57 -20
- package/main/agent-updates.mjs +9 -22
- package/main/analytics.mjs +30 -0
- package/main/claude-bin.mjs +45 -311
- package/main/claude-input.mjs +9 -3
- package/main/claude-launch-env.mjs +54 -0
- package/main/claude-models.mjs +39 -7
- package/main/claude-plan.mjs +16 -16
- package/main/claude-usage.mjs +82 -25
- package/main/codex-bin.mjs +51 -249
- package/main/codex-session.mjs +38 -3
- package/main/codex-threads.mjs +41 -8
- package/main/codex-watch.mjs +67 -3
- package/main/config.mjs +26 -4
- package/main/corner-tag.mjs +489 -0
- package/main/crash-report.mjs +26 -3
- package/main/engine-bin.mjs +595 -0
- package/main/engine-setup.mjs +4 -15
- package/main/feedback.mjs +11 -6
- package/main/harnesses/claude-stream.mjs +208 -0
- package/main/harnesses/claude.mjs +84 -0
- package/main/harnesses/codex.mjs +65 -0
- package/main/harnesses/common.mjs +78 -0
- package/main/harnesses/instructions-file.mjs +116 -0
- package/main/harnesses/opencode.mjs +234 -0
- package/main/harnesses.mjs +65 -0
- package/main/importable-threads.mjs +38 -0
- package/main/ipc.mjs +165 -31
- package/main/local-app-frames.mjs +82 -0
- package/main/main.mjs +100 -4
- package/main/memory-gate-history.mjs +51 -12
- package/main/memory-gate-server.mjs +67 -11
- package/main/memory-gate.mjs +49 -8
- package/main/memory-log.mjs +124 -0
- package/main/notify.mjs +47 -4
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-link.mjs +551 -0
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +246 -0
- package/main/row-label.mjs +6 -6
- package/main/runs-on.mjs +32 -35
- package/main/serve.mjs +113 -9
- package/main/settings.mjs +148 -10
- package/main/shell-path.mjs +86 -0
- package/main/ship-queue.mjs +24 -6
- package/main/sign-in-files.mjs +3 -7
- package/main/source-updater.mjs +27 -1
- package/main/start-hold.mjs +73 -0
- package/main/store.mjs +1 -1
- package/main/supervisor.mjs +832 -428
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +490 -98
- package/main/task-terminals.mjs +37 -7
- package/main/updater.mjs +71 -9
- package/main/worktree-include.mjs +494 -0
- package/package.json +10 -1
- package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
- package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
- package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
- package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
- package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
- package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
- package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
- package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
- package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
- package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
- package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
- package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
- package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
- package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
- package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
- package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
- package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
- package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
- package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
- package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
- package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
- package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
- package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
- package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
- package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
- package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
- package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
- package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
- package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
- package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
- package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
- package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
- package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
- package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
- package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
- package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
- package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
- package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
- package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
- package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
- package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
- package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
- package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
- package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
- package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
- package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
- package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
- package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
- package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
- package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
- package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
- package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
- package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
- package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
- package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
- package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
- package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
- package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
- package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
- package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
- package/renderer/dist/corner-tag.html +13 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +297 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/index.html +16 -3
- package/scripts/approval-server.sh +24 -5
- package/scripts/before-publish.mjs +12 -10
- package/shared/agent-import.mjs +2 -0
- package/shared/agents.d.mts +1 -0
- package/shared/bridge-map.mjs +18 -0
- package/shared/claude-commands.generated.mjs +6 -4
- package/shared/claude-models.generated.mjs +6 -4
- package/shared/corner-tag.d.mts +14 -0
- package/shared/corner-tag.mjs +141 -0
- package/shared/engines.mjs +20 -5
- package/shared/harness-definitions.mjs +103 -0
- package/shared/notify-rules.d.mts +25 -0
- package/shared/notify-rules.mjs +19 -0
- package/shared/provider-commands.mjs +10 -2
- package/shared/rank.d.mts +2 -0
- package/shared/rank.mjs +50 -0
- package/shared/relay-address.mjs +6 -0
- package/shared/relay-crypto.mjs +165 -0
- package/shared/runs-on.d.mts +0 -10
- package/shared/runs-on.mjs +25 -49
- package/shared/shortcuts.mjs +10 -4
- package/shared/spawn-trouble.mjs +57 -6
- package/shared/terminal-input.d.mts +1 -0
- package/shared/terminal-input.mjs +30 -0
- package/shared/terminal-keys.d.mts +1 -0
- package/shared/terminal-keys.mjs +16 -0
- package/shared/terminal-size.d.mts +4 -0
- package/shared/terminal-size.mjs +29 -0
- package/shared/usage.d.mts +3 -0
- package/shared/usage.mjs +7 -0
- package/shared/where-it-runs-from.d.mts +43 -0
- package/shared/where-it-runs-from.mjs +155 -0
- package/shared/work-items.mjs +25 -1
- package/shared/work-lines.mjs +1 -1
- package/shared/xterm-invalid-range.d.mts +2 -0
- package/shared/xterm-invalid-range.mjs +58 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
- 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
|
+
}
|
package/shared/runs-on.d.mts
CHANGED
|
@@ -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;
|
package/shared/runs-on.mjs
CHANGED
|
@@ -1,30 +1,39 @@
|
|
|
1
|
-
//
|
|
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:
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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.
|
|
13
|
-
//
|
|
14
|
-
//
|
|
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
|
|
20
|
-
//
|
|
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.
|
|
25
|
-
//
|
|
26
|
-
// than "checking…" or a dash.
|
|
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
|
|
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
|
}
|
package/shared/shortcuts.mjs
CHANGED
|
@@ -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
|
|
170
|
-
//
|
|
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
|
-
|
|
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
|
],
|
package/shared/spawn-trouble.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/shared/usage.d.mts
CHANGED
|
@@ -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.
|