agentbox-app 0.1.7 → 0.1.12

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 (255) hide show
  1. package/README.md +220 -58
  2. package/bin/agentbox.mjs +18 -3
  3. package/briefs/message-rules.md +24 -3
  4. package/briefs/worker.md +63 -49
  5. package/briefs/writing-rules.md +90 -131
  6. package/main/account-discovery.mjs +21 -20
  7. package/main/account-tooling.mjs +11 -11
  8. package/main/agent-activity.mjs +45 -5
  9. package/main/agent-schedule.mjs +14 -14
  10. package/main/agent-sessions.mjs +94 -10
  11. package/main/agent-updates.mjs +116 -0
  12. package/main/agents.mjs +52 -46
  13. package/main/analytics.mjs +27 -0
  14. package/main/approval-prompt-server.mjs +2 -2
  15. package/main/approvals.mjs +14 -5
  16. package/main/artifact-path.mjs +11 -0
  17. package/main/claude-bin.mjs +32 -5
  18. package/main/claude-input.mjs +45 -3
  19. package/main/claude-plan.mjs +25 -13
  20. package/main/code-change.mjs +3 -3
  21. package/main/codex-account.mjs +1 -1
  22. package/main/codex-bin.mjs +18 -0
  23. package/main/codex-launch-env.mjs +31 -0
  24. package/main/codex-memory-gate.mjs +260 -0
  25. package/main/codex-models.mjs +12 -7
  26. package/main/codex-threads.mjs +46 -23
  27. package/main/codex-usage-file.mjs +7 -7
  28. package/main/codex-usage.mjs +4 -4
  29. package/main/codex-watch.mjs +12 -12
  30. package/main/config.mjs +82 -17
  31. package/main/demo.mjs +17 -14
  32. package/main/dev-window.mjs +9 -9
  33. package/main/doc-scheme.mjs +2 -0
  34. package/main/engine-setup.mjs +204 -0
  35. package/main/feedback.mjs +55 -0
  36. package/main/folder-pictures.mjs +48 -0
  37. package/main/frame-scrollbars.mjs +90 -0
  38. package/main/fresh-user.mjs +7 -4
  39. package/main/git-change-offthread.mjs +70 -0
  40. package/main/git-change-thread.mjs +11 -0
  41. package/main/git-change.mjs +9 -3
  42. package/main/img-scheme.mjs +70 -1
  43. package/main/instruction-settings.mjs +134 -60
  44. package/main/ipc.mjs +335 -42
  45. package/main/leftovers.mjs +518 -0
  46. package/main/live-replies.mjs +43 -5
  47. package/main/machine.mjs +48 -1
  48. package/main/main.mjs +138 -15
  49. package/main/memory-gate-history.mjs +254 -0
  50. package/main/memory-gate-server.mjs +461 -0
  51. package/main/memory-gate.mjs +206 -0
  52. package/main/message-priority.mjs +87 -0
  53. package/main/notify.mjs +1 -1
  54. package/main/rail-note.mjs +2 -2
  55. package/main/refresh-agent-models.mjs +21 -0
  56. package/main/repeats.mjs +4 -4
  57. package/main/row-label.mjs +115 -51
  58. package/main/runs-on.mjs +93 -0
  59. package/main/serve.mjs +77 -5
  60. package/main/settings.mjs +245 -26
  61. package/main/ship-queue.mjs +279 -0
  62. package/main/sign-in-files.mjs +39 -0
  63. package/main/source-updater.mjs +298 -0
  64. package/main/store/home.mjs +84 -7
  65. package/main/store/project.mjs +15 -19
  66. package/main/store/work-items.mjs +150 -6
  67. package/main/store.mjs +245 -13
  68. package/main/supervisor.mjs +1634 -417
  69. package/main/task-commands.mjs +8 -10
  70. package/main/task-folders.mjs +94 -16
  71. package/main/task-remote-control.mjs +3 -3
  72. package/main/task-terminals.mjs +14 -2
  73. package/main/team/index.mjs +440 -0
  74. package/main/team/memory-cloud.mjs +247 -0
  75. package/main/team/projects.mjs +162 -0
  76. package/main/team/session.mjs +225 -0
  77. package/main/team/sign-in.mjs +106 -0
  78. package/main/team/supabase-backend.mjs +231 -0
  79. package/main/team/sync.mjs +163 -0
  80. package/main/updater.mjs +15 -5
  81. package/main/write-before-reload.mjs +50 -0
  82. package/mcp/core/page.mjs +10 -12
  83. package/mcp/core/work.mjs +123 -26
  84. package/mcp/tools.mjs +13 -6
  85. package/package.json +21 -9
  86. package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
  87. package/renderer/dist/assets/index-ChKsQkRw.js +1169 -0
  88. package/renderer/dist/assets/index-ClBXnq3B.css +32 -0
  89. package/renderer/dist/index.html +2 -2
  90. package/scripts/after-install.mjs +14 -0
  91. package/scripts/approval-server.sh +7 -2
  92. package/scripts/memory-gate-hook.sh +59 -0
  93. package/shared/agent-import.mjs +39 -16
  94. package/shared/agents.d.mts +26 -8
  95. package/shared/agents.mjs +100 -108
  96. package/shared/analytics-events.mjs +19 -2
  97. package/shared/answers.mjs +12 -0
  98. package/shared/bridge-map.d.mts +12 -0
  99. package/shared/bridge-map.mjs +38 -1
  100. package/shared/claude-commands.generated.mjs +2 -2
  101. package/shared/claude-models.generated.mjs +6 -6
  102. package/shared/claude-usage.mjs +6 -6
  103. package/shared/codex-import.mjs +7 -7
  104. package/shared/codex-modes.d.mts +50 -0
  105. package/shared/codex-modes.mjs +1 -1
  106. package/shared/codex-usage.mjs +7 -8
  107. package/shared/demo-world.mjs +6 -6
  108. package/shared/engines.mjs +27 -10
  109. package/shared/feedback.d.mts +14 -0
  110. package/shared/feedback.mjs +44 -0
  111. package/shared/first-run-examples.mjs +4 -4
  112. package/shared/first-run-practice.d.mts +16 -0
  113. package/shared/first-run-practice.mjs +176 -70
  114. package/shared/fresh-user-home.mjs +34 -4
  115. package/shared/instruction-defaults.json +2 -2
  116. package/shared/product-name.mjs +4 -2
  117. package/shared/project-folder-check.mjs +9 -3
  118. package/shared/rank.d.mts +1 -0
  119. package/shared/rank.mjs +22 -0
  120. package/shared/referenced-files.mjs +1 -1
  121. package/shared/runs-on.d.mts +20 -0
  122. package/shared/runs-on.mjs +79 -0
  123. package/shared/schemes.d.mts +11 -0
  124. package/shared/schemes.mjs +1 -1
  125. package/shared/shortcuts.d.mts +41 -0
  126. package/shared/shortcuts.mjs +236 -0
  127. package/shared/spawn-trouble.d.mts +2 -0
  128. package/shared/spawn-trouble.mjs +40 -19
  129. package/shared/team-rules.d.mts +17 -0
  130. package/shared/team-rules.mjs +230 -0
  131. package/shared/team-status.d.mts +11 -0
  132. package/shared/team-status.mjs +90 -0
  133. package/shared/terminal-links.d.mts +2 -0
  134. package/shared/terminal-links.mjs +55 -0
  135. package/shared/terminal-state.d.mts +1 -0
  136. package/shared/terminal-state.mjs +16 -0
  137. package/shared/thread-cards.d.mts +19 -0
  138. package/shared/thread-cards.mjs +254 -0
  139. package/shared/usage.d.mts +1 -1
  140. package/shared/usage.mjs +6 -7
  141. package/shared/work-items.mjs +258 -28
  142. package/shared/work-lines.mjs +393 -16
  143. package/worker-permissions.json +1 -1
  144. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  145. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  146. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  147. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  148. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  149. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  150. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  151. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  152. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  153. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  154. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  155. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  156. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  157. package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
  158. package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
  159. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  160. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  161. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  162. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  163. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  164. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  165. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  166. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  167. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  168. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  169. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  170. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  171. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  172. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  173. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  174. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  175. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  176. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  177. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  178. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  179. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  180. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  181. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  182. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  183. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  184. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  185. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  186. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  187. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  188. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  189. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  190. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  191. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  192. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  193. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  194. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  195. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  196. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  197. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  198. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  199. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  200. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  201. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  202. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  203. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  204. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  205. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  206. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  207. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  208. package/renderer/dist/emulator/audio-rules.mjs +0 -74
  209. package/renderer/dist/emulator/data/compression/README.md +0 -11
  210. package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
  211. package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
  212. package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
  213. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  214. package/renderer/dist/emulator/data/cores/README.md +0 -22
  215. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  216. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  217. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  218. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  219. package/renderer/dist/emulator/data/cores/package.json +0 -65
  220. package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
  221. package/renderer/dist/emulator/data/emulator.css +0 -1614
  222. package/renderer/dist/emulator/data/loader.js +0 -170
  223. package/renderer/dist/emulator/data/localization/README.md +0 -63
  224. package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
  225. package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
  226. package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
  227. package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
  228. package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
  229. package/renderer/dist/emulator/data/localization/en-US.json +0 -339
  230. package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
  231. package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
  232. package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
  233. package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
  234. package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
  235. package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
  236. package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
  237. package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
  238. package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
  239. package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
  240. package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
  241. package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
  242. package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
  243. package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
  244. package/renderer/dist/emulator/data/src/GameManager.js +0 -464
  245. package/renderer/dist/emulator/data/src/compression.js +0 -142
  246. package/renderer/dist/emulator/data/src/emulator.js +0 -6158
  247. package/renderer/dist/emulator/data/src/gamepad.js +0 -167
  248. package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
  249. package/renderer/dist/emulator/data/src/shaders.js +0 -236
  250. package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
  251. package/renderer/dist/emulator/data/src/storage.js +0 -125
  252. package/renderer/dist/emulator/data/version.json +0 -5
  253. package/renderer/dist/emulator/host.js +0 -299
  254. package/renderer/dist/emulator/index.html +0 -28
  255. package/renderer/dist/emulator/provenance.json +0 -6
@@ -0,0 +1,230 @@
1
+ // THE TEAM'S RULES ABOUT ROWS, IN ONE PLACE. Pure and browser-safe: the
2
+ // supervisor asks them before it runs anything, and the window asks them
3
+ // before it puts a row in somebody's inbox, so the two can never disagree about
4
+ // whose a row is.
5
+ //
6
+ // A project is shared when its record carries a team id (main/team/projects.mjs).
7
+ // Everything here leaves a private project exactly as it always was.
8
+ import { pickFields } from './work-items.mjs';
9
+
10
+ export const isShared = (product) => !!product?.team?.projectId;
11
+
12
+ // THE PERSON WHOSE MAC RUNS AGENTS ON A ROW: whoever it was handed to, else
13
+ // whoever started it, else (a row from before anyone signed in) whoever shared
14
+ // the project.
15
+ export function runnerOf(item, product) {
16
+ return item?.runner || item?.createdBy || product?.team?.sharedBy || null;
17
+ }
18
+
19
+ // A row a person has been given waits for that person, not for an agent.
20
+ export const heldByAPerson = (item) => !!item?.assignee && item.assignee !== 'agent';
21
+
22
+ // May a worker run on this row on this Mac, signed in as `me`?
23
+ export function mayRunHere(item, product, me) {
24
+ if (!isShared(product)) return true;
25
+ if (!me) return false;
26
+ // A MESSAGE BETWEEN TWO PEOPLE NEVER RUNS AN AGENT, on either Mac. Turning
27
+ // one into work is @-mentioning an agent in the reply box, which starts a new
28
+ // thread of your own in the project the mention names; nothing a teammate
29
+ // writes into the shared record can start one here.
30
+ if (product?.team?.direct) return false;
31
+ if (heldByAPerson(item)) return false;
32
+ // ONLY YOU PUT YOUR MAC IN CHARGE OF A ROW. A runner naming you that somebody
33
+ // else's line wrote is a teammate trying to start an agent on your Mac with
34
+ // their words, so it does not count.
35
+ const runnerBy = item?.wrote?.runner?.by;
36
+ if (item?.runner === me && runnerBy && runnerBy !== me) return false;
37
+ // NOR DOES A TEAMMATE'S WORD START ONE. Lines pulled from teammates may not
38
+ // set these fields at all (whatATeammateMaySet below); this is the second
39
+ // wall, for a ledger that already holds such a line from before that rule.
40
+ // A field with no writer is from before anyone signed in, and is yours.
41
+ for (const field of STARTS_A_RUN) {
42
+ const by = item?.wrote?.[field]?.by;
43
+ if (by && by !== me) return false;
44
+ }
45
+ return runnerOf(item, product) === me;
46
+ }
47
+ const STARTS_A_RUN = ['body', 'answer', 'status', 'runAt'];
48
+
49
+ // WHOSE INBOX A SHARED ROW IS IN: one person's at a time, whoever has to act.
50
+ // A row given to a person is theirs until it is done; an agent's row is its
51
+ // runner's. True leaves the row to the inbox's ordinary rules, which is what a
52
+ // private row, or any row on a Mac nobody is signed in on, always gets.
53
+ // WHO SPOKE LAST IN A CONVERSATION: the writer of its newest message.
54
+ export function lastSpeaker(item) {
55
+ const a = item?.wrote?.answer;
56
+ const b = item?.wrote?.body;
57
+ const latest = a && (!b || (a.ts ?? 0) >= (b.ts ?? 0)) ? a : b;
58
+ return latest?.by ?? item?.createdBy ?? null;
59
+ }
60
+
61
+ /**
62
+ * WHAT IN A CONVERSATION IS WAITING ON YOU (w-920461cbe6). Asked the day
63
+ * threads shipped: in a busy chat, with three people answering your thread and
64
+ * others carrying on in the chat, what reaches your inbox? Not a row per
65
+ * message, and not a thread you are not in; and a thread waiting on you must
66
+ * not be lost because you said something in the chat.
67
+ *
68
+ * So: `chat` is how many messages in the main chat came after your own newest
69
+ * one there, and `threads` is every thread you are IN (you wrote the message it
70
+ * hangs off, or replied in it) where somebody else spoke after you, newest
71
+ * first, with how many replies are new, who wrote them, what the thread is
72
+ * about and its newest words. Null for a row with no record of who spoke where
73
+ * (one from before threads), which then reads by `lastSpeaker` as it always did.
74
+ */
75
+ export function whatWaits(item, me) {
76
+ const talk = item?.talk;
77
+ if (!talk || !me) return null;
78
+ const since = (list) => { let n = 0; for (let k = list.length - 1; k >= 0 && list[k].by !== me; k -= 1) n += 1; return n; };
79
+ const threads = [];
80
+ for (const [uid, t] of Object.entries(talk.threads ?? {})) {
81
+ const replies = t.replies ?? [];
82
+ if (t.by !== me && !replies.some((r) => r.by === me)) continue;
83
+ const fresh = since(replies);
84
+ if (!fresh) continue;
85
+ const news = replies.slice(-fresh);
86
+ threads.push({
87
+ uid, mine: t.by === me, fresh,
88
+ people: [...new Set(news.map((r) => r.by))],
89
+ text: t.text, last: news[news.length - 1].text, lastBy: news[news.length - 1].by,
90
+ at: news[news.length - 1].ts,
91
+ });
92
+ }
93
+ threads.sort((a, b) => b.at - a.at);
94
+ return { chat: since(talk.chat ?? []), threads: threads.map(({ at, ...t }) => t) };
95
+ }
96
+
97
+ export function inMyInbox(item, product, me) {
98
+ if (!isShared(product) || !me) return true;
99
+ // A CONVERSATION NEEDS WHOEVER DID NOT SPEAK LAST (2026-10-01), so a message
100
+ // to three people is in all three inboxes until one of them answers, and the
101
+ // answer puts it back in everyone else's. One person to one works the same.
102
+ // WITH THREADS (w-920461cbe6) the same rule is read per place: the chat, and
103
+ // each thread you are in. One row, whichever of them is waiting.
104
+ if (product?.team?.direct) {
105
+ if (item.status === 'done') return false;
106
+ const waits = whatWaits(item, me);
107
+ if (waits) return waits.chat > 0 || waits.threads.length > 0;
108
+ const by = lastSpeaker(item);
109
+ return by ? by !== me : item.assignee === me;
110
+ }
111
+ if (heldByAPerson(item)) return item.assignee === me && item.status !== 'done';
112
+ return runnerOf(item, product) === me;
113
+ }
114
+
115
+ // A CONVERSATION WHERE YOU SPOKE LAST IS DONE FOR YOU (w-57a202a968), until
116
+ // the other person writes again and `inMyInbox` brings it back. Without it a
117
+ // message you answered was on no tab and no column: out of your inbox by the
118
+ // rule above, never In progress, and not Done because its status is not done.
119
+ // Her words: "It's not supposed to leave the board; it's supposed to go in Done."
120
+ export function iSpokeLast(item, product, me) {
121
+ if (!me || !product?.team?.direct || item?.status === 'done') return false;
122
+ // Done for you means nothing in it waits on you: the chat or any thread.
123
+ const waits = whatWaits(item, me);
124
+ if (waits) return waits.chat === 0 && waits.threads.length === 0;
125
+ return lastSpeaker(item) === me;
126
+ }
127
+
128
+ // A REPLY HANDS A PERSON-TO-PERSON ROW TO THE OTHER PERSON, so a conversation
129
+ // goes back and forth between two inboxes instead of sitting in both. Returns
130
+ // who it goes to, or null when a reply changes nothing about whose it is.
131
+ export function handedOnByReply(item, product, me) {
132
+ if (!isShared(product) || !me || !heldByAPerson(item) || item.assignee !== me) return null;
133
+ const people = Array.isArray(item.people) ? item.people : [];
134
+ return people.find((p) => p !== me) || (item.createdBy && item.createdBy !== me ? item.createdBy : null);
135
+ }
136
+
137
+ // WHAT A TEAMMATE'S LINE MAY SET ON THIS MAC. Found by review 2026-10-01: a
138
+ // teammate's line with source "founder" could set the body, the answer, the
139
+ // status, the schedule or the model of any row in a shared project, including
140
+ // a row whose runner is you, and the supervisor then started an agent here on
141
+ // their words. So a pulled line keeps only the summary (which the board shows
142
+ // anyway), who has to act next, and, in a message between two people (which
143
+ // never runs an agent), the message itself. Everything else is dropped before
144
+ // it reaches the disk, claims and epochs included. Returns null when nothing
145
+ // is left.
146
+ const A_TEAMMATE_MAY_SET = ['problem', 'progress', 'solution', 'blockedBy', 'blocks', 'assignee'];
147
+ // The status rides too: a new message reopens a conversation put away, and a
148
+ // message record never starts an agent (mayRunHere).
149
+ //
150
+ // AND A REACTION, which is the chips under a message (w-560647d4db). It is on
151
+ // this list rather than the one above because it is only ever drawn in a
152
+ // conversation, and because an unrecognised delta on an ordinary shared task
153
+ // would be a teammate writing a field nothing there reads. It cannot start an
154
+ // agent: `STARTS_A_RUN` above names the four fields that can, and a reaction
155
+ // touches none of them, so a chip never puts the row in anybody's inbox.
156
+ //
157
+ // AND WHICH MESSAGE A REPLY ANSWERS (w-920461cbe6), for the same two reasons:
158
+ // only a conversation draws a thread, and the mark rides beside an `answer`
159
+ // this list already lets through. Without it a teammate's thread reply would
160
+ // land in the middle of your chat instead of in the thread.
161
+ const IN_A_MESSAGE_ALSO = ['title', 'body', 'answer', 'people', 'status', 'react', 'inReplyTo'];
162
+ // AND THE LEVEL THE SENDER PICKED, ON THE OPENING LINE ALONE (w-7ba439c883).
163
+ // A message now carries how urgent its sender thought it was, and that number
164
+ // decides where it sits in the inbox it lands in, so it has to survive the
165
+ // pull. It may not become a way to re-rank a row afterwards, though: the level
166
+ // rides the line that MAKES the row and no other.
167
+ //
168
+ // `kind` is how that line is known. Only a row's first line sets it, and a
169
+ // teammate may not set it here at all, so it is a marker rather than a field:
170
+ // it is read, then dropped with everything else that is not allowed.
171
+ //
172
+ // WHAT THIS DOES NOT STOP, said plainly: a teammate who forges a line carrying
173
+ // a kind can still re-rank the one conversation the two of you share. They can
174
+ // already rewrite that row's title, body, answer and status (above), so this
175
+ // opens no row that was closed to them, and nothing outside a conversation is
176
+ // reachable either way.
177
+ const WHEN_A_MESSAGE_OPENS = ['priority'];
178
+ const opensARow = (patch) => 'kind' in patch;
179
+ export function whatATeammateMaySet(line, { direct = false } = {}) {
180
+ if (!line || typeof line !== 'object' || !line.patch || typeof line.patch !== 'object') return null;
181
+ const allowed = direct
182
+ ? [...A_TEAMMATE_MAY_SET, ...IN_A_MESSAGE_ALSO, ...(opensARow(line.patch) ? WHEN_A_MESSAGE_OPENS : [])]
183
+ : A_TEAMMATE_MAY_SET;
184
+ const kept = {};
185
+ for (const field of allowed) if (field in line.patch) kept[field] = line.patch[field];
186
+ const patch = pickFields(kept);
187
+ if (!patch) return null;
188
+ return { id: line.id, ts: line.ts, source: line.source, by: line.by, uid: line.uid, patch };
189
+ }
190
+
191
+ // A LEASE BELONGS TO THE MAC THAT TOOK IT. A claim, an epoch, a release or a
192
+ // heartbeat is a worker's hold on one Mac, so it never rides a line across to
193
+ // another one — not even to a second copy of the app that is also yours.
194
+ export function withoutALease(line) {
195
+ const out = { ...line };
196
+ delete out.claim; delete out.epoch; delete out.release; delete out.heartbeat;
197
+ return out;
198
+ }
199
+
200
+ // A LINE AS IT COMES BACK FROM THE CLOUD, the same on both backends. Who wrote
201
+ // it and when are the database's (by_person, created_at), never the copy in
202
+ // the body, and a teammate's line carries no lease (above).
203
+ export function asPulled(body, { by, at, me }) {
204
+ const line = { ...body, by };
205
+ if (by === me) return line;
206
+ const kept = withoutALease(line);
207
+ if (Number.isFinite(at)) kept.ts = at;
208
+ return kept;
209
+ }
210
+
211
+ // YOUR OWN LINE, COMING BACK FROM A SECOND COPY OF THE APP SIGNED IN AS YOU.
212
+ //
213
+ // Reported 2026-10-04 (w-2fce569057): a message sent to a teammate never came
214
+ // back. It had been sent from a second copy. It reached the cloud and it
215
+ // reached the teammate's app; what it never reached was the everyday copy,
216
+ // because the pull skipped every line whose writer was you, on the reasoning
217
+ // that a line you wrote is already here. True of the copy that wrote it, false
218
+ // of every other copy you own, and a conversation missing your own half reads
219
+ // as a message that never sent.
220
+ //
221
+ // ONLY IN A MESSAGE RECORD, where no agent ever runs (mayRunHere). On an
222
+ // ordinary shared row your own answer, body or status is exactly what starts
223
+ // one, and `mayRunHere` believes a line whose writer is you, because it is you:
224
+ // carrying those across would start a second agent on the same row on your
225
+ // other Mac. So nothing crosses there, and your own words stay where you typed
226
+ // them. Returns null when the line is not yours to bring back.
227
+ export function whatYourOtherCopyMaySet(line, { direct = false } = {}) {
228
+ if (!direct || !line || typeof line !== 'object') return null;
229
+ return withoutALease(line);
230
+ }
@@ -0,0 +1,11 @@
1
+ export interface PersonStatus { text: string; until: number | null }
2
+ export type HoldId = 'today' | 'tomorrow' | 'week' | 'open';
3
+
4
+ export const STATUS_MAX: number;
5
+ export const HOLDS: { id: HoldId; label: string }[];
6
+
7
+ export function cleanStatus(text: unknown): string | null;
8
+ export function liveStatus(status: PersonStatus | null | undefined, now?: number): PersonStatus | null;
9
+ export function hasLapsed(status: PersonStatus | null | undefined, now?: number): boolean;
10
+ export function holdsUntil(until: number | null | undefined, now?: number): string;
11
+ export function holdEnds(id: HoldId | string, now?: number): number | null;
@@ -0,0 +1,90 @@
1
+ // A STATUS LINE YOU WRITE YOURSELF: the rules, in one place.
2
+ //
3
+ // Four of six people interviewed asked for the same thing, and the founder
4
+ // put it plainest: "If I'm going to spend a day or a few days in meetings, I
5
+ // don't want the board to make it seem like I'm not doing any work." So a
6
+ // person carries one short line they wrote, and it holds until a time they
7
+ // chose.
8
+ //
9
+ // THE EXPIRY IS READ, NOT SWEPT. An expired line is nothing to everyone who
10
+ // reads it, at the moment it expires, without anyone's Mac having to be awake
11
+ // to clear it. The owner's Mac also clears it in the cloud on the next sync
12
+ // after it lapses, so the text does not sit there for good; that is tidying,
13
+ // not what makes it disappear.
14
+
15
+ /** A line is one line. Longer than this is a note, and the places it is drawn
16
+ * (a sidebar row, a hover card) cannot hold one. */
17
+ export const STATUS_MAX = 80;
18
+
19
+ /** What the app stores from what somebody typed: trimmed, cut to one line and
20
+ * to STATUS_MAX, or null when they said nothing. */
21
+ export function cleanStatus(text) {
22
+ const one = String(text ?? '').replace(/\s+/g, ' ').trim();
23
+ return one ? one.slice(0, STATUS_MAX) : null;
24
+ }
25
+
26
+ /** The status as it reads NOW: null when there is no text, or when the time
27
+ * they chose has passed. Everything that draws a status goes through here. */
28
+ export function liveStatus(status, now = Date.now()) {
29
+ const text = cleanStatus(status?.text);
30
+ if (!text) return null;
31
+ const until = status?.until ?? null;
32
+ if (until && until <= now) return null;
33
+ return { text, until };
34
+ }
35
+
36
+ /** True when there is text in the cloud that no longer reads, which is the
37
+ * owner's Mac's cue to clear it. */
38
+ export function hasLapsed(status, now = Date.now()) {
39
+ return !!cleanStatus(status?.text) && !!status?.until && status.until <= now;
40
+ }
41
+
42
+ /** How long it holds, in words, for the end of a line: "until 6pm today",
43
+ * "until Monday", "until Mar 3". Empty when it holds until they clear it. */
44
+ export function holdsUntil(until, now = Date.now()) {
45
+ if (!until || until <= now) return '';
46
+ const d = new Date(until);
47
+ if (new Date(now).toDateString() === d.toDateString()) {
48
+ const h = d.getHours();
49
+ const mins = d.getMinutes() ? `:${String(d.getMinutes()).padStart(2, '0')}` : '';
50
+ return `until ${h % 12 === 0 ? 12 : h % 12}${mins}${h < 12 ? 'am' : 'pm'} today`;
51
+ }
52
+ // Inside a week a weekday names itself. Eight days out, "until Monday"
53
+ // would be the Monday after the one they meant, so the date is safer.
54
+ if (until - now < 7 * 86_400_000) return `until ${d.toLocaleDateString(undefined, { weekday: 'long' })}`;
55
+ return `until ${d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}`;
56
+ }
57
+
58
+ /** The four holds the app offers, in the order they are drawn. */
59
+ export const HOLDS = [
60
+ { id: 'today', label: 'Today' },
61
+ { id: 'tomorrow', label: 'Tomorrow' },
62
+ { id: 'week', label: 'This week' },
63
+ { id: 'open', label: 'Until I clear it' },
64
+ ];
65
+
66
+ /** When a status set now runs out. A moment they chose wins while it is still
67
+ * ahead ("they should be able to choose their own timelines", 2026-10-04);
68
+ * anything else, now, the past or not a number at all, falls back to the
69
+ * named hold, so a stale or garbled moment can never end a status at once. */
70
+ export function statusEnds({ hold = 'open', until } = {}, now = Date.now()) {
71
+ if (typeof until === 'number' && Number.isFinite(until) && until > now) return until;
72
+ return holdEnds(hold, now);
73
+ }
74
+
75
+ /** When a hold runs out. Today and tomorrow end at the end of that day, so a
76
+ * line set at 9am and one set at 4pm both last the day out. */
77
+ export function holdEnds(id, now = Date.now()) {
78
+ if (id === 'open') return null;
79
+ const d = new Date(now);
80
+ d.setHours(23, 59, 59, 999);
81
+ if (id === 'tomorrow') d.setDate(d.getDate() + 1);
82
+ // THE PARENTHESES ARE LOAD BEARING. Written as `d.getDate() + (7 - day) % 7
83
+ // || 7`, `+` binds tighter than `||`, so on a Sunday it read
84
+ // `(11 + 0) || 7` and set the date to today: "this week" ended tonight.
85
+ if (id === 'week') {
86
+ const toSunday = (7 - d.getDay()) % 7;
87
+ d.setDate(d.getDate() + (toSunday || 7));
88
+ }
89
+ return d.getTime();
90
+ }
@@ -0,0 +1,2 @@
1
+ export type TerminalLink = { url: string; range: { start: { x: number; y: number }; end: { x: number; y: number } } };
2
+ export function linksAtRow(at: (y: number) => { text: string; wrapped: boolean } | undefined, row: number): TerminalLink[];
@@ -0,0 +1,55 @@
1
+ // THE WEB ADDRESSES ON ONE ROW OF THE APP'S TERMINAL, for xterm's link provider
2
+ // (renderer/src/components/TaskTerminal.tsx), which opens them in the browser.
3
+ //
4
+ // The terminal had none, so `claude auth login`'s "If the browser didn't open,
5
+ // visit: https://..." was text you could not click (2026-10-05). That address
6
+ // is about 600 characters and wraps over many rows, so the search runs over the
7
+ // whole LOGICAL line, every row joined to the ones it wraps onto, and a link is
8
+ // handed back for any row it touches, with its range spanning all of them.
9
+ //
10
+ // IT LIVES IN `shared` SO IT CAN BE TESTED without a DOM: xterm's buffer is
11
+ // read through `at(y)`, which answers `{ text, wrapped }` for a 0-based row,
12
+ // `wrapped` being xterm's `isWrapped` (this row continues the one above).
13
+ // Ranges come back the way xterm takes them, 1-based and inclusive. One
14
+ // character is one cell, which holds for anything an address can contain.
15
+
16
+ // The scheme, at least one character after it, and none of the characters that
17
+ // end an address in prose.
18
+ const ADDRESS = /https?:\/\/[^\s"'<>`]+/g;
19
+ // Punctuation that closes the sentence around an address, not the address.
20
+ const TRAILING = /[.,;:!?)\]}'"]+$/;
21
+
22
+ /**
23
+ * @param {(y: number) => ({ text: string, wrapped: boolean } | undefined)} at
24
+ * @param {number} row 0-based
25
+ * @returns {{ url: string, range: { start: { x: number, y: number }, end: { x: number, y: number } } }[]}
26
+ */
27
+ export function linksAtRow(at, row) {
28
+ if (!at(row)) return [];
29
+ let top = row;
30
+ while (top > 0 && at(top)?.wrapped && at(top - 1)) top -= 1;
31
+ const rows = [at(top).text];
32
+ for (let y = top + 1; at(y)?.wrapped; y += 1) rows.push(at(y).text);
33
+
34
+ // Where each row starts in the joined string, to turn an index back into a
35
+ // cell.
36
+ const starts = [];
37
+ let joined = '';
38
+ for (const text of rows) { starts.push(joined.length); joined += text; }
39
+ const cell = (i) => {
40
+ let r = starts.length - 1;
41
+ while (r > 0 && starts[r] > i) r -= 1;
42
+ return { x: i - starts[r] + 1, y: top + r + 1 };
43
+ };
44
+
45
+ const out = [];
46
+ for (const m of joined.matchAll(ADDRESS)) {
47
+ const url = m[0].replace(TRAILING, '');
48
+ if (!/^https?:\/\/./.test(url)) continue;
49
+ const start = cell(m.index);
50
+ const end = cell(m.index + url.length - 1);
51
+ if (start.y > row + 1 || end.y < row + 1) continue;
52
+ out.push({ url, range: { start, end } });
53
+ }
54
+ return out;
55
+ }
@@ -1 +1,2 @@
1
1
  export function commandRunning(processName: string, exited: boolean): boolean;
2
+ export function whenACommandFinishes(onFinish: () => void): (processName: string, exited: boolean) => void;
@@ -56,3 +56,19 @@ export function commandRunning(processName, exited) {
56
56
  if (!name) return false;
57
57
  return !SHELLS.test(shellName(name));
58
58
  }
59
+
60
+ // A COMMAND HAS JUST FINISHED: the terminal went from running something back to
61
+ // its prompt, or the shell ended under it. Settings' Add account panel reloads
62
+ // the accounts on this, because "the account appears above when it is done"
63
+ // was a promise nothing kept: the row read "Not signed in" after `claude auth
64
+ // login` said "Login successful." (2026-10-05). Feed it every reading the
65
+ // terminal polls; it calls `onFinish` once per command, and never for a shell
66
+ // that has only ever sat at its prompt.
67
+ export function whenACommandFinishes(onFinish) {
68
+ let running = false;
69
+ return (processName, exited) => {
70
+ const now = commandRunning(processName, exited);
71
+ if (running && !now) onFinish();
72
+ running = now;
73
+ };
74
+ }
@@ -0,0 +1,19 @@
1
+ // Types for shared/thread-cards.mjs, for the window side.
2
+ import type { ThreadStateWord, WorkItem } from '../renderer/src/types';
3
+
4
+ export const THREAD_STATES: ThreadStateWord[];
5
+ export function threadState(item: Partial<WorkItem> & { claimExpired?: boolean }, now?: number): ThreadStateWord;
6
+ export function firstSentence(text: string | undefined | null, max?: number): string;
7
+ export const SUMMARY_WORDS: number;
8
+ export function wordsIn(text: string | undefined | null): number;
9
+ export const CONTEXT_WORDS: number;
10
+ export const DONE_STEPS: number;
11
+ export const DONE_STEP_WORDS: number;
12
+ export function doneSteps(progress: string | undefined | null): string[];
13
+ export function summaryOf(item: Partial<WorkItem>): { problem: string; progress: string; solution: string; written: boolean };
14
+ export function cardsFor(p: { products: unknown[]; readItems: (product: any) => any[]; now?: number; since?: number | null }): any[];
15
+ type ProjectSeen = { personal?: boolean; seenBy?: string; seenByPeople?: string[] } | null | undefined;
16
+ export function shownToTeam(item: Partial<WorkItem>, since?: number | null, product?: ProjectSeen): boolean;
17
+ export function visibilityOf(item: Partial<WorkItem>, product?: ProjectSeen): 'team' | 'people' | 'private' | undefined;
18
+ export function projectSeenBy(product: ProjectSeen): { who: 'private' | 'team' | 'people'; people: string[] };
19
+ export function shownToPeople(item: Partial<WorkItem>, product?: ProjectSeen): string[];