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/shared/agents.mjs
CHANGED
|
@@ -1,33 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// Measured on her machine 2026-08-16, and this is the whole reason the feature
|
|
11
|
-
// exists: 16 Claude Code sessions live, 13 of them started by another app, and
|
|
12
|
-
// session-71 sitting on `waitingFor: "input needed"` since Wednesday 17:03.
|
|
13
|
-
// Nothing on the machine surfaced that.
|
|
14
|
-
|
|
15
|
-
/* ------------------------- is that process still it ---------------------- */
|
|
16
|
-
// A PID IS NOT AN IDENTITY: a session's state file outlives the process it
|
|
17
|
-
// describes and pids get reused, so a record is only believed when a live
|
|
18
|
-
// process at that pid started at the same moment.
|
|
19
|
-
//
|
|
20
|
-
// THE TWO CLOCKS DO NOT AGREE ON THEIR LABEL, and this is the bug this function
|
|
21
|
-
// exists for. The state file prints its start time in whatever timezone the
|
|
22
|
-
// session was started under; `ps -o lstart` prints local. On her machine
|
|
23
|
-
// 2026-08-16 session-71 read "Wed Aug 12 22:57:59 2026" in its state file and
|
|
24
|
-
// "Wed Aug 12 15:57:59 2026" in ps — the same instant, seven hours apart on the
|
|
25
|
-
// label. Comparing the two strings called all sixteen of her live sessions dead
|
|
26
|
-
// and showed her three, which is the whole feature quietly not working.
|
|
27
|
-
//
|
|
28
|
-
// So they are compared as instants, and a gap that is an exact whole number of
|
|
29
|
-
// hours is exactly what a timezone label costs and nothing else: two genuinely
|
|
30
|
-
// different processes do not start a whole number of hours apart to the second.
|
|
1
|
+
// Pure rules for the user's own Claude Code agents as rows: which ones Agentbox
|
|
2
|
+
// shows, which reach the inbox, and what each says. main/agents.mjs reads the
|
|
3
|
+
// machine; nothing here touches a process, a socket or a file. The rules live
|
|
4
|
+
// apart from the reader because a row missing from a list has no symptom.
|
|
5
|
+
|
|
6
|
+
// A pid is not an identity, so a record is believed only when the live process
|
|
7
|
+
// at that pid started at the same moment. The state file prints its start time
|
|
8
|
+
// in the session's own timezone and `ps -o lstart` prints local time, so a gap
|
|
9
|
+
// of an exact whole number of hours counts as the same instant.
|
|
31
10
|
import { NAME } from '../shared/product-name.mjs';
|
|
32
11
|
const HOUR = 3_600_000;
|
|
33
12
|
|
|
@@ -39,199 +18,85 @@ export function sameInstant(a, b) {
|
|
|
39
18
|
return gap % HOUR === 0 && gap <= 14 * HOUR;
|
|
40
19
|
}
|
|
41
20
|
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// on the machine has that shape. That is the dedupe key, and it needs no name
|
|
46
|
-
// matching or guessing.
|
|
47
|
-
//
|
|
48
|
-
// The sessionId is a second, independent key for the same fact: the supervisor
|
|
49
|
-
// already records each worker's sessionId when it spawns it. Either alone is
|
|
50
|
-
// enough; both means the fold cannot drift as one of them changes.
|
|
51
|
-
//
|
|
52
|
-
// This matters because without it Agentbox shouts its own work back at the
|
|
53
|
-
// user. In the raw list, Agentbox's own workers, already in the inbox as work
|
|
54
|
-
// items, showed up again as a second row about themselves.
|
|
21
|
+
// Agentbox's own workers are already work items, so they must not show again
|
|
22
|
+
// as agent rows. The supervisor spawns them from the Electron main process, so
|
|
23
|
+
// their ppid is the app's pid; the sessionId it records is a second key.
|
|
55
24
|
export function startedByZero(agent, { pids = new Set(), sessionIds = new Set() } = {}) {
|
|
56
25
|
if (agent.sessionId && sessionIds.has(agent.sessionId)) return true;
|
|
57
26
|
return !!agent.ppid && pids.has(agent.ppid);
|
|
58
27
|
}
|
|
59
28
|
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
// place this feature could most easily break it: most outside agents on a
|
|
63
|
-
// machine are idle at any moment, and filing them all puts a wall of rows in
|
|
64
|
-
// the inbox that say "nothing is asked".
|
|
65
|
-
//
|
|
66
|
-
// So the inbox takes exactly the agents that are waiting on a human. The
|
|
67
|
-
// others are still listed, still findable, one keystroke away — they just do
|
|
68
|
-
// not interrupt.
|
|
29
|
+
// Only an agent waiting on a person reaches the inbox. Most are idle at any
|
|
30
|
+
// moment; they stay listed but do not interrupt.
|
|
69
31
|
export function asksSomething(agent) {
|
|
70
32
|
return agent.status === 'waiting';
|
|
71
33
|
}
|
|
72
34
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
35
|
+
// Claude Code has two waiting states. On "input needed" a message written to
|
|
36
|
+
// the session's socket is taken as the next human turn. On "permission prompt"
|
|
37
|
+
// it queues behind the approval box and changes nothing.
|
|
76
38
|
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// Proven on this machine 2026-08-16: a message written
|
|
80
|
-
// to a live session's socket arrived, was read as the
|
|
81
|
-
// human turn, and the agent went back to work.
|
|
82
|
-
//
|
|
83
|
-
// "permission prompt" the agent is frozen on a box asking to approve a tool
|
|
84
|
-
// call. A message queues BEHIND that box. Sending one
|
|
85
|
-
// would look like it worked and change nothing, which is
|
|
86
|
-
// the failure this codebase cares about most: the system
|
|
87
|
-
// swallowing something the user said.
|
|
88
|
-
//
|
|
89
|
-
// So a reply box appears on the first and never on the second, and the second
|
|
90
|
-
// says out loud that it has to be answered where it is running.
|
|
91
|
-
//
|
|
92
|
-
// `replyReaches` is about the ROW: whether the agent stopped and is waiting on
|
|
93
|
-
// her, which is what decides the row's headline and whether it interrupts at
|
|
94
|
-
// all. It is NOT the gate on the reply box any more — see `replyIsSwallowed`.
|
|
39
|
+
// This decides the row's headline and whether it interrupts. The reply box is
|
|
40
|
+
// gated by `replyIsSwallowed`, not by this.
|
|
95
41
|
export function replyReaches(agent) {
|
|
96
42
|
return asksSomething(agent) && agent.waitingFor === 'input needed';
|
|
97
43
|
}
|
|
98
44
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// `input needed`, which is nearly all of them, and the dock said out loud that
|
|
103
|
-
// there was nothing there to answer. That sentence was wrong.
|
|
104
|
-
//
|
|
105
|
-
// MEASURED 2026-08-17, not reasoned: a throwaway session was left sitting at
|
|
106
|
-
// its prompt with nothing asked, the exact state of the card in question, and
|
|
107
|
-
// the same line `reply` writes was written into its socket. It arrived, the
|
|
108
|
-
// session read it and went back to work and answered it. An idle session is
|
|
109
|
-
// listening; idle is what a session at its prompt IS.
|
|
110
|
-
//
|
|
111
|
-
// So the gate is the single case where a message really is swallowed: the
|
|
112
|
-
// agent frozen on a permission box, where anything she types queues behind the
|
|
113
|
-
// box instead of clearing it. `main/agents.mjs` already refused exactly that
|
|
114
|
-
// one and nothing else, so the UI was the whole of the block.
|
|
45
|
+
// The only agent a reply cannot reach is one stopped on a permission prompt.
|
|
46
|
+
// An idle session at its prompt does read a line written to its socket.
|
|
47
|
+
// `reply` in main/agents.mjs refuses the same case.
|
|
115
48
|
export function replyIsSwallowed(agent) {
|
|
116
49
|
return agent?.waitingFor === 'permission prompt';
|
|
117
50
|
}
|
|
118
51
|
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
// So a replied-to agent goes where a replied-to work item goes: In progress,
|
|
126
|
-
// which in this app is a PROMISE that something is on it or about to be. The
|
|
127
|
-
// promise has to be one that ends, or the row never comes back and she has a
|
|
128
|
-
// list that only grows. It ends the moment the session comes to a stop she can
|
|
129
|
-
// see, and there are exactly two of those:
|
|
130
|
-
//
|
|
131
|
-
// idle it finished the turn and is back at its prompt. That is the
|
|
132
|
-
// answer arriving, and the answer belongs in her inbox.
|
|
133
|
-
// waiting it stopped to ask her something. Same, more so.
|
|
134
|
-
//
|
|
135
|
-
// MEASURED, not reasoned, on her machine 2026-08-17 (claude 2.1.234): a session
|
|
136
|
-
// publishes `status` as one of busy, shell, idle, waiting, and stamps
|
|
137
|
-
// `statusUpdatedAt` every time that moves. 15 of her 16 live sessions were
|
|
138
|
-
// publishing one. So the comparison is against a real clock the session keeps
|
|
139
|
-
// itself, and the row leaves In progress because the SESSION stopped, never
|
|
140
|
-
// because a timer here ran out.
|
|
141
|
-
//
|
|
142
|
-
// The comparison is `>`, against the stamp and not the status alone, because
|
|
143
|
-
// the session is still idle for the moment or two between the user pressing
|
|
144
|
-
// send and it picking the message up. Reading the status alone would send the
|
|
145
|
-
// row straight back to the inbox, which is the original bug wearing a hat.
|
|
146
|
-
//
|
|
147
|
-
// AND THE ONE SESSION THIS CANNOT PROMISE ANYTHING ABOUT: one that publishes no
|
|
148
|
-
// status at all (an older CLI). Agentbox cannot see that one work
|
|
149
|
-
// and cannot see it stop, so it is not moved anywhere and the reply is still
|
|
150
|
-
// delivered. A promise that could never be kept is worse than no promise.
|
|
52
|
+
// A replied-to agent sits in In progress until the session stops again (idle
|
|
53
|
+
// or waiting) after the reply. Claude Code publishes `status` (busy, shell,
|
|
54
|
+
// idle, waiting) and stamps `statusUpdatedAt` when it changes. The stamp must
|
|
55
|
+
// be newer than the reply, because the session is still idle for a moment
|
|
56
|
+
// before it picks the message up. An older CLI that publishes no status is
|
|
57
|
+
// never moved, since nothing could show it stopping.
|
|
151
58
|
export function tookHerReply(agent) {
|
|
152
59
|
const spoke = agent?.repliedAt ?? 0;
|
|
153
60
|
if (!spoke) return false;
|
|
154
|
-
if (!agent?.status) return false;
|
|
155
|
-
if (putAway(agent)) return false;
|
|
61
|
+
if (!agent?.status) return false;
|
|
62
|
+
if (putAway(agent)) return false;
|
|
156
63
|
return !stoppedSince(agent, spoke);
|
|
157
64
|
}
|
|
158
65
|
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// seen to stop and this is false — which is why `tookHerReply` refuses that
|
|
162
|
-
// case up front rather than trusting this.
|
|
66
|
+
// `statusAt` is the session's own stamp on its status. A session with none is
|
|
67
|
+
// never seen to stop, so `tookHerReply` handles that case first.
|
|
163
68
|
export function stoppedSince(agent, since) {
|
|
164
69
|
const at = agent?.statusAt ?? 0;
|
|
165
70
|
if (at <= since) return false;
|
|
166
71
|
return agent?.status === 'idle' || agent?.status === 'waiting';
|
|
167
72
|
}
|
|
168
73
|
|
|
169
|
-
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
// What deferring one MEANS is only ever about her inbox. Nothing is sent, no
|
|
175
|
-
// process is touched and the session keeps waiting exactly as it was: the row
|
|
176
|
-
// stops interrupting her until the moment she named, and it is in Scheduled the
|
|
177
|
-
// whole time. That is the honest whole of it, and it is why
|
|
178
|
-
// this is a moment kept beside the reading rather than anything written into
|
|
179
|
-
// somebody else's terminal.
|
|
180
|
-
|
|
181
|
-
// WHICH AGENT SHE PUT OFF, and it cannot be the pid. The row's id is
|
|
182
|
-
// `agent:<pid>` because that is what the list needs today, but a pid is not an
|
|
183
|
-
// identity (see sameInstant above): pids get reused, and the next session to
|
|
184
|
-
// land on 68909 would inherit a deferral she set on session-71. The sessionId
|
|
185
|
-
// is the session's own name for itself and is what the transcript, the state
|
|
186
|
-
// file and the CLI all agree on, so it is the key whenever there is one. When
|
|
187
|
-
// there is not, the pid is qualified by the instant that process started, which
|
|
188
|
-
// is the same pair `stillTheSame` trusts.
|
|
74
|
+
// Putting off or closing an agent row only changes the inbox. Nothing is sent
|
|
75
|
+
// and the session is not touched.
|
|
76
|
+
|
|
77
|
+
// The key a deferral or close is stored under. Pids get reused, so it is the
|
|
78
|
+
// sessionId when there is one, otherwise the pid plus its start time.
|
|
189
79
|
export function agentKey(agent) {
|
|
190
80
|
if (agent?.sessionId) return `session:${agent.sessionId}`;
|
|
191
81
|
return `pid:${agent?.pid ?? 0}:${agent?.startedAt ?? 0}`;
|
|
192
82
|
}
|
|
193
83
|
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
// follow. So a missed one comes back late rather than being lost, and nothing
|
|
197
|
-
// has to fire at 8am.
|
|
84
|
+
// A predicate, not a timer, like `isDue` for work items: once `runAt` passes
|
|
85
|
+
// the row is back, so a missed moment comes back late rather than being lost.
|
|
198
86
|
export function putOff(agent, now = Date.now()) {
|
|
199
87
|
return (agent?.runAt ?? 0) > now;
|
|
200
88
|
}
|
|
201
89
|
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
// session did anything newer. The reasoning was that a session which has spoken
|
|
207
|
-
// since she dismissed it is news again. A session she has closed goes on
|
|
208
|
-
// working in its own terminal, its activity passed the mark within minutes, and
|
|
209
|
-
// the row she had dealt with came back.
|
|
210
|
-
//
|
|
211
|
-
// So the mark is now read as a flag: closed is closed, for as long as that
|
|
212
|
-
// session exists. WHAT THIS COSTS, stated rather than hidden: a session she
|
|
213
|
-
// closes while it is working will not return to her inbox when it later stops
|
|
214
|
-
// and asks something. That is the same sentence read the other way, and it is
|
|
215
|
-
// the one that was chosen.
|
|
216
|
-
//
|
|
217
|
-
// "UNLESS ASKED OTHERWISE" IS THE UNDO, and it is why this stays a number in
|
|
218
|
-
// the file instead of becoming a boolean: `close(key, 0)` clears it, which is
|
|
219
|
-
// what Z does after she closes a row (`closeAgentRow` in App.tsx). The value
|
|
220
|
-
// still records WHEN she closed it, which is worth keeping and is no longer
|
|
221
|
-
// compared against anything.
|
|
222
|
-
//
|
|
223
|
-
// Nothing is sent and the session is not touched, exactly as with putting one
|
|
224
|
-
// off: this is a fact about her inbox.
|
|
90
|
+
// A closed row stays closed for the life of the session, even if it later asks
|
|
91
|
+
// something; it is not compared against later activity. `doneThrough` is a
|
|
92
|
+
// timestamp, not a boolean, so `close(key, 0)` can undo it (`closeAgentRow` in
|
|
93
|
+
// App.tsx).
|
|
225
94
|
export function putAway(agent) {
|
|
226
95
|
return (agent?.doneThrough ?? 0) > 0;
|
|
227
96
|
}
|
|
228
97
|
|
|
229
|
-
|
|
230
|
-
//
|
|
231
|
-
// the one thing the user has to do about it, in bold, one
|
|
232
|
-
// sentence, under fifteen words (instructions.md). The row clips at
|
|
233
|
-
// SUMMARY_BUDGET = 112 characters on the last full stop inside it, so that
|
|
234
|
-
// sentence has to carry the whole point on its own.
|
|
98
|
+
// The words. The row clips the summary at SUMMARY_BUDGET (112 characters) on
|
|
99
|
+
// the last full stop, so the first sentence has to carry the point alone.
|
|
235
100
|
export function howLong(ms) {
|
|
236
101
|
if (!Number.isFinite(ms) || ms < 0) return 'a moment';
|
|
237
102
|
const days = Math.floor(ms / 86400000);
|
|
@@ -242,9 +107,6 @@ export function howLong(ms) {
|
|
|
242
107
|
return `${mins} minute${mins === 1 ? '' : 's'}`;
|
|
243
108
|
}
|
|
244
109
|
|
|
245
|
-
// The folder it is working in, said the way she would say it. Not the path: A
|
|
246
|
-
// long "/Users/you/Desktop/dev/<repo>" is four words of noise around the one
|
|
247
|
-
// word that identifies it.
|
|
248
110
|
export function whereItRuns(agent) {
|
|
249
111
|
const owner = agent.productName;
|
|
250
112
|
if (owner) return owner;
|
|
@@ -253,13 +115,8 @@ export function whereItRuns(agent) {
|
|
|
253
115
|
return leaf || 'an unknown folder';
|
|
254
116
|
}
|
|
255
117
|
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
// goes because a card is not a diff viewer.
|
|
259
|
-
//
|
|
260
|
-
// Split out from `oneSentence` when `howItEnded` needed the same cleaning from
|
|
261
|
-
// the other end of the message. One copy, so a quote of the user's and a quote of its
|
|
262
|
-
// answer can never be tidied by two different rules.
|
|
118
|
+
// Flattens typing to one line. Markdown markers go because the card quotes
|
|
119
|
+
// this inside its own markdown.
|
|
263
120
|
function readableWords(raw) {
|
|
264
121
|
return String(raw ?? '')
|
|
265
122
|
.replace(/```[\s\S]*?```/g, ' ')
|
|
@@ -270,8 +127,7 @@ function readableWords(raw) {
|
|
|
270
127
|
.trim();
|
|
271
128
|
}
|
|
272
129
|
|
|
273
|
-
//
|
|
274
|
-
// right for her and wrong for an agent; see `howItEnded`.
|
|
130
|
+
// Cuts from the front, which suits a person's message. See `howItEnded`.
|
|
275
131
|
function oneSentence(raw, budget = 180) {
|
|
276
132
|
const text = readableWords(raw);
|
|
277
133
|
if (!text) return '';
|
|
@@ -286,37 +142,21 @@ export function lastWord(agent, budget = 180) {
|
|
|
286
142
|
return oneSentence(agent.lastSaid, budget);
|
|
287
143
|
}
|
|
288
144
|
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
// `oneSentence` takes the FRONT of a message, and for her that is right: she
|
|
292
|
-
// opens with the ask. An agent does the opposite. It opens with what it looked
|
|
293
|
-
// at and closes with what it found, so quoting its first sentence on the card
|
|
294
|
-
// reliably showed her the preamble and hid the point.
|
|
295
|
-
//
|
|
296
|
-
// So this reads the same message from the other end. Same cleaning as
|
|
297
|
-
// `oneSentence`, then whole sentences off the BACK until the budget is full,
|
|
298
|
-
// with a leading ellipsis when there was more in front of them.
|
|
145
|
+
// Whole sentences from the end of a message, because an agent puts what it
|
|
146
|
+
// found last.
|
|
299
147
|
export function howItEnded(raw, budget = 320) {
|
|
300
148
|
const text = readableWords(raw);
|
|
301
149
|
if (!text) return '';
|
|
302
150
|
if (text.length <= budget) return text;
|
|
303
151
|
const tail = text.slice(-budget);
|
|
304
|
-
// Start at a sentence, so the quote never opens mid-clause. If the last
|
|
305
|
-
// sentence is itself longer than the budget there is no boundary to find, and
|
|
306
|
-
// a clean word break is the best honest cut.
|
|
307
152
|
const starts = [tail.indexOf('. '), tail.indexOf('? '), tail.indexOf('! ')].filter((i) => i >= 0);
|
|
308
153
|
const at = starts.length ? Math.min(...starts) + 2 : -1;
|
|
309
154
|
const cut = at >= 0 && at < budget - 40 ? tail.slice(at) : tail.slice(tail.indexOf(' ') + 1);
|
|
310
155
|
return `…${cut.trim()}`;
|
|
311
156
|
}
|
|
312
157
|
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
// The card used to head the quote "It stopped here:" with no time on it at all,
|
|
316
|
-
// so a session that had answered every one of her messages and a session that
|
|
317
|
-
// had said nothing for two days drew the identical heading. Six cards that
|
|
318
|
-
// looked the same because the one fact that told them apart was not on any of
|
|
319
|
-
// them.
|
|
158
|
+
// The heading over the agent's last words: when it spoke, and whether that came
|
|
159
|
+
// after the user's latest message.
|
|
320
160
|
function saidWhen(agent, now) {
|
|
321
161
|
const at = Number(agent?.saidAt) || 0;
|
|
322
162
|
if (!at) return '**It stopped here:**';
|
|
@@ -329,26 +169,15 @@ function saidWhen(agent, now) {
|
|
|
329
169
|
return `**It answered ${when}:**`;
|
|
330
170
|
}
|
|
331
171
|
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
// message's clothes.
|
|
172
|
+
// The last thing the user typed, from the session's own `last-prompt` line,
|
|
173
|
+
// which never holds a tool result.
|
|
335
174
|
export function lastAsk(agent, budget = 180) {
|
|
336
175
|
return oneSentence(agent.lastAsked, budget);
|
|
337
176
|
}
|
|
338
177
|
|
|
339
|
-
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
// Every Claude Code session writes a one-line title of ITSELF into its own
|
|
344
|
-
// transcript and keeps it current. Every outside session measured had one, and
|
|
345
|
-
// it is usually the very sentence a person would use to recall the work,
|
|
346
|
-
// sitting in a file Agentbox was already reading and throwing away.
|
|
347
|
-
//
|
|
348
|
-
// So the title of the row is what the work IS, with the session's name in front
|
|
349
|
-
// of it because two sessions can run in the same folder and the name is how
|
|
350
|
-
// they are told apart. When a session has no title of its own the row says
|
|
351
|
-
// what it used to say, which is better than a blank.
|
|
178
|
+
// Claude Code keeps a one-line title of each session in its transcript; that
|
|
179
|
+
// is `about`. The row title puts the session's name in front of it, because
|
|
180
|
+
// two sessions can run in the same folder.
|
|
352
181
|
export function whatItIs(agent) {
|
|
353
182
|
return String(agent?.about ?? '').replace(/\s+/g, ' ').trim();
|
|
354
183
|
}
|
|
@@ -358,62 +187,25 @@ export function agentTitle(agent, now = Date.now()) {
|
|
|
358
187
|
if (about) return `${agent.name}: ${about}`;
|
|
359
188
|
if (replyReaches(agent)) return `${agent.name} is waiting for you`;
|
|
360
189
|
if (asksSomething(agent)) return `${agent.name} needs you where it is running`;
|
|
361
|
-
// NOT "QUIET" ONCE THE USER HAS SPOKEN TO IT. Without this the row just
|
|
362
|
-
// replied to sat in In progress under a heading saying it had been quiet for
|
|
363
|
-
// three days, which is the same sentence it wore before anything was said.
|
|
364
190
|
if (tookHerReply(agent)) return `${agent.name} is working on what you told it`;
|
|
365
191
|
return `${agent.name} has been quiet for ${howLong(now - (agent.lastActiveAt ?? agent.startedAt ?? now))}`;
|
|
366
192
|
}
|
|
367
193
|
|
|
368
|
-
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
//
|
|
374
|
-
// the
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// The human side of a conversation is also small enough to just print: a
|
|
379
|
-
// handful of turns per session, a sentence or two each. The megabytes in a
|
|
380
|
-
// transcript are the agent's half. So the recall is the user's own words, read
|
|
381
|
-
// off the file, and no model sees any of it.
|
|
382
|
-
//
|
|
383
|
-
// ONE SHAPE, NOT THREE, AND IT IS NOT CONFIGURABLE.
|
|
384
|
-
//
|
|
385
|
-
// So the card does the same thing on every session: the message the user
|
|
386
|
-
// started it with, up to three later messages of theirs, where it stopped, and
|
|
387
|
-
// where it is working. `ask` and `trail` are gone, and there is deliberately no
|
|
388
|
-
// setting for this: deciding per session which of them a card deserves is a
|
|
389
|
-
// judgement that should not be made, whether by a model or by a knob.
|
|
390
|
-
|
|
391
|
-
// The messages the user typed, oldest first, with the newest ones kept whole and the
|
|
392
|
-
// opening always present.
|
|
393
|
-
//
|
|
394
|
-
// AN EMPTY LIST IS NOT A FALLBACK TO `lastAsked`, and the difference is the
|
|
395
|
-
// whole point of this change rather than a nicety. A session whose transcript
|
|
396
|
-
// could not be read has exactly one thing the user typed on record, and it is
|
|
397
|
-
// the LAST one. So the caller is told which of the two it has. WHAT THE USER
|
|
398
|
-
// SAID FROM AGENTBOX IS ALSO WHAT THE USER SAID.
|
|
399
|
-
//
|
|
400
|
-
// So the card went on saying only what the session was started with.
|
|
401
|
-
//
|
|
402
|
-
// `spoke` is the other half, written by main/agent-schedule.mjs at the moment
|
|
403
|
-
// of delivery. It is a RECORD, not a guess: Agentbox does not try to work out
|
|
404
|
-
// which wrapped peer message in a transcript was the user's, because another of
|
|
405
|
-
// their sessions pinging this one is a real peer and drawing that as the user's
|
|
406
|
-
// words would put a sentence they never typed on the one card built to be trusted.
|
|
194
|
+
// Recalling a session. The card shows the user's own words read off the
|
|
195
|
+
// transcript (no model sees them): the opening ask, up to three later messages,
|
|
196
|
+
// where it stopped and where it works. The same shape on every session, with no
|
|
197
|
+
// setting.
|
|
198
|
+
|
|
199
|
+
// The user's messages, opening first, then the rest by time. `asked` comes from
|
|
200
|
+
// the transcript. `spoke` is what main/agent-schedule.mjs recorded as delivered
|
|
201
|
+
// from Agentbox, because a peer message in the transcript cannot be told apart
|
|
202
|
+
// from the user's. Deduped on text in case the session also recorded it.
|
|
407
203
|
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
// its own (a future Claude Code, or one typed into directly) must not show it
|
|
411
|
-
// twice.
|
|
204
|
+
// An unreadable transcript gives an empty list, never a fallback to
|
|
205
|
+
// `lastAsked`, so the caller knows it only has the last message.
|
|
412
206
|
export function herWords(agent) {
|
|
413
207
|
const asked = (agent?.asked ?? []).filter((t) => t && t.text);
|
|
414
208
|
const spoke = (agent?.spoke ?? []).filter((t) => t && t.text);
|
|
415
|
-
// AN EMPTY LIST STAYS EMPTY, and that is not laziness. Nothing here is
|
|
416
|
-
// allowed to invent an opening; see the note above this function.
|
|
417
209
|
if (!spoke.length || !asked.length) return asked;
|
|
418
210
|
const seen = new Set(asked.map((t) => t.text.replace(/\s+/g, ' ').trim()));
|
|
419
211
|
const merged = [...asked];
|
|
@@ -423,21 +215,15 @@ export function herWords(agent) {
|
|
|
423
215
|
seen.add(flat);
|
|
424
216
|
merged.push({ at: t.at, text: flat });
|
|
425
217
|
}
|
|
426
|
-
// The opening
|
|
427
|
-
//
|
|
218
|
+
// The opening stays first even if a clock skewed, because the card labels it
|
|
219
|
+
// as the start.
|
|
428
220
|
const [first, ...rest] = merged;
|
|
429
221
|
rest.sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
|
|
430
222
|
return first ? [first, ...rest] : [];
|
|
431
223
|
}
|
|
432
224
|
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
// agent works is driving it, not describing it: short nudges to restart,
|
|
436
|
-
// carry on or approve are almost always under sixty characters, while a turn
|
|
437
|
-
// that describes the work typically runs well past a hundred.
|
|
438
|
-
//
|
|
439
|
-
// So the long ones are preferred, and the short ones are still there when
|
|
440
|
-
// there is nothing longer, because a card that drops to silence is worse.
|
|
225
|
+
// Short messages are usually nudges ("carry on", "approve"), so longer ones
|
|
226
|
+
// are preferred when there are any.
|
|
441
227
|
const RECALLS_SOMETHING = 60;
|
|
442
228
|
|
|
443
229
|
function worthRecalling(turns) {
|
|
@@ -445,8 +231,6 @@ function worthRecalling(turns) {
|
|
|
445
231
|
return meaty.length ? meaty : turns;
|
|
446
232
|
}
|
|
447
233
|
|
|
448
|
-
// How long ago, said the way she would say it, and never a clock time: a
|
|
449
|
-
// weekday name five days later is a small puzzle and "5 days ago" is not.
|
|
450
234
|
function ago(at, now) {
|
|
451
235
|
if (!at) return '';
|
|
452
236
|
const gap = now - at;
|
|
@@ -455,8 +239,6 @@ function ago(at, now) {
|
|
|
455
239
|
return `${howLong(gap)} ago`;
|
|
456
240
|
}
|
|
457
241
|
|
|
458
|
-
// The rest of the context, in the user's sentences: what they asked for, where the
|
|
459
|
-
// thread got to, what it last said back, and what it has had its hands on.
|
|
460
242
|
function context(agent, now = Date.now()) {
|
|
461
243
|
const words = herWords(agent);
|
|
462
244
|
const said = howItEnded(agent.lastSaid);
|
|
@@ -468,14 +250,10 @@ function context(agent, now = Date.now()) {
|
|
|
468
250
|
const when = ago(opening.at, now);
|
|
469
251
|
lines.push(`**You started it${when ? ` ${when}` : ''} with:**\n\n“${oneSentence(opening.text, 420)}”`);
|
|
470
252
|
} else {
|
|
471
|
-
// No history, so the only thing on record is the LAST thing the user typed, and
|
|
472
|
-
// it gets the label that is true of it.
|
|
473
253
|
const last = lastAsk(agent);
|
|
474
254
|
if (last) lines.push(`**The last thing you told it:**\n\n“${last}”`);
|
|
475
255
|
}
|
|
476
256
|
|
|
477
|
-
// Everything else the user typed, oldest first, so the thread reads forwards.
|
|
478
|
-
// Three of them; more than that is the transcript rather than a card.
|
|
479
257
|
if (rest.length) {
|
|
480
258
|
const recent = worthRecalling(rest).slice(-3);
|
|
481
259
|
const bullets = recent.map((t) => {
|
|
@@ -486,9 +264,8 @@ function context(agent, now = Date.now()) {
|
|
|
486
264
|
lines.push(`**Then${over} you said:**\n\n${bullets.join('\n')}`);
|
|
487
265
|
}
|
|
488
266
|
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
// had just chosen and handed back the front of it.
|
|
267
|
+
// Already cut from the end by `howItEnded`; `oneSentence` would cut it again
|
|
268
|
+
// from the front.
|
|
492
269
|
if (said) lines.push(`${saidWhen(agent, now)}\n\n“${said}”`);
|
|
493
270
|
|
|
494
271
|
const files = (agent.touched ?? []).slice(-3);
|
|
@@ -508,13 +285,7 @@ export function agentRow(agent, now = Date.now()) {
|
|
|
508
285
|
const title = agentTitle(agent, now);
|
|
509
286
|
const rest = context(agent, now);
|
|
510
287
|
|
|
511
|
-
//
|
|
512
|
-
// describes a session that has stopped, and this one has not: the row that
|
|
513
|
-
// sent her back here was in her inbox under "It is asking for nothing and has
|
|
514
|
-
// done nothing for 1 minute" moments after she told it to do something. This
|
|
515
|
-
// row asks her for nothing on purpose — it is in In progress, which is where
|
|
516
|
-
// the app puts what it is getting on with — so the opening line is the news
|
|
517
|
-
// rather than a question.
|
|
288
|
+
// First, because every branch below describes a session that has stopped.
|
|
518
289
|
if (tookHerReply(agent)) {
|
|
519
290
|
return {
|
|
520
291
|
title,
|
|
@@ -533,86 +304,47 @@ export function agentRow(agent, now = Date.now()) {
|
|
|
533
304
|
body: `**Answer ${agent.name} in its own window, not here.**\n\nIt is asking to approve something, and a message from ${NAME} would wait behind that box instead of clearing it.${rest}`,
|
|
534
305
|
};
|
|
535
306
|
}
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
// is about her inbox and nothing else — the session is not touched — and the
|
|
539
|
-
// body says exactly that, because a row that looks like it might kill
|
|
540
|
-
// somebody's terminal is a row she cannot press at speed.
|
|
307
|
+
// The body says the session keeps running, so closing the row does not look
|
|
308
|
+
// like it might kill a terminal.
|
|
541
309
|
return {
|
|
542
310
|
title,
|
|
543
311
|
body: `**Close ${agent.name} if you no longer need it here.**\n\nIt is asking for nothing and has done nothing for ${waited}. Closing it takes it out of your inbox and leaves the session running.${rest}`,
|
|
544
312
|
};
|
|
545
313
|
}
|
|
546
314
|
|
|
547
|
-
|
|
548
|
-
//
|
|
549
|
-
// either in her inbox or nowhere, which is the whole of "merge the inbox into
|
|
550
|
-
// one".
|
|
551
|
-
//
|
|
552
|
-
// WHICH OF THEM IS IN IT IS HER SETTING, because she named both sides of it
|
|
553
|
-
// herself in the same message:
|
|
315
|
+
// There is no separate agents tab: an outside agent is in the inbox or
|
|
316
|
+
// nowhere. Which ones is a setting.
|
|
554
317
|
export const AGENT_MODES = ['all', 'waiting', 'off'];
|
|
555
318
|
|
|
556
319
|
export function reachesInbox(agent, now = Date.now(), mode = 'all') {
|
|
557
320
|
if (agent.startedByZero) return false;
|
|
558
321
|
if (mode === 'off') return false;
|
|
559
|
-
//
|
|
560
|
-
// stops for her again; showing it here at the same time is the "one row, two
|
|
561
|
-
// tabs" failure this codebase has already had once, and it would still be
|
|
562
|
-
// sitting under a headline saying it asks for nothing.
|
|
322
|
+
// A replied-to session is in In progress instead (`progressAfterReply`).
|
|
563
323
|
if (tookHerReply(agent)) return false;
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
// published no title of its own, and the body under it asks her to close a
|
|
567
|
-
// row that asked her nothing, which instructions.md says should never have
|
|
568
|
-
// been filed at all. It cannot be fixed by closing one either: a throwaway
|
|
569
|
-
// session gets a fresh pid every time, so E is keyed to an id that never
|
|
570
|
-
// comes back and the next one arrives just as loud.
|
|
571
|
-
//
|
|
572
|
-
// The sweep `all` was built for is untouched. A forgotten session that has a
|
|
573
|
-
// title still carries it here, which is the one she can recognise and decide
|
|
574
|
-
// about; this drops only the ones with nothing to show her.
|
|
324
|
+
// An idle session with no title has nothing to show, and closing it does not
|
|
325
|
+
// help: a throwaway session gets a fresh pid each time.
|
|
575
326
|
if (!asksSomething(agent) && !whatItIs(agent)) return false;
|
|
576
327
|
if (mode === 'waiting' && !asksSomething(agent)) return false;
|
|
577
328
|
return !putOff(agent, now) && !putAway(agent);
|
|
578
329
|
}
|
|
579
330
|
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
// THIS IS NOT `reachesProgress`, WHICH WAS REMOVED, and the name is different
|
|
586
|
-
// on purpose. It stays dead: a working session the user has not spoken to is in
|
|
587
|
-
// the inbox, exactly as they left it. What moves a row here is THE USER, and
|
|
588
|
-
// only the user: the reply they wrote.
|
|
331
|
+
// The In progress counterpart of `belongsInProgress` for work items. Only the
|
|
332
|
+
// user's reply moves an agent here; a working session nobody spoke to stays in
|
|
333
|
+
// the inbox. A reply cancels a deferral, as `replyClearsSchedule` does for work
|
|
334
|
+
// items, so `putOff` is not checked.
|
|
589
335
|
export function progressAfterReply(agent, now = Date.now(), mode = 'all') {
|
|
590
336
|
if (agent.startedByZero) return false;
|
|
591
337
|
if (mode === 'off') return false;
|
|
592
338
|
return tookHerReply(agent);
|
|
593
339
|
}
|
|
594
340
|
|
|
595
|
-
// Everything Agentbox will admit exists: every live session except its own
|
|
596
|
-
// workers, which are already in her inbox as work items.
|
|
597
341
|
export function listed(agent) {
|
|
598
342
|
return !agent.startedByZero;
|
|
599
343
|
}
|
|
600
344
|
|
|
601
|
-
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
// kind.
|
|
605
|
-
//
|
|
606
|
-
// THE CUT IS A DAY, AND IT IS THE ONLY FILTER.The premise is worth correcting
|
|
607
|
-
// where the rule lives, because it decides what the rule can be: nothing
|
|
608
|
-
// FINISHED is ever on this list and never has been. `main/agents.mjs` drops any
|
|
609
|
-
// record whose process is gone, so a session that finishes exits and disappears
|
|
610
|
-
// by itself. Measured: most live sessions were terminals sitting at a prompt
|
|
611
|
-
// for between two and seven days.
|
|
612
|
-
// So length comes from sessions that are alive and forgotten, and a day is
|
|
613
|
-
// where the app's own clock (`format.ts`) stops saying hours and starts saying
|
|
614
|
-
// `1d`. The panel therefore holds exactly what the clock beside each row can
|
|
615
|
-
// say in hours, which is the same sentence read twice.
|
|
345
|
+
// The sidebar shows sessions active in the last day, the point where the
|
|
346
|
+
// app's clock (`format.ts`) switches from hours to `1d`. Finished sessions
|
|
347
|
+
// never appear, because main/agents.mjs drops records whose process is gone.
|
|
616
348
|
export const RAIL_CUT_MS = 24 * 60 * 60 * 1000;
|
|
617
349
|
|
|
618
350
|
export function onTheRail(agent, now = Date.now()) {
|
|
@@ -621,45 +353,20 @@ export function onTheRail(agent, now = Date.now()) {
|
|
|
621
353
|
return moved > 0 && now - moved <= RAIL_CUT_MS;
|
|
622
354
|
}
|
|
623
355
|
|
|
624
|
-
//
|
|
625
|
-
// `about` is the one-line title every Claude Code session keeps current for
|
|
626
|
-
// itself; `lastSaid` is the fallback for a session Agentbox started, which has no
|
|
627
|
-
// title. Neither is cut here: the row is one line with `text-overflow` on it,
|
|
628
|
-
// so the width she is actually looking at does the cutting, once.
|
|
356
|
+
// Not cut here; the row's CSS `text-overflow` cuts it to the width shown.
|
|
629
357
|
export function railLine(agent) {
|
|
630
358
|
return whatItIs(agent) || lastWord(agent) || 'Has not said anything yet';
|
|
631
359
|
}
|
|
632
360
|
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
// stopped one to the top instead read well in a sentence and drew badly: the
|
|
636
|
-
// list groups by day as it goes, so a waiting agent from Wednesday above a
|
|
637
|
-
// row from July put "Last 7 days" on screen twice, once at the top and once
|
|
638
|
-
// eight rows down (measured in the built app, 2026-08-16).
|
|
639
|
-
//
|
|
640
|
-
// The stopped one does not need pinning here. It is scored in the inbox by the
|
|
641
|
-
// same rule everything else there is scored by, which puts an agent asking
|
|
642
|
-
// something above one asking nothing without this having to know about it.
|
|
361
|
+
// Pure recency, because the list draws day headings as it goes and any other
|
|
362
|
+
// order repeats a heading. Waiting agents are ranked in the inbox, not here.
|
|
643
363
|
export function byRecency(a, b) {
|
|
644
364
|
return (b.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0);
|
|
645
365
|
}
|
|
646
366
|
|
|
647
|
-
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
// drawn because they pressed something, so it can be the conversation.
|
|
651
|
-
//
|
|
652
|
-
// STILL NO MODEL, AND NOW NO COST AT ALL UNTIL IT IS PRESSED. Measured: the
|
|
653
|
-
// readable conversation is 0.6% of a transcript's bytes, and the worst single
|
|
654
|
-
// file measured (160MB) carries 3,800 readable turns, 2.6MB of text, 729ms to
|
|
655
|
-
// pull out. That last number is why the read is streamed and never happens on
|
|
656
|
-
// a draw.
|
|
657
|
-
//
|
|
658
|
-
// WHY IT IS A WINDOW AND NOT THE WHOLE FILE. 3,800 turns is not a conversation
|
|
659
|
-
// anyone can get up to speed on, it is a scroll bar. So it is the opening,
|
|
660
|
-
// which is the ask in every session measured, and the end, which is where it got to,
|
|
661
|
-
// and one plain line saying how many are in between rather than pretending
|
|
662
|
-
// there are none.
|
|
367
|
+
// The full conversation, read only when the user opens it, since a large
|
|
368
|
+
// transcript takes most of a second to read. It shows the opening turns and the
|
|
369
|
+
// latest ones, with a line counting what is in between.
|
|
663
370
|
export const TRAIL_OPENING = 3;
|
|
664
371
|
export const TRAIL_TURNS = 40;
|
|
665
372
|
|
|
@@ -671,116 +378,56 @@ export function conversationWindow(turns, { opening = TRAIL_OPENING, keep = TRAI
|
|
|
671
378
|
return { turns: [...head, ...tail], omitted: all.length - head.length - tail.length };
|
|
672
379
|
}
|
|
673
380
|
|
|
674
|
-
// The
|
|
675
|
-
//
|
|
676
|
-
// else the app is rounding off.
|
|
677
|
-
//
|
|
678
|
-
// And it is not rare. Rebuilding every row's thread the way the pane draws it,
|
|
679
|
-
// nearly half the rows with a conversation hide part of it, many of them
|
|
680
|
-
// hiding messages the user wrote, and the line was a plain div with nothing to
|
|
681
|
-
// press. "Not shown" was true and it was also the end of the road, so a thread
|
|
682
|
-
// that had eaten a follow-up simply said so and stopped.
|
|
683
|
-
//
|
|
684
|
-
// So the words say what pressing does. The window itself is untouched: it still
|
|
685
|
-
// opens on the end of a long conversation, because an 18,100px page is the
|
|
686
|
-
// thing it was built for. What changed is that the middle can be asked for.
|
|
381
|
+
// The line that stands for the hidden middle. It gives the count, and pressing
|
|
382
|
+
// it shows those messages.
|
|
687
383
|
export function conversationGap(omitted) {
|
|
688
384
|
if (!omitted) return '';
|
|
689
385
|
return `Show the ${omitted} message${omitted === 1 ? '' : 's'} in between`;
|
|
690
386
|
}
|
|
691
387
|
|
|
692
|
-
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
// file decides what one of those lines SAYS. What it looks like is the frosted
|
|
696
|
-
// treatment chosen in round two and lives in the stylesheet.
|
|
697
|
-
//
|
|
698
|
-
// WHY IT IS BOUNDED THREE WAYS, all measured over the 40 largest transcripts
|
|
699
|
-
// available:
|
|
700
|
-
//
|
|
701
|
-
// * one session held 6,070 tool calls and 8MB of output. So output is capped
|
|
702
|
-
// per line (WORK_CHARS) rather than sent whole.
|
|
703
|
-
// * the longest unbroken run of tool calls between two messages was 111, and
|
|
704
|
-
// the median session's longest run was 25. So a run is capped (WORK_RUN)
|
|
705
|
-
// and says how many it is not showing, because 111 quiet lines standing
|
|
706
|
-
// between a question and its answer is the original problem wearing a
|
|
707
|
-
// different hat.
|
|
708
|
-
// * a windowed thread of 40 messages could still carry thousands of work
|
|
709
|
-
// lines, so the whole thread is capped too (WORK_TOTAL), oldest first.
|
|
710
|
-
//
|
|
711
|
-
// Every cap says its own number out loud where it bites. Nothing here rounds
|
|
712
|
-
// off silently: "some output" is the kind of sentence that makes her wonder
|
|
713
|
-
// what else the app is not telling her.
|
|
388
|
+
// Each tool call the agent ran is a line between the messages. Caps: output
|
|
389
|
+
// per line (WORK_CHARS), one unbroken run of calls (WORK_RUN), and the whole
|
|
390
|
+
// thread (WORK_TOTAL, oldest dropped first). Every cap shows how much it hid.
|
|
714
391
|
export const WORK_CHARS = 2000;
|
|
715
392
|
export const WORK_RUN = 20;
|
|
716
393
|
export const WORK_TOTAL = 200;
|
|
717
394
|
|
|
718
|
-
//
|
|
719
|
-
// because a report on the imported threads turned one rule into
|
|
720
|
-
// three: the verb, the subject and the fold. It is re-exported here so that
|
|
721
|
-
// nothing that already reads this file has to learn a second import.
|
|
395
|
+
// Re-exported so existing importers of this file keep working.
|
|
722
396
|
export {
|
|
723
397
|
workVerb, workSubject, fullSubject, shortPath, plainCommand, commandWork,
|
|
724
398
|
groupWork, runSummary, runFailures, RUN_MIN, isBookkeeping,
|
|
725
399
|
changedFile, fileInChange, CHANGES_A_FILE,
|
|
726
400
|
} from './work-lines.mjs';
|
|
727
401
|
|
|
728
|
-
// The line that stands where a capped run of work is. Same rule as the message
|
|
729
|
-
// gap above: say the number.
|
|
730
402
|
export function runOverflow(more) {
|
|
731
403
|
if (!more) return '';
|
|
732
404
|
return `and ${more} more thing${more === 1 ? '' : 's'} it ran, not shown`;
|
|
733
405
|
}
|
|
734
406
|
|
|
735
|
-
// The line at the foot of an output that was longer than we keep.
|
|
736
407
|
export function outputCut(shownLines, allLines) {
|
|
737
408
|
if (!allLines || allLines <= shownLines) return '';
|
|
738
409
|
return `${allLines - shownLines} more line${allLines - shownLines === 1 ? '' : 's'} came back, not kept`;
|
|
739
410
|
}
|
|
740
411
|
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
// exactly as before, and the work that sits between two kept messages comes
|
|
746
|
-
// with them. `omitted` still counts messages, because that is what the line
|
|
747
|
-
// under the gap says.
|
|
748
|
-
//
|
|
749
|
-
// `whole` IS WHAT THE GAP LINE PRESSES. Every message stands, and nothing else
|
|
750
|
-
// changes: the work is still capped by `capRuns` below, because 530 tool calls
|
|
751
|
-
// is not a conversation and what is wanted is the follow-ups, not the output
|
|
752
|
-
// under them. Opening it is the user's decision and it lasts until they leave the row,
|
|
753
|
-
// so no thread is ever born at this size.
|
|
412
|
+
// Like `conversationWindow`, but over a thread with work lines in it. The
|
|
413
|
+
// window counts messages only, so tool calls cannot push messages out, and work
|
|
414
|
+
// between two kept messages comes with them. `whole` is what pressing the gap
|
|
415
|
+
// line asks for: every message, with work still capped by `capRuns`.
|
|
754
416
|
export function threadWindow(events, { opening = TRAIL_OPENING, keep = TRAIL_TURNS, whole = false } = {}) {
|
|
755
417
|
const all = (events ?? []).filter((e) => e && (e.kind === 'work' ? true : !!e.text));
|
|
756
418
|
const saidAt = [];
|
|
757
419
|
all.forEach((e, i) => { if (e.kind !== 'work') saidAt.push(i); });
|
|
758
420
|
if (whole || saidAt.length <= keep) return { events: capRuns(all), omitted: 0 };
|
|
759
421
|
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
// never shows output belonging to a message that is not on screen.
|
|
422
|
+
// Everything between these is dropped, work and messages together, so no
|
|
423
|
+
// output shows for a message that is not on screen.
|
|
763
424
|
const from = saidAt[opening];
|
|
764
425
|
const to = saidAt[saidAt.length - (keep - opening)];
|
|
765
426
|
|
|
766
|
-
//
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
// a lie. Whatever it was continuing is in the missing middle, so it draws as
|
|
771
|
-
// a nameless paragraph hanging off whichever message the window happened to
|
|
772
|
-
// end the opening with, which is a different run from a different hour.
|
|
773
|
-
//
|
|
774
|
-
// Measured on her store, 2026-08-28: of the 123 rows with a gap in them, 93
|
|
775
|
-
// resumed on a nameless block.
|
|
776
|
-
//
|
|
777
|
-
// `resumed` RATHER THAN CLEARING `same`, and the difference matters. `same`
|
|
778
|
-
// is what `saidCount` folds on, so a block that stops being a continuation
|
|
779
|
-
// becomes a second message in the header's count while still being one
|
|
780
|
-
// message in the row. Measured before this note was written: clearing it put
|
|
781
|
-
// `omitted` at -1 on 15 of her rows, which is the gap line quoting her a
|
|
782
|
-
// number that cannot be true. So the fold is left exactly as it was and only
|
|
783
|
-
// the DRAWING changes: this block gets its name and its time back.
|
|
427
|
+
// A `same` block continues the one above and draws with no name or time. The
|
|
428
|
+
// first block after the cut continues something hidden, so it is marked
|
|
429
|
+
// `resumed` to draw its name again. Clearing `same` instead would change
|
|
430
|
+
// `saidCount` and make `omitted` disagree with it.
|
|
784
431
|
const tail = all.slice(to);
|
|
785
432
|
if (tail.length && tail[0].kind !== 'work' && tail[0].same) tail[0] = { ...tail[0], resumed: true };
|
|
786
433
|
const kept = [...all.slice(0, from), ...tail];
|
|
@@ -788,23 +435,10 @@ export function threadWindow(events, { opening = TRAIL_OPENING, keep = TRAIL_TUR
|
|
|
788
435
|
}
|
|
789
436
|
|
|
790
437
|
/**
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
* `same` continuations, while the cut above keeps the first `opening` BLOCKS
|
|
796
|
-
* with continuations counted. The two agree only when no run's second sentence
|
|
797
|
-
* lands in the opening, and the ordinary shape of a thread is the ask, a run's
|
|
798
|
-
* first line, that run's second line.
|
|
799
|
-
*
|
|
800
|
-
* Measured over her own store, 2026-08-28, rebuilding all 267 rows the way the
|
|
801
|
-
* pane draws them: 123 have a gap, and on 56 of those the line was printed away
|
|
802
|
-
* from the seam, by as much as 30 messages. It sat under messages she could read
|
|
803
|
-
* saying some were missing, and where they had really gone there was nothing.
|
|
804
|
-
*
|
|
805
|
-
* Returns the index of the LAST event the opening kept, so work belonging to the
|
|
806
|
-
* opening stays above the line rather than below it, and -1 when nothing is
|
|
807
|
-
* missing.
|
|
438
|
+
* Where the gap line stands in a thread from `threadWindow`: the index of the
|
|
439
|
+
* last event the opening kept, or -1 when nothing is missing. It counts blocks,
|
|
440
|
+
* `same` continuations included, exactly as `threadWindow` cuts, which is why
|
|
441
|
+
* the renderer must not work this out itself.
|
|
808
442
|
*/
|
|
809
443
|
export function gapIndex(events, omitted, { opening = TRAIL_OPENING } = {}) {
|
|
810
444
|
if (!omitted) return -1;
|
|
@@ -814,13 +448,11 @@ export function gapIndex(events, omitted, { opening = TRAIL_OPENING } = {}) {
|
|
|
814
448
|
blocks += 1;
|
|
815
449
|
if (blocks === opening + 1) return i - 1;
|
|
816
450
|
}
|
|
817
|
-
// Every block on the screen belongs to the opening: the seam is the end of it.
|
|
818
451
|
return (events?.length ?? 0) - 1;
|
|
819
452
|
}
|
|
820
453
|
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
// WORK_TOTAL, oldest work first, because the newest is the part she came for.
|
|
454
|
+
// A run past WORK_RUN records how many it dropped in `more`. Past WORK_TOTAL
|
|
455
|
+
// the oldest work lines go and every message stays.
|
|
824
456
|
export function capRuns(events, { run = WORK_RUN, total = WORK_TOTAL } = {}) {
|
|
825
457
|
const out = [];
|
|
826
458
|
let inRun = 0;
|
|
@@ -840,7 +472,6 @@ export function capRuns(events, { run = WORK_RUN, total = WORK_TOTAL } = {}) {
|
|
|
840
472
|
|
|
841
473
|
const work = out.filter((e) => e.kind === 'work').length;
|
|
842
474
|
if (work <= total) return out;
|
|
843
|
-
// Drop the oldest work lines and leave every message standing.
|
|
844
475
|
let over = work - total;
|
|
845
476
|
const trimmed = [];
|
|
846
477
|
for (const e of out) {
|
|
@@ -850,19 +481,9 @@ export function capRuns(events, { run = WORK_RUN, total = WORK_TOTAL } = {}) {
|
|
|
850
481
|
return trimmed;
|
|
851
482
|
}
|
|
852
483
|
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
//
|
|
856
|
-
// But a person counting would say the agent sent ONE message, so the header
|
|
857
|
-
// that reads "40 messages, oldest first" counts messages and not blocks.
|
|
858
|
-
// Getting this wrong would put "128 messages" over a conversation she
|
|
859
|
-
// remembers having six times.
|
|
860
|
-
//
|
|
861
|
-
// The reader marks every block that is a continuation of the one above it, and
|
|
862
|
-
// it is the only thing that knows: a block is a continuation when nothing but
|
|
863
|
-
// work stands between it and the same side's last block, and when nothing
|
|
864
|
-
// ARRIVED in the gap. A message that arrived ends the answer above it even
|
|
865
|
-
// when it is never drawn, which is the whole of's first defect.
|
|
484
|
+
// Messages, not blocks: a reply split by tool calls draws as several blocks
|
|
485
|
+
// but counts once. The transcript reader sets `same` on each continuation
|
|
486
|
+
// block, and only it can tell.
|
|
866
487
|
export function saidCount(events) {
|
|
867
488
|
let n = 0;
|
|
868
489
|
for (const e of events ?? []) {
|
|
@@ -872,31 +493,14 @@ export function saidCount(events) {
|
|
|
872
493
|
return n;
|
|
873
494
|
}
|
|
874
495
|
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
// of the call still stuck to the end. On w-b108b1d596 the result that reached
|
|
880
|
-
// the pane ended "7,369 tests both times.</result>\n</invoke>" and was shown as
|
|
881
|
-
// is. Measured: dozens of field writes across dozens of rows carried it.
|
|
882
|
-
//
|
|
883
|
-
// IT ALSO BREAKS THE FOLD THAT SAYS ONE THING ONCE. That run noticed the leak
|
|
884
|
-
// and wrote its result again, clean. The fold keeps the FULLER of two copies of
|
|
885
|
-
// one answer, and the junk made the broken copy the fuller one, so the leak was
|
|
886
|
-
// what stayed on the screen and the good rewrite was the copy thrown away. That
|
|
887
|
-
// is what happened on that row, and it is why the cut happens before anything
|
|
888
|
-
// compares two messages.
|
|
889
|
-
//
|
|
890
|
-
// The cut runs from the first tag to the end, because nothing after one is
|
|
891
|
-
// prose: it is the rest of a call that was never meant to be said out loud. On
|
|
892
|
-
// that row it was a `note` parameter, which the next write then sent properly.
|
|
496
|
+
// A model writing a tool call sometimes emits the call's closing tags as plain
|
|
497
|
+
// text inside a field. Everything from the first tag on is cut. This must run
|
|
498
|
+
// before any comparison of two messages, because the fold keeps the fuller
|
|
499
|
+
// copy and the leaked junk would make the broken copy win.
|
|
893
500
|
const LEAKED = /<\/(?:antml:)?(?:result|invoke|parameter|function_calls)>|<(?:antml:)?parameter\s+name=/;
|
|
894
501
|
|
|
895
|
-
// A
|
|
896
|
-
//
|
|
897
|
-
// backticks, mid-paragraph, and a cut at the first one anywhere would have eaten
|
|
898
|
-
// the rest of her answer to make the example go away. So the spans in backticks,
|
|
899
|
-
// inline and fenced, are stepped over and only prose is cut.
|
|
502
|
+
// A tag inside backticks, inline or fenced, was typed on purpose and is not a
|
|
503
|
+
// leak, so code spans are skipped.
|
|
900
504
|
function firstLeak(text) {
|
|
901
505
|
const code = [];
|
|
902
506
|
let i = 0;
|