agentbox-app 0.1.12 → 0.1.13
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/main/acp.mjs +296 -0
- package/main/agent-homes.mjs +158 -0
- package/main/agent-sessions.mjs +57 -20
- package/main/agent-updates.mjs +9 -22
- package/main/analytics.mjs +30 -0
- package/main/claude-bin.mjs +45 -311
- package/main/claude-input.mjs +9 -3
- package/main/claude-launch-env.mjs +54 -0
- package/main/claude-models.mjs +39 -7
- package/main/claude-plan.mjs +16 -16
- package/main/claude-usage.mjs +82 -25
- package/main/codex-bin.mjs +51 -249
- package/main/codex-session.mjs +38 -3
- package/main/codex-threads.mjs +41 -8
- package/main/codex-watch.mjs +67 -3
- package/main/config.mjs +26 -4
- package/main/corner-tag.mjs +489 -0
- package/main/crash-report.mjs +26 -3
- package/main/engine-bin.mjs +595 -0
- package/main/engine-setup.mjs +4 -15
- package/main/feedback.mjs +11 -6
- package/main/harnesses/claude-stream.mjs +208 -0
- package/main/harnesses/claude.mjs +84 -0
- package/main/harnesses/codex.mjs +65 -0
- package/main/harnesses/common.mjs +78 -0
- package/main/harnesses/instructions-file.mjs +116 -0
- package/main/harnesses/opencode.mjs +234 -0
- package/main/harnesses.mjs +65 -0
- package/main/importable-threads.mjs +38 -0
- package/main/ipc.mjs +165 -31
- package/main/local-app-frames.mjs +82 -0
- package/main/main.mjs +100 -4
- package/main/memory-gate-history.mjs +51 -12
- package/main/memory-gate-server.mjs +67 -11
- package/main/memory-gate.mjs +49 -8
- package/main/memory-log.mjs +124 -0
- package/main/notify.mjs +47 -4
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-link.mjs +551 -0
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +246 -0
- package/main/row-label.mjs +6 -6
- package/main/runs-on.mjs +32 -35
- package/main/serve.mjs +113 -9
- package/main/settings.mjs +148 -10
- package/main/shell-path.mjs +86 -0
- package/main/ship-queue.mjs +24 -6
- package/main/sign-in-files.mjs +3 -7
- package/main/source-updater.mjs +27 -1
- package/main/start-hold.mjs +73 -0
- package/main/store.mjs +1 -1
- package/main/supervisor.mjs +832 -428
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +490 -98
- package/main/task-terminals.mjs +37 -7
- package/main/updater.mjs +71 -9
- package/main/worktree-include.mjs +494 -0
- package/package.json +10 -1
- package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
- package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
- package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
- package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
- package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
- package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
- package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
- package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
- package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
- package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
- package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
- package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
- package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
- package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
- package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
- package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
- package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
- package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
- package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
- package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
- package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
- package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
- package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
- package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
- package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
- package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
- package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
- package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
- package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
- package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
- package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
- package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
- package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
- package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
- package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
- package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
- package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
- package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
- package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
- package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
- package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
- package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
- package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
- package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
- package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
- package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
- package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
- package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
- package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
- package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
- package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
- package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
- package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
- package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
- package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
- package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
- package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
- package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
- package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
- package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
- package/renderer/dist/corner-tag.html +13 -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 +297 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/index.html +16 -3
- package/scripts/approval-server.sh +24 -5
- package/scripts/before-publish.mjs +12 -10
- package/shared/agent-import.mjs +2 -0
- package/shared/agents.d.mts +1 -0
- package/shared/bridge-map.mjs +18 -0
- package/shared/claude-commands.generated.mjs +6 -4
- package/shared/claude-models.generated.mjs +6 -4
- package/shared/corner-tag.d.mts +14 -0
- package/shared/corner-tag.mjs +141 -0
- package/shared/engines.mjs +20 -5
- package/shared/harness-definitions.mjs +103 -0
- package/shared/notify-rules.d.mts +25 -0
- package/shared/notify-rules.mjs +19 -0
- package/shared/provider-commands.mjs +10 -2
- package/shared/rank.d.mts +2 -0
- package/shared/rank.mjs +50 -0
- package/shared/relay-address.mjs +6 -0
- package/shared/relay-crypto.mjs +165 -0
- package/shared/runs-on.d.mts +0 -10
- package/shared/runs-on.mjs +25 -49
- package/shared/shortcuts.mjs +10 -4
- package/shared/spawn-trouble.mjs +57 -6
- package/shared/terminal-input.d.mts +1 -0
- package/shared/terminal-input.mjs +30 -0
- package/shared/terminal-keys.d.mts +1 -0
- package/shared/terminal-keys.mjs +16 -0
- package/shared/terminal-size.d.mts +4 -0
- package/shared/terminal-size.mjs +29 -0
- package/shared/usage.d.mts +3 -0
- package/shared/usage.mjs +7 -0
- package/shared/where-it-runs-from.d.mts +43 -0
- package/shared/where-it-runs-from.mjs +155 -0
- package/shared/work-items.mjs +25 -1
- package/shared/work-lines.mjs +1 -1
- package/shared/xterm-invalid-range.d.mts +2 -0
- package/shared/xterm-invalid-range.mjs +58 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
- package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
|
@@ -39,10 +39,29 @@ case "$here" in
|
|
|
39
39
|
exec env ELECTRON_RUN_AS_NODE=1 "$app/MacOS/$exe" "$res/app.asar/main/approval-prompt-server.mjs"
|
|
40
40
|
;;
|
|
41
41
|
*)
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
# RUNNING FROM SOURCE, AND THIS LINE USED TO ASSUME NVM. It prepended
|
|
43
|
+
# $HOME/.nvm/versions/node/<newest>/bin and exec'd node. On a Mac that
|
|
44
|
+
# installed node by Homebrew, Volta, asdf or not at all, that folder is
|
|
45
|
+
# absent, PATH is untouched, and an MCP server booting from a bare login
|
|
46
|
+
# shell has no node on it: the exec failed and every agent died the same
|
|
47
|
+
# four-second death described at the top of this file. Reported from
|
|
48
|
+
# source on 2026-10-07 (issue 21).
|
|
49
|
+
#
|
|
50
|
+
# So the app hands its own binary down in ZERO_APPROVALS_RUNTIME, and
|
|
51
|
+
# ELECTRON_RUN_AS_NODE=1 makes that binary a node, exactly as the packaged
|
|
52
|
+
# branch above does. Nothing on the machine is needed.
|
|
53
|
+
server="$(cd "$here/.." && pwd)/main/approval-prompt-server.mjs"
|
|
54
|
+
if [ -f "$ZERO_APPROVALS_RUNTIME" ] && [ -x "$ZERO_APPROVALS_RUNTIME" ]; then
|
|
55
|
+
exec env ELECTRON_RUN_AS_NODE=1 "$ZERO_APPROVALS_RUNTIME" "$server"
|
|
56
|
+
fi
|
|
57
|
+
# Nobody handed one down, so this is somebody running the script by hand.
|
|
58
|
+
# A node already on PATH wins; nvm's newest is the last resort, for the
|
|
59
|
+
# bare login shell that has never sourced nvm.
|
|
60
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
61
|
+
nvm="$HOME/.nvm/versions/node"
|
|
62
|
+
newest="$(ls "$nvm" 2>/dev/null | tail -1)"
|
|
63
|
+
[ -n "$newest" ] && export PATH="$nvm/$newest/bin:$PATH"
|
|
64
|
+
fi
|
|
65
|
+
exec node "$server"
|
|
47
66
|
;;
|
|
48
67
|
esac
|
|
@@ -69,17 +69,19 @@ function wouldShip(changed) {
|
|
|
69
69
|
// If npm cannot tell us, the honest answer is the cautious one.
|
|
70
70
|
return changed;
|
|
71
71
|
}
|
|
72
|
-
// THE FILES
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
72
|
+
// THE FILES A PUBLISH USED TO REWRITE, which made every attempt poison the
|
|
73
|
+
// next one. `prepack` runs `npm run build`, and the build read the installed
|
|
74
|
+
// Claude Code and wrote what it found over shared/*.generated.*. So a failed
|
|
75
|
+
// publish left those modified and the next publish refused because of dirt the
|
|
76
|
+
// last publish had made. Measured 2026-09-28: one failed attempt left
|
|
77
|
+
// claude-models.generated.mjs changed by a version number, and that alone
|
|
78
|
+
// blocked the retry.
|
|
79
79
|
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// committed
|
|
80
|
+
// THE BUILD NO LONGER WRITES THEM (w-6bbb709b1f): it writes
|
|
81
|
+
// shared/*.local.json, which is gitignored, and `npm run read:claude` is what
|
|
82
|
+
// updates the committed tables. This filter stays as the belt for a folder an
|
|
83
|
+
// OLDER build already dirtied, and because what ships is whatever the build
|
|
84
|
+
// produces on the machine publishing, so a difference here changes nothing.
|
|
83
85
|
return changed.filter((f) => packed.has(f) && !/\.generated\./.test(f));
|
|
84
86
|
}
|
|
85
87
|
|
package/shared/agent-import.mjs
CHANGED
|
@@ -148,6 +148,8 @@ export function whenWords(when, now = Date.now()) {
|
|
|
148
148
|
/** Where she was when she started it, in the two places she named. */
|
|
149
149
|
export function startedIn(source) {
|
|
150
150
|
if (source === 'codex') return 'in Codex';
|
|
151
|
+
// VS Code, Cursor or Windsurf: the extension says which family, not which one.
|
|
152
|
+
if (source === 'editor') return 'in your code editor';
|
|
151
153
|
return source === 'desktop' ? 'in the Claude Code desktop app' : 'in a terminal';
|
|
152
154
|
}
|
|
153
155
|
|
package/shared/agents.d.mts
CHANGED
package/shared/bridge-map.mjs
CHANGED
|
@@ -15,6 +15,10 @@ export const REQUEST_CHANNELS = {
|
|
|
15
15
|
terminal: 'zero:terminal',
|
|
16
16
|
agentUpdate: 'zero:agent-update',
|
|
17
17
|
snapshot: 'zero:snapshot',
|
|
18
|
+
refreshUsage: 'zero:refresh-usage',
|
|
19
|
+
// Whether a screen reader is running, which is the terminal pane's question
|
|
20
|
+
// before it builds xterm's accessibility tree.
|
|
21
|
+
assistiveTech: 'zero:assistiveTech',
|
|
18
22
|
// The next page of old finished threads, at the foot of Done and All.
|
|
19
23
|
olderItems: 'zero:older-items',
|
|
20
24
|
dashboard: 'zero:dashboard',
|
|
@@ -73,9 +77,11 @@ export const REQUEST_CHANNELS = {
|
|
|
73
77
|
agentThreads: 'zero:agent-threads',
|
|
74
78
|
importThreads: 'zero:import-threads',
|
|
75
79
|
badge: 'zero:badge',
|
|
80
|
+
cornerTag: 'zero:corner-tag',
|
|
76
81
|
bootInfo: 'zero:boot-info',
|
|
77
82
|
updateCheck: 'zero:update-check',
|
|
78
83
|
updateInstall: 'zero:update-install',
|
|
84
|
+
updateMove: 'zero:update-move',
|
|
79
85
|
notify: 'zero:notify',
|
|
80
86
|
stopSession: 'zero:stop-session',
|
|
81
87
|
runNow: 'zero:run-now',
|
|
@@ -91,6 +97,10 @@ export const REQUEST_CHANNELS = {
|
|
|
91
97
|
settingsRead: 'zero:settings-read',
|
|
92
98
|
claudeRecheck: 'zero:claude-recheck',
|
|
93
99
|
codexRecheck: 'zero:codex-recheck',
|
|
100
|
+
// Point at the coding agent yourself, or hand the choice back to the search.
|
|
101
|
+
// The browser tab has no file panel, so main answers it ok-with-nothing-done
|
|
102
|
+
// there (main/serve.mjs stubs showOpenDialog) and the row stays honest.
|
|
103
|
+
engineBin: 'zero:engine-bin',
|
|
94
104
|
engineSetup: 'zero:engine-setup',
|
|
95
105
|
codexAddAccount: 'zero:codex-add-account',
|
|
96
106
|
claudeAddAccount: 'zero:claude-add-account',
|
|
@@ -128,8 +138,13 @@ export const REQUEST_CHANNELS = {
|
|
|
128
138
|
resumeItems: 'zero:resume-items',
|
|
129
139
|
redeliver: 'zero:redeliver',
|
|
130
140
|
setProductOrder: 'zero:set-product-order',
|
|
141
|
+
setThreadPlaces: 'zero:set-thread-places',
|
|
142
|
+
setThreadPins: 'zero:set-thread-pins',
|
|
131
143
|
setProductHidden: 'zero:set-product-hidden',
|
|
132
144
|
approve: 'zero:approve',
|
|
145
|
+
// The phone door's Settings page (main/phone-link.mjs): its state, on, off,
|
|
146
|
+
// a new key, and which address the QR code carries. The desktop window only.
|
|
147
|
+
phoneLink: 'zero:phone-link',
|
|
133
148
|
};
|
|
134
149
|
|
|
135
150
|
/** Told rather than asked: the app talks and the screen listens. */
|
|
@@ -140,9 +155,12 @@ export const PUSH_CHANNELS = {
|
|
|
140
155
|
onZoomPercent: 'zero:zoom-percent',
|
|
141
156
|
onRecovered: 'zero:recovered',
|
|
142
157
|
onOpenItem: 'zero:open-item',
|
|
158
|
+
onOpenSettings: 'zero:open-settings',
|
|
143
159
|
onScreenDetail: 'zero:screen-detail',
|
|
144
160
|
onEscapeBrowser: 'zero:escape-browser',
|
|
145
161
|
onKeyInTheFile: 'zero:key-in-the-file',
|
|
162
|
+
// A phone connected, left, or the key changed.
|
|
163
|
+
onPhoneChanged: 'zero:phone-changed',
|
|
146
164
|
};
|
|
147
165
|
|
|
148
166
|
// THE FEW THAT ARE NOT A STRAIGHT HANDOVER. Each is called with bare values
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// GENERATED BY scripts/read-claude-commands.mjs. DO NOT EDIT BY HAND.
|
|
2
2
|
//
|
|
3
3
|
// Claude Code's own words for the commands Agentbox's reply box offers,
|
|
4
|
-
// read out of the copy of Claude Code installed on
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// read out of the copy of Claude Code installed on a machine that ran
|
|
5
|
+
// `npm run read:claude`. A BUILD DOES NOT REWRITE THIS: a build wrote what it
|
|
6
|
+
// read over this tracked file, and every folder that had run the app then
|
|
7
|
+
// looked like it held work. Committed so a machine with no Claude Code on it
|
|
8
|
+
// can still build, and so a version that renames one of the eight shows up as
|
|
9
|
+
// a diff rather than as a wrong menu.
|
|
8
10
|
//
|
|
9
11
|
// WHY IT IS READ AND NOT TYPED: reading them out of the installed binary at
|
|
10
12
|
// build time, rather than typing them into a file, is the only way the list
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// GENERATED BY scripts/read-claude-models.mjs. DO NOT EDIT BY HAND.
|
|
2
2
|
//
|
|
3
3
|
// Which model each alias really is, and Claude Code's own name for it, read
|
|
4
|
-
// out of the copy of Claude Code installed on
|
|
5
|
-
// `npm run
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// out of the copy of Claude Code installed on a machine that ran
|
|
5
|
+
// `npm run read:claude`. A BUILD DOES NOT REWRITE THIS: a build wrote what it
|
|
6
|
+
// read over this tracked file, and every folder that had run the app then
|
|
7
|
+
// looked like it held work. Committed so a machine with no Claude Code on it
|
|
8
|
+
// can still build, and so a version that points an alias at a different model
|
|
9
|
+
// shows up as a diff rather than as a wrong label on the picker.
|
|
8
10
|
//
|
|
9
11
|
// WHY IT IS READ AND NOT TYPED: labelling a model wrong is expensive, because
|
|
10
12
|
// Agentbox sends the alias, the CLI decides what the alias means, and a version
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Types for shared/corner-tag.mjs, the corner tag's rules, which main (Node)
|
|
2
|
+
// and the tag's page (Vite) share one copy of. Only what the page calls is
|
|
3
|
+
// declared; the window placement is main's half.
|
|
4
|
+
|
|
5
|
+
export type HideChoice = { key: '5m' | '30m' | 'today'; label: string };
|
|
6
|
+
export const HIDE_CHOICES: HideChoice[];
|
|
7
|
+
|
|
8
|
+
export function tagSays(counts: { ready?: number; working?: number }): { kind: 'ready' | 'working'; text: string } | null;
|
|
9
|
+
|
|
10
|
+
export type ReadyLine = { id: string; title: string; says: string; since?: number; open?: string | null };
|
|
11
|
+
export function cardLines<T extends ReadyLine>(ready: T[], now: number, max?: number): {
|
|
12
|
+
lines: (T & { waited: string })[];
|
|
13
|
+
more: number;
|
|
14
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// THE CORNER TAG (w-dafae58a23): the rules, and nothing that touches a window.
|
|
2
|
+
//
|
|
3
|
+
// You lose track of agents once you are in another app, and the ones waiting on
|
|
4
|
+
// you sit idle. The answer that survived eight rounds of drawings is a small tag
|
|
5
|
+
// in a corner of the screen that says "2 ready for you" when an agent has
|
|
6
|
+
// something for you and "20 working" when none does. Twenty agents running is
|
|
7
|
+
// ordinary, so working agents are a count and never a list: only what is ready
|
|
8
|
+
// for you is worth a line.
|
|
9
|
+
//
|
|
10
|
+
// It must never become a nag. So it hides for a while and comes back on its
|
|
11
|
+
// own, there is no one-click way to make it gone for good (that becomes a thing
|
|
12
|
+
// you closed once and forgot existed), it is turned off only in Settings, and
|
|
13
|
+
// it steps aside while you are in the app itself.
|
|
14
|
+
//
|
|
15
|
+
// main/corner-tag.mjs owns the window and renderer/src/corner-tag owns the
|
|
16
|
+
// page; both decide through this file so the tests pin behaviour, not wiring.
|
|
17
|
+
|
|
18
|
+
/** Space between the tag and the edge of the screen when it rests in a corner. */
|
|
19
|
+
export const MARGIN = 18;
|
|
20
|
+
|
|
21
|
+
/** The ways to hide it. All of them come back; none of them is permanent. */
|
|
22
|
+
export const HIDE_CHOICES = [
|
|
23
|
+
{ key: '5m', label: '5 min' },
|
|
24
|
+
{ key: '30m', label: '30 min' },
|
|
25
|
+
{ key: 'today', label: 'Today' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** When a hide chosen at `now` runs out. 0 means "not hidden". */
|
|
29
|
+
export function hiddenUntil(choice, now) {
|
|
30
|
+
if (choice === '5m') return now + 5 * 60_000;
|
|
31
|
+
if (choice === '30m') return now + 30 * 60_000;
|
|
32
|
+
if (choice === 'today') {
|
|
33
|
+
const d = new Date(now);
|
|
34
|
+
d.setHours(24, 0, 0, 0);
|
|
35
|
+
return d.getTime();
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The words on the tag, or null when there is nothing to say. Short, because
|
|
41
|
+
* "22 ready for you" in the corner of a full screen was more than it needed;
|
|
42
|
+
* "waiting" was picked over "ready" and "need you". */
|
|
43
|
+
export function tagSays({ ready = 0, working = 0 } = {}) {
|
|
44
|
+
if (ready > 0) return { kind: 'ready', text: `${ready} waiting` };
|
|
45
|
+
if (working > 0) return { kind: 'working', text: `${working} working` };
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What the tag counts and lists: everything waiting on you, newest first.
|
|
51
|
+
* The first version put threads from weeks ago at the top of the list; the
|
|
52
|
+
* second cut everything older than a day, and then a Needs you of 58 read
|
|
53
|
+
* "3 waiting" in the corner (w-0961e5523b). So the count is the whole of it,
|
|
54
|
+
* the same number the app shows, and only the order keeps the old ones out
|
|
55
|
+
* of the way: the list shows the newest three and folds the rest into "and N
|
|
56
|
+
* more". Something with no moment it became ready (an agent stopped on a
|
|
57
|
+
* yes, right now) sorts as now.
|
|
58
|
+
*/
|
|
59
|
+
export function readyNow(ready, now) {
|
|
60
|
+
return [...(ready ?? [])]
|
|
61
|
+
.sort((a, b) => (Number.isFinite(b.since) ? b.since : now) - (Number.isFinite(a.since) ? a.since : now));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whether the tag is on screen right now. */
|
|
65
|
+
export function tagShows({ on, hiddenUntil: until = 0, now, inApp, ready = 0, working = 0 }) {
|
|
66
|
+
if (on === false) return false;
|
|
67
|
+
if (until > now) return false;
|
|
68
|
+
// In the app itself the inbox already says all of this, bigger.
|
|
69
|
+
if (inApp) return false;
|
|
70
|
+
return !!tagSays({ ready, working });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** How long something has waited, in units. */
|
|
74
|
+
export function waitedFor(since, now) {
|
|
75
|
+
if (!Number.isFinite(since)) return '';
|
|
76
|
+
const min = Math.floor((now - since) / 60_000);
|
|
77
|
+
if (min < 1) return 'now';
|
|
78
|
+
if (min < 60) return `${min} min`;
|
|
79
|
+
return `${Math.floor(min / 60)} h`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The lines under the tag: the newest first, three at most. */
|
|
83
|
+
export function cardLines(ready, now, max = 3) {
|
|
84
|
+
const sorted = readyNow(ready, now, Infinity);
|
|
85
|
+
const lines = sorted.slice(0, max).map((r) => ({ ...r, waited: waitedFor(r.since, now) }));
|
|
86
|
+
return { lines, more: Math.max(0, sorted.length - lines.length) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
90
|
+
|
|
91
|
+
function areaFor(point, areas) {
|
|
92
|
+
return areas.find((a) => point.x >= a.x && point.x < a.x + a.width && point.y >= a.y && point.y < a.y + a.height) ?? areas[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Where the tag goes when it appears: back where you last dragged it, pulled
|
|
97
|
+
* onto a screen if that screen is gone, or the bottom-right corner the first
|
|
98
|
+
* time. `spot` is the tag's top-left, `areas` are the screens' work areas.
|
|
99
|
+
*/
|
|
100
|
+
export function restingSpot(spot, size, areas) {
|
|
101
|
+
if (!spot) {
|
|
102
|
+
const a = areas[0];
|
|
103
|
+
return { x: a.x + a.width - MARGIN - size.width, y: a.y + a.height - MARGIN - size.height };
|
|
104
|
+
}
|
|
105
|
+
const a = areaFor({ x: spot.x + size.width / 2, y: spot.y + size.height / 2 }, areas) ?? areas[0];
|
|
106
|
+
return {
|
|
107
|
+
x: clamp(spot.x, a.x, a.x + a.width - size.width),
|
|
108
|
+
y: clamp(spot.y, a.y, a.y + a.height - size.height),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function inRightHalf(r, a) { return r.x + r.width / 2 > a.x + a.width / 2; }
|
|
113
|
+
function inLowerHalf(r, a) { return r.y + r.height / 2 > a.y + a.height / 2; }
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The tag's new bounds when its words change length. The edge nearest its
|
|
117
|
+
* corner stays put, so "2 ready for you" turning into "20 working" does not
|
|
118
|
+
* walk the tag away from the edge you parked it against.
|
|
119
|
+
*/
|
|
120
|
+
export function keepCorner(bounds, size, area) {
|
|
121
|
+
const x = inRightHalf(bounds, area) ? bounds.x + bounds.width - size.width : bounds.x;
|
|
122
|
+
const y = inLowerHalf(bounds, area) ? bounds.y + bounds.height - size.height : bounds.y;
|
|
123
|
+
return { x, y, width: size.width, height: size.height };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Where the list opens when you point at the tag: toward the middle of the
|
|
128
|
+
* screen, so it never runs off an edge, lined up with the tag's outer edge.
|
|
129
|
+
*/
|
|
130
|
+
export function cardPlacement(tag, card, area, gap = 8) {
|
|
131
|
+
let above = inLowerHalf(tag, area);
|
|
132
|
+
const alignRight = inRightHalf(tag, area);
|
|
133
|
+
const x = clamp(alignRight ? tag.x + tag.width - card.width : tag.x, area.x, area.x + area.width - card.width);
|
|
134
|
+
const up = tag.y - gap - card.height;
|
|
135
|
+
const down = tag.y + tag.height + gap;
|
|
136
|
+
// A tall list near a short screen's middle may not fit the side it prefers.
|
|
137
|
+
if (above && up < area.y) above = false;
|
|
138
|
+
else if (!above && down + card.height > area.y + area.height) above = true;
|
|
139
|
+
const y = clamp(above ? up : down, area.y, area.y + area.height - card.height);
|
|
140
|
+
return { above, alignRight, card: { x, y, width: card.width, height: card.height } };
|
|
141
|
+
}
|
package/shared/engines.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HARNESS_DEFINITIONS } from './harness-definitions.mjs';
|
|
1
2
|
// WHICH CODING AGENT A TASK IS HANDED TO.
|
|
2
3
|
//
|
|
3
4
|
// AND THE PREMISE SHE GAVE FOR IT DID NOT HOLD. Measured head to head on her
|
|
@@ -44,11 +45,25 @@
|
|
|
44
45
|
// main/codex.mjs when that comes back, because the renderer must never import
|
|
45
46
|
// spawn code.
|
|
46
47
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The engines, in the order the picker draws them.
|
|
50
|
+
*
|
|
51
|
+
* ADMITTED ONES ONLY, and that word is doing real work as of 2026-10-07. A
|
|
52
|
+
* harness being integrated and a harness being offered are two different
|
|
53
|
+
* decisions, and this list is the second one. OpenCode is registered in
|
|
54
|
+
* main/harnesses.mjs, its transport is measured and tested, and it is
|
|
55
|
+
* deliberately absent from here -- so `ENGINE_IDS`, `isEngine`, `engineFor`,
|
|
56
|
+
* the composer's picker, the Settings row and the byline all behave as if it
|
|
57
|
+
* were not there, while `harnessFor('opencode')` works for anything that asks
|
|
58
|
+
* for it by name. Without this filter, adding the definition alone would have
|
|
59
|
+
* put a third word in front of every user the moment it landed.
|
|
60
|
+
*
|
|
61
|
+
* docs/harnesses.md holds the bar a harness clears before `admitted` goes
|
|
62
|
+
* true, and the product review that turning it on requires.
|
|
63
|
+
*/
|
|
64
|
+
export const ENGINES = HARNESS_DEFINITIONS
|
|
65
|
+
.filter(({ admitted }) => admitted !== false)
|
|
66
|
+
.map(({ id, label, word }) => ({ id, label, word }));
|
|
52
67
|
|
|
53
68
|
export const ENGINE_IDS = ENGINES.map((e) => e.id);
|
|
54
69
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Names, flags, paths, and capabilities are data. This file is safe for the
|
|
2
|
+
// renderer: it imports no processes, credentials, filesystem, or transports.
|
|
3
|
+
const freeze = value => {
|
|
4
|
+
if (value && typeof value === 'object') { for (const child of Object.values(value)) freeze(child); Object.freeze(value); }
|
|
5
|
+
return value;
|
|
6
|
+
};
|
|
7
|
+
export const HARNESS_DEFINITIONS = freeze([
|
|
8
|
+
{
|
|
9
|
+
id: 'claude', label: 'Claude Code', word: 'Claude Code', binKey: 'claudeBin',
|
|
10
|
+
homeEnv: 'CLAUDE_CONFIG_DIR', accountPrefix: '', package: '@anthropic-ai/claude-code',
|
|
11
|
+
commandMenu: 'claude', commandRoute: 'claude',
|
|
12
|
+
casks: ['claude-code', 'claude-code@latest'],
|
|
13
|
+
installer: { shell: '/bin/bash', url: 'https://claude.ai/install.sh', pipe: 'bash' },
|
|
14
|
+
loginArgs: ['auth', 'login', '--claudeai'], statusArgs: ['auth', 'status', '--json'],
|
|
15
|
+
credentialFiles: ['.claude.json', '.credentials.json'],
|
|
16
|
+
// HOW A WORKER IS STARTED, AS DATA. This was six hardcoded flag names in
|
|
17
|
+
// main/supervisor.mjs, which made the cheapest possible new engine — one
|
|
18
|
+
// that speaks this same stream-json and differs by a binary and the
|
|
19
|
+
// `outputFormat` value, as Grok Build does in
|
|
20
|
+
// github.com/savannahfeder/agentbox/issues/21 — a supervisor edit. A key
|
|
21
|
+
// left out here means this engine does not take that flag, and the builder
|
|
22
|
+
// leaves it off the line rather than the call site asking who it is.
|
|
23
|
+
run: {
|
|
24
|
+
print: '-p',
|
|
25
|
+
outputFormat: { flag: '--output-format', value: 'stream-json' },
|
|
26
|
+
stream: ['--verbose', '--include-partial-messages'],
|
|
27
|
+
effort: '--effort',
|
|
28
|
+
system: '--append-system-prompt',
|
|
29
|
+
resume: '--resume',
|
|
30
|
+
fork: '--fork-session',
|
|
31
|
+
// Replies arrive on stdin, so the prompt leaves the line when they do.
|
|
32
|
+
input: ['--input-format', 'stream-json', '--replay-user-messages'],
|
|
33
|
+
},
|
|
34
|
+
scrubPatterns: ['^ANTHROPIC_', '^CLAUDE_CODE_', '^CLAUDECODE$', '^CLAUDE_PID$', '^CLAUDE_EFFORT$', '^CLAUDE_CONFIG_DIR$'],
|
|
35
|
+
capabilities: { nativeCommands: true, fork: true, nativeReview: false, remoteControl: true, inlineImages: false, claudeEffort: true },
|
|
36
|
+
imageInstruction: 'Open each one with the Read tool, at the absolute path below.',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'codex', label: 'Codex', word: 'Codex', binKey: 'codexBin',
|
|
40
|
+
homeEnv: 'CODEX_HOME', accountPrefix: 'codex:', package: '@openai/codex', casks: ['codex'],
|
|
41
|
+
commandMenu: 'codex', commandRoute: 'codex',
|
|
42
|
+
installer: { shell: '/bin/sh', url: 'https://chatgpt.com/codex/install.sh', pipe: 'sh' },
|
|
43
|
+
loginArgs: ['login'], statusArgs: ['login', 'status'], credentialFiles: ['auth.json'],
|
|
44
|
+
// NO `run`, AND THAT IS THE POINT OF THE SHAPE. Codex is driven over its
|
|
45
|
+
// app-server RPC: the prompt is a turn's `input`, the system block a
|
|
46
|
+
// thread's `developerInstructions`, the resume a thread id. An engine with
|
|
47
|
+
// no command line is handed none rather than handed Claude Code's.
|
|
48
|
+
scrubPatterns: ['^ANTHROPIC_', '^CLAUDE_CODE_', '^CLAUDECODE$', '^CLAUDE_PID$', '^CLAUDE_EFFORT$', '^CLAUDE_CONFIG_DIR$', '^OPENAI_', '^CODEX_'],
|
|
49
|
+
capabilities: { nativeCommands: false, fork: false, nativeReview: true, remoteControl: false, inlineImages: true, claudeEffort: false },
|
|
50
|
+
imageInstruction: 'Each one is attached to this message as an image, and is also on disk at the absolute path below.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
// OPENCODE, AND IT IS NOT OFFERED TO ANYBODY YET -- see `admitted` below.
|
|
54
|
+
//
|
|
55
|
+
// THE ONE THING THAT MAKES IT DIFFERENT FROM THE OTHER TWO: it needs no
|
|
56
|
+
// sign-in at all. Measured on a real OpenCode 1.18.35 on 2026-10-07 with
|
|
57
|
+
// `auth.json` holding zero credentials, `opencode models` still listed ten
|
|
58
|
+
// models and a run on one of them finished and reported `"cost":0`. They
|
|
59
|
+
// are the OpenCode Zen free tier. So this is the first harness where the
|
|
60
|
+
// first run costs nothing and asks for nothing, which is also why
|
|
61
|
+
// `signIn` below is a real command rather than a requirement.
|
|
62
|
+
//
|
|
63
|
+
// ITS CREDENTIAL STORE IS ITS OWN AND AGENTBOX NEVER TOUCHES IT. Keys go
|
|
64
|
+
// into ~/.local/share/opencode/auth.json by `opencode auth login`, which
|
|
65
|
+
// is the same shape as `claude auth login` and `codex login` above. The
|
|
66
|
+
// scrub list is therefore WIDER than the other two: it takes out every
|
|
67
|
+
// provider key OpenCode could otherwise read out of our environment,
|
|
68
|
+
// because CLAUDE.md says no API key goes into a worker, and OpenCode is
|
|
69
|
+
// the one tool here that would happily use one if it found it.
|
|
70
|
+
id: 'opencode', label: 'OpenCode', word: 'OpenCode', binKey: 'opencodeBin',
|
|
71
|
+
homeEnv: 'OPENCODE_CONFIG', accountPrefix: 'opencode:', package: 'opencode-ai',
|
|
72
|
+
commandMenu: 'opencode', commandRoute: 'opencode',
|
|
73
|
+
casks: ['opencode', 'sst/tap/opencode'],
|
|
74
|
+
installer: { shell: '/bin/bash', url: 'https://opencode.ai/install', pipe: 'bash' },
|
|
75
|
+
loginArgs: ['auth', 'login'], statusArgs: ['auth', 'list'],
|
|
76
|
+
credentialFiles: ['auth.json'],
|
|
77
|
+
scrubPatterns: [
|
|
78
|
+
'^ANTHROPIC_', '^CLAUDE_CODE_', '^CLAUDECODE$', '^CLAUDE_PID$', '^CLAUDE_EFFORT$', '^CLAUDE_CONFIG_DIR$',
|
|
79
|
+
'^OPENAI_', '^CODEX_', '^OPENCODE_', '^OPENROUTER_', '^GEMINI_', '^GOOGLE_',
|
|
80
|
+
'^AWS_', '^AZURE_', '^GROQ_', '^MISTRAL_', '^DEEPSEEK_', '^XAI_', '^CEREBRAS_',
|
|
81
|
+
'^TOGETHER_', '^FIREWORKS_', '^DEEPINFRA_', '^BASETEN_', '^NVIDIA_', '^GITLAB_TOKEN$',
|
|
82
|
+
'_API_KEY$', '_ACCESS_TOKEN$',
|
|
83
|
+
],
|
|
84
|
+
// NO USAGE AND NO PLAN. There is no endpoint on the server and no command
|
|
85
|
+
// on the CLI that reports what is left of anything, because OpenCode bills
|
|
86
|
+
// through whichever provider the user connected and does not know. Per-run
|
|
87
|
+
// tokens and cost DO arrive (`step-finish`), and the adapter reports those;
|
|
88
|
+
// a plan limit is declared missing rather than faked as zero.
|
|
89
|
+
capabilities: { nativeCommands: true, fork: true, nativeReview: false, remoteControl: false, inlineImages: false, claudeEffort: false, planLimits: false },
|
|
90
|
+
imageInstruction: 'Open each one with the read tool, at the absolute path below.',
|
|
91
|
+
// ADMISSION IS A SEPARATE DECISION FROM INTEGRATION, and it is not ours.
|
|
92
|
+
// docs/harnesses.md: "A provider is not offered until that path works", and
|
|
93
|
+
// "a new provider's networking or permission scope requires the
|
|
94
|
+
// corresponding product policy review before admission." The transport is
|
|
95
|
+
// verified (see main/opencode-server.mjs for what was measured and how);
|
|
96
|
+
// whether OpenCode appears in the picker is a product call, so this stays
|
|
97
|
+
// false until somebody turns it on deliberately.
|
|
98
|
+
admitted: false,
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
export function harnessDefinition(id = 'claude') {
|
|
102
|
+
return HARNESS_DEFINITIONS.find(h => h.id === id) ?? null;
|
|
103
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Types for shared/notify-rules.mjs, which is plain ESM so that main (Node)
|
|
2
|
+
// and the screen (Vite) can share one copy of the rule about when Agentbox is
|
|
3
|
+
// allowed to interrupt you. The screen asks only `isNew`, which is the
|
|
4
|
+
// question before "are you away"; the rest is main's.
|
|
5
|
+
|
|
6
|
+
export const AWAY_AFTER_MS: number;
|
|
7
|
+
|
|
8
|
+
export function atTheApp(state?: { focused?: boolean; idleMs?: number }): boolean;
|
|
9
|
+
|
|
10
|
+
// `known` is the ids on the snapshot before this one, and null means there has
|
|
11
|
+
// not been one. Empty and never-looked are different facts, and the module
|
|
12
|
+
// says what reading one as the other cost.
|
|
13
|
+
export function isNew(known: ReadonlySet<string> | null | undefined, id: string): boolean;
|
|
14
|
+
|
|
15
|
+
export function clip(text: unknown, max?: number): string;
|
|
16
|
+
|
|
17
|
+
export function shouldSpeak(state?: {
|
|
18
|
+
spoke?: boolean;
|
|
19
|
+
spokeAsk?: boolean;
|
|
20
|
+
waiting?: ReadonlyArray<{ kind?: string } | null | undefined>;
|
|
21
|
+
}): boolean;
|
|
22
|
+
|
|
23
|
+
export function banner(
|
|
24
|
+
waiting: ReadonlyArray<{ id?: string; title?: unknown; kind?: string } | null | undefined> | null | undefined,
|
|
25
|
+
): { title: string; body: string; open: string | null } | null;
|
package/shared/notify-rules.mjs
CHANGED
|
@@ -31,6 +31,25 @@ export function atTheApp({ focused = false, idleMs = 0 } = {}) {
|
|
|
31
31
|
return !!focused && idleMs < AWAY_AFTER_MS;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// WHAT COUNTS AS NEWS, which is the question before "is she away".
|
|
35
|
+
//
|
|
36
|
+
// renderer/src/App.tsx owns the definition of the inbox and asks this of every
|
|
37
|
+
// row in it, against the ids it remembered from the snapshot before. `known` is
|
|
38
|
+
// those ids, and `null` means there has not been a snapshot yet: on the app's
|
|
39
|
+
// first look, everything waiting has been waiting, so none of it is a banner.
|
|
40
|
+
//
|
|
41
|
+
// EMPTY AND NEVER-LOOKED ARE DIFFERENT FACTS, and reading one as the other is
|
|
42
|
+
// what sent this feature's best moment into the bin. App.tsx used to decide it
|
|
43
|
+
// had never looked by asking whether the ids it remembered were empty, which is
|
|
44
|
+
// also true of inbox zero — the state the whole product aims at. So the
|
|
45
|
+
// arrival that most deserved to reach her, the first one after she got clear,
|
|
46
|
+
// was the one the app silently dropped before any rule here could see it.
|
|
47
|
+
// tests/the-first-arrival-after-inbox-zero-still-speaks.test.mjs.
|
|
48
|
+
export function isNew(known, id) {
|
|
49
|
+
if (!known) return false;
|
|
50
|
+
return !known.has(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
// A title long enough to be a paragraph is normal here; hers run past a
|
|
35
54
|
// hundred characters. macOS will hard-truncate mid-word, so cut on a space and
|
|
36
55
|
// say that it was cut.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// A command is an operation, never prose asking a model to imitate a CLI.
|
|
2
2
|
import { CLAUDE_COMMANDS, commandWords } from './claude-commands.mjs';
|
|
3
|
+
import { harnessDefinition } from './harness-definitions.mjs';
|
|
3
4
|
const command = (name, description, argumentHint = null, aliases = []) => ({ name, description, menuDescription: null, whole: true, argumentHint, aliases });
|
|
4
5
|
export const CODEX_COMMANDS = [
|
|
5
6
|
command('model','Show models or change this task’s model','[model]'),
|
|
@@ -23,12 +24,18 @@ export const LOCAL_COMMANDS = CODEX_COMMANDS.filter(c=>['model','effort','diff',
|
|
|
23
24
|
// is refused on Codex for now, in a sentence that says why, because that call
|
|
24
25
|
// has never been made against a real server here.
|
|
25
26
|
const FORK = command('fork','Start a new task from this conversation','<what to try instead>');
|
|
27
|
+
const COMMAND_MENUS = {
|
|
28
|
+
codex: CODEX_COMMANDS,
|
|
29
|
+
claude: [command('remote-control','Continue this conversation in Claude on another device',null,['rc']),FORK,...CLAUDE_COMMANDS.map(c=>LOCAL_COMMANDS.find(l=>l.name===c.name)??c),...LOCAL_COMMANDS.filter(l=>!CLAUDE_COMMANDS.some(c=>c.name===l.name))],
|
|
30
|
+
local: LOCAL_COMMANDS,
|
|
31
|
+
};
|
|
26
32
|
export function providerCommands(engine) {
|
|
27
33
|
// NOT ON THE CODEX MENU, because this menu's rule is that it offers nothing
|
|
28
34
|
// that engine cannot run, and Codex forking is not connected yet. Typed there
|
|
29
35
|
// it gets the ordinary unavailable refusal, which points at the native Codex
|
|
30
36
|
// client, where `codex fork` genuinely works.
|
|
31
|
-
|
|
37
|
+
const definition = harnessDefinition(engine) ?? harnessDefinition();
|
|
38
|
+
return COMMAND_MENUS[definition.commandMenu] ?? [];
|
|
32
39
|
}
|
|
33
40
|
export function providerCommand(text, engine) {
|
|
34
41
|
if(typeof text!=='string')return null;
|
|
@@ -38,7 +45,8 @@ export function providerCommand(text, engine) {
|
|
|
38
45
|
const cmd=providerCommands(engine).find(c=>commandWords(c).includes(word));
|
|
39
46
|
if(!cmd) return {name:word,args,route:'unavailable'};
|
|
40
47
|
if(cmd.name==='fork')return {name:'fork',args,route:'fork'};
|
|
41
|
-
|
|
48
|
+
const route = (harnessDefinition(engine) ?? harnessDefinition()).commandRoute;
|
|
49
|
+
return {name:cmd.name,args,route:cmd.name==='remote-control'?'remote':route==='codex'?'codex':LOCAL_COMMANDS.some(c=>c.name===cmd.name)?'local':route};
|
|
42
50
|
}
|
|
43
51
|
export function reviewTarget(args='') {
|
|
44
52
|
args=args.trim();
|
package/shared/rank.d.mts
CHANGED
|
@@ -18,3 +18,5 @@ export function landingIndex(
|
|
|
18
18
|
x: number, y: number,
|
|
19
19
|
): number;
|
|
20
20
|
export function moveProduct(order: string[], slug: string, toIndex: number): string[];
|
|
21
|
+
export function placedScore(place: number | null | undefined, natural: number): number;
|
|
22
|
+
export function dropPlaces(rows: Array<{ id: string; score: number }>, from: number, to: number): Record<string, number> | null;
|
package/shared/rank.mjs
CHANGED
|
@@ -155,6 +155,56 @@ export function landingIndex(rects, x, y) {
|
|
|
155
155
|
return rects.length;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* A THREAD'S OWN PLACE, SET BY DRAGGING IT IN THE LIST (w-6e5b532a95,
|
|
160
|
+
* 2026-10-07). Her words: "sometimes there are tasks that are higher priority
|
|
161
|
+
* than other projects and it's hard to articulate. I'd rather be able to drag
|
|
162
|
+
* things around here ... and have that priority remembered and that's how the
|
|
163
|
+
* app processes it." A project's place and a thread's level could never say
|
|
164
|
+
* that one Low thread matters more today than a whole project above it.
|
|
165
|
+
*
|
|
166
|
+
* So a dragged thread carries a PLACE: a number on the same scale as the score
|
|
167
|
+
* it would otherwise have, which replaces that score outright. It is a number
|
|
168
|
+
* and not a list of ids so that everything it was not dragged past keeps
|
|
169
|
+
* sorting the way it always did: a new Urgent thread still lands by its level,
|
|
170
|
+
* next to the placed ones rather than under all of them.
|
|
171
|
+
*/
|
|
172
|
+
export function placedScore(place, natural) {
|
|
173
|
+
return Number.isFinite(place) ? place : natural;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The places a drop writes. `rows` is the list as drawn, top first, each with
|
|
178
|
+
* the score it sorts by (its place, or its project and level); `from` is the
|
|
179
|
+
* dragged row and `to` the row it was let go in front of, or the length for
|
|
180
|
+
* the end, both counted in `rows`. Null when it lands where it was.
|
|
181
|
+
*
|
|
182
|
+
* The dragged row goes between its new neighbours. When those two TIE (two
|
|
183
|
+
* Medium threads in one project, whose order is only which changed last) no
|
|
184
|
+
* number fits between them, so the tied rows above the drop are given places
|
|
185
|
+
* of their own first, spread in the order they were drawn. The rows below are
|
|
186
|
+
* never written.
|
|
187
|
+
*/
|
|
188
|
+
export function dropPlaces(rows, from, to) {
|
|
189
|
+
if (!Array.isArray(rows) || from < 0 || from >= rows.length) return null;
|
|
190
|
+
const rest = rows.filter((_, i) => i !== from);
|
|
191
|
+
const at = Math.max(0, Math.min(rest.length, to > from ? to - 1 : to));
|
|
192
|
+
if (at === from || !rest.length) return null;
|
|
193
|
+
const close = (a, b) => Math.abs(a - b) < 1e-6;
|
|
194
|
+
const below = rest[at]?.score;
|
|
195
|
+
const floor = below ?? rest[at - 1].score - 2;
|
|
196
|
+
// The run of rows just above the drop that tie with the floor.
|
|
197
|
+
let top = at;
|
|
198
|
+
while (top > 0 && close(rest[top - 1].score, floor)) top -= 1;
|
|
199
|
+
const ceiling = top > 0 ? rest[top - 1].score : floor + 2;
|
|
200
|
+
const run = rest.slice(top, at);
|
|
201
|
+
const step = (ceiling - floor) / (run.length + 2);
|
|
202
|
+
const out = {};
|
|
203
|
+
run.forEach((r, j) => { out[r.id] = floor + (run.length + 1 - j) * step; });
|
|
204
|
+
out[rows[from].id] = floor + step;
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
158
208
|
/**
|
|
159
209
|
* Move one product to a new index, returning a fresh order. Products that were
|
|
160
210
|
* never in the order join it on their first move, so dragging an unranked chip
|