agentbox-app 0.1.13 → 0.1.16
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 -9
- package/bin/agentbox.mjs +23 -1
- package/briefs/message-rules.md +36 -8
- package/briefs/worker.md +46 -23
- package/main/account-discovery.mjs +122 -30
- package/main/account-tooling.mjs +29 -1
- package/main/account-usage.mjs +46 -0
- package/main/agent-sessions.mjs +282 -268
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +12 -2
- package/main/answer-modes.mjs +103 -37
- package/main/approval-prompt-core.mjs +197 -0
- package/main/approval-prompt-server.mjs +4 -138
- package/main/approvals.mjs +101 -297
- package/main/artifact-path.mjs +47 -18
- package/main/claude-input.mjs +48 -4
- package/main/claude-plan.mjs +48 -0
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +18 -40
- package/main/cloud/account.mjs +179 -0
- package/main/cloud/aws.mjs +127 -0
- package/main/cloud/browser-return.mjs +58 -0
- package/main/cloud/clients.mjs +15 -0
- package/main/cloud/codex-cloud.mjs +53 -0
- package/main/cloud/config.mjs +107 -0
- package/main/cloud/docs.mjs +219 -0
- package/main/cloud/exec.mjs +27 -0
- package/main/cloud/host.mjs +165 -0
- package/main/cloud/kit.mjs +80 -0
- package/main/cloud/logins.mjs +543 -0
- package/main/cloud/managed.mjs +115 -0
- package/main/cloud/marker.mjs +104 -0
- package/main/cloud/org.mjs +53 -0
- package/main/cloud/reuse.mjs +208 -0
- package/main/cloud/run-cli.mjs +177 -0
- package/main/cloud/run.mjs +1934 -0
- package/main/cloud/usage.mjs +76 -0
- package/main/cloud-permission.mjs +51 -0
- package/main/codex-account.mjs +78 -0
- package/main/codex-app-server.mjs +276 -409
- package/main/codex-approvals.mjs +148 -433
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +212 -507
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +71 -3
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +17 -3
- package/main/codex.mjs +95 -363
- package/main/config.mjs +11 -1
- package/main/corner-tag.mjs +340 -19
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +232 -282
- package/main/engine-setup.mjs +21 -9
- package/main/feedback.mjs +3 -25
- package/main/folder-pictures.mjs +2 -2
- package/main/freeze-recorder-thread.mjs +86 -0
- package/main/freeze-recorder.mjs +146 -0
- package/main/fresh-user.mjs +20 -9
- package/main/git-change-offthread.mjs +17 -3
- package/main/git-change-thread.mjs +5 -2
- package/main/harnesses/claude.mjs +32 -11
- package/main/harnesses/codex.mjs +9 -2
- package/main/importable-threads.mjs +64 -6
- package/main/ipc.mjs +362 -111
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +199 -84
- package/main/memory-gate-history.mjs +36 -10
- package/main/memory-gate-server.mjs +62 -0
- package/main/memory-gate.mjs +14 -1
- package/main/menu.mjs +4 -2
- package/main/message-priority.mjs +2 -2
- package/main/mic.mjs +38 -0
- package/main/native/liquid-glass/build/liquid-glass.node +0 -0
- package/main/new-chats-watch.mjs +110 -0
- package/main/new-user-window.mjs +25 -0
- package/main/notify.mjs +24 -8
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +29 -9
- package/main/phone-uploads.mjs +476 -0
- package/main/picture-shape.mjs +114 -0
- package/main/quit-sequence.mjs +143 -0
- package/main/refresh-agent-models.mjs +15 -2
- package/main/relay-client.mjs +69 -1
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +12 -41
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +45 -3
- package/main/settings.mjs +220 -44
- package/main/shell-path.mjs +185 -37
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +115 -19
- package/main/source-updater.mjs +18 -10
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/store/home.mjs +87 -232
- package/main/store/paths.mjs +11 -0
- package/main/store/teammates-files.mjs +99 -0
- package/main/store/work-items.mjs +231 -13
- package/main/store.mjs +226 -52
- package/main/supervisor.mjs +3103 -341
- package/main/task-folders.mjs +430 -30
- package/main/task-terminals.mjs +110 -34
- package/main/team/index.mjs +114 -10
- package/main/team/memory-cloud.mjs +183 -1
- package/main/team/room-hours.mjs +48 -0
- package/main/team/room-setup.mjs +31 -0
- package/main/team/rooms-memory.mjs +454 -0
- package/main/team/rooms-shape.mjs +42 -0
- package/main/team/rooms-supabase.mjs +324 -0
- package/main/team/rooms.mjs +575 -0
- package/main/team/session.mjs +96 -26
- package/main/team/supabase-backend.mjs +180 -0
- package/main/team/sync.mjs +109 -1
- package/main/teammates.mjs +580 -0
- package/main/terminal-host.mjs +143 -0
- package/main/terminal-hosts.mjs +261 -0
- package/main/transcript-search.mjs +80 -0
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +90 -219
- package/mcp/core/documents.mjs +3 -1
- package/mcp/core/repeats.mjs +88 -0
- package/mcp/core/teammates.mjs +222 -0
- package/mcp/core/work.mjs +76 -4
- package/mcp/stdio.mjs +24 -11
- package/mcp/tools.mjs +116 -8
- package/package.json +21 -5
- package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
- package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
- package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
- package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
- package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
- package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
- package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
- package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
- package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
- package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
- package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
- package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
- package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
- package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
- package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
- package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
- package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
- package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
- package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
- package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
- package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
- package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
- package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
- package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
- package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
- package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
- package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
- package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
- package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
- package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
- package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
- package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
- package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
- package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
- package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
- package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
- package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
- package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
- package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
- package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
- package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
- package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
- package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
- package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
- package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
- package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
- package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
- package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
- package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
- package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
- package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
- package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
- package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
- package/renderer/dist/corner-tag.html +3 -3
- package/renderer/dist/index.html +3 -3
- package/scripts/after-install.mjs +30 -8
- package/scripts/cloud-shim.pl +57 -0
- package/shared/account-names.d.mts +6 -0
- package/shared/account-names.mjs +15 -0
- package/shared/agent-hours.mjs +136 -0
- package/shared/agent-import.mjs +60 -34
- package/shared/agents.mjs +126 -522
- package/shared/analytics-events.mjs +12 -0
- package/shared/approval-card.mjs +36 -0
- package/shared/bridge-map.mjs +27 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-usage.d.mts +1 -0
- package/shared/claude-usage.mjs +42 -1
- package/shared/cloud-hand-back.d.mts +7 -0
- package/shared/cloud-hand-back.mjs +20 -0
- package/shared/cloud-kit.d.mts +219 -0
- package/shared/cloud-kit.mjs +341 -0
- package/shared/cloud-login-name.mjs +4 -0
- package/shared/cloud-start.mjs +17 -0
- package/shared/cloud-versions.mjs +23 -0
- package/shared/codex-import.mjs +2 -2
- package/shared/codex-modes.d.mts +22 -9
- package/shared/codex-modes.mjs +67 -11
- package/shared/codex-usage.mjs +108 -2
- package/shared/corner-tag.d.mts +23 -1
- package/shared/corner-tag.mjs +284 -29
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +70 -309
- package/shared/feedback.d.mts +7 -0
- package/shared/feedback.mjs +25 -0
- package/shared/first-run-practice.mjs +17 -24
- package/shared/harness-definitions.mjs +8 -0
- package/shared/instruction-defaults.json +2 -2
- package/shared/key-caps.d.mts +14 -0
- package/shared/key-caps.mjs +68 -0
- package/shared/message-edits.d.mts +7 -0
- package/shared/message-edits.mjs +35 -0
- package/shared/move-thread.d.mts +24 -0
- package/shared/move-thread.mjs +112 -0
- package/shared/named-threads.mjs +72 -0
- package/shared/not-shipped.d.mts +22 -0
- package/shared/not-shipped.mjs +89 -0
- package/shared/notify-rules.d.mts +6 -0
- package/shared/notify-rules.mjs +36 -8
- package/shared/phone-files.mjs +28 -0
- package/shared/phone-settings.mjs +118 -0
- package/shared/phone-snapshot.d.mts +16 -0
- package/shared/phone-snapshot.mjs +348 -0
- package/shared/picture-formats.d.mts +1 -0
- package/shared/picture-formats.mjs +13 -0
- package/shared/rank.d.mts +2 -1
- package/shared/rank.mjs +32 -19
- package/shared/repeats.d.mts +8 -0
- package/shared/repeats.mjs +33 -2
- package/shared/room-cards.mjs +167 -0
- package/shared/room-links.d.mts +4 -0
- package/shared/room-links.mjs +21 -0
- package/shared/runs-on.d.mts +3 -3
- package/shared/runs-on.mjs +8 -5
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +14 -6
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +443 -40
- package/shared/team-rules.mjs +9 -2
- package/shared/teammates.d.mts +19 -0
- package/shared/teammates.mjs +756 -0
- package/shared/terminal-focus.d.mts +1 -0
- package/shared/terminal-focus.mjs +13 -0
- package/shared/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +9 -0
- package/shared/usage.mjs +49 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/work-items.mjs +127 -30
- package/shared/work-lines.mjs +92 -244
- package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
- package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
- package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// A PICTURE'S WIDTH AND HEIGHT, OFF THE FIRST BYTES OF ITS FILE.
|
|
2
|
+
//
|
|
3
|
+
// The phone draws a picture it has never seen as a 4:3 box until the last byte
|
|
4
|
+
// arrives, because WebKit gives a picture no size before it is whole, and then
|
|
5
|
+
// the words under it jump by the difference: 514px for a phone screenshot at
|
|
6
|
+
// 390px wide (tests/a-picture-the-phone-has-never-seen-gets-its-shape-before-it-arrives).
|
|
7
|
+
// The phone asks for this first (main/serve.mjs, /f/<key>/shape/) and draws
|
|
8
|
+
// the box at the picture's own shape.
|
|
9
|
+
//
|
|
10
|
+
// PNG, GIF, JPEG and WebP, which is what agents and phones make. Anything else,
|
|
11
|
+
// or a file cut short, is null: the box stays 4:3, which is no worse than it
|
|
12
|
+
// was, rather than a guessed shape that is wrong.
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
|
|
15
|
+
/** How much of a file is read. A JPEG's frame sits after its Exif and colour profile. */
|
|
16
|
+
const HEAD_BYTES = 256 * 1024;
|
|
17
|
+
|
|
18
|
+
const shape = (w, h) => (w > 0 && h > 0 ? { w, h } : null);
|
|
19
|
+
|
|
20
|
+
function png(b) {
|
|
21
|
+
if (b.length < 24 || b.readUInt32BE(0) !== 0x89504e47 || b.toString('latin1', 12, 16) !== 'IHDR') return null;
|
|
22
|
+
return shape(b.readUInt32BE(16), b.readUInt32BE(20));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function gif(b) {
|
|
26
|
+
if (b.length < 10 || b.toString('latin1', 0, 4) !== 'GIF8') return null;
|
|
27
|
+
return shape(b.readUInt16LE(6), b.readUInt16LE(8));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function webp(b) {
|
|
31
|
+
if (b.length < 30 || b.toString('latin1', 0, 4) !== 'RIFF' || b.toString('latin1', 8, 12) !== 'WEBP') return null;
|
|
32
|
+
const chunk = b.toString('latin1', 12, 16);
|
|
33
|
+
if (chunk === 'VP8X') return shape(1 + b.readUIntLE(24, 3), 1 + b.readUIntLE(27, 3));
|
|
34
|
+
if (chunk === 'VP8 ') return shape(b.readUInt16LE(26) & 0x3fff, b.readUInt16LE(28) & 0x3fff);
|
|
35
|
+
if (chunk === 'VP8L') {
|
|
36
|
+
const bits = b.readUInt32LE(21);
|
|
37
|
+
return shape(1 + (bits & 0x3fff), 1 + ((bits >>> 14) & 0x3fff));
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The Exif orientation in an APP1 block, or 0. 5 to 8 mean it is drawn turned a quarter. */
|
|
43
|
+
function orientation(b, start, end) {
|
|
44
|
+
if (b.toString('latin1', start, start + 6) !== 'Exif\0\0') return 0;
|
|
45
|
+
const tiff = start + 6;
|
|
46
|
+
if (tiff + 8 > end) return 0;
|
|
47
|
+
const little = b.toString('latin1', tiff, tiff + 2) === 'II';
|
|
48
|
+
const u16 = (at) => (little ? b.readUInt16LE(at) : b.readUInt16BE(at));
|
|
49
|
+
const u32 = (at) => (little ? b.readUInt32LE(at) : b.readUInt32BE(at));
|
|
50
|
+
const ifd = tiff + u32(tiff + 4);
|
|
51
|
+
if (ifd + 2 > end) return 0;
|
|
52
|
+
const count = u16(ifd);
|
|
53
|
+
for (let i = 0; i < count; i++) {
|
|
54
|
+
const entry = ifd + 2 + i * 12;
|
|
55
|
+
if (entry + 12 > end) return 0;
|
|
56
|
+
if (u16(entry) === 0x0112) return u16(entry + 8);
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function jpeg(b) {
|
|
62
|
+
if (b.length < 4 || b[0] !== 0xff || b[1] !== 0xd8) return null;
|
|
63
|
+
let turned = 0;
|
|
64
|
+
let i = 2;
|
|
65
|
+
while (i + 4 <= b.length) {
|
|
66
|
+
if (b[i] !== 0xff) return null;
|
|
67
|
+
const marker = b[i + 1];
|
|
68
|
+
// Fill bytes before a marker.
|
|
69
|
+
if (marker === 0xff) { i += 1; continue; }
|
|
70
|
+
const length = b.readUInt16BE(i + 2);
|
|
71
|
+
// Every start-of-frame but the three that are not frames (DHT, JPG, DAC).
|
|
72
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
73
|
+
if (i + 9 > b.length) return null;
|
|
74
|
+
const h = b.readUInt16BE(i + 5);
|
|
75
|
+
const w = b.readUInt16BE(i + 7);
|
|
76
|
+
return turned >= 5 && turned <= 8 ? shape(h, w) : shape(w, h);
|
|
77
|
+
}
|
|
78
|
+
if (marker === 0xe1 && !turned) turned = orientation(b, i + 4, Math.min(b.length, i + 2 + length));
|
|
79
|
+
i += 2 + length;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `{ w, h }` as the browser will draw it, or null. */
|
|
85
|
+
export function pictureShape(bytes) {
|
|
86
|
+
if (!bytes?.length) return null;
|
|
87
|
+
try {
|
|
88
|
+
return png(bytes) ?? gif(bytes) ?? webp(bytes) ?? jpeg(bytes);
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The same, read off a file on disk without waiting, for a pass that lists many. */
|
|
95
|
+
export function readPictureShapeSync(file) {
|
|
96
|
+
const fd = fs.openSync(file, 'r');
|
|
97
|
+
try {
|
|
98
|
+
const buffer = Buffer.alloc(HEAD_BYTES);
|
|
99
|
+
return pictureShape(buffer.subarray(0, fs.readSync(fd, buffer, 0, HEAD_BYTES, 0)));
|
|
100
|
+
} finally {
|
|
101
|
+
fs.closeSync(fd);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The same, read off a file on disk. */
|
|
106
|
+
export async function readPictureShape(file) {
|
|
107
|
+
const handle = await fs.promises.open(file, 'r');
|
|
108
|
+
try {
|
|
109
|
+
const { buffer, bytesRead } = await handle.read({ buffer: Buffer.alloc(HEAD_BYTES), position: 0 });
|
|
110
|
+
return pictureShape(buffer.subarray(0, bytesRead));
|
|
111
|
+
} finally {
|
|
112
|
+
await handle.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// THE WAY OUT, AND NOTHING ON IT MAY KILL THE APP (w-60c8ddbab6, 2026-10-15).
|
|
2
|
+
//
|
|
3
|
+
// Quitting used to be six bare `app.on('before-quit', ...)` listeners and one
|
|
4
|
+
// `will-quit` that started the analytics flush and did not wait for it. Both
|
|
5
|
+
// halves of that cost us the end of every session.
|
|
6
|
+
//
|
|
7
|
+
// ONE STEP THAT THROWS ENDED THE WHOLE QUIT. On 0.1.13 the phone step did:
|
|
8
|
+
// closing the main window nulls `window`, and `phone.close()` makes the relay
|
|
9
|
+
// report the change back to a handler that asked the window whether it was
|
|
10
|
+
// destroyed (main/main.mjs, the `zero:phone-changed` line). An EventEmitter
|
|
11
|
+
// stops calling listeners at the first one that throws, `main/crash-report.mjs`
|
|
12
|
+
// rethrows an uncaught main error on purpose, and the process then dies
|
|
13
|
+
// fatally. Measured on an Electron harness: `updater.stop()` never ran,
|
|
14
|
+
// `will-quit` never fired, `analytics.flush()` never ran, and `quit` never
|
|
15
|
+
// came. The window's own guard is fixed where it lives, but the shape was the
|
|
16
|
+
// real fault: a quit built so that any one step can swallow the rest is a quit
|
|
17
|
+
// that will do this again for some other reason.
|
|
18
|
+
//
|
|
19
|
+
// AND THE FLUSH WAS FIRE AND FORGET. `will-quit` without `preventDefault` means
|
|
20
|
+
// the process goes as soon as the listeners return, so a batch handed to
|
|
21
|
+
// `posthog-node` a moment earlier is a POST that never leaves. Measured the
|
|
22
|
+
// same way: the flush resolved AFTER `process.on('exit')` had already run, and
|
|
23
|
+
// it only resolved at all because the harness's destination was in the same
|
|
24
|
+
// process. Against us.i.posthog.com it is a race the app loses.
|
|
25
|
+
//
|
|
26
|
+
// WHICH MATTERS BECAUSE THE END OF A SESSION IS WHAT WE READ RETENTION FROM.
|
|
27
|
+
// Of the fourteen installs that arrived on 0.1.13 or 0.1.14, thirteen opened
|
|
28
|
+
// the app once and never opened it again (PostHog, 2026-10-15), so for thirteen
|
|
29
|
+
// of fourteen people the batch dropped here is the LAST thing we would ever
|
|
30
|
+
// have heard, and the crash report explaining it is still sitting on their disk
|
|
31
|
+
// waiting for a launch that is not coming.
|
|
32
|
+
|
|
33
|
+
/** How long the quit is held open for the last batch of counts, and no longer. */
|
|
34
|
+
export const FLUSH_DEADLINE_MS = 2000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* How long a quit we asked for is given before the process is ended outright.
|
|
38
|
+
* Longer than the flush above on purpose: a quit holding itself open for the
|
|
39
|
+
* last counts is a quit that is working. */
|
|
40
|
+
export const EXIT_DEADLINE_MS = 8000;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How long the second ask gets, once the counts are out and there is nothing
|
|
44
|
+
* left to wait for. On Linux a quit asked for with a window still open — the
|
|
45
|
+
* menu's Quit, the tray's Quit — ignores it outright
|
|
46
|
+
* (tests/a-quit-held-for-the-last-counts-still-ends-the-app), and by then the
|
|
47
|
+
* window is gone and there is nothing left to show anybody. */
|
|
48
|
+
export const SECOND_QUIT_DEADLINE_MS = 2000;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A STEP ON THE WAY OUT, which runs once and cannot throw.
|
|
52
|
+
*
|
|
53
|
+
* `name` is in the warning rather than in a comment, because the one thing
|
|
54
|
+
* somebody reading a log needs to know is which step gave up. A swallow with
|
|
55
|
+
* nothing said is how this bug lived through three releases.
|
|
56
|
+
*
|
|
57
|
+
* ONCE, not once per `app.quit()`. Holding the quit for the flush below means
|
|
58
|
+
* calling `app.quit()` a second time, which emits `before-quit` again, and a
|
|
59
|
+
* `killAll` or a `close` run twice is at best wasted and at worst a second
|
|
60
|
+
* teardown of something already gone.
|
|
61
|
+
*/
|
|
62
|
+
export function quitStep(app, event, name, fn, { log = console } = {}) {
|
|
63
|
+
let ran = false;
|
|
64
|
+
app.on(event, () => {
|
|
65
|
+
if (ran) return;
|
|
66
|
+
ran = true;
|
|
67
|
+
try {
|
|
68
|
+
fn();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// Never rethrown: the app is on its way out and every step after this one
|
|
71
|
+
// still has to happen.
|
|
72
|
+
log?.warn?.(`zero: ${name} failed while quitting: ${err?.message ?? err}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* HOLD THE QUIT OPEN FOR THE LAST BATCH OF COUNTS.
|
|
79
|
+
*
|
|
80
|
+
* Three things it will not do, each of them a way a quit becomes a hang:
|
|
81
|
+
*
|
|
82
|
+
* - It will not wait past `FLUSH_DEADLINE_MS`. A machine with no network, or
|
|
83
|
+
* a destination that accepts the connection and never answers, must cost
|
|
84
|
+
* somebody two seconds on the way out and not their afternoon.
|
|
85
|
+
* - It will not care whether the flush worked. There is nothing to be done
|
|
86
|
+
* about a flush that failed, and a count is never worth a quit.
|
|
87
|
+
* - It will not hold twice. `will-quit` comes round again when we ask again,
|
|
88
|
+
* and holding that one too is a quit that never happens.
|
|
89
|
+
* - It will not trust the second ask either. On Linux, a quit that began with
|
|
90
|
+
* a window open ignores it and the process stays for ever, which is what
|
|
91
|
+
* `quitThenExit` is for
|
|
92
|
+
* (tests/a-quit-held-for-the-last-counts-still-ends-the-app).
|
|
93
|
+
*/
|
|
94
|
+
export function flushBeforeQuit(app, flush, { deadlineMs = FLUSH_DEADLINE_MS, log = console } = {}) {
|
|
95
|
+
let held = false;
|
|
96
|
+
app.on('will-quit', (event) => {
|
|
97
|
+
if (held) return; // the second time round is the one we asked for
|
|
98
|
+
held = true;
|
|
99
|
+
event?.preventDefault?.();
|
|
100
|
+
let gone = false;
|
|
101
|
+
const go = () => {
|
|
102
|
+
if (gone) return;
|
|
103
|
+
gone = true;
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
quitThenExit(app, { what: 'the last counts', deadlineMs: SECOND_QUIT_DEADLINE_MS, log });
|
|
106
|
+
};
|
|
107
|
+
const timer = setTimeout(go, deadlineMs);
|
|
108
|
+
// The deadline is armed before the flush is even called, so a `flush` that
|
|
109
|
+
// throws synchronously is covered by the same promise as one that rejects.
|
|
110
|
+
Promise.resolve()
|
|
111
|
+
.then(() => flush())
|
|
112
|
+
.catch((err) => { log?.warn?.(`zero: the last counts did not get out: ${err?.message ?? err}`); })
|
|
113
|
+
.then(go, go);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* ASK TO QUIT, AND GO ANYWAY IF THE QUIT DOES NOT COME.
|
|
119
|
+
*
|
|
120
|
+
* `app.quit()` only asks, and on Linux an app that has already refused one
|
|
121
|
+
* `will-quit` ignores every ask after it: the measurement is in
|
|
122
|
+
* tests/a-quit-held-for-the-last-counts-still-ends-the-app. It is still asked,
|
|
123
|
+
* because that is what runs the quit steps, lets the workers go and fires
|
|
124
|
+
* `quit` for whoever is listening, and the deadline ends the process when the
|
|
125
|
+
* quit has not.
|
|
126
|
+
*
|
|
127
|
+
* Both callers pass a deadline that suits what they are waiting for: a
|
|
128
|
+
* screenshot run has a whole quit ahead of it, the flush above has nothing
|
|
129
|
+
* left but the `quit` event. The timer is unreffed: an app with nothing left
|
|
130
|
+
* to do must not be kept alive by the thing watching it. */
|
|
131
|
+
export function quitThenExit(app, { what = 'this run', deadlineMs = EXIT_DEADLINE_MS, log = console } = {}) {
|
|
132
|
+
let over = false;
|
|
133
|
+
const timer = setTimeout(() => {
|
|
134
|
+
if (over) return;
|
|
135
|
+
over = true;
|
|
136
|
+
log?.warn?.(`zero: ${what} asked the app to quit ${deadlineMs} ms ago and it is still here, so it is ending now`);
|
|
137
|
+
app.exit(0);
|
|
138
|
+
}, deadlineMs);
|
|
139
|
+
timer.unref?.();
|
|
140
|
+
app.once?.('quit', () => { over = true; clearTimeout(timer); });
|
|
141
|
+
app.quit();
|
|
142
|
+
return timer;
|
|
143
|
+
}
|
|
@@ -4,11 +4,24 @@ import {rememberCodexModels} from './codex-models.mjs';
|
|
|
4
4
|
import {forget,claudeModels} from './claude-models.mjs';
|
|
5
5
|
import {terminalEnv} from './task-terminals.mjs';
|
|
6
6
|
import {codexLaunchEnv} from './codex-launch-env.mjs';
|
|
7
|
-
|
|
7
|
+
import {ensureCodexHome} from './codex-account.mjs';
|
|
8
|
+
// `homes` is every Codex login to ask, one after another. A login that cannot
|
|
9
|
+
// answer keeps its last list and does not cost the others theirs; only all of
|
|
10
|
+
// them failing is an error.
|
|
11
|
+
export async function refreshAgentModels(engine,config,homes,{open=home=>openAppServer(config,home)}={}){
|
|
8
12
|
if(engine==='claude'){forget();claudeModels({bin:config.claudeBin});return;}
|
|
9
13
|
// Codex is optional and codexBin is null without it: nothing to ask, and spawn(null) only threw into the terminal.
|
|
10
14
|
if(!config.codexBin)return;
|
|
11
|
-
const
|
|
15
|
+
const list=[...new Set([homes].flat().filter(Boolean))];const errors=[];
|
|
16
|
+
for(const home of list){try{await refreshOne(open(home),home);}catch(error){errors.push(error);}}
|
|
17
|
+
if(list.length&&errors.length===list.length)throw errors[0];
|
|
18
|
+
}
|
|
19
|
+
function openAppServer(config,home){
|
|
20
|
+
// codex-cli refuses a CODEX_HOME that does not exist rather than making it.
|
|
21
|
+
ensureCodexHome(home);
|
|
22
|
+
return createCodexAppServer({spawn:()=>spawn(config.codexBin,['app-server'],{cwd:config.home,env:codexLaunchEnv(config.codexBin,{...terminalEnv(),CODEX_HOME:home}),stdio:['pipe','pipe','pipe']}),requestTimeoutMs:10000});
|
|
23
|
+
}
|
|
24
|
+
async function refreshOne(client,home){
|
|
12
25
|
const deadline=setTimeout(()=>client.close('Model refresh timed out'),15000);
|
|
13
26
|
try{
|
|
14
27
|
await client.initialize();let cursor=null;const models=[];const seen=new Set();
|
package/main/relay-client.mjs
CHANGED
|
@@ -26,6 +26,19 @@ const PASS = ['accept', 'content-type', 'range', 'user-agent', 'if-none-match',
|
|
|
26
26
|
* 30 s the room gives a Mac to start answering (relay/room.mjs).
|
|
27
27
|
*/
|
|
28
28
|
const WAIT_MS = 10_000;
|
|
29
|
+
/**
|
|
30
|
+
* THE LINK IS CHECKED, NOT TRUSTED (tests/the-computer-notices-a-dead-relay-link-and-reconnects).
|
|
31
|
+
* A socket that carries nothing cannot tell this end it has died: a laptop that
|
|
32
|
+
* slept or changed networks kept one it believed open while the relay had let
|
|
33
|
+
* it go, and phones were told the computer was not connected. So this end says
|
|
34
|
+
* "ping" this often and the relay answers "pong" without waking the room.
|
|
35
|
+
*/
|
|
36
|
+
export const PING_MS = 25_000;
|
|
37
|
+
/** How long past a ping's turn a missing pong is allowed before the link is called dead. */
|
|
38
|
+
export const PONG_GRACE_MS = 10_000;
|
|
39
|
+
/** What the platform sends as text on the socket. Never a request: those are bytes. */
|
|
40
|
+
const PING = 'ping';
|
|
41
|
+
const PONG = 'pong';
|
|
29
42
|
/** What the phone does not want back: the relay's stream is its own framing. */
|
|
30
43
|
const DROP = new Set(['connection', 'keep-alive', 'transfer-encoding', 'content-length', 'content-encoding']);
|
|
31
44
|
|
|
@@ -34,9 +47,14 @@ const hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, '0'))
|
|
|
34
47
|
|
|
35
48
|
export function createRelayClient({
|
|
36
49
|
relayUrl, key, port, WebSocketImpl = globalThis.WebSocket, fetchImpl = (...a) => globalThis.fetch(...a),
|
|
37
|
-
log = console, onChange = () => {}, waitMs = WAIT_MS,
|
|
50
|
+
log = console, onChange = () => {}, waitMs = WAIT_MS, pingMs = PING_MS, graceMs = PONG_GRACE_MS,
|
|
38
51
|
}) {
|
|
39
52
|
let ws = null;
|
|
53
|
+
let pinger = null;
|
|
54
|
+
/** When the relay last answered a ping on this link; null until it ever has. */
|
|
55
|
+
let ponged = null;
|
|
56
|
+
/** When the ping timer last ran, to tell a sleep from a slow tick. */
|
|
57
|
+
let ticked = 0;
|
|
40
58
|
let want = false;
|
|
41
59
|
let connected = false;
|
|
42
60
|
let error = null;
|
|
@@ -147,6 +165,10 @@ export function createRelayClient({
|
|
|
147
165
|
}
|
|
148
166
|
|
|
149
167
|
async function onMessage(sock, data) {
|
|
168
|
+
if (typeof data === 'string') {
|
|
169
|
+
if (data === PONG) ponged = Date.now();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
150
172
|
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data)
|
|
151
173
|
: data instanceof Uint8Array ? data
|
|
152
174
|
: new Uint8Array(await data.arrayBuffer());
|
|
@@ -178,6 +200,49 @@ export function createRelayClient({
|
|
|
178
200
|
}
|
|
179
201
|
}
|
|
180
202
|
|
|
203
|
+
// Two ways a link is known dead without waiting on the operating system: a
|
|
204
|
+
// relay that has answered pings stops answering, or this timer ran far too
|
|
205
|
+
// late, which is a laptop waking from sleep with every socket gone under it.
|
|
206
|
+
// A relay that never answers (one deployed before pings) is only ever judged
|
|
207
|
+
// by the second, so an old relay is not mistaken for a dead link.
|
|
208
|
+
function startPinging(sock) {
|
|
209
|
+
stopPinging();
|
|
210
|
+
ponged = null;
|
|
211
|
+
ticked = Date.now();
|
|
212
|
+
pinger = setInterval(() => {
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const slept = now - ticked > pingMs * 2;
|
|
215
|
+
ticked = now;
|
|
216
|
+
const silent = ponged !== null && now - ponged > pingMs + graceMs;
|
|
217
|
+
if (slept || silent) {
|
|
218
|
+
log.info?.(`[relay] the link went quiet (${slept ? 'woke from sleep' : 'no answer to ping'}); reconnecting`);
|
|
219
|
+
replace(sock);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
send(sock, PING);
|
|
223
|
+
}, pingMs);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function stopPinging() {
|
|
227
|
+
if (pinger) clearInterval(pinger);
|
|
228
|
+
pinger = null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Let a dead link go and open a new one now, not after the backoff. */
|
|
232
|
+
function replace(sock) {
|
|
233
|
+
if (ws !== sock) return;
|
|
234
|
+
stopPinging();
|
|
235
|
+
ws = null;
|
|
236
|
+
connected = false;
|
|
237
|
+
for (const c of live.values()) c.abort();
|
|
238
|
+
live.clear();
|
|
239
|
+
try { sock.close(4002, 'No answer; reconnecting.'); } catch { /* already gone */ }
|
|
240
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
241
|
+
retry = 1000;
|
|
242
|
+
onChange();
|
|
243
|
+
void connect();
|
|
244
|
+
}
|
|
245
|
+
|
|
181
246
|
function schedule() {
|
|
182
247
|
if (!want || timer) return;
|
|
183
248
|
timer = setTimeout(() => { timer = null; void connect(); }, retry);
|
|
@@ -198,12 +263,14 @@ export function createRelayClient({
|
|
|
198
263
|
connected = true;
|
|
199
264
|
error = null;
|
|
200
265
|
retry = 1000;
|
|
266
|
+
startPinging(sock);
|
|
201
267
|
onChange();
|
|
202
268
|
};
|
|
203
269
|
sock.onmessage = (event) => { if (ws === sock) void onMessage(sock, event.data); };
|
|
204
270
|
sock.onerror = () => { if (ws === sock && !connected) error = 'Could not reach the relay.'; };
|
|
205
271
|
sock.onclose = (event) => {
|
|
206
272
|
if (ws !== sock) return;
|
|
273
|
+
stopPinging();
|
|
207
274
|
ws = null;
|
|
208
275
|
const was = connected;
|
|
209
276
|
connected = false;
|
|
@@ -226,6 +293,7 @@ export function createRelayClient({
|
|
|
226
293
|
stop() {
|
|
227
294
|
want = false;
|
|
228
295
|
gen += 1;
|
|
296
|
+
stopPinging();
|
|
229
297
|
if (timer) { clearTimeout(timer); timer = null; }
|
|
230
298
|
const sock = ws;
|
|
231
299
|
ws = null;
|
package/main/repeats.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import fs from 'node:fs';
|
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import {
|
|
18
18
|
DAY_NAMES, foldRepeats, isOwed, isRepeatRule, occurrenceId, periodKey, repeatLabel,
|
|
19
|
+
standingInstruction, PROTOCOL_OPENS, INSTRUCTION_MARK,
|
|
19
20
|
} from '../shared/repeats.mjs';
|
|
20
21
|
import { machineryPath } from './store/home.mjs';
|
|
21
22
|
import { NAME } from '../shared/product-name.mjs';
|
|
@@ -41,8 +42,8 @@ const MISSES_BEFORE_ALERT = 3;
|
|
|
41
42
|
// last can be truncated away entirely, and a worker that never saw it cannot
|
|
42
43
|
// know that a quiet finish has to be marked.
|
|
43
44
|
export const OCCURRENCE_PROTOCOL = [
|
|
44
|
-
|
|
45
|
-
'here, so this run does not have to carry the future.',
|
|
45
|
+
`${PROTOCOL_OPENS} It runs again at its next time whatever`,
|
|
46
|
+
'happens here, so this run does not have to carry the future.',
|
|
46
47
|
'',
|
|
47
48
|
'FINISH ONE OF TWO WAYS.',
|
|
48
49
|
'',
|
|
@@ -60,7 +61,12 @@ export const OCCURRENCE_PROTOCOL = [
|
|
|
60
61
|
`only place in ${NAME} where finishing quietly hides something, so the marker is`,
|
|
61
62
|
'required to hide, never to show.',
|
|
62
63
|
'',
|
|
63
|
-
'
|
|
64
|
+
'IF THEY TELL YOU HOW THIS TASK SHOULD GO FROM NOW ON, CHANGE IT YOURSELF with',
|
|
65
|
+
'the store tool set_repeating_task. Editing this run or writing a memory note',
|
|
66
|
+
'reaches no later run: this body is a copy, and every run gets a new task',
|
|
67
|
+
'folder. Never ask them to go and edit the task by hand.',
|
|
68
|
+
'',
|
|
69
|
+
INSTRUCTION_MARK,
|
|
64
70
|
].join('\n');
|
|
65
71
|
|
|
66
72
|
export class Repeats {
|
|
@@ -212,7 +218,7 @@ export class Repeats {
|
|
|
212
218
|
|
|
213
219
|
const { created } = workItemsDisk.createWorkItemIfAbsent(productDir, id, {
|
|
214
220
|
title: `${rule.title} · ${dayStamp(key)}`,
|
|
215
|
-
body: `${OCCURRENCE_PROTOCOL.replace('<REPEAT_LABEL>', repeatLabel(ruleId))}\n\n${rule
|
|
221
|
+
body: `${OCCURRENCE_PROTOCOL.replace('<REPEAT_LABEL>', repeatLabel(ruleId))}\n\n${standingInstruction(rule)}`,
|
|
216
222
|
kind: 'directive',
|
|
217
223
|
priority: rule.priority ?? 5,
|
|
218
224
|
labels: ['founder', repeatLabel(ruleId)],
|
package/main/row-label.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { harnessFor } from './harnesses.mjs';
|
|
2
|
+
import { teammateIdOf } from '../shared/teammates.mjs';
|
|
2
3
|
// THE NAME ON THE ROW, WRITTEN RATHER THAN TAKEN FROM THE FIRST SENTENCE.
|
|
3
4
|
//
|
|
4
5
|
// A dictated message keeps its first line as its title, verbatim and forever
|
|
@@ -62,51 +63,18 @@ export const NAME_MAX = 64;
|
|
|
62
63
|
* archive; everything else is a row that can still appear in a list. */
|
|
63
64
|
const LIVE = new Set(['open', 'claimed', 'blocked']);
|
|
64
65
|
|
|
65
|
-
/**
|
|
66
|
-
* PURE. Is this title already a name somebody wrote?
|
|
67
|
-
*
|
|
68
|
-
* The one question that decides whether this module speaks at all, and it is
|
|
69
|
-
* answered off the words alone because that is all there is: nothing on a row
|
|
70
|
-
* records whether its title was typed or derived.
|
|
71
|
-
*
|
|
72
|
-
* Three marks, and together they separate the two shapes `splitMessage` makes
|
|
73
|
-
* (renderer/src/message-split.ts). A first line of 120 characters or less is
|
|
74
|
-
* kept VERBATIM and is the name its person wrote. A longer one is clipped into
|
|
75
|
-
* a title by the app, and every clip it can produce is either a whole sentence
|
|
76
|
-
* of at least 25 characters or a hard cut ending in an ellipsis.
|
|
77
|
-
*
|
|
78
|
-
* as short as a name we would write NAME_MAX, so a title already that tight
|
|
79
|
-
* has nothing for us to improve,
|
|
80
|
-
* as few words as a name has the same eight `cleanName` allows,
|
|
81
|
-
* not ending as a sentence does '.', '!', '?' and '…' are how a clipped
|
|
82
|
-
* title ends and not how a name does.
|
|
83
|
-
*
|
|
84
|
-
* It errs towards leaving the words alone, which is the right way to be wrong:
|
|
85
|
-
* the cost is a short typed sentence keeping its own first line, and the cost
|
|
86
|
-
* the other way is somebody losing the thread they named.
|
|
87
|
-
*/
|
|
88
|
-
export function isWrittenName(title) {
|
|
89
|
-
const t = String(title ?? '').trim();
|
|
90
|
-
if (!t) return false;
|
|
91
|
-
if (t.length > NAME_MAX) return false;
|
|
92
|
-
if (t.split(/\s+/).length > 8) return false;
|
|
93
|
-
return !/[.!?…]$/.test(t);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
66
|
/**
|
|
97
67
|
* PURE. Does this row want naming?
|
|
98
68
|
*
|
|
99
|
-
*
|
|
100
|
-
* own threads on 2026-10-01: "Add comment to cart.js" became "Improve cart.js
|
|
101
|
-
* header comment" and then "Update cart file comment" inside two minutes.
|
|
69
|
+
* Three ways to say no:
|
|
102
70
|
*
|
|
103
71
|
* - it is done, so nobody is scanning for it,
|
|
104
72
|
* - it has nothing to name from, which is a row with no words on it yet,
|
|
105
|
-
* - ITS PERSON ALREADY NAMED IT. A short clear title is the name they will
|
|
106
|
-
* look for, and on a team it is the name they will point someone else at.
|
|
107
|
-
* Improving it is not an improvement.
|
|
108
73
|
* - IT HAS BEEN NAMED. Once, when the thread was made, and then never again.
|
|
109
74
|
*
|
|
75
|
+
* A short typed title is named too: a list where some rows are typed lines and
|
|
76
|
+
* the rest are names reads as two styles. The prompt keeps such a line's words.
|
|
77
|
+
*
|
|
110
78
|
* That last line used to read `wroteAt < contentTouchedAt(item)`, so the name
|
|
111
79
|
* followed where the thread stood: an agent writes a note and a result on every
|
|
112
80
|
* run, so every run earned a new name. Following the thread was the intended
|
|
@@ -122,8 +90,8 @@ export function isWrittenName(title) {
|
|
|
122
90
|
*/
|
|
123
91
|
export function wantsName(item) {
|
|
124
92
|
if (!item || !LIVE.has(item.status)) return false;
|
|
93
|
+
if (teammateIdOf(item)) return false;
|
|
125
94
|
if (!String(item.title ?? '').trim() && !String(item.body ?? '').trim()) return false;
|
|
126
|
-
if (isWrittenName(item.title)) return false;
|
|
127
95
|
return !String(item.label ?? '').trim();
|
|
128
96
|
}
|
|
129
97
|
|
|
@@ -139,12 +107,15 @@ export function namePrompt(item) {
|
|
|
139
107
|
'Here is a work thread: the message that opened it, and the latest thing',
|
|
140
108
|
'written on it. Write a short name for the thread AS IT STANDS NOW: 3 to 7',
|
|
141
109
|
'words, sentence case, no quotes, no trailing punctuation, naming the thing',
|
|
142
|
-
'being worked on rather than restating the sentence.
|
|
143
|
-
'
|
|
110
|
+
'being worked on rather than restating the sentence. If the opening line is',
|
|
111
|
+
'already a short name, keep its words and only fix the case. Reply with the',
|
|
112
|
+
'name and nothing else.',
|
|
144
113
|
'',
|
|
145
114
|
'--- opened with ---',
|
|
146
115
|
cut(item?.title, 300),
|
|
147
|
-
|
|
116
|
+
// A teammate's run is titled with its own part of a message that may have
|
|
117
|
+
// woken several (shared/teammates.mjs `runTitle`); the whole would mix them.
|
|
118
|
+
(item?.labels ?? []).some((l) => String(l).startsWith('teammate:')) ? '' : cut(item?.body, 1200),
|
|
148
119
|
'--- latest ---',
|
|
149
120
|
latest || '(nothing written yet)',
|
|
150
121
|
'---',
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// WHAT A RUN CHANGED: the checkout read back against its photograph, then the
|
|
2
|
+
// change file written. Git commands and a whole transcript, so it runs on the
|
|
3
|
+
// git thread (main/git-change-offthread.mjs), not on the app's main thread.
|
|
4
|
+
import { changeFromRepo } from './git-change.mjs';
|
|
5
|
+
import { writeChangeForRun } from './code-change.mjs';
|
|
6
|
+
|
|
7
|
+
export function saveRunChange({ repoBefore = null, ...run }) {
|
|
8
|
+
let repos = [];
|
|
9
|
+
try {
|
|
10
|
+
if (repoBefore) repos = [changeFromRepo(repoBefore)];
|
|
11
|
+
} catch (e) { console.warn('zero: could not read the repository back:', e.message); }
|
|
12
|
+
return writeChangeForRun({ ...run, repos });
|
|
13
|
+
}
|