agentbox-app 0.1.6 → 0.1.11

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 (244) hide show
  1. package/README.md +209 -55
  2. package/bin/agentbox.mjs +6 -0
  3. package/briefs/message-rules.md +13 -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 +9 -10
  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 +114 -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/artifact-path.mjs +11 -0
  16. package/main/claude-bin.mjs +31 -5
  17. package/main/claude-input.mjs +45 -3
  18. package/main/claude-plan.mjs +11 -11
  19. package/main/code-change.mjs +3 -3
  20. package/main/codex-account.mjs +1 -1
  21. package/main/codex-bin.mjs +17 -0
  22. package/main/codex-memory-gate.mjs +260 -0
  23. package/main/codex-models.mjs +12 -7
  24. package/main/codex-threads.mjs +46 -23
  25. package/main/codex-usage-file.mjs +7 -7
  26. package/main/codex-usage.mjs +4 -4
  27. package/main/codex-watch.mjs +12 -12
  28. package/main/config.mjs +68 -8
  29. package/main/dev-window.mjs +9 -9
  30. package/main/doc-scheme.mjs +2 -0
  31. package/main/engine-setup.mjs +193 -0
  32. package/main/feedback.mjs +55 -0
  33. package/main/folder-pictures.mjs +48 -0
  34. package/main/frame-scrollbars.mjs +90 -0
  35. package/main/fresh-user.mjs +6 -3
  36. package/main/git-change-offthread.mjs +70 -0
  37. package/main/git-change-thread.mjs +11 -0
  38. package/main/git-change.mjs +1 -1
  39. package/main/img-scheme.mjs +70 -1
  40. package/main/instruction-settings.mjs +92 -54
  41. package/main/ipc.mjs +316 -42
  42. package/main/leftovers.mjs +518 -0
  43. package/main/live-replies.mjs +43 -5
  44. package/main/machine.mjs +47 -0
  45. package/main/main.mjs +119 -15
  46. package/main/memory-gate-history.mjs +254 -0
  47. package/main/memory-gate-server.mjs +461 -0
  48. package/main/memory-gate.mjs +206 -0
  49. package/main/message-priority.mjs +87 -0
  50. package/main/rail-note.mjs +2 -2
  51. package/main/refresh-agent-models.mjs +20 -0
  52. package/main/repeats.mjs +4 -4
  53. package/main/row-label.mjs +114 -51
  54. package/main/serve.mjs +67 -5
  55. package/main/settings.mjs +223 -26
  56. package/main/ship-queue.mjs +279 -0
  57. package/main/sign-in-files.mjs +39 -0
  58. package/main/source-updater.mjs +298 -0
  59. package/main/store/home.mjs +47 -2
  60. package/main/store/work-items.mjs +150 -6
  61. package/main/store.mjs +235 -9
  62. package/main/supervisor.mjs +1444 -406
  63. package/main/task-commands.mjs +8 -10
  64. package/main/task-folders.mjs +74 -13
  65. package/main/task-remote-control.mjs +3 -3
  66. package/main/task-terminals.mjs +2 -1
  67. package/main/team/index.mjs +440 -0
  68. package/main/team/memory-cloud.mjs +247 -0
  69. package/main/team/projects.mjs +162 -0
  70. package/main/team/session.mjs +225 -0
  71. package/main/team/sign-in.mjs +106 -0
  72. package/main/team/supabase-backend.mjs +231 -0
  73. package/main/team/sync.mjs +163 -0
  74. package/main/updater.mjs +15 -5
  75. package/main/write-before-reload.mjs +50 -0
  76. package/mcp/core/page.mjs +9 -11
  77. package/mcp/core/work.mjs +91 -25
  78. package/mcp/tools.mjs +12 -6
  79. package/package.json +21 -9
  80. package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
  81. package/renderer/dist/assets/index-B-2Az952.css +32 -0
  82. package/renderer/dist/assets/index-D6qncgai.js +1168 -0
  83. package/renderer/dist/index.html +2 -2
  84. package/scripts/memory-gate-hook.sh +59 -0
  85. package/shared/agent-import.mjs +38 -15
  86. package/shared/agents.d.mts +26 -8
  87. package/shared/agents.mjs +100 -108
  88. package/shared/analytics-events.mjs +19 -2
  89. package/shared/answers.mjs +12 -0
  90. package/shared/bridge-map.d.mts +12 -0
  91. package/shared/bridge-map.mjs +37 -1
  92. package/shared/claude-commands.generated.mjs +2 -2
  93. package/shared/claude-models.generated.mjs +4 -4
  94. package/shared/claude-usage.mjs +6 -6
  95. package/shared/codex-import.mjs +7 -7
  96. package/shared/codex-modes.d.mts +50 -0
  97. package/shared/codex-usage.mjs +7 -8
  98. package/shared/demo-world.mjs +5 -5
  99. package/shared/engines.mjs +27 -10
  100. package/shared/feedback.d.mts +14 -0
  101. package/shared/feedback.mjs +44 -0
  102. package/shared/first-run-examples.mjs +4 -4
  103. package/shared/first-run-practice.d.mts +16 -0
  104. package/shared/first-run-practice.mjs +176 -70
  105. package/shared/fresh-user-home.mjs +22 -3
  106. package/shared/instruction-defaults.json +2 -2
  107. package/shared/product-name.mjs +4 -2
  108. package/shared/project-folder-check.mjs +6 -0
  109. package/shared/rank.d.mts +1 -0
  110. package/shared/rank.mjs +22 -0
  111. package/shared/referenced-files.mjs +1 -1
  112. package/shared/schemes.d.mts +11 -0
  113. package/shared/schemes.mjs +1 -1
  114. package/shared/shortcuts.d.mts +41 -0
  115. package/shared/shortcuts.mjs +236 -0
  116. package/shared/spawn-trouble.d.mts +2 -0
  117. package/shared/spawn-trouble.mjs +39 -18
  118. package/shared/team-rules.d.mts +17 -0
  119. package/shared/team-rules.mjs +230 -0
  120. package/shared/team-status.d.mts +11 -0
  121. package/shared/team-status.mjs +90 -0
  122. package/shared/terminal-links.d.mts +2 -0
  123. package/shared/terminal-links.mjs +55 -0
  124. package/shared/terminal-state.d.mts +1 -0
  125. package/shared/terminal-state.mjs +16 -0
  126. package/shared/thread-cards.d.mts +19 -0
  127. package/shared/thread-cards.mjs +254 -0
  128. package/shared/usage.d.mts +1 -1
  129. package/shared/usage.mjs +6 -7
  130. package/shared/work-items.mjs +255 -27
  131. package/shared/work-lines.mjs +393 -16
  132. package/worker-permissions.json +1 -1
  133. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  134. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  135. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  136. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  137. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  138. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  139. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  140. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  141. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  142. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  143. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  144. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  145. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  146. package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
  147. package/renderer/dist/assets/index-D9hCAVPl.js +0 -684
  148. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  149. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  150. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  151. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  152. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  153. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  154. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  155. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  156. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  157. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  158. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  159. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  160. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  161. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  162. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  163. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  164. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  165. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  166. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  167. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  168. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  169. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  170. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  171. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  172. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  173. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  174. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  175. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  176. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  177. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  178. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  179. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  180. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  181. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  182. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  183. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  184. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  185. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  186. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  187. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  188. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  189. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  190. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  191. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  192. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  193. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  194. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  195. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  196. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  197. package/renderer/dist/emulator/audio-rules.mjs +0 -74
  198. package/renderer/dist/emulator/data/compression/README.md +0 -11
  199. package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
  200. package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
  201. package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
  202. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  203. package/renderer/dist/emulator/data/cores/README.md +0 -22
  204. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  205. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  206. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  207. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  208. package/renderer/dist/emulator/data/cores/package.json +0 -65
  209. package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
  210. package/renderer/dist/emulator/data/emulator.css +0 -1614
  211. package/renderer/dist/emulator/data/loader.js +0 -170
  212. package/renderer/dist/emulator/data/localization/README.md +0 -63
  213. package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
  214. package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
  215. package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
  216. package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
  217. package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
  218. package/renderer/dist/emulator/data/localization/en-US.json +0 -339
  219. package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
  220. package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
  221. package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
  222. package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
  223. package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
  224. package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
  225. package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
  226. package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
  227. package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
  228. package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
  229. package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
  230. package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
  231. package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
  232. package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
  233. package/renderer/dist/emulator/data/src/GameManager.js +0 -464
  234. package/renderer/dist/emulator/data/src/compression.js +0 -142
  235. package/renderer/dist/emulator/data/src/emulator.js +0 -6158
  236. package/renderer/dist/emulator/data/src/gamepad.js +0 -167
  237. package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
  238. package/renderer/dist/emulator/data/src/shaders.js +0 -236
  239. package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
  240. package/renderer/dist/emulator/data/src/storage.js +0 -125
  241. package/renderer/dist/emulator/data/version.json +0 -5
  242. package/renderer/dist/emulator/host.js +0 -299
  243. package/renderer/dist/emulator/index.html +0 -28
  244. package/renderer/dist/emulator/provenance.json +0 -6
@@ -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[];
@@ -0,0 +1,254 @@
1
+ // A THREAD'S STATE, ITS SUMMARY, AND THE CARD A TEAMMATE SEES OF IT.
2
+ //
3
+ // Approved 2026-10-01 (w-e731ca9376). Three pure rules, shared by the Mac that
4
+ // publishes cards (main/team) and the window that draws them (renderer), so
5
+ // the owner and a teammate are always shown the same words.
6
+ //
7
+ // threadState one of four states and only four: waiting, running,
8
+ // scheduled, done. A status is always one of those
9
+ // predefined states, never "needs your pick".
10
+ // summaryOf problem, progress and solution. The agent keeps them
11
+ // current (update_work_item); until it has written one, the
12
+ // ask and the latest word on the thread stand in, so a new
13
+ // thread is never a blank summary.
14
+ // cardsFor every thread worth showing the team, as a card: the summary
15
+ // and nothing more; a card never adds information. A
16
+ // private thread publishes no card at all (decided 2026-10-01
17
+ // from user research): not a wordless one, not a count.
18
+
19
+ export const THREAD_STATES = ['waiting', 'running', 'scheduled', 'done'];
20
+
21
+ /** Which of the four states a thread is in. */
22
+ export function threadState(item, now = Date.now()) {
23
+ if (item.status === 'done') return 'done';
24
+ // A thread in Later is not running and nobody has to act on it, which is the
25
+ // scheduled family. It has no moment, so what it SAYS is "Not started"
26
+ // (threadStateWords in the window); the four card states are unchanged.
27
+ if (item.start === 'later') return 'scheduled';
28
+ if (Number.isFinite(item.runAt) && item.runAt > now) return 'scheduled';
29
+ if (item.status === 'claimed' && item.claim && !item.claimExpired) return 'running';
30
+ // Sent to an agent and not yet picked up: it is about to run, nobody has to
31
+ // act on it, so it is not waiting on anyone.
32
+ const sentToAnAgent = (item.labels ?? []).includes('founder') && !item.result && !item.note
33
+ && item.kind !== 'question' && item.kind !== 'review' && !personHolds(item);
34
+ return sentToAnAgent ? 'running' : 'waiting';
35
+ }
36
+
37
+ const personHolds = (item) => typeof item.assignee === 'string' && item.assignee !== 'agent';
38
+
39
+ // ONLY EMPHASIS IS STRIPPED, AND ONLY WHERE IT IS A PAIR (w-a33b339772).
40
+ //
41
+ // This used to be `.replace(/[*_`>]/g, '')`: every star, underscore, backtick
42
+ // and angle bracket in the text, gone. A pasted search command
43
+ // (`--include=*.md --include=*.json`) lost all three of its stars on the
44
+ // summary line, and `user_id_v2` came back as `useridv2`.
45
+ //
46
+ // A pair is a mark, then a WORD character, then the closing mark: that is what
47
+ // `*bold*` looks like and what a glob never does, because a glob's star is
48
+ // followed by a dot or a slash. Underscores have to clear the word on either
49
+ // side too, or `user_id_v2` reads as emphasis around `id`. A mark with no
50
+ // partner is left exactly where the person put it.
51
+ const EMPHASIS = [
52
+ [/\*\*(\w[^*\n]*?)\*\*/g, '$1'],
53
+ [/(?<!\w)__(\w[^_\n]*?)__(?!\w)/g, '$1'],
54
+ [/\*(\w[^*\n]*?)\*/g, '$1'],
55
+ [/(?<!\w)_(\w[^_\n]*?)_(?!\w)/g, '$1'],
56
+ [/`([^`\n]+)`/g, '$1'],
57
+ ];
58
+
59
+ // WHERE A SENTENCE REALLY ENDS. The old cut was "any of .!? then a space", so
60
+ // the lone ` . ` that means "this directory" ended the sentence and the
61
+ // summary showed `grep -r foo .` and nothing else. A full stop ends a sentence
62
+ // when it closes a word AND what follows is a new sentence: whitespace then a
63
+ // capital (through an opening quote or bracket), or the end of the text.
64
+ const SENTENCE = /^(.+?\w[.!?]["'”’)\]]?)(?=\s+["“'(\[]?[A-Z]|\s*$)/;
65
+
66
+ // The first sentence of some prose, without markdown, short enough for a line.
67
+ export function firstSentence(text, max = 240) {
68
+ if (typeof text !== 'string') return '';
69
+ let plain = text
70
+ .replace(/^#+\s.*$/gm, '')
71
+ .replace(/^##\s*Options[\s\S]*$/m, '')
72
+ // A blockquote's mark is the one at the start of its line. Elsewhere `>`
73
+ // is a redirect or an arrow and belongs to whoever typed it.
74
+ .replace(/^\s*>\s?/gm, '');
75
+ for (const [re, to] of EMPHASIS) plain = plain.replace(re, to);
76
+ plain = plain.replace(/\s+/g, ' ').trim();
77
+ const m = plain.match(SENTENCE);
78
+ const one = (m ? m[1] : plain).trim();
79
+ return one.length > max ? `${one.slice(0, max - 1).trimEnd()}…` : one;
80
+ }
81
+
82
+ // HOW LONG A SUMMARY LINE AN AGENT WRITES MAY BE (w-b51b2e1c86): about four
83
+ // lines of the panel, so the name, the three lines and the properties fit one
84
+ // window without scrolling. Asked for on 2026-10-04 off a summary of 50, 63 and
85
+ // 95 words that ran the properties off the bottom; agents' lines then ran a
86
+ // median of 30 to 36 words. The store tool refuses a longer one
87
+ // (mcp/core/work.mjs); a person's own edits are not limited.
88
+ export const SUMMARY_WORDS = 25;
89
+
90
+ // THE SUMMARY IS CONTEXT, THEN DONE (w-54e9c7243f, 2026-10-05). `problem` is
91
+ // drawn as Context, a few sentences that bring the thread back to mind, and
92
+ // `progress` as Done, the steps taken so far, one per line, newest last.
93
+ // `solution` is no longer asked for or drawn. Measured on the approved
94
+ // pictures: about 35 words of context filled five lines and was enough to
95
+ // answer from; steps ran 4 to 15 words, and six left the properties on screen.
96
+ // The old field names stay so the team cloud needs no migration.
97
+ export const CONTEXT_WORDS = 45;
98
+ export const DONE_STEPS = 6;
99
+ export const DONE_STEP_WORDS = 15;
100
+
101
+ export function wordsIn(text) {
102
+ return typeof text === 'string' ? text.split(/\s+/).filter(Boolean).length : 0;
103
+ }
104
+
105
+ /** The Done steps in `progress`: one per line, the marks a writer puts in front of a step taken off. */
106
+ export function doneSteps(progress) {
107
+ if (typeof progress !== 'string') return [];
108
+ return progress.split('\n').map((l) => l.trim().replace(/^(?:[-*•✓✔]\s*)+/, '').trim()).filter(Boolean);
109
+ }
110
+
111
+ /** The thread's summary: what the agent or the person wrote, else stand-ins. */
112
+ export function summaryOf(item) {
113
+ const has = (f) => typeof item[f] === 'string';
114
+ // A finished turn's result is what was done, so it stands in for the
115
+ // Solution, and a checkpoint for the Progress. Read the other way round, a
116
+ // persona saw "Progress: Done: ..." over "Solution: Not written yet" and
117
+ // could not tell whether the thread was finished or stuck.
118
+ // A result that asks something is still progress: the thread waits on its
119
+ // owner's answer, and nothing is solved yet.
120
+ const said = firstSentence(item.result || '');
121
+ const asks = said.endsWith('?');
122
+ return {
123
+ problem: has('problem') ? item.problem : firstSentence(item.body || item.title),
124
+ progress: has('progress') ? item.progress : (asks ? said : firstSentence(item.result ? '' : item.note || '')),
125
+ solution: has('solution') ? item.solution : (asks ? '' : said),
126
+ written: has('problem') || has('progress') || has('solution'),
127
+ };
128
+ }
129
+
130
+ const startOfDay = (now) => { const d = new Date(now); d.setHours(0, 0, 0, 0); return d.getTime(); };
131
+
132
+ /**
133
+ * One card per thread worth showing the team: every open thread, and the ones
134
+ * finished today. Message threads between two people are never carded; they
135
+ * are nobody else's business and they are not work. Nor is a private thread.
136
+ */
137
+ /**
138
+ * NOTHING FROM BEFORE YOU JOINED IS SHARED UNLESS YOU SHARE IT (2026-10-01).
139
+ * Her own store holds hundreds of threads written for nobody but her, and the
140
+ * first sign-in would have put a summary of every one of them on the team's
141
+ * board. So a thread is shown when it was marked Team, or when it is not
142
+ * private and was started after `since`, the moment this person began sharing
143
+ * on this Mac. No `since` is the old rule: everything not private.
144
+ */
145
+ /**
146
+ * AND A THREAD MAY BE SHARED WITH CHOSEN PEOPLE RATHER THAN THE TEAM
147
+ * (w-41ff964775: a thread may be seen by a few people, not only by everyone
148
+ * or nobody). That is still sharing, so this stays the one rule for
149
+ * whether a card is published at all, and `shownToPeople` says who it reaches.
150
+ *
151
+ * Chosen people naming NOBODY reaches nobody, so nothing is published: a card
152
+ * carrying an empty list would otherwise read as the whole team's, and that is
153
+ * the one mistake nobody can take back.
154
+ */
155
+ // A THREAD WITH NO WORD OF ITS OWN FOLLOWS ITS PROJECT (w-b989839656). A
156
+ // thread from before you joined still stays yours whatever the project says.
157
+ export function shownToTeam(item, since = null, product = null) {
158
+ const own = ownVisibility(item);
159
+ if (own === 'team') return true;
160
+ if (own === 'people') return shownToPeople(item, product).length > 0;
161
+ if (own === 'private') return false;
162
+ if (since && (item?.createdAt ?? 0) < since) return false;
163
+ return projectSeenBy(product).who !== 'private';
164
+ }
165
+
166
+ const ownVisibility = (item) => {
167
+ const v = item?.visibility;
168
+ return v === 'team' || v === 'people' || v === 'private' ? v : undefined;
169
+ };
170
+ const cleanIds = (ids) => [...new Set((Array.isArray(ids) ? ids : []).filter((id) => typeof id === 'string' && id))];
171
+
172
+ /**
173
+ * WHO SEES A PROJECT'S THREADS (w-b989839656, approved 2026-10-05). Projects
174
+ * stay on each Mac; what a project decides is only whose Team page its
175
+ * threads' summaries reach. 'private' (Just you), 'team', or 'people' with
176
+ * the people named. A project from before this setting existed keeps the team,
177
+ * which is what it already did; a new one is made Just you (main/store.mjs);
178
+ * My Workspace is Just you whatever its file says. Chosen people naming nobody
179
+ * reaches nobody, so it reads as Just you and never as the team.
180
+ */
181
+ export function projectSeenBy(product) {
182
+ if (product?.personal === true || product?.seenBy === 'private') return { who: 'private', people: [] };
183
+ if (product?.seenBy === 'people') {
184
+ const people = cleanIds(product.seenByPeople);
185
+ return people.length ? { who: 'people', people } : { who: 'private', people: [] };
186
+ }
187
+ return { who: 'team', people: [] };
188
+ }
189
+
190
+ /**
191
+ * THE WORD A THREAD'S VISIBILITY READS AS, once its project has had its say:
192
+ * its own word if it has one, else its project's. Undefined where the project
193
+ * leaves it to the team, so the "before you joined" rule still applies.
194
+ * Failing closed on the project, rather than writing a word on each new
195
+ * thread, is what covers every path a thread arrives by at once: the
196
+ * composer, an agent filing a proposal, a repeat.
197
+ */
198
+ export function visibilityOf(item, product = null) {
199
+ const own = ownVisibility(item);
200
+ if (own) return own;
201
+ const { who } = projectSeenBy(product);
202
+ return who === 'team' ? undefined : who;
203
+ }
204
+
205
+ /** Whom a shared thread reaches, by id, or [] for the whole team. */
206
+ export function shownToPeople(item, product = null) {
207
+ const own = ownVisibility(item);
208
+ if (own === 'people') return cleanIds(item.visibleTo);
209
+ if (own) return [];
210
+ const seen = projectSeenBy(product);
211
+ return seen.who === 'people' ? seen.people : [];
212
+ }
213
+
214
+ export function cardsFor({ products, readItems, now = Date.now(), since = null }) {
215
+ const cards = [];
216
+ const titleOf = new Map();
217
+ const all = [];
218
+ for (const product of products) {
219
+ if (product.team?.direct) continue;
220
+ for (const item of readItems(product)) {
221
+ if (item.agent) continue;
222
+ // A LINK NAMES A THREAD ONLY IF THAT THREAD IS ITSELF VISIBLE. A public
223
+ // card that is blocked by a private thread said the private thread's
224
+ // title out loud (review, 2026-10-01); now it says only that one exists.
225
+ //
226
+ // A thread shared with chosen people is named by nothing either
227
+ // (w-41ff964775): every card is read by its own set of people, and a
228
+ // blocker shared with two of them would have told a third its title.
229
+ // Only a thread the whole team may read is safe in anybody's links.
230
+ const open = shownToTeam(item, since, product) && !shownToPeople(item, product).length;
231
+ titleOf.set(item.id, open ? item.label || item.title : null);
232
+ all.push({ item, product });
233
+ }
234
+ }
235
+ const linked = (ids) => (Array.isArray(ids) ? ids : []).map((id) => ({ id, title: titleOf.get(id) ?? null }));
236
+ const today = startOfDay(now);
237
+ for (const { item, product } of all) {
238
+ if (!shownToTeam(item, since, product)) continue;
239
+ const state = threadState(item, now);
240
+ if (state === 'done' && !(item.updatedAt >= today)) continue;
241
+ const s = summaryOf(item);
242
+ cards.push({
243
+ threadId: item.id, visible: true, title: item.label || item.title, project: product.name,
244
+ // WHO IT REACHES: the chosen people, or NO LIST AT ALL for the whole
245
+ // team. Never an empty list, which would have to mean both. Both clouds
246
+ // carry it and refuse to show the card to anybody else.
247
+ people: shownToPeople(item, product).length ? shownToPeople(item, product) : null,
248
+ state, priority: Number.isFinite(item.priority) ? item.priority : null,
249
+ problem: s.problem || null, progress: s.progress || null, solution: s.solution || null,
250
+ blockedBy: linked(item.blockedBy), blocks: linked(item.blocks), updatedAt: item.updatedAt ?? now,
251
+ });
252
+ }
253
+ return cards;
254
+ }
@@ -6,7 +6,7 @@
6
6
  export interface UsageLimit {
7
7
  span: 'session' | 'week';
8
8
  qualifier: string | null;
9
- /** What the panel calls it, in her words, stamped on by whichever reader
9
+ /** What the panel calls it, in plain words, stamped on by whichever reader
10
10
  * produced it. See renderer/src/types.ts for why it is not derived here. */
11
11
  name: string;
12
12
  percent: number;
package/shared/usage.mjs CHANGED
@@ -26,7 +26,7 @@
26
26
  // AND A LIMIT NOW CARRIES ITS OWN NAME. `limitName` used to derive one from
27
27
  // Claude Code's three spans and its qualifier, which is how "This week, Fable"
28
28
  // -- a Claude model -- could have appeared over a Codex reading. Each reader
29
- // names its own limits, in her words, and this file prints the name it is
29
+ // names its own limits, in plain words, and this file prints the name it is
30
30
  // given.
31
31
 
32
32
  import { DEFAULT_ENGINE } from './engines.mjs';
@@ -171,12 +171,11 @@ export function sessionLimit(limits) {
171
171
  * THE ONE LIMIT THE CORNER IS ABOUT, on an account that may not have a five hour
172
172
  * window at all.
173
173
  *
174
- * The bar used to be the session limit or nothing, and on 2026-09-18 that turned
175
- * out to mean NOTHING on the founder's own Codex plan: measured across the 1,140
176
- * rate-limit records in her twenty-five most recent Codex sessions, every window
177
- * she has is a seven day one and `secondary` is never populated. Her weekly limit
178
- * was at 100% used that day, which is exactly the moment a person needs the
179
- * corner, and the corner would have been blank.
174
+ * The bar used to be the session limit or nothing, and that turned out to mean
175
+ * NOTHING on some Codex plans: every window such an account has is a seven day
176
+ * one and `secondary` is never populated. A weekly limit at 100% used is
177
+ * exactly the moment a person needs the corner, and the corner would have been
178
+ * blank.
180
179
  *
181
180
  * The session window still wins whenever there is one, because it is the one that
182
181
  * stops her today. When there is not, the fullest window she does have is drawn