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
package/main/ipc.mjs
CHANGED
|
@@ -40,15 +40,16 @@ import { readDoc, writeDoc } from './doc-file.mjs';
|
|
|
40
40
|
import { readChange } from './code-change.mjs';
|
|
41
41
|
import { findFile, readCodeFile, writeCodeFile } from './code-file.mjs';
|
|
42
42
|
import { readNote, writeNote } from './rail-note.mjs';
|
|
43
|
-
import { addClaudeAccount, addCodexAccount, outsideAgentsMode, permissionMode, readSettings, recheckClaude, recheckCodex, setProjectSetting, setWorkspaceSetting } from './settings.mjs';
|
|
43
|
+
import { addClaudeAccount, addCodexAccount, outsideAgentsMode, permissionMode, pointAtEngineBin, readSettings, recheckClaude, recheckCodex, searchForEngineBin, setProjectSetting, setWorkspaceSetting } from './settings.mjs';
|
|
44
44
|
import { ICON_KINDS, clearProjectIcon, setProjectIcon, setProjectName } from './project-identity.mjs';
|
|
45
45
|
import { ClaudeUsage } from './claude-usage.mjs';
|
|
46
46
|
import { usageEngine } from '../shared/usage.mjs';
|
|
47
|
-
import { DEFAULT_ENGINE } from '../shared/engines.mjs';
|
|
47
|
+
import { DEFAULT_ENGINE, isEngine } from '../shared/engines.mjs';
|
|
48
48
|
import * as agents from './agents.mjs';
|
|
49
49
|
import { findAgentFolders, readAgentFiles, readFolderAgents } from './agent-files.mjs';
|
|
50
|
-
import { readSessionThreads } from './agent-sessions.mjs';
|
|
51
50
|
import { readCodexThreads } from './codex-threads.mjs';
|
|
51
|
+
import { appSessionFolders } from './agent-homes.mjs';
|
|
52
|
+
import { readImportableThreads } from './importable-threads.mjs';
|
|
52
53
|
import { markImported } from '../shared/agent-import.mjs';
|
|
53
54
|
import { codexIdOf, importChoice, isCodexImportRow, isNotImportedRow } from '../shared/codex-import.mjs';
|
|
54
55
|
import { checkProjectFolder } from '../shared/project-folder-check.mjs';
|
|
@@ -91,6 +92,9 @@ const NO_UPDATER = {
|
|
|
91
92
|
}),
|
|
92
93
|
check: async () => NO_UPDATER.state(),
|
|
93
94
|
install: () => false,
|
|
95
|
+
// Nowhere to move to and nothing to move: a copy with no updater behind it
|
|
96
|
+
// is a copy run from source (shared/where-it-runs-from.mjs).
|
|
97
|
+
move: () => ({ moved: false, error: null }),
|
|
94
98
|
};
|
|
95
99
|
|
|
96
100
|
export function registerIpc({ store, supervisor, config, window, analytics = NO_ANALYTICS, docGrants = null, updater = NO_UPDATER, host, team = null }) {
|
|
@@ -259,9 +263,17 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
259
263
|
// while the app is running and the settings screen already promises that
|
|
260
264
|
// works without a restart.
|
|
261
265
|
bin: () => config.claudeBin,
|
|
266
|
+
// The account marked In use, asked each time so a switch is read at once.
|
|
267
|
+
account: () => supervisor.usageAccount(),
|
|
262
268
|
onChange: push,
|
|
263
269
|
});
|
|
264
270
|
|
|
271
|
+
ipcMain.handle('zero:refresh-usage', async event => {
|
|
272
|
+
if (event.senderFrame?.parent || (event.sender && event.sender !== window.webContents)) throw Error('Usage refresh is only available in the main app window.');
|
|
273
|
+
const result = await usage.refreshNow();
|
|
274
|
+
return result.reading ? { ...result, reading: { engine: DEFAULT_ENGINE, ...result.reading } } : result;
|
|
275
|
+
});
|
|
276
|
+
|
|
265
277
|
// Agents this run has already counted. In memory on purpose: a fresh launch
|
|
266
278
|
// seeing the same agent again is a fresh fact about the app being used.
|
|
267
279
|
const seenAgents = new Set();
|
|
@@ -279,11 +291,17 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
279
291
|
// send one event per agent per tick, all day. The keys are only ever
|
|
280
292
|
// compared in this process; nothing about an agent is sent with the
|
|
281
293
|
// count, which carries no name, no path and no title.
|
|
294
|
+
//
|
|
295
|
+
// AND WHICH ENGINE IT WAS (w-1116fbb68a, 2026-10-08). This list is Claude
|
|
296
|
+
// Code processes and only ever has been; a Codex conversation has no
|
|
297
|
+
// process to find and is counted from main/codex-watch.mjs instead. The
|
|
298
|
+
// engine is what lets the two be read apart, and without it on this side
|
|
299
|
+
// the Codex count would look like a drop in the Claude one.
|
|
282
300
|
for (const agent of live) {
|
|
283
301
|
const key = agentKey(agent);
|
|
284
302
|
if (!key || seenAgents.has(key)) continue;
|
|
285
303
|
seenAgents.add(key);
|
|
286
|
-
analytics.track('agent_seen');
|
|
304
|
+
analytics.track('agent_seen', { engine: 'claude' });
|
|
287
305
|
}
|
|
288
306
|
return live;
|
|
289
307
|
} catch {
|
|
@@ -341,13 +359,13 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
341
359
|
});
|
|
342
360
|
ipcMain.handle('zero:terminal',(_event,payload={})=>{
|
|
343
361
|
if(_event.senderFrame?.parent || (_event.sender && _event.sender!==window.webContents)) throw Error('Terminal is only available in the main app window.');
|
|
344
|
-
const {product,id,action,data,cols,rows,offset}=payload;
|
|
362
|
+
const {product,id,action,data,cols,rows,offset,wait}=payload;
|
|
345
363
|
if(typeof product!=='string'||typeof id!=='string'||!id||id.length>300)throw Error('Invalid task.');
|
|
346
364
|
const key=JSON.stringify({product,id});
|
|
347
365
|
if(product==='@agent-update')return agentUpdates.terminal(id,payload);
|
|
348
366
|
switch(action){
|
|
349
367
|
case 'open':return terminals.open(key);
|
|
350
|
-
case 'read':return terminals.read(key,offset);
|
|
368
|
+
case 'read':return terminals.read(key,offset,wait);
|
|
351
369
|
case 'write':terminals.write(key,data);return true;
|
|
352
370
|
case 'resize':terminals.resize(key,cols,rows);return true;
|
|
353
371
|
case 'close':terminals.close(key);return true;
|
|
@@ -360,6 +378,21 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
360
378
|
ipcMain.handle('zero:older-items', (_e, { offset = 0, limit = 50 } = {}) =>
|
|
361
379
|
store.listOlderItems({ offset: Number(offset) || 0, limit: Math.min(Number(limit) || 50, 200) }));
|
|
362
380
|
|
|
381
|
+
// IS ANYBODY ACTUALLY USING A SCREEN READER. Asked by the terminal pane
|
|
382
|
+
// before it builds an xterm, because xterm's accessibility tree is what
|
|
383
|
+
// `screenReaderMode` turns on and we used to turn it on for everybody.
|
|
384
|
+
// Electron's own words beside this property: "Rendering accessibility tree
|
|
385
|
+
// can significantly affect the performance of your app. It should not be
|
|
386
|
+
// enabled by default." It also carried xterm's 'invalid range' bug to an
|
|
387
|
+
// install that had no use for it (one crash report, 2026-10-08).
|
|
388
|
+
//
|
|
389
|
+
// Absent on Linux and before `ready`, where absent means no, and reading it
|
|
390
|
+
// may not throw: this is on the path that opens a terminal, and a terminal
|
|
391
|
+
// that cannot ask the question still has to open.
|
|
392
|
+
ipcMain.handle('zero:assistiveTech', () => {
|
|
393
|
+
try { return !!host.app?.accessibilitySupportEnabled; } catch { return false; }
|
|
394
|
+
});
|
|
395
|
+
|
|
363
396
|
ipcMain.handle('zero:snapshot', () => {
|
|
364
397
|
const now = Date.now();
|
|
365
398
|
const items = store.listItems(now);
|
|
@@ -379,7 +412,8 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
379
412
|
const reading = usageFor === DEFAULT_ENGINE ? usage.read() : supervisor.codexUsage();
|
|
380
413
|
return {
|
|
381
414
|
products: store.listProducts(),
|
|
382
|
-
|
|
415
|
+
// Each with the place she dragged it to, if she did (w-6e5b532a95).
|
|
416
|
+
items: supervisor.withPlaces(items),
|
|
383
417
|
// THE TEAM: who is signed in, their team and its people, and the
|
|
384
418
|
// title-free activity of their private work. Null on a build with no
|
|
385
419
|
// team cloud, which is the single-person app.
|
|
@@ -400,16 +434,16 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
400
434
|
// coding agent this is one entry and an empty map, and nothing anywhere
|
|
401
435
|
// draws anything new.
|
|
402
436
|
engines,
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
// spends it in silence.
|
|
437
|
+
// WHETHER THIS MAC WAS ALREADY SIGNED INTO SOMETHING (w-e217e577e5,
|
|
438
|
+
// 2026-10-07), so the walk can say so once. A new user ran an agent and
|
|
439
|
+
// could not tell where the tokens were coming from, because the app finds
|
|
440
|
+
// whichever sign-in is already on the Mac and spends it in silence.
|
|
407
441
|
//
|
|
408
|
-
// It rides the snapshot rather than the settings model because the
|
|
409
|
-
// reads it,
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
442
|
+
// It rides the snapshot rather than the settings model because the WALK
|
|
443
|
+
// reads it, and the settings model is only fetched while the settings
|
|
444
|
+
// screen is open. It costs no file read per tick: `main/runs-on.mjs`
|
|
445
|
+
// caches on the login files' own modification times, so this re-parses
|
|
446
|
+
// only after a sign-in or a profile refetch.
|
|
413
447
|
runsOn: supervisor.runsOnAccount(engines.workspace),
|
|
414
448
|
// Whether this process is still the app on disk. It rides the snapshot
|
|
415
449
|
// rather than boot-info because the staleness DEVELOPS while the app
|
|
@@ -480,7 +514,19 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
480
514
|
// it and the user's own sentence never reached the card: the row went on
|
|
481
515
|
// saying only what the session was started with days before. This is the
|
|
482
516
|
// only process that knows the user said it, so this is where it is written.
|
|
483
|
-
|
|
517
|
+
//
|
|
518
|
+
// AND IT IS A REPLY, SO IT IS COUNTED (w-1116fbb68a, 2026-10-08). A reply
|
|
519
|
+
// on a task has always sent `reply_sent`; one typed into a live session
|
|
520
|
+
// sent nothing at all, and activation is read as three real replies to
|
|
521
|
+
// agents in the first day, so the people who talk to their own sessions
|
|
522
|
+
// rather than through a row were missing from it. `kind` says which of
|
|
523
|
+
// the two this was. It waits on the same delivery the mark does, and for
|
|
524
|
+
// the same reason: a count for words that never arrived would read as
|
|
525
|
+
// activation nobody had.
|
|
526
|
+
if (out?.ok) {
|
|
527
|
+
if (out.key) agentSchedule.replied(out.key, out.at ?? Date.now(), Date.now(), String(text ?? ''));
|
|
528
|
+
analytics.track('reply_sent', { kind: 'agent' });
|
|
529
|
+
}
|
|
484
530
|
push();
|
|
485
531
|
return out;
|
|
486
532
|
} catch {
|
|
@@ -546,6 +592,12 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
546
592
|
|
|
547
593
|
ipcMain.handle('zero:answer', async (_e, { product, id, answer, status, priority, permissionMode, model, effort, now, inReplyTo }) => {
|
|
548
594
|
if (isAgentRow(id)) return { ok: false };
|
|
595
|
+
// A level picked from ⌘K arrives here with no words, and gives way the
|
|
596
|
+
// same as one picked on the summary (zero:thread-edit). A reply that
|
|
597
|
+
// happens to carry its row's level does not.
|
|
598
|
+
if (priority != null && !answer && id) supervisor.setThreadPlaces({ [id]: null });
|
|
599
|
+
// CLOSING A THREAD IS THE END OF WHAT IT WAS PINNED FOR (w-20323283e6).
|
|
600
|
+
if (id && status === 'done') supervisor.setThreadPins({ [id]: null });
|
|
549
601
|
// A REPLY IN A THREAD (w-920461cbe6) only means something in a conversation
|
|
550
602
|
// between people, which is the only place a thread is drawn and the only
|
|
551
603
|
// record the mark crosses to a teammate in (shared/team-rules.mjs). Anywhere
|
|
@@ -564,7 +616,10 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
564
616
|
if (choice) {
|
|
565
617
|
let thread = null;
|
|
566
618
|
if (choice === 'yes') {
|
|
567
|
-
try {
|
|
619
|
+
try {
|
|
620
|
+
const { codexDirs } = appSessionFolders({ config, supervisor });
|
|
621
|
+
thread = readCodexThreads({ codexDirs }).threads.find((t) => t.id === codexIdOf(row)) ?? null;
|
|
622
|
+
} catch { thread = null; }
|
|
568
623
|
}
|
|
569
624
|
const out = store.answerCodexImport(product, id, choice, { thread });
|
|
570
625
|
push();
|
|
@@ -610,6 +665,21 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
610
665
|
return supervisor.status();
|
|
611
666
|
});
|
|
612
667
|
|
|
668
|
+
// A THREAD DRAGGED TO A PLACE OF ITS OWN in the list (w-6e5b532a95): the
|
|
669
|
+
// places the drop worked out (`dropPlaces`, shared/rank.mjs), id to number.
|
|
670
|
+
ipcMain.handle('zero:set-thread-places', (_e, { places }) => {
|
|
671
|
+
supervisor.setThreadPlaces(places);
|
|
672
|
+
push();
|
|
673
|
+
return { ok: true };
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// PINNED THREADS (w-20323283e6): id to true, or null to unpin.
|
|
677
|
+
ipcMain.handle('zero:set-thread-pins', (_e, { pins }) => {
|
|
678
|
+
supervisor.setThreadPins(pins);
|
|
679
|
+
push();
|
|
680
|
+
return { ok: true };
|
|
681
|
+
});
|
|
682
|
+
|
|
613
683
|
// Hidden from the composer's picker and nothing else: a hidden product still
|
|
614
684
|
// ranks, still spawns workers, and its work still reaches the inbox.
|
|
615
685
|
ipcMain.handle('zero:set-product-hidden', (_e, { product, hidden }) => {
|
|
@@ -696,6 +766,9 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
696
766
|
ipcMain.handle('zero:thread-edit', (_e, { product, id, patch }) => {
|
|
697
767
|
try {
|
|
698
768
|
store.threadEdit(product, id, patch);
|
|
769
|
+
// PICKING A LEVEL IS THE NEWER WORD on where a thread goes, so a place
|
|
770
|
+
// it was dragged to gives way and it sorts by its level again.
|
|
771
|
+
if (patch?.priority != null) supervisor.setThreadPlaces({ [id]: null });
|
|
699
772
|
push();
|
|
700
773
|
return { ok: true };
|
|
701
774
|
} catch (err) {
|
|
@@ -938,11 +1011,18 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
938
1011
|
// an archive, so it does not collide with what done means on a work item.
|
|
939
1012
|
ipcMain.handle('zero:end-repeat', (_e, { product, id }) => store.endRepeat(product, id));
|
|
940
1013
|
|
|
941
|
-
ipcMain.handle('zero:create-product', (_e, { name, repoPath }) => {
|
|
1014
|
+
ipcMain.handle('zero:create-product', (_e, { name, repoPath, ofMany }) => {
|
|
942
1015
|
const out = store.createProduct({ name, repoPath });
|
|
943
1016
|
// (privacy page 5.1). The count is the whole payload; the path it was
|
|
944
1017
|
// connected to never leaves.
|
|
945
|
-
|
|
1018
|
+
//
|
|
1019
|
+
// AND A PRESS THAT CONNECTS NINE FOLDERS IS ONE THING SOMEBODY DID, NOT
|
|
1020
|
+
// NINE (w-1116fbb68a, 2026-10-08). The import makes a project per folder,
|
|
1021
|
+
// so this handler fired nine times for one press and nine events read as
|
|
1022
|
+
// nine separate connections to anybody looking at them as activity rather
|
|
1023
|
+
// than as a funnel step. `ofMany` is the caller saying it will count the
|
|
1024
|
+
// press itself, once, with how many folders it connected.
|
|
1025
|
+
if (!ofMany && typeof repoPath === 'string' && repoPath.trim()) analytics.track('repo_connected');
|
|
946
1026
|
return out;
|
|
947
1027
|
});
|
|
948
1028
|
|
|
@@ -1082,13 +1162,10 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1082
1162
|
// 'codex'`, land under the project whose folder they ran in exactly as the
|
|
1083
1163
|
// Claude Code ones do, and the ones the user said not now to are here too, so
|
|
1084
1164
|
// there is a way back to them.
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
try { ({ threads: codex } = readCodexThreads({})); } catch { codex = []; }
|
|
1090
|
-
return [...claude, ...codex].sort((a, b) => (b.when ?? 0) - (a.when ?? 0));
|
|
1091
|
-
};
|
|
1165
|
+
// EVERY FOLDER EITHER TOOL KEEPS SESSIONS IN (main/agent-homes.mjs), not only
|
|
1166
|
+
// ~/.claude and ~/.codex, which is where the import used to look and nothing
|
|
1167
|
+
// else (w-e46f2e9cea).
|
|
1168
|
+
const readAllThreads = () => readImportableThreads({ config, supervisor });
|
|
1092
1169
|
ipcMain.handle('zero:agent-threads', () => {
|
|
1093
1170
|
const threads = readAllThreads();
|
|
1094
1171
|
lastThreads = threads;
|
|
@@ -1315,15 +1392,64 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1315
1392
|
}
|
|
1316
1393
|
});
|
|
1317
1394
|
|
|
1395
|
+
/*
|
|
1396
|
+
* AND WHEN NEITHER BUTTON CAN FIND IT: POINT AT THE FILE YOURSELF
|
|
1397
|
+
* (w-ce8c1a9275). The search asks the login shell first now, which covers
|
|
1398
|
+
* every installer a person's own PATH knows about -- but a wrapper script
|
|
1399
|
+
* somewhere nobody would guess, a tool on a volume, or a machine whose login
|
|
1400
|
+
* shell will not start at all still used to mean a path WE ship and a release
|
|
1401
|
+
* THEY wait for. This is the half that ends that.
|
|
1402
|
+
*
|
|
1403
|
+
* ONE ROUND TRIP, like `zero:project-icon` below and for the same reason: the
|
|
1404
|
+
* panel opens in this process and the choice is applied in the same call, so
|
|
1405
|
+
* the only path that ever reaches `pointAtEngineBin` is one the open panel
|
|
1406
|
+
* itself produced. The window says WHICH coding agent it is asking about and
|
|
1407
|
+
* nothing else, which is the whole of what it is trusted with -- `codexBin`
|
|
1408
|
+
* is a path that gets spawned, and every worker on this machine has a broad
|
|
1409
|
+
* Bash allow (tests/where-codex-runs-from-is-not-a-file-a-worker-can-write).
|
|
1410
|
+
*
|
|
1411
|
+
* THE REFUSAL IS A SENTENCE AND NOT AN ERROR. Picking a folder, or a file
|
|
1412
|
+
* that cannot be run, is an ordinary mistake at a file panel; it comes back
|
|
1413
|
+
* as words the row prints, the way `zero:choose-folder` already does.
|
|
1414
|
+
*/
|
|
1415
|
+
ipcMain.handle('zero:engine-bin', async (_e, { engine, action } = {}) => {
|
|
1416
|
+
if (engine !== 'claude' && engine !== 'codex') return { ok: false, error: 'Unknown coding agent.' };
|
|
1417
|
+
try {
|
|
1418
|
+
// Handing the choice back needs no panel: it is the row saying "look for
|
|
1419
|
+
// it again", and the search takes over from there.
|
|
1420
|
+
if (action === 'search') {
|
|
1421
|
+
const gave = searchForEngineBin(config, engine);
|
|
1422
|
+
if (!gave.ok) return { ok: false, refused: gave.say };
|
|
1423
|
+
return { ok: true, ...readSettings({ config, supervisor, store }) };
|
|
1424
|
+
}
|
|
1425
|
+
const { dialog } = await import('electron');
|
|
1426
|
+
const r = await dialog.showOpenDialog(window, {
|
|
1427
|
+
title: engine === 'codex' ? 'Where is Codex?' : 'Where is Claude Code?',
|
|
1428
|
+
buttonLabel: 'Use this one',
|
|
1429
|
+
properties: ['openFile', 'showHiddenFiles', 'treatPackageAsDirectory'],
|
|
1430
|
+
});
|
|
1431
|
+
// Cancelling is not a failure and must not draw one. `ok` with nothing
|
|
1432
|
+
// changed is the honest answer: they opened the panel and closed it again.
|
|
1433
|
+
if (r.canceled || !r.filePaths?.length) return { ok: true, canceled: true };
|
|
1434
|
+
const took = pointAtEngineBin(config, engine, r.filePaths[0]);
|
|
1435
|
+
if (!took.ok) return { ok: false, refused: took.say };
|
|
1436
|
+
return { ok: true, ...readSettings({ config, supervisor, store }) };
|
|
1437
|
+
} catch (err) {
|
|
1438
|
+
return { ok: false, error: String(err.message) };
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1318
1442
|
// SETTING UP THEIR PLAN FOR THEM (w-9f6975906c). The walk asks which plan
|
|
1319
1443
|
// they pay for and this installs the tool and starts its own sign-in. The
|
|
1320
1444
|
// finder is the recheck, so what gets installed lands on the config and the
|
|
1321
1445
|
// rest of the app runs on it without a restart.
|
|
1322
1446
|
const engineSetup = createEngineSetup({
|
|
1323
1447
|
find: (engine) => {
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1448
|
+
const finders = {
|
|
1449
|
+
claude: () => { const s = recheckClaude(config); return { found: s.claudeFound, path: s.claudeBin }; },
|
|
1450
|
+
codex: () => { const s = recheckCodex(config); return { found: s.found, path: s.bin }; },
|
|
1451
|
+
};
|
|
1452
|
+
return finders[engine]?.() ?? { found: false, path: null };
|
|
1327
1453
|
},
|
|
1328
1454
|
// WHICH ONE IS SIGNED IN, kept on the config for the run that reads it
|
|
1329
1455
|
// (`Supervisor#_enginesFound`) and never saved: it changes outside the app.
|
|
@@ -1342,7 +1468,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1342
1468
|
// A sign-in left waiting on the browser is not left running after the app.
|
|
1343
1469
|
app.on('will-quit', () => { engineSetup.cancel('claude'); engineSetup.cancel('codex'); });
|
|
1344
1470
|
ipcMain.handle('zero:engine-setup', async (_e, { action, engine } = {}) => {
|
|
1345
|
-
if (engine
|
|
1471
|
+
if (!isEngine(engine)) return { ok: false, error: 'Unknown coding agent.' };
|
|
1346
1472
|
try {
|
|
1347
1473
|
if (action === 'ready') return { ok: true, ...(await engineSetup.readiness(engine)) };
|
|
1348
1474
|
if (action === 'start') return { ok: true, ...engineSetup.start(engine) };
|
|
@@ -1842,6 +1968,14 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1842
1968
|
// to decide is when the app is allowed to disappear and come back.
|
|
1843
1969
|
ipcMain.handle('zero:update-check', async () => updater.check({ manual: true }));
|
|
1844
1970
|
ipcMain.handle('zero:update-install', () => ({ started: updater.install() }));
|
|
1971
|
+
// AND A THIRD, for the copy that cannot update itself from where it is: move
|
|
1972
|
+
// the app into Applications (shared/where-it-runs-from.mjs). It is a door of
|
|
1973
|
+
// its own rather than part of the install, because it is the opposite
|
|
1974
|
+
// decision — nothing has been downloaded and nothing will be until the app
|
|
1975
|
+
// is somewhere macOS lets it write.
|
|
1976
|
+
// A copy run from source answers no to it rather than throwing: there is no
|
|
1977
|
+
// bundle to move (main/source-updater.mjs).
|
|
1978
|
+
ipcMain.handle('zero:update-move', () => updater.move?.() ?? { moved: false, error: null });
|
|
1845
1979
|
|
|
1846
1980
|
// Handed back so the Cmd+Y/Cmd+N chord answers through the same door the
|
|
1847
1981
|
// buttons do, rather than reaching past it into approvals.answer.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// A LOCAL APP SHOWS IN THE PANE AND STAYS SIGNED IN.
|
|
2
|
+
//
|
|
3
|
+
// The pane draws an address like http://localhost:3006 in a frame, because a
|
|
4
|
+
// frame keeps every shortcut the app answers from inside a page (the
|
|
5
|
+
// before-input-event handler in main.mjs hears a frame; it would not hear a
|
|
6
|
+
// webview's guest). Two things a local app commonly sends broke that frame:
|
|
7
|
+
//
|
|
8
|
+
// X-Frame-Options / CSP frame-ancestors Chromium refuses to draw the page
|
|
9
|
+
// at all, so the pane sat blank.
|
|
10
|
+
// Next.js apps send both.
|
|
11
|
+
// a SameSite=Lax sign-in cookie the window is a file:// page, so
|
|
12
|
+
// the app in it is cross-site and the
|
|
13
|
+
// cookie is refused: sign-in loops.
|
|
14
|
+
//
|
|
15
|
+
// Both are rewritten here, below the page, for local addresses only and only
|
|
16
|
+
// inside the app's own window. Framing protection exists to stop another site
|
|
17
|
+
// drawing yours under a stranger's pointer; the app drawing her own dev server
|
|
18
|
+
// for her is not that. tests/a-local-app-shows-in-the-pane-and-stays-signed-in.test.mjs
|
|
19
|
+
|
|
20
|
+
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
21
|
+
|
|
22
|
+
export function isLocalAddress(url) {
|
|
23
|
+
try {
|
|
24
|
+
const u = new URL(url);
|
|
25
|
+
return (u.protocol === 'http:' || u.protocol === 'https:') && LOCAL_HOSTS.has(u.hostname);
|
|
26
|
+
} catch { return false; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const keyOf = (headers, name) => Object.keys(headers).find((k) => k.toLowerCase() === name);
|
|
30
|
+
|
|
31
|
+
function withoutFrameAncestors(policy) {
|
|
32
|
+
return policy.split(';').map((d) => d.trim()).filter((d) => d && !/^frame-ancestors(\s|$)/i.test(d)).join('; ');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// SameSite=None needs Secure, and Chromium takes Secure from http://localhost
|
|
36
|
+
// because it counts loopback as a secure origin.
|
|
37
|
+
function sentInsideTheWindow(cookie) {
|
|
38
|
+
const parts = cookie.split(';').map((p) => p.trim());
|
|
39
|
+
if (parts.some((p) => /^samesite\s*=\s*none$/i.test(p))) return cookie;
|
|
40
|
+
const kept = parts.filter((p) => !/^samesite\s*=/i.test(p));
|
|
41
|
+
if (!kept.some((p) => /^secure$/i.test(p))) kept.push('SameSite=None', 'Secure');
|
|
42
|
+
else kept.push('SameSite=None');
|
|
43
|
+
return kept.join('; ');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The headers a local app's response should carry inside the pane, or null
|
|
48
|
+
* when nothing changes. `details` is Electron's onHeadersReceived details.
|
|
49
|
+
*/
|
|
50
|
+
export function localAppHeaders({ url, resourceType, responseHeaders }) {
|
|
51
|
+
if (!responseHeaders || resourceType === 'mainFrame' || !isLocalAddress(url)) return null;
|
|
52
|
+
const out = { ...responseHeaders };
|
|
53
|
+
let changed = false;
|
|
54
|
+
if (resourceType === 'subFrame') {
|
|
55
|
+
const xfo = keyOf(out, 'x-frame-options');
|
|
56
|
+
if (xfo) { delete out[xfo]; changed = true; }
|
|
57
|
+
const csp = keyOf(out, 'content-security-policy');
|
|
58
|
+
if (csp) {
|
|
59
|
+
const policies = out[csp].map(withoutFrameAncestors).filter(Boolean);
|
|
60
|
+
if (policies.join() !== out[csp].join()) {
|
|
61
|
+
changed = true;
|
|
62
|
+
if (policies.length) out[csp] = policies; else delete out[csp];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const cookies = keyOf(out, 'set-cookie');
|
|
67
|
+
if (cookies) {
|
|
68
|
+
const next = out[cookies].map(sentInsideTheWindow);
|
|
69
|
+
if (next.join('\n') !== out[cookies].join('\n')) { out[cookies] = next; changed = true; }
|
|
70
|
+
}
|
|
71
|
+
return changed ? out : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Wire it to the window's own session, for that window's requests only. */
|
|
75
|
+
export function letLocalAppsShowInThePane(contents) {
|
|
76
|
+
const filter = { urls: ['*://localhost/*', '*://127.0.0.1/*', '*://[::1]/*'] };
|
|
77
|
+
contents.session.webRequest.onHeadersReceived(filter, (details, callback) => {
|
|
78
|
+
const ours = details.webContentsId === contents.id;
|
|
79
|
+
const responseHeaders = ours ? localAppHeaders(details) : null;
|
|
80
|
+
callback(responseHeaders ? { responseHeaders } : {});
|
|
81
|
+
});
|
|
82
|
+
}
|
package/main/main.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { ensurePersonalProject } from './team/projects.mjs';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
14
|
-
import { configDir, loadConfig } from './config.mjs';
|
|
14
|
+
import { configDir, loadConfig, saveConfig } from './config.mjs';
|
|
15
15
|
import { installMenu, applyZoom, requestFind } from './menu.mjs';
|
|
16
16
|
import { zoomDeltaFor } from './zoom-keys.mjs';
|
|
17
17
|
import { copySelection } from './copy-selection.mjs';
|
|
@@ -20,7 +20,13 @@ import { Store } from './store.mjs';
|
|
|
20
20
|
import { storeRootEnv } from './store/home.mjs';
|
|
21
21
|
import { Supervisor } from './supervisor.mjs';
|
|
22
22
|
import { startCodexWatch } from './codex-watch.mjs';
|
|
23
|
+
import { readCodexThreads } from './codex-threads.mjs';
|
|
24
|
+
import { appSessionFolders } from './agent-homes.mjs';
|
|
23
25
|
import { registerIpc } from './ipc.mjs';
|
|
26
|
+
import { createPhoneLink } from './phone-link.mjs';
|
|
27
|
+
import { relayAddress } from './relay-address.mjs';
|
|
28
|
+
import { atTheApp } from '../shared/notify-rules.mjs';
|
|
29
|
+
import { answerWhatOnlyTheWindowAnswered } from './serve.mjs';
|
|
24
30
|
import { carryHerBriefsAcross, joinMessageRules, setAsideShippedMessageRules, unifyAgentInstructions } from './instruction-settings.mjs';
|
|
25
31
|
import * as approvals from './approvals.mjs';
|
|
26
32
|
import { recoveryToast } from '../shared/recovery.mjs';
|
|
@@ -30,10 +36,14 @@ import { openFreshUser } from './fresh-user.mjs';
|
|
|
30
36
|
import { installCrashReports, reportFromRenderer, pending as pendingCrashes, setTransport } from './crash-report.mjs';
|
|
31
37
|
import { createAnalytics, createDailyCount } from './analytics.mjs';
|
|
32
38
|
import { createUpdater } from './updater.mjs';
|
|
39
|
+
import { createPlace } from '../shared/where-it-runs-from.mjs';
|
|
33
40
|
import { createSourceUpdater } from './source-updater.mjs';
|
|
41
|
+
import { learnShellDataDirs, recoverShellPath } from './shell-path.mjs';
|
|
34
42
|
import { installNotifier } from './notify.mjs';
|
|
43
|
+
import { createCornerTag } from './corner-tag.mjs';
|
|
35
44
|
import { DOC_SCHEMES, DocGrants, docPath } from './doc-scheme.mjs';
|
|
36
45
|
import { quietTheFramesScrollbars } from './frame-scrollbars.mjs';
|
|
46
|
+
import { letLocalAppsShowInThePane } from './local-app-frames.mjs';
|
|
37
47
|
import { IMG_SCHEMES, imgPath, mediaResponse, mediaType, servable } from './img-scheme.mjs';
|
|
38
48
|
import { hotWindowVerdict, storeHasWork } from './dev-window.mjs';
|
|
39
49
|
import { writeHeldThenReload } from './write-before-reload.mjs';
|
|
@@ -275,6 +285,13 @@ function reloadRenderer() {
|
|
|
275
285
|
}
|
|
276
286
|
|
|
277
287
|
async function createWindow() {
|
|
288
|
+
const shellPath = await recoverShellPath();
|
|
289
|
+
process.env.PATH = shellPath.path;
|
|
290
|
+
if (shellPath.error) console.warn('agentbox: could not load your shell PATH; using the inherited PATH:', shellPath.error);
|
|
291
|
+
// Where the shell says Claude Code and Codex keep their sessions, for the
|
|
292
|
+
// import only and never into this environment (main/shell-path.mjs). Not
|
|
293
|
+
// waited for: it lands long before setup reaches its last card.
|
|
294
|
+
void learnShellDataDirs();
|
|
278
295
|
const config = loadConfig(dataDir);
|
|
279
296
|
// The store modules read the app's own home variable at call time; setting it
|
|
280
297
|
// here points auth session files, sync, and every store path at its root.
|
|
@@ -399,7 +416,16 @@ async function createWindow() {
|
|
|
399
416
|
// HER CODEX CONVERSATIONS ASK TO COME IN. Once a minute: a new one in a
|
|
400
417
|
// folder a project points at gets a row asking yes or no, and the ones she
|
|
401
418
|
// said yes to follow Codex's latest answer.
|
|
402
|
-
|
|
419
|
+
// AND IT IS WHERE A CODEX AGENT IS COUNTED, because it is the one thing that
|
|
420
|
+
// already reads them (main/codex-watch.mjs). The switch is checked by
|
|
421
|
+
// `analytics.track` itself, so this hands it over unconditionally.
|
|
422
|
+
// Every Codex folder on the machine, read fresh each minute so a login made
|
|
423
|
+
// while the app is open is followed too (main/agent-homes.mjs).
|
|
424
|
+
const codexWatch = startCodexWatch({
|
|
425
|
+
store,
|
|
426
|
+
count: (name, props) => analytics.track(name, props),
|
|
427
|
+
readThreads: (opts) => readCodexThreads({ ...opts, codexDirs: appSessionFolders({ config, supervisor }).codexDirs }),
|
|
428
|
+
});
|
|
403
429
|
app.on('before-quit', () => codexWatch.stop());
|
|
404
430
|
// Whatever is still queued goes with the app rather than dying in memory.
|
|
405
431
|
app.on('will-quit', () => { analytics.flush(); });
|
|
@@ -661,9 +687,16 @@ async function createWindow() {
|
|
|
661
687
|
// AND A COPY RUN FROM SOURCE watches its git branch instead of a release
|
|
662
688
|
// feed (main/source-updater.mjs), because that is how teammates run the team
|
|
663
689
|
// build and their updates are pushes to main, not releases.
|
|
690
|
+
//
|
|
691
|
+
// AND WHERE THIS COPY IS RUNNING FROM (shared/where-it-runs-from.mjs). An
|
|
692
|
+
// app run out of the disk image, or out of a read-only copy macOS made of
|
|
693
|
+
// it, can never update itself, and until 2026-10-12 nothing in the app said
|
|
694
|
+
// so: two installs sat on 0.1.11 for a week. The fact rides the updater's
|
|
695
|
+
// state so the Settings row, the sidebar card and ⌘K all read one answer.
|
|
664
696
|
let pushUpdate = () => {};
|
|
665
697
|
const updater = app.isPackaged ? createUpdater({
|
|
666
698
|
app,
|
|
699
|
+
place: createPlace({ app }),
|
|
667
700
|
onChanged: () => pushUpdate(),
|
|
668
701
|
}) : createSourceUpdater({
|
|
669
702
|
app,
|
|
@@ -673,9 +706,25 @@ async function createWindow() {
|
|
|
673
706
|
|
|
674
707
|
// The desktop door. `main/serve.mjs` is the other one and hands over plain
|
|
675
708
|
// Node stand-ins for these three; everything else about the call is the same.
|
|
709
|
+
// THE PHONE DOOR (main/phone-link.mjs) answers with these same handlers, so
|
|
710
|
+
// each one is filed in a Map as Electron gets it. And every push the window
|
|
711
|
+
// gets is handed to the phones' event streams too.
|
|
712
|
+
const phoneChannels = new Map();
|
|
713
|
+
const phoneListeners = new Set();
|
|
714
|
+
const filingIpcMain = {
|
|
715
|
+
handle(channel, fn) { ipcMain.handle(channel, fn); phoneChannels.set(channel, fn); },
|
|
716
|
+
};
|
|
717
|
+
const filingHost = { ipcMain: filingIpcMain, app, dialog };
|
|
718
|
+
const sendToWindow = window.webContents.send.bind(window.webContents);
|
|
719
|
+
window.webContents.send = (channel, ...args) => {
|
|
720
|
+
sendToWindow(channel, ...args);
|
|
721
|
+
for (const send of phoneListeners) {
|
|
722
|
+
try { send({ channel, args }); } catch { /* a closed phone is not an error */ }
|
|
723
|
+
}
|
|
724
|
+
};
|
|
676
725
|
const ipc = registerIpc({
|
|
677
726
|
store, supervisor, config, window, analytics, docGrants, updater,
|
|
678
|
-
host:
|
|
727
|
+
host: filingHost,
|
|
679
728
|
team,
|
|
680
729
|
});
|
|
681
730
|
pushUpdate = ipc.push;
|
|
@@ -683,6 +732,33 @@ async function createWindow() {
|
|
|
683
732
|
// See the `installMenu` call above for what writing the file instead did to a
|
|
684
733
|
// Codex card.
|
|
685
734
|
answerApproval = ipc.answerApproval;
|
|
735
|
+
// What the window answers itself (badge, boot info) a phone gets the plain
|
|
736
|
+
// stand-ins for, as a browser tab does.
|
|
737
|
+
answerWhatOnlyTheWindowAnswered({ handle: (channel, fn) => { if (!phoneChannels.has(channel)) phoneChannels.set(channel, fn); } });
|
|
738
|
+
const phone = createPhoneLink({
|
|
739
|
+
file: path.join(userDir, 'phone-link.json'),
|
|
740
|
+
channels: phoneChannels,
|
|
741
|
+
listeners: phoneListeners,
|
|
742
|
+
dist: path.join(appDir, 'renderer', 'dist'),
|
|
743
|
+
// Pictures and design pages, under the same two rules the window's own
|
|
744
|
+
// schemes use below: a page the pane was granted, and a picture inside a
|
|
745
|
+
// root the app owns (main/serve.mjs answerFile).
|
|
746
|
+
files: { allows: (kind, f) => (kind === 'doc' ? docGrants.allows(f) : servable(f, imageRoots())) },
|
|
747
|
+
// "Anywhere": the relay a phone reaches this Mac through from any network
|
|
748
|
+
// (main/relay-client.mjs). None until it is deployed, and then no choice.
|
|
749
|
+
relayUrl: relayAddress({ appDir }),
|
|
750
|
+
onChange: () => { if (!window.isDestroyed()) sendToWindow('zero:phone-changed'); },
|
|
751
|
+
// The phone stays quiet while she is at the desktop app, by the same rule
|
|
752
|
+
// the desktop's own notifications use (shared/notify-rules.mjs).
|
|
753
|
+
atDesk: () => {
|
|
754
|
+
try { return atTheApp({ focused: !window.isDestroyed() && window.isFocused(), idleMs: powerMonitor.getSystemIdleTime() * 1000 }); } catch { return false; }
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
window.on('focus', () => phone.seen());
|
|
758
|
+
// Settings' own channel, on the window only: a phone does not get to turn
|
|
759
|
+
// the phone door off or hand out its key.
|
|
760
|
+
ipcMain.handle('zero:phone-link', (_e, payload) => phone.act(payload ?? {}));
|
|
761
|
+
app.on('before-quit', () => phone.close());
|
|
686
762
|
updater.start();
|
|
687
763
|
// A task the app just shipped moved main: look now rather than in half an
|
|
688
764
|
// hour, so the restart is offered while she still remembers the task.
|
|
@@ -855,6 +931,7 @@ async function createWindow() {
|
|
|
855
931
|
// Unread badge + notification on new inbox arrivals are driven by the
|
|
856
932
|
// renderer (it owns the definition of "inbox"), through the badge API.
|
|
857
933
|
ipcMain.handle('zero:badge', (_e, count) => {
|
|
934
|
+
phone.count(count);
|
|
858
935
|
if (process.platform === 'darwin') app.dock.setBadge(count > 0 ? String(count) : '');
|
|
859
936
|
});
|
|
860
937
|
// WHETHER to speak, and what to say, is not the renderer's call: only this
|
|
@@ -863,7 +940,21 @@ async function createWindow() {
|
|
|
863
940
|
// a locked screen all read the same from in there). The renderer says what
|
|
864
941
|
// arrived; main/notify.mjs decides. tell me when I am in another app, never
|
|
865
942
|
// while I am in Agentbox.
|
|
866
|
-
installNotifier({ app, window, Notification, powerMonitor, ipcMain });
|
|
943
|
+
installNotifier({ app, window, Notification, powerMonitor, ipcMain, nativeImage, onArrivals: (arrivals) => phone.arrivals(arrivals) });
|
|
944
|
+
// THE CORNER TAG (w-dafae58a23): what is ready for her, floating over
|
|
945
|
+
// whatever app she is in. Its page is built beside the app's own, and comes
|
|
946
|
+
// from the dev server only when the app's own page does.
|
|
947
|
+
createCornerTag({
|
|
948
|
+
BrowserWindow, screen: electronScreen, ipcMain, Menu, powerMonitor, config, saveConfig, mainWindow: window,
|
|
949
|
+
preload: path.join(appDir, 'main', 'corner-tag-preload.cjs'),
|
|
950
|
+
// `part` is the tag itself or the list beside it, one page drawn twice.
|
|
951
|
+
load: (tagWindow, part) => {
|
|
952
|
+
const devTag = process.env.ZERO_DEV_URL;
|
|
953
|
+
const builtTag = path.join(appDir, 'renderer', 'dist', 'corner-tag.html');
|
|
954
|
+
if (devTag && !fs.existsSync(builtTag)) tagWindow.loadURL(`${devTag.replace(/\/$/, '')}/corner-tag.html?part=${part}`);
|
|
955
|
+
else tagWindow.loadFile(builtTag, { search: `part=${part}` });
|
|
956
|
+
},
|
|
957
|
+
});
|
|
867
958
|
// Asked for once by the renderer as it mounts, rather than pushed on
|
|
868
959
|
// did-finish-load, which races the first effects and would drop the very
|
|
869
960
|
// message it exists to deliver.
|
|
@@ -907,6 +998,11 @@ async function createWindow() {
|
|
|
907
998
|
// (frame-scrollbars.mjs). Without it, a mouse on macOS gets a white track.
|
|
908
999
|
quietTheFramesScrollbars(window.webContents);
|
|
909
1000
|
|
|
1001
|
+
// A local app a worker names (http://localhost:3006) draws in the pane even
|
|
1002
|
+
// when it refuses to be framed, and its sign-in cookie sticks there
|
|
1003
|
+
// (main/local-app-frames.mjs).
|
|
1004
|
+
letLocalAppsShowInThePane(window.webContents);
|
|
1005
|
+
|
|
910
1006
|
// Escape must ALWAYS leave the junk browser, even when the guest page has
|
|
911
1007
|
// focus and would otherwise swallow the key. Intercept it below the page.
|
|
912
1008
|
app.on('web-contents-created', (_event, contents) => {
|