agentbox-app 0.1.6
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/LICENSE +674 -0
- package/README.md +176 -0
- package/bin/agentbox.mjs +103 -0
- package/briefs/adhd-mode.md +5 -0
- package/briefs/message-rules.md +95 -0
- package/briefs/worker.md +230 -0
- package/briefs/writing-rules.md +336 -0
- package/main/account-discovery.mjs +145 -0
- package/main/account-tooling.mjs +261 -0
- package/main/agent-activity.mjs +56 -0
- package/main/agent-files.mjs +422 -0
- package/main/agent-schedule.mjs +254 -0
- package/main/agent-sessions.mjs +435 -0
- package/main/agents.mjs +1095 -0
- package/main/analytics.mjs +206 -0
- package/main/answer-modes.mjs +206 -0
- package/main/approval-prompt-server.mjs +160 -0
- package/main/approvals.mjs +487 -0
- package/main/artifact-path.mjs +144 -0
- package/main/claude-bin.mjs +311 -0
- package/main/claude-input.mjs +117 -0
- package/main/claude-models.mjs +244 -0
- package/main/claude-plan.mjs +148 -0
- package/main/claude-usage.mjs +149 -0
- package/main/code-change.mjs +383 -0
- package/main/code-file.mjs +116 -0
- package/main/codex-account.mjs +146 -0
- package/main/codex-app-server.mjs +796 -0
- package/main/codex-approvals.mjs +712 -0
- package/main/codex-bin.mjs +261 -0
- package/main/codex-compaction.mjs +49 -0
- package/main/codex-models.mjs +194 -0
- package/main/codex-session.mjs +797 -0
- package/main/codex-threads.mjs +268 -0
- package/main/codex-usage-file.mjs +192 -0
- package/main/codex-usage.mjs +149 -0
- package/main/codex-watch.mjs +35 -0
- package/main/codex.mjs +757 -0
- package/main/config.mjs +345 -0
- package/main/copy-selection.mjs +55 -0
- package/main/crash-report.mjs +158 -0
- package/main/demo.mjs +254 -0
- package/main/dev-window.mjs +103 -0
- package/main/doc-file.mjs +99 -0
- package/main/doc-scheme.mjs +126 -0
- package/main/first-run.mjs +190 -0
- package/main/folders.mjs +91 -0
- package/main/fresh-user.mjs +120 -0
- package/main/git-change.mjs +323 -0
- package/main/img-scheme.mjs +90 -0
- package/main/instruction-settings.mjs +205 -0
- package/main/ipc.mjs +1558 -0
- package/main/live-replies.mjs +77 -0
- package/main/machine.mjs +113 -0
- package/main/main.mjs +972 -0
- package/main/menu.mjs +177 -0
- package/main/notify.mjs +155 -0
- package/main/open-source-file.mjs +8 -0
- package/main/project-identity.mjs +149 -0
- package/main/provider-commands.mjs +32 -0
- package/main/rail-note.mjs +74 -0
- package/main/repeats.mjs +261 -0
- package/main/row-label.mjs +185 -0
- package/main/screen-detail.mjs +44 -0
- package/main/serve.mjs +262 -0
- package/main/settings.mjs +912 -0
- package/main/staleness.mjs +76 -0
- package/main/store/chats.mjs +188 -0
- package/main/store/home.mjs +383 -0
- package/main/store/migrate.mjs +40 -0
- package/main/store/paths.mjs +82 -0
- package/main/store/pending-writes.mjs +122 -0
- package/main/store/project-lock.mjs +200 -0
- package/main/store/project.mjs +523 -0
- package/main/store/queues.mjs +31 -0
- package/main/store/work-items.mjs +299 -0
- package/main/store/write-queue.mjs +14 -0
- package/main/store-modules.mjs +23 -0
- package/main/store.mjs +1269 -0
- package/main/supervisor.mjs +6875 -0
- package/main/task-commands.mjs +161 -0
- package/main/task-folders-offthread.mjs +84 -0
- package/main/task-folders-thread.mjs +13 -0
- package/main/task-folders.mjs +409 -0
- package/main/task-remote-control.mjs +65 -0
- package/main/task-terminals.mjs +169 -0
- package/main/terminal-access.mjs +6 -0
- package/main/updater.mjs +240 -0
- package/main/zoom-keys.mjs +54 -0
- package/main/zoom-readout.mjs +20 -0
- package/mcp/agentbox-mcp.sh +34 -0
- package/mcp/core/account.mjs +77 -0
- package/mcp/core/chat.mjs +61 -0
- package/mcp/core/documents.mjs +238 -0
- package/mcp/core/page.mjs +152 -0
- package/mcp/core/products.mjs +110 -0
- package/mcp/core/skills.mjs +126 -0
- package/mcp/core/work.mjs +377 -0
- package/mcp/stdio.mjs +70 -0
- package/mcp/tools.mjs +257 -0
- package/package.json +186 -0
- package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
- package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
- package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
- package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
- 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/gouache-valley-ChSu14-t.webp +0 -0
- package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
- package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.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-CwiDv5bL.css +32 -0
- package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
- 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/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +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/assets/xterm-BT7tBf8A.js +27 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- 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 +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +299 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
- package/renderer/dist/index.html +13 -0
- package/scripts/after-install.mjs +59 -0
- package/scripts/approval-server.sh +43 -0
- package/scripts/before-publish.mjs +133 -0
- package/shared/agent-import.d.mts +13 -0
- package/shared/agent-import.mjs +252 -0
- package/shared/agents.d.mts +190 -0
- package/shared/agents.mjs +937 -0
- package/shared/analytics-events.mjs +63 -0
- package/shared/answers.d.mts +12 -0
- package/shared/answers.mjs +80 -0
- package/shared/artifact-keys.d.mts +28 -0
- package/shared/artifact-keys.mjs +140 -0
- package/shared/bridge-map.mjs +128 -0
- package/shared/claude-commands.d.mts +20 -0
- package/shared/claude-commands.generated.mjs +149 -0
- package/shared/claude-commands.mjs +131 -0
- package/shared/claude-models.generated.d.mts +15 -0
- package/shared/claude-models.generated.mjs +26 -0
- package/shared/claude-usage.d.mts +16 -0
- package/shared/claude-usage.mjs +172 -0
- package/shared/codex-commands.d.mts +4 -0
- package/shared/codex-commands.mjs +17 -0
- package/shared/codex-import.d.mts +38 -0
- package/shared/codex-import.mjs +232 -0
- package/shared/codex-modes.mjs +159 -0
- package/shared/codex-usage.mjs +180 -0
- package/shared/contracts.mjs +412 -0
- package/shared/crash-scrub.mjs +182 -0
- package/shared/dashboard.mjs +481 -0
- package/shared/dead-run-trace.mjs +127 -0
- package/shared/demo-world.mjs +415 -0
- package/shared/effort-levels.d.mts +15 -0
- package/shared/effort-levels.mjs +76 -0
- package/shared/engines.d.mts +62 -0
- package/shared/engines.mjs +389 -0
- package/shared/first-run-examples.mjs +60 -0
- package/shared/first-run-line.mjs +149 -0
- package/shared/first-run-practice.d.mts +32 -0
- package/shared/first-run-practice.mjs +254 -0
- package/shared/first-run-shape.mjs +100 -0
- package/shared/fresh-user-home.mjs +189 -0
- package/shared/hook-failure.mjs +22 -0
- package/shared/install-id.mjs +39 -0
- package/shared/instruction-defaults.json +5 -0
- package/shared/library-folder.mjs +28 -0
- package/shared/notify-rules.mjs +120 -0
- package/shared/product-name.d.mts +6 -0
- package/shared/product-name.mjs +144 -0
- package/shared/project-folder-check.d.mts +22 -0
- package/shared/project-folder-check.mjs +152 -0
- package/shared/provider-commands.d.mts +8 -0
- package/shared/provider-commands.mjs +58 -0
- package/shared/rank.d.mts +19 -0
- package/shared/rank.mjs +146 -0
- package/shared/recovery.mjs +26 -0
- package/shared/referenced-files.d.mts +11 -0
- package/shared/referenced-files.mjs +122 -0
- package/shared/repeats.d.mts +45 -0
- package/shared/repeats.mjs +193 -0
- package/shared/schemes.mjs +33 -0
- package/shared/settings-terminal.d.mts +1 -0
- package/shared/settings-terminal.mjs +14 -0
- package/shared/side-build.mjs +57 -0
- package/shared/spawn-trouble.d.mts +8 -0
- package/shared/spawn-trouble.mjs +599 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +58 -0
- package/shared/usage.d.mts +53 -0
- package/shared/usage.mjs +319 -0
- package/shared/work-items.mjs +561 -0
- package/shared/work-lines.mjs +360 -0
- package/worker-permissions.json +6 -0
package/main/menu.mjs
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// The application menu. The app is keyboard-first, so the menu's job is mostly
|
|
2
|
+
// to make the OS-level chords real: zoom, copy/paste, window management.
|
|
3
|
+
// Zoom targets the content the keystroke arrived on, which matters because an
|
|
4
|
+
// open document is a webview with its own zoom.
|
|
5
|
+
|
|
6
|
+
import { app, BrowserWindow, Menu, webContents } from 'electron';
|
|
7
|
+
import { zoomTarget } from './zoom-keys.mjs';
|
|
8
|
+
import { zoomPercentFor } from './zoom-readout.mjs';
|
|
9
|
+
|
|
10
|
+
// Approval hotkeys live in the MENU, not the renderer: a child web contents
|
|
11
|
+
// owns the keyboard whenever one is up, so a renderer listener never hears
|
|
12
|
+
// anything while she is inside one. Menu accelerators fire regardless of what
|
|
13
|
+
// has focus.
|
|
14
|
+
|
|
15
|
+
// TWO ROUTES TO ONE STEP. ⌘=, ⌘- and ⌘0 all did nothing. The menu accelerator
|
|
16
|
+
// was the only route, and this file already knows what that is worth: the note
|
|
17
|
+
// below ⌘R records that accelerators here "do not fire while a child web
|
|
18
|
+
// contents owns the keyboard", which is why ⌘R, ⌘Y and ⌘N were all moved into
|
|
19
|
+
// before-input-event in the main process. Zoom was left behind, on a chord she
|
|
20
|
+
// cannot work around, because Chromium persists the zoom level per page URL
|
|
21
|
+
// and hands it back after every reload.
|
|
22
|
+
//
|
|
23
|
+
// So the chords are now handled in main.mjs as well, and this stays for the
|
|
24
|
+
// menu's label and for a click. On macOS the two are exclusive (AppKit eats a
|
|
25
|
+
// key equivalent before the page can see it); the guard below is what keeps
|
|
26
|
+
// that an assumption rather than a load-bearing one, exactly like the reload
|
|
27
|
+
// debounce in main.mjs.
|
|
28
|
+
let lastZoomAt = 0;
|
|
29
|
+
let lastZoomDelta = null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Move the zoom one step. `target` is the web contents the caller named: the
|
|
33
|
+
* one a keystroke arrived on, or the window a menu click was chosen from. When
|
|
34
|
+
* nobody names one it falls back to whatever is focused, and then to the app's
|
|
35
|
+
* own window. That last fallback is the fix for her second report: focus can be
|
|
36
|
+
* nowhere, and a target of null was a silent no-op.
|
|
37
|
+
*/
|
|
38
|
+
export function applyZoom(delta, target = null) {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
if (delta === lastZoomDelta && now - lastZoomAt < 40) return;
|
|
41
|
+
lastZoomAt = now;
|
|
42
|
+
lastZoomDelta = delta;
|
|
43
|
+
const contents = zoomTarget({
|
|
44
|
+
named: target,
|
|
45
|
+
focused: webContents.getFocusedWebContents(),
|
|
46
|
+
appWindow: appWindowContents(),
|
|
47
|
+
});
|
|
48
|
+
if (!contents) return;
|
|
49
|
+
if (delta === 0) contents.setZoomLevel(0);
|
|
50
|
+
else contents.setZoomLevel(Math.max(-6, Math.min(6, contents.getZoomLevel() + delta)));
|
|
51
|
+
announceZoom(contents.getZoomLevel());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The number is worked out here, where the level actually changes, and the
|
|
55
|
+
// page only draws what it is handed.
|
|
56
|
+
//
|
|
57
|
+
// It always goes to the APP window, even when the level that moved belongs to
|
|
58
|
+
// the junk browser's guest: the guest is a page inside her window, and a
|
|
59
|
+
// browser puts its own readout in its own corner rather than in the page.
|
|
60
|
+
function announceZoom(level) {
|
|
61
|
+
const percent = zoomPercentFor(level);
|
|
62
|
+
if (percent === null) return;
|
|
63
|
+
const target = appWindowContents();
|
|
64
|
+
if (!target || target.isDestroyed?.()) return;
|
|
65
|
+
target.send?.('zero:zoom-percent', { percent });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ⌘F, ⌘G AND ⇧⌘G. The app finds its own words (renderer/src/find-in-page.ts
|
|
69
|
+
// says why it is not Chromium's findInPage), so all this does is tell the page
|
|
70
|
+
// which one was pressed: 0 opens the box, 1 and -1 walk the matches.
|
|
71
|
+
//
|
|
72
|
+
// Two routes again, the menu and before-input-event in main.mjs, for the reason
|
|
73
|
+
// zoom has two, and the same guard keeps one press from arriving twice.
|
|
74
|
+
let lastFindAt = 0;
|
|
75
|
+
let lastFindStep = null;
|
|
76
|
+
|
|
77
|
+
export function requestFind(step) {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
if (step === lastFindStep && now - lastFindAt < 40) return;
|
|
80
|
+
lastFindAt = now;
|
|
81
|
+
lastFindStep = step;
|
|
82
|
+
const target = appWindowContents();
|
|
83
|
+
if (!target || target.isDestroyed?.()) return;
|
|
84
|
+
target.send?.('zero:find', { step });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function appWindowContents() {
|
|
88
|
+
const window = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0];
|
|
89
|
+
return window && !window.isDestroyed() ? window.webContents : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function installMenu({ onApproval, onReload, onZoom } = {}) {
|
|
93
|
+
// A CLICK NAMES ITS WINDOW.after the chords were fixed. Electron hands a menu
|
|
94
|
+
// click the window it was chosen from, so the click stops asking what is
|
|
95
|
+
// focused, which is the question that was coming back null. main.mjs decides
|
|
96
|
+
// the keystroke's target from where the event arrived: inside the junk
|
|
97
|
+
// browser it is the guest, which carries its own zoom.
|
|
98
|
+
const zoom = (delta, window) => {
|
|
99
|
+
const target = window && !window.isDestroyed() ? (window.webContents ?? null) : null;
|
|
100
|
+
return onZoom ? onZoom(delta, target) : applyZoom(delta, target);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const template = [
|
|
104
|
+
{
|
|
105
|
+
label: app.name,
|
|
106
|
+
submenu: [
|
|
107
|
+
{ role: 'about' },
|
|
108
|
+
{ type: 'separator' },
|
|
109
|
+
{ role: 'hide' },
|
|
110
|
+
{ role: 'hideOthers' },
|
|
111
|
+
{ role: 'unhide' },
|
|
112
|
+
{ type: 'separator' },
|
|
113
|
+
{ role: 'quit' },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
label: 'Edit',
|
|
118
|
+
submenu: [
|
|
119
|
+
{ role: 'undo' },
|
|
120
|
+
{ role: 'redo' },
|
|
121
|
+
{ type: 'separator' },
|
|
122
|
+
{ role: 'cut' },
|
|
123
|
+
{ role: 'copy' },
|
|
124
|
+
{ role: 'paste' },
|
|
125
|
+
{ role: 'selectAll' },
|
|
126
|
+
{ type: 'separator' },
|
|
127
|
+
{ label: 'Find…', accelerator: 'CommandOrControl+F', click: () => requestFind(0) },
|
|
128
|
+
{ label: 'Find Next', accelerator: 'CommandOrControl+G', click: () => requestFind(1) },
|
|
129
|
+
{ label: 'Find Previous', accelerator: 'Shift+CommandOrControl+G', click: () => requestFind(-1) },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
label: 'View',
|
|
134
|
+
submenu: [
|
|
135
|
+
{ label: 'Zoom In', accelerator: 'CommandOrControl+=', click: (_item, window) => zoom(0.5, window) },
|
|
136
|
+
{ label: 'Zoom In (numpad)', accelerator: 'CommandOrControl+Plus', click: (_item, window) => zoom(0.5, window), visible: false, acceleratorWorksWhenHidden: true },
|
|
137
|
+
{ label: 'Zoom Out', accelerator: 'CommandOrControl+-', click: (_item, window) => zoom(-0.5, window) },
|
|
138
|
+
{ label: 'Actual Size', accelerator: 'CommandOrControl+0', click: (_item, window) => zoom(0, window) },
|
|
139
|
+
{ type: 'separator' },
|
|
140
|
+
// ⌘R PUTS THE BUILT RENDERER ON SCREEN, which is the only thing anyone
|
|
141
|
+
// ever presses it for here.
|
|
142
|
+
//
|
|
143
|
+
// The main document has exactly the same problem and it is worse
|
|
144
|
+
// there, because index.html names a CONTENT-HASHED bundle: one cached
|
|
145
|
+
// index.html hands back the old filename, so the old renderer loads
|
|
146
|
+
// and every change is invisible. ⇧⌘R was added as the workaround
|
|
147
|
+
// rather than the fix.
|
|
148
|
+
//
|
|
149
|
+
// Both chords now ignore the cache, and both go through the app's own
|
|
150
|
+
// window rather than "whatever is focused", so a webview holding focus
|
|
151
|
+
// cannot turn the key into a no-op either.
|
|
152
|
+
{ label: 'Reload', accelerator: 'CommandOrControl+R', click: () => onReload?.() },
|
|
153
|
+
{ label: 'Force Reload', accelerator: 'Shift+CommandOrControl+R', click: () => onReload?.() },
|
|
154
|
+
{ role: 'toggleDevTools' },
|
|
155
|
+
{ type: 'separator' },
|
|
156
|
+
{ role: 'togglefullscreen' },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
label: 'Agents',
|
|
161
|
+
submenu: [
|
|
162
|
+
{ label: 'Allow Pending Approval', accelerator: 'Command+Y', click: () => onApproval?.(true) },
|
|
163
|
+
{ label: 'Deny Pending Approval', accelerator: 'Command+N', click: () => onApproval?.(false) },
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
label: 'Window',
|
|
168
|
+
submenu: [
|
|
169
|
+
{ role: 'minimize' },
|
|
170
|
+
{ role: 'zoom' },
|
|
171
|
+
{ type: 'separator' },
|
|
172
|
+
{ role: 'front' },
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
177
|
+
}
|
package/main/notify.mjs
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// THE BANNER. Once while she is away, only while she is somewhere else, taken
|
|
2
|
+
// down the moment she comes back.
|
|
3
|
+
//
|
|
4
|
+
// The rule it obeys is in shared/notify-rules.mjs; this file is the live half:
|
|
5
|
+
// the window's focus, the machine's idle clock, and the lifetime of the one
|
|
6
|
+
// notification that is allowed to exist. Nothing decidable lives here, so that
|
|
7
|
+
// what the tests pin is the behaviour and not the wiring.
|
|
8
|
+
//
|
|
9
|
+
// Before this, the renderer called a bare `new Notification.show` per arrival,
|
|
10
|
+
// up to three per batch, with no idea whether she was looking at the app. tell
|
|
11
|
+
// me when I am elsewhere, and never when I am here.
|
|
12
|
+
//
|
|
13
|
+
// AND ONCE, NOT TWENTY TIMES. One at a time is not once. `spoke` below is the
|
|
14
|
+
// fix, and shared/notify-rules.mjs carries the rule.
|
|
15
|
+
|
|
16
|
+
import { atTheApp, banner, shouldSpeak } from '../shared/notify-rules.mjs';
|
|
17
|
+
import { NAME } from '../shared/product-name.mjs';
|
|
18
|
+
|
|
19
|
+
// A BURST IS ONE PIECE OF NEWS. Four workers finishing inside the same
|
|
20
|
+
// supervisor tick is the ordinary case, and it arrives as four pushes over a
|
|
21
|
+
// second or two. Waiting a beat before speaking turns that into one banner
|
|
22
|
+
// with one sound instead of four rewrites and four chimes.
|
|
23
|
+
const COALESCE_MS = 1500;
|
|
24
|
+
|
|
25
|
+
export function createNotifier({ window, Notification, powerMonitor, coalesceMs = COALESCE_MS }) {
|
|
26
|
+
// Everything she has not seen since she last had the window, keyed by id so
|
|
27
|
+
// a repeated push is idempotent. Insertion order is arrival order.
|
|
28
|
+
const unseen = new Map();
|
|
29
|
+
let live = null; // the one notification currently on screen, if any
|
|
30
|
+
let timer = null;
|
|
31
|
+
// HOW MANY TIMES SHE HAS BEEN INTERRUPTED SINCE SHE LAST HAD THE WINDOW.
|
|
32
|
+
// Both reset in seen, so a stretch away is the unit. shouldSpeak in
|
|
33
|
+
// shared/notify-rules.mjs is the rule these two feed; the short version is
|
|
34
|
+
// once, plus one more if an approval turns up afterwards.
|
|
35
|
+
let spoke = false;
|
|
36
|
+
let spokeAsk = false;
|
|
37
|
+
|
|
38
|
+
const idleMs = () => {
|
|
39
|
+
try { return powerMonitor.getSystemIdleTime() * 1000; } catch { return 0; }
|
|
40
|
+
};
|
|
41
|
+
const focused = () => {
|
|
42
|
+
try { return !!window && !window.isDestroyed() && window.isFocused(); } catch { return false; }
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const takeDown = () => {
|
|
46
|
+
if (!live) return;
|
|
47
|
+
try { live.close(); } catch {}
|
|
48
|
+
live = null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// She is looking at Agentbox, so everything in hand has been seen on the row
|
|
52
|
+
// it lives on. This is also why arrivals that land while she is here are
|
|
53
|
+
// dropped rather than banked: showing them later, when she steps away, would
|
|
54
|
+
// be telling her about work she already watched arrive.
|
|
55
|
+
const seen = () => {
|
|
56
|
+
unseen.clear();
|
|
57
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
58
|
+
spoke = false;
|
|
59
|
+
spokeAsk = false;
|
|
60
|
+
takeDown();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const speak = () => {
|
|
64
|
+
timer = null;
|
|
65
|
+
if (atTheApp({ focused: focused(), idleMs: idleMs() })) return seen();
|
|
66
|
+
const items = [...unseen.values()];
|
|
67
|
+
// She has already been told once this stretch. Everything since is banked
|
|
68
|
+
// on its own row and she will have it the moment she is back; saying it out
|
|
69
|
+
// loud again is the twenty-chimes-in-a-meeting problem she asked us to fix.
|
|
70
|
+
if (!shouldSpeak({ spoke, spokeAsk, waiting: items })) return;
|
|
71
|
+
const say = banner(items);
|
|
72
|
+
if (!say) return;
|
|
73
|
+
takeDown();
|
|
74
|
+
let n;
|
|
75
|
+
try {
|
|
76
|
+
// No subtitle. macOS puts that field directly under the title in nearly
|
|
77
|
+
// the same type, and she read a lone project name there as the title
|
|
78
|
+
// bleeding over mid-render. The project is in the title now; see
|
|
79
|
+
// shared/notify-rules.mjs.
|
|
80
|
+
n = new Notification({
|
|
81
|
+
title: say.title,
|
|
82
|
+
body: say.body,
|
|
83
|
+
silent: false,
|
|
84
|
+
});
|
|
85
|
+
} catch { return; }
|
|
86
|
+
// A banner she can act on. Clicking it brings Agentbox forward and opens the
|
|
87
|
+
// row it was about, which is the difference between being told and having
|
|
88
|
+
// to go find it.
|
|
89
|
+
n.on('click', () => {
|
|
90
|
+
seen();
|
|
91
|
+
if (!window || window.isDestroyed()) return;
|
|
92
|
+
if (window.isMinimized()) window.restore();
|
|
93
|
+
window.show();
|
|
94
|
+
window.focus();
|
|
95
|
+
if (say.open) window.webContents.send('zero:open-item', { id: say.open });
|
|
96
|
+
});
|
|
97
|
+
n.on('close', () => { if (live === n) live = null; });
|
|
98
|
+
// A BANNER macOS REFUSED LOOKS EXACTLY LIKE NO AGENT FINISHING, which is
|
|
99
|
+
// the shape of failure this codebase keeps paying for. Measured on her
|
|
100
|
+
// machine 2026-08-20: across 107 apps registered with Notification Center,
|
|
101
|
+
// neither `ac.astral.app` nor `com.github. Electron` had a record at all, so
|
|
102
|
+
// there is no evidence a banner from here has ever been delivered. If the
|
|
103
|
+
// OS is dropping them, this line is what turns that from a silence into a
|
|
104
|
+
// two-minute diagnosis.
|
|
105
|
+
n.on('failed', (_e, err) => {
|
|
106
|
+
live = null;
|
|
107
|
+
console.warn(`zero: macOS refused the notification (${err}). Check ${NAME} in System Settings > Notifications.`);
|
|
108
|
+
});
|
|
109
|
+
live = n;
|
|
110
|
+
try {
|
|
111
|
+
n.show();
|
|
112
|
+
spoke = true;
|
|
113
|
+
if (items.some((i) => i.kind === 'approval')) spokeAsk = true;
|
|
114
|
+
} catch { live = null; }
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
// Fresh arrivals from the renderer, which owns what counts as news (it is
|
|
119
|
+
// the only place that knows what the inbox is). Each is
|
|
120
|
+
// { id, title, product, productName, kind }.
|
|
121
|
+
add(arrivals) {
|
|
122
|
+
if (!Array.isArray(arrivals) || !arrivals.length) return;
|
|
123
|
+
// Asked before banking anything, so that a burst arriving while she is
|
|
124
|
+
// at the app is dropped rather than saved up for when she leaves.
|
|
125
|
+
if (atTheApp({ focused: focused(), idleMs: idleMs() })) return seen();
|
|
126
|
+
for (const a of arrivals) {
|
|
127
|
+
if (!a || !a.id || unseen.has(a.id)) continue;
|
|
128
|
+
unseen.set(a.id, a);
|
|
129
|
+
}
|
|
130
|
+
if (!unseen.size || timer) return;
|
|
131
|
+
timer = setTimeout(speak, coalesceMs);
|
|
132
|
+
},
|
|
133
|
+
// The window came forward. Whatever was on screen is now behind her.
|
|
134
|
+
seen,
|
|
135
|
+
// Tests, and nothing else.
|
|
136
|
+
_pending: () => [...unseen.values()],
|
|
137
|
+
_spoken: () => ({ spoke, spokeAsk }),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function installNotifier({ app, window, Notification, powerMonitor, ipcMain }) {
|
|
142
|
+
if (Notification.isSupported && !Notification.isSupported()) {
|
|
143
|
+
return { add() {}, seen() {} };
|
|
144
|
+
}
|
|
145
|
+
const notifier = createNotifier({ window, Notification, powerMonitor });
|
|
146
|
+
window.on('focus', () => notifier.seen());
|
|
147
|
+
// Unlocking with Agentbox already the front app never fires 'focus' (it never
|
|
148
|
+
// lost it), so without this the banner that fired while the screen was
|
|
149
|
+
// locked would sit in Notification Center after she is back at the row.
|
|
150
|
+
try { powerMonitor.on('unlock-screen', () => { if (window.isFocused()) notifier.seen(); }); } catch {}
|
|
151
|
+
ipcMain.handle('zero:notify', (_e, payload = {}) => {
|
|
152
|
+
notifier.add(Array.isArray(payload.arrivals) ? payload.arrivals : []);
|
|
153
|
+
});
|
|
154
|
+
return notifier;
|
|
155
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {execFile} from 'node:child_process';
|
|
2
|
+
import {promisify} from 'node:util';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const run = promisify(execFile);
|
|
5
|
+
export async function openSourceFile(file, execute = run) {
|
|
6
|
+
if (typeof file !== 'string' || !path.isAbsolute(file)) throw Error('Resolve the source file before opening it.');
|
|
7
|
+
await execute('/usr/bin/open', ['-t', file], {timeout:10000});
|
|
8
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// WHAT A PROJECT IS CALLED, AND THE MARK BESIDE THE NAME. Both of them hers to
|
|
2
|
+
// change, and both written into the project's OWN project.json rather than into
|
|
3
|
+
// her workspace config.
|
|
4
|
+
//
|
|
5
|
+
// WHY project.json AND NOT settings.json. Every other switch on the project
|
|
6
|
+
// screen is a preference about how Agentbox treats a folder, so it belongs to this
|
|
7
|
+
// Mac and lives in her config. A name and a mark are what the project IS. They
|
|
8
|
+
// travel with the folder: a project copied to another machine, or opened by the
|
|
9
|
+
// MCP server, or read by a worker session, is still called the same thing. The
|
|
10
|
+
// slug never moves, so nothing that points at a project by id breaks when the
|
|
11
|
+
// name changes.
|
|
12
|
+
//
|
|
13
|
+
// THE ICON IS COPIED IN, NOT POINTED AT, and that is the one place this departs
|
|
14
|
+
// from what she suggested.
|
|
15
|
+
//
|
|
16
|
+
// She is right that we cannot host it, and nothing here does: the file never
|
|
17
|
+
// leaves the Mac. But a pointer at wherever she found the picture is a pointer
|
|
18
|
+
// into her Downloads folder, and the mark in her rail then disappears the day she
|
|
19
|
+
// empties it. Copying the file into the project's own folder keeps every property
|
|
20
|
+
// she asked for (local, no server, no upload) and drops the one failure she did
|
|
21
|
+
// not ask for. It costs about 20KB per project.
|
|
22
|
+
//
|
|
23
|
+
// It also has to be copied for a second reason that only shows up later: the
|
|
24
|
+
// renderer cannot read an arbitrary path off her disk. Images already render
|
|
25
|
+
// through the product's own roots (`img-scheme.mjs`); a file inside the project
|
|
26
|
+
// folder is reachable by that machinery on the day it is written, and a file in
|
|
27
|
+
// ~/Downloads would need a new grant into a folder Agentbox has no business in.
|
|
28
|
+
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { patchProjectAt, loadProjectAt } from './store/project.mjs';
|
|
32
|
+
import { NAME } from '../shared/product-name.mjs';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What a project may be called. Long enough for a real product name, short
|
|
36
|
+
* enough that the rail and the settings nav still read as a list of names. */
|
|
37
|
+
export const NAME_MAX = 60;
|
|
38
|
+
|
|
39
|
+
// The picture kinds a mark can be. Deliberately short: these are the four that
|
|
40
|
+
// every Mac can produce, that Chromium draws without a plugin, and that survive
|
|
41
|
+
// being shrunk to a 24px burst-sized square. No svg, because an svg is a
|
|
42
|
+
// document with script in it and this one gets drawn inside her app.
|
|
43
|
+
export const ICON_KINDS = ['.png', '.jpg', '.jpeg', '.webp'];
|
|
44
|
+
|
|
45
|
+
// The one name a mark is ever stored under, plus its extension. One file per
|
|
46
|
+
// project, so choosing a new mark REPLACES the old rather than leaving her
|
|
47
|
+
// folder collecting every picture she ever tried.
|
|
48
|
+
const ICON_STEM = 'icon';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* PURE. The name a project would end up with, or null when the rename should be
|
|
52
|
+
* refused.
|
|
53
|
+
*
|
|
54
|
+
* Refused rather than coerced when it is empty, because a nameless project is a
|
|
55
|
+
* blank row in her sidebar with no way back: the only handle left to rename it
|
|
56
|
+
* by is the one that just disappeared. Trimmed and collapsed otherwise, so a
|
|
57
|
+
* name pasted out of a document does not arrive with a newline in it and push
|
|
58
|
+
* every row below it down.
|
|
59
|
+
*/
|
|
60
|
+
export function cleanName(value) {
|
|
61
|
+
const name = String(value ?? '').replace(/\s+/g, ' ').trim().slice(0, NAME_MAX);
|
|
62
|
+
return name || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** PURE. Whether this file is a picture a mark can be made of. */
|
|
66
|
+
export function isIconKind(file) {
|
|
67
|
+
return ICON_KINDS.includes(path.extname(String(file ?? '')).toLowerCase());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Rename the project in the folder a caller has already resolved.
|
|
72
|
+
*
|
|
73
|
+
* `patchProjectAt` rather than `renameProject`, on the note that function itself
|
|
74
|
+
* carries: a write that re-derives its folder from an id lands wherever that id
|
|
75
|
+
* happens to name, and two products can hold the same id.
|
|
76
|
+
*/
|
|
77
|
+
export function setProjectName(dir, value) {
|
|
78
|
+
const name = cleanName(value);
|
|
79
|
+
if (!name) throw new Error('a project needs a name');
|
|
80
|
+
if (!loadProjectAt(dir)) throw new Error('not a project folder');
|
|
81
|
+
patchProjectAt(dir, { name });
|
|
82
|
+
return name;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Copy a picture into the project folder and make it the mark.
|
|
87
|
+
*
|
|
88
|
+
* Returns the path the renderer should draw, or throws with a sentence she can
|
|
89
|
+
* read. `source` is whatever the open panel handed back, which is a real file on
|
|
90
|
+
* her disk and never anything typed.
|
|
91
|
+
*
|
|
92
|
+
* THE STORED VALUE IS RELATIVE. `icon.png`, not `/Users/.../agentbox/icon.png`,
|
|
93
|
+
* for the same reason the name lives here at all: a project.json that names an
|
|
94
|
+
* absolute path stops being true the moment the folder moves or is opened on
|
|
95
|
+
* another Mac. The caller joins it to the project's own dir, which it already
|
|
96
|
+
* has.
|
|
97
|
+
*/
|
|
98
|
+
export function setProjectIcon(dir, source) {
|
|
99
|
+
if (!loadProjectAt(dir)) throw new Error('not a project folder');
|
|
100
|
+
if (!isIconKind(source)) throw new Error(`that file is not a picture ${NAME} can draw`);
|
|
101
|
+
let stat = null;
|
|
102
|
+
try { stat = fs.statSync(source); } catch { throw new Error('that file could not be read'); }
|
|
103
|
+
if (!stat.isFile()) throw new Error('that is not a file');
|
|
104
|
+
|
|
105
|
+
const ext = path.extname(source).toLowerCase();
|
|
106
|
+
const name = `${ICON_STEM}${ext}`;
|
|
107
|
+
// The old mark goes first, and every kind of it, or a project that had a .png
|
|
108
|
+
// and now has a .webp keeps both on disk and the stale one is what a future
|
|
109
|
+
// reader trips over.
|
|
110
|
+
clearIconFiles(dir);
|
|
111
|
+
fs.copyFileSync(source, path.join(dir, name));
|
|
112
|
+
patchProjectAt(dir, { logo: name });
|
|
113
|
+
return name;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Back to the drawn burst. The picture is deleted with the setting: leaving it
|
|
118
|
+
* behind means "no icon" and a file called icon.png sitting in her folder, which
|
|
119
|
+
* is the kind of thing that gets asked about a month later. */
|
|
120
|
+
export function clearProjectIcon(dir) {
|
|
121
|
+
if (!loadProjectAt(dir)) throw new Error('not a project folder');
|
|
122
|
+
clearIconFiles(dir);
|
|
123
|
+
patchProjectAt(dir, { logo: null });
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function clearIconFiles(dir) {
|
|
128
|
+
for (const ext of ICON_KINDS) {
|
|
129
|
+
try { fs.rmSync(path.join(dir, `${ICON_STEM}${ext}`), { force: true }); } catch { /* nothing to remove */ }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The mark's full path for a product the store has already listed, or null.
|
|
135
|
+
*
|
|
136
|
+
* CHECKED AGAINST DISK, not merely read off project.json. A `logo` that names a
|
|
137
|
+
* file which is no longer there would draw a broken image where the burst used
|
|
138
|
+
* to be, and the burst is a correct answer to "this project has no mark" while a
|
|
139
|
+
* broken image is not. This is the same rule the attachment chips follow: resolve
|
|
140
|
+
* before you promise.
|
|
141
|
+
*/
|
|
142
|
+
export function iconPathFor(product) {
|
|
143
|
+
const rel = typeof product?.logo === 'string' ? product.logo.trim() : '';
|
|
144
|
+
// Only ever the one file this module writes. A project.json edited by hand to
|
|
145
|
+
// say `../../../somewhere` must not turn into a read outside the folder.
|
|
146
|
+
if (!rel || rel !== path.basename(rel) || !isIconKind(rel)) return null;
|
|
147
|
+
const full = path.join(product.dir, rel);
|
|
148
|
+
try { return fs.statSync(full).isFile() ? full : null; } catch { return null; }
|
|
149
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Small adapters over provider operations. The supervisor owns routing, locks,
|
|
2
|
+
// account selection and persistence; these operations never start a fake prompt.
|
|
3
|
+
import { NAME } from '../shared/product-name.mjs';
|
|
4
|
+
export async function runCodexCommand({server,threadId,cwd,name,args=''}) {
|
|
5
|
+
if(['skills','mcp','status','copy'].includes(name)&&args)throw new Error(`Run /${name} on its own. Your draft has been kept.`);
|
|
6
|
+
if(name==='goal') {
|
|
7
|
+
if(!args){const {goal}=await server.request('thread/goal/get',{threadId});return goal?`${goal.objective}\nStatus: ${goal.status}\nTokens used: ${goal.tokensUsed}`:'This conversation has no goal.';}
|
|
8
|
+
if(args==='clear'){await server.request('thread/goal/clear',{threadId});return 'Goal cleared.';}
|
|
9
|
+
if(args!=='pause')throw new Error(`Setting or resuming a Codex goal is not connected to ${NAME}’s worker lifecycle yet. Your draft has been kept.`);
|
|
10
|
+
await server.request('thread/goal/set',{threadId,status:'paused'});
|
|
11
|
+
return 'Goal paused.';
|
|
12
|
+
}
|
|
13
|
+
if(name==='skills') {
|
|
14
|
+
const result=await server.request('skills/list',{cwds:[cwd],forceReload:true});
|
|
15
|
+
return result.data?.flatMap(d=>[...(d.skills??[]).map(s=>`${s.name}${s.enabled===false?' (disabled)':''}: ${s.description??''}`),...(d.errors??[]).map(e=>`Could not read ${e.path}: ${e.message}`)]).join('\n')||'No skills found in this project.';
|
|
16
|
+
}
|
|
17
|
+
if(name==='mcp') {
|
|
18
|
+
const rows=[];let cursor;
|
|
19
|
+
do {const result=await server.request('mcpServerStatus/list',{threadId,...(cursor?{cursor}:{})});rows.push(...(result.data??[]));cursor=result.nextCursor;}while(cursor);
|
|
20
|
+
return rows.map(s=>`${s.name}: ${Object.keys(s.tools??{}).length} tools; authentication ${s.authStatus??'unknown'}`).join('\n')||'No MCP servers are connected to this conversation.';
|
|
21
|
+
}
|
|
22
|
+
if(name==='status'||name==='copy') {
|
|
23
|
+
const {thread}=await server.request('thread/read',{threadId,includeTurns:true});
|
|
24
|
+
if(name==='copy') {
|
|
25
|
+
const messages=(thread.turns??[]).filter(t=>t.status==='completed').flatMap(t=>t.items??[]).filter(i=>i.type==='agentMessage'&&i.text);
|
|
26
|
+
if(!messages.length)throw new Error('There is no assistant response to copy yet.');
|
|
27
|
+
return messages.at(-1).text;
|
|
28
|
+
}
|
|
29
|
+
return `Conversation: ${thread.id}\nStatus: ${thread.status?.type??'unknown'}\nFolder: ${thread.cwd??cwd}\nTurns: ${thread.turns?.length??0}`;
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`/${name} is not available through ${NAME}’s Codex connection yet. Your draft has been kept.`);
|
|
32
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// THE NOTE SHE KEEPS IN THE SIDEBAR, and the file it lives in.
|
|
2
|
+
//
|
|
3
|
+
// THE NOTE IS A REAL MARKDOWN FILE IN THE PRODUCT'S OWN FOLDER, `pinned.md`,
|
|
4
|
+
// and that is the whole design decision here. It could have been a row in some
|
|
5
|
+
// settings blob, which would have been fewer lines. It is a file because every
|
|
6
|
+
// agent on this product can already read the product's folder, so a priority
|
|
7
|
+
// she types into the sidebar is a priority a worker can be pointed at, and
|
|
8
|
+
// because a file survives this app being reinstalled, rebuilt, or replaced.
|
|
9
|
+
//
|
|
10
|
+
// It is NOT `notes.md`. That name is taken on at least one product by a map an
|
|
11
|
+
// agent wrote in round one, and loading somebody else's stale page into her
|
|
12
|
+
// sidebar the first time she opens it is the one failure this file could have
|
|
13
|
+
// that she would never think to report.
|
|
14
|
+
//
|
|
15
|
+
// ONE NOTE PER PRODUCT, because the panel it draws in is per product: the rail
|
|
16
|
+
// re-orients her on the thing she just opened, and a single note shared across
|
|
17
|
+
// twenty products would be answering a question about some other product, which
|
|
18
|
+
// is the exact fault the agents list had before it was narrowed (Rail.tsx).
|
|
19
|
+
//
|
|
20
|
+
// Pure of Electron on purpose, like doc-file.mjs beside it: the IPC layer hands
|
|
21
|
+
// in the product record, and these two functions are the whole of the disk
|
|
22
|
+
// access. The path is DERIVED from the product, never passed in, so there is no
|
|
23
|
+
// path to traverse and nothing to validate.
|
|
24
|
+
import fsDefault from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
|
|
27
|
+
export const NOTE_NAME = 'pinned.md';
|
|
28
|
+
|
|
29
|
+
// Far past anything a sidebar column can hold, and still small enough that a
|
|
30
|
+
// runaway paste cannot fill her disk. A note that hits this is refused rather
|
|
31
|
+
// than truncated: half her words written back is worse than none.
|
|
32
|
+
export const MAX_NOTE_BYTES = 200_000;
|
|
33
|
+
|
|
34
|
+
/** The file this product's sidebar note lives in, or null if it has no folder. */
|
|
35
|
+
export function notePath(prod) {
|
|
36
|
+
return prod?.dir ? path.join(prod.dir, NOTE_NAME) : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What she has written on this product. A note that has never been started is
|
|
41
|
+
* empty text, not an error: the sidebar draws its placeholder from that. */
|
|
42
|
+
export function readNote({ product, fs = fsDefault }) {
|
|
43
|
+
const file = notePath(product);
|
|
44
|
+
if (!file) return { ok: false, error: 'That project has no folder to keep a note in.' };
|
|
45
|
+
try {
|
|
46
|
+
const stat = fs.statSync(file);
|
|
47
|
+
if (stat.size > MAX_NOTE_BYTES) {
|
|
48
|
+
return { ok: false, error: 'That note is too long for the sidebar to open.' };
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, text: fs.readFileSync(file, 'utf8'), mtime: stat.mtimeMs };
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err?.code === 'ENOENT') return { ok: true, text: '', mtime: 0 };
|
|
53
|
+
return { ok: false, error: 'That note could not be read.' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Her note, written back. The file is CREATED on the first keystroke rather
|
|
59
|
+
* than at install time, so a product she never writes on never grows a file. */
|
|
60
|
+
export function writeNote({ product, text, fs = fsDefault }) {
|
|
61
|
+
const file = notePath(product);
|
|
62
|
+
if (!file) return { ok: false, error: 'That project has no folder to keep a note in.' };
|
|
63
|
+
const out = typeof text === 'string' ? text : '';
|
|
64
|
+
if (Buffer.byteLength(out, 'utf8') > MAX_NOTE_BYTES) {
|
|
65
|
+
return { ok: false, error: 'That note is too long to save.' };
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
69
|
+
fs.writeFileSync(file, out, 'utf8');
|
|
70
|
+
return { ok: true, mtime: fs.statSync(file).mtimeMs };
|
|
71
|
+
} catch {
|
|
72
|
+
return { ok: false, error: 'That note could not be saved.' };
|
|
73
|
+
}
|
|
74
|
+
}
|