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
package/shared/notify-rules.mjs
CHANGED
|
@@ -31,6 +31,15 @@ export function atTheApp({ focused = false, idleMs = 0 } = {}) {
|
|
|
31
31
|
return !!focused && idleMs < AWAY_AFTER_MS;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// THE PHONE'S RULE IS WIDER: she is at the computer, in any app, so the phone
|
|
35
|
+
// says nothing. Someone in a browser on the laptop is not at the app, and the
|
|
36
|
+
// desktop banner above is how she hears; buzzing the phone in her bag as well
|
|
37
|
+
// is the "tons and tons of notifications" she reported (2026-10-13). The
|
|
38
|
+
// phone is for when she has left the machine, which only the idle clock knows.
|
|
39
|
+
export function atTheComputer({ idleMs = 0 } = {}) {
|
|
40
|
+
return idleMs < AWAY_AFTER_MS;
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
// WHAT COUNTS AS NEWS, which is the question before "is she away".
|
|
35
44
|
//
|
|
36
45
|
// renderer/src/App.tsx owns the definition of the inbox and asks this of every
|
|
@@ -78,14 +87,15 @@ export function clip(text, max = 120) {
|
|
|
78
87
|
// main/notify.mjs resets this the moment she has the window, so the next time
|
|
79
88
|
// she walks away she can be told again.
|
|
80
89
|
//
|
|
81
|
-
// THE ONE EXCEPTION, and it is here because an approval
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// first banner of the stretch was a
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
90
|
+
// THE ONE EXCEPTION, and it is here because an approval is the one thing in the
|
|
91
|
+
// inbox that nothing else can move. A finished agent has said its piece and the
|
|
92
|
+
// thread is readable without her. An approval is a worker frozen mid-run with
|
|
93
|
+
// her as the only way through, and it waits exactly that long: no clock answers
|
|
94
|
+
// it (main/codex-approvals.mjs). If the first banner of the stretch was a
|
|
95
|
+
// finished agent, an approval landing twenty minutes later would die unheard
|
|
96
|
+
// under a strict one-and-done rule, and the run would still be standing there
|
|
97
|
+
// when she got back. So an approval gets to speak once more, and that is the
|
|
98
|
+
// cap: at most two in one stretch away, never more.
|
|
89
99
|
export function shouldSpeak({ spoke = false, spokeAsk = false, waiting = [] } = {}) {
|
|
90
100
|
const items = (waiting ?? []).filter(Boolean);
|
|
91
101
|
if (!items.length) return false;
|
|
@@ -137,3 +147,21 @@ export function banner(waiting) {
|
|
|
137
147
|
open: lead.kind === 'approval' ? null : (lead.id ?? null),
|
|
138
148
|
};
|
|
139
149
|
}
|
|
150
|
+
|
|
151
|
+
// THE PHONE'S ALERT IS HEADED BY THE THREAD (2026-10-13). On a phone the
|
|
152
|
+
// banner is read in a stack of others, and she asked for the bold line to be
|
|
153
|
+
// "the specific chat title so I know which thread we're talking about". iOS
|
|
154
|
+
// wraps the body and gives the title a line of its own, so the macOS reason
|
|
155
|
+
// above for keeping a name out of the title does not hold there. The sentence
|
|
156
|
+
// moves under it. An approval names the thread that is asking (`thread`) and
|
|
157
|
+
// what it wants to run; with no thread to name, it is the desktop's words.
|
|
158
|
+
export function phoneBanner(waiting) {
|
|
159
|
+
const say = banner(waiting);
|
|
160
|
+
if (!say) return null;
|
|
161
|
+
const items = waiting.filter(Boolean);
|
|
162
|
+
const asks = items.filter((i) => i.kind === 'approval');
|
|
163
|
+
const lead = asks.length ? asks[0] : items[items.length - 1];
|
|
164
|
+
const name = clip(asks.length ? lead.thread : lead.title, 80);
|
|
165
|
+
if (!name) return say;
|
|
166
|
+
return { title: name, body: asks.length ? `Waiting on you: ${say.body}` : say.title, open: say.open };
|
|
167
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// A FILE HER PHONE SENT WITH A REPLY OR A NEW THREAD: put in her storage, in
|
|
2
|
+
// the folder of the computer the thread is on, named by what is in it
|
|
3
|
+
// (renderer/src/phone-sends-files.ts), and linked in her words. The computer
|
|
4
|
+
// (main/phone-inbox.mjs) and the cloud server (cloud/gateway/phone.mjs) each
|
|
5
|
+
// put it in the project's attachments under the same name, made from what is
|
|
6
|
+
// in it, so a turn the cloud ran and the row the computer keeps link one file.
|
|
7
|
+
|
|
8
|
+
export const PHONE_FILE = /(!?)\[([^\]\n]*)\]\(phone-files\/([0-9a-f]{64}\.[a-z0-9]{1,8})\)/g;
|
|
9
|
+
export const PHONE_FILES_MAX = 10;
|
|
10
|
+
// The bucket's own bound on a file (20261017000000_phone_files.sql).
|
|
11
|
+
export const PHONE_FILE_MAX_BYTES = 25 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
export function phoneFilesIn(text) {
|
|
14
|
+
return [...String(text ?? '').matchAll(PHONE_FILE)].map((m) => ({ link: m[0], picture: m[1] === '!', label: m[2], name: m[3] }));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The name it goes under, as main/save-attachments.mjs makes one safe. */
|
|
18
|
+
export const phoneFileSafeName = (f) => String(String(f.label ?? '').trim() || f.name).replace(/[^\w.-]+/g, '-').slice(0, 80);
|
|
19
|
+
|
|
20
|
+
/** Where it goes in the project's documents folder: the same file, the same place. */
|
|
21
|
+
export const phoneAttachmentRel = (f) => `attachments/${String(f.name).slice(0, 16)}-${phoneFileSafeName(f)}`;
|
|
22
|
+
|
|
23
|
+
/** Her words with each file linked where it was put. */
|
|
24
|
+
export function linkPhoneFiles(text, found = phoneFilesIn(text)) {
|
|
25
|
+
let out = String(text ?? '');
|
|
26
|
+
for (const f of found) out = out.replace(f.link, () => `${f.picture ? '!' : ''}[${phoneFileSafeName(f)}](${phoneAttachmentRel(f)})`);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// YOUR COMPUTER'S SETTINGS ON YOUR PHONE: the copy the Mac sends, and the
|
|
2
|
+
// changes the phone may ask for. The Mac's own handlers (main/settings.mjs)
|
|
3
|
+
// still decide every value. This keeps paths and instruction text out of the
|
|
4
|
+
// cloud, and refuses any key the phone has no page for.
|
|
5
|
+
import { CLAUDE_PERMISSION_MODES } from './work-items.mjs';
|
|
6
|
+
import { CODEX_MODE_ORDER } from './codex-modes.mjs';
|
|
7
|
+
import { AGENT_MODES } from './agents.mjs';
|
|
8
|
+
|
|
9
|
+
export const SETTINGS_VERSION = 1;
|
|
10
|
+
|
|
11
|
+
/** A login by the last part of its folder's name; the folder itself stays on the Mac. */
|
|
12
|
+
export const accountIdOf = (profile) => (profile === 'default' ? 'default' : String(profile ?? '').split(/[\\/]/).filter(Boolean).pop() ?? '');
|
|
13
|
+
|
|
14
|
+
const ACCOUNT_ID = /^[\w.-]{1,80}$/;
|
|
15
|
+
const SLUG = /^[a-z0-9][\w-]{0,119}$/i;
|
|
16
|
+
const bool = (v) => typeof v === 'boolean';
|
|
17
|
+
const oneOf = (list) => (v) => list.includes(v);
|
|
18
|
+
const count = (v) => Number.isInteger(v) && v >= 1 && v <= 64;
|
|
19
|
+
const accountId = (v) => typeof v === 'string' && ACCOUNT_ID.test(v) && v !== '.' && v !== '..';
|
|
20
|
+
|
|
21
|
+
const WORKSPACE = {
|
|
22
|
+
cornerTag: bool,
|
|
23
|
+
diagnostics: bool,
|
|
24
|
+
adhdMode: bool,
|
|
25
|
+
alphaRooms: bool,
|
|
26
|
+
alphaTeammates: bool,
|
|
27
|
+
agentsAuto: bool,
|
|
28
|
+
memoryGate: bool,
|
|
29
|
+
cleanupLeftovers: bool,
|
|
30
|
+
sessionsAtOnce: count,
|
|
31
|
+
memoryGateSlots: (v) => v === null || count(v),
|
|
32
|
+
agentsFeltSlow: oneOf(['more', 'reset']),
|
|
33
|
+
outsideAgents: oneOf(AGENT_MODES),
|
|
34
|
+
permission: oneOf(CLAUDE_PERMISSION_MODES),
|
|
35
|
+
codexMode: oneOf(CODEX_MODE_ORDER),
|
|
36
|
+
phoneProjects: (v) => !!v && bool(v.all) && Array.isArray(v.except) && v.except.length <= 500 && v.except.every((s) => typeof s === 'string' && SLUG.test(s)),
|
|
37
|
+
activeAccount: (v) => !!v && oneOf(['claude', 'codex'])(v.engine) && (v.account === null || accountId(v.account)),
|
|
38
|
+
};
|
|
39
|
+
const PROJECT = {
|
|
40
|
+
autonomous: bool,
|
|
41
|
+
permission: oneOf(['workspace', ...CLAUDE_PERMISSION_MODES]),
|
|
42
|
+
codexMode: oneOf(['workspace', ...CODEX_MODE_ORDER]),
|
|
43
|
+
account: (v) => v === 'any' || accountId(v),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Why the Mac should not carry out this change, or null. */
|
|
47
|
+
export function checkSetting(p) {
|
|
48
|
+
if (!p || typeof p !== 'object') return 'not a setting';
|
|
49
|
+
const rules = p.scope === 'workspace' ? WORKSPACE : p.scope === 'project' ? PROJECT : null;
|
|
50
|
+
if (!rules || !Object.hasOwn(rules, p.key)) return 'not a setting your phone can change';
|
|
51
|
+
if (p.scope === 'project' && !(typeof p.product === 'string' && SLUG.test(p.product))) return 'that setting needs its project';
|
|
52
|
+
return rules[p.key](p.value) ? null : 'not a value that setting takes';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function profileFor(model, engine, id) {
|
|
56
|
+
if (id === null) return null;
|
|
57
|
+
const list = engine === 'codex' ? model?.workspace?.codex?.accounts : model?.workspace?.accounts;
|
|
58
|
+
const hit = (list ?? []).find((a) => accountIdOf(a.profile) === id);
|
|
59
|
+
if (!hit) throw new Error('that account is not on this computer');
|
|
60
|
+
return hit.profile;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The window's own handler for a checked change, and what to hand it. */
|
|
64
|
+
export function settingRoute(p, model) {
|
|
65
|
+
if (p.scope === 'project') {
|
|
66
|
+
const value = p.key === 'account' && p.value !== 'any' ? profileFor(model, 'claude', p.value) : p.value;
|
|
67
|
+
return ['zero:settings-set-project', { product: p.product, key: p.key, value }];
|
|
68
|
+
}
|
|
69
|
+
const value = p.key === 'activeAccount' ? { engine: p.value.engine, profile: profileFor(model, p.value.engine, p.value.account) } : p.value;
|
|
70
|
+
return ['zero:settings-set-workspace', { key: p.key, value }];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* What the phone's Settings pages draw, from the Mac's settings model. Null
|
|
75
|
+
* when the Mac could not read its own settings.
|
|
76
|
+
*/
|
|
77
|
+
export function phoneSettingsOf(model, { version }) {
|
|
78
|
+
if (!model?.ok || !model.workspace) return null;
|
|
79
|
+
const w = model.workspace;
|
|
80
|
+
const gate = w.memoryGate ?? {};
|
|
81
|
+
return {
|
|
82
|
+
v: SETTINGS_VERSION,
|
|
83
|
+
version: String(version ?? ''),
|
|
84
|
+
workspace: {
|
|
85
|
+
cornerTag: !!w.cornerTag,
|
|
86
|
+
diagnostics: !!w.diagnostics,
|
|
87
|
+
adhdMode: !!w.adhdMode,
|
|
88
|
+
alpha: { rooms: !!w.alpha?.rooms },
|
|
89
|
+
outsideAgents: w.outsideAgents,
|
|
90
|
+
permission: w.permission,
|
|
91
|
+
codexMode: w.codexMode ?? null,
|
|
92
|
+
engines: (w.engineChoices ?? []).map((e) => e.id),
|
|
93
|
+
claude: { found: !!w.claudeFound },
|
|
94
|
+
codex: { found: !!w.codex?.found },
|
|
95
|
+
agentsAuto: !!w.agentsAuto,
|
|
96
|
+
sessionsAtOnce: w.sessionsAtOnce,
|
|
97
|
+
agentsTotal: w.agentsTotal,
|
|
98
|
+
agentsAutoTotal: w.agentsAutoTotal,
|
|
99
|
+
agentsNudge: w.agentsNudge ?? 0,
|
|
100
|
+
slotsMax: w.slotsMax,
|
|
101
|
+
slotsSuggested: w.slotsSuggested,
|
|
102
|
+
running: w.running,
|
|
103
|
+
memoryGate: { on: !!gate.on, slots: gate.slots ?? null, slotsAuto: gate.slotsAuto ?? null, slotsMax: gate.slotsMax ?? null, now: gate.now ?? null },
|
|
104
|
+
leftovers: { on: !!w.leftovers?.on, now: w.leftovers?.now ?? null },
|
|
105
|
+
accounts: (w.accounts ?? []).map((a) => ({
|
|
106
|
+
id: accountIdOf(a.profile), email: a.email ?? null, chosen: !!a.chosen, state: a.state,
|
|
107
|
+
note: a.trouble?.note ?? null, plan: a.plan?.label ?? null, running: a.running ?? 0,
|
|
108
|
+
})),
|
|
109
|
+
codexAccounts: (w.codex?.accounts ?? []).map((a) => ({
|
|
110
|
+
id: accountIdOf(a.profile), email: a.email ?? null, name: a.name ?? null, plan: a.plan ?? null, signedIn: !!a.signedIn, chosen: !!a.chosen,
|
|
111
|
+
})),
|
|
112
|
+
},
|
|
113
|
+
projects: (model.projects ?? []).map((p) => ({
|
|
114
|
+
slug: p.slug, name: p.name, onPhone: !!p.onPhone, autonomous: !!p.autonomous,
|
|
115
|
+
permission: p.permission, codexMode: p.codexMode, account: p.account === 'any' ? 'any' : accountIdOf(p.account), running: p.running ?? 0,
|
|
116
|
+
})),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Types for shared/phone-snapshot.mjs, for the phone with the computer closed.
|
|
2
|
+
export const SNAPSHOT_LIMITS: { lines: number; textChars: number; items: number; bytes: number };
|
|
3
|
+
export const PHONE_COMMANDS: string[];
|
|
4
|
+
export function hideSecrets(text: string, opts?: { home?: string | null }): string;
|
|
5
|
+
export function bytesOf(value: unknown): number;
|
|
6
|
+
export function threadSnapshot(item: Record<string, unknown>, history?: unknown[], limits?: typeof SNAPSHOT_LIMITS, opts?: { home?: string | null }): { v: number; item: Record<string, unknown>; history: unknown[]; olderLines: number };
|
|
7
|
+
export function threadsWorthCarrying<T>(items: T[], opts?: { now?: number }): T[];
|
|
8
|
+
export function snapshotKey(snapshot: unknown): string;
|
|
9
|
+
export function madeHere(commandId: string): string;
|
|
10
|
+
export function phoneChoices(kind: string, p: Record<string, unknown> | null | undefined): Record<string, unknown>;
|
|
11
|
+
export function phoneChoiceOf(saved: unknown, personId: string | null | undefined, every?: string[]): { all: boolean; except: string[] };
|
|
12
|
+
export function phoneProjectsOf(saved: unknown, personId: string | null | undefined, every?: string[]): string[];
|
|
13
|
+
export function withPhoneProject(saved: unknown, personId: string | null | undefined, slug: string, on: boolean, every?: string[]): { owner: string; all: boolean; except: string[] };
|
|
14
|
+
export function withPhoneAll(saved: unknown, personId: string | null | undefined, on: boolean, every?: string[]): { owner: string; all: boolean; except: string[] };
|
|
15
|
+
export function checkCommand(cmd: unknown, ctx: { projects: string[]; hasItem: (project: string, id: string) => boolean }): string | null;
|
|
16
|
+
export function phoneEligible(products: { slug?: string }[]): string[];
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// ONE THREAD, AS HER PHONE SEES IT WITH THE MAC SHUT (docs/cloud/PHONE-PLAN.md).
|
|
2
|
+
//
|
|
3
|
+
// Her Mac writes this for each thread of a project she put on her phone, and
|
|
4
|
+
// the phone reads it. It is what the phone's own screens already draw: the row
|
|
5
|
+
// as the Mac folded it (the phone never decides a thread's state from raw
|
|
6
|
+
// lines; Codex's review of the plan, finding 5), and the thread's history as
|
|
7
|
+
// the window reads it (store.readHistory), which the thread view turns into
|
|
8
|
+
// the conversation. Long text is cut, and only the latest lines go. Browser-
|
|
9
|
+
// safe: no node imports.
|
|
10
|
+
|
|
11
|
+
import { checkSetting } from './phone-settings.mjs';
|
|
12
|
+
|
|
13
|
+
// How much of one thread goes up. The latest of a thread is what she reads on
|
|
14
|
+
// a phone; the whole of it stays on the Mac. `bytes` is measured in UTF-8, as
|
|
15
|
+
// the database measures it (Codex's review of step 1, finding 9).
|
|
16
|
+
export const SNAPSHOT_LIMITS = { lines: 400, textChars: 12_000, items: 300, bytes: 900_000 };
|
|
17
|
+
|
|
18
|
+
// WHAT A COPY MAY CARRY, BY NAME (Codex's review of step 1, finding 2). A copy
|
|
19
|
+
// is built out of these and nothing else, never by copying a row and deleting
|
|
20
|
+
// the bad parts: a lease, an epoch, who may see a team thread, a field some
|
|
21
|
+
// later version adds, none of it leaves unless it is named here.
|
|
22
|
+
const ROW_FIELDS = [
|
|
23
|
+
'id', 'title', 'label', 'body', 'kind', 'labels', 'priority', 'parent', 'status',
|
|
24
|
+
'result', 'note', 'answer', 'runAt', 'snoozedUntil', 'answeredThrough',
|
|
25
|
+
'engine', 'model', 'start', 'due', 'problem', 'progress', 'solution',
|
|
26
|
+
'createdAt', 'updatedAt',
|
|
27
|
+
];
|
|
28
|
+
// The fields a line in a thread's history may carry in its patch: what the
|
|
29
|
+
// thread view draws as the conversation and its state changes.
|
|
30
|
+
const LINE_FIELDS = [
|
|
31
|
+
'title', 'label', 'body', 'kind', 'labels', 'priority', 'status', 'result',
|
|
32
|
+
'note', 'answer', 'runAt', 'snoozedUntil', 'start', 'due', 'problem',
|
|
33
|
+
'progress', 'solution', 'inReplyTo',
|
|
34
|
+
];
|
|
35
|
+
const SOURCES = new Set(['founder', 'agent', 'system']);
|
|
36
|
+
|
|
37
|
+
// KEYS AND TOKENS IN WHAT AN AGENT OR SHE WROTE, hidden. A conversation can
|
|
38
|
+
// quote one ("here is the key it used"), and a copy online is no place for it.
|
|
39
|
+
const SECRETS = [
|
|
40
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g,
|
|
41
|
+
/\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{20,}/g,
|
|
42
|
+
/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{30,}\b/g,
|
|
43
|
+
/\bgithub_pat_[A-Za-z0-9_]{40,}\b/g,
|
|
44
|
+
/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g,
|
|
45
|
+
/\bxox[abposr]-[A-Za-z0-9-]{10,}/g,
|
|
46
|
+
/\bAIza[0-9A-Za-z_-]{35}\b/g,
|
|
47
|
+
/\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
48
|
+
/\b(Bearer\s+)[A-Za-z0-9._~+/-]{20,}=*/gi,
|
|
49
|
+
/\b(Basic\s+)[A-Za-z0-9+/]{12,}=*/gi,
|
|
50
|
+
];
|
|
51
|
+
// A VALUE GIVEN A SECRET'S NAME, whatever it looks like (Codex's second and
|
|
52
|
+
// third reviews): `AWS_SECRET_ACCESS_KEY=...`, `password: hunter22`,
|
|
53
|
+
// `PASSWORD=12345678`, `password="correct horse battery staple"`. The name
|
|
54
|
+
// stays, so the sentence still reads; a quoted value goes whole.
|
|
55
|
+
const STRONG = '[A-Za-z0-9_.-]*(?:passw(?:or)?d|passphrase|secret|api[_-]?key|access[_-]?key|private[_-]?key|credentials?|authorization|(?:auth|access|refresh|session|bearer|id)[_-]?token)[A-Za-z0-9_.-]*';
|
|
56
|
+
// "token" alone is a word people use ("token: unexpected identifier"), so it
|
|
57
|
+
// counts only as an ENV_STYLE name, or before a long run that looks like a key.
|
|
58
|
+
const WEAK = '(?:[A-Z0-9_]*TOKEN[A-Z0-9_]*|token)';
|
|
59
|
+
// A quoted value goes whole, however long, escaped quotes and all; a bare one
|
|
60
|
+
// is everything up to a space, brackets included (Codex's fourth review).
|
|
61
|
+
const VALUE = '(?:"((?:[^"\\\\\\n]|\\\\.)*)"|\'((?:[^\'\\\\\\n]|\\\\.)*)\'|([^\\s"\',;]+))';
|
|
62
|
+
const NAMED_STRONG = new RegExp(`\\b(${STRONG})(["']?\\s*[:=]\\s*)${VALUE}`, 'gi');
|
|
63
|
+
const NAMED_WEAK = new RegExp(`\\b(${WEAK})(["']?\\s*[:=]\\s*)${VALUE}`, 'g');
|
|
64
|
+
const hidden = (name, sep, dq, sq, bare) => `${name}${sep}${dq !== undefined ? '"[hidden]"' : sq !== undefined ? "'[hidden]'" : '[hidden]'}`;
|
|
65
|
+
function hideNamed(text) {
|
|
66
|
+
let out = text.replace(NAMED_STRONG, (m, name, sep, dq, sq, bare) => (bare === '[hidden]' ? m : hidden(name, sep, dq, sq, bare)));
|
|
67
|
+
out = out.replace(NAMED_WEAK, (m, name, sep, dq, sq, bare) => {
|
|
68
|
+
const value = dq ?? sq ?? bare ?? '';
|
|
69
|
+
// MAX_TOKENS=128000 and TOKEN_LIMIT=8192 are settings: a bare number is
|
|
70
|
+
// never taken for a key under this weaker name.
|
|
71
|
+
const envStyle = name !== 'token' && name === name.toUpperCase() && !/^[0-9]+$/.test(value);
|
|
72
|
+
const keyLike = value.length >= 16 && /[0-9]/.test(value) && !/\s/.test(value);
|
|
73
|
+
return value !== '[hidden]' && (envStyle || keyLike) ? hidden(name, sep, dq, sq, bare) : m;
|
|
74
|
+
});
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
// AND A LONG RUN OF LETTERS AND DIGITS THAT LOOKS LIKE NOTHING BUT A KEY: 32 or
|
|
78
|
+
// more, upper and lower case and digits all present. A commit id or a uuid is
|
|
79
|
+
// one case of hex and is left alone.
|
|
80
|
+
const KEYLIKE = /\b(?=[A-Za-z0-9_-]{32,}\b)(?=[A-Za-z0-9_-]*[a-z])(?=[A-Za-z0-9_-]*[A-Z])(?=[A-Za-z0-9_-]*[0-9])[A-Za-z0-9_-]{32,}\b/g;
|
|
81
|
+
export function hideSecrets(text, { home = null } = {}) {
|
|
82
|
+
let out = String(text);
|
|
83
|
+
for (const re of SECRETS) out = out.replace(re, (m, bearer) => (typeof bearer === 'string' && /^(Bearer|Basic)/i.test(bearer) ? `${bearer}[hidden]` : '[hidden]'));
|
|
84
|
+
out = hideNamed(out);
|
|
85
|
+
out = out.replace(KEYLIKE, '[hidden]');
|
|
86
|
+
// Her home folder is ~ online, as it is in a crash report (shared/crash-scrub.mjs).
|
|
87
|
+
if (home && home.length > 1) out = out.split(home).join('~');
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const clip = (s, n, ctx) => {
|
|
92
|
+
const t = hideSecrets(s, ctx);
|
|
93
|
+
return t.length > n ? `${t.slice(0, n)}…` : t;
|
|
94
|
+
};
|
|
95
|
+
// A value of a named field: text cut and cleaned, numbers as they are, short
|
|
96
|
+
// lists of words, nothing nested.
|
|
97
|
+
function plain(v, n, ctx) {
|
|
98
|
+
if (typeof v === 'string') return clip(v, n, ctx);
|
|
99
|
+
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
|
100
|
+
if (typeof v === 'boolean' || v === null) return v;
|
|
101
|
+
if (Array.isArray(v)) return v.filter((x) => typeof x === 'string').slice(0, 40).map((x) => clip(x, 200, ctx));
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
function pick(obj, fields, n, ctx) {
|
|
105
|
+
const out = {};
|
|
106
|
+
for (const f of fields) {
|
|
107
|
+
if (!obj || !(f in obj)) continue;
|
|
108
|
+
const v = plain(obj[f], n, ctx);
|
|
109
|
+
if (v !== undefined) out[f] = v;
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const bytesOf = (value) => new TextEncoder().encode(JSON.stringify(value)).length;
|
|
115
|
+
|
|
116
|
+
// `item` is the folded row; `history` is store.readHistory for it. `home` is
|
|
117
|
+
// her home folder, written ~ in the copy.
|
|
118
|
+
export function threadSnapshot(item, history = [], limits = SNAPSHOT_LIMITS, { home = null } = {}) {
|
|
119
|
+
const ctx = { home };
|
|
120
|
+
const row = pick(item, ROW_FIELDS, limits.textChars, ctx);
|
|
121
|
+
// Who wrote each field, which is how the screens tell her words from an
|
|
122
|
+
// agent's: the source and the moment, nothing else of it.
|
|
123
|
+
if (item?.wrote && typeof item.wrote === 'object') {
|
|
124
|
+
const wrote = {};
|
|
125
|
+
for (const f of ROW_FIELDS) {
|
|
126
|
+
const w = item.wrote[f];
|
|
127
|
+
if (w && SOURCES.has(w.source) && Number.isFinite(w.ts)) wrote[f] = { ts: w.ts, source: w.source };
|
|
128
|
+
}
|
|
129
|
+
row.wrote = wrote;
|
|
130
|
+
}
|
|
131
|
+
const lines = [];
|
|
132
|
+
for (const l of Array.isArray(history) ? history : []) {
|
|
133
|
+
if (!l || l.claim || l.release || l.heartbeat || !SOURCES.has(l.source) || !Number.isFinite(l.ts)) continue;
|
|
134
|
+
const patch = pick(l.patch, LINE_FIELDS, limits.textChars, ctx);
|
|
135
|
+
if (!Object.keys(patch).length) continue;
|
|
136
|
+
lines.push({ id: row.id, ts: l.ts, source: l.source, patch });
|
|
137
|
+
}
|
|
138
|
+
// Under the database's megabyte, with room: fewer lines until it fits, and
|
|
139
|
+
// then shorter text, so one long thread never stops the rest being sent.
|
|
140
|
+
for (let keep = limits.lines, chars = limits.textChars; ;) {
|
|
141
|
+
const kept = lines.slice(-keep);
|
|
142
|
+
const snap = {
|
|
143
|
+
v: 3,
|
|
144
|
+
item: chars === limits.textChars ? row : pick(row, ROW_FIELDS, chars, ctx),
|
|
145
|
+
history: chars === limits.textChars ? kept : kept.map((l) => ({ ...l, patch: pick(l.patch, LINE_FIELDS, chars, ctx) })),
|
|
146
|
+
olderLines: lines.length - kept.length,
|
|
147
|
+
};
|
|
148
|
+
if (row.wrote && snap.item !== row) snap.item.wrote = row.wrote;
|
|
149
|
+
if (bytesOf(snap) <= limits.bytes) return snap;
|
|
150
|
+
if (keep > 1) keep = Math.floor(keep / 2);
|
|
151
|
+
else if (chars > 200) chars = Math.floor(chars / 4);
|
|
152
|
+
else return { v: 3, item: { id: row.id, title: clip(row.title ?? '', 200, ctx), status: row.status, updatedAt: row.updatedAt }, history: [], olderLines: lines.length };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// WHICH PROJECTS ARE ON THE PHONE OF THE PERSON SIGNED IN.
|
|
157
|
+
//
|
|
158
|
+
// ALL OF THEM, UNLESS SHE SAYS OTHERWISE (2026-10-15: "95% of people just want
|
|
159
|
+
// all of their projects connected ... I just turn them all on"). One switch
|
|
160
|
+
// for the lot, on until she turns it off, and new projects go up with the
|
|
161
|
+
// rest; keeping a project on this computer only is the exception, named
|
|
162
|
+
// project by project (`except`).
|
|
163
|
+
//
|
|
164
|
+
// Her choice is kept with her id on it (`phoneProducts` in the config:
|
|
165
|
+
// { owner, all, except }), so somebody else signing in on this computer starts
|
|
166
|
+
// from the default rather than from her choices (Codex's review of step 1,
|
|
167
|
+
// finding 1). A choice saved in the first shape, a list of the projects she
|
|
168
|
+
// turned on ({ owner, slugs }), reads as all of them except the ones that list
|
|
169
|
+
// left out, so the projects she had on stay on.
|
|
170
|
+
// `every` is every project here, by slug.
|
|
171
|
+
// The projects that can go to a phone at all: not the walk's practice project.
|
|
172
|
+
export const phoneEligible = (products) => (Array.isArray(products) ? products : []).map((p) => p?.slug).filter((s) => typeof s === 'string' && s && s !== 'practice');
|
|
173
|
+
export function phoneChoiceOf(saved, personId, every = []) {
|
|
174
|
+
const mine = personId && saved && typeof saved === 'object' && saved.owner === personId ? saved : null;
|
|
175
|
+
if (!mine) return { all: true, except: [] };
|
|
176
|
+
if (mine.all === undefined && Array.isArray(mine.slugs)) {
|
|
177
|
+
const on = new Set(mine.slugs);
|
|
178
|
+
return { all: true, except: every.filter((s) => !on.has(s)) };
|
|
179
|
+
}
|
|
180
|
+
return { all: mine.all !== false, except: Array.isArray(mine.except) ? mine.except.filter((s) => typeof s === 'string' && s) : [] };
|
|
181
|
+
}
|
|
182
|
+
export function phoneProjectsOf(saved, personId, every = []) {
|
|
183
|
+
if (!personId) return [];
|
|
184
|
+
const { all, except } = phoneChoiceOf(saved, personId, every);
|
|
185
|
+
if (!all) return [];
|
|
186
|
+
const kept = new Set(except);
|
|
187
|
+
return every.filter((s) => typeof s === 'string' && s && !kept.has(s));
|
|
188
|
+
}
|
|
189
|
+
// One project on or off the phone; the rest as they were.
|
|
190
|
+
export function withPhoneProject(saved, personId, slug, on, every = []) {
|
|
191
|
+
if (!personId) throw new Error('sign in first');
|
|
192
|
+
const { all, except } = phoneChoiceOf(saved, personId, every);
|
|
193
|
+
const kept = new Set(except);
|
|
194
|
+
if (on) kept.delete(slug); else kept.add(slug);
|
|
195
|
+
return { owner: personId, all, except: [...kept] };
|
|
196
|
+
}
|
|
197
|
+
// The whole phone on or off; which projects stay local is remembered.
|
|
198
|
+
export function withPhoneAll(saved, personId, on, every = []) {
|
|
199
|
+
if (!personId) throw new Error('sign in first');
|
|
200
|
+
const { except } = phoneChoiceOf(saved, personId, every);
|
|
201
|
+
return { owner: personId, all: !!on, except };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// A NEW THREAD STARTED ON THE PHONE has a row there before the Mac makes it,
|
|
205
|
+
// under this id, and a reply to it before then is a reply to this id. The Mac
|
|
206
|
+
// remembers which real row it became (main/phone-inbox.mjs) (Codex's review of
|
|
207
|
+
// step 1, finding 8). Shaped like a row id, so every screen takes it.
|
|
208
|
+
export const madeHere = (commandId) => `w-${String(commandId).replace(/[^0-9a-f]/gi, '').slice(0, 10).toLowerCase()}`;
|
|
209
|
+
|
|
210
|
+
// The threads of a project worth carrying: everything not yet archived, and
|
|
211
|
+
// what was finished in the last fortnight, newest first, at most `items`.
|
|
212
|
+
export function threadsWorthCarrying(items, { now = Date.now(), limits = SNAPSHOT_LIMITS } = {}) {
|
|
213
|
+
const fortnight = 14 * 24 * 3600_000;
|
|
214
|
+
return (Array.isArray(items) ? items : [])
|
|
215
|
+
.filter((i) => i && !i.agent && (i.status !== 'done' || now - (i.updatedAt ?? 0) < fortnight))
|
|
216
|
+
.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))
|
|
217
|
+
.slice(0, limits.items);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// A cheap, stable fingerprint, so a thread that did not change is not sent.
|
|
221
|
+
export function snapshotKey(snapshot) {
|
|
222
|
+
const text = JSON.stringify(snapshot);
|
|
223
|
+
let h = 2166136261;
|
|
224
|
+
for (let i = 0; i < text.length; i += 1) { h ^= text.charCodeAt(i); h = Math.imul(h, 16777619); }
|
|
225
|
+
return `${text.length}:${(h >>> 0).toString(36)}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// WHAT HER PHONE MAY ASK HER MAC TO DO, and nothing else. A command that is not
|
|
229
|
+
// one of these, or names a project she did not put on her phone, or a thread
|
|
230
|
+
// that is not in it, is refused and says why.
|
|
231
|
+
// A `setting` belongs to the computer, not a project (shared/phone-settings.mjs).
|
|
232
|
+
export const PHONE_COMMANDS = ['reply', 'new', 'snooze', 'close', 'stop', 'run-now', 'send-now', 'reopen', 'schedule', 'approve', 'setting'];
|
|
233
|
+
|
|
234
|
+
// A PERMISSION CARD AS HER PHONE DRAWS IT: what the agent asks to do, with the
|
|
235
|
+
// secrets in it hidden as a thread's words are, and the fingerprint of the card
|
|
236
|
+
// her Mac drew, which her answer must carry back.
|
|
237
|
+
export function phoneApproval(request, card, ctx = {}) {
|
|
238
|
+
// Her Allow signs the whole request by its digest, so the phone may carry the
|
|
239
|
+
// digest only when it drew the request whole; a card that hid, clipped or
|
|
240
|
+
// dropped anything carries none and is answered on the computer. A home path
|
|
241
|
+
// written ~ is for her eyes, not a value hidden from what she signs.
|
|
242
|
+
let lossy = false;
|
|
243
|
+
const text = (s, n) => {
|
|
244
|
+
if (hideSecrets(s) !== s) lossy = true;
|
|
245
|
+
const t = hideSecrets(s, ctx);
|
|
246
|
+
if (t.length > n) { lossy = true; return `${t.slice(0, n)}…`; }
|
|
247
|
+
return t;
|
|
248
|
+
};
|
|
249
|
+
const clean = (v, depth = 0) => {
|
|
250
|
+
if (typeof v === 'string') return text(v, 4000);
|
|
251
|
+
if (typeof v === 'number' || typeof v === 'boolean' || v === null) return v;
|
|
252
|
+
if (depth > 3) { lossy = true; return undefined; }
|
|
253
|
+
if (Array.isArray(v)) { if (v.length > 40) lossy = true; return v.slice(0, 40).map((x) => clean(x, depth + 1)); }
|
|
254
|
+
if (v && typeof v === 'object') {
|
|
255
|
+
const entries = Object.entries(v);
|
|
256
|
+
if (entries.length > 40) lossy = true;
|
|
257
|
+
return Object.fromEntries(entries.slice(0, 40).map(([k, x]) => [k, clean(x, depth + 1)]));
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
};
|
|
261
|
+
const shown = { id: String(request.id), at: Number(request.at) || 0, tool: text(String(request.tool ?? 'unknown'), 200), input: clean(request.input ?? {}) };
|
|
262
|
+
return lossy ? { ...shown, card: null, answerOnComputer: true } : { ...shown, card };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// WHAT SHE PICKED WITH A REPLY OR A NEW THREAD, as the window's own handlers
|
|
266
|
+
// take it (main/ipc.mjs zero:answer, zero:compose): only these fields, each
|
|
267
|
+
// only in its right shape. Null on a reply clears a choice, as in the window.
|
|
268
|
+
const WORD = /^[A-Za-z][\w.:-]{0,79}$/;
|
|
269
|
+
const word = (v, n = 80) => typeof v === 'string' && v.length <= n && WORD.test(v);
|
|
270
|
+
const orNull = (ok) => (v) => v === null || ok(v);
|
|
271
|
+
const CHOICES = {
|
|
272
|
+
reply: {
|
|
273
|
+
permissionMode: orNull((v) => word(v, 40)), model: orNull(word), effort: orNull((v) => word(v, 20)),
|
|
274
|
+
priority: (v) => Number.isInteger(v) && v >= 0 && v <= 10, now: (v) => v === true, unschedule: (v) => v === true,
|
|
275
|
+
},
|
|
276
|
+
new: {
|
|
277
|
+
runAt: (v) => Number.isFinite(v) && v > 0, start: (v) => v === 'later' || v === 'now',
|
|
278
|
+
engine: (v) => word(v, 40), model: word, effort: (v) => word(v, 20), priority: (v) => Number.isInteger(v) && v >= 0 && v <= 10,
|
|
279
|
+
assignee: (v) => typeof v === 'string' && v.length >= 1 && v.length <= 80, due: (v) => typeof v === 'string' && v.length <= 40,
|
|
280
|
+
visibility: (v) => ['team', 'people', 'private'].includes(v),
|
|
281
|
+
visibleTo: (v) => Array.isArray(v) && v.length <= 50 && v.every((x) => typeof x === 'string' && x.length <= 80),
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
export function phoneChoices(kind, p) {
|
|
285
|
+
const out = {};
|
|
286
|
+
for (const [k, ok] of Object.entries(CHOICES[kind] ?? {})) if (p && k in p && p[k] !== undefined && ok(p[k])) out[k] = p[k];
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
export function checkCommand(cmd, { projects, hasItem }) {
|
|
290
|
+
if (!cmd || !PHONE_COMMANDS.includes(cmd.kind)) return 'not something the phone may do';
|
|
291
|
+
if (cmd.kind === 'setting') return cmd.project != null || cmd.item != null ? 'a setting names no project or thread' : checkSetting(cmd.payload);
|
|
292
|
+
if (!projects.includes(cmd.project)) return 'that project is not on your phone';
|
|
293
|
+
const p = cmd.payload || {};
|
|
294
|
+
if (cmd.kind === 'new') {
|
|
295
|
+
const title = typeof p.title === 'string' ? p.title.trim() : '';
|
|
296
|
+
const body = typeof p.body === 'string' ? p.body.trim() : '';
|
|
297
|
+
if (!title && !body) return 'a new thread needs some words';
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
if (!cmd.item || !hasItem(cmd.project, cmd.item)) return 'that thread is not on your phone';
|
|
301
|
+
if (cmd.kind === 'reply' && !(typeof p.text === 'string' && p.text.trim())) return 'an empty reply';
|
|
302
|
+
if (cmd.kind === 'snooze' && !(Number.isFinite(p.until) && p.until >= 0)) return 'a snooze needs a time';
|
|
303
|
+
if (cmd.kind === 'schedule' && !(Number.isFinite(p.runAt) && p.runAt >= 0)) return 'a schedule needs a time';
|
|
304
|
+
if (cmd.kind === 'approve' && !(typeof p.approval === 'string' && /^[\w-]{1,80}$/.test(p.approval) && typeof p.allow === 'boolean'
|
|
305
|
+
&& typeof p.card === 'string' && /^[\w+/=-]{8,200}$/.test(p.card) && (p.note === undefined || (typeof p.note === 'string' && p.note.length <= 500)))) return 'an answer needs its card';
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// A TURN THE CLOUD TOOK WITH THE COMPUTER SHUT, written into the thread's copy
|
|
310
|
+
// by the cloud server (cloud/gateway/phone.mjs), as the computer would have
|
|
311
|
+
// written it: her reply, the agent's answer and the row waiting on her
|
|
312
|
+
// (main/store.mjs recordSessionResult). The computer's own copy replaces this
|
|
313
|
+
// one once it is back and has brought the run home.
|
|
314
|
+
export function withCloudTurn(snapshot, { reply, answer, repliedAt, answeredAt, status = 'blocked' }, limits = SNAPSHOT_LIMITS) {
|
|
315
|
+
if (!snapshot || typeof snapshot !== 'object' || !snapshot.item?.id) return null;
|
|
316
|
+
const ctx = { home: null };
|
|
317
|
+
const id = snapshot.item.id;
|
|
318
|
+
// No reply for a new thread's first turn: her ask is already its first line.
|
|
319
|
+
const replied = reply !== null && reply !== undefined;
|
|
320
|
+
const said = clip(String(reply ?? ''), limits.textChars, ctx);
|
|
321
|
+
const result = clip(String(answer ?? ''), limits.textChars, ctx);
|
|
322
|
+
const lines = [
|
|
323
|
+
...(replied ? [{ id, ts: repliedAt, source: 'founder', patch: { answer: said, status: 'open' } }] : []),
|
|
324
|
+
{ id, ts: answeredAt, source: 'agent', patch: { result } },
|
|
325
|
+
{ id, ts: answeredAt, source: 'system', patch: { status } },
|
|
326
|
+
];
|
|
327
|
+
const wrote = { ...(snapshot.item.wrote ?? {}), ...(replied ? { answer: { ts: repliedAt, source: 'founder' } } : {}), result: { ts: answeredAt, source: 'agent' }, status: { ts: answeredAt, source: 'system' } };
|
|
328
|
+
const item = { ...snapshot.item, ...(replied ? { answer: said } : {}), result, status, updatedAt: answeredAt, wrote };
|
|
329
|
+
let history = [...(Array.isArray(snapshot.history) ? snapshot.history : []), ...lines];
|
|
330
|
+
let older = Number(snapshot.olderLines) || 0;
|
|
331
|
+
while (history.length > limits.lines || (history.length > lines.length && bytesOf({ ...snapshot, item, history }) > limits.bytes)) {
|
|
332
|
+
history = history.slice(1);
|
|
333
|
+
older += 1;
|
|
334
|
+
}
|
|
335
|
+
return { ...snapshot, item, history, olderLines: older };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// THE PHONE'S COPY OF A THREAD THE CLOUD STARTED FROM THE PHONE, before its
|
|
339
|
+
// computer has one: the row her ask made, being worked on.
|
|
340
|
+
export function newThreadSnapshot({ itemId, title, body = '', at }, limits = SNAPSHOT_LIMITS) {
|
|
341
|
+
const words = { title: String(title ?? ''), ...(body ? { body: String(body) } : {}) };
|
|
342
|
+
const said = { ts: at, source: 'founder' };
|
|
343
|
+
return threadSnapshot(
|
|
344
|
+
{ id: itemId, ...words, kind: 'directive', labels: ['founder'], priority: 0, status: 'claimed', createdAt: at, updatedAt: at, wrote: { title: said, ...(body ? { body: said } : {}) } },
|
|
345
|
+
[{ ts: at, source: 'founder', patch: { ...words, status: 'open' } }],
|
|
346
|
+
limits,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function turnedName(name: string | null | undefined): string | null;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// PICTURES THE WINDOW CANNOT DRAW, and what each is turned into so it can.
|
|
2
|
+
// Chromium decodes no TIFF and no HEIC, so one pasted or dropped into a reply
|
|
3
|
+
// box is turned into a PNG or a JPEG as it comes in (main/stage-attachment.mjs).
|
|
4
|
+
// Both sides read this: the window to know a dropped file is worth staging,
|
|
5
|
+
// the main process to turn it.
|
|
6
|
+
const TURNED = [[/\.tiff?$/i, '.png'], [/\.hei[cf]$/i, '.jpg']];
|
|
7
|
+
|
|
8
|
+
/** The name the picture goes on under once turned, or null when it is not one of these. */
|
|
9
|
+
export function turnedName(name) {
|
|
10
|
+
const s = String(name ?? '');
|
|
11
|
+
const hit = TURNED.find(([re]) => re.test(s));
|
|
12
|
+
return hit ? s.replace(/\.[^.]+$/, hit[1]) : null;
|
|
13
|
+
}
|
package/shared/rank.d.mts
CHANGED
|
@@ -8,7 +8,8 @@ export function placeScore(order: string[], slug: string, direct?: ReadonlySet<s
|
|
|
8
8
|
|
|
9
9
|
export const DEFAULT_PRIORITY: number;
|
|
10
10
|
|
|
11
|
-
export function itemPriority(item: { priority?: number; wrote?: Record<string, { source?: string } | undefined> } | null | undefined): number;
|
|
11
|
+
export function itemPriority(item: { priority?: number | null; wrote?: Record<string, { source?: string } | undefined> } | null | undefined): number;
|
|
12
|
+
export function shownPriority(item: { priority?: number | null; wrote?: Record<string, { source?: string } | undefined> } | null | undefined, conversation?: boolean): number | null;
|
|
12
13
|
export const URGENT_PRIORITY: number;
|
|
13
14
|
export function isUrgent(item: { priority?: number; wrote?: Record<string, { source?: string } | undefined> } | null | undefined): boolean;
|
|
14
15
|
|