agentbox-app 0.1.11 → 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/README.md +24 -16
- package/bin/agentbox.mjs +18 -9
- package/briefs/message-rules.md +15 -4
- package/main/account-tooling.mjs +2 -1
- 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 -20
- package/main/analytics.mjs +30 -0
- package/main/approvals.mjs +14 -5
- package/main/claude-bin.mjs +45 -310
- 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 +14 -2
- package/main/claude-usage.mjs +82 -25
- package/main/codex-bin.mjs +51 -248
- package/main/codex-launch-env.mjs +31 -0
- 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 +40 -13
- package/main/corner-tag.mjs +489 -0
- package/main/crash-report.mjs +26 -3
- package/main/demo.mjs +17 -14
- package/main/engine-bin.mjs +595 -0
- package/main/engine-setup.mjs +27 -27
- package/main/feedback.mjs +11 -6
- package/main/fresh-user.mjs +1 -1
- package/main/git-change.mjs +8 -2
- 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/instruction-settings.mjs +47 -11
- package/main/ipc.mjs +175 -22
- package/main/local-app-frames.mjs +82 -0
- package/main/machine.mjs +1 -1
- package/main/main.mjs +120 -5
- 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 +48 -5
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-link.mjs +551 -0
- package/main/refresh-agent-models.mjs +2 -1
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +246 -0
- package/main/row-label.mjs +8 -7
- package/main/runs-on.mjs +90 -0
- package/main/serve.mjs +124 -10
- package/main/settings.mjs +171 -11
- package/main/shell-path.mjs +86 -0
- package/main/ship-queue.mjs +25 -7
- 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/home.mjs +37 -5
- package/main/store/project.mjs +15 -19
- package/main/store.mjs +10 -4
- package/main/supervisor.mjs +1030 -447
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +491 -82
- package/main/task-terminals.mjs +49 -8
- package/main/team/index.mjs +2 -2
- package/main/team/projects.mjs +1 -1
- package/main/updater.mjs +71 -9
- package/main/worktree-include.mjs +494 -0
- package/mcp/core/page.mjs +1 -1
- package/mcp/core/work.mjs +34 -3
- package/mcp/tools.mjs +2 -1
- 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/after-install.mjs +14 -0
- package/scripts/approval-server.sh +31 -7
- package/scripts/before-publish.mjs +12 -10
- package/shared/agent-import.mjs +3 -1
- package/shared/agents.d.mts +1 -0
- package/shared/bridge-map.mjs +19 -0
- package/shared/claude-commands.generated.mjs +8 -6
- package/shared/claude-models.generated.mjs +10 -8
- package/shared/codex-modes.mjs +1 -1
- package/shared/corner-tag.d.mts +14 -0
- package/shared/corner-tag.mjs +141 -0
- package/shared/demo-world.mjs +1 -1
- package/shared/engines.mjs +20 -5
- package/shared/fresh-user-home.mjs +12 -1
- package/shared/harness-definitions.mjs +103 -0
- package/shared/instruction-defaults.json +1 -1
- package/shared/notify-rules.d.mts +25 -0
- package/shared/notify-rules.mjs +19 -0
- package/shared/project-folder-check.mjs +3 -3
- 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 +10 -0
- package/shared/runs-on.mjs +55 -0
- package/shared/shortcuts.mjs +11 -5
- package/shared/spawn-trouble.mjs +58 -7
- 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 +28 -2
- 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-B-2Az952.css +0 -32
- package/renderer/dist/assets/index-D6qncgai.js +0 -1168
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,10 +274,11 @@ 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.';
|
|
252
|
-
return 'No agents can start on this
|
|
281
|
+
return 'No agents can start on this computer right now.';
|
|
253
282
|
}
|
|
254
283
|
|
|
255
284
|
// THE SECOND LINE: what happens next, so the first line is never a dead end.
|
|
@@ -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.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Types for shared/where-it-runs-from.mjs, which is plain ESM so that main
|
|
2
|
+
// (Node) and the screen (Vite) share one copy of the rule about whether this
|
|
3
|
+
// copy of the app can update itself from where it is, and one copy of the
|
|
4
|
+
// words that say so. The screen asks for the words and `sayWhere`; the places
|
|
5
|
+
// and the move are main's.
|
|
6
|
+
|
|
7
|
+
/** The place a copy of the app is in, as far as updating itself is concerned. */
|
|
8
|
+
export type AppPlace =
|
|
9
|
+
| 'disk-image' | 'translocated' | 'applications' | 'elsewhere' | 'source' | 'not-mac';
|
|
10
|
+
|
|
11
|
+
export const PLACE: {
|
|
12
|
+
diskImage: 'disk-image';
|
|
13
|
+
translocated: 'translocated';
|
|
14
|
+
applications: 'applications';
|
|
15
|
+
elsewhere: 'elsewhere';
|
|
16
|
+
source: 'source';
|
|
17
|
+
notMac: 'not-mac';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const MOVE_SAY: {
|
|
21
|
+
cannotUpdateHere: string;
|
|
22
|
+
button: string;
|
|
23
|
+
title: string;
|
|
24
|
+
where: Record<string, string>;
|
|
25
|
+
shortWhere: Record<string, string>;
|
|
26
|
+
fix: string;
|
|
27
|
+
retry: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** The sentence for a place, or null where there is nothing to say. */
|
|
31
|
+
export function sayWhere(place: string | null | undefined, how?: { short?: boolean }): string | null;
|
|
32
|
+
|
|
33
|
+
export function placeOfApp(facts?: {
|
|
34
|
+
platform?: string;
|
|
35
|
+
packaged?: boolean;
|
|
36
|
+
exePath?: string | null;
|
|
37
|
+
inApplications?: boolean;
|
|
38
|
+
}): { place: AppPlace; canUpdate: boolean; offerMove: boolean };
|
|
39
|
+
|
|
40
|
+
export function createPlace(host: { app: unknown; log?: unknown }): {
|
|
41
|
+
state(): { place: AppPlace; canUpdate: boolean; offerMove: boolean };
|
|
42
|
+
move(): { moved: boolean; error: string | null };
|
|
43
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// AN APP IN THE WRONG PLACE CAN NEVER UPDATE ITSELF, AND NEVER SAID SO.
|
|
2
|
+
//
|
|
3
|
+
// macOS has two places it will happily run Agentbox from and never let it
|
|
4
|
+
// replace itself:
|
|
5
|
+
//
|
|
6
|
+
// THE DISK IMAGE. You double-click the .dmg, you double-click the app
|
|
7
|
+
// inside it, and it works. The volume is read-only, so Squirrel.Mac refuses
|
|
8
|
+
// the swap with "Cannot update while running on a read-only volume".
|
|
9
|
+
//
|
|
10
|
+
// A TRANSLOCATED COPY. You drag the app to Downloads and open it. Because
|
|
11
|
+
// it still carries the quarantine flag and is not in Applications, macOS
|
|
12
|
+
// runs it from a read-only copy it makes under
|
|
13
|
+
// `/private/var/folders/…/AppTranslocation/<uuid>/d/`. Same refusal, and
|
|
14
|
+
// this is why Squirrel's own sentence talks about the Downloads directory.
|
|
15
|
+
//
|
|
16
|
+
// Measured in PostHog on 2026-10-12: 23 crash reports over three weeks, two
|
|
17
|
+
// installs, both on 0.1.11, one report every six hours (`EVERY_MS` in
|
|
18
|
+
// main/updater.mjs) for as long as the app was open. The reports stopped being
|
|
19
|
+
// filed as crashes in 292d419. The two Macs are still on 0.1.11.
|
|
20
|
+
//
|
|
21
|
+
// SO THE APP READS WHERE IT IS AND OFFERS THE ONE PRESS THAT FIXES IT. Electron
|
|
22
|
+
// has had `moveToApplicationsFolder` since 4: it moves the bundle, replaces an
|
|
23
|
+
// older copy if you say so, and relaunches from the new place. After that the
|
|
24
|
+
// ordinary six-hourly check works and the person never thinks about it again.
|
|
25
|
+
//
|
|
26
|
+
// EVERYTHING HERE IS A FACT AND NOT A GUESS, which is why the decision is a
|
|
27
|
+
// pure function with the four facts passed in: a wrong "you are in the wrong
|
|
28
|
+
// place" is a nag on a healthy install, and two of those are worse than the
|
|
29
|
+
// silence this replaces.
|
|
30
|
+
|
|
31
|
+
import { Name } from './product-name.mjs';
|
|
32
|
+
|
|
33
|
+
// THE WORDS, IN ONE PLACE, and main and the renderer both read them from here.
|
|
34
|
+
// `cannotUpdateHere` is the sentence `shortError` in main/updater.mjs puts in
|
|
35
|
+
// place of Squirrel's own paragraph, which is 240 characters with a URL in the
|
|
36
|
+
// middle and was arriving on the Settings row cut off at 160.
|
|
37
|
+
//
|
|
38
|
+
// Every sentence says where the app is and what one press does about it, and
|
|
39
|
+
// none of them says "read-only volume": that is the mechanism, and nobody
|
|
40
|
+
// holding the Mac asked about the mechanism.
|
|
41
|
+
export const MOVE_SAY = {
|
|
42
|
+
cannotUpdateHere: `${Name} cannot update itself from where it is.`,
|
|
43
|
+
button: 'Move to Applications',
|
|
44
|
+
/** The sidebar card's heading. */
|
|
45
|
+
title: 'Updates are stuck',
|
|
46
|
+
/** What the place is, as a person would say it. */
|
|
47
|
+
where: {
|
|
48
|
+
'disk-image': `${Name} is running from the disk image it was downloaded in.`,
|
|
49
|
+
translocated: `${Name} is running from a read-only copy macOS made of it.`,
|
|
50
|
+
},
|
|
51
|
+
/** The same fact in half the words, for the sidebar card, which is one
|
|
52
|
+
* column wide and had the row's sentence running to six lines in it. */
|
|
53
|
+
shortWhere: {
|
|
54
|
+
'disk-image': `${Name} can’t update itself from the disk image.`,
|
|
55
|
+
translocated: `${Name} can’t update itself from the copy macOS made.`,
|
|
56
|
+
},
|
|
57
|
+
/** And the press, which is the same either way. */
|
|
58
|
+
fix: 'Moving it to your Applications folder takes a few seconds and gets updates working again.',
|
|
59
|
+
retry: 'Pressing it again tries again.',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** The sentence for a place, or null where there is nothing to say. `short` is
|
|
63
|
+
* the sidebar card's half of it: the button beside it already says the fix. */
|
|
64
|
+
export function sayWhere(place, { short = false } = {}) {
|
|
65
|
+
const words = short ? MOVE_SAY.shortWhere : MOVE_SAY.where;
|
|
66
|
+
return words[place] ?? null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Where a copy of the app can be, as far as updating itself is concerned. */
|
|
70
|
+
export const PLACE = {
|
|
71
|
+
/** Run out of the mounted .dmg. Read-only; cannot update. */
|
|
72
|
+
diskImage: 'disk-image',
|
|
73
|
+
/** macOS is running a read-only copy of its own. Cannot update. */
|
|
74
|
+
translocated: 'translocated',
|
|
75
|
+
/** Where it belongs. */
|
|
76
|
+
applications: 'applications',
|
|
77
|
+
/** Somewhere writable that is not Applications: updates itself fine. */
|
|
78
|
+
elsewhere: 'elsewhere',
|
|
79
|
+
/** `npm start` from a checkout: main/source-updater.mjs, nothing to move. */
|
|
80
|
+
source: 'source',
|
|
81
|
+
/** No Applications folder and no `moveToApplicationsFolder`. */
|
|
82
|
+
notMac: 'not-mac',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// The folder macOS puts a translocated copy in. The path is always under
|
|
86
|
+
// `/private/var/folders`, but the one segment that names the mechanism is this
|
|
87
|
+
// one, and it is the only part Apple has kept stable. Written with "/" and
|
|
88
|
+
// not `path.sep`: these are only ever read on a Mac (`placeOfApp` returns
|
|
89
|
+
// before them anywhere else), and the screen imports this file, so it may not
|
|
90
|
+
// import anything from Node.
|
|
91
|
+
const TRANSLOCATED = '/AppTranslocation/';
|
|
92
|
+
// Every mounted volume lives here, including the disk image the download is.
|
|
93
|
+
const VOLUMES = '/Volumes/';
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* PURE. The whole decision, off four facts, so every case can be tested
|
|
97
|
+
* without a packaged app, a disk image, or a Mac.
|
|
98
|
+
*
|
|
99
|
+
* `exePath` is `app.getPath('exe')`, the binary inside the bundle;
|
|
100
|
+
* `inApplications` is Electron's own `app.isInApplicationsFolder()`, which
|
|
101
|
+
* knows about `~/Applications` as well as `/Applications` and is therefore
|
|
102
|
+
* never re-derived from the path here.
|
|
103
|
+
*/
|
|
104
|
+
export function placeOfApp({ platform, packaged, exePath, inApplications } = {}) {
|
|
105
|
+
const no = (place) => ({ place, canUpdate: true, offerMove: false });
|
|
106
|
+
if (platform !== 'darwin') return no(PLACE.notMac);
|
|
107
|
+
if (!packaged) return no(PLACE.source);
|
|
108
|
+
if (inApplications) return no(PLACE.applications);
|
|
109
|
+
if (typeof exePath !== 'string' || !exePath) return no(PLACE.elsewhere);
|
|
110
|
+
const where = exePath.includes(TRANSLOCATED) ? PLACE.translocated
|
|
111
|
+
: exePath.startsWith(VOLUMES) ? PLACE.diskImage
|
|
112
|
+
: null;
|
|
113
|
+
// THE PLACES THAT ARE NOT APPLICATIONS AND UPDATE THEMSELVES ANYWAY are left
|
|
114
|
+
// alone on purpose. A copy sitting in Downloads that macOS did not
|
|
115
|
+
// translocate is on a writable disk and Squirrel swaps it happily; offering
|
|
116
|
+
// to move it would be the app asking for something it does not need.
|
|
117
|
+
if (!where) return no(PLACE.elsewhere);
|
|
118
|
+
return { place: where, canUpdate: false, offerMove: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The live one, over Electron's `app`. Read ONCE, at construction: the bundle
|
|
123
|
+
* cannot move under a running process except by the move below, which
|
|
124
|
+
* relaunches.
|
|
125
|
+
*/
|
|
126
|
+
export function createPlace({ app, log = console }) {
|
|
127
|
+
const where = placeOfApp({
|
|
128
|
+
platform: app?.platform ?? process.platform,
|
|
129
|
+
packaged: !!app?.isPackaged,
|
|
130
|
+
exePath: (() => { try { return app?.getPath?.('exe') ?? null; } catch { return null; } })(),
|
|
131
|
+
inApplications: (() => { try { return !!app?.isInApplicationsFolder?.(); } catch { return false; } })(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
state: () => ({ ...where }),
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* THE PRESS. Electron moves the bundle and relaunches, so nothing after a
|
|
139
|
+
* `true` here ever runs. It answers false when the person cancels its own
|
|
140
|
+
* "a copy is already there" box, which is an answer and not a failure.
|
|
141
|
+
*/
|
|
142
|
+
move() {
|
|
143
|
+
if (!where.offerMove) return { moved: false, error: null };
|
|
144
|
+
try {
|
|
145
|
+
const moved = !!app.moveToApplicationsFolder();
|
|
146
|
+
return { moved, error: null };
|
|
147
|
+
} catch (e) {
|
|
148
|
+
// One line, because Settings shows this string to a person.
|
|
149
|
+
const error = String(e?.message ?? e ?? 'The move did not work.').split('\n')[0].trim();
|
|
150
|
+
log.warn?.('[place] could not move the app into Applications:', error);
|
|
151
|
+
return { moved: false, error };
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
package/shared/work-items.mjs
CHANGED
|
@@ -108,7 +108,7 @@ export const CLAUDE_PERMISSION_MODES = [
|
|
|
108
108
|
// search matches either name.
|
|
109
109
|
export const WORK_ITEM_FIELDS = [
|
|
110
110
|
'title', 'label', 'body', 'kind', 'labels', 'priority', 'parent',
|
|
111
|
-
'status', 'result', 'note', 'answer', 'product', 'runAt', 'answeredThrough',
|
|
111
|
+
'status', 'result', 'note', 'answer', 'product', 'runAt', 'snoozedUntil', 'answeredThrough',
|
|
112
112
|
'engine', 'model', 'effort',
|
|
113
113
|
// WHETHER THIS HAS BEEN STARTED AT ALL. 'later' is a thread written down and
|
|
114
114
|
// deliberately not begun: it waits for a person, not for a clock, and
|
|
@@ -366,6 +366,14 @@ function str(value, max) {
|
|
|
366
366
|
return trimmed.slice(0, lo) + mark(trimmed.length - lo);
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
// A text field whose empty string MEANS something: nothing chosen. Only a
|
|
370
|
+
// string may clear it, so a `null` that reached here without going through
|
|
371
|
+
// `answerItem` is still dropped rather than guessed at.
|
|
372
|
+
function clearable(value, max) {
|
|
373
|
+
if (typeof value !== 'string') return undefined;
|
|
374
|
+
return value.trim() ? str(value, max) : '';
|
|
375
|
+
}
|
|
376
|
+
|
|
369
377
|
function coerceField(field, value) {
|
|
370
378
|
switch (field) {
|
|
371
379
|
case 'title': return str(value, MAX_TITLE);
|
|
@@ -388,7 +396,23 @@ function coerceField(field, value) {
|
|
|
388
396
|
// shared/effort-levels.mjs, and the spawn gates on them, so a value this
|
|
389
397
|
// module does not recognise is a run at Claude Code's own choice rather
|
|
390
398
|
// than a row that never runs.
|
|
391
|
-
case 'kind': case 'product': case 'parent': case 'engine':
|
|
399
|
+
case 'kind': case 'product': case 'parent': case 'engine': return str(value, MAX_SHORT);
|
|
400
|
+
// THESE TWO ARE CLEARED BY WRITING '' AND SO '' IS A REAL VALUE, exactly
|
|
401
|
+
// as runAt's 0 and start's 'now' are, and for the same reason: a field that
|
|
402
|
+
// coerces to undefined is dropped by the fold, so without a falsy value
|
|
403
|
+
// that survives, a model could be chosen and never unchosen.
|
|
404
|
+
//
|
|
405
|
+
// They used to sit on the line above, which cost us the most common crash
|
|
406
|
+
// of launch week. The reply box's drawer hands a pick back as null for
|
|
407
|
+
// "nothing chosen" and sends `{ model, effort }` whenever it was touched at
|
|
408
|
+
// all; `answerItem` turns null into '' because `modelForEngine` reads the
|
|
409
|
+
// empty string as nothing chosen. `str()` then dropped it, `pickFields`
|
|
410
|
+
// answered null, and `buildLine` threw — before the answer was written,
|
|
411
|
+
// because the model is written first on purpose. So opening the drawer,
|
|
412
|
+
// picking a model and pressing send lost the message, with nothing on
|
|
413
|
+
// screen to say so. Five of the 14 crashes since the Oct 6 launch, on two
|
|
414
|
+
// installs (PostHog, 2026-10-08).
|
|
415
|
+
case 'model': case 'effort': return clearable(value, MAX_SHORT);
|
|
392
416
|
case 'status': return WORK_ITEM_STATUSES.includes(value) ? value : undefined;
|
|
393
417
|
case 'priority': return Number.isFinite(value) ? Math.trunc(value) : undefined;
|
|
394
418
|
// When this item may next be acted on, in epoch ms. 0 means unscheduled,
|
|
@@ -396,6 +420,8 @@ function coerceField(field, value) {
|
|
|
396
420
|
// that coerces to undefined is dropped by the fold, so without a falsy
|
|
397
421
|
// sentinel a schedule could be set and never cleared.
|
|
398
422
|
case 'runAt': return Number.isFinite(value) ? Math.trunc(value) : undefined;
|
|
423
|
+
// An inbox reminder, independent of when work may start. 0 brings it back.
|
|
424
|
+
case 'snoozedUntil': return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : undefined;
|
|
399
425
|
// 'later' or 'now', and 'now' is the falsy sentinel for the same reason
|
|
400
426
|
// runAt keeps 0: a field that coerces to undefined is dropped by the fold,
|
|
401
427
|
// so without a real value for "started" nothing could ever clear it.
|
package/shared/work-lines.mjs
CHANGED
|
@@ -637,7 +637,7 @@ export function groupWork(events, { min = RUN_MIN } = {}) {
|
|
|
637
637
|
for (const e of events ?? []) {
|
|
638
638
|
// SOMETHING SHE DID IS NOT THE AGENT'S WORK and never folds into it: three
|
|
639
639
|
// snoozes are three lines, not one "You put it off" (w-49b4e45403).
|
|
640
|
-
if (e && e.kind === 'work' && !e.yours) { run.push(e); continue; }
|
|
640
|
+
if (e && e.kind === 'work' && !e.yours && !e.standalone) { run.push(e); continue; }
|
|
641
641
|
flush();
|
|
642
642
|
out.push(e);
|
|
643
643
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// XTERM'S OWN ASSERTION, CAUGHT WHERE WE CAN REACH IT.
|
|
2
|
+
//
|
|
3
|
+
// @xterm/xterm 6.0.0's AccessibilityManager listens on the document for
|
|
4
|
+
// 'selectionchange', maps the selection onto terminal rows and columns, and
|
|
5
|
+
// does `throw new Error('invalid range')` when the result covers no cells,
|
|
6
|
+
// under its own comment "This should not happen unless we have some bugs". It
|
|
7
|
+
// happens. Three ordinary selections reach it, measured against the published
|
|
8
|
+
// bundle; the test beside this file
|
|
9
|
+
// (tests/a-selection-that-covers-no-terminal-cells-is-not-a-crash.test.mjs)
|
|
10
|
+
// lists them and says how.
|
|
11
|
+
//
|
|
12
|
+
// Only a screen reader user gets that handler at all, because the pane asks
|
|
13
|
+
// `app.accessibilitySupportEnabled` before turning `screenReaderMode` on. For
|
|
14
|
+
// them the throw escapes as an unhandled renderer error, and the terminal keeps
|
|
15
|
+
// whatever was selected before it, so a copy afterwards takes stale text.
|
|
16
|
+
//
|
|
17
|
+
// WHY A WRAPPER AND NOT A PATCHED BUNDLE. What we build against is minified
|
|
18
|
+
// `lib/xterm.mjs`, and rewriting a file inside node_modules at build time is
|
|
19
|
+
// machinery that has to hold through `npm ci` on two CI runners and every
|
|
20
|
+
// release. xterm dispatches the handler as `() => this._handleSelectionChange()`,
|
|
21
|
+
// off the instance, so one own property on the manager shadows it and the
|
|
22
|
+
// wrapper dies with the terminal.
|
|
23
|
+
//
|
|
24
|
+
// IT SWALLOWS ONE MESSAGE AND NOTHING ELSE. The same handler warns about two
|
|
25
|
+
// genuine race conditions and returns; anything that is not this exact
|
|
26
|
+
// assertion is somebody's bug and goes on up.
|
|
27
|
+
//
|
|
28
|
+
// This is a workaround, and the upstream fix is the assertion becoming a
|
|
29
|
+
// clearSelection and a return. When an xterm release carries that, the test
|
|
30
|
+
// above turns red on the bundle it reads, and this file goes.
|
|
31
|
+
export const INVALID_RANGE = 'invalid range';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wrap the accessibility manager's selection handler on `terminal`, which must
|
|
35
|
+
* already be open. Answers whether there was one to wrap: false when
|
|
36
|
+
* screenReaderMode is off, which is every install not running a screen reader,
|
|
37
|
+
* and false when it is already wrapped.
|
|
38
|
+
*/
|
|
39
|
+
export function guardInvalidRange(terminal, { warn = console.warn } = {}) {
|
|
40
|
+
const manager = terminal?._core?._accessibilityManager?.value;
|
|
41
|
+
const handler = manager?._handleSelectionChange;
|
|
42
|
+
if (typeof handler !== 'function' || handler.guardsInvalidRange) return false;
|
|
43
|
+
const guarded = function (...args) {
|
|
44
|
+
try {
|
|
45
|
+
return handler.apply(this, args);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
// Message, not instanceof: an Error from another realm is still this bug.
|
|
48
|
+
if (e?.message !== INVALID_RANGE) throw e;
|
|
49
|
+
warn(`xterm mapped the selection onto no cells and threw "${INVALID_RANGE}"; cleared the terminal's selection instead`);
|
|
50
|
+
// It threw before mirroring the selection, so the terminal is still
|
|
51
|
+
// holding the previous one. Nothing is selected; say that.
|
|
52
|
+
terminal.clearSelection();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
guarded.guardsInvalidRange = true;
|
|
56
|
+
manager._handleSelectionChange = guarded;
|
|
57
|
+
return true;
|
|
58
|
+
}
|