agentbox-app 0.1.13 → 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/agent-sessions.mjs +282 -268
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +12 -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-input.mjs +48 -4
- package/main/claude-plan.mjs +48 -0
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +18 -40
- 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-models.mjs +20 -2
- package/main/codex-session.mjs +212 -507
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +71 -3
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +17 -3
- package/main/codex.mjs +95 -363
- package/main/config.mjs +11 -1
- package/main/corner-tag.mjs +340 -19
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +232 -282
- package/main/engine-setup.mjs +21 -9
- package/main/feedback.mjs +3 -25
- 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.mjs +32 -11
- package/main/harnesses/codex.mjs +9 -2
- package/main/importable-threads.mjs +64 -6
- package/main/ipc.mjs +362 -111
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +199 -84
- package/main/memory-gate-history.mjs +36 -10
- package/main/memory-gate-server.mjs +62 -0
- package/main/memory-gate.mjs +14 -1
- 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 +24 -8
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +29 -9
- 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-client.mjs +69 -1
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +12 -41
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +45 -3
- package/main/settings.mjs +220 -44
- package/main/shell-path.mjs +185 -37
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +115 -19
- package/main/source-updater.mjs +18 -10
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -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 +226 -52
- package/main/supervisor.mjs +3103 -341
- package/main/task-folders.mjs +430 -30
- package/main/task-terminals.mjs +110 -34
- 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/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +90 -219
- 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 +21 -5
- package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
- package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
- package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
- package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
- package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
- package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
- package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
- package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
- package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
- package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
- package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
- package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
- package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
- package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
- 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-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
- package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
- package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
- package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
- package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
- package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
- package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
- package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
- package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
- package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
- package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
- package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
- package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
- package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
- package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
- package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
- package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
- package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
- package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
- package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
- package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
- package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
- package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
- package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
- package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
- package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
- package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
- package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
- package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
- package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
- package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
- package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
- package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
- package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
- package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
- package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
- package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
- package/renderer/dist/corner-tag.html +3 -3
- package/renderer/dist/index.html +3 -3
- package/scripts/after-install.mjs +30 -8
- 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 +60 -34
- 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 +27 -1
- package/shared/claude-code-shape.mjs +6 -0
- 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 +23 -1
- package/shared/corner-tag.mjs +284 -29
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +70 -309
- 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 +8 -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 +6 -0
- package/shared/notify-rules.mjs +36 -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/rank.d.mts +2 -1
- package/shared/rank.mjs +32 -19
- 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 -3
- package/shared/runs-on.mjs +8 -5
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +14 -6
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +443 -40
- 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/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +9 -0
- package/shared/usage.mjs +49 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/work-items.mjs +127 -30
- package/shared/work-lines.mjs +92 -244
- package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
- package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
- package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// TASK TERMINALS OUTLIVE THE APP. This process holds the shells. It is started
|
|
2
|
+
// detached, so quitting or restarting the app leaves them running; restarting
|
|
3
|
+
// the computer ends them like everything else. The app reconnects when it starts.
|
|
4
|
+
//
|
|
5
|
+
// Lines of JSON over a socket only this user can reach: `{id, op, args}` in,
|
|
6
|
+
// `{id, value}` or `{id, error}` back. The ops are TaskTerminals' own.
|
|
7
|
+
// With no terminal and no app connected for a minute it ends itself.
|
|
8
|
+
//
|
|
9
|
+
// ELECTRON_RUN_AS_NODE=1 <electron> main/terminal-host.mjs <socket>
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import net from 'node:net';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { nameSlug } from '../shared/product-name.mjs';
|
|
17
|
+
|
|
18
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
// The app talks to any host speaking this, whichever version started it, so
|
|
20
|
+
// the terminals an older app left are still reachable after an update. Change
|
|
21
|
+
// it only with a client that still speaks the old one.
|
|
22
|
+
export const PROTOCOL = 1;
|
|
23
|
+
// A host started by older code keeps the terminals it has; new terminals open
|
|
24
|
+
// in a host made of the code the app is running now.
|
|
25
|
+
const MADE_OF = ['terminal-host.mjs', 'task-terminals.mjs', '../shared/terminal-state.mjs'];
|
|
26
|
+
export const HOST_ALREADY_THERE = 3;
|
|
27
|
+
const OPS = new Set(['open', 'read', 'write', 'resize', 'close', 'end', 'list']);
|
|
28
|
+
|
|
29
|
+
export function terminalHostStamp() {
|
|
30
|
+
const h = crypto.createHash('sha1');
|
|
31
|
+
for (const f of MADE_OF) { try { h.update(fs.readFileSync(path.join(HERE, f))); } catch { h.update(f); } }
|
|
32
|
+
return h.digest('hex').slice(0, 12);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const prefix = (uid) => `${nameSlug}-terminals-${uid}-`;
|
|
36
|
+
|
|
37
|
+
// Short enough for a socket's path (104 bytes on macOS), and this user's own.
|
|
38
|
+
export function terminalHostSocket({ stamp = terminalHostStamp(), dir = os.tmpdir(), uid = process.getuid?.() ?? 0 } = {}) {
|
|
39
|
+
return path.join(dir, `${prefix(uid)}${stamp}.sock`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Every host socket of this user's, newest code or not. */
|
|
43
|
+
export function terminalHostSockets({ dir = os.tmpdir(), uid = process.getuid?.() ?? 0 } = {}) {
|
|
44
|
+
try {
|
|
45
|
+
return fs.readdirSync(dir).filter((f) => f.startsWith(prefix(uid)) && f.endsWith('.sock')).map((f) => path.join(dir, f));
|
|
46
|
+
} catch { return []; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function serveTerminals({ sockPath, terminals, stamp = terminalHostStamp(), idleMs = 60_000, onIdle = () => {}, log = () => {} }) {
|
|
50
|
+
let clients = 0;
|
|
51
|
+
let idleTimer = null;
|
|
52
|
+
const idle = () => {
|
|
53
|
+
clearTimeout(idleTimer);
|
|
54
|
+
if (clients || terminals.sessions.size) return;
|
|
55
|
+
idleTimer = setTimeout(() => { if (!clients && !terminals.sessions.size) onIdle(); }, idleMs);
|
|
56
|
+
idleTimer.unref?.();
|
|
57
|
+
};
|
|
58
|
+
const watch = setInterval(idle, idleMs);
|
|
59
|
+
watch.unref?.();
|
|
60
|
+
const answer = async ({ op, args = [] }) => {
|
|
61
|
+
if (op === 'hello') return { protocol: PROTOCOL, stamp, pid: process.pid };
|
|
62
|
+
if (!OPS.has(op)) throw new Error(`Unknown terminal action: ${op}`);
|
|
63
|
+
// The host cannot read the store, so it never works out a folder itself:
|
|
64
|
+
// a terminal it no longer has is one the app opens afresh.
|
|
65
|
+
if (op === 'open' && !args[1] && !terminals.sessions.has(args[0])) throw new Error('Terminal session ended. Open a new terminal to continue.');
|
|
66
|
+
return terminals[op](...args);
|
|
67
|
+
};
|
|
68
|
+
const server = net.createServer((sock) => {
|
|
69
|
+
clients += 1;
|
|
70
|
+
clearTimeout(idleTimer);
|
|
71
|
+
let buf = '';
|
|
72
|
+
const send = (msg) => { if (!sock.destroyed) sock.write(`${JSON.stringify(msg)}\n`); };
|
|
73
|
+
sock.setEncoding('utf8');
|
|
74
|
+
sock.on('data', (d) => {
|
|
75
|
+
buf += d;
|
|
76
|
+
let n;
|
|
77
|
+
while ((n = buf.indexOf('\n')) >= 0) {
|
|
78
|
+
const line = buf.slice(0, n);
|
|
79
|
+
buf = buf.slice(n + 1);
|
|
80
|
+
let msg;
|
|
81
|
+
try { msg = JSON.parse(line); } catch { continue; }
|
|
82
|
+
Promise.resolve().then(() => answer(msg))
|
|
83
|
+
.then((value) => send({ id: msg.id, value: value ?? null }), (err) => send({ id: msg.id, error: String(err?.message ?? err) }));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
sock.on('close', () => { clients -= 1; idle(); });
|
|
87
|
+
sock.on('error', () => {});
|
|
88
|
+
});
|
|
89
|
+
server.on('error', (err) => log(`terminal host: ${err.message}`));
|
|
90
|
+
// Node deletes the path a server listened on as it closes, so it listens on
|
|
91
|
+
// a name of its own and moves it into place, and on the way out takes away
|
|
92
|
+
// only a socket that is still its own.
|
|
93
|
+
let mine = null;
|
|
94
|
+
const own = `${sockPath}.${process.pid}`;
|
|
95
|
+
const ours = () => { try { return mine !== null && fs.statSync(sockPath).ino === mine; } catch { return false; } };
|
|
96
|
+
const started = new Promise((resolve) => {
|
|
97
|
+
const probe = net.connect(sockPath);
|
|
98
|
+
const timer = setTimeout(() => { probe.destroy(); resolve('taken'); }, 2_000);
|
|
99
|
+
probe.once('connect', () => { clearTimeout(timer); probe.destroy(); resolve('taken'); });
|
|
100
|
+
probe.once('error', () => {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
try { fs.unlinkSync(own); } catch { /* not there */ }
|
|
103
|
+
server.listen(own, () => {
|
|
104
|
+
try { fs.chmodSync(own, 0o600); fs.renameSync(own, sockPath); mine = fs.statSync(sockPath).ino; } catch (err) { log(`terminal host: ${err.message}`); }
|
|
105
|
+
idle();
|
|
106
|
+
resolve('serving');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
server,
|
|
112
|
+
started,
|
|
113
|
+
close: () => started.then(() => new Promise((resolve) => {
|
|
114
|
+
clearTimeout(idleTimer);
|
|
115
|
+
clearInterval(watch);
|
|
116
|
+
if (ours()) { try { fs.unlinkSync(sockPath); } catch { /* gone */ } }
|
|
117
|
+
if (server.listening) server.close(() => resolve()); else resolve();
|
|
118
|
+
})),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Started as a program by the app (main/terminal-hosts.mjs).
|
|
123
|
+
if (path.basename(process.argv[1] || '') === 'terminal-host.mjs') {
|
|
124
|
+
const sockPath = process.argv[2];
|
|
125
|
+
// Loaded before the socket opens, so a host that cannot make a shell never
|
|
126
|
+
// answers and the app keeps its terminals itself.
|
|
127
|
+
(await import('node:module')).createRequire(import.meta.url)('node-pty');
|
|
128
|
+
const { TaskTerminals } = await import('./task-terminals.mjs');
|
|
129
|
+
const { readMacPressureWord } = await import('./leftovers.mjs');
|
|
130
|
+
const terminals = new TaskTerminals({ readPressure: process.platform === 'darwin' ? readMacPressureWord : null });
|
|
131
|
+
const host = serveTerminals({
|
|
132
|
+
sockPath,
|
|
133
|
+
terminals,
|
|
134
|
+
onIdle: () => host.close().then(() => process.exit(0)),
|
|
135
|
+
log: (m) => process.stderr.write(`${m}\n`),
|
|
136
|
+
});
|
|
137
|
+
if (await host.started === 'taken') process.exit(HOST_ALREADY_THERE);
|
|
138
|
+
// One shell's stray error is that shell's, not every terminal's.
|
|
139
|
+
process.on('uncaughtException', (err) => process.stderr.write(`terminal host: ${err?.stack || err}\n`));
|
|
140
|
+
process.on('unhandledRejection', (err) => process.stderr.write(`terminal host: ${err?.stack || err}\n`));
|
|
141
|
+
// The app going is not a reason to end, and nor is the terminal it ran from.
|
|
142
|
+
process.on('SIGHUP', () => {});
|
|
143
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// THE APP'S SIDE OF ./terminal-host.mjs: the same calls as TaskTerminals, each
|
|
2
|
+
// sent to whichever host holds that terminal. A new terminal opens in the host
|
|
3
|
+
// made of this app's code, started when there is none; one an older app left
|
|
4
|
+
// stays in the older host until it ends. If no host will start, terminals run
|
|
5
|
+
// inside the app as they used to, and the terminal's first line says so.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import net from 'node:net';
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { PROTOCOL, HOST_ALREADY_THERE, terminalHostStamp, terminalHostSocket, terminalHostSockets } from './terminal-host.mjs';
|
|
12
|
+
import { ENDED } from './task-terminals.mjs';
|
|
13
|
+
import { SETTINGS_TERMINAL } from '../shared/settings-terminal.mjs';
|
|
14
|
+
import { NAME } from '../shared/product-name.mjs';
|
|
15
|
+
|
|
16
|
+
const ENDED_SESSION = 'Terminal session ended. Open a new terminal to continue.';
|
|
17
|
+
// Closing a task ends its terminal this long after, so a Z that reopens it
|
|
18
|
+
// keeps the terminal.
|
|
19
|
+
export const CLOSE_GRACE_MS = 2 * 60_000;
|
|
20
|
+
// A host that would not start is not tried again for this long.
|
|
21
|
+
const RETRY_MS = 5 * 60_000;
|
|
22
|
+
|
|
23
|
+
export function launchTerminalHost(sockPath, { logFile = null } = {}) {
|
|
24
|
+
const out = logFile ? fs.openSync(logFile, 'a') : 'ignore';
|
|
25
|
+
try {
|
|
26
|
+
const proc = spawn(process.execPath, [fileURLToPath(new URL('./terminal-host.mjs', import.meta.url)), sockPath], {
|
|
27
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
|
|
28
|
+
detached: true,
|
|
29
|
+
stdio: ['ignore', out, out],
|
|
30
|
+
});
|
|
31
|
+
proc.on('error', () => {});
|
|
32
|
+
proc.unref();
|
|
33
|
+
return proc;
|
|
34
|
+
} finally {
|
|
35
|
+
if (typeof out === 'number') fs.closeSync(out);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One socket to one host, every call with a deadline that ends it. */
|
|
40
|
+
export function connectTerminalHost(sockPath, { connectMs = 2_000, callMs = 5_000 } = {}) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const sock = net.connect(sockPath);
|
|
43
|
+
const pending = new Map();
|
|
44
|
+
let next = 1;
|
|
45
|
+
let buf = '';
|
|
46
|
+
const conn = {
|
|
47
|
+
sockPath,
|
|
48
|
+
closed: false,
|
|
49
|
+
call(op, ...args) {
|
|
50
|
+
if (conn.closed) return Promise.reject(new Error(ENDED_SESSION));
|
|
51
|
+
const id = next++;
|
|
52
|
+
return new Promise((res, rej) => {
|
|
53
|
+
const timer = setTimeout(() => { pending.delete(id); rej(new Error('The terminal did not answer.')); }, callMs);
|
|
54
|
+
pending.set(id, { res, rej, timer });
|
|
55
|
+
sock.write(`${JSON.stringify({ id, op, args })}\n`);
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
close() { sock.destroy(); },
|
|
59
|
+
};
|
|
60
|
+
const timer = setTimeout(() => { sock.destroy(); reject(new Error('The terminal host did not answer.')); }, connectMs);
|
|
61
|
+
sock.setEncoding('utf8');
|
|
62
|
+
sock.once('connect', () => { clearTimeout(timer); resolve(conn); });
|
|
63
|
+
sock.on('data', (d) => {
|
|
64
|
+
buf += d;
|
|
65
|
+
let n;
|
|
66
|
+
while ((n = buf.indexOf('\n')) >= 0) {
|
|
67
|
+
let msg = null;
|
|
68
|
+
try { msg = JSON.parse(buf.slice(0, n)); } catch { /* not ours */ }
|
|
69
|
+
buf = buf.slice(n + 1);
|
|
70
|
+
const p = msg && pending.get(msg.id);
|
|
71
|
+
if (!p) continue;
|
|
72
|
+
pending.delete(msg.id);
|
|
73
|
+
clearTimeout(p.timer);
|
|
74
|
+
if (msg.error !== undefined) p.rej(new Error(msg.error)); else p.res(msg.value);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
sock.on('error', (err) => { clearTimeout(timer); reject(err); });
|
|
78
|
+
sock.on('close', () => {
|
|
79
|
+
conn.closed = true;
|
|
80
|
+
for (const p of pending.values()) { clearTimeout(p.timer); p.rej(new Error(ENDED_SESSION)); }
|
|
81
|
+
pending.clear();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether a terminal's task is closed. Only a task the store says is done: one
|
|
88
|
+
* it cannot find may be another store's, and the sign-in terminal and an
|
|
89
|
+
* outside agent's belong to no task.
|
|
90
|
+
*/
|
|
91
|
+
export const taskClosed = (store) => (key) => {
|
|
92
|
+
const { product, id } = JSON.parse(key);
|
|
93
|
+
if (product === SETTINGS_TERMINAL.product || String(id).startsWith('agent:')) return false;
|
|
94
|
+
return store.readItem?.(product, id)?.status === 'done';
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Terminals whose task has been closed for `graceMs` on every look since. `seen`
|
|
99
|
+
* carries when each was first seen closed from one look to the next.
|
|
100
|
+
*/
|
|
101
|
+
export function closedTerminals(list, isClosed, seen, now, graceMs = CLOSE_GRACE_MS) {
|
|
102
|
+
const keys = new Set(list.map((s) => s.key));
|
|
103
|
+
for (const key of [...seen.keys()]) if (!keys.has(key)) seen.delete(key);
|
|
104
|
+
const due = [];
|
|
105
|
+
for (const { key } of list) {
|
|
106
|
+
let closed = false;
|
|
107
|
+
try { closed = !!isClosed(key); } catch { closed = false; }
|
|
108
|
+
if (!closed) { seen.delete(key); continue; }
|
|
109
|
+
if (!seen.has(key)) seen.set(key, now);
|
|
110
|
+
if (now - seen.get(key) >= graceMs) due.push(key);
|
|
111
|
+
}
|
|
112
|
+
return due;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export class TerminalHosts {
|
|
116
|
+
/**
|
|
117
|
+
* `local` is the app's own TaskTerminals, for when no host will start.
|
|
118
|
+
*/
|
|
119
|
+
constructor({
|
|
120
|
+
local,
|
|
121
|
+
stamp = terminalHostStamp(),
|
|
122
|
+
dir = os.tmpdir(),
|
|
123
|
+
uid = process.getuid?.() ?? 0,
|
|
124
|
+
launch = launchTerminalHost,
|
|
125
|
+
connect = connectTerminalHost,
|
|
126
|
+
logFile = null,
|
|
127
|
+
startMs = 8_000,
|
|
128
|
+
now = () => Date.now(),
|
|
129
|
+
log = () => {},
|
|
130
|
+
}) {
|
|
131
|
+
Object.assign(this, { local, stamp, dir, uid, launch, connect, logFile, startMs, now, log });
|
|
132
|
+
this.ownPath = terminalHostSocket({ stamp, dir, uid });
|
|
133
|
+
this.conns = new Map();
|
|
134
|
+
this.where = new Map();
|
|
135
|
+
this.closedSeen = new Map();
|
|
136
|
+
this.failedAt = null;
|
|
137
|
+
this.failure = null;
|
|
138
|
+
const t = local;
|
|
139
|
+
this.inApp = { local: true, call: async (op, ...args) => t[op](...args) };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async _hello(sockPath) {
|
|
143
|
+
const had = this.conns.get(sockPath);
|
|
144
|
+
if (had && !had.closed) return had;
|
|
145
|
+
// Every open hands over this user's environment, so never to a socket
|
|
146
|
+
// somebody else put at the name.
|
|
147
|
+
try { if (process.getuid && fs.statSync(sockPath).uid !== process.getuid()) return null; } catch { return null; }
|
|
148
|
+
let conn;
|
|
149
|
+
try { conn = await this.connect(sockPath); } catch { return null; }
|
|
150
|
+
try {
|
|
151
|
+
const hi = await conn.call('hello');
|
|
152
|
+
if (hi?.protocol !== PROTOCOL) { conn.close(); return null; }
|
|
153
|
+
} catch { conn.close(); return null; }
|
|
154
|
+
this.conns.set(sockPath, conn);
|
|
155
|
+
return conn;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The hosts an earlier run of the app left, found once. */
|
|
159
|
+
ready() { return (this.discovered ??= this.discover().catch(() => {})); }
|
|
160
|
+
|
|
161
|
+
/** Every host this user has, and which terminal is in which. */
|
|
162
|
+
async discover() {
|
|
163
|
+
for (const sockPath of terminalHostSockets({ dir: this.dir, uid: this.uid })) await this._hello(sockPath);
|
|
164
|
+
await this.list();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The host made of this app's code, started if there is none. */
|
|
168
|
+
async _mine() {
|
|
169
|
+
const there = await this._hello(this.ownPath);
|
|
170
|
+
if (there) return there;
|
|
171
|
+
if (this.failedAt !== null && this.now() - this.failedAt < RETRY_MS) throw new Error(this.failure);
|
|
172
|
+
let proc;
|
|
173
|
+
try { proc = this.launch(this.ownPath, { logFile: this.logFile }); } catch (err) { return this._failed(err.message); }
|
|
174
|
+
const end = Date.now() + this.startMs;
|
|
175
|
+
while (Date.now() < end) {
|
|
176
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
177
|
+
const conn = await this._hello(this.ownPath);
|
|
178
|
+
if (conn) { this.failedAt = null; this.failure = null; return conn; }
|
|
179
|
+
if (Number.isInteger(proc?.exitCode) && proc.exitCode !== HOST_ALREADY_THERE) return this._failed(`it exited with code ${proc.exitCode}`);
|
|
180
|
+
}
|
|
181
|
+
return this._failed(`no answer in ${Math.round(this.startMs / 1000)} seconds`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
_failed(why) {
|
|
185
|
+
this.failedAt = this.now();
|
|
186
|
+
this.failure = why;
|
|
187
|
+
this.log(`terminal host: ${why}`);
|
|
188
|
+
throw new Error(why);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_at(key) {
|
|
192
|
+
const at = this.where.get(key);
|
|
193
|
+
if (!at || at.closed) throw new Error(ENDED_SESSION);
|
|
194
|
+
return at;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async open(key, place) {
|
|
198
|
+
await this.ready();
|
|
199
|
+
const at = this.where.get(key);
|
|
200
|
+
if (at && !at.closed && (!at.local || this.local.sessions.has(key))) {
|
|
201
|
+
try { return await at.call('open', key); } catch { /* ended there: a new one below */ }
|
|
202
|
+
}
|
|
203
|
+
this.where.delete(key);
|
|
204
|
+
let host;
|
|
205
|
+
try { host = await this._mine(); } catch (err) {
|
|
206
|
+
const notice = `This terminal ends when ${NAME} quits, because the process that keeps terminals running through a restart would not start (${err.message}).`;
|
|
207
|
+
const state = this.local.open(key, { ...place, notice: [place?.notice, notice].filter(Boolean).join(' ') });
|
|
208
|
+
this.where.set(key, this.inApp);
|
|
209
|
+
return state;
|
|
210
|
+
}
|
|
211
|
+
const state = await host.call('open', key, place);
|
|
212
|
+
this.where.set(key, host);
|
|
213
|
+
return state;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
read(key, offset, wait) { return this._at(key).call('read', key, offset, wait); }
|
|
217
|
+
write(key, data) { return this._at(key).call('write', key, data); }
|
|
218
|
+
resize(key, cols, rows) { return this._at(key).call('resize', key, cols, rows); }
|
|
219
|
+
async close(key) {
|
|
220
|
+
const at = this.where.get(key);
|
|
221
|
+
this.where.delete(key);
|
|
222
|
+
if (at && !at.closed) await at.call('close', key);
|
|
223
|
+
}
|
|
224
|
+
async end(key, why) {
|
|
225
|
+
const at = this.where.get(key);
|
|
226
|
+
if (at && !at.closed) await at.call('end', key, why);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Every terminal, in every host and in the app, freshly asked. */
|
|
230
|
+
async list() {
|
|
231
|
+
const out = [];
|
|
232
|
+
const answered = new Set();
|
|
233
|
+
for (const [sockPath, conn] of this.conns) {
|
|
234
|
+
if (conn.closed) { this.conns.delete(sockPath); continue; }
|
|
235
|
+
let got = [];
|
|
236
|
+
try { got = await conn.call('list'); } catch { continue; }
|
|
237
|
+
answered.add(conn);
|
|
238
|
+
// An older host with nothing left ends itself once nobody is connected.
|
|
239
|
+
if (!got.length && sockPath !== this.ownPath) { conn.close(); this.conns.delete(sockPath); continue; }
|
|
240
|
+
for (const s of got) { this.where.set(s.key, conn); out.push(s); }
|
|
241
|
+
}
|
|
242
|
+
const listed = new Set(out.map((s) => s.key));
|
|
243
|
+
for (const [key, at] of this.where) {
|
|
244
|
+
if (at.closed || (answered.has(at) && !listed.has(key)) || (at.local && !this.local.sessions.has(key))) this.where.delete(key);
|
|
245
|
+
}
|
|
246
|
+
for (const s of this.local.list()) if (this.where.get(s.key) === this.inApp) out.push(s);
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Whether this task has a terminal, as far as the app last knew. */
|
|
251
|
+
hasTerminal(id) {
|
|
252
|
+
for (const key of this.where.keys()) { try { if (JSON.parse(key).id === id) return true; } catch { /* not a task's */ } }
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async endClosed(isClosed) {
|
|
257
|
+
const due = closedTerminals(await this.list(), isClosed, this.closedSeen, this.now());
|
|
258
|
+
for (const key of due) { try { await this.end(key, ENDED.closed); } catch { /* gone already */ } }
|
|
259
|
+
return due;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// FINDING A CLAUDE CODE TRANSCRIPT WITHOUT HOLDING THE APP MORE THAN ONCE.
|
|
2
|
+
//
|
|
3
|
+
// Claude Code keeps a session at `<projects>/<slug of its folder>/<id>.jsonl`.
|
|
4
|
+
// When it is not there the only way to find it is to look in every folder:
|
|
5
|
+
// ten thousand on a busy Mac, 1.4 s on the window's thread. So a session found
|
|
6
|
+
// nowhere is not looked for again for ten minutes, and a caller picks how to look:
|
|
7
|
+
// 'now' in this call, for an answer that decides a resume (an imported
|
|
8
|
+
// thread is filed under the folder it first ran in)
|
|
9
|
+
// 'later' in the background; this call answers null, a later one finds it
|
|
10
|
+
// 'never' only at the slug path: a cloud run's transcript comes home there
|
|
11
|
+
// and nowhere else (main/cloud/run.mjs transcriptFolderName)
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
export const MISS_KEPT_MS = 10 * 60_000;
|
|
16
|
+
|
|
17
|
+
const found = new Map(); // `${projects}\0${id}` -> transcript path
|
|
18
|
+
const missed = new Map(); // `${projects}\0${id}` -> when it was last looked for and not found
|
|
19
|
+
const wanted = new Map(); // projects folder -> session ids to look for
|
|
20
|
+
let search = null;
|
|
21
|
+
|
|
22
|
+
export const slugFor = (cwd) => String(cwd || '').replace(/[^a-zA-Z0-9]/g, '-');
|
|
23
|
+
const keyOf = (projects, id) => `${projects}\0${id}`;
|
|
24
|
+
|
|
25
|
+
function remember(projects, id, file) {
|
|
26
|
+
if (file) { found.set(keyOf(projects, id), file); missed.delete(keyOf(projects, id)); return; }
|
|
27
|
+
missed.set(keyOf(projects, id), Date.now());
|
|
28
|
+
while (missed.size > 1000) missed.delete(missed.keys().next().value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The transcript's path, or null. `look` says where else to look when it is not at the slug path. */
|
|
32
|
+
export function findTranscript(projects, sessionId, cwd, { look = 'later' } = {}) {
|
|
33
|
+
if (!projects || !sessionId) return null;
|
|
34
|
+
const key = keyOf(projects, sessionId);
|
|
35
|
+
const known = found.get(key);
|
|
36
|
+
if (known && fs.existsSync(known)) return known;
|
|
37
|
+
// Claude Code names the folder from the resolved path: /tmp is /private/tmp on a Mac.
|
|
38
|
+
let real = cwd;
|
|
39
|
+
try { real = cwd ? fs.realpathSync(cwd) : cwd; } catch { /* the folder is gone */ }
|
|
40
|
+
for (const at of new Set([cwd, real])) {
|
|
41
|
+
const direct = path.join(projects, slugFor(at), `${sessionId}.jsonl`);
|
|
42
|
+
if (fs.existsSync(direct)) { found.set(key, direct); return direct; }
|
|
43
|
+
}
|
|
44
|
+
if (look === 'never' || Date.now() - (missed.get(key) ?? -Infinity) < MISS_KEPT_MS) return null;
|
|
45
|
+
if (look === 'later') { lookEverywhere(projects, sessionId); return null; }
|
|
46
|
+
let file = null;
|
|
47
|
+
try {
|
|
48
|
+
for (const dir of fs.readdirSync(projects)) {
|
|
49
|
+
const p = path.join(projects, dir, `${sessionId}.jsonl`);
|
|
50
|
+
if (fs.existsSync(p)) { file = p; break; }
|
|
51
|
+
}
|
|
52
|
+
} catch { /* no projects folder */ }
|
|
53
|
+
remember(projects, sessionId, file);
|
|
54
|
+
return file;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function lookEverywhere(projects, sessionId) {
|
|
58
|
+
if (!wanted.has(projects)) wanted.set(projects, new Set());
|
|
59
|
+
wanted.get(projects).add(sessionId);
|
|
60
|
+
search ??= (async () => {
|
|
61
|
+
while (wanted.size) {
|
|
62
|
+
const [root, ids] = wanted.entries().next().value;
|
|
63
|
+
wanted.delete(root);
|
|
64
|
+
const left = new Set(ids);
|
|
65
|
+
let dirs = [];
|
|
66
|
+
try { dirs = await fs.promises.readdir(root); } catch { /* no projects folder */ }
|
|
67
|
+
for (const dir of dirs) {
|
|
68
|
+
if (!left.size) break;
|
|
69
|
+
for (const id of [...left]) {
|
|
70
|
+
const p = path.join(root, dir, `${id}.jsonl`);
|
|
71
|
+
try { await fs.promises.access(p); remember(root, id, p); left.delete(id); } catch { /* not here */ }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const id of left) remember(root, id, null);
|
|
75
|
+
}
|
|
76
|
+
})().finally(() => { search = null; });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The background look, when one is running; settled otherwise. */
|
|
80
|
+
export const transcriptSearch = () => search ?? Promise.resolve();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// What an embedded page may do, decided here rather than on the tag that
|
|
2
|
+
// asked for it. The preload is the one that matters: `preload.cjs` is the
|
|
3
|
+
// bridge the app's own page talks to the store through, so a page off the web
|
|
4
|
+
// holding it holds everything the person holds.
|
|
5
|
+
//
|
|
6
|
+
// The partition is deliberately left alone. It says WHERE a page's sign-ins
|
|
7
|
+
// go rather than what the page may reach, and flattening it would merge the
|
|
8
|
+
// website pane's store into the default one (shared/websites.mjs).
|
|
9
|
+
//
|
|
10
|
+
// Pinned by tests/a-website-in-the-pane-cannot-reach-the-app.test.mjs.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Pin one attaching webview to the floor. `webPreferences` and `params` are
|
|
14
|
+
* Electron's own objects from `will-attach-webview`, mutated in place, which
|
|
15
|
+
* is how that event is answered.
|
|
16
|
+
*/
|
|
17
|
+
export function hardenAttachedWebview(webPreferences, params) {
|
|
18
|
+
if (webPreferences) {
|
|
19
|
+
// Nothing of ours reaches the page.
|
|
20
|
+
delete webPreferences.preload;
|
|
21
|
+
delete webPreferences.preloadURL;
|
|
22
|
+
webPreferences.nodeIntegration = false;
|
|
23
|
+
webPreferences.nodeIntegrationInSubFrames = false;
|
|
24
|
+
webPreferences.nodeIntegrationInWorker = false;
|
|
25
|
+
webPreferences.contextIsolation = true;
|
|
26
|
+
webPreferences.sandbox = true;
|
|
27
|
+
webPreferences.webSecurity = true;
|
|
28
|
+
webPreferences.allowRunningInsecureContent = false;
|
|
29
|
+
webPreferences.experimentalFeatures = false;
|
|
30
|
+
webPreferences.enableBlinkFeatures = '';
|
|
31
|
+
|
|
32
|
+
// No embedded page starts sound before a click in it. Electron's default
|
|
33
|
+
// lets any page start media the moment it loads.
|
|
34
|
+
webPreferences.autoplayPolicy = 'document-user-activation-required';
|
|
35
|
+
}
|
|
36
|
+
if (params) {
|
|
37
|
+
// A window the guest opens for itself would be a window with none of the
|
|
38
|
+
// above. Links that want one go out to the real browser instead
|
|
39
|
+
// (setWindowOpenHandler in main/main.mjs).
|
|
40
|
+
params.allowpopups = false;
|
|
41
|
+
params.nodeintegration = false;
|
|
42
|
+
params.nodeintegrationinsubframes = false;
|
|
43
|
+
}
|
|
44
|
+
return webPreferences;
|
|
45
|
+
}
|