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.
Files changed (278) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/agent-sessions.mjs +282 -268
  9. package/main/agents.mjs +179 -466
  10. package/main/analytics.mjs +12 -2
  11. package/main/answer-modes.mjs +103 -37
  12. package/main/approval-prompt-core.mjs +197 -0
  13. package/main/approval-prompt-server.mjs +4 -138
  14. package/main/approvals.mjs +101 -297
  15. package/main/artifact-path.mjs +47 -18
  16. package/main/claude-input.mjs +48 -4
  17. package/main/claude-plan.mjs +48 -0
  18. package/main/claude-session-open.mjs +28 -0
  19. package/main/claude-usage-process.mjs +40 -0
  20. package/main/claude-usage.mjs +18 -40
  21. package/main/cloud/account.mjs +179 -0
  22. package/main/cloud/aws.mjs +127 -0
  23. package/main/cloud/browser-return.mjs +58 -0
  24. package/main/cloud/clients.mjs +15 -0
  25. package/main/cloud/codex-cloud.mjs +53 -0
  26. package/main/cloud/config.mjs +107 -0
  27. package/main/cloud/docs.mjs +219 -0
  28. package/main/cloud/exec.mjs +27 -0
  29. package/main/cloud/host.mjs +165 -0
  30. package/main/cloud/kit.mjs +80 -0
  31. package/main/cloud/logins.mjs +543 -0
  32. package/main/cloud/managed.mjs +115 -0
  33. package/main/cloud/marker.mjs +104 -0
  34. package/main/cloud/org.mjs +53 -0
  35. package/main/cloud/reuse.mjs +208 -0
  36. package/main/cloud/run-cli.mjs +177 -0
  37. package/main/cloud/run.mjs +1934 -0
  38. package/main/cloud/usage.mjs +76 -0
  39. package/main/cloud-permission.mjs +51 -0
  40. package/main/codex-account.mjs +78 -0
  41. package/main/codex-app-server.mjs +276 -409
  42. package/main/codex-approvals.mjs +148 -433
  43. package/main/codex-models.mjs +20 -2
  44. package/main/codex-session.mjs +212 -507
  45. package/main/codex-threads-worker.mjs +12 -0
  46. package/main/codex-threads.mjs +71 -3
  47. package/main/codex-usage-file.mjs +5 -3
  48. package/main/codex-usage.mjs +56 -7
  49. package/main/codex-watch.mjs +17 -3
  50. package/main/codex.mjs +95 -363
  51. package/main/config.mjs +11 -1
  52. package/main/corner-tag.mjs +340 -19
  53. package/main/dev-window.mjs +1 -1
  54. package/main/engine-bin.mjs +232 -282
  55. package/main/engine-setup.mjs +21 -9
  56. package/main/feedback.mjs +3 -25
  57. package/main/folder-pictures.mjs +2 -2
  58. package/main/freeze-recorder-thread.mjs +86 -0
  59. package/main/freeze-recorder.mjs +146 -0
  60. package/main/fresh-user.mjs +20 -9
  61. package/main/git-change-offthread.mjs +17 -3
  62. package/main/git-change-thread.mjs +5 -2
  63. package/main/harnesses/claude.mjs +32 -11
  64. package/main/harnesses/codex.mjs +9 -2
  65. package/main/importable-threads.mjs +64 -6
  66. package/main/ipc.mjs +362 -111
  67. package/main/leftovers.mjs +117 -25
  68. package/main/liquid-glass.mjs +26 -0
  69. package/main/live-replies.mjs +10 -4
  70. package/main/main-went-red.mjs +330 -0
  71. package/main/main.mjs +199 -84
  72. package/main/memory-gate-history.mjs +36 -10
  73. package/main/memory-gate-server.mjs +62 -0
  74. package/main/memory-gate.mjs +14 -1
  75. package/main/menu.mjs +4 -2
  76. package/main/message-priority.mjs +2 -2
  77. package/main/mic.mjs +38 -0
  78. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  79. package/main/new-chats-watch.mjs +110 -0
  80. package/main/new-user-window.mjs +25 -0
  81. package/main/notify.mjs +24 -8
  82. package/main/phone-alerts.mjs +117 -0
  83. package/main/phone-inbox.mjs +625 -0
  84. package/main/phone-link.mjs +29 -9
  85. package/main/phone-uploads.mjs +476 -0
  86. package/main/picture-shape.mjs +114 -0
  87. package/main/quit-sequence.mjs +143 -0
  88. package/main/refresh-agent-models.mjs +15 -2
  89. package/main/relay-client.mjs +69 -1
  90. package/main/repeats.mjs +10 -4
  91. package/main/row-label.mjs +12 -41
  92. package/main/run-change.mjs +13 -0
  93. package/main/run-ledger.mjs +135 -0
  94. package/main/save-attachments.mjs +41 -0
  95. package/main/serve.mjs +45 -3
  96. package/main/settings.mjs +220 -44
  97. package/main/shell-path.mjs +185 -37
  98. package/main/ship-conflict-brief.mjs +119 -0
  99. package/main/ship-queue.mjs +115 -19
  100. package/main/source-updater.mjs +18 -10
  101. package/main/stage-attachment.mjs +68 -0
  102. package/main/stall-watch.mjs +50 -0
  103. package/main/store/home.mjs +87 -232
  104. package/main/store/paths.mjs +11 -0
  105. package/main/store/teammates-files.mjs +99 -0
  106. package/main/store/work-items.mjs +231 -13
  107. package/main/store.mjs +226 -52
  108. package/main/supervisor.mjs +3103 -341
  109. package/main/task-folders.mjs +430 -30
  110. package/main/task-terminals.mjs +110 -34
  111. package/main/team/index.mjs +114 -10
  112. package/main/team/memory-cloud.mjs +183 -1
  113. package/main/team/room-hours.mjs +48 -0
  114. package/main/team/room-setup.mjs +31 -0
  115. package/main/team/rooms-memory.mjs +454 -0
  116. package/main/team/rooms-shape.mjs +42 -0
  117. package/main/team/rooms-supabase.mjs +324 -0
  118. package/main/team/rooms.mjs +575 -0
  119. package/main/team/session.mjs +96 -26
  120. package/main/team/supabase-backend.mjs +180 -0
  121. package/main/team/sync.mjs +109 -1
  122. package/main/teammates.mjs +580 -0
  123. package/main/terminal-host.mjs +143 -0
  124. package/main/terminal-hosts.mjs +261 -0
  125. package/main/transcript-search.mjs +80 -0
  126. package/main/webview-guard.mjs +45 -0
  127. package/main/worktree-include.mjs +90 -219
  128. package/mcp/core/documents.mjs +3 -1
  129. package/mcp/core/repeats.mjs +88 -0
  130. package/mcp/core/teammates.mjs +222 -0
  131. package/mcp/core/work.mjs +76 -4
  132. package/mcp/stdio.mjs +24 -11
  133. package/mcp/tools.mjs +116 -8
  134. package/package.json +21 -5
  135. package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
  136. package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
  137. package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
  138. package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
  139. package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
  140. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  141. package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
  142. package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
  143. package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
  144. package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
  145. package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
  146. package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
  147. package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
  148. package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
  149. package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
  150. package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
  151. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  152. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  153. package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
  154. package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
  155. package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
  156. package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
  157. package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
  158. package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
  159. package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
  160. package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
  161. package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
  162. package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
  163. package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
  164. package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
  165. package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
  166. package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
  167. package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
  168. package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
  169. package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
  170. package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
  171. package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
  172. package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
  173. package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
  174. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  175. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  176. package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
  177. package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
  178. package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
  179. package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
  180. package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
  181. package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
  182. package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
  183. package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
  184. package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
  185. package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
  186. package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
  187. package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
  188. package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
  189. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  190. package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
  191. package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
  192. package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
  193. package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
  194. package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
  195. package/renderer/dist/corner-tag.html +3 -3
  196. package/renderer/dist/index.html +3 -3
  197. package/scripts/after-install.mjs +30 -8
  198. package/scripts/cloud-shim.pl +57 -0
  199. package/shared/account-names.d.mts +6 -0
  200. package/shared/account-names.mjs +15 -0
  201. package/shared/agent-hours.mjs +136 -0
  202. package/shared/agent-import.mjs +60 -34
  203. package/shared/agents.mjs +126 -522
  204. package/shared/analytics-events.mjs +12 -0
  205. package/shared/approval-card.mjs +36 -0
  206. package/shared/bridge-map.mjs +27 -1
  207. package/shared/claude-code-shape.mjs +6 -0
  208. package/shared/claude-usage.d.mts +1 -0
  209. package/shared/claude-usage.mjs +42 -1
  210. package/shared/cloud-hand-back.d.mts +7 -0
  211. package/shared/cloud-hand-back.mjs +20 -0
  212. package/shared/cloud-kit.d.mts +219 -0
  213. package/shared/cloud-kit.mjs +341 -0
  214. package/shared/cloud-login-name.mjs +4 -0
  215. package/shared/cloud-start.mjs +17 -0
  216. package/shared/cloud-versions.mjs +23 -0
  217. package/shared/codex-import.mjs +2 -2
  218. package/shared/codex-modes.d.mts +22 -9
  219. package/shared/codex-modes.mjs +67 -11
  220. package/shared/codex-usage.mjs +108 -2
  221. package/shared/corner-tag.d.mts +23 -1
  222. package/shared/corner-tag.mjs +284 -29
  223. package/shared/dead-run-trace.mjs +19 -2
  224. package/shared/engines.mjs +70 -309
  225. package/shared/feedback.d.mts +7 -0
  226. package/shared/feedback.mjs +25 -0
  227. package/shared/first-run-practice.mjs +17 -24
  228. package/shared/harness-definitions.mjs +8 -0
  229. package/shared/instruction-defaults.json +2 -2
  230. package/shared/key-caps.d.mts +14 -0
  231. package/shared/key-caps.mjs +68 -0
  232. package/shared/message-edits.d.mts +7 -0
  233. package/shared/message-edits.mjs +35 -0
  234. package/shared/move-thread.d.mts +24 -0
  235. package/shared/move-thread.mjs +112 -0
  236. package/shared/named-threads.mjs +72 -0
  237. package/shared/not-shipped.d.mts +22 -0
  238. package/shared/not-shipped.mjs +89 -0
  239. package/shared/notify-rules.d.mts +6 -0
  240. package/shared/notify-rules.mjs +36 -8
  241. package/shared/phone-files.mjs +28 -0
  242. package/shared/phone-settings.mjs +118 -0
  243. package/shared/phone-snapshot.d.mts +16 -0
  244. package/shared/phone-snapshot.mjs +348 -0
  245. package/shared/picture-formats.d.mts +1 -0
  246. package/shared/picture-formats.mjs +13 -0
  247. package/shared/rank.d.mts +2 -1
  248. package/shared/rank.mjs +32 -19
  249. package/shared/repeats.d.mts +8 -0
  250. package/shared/repeats.mjs +33 -2
  251. package/shared/room-cards.mjs +167 -0
  252. package/shared/room-links.d.mts +4 -0
  253. package/shared/room-links.mjs +21 -0
  254. package/shared/runs-on.d.mts +3 -3
  255. package/shared/runs-on.mjs +8 -5
  256. package/shared/shipped-note.d.mts +4 -0
  257. package/shared/shipped-note.mjs +6 -0
  258. package/shared/shortcuts.mjs +14 -6
  259. package/shared/spawn-trouble.d.mts +4 -0
  260. package/shared/spawn-trouble.mjs +443 -40
  261. package/shared/team-rules.mjs +9 -2
  262. package/shared/teammates.d.mts +19 -0
  263. package/shared/teammates.mjs +756 -0
  264. package/shared/terminal-focus.d.mts +1 -0
  265. package/shared/terminal-focus.mjs +13 -0
  266. package/shared/thread-cards.mjs +4 -1
  267. package/shared/usage.d.mts +9 -0
  268. package/shared/usage.mjs +49 -3
  269. package/shared/websites.d.mts +15 -0
  270. package/shared/websites.mjs +61 -0
  271. package/shared/work-items.mjs +127 -30
  272. package/shared/work-lines.mjs +92 -244
  273. package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
  274. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
  275. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
  276. package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
  277. package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
  278. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
@@ -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 card has a clock on it.
82
- // A finished agent waits as long as it takes. An approval is a worker frozen
83
- // mid-run with her as the only way through, and it is DENIED AUTOMATICALLY
84
- // after fifteen minutes (TIMEOUT_MS, main/approval-prompt-server.mjs). If the
85
- // first banner of the stretch was a finished agent, an approval landing twenty
86
- // minutes later would die unheard under a strict one-and-done rule. So an
87
- // approval gets to speak once more, and that is the cap: at most two in one
88
- // stretch away, never more.
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