agentbox-app 0.1.13 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -9
- package/bin/agentbox.mjs +23 -1
- package/briefs/message-rules.md +36 -8
- package/briefs/worker.md +46 -23
- package/main/account-discovery.mjs +122 -30
- package/main/account-tooling.mjs +29 -1
- package/main/account-usage.mjs +46 -0
- package/main/agent-sessions.mjs +282 -268
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +12 -2
- package/main/answer-modes.mjs +103 -37
- package/main/approval-prompt-core.mjs +197 -0
- package/main/approval-prompt-server.mjs +4 -138
- package/main/approvals.mjs +101 -297
- package/main/artifact-path.mjs +47 -18
- package/main/claude-input.mjs +48 -4
- package/main/claude-plan.mjs +48 -0
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +18 -40
- package/main/cloud/account.mjs +179 -0
- package/main/cloud/aws.mjs +127 -0
- package/main/cloud/browser-return.mjs +58 -0
- package/main/cloud/clients.mjs +15 -0
- package/main/cloud/codex-cloud.mjs +53 -0
- package/main/cloud/config.mjs +107 -0
- package/main/cloud/docs.mjs +219 -0
- package/main/cloud/exec.mjs +27 -0
- package/main/cloud/host.mjs +165 -0
- package/main/cloud/kit.mjs +80 -0
- package/main/cloud/logins.mjs +543 -0
- package/main/cloud/managed.mjs +115 -0
- package/main/cloud/marker.mjs +104 -0
- package/main/cloud/org.mjs +53 -0
- package/main/cloud/reuse.mjs +208 -0
- package/main/cloud/run-cli.mjs +177 -0
- package/main/cloud/run.mjs +1934 -0
- package/main/cloud/usage.mjs +76 -0
- package/main/cloud-permission.mjs +51 -0
- package/main/codex-account.mjs +78 -0
- package/main/codex-app-server.mjs +276 -409
- package/main/codex-approvals.mjs +148 -433
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +212 -507
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +71 -3
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +17 -3
- package/main/codex.mjs +95 -363
- package/main/config.mjs +11 -1
- package/main/corner-tag.mjs +340 -19
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +232 -282
- package/main/engine-setup.mjs +21 -9
- package/main/feedback.mjs +3 -25
- package/main/folder-pictures.mjs +2 -2
- package/main/freeze-recorder-thread.mjs +86 -0
- package/main/freeze-recorder.mjs +146 -0
- package/main/fresh-user.mjs +20 -9
- package/main/git-change-offthread.mjs +17 -3
- package/main/git-change-thread.mjs +5 -2
- package/main/harnesses/claude.mjs +32 -11
- package/main/harnesses/codex.mjs +9 -2
- package/main/importable-threads.mjs +64 -6
- package/main/ipc.mjs +362 -111
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +199 -84
- package/main/memory-gate-history.mjs +36 -10
- package/main/memory-gate-server.mjs +62 -0
- package/main/memory-gate.mjs +14 -1
- package/main/menu.mjs +4 -2
- package/main/message-priority.mjs +2 -2
- package/main/mic.mjs +38 -0
- package/main/native/liquid-glass/build/liquid-glass.node +0 -0
- package/main/new-chats-watch.mjs +110 -0
- package/main/new-user-window.mjs +25 -0
- package/main/notify.mjs +24 -8
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +29 -9
- package/main/phone-uploads.mjs +476 -0
- package/main/picture-shape.mjs +114 -0
- package/main/quit-sequence.mjs +143 -0
- package/main/refresh-agent-models.mjs +15 -2
- package/main/relay-client.mjs +69 -1
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +12 -41
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +45 -3
- package/main/settings.mjs +220 -44
- package/main/shell-path.mjs +185 -37
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +115 -19
- package/main/source-updater.mjs +18 -10
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/store/home.mjs +87 -232
- package/main/store/paths.mjs +11 -0
- package/main/store/teammates-files.mjs +99 -0
- package/main/store/work-items.mjs +231 -13
- package/main/store.mjs +226 -52
- package/main/supervisor.mjs +3103 -341
- package/main/task-folders.mjs +430 -30
- package/main/task-terminals.mjs +110 -34
- package/main/team/index.mjs +114 -10
- package/main/team/memory-cloud.mjs +183 -1
- package/main/team/room-hours.mjs +48 -0
- package/main/team/room-setup.mjs +31 -0
- package/main/team/rooms-memory.mjs +454 -0
- package/main/team/rooms-shape.mjs +42 -0
- package/main/team/rooms-supabase.mjs +324 -0
- package/main/team/rooms.mjs +575 -0
- package/main/team/session.mjs +96 -26
- package/main/team/supabase-backend.mjs +180 -0
- package/main/team/sync.mjs +109 -1
- package/main/teammates.mjs +580 -0
- package/main/terminal-host.mjs +143 -0
- package/main/terminal-hosts.mjs +261 -0
- package/main/transcript-search.mjs +80 -0
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +90 -219
- package/mcp/core/documents.mjs +3 -1
- package/mcp/core/repeats.mjs +88 -0
- package/mcp/core/teammates.mjs +222 -0
- package/mcp/core/work.mjs +76 -4
- package/mcp/stdio.mjs +24 -11
- package/mcp/tools.mjs +116 -8
- package/package.json +21 -5
- package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
- package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
- package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
- package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
- package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
- package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
- package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
- package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
- package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
- package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
- package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
- package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
- package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
- package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
- package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
- package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
- package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
- package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
- package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
- package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
- package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
- package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
- package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
- package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
- package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
- package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
- package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
- package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
- package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
- package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
- package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
- package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
- package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
- package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
- package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
- package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
- package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
- package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
- package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
- package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
- package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
- package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
- package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
- package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
- package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
- package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
- package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
- package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
- package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
- package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
- package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
- package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
- package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
- package/renderer/dist/corner-tag.html +3 -3
- package/renderer/dist/index.html +3 -3
- package/scripts/after-install.mjs +30 -8
- package/scripts/cloud-shim.pl +57 -0
- package/shared/account-names.d.mts +6 -0
- package/shared/account-names.mjs +15 -0
- package/shared/agent-hours.mjs +136 -0
- package/shared/agent-import.mjs +60 -34
- package/shared/agents.mjs +126 -522
- package/shared/analytics-events.mjs +12 -0
- package/shared/approval-card.mjs +36 -0
- package/shared/bridge-map.mjs +27 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-usage.d.mts +1 -0
- package/shared/claude-usage.mjs +42 -1
- package/shared/cloud-hand-back.d.mts +7 -0
- package/shared/cloud-hand-back.mjs +20 -0
- package/shared/cloud-kit.d.mts +219 -0
- package/shared/cloud-kit.mjs +341 -0
- package/shared/cloud-login-name.mjs +4 -0
- package/shared/cloud-start.mjs +17 -0
- package/shared/cloud-versions.mjs +23 -0
- package/shared/codex-import.mjs +2 -2
- package/shared/codex-modes.d.mts +22 -9
- package/shared/codex-modes.mjs +67 -11
- package/shared/codex-usage.mjs +108 -2
- package/shared/corner-tag.d.mts +23 -1
- package/shared/corner-tag.mjs +284 -29
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +70 -309
- package/shared/feedback.d.mts +7 -0
- package/shared/feedback.mjs +25 -0
- package/shared/first-run-practice.mjs +17 -24
- package/shared/harness-definitions.mjs +8 -0
- package/shared/instruction-defaults.json +2 -2
- package/shared/key-caps.d.mts +14 -0
- package/shared/key-caps.mjs +68 -0
- package/shared/message-edits.d.mts +7 -0
- package/shared/message-edits.mjs +35 -0
- package/shared/move-thread.d.mts +24 -0
- package/shared/move-thread.mjs +112 -0
- package/shared/named-threads.mjs +72 -0
- package/shared/not-shipped.d.mts +22 -0
- package/shared/not-shipped.mjs +89 -0
- package/shared/notify-rules.d.mts +6 -0
- package/shared/notify-rules.mjs +36 -8
- package/shared/phone-files.mjs +28 -0
- package/shared/phone-settings.mjs +118 -0
- package/shared/phone-snapshot.d.mts +16 -0
- package/shared/phone-snapshot.mjs +348 -0
- package/shared/picture-formats.d.mts +1 -0
- package/shared/picture-formats.mjs +13 -0
- package/shared/rank.d.mts +2 -1
- package/shared/rank.mjs +32 -19
- package/shared/repeats.d.mts +8 -0
- package/shared/repeats.mjs +33 -2
- package/shared/room-cards.mjs +167 -0
- package/shared/room-links.d.mts +4 -0
- package/shared/room-links.mjs +21 -0
- package/shared/runs-on.d.mts +3 -3
- package/shared/runs-on.mjs +8 -5
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +14 -6
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +443 -40
- package/shared/team-rules.mjs +9 -2
- package/shared/teammates.d.mts +19 -0
- package/shared/teammates.mjs +756 -0
- package/shared/terminal-focus.d.mts +1 -0
- package/shared/terminal-focus.mjs +13 -0
- package/shared/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +9 -0
- package/shared/usage.mjs +49 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/work-items.mjs +127 -30
- package/shared/work-lines.mjs +92 -244
- package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
- package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
- package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function terminalKeys(options?: { startOpen?: boolean; wasOpen?: boolean }): { takesKeys(revision?: number): boolean; hidden(): void };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Whether a terminal takes the keyboard when TaskTerminal builds it. Opening
|
|
2
|
+
// it, showing it again, Retry and New session do. Coming back to a thread that
|
|
3
|
+
// left its terminal open does not: xterm's input is a textarea, and the app's
|
|
4
|
+
// shortcuts stand aside for a textarea. `startOpen` is a page that is only a
|
|
5
|
+
// terminal, which always takes it.
|
|
6
|
+
// Pinned by tests/coming-back-to-an-open-terminal-leaves-the-keyboard-alone.test.mjs.
|
|
7
|
+
export function terminalKeys({ startOpen = false, wasOpen = false } = {}) {
|
|
8
|
+
let cameBack = wasOpen === true && !startOpen;
|
|
9
|
+
return {
|
|
10
|
+
takesKeys: (revision = 0) => !cameBack || revision > 0,
|
|
11
|
+
hidden: () => { cameBack = false; },
|
|
12
|
+
};
|
|
13
|
+
}
|
package/shared/thread-cards.mjs
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
// private thread publishes no card at all (decided 2026-10-01
|
|
17
17
|
// from user research): not a wordless one, not a count.
|
|
18
18
|
|
|
19
|
+
import { shownPriority } from './rank.mjs';
|
|
20
|
+
|
|
19
21
|
export const THREAD_STATES = ['waiting', 'running', 'scheduled', 'done'];
|
|
20
22
|
|
|
21
23
|
/** Which of the four states a thread is in. */
|
|
@@ -245,7 +247,8 @@ export function cardsFor({ products, readItems, now = Date.now(), since = null }
|
|
|
245
247
|
// team. Never an empty list, which would have to mean both. Both clouds
|
|
246
248
|
// carry it and refuse to show the card to anybody else.
|
|
247
249
|
people: shownToPeople(item, product).length ? shownToPeople(item, product) : null,
|
|
248
|
-
|
|
250
|
+
// The level it runs at, so a teammate reads what the owner reads.
|
|
251
|
+
state, priority: shownPriority(item),
|
|
249
252
|
problem: s.problem || null, progress: s.progress || null, solution: s.solution || null,
|
|
250
253
|
blockedBy: linked(item.blockedBy), blocks: linked(item.blocks), updatedAt: item.updatedAt ?? now,
|
|
251
254
|
});
|
package/shared/usage.d.mts
CHANGED
|
@@ -42,8 +42,17 @@ export declare function sessionLimit(limits: UsageLimit[] | null | undefined): U
|
|
|
42
42
|
export declare function headlineLimit(limits: UsageLimit[] | null | undefined): UsageLimit | null;
|
|
43
43
|
export declare const ANY_MOMENT: string;
|
|
44
44
|
export declare function spanLeft(ms: number): string;
|
|
45
|
+
export declare const toMinute: (at: number) => number;
|
|
46
|
+
export declare function resetDay(at: number, now?: number): string | null;
|
|
45
47
|
export declare function untilReset(ms: number): string;
|
|
46
48
|
|
|
49
|
+
/** The windows a reading can still answer for: one whose reset has gone past
|
|
50
|
+
* measures a window nobody is in any more. */
|
|
51
|
+
export declare function limitsStillRunning(
|
|
52
|
+
limits: UsageLimit[] | null | undefined,
|
|
53
|
+
now?: number,
|
|
54
|
+
): UsageLimit[];
|
|
55
|
+
|
|
47
56
|
export declare function limitRows(
|
|
48
57
|
limits: UsageLimit[] | null | undefined,
|
|
49
58
|
now?: number,
|
package/shared/usage.mjs
CHANGED
|
@@ -234,6 +234,51 @@ export function spanLeft(ms) {
|
|
|
234
234
|
return `${hours}h ${minutes % 60}m`;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* HOW LONG A WINDOW GOES ON BEING DRAWN AFTER ITS RESET, so a countdown
|
|
239
|
+
* crossing zero does not flicker out of the panel a minute before the next
|
|
240
|
+
* reading lands. Inside it "any moment" is what it says: the reset is here.
|
|
241
|
+
*/
|
|
242
|
+
const JUST_RESET_MS = 2 * 60_000;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* THE WINDOWS A READING CAN STILL ANSWER FOR (w-7b7182ef61).
|
|
246
|
+
*
|
|
247
|
+
* A window whose reset has gone past is over, and the one running in its place
|
|
248
|
+
* has never been read. shared/codex-usage.mjs already holds the rule for that:
|
|
249
|
+
* absence of data and a real zero are different facts and must never look the
|
|
250
|
+
* same. So it is DROPPED rather than relabelled -- no wording makes a
|
|
251
|
+
* percentage about a window nobody is in worth reading -- and the corner falls
|
|
252
|
+
* back to whatever the reading can still answer for (`headlineLimit`).
|
|
253
|
+
*
|
|
254
|
+
* A RESET WE COULD NOT PLACE ON A CLOCK KEEPS ITS ROW. `resetsAt` is null
|
|
255
|
+
* whenever Claude Code printed its stamp in a timezone we could not honestly
|
|
256
|
+
* convert, and a limit we cannot time is not a limit we may call finished.
|
|
257
|
+
*/
|
|
258
|
+
export function limitsStillRunning(limits, now = Date.now()) {
|
|
259
|
+
return (limits ?? []).filter((l) => !Number.isFinite(l?.resetsAt) || l.resetsAt + JUST_RESET_MS > now);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* A reset to the nearest minute. Claude Code gives one a moment before the
|
|
264
|
+
* minute (11:59:59.6) and the cloud's header the minute itself, and both name
|
|
265
|
+
* the same reset.
|
|
266
|
+
*/
|
|
267
|
+
export const toMinute = (at) => Math.round(at / 60_000) * 60_000;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* THE DAY A RESET FALLS ON, in this machine's calendar: null today, "Thu" this
|
|
271
|
+
* week, and "Tue Oct 27" six or more days out, where a weekday alone would
|
|
272
|
+
* read as today.
|
|
273
|
+
*/
|
|
274
|
+
export function resetDay(at, now = Date.now()) {
|
|
275
|
+
const d = new Date(toMinute(at));
|
|
276
|
+
if (d.toDateString() === new Date(now).toDateString()) return null;
|
|
277
|
+
const weekday = d.toLocaleDateString('en-US', { weekday: 'short' });
|
|
278
|
+
if (at - now < 6 * 86_400_000) return weekday;
|
|
279
|
+
return `${weekday} ${d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
237
282
|
/* -------------------------------- the panel ------------------------------- */
|
|
238
283
|
|
|
239
284
|
/**
|
|
@@ -281,7 +326,7 @@ export function spanLeft(ms) {
|
|
|
281
326
|
*/
|
|
282
327
|
export function limitRows(limits, now = Date.now()) {
|
|
283
328
|
const here = localZone();
|
|
284
|
-
return (limits
|
|
329
|
+
return limitsStillRunning(limits, now).map((l) => ({
|
|
285
330
|
// Stable across redraws, and it has to carry the qualifier: Claude Code's
|
|
286
331
|
// two week rows are otherwise the same string.
|
|
287
332
|
key: `${l.span}:${l.qualifier ?? ''}`,
|
|
@@ -316,8 +361,9 @@ function whenItStartsOver(l, now, here) {
|
|
|
316
361
|
* 2026-09-01.
|
|
317
362
|
*/
|
|
318
363
|
export function usageSentence(limits, now = Date.now(), engineWord = null) {
|
|
319
|
-
|
|
320
|
-
|
|
364
|
+
const live = limitsStillRunning(limits, now);
|
|
365
|
+
if (!live.length) return null;
|
|
366
|
+
const said = live.map((l) => {
|
|
321
367
|
const when = l.resetsAt ? `, ${untilReset(l.resetsAt - now)}` : l.resetsText ? `, resets ${l.resetsText}` : '';
|
|
322
368
|
return `${l.name}: ${l.percent}% used${when}.`;
|
|
323
369
|
}).join(' ');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Types for shared/websites.mjs, the rules the main process and the screen
|
|
2
|
+
// share about a page off the open web: which addresses are one, what the
|
|
3
|
+
// pane's header calls it, and where its sign-ins go.
|
|
4
|
+
|
|
5
|
+
/** The session partition a website's cookies and sign-ins live in. */
|
|
6
|
+
export const WEBSITE_PARTITION: string;
|
|
7
|
+
|
|
8
|
+
/** Is this an ordinary page on the web, rather than a local app or a file? */
|
|
9
|
+
export function isWebsiteAddress(src: string | undefined | null): boolean;
|
|
10
|
+
|
|
11
|
+
/** What an address typed into the pane's bar means, with its scheme, or null. */
|
|
12
|
+
export function addressYouTyped(typed: string | undefined | null): string | null;
|
|
13
|
+
|
|
14
|
+
/** Can this page embed a website in a browser of its own (Electron only)? */
|
|
15
|
+
export function canFrameAWebsite(doc: { createElement(tag: string): unknown } | null | undefined): boolean;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// PURE. What counts as a website, and where its sign-ins live. Shared because
|
|
2
|
+
// the main process pins what an embedded page may do and the screen decides
|
|
3
|
+
// what to draw, and the two must agree.
|
|
4
|
+
//
|
|
5
|
+
// A local address is NOT a website here. A dev server is drawn in an ordinary
|
|
6
|
+
// frame with a liveness check in front of it (renderer/src/local-preview.ts)
|
|
7
|
+
// and its framing headers rewritten below the page (main/local-app-frames.mjs);
|
|
8
|
+
// none of that applies to a site on the web.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The store a website's cookies and sign-ins go into. Not `persist:junk`:
|
|
12
|
+
* that one is safe for an agent to drive precisely because it carries
|
|
13
|
+
* nothing, and a person may sign into a page here.
|
|
14
|
+
*/
|
|
15
|
+
export const WEBSITE_PARTITION = 'persist:websites';
|
|
16
|
+
|
|
17
|
+
const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
|
|
18
|
+
|
|
19
|
+
/** Is this an ordinary page on the web, the kind the pane opens in a browser? */
|
|
20
|
+
export function isWebsiteAddress(src) {
|
|
21
|
+
if (typeof src !== 'string' || !src) return false;
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(src);
|
|
24
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
|
|
25
|
+
return !LOOPBACK.has(url.hostname);
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* What an address typed into the pane's bar means: a page on the web or a
|
|
33
|
+
* local app, with the scheme a browser would add, or null. Never a search,
|
|
34
|
+
* which would send what you typed to somebody else.
|
|
35
|
+
*/
|
|
36
|
+
export function addressYouTyped(typed) {
|
|
37
|
+
const text = typeof typed === 'string' ? typed.trim() : '';
|
|
38
|
+
if (!text || /\s/.test(text)) return null;
|
|
39
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(text);
|
|
40
|
+
const bareHost = text.split(/[/?#]/)[0].replace(/:\d+$/, '').toLowerCase();
|
|
41
|
+
const full = hasScheme ? text : `${LOOPBACK.has(bareHost) ? 'http' : 'https'}://${text}`;
|
|
42
|
+
let url;
|
|
43
|
+
try { url = new URL(full); } catch { return null; }
|
|
44
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
|
|
45
|
+
if (url.username || url.password) return null;
|
|
46
|
+
if (!LOOPBACK.has(url.hostname) && !url.hostname.includes('.')) return null;
|
|
47
|
+
return url.href;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Can this page embed a website in a browser of its own? Only the desktop
|
|
52
|
+
* window can: `<webview>` is Electron's, and nothing registers the tag in an
|
|
53
|
+
* ordinary tab or on a phone. Asking the tag beats guessing which door this is.
|
|
54
|
+
*/
|
|
55
|
+
export function canFrameAWebsite(doc) {
|
|
56
|
+
try {
|
|
57
|
+
return !!doc && 'partition' in doc.createElement('webview');
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/shared/work-items.mjs
CHANGED
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
// epoch, and neither erases the other. Same rule as beats in
|
|
33
33
|
// shared/dashboard.mjs.
|
|
34
34
|
|
|
35
|
+
import { resolveEdits } from './message-edits.mjs';
|
|
36
|
+
|
|
35
37
|
/* ------------------------------- lifecycle ------------------------------- */
|
|
36
38
|
// The only states the claim machinery reasons about. Everything else about what
|
|
37
39
|
// a work item is (kind, labels) is free-form text the layer above interprets.
|
|
@@ -155,6 +157,16 @@ export const WORK_ITEM_FIELDS = [
|
|
|
155
157
|
// mark on ONE line rather than a value the row holds, so the fold skips it;
|
|
156
158
|
// the chat reads it per line (renderer/src/thread-history.ts).
|
|
157
159
|
'inReplyTo',
|
|
160
|
+
// AN EDIT OR A DELETION OF ONE MESSAGE: `{ on, text }` or `{ on, gone: true }`,
|
|
161
|
+
// where `on` is the message's line uid. A delta like `react`, read back as
|
|
162
|
+
// `edits` and only from the message's own author (shared/message-edits.mjs).
|
|
163
|
+
'edit',
|
|
164
|
+
// WHICH COMMAND FROM HER PHONE WROTE THIS LINE (docs/cloud/PHONE-PLAN.md):
|
|
165
|
+
// the command's id, on the line of the reply or the new thread it made. It
|
|
166
|
+
// is how the computer, starting again after stopping half way through a
|
|
167
|
+
// command, knows for certain whether that command already landed, rather
|
|
168
|
+
// than guessing from the words (Codex's third review of step 1, finding 5).
|
|
169
|
+
'fromPhone',
|
|
158
170
|
];
|
|
159
171
|
|
|
160
172
|
// THE SUMMARY IS SHARED BETWEEN THE PERSON AND THE AGENT. Everywhere else the
|
|
@@ -396,7 +408,7 @@ function coerceField(field, value) {
|
|
|
396
408
|
// shared/effort-levels.mjs, and the spawn gates on them, so a value this
|
|
397
409
|
// module does not recognise is a run at Claude Code's own choice rather
|
|
398
410
|
// than a row that never runs.
|
|
399
|
-
case 'kind': case 'product': case 'parent':
|
|
411
|
+
case 'kind': case 'product': case 'parent': return str(value, MAX_SHORT);
|
|
400
412
|
// THESE TWO ARE CLEARED BY WRITING '' AND SO '' IS A REAL VALUE, exactly
|
|
401
413
|
// as runAt's 0 and start's 'now' are, and for the same reason: a field that
|
|
402
414
|
// coerces to undefined is dropped by the fold, so without a falsy value
|
|
@@ -412,7 +424,12 @@ function coerceField(field, value) {
|
|
|
412
424
|
// picking a model and pressing send lost the message, with nothing on
|
|
413
425
|
// screen to say so. Five of the 14 crashes since the Oct 6 launch, on two
|
|
414
426
|
// installs (PostHog, 2026-10-08).
|
|
415
|
-
|
|
427
|
+
//
|
|
428
|
+
// `engine` JOINED THEM WHEN A REPLY COULD FIRST CARRY ONE (w-c9da359466).
|
|
429
|
+
// It is written beside the model by the same call, so clearing it back to
|
|
430
|
+
// the workspace default would have thrown in exactly the place described
|
|
431
|
+
// above, before the answer was written, losing the message.
|
|
432
|
+
case 'model': case 'effort': case 'engine': return clearable(value, MAX_SHORT);
|
|
416
433
|
case 'status': return WORK_ITEM_STATUSES.includes(value) ? value : undefined;
|
|
417
434
|
case 'priority': return Number.isFinite(value) ? Math.trunc(value) : undefined;
|
|
418
435
|
// When this item may next be acted on, in epoch ms. 0 means unscheduled,
|
|
@@ -440,10 +457,11 @@ function coerceField(field, value) {
|
|
|
440
457
|
// A ts rather than a flag, so her NEXT answer outdates it by carrying a
|
|
441
458
|
// later one, with nothing to clear and nobody who has to remember to.
|
|
442
459
|
case 'answeredThrough': return Number.isFinite(value) ? Math.trunc(value) : undefined;
|
|
460
|
+
// An empty list is a real value: it is how the last label, `ship` above
|
|
461
|
+
// all, comes off.
|
|
443
462
|
case 'labels': {
|
|
444
463
|
if (!Array.isArray(value)) return undefined;
|
|
445
|
-
|
|
446
|
-
return labels.length ? labels : undefined;
|
|
464
|
+
return value.map((l) => str(l, MAX_SHORT)).filter(Boolean).slice(0, MAX_LABELS);
|
|
447
465
|
}
|
|
448
466
|
case 'assignee': case 'runner': return shortId(value) ?? undefined;
|
|
449
467
|
case 'due': return calendarDay(value);
|
|
@@ -464,6 +482,15 @@ function coerceField(field, value) {
|
|
|
464
482
|
return value.off === true ? { on, emoji, off: true } : { on, emoji };
|
|
465
483
|
}
|
|
466
484
|
case 'inReplyTo': return shortId(value) ?? undefined;
|
|
485
|
+
case 'edit': {
|
|
486
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
487
|
+
const on = shortId(value.on);
|
|
488
|
+
if (!on) return undefined;
|
|
489
|
+
if (value.gone === true) return { on, gone: true };
|
|
490
|
+
const text = str(value.text, MAX_TEXT_BYTES);
|
|
491
|
+
return text ? { on, text } : undefined;
|
|
492
|
+
}
|
|
493
|
+
case 'fromPhone': return typeof value === 'string' && /^[0-9a-f-]{8,64}$/i.test(value) ? value : undefined;
|
|
467
494
|
case 'visibility': return value === 'private' || value === 'team' || value === 'people' ? value : undefined;
|
|
468
495
|
// An empty list is a real value: it is how the last chosen person comes
|
|
469
496
|
// off a thread, and the thread then reaches nobody until she names one.
|
|
@@ -521,12 +548,67 @@ function rollUpReactions(presses) {
|
|
|
521
548
|
return out;
|
|
522
549
|
}
|
|
523
550
|
|
|
524
|
-
|
|
525
|
-
|
|
551
|
+
// Lease expiry is computed at READ time, never written. A held claim whose
|
|
552
|
+
// lease has lapsed reads as open again, which is what makes an abandoned item
|
|
553
|
+
// recoverable without anyone having to notice it was abandoned.
|
|
554
|
+
//
|
|
555
|
+
// The same derivation covers a released claim, and that is the point: only an
|
|
556
|
+
// item still sitting at `claimed` with nobody holding it goes back to open.
|
|
557
|
+
// Any status a worker or the founder actually chose (done, blocked, or one we
|
|
558
|
+
// have not invented yet) is theirs, and nothing about giving a claim back
|
|
559
|
+
// overwrites it.
|
|
560
|
+
export function finishItem(raw, now = Date.now()) {
|
|
561
|
+
// A copy, one level deep, so the raw item can be folded on and a finished
|
|
562
|
+
// one handed out is never changed under its holder.
|
|
563
|
+
const item = {};
|
|
564
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
565
|
+
item[k] = Array.isArray(v) ? [...v]
|
|
566
|
+
: v instanceof Map ? new Map(v)
|
|
567
|
+
: v instanceof Set ? new Set(v)
|
|
568
|
+
: v && Object.getPrototypeOf(v) === Object.prototype ? { ...v } : v;
|
|
569
|
+
}
|
|
570
|
+
item.claimExpired = !!(item.claim && item.claim.leaseUntil < now);
|
|
571
|
+
if (item.status === 'claimed' && (!item.claim || item.claimExpired)) item.status = 'open';
|
|
572
|
+
if (item.reacts) {
|
|
573
|
+
const chips = rollUpReactions(item.reacts);
|
|
574
|
+
delete item.reacts;
|
|
575
|
+
if (chips) item.reactions = chips;
|
|
576
|
+
}
|
|
577
|
+
const closes = item.closes;
|
|
578
|
+
delete item.closes;
|
|
579
|
+
const editLines = item.editLines;
|
|
580
|
+
delete item.editLines;
|
|
581
|
+
const edits = editLines ? resolveEdits(item.said ?? [], editLines) : null;
|
|
582
|
+
if (edits?.size) item.edits = Object.fromEntries(edits);
|
|
583
|
+
if (item.said) {
|
|
584
|
+
const said = edits?.size ? item.said.flatMap((s) => {
|
|
585
|
+
const e = s.uid ? edits.get(s.uid) : null;
|
|
586
|
+
if (!e) return [s];
|
|
587
|
+
return e.gone ? [] : [{ ...s, text: e.text.trim().slice(0, TALK_TEXT) }];
|
|
588
|
+
}) : item.said;
|
|
589
|
+
delete item.said;
|
|
590
|
+
if (Array.isArray(item.people) && item.people.length) {
|
|
591
|
+
item.talk = talkOf(said);
|
|
592
|
+
const settled = settledBy(closes ?? [], said);
|
|
593
|
+
if (settled) item.talk.settled = settled;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return item;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** A line that writes the row's short name and nothing else (see the fold's foot). */
|
|
600
|
+
function onlyTheName(line) {
|
|
601
|
+
if (line.claim || line.release || line.heartbeat) return false;
|
|
602
|
+
const fields = Object.keys(line.patch ?? {});
|
|
603
|
+
return fields.length === 1 && fields[0] === 'label';
|
|
604
|
+
}
|
|
526
605
|
|
|
606
|
+
/** Lines folded onto `items` in place, unfinished: `finishItem` makes each one readable. */
|
|
607
|
+
export function foldRawInto(items, lines, touched = null) {
|
|
527
608
|
for (const raw of Array.isArray(lines) ? lines : []) {
|
|
528
609
|
const line = normalizeLine(raw);
|
|
529
610
|
if (!line) continue;
|
|
611
|
+
touched?.add(line.id);
|
|
530
612
|
|
|
531
613
|
let item = items.get(line.id);
|
|
532
614
|
if (!item) {
|
|
@@ -584,6 +666,11 @@ export function foldWorkItems(lines, now = Date.now()) {
|
|
|
584
666
|
if (line.by && words && words.trim()) {
|
|
585
667
|
(item.said ??= []).push({ uid: line.uid ?? null, by: line.by, ts: line.ts, text: words.trim().slice(0, TALK_TEXT), inReplyTo: patch.inReplyTo ?? null, body: typeof patch.answer !== 'string' });
|
|
586
668
|
}
|
|
669
|
+
// Who closed the chat and when, so `whatWaits` can tell a reopen by your own
|
|
670
|
+
// message (still settled) from an undo of the close (not).
|
|
671
|
+
if (line.by && typeof patch.status === 'string') {
|
|
672
|
+
(item.closes ??= []).push({ by: line.by, ts: line.ts, done: patch.status === 'done', spoke: !!(words && words.trim()) });
|
|
673
|
+
}
|
|
587
674
|
|
|
588
675
|
for (const [field, value] of Object.entries(patch)) {
|
|
589
676
|
// A REACTION IS ACCUMULATED, NOT HELD. Every other field below keeps one
|
|
@@ -605,6 +692,10 @@ export function foldWorkItems(lines, now = Date.now()) {
|
|
|
605
692
|
// WHICH MESSAGE A REPLY ANSWERS belongs to that reply's line alone. Held
|
|
606
693
|
// as a value, the row would carry the newest reply's parent forever.
|
|
607
694
|
if (field === 'inReplyTo') continue;
|
|
695
|
+
if (field === 'edit') {
|
|
696
|
+
(item.editLines ??= []).push({ on: value.on, by: line.by ?? null, ts: line.ts, ...(value.gone ? { gone: true } : { text: value.text }) });
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
608
699
|
const held = item.wrote[field];
|
|
609
700
|
// There used to be an exception here, for machinery spending a one-off
|
|
610
701
|
// permission mode over the founder's own grant. That field is not on the
|
|
@@ -617,34 +708,23 @@ export function foldWorkItems(lines, now = Date.now()) {
|
|
|
617
708
|
if (line.by) item.wrote[field].by = line.by;
|
|
618
709
|
}
|
|
619
710
|
|
|
620
|
-
|
|
711
|
+
// THE APP WRITING A ROW'S NAME IS NOT NEWS ABOUT THE ROW. Lists break ties
|
|
712
|
+
// on `updatedAt`, so every unnamed thread the namer reached climbed to the
|
|
713
|
+
// top reading "updated just now". Read here rather than at the write, as
|
|
714
|
+
// the heartbeat rule above is, because the lines that did it are in the
|
|
715
|
+
// ledger already. Only the name ALONE: a line that carries it with anything
|
|
716
|
+
// else carries that too.
|
|
717
|
+
if (!onlyTheName(line)) item.updatedAt = Math.max(item.updatedAt, line.ts);
|
|
621
718
|
if (line.ts < item.createdAt) item.createdAt = line.ts;
|
|
622
719
|
}
|
|
623
720
|
|
|
624
|
-
|
|
625
|
-
// lease has lapsed reads as open again, which is what makes an abandoned item
|
|
626
|
-
// recoverable without anyone having to notice it was abandoned.
|
|
627
|
-
//
|
|
628
|
-
// The same derivation covers a released claim, and that is the point: only an
|
|
629
|
-
// item still sitting at `claimed` with nobody holding it goes back to open.
|
|
630
|
-
// Any status a worker or the founder actually chose (done, blocked, or one we
|
|
631
|
-
// have not invented yet) is theirs, and nothing about giving a claim back
|
|
632
|
-
// overwrites it.
|
|
633
|
-
for (const item of items.values()) {
|
|
634
|
-
item.claimExpired = !!(item.claim && item.claim.leaseUntil < now);
|
|
635
|
-
if (item.status === 'claimed' && (!item.claim || item.claimExpired)) item.status = 'open';
|
|
636
|
-
if (item.reacts) {
|
|
637
|
-
const chips = rollUpReactions(item.reacts);
|
|
638
|
-
delete item.reacts;
|
|
639
|
-
if (chips) item.reactions = chips;
|
|
640
|
-
}
|
|
641
|
-
if (item.said) {
|
|
642
|
-
const said = item.said;
|
|
643
|
-
delete item.said;
|
|
644
|
-
if (Array.isArray(item.people) && item.people.length) item.talk = talkOf(said);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
721
|
+
}
|
|
647
722
|
|
|
723
|
+
export function foldWorkItems(lines, now = Date.now()) {
|
|
724
|
+
const raw = new Map();
|
|
725
|
+
foldRawInto(raw, lines);
|
|
726
|
+
const items = new Map();
|
|
727
|
+
for (const [id, item] of raw) items.set(id, finishItem(item, now));
|
|
648
728
|
return items;
|
|
649
729
|
}
|
|
650
730
|
|
|
@@ -664,6 +744,23 @@ const TALK_KEEP = 100;
|
|
|
664
744
|
* `whatWaits` (shared/team-rules.mjs) and the row's one line; the messages
|
|
665
745
|
* themselves are read off the ledger by the thread view, as before.
|
|
666
746
|
*/
|
|
747
|
+
/**
|
|
748
|
+
* For each person, the moment they last marked the chat done, or nothing if
|
|
749
|
+
* they reopened it since without saying anything (an undo). A reopen that
|
|
750
|
+
* carries their words keeps the moment: speaking is not taking the close back.
|
|
751
|
+
* So does one that follows their words since the close, because sending
|
|
752
|
+
* writes the message and the reopen as two lines (`answerItem`, main/store.mjs).
|
|
753
|
+
*/
|
|
754
|
+
function settledBy(closes, said = []) {
|
|
755
|
+
const at = {};
|
|
756
|
+
const spokeSince = (by, from, to) => said.some((s) => s.by === by && !s.body && s.ts > from && s.ts <= to);
|
|
757
|
+
for (const c of [...closes].sort((a, b) => a.ts - b.ts)) {
|
|
758
|
+
if (c.done) at[c.by] = c.ts;
|
|
759
|
+
else if (!c.spoke && !spokeSince(c.by, at[c.by], c.ts)) delete at[c.by];
|
|
760
|
+
}
|
|
761
|
+
return Object.keys(at).length ? at : null;
|
|
762
|
+
}
|
|
763
|
+
|
|
667
764
|
function talkOf(said) {
|
|
668
765
|
const lines = [...said].sort((a, b) => a.ts - b.ts);
|
|
669
766
|
const chat = [];
|