agentbox-app 0.1.12 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -9
- package/bin/agentbox.mjs +23 -1
- package/briefs/message-rules.md +36 -8
- package/briefs/worker.md +46 -23
- package/main/account-discovery.mjs +122 -30
- package/main/account-tooling.mjs +29 -1
- package/main/account-usage.mjs +46 -0
- package/main/acp.mjs +296 -0
- package/main/agent-homes.mjs +158 -0
- package/main/agent-sessions.mjs +311 -260
- package/main/agent-updates.mjs +9 -22
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +42 -2
- package/main/answer-modes.mjs +103 -37
- package/main/approval-prompt-core.mjs +197 -0
- package/main/approval-prompt-server.mjs +4 -138
- package/main/approvals.mjs +101 -297
- package/main/artifact-path.mjs +47 -18
- package/main/claude-bin.mjs +45 -311
- package/main/claude-input.mjs +57 -7
- package/main/claude-launch-env.mjs +54 -0
- package/main/claude-models.mjs +39 -7
- package/main/claude-plan.mjs +64 -16
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +94 -59
- package/main/cloud/account.mjs +179 -0
- package/main/cloud/aws.mjs +127 -0
- package/main/cloud/browser-return.mjs +58 -0
- package/main/cloud/clients.mjs +15 -0
- package/main/cloud/codex-cloud.mjs +53 -0
- package/main/cloud/config.mjs +107 -0
- package/main/cloud/docs.mjs +219 -0
- package/main/cloud/exec.mjs +27 -0
- package/main/cloud/host.mjs +165 -0
- package/main/cloud/kit.mjs +80 -0
- package/main/cloud/logins.mjs +543 -0
- package/main/cloud/managed.mjs +115 -0
- package/main/cloud/marker.mjs +104 -0
- package/main/cloud/org.mjs +53 -0
- package/main/cloud/reuse.mjs +208 -0
- package/main/cloud/run-cli.mjs +177 -0
- package/main/cloud/run.mjs +1934 -0
- package/main/cloud/usage.mjs +76 -0
- package/main/cloud-permission.mjs +51 -0
- package/main/codex-account.mjs +78 -0
- package/main/codex-app-server.mjs +276 -409
- package/main/codex-approvals.mjs +148 -433
- package/main/codex-bin.mjs +51 -249
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +246 -506
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +112 -11
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +83 -5
- package/main/codex.mjs +95 -363
- package/main/config.mjs +37 -5
- package/main/corner-tag.mjs +810 -0
- package/main/crash-report.mjs +26 -3
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +545 -0
- package/main/engine-setup.mjs +25 -24
- package/main/feedback.mjs +14 -31
- package/main/folder-pictures.mjs +2 -2
- package/main/freeze-recorder-thread.mjs +86 -0
- package/main/freeze-recorder.mjs +146 -0
- package/main/fresh-user.mjs +20 -9
- package/main/git-change-offthread.mjs +17 -3
- package/main/git-change-thread.mjs +5 -2
- package/main/harnesses/claude-stream.mjs +208 -0
- package/main/harnesses/claude.mjs +105 -0
- package/main/harnesses/codex.mjs +72 -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 +96 -0
- package/main/ipc.mjs +520 -135
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/local-app-frames.mjs +82 -0
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +248 -37
- package/main/memory-gate-history.mjs +85 -20
- package/main/memory-gate-server.mjs +129 -11
- package/main/memory-gate.mjs +62 -8
- package/main/memory-log.mjs +124 -0
- package/main/menu.mjs +4 -2
- package/main/message-priority.mjs +2 -2
- package/main/mic.mjs +38 -0
- package/main/native/liquid-glass/build/liquid-glass.node +0 -0
- package/main/new-chats-watch.mjs +110 -0
- package/main/new-user-window.mjs +25 -0
- package/main/notify.mjs +64 -5
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +571 -0
- package/main/phone-uploads.mjs +476 -0
- package/main/picture-shape.mjs +114 -0
- package/main/quit-sequence.mjs +143 -0
- package/main/refresh-agent-models.mjs +15 -2
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +314 -0
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +18 -47
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/runs-on.mjs +32 -35
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +155 -9
- package/main/settings.mjs +364 -50
- package/main/shell-path.mjs +234 -0
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +134 -20
- package/main/sign-in-files.mjs +3 -7
- package/main/source-updater.mjs +45 -11
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/start-hold.mjs +73 -0
- package/main/store/home.mjs +87 -232
- package/main/store/paths.mjs +11 -0
- package/main/store/teammates-files.mjs +99 -0
- package/main/store/work-items.mjs +231 -13
- package/main/store.mjs +227 -53
- package/main/supervisor.mjs +3865 -699
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +897 -105
- package/main/task-terminals.mjs +147 -41
- package/main/team/index.mjs +114 -10
- package/main/team/memory-cloud.mjs +183 -1
- package/main/team/room-hours.mjs +48 -0
- package/main/team/room-setup.mjs +31 -0
- package/main/team/rooms-memory.mjs +454 -0
- package/main/team/rooms-shape.mjs +42 -0
- package/main/team/rooms-supabase.mjs +324 -0
- package/main/team/rooms.mjs +575 -0
- package/main/team/session.mjs +96 -26
- package/main/team/supabase-backend.mjs +180 -0
- package/main/team/sync.mjs +109 -1
- package/main/teammates.mjs +580 -0
- package/main/terminal-host.mjs +143 -0
- package/main/terminal-hosts.mjs +261 -0
- package/main/transcript-search.mjs +80 -0
- package/main/updater.mjs +71 -9
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +365 -0
- package/mcp/core/documents.mjs +3 -1
- package/mcp/core/repeats.mjs +88 -0
- package/mcp/core/teammates.mjs +222 -0
- package/mcp/core/work.mjs +76 -4
- package/mcp/stdio.mjs +24 -11
- package/mcp/tools.mjs +116 -8
- package/package.json +30 -5
- package/renderer/dist/assets/abnfDiagram-YUKMZFIV-CoWB6YUK.js +1 -0
- package/renderer/dist/assets/arc-Ch0NrCul.js +1 -0
- package/renderer/dist/assets/architectureDiagram-47P4ROYG-CXz9M5tQ.js +36 -0
- package/renderer/dist/assets/blockDiagram-E7TT5TSB-DchGUuHH.js +140 -0
- package/renderer/dist/assets/c4Diagram-CRA5TL53-Cip-zNyr.js +38 -0
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/chunk-2Q5K7J3B-BoXid_2r.js +1 -0
- package/renderer/dist/assets/chunk-5VM5RSS4-Cpez1rRz.js +15 -0
- package/renderer/dist/assets/chunk-7TKQ45FW-DKA84YkE.js +1 -0
- package/renderer/dist/assets/chunk-AW2ZBBNX-Dg-SumPG.js +88 -0
- package/renderer/dist/assets/chunk-FVRAUYC3-06TCvPm1.js +2 -0
- package/renderer/dist/assets/chunk-HTAEGDNF-CMEBy1Td.js +1 -0
- package/renderer/dist/assets/chunk-JWPE2WC7-BJ2ObmV-.js +1 -0
- package/renderer/dist/assets/chunk-KQW6MTUR-CGWOYXUE.js +1 -0
- package/renderer/dist/assets/chunk-XXDRQBXY-CiZp07XC.js +1 -0
- package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-DJNIX719.js +217 -0
- package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
- package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
- package/renderer/dist/assets/cose-bilkent-JH36ORCC-CbkyZOUH.js +1 -0
- package/renderer/dist/assets/cynefin-EF2NZ3EQ-78iOroZO.js +141 -0
- package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-GcCopMs7.js +62 -0
- package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/renderer/dist/assets/dagre-W4DXFKR2-DHLlXLkz.js +4 -0
- package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/renderer/dist/assets/diagram-2UJZ2QOL-DuZSUHcH.js +200 -0
- package/renderer/dist/assets/diagram-OVF4WLC6-Ckxu3cqH.js +24 -0
- package/renderer/dist/assets/diagram-PFPMY2P6-CsTw9Ycn.js +30 -0
- package/renderer/dist/assets/diagram-UMYRVEAY-CTXd8qVg.js +24 -0
- package/renderer/dist/assets/diagram-YEKJPTXX-CJs1T_9m.js +41 -0
- package/renderer/dist/assets/diagram-ZIFT7M5P-CvjI23mf.js +3 -0
- package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-d_IeAzOJ.js +1 -0
- package/renderer/dist/assets/elk-IJKZMXRS-zo9KFP3N.js +27 -0
- package/renderer/dist/assets/erDiagram-O2IAPWRE-DqfYf5Dt.js +99 -0
- package/renderer/dist/assets/flowDiagram-OXPTDLAJ-DAWsMLmo.js +213 -0
- package/renderer/dist/assets/ganttDiagram-R7TSEDQI-4ml_bhwR.js +292 -0
- package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-B_WcqSId.js +106 -0
- package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
- package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-CI-jYqzc.js +2 -0
- package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
- package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-B56VWRlV.js +70 -0
- package/renderer/dist/assets/journeyDiagram-COXZFDF6-D1-p09Lr.js +139 -0
- package/renderer/dist/assets/kanban-definition-P3RFRI5V-Cv3w7JoN.js +89 -0
- package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
- package/renderer/dist/assets/linear-C35V2Spi.js +1 -0
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/mermaid.core-BzGT5Vo9.js +314 -0
- package/renderer/dist/assets/mindmap-definition-LZFPQGKD-BZUfuymC.js +96 -0
- package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/renderer/dist/assets/pegDiagram-BGZESJAR-DFh9XNGe.js +1 -0
- package/renderer/dist/assets/pieDiagram-CAPJLFHJ-C65tPVsy.js +39 -0
- package/renderer/dist/assets/product-name-CSNW6d7s.js +40 -0
- package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-Cushj_nk.js +7 -0
- package/renderer/dist/assets/railroadDiagram-SM67HX2B-DZGebGx5.js +1 -0
- package/renderer/dist/assets/requirementDiagram-X7JNWC4A-DpAB0MNo.js +84 -0
- package/renderer/dist/assets/sankeyDiagram-UM26HJRW-D-29rHra.js +40 -0
- package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-FfOQAZol.js +169 -0
- package/renderer/dist/assets/sizeCapture-INFHLROL-C_n6fDBY.js +1 -0
- package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-BZjenzJG.js +291 -0
- package/renderer/dist/assets/swimlanes-N4OXWK64-DxaMU9vX.js +1 -0
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/timeline-definition-YOQAKGHF-DTg5HJKz.js +120 -0
- package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-Bc3MZDsl.js +370 -0
- package/renderer/dist/assets/vennDiagram-BLWOH2XV-GIVGk9Id.js +34 -0
- package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-NmJnbQOg.js +78 -0
- package/renderer/dist/assets/xychartDiagram-TAQBALBS-p-kfGfnW.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/after-install.mjs +30 -8
- package/scripts/approval-server.sh +24 -5
- package/scripts/before-publish.mjs +12 -10
- package/scripts/cloud-shim.pl +57 -0
- package/shared/account-names.d.mts +6 -0
- package/shared/account-names.mjs +15 -0
- package/shared/agent-hours.mjs +136 -0
- package/shared/agent-import.mjs +62 -34
- package/shared/agents.d.mts +1 -0
- package/shared/agents.mjs +126 -522
- package/shared/analytics-events.mjs +12 -0
- package/shared/approval-card.mjs +36 -0
- package/shared/bridge-map.mjs +45 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-commands.generated.mjs +6 -4
- package/shared/claude-models.generated.mjs +6 -4
- package/shared/claude-usage.d.mts +1 -0
- package/shared/claude-usage.mjs +42 -1
- package/shared/cloud-hand-back.d.mts +7 -0
- package/shared/cloud-hand-back.mjs +20 -0
- package/shared/cloud-kit.d.mts +219 -0
- package/shared/cloud-kit.mjs +341 -0
- package/shared/cloud-login-name.mjs +4 -0
- package/shared/cloud-start.mjs +17 -0
- package/shared/cloud-versions.mjs +23 -0
- package/shared/codex-import.mjs +2 -2
- package/shared/codex-modes.d.mts +22 -9
- package/shared/codex-modes.mjs +67 -11
- package/shared/codex-usage.mjs +108 -2
- package/shared/corner-tag.d.mts +36 -0
- package/shared/corner-tag.mjs +396 -0
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +76 -300
- package/shared/feedback.d.mts +7 -0
- package/shared/feedback.mjs +25 -0
- package/shared/first-run-practice.mjs +17 -24
- package/shared/harness-definitions.mjs +111 -0
- package/shared/instruction-defaults.json +2 -2
- package/shared/key-caps.d.mts +14 -0
- package/shared/key-caps.mjs +68 -0
- package/shared/message-edits.d.mts +7 -0
- package/shared/message-edits.mjs +35 -0
- package/shared/move-thread.d.mts +24 -0
- package/shared/move-thread.mjs +112 -0
- package/shared/named-threads.mjs +72 -0
- package/shared/not-shipped.d.mts +22 -0
- package/shared/not-shipped.mjs +89 -0
- package/shared/notify-rules.d.mts +31 -0
- package/shared/notify-rules.mjs +55 -8
- package/shared/phone-files.mjs +28 -0
- package/shared/phone-settings.mjs +118 -0
- package/shared/phone-snapshot.d.mts +16 -0
- package/shared/phone-snapshot.mjs +348 -0
- package/shared/picture-formats.d.mts +1 -0
- package/shared/picture-formats.mjs +13 -0
- package/shared/provider-commands.mjs +10 -2
- package/shared/rank.d.mts +4 -1
- package/shared/rank.mjs +82 -19
- package/shared/relay-address.mjs +6 -0
- package/shared/relay-crypto.mjs +165 -0
- package/shared/repeats.d.mts +8 -0
- package/shared/repeats.mjs +33 -2
- package/shared/room-cards.mjs +167 -0
- package/shared/room-links.d.mts +4 -0
- package/shared/room-links.mjs +21 -0
- package/shared/runs-on.d.mts +3 -13
- package/shared/runs-on.mjs +31 -52
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +24 -10
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +484 -30
- package/shared/team-rules.mjs +9 -2
- package/shared/teammates.d.mts +19 -0
- package/shared/teammates.mjs +756 -0
- package/shared/terminal-focus.d.mts +1 -0
- package/shared/terminal-focus.mjs +13 -0
- 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/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +12 -0
- package/shared/usage.mjs +56 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -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 +150 -29
- package/shared/work-lines.mjs +93 -245
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { openSourceFile } from './open-source-file.mjs';
|
|
2
2
|
import {submitReply} from './live-replies.mjs';
|
|
3
|
+
import { whyItCannotMove } from '../shared/move-thread.mjs';
|
|
3
4
|
import { createEngineSetup } from './engine-setup.mjs';
|
|
4
5
|
// IPC: a thin, typed-by-convention surface. The renderer asks; the main
|
|
5
6
|
// process derives from files and answers. Pushes go one way: "state changed,
|
|
@@ -20,13 +21,15 @@ import { createEngineSetup } from './engine-setup.mjs';
|
|
|
20
21
|
// process can hand over its own, and the same 78 channels then answer a
|
|
21
22
|
// browser over http without one line of this file being written twice.
|
|
22
23
|
import {terminalSenderAllowed} from './terminal-access.mjs';
|
|
23
|
-
import {TaskTerminals,terminalPlace} from './task-terminals.mjs';
|
|
24
|
+
import {TaskTerminals,terminalPlace,terminalEnv} from './task-terminals.mjs';
|
|
25
|
+
import {TerminalHosts,taskClosed} from './terminal-hosts.mjs';
|
|
24
26
|
import {SETTINGS_TERMINAL} from '../shared/settings-terminal.mjs';
|
|
25
27
|
import {AgentUpdates,installedAgent} from './agent-updates.mjs';
|
|
26
28
|
import {refreshAgentModels} from './refresh-agent-models.mjs';
|
|
27
29
|
import {readInstruction,writeInstruction,listVersions,readVersion} from './instruction-settings.mjs';
|
|
28
30
|
import path from 'node:path';
|
|
29
31
|
import os from 'node:os';
|
|
32
|
+
import { looksLikeClaudeCode } from '../shared/claude-code-shape.mjs';
|
|
30
33
|
import fsWatch from 'node:fs';
|
|
31
34
|
import { listFolders } from './folders.mjs';
|
|
32
35
|
import { feedbackUrl, sendFeedback } from './feedback.mjs';
|
|
@@ -40,15 +43,20 @@ import { readDoc, writeDoc } from './doc-file.mjs';
|
|
|
40
43
|
import { readChange } from './code-change.mjs';
|
|
41
44
|
import { findFile, readCodeFile, writeCodeFile } from './code-file.mjs';
|
|
42
45
|
import { readNote, writeNote } from './rail-note.mjs';
|
|
43
|
-
import {
|
|
46
|
+
import { teamForWindow } from './team/index.mjs';
|
|
47
|
+
import { addClaudeAccount, addCodexAccount, alphaFeatures, cornerTagTurnedOff, countsTurnedOff, lookAgainFor, outsideAgentsMode, permissionMode, pointAtEngineBin, readSettings, searchForEngineBin, setProjectSetting, setWorkspaceSetting } from './settings.mjs';
|
|
44
48
|
import { ICON_KINDS, clearProjectIcon, setProjectIcon, setProjectName } from './project-identity.mjs';
|
|
45
49
|
import { ClaudeUsage } from './claude-usage.mjs';
|
|
50
|
+
import { AccountUsage } from './account-usage.mjs';
|
|
51
|
+
import { effectiveProfiles } from './account-discovery.mjs';
|
|
46
52
|
import { usageEngine } from '../shared/usage.mjs';
|
|
47
|
-
import { DEFAULT_ENGINE } from '../shared/engines.mjs';
|
|
53
|
+
import { DEFAULT_ENGINE, isEngine } from '../shared/engines.mjs';
|
|
48
54
|
import * as agents from './agents.mjs';
|
|
49
55
|
import { findAgentFolders, readAgentFiles, readFolderAgents } from './agent-files.mjs';
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
56
|
+
import { readCodexThreadsOffThread } from './codex-threads.mjs';
|
|
57
|
+
import { appSessionFolders } from './agent-homes.mjs';
|
|
58
|
+
import { readImportableThreads, readSearchableThreads } from './importable-threads.mjs';
|
|
59
|
+
import { readChatEnds } from './agent-sessions.mjs';
|
|
52
60
|
import { markImported } from '../shared/agent-import.mjs';
|
|
53
61
|
import { codexIdOf, importChoice, isCodexImportRow, isNotImportedRow } from '../shared/codex-import.mjs';
|
|
54
62
|
import { checkProjectFolder } from '../shared/project-folder-check.mjs';
|
|
@@ -60,6 +68,9 @@ import { machineryPath } from './store/home.mjs';
|
|
|
60
68
|
import { EVENT_NAMES } from '../shared/analytics-events.mjs';
|
|
61
69
|
import { Name } from '../shared/product-name.mjs';
|
|
62
70
|
import { handedOnByReply } from '../shared/team-rules.mjs';
|
|
71
|
+
import { closeRefusal, closedAnywayNote } from '../shared/not-shipped.mjs';
|
|
72
|
+
import { saveAttachments } from './save-attachments.mjs';
|
|
73
|
+
import { stageAttachment } from './stage-attachment.mjs';
|
|
63
74
|
|
|
64
75
|
// WHAT A WORKER IS TOLD WHEN HER DECISION COULD NOT BE RECORDED.
|
|
65
76
|
const SPOOL_REFUSED = `${Name} could not record the founder's decision, so nothing was allowed. Finish what your grants cover and file the rest as a review.`;
|
|
@@ -91,8 +102,14 @@ const NO_UPDATER = {
|
|
|
91
102
|
}),
|
|
92
103
|
check: async () => NO_UPDATER.state(),
|
|
93
104
|
install: () => false,
|
|
105
|
+
// Nowhere to move to and nothing to move: a copy with no updater behind it
|
|
106
|
+
// is a copy run from source (shared/where-it-runs-from.mjs).
|
|
107
|
+
move: () => ({ moved: false, error: null }),
|
|
94
108
|
};
|
|
95
109
|
|
|
110
|
+
const TEAMMATE_OPS = new Set(['create', 'describe', 'start', 'update', 'remove', 'post', 'goAhead', 'remember', 'forget']);
|
|
111
|
+
const ROOM_OPS = new Set(['refresh', 'send', 'markRead', 'markUnread', 'react', 'edit', 'deleteMessage', 'putFile', 'file', 'older', 'saveProfile', 'goInvisible', 'comeBack', 'link', 'revokeLink', 'preview', 'listed', 'invitesSent', 'joinByLink', 'joinListed', 'acceptInvite', 'invite', 'create', 'rename', 'leave', 'remove', 'pin', 'mute']);
|
|
112
|
+
|
|
96
113
|
export function registerIpc({ store, supervisor, config, window, analytics = NO_ANALYTICS, docGrants = null, updater = NO_UPDATER, host, team = null }) {
|
|
97
114
|
// The three Electron things, or a plain Node stand-in for them. There is no
|
|
98
115
|
// default: a caller that forgets says so here rather than throwing eighty
|
|
@@ -163,8 +180,8 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
163
180
|
// THROW, out of an ipc handler and out of the ⌘Y `before-input-event`
|
|
164
181
|
// listener, which is worse than either outcome here. A Codex worker is parked
|
|
165
182
|
// on a promise in this process, so it is told the refusal directly and
|
|
166
|
-
// unblocks; a Claude Code worker has only the file, so it
|
|
167
|
-
//
|
|
183
|
+
// unblocks; a Claude Code worker has only the file, so it goes on waiting on
|
|
184
|
+
// its own card, which stays on screen and answerable. Nothing on this
|
|
168
185
|
// path can turn a failure into an allow, and the deny falls out of the SAME
|
|
169
186
|
// two calls the yes does rather than a second door beside them: `allow` and
|
|
170
187
|
// `note` stop meaning what she pressed the moment the write fails, and start
|
|
@@ -217,7 +234,10 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
217
234
|
try { return store.listProducts().some((p) => p.slug === slug && p.practice === true); } catch { return false; }
|
|
218
235
|
};
|
|
219
236
|
|
|
220
|
-
|
|
237
|
+
// From her phone the answer names the card it drew (main/phone-inbox.mjs),
|
|
238
|
+
// and a request rewritten since gets nothing.
|
|
239
|
+
ipcMain.handle('zero:approve', (_e, { id, allow, note, card }) => {
|
|
240
|
+
if (card !== undefined && !approvals.cardStillShown(config.storeRoot, id, card)) return { ok: false, error: 'the card changed after your phone showed it, so nothing was allowed' };
|
|
221
241
|
return { ok: answerApproval(id, allow, note) };
|
|
222
242
|
});
|
|
223
243
|
|
|
@@ -259,7 +279,42 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
259
279
|
// while the app is running and the settings screen already promises that
|
|
260
280
|
// works without a restart.
|
|
261
281
|
bin: () => config.claudeBin,
|
|
262
|
-
|
|
282
|
+
// The account marked In use, asked each time so a switch is read at once.
|
|
283
|
+
account: () => supervisor.usageAccount(),
|
|
284
|
+
onChange: () => {
|
|
285
|
+
const reading = usage.peek();
|
|
286
|
+
if (reading) supervisor.noteUsageReading(reading.profile, reading);
|
|
287
|
+
push();
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
ipcMain.handle('zero:refresh-usage', async (event, { engine = DEFAULT_ENGINE } = {}) => {
|
|
292
|
+
if (event.senderFrame?.parent || (event.sender && event.sender !== window.webContents)) throw Error('Usage refresh is only available in the main app window.');
|
|
293
|
+
// Codex is asked of this computer and not of OpenAI; main/codex-usage.mjs
|
|
294
|
+
// says why there is no explicit read.
|
|
295
|
+
if (engine !== DEFAULT_ENGINE) {
|
|
296
|
+
const reading = supervisor.refreshCodexUsage();
|
|
297
|
+
return reading ? { ok: true, reading: { engine, ...reading } } : { ok: false, error: 'Codex has reported no usage on this computer yet.' };
|
|
298
|
+
}
|
|
299
|
+
const result = await usage.refreshNow();
|
|
300
|
+
return result.reading ? { ...result, reading: { engine: DEFAULT_ENGINE, ...result.reading } } : result;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Every Claude login's own reading, asked for only by the Settings page.
|
|
304
|
+
const accountUsage = new AccountUsage({
|
|
305
|
+
bin: () => config.claudeBin,
|
|
306
|
+
env: () => supervisor._workerEnv(DEFAULT_ENGINE),
|
|
307
|
+
onReading: (profile, reading) => supervisor.noteUsageReading(profile, reading),
|
|
308
|
+
});
|
|
309
|
+
ipcMain.handle('zero:account-usage', async (event, { refresh = false, engine = DEFAULT_ENGINE } = {}) => {
|
|
310
|
+
if (event.senderFrame?.parent || (event.sender && event.sender !== window.webContents)) throw Error('Usage is only available in the main app window.');
|
|
311
|
+
// Codex's are files on this computer, read on every ask.
|
|
312
|
+
if (engine === 'codex') return { readings: supervisor.codexAccountUsage() };
|
|
313
|
+
const profiles = effectiveProfiles(config.authProfiles, { home: config.home });
|
|
314
|
+
// The cloud's logins too; a look takes a sandbox session, so the page is
|
|
315
|
+
// not held for it and redraws off the snapshot when it lands.
|
|
316
|
+
void supervisor.lookAtCloudLogins({ force: !!refresh });
|
|
317
|
+
return { readings: await accountUsage.readAll(profiles.length ? profiles : ['default'], { refresh: !!refresh }) };
|
|
263
318
|
});
|
|
264
319
|
|
|
265
320
|
// Agents this run has already counted. In memory on purpose: a fresh launch
|
|
@@ -279,11 +334,17 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
279
334
|
// send one event per agent per tick, all day. The keys are only ever
|
|
280
335
|
// compared in this process; nothing about an agent is sent with the
|
|
281
336
|
// count, which carries no name, no path and no title.
|
|
337
|
+
//
|
|
338
|
+
// AND WHICH ENGINE IT WAS (w-1116fbb68a, 2026-10-08). This list is Claude
|
|
339
|
+
// Code processes and only ever has been; a Codex conversation has no
|
|
340
|
+
// process to find and is counted from main/codex-watch.mjs instead. The
|
|
341
|
+
// engine is what lets the two be read apart, and without it on this side
|
|
342
|
+
// the Codex count would look like a drop in the Claude one.
|
|
282
343
|
for (const agent of live) {
|
|
283
344
|
const key = agentKey(agent);
|
|
284
345
|
if (!key || seenAgents.has(key)) continue;
|
|
285
346
|
seenAgents.add(key);
|
|
286
|
-
analytics.track('agent_seen');
|
|
347
|
+
analytics.track('agent_seen', { engine: 'claude' });
|
|
287
348
|
}
|
|
288
349
|
return live;
|
|
289
350
|
} catch {
|
|
@@ -316,12 +377,25 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
316
377
|
if(product===SETTINGS_TERMINAL.product&&id===SETTINGS_TERMINAL.id){
|
|
317
378
|
return pendingLogin ?? config.home ?? os.homedir();
|
|
318
379
|
}
|
|
380
|
+
return taskPlace(product,id);
|
|
381
|
+
}});
|
|
382
|
+
const taskPlace=(product,id)=>{
|
|
319
383
|
const agent=id.startsWith('agent:')?liveAgents().find(a=>'agent:'+a.pid===id && (a.product??'')===product):null;
|
|
320
384
|
return terminalPlace({store,supervisor,product,id,agent});
|
|
321
|
-
}
|
|
385
|
+
};
|
|
386
|
+
/*
|
|
387
|
+
* A TASK'S TERMINAL LIVES AS LONG AS THE TASK IS OPEN, in a process that
|
|
388
|
+
outlives the app (main/terminal-host.mjs). The sign-in terminal on
|
|
389
|
+
Settings belongs to no task and stays in the app. */
|
|
390
|
+
const taskTerminals=new TerminalHosts({local:terminals,logFile:path.join(supervisor.userDir,'terminal-host.log'),log:line=>console.warn(`zero: ${line}`)});
|
|
391
|
+
// A task's folder is not deleted while its terminal is open (supervisor.mjs, `_folderInUse`).
|
|
392
|
+
supervisor.terminalOpen=itemId=>taskTerminals.hasTerminal(itemId);
|
|
393
|
+
const lookAtTerminals=()=>taskTerminals.endClosed(taskClosed(store)).catch(e=>console.warn('zero: terminals:',e.message));
|
|
394
|
+
void taskTerminals.ready().then(lookAtTerminals);
|
|
395
|
+
setInterval(lookAtTerminals,60_000).unref?.();
|
|
322
396
|
const agentUpdates=new AgentUpdates({
|
|
323
|
-
installation:async engine=>{
|
|
324
|
-
refresh:async engine=>{
|
|
397
|
+
installation:async engine=>{await lookAgainFor(engine,config);return installedAgent(engine,config);},
|
|
398
|
+
refresh:async engine=>{await lookAgainFor(engine,config);await refreshAgentModels(engine,config,supervisor._codexOfferHomes());if(engine==='codex')for(const profile of supervisor._codexProfiles())supervisor._releaseIdleCodex(profile);},
|
|
325
399
|
});
|
|
326
400
|
ipcMain.handle('zero:agent-update',async(event,{engine,action}={})=>{
|
|
327
401
|
if(event.senderFrame?.parent||(event.sender&&event.sender!==window.webContents))throw Error('Updates are only available in the main app window.');
|
|
@@ -330,7 +404,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
330
404
|
if(action==='recheck')return agentUpdates.check(engine,true);
|
|
331
405
|
if(action==='start')return agentUpdates.start(engine);
|
|
332
406
|
if(action==='status')return agentUpdates.status(engine);
|
|
333
|
-
if(action==='refresh'){await refreshAgentModels(engine,config,supervisor.
|
|
407
|
+
if(action==='refresh'){await refreshAgentModels(engine,config,supervisor._codexOfferHomes());return true;}
|
|
334
408
|
throw Error('Unknown update action.');
|
|
335
409
|
});
|
|
336
410
|
let terminalQuitPending=false,terminalQuitReady=false;
|
|
@@ -339,20 +413,28 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
339
413
|
event.preventDefault();
|
|
340
414
|
if(!terminalQuitPending){terminalQuitPending=true;void Promise.all([terminals.shutdown(),agentUpdates.shutdown()]).finally(()=>{terminalQuitReady=true;app.quit();});}
|
|
341
415
|
});
|
|
342
|
-
ipcMain.handle('zero:terminal',(_event,payload={})=>{
|
|
416
|
+
ipcMain.handle('zero:terminal',async(_event,payload={})=>{
|
|
343
417
|
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;
|
|
418
|
+
const {product,id,action,data,cols,rows,offset,wait}=payload;
|
|
345
419
|
if(typeof product!=='string'||typeof id!=='string'||!id||id.length>300)throw Error('Invalid task.');
|
|
346
420
|
const key=JSON.stringify({product,id});
|
|
347
421
|
if(product==='@agent-update')return agentUpdates.terminal(id,payload);
|
|
348
|
-
switch(action){
|
|
422
|
+
if(product===SETTINGS_TERMINAL.product&&id===SETTINGS_TERMINAL.id)switch(action){
|
|
349
423
|
case 'open':return terminals.open(key);
|
|
350
|
-
case 'read':return terminals.read(key,offset);
|
|
424
|
+
case 'read':return terminals.read(key,offset,wait);
|
|
351
425
|
case 'write':terminals.write(key,data);return true;
|
|
352
426
|
case 'resize':terminals.resize(key,cols,rows);return true;
|
|
353
427
|
case 'close':terminals.close(key);return true;
|
|
354
428
|
default:throw Error('Unknown terminal action.');
|
|
355
429
|
}
|
|
430
|
+
switch(action){
|
|
431
|
+
case 'open':return taskTerminals.open(key,{...taskPlace(product,id),env:terminalEnv()});
|
|
432
|
+
case 'read':return taskTerminals.read(key,offset,wait);
|
|
433
|
+
case 'write':await taskTerminals.write(key,data);return true;
|
|
434
|
+
case 'resize':await taskTerminals.resize(key,cols,rows);return true;
|
|
435
|
+
case 'close':await taskTerminals.close(key);return true;
|
|
436
|
+
default:throw Error('Unknown terminal action.');
|
|
437
|
+
}
|
|
356
438
|
});
|
|
357
439
|
|
|
358
440
|
// The Done and All tabs' next page of finished threads the snapshot's 8 MB
|
|
@@ -360,6 +442,21 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
360
442
|
ipcMain.handle('zero:older-items', (_e, { offset = 0, limit = 50 } = {}) =>
|
|
361
443
|
store.listOlderItems({ offset: Number(offset) || 0, limit: Math.min(Number(limit) || 50, 200) }));
|
|
362
444
|
|
|
445
|
+
// IS ANYBODY ACTUALLY USING A SCREEN READER. Asked by the terminal pane
|
|
446
|
+
// before it builds an xterm, because xterm's accessibility tree is what
|
|
447
|
+
// `screenReaderMode` turns on and we used to turn it on for everybody.
|
|
448
|
+
// Electron's own words beside this property: "Rendering accessibility tree
|
|
449
|
+
// can significantly affect the performance of your app. It should not be
|
|
450
|
+
// enabled by default." It also carried xterm's 'invalid range' bug to an
|
|
451
|
+
// install that had no use for it (one crash report, 2026-10-08).
|
|
452
|
+
//
|
|
453
|
+
// Absent on Linux and before `ready`, where absent means no, and reading it
|
|
454
|
+
// may not throw: this is on the path that opens a terminal, and a terminal
|
|
455
|
+
// that cannot ask the question still has to open.
|
|
456
|
+
ipcMain.handle('zero:assistiveTech', () => {
|
|
457
|
+
try { return !!host.app?.accessibilitySupportEnabled; } catch { return false; }
|
|
458
|
+
});
|
|
459
|
+
|
|
363
460
|
ipcMain.handle('zero:snapshot', () => {
|
|
364
461
|
const now = Date.now();
|
|
365
462
|
const items = store.listItems(now);
|
|
@@ -377,13 +474,19 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
377
474
|
// draw -- which is what a Codex-only workflow paid every five minutes until
|
|
378
475
|
// now. The Codex side never starts anything at all (main/codex-usage.mjs).
|
|
379
476
|
const reading = usageFor === DEFAULT_ENGINE ? usage.read() : supervisor.codexUsage();
|
|
477
|
+
const alpha = alphaFeatures(config);
|
|
380
478
|
return {
|
|
381
479
|
products: store.listProducts(),
|
|
382
|
-
|
|
480
|
+
// Each with the place she dragged it to, if she did (w-6e5b532a95).
|
|
481
|
+
items: supervisor.withPlaces(items),
|
|
383
482
|
// THE TEAM: who is signed in, their team and its people, and the
|
|
384
483
|
// title-free activity of their private work. Null on a build with no
|
|
385
484
|
// team cloud, which is the single-person app.
|
|
386
|
-
team: team ? team.state() : null,
|
|
485
|
+
team: team ? teamForWindow(team.state(), alpha.multiplayer) : null,
|
|
486
|
+
// Which alpha features are on (Settings, Alpha features).
|
|
487
|
+
alpha,
|
|
488
|
+
// TEAMMATES: who they are, what each is on, and the one team chat.
|
|
489
|
+
teammates: alpha.teammates ? (() => { try { return store.teammatesView(items, now); } catch (err) { return { list: [], chat: [], runs: [], error: err.message }; } })() : null,
|
|
387
490
|
agents: liveAgents(),
|
|
388
491
|
approvals: approvals.listPending(config.storeRoot),
|
|
389
492
|
supervisor: running,
|
|
@@ -400,16 +503,16 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
400
503
|
// coding agent this is one entry and an empty map, and nothing anywhere
|
|
401
504
|
// draws anything new.
|
|
402
505
|
engines,
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
// spends it in silence.
|
|
506
|
+
// WHETHER THIS MAC WAS ALREADY SIGNED INTO SOMETHING (w-e217e577e5,
|
|
507
|
+
// 2026-10-07), so the walk can say so once. A new user ran an agent and
|
|
508
|
+
// could not tell where the tokens were coming from, because the app finds
|
|
509
|
+
// whichever sign-in is already on the Mac and spends it in silence.
|
|
407
510
|
//
|
|
408
|
-
// It rides the snapshot rather than the settings model because the
|
|
409
|
-
// reads it,
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
511
|
+
// It rides the snapshot rather than the settings model because the WALK
|
|
512
|
+
// reads it, and the settings model is only fetched while the settings
|
|
513
|
+
// screen is open. It costs no file read per tick: `main/runs-on.mjs`
|
|
514
|
+
// caches on the login files' own modification times, so this re-parses
|
|
515
|
+
// only after a sign-in or a profile refetch.
|
|
413
516
|
runsOn: supervisor.runsOnAccount(engines.workspace),
|
|
414
517
|
// Whether this process is still the app on disk. It rides the snapshot
|
|
415
518
|
// rather than boot-info because the staleness DEVELOPS while the app
|
|
@@ -417,6 +520,9 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
417
520
|
// while fixes landed underneath it, and a boot-time check cannot see
|
|
418
521
|
// that. See main/staleness.mjs.
|
|
419
522
|
restartNeeded: restartNeeded(supervisor.appDir),
|
|
523
|
+
// WHERE THREADS RUN, for the cloud button beside the filters
|
|
524
|
+
// (renderer/src/cloud-menu.ts). `setUp` false draws no button at all.
|
|
525
|
+
cloud: supervisor.cloudStatus(),
|
|
420
526
|
// HER RATE LIMIT, WHICH IS WHAT ACTUALLY CAPS THE FLEET. Null until the
|
|
421
527
|
// first reading lands, and the corner draws nothing at all until then
|
|
422
528
|
// rather than a pill full of dashes.
|
|
@@ -480,7 +586,19 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
480
586
|
// it and the user's own sentence never reached the card: the row went on
|
|
481
587
|
// saying only what the session was started with days before. This is the
|
|
482
588
|
// only process that knows the user said it, so this is where it is written.
|
|
483
|
-
|
|
589
|
+
//
|
|
590
|
+
// AND IT IS A REPLY, SO IT IS COUNTED (w-1116fbb68a, 2026-10-08). A reply
|
|
591
|
+
// on a task has always sent `reply_sent`; one typed into a live session
|
|
592
|
+
// sent nothing at all, and activation is read as three real replies to
|
|
593
|
+
// agents in the first day, so the people who talk to their own sessions
|
|
594
|
+
// rather than through a row were missing from it. `kind` says which of
|
|
595
|
+
// the two this was. It waits on the same delivery the mark does, and for
|
|
596
|
+
// the same reason: a count for words that never arrived would read as
|
|
597
|
+
// activation nobody had.
|
|
598
|
+
if (out?.ok) {
|
|
599
|
+
if (out.key) agentSchedule.replied(out.key, out.at ?? Date.now(), Date.now(), String(text ?? ''));
|
|
600
|
+
analytics.track('reply_sent', { kind: 'agent' });
|
|
601
|
+
}
|
|
484
602
|
push();
|
|
485
603
|
return out;
|
|
486
604
|
} catch {
|
|
@@ -544,8 +662,25 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
544
662
|
// answered at once (supervisor.sendNow).
|
|
545
663
|
ipcMain.handle('zero:send-now', (_e, { product, id }) => supervisor.sendNow(product, id));
|
|
546
664
|
|
|
547
|
-
ipcMain.handle('zero:answer', async (_e, { product, id, answer, status, priority, permissionMode, model, effort, now, inReplyTo }) => {
|
|
665
|
+
ipcMain.handle('zero:answer', async (_e, { product, id, answer, status, priority, permissionMode, engine, model, effort, now, inReplyTo, fromPhone, closeUnshipped }) => {
|
|
548
666
|
if (isAgentRow(id)) return { ok: false };
|
|
667
|
+
// WORK THAT NEVER REACHED MAIN IS NOT CLOSED UNASKED (shared/not-shipped.mjs).
|
|
668
|
+
// The window asks first and says so with `closeUnshipped`; the phone and
|
|
669
|
+
// anything else are turned down with the reason.
|
|
670
|
+
if (status === 'done' && product && id) {
|
|
671
|
+
let row = null;
|
|
672
|
+
try { row = store.readItem(product, id); } catch { row = null; }
|
|
673
|
+
const refused = closeRefusal(row, { closeUnshipped });
|
|
674
|
+
if (refused) return refused;
|
|
675
|
+
const said = closeUnshipped ? closedAnywayNote(row) : null;
|
|
676
|
+
if (said) store.noteItem(product, id, said);
|
|
677
|
+
}
|
|
678
|
+
// A level picked from ⌘K arrives here with no words, and gives way the
|
|
679
|
+
// same as one picked on the summary (zero:thread-edit). A reply that
|
|
680
|
+
// happens to carry its row's level does not.
|
|
681
|
+
if (priority != null && !answer && id) supervisor.setThreadPlaces({ [id]: null });
|
|
682
|
+
// CLOSING A THREAD IS THE END OF WHAT IT WAS PINNED FOR (w-20323283e6).
|
|
683
|
+
if (id && status === 'done') supervisor.setThreadPins({ [id]: null });
|
|
549
684
|
// A REPLY IN A THREAD (w-920461cbe6) only means something in a conversation
|
|
550
685
|
// between people, which is the only place a thread is drawn and the only
|
|
551
686
|
// record the mark crosses to a teammate in (shared/team-rules.mjs). Anywhere
|
|
@@ -564,7 +699,10 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
564
699
|
if (choice) {
|
|
565
700
|
let thread = null;
|
|
566
701
|
if (choice === 'yes') {
|
|
567
|
-
try {
|
|
702
|
+
try {
|
|
703
|
+
const { codexDirs } = appSessionFolders({ config, supervisor });
|
|
704
|
+
thread = (await readCodexThreadsOffThread({ codexDirs })).threads.find((t) => t.id === codexIdOf(row)) ?? null;
|
|
705
|
+
} catch { thread = null; }
|
|
568
706
|
}
|
|
569
707
|
const out = store.answerCodexImport(product, id, choice, { thread });
|
|
570
708
|
push();
|
|
@@ -582,7 +720,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
582
720
|
// handing them over in one call and writing the model afterwards would be a
|
|
583
721
|
// race the run usually wins, so she would pick a model and watch the old one
|
|
584
722
|
// take the task. `answerItem` writes both, and it writes the model first.
|
|
585
|
-
const out = await submitReply(supervisor,{product,id,answer,status,permissionMode,now:!!now},()=>store.answerItem(product, id, { answer, status, priority, permissionMode, model, effort, inReplyTo: thread }));
|
|
723
|
+
const out = await submitReply(supervisor,{product,id,answer,status,permissionMode,now:!!now},()=>store.answerItem(product, id, { answer, status, priority, permissionMode, engine, model, effort, inReplyTo: thread, fromPhone }));
|
|
586
724
|
// A REPLY ON A ROW A TEAMMATE GAVE YOU HANDS IT BACK TO THEM, so the
|
|
587
725
|
// conversation moves to their inbox instead of sitting in both
|
|
588
726
|
// (shared/team-rules.mjs handedOnByReply). Archiving hands nothing on.
|
|
@@ -610,6 +748,28 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
610
748
|
return supervisor.status();
|
|
611
749
|
});
|
|
612
750
|
|
|
751
|
+
// A THREAD DRAGGED TO A PLACE OF ITS OWN in the list (w-6e5b532a95): the
|
|
752
|
+
// places the drop worked out (`dropPlaces`, shared/rank.mjs), id to number.
|
|
753
|
+
ipcMain.handle('zero:set-thread-places', (_e, { places }) => {
|
|
754
|
+
supervisor.setThreadPlaces(places);
|
|
755
|
+
push();
|
|
756
|
+
return { ok: true };
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// THREADS KEPT ON THIS MAC whatever the cloud is set to: id to true, or null.
|
|
760
|
+
ipcMain.handle('zero:set-thread-on-mac', (_e, { onMac }) => {
|
|
761
|
+
supervisor.setThreadOnMac(onMac);
|
|
762
|
+
push();
|
|
763
|
+
return { ok: true };
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// PINNED THREADS (w-20323283e6): id to true, or null to unpin.
|
|
767
|
+
ipcMain.handle('zero:set-thread-pins', (_e, { pins }) => {
|
|
768
|
+
supervisor.setThreadPins(pins);
|
|
769
|
+
push();
|
|
770
|
+
return { ok: true };
|
|
771
|
+
});
|
|
772
|
+
|
|
613
773
|
// Hidden from the composer's picker and nothing else: a hidden product still
|
|
614
774
|
// ranks, still spawns workers, and its work still reaches the inbox.
|
|
615
775
|
ipcMain.handle('zero:set-product-hidden', (_e, { product, hidden }) => {
|
|
@@ -642,14 +802,18 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
642
802
|
// THE TEAM'S DOORS. Each answers { ok, team } or { ok: false, error } with
|
|
643
803
|
// the error in words, so the window can say what went wrong where it
|
|
644
804
|
// happened instead of a row silently not appearing.
|
|
805
|
+
const teamNow = () => teamForWindow(team.state(), alphaFeatures(config).multiplayer);
|
|
645
806
|
const teamCall = (fn) => async (_e, arg) => {
|
|
646
807
|
if (!team) return { ok: false, error: 'This build has no team cloud set up.' };
|
|
647
808
|
try {
|
|
648
|
-
await fn(arg ?? {});
|
|
809
|
+
const got = await fn(arg ?? {});
|
|
649
810
|
push();
|
|
650
|
-
|
|
811
|
+
// A call that wrote a row (a message, into its conversation) says which,
|
|
812
|
+
// so the toast after it can open it with O (w-5513afd09a).
|
|
813
|
+
const wrote = typeof got?.id === 'string' && typeof got?.product === 'string';
|
|
814
|
+
return { ok: true, team: teamNow(), ...(wrote ? { made: { product: got.product, id: got.id } } : {}) };
|
|
651
815
|
} catch (err) {
|
|
652
|
-
return { ok: false, error: String(err?.message ?? err), team:
|
|
816
|
+
return { ok: false, error: String(err?.message ?? err), team: teamNow() };
|
|
653
817
|
}
|
|
654
818
|
};
|
|
655
819
|
// Signing in finishes in the browser, so the app comes back to the front
|
|
@@ -669,9 +833,9 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
669
833
|
try {
|
|
670
834
|
const out = await team.signUp(email, password, name);
|
|
671
835
|
push();
|
|
672
|
-
return { ok: true, team:
|
|
836
|
+
return { ok: true, team: teamNow(), confirm: !!out?.confirm };
|
|
673
837
|
} catch (err) {
|
|
674
|
-
return { ok: false, error: String(err?.message ?? err), team:
|
|
838
|
+
return { ok: false, error: String(err?.message ?? err), team: teamNow() };
|
|
675
839
|
}
|
|
676
840
|
});
|
|
677
841
|
ipcMain.handle('zero:team-create', teamCall(({ name }) => team.createTeam(name)));
|
|
@@ -689,19 +853,81 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
689
853
|
ipcMain.handle('zero:team-sync', teamCall(() => team.syncNow()));
|
|
690
854
|
// A MESSAGE TO A PERSON (approved 2026-10-01: people get messages, never
|
|
691
855
|
// tasks). main/team/index.mjs keeps it between the two of them.
|
|
692
|
-
ipcMain.handle('zero:team-message', teamCall(({ to, body, priority }) => team.message(to, body, priority)));
|
|
856
|
+
ipcMain.handle('zero:team-message', teamCall(({ to, body, priority, files }) => team.message(to, body, priority, files)));
|
|
857
|
+
// ROOMS (alpha): one door, and only these calls through it (main/team/rooms.mjs).
|
|
858
|
+
ipcMain.handle('zero:teammates', async (_e, { op, args = {} } = {}) => {
|
|
859
|
+
const mates = store.teammates;
|
|
860
|
+
if (!mates) return { ok: false, error: 'Teammates need a store.' };
|
|
861
|
+
if (!TEAMMATE_OPS.has(op)) return { ok: false, error: `no teammates call named ${op}` };
|
|
862
|
+
if (!alphaFeatures(config).teammates) return { ok: false, error: 'Teammates are an alpha feature. Turn them on in Settings, Alpha features.' };
|
|
863
|
+
try {
|
|
864
|
+
const a = args && typeof args === 'object' ? args : {};
|
|
865
|
+
let value;
|
|
866
|
+
if (op === 'create') {
|
|
867
|
+
value = mates.create({ ...a, engine: supervisor.engineOffered(a.engine) ?? null });
|
|
868
|
+
if (a.start !== false) value = mates.start(value.id);
|
|
869
|
+
} else if (op === 'describe') value = mates.describe({ ...a, engine: supervisor.engineOffered(a.engine) ?? null });
|
|
870
|
+
else if (op === 'start') value = mates.start(a.id);
|
|
871
|
+
else if (op === 'update') value = mates.update(a.id, { ...a.patch, ...(a.patch && 'engine' in a.patch ? { engine: supervisor.engineOffered(a.patch.engine) ?? null } : {}) });
|
|
872
|
+
else if (op === 'remove') value = mates.remove(a.id);
|
|
873
|
+
else if (op === 'post') value = mates.post({ text: a.text, to: a.to, fresh: a.fresh === true });
|
|
874
|
+
else if (op === 'goAhead') value = mates.goAhead(a.id, a.ref);
|
|
875
|
+
else if (op === 'remember') value = mates.remember(a.id, a.text, { from: 'you' });
|
|
876
|
+
else if (op === 'forget') value = mates.forget(a.id, a.memory);
|
|
877
|
+
supervisor.wake?.();
|
|
878
|
+
push();
|
|
879
|
+
return { ok: true, value: value ?? null };
|
|
880
|
+
} catch (err) {
|
|
881
|
+
return { ok: false, error: String(err?.message ?? err) };
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
ipcMain.handle('zero:rooms', async (_e, { op, args = [] } = {}) => {
|
|
885
|
+
const rooms = team?.rooms;
|
|
886
|
+
if (!rooms) return { ok: false, error: 'This build has no team cloud set up.' };
|
|
887
|
+
if (!ROOM_OPS.has(op)) return { ok: false, error: `no room call named ${op}` };
|
|
888
|
+
try {
|
|
889
|
+
const value = await rooms[op](...(Array.isArray(args) ? args : [args]));
|
|
890
|
+
push();
|
|
891
|
+
return { ok: true, value: value ?? null, rooms: rooms.state() };
|
|
892
|
+
} catch (err) {
|
|
893
|
+
return { ok: false, error: String(err?.message ?? err), rooms: rooms.state() };
|
|
894
|
+
}
|
|
895
|
+
});
|
|
693
896
|
// AN EDIT TO A THREAD, MADE IN PLACE from its summary: the summary's lines,
|
|
694
897
|
// who sees it, its priority and what it is linked to. Works with or without a
|
|
695
898
|
// team, since every thread has a summary.
|
|
696
899
|
ipcMain.handle('zero:thread-edit', (_e, { product, id, patch }) => {
|
|
697
900
|
try {
|
|
698
901
|
store.threadEdit(product, id, patch);
|
|
902
|
+
// PICKING A LEVEL IS THE NEWER WORD on where a thread goes, so a place
|
|
903
|
+
// it was dragged to gives way and it sorts by its level again.
|
|
904
|
+
if (patch?.priority != null) supervisor.setThreadPlaces({ [id]: null });
|
|
699
905
|
push();
|
|
700
906
|
return { ok: true };
|
|
701
907
|
} catch (err) {
|
|
702
908
|
return { ok: false, error: String(err?.message ?? err) };
|
|
703
909
|
}
|
|
704
910
|
});
|
|
911
|
+
// MOVE A THREAD TO ANOTHER PROJECT (w-c9da359466). Only a thread nothing has
|
|
912
|
+
// ever run on: shared/move-thread.mjs says why, and the store asks again.
|
|
913
|
+
// The one fact only this side has is whether a worker is inside it right
|
|
914
|
+
// now, which no field on the row can stand in for.
|
|
915
|
+
ipcMain.handle('zero:move-item', (_e, { product, id, to }) => {
|
|
916
|
+
try {
|
|
917
|
+
const live = supervisor.status().running.some((r) => r.itemId === id);
|
|
918
|
+
if (live) {
|
|
919
|
+
const why = whyItCannotMove(store.readItem(product, id), { live: true });
|
|
920
|
+
return { ok: false, error: why };
|
|
921
|
+
}
|
|
922
|
+
const out = store.moveItem(product, id, to);
|
|
923
|
+
push();
|
|
924
|
+
supervisor.wake();
|
|
925
|
+
return { ok: true, ...out };
|
|
926
|
+
} catch (err) {
|
|
927
|
+
return { ok: false, error: String(err?.message ?? err) };
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
|
|
705
931
|
// A TASK A TEAMMATE GAVE YOU, ROUTED WITH ONE KEY. To an agent: it runs on
|
|
706
932
|
// your Mac (you become its runner). Keep it: it stays yours and the routing
|
|
707
933
|
// choices go away. Hand it back: it returns to whoever gave it to you.
|
|
@@ -737,7 +963,24 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
737
963
|
await team.syncNow();
|
|
738
964
|
}));
|
|
739
965
|
|
|
740
|
-
|
|
966
|
+
// 'granted', or 'settings' once macOS's Microphone page has been opened instead.
|
|
967
|
+
ipcMain.handle('zero:mic-access', async () => {
|
|
968
|
+
try { return host.micAccess ? await host.micAccess() : 'granted'; } catch { return 'settings'; }
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// EDIT OR DELETE A MESSAGE YOU SENT TO A TEAMMATE, in a conversation only,
|
|
972
|
+
// for the same reason as a reaction above.
|
|
973
|
+
ipcMain.handle('zero:team-edit', teamCall(async ({ product, id, on, text, gone }) => {
|
|
974
|
+
if (!teamMe()) throw new Error('sign in first');
|
|
975
|
+
if (!store.readItem(product, id)) throw new Error('that conversation is gone');
|
|
976
|
+
const here = store.listProducts().find((p) => p.slug === product);
|
|
977
|
+
if (!here?.team?.direct) throw new Error('only a message to a teammate can be edited');
|
|
978
|
+
if (!gone && !String(text ?? '').trim()) throw new Error('a message needs words; delete it instead');
|
|
979
|
+
store.editMessage(product, id, { on, text, gone: gone === true });
|
|
980
|
+
await team.syncNow();
|
|
981
|
+
}));
|
|
982
|
+
|
|
983
|
+
ipcMain.handle('zero:compose', (_e, { product, title, body, kind, priority, runAt, start, labels, engine, model, effort, assignee, due, visibility, visibleTo, fromPhone }) => {
|
|
741
984
|
const out = store.composeItem(product, {
|
|
742
985
|
// HOW HARD IT THINKS rides through unjudged: the store keeps any word
|
|
743
986
|
// shaped like a level, and the spawn is the gate that knows each
|
|
@@ -751,6 +994,8 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
751
994
|
// AND WHO MAY SEE IT, when she chose people rather than the team
|
|
752
995
|
// (w-41ff964775). The store keeps the list only beside 'people'.
|
|
753
996
|
visibleTo,
|
|
997
|
+
// The phone command that asked for it (main/phone-inbox.mjs).
|
|
998
|
+
fromPhone,
|
|
754
999
|
people: assignee && teamMe() ? [teamMe(), assignee] : undefined,
|
|
755
1000
|
// WHICH CODING AGENT SHE CHOSE, AND ONLY IF SHE COULD HAVE. `engineOffered`
|
|
756
1001
|
// is the supervisor's own test and the door makes no judgement of its own:
|
|
@@ -938,11 +1183,18 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
938
1183
|
// an archive, so it does not collide with what done means on a work item.
|
|
939
1184
|
ipcMain.handle('zero:end-repeat', (_e, { product, id }) => store.endRepeat(product, id));
|
|
940
1185
|
|
|
941
|
-
ipcMain.handle('zero:create-product', (_e, { name, repoPath }) => {
|
|
1186
|
+
ipcMain.handle('zero:create-product', (_e, { name, repoPath, ofMany }) => {
|
|
942
1187
|
const out = store.createProduct({ name, repoPath });
|
|
943
1188
|
// (privacy page 5.1). The count is the whole payload; the path it was
|
|
944
1189
|
// connected to never leaves.
|
|
945
|
-
|
|
1190
|
+
//
|
|
1191
|
+
// AND A PRESS THAT CONNECTS NINE FOLDERS IS ONE THING SOMEBODY DID, NOT
|
|
1192
|
+
// NINE (w-1116fbb68a, 2026-10-08). The import makes a project per folder,
|
|
1193
|
+
// so this handler fired nine times for one press and nine events read as
|
|
1194
|
+
// nine separate connections to anybody looking at them as activity rather
|
|
1195
|
+
// than as a funnel step. `ofMany` is the caller saying it will count the
|
|
1196
|
+
// press itself, once, with how many folders it connected.
|
|
1197
|
+
if (!ofMany && typeof repoPath === 'string' && repoPath.trim()) analytics.track('repo_connected');
|
|
946
1198
|
return out;
|
|
947
1199
|
});
|
|
948
1200
|
|
|
@@ -961,7 +1213,10 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
961
1213
|
// is the sentence, which the screens show as it stands.
|
|
962
1214
|
ipcMain.handle('zero:choose-folder', async (_e, { startIn } = {}) => {
|
|
963
1215
|
const r = await dialog.showOpenDialog(window, {
|
|
964
|
-
title
|
|
1216
|
+
// The title is what the screen behind it asks. On Linux the chooser
|
|
1217
|
+
// draws one, and it was still asking "Where is its code?", the heading
|
|
1218
|
+
// that screen gave up in w-ec62ab6b38.
|
|
1219
|
+
title: 'Pick a project folder',
|
|
965
1220
|
buttonLabel: 'Use this folder',
|
|
966
1221
|
properties: ['openDirectory', 'createDirectory'],
|
|
967
1222
|
...(startIn ? { defaultPath: startIn } : {}),
|
|
@@ -1082,15 +1337,12 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1082
1337
|
// 'codex'`, land under the project whose folder they ran in exactly as the
|
|
1083
1338
|
// Claude Code ones do, and the ones the user said not now to are here too, so
|
|
1084
1339
|
// there is a way back to them.
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
};
|
|
1092
|
-
ipcMain.handle('zero:agent-threads', () => {
|
|
1093
|
-
const threads = readAllThreads();
|
|
1340
|
+
// EVERY FOLDER EITHER TOOL KEEPS SESSIONS IN (main/agent-homes.mjs), not only
|
|
1341
|
+
// ~/.claude and ~/.codex, which is where the import used to look and nothing
|
|
1342
|
+
// else (w-e46f2e9cea).
|
|
1343
|
+
const readAllThreads = () => readImportableThreads({ config, supervisor, readCodex: readCodexThreadsOffThread });
|
|
1344
|
+
ipcMain.handle('zero:agent-threads', async () => {
|
|
1345
|
+
const threads = await readAllThreads();
|
|
1094
1346
|
lastThreads = threads;
|
|
1095
1347
|
// Each one already in the inbox says so, and the card leaves it out
|
|
1096
1348
|
// (w-db6f5e331e). The walk's folder suggestions still read all of them.
|
|
@@ -1099,12 +1351,48 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1099
1351
|
return { threads: markImported(threads, items) };
|
|
1100
1352
|
});
|
|
1101
1353
|
|
|
1354
|
+
/**
|
|
1355
|
+
* EVERY CHAT THE NAME FIELD CAN SEARCH, read once and kept (w-6418318cd5).
|
|
1356
|
+
*
|
|
1357
|
+
* The card's own list is ten days. Typing a name has to reach past that, and
|
|
1358
|
+
* a name lives inside a transcript, so finding one means opening files —
|
|
1359
|
+
* 29,703 of them on the Mac this was written on. It is taken ONCE, the first
|
|
1360
|
+
* time somebody types, and held for the life of the window: every keystroke
|
|
1361
|
+
* after the first is a filter in the renderer, not a disk walk. `Look again`
|
|
1362
|
+
* on the card drops it, which is the one way back to the disk.
|
|
1363
|
+
*
|
|
1364
|
+
* `reached` is the moment the read gave up at, zero when it read the whole
|
|
1365
|
+
* year. The card prints it, because "no chat with that name" over a read
|
|
1366
|
+
* that stopped in August would be a lie nobody could see.
|
|
1367
|
+
*
|
|
1368
|
+
* What it searches is the card's latest ten-day list joined with that kept
|
|
1369
|
+
* read, so a chat newer than the first search is found too, and the join is
|
|
1370
|
+
* `lastThreads`, because importing resolves an id against it. A chat the
|
|
1371
|
+
* list holds keeps the list's copy: the search reads no title, answer or
|
|
1372
|
+
* ending, and its copy would bring the chat in without them.
|
|
1373
|
+
*/
|
|
1374
|
+
let searchable = null;
|
|
1375
|
+
ipcMain.handle('zero:search-agent-threads', async (_e, { again = false } = {}) => {
|
|
1376
|
+
if (again) searchable = null;
|
|
1377
|
+
if (!searchable) {
|
|
1378
|
+
let got = { threads: [], reached: 0 };
|
|
1379
|
+
try { got = await readSearchableThreads({ config, supervisor, readCodex: readCodexThreadsOffThread }); } catch { got = { threads: [], reached: 0 }; }
|
|
1380
|
+
searchable = got;
|
|
1381
|
+
}
|
|
1382
|
+
const byId = new Map(lastThreads.map((t) => [String(t?.id ?? ''), t]));
|
|
1383
|
+
for (const t of searchable.threads) if (!byId.has(String(t?.id ?? ''))) byId.set(String(t?.id ?? ''), t);
|
|
1384
|
+
lastThreads = [...byId.values()];
|
|
1385
|
+
let items = [];
|
|
1386
|
+
try { items = store.listItems(); } catch { items = []; }
|
|
1387
|
+
return { threads: markImported(lastThreads, items), reached: searchable.reached };
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1102
1390
|
// So this is `zero:import-agents` with the agent taken out of it. No ticks are
|
|
1103
1391
|
// saved, because there is nothing for the card to remember: an imported thread
|
|
1104
1392
|
// is a row, and a row is what stops it being imported twice
|
|
1105
|
-
// (`threadsNeedingRows`).
|
|
1106
|
-
//
|
|
1107
|
-
ipcMain.handle('zero:import-threads', (_e, { product, threads } = {}) => {
|
|
1393
|
+
// (`threadsNeedingRows`). The row's first reply resumes the conversation as a
|
|
1394
|
+
// copy (`Supervisor#importedThreadFor`, GitHub #44).
|
|
1395
|
+
ipcMain.handle('zero:import-threads', async (_e, { product, threads, land } = {}) => {
|
|
1108
1396
|
if (!product) return { ok: false, added: 0, already: 0, ids: [] };
|
|
1109
1397
|
const want = new Set(
|
|
1110
1398
|
(Array.isArray(threads) ? threads : [])
|
|
@@ -1114,15 +1402,17 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1114
1402
|
if (!want.size) { return { ok: true, added: 0, already: 0, ids: [] }; }
|
|
1115
1403
|
// The read is taken again only if nothing has been handed out yet, which is
|
|
1116
1404
|
// the case a test or a fresh window can reach and a person cannot.
|
|
1117
|
-
if (!lastThreads.length) lastThreads = readAllThreads();
|
|
1405
|
+
if (!lastThreads.length) lastThreads = await readAllThreads();
|
|
1118
1406
|
// Two kinds, two rows.
|
|
1119
|
-
const chosen = lastThreads.filter((t) => want.has(String(t?.id ?? '')));
|
|
1120
|
-
let out = { ids: [], added: 0, already: 0 };
|
|
1407
|
+
const chosen = readChatEnds(lastThreads.filter((t) => want.has(String(t?.id ?? ''))));
|
|
1408
|
+
let out = { ids: [], added: 0, already: 0, done: 0 };
|
|
1121
1409
|
try {
|
|
1122
1410
|
// Each store method takes its own kind off the list and leaves the other.
|
|
1123
|
-
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1411
|
+
// Setup's last screen sorts what it brings in: finished chats go to Done.
|
|
1412
|
+
const how = { land: land === 'sorted' ? 'sorted' : 'inbox' };
|
|
1413
|
+
const claude = store.importThreadRows(product, chosen, how);
|
|
1414
|
+
const codex = store.importCodexThreads(product, chosen, how);
|
|
1415
|
+
out = { ids: [...claude.ids, ...codex.ids], added: claude.added + codex.added, already: claude.already + codex.already, done: claude.done + codex.done };
|
|
1126
1416
|
} catch (err) {
|
|
1127
1417
|
push();
|
|
1128
1418
|
return { ok: false, error: String(err.message), added: 0, already: 0, ids: [] };
|
|
@@ -1201,6 +1491,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1201
1491
|
if (isAgentRow(id)) return { ok: false };
|
|
1202
1492
|
const out = store.answerItem(product, id, { status: 'open' });
|
|
1203
1493
|
supervisor.liftBrakeForHer?.();
|
|
1494
|
+
supervisor.forgetDeadStarts?.({ product, id });
|
|
1204
1495
|
supervisor.wake();
|
|
1205
1496
|
return out;
|
|
1206
1497
|
});
|
|
@@ -1281,9 +1572,9 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1281
1572
|
// it. Every handler answers `{ ok, error }` rather than throwing across the
|
|
1282
1573
|
// bridge, because the one thing this screen must never do is leave her
|
|
1283
1574
|
// believing a setting took when it did not.
|
|
1284
|
-
ipcMain.handle('zero:settings-read', () => {
|
|
1575
|
+
ipcMain.handle('zero:settings-read', (_e, how) => {
|
|
1285
1576
|
try {
|
|
1286
|
-
return { ok: true, ...readSettings({ config, supervisor, store }) };
|
|
1577
|
+
return { ok: true, ...readSettings({ config, supervisor, store }, { search: how?.search !== false }) };
|
|
1287
1578
|
} catch (err) {
|
|
1288
1579
|
return { ok: false, error: String(err.message) };
|
|
1289
1580
|
}
|
|
@@ -1291,12 +1582,12 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1291
1582
|
|
|
1292
1583
|
// CHECK AGAIN, from the last card of the walk. The card will not open the
|
|
1293
1584
|
// inbox on a Mac with no Claude Code on it, so the way through it is to
|
|
1294
|
-
// install Claude Code and press this. It
|
|
1295
|
-
//
|
|
1296
|
-
//
|
|
1297
|
-
ipcMain.handle('zero:claude-recheck', () => {
|
|
1585
|
+
// install Claude Code and press this. It waits for a fresh shell answer,
|
|
1586
|
+
// asked off the window's thread, because a button that says it looked again
|
|
1587
|
+
// and reads an old memory is the same broken promise as a key that does nothing.
|
|
1588
|
+
ipcMain.handle('zero:claude-recheck', async () => {
|
|
1298
1589
|
try {
|
|
1299
|
-
return { ok: true, workspace:
|
|
1590
|
+
return { ok: true, workspace: await lookAgainFor('claude', config) };
|
|
1300
1591
|
} catch (err) {
|
|
1301
1592
|
return { ok: false, error: String(err.message) };
|
|
1302
1593
|
}
|
|
@@ -1307,9 +1598,56 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1307
1598
|
// search finds lands on `config.codexBin`, which is the fact `engineChoices`
|
|
1308
1599
|
// reads, so a press that finds Codex is also what makes the Coding agent row
|
|
1309
1600
|
// appear -- without the restart a main-process fact usually costs.
|
|
1310
|
-
ipcMain.handle('zero:codex-recheck', () => {
|
|
1601
|
+
ipcMain.handle('zero:codex-recheck', async () => {
|
|
1602
|
+
try {
|
|
1603
|
+
return { ok: true, workspace: await lookAgainFor('codex', config) };
|
|
1604
|
+
} catch (err) {
|
|
1605
|
+
return { ok: false, error: String(err.message) };
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
/*
|
|
1610
|
+
* AND WHEN NEITHER BUTTON CAN FIND IT: POINT AT THE FILE YOURSELF
|
|
1611
|
+
* (w-ce8c1a9275). The search asks the login shell first now, which covers
|
|
1612
|
+
* every installer a person's own PATH knows about -- but a wrapper script
|
|
1613
|
+
* somewhere nobody would guess, a tool on a volume, or a machine whose login
|
|
1614
|
+
* shell will not start at all still used to mean a path WE ship and a release
|
|
1615
|
+
* THEY wait for. This is the half that ends that.
|
|
1616
|
+
*
|
|
1617
|
+
* ONE ROUND TRIP, like `zero:project-icon` below and for the same reason: the
|
|
1618
|
+
* panel opens in this process and the choice is applied in the same call, so
|
|
1619
|
+
* the only path that ever reaches `pointAtEngineBin` is one the open panel
|
|
1620
|
+
* itself produced. The window says WHICH coding agent it is asking about and
|
|
1621
|
+
* nothing else, which is the whole of what it is trusted with -- `codexBin`
|
|
1622
|
+
* is a path that gets spawned, and every worker on this machine has a broad
|
|
1623
|
+
* Bash allow (tests/where-codex-runs-from-is-not-a-file-a-worker-can-write).
|
|
1624
|
+
*
|
|
1625
|
+
* THE REFUSAL IS A SENTENCE AND NOT AN ERROR. Picking a folder, or a file
|
|
1626
|
+
* that cannot be run, is an ordinary mistake at a file panel; it comes back
|
|
1627
|
+
* as words the row prints, the way `zero:choose-folder` already does.
|
|
1628
|
+
*/
|
|
1629
|
+
ipcMain.handle('zero:engine-bin', async (_e, { engine, action } = {}) => {
|
|
1630
|
+
if (engine !== 'claude' && engine !== 'codex') return { ok: false, error: 'Unknown coding agent.' };
|
|
1311
1631
|
try {
|
|
1312
|
-
|
|
1632
|
+
// Handing the choice back needs no panel: it is the row saying "look for
|
|
1633
|
+
// it again", and the search takes over from there.
|
|
1634
|
+
if (action === 'search') {
|
|
1635
|
+
const gave = searchForEngineBin(config, engine);
|
|
1636
|
+
if (!gave.ok) return { ok: false, refused: gave.say };
|
|
1637
|
+
return { ok: true, ...readSettings({ config, supervisor, store }) };
|
|
1638
|
+
}
|
|
1639
|
+
const { dialog } = await import('electron');
|
|
1640
|
+
const r = await dialog.showOpenDialog(window, {
|
|
1641
|
+
title: engine === 'codex' ? 'Where is Codex?' : 'Where is Claude Code?',
|
|
1642
|
+
buttonLabel: 'Use this one',
|
|
1643
|
+
properties: ['openFile', 'showHiddenFiles', 'treatPackageAsDirectory'],
|
|
1644
|
+
});
|
|
1645
|
+
// Cancelling is not a failure and must not draw one. `ok` with nothing
|
|
1646
|
+
// changed is the honest answer: they opened the panel and closed it again.
|
|
1647
|
+
if (r.canceled || !r.filePaths?.length) return { ok: true, canceled: true };
|
|
1648
|
+
const took = pointAtEngineBin(config, engine, r.filePaths[0]);
|
|
1649
|
+
if (!took.ok) return { ok: false, refused: took.say };
|
|
1650
|
+
return { ok: true, ...readSettings({ config, supervisor, store }) };
|
|
1313
1651
|
} catch (err) {
|
|
1314
1652
|
return { ok: false, error: String(err.message) };
|
|
1315
1653
|
}
|
|
@@ -1321,9 +1659,11 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1321
1659
|
// rest of the app runs on it without a restart.
|
|
1322
1660
|
const engineSetup = createEngineSetup({
|
|
1323
1661
|
find: (engine) => {
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1662
|
+
const finders = {
|
|
1663
|
+
claude: async () => { const s = await lookAgainFor('claude', config); return { found: s.claudeFound, path: s.claudeBin }; },
|
|
1664
|
+
codex: async () => { const s = await lookAgainFor('codex', config); return { found: s.found, path: s.bin }; },
|
|
1665
|
+
};
|
|
1666
|
+
return finders[engine]?.() ?? { found: false, path: null };
|
|
1327
1667
|
},
|
|
1328
1668
|
// WHICH ONE IS SIGNED IN, kept on the config for the run that reads it
|
|
1329
1669
|
// (`Supervisor#_enginesFound`) and never saved: it changes outside the app.
|
|
@@ -1342,7 +1682,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1342
1682
|
// A sign-in left waiting on the browser is not left running after the app.
|
|
1343
1683
|
app.on('will-quit', () => { engineSetup.cancel('claude'); engineSetup.cancel('codex'); });
|
|
1344
1684
|
ipcMain.handle('zero:engine-setup', async (_e, { action, engine } = {}) => {
|
|
1345
|
-
if (engine
|
|
1685
|
+
if (!isEngine(engine)) return { ok: false, error: 'Unknown coding agent.' };
|
|
1346
1686
|
try {
|
|
1347
1687
|
if (action === 'ready') return { ok: true, ...(await engineSetup.readiness(engine)) };
|
|
1348
1688
|
if (action === 'start') return { ok: true, ...engineSetup.start(engine) };
|
|
@@ -1389,7 +1729,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1389
1729
|
|
|
1390
1730
|
ipcMain.handle('zero:settings-set-project', (_e, payload) => {
|
|
1391
1731
|
try {
|
|
1392
|
-
setProjectSetting({ config, supervisor }, payload ?? {});
|
|
1732
|
+
setProjectSetting({ config, supervisor, store }, payload ?? {});
|
|
1393
1733
|
push();
|
|
1394
1734
|
return { ok: true, ...readSettings({ config, supervisor, store }) };
|
|
1395
1735
|
} catch (err) {
|
|
@@ -1397,9 +1737,90 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1397
1737
|
}
|
|
1398
1738
|
});
|
|
1399
1739
|
|
|
1740
|
+
// SIGNING IN TO AGENTBOX CLOUD ITSELF, on the app's own sign-in page
|
|
1741
|
+
// (main/cloud/account.mjs): Google, or an email and a password, traded for
|
|
1742
|
+
// this Mac's cloud key. One session per page, made when the page first asks.
|
|
1743
|
+
let cloudAccount = null;
|
|
1744
|
+
const cloudAccountSessionNow = async () => {
|
|
1745
|
+
const home = supervisor.config.home || os.homedir();
|
|
1746
|
+
const { cloudAccountConfig, cloudAccountSession } = await import('./cloud/account.mjs');
|
|
1747
|
+
const config = cloudAccountConfig(supervisor.appDir, { home });
|
|
1748
|
+
if (!config) return null;
|
|
1749
|
+
if (!cloudAccount || cloudAccount.config !== JSON.stringify(config)) {
|
|
1750
|
+
const { shell } = await import('electron');
|
|
1751
|
+
cloudAccount = { config: JSON.stringify(config), value: config, session: cloudAccountSession({ config, openExternal: (u) => shell.openExternal(u), log: (l) => console.log(`cloud account: ${l}`) }) };
|
|
1752
|
+
}
|
|
1753
|
+
return cloudAccount;
|
|
1754
|
+
};
|
|
1755
|
+
ipcMain.handle('zero:cloud-account-sign-in', async (_e, { method, email, password, name, acceptTerms } = {}) => {
|
|
1756
|
+
const acct = await cloudAccountSessionNow();
|
|
1757
|
+
if (!acct) return { ok: false, error: 'This copy of the app has no cloud to sign in to.' };
|
|
1758
|
+
const { cloudSignIn } = await import('./cloud/account.mjs');
|
|
1759
|
+
const out = await cloudSignIn({ config: acct.value, session: acct.session, method, email, password, name, acceptTerms: acceptTerms === true, home: supervisor.config.home || os.homedir() });
|
|
1760
|
+
if (method === 'google') { try { if (window?.isMinimized?.()) window.restore(); window?.show?.(); } catch { /* no window to raise */ } }
|
|
1761
|
+
push();
|
|
1762
|
+
return out;
|
|
1763
|
+
});
|
|
1764
|
+
ipcMain.handle('zero:cloud-account-cancel', async () => { (await cloudAccountSessionNow())?.session.cancel(); return { ok: true }; });
|
|
1765
|
+
ipcMain.handle('zero:cloud-account-reopen', async () => { await (await cloudAccountSessionNow())?.session.reopen(); return { ok: true }; });
|
|
1766
|
+
ipcMain.handle('zero:cloud-account-sign-out', async () => {
|
|
1767
|
+
const { signOutOfCloud } = await import('./cloud/account.mjs');
|
|
1768
|
+
const out = await signOutOfCloud({ home: supervisor.config.home || os.homedir() });
|
|
1769
|
+
push();
|
|
1770
|
+
return out;
|
|
1771
|
+
});
|
|
1772
|
+
// PAYING FOR A CLOUD PLAN: Stripe's own pages, in the browser, as other
|
|
1773
|
+
// desktop apps do. An app window drew them badly and never offers Apple Pay.
|
|
1774
|
+
const cloudCall = async (name, args = {}) => {
|
|
1775
|
+
try {
|
|
1776
|
+
const account = await import('./cloud/account.mjs');
|
|
1777
|
+
const { shell } = await import('electron');
|
|
1778
|
+
return await account[name]({ ...args, openExternal: (u) => shell.openExternal(u), home: supervisor.config.home || os.homedir() });
|
|
1779
|
+
} catch (err) {
|
|
1780
|
+
return { ok: false, error: 'unreachable', message: `The cloud did not answer: ${err?.message || err}` };
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
ipcMain.handle('zero:cloud-account-plan', async () => {
|
|
1784
|
+
const plan = await cloudCall('cloudPlan');
|
|
1785
|
+
supervisor.cloudPlanSeen?.(plan);
|
|
1786
|
+
return plan;
|
|
1787
|
+
});
|
|
1788
|
+
ipcMain.handle('zero:cloud-account-pay', (_e, { plan } = {}) => cloudCall('payForCloud', { plan }));
|
|
1789
|
+
ipcMain.handle('zero:cloud-account-billing', () => cloudCall('openCloudBilling'));
|
|
1790
|
+
|
|
1791
|
+
// THE CLOUD SETUP'S SIGN-IN for one account, made in the cloud
|
|
1792
|
+
// (main/cloud/logins.mjs). The Mac relays the link and the code only.
|
|
1793
|
+
ipcMain.handle('zero:cloud-signin', async (_e, { action, engine, key, code } = {}) => {
|
|
1794
|
+
try {
|
|
1795
|
+
const out = await supervisor.cloudSignIn(action, engine, key ?? null, code);
|
|
1796
|
+
push();
|
|
1797
|
+
return { ok: true, key: out?.key ?? null };
|
|
1798
|
+
} catch (err) {
|
|
1799
|
+
return { ok: false, error: String(err.message) };
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1802
|
+
// CLAUDE'S CODE, FROM THE CLIPBOARD, WHEN SHE COMES BACK (2026-10-14: she
|
|
1803
|
+
// approved on Claude's page and waited, not seeing that the code it shows
|
|
1804
|
+
// has to come back). Answered only while a Claude sign-in is waiting for
|
|
1805
|
+
// its code, and only with text shaped like one: anything else she copied
|
|
1806
|
+
// stays where it is.
|
|
1807
|
+
ipcMain.handle('zero:cloud-clipboard-code', () => {
|
|
1808
|
+
try {
|
|
1809
|
+
const waiting = (supervisor.cloudStatus?.().signIns || []).some((s) => s.engine === 'claude' && s.state === 'needs-code');
|
|
1810
|
+
if (!waiting) return { code: null };
|
|
1811
|
+
const text = String(host?.readClipboardText?.() ?? '').trim();
|
|
1812
|
+
return { code: looksLikeClaudeCode(text) ? text : null };
|
|
1813
|
+
} catch {
|
|
1814
|
+
return { code: null };
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1400
1817
|
ipcMain.handle('zero:settings-set-workspace', (_e, payload) => {
|
|
1401
1818
|
try {
|
|
1402
|
-
|
|
1819
|
+
// Before the save: once the switch is off, nothing leaves, not even this.
|
|
1820
|
+
if (countsTurnedOff(payload)) { analytics.track('counts_turned_off'); analytics.flush(); }
|
|
1821
|
+
setWorkspaceSetting({ config, supervisor, store }, payload ?? {});
|
|
1822
|
+
if (payload?.key === 'alphaRooms') team?.rooms?.tick().catch(() => {});
|
|
1823
|
+
if (cornerTagTurnedOff(payload)) analytics.track('tag_turned_off');
|
|
1403
1824
|
push();
|
|
1404
1825
|
return { ok: true, ...readSettings({ config, supervisor, store }) };
|
|
1405
1826
|
} catch (err) {
|
|
@@ -1523,29 +1944,9 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1523
1944
|
// in the product's attachments/ dir and come back as relative paths the
|
|
1524
1945
|
// caller embeds in the body as markdown. Relative paths keep them readable
|
|
1525
1946
|
// by workers through the MCP and renderable in the message view.
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
const outDir = path.join(dir, 'attachments');
|
|
1530
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
1531
|
-
const saved = [];
|
|
1532
|
-
for (const file of files ?? []) {
|
|
1533
|
-
const safe = String(file.name ?? 'file').replace(/[^\w.-]+/g, '-').slice(0, 80);
|
|
1534
|
-
// A RANDOM SUFFIX, THE SAME ONE THE STAGING PATH ALREADY USES. The name
|
|
1535
|
-
// was the millisecond plus the filename, so two files saved in the same
|
|
1536
|
-
// millisecond with the same name overwrote each other and the message
|
|
1537
|
-
// ended up with two links to one picture. Screenshots are often sent in
|
|
1538
|
-
// pairs, which is exactly the shape that collides. It has never been observed
|
|
1539
|
-
// happening; it is one line to make impossible.
|
|
1540
|
-
const rel = `attachments/${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}-${safe}`;
|
|
1541
|
-
const abs = path.join(dir, rel);
|
|
1542
|
-
if (file.dataBase64) fs.writeFileSync(abs, Buffer.from(file.dataBase64, 'base64'));
|
|
1543
|
-
else if (file.srcPath) fs.copyFileSync(file.srcPath, abs);
|
|
1544
|
-
else continue;
|
|
1545
|
-
saved.push({ rel, name: safe, image: /\.(png|jpe?g|gif|webp)$/i.test(safe) });
|
|
1546
|
-
}
|
|
1547
|
-
return saved;
|
|
1548
|
-
});
|
|
1947
|
+
// The saving itself is main/save-attachments.mjs, shared with a message to a
|
|
1948
|
+
// person, which saves its own once it knows which conversation it is in.
|
|
1949
|
+
ipcMain.handle('zero:save-attachments', async (_e, { product, files }) => saveAttachments(store.productDir(product), files));
|
|
1549
1950
|
|
|
1550
1951
|
// A PASTED SCREENSHOT GOES TO DISK THE MOMENT IT IS PASTED.
|
|
1551
1952
|
//
|
|
@@ -1586,32 +1987,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1586
1987
|
// costs nothing at send time because `zero:save-attachments` above already
|
|
1587
1988
|
// copies from `srcPath`. Bytes are copied on disk rather than carried through
|
|
1588
1989
|
// this bridge as base64, which a file picked off disk should never need.
|
|
1589
|
-
ipcMain.handle('zero:stage-attachment', async (_e,
|
|
1590
|
-
const fs = await import('node:fs');
|
|
1591
|
-
const dir = path.join(config.accountRoot, '.staging');
|
|
1592
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1593
|
-
// Sweep on the way in rather than on a timer: an abandoned paste is only
|
|
1594
|
-
// ever discovered by the next one, and a week is longer than any draft she
|
|
1595
|
-
// has ever left open. Failure to sweep must never fail the paste.
|
|
1596
|
-
try {
|
|
1597
|
-
const week = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
1598
|
-
for (const f of fs.readdirSync(dir)) {
|
|
1599
|
-
const p = path.join(dir, f);
|
|
1600
|
-
try { if (fs.statSync(p).mtimeMs < week) fs.unlinkSync(p); } catch { /* leave it */ }
|
|
1601
|
-
}
|
|
1602
|
-
} catch { /* a sweep that cannot run is not a paste that failed */ }
|
|
1603
|
-
const safe = String(name ?? 'pasted.png').replace(/[^\w.-]+/g, '-').slice(0, 80);
|
|
1604
|
-
const abs = path.join(dir, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}-${safe}`);
|
|
1605
|
-
// A path is a drag, bytes are a paste. Already inside the folder we serve
|
|
1606
|
-
// from is left alone: copying it would only make a second file to sweep.
|
|
1607
|
-
if (typeof srcPath === 'string' && srcPath) {
|
|
1608
|
-
if (path.resolve(srcPath) === path.resolve(path.join(dir, path.basename(srcPath)))) return { path: srcPath };
|
|
1609
|
-
fs.copyFileSync(srcPath, abs);
|
|
1610
|
-
return { path: abs };
|
|
1611
|
-
}
|
|
1612
|
-
fs.writeFileSync(abs, Buffer.from(String(dataBase64 ?? ''), 'base64'));
|
|
1613
|
-
return { path: abs };
|
|
1614
|
-
});
|
|
1990
|
+
ipcMain.handle('zero:stage-attachment', async (_e, file) => stageAttachment(path.join(config.accountRoot, '.staging'), file));
|
|
1615
1991
|
|
|
1616
1992
|
// Unstick ONE stalled item: forget its answer's delivery mark and tick, so
|
|
1617
1993
|
// a fresh worker picks the answer up now.
|
|
@@ -1620,6 +1996,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1620
1996
|
const item = store.listItems().find((i) => i.product === product && i.id === id);
|
|
1621
1997
|
if (!item?.answer || item.answer === '(withdrawn)') return { ok: false, error: 'no live answer to redeliver' };
|
|
1622
1998
|
supervisor.liftBrakeForHer?.();
|
|
1999
|
+
supervisor.forgetDeadStarts?.(item);
|
|
1623
2000
|
supervisor.redeliverAnswer(item, item.answer);
|
|
1624
2001
|
supervisor.tick?.().catch(() => {});
|
|
1625
2002
|
return { ok: true };
|
|
@@ -1661,7 +2038,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1661
2038
|
const { shell } = await import('electron');
|
|
1662
2039
|
if (typeof src !== 'string' || !src) return { ok: false, error: 'no path' };
|
|
1663
2040
|
if (/^https?:/.test(src)) { if (mode !== 'resolve') shell.openExternal(src); return { ok: true, opened: src } }
|
|
1664
|
-
const found = resolveArtifact({
|
|
2041
|
+
const found = await resolveArtifact({
|
|
1665
2042
|
src,
|
|
1666
2043
|
product,
|
|
1667
2044
|
products: store.listProducts(),
|
|
@@ -1707,7 +2084,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1707
2084
|
// text rather than the path. What may be written is decided in
|
|
1708
2085
|
// main/doc-file.mjs: markdown only, inside this product's own folders only.
|
|
1709
2086
|
ipcMain.handle('zero:read-doc', async (_e, { product, src }) => {
|
|
1710
|
-
const found = resolveArtifact({
|
|
2087
|
+
const found = await resolveArtifact({
|
|
1711
2088
|
src,
|
|
1712
2089
|
product,
|
|
1713
2090
|
products: store.listProducts(),
|
|
@@ -1721,7 +2098,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1721
2098
|
// THE CHANGE A RUN MADE. Same finder as every other artifact, so a click on
|
|
1722
2099
|
// the chip and a click on a path in her message open the same file.
|
|
1723
2100
|
ipcMain.handle('zero:code-change', async (_e, { product, src }) => {
|
|
1724
|
-
const found = resolveArtifact({
|
|
2101
|
+
const found = await resolveArtifact({
|
|
1725
2102
|
src,
|
|
1726
2103
|
product,
|
|
1727
2104
|
products: store.listProducts(),
|
|
@@ -1742,8 +2119,8 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1742
2119
|
// same pair the pane already has on screen. The change is re-read here rather
|
|
1743
2120
|
// than trusted from the renderer, because it is what says which folders a
|
|
1744
2121
|
// write may land in and that decision may not come from the window.
|
|
1745
|
-
const openChange = (product, src) => {
|
|
1746
|
-
const found = resolveArtifact({
|
|
2122
|
+
const openChange = async (product, src) => {
|
|
2123
|
+
const found = await resolveArtifact({
|
|
1747
2124
|
src,
|
|
1748
2125
|
product,
|
|
1749
2126
|
products: store.listProducts(),
|
|
@@ -1759,7 +2136,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1759
2136
|
};
|
|
1760
2137
|
|
|
1761
2138
|
ipcMain.handle('zero:code-file', async (_e, { product, src, path: filePath }) => {
|
|
1762
|
-
const opened = openChange(product, src);
|
|
2139
|
+
const opened = await openChange(product, src);
|
|
1763
2140
|
if (!opened.ok) return opened;
|
|
1764
2141
|
const found = findFile({ change: opened.change, product: opened.prod, filePath });
|
|
1765
2142
|
if (!found.ok) return found;
|
|
@@ -1767,7 +2144,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1767
2144
|
});
|
|
1768
2145
|
|
|
1769
2146
|
ipcMain.handle('zero:save-code-file', async (_e, { product, src, path: filePath, text, mtime }) => {
|
|
1770
|
-
const opened = openChange(product, src);
|
|
2147
|
+
const opened = await openChange(product, src);
|
|
1771
2148
|
if (!opened.ok) return opened;
|
|
1772
2149
|
const found = findFile({ change: opened.change, product: opened.prod, filePath });
|
|
1773
2150
|
if (!found.ok) return found;
|
|
@@ -1776,7 +2153,7 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1776
2153
|
|
|
1777
2154
|
ipcMain.handle('zero:write-doc', async (_e, { product, src, text, mtime }) => {
|
|
1778
2155
|
const prod = store.listProducts().find((p) => p.slug === product) ?? null;
|
|
1779
|
-
const found = resolveArtifact({
|
|
2156
|
+
const found = await resolveArtifact({
|
|
1780
2157
|
src,
|
|
1781
2158
|
product,
|
|
1782
2159
|
products: store.listProducts(),
|
|
@@ -1828,9 +2205,9 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1828
2205
|
|
|
1829
2206
|
// The thread history behind the time under a task's title. Raw ledger lines,
|
|
1830
2207
|
// read fresh: the renderer owns what they mean, this owns getting them across.
|
|
1831
|
-
ipcMain.handle('zero:item-history', async (_e, { product, id }) => {
|
|
2208
|
+
ipcMain.handle('zero:item-history', async (_e, { product, id, before }) => {
|
|
1832
2209
|
try {
|
|
1833
|
-
return { ok: true, lines: store.readHistory(product, id) };
|
|
2210
|
+
return { ok: true, lines: store.readHistory(product, id, { before }) };
|
|
1834
2211
|
} catch (err) {
|
|
1835
2212
|
return { ok: false, error: err.message, lines: [] };
|
|
1836
2213
|
}
|
|
@@ -1842,6 +2219,14 @@ export function registerIpc({ store, supervisor, config, window, analytics = NO_
|
|
|
1842
2219
|
// to decide is when the app is allowed to disappear and come back.
|
|
1843
2220
|
ipcMain.handle('zero:update-check', async () => updater.check({ manual: true }));
|
|
1844
2221
|
ipcMain.handle('zero:update-install', () => ({ started: updater.install() }));
|
|
2222
|
+
// AND A THIRD, for the copy that cannot update itself from where it is: move
|
|
2223
|
+
// the app into Applications (shared/where-it-runs-from.mjs). It is a door of
|
|
2224
|
+
// its own rather than part of the install, because it is the opposite
|
|
2225
|
+
// decision — nothing has been downloaded and nothing will be until the app
|
|
2226
|
+
// is somewhere macOS lets it write.
|
|
2227
|
+
// A copy run from source answers no to it rather than throwing: there is no
|
|
2228
|
+
// bundle to move (main/source-updater.mjs).
|
|
2229
|
+
ipcMain.handle('zero:update-move', () => updater.move?.() ?? { moved: false, error: null });
|
|
1845
2230
|
|
|
1846
2231
|
// Handed back so the Cmd+Y/Cmd+N chord answers through the same door the
|
|
1847
2232
|
// buttons do, rather than reaching past it into approvals.answer.
|