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.
- package/README.md +209 -55
- package/bin/agentbox.mjs +6 -0
- package/briefs/message-rules.md +13 -3
- package/briefs/worker.md +63 -49
- package/briefs/writing-rules.md +90 -131
- package/main/account-discovery.mjs +21 -20
- package/main/account-tooling.mjs +9 -10
- package/main/agent-activity.mjs +45 -5
- package/main/agent-schedule.mjs +14 -14
- package/main/agent-sessions.mjs +94 -10
- package/main/agent-updates.mjs +114 -0
- package/main/agents.mjs +52 -46
- package/main/analytics.mjs +27 -0
- package/main/approval-prompt-server.mjs +2 -2
- package/main/artifact-path.mjs +11 -0
- package/main/claude-bin.mjs +31 -5
- package/main/claude-input.mjs +45 -3
- package/main/claude-plan.mjs +11 -11
- package/main/code-change.mjs +3 -3
- package/main/codex-account.mjs +1 -1
- package/main/codex-bin.mjs +17 -0
- package/main/codex-memory-gate.mjs +260 -0
- package/main/codex-models.mjs +12 -7
- package/main/codex-threads.mjs +46 -23
- package/main/codex-usage-file.mjs +7 -7
- package/main/codex-usage.mjs +4 -4
- package/main/codex-watch.mjs +12 -12
- package/main/config.mjs +68 -8
- package/main/dev-window.mjs +9 -9
- package/main/doc-scheme.mjs +2 -0
- package/main/engine-setup.mjs +193 -0
- package/main/feedback.mjs +55 -0
- package/main/folder-pictures.mjs +48 -0
- package/main/frame-scrollbars.mjs +90 -0
- package/main/fresh-user.mjs +6 -3
- package/main/git-change-offthread.mjs +70 -0
- package/main/git-change-thread.mjs +11 -0
- package/main/git-change.mjs +1 -1
- package/main/img-scheme.mjs +70 -1
- package/main/instruction-settings.mjs +92 -54
- package/main/ipc.mjs +316 -42
- package/main/leftovers.mjs +518 -0
- package/main/live-replies.mjs +43 -5
- package/main/machine.mjs +47 -0
- package/main/main.mjs +119 -15
- package/main/memory-gate-history.mjs +254 -0
- package/main/memory-gate-server.mjs +461 -0
- package/main/memory-gate.mjs +206 -0
- package/main/message-priority.mjs +87 -0
- package/main/rail-note.mjs +2 -2
- package/main/refresh-agent-models.mjs +20 -0
- package/main/repeats.mjs +4 -4
- package/main/row-label.mjs +114 -51
- package/main/serve.mjs +67 -5
- package/main/settings.mjs +223 -26
- package/main/ship-queue.mjs +279 -0
- package/main/sign-in-files.mjs +39 -0
- package/main/source-updater.mjs +298 -0
- package/main/store/home.mjs +47 -2
- package/main/store/work-items.mjs +150 -6
- package/main/store.mjs +235 -9
- package/main/supervisor.mjs +1444 -406
- package/main/task-commands.mjs +8 -10
- package/main/task-folders.mjs +74 -13
- package/main/task-remote-control.mjs +3 -3
- package/main/task-terminals.mjs +2 -1
- package/main/team/index.mjs +440 -0
- package/main/team/memory-cloud.mjs +247 -0
- package/main/team/projects.mjs +162 -0
- package/main/team/session.mjs +225 -0
- package/main/team/sign-in.mjs +106 -0
- package/main/team/supabase-backend.mjs +231 -0
- package/main/team/sync.mjs +163 -0
- package/main/updater.mjs +15 -5
- package/main/write-before-reload.mjs +50 -0
- package/mcp/core/page.mjs +9 -11
- package/mcp/core/work.mjs +91 -25
- package/mcp/tools.mjs +12 -6
- package/package.json +21 -9
- package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
- package/renderer/dist/assets/index-B-2Az952.css +32 -0
- package/renderer/dist/assets/index-D6qncgai.js +1168 -0
- package/renderer/dist/index.html +2 -2
- package/scripts/memory-gate-hook.sh +59 -0
- package/shared/agent-import.mjs +38 -15
- package/shared/agents.d.mts +26 -8
- package/shared/agents.mjs +100 -108
- package/shared/analytics-events.mjs +19 -2
- package/shared/answers.mjs +12 -0
- package/shared/bridge-map.d.mts +12 -0
- package/shared/bridge-map.mjs +37 -1
- package/shared/claude-commands.generated.mjs +2 -2
- package/shared/claude-models.generated.mjs +4 -4
- package/shared/claude-usage.mjs +6 -6
- package/shared/codex-import.mjs +7 -7
- package/shared/codex-modes.d.mts +50 -0
- package/shared/codex-usage.mjs +7 -8
- package/shared/demo-world.mjs +5 -5
- package/shared/engines.mjs +27 -10
- package/shared/feedback.d.mts +14 -0
- package/shared/feedback.mjs +44 -0
- package/shared/first-run-examples.mjs +4 -4
- package/shared/first-run-practice.d.mts +16 -0
- package/shared/first-run-practice.mjs +176 -70
- package/shared/fresh-user-home.mjs +22 -3
- package/shared/instruction-defaults.json +2 -2
- package/shared/product-name.mjs +4 -2
- package/shared/project-folder-check.mjs +6 -0
- package/shared/rank.d.mts +1 -0
- package/shared/rank.mjs +22 -0
- package/shared/referenced-files.mjs +1 -1
- package/shared/schemes.d.mts +11 -0
- package/shared/schemes.mjs +1 -1
- package/shared/shortcuts.d.mts +41 -0
- package/shared/shortcuts.mjs +236 -0
- package/shared/spawn-trouble.d.mts +2 -0
- package/shared/spawn-trouble.mjs +39 -18
- package/shared/team-rules.d.mts +17 -0
- package/shared/team-rules.mjs +230 -0
- package/shared/team-status.d.mts +11 -0
- package/shared/team-status.mjs +90 -0
- package/shared/terminal-links.d.mts +2 -0
- package/shared/terminal-links.mjs +55 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +16 -0
- package/shared/thread-cards.d.mts +19 -0
- package/shared/thread-cards.mjs +254 -0
- package/shared/usage.d.mts +1 -1
- package/shared/usage.mjs +6 -7
- package/shared/work-items.mjs +255 -27
- package/shared/work-lines.mjs +393 -16
- package/worker-permissions.json +1 -1
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
- package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/emulator/audio-rules.mjs +0 -74
- package/renderer/dist/emulator/data/compression/README.md +0 -11
- package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
- package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
- package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +0 -22
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +0 -65
- package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
- package/renderer/dist/emulator/data/emulator.css +0 -1614
- package/renderer/dist/emulator/data/loader.js +0 -170
- package/renderer/dist/emulator/data/localization/README.md +0 -63
- package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
- package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
- package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
- package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
- package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
- package/renderer/dist/emulator/data/localization/en-US.json +0 -339
- package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
- package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
- package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
- package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
- package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
- package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
- package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
- package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
- package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
- package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
- package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
- package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
- package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
- package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
- package/renderer/dist/emulator/data/src/GameManager.js +0 -464
- package/renderer/dist/emulator/data/src/compression.js +0 -142
- package/renderer/dist/emulator/data/src/emulator.js +0 -6158
- package/renderer/dist/emulator/data/src/gamepad.js +0 -167
- package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
- package/renderer/dist/emulator/data/src/shaders.js +0 -236
- package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
- package/renderer/dist/emulator/data/src/storage.js +0 -125
- package/renderer/dist/emulator/data/version.json +0 -5
- package/renderer/dist/emulator/host.js +0 -299
- package/renderer/dist/emulator/index.html +0 -28
- 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
|
+
}
|
package/main/fresh-user.mjs
CHANGED
|
@@ -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
|
+
});
|
package/main/git-change.mjs
CHANGED
|
@@ -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
|
|
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
|
package/main/img-scheme.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|