agentbox-app 0.1.7 → 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-B7hhfHrT.js +0 -684
  147. package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
  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,55 @@
1
+ // SENDING FEEDBACK TO THE AGENTBOX TEAM (w-1b574413db, 2026-10-04).
2
+ //
3
+ // The card hands the main process what you wrote and what you attached, and
4
+ // this file posts it to one server function. That function holds who it goes
5
+ // to as a secret (`FEEDBACK_TO`, cloud/supabase/functions/feedback), so the
6
+ // address is in no file of this public repository and no install can show it.
7
+ //
8
+ // WHERE IT GOES IS BAKED IN, like the analytics key (main/analytics.mjs): the
9
+ // release writes `bakedFeedbackUrl` into the packaged package.json, a copy run
10
+ // from source carries none, and a `feedbackUrl` in zero.config.json outranks
11
+ // both. A copy with nowhere to send says so in words rather than pretending.
12
+ //
13
+ // It is not behind the diagnostics switch. That switch is about what Agentbox
14
+ // sends on its own; this is something a person typed and pressed Send on.
15
+ import { createRequire } from 'node:module';
16
+ import { checkFeedback } from '../shared/feedback.mjs';
17
+ import { Name, readEnv } from '../shared/product-name.mjs';
18
+
19
+ function bakedUrl() {
20
+ try {
21
+ return createRequire(import.meta.url)('../package.json')?.bakedFeedbackUrl ?? null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ const BAKED = bakedUrl();
28
+ const clean = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
29
+
30
+ export function feedbackUrl(config, baked = BAKED, env = process.env) {
31
+ return clean(config?.feedbackUrl) ?? clean(readEnv('FEEDBACK_URL', env)) ?? clean(baked);
32
+ }
33
+
34
+ // Answers { ok: true } or { ok: false, error } with the error as a sentence.
35
+ // It never throws, so the card always has something to say.
36
+ export async function sendFeedback({ url, payload, fetchImpl = globalThis.fetch, timeoutMs = 60_000 }) {
37
+ if (!url) return { ok: false, error: `Feedback is not set up in this copy of ${Name}.` };
38
+ // Checked again here, off the bytes that would actually travel, since the
39
+ // renderer is not the last word on what leaves this machine.
40
+ const sized = (payload?.files ?? []).map((f) => ({ size: Math.floor((String(f?.data ?? '').length * 3) / 4) }));
41
+ const check = checkFeedback({ text: payload?.text, files: sized });
42
+ if (!check.ok) return { ok: false, error: check.reason };
43
+ try {
44
+ const res = await fetchImpl(url, {
45
+ method: 'POST',
46
+ headers: { 'content-type': 'application/json' },
47
+ body: JSON.stringify(payload),
48
+ signal: AbortSignal.timeout(timeoutMs),
49
+ });
50
+ if (res?.ok) return { ok: true };
51
+ return { ok: false, error: 'It did not send. Try again in a moment.' };
52
+ } catch {
53
+ return { ok: false, error: 'It did not send. Check your connection and try again.' };
54
+ }
55
+ }
@@ -0,0 +1,48 @@
1
+ // THE PICTURES IN A FOLDER A MESSAGE NAMES, newest first.
2
+ //
3
+ // A worker that drew several versions names their folder on a line of its own,
4
+ // and until 2026-10-04 that line was grey text. Focus now draws the pictures
5
+ // under it (FolderPreview), and this is the one thing the window cannot do for
6
+ // itself: look inside a folder.
7
+ //
8
+ // Two rules, the same two the picture scheme enforces (./img-scheme.mjs), so a
9
+ // folder line is never a way to list anything else: the folder is inside this
10
+ // product's own folders or the account root, and only pictures come back. One
11
+ // level only, because a design folder keeps old rounds in folders of their own.
12
+ // tests/a-named-folder-shows-its-pictures.test.mjs holds every edge.
13
+ import fsDefault from 'node:fs';
14
+ import path from 'node:path';
15
+ import { resolveArtifact } from './artifact-path.mjs';
16
+ import { servable } from './img-scheme.mjs';
17
+
18
+ const PICTURE = /\.(png|jpe?g|gif|webp|avif)$/i;
19
+
20
+ export function folderPictures({ src, product, products = [], accountRoot, storeRoot, limit = 60, fs = fsDefault }) {
21
+ if (typeof src !== 'string' || !src.trim()) return { ok: false, error: 'No folder was named.' };
22
+ const named = src.replace(/\/+$/, '');
23
+ const found = resolveArtifact({ src: named, product, products, accountRoot, storeRoot, fs });
24
+ if (!found.ok) return { ok: false, error: found.error };
25
+ const own = products.find((p) => p.slug === product) ?? null;
26
+ const roots = [accountRoot, own?.dir, own?.repoPath].filter(Boolean);
27
+ let entries;
28
+ try {
29
+ if (!fs.statSync(found.path).isDirectory()) return { ok: false, error: `${src} is not a folder.` };
30
+ entries = fs.readdirSync(found.path, { withFileTypes: true });
31
+ } catch (e) {
32
+ return { ok: false, error: `${src} could not be read (${e.message}).` };
33
+ }
34
+ const pictures = [];
35
+ for (const entry of entries) {
36
+ if (!entry.isFile() || !PICTURE.test(entry.name)) continue;
37
+ const file = path.join(found.path, entry.name);
38
+ if (!servable(file, roots)) continue;
39
+ let mtime = 0;
40
+ try { mtime = fs.statSync(file).mtimeMs; } catch { continue; }
41
+ pictures.push({ file, mtime });
42
+ }
43
+ if (!pictures.length && !roots.some((r) => found.path === r || found.path.startsWith(r + path.sep))) {
44
+ return { ok: false, error: `${src} is not in this project.` };
45
+ }
46
+ pictures.sort((a, b) => b.mtime - a.mtime || a.file.localeCompare(b.file));
47
+ return { ok: true, folder: found.path, pictures: pictures.slice(0, limit).map((p) => p.file), total: pictures.length };
48
+ }
@@ -0,0 +1,90 @@
1
+ // A PAGE IN THE PANE KEEPS ITS SCROLLBAR OUT OF SIGHT UNTIL YOU SCROLL.
2
+ //
3
+ // The app's own scrollbars are invisible at rest (styles.css, "SCROLLBARS ARE
4
+ // NOT FURNITURE"), but a page drawn in the document pane is its own document
5
+ // and that stylesheet stops at the frame. With a mouse plugged in, macOS draws
6
+ // classic scrollbars, and a page that does not declare itself dark gets the
7
+ // light ones: a white track the full height of the pane with a grey thumb in
8
+ // it. That was the report, with a screenshot, on 2026-10-05.
9
+ //
10
+ // THE FILE IS STILL SERVED EXACTLY AS IT WAS WRITTEN (doc-scheme.mjs). What is
11
+ // added here is the app's own chrome, from outside, once the page's document
12
+ // exists: one adopted stylesheet that touches nothing but scrollbars, inside a
13
+ // cascade layer so a page that styled its own scrollbars keeps them, and two
14
+ // passive listeners that only ever swap that sheet. Nothing a page does is
15
+ // changed or stopped, and no key or gesture of hers is listened to.
16
+ //
17
+ // A frame whose sandbox has no allow-scripts cannot be handed this (measured:
18
+ // "Script not run"). The one such frame is the small preview under a message,
19
+ // and that one is told not to scroll at all (ArtifactThumbnail.tsx).
20
+ import { DOC_SCHEMES, isScheme } from '../shared/schemes.mjs';
21
+
22
+ const RULES = (thumb) => `@layer agentbox-scrollbars {
23
+ ::-webkit-scrollbar { width: 10px; height: 10px; }
24
+ ::-webkit-scrollbar-track { background: transparent; }
25
+ ::-webkit-scrollbar-corner { background: transparent; }
26
+ ::-webkit-scrollbar-thumb { background: ${thumb}; border-radius: 10px; border: 3px solid transparent; background-clip: content-box; }
27
+ ::-webkit-scrollbar-thumb:hover { background: rgba(150, 150, 150, 0.7); background-clip: content-box; }
28
+ }`;
29
+
30
+ /** No track and no thumb: what a page looks like while nobody is scrolling it. */
31
+ export const AT_REST = RULES('transparent');
32
+ /** A thin soft thumb that reads on a dark page and a light one alike. */
33
+ export const SHOWN = RULES('rgba(140, 140, 140, 0.45)');
34
+
35
+ /** Is this a page of ours or a file, rather than a running app or the web. */
36
+ export function wantsQuietScrollbars(url) {
37
+ let parsed;
38
+ try { parsed = new URL(String(url ?? '')); } catch { return false; }
39
+ return parsed.protocol === 'file:' || isScheme(DOC_SCHEMES, parsed.protocol);
40
+ }
41
+
42
+ /**
43
+ * Runs INSIDE the page, so it is written to be serialised: everything it uses
44
+ * comes in through its arguments. Shown while the page or anything in it
45
+ * scrolls and for a moment after, and while the pointer is at the right edge
46
+ * where the thumb would be grabbed.
47
+ */
48
+ export function quietScrollbars(page, atRest, shown) {
49
+ const doc = page.document;
50
+ if (doc.adoptedStyleSheets.some((s) => s.agentboxScrollbars)) return;
51
+ const sheet = new page.CSSStyleSheet();
52
+ sheet.agentboxScrollbars = true;
53
+ sheet.replaceSync(atRest);
54
+ doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, sheet];
55
+ let showing = false;
56
+ let atEdge = false;
57
+ let timer = null;
58
+ const show = () => { if (!showing) { sheet.replaceSync(shown); showing = true; } };
59
+ const hideSoon = () => {
60
+ page.clearTimeout(timer);
61
+ timer = page.setTimeout(() => { timer = null; if (!atEdge) { sheet.replaceSync(atRest); showing = false; } }, 900);
62
+ };
63
+ page.addEventListener('scroll', () => { show(); if (!atEdge) hideSoon(); }, { capture: true, passive: true });
64
+ page.addEventListener('mousemove', (e) => {
65
+ const edge = e.clientX >= doc.documentElement.clientWidth - 14;
66
+ if (edge === atEdge) return;
67
+ atEdge = edge;
68
+ if (edge) { page.clearTimeout(timer); timer = null; show(); } else hideSoon();
69
+ }, { passive: true });
70
+ page.addEventListener('mouseout', (e) => {
71
+ if (e.relatedTarget || !atEdge) return;
72
+ atEdge = false;
73
+ hideSoon();
74
+ }, { passive: true });
75
+ }
76
+
77
+ const SCRIPT = `(${quietScrollbars.toString()})(window, ${JSON.stringify(AT_REST)}, ${JSON.stringify(SHOWN)})`;
78
+
79
+ function quiet(frame) {
80
+ if (!frame || !wantsQuietScrollbars(frame.url)) return;
81
+ frame.executeJavaScript(SCRIPT).catch(() => { /* a sandbox without scripts, or the frame went away */ });
82
+ }
83
+
84
+ /** Every page frame in this window gets the quiet scrollbar as its document is ready. */
85
+ export function quietTheFramesScrollbars(contents) {
86
+ contents.on('frame-created', (_event, { frame }) => {
87
+ if (!frame || frame === contents.mainFrame) return;
88
+ frame.on('dom-ready', () => quiet(frame));
89
+ });
90
+ }
@@ -57,6 +57,9 @@ export function relaunchCommand({ execPath = process.execPath, argv = process.ar
57
57
  * It cannot break inbox zero: the walk shows only its own three examples. */
58
58
  export function openFreshUser({
59
59
  withAgents = true,
60
+ // False is a new user with no Claude Code or Codex of hers, who signs in
61
+ // for itself: the way to see the walk's plan question (w-9f6975906c).
62
+ withTools = true,
60
63
  execPath = process.execPath,
61
64
  argv = process.argv,
62
65
  packaged = true,
@@ -65,7 +68,7 @@ export function openFreshUser({
65
68
  } = {}) {
66
69
  let prepared;
67
70
  try {
68
- prepared = prepareHome(makeHome(now), { withAgents });
71
+ prepared = prepareHome(makeHome(now), { withAgents: withTools && withAgents, withTools });
69
72
  } catch (err) {
70
73
  return { ok: false, error: `Could not make a throwaway home under ${HOMES}: ${err?.message ?? err}` };
71
74
  }
@@ -80,7 +83,7 @@ export function openFreshUser({
80
83
  child = spawnFn(bin, args, {
81
84
  detached: true,
82
85
  stdio: 'ignore',
83
- env: freshEnv(prepared.home),
86
+ env: freshEnv(prepared.home, process.env, { ownSignIns: !withTools }),
84
87
  });
85
88
  child.unref?.();
86
89
  } catch (err) {
@@ -91,7 +94,7 @@ export function openFreshUser({
91
94
  // be missing without looking broken, and a test that quietly cannot run an
92
95
  // agent is a test that lies. They are said rather than assumed.
93
96
  const notes = [];
94
- if (!prepared.claudeBin) notes.push('Claude Code is not at ~/.local/bin/claude, so agents will not run in it.');
97
+ if (withTools && !prepared.claudeBin) notes.push('Claude Code is not at ~/.local/bin/claude, so agents will not run in it.');
95
98
  if (!prepared.keychain) notes.push('There is no ~/Library/Keychains here, so Claude Code will say it is not logged in.');
96
99
  if (withAgents && !prepared.agents) notes.push('There is no ~/.claude on this Mac, so it will have no agents to offer.');
97
100
 
@@ -0,0 +1,70 @@
1
+ // THE CHECKOUT IS PHOTOGRAPHED OFF THE THREAD THAT DRAWS HER WINDOW.
2
+ //
3
+ // Measured 2026-10-01 on a test copy: one snapshot of the app took 2.56 seconds
4
+ // while an agent was starting, against a few milliseconds either side. The
5
+ // spawn ran `snapshotRepo` inline, which is four git commands and a read of
6
+ // every untracked file, and on a busy Mac each git command costs half a second.
7
+ // That is the "long delay" after pressing Send. The same shape as
8
+ // main/task-folders-offthread.mjs: main/git-change.mjs stays synchronous and
9
+ // runs whole inside a worker thread, and if the thread cannot start the job
10
+ // runs here, the old way.
11
+ import { Worker } from 'node:worker_threads';
12
+ import * as change from './git-change.mjs';
13
+
14
+ const JOBS = new Set(['snapshotRepo']);
15
+
16
+ let worker = null;
17
+ let broken = false;
18
+ let seq = 0;
19
+ const waiting = new Map();
20
+
21
+ function runHere({ name, args, resolve, reject }) {
22
+ try { resolve(change[name](...args)); } catch (error) { reject(error); }
23
+ }
24
+
25
+ function giveUp() {
26
+ broken = true;
27
+ worker = null;
28
+ const left = [...waiting.values()];
29
+ waiting.clear();
30
+ for (const job of left) runHere(job);
31
+ }
32
+
33
+ function thread() {
34
+ if (worker || broken) return worker;
35
+ try {
36
+ worker = new Worker(new URL('./git-change-thread.mjs', import.meta.url));
37
+ } catch (error) {
38
+ console.warn('zero: the checkout will be read on the main thread:', error.message);
39
+ broken = true;
40
+ return null;
41
+ }
42
+ worker.on('message', ({ id, ok, value, error }) => {
43
+ const job = waiting.get(id);
44
+ if (!job) return;
45
+ waiting.delete(id);
46
+ if (!waiting.size) worker?.unref();
47
+ if (ok) job.resolve(value); else job.reject(new Error(error));
48
+ });
49
+ worker.on('error', (error) => {
50
+ console.warn('zero: the checkout thread failed:', error.message);
51
+ giveUp();
52
+ });
53
+ worker.on('exit', () => { if (worker) giveUp(); });
54
+ worker.unref();
55
+ return worker;
56
+ }
57
+
58
+ /** Run one of git-change.mjs's exports without blocking this thread. */
59
+ export function gitJob(name, ...args) {
60
+ if (!JOBS.has(name)) return Promise.reject(new Error(`not a git job: ${name}`));
61
+ return new Promise((resolve, reject) => {
62
+ const job = { name, args, resolve, reject };
63
+ const w = thread();
64
+ if (!w) { runHere(job); return; }
65
+ const id = ++seq;
66
+ waiting.set(id, job);
67
+ w.ref();
68
+ w.postMessage({ id, name, args });
69
+ });
70
+ }
@@ -0,0 +1,11 @@
1
+ // THE OTHER END OF main/git-change-offthread.mjs. One job at a time, in order.
2
+ import { parentPort } from 'node:worker_threads';
3
+ import * as change from './git-change.mjs';
4
+
5
+ parentPort.on('message', ({ id, name, args }) => {
6
+ try {
7
+ parentPort.postMessage({ id, ok: true, value: change[name](...args) });
8
+ } catch (error) {
9
+ parentPort.postMessage({ id, ok: false, error: String(error?.message ?? error) });
10
+ }
11
+ });
@@ -5,7 +5,7 @@
5
5
  // the Edit or Write tool. Measured on this Mac 2026-08-24: of 105 runs that
6
6
  // changed a file over two days, 4 would have shown the whole change. The other
7
7
  // 101 rewrote their files with sed, a heredoc or a script, and a card drawn
8
- // from the conversation has nothing in it. That is what David was clicking.
8
+ // from the conversation has nothing in it. That is what a tester was clicking.
9
9
  //
10
10
  // SO THE CARD STOPS ASKING HOW THE AGENT TYPED. A run's checkout is
11
11
  // photographed when the session spawns and read again when it exits, and the
@@ -36,7 +36,9 @@
36
36
  // product's docs, its attachments, its designs and the `.staging` folder a
37
37
  // paste lands in) or a product's own code repo, which is where a worker
38
38
  // writes `shots/…` and then names it on a card.
39
+ import fs from 'node:fs';
39
40
  import path from 'node:path';
41
+ import { Readable } from 'node:stream';
40
42
  import { IMG_SCHEME, IMG_SCHEMES, isScheme } from '../shared/schemes.mjs';
41
43
 
42
44
  // The name is read rather than typed, and every spelling this app has used is
@@ -51,6 +53,12 @@ export const IMG_HOST = 'file';
51
53
 
52
54
  const PICTURES = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.bmp', '.ico']);
53
55
 
56
+ // AND A SOUND OR A FILM, SINCE 2026-09-29, so a voice sample or a render a
57
+ // worker names plays in the message (ArtifactMedia in Focus.tsx) instead of
58
+ // sitting there as a grey path. Same two rules, same roots: this only widens
59
+ // what counts as something to look at, to include something to listen to.
60
+ const MEDIA = new Set(['.mp3', '.wav', '.m4a', '.aac', '.ogg', '.mp4', '.mov', '.m4v', '.webm']);
61
+
54
62
  /** The url the renderer draws, for a picture already resolved on disk. */
55
63
  export function imgUrl(file) {
56
64
  if (typeof file !== 'string' || !path.isAbsolute(file)) return null;
@@ -82,9 +90,70 @@ function inside(dir, file) {
82
90
  return f === d || f.startsWith(d + path.sep);
83
91
  }
84
92
 
93
+ const MEDIA_TYPES = {
94
+ '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.m4a': 'audio/mp4', '.aac': 'audio/aac', '.ogg': 'audio/ogg',
95
+ '.mp4': 'video/mp4', '.mov': 'video/quicktime', '.m4v': 'video/mp4', '.webm': 'video/webm',
96
+ };
97
+
98
+ /** The content type of a sound or a film, or null when `file` is neither. */
99
+ export function mediaType(file) {
100
+ return MEDIA_TYPES[path.extname(String(file ?? '')).toLowerCase()] ?? null;
101
+ }
102
+
103
+ // A PLAYER ASKS FOR A SOUND IN PIECES, AND net.fetch IGNORES THE ASK.
104
+ // Measured in a hidden Electron window on 2026-09-29 against one of her voice
105
+ // samples, served on this scheme through net.fetch: it loaded and reported its
106
+ // 39.36 seconds, and could not be moved. `seekable` was [0, 0] and a jump to
107
+ // the middle stayed at 0, with or without the Range header passed through,
108
+ // because a file:// fetch answers every range with the whole file and a 200.
109
+ // So a sound or a film is answered here, in the byte range the player asked
110
+ // for, with a 206.
111
+ /**
112
+ * The bytes a Range header asks for, as { start, end } inclusive, or null for
113
+ * the whole file, or false when the range cannot be served. Only one range.
114
+ */
115
+ export function byteRange(header, size) {
116
+ if (!header) return null;
117
+ const m = /^bytes=(\d*)-(\d*)$/.exec(String(header).trim());
118
+ if (!m || (m[1] === '' && m[2] === '')) return null;
119
+ let start;
120
+ let end;
121
+ if (m[1] === '') {
122
+ // The last N bytes.
123
+ start = Math.max(0, size - Number(m[2]));
124
+ end = size - 1;
125
+ } else {
126
+ start = Number(m[1]);
127
+ end = m[2] === '' ? size - 1 : Math.min(Number(m[2]), size - 1);
128
+ }
129
+ if (start >= size || start > end) return false;
130
+ return { start, end };
131
+ }
132
+
133
+ /**
134
+ * A sound or a film off disk, answered in the range the player asked for.
135
+ * The caller has already decided the file may be served (`servable`).
136
+ */
137
+ export async function mediaResponse(file, rangeHeader) {
138
+ const { size } = await fs.promises.stat(file);
139
+ const range = byteRange(rangeHeader, size);
140
+ const headers = new Headers({ 'content-type': mediaType(file) ?? 'application/octet-stream', 'accept-ranges': 'bytes' });
141
+ if (range === false) {
142
+ headers.set('content-range', `bytes */${size}`);
143
+ return new Response(null, { status: 416, headers });
144
+ }
145
+ if (size === 0) return new Response(null, { status: 200, headers });
146
+ const { start, end } = range ?? { start: 0, end: size - 1 };
147
+ headers.set('content-length', String(end - start + 1));
148
+ if (range) headers.set('content-range', `bytes ${start}-${end}/${size}`);
149
+ const body = Readable.toWeb(fs.createReadStream(file, { start, end }));
150
+ return new Response(body, { status: range ? 206 : 200, headers });
151
+ }
152
+
85
153
  /** Is this a picture the window is allowed to draw? Pure, so it is testable. */
86
154
  export function servable(file, roots) {
87
155
  if (typeof file !== 'string' || !path.isAbsolute(file)) return false;
88
- if (!PICTURES.has(path.extname(file).toLowerCase())) return false;
156
+ const ext = path.extname(file).toLowerCase();
157
+ if (!PICTURES.has(ext) && !MEDIA.has(ext)) return false;
89
158
  return (roots ?? []).some((root) => inside(root, file));
90
159
  }