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
package/main/task-terminals.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import {randomUUID} from 'node:crypto';
|
|
|
4
4
|
import {execFileSync} from 'node:child_process';
|
|
5
5
|
import {createRequire} from 'node:module';
|
|
6
6
|
import {commandRunning} from '../shared/terminal-state.mjs';
|
|
7
|
+
import {loginShell} from './shell-path.mjs';
|
|
7
8
|
const require=createRequire(import.meta.url);
|
|
8
9
|
const LIMIT=262144;
|
|
9
10
|
export function terminalEnv(source=process.env){
|
|
@@ -37,17 +38,6 @@ export function terminalPlace({store,supervisor,product,id,agent}){
|
|
|
37
38
|
return {cwd,notice};
|
|
38
39
|
}
|
|
39
40
|
export function terminalFolder(args){return terminalPlace(args).cwd;}
|
|
40
|
-
// The shell a person actually has. `$SHELL` when that file exists, then
|
|
41
|
-
// zsh, bash, and sh. A machine with no zsh used to spawn `/bin/zsh` and
|
|
42
|
-
// the terminal never came up.
|
|
43
|
-
export function loginShell(env=process.env,exists=fs.existsSync){
|
|
44
|
-
const preferred=env.SHELL;
|
|
45
|
-
if(preferred&&exists(preferred))return preferred;
|
|
46
|
-
for(const candidate of ['/bin/zsh','/bin/bash','/bin/sh']){
|
|
47
|
-
if(exists(candidate))return candidate;
|
|
48
|
-
}
|
|
49
|
-
return preferred||'/bin/sh';
|
|
50
|
-
}
|
|
51
41
|
// Enumerate only descendants of our own shell, including background process
|
|
52
42
|
// groups. Kill children before shell; closing a pane never calls this.
|
|
53
43
|
export function disposeTerminalProcess(pty){
|
|
@@ -79,20 +69,62 @@ export function disposeTerminalProcess(pty){
|
|
|
79
69
|
* (TaskTerminal.tsx). So `lastReadAt` is not a guess about attention, it is the
|
|
80
70
|
* last time the app was actually asked what this shell had to say.
|
|
81
71
|
*
|
|
82
|
-
* Idle AND unwatched for
|
|
83
|
-
* worth nothing, and it goes. Everything else stays until
|
|
72
|
+
* Idle AND unwatched for an hour is a shell that costs something and is
|
|
73
|
+
* worth nothing, and it goes. Everything else stays until its task is closed:
|
|
74
|
+
* task terminals run in ./terminal-host.mjs, which outlives the app.
|
|
75
|
+
*
|
|
76
|
+
* IT WAS TEN MINUTES, AND THAT WAS TOO SHORT (w-6841800f17): "I come back after
|
|
77
|
+
* maybe 15 minutes and it's gone ... I don't see the error message." A deploy
|
|
78
|
+
* that fails leaves an idle shell with the error on it, and ten minutes away
|
|
79
|
+
* from the task took the error with the shell. So the wait is an hour, and when
|
|
80
|
+
* a shell does go, the end of what it showed comes back with the next one.
|
|
81
|
+
*
|
|
82
|
+
* AND A SHELL THAT PRINTED SOMETHING SHE HAS NOT SEEN IS NOT WORTH NOTHING
|
|
83
|
+
* (w-f147e487a1). She started the AWS tools install, left the thread while it
|
|
84
|
+
* ran, and came back to a new shell: the install finished while she was away,
|
|
85
|
+
* and since the idle clock ran from her last look, the shell went on the next
|
|
86
|
+
* sweep with its output unread. "I was running it." So the hour starts only
|
|
87
|
+
* once she has read everything it printed, and at the cap a shell she has read
|
|
88
|
+
* goes before one she has not; when all twenty hold unread output, the least
|
|
89
|
+
* recently watched still makes room.
|
|
84
90
|
*/
|
|
85
|
-
const IDLE_MS=
|
|
91
|
+
const IDLE_MS=3600000;
|
|
92
|
+
// How much of a closed shell's screen comes back. The end is where the error
|
|
93
|
+
// is; 64K characters is several hundred lines of it.
|
|
94
|
+
const KEPT_SCREEN=65536;
|
|
95
|
+
// Closed shells whose screen is still held for her. Twice the cap is every
|
|
96
|
+
// terminal open now plus as many again that the app has tidied away.
|
|
97
|
+
const KEPT_SHELLS=40;
|
|
98
|
+
/*
|
|
99
|
+
* THE END OF A SCREEN, CUT AT A LINE. Cutting mid-line would start the replay
|
|
100
|
+
halfway through a word or an escape sequence. */
|
|
101
|
+
function keptScreen(buffer){
|
|
102
|
+
if(buffer.length<=KEPT_SCREEN)return buffer;
|
|
103
|
+
const tail=buffer.slice(-KEPT_SCREEN);
|
|
104
|
+
const line=tail.indexOf('\n');
|
|
105
|
+
return line<0?tail:tail.slice(line+1);
|
|
106
|
+
}
|
|
86
107
|
// The longest a read waits for output before answering with none, so the panel
|
|
87
108
|
// still hears about a command that finished quietly and the sweep still sees
|
|
88
109
|
// it watching.
|
|
89
110
|
const READ_WAIT_MS=1000;
|
|
90
111
|
const SWEEP_MS=60000;
|
|
91
112
|
const MAX_TERMINALS=20;
|
|
113
|
+
/*
|
|
114
|
+
* A TERMINAL LIVES AS LONG AS ITS TASK IS OPEN. Closing the task ends it; so,
|
|
115
|
+
* while memory is critical, does going unwatched. Each says why on the next
|
|
116
|
+
* terminal, in these words.
|
|
117
|
+
*/
|
|
118
|
+
export const ENDED={
|
|
119
|
+
closed:'This task was closed, so its terminal was ended.',
|
|
120
|
+
memory:'This terminal was ended because this computer was running out of memory.',
|
|
121
|
+
};
|
|
122
|
+
// While memory is critical, a terminal looked at this recently is still in use.
|
|
123
|
+
const WATCHED_MS=600000;
|
|
92
124
|
export class TaskTerminals {
|
|
93
|
-
constructor({spawn,resolve,disposeProcess=disposeTerminalProcess,env=terminalEnv(),shell=loginShell(),idleMs=IDLE_MS,sweepMs=SWEEP_MS,now=()=>Date.now()}={}){
|
|
125
|
+
constructor({spawn,resolve,disposeProcess=disposeTerminalProcess,env=terminalEnv(),shell=loginShell(),idleMs=IDLE_MS,sweepMs=SWEEP_MS,now=()=>Date.now(),readPressure=null,watchedMs=WATCHED_MS}={}){
|
|
94
126
|
this.spawn=spawn??((...args)=>require('node-pty').spawn(...args));this.resolve=resolve;this.disposeProcess=disposeProcess;this.env=env;this.shell=shell;this.sessions=new Map();
|
|
95
|
-
this.idleMs=idleMs;this.now=now;
|
|
127
|
+
this.idleMs=idleMs;this.now=now;this.readPressure=readPressure;this.watchedMs=watchedMs;
|
|
96
128
|
// Keys whose shell this class ended on its own, and WHY. The next open on
|
|
97
129
|
// one says so rather than handing her a blank screen where her scrollback
|
|
98
130
|
// was, and the two reasons are different sentences because they are
|
|
@@ -102,7 +134,7 @@ export class TaskTerminals {
|
|
|
102
134
|
// `unref` so a sweep that is merely pending never holds the app open, and
|
|
103
135
|
// never in a test: a suite that leaves an interval behind hangs the runner.
|
|
104
136
|
if(sweepMs>0&&typeof setInterval==='function'){
|
|
105
|
-
this.timer=setInterval(()=>{try{this.sweepIdle();}catch{}},sweepMs);
|
|
137
|
+
this.timer=setInterval(()=>{try{this.sweepIdle();}catch{}this.relieveMemory().catch(()=>{});},sweepMs);
|
|
106
138
|
this.timer?.unref?.();
|
|
107
139
|
}
|
|
108
140
|
}
|
|
@@ -115,29 +147,69 @@ export class TaskTerminals {
|
|
|
115
147
|
if(session.exited)return false;
|
|
116
148
|
try{return commandRunning(session.pty.process||'Shell',false);}catch{return false;}
|
|
117
149
|
}
|
|
150
|
+
// Output past the last byte a read handed to the panel.
|
|
151
|
+
unread(session){return session.offset+session.buffer.length>(session.seenTo??0);}
|
|
118
152
|
/*
|
|
119
|
-
* THE ONE THE CAP TAKES:
|
|
120
|
-
|
|
121
|
-
where she is told
|
|
153
|
+
* THE ONE THE CAP TAKES: one she has read everything in before one she has
|
|
154
|
+
not, then least recently watched, and never a busy one. Null means every
|
|
155
|
+
slot is genuinely working, which is the only case left where she is told
|
|
156
|
+
no. */
|
|
122
157
|
oldestIdle(){
|
|
123
|
-
let pick=null,at=Infinity;
|
|
158
|
+
let pick=null,pickUnread=true,at=Infinity;
|
|
124
159
|
for(const [key,s] of this.sessions){
|
|
125
160
|
if(this.busy(s))continue;
|
|
126
|
-
const seen=s.lastReadAt??0;
|
|
127
|
-
if(seen<at){at=seen;
|
|
161
|
+
const unread=this.unread(s),seen=s.lastReadAt??0;
|
|
162
|
+
if(pick===null||(pickUnread&&!unread)||(unread===pickUnread&&seen<at)){pick=key;pickUnread=unread;at=seen;}
|
|
128
163
|
}
|
|
129
164
|
return pick;
|
|
130
165
|
}
|
|
166
|
+
/*
|
|
167
|
+
* THE APP ENDS A SHELL, AND KEEPS WHAT IT SAID. Only these two paths keep the
|
|
168
|
+
screen: when she presses End shell herself, `close` is called directly and
|
|
169
|
+
nothing comes back, because she chose to throw it away. */
|
|
170
|
+
retire(key,why){
|
|
171
|
+
const s=this.sessions.get(key);
|
|
172
|
+
const screen=s?keptScreen(s.buffer):'';
|
|
173
|
+
this.close(key);
|
|
174
|
+
this.reaped.delete(key);
|
|
175
|
+
this.reaped.set(key,{why,screen});
|
|
176
|
+
while(this.reaped.size>KEPT_SHELLS)this.reaped.delete(this.reaped.keys().next().value);
|
|
177
|
+
}
|
|
131
178
|
sweepIdle(){
|
|
132
179
|
const cutoff=this.now()-this.idleMs;
|
|
133
180
|
for(const [key,s] of [...this.sessions]){
|
|
134
|
-
if(this.busy(s))continue;
|
|
181
|
+
if(this.busy(s)||this.unread(s))continue;
|
|
135
182
|
if((s.lastReadAt??0)>cutoff)continue;
|
|
136
|
-
this.
|
|
137
|
-
|
|
183
|
+
this.retire(key,'The previous shell here was closed after an hour idle.');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
end(key,why){if(this.sessions.has(key))this.retire(key,why);}
|
|
187
|
+
list(){
|
|
188
|
+
return [...this.sessions].map(([key,s])=>({key,busy:this.busy(s),exited:s.exited,unread:this.unread(s),lastReadAt:s.lastReadAt??0,cwd:s.cwd,pid:s.pty.pid}));
|
|
189
|
+
}
|
|
190
|
+
/*
|
|
191
|
+
* TOO MUCH FOR THIS COMPUTER. Only while memory is critical, checked once a
|
|
192
|
+
minute: every shell at its prompt that nobody has looked at for ten
|
|
193
|
+
minutes, and one terminal running something, the one looked at least
|
|
194
|
+
recently. Never one looked at inside those ten minutes. */
|
|
195
|
+
async relieveMemory(){
|
|
196
|
+
if(!this.readPressure||!this.sessions.size)return [];
|
|
197
|
+
let pressure='normal';
|
|
198
|
+
try{pressure=await this.readPressure();}catch{}
|
|
199
|
+
if(pressure!=='critical')return [];
|
|
200
|
+
const cutoff=this.now()-this.watchedMs,idle=[];let busy=null,at=Infinity;
|
|
201
|
+
for(const [key,s] of this.sessions){
|
|
202
|
+
if(s.exited||(s.lastReadAt??0)>cutoff)continue;
|
|
203
|
+
if(!this.busy(s)){idle.push(key);continue;}
|
|
204
|
+
if((s.lastReadAt??0)<at){busy=key;at=s.lastReadAt??0;}
|
|
138
205
|
}
|
|
206
|
+
const ended=busy?[...idle,busy]:idle;
|
|
207
|
+
for(const key of ended)this.retire(key,ENDED.memory);
|
|
208
|
+
return ended;
|
|
139
209
|
}
|
|
140
|
-
open
|
|
210
|
+
// `given` is where to open it when the caller has already worked that out:
|
|
211
|
+
// the terminal host (./terminal-host.mjs) cannot read the store.
|
|
212
|
+
open(key,given){
|
|
141
213
|
if(this.sessions.has(key))return this.read(key,0);
|
|
142
214
|
if(this.sessions.size>=MAX_TERMINALS){
|
|
143
215
|
/*
|
|
@@ -149,22 +221,25 @@ export class TaskTerminals {
|
|
|
149
221
|
and says so in those words. */
|
|
150
222
|
const victim=this.oldestIdle();
|
|
151
223
|
if(victim===null)throw Error('20 terminals are open and every one of them is running something. Stop one of those commands before opening another terminal.');
|
|
152
|
-
this.
|
|
153
|
-
this.reaped.set(victim,'This shell was closed to make room for a newer terminal.');
|
|
224
|
+
this.retire(victim,'This shell was closed to make room for a newer terminal.');
|
|
154
225
|
}
|
|
155
|
-
const place=this.resolve(key);
|
|
226
|
+
const place=given??this.resolve(key);
|
|
156
227
|
const cwd=typeof place==='string'?place:place.cwd;
|
|
157
228
|
const command=place?.command;
|
|
158
|
-
const pty=this.spawn(command?.file??this.shell,command?.args??['-l'],{cwd,env:{...this.env,...command?.env},cols:80,rows:24,name:'xterm-256color'});
|
|
229
|
+
const pty=this.spawn(command?.file??place?.shell??this.shell,command?.args??['-l'],{cwd,env:{...(place?.env??this.env),...command?.env},cols:80,rows:24,name:'xterm-256color'});
|
|
159
230
|
let resolveExit;const exitPromise=new Promise(resolve=>{resolveExit=resolve;});
|
|
160
231
|
const session={token:randomUUID(),pty,cwd,buffer:'',offset:0,exitCode:null,exited:false,exitPromise,lastReadAt:this.now(),waiters:new Set()};this.sessions.set(key,session);
|
|
161
232
|
/*
|
|
162
233
|
* AND IT SAYS WHAT HAPPENED TO THE LAST ONE. Without this line, reopening a
|
|
163
234
|
terminal the app tidied away looks exactly like the app losing her
|
|
164
235
|
scrollback, which is the same event read as a bug. Dim, one line, above a
|
|
165
|
-
real prompt. */
|
|
166
|
-
const
|
|
167
|
-
if(
|
|
236
|
+
real prompt, under whatever the old shell last showed. */
|
|
237
|
+
const gone=this.reaped.get(key);
|
|
238
|
+
if(gone!==undefined){
|
|
239
|
+
this.reaped.delete(key);
|
|
240
|
+
const old=gone.screen?`${gone.screen}\x1b[0m${gone.screen.endsWith('\n')?'':'\r\n'}`:'';
|
|
241
|
+
session.buffer=`${old}\x1b[2m[${gone.why}${old?' Its output is above.':''} This is a new one.]\x1b[0m\r\n`;
|
|
242
|
+
}
|
|
168
243
|
if(place?.notice)session.buffer+=`\x1b[2m[${place.notice}]\x1b[0m\r\n`;
|
|
169
244
|
pty.onData(data=>{session.buffer+=data;const trim=Math.max(0,session.buffer.length-LIMIT);session.buffer=session.buffer.slice(trim);session.offset+=trim;this.wake(session);});
|
|
170
245
|
pty.onExit(({exitCode})=>{session.exited=true;session.exitCode=exitCode;resolveExit();this.wake(session);});
|
|
@@ -188,6 +263,7 @@ export class TaskTerminals {
|
|
|
188
263
|
const timer=setTimeout(done,Math.min(wait,READ_WAIT_MS));
|
|
189
264
|
s.waiters.add(done);
|
|
190
265
|
});
|
|
266
|
+
s.seenTo=end;
|
|
191
267
|
const state={token:s.token,cwd:s.cwd,data:s.buffer.slice(Math.max(0,offset-s.offset)),offset:end,truncated:offset<s.offset,exited:s.exited,exitCode:s.exitCode,process:s.exited?'Exited':s.pty.process||'Shell'};
|
|
192
268
|
return wait>0?{...state,live:true}:state;
|
|
193
269
|
}
|
package/main/team/index.mjs
CHANGED
|
@@ -17,28 +17,58 @@ import { listSharedProjects, joinSharedProject, markShared, makeDirect } from '.
|
|
|
17
17
|
import { firstSentence } from '../../shared/thread-cards.mjs';
|
|
18
18
|
import { cardsFor } from '../../shared/thread-cards.mjs';
|
|
19
19
|
import fs from 'node:fs';
|
|
20
|
+
import { saveAttachments, attachmentMarkdown } from '../save-attachments.mjs';
|
|
20
21
|
import { hasLapsed, statusEnds } from '../../shared/team-status.mjs';
|
|
22
|
+
import QRCode from 'qrcode';
|
|
23
|
+
import { createPhoneInbox } from '../phone-inbox.mjs';
|
|
24
|
+
import { SNAPSHOT_LIMITS, phoneProjectsOf, phoneEligible } from '../../shared/phone-snapshot.mjs';
|
|
25
|
+
import { createRooms } from './rooms.mjs';
|
|
21
26
|
|
|
22
27
|
// `signingIn` is { url } while a Google sign-in waits on the browser, so the
|
|
23
28
|
// page can offer that link again rather than a button that does nothing.
|
|
24
|
-
const EMPTY = { configured: false, started: false, signedIn: false, signingIn: null, me: null, team: null, invites: [], sent: [], people: [], cards: [], lastSyncAt: null, error: null };
|
|
29
|
+
const EMPTY = { configured: false, canSignIn: false, started: false, settling: false, signedIn: false, signingIn: null, me: null, team: null, invites: [], sent: [], people: [], cards: [], lastSyncAt: null, error: null };
|
|
30
|
+
|
|
31
|
+
// With Alpha features › Multiplayer off the window gets the sign-in the phone
|
|
32
|
+
// needs and no team, so it draws the single-person app.
|
|
33
|
+
export function teamForWindow(state, multiplayer) {
|
|
34
|
+
if (multiplayer || !state) return state;
|
|
35
|
+
const { rooms, ...rest } = state;
|
|
36
|
+
return { ...rest, configured: false, team: null, invites: [], sent: [], people: [], cards: [] };
|
|
37
|
+
}
|
|
25
38
|
|
|
26
39
|
export function createTeamService({
|
|
27
40
|
session, store, disk, accountRoot, stateFile, onChange = () => {}, log = () => {}, intervalMs = 5000, startRetryMs = 1500,
|
|
28
41
|
// The clock, so a test can move past the moment a status runs out.
|
|
29
42
|
now = Date.now,
|
|
43
|
+
// HER PHONE WITH THE MAC SHUT (docs/cloud/PHONE-PLAN.md, step 1): which
|
|
44
|
+
// projects she put on it (slugs), how a command from it is done, and where
|
|
45
|
+
// this Mac remembers what it sent. Needs no team, only a sign-in.
|
|
46
|
+
phone = null,
|
|
47
|
+
// ROOMS (alpha, docs/team/ROOMS-PLAN.md): { enabled, device, presenceFacts, prefsFile }.
|
|
48
|
+
rooms: roomsSetup = null,
|
|
30
49
|
}) {
|
|
31
|
-
|
|
50
|
+
// The phone site's address, only an https one, and its code for Settings.
|
|
51
|
+
const site = typeof phone?.site === 'string' && /^https:\/\/[^\s/]+(\/[^\s]*)?$/.test(phone.site) ? phone.site : null;
|
|
52
|
+
let siteQr = null;
|
|
53
|
+
// `settling` holds the window's boot screen until the team is known, so it
|
|
54
|
+
// never draws the single-person app first and then redraws with the team.
|
|
55
|
+
// `canSignIn` survives teamForWindow, so a build with Multiplayer off still asks.
|
|
56
|
+
let state = { ...EMPTY, configured: !!session?.configured, canSignIn: !!session?.configured, settling: !!session?.configured, phoneSite: site, phoneSiteQr: null };
|
|
32
57
|
let backend = null;
|
|
33
58
|
let sync = null;
|
|
34
59
|
let timer = null;
|
|
35
60
|
let unsubscribe = null;
|
|
36
61
|
let pending = null;
|
|
62
|
+
let phoneInbox = null;
|
|
37
63
|
// The sync cursors and which team this Mac is in, in one file.
|
|
38
64
|
let local = null;
|
|
39
65
|
const kept = () => (local ??= fileSyncState(stateFile, fs));
|
|
40
66
|
|
|
41
67
|
const set = (patch) => { state = { ...state, ...patch }; onChange(); };
|
|
68
|
+
const rooms = roomsSetup ? createRooms({
|
|
69
|
+
backend: () => backend, me: () => state.me, team: () => state, now, log, onChange, ...roomsSetup,
|
|
70
|
+
}) : null;
|
|
71
|
+
if (site) QRCode.toString(site, { type: 'svg', margin: 1, errorCorrectionLevel: 'M' }).then((qr) => { siteQr = qr; set({ phoneSiteQr: qr }); }).catch(() => {});
|
|
42
72
|
|
|
43
73
|
function products() {
|
|
44
74
|
try { return store.listProducts(); } catch { return []; }
|
|
@@ -139,12 +169,71 @@ export function createTeamService({
|
|
|
139
169
|
set({ error: String(err?.message ?? err) });
|
|
140
170
|
return null;
|
|
141
171
|
} finally {
|
|
172
|
+
// On its own, so a team sync that failed does not hold up her phone,
|
|
173
|
+
// nor her phone the team.
|
|
174
|
+
await phoneOnce();
|
|
175
|
+
await rooms?.tick().catch((err) => log(`rooms: ${err.message}`));
|
|
142
176
|
pending = null;
|
|
143
177
|
}
|
|
144
178
|
});
|
|
145
179
|
return pending;
|
|
146
180
|
}
|
|
147
181
|
|
|
182
|
+
async function phoneOnce() {
|
|
183
|
+
// Not before the app's handlers exist: a command taken then would be
|
|
184
|
+
// marked refused for no reason of hers.
|
|
185
|
+
if (!phoneInbox || phone.ready?.() === false) return;
|
|
186
|
+
try {
|
|
187
|
+
await phoneInbox.syncOnce();
|
|
188
|
+
if (state.phoneError) set({ phoneError: null });
|
|
189
|
+
} catch (err) {
|
|
190
|
+
set({ phoneError: String(err?.message ?? err) });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function makePhoneInbox() {
|
|
195
|
+
if (!phone) return null;
|
|
196
|
+
const productOf = (slug) => products().find((p) => p.slug === slug) ?? null;
|
|
197
|
+
// WHO IT SENDS AS, fixed now, and whether that is still who is signed in
|
|
198
|
+
// (Codex's second review, finding 1).
|
|
199
|
+
const owner = state.me?.id;
|
|
200
|
+
const signedInAs = backend;
|
|
201
|
+
if (!owner) return null;
|
|
202
|
+
let inbox = null;
|
|
203
|
+
inbox = createPhoneInbox({
|
|
204
|
+
backend,
|
|
205
|
+
owner,
|
|
206
|
+
alive: () => phoneInbox === inbox && backend === signedInAs && state.me?.id === owner,
|
|
207
|
+
device: phone.device?.() ?? null,
|
|
208
|
+
projects: () => {
|
|
209
|
+
const on = new Set(phoneProjectsOf(phone.projects(), owner, phoneEligible(products())));
|
|
210
|
+
return products().filter((p) => on.has(p.slug)).map((p) => ({ slug: p.slug, name: p.name || p.slug, dir: p.dir || null, repoPath: p.repoPath || null }));
|
|
211
|
+
},
|
|
212
|
+
listItems: (slug) => { const p = productOf(slug); try { return p ? disk.readWorkItems(p.dir) : []; } catch { return []; } },
|
|
213
|
+
historyOf: (slug, id, { all = false } = {}) => { try { return store.readHistory(slug, id, { max: all ? Number.MAX_SAFE_INTEGER : SNAPSHOT_LIMITS.lines * 4 }); } catch { return []; } },
|
|
214
|
+
apply: phone.apply,
|
|
215
|
+
liveOf: (slug, id) => phone.live?.(slug, id) ?? null,
|
|
216
|
+
approvalsOf: () => phone.approvals?.() ?? [],
|
|
217
|
+
readSettings: phone.readSettings ?? null,
|
|
218
|
+
version: phone.version ?? '',
|
|
219
|
+
idleMs: phone.idleMs ?? null,
|
|
220
|
+
// The lid shut: projects whose threads carry on in the cloud, and runs it started (shared/cloud-kit.mjs).
|
|
221
|
+
carrying: phone.carrying ?? null,
|
|
222
|
+
kitsFor: phone.kitsFor ?? null,
|
|
223
|
+
adopt: phone.adopt ?? null,
|
|
224
|
+
cloudReplies: phone.cloudReplies ?? null,
|
|
225
|
+
runHome: phone.runHome ?? null,
|
|
226
|
+
newThreadKits: phone.newThreadKits ?? null,
|
|
227
|
+
adoptNew: phone.adoptNew ?? null,
|
|
228
|
+
// One file per person: what this Mac sent up for one account is not
|
|
229
|
+
// what the next one to sign in here has.
|
|
230
|
+
stateFile: phone.stateFile.replace(/(\.json)?$/, `.${owner}.json`),
|
|
231
|
+
now,
|
|
232
|
+
log,
|
|
233
|
+
});
|
|
234
|
+
return inbox;
|
|
235
|
+
}
|
|
236
|
+
|
|
148
237
|
function stopLoop() {
|
|
149
238
|
if (timer) clearInterval(timer);
|
|
150
239
|
timer = null;
|
|
@@ -165,9 +254,12 @@ export function createTeamService({
|
|
|
165
254
|
// no team cloud. So the single-person app opened on no projects at all.
|
|
166
255
|
// Everybody gets one at boot now (main/main.mjs), signed in or not.
|
|
167
256
|
await refreshTeam();
|
|
257
|
+
if (state.settling) set({ settling: false });
|
|
258
|
+
phoneInbox = makePhoneInbox();
|
|
168
259
|
sync = createTeamSync({
|
|
169
260
|
backend,
|
|
170
261
|
disk,
|
|
262
|
+
log,
|
|
171
263
|
state: kept(),
|
|
172
264
|
// ARCHIVED ONES COUNT AS HERE. Asked of the visible list, a shared
|
|
173
265
|
// project you archived read as missing and was joined again beside it.
|
|
@@ -184,7 +276,10 @@ export function createTeamService({
|
|
|
184
276
|
}
|
|
185
277
|
|
|
186
278
|
return {
|
|
187
|
-
state: () => state,
|
|
279
|
+
state: () => (rooms ? { ...state, rooms: rooms.state() } : state),
|
|
280
|
+
rooms,
|
|
281
|
+
/** What the desktop's banner was told arrived, for her phone's alerts (main/phone-alerts.mjs). */
|
|
282
|
+
phoneArrivals: (list, needsYou) => phoneInbox?.arrivals(list, needsYou),
|
|
188
283
|
|
|
189
284
|
// THREE TRIES AT START, because one dropped request left the app signed
|
|
190
285
|
// out until it was restarted (measured 2026-10-01 on a loaded Mac: the
|
|
@@ -199,10 +294,10 @@ export function createTeamService({
|
|
|
199
294
|
try {
|
|
200
295
|
const b = await session.restore();
|
|
201
296
|
if (b) await signedIn(b);
|
|
202
|
-
set({ started: true });
|
|
297
|
+
set({ started: true, settling: false });
|
|
203
298
|
return state;
|
|
204
299
|
} catch (err) {
|
|
205
|
-
set({ error: String(err?.message ?? err) });
|
|
300
|
+
set({ error: String(err?.message ?? err), settling: false });
|
|
206
301
|
if (attempt < 2) await new Promise((r) => setTimeout(r, startRetryMs * (attempt + 1)));
|
|
207
302
|
}
|
|
208
303
|
}
|
|
@@ -251,13 +346,16 @@ export function createTeamService({
|
|
|
251
346
|
|
|
252
347
|
async signOut() {
|
|
253
348
|
stopLoop();
|
|
349
|
+
// While still signed in, so this Mac can say it has gone.
|
|
350
|
+
await rooms?.stop().catch(() => {});
|
|
254
351
|
await session.signOut?.();
|
|
255
352
|
disk.setLineAuthor(null);
|
|
256
353
|
delete process.env.AGENTBOX_PERSON_ID;
|
|
257
354
|
backend = null;
|
|
258
355
|
sync = null;
|
|
356
|
+
phoneInbox = null;
|
|
259
357
|
// Signed out is a known answer, so the window shows the sign-in page at once.
|
|
260
|
-
state = { ...EMPTY, configured: !!session?.configured, started: true };
|
|
358
|
+
state = { ...EMPTY, configured: !!session?.configured, started: true, phoneSite: site, phoneSiteQr: siteQr };
|
|
261
359
|
onChange();
|
|
262
360
|
return state;
|
|
263
361
|
},
|
|
@@ -372,7 +470,7 @@ export function createTeamService({
|
|
|
372
470
|
* later is not a chance to lift it. Nothing picked means nothing sent, so
|
|
373
471
|
* the row keeps composeItem's 0, which the inbox reads as Medium.
|
|
374
472
|
*/
|
|
375
|
-
async message(toIn, body, priority) {
|
|
473
|
+
async message(toIn, body, priority, files = []) {
|
|
376
474
|
if (!backend || !state.team || !state.me) throw new Error('start or join a team first');
|
|
377
475
|
const me = state.me.id;
|
|
378
476
|
// One person or a few: the same conversation for the same people.
|
|
@@ -381,8 +479,9 @@ export function createTeamService({
|
|
|
381
479
|
if (others.length > 11) throw new Error('a message goes to at most eleven people');
|
|
382
480
|
if (others.some((p) => !state.people.some((q) => q.id === p))) throw new Error('that person is not on your team');
|
|
383
481
|
const to = others.length === 1 ? others[0] : others;
|
|
384
|
-
const
|
|
385
|
-
|
|
482
|
+
const words = String(body ?? '').trim();
|
|
483
|
+
const carried = (Array.isArray(files) ? files : []).filter((f) => f && (f.dataBase64 || f.srcPath));
|
|
484
|
+
if (!words && !carried.length) throw new Error('a message needs some words');
|
|
386
485
|
// THE RECORD IS REUSED ONLY IF IT IS EXACTLY THE TWO OF YOU AND ONE OF
|
|
387
486
|
// YOU MADE IT. Found by review 2026-10-01: a teammate could make a
|
|
388
487
|
// "direct" record holding you and somebody else, and the next message
|
|
@@ -404,6 +503,10 @@ export function createTeamService({
|
|
|
404
503
|
}
|
|
405
504
|
product = products().find((p) => p.dir === made.dir) ?? { slug: made.slug, dir: made.dir };
|
|
406
505
|
}
|
|
506
|
+
// ITS FILES, saved into the conversation now that there is one, and
|
|
507
|
+
// linked under the words the way the reply box links them. The sync
|
|
508
|
+
// sends each one on before the line that names it (./sync.mjs).
|
|
509
|
+
const text = [words, attachmentMarkdown(saveAttachments(product.dir, carried))].filter(Boolean).join('\n\n');
|
|
407
510
|
// ONE CONVERSATION PER PERSON (decided 2026-10-01 from user research:
|
|
408
511
|
// people expect to click a name and see what was said before, and a
|
|
409
512
|
// subject line on every message between two coworkers makes talking
|
|
@@ -411,7 +514,8 @@ export function createTeamService({
|
|
|
411
514
|
// conversation the two of you already have, reopened if it was put away,
|
|
412
515
|
// and only the first message ever starts one. Work stays in threads; this
|
|
413
516
|
// is talk.
|
|
414
|
-
|
|
517
|
+
// Named by its words; a message that is only files is named by them.
|
|
518
|
+
const title = firstSentence(words, 90) || words.slice(0, 90) || (carried.length === 1 ? String(carried[0].name ?? 'A file') : `${carried.length} files`);
|
|
415
519
|
const convo = conversationIn(product.slug);
|
|
416
520
|
if (convo) {
|
|
417
521
|
store.answerItem(product.slug, convo.id, { answer: text });
|