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/codex-usage.mjs
CHANGED
|
@@ -125,6 +125,7 @@ export function windowSpan(minutes) {
|
|
|
125
125
|
*/
|
|
126
126
|
export function codexLimits(snapshot) {
|
|
127
127
|
const out = [];
|
|
128
|
+
const refused = reachedWindow(snapshot);
|
|
128
129
|
for (const key of WINDOWS) {
|
|
129
130
|
const window = snapshot?.[key];
|
|
130
131
|
if (!plain(window)) continue;
|
|
@@ -133,11 +134,18 @@ export function codexLimits(snapshot) {
|
|
|
133
134
|
const minutes = field(window, 'windowDurationMins', 'window_minutes');
|
|
134
135
|
const [span, name] = windowSpan(minutes) ?? FALLBACK[key];
|
|
135
136
|
const resets = field(window, 'resetsAt', 'resets_at');
|
|
137
|
+
const reached = refused === key;
|
|
136
138
|
out.push({
|
|
137
139
|
span,
|
|
138
140
|
qualifier: null,
|
|
139
141
|
name,
|
|
140
|
-
percent:
|
|
142
|
+
percent: drawnPercent(percent),
|
|
143
|
+
// WHETHER THIS WINDOW IS ACTUALLY FINISHED, which a percentage cannot say
|
|
144
|
+
// (w-36c51443df). Only the provider knows: a window can be refused below
|
|
145
|
+
// a hundred by a spend control or a per-account limit, and one sitting at
|
|
146
|
+
// a hundred may still be serving. So this is read off the payload and
|
|
147
|
+
// never derived from `percent`.
|
|
148
|
+
reached,
|
|
141
149
|
resetsText: null,
|
|
142
150
|
resetsOn: null,
|
|
143
151
|
resetsAt: resets === null ? null : resets * 1000,
|
|
@@ -147,6 +155,78 @@ export function codexLimits(snapshot) {
|
|
|
147
155
|
return out;
|
|
148
156
|
}
|
|
149
157
|
|
|
158
|
+
/**
|
|
159
|
+
* WHICH WINDOW THE PROVIDER SAYS IS SPENT, or null when it says nothing.
|
|
160
|
+
*
|
|
161
|
+
* `rateLimitReachedType` NAMES a window (`primary` / `secondary`), measured on a
|
|
162
|
+
* live push 2026-10-09, so it belongs on that window's own line rather than on
|
|
163
|
+
* the reading as a whole. A name for a window that is not in the payload is
|
|
164
|
+
* ignored rather than guessed at: marking a line we were not told about is how
|
|
165
|
+
* a confident wrong number gets drawn.
|
|
166
|
+
*/
|
|
167
|
+
function reachedWindow(snapshot) {
|
|
168
|
+
const named = snapshot?.rateLimitReachedType ?? snapshot?.rate_limit_reached_type;
|
|
169
|
+
return WINDOWS.includes(named) ? named : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* THE FIGURE TO DRAW, WHICH MAY NOT MANUFACTURE A HUNDRED (w-36c51443df).
|
|
174
|
+
*
|
|
175
|
+
* `Math.round` drew 99.6% as "100%", so the one number on the screen that
|
|
176
|
+
* should have meant "stop" meant "nearly". A reading nobody can act on is worse
|
|
177
|
+
* than a blunter one, so anything short of the whole window draws at most 99
|
|
178
|
+
* and 100 means a hundred.
|
|
179
|
+
*
|
|
180
|
+
* AND `reached` IS DELIBERATELY NOT AN INPUT HERE. The first draft of this fix
|
|
181
|
+
* forced 100 whenever the provider refused the window, which is the same sin
|
|
182
|
+
* one field along: a window capped at 12% by a spend control really is at 12%,
|
|
183
|
+
* and writing 100 over it would be inventing usage that was never reported.
|
|
184
|
+
* `percent` says how much has been spent; `reached` says whether it is refused.
|
|
185
|
+
* Two facts, two fields, and the panel reads "stop" off the second one.
|
|
186
|
+
*
|
|
187
|
+
* The clamp stays exactly as it was: a provider that over- or under-reports
|
|
188
|
+
* must not put an impossible number on her screen.
|
|
189
|
+
*/
|
|
190
|
+
function drawnPercent(percent) {
|
|
191
|
+
const inRange = Math.max(0, Math.min(100, percent));
|
|
192
|
+
if (inRange >= 100) return 100;
|
|
193
|
+
return Math.min(99, Math.round(inRange));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* THE FACTS ABOUT THE ACCOUNT RATHER THAN ABOUT ONE WINDOW (w-36c51443df).
|
|
198
|
+
*
|
|
199
|
+
* The push carries ten fields and `codexLimits` read two of them. These four
|
|
200
|
+
* are the rest of what a person would want to know, and the campaign that found
|
|
201
|
+
* this could not answer any of them from the screen:
|
|
202
|
+
*
|
|
203
|
+
* plan which subscription is paying. Measured: "prolite".
|
|
204
|
+
* credits whether there is anything behind the window when it
|
|
205
|
+
* goes. Measured: `{hasCredits:false, balance:"0"}`, so
|
|
206
|
+
* nothing -- and "100% used" reads very differently with
|
|
207
|
+
* and without that.
|
|
208
|
+
* spendControlReached a cap she or her organisation set, not the plan's.
|
|
209
|
+
* reached which window the provider refused, for any surface
|
|
210
|
+
* drawing the reading whole rather than line by line.
|
|
211
|
+
*
|
|
212
|
+
* NULLS RATHER THAN GUESSES, for the reason main/codex-usage.mjs gives about
|
|
213
|
+
* percentages: a plan nobody has reported is not a plan of none, and a reading
|
|
214
|
+
* that invents one is worse than a panel that stays quiet.
|
|
215
|
+
*/
|
|
216
|
+
export function codexPlan(snapshot) {
|
|
217
|
+
const credits = plain(snapshot?.credits) ? snapshot.credits : null;
|
|
218
|
+
const bool = (value) => (typeof value === 'boolean' ? value : null);
|
|
219
|
+
return {
|
|
220
|
+
plan: typeof snapshot?.planType === 'string' && snapshot.planType
|
|
221
|
+
? snapshot.planType
|
|
222
|
+
: (typeof snapshot?.plan_type === 'string' && snapshot.plan_type ? snapshot.plan_type : null),
|
|
223
|
+
credits: credits ? (bool(credits.hasCredits) ?? bool(credits.has_credits)) : null,
|
|
224
|
+
unlimitedCredits: credits ? bool(credits.unlimited) : null,
|
|
225
|
+
spendControlReached: bool(snapshot?.spendControlReached ?? snapshot?.spend_control_reached),
|
|
226
|
+
reached: reachedWindow(snapshot),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
150
230
|
/**
|
|
151
231
|
* FOLD A ROLLING UPDATE INTO WHAT WE ALREADY HAD, WHICH IS THE PROTOCOL'S OWN
|
|
152
232
|
* INSTRUCTION AND NOT A KINDNESS.
|
|
@@ -155,7 +235,7 @@ export function codexLimits(snapshot) {
|
|
|
155
235
|
* carrying only the five hour window would take the week away, and one carrying
|
|
156
236
|
* neither would take both -- and this repo already puts exactly that on the wire
|
|
157
237
|
* (`{ rateLimits: {} }`, in
|
|
158
|
-
* tests/one-codex-process-carries-many-threads-and-a-card-nobody-answers-
|
|
238
|
+
* tests/one-codex-process-carries-many-threads-and-a-card-nobody-answers-waits.test.mjs).
|
|
159
239
|
* The failure would look like the corner going quiet for no reason, which is the
|
|
160
240
|
* complaint this whole surface started from.
|
|
161
241
|
*
|
|
@@ -168,6 +248,23 @@ export function codexLimits(snapshot) {
|
|
|
168
248
|
* Only the two windows are carried. Nothing else in the payload is drawn, and a
|
|
169
249
|
* merge that kept fields nobody reads would be inviting somebody to read them.
|
|
170
250
|
*/
|
|
251
|
+
/**
|
|
252
|
+
* THE ACCOUNT-LEVEL FIELDS A FOLD ALSO HAS TO KEEP (w-36c51443df).
|
|
253
|
+
*
|
|
254
|
+
* The rule above was "only the two windows, because a merge that kept fields
|
|
255
|
+
* nobody reads would be inviting somebody to read them". That was right while
|
|
256
|
+
* nothing read them, and it is exactly why a cap was unreadable. `codexPlan`
|
|
257
|
+
* reads these now, so they have to survive a fold for the same reason the
|
|
258
|
+
* windows do: a push carrying only a window would otherwise drop a
|
|
259
|
+
* `rateLimitReachedType` that arrived a beat earlier, and the corner would fall
|
|
260
|
+
* back from "spent" to a percentage for no reason -- which is the failure this
|
|
261
|
+
* function exists to prevent, one field along.
|
|
262
|
+
*
|
|
263
|
+
* Still nothing beyond what is drawn. `limitId`, `limitName`, `normalModelSlug`
|
|
264
|
+
* and `individualLimit` stay out, because nothing reads them.
|
|
265
|
+
*/
|
|
266
|
+
const CARRIED = ['planType', 'credits', 'spendControlReached', 'rateLimitReachedType'];
|
|
267
|
+
|
|
171
268
|
export function mergeRateLimits(previous, update) {
|
|
172
269
|
const out = {};
|
|
173
270
|
for (const key of WINDOWS) {
|
|
@@ -175,5 +272,14 @@ export function mergeRateLimits(previous, update) {
|
|
|
175
272
|
else if (plain(previous?.[key])) out[key] = previous[key];
|
|
176
273
|
else out[key] = null;
|
|
177
274
|
}
|
|
275
|
+
// `undefined` AND `null` BOTH MEAN "THIS PUSH SAID NOTHING", so either keeps
|
|
276
|
+
// what was already known -- the measured push carried explicit nulls for
|
|
277
|
+
// `spendControlReached` and `rateLimitReachedType` while they were fine, and
|
|
278
|
+
// treating those as news would forget a cap on the very next beat. A push
|
|
279
|
+
// genuinely lifting one carries `false`, which is a value and does replace.
|
|
280
|
+
for (const key of CARRIED) {
|
|
281
|
+
const said = update?.[key];
|
|
282
|
+
out[key] = said === undefined || said === null ? (previous?.[key] ?? null) : said;
|
|
283
|
+
}
|
|
178
284
|
return out;
|
|
179
285
|
}
|
package/shared/corner-tag.d.mts
CHANGED
|
@@ -5,10 +5,32 @@
|
|
|
5
5
|
export type HideChoice = { key: '5m' | '30m' | 'today'; label: string };
|
|
6
6
|
export const HIDE_CHOICES: HideChoice[];
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/** text is the count on the tag; label is what it says aloud ("2 waiting"). */
|
|
9
|
+
export function tagSays(counts: { ready?: number }): { kind: 'ready'; text: string; label: string } | null;
|
|
9
10
|
|
|
10
11
|
export type ReadyLine = { id: string; title: string; says: string; since?: number; open?: string | null };
|
|
11
12
|
export function cardLines<T extends ReadyLine>(ready: T[], now: number, max?: number): {
|
|
12
13
|
lines: (T & { waited: string })[];
|
|
13
14
|
more: number;
|
|
14
15
|
};
|
|
16
|
+
|
|
17
|
+
/** The four corners that resize it, and the arrow each one draws. */
|
|
18
|
+
export const GRIPS: readonly ['tl', 'tr', 'bl', 'br'];
|
|
19
|
+
export type Grip = 'tl' | 'tr' | 'bl' | 'br';
|
|
20
|
+
|
|
21
|
+
/** The nine dots, in whole pixels, for a tag this tall. */
|
|
22
|
+
export const LATTICE: { box: number; dot: number; gap: number };
|
|
23
|
+
export function latticeGrid(box: number, dpr?: number): {
|
|
24
|
+
dot: number; gap: number; pitch: number; side: number; size: number;
|
|
25
|
+
dots: { cx: number; cy: number }[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** How much of the tag's size the list takes; it is read, not glanced at. */
|
|
29
|
+
export function cardScale(scale: number): number;
|
|
30
|
+
|
|
31
|
+
type Rect = { x: number; y: number; width: number; height: number };
|
|
32
|
+
/** A page's measurement of itself, in screen points at the zoom it is drawn at. */
|
|
33
|
+
export function inPoints(
|
|
34
|
+
m: { width: number; height: number; glass?: Rect; radius?: number },
|
|
35
|
+
zoom?: number,
|
|
36
|
+
): { width: number; height: number; glass?: Rect; radius?: number };
|
package/shared/corner-tag.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// THE CORNER TAG (w-dafae58a23): the rules, and nothing that touches a window.
|
|
2
2
|
//
|
|
3
3
|
// You lose track of agents once you are in another app, and the ones waiting on
|
|
4
|
-
// you sit idle. The answer
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// for you is worth a line.
|
|
4
|
+
// you sit idle. The answer is a small tag in a corner of the screen: the
|
|
5
|
+
// nine-dot mark and how many started waiting since you last had the app in
|
|
6
|
+
// front. When nothing is new it is gone (w-93b3a1a796), because a tag that is
|
|
7
|
+
// always there is a tag nobody reads.
|
|
9
8
|
//
|
|
10
9
|
// It must never become a nag. So it hides for a while and comes back on its
|
|
11
10
|
// own, there is no one-click way to make it gone for good (that becomes a thing
|
|
@@ -18,6 +17,17 @@
|
|
|
18
17
|
/** Space between the tag and the edge of the screen when it rests in a corner. */
|
|
19
18
|
export const MARGIN = 18;
|
|
20
19
|
|
|
20
|
+
/**
|
|
21
|
+
* HOW FAR PAST THE EDGE SHE MAY PARK IT (2026-10-13). The resting rule used to
|
|
22
|
+
* hold the whole tag inside the screen, so a drag to the right stopped with
|
|
23
|
+
* the tag flush against the edge and sprang back from anything further: "I
|
|
24
|
+
* can't seem to put it further to the right. It snaps back into place in this
|
|
25
|
+
* slot but I often want it a little bit further to the right side of the
|
|
26
|
+
* screen." It may now hang this far off any edge — enough to tuck it away,
|
|
27
|
+
* never enough to lose it.
|
|
28
|
+
*/
|
|
29
|
+
export const EDGE_SLACK = 24;
|
|
30
|
+
|
|
21
31
|
/** The ways to hide it. All of them come back; none of them is permanent. */
|
|
22
32
|
export const HIDE_CHOICES = [
|
|
23
33
|
{ key: '5m', label: '5 min' },
|
|
@@ -37,37 +47,244 @@ export function hiddenUntil(choice, now) {
|
|
|
37
47
|
return 0;
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
/**
|
|
51
|
+
* What the tag says: the mark and how many are new, or null, in which case it
|
|
52
|
+
* is gone. A working count kept it on screen all day and made it wallpaper
|
|
53
|
+
* (w-93b3a1a796), so working agents only appear in its list. "Waiting" won
|
|
54
|
+
* over "ready" and "need you", and is what the tag says aloud.
|
|
55
|
+
*/
|
|
56
|
+
export function tagSays({ ready = 0 } = {}) {
|
|
57
|
+
if (ready > 0) return { kind: 'ready', text: String(ready), label: `${ready} waiting` };
|
|
46
58
|
return null;
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
* A page's measurement of itself, in screen points. A page drawn at a zoom
|
|
63
|
+
* other than 100% measures in its own units, and the window and the Mac's
|
|
64
|
+
* glass are placed in points: at 87% the glass stood out past the tag
|
|
65
|
+
* (w-93b3a1a796). A zoom it cannot read counts as 100%.
|
|
66
|
+
*/
|
|
67
|
+
export function inPoints({ width, height, glass, radius }, zoom) {
|
|
68
|
+
const z = Number.isFinite(zoom) && zoom > 0 ? zoom : 1;
|
|
69
|
+
return {
|
|
70
|
+
width: width * z,
|
|
71
|
+
height: height * z,
|
|
72
|
+
glass: glass && { x: glass.x * z, y: glass.y * z, width: glass.width * z, height: glass.height * z },
|
|
73
|
+
radius: Number.isFinite(radius) ? radius * z : radius,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* What started waiting after you last looked, which is the last time the
|
|
79
|
+
* app's window was in front, and whatever a notification told you about since
|
|
80
|
+
* you were last at the app (`told`, { id, at }), dated from when it told you.
|
|
81
|
+
* Before you have ever looked, everything counts.
|
|
82
|
+
*/
|
|
83
|
+
export function newSince(ready, lookedAt, told = []) {
|
|
84
|
+
const toldAt = new Map();
|
|
85
|
+
for (const t of told ?? []) if (t?.id != null) toldAt.set(t.id, Number.isFinite(t.at) ? t.at : 0);
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const r of ready ?? []) {
|
|
88
|
+
const own = Number.isFinite(r.since) ? r.since : null;
|
|
89
|
+
if (toldAt.has(r.id)) out.push(own != null && own >= toldAt.get(r.id) ? r : { ...r, since: toldAt.get(r.id) });
|
|
90
|
+
else if (!(lookedAt > 0) || (own != null && own > lookedAt)) out.push(r);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* What is left once you have crossed some off the tray (`dismissed`, id to
|
|
97
|
+
* when). A thread stays off until it has something newer than that.
|
|
98
|
+
*/
|
|
99
|
+
export function notDismissed(list, dismissed = {}) {
|
|
100
|
+
return (list ?? []).filter((r) => {
|
|
101
|
+
const at = dismissed?.[r.id];
|
|
102
|
+
return !(Number.isFinite(at) && !(r.since > at));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* HOW BIG THE TAG DRAWS, from the width of the screen it rests on (2026-10-13).
|
|
108
|
+
* One number: the page multiplies everything in the tag and its list by it, so
|
|
109
|
+
* the words, the chevron, the nine-dot mark and the padding grow together.
|
|
110
|
+
*
|
|
111
|
+
* She reads the tray on a 32" screen and on a laptop, and 28px was too small on
|
|
112
|
+
* both, worst on the big one: "a little bigger... esp on my 32 inch screen but
|
|
113
|
+
* also maybe on smaller screens too". So it is a ladder off the screen's own
|
|
114
|
+
* width in points, not one size for everything and not a setting to find.
|
|
115
|
+
*
|
|
116
|
+
* The first ladder climbed too gently — "34 px on a 32-inch screen is still far
|
|
117
|
+
* too small so you need to scale it more with screen size" — so the steps are
|
|
118
|
+
* now 31px on a laptop, 36px, 42px, and 48px on a screen as wide as hers
|
|
119
|
+
* (3413 points). 1 would be the old 28px tag, which nothing asks for any more.
|
|
120
|
+
*
|
|
121
|
+
* THE STEPS ARE WHOLE PIXELS, and the scale is worked back from them. A tag
|
|
122
|
+
* 30.8px tall puts its bottom border on half a device pixel, where it goes
|
|
123
|
+
* soft and its bottom corners break up; she saw exactly that and called it
|
|
124
|
+
* messed up (2026-10-13). Whole heights keep every edge on a real pixel.
|
|
125
|
+
*/
|
|
126
|
+
export const TAG_BASE = 28;
|
|
127
|
+
export const TAG_HEIGHTS = [
|
|
128
|
+
{ from: 0, px: 31 },
|
|
129
|
+
{ from: 1900, px: 36 },
|
|
130
|
+
{ from: 2400, px: 42 },
|
|
131
|
+
{ from: 3000, px: 48 },
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
/** How tall the tag draws on a screen this many points wide, in whole pixels. */
|
|
135
|
+
export function tagHeight(width) {
|
|
136
|
+
const w = Number.isFinite(width) ? width : 0;
|
|
137
|
+
let px = TAG_HEIGHTS[0].px;
|
|
138
|
+
for (const step of TAG_HEIGHTS) if (w >= step.from) px = step.px;
|
|
139
|
+
return px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The one number the page multiplies everything by. */
|
|
143
|
+
export function tagScale(width) {
|
|
144
|
+
return tagHeight(width) / TAG_BASE;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* HER OWN SIZE, KEPT AS A MULTIPLE OF THE AUTOMATIC ONE (2026-10-13). She
|
|
149
|
+
* asked to be able to resize the tray, by dragging a corner of it.
|
|
150
|
+
*
|
|
151
|
+
* What she sets is not a height in pixels but how much bigger or smaller than
|
|
152
|
+
* the automatic size she wants it, because the automatic size is the thing
|
|
153
|
+
* that knows about screens: a tag she grows by a fifth on her laptop is a
|
|
154
|
+
* fifth bigger on the 32" too, rather than shrinking back to a laptop's
|
|
155
|
+
* number the moment she plugs the big screen in.
|
|
156
|
+
*/
|
|
157
|
+
export const TAG_ZOOM = { min: 0.75, max: 2, step: 0.1 };
|
|
158
|
+
/** However far she drags, the tag stays a tag. */
|
|
159
|
+
export const TAG_LIMITS = { min: 24, max: 80 };
|
|
160
|
+
|
|
161
|
+
/** Her multiple, kept sane; nothing stored means the automatic size. */
|
|
162
|
+
export function tagZoom(zoom) {
|
|
163
|
+
const z = Number.isFinite(zoom) ? zoom : 1;
|
|
164
|
+
return clamp(Math.round(z * 100) / 100, TAG_ZOOM.min, TAG_ZOOM.max);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** How tall the tag draws on this screen, with her multiple applied. */
|
|
168
|
+
export function tagHeightOn(width, zoom) {
|
|
169
|
+
return clamp(Math.round(tagHeight(width) * tagZoom(zoom)), TAG_LIMITS.min, TAG_LIMITS.max);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The scale the page is handed: the automatic size times her multiple. */
|
|
173
|
+
export function tagScaleOn(width, zoom) {
|
|
174
|
+
return tagHeightOn(width, zoom) / TAG_BASE;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The multiple a tag dragged to this height means, on this screen. */
|
|
178
|
+
export function zoomFor(height, width) {
|
|
179
|
+
const h = clamp(Number.isFinite(height) ? height : 0, TAG_LIMITS.min, TAG_LIMITS.max);
|
|
180
|
+
return tagZoom(h / tagHeight(width));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** One notch bigger or smaller, for the tag's own menu. */
|
|
184
|
+
export function zoomStep(zoom, by) {
|
|
185
|
+
return tagZoom(tagZoom(zoom) + by * TAG_ZOOM.step);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Every corner resizes it, because one ledge in one corner was not findable. */
|
|
189
|
+
export const GRIPS = ['tl', 'tr', 'bl', 'br'];
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* How tall a drag on this corner means. The corner follows the cursor and the
|
|
193
|
+
* opposite one stays where it is, the way a window resizes. The tag is four
|
|
194
|
+
* times wider than it is tall, so sideways travel counts for about a third:
|
|
195
|
+
* the diagonal still feels like a diagonal without the width running away.
|
|
196
|
+
*/
|
|
197
|
+
export function heightFromDrag(corner, fromHeight, dx, dy) {
|
|
198
|
+
const down = corner.startsWith('b') ? dy : -dy;
|
|
199
|
+
const out = corner.endsWith('r') ? dx : -dx;
|
|
200
|
+
return Math.round(fromHeight + down + out * 0.35);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* HOW MUCH OF THE TAG'S SIZE THE LIST TAKES (2026-10-13). The tag is a glance
|
|
205
|
+
* from across a desk and wants to be big on a big screen; the list is read a
|
|
206
|
+
* line at a time and does not. At the tag's full scale on her 32" the rows came
|
|
207
|
+
* out "really, really big... way bigger than anything else I'm used to seeing
|
|
208
|
+
* on the screen", so the list follows the tag less than a third of the way and
|
|
209
|
+
* stops at a quarter bigger than the app's own text.
|
|
210
|
+
*/
|
|
211
|
+
export function cardScale(scale) {
|
|
212
|
+
const s = Number.isFinite(scale) ? scale : 1;
|
|
213
|
+
return clamp(Math.round((1 + (s - 1) * 0.3) * 1000) / 1000, 1, 1.25);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* THE NINE DOTS ARE ONE DRAWING, not nine boxes (2026-10-13).
|
|
218
|
+
*
|
|
219
|
+
* They were boxes in a grid, and twice the fix was to give the dot and the gap
|
|
220
|
+
* whole pixels. That only holds on a screen whose point is a whole device
|
|
221
|
+
* pixel. Hers is not: a Mac in a scaled mode draws 1.5 device pixels to the
|
|
222
|
+
* point, so a 3pt dot is 4.5 and a 1pt gap is 1.5, and the browser snaps every
|
|
223
|
+
* box on its own — 5,5,5 across with gaps of 2 and 1. Rasterised through the
|
|
224
|
+
* real page, 16 of 28 sizes came out uneven at that scale and none at 1x or
|
|
225
|
+
* 2x. "It's the same issue we've had six times before."
|
|
226
|
+
*
|
|
227
|
+
* An svg of nine circles on one pitch is rasterised in one go, so the spacing
|
|
228
|
+
* cannot round apart whatever the screen does, and the geometry can go back to
|
|
229
|
+
* the launch film's own: dots of 2.7 and gaps of 0.95 in a box of 10.
|
|
230
|
+
*/
|
|
231
|
+
export const LATTICE = { box: 10, dot: 2.7, gap: 0.95 };
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The mark for a box this many points across, on a screen drawing this many
|
|
235
|
+
* device pixels to the point.
|
|
236
|
+
*
|
|
237
|
+
* THE PITCH IS A WHOLE NUMBER OF DEVICE PIXELS. That is the whole trick. Every
|
|
238
|
+
* dot then sits at the same distance from a device-pixel boundary as every
|
|
239
|
+
* other, so all nine are rasterised identically and the spacing cannot round
|
|
240
|
+
* apart — however the mark's own corner happens to land. Whole POINTS were the
|
|
241
|
+
* old rule, and a point on her screen is 1.5 device pixels, which is why it
|
|
242
|
+
* kept coming back.
|
|
243
|
+
*/
|
|
244
|
+
export function latticeGrid(box, dpr = 1) {
|
|
245
|
+
const px = Number.isFinite(dpr) && dpr > 0 ? dpr : 1;
|
|
246
|
+
const want = (Number.isFinite(box) && box > 0 ? box : LATTICE.box) * px;
|
|
247
|
+
// Both whole numbers either side of the film's dot and gap, and the pair
|
|
248
|
+
// whose mark comes closest to the size asked for — a dot and a gap rounded
|
|
249
|
+
// on their own can leave the mark a fifth too big, which shows.
|
|
250
|
+
const ideal = { dot: (want * LATTICE.dot) / LATTICE.box, gap: (want * LATTICE.gap) / LATTICE.box };
|
|
251
|
+
const ways = [];
|
|
252
|
+
for (const dot of [Math.floor(ideal.dot), Math.ceil(ideal.dot)]) {
|
|
253
|
+
for (const gap of [Math.floor(ideal.gap), Math.ceil(ideal.gap)]) {
|
|
254
|
+
const d = Math.max(2, dot);
|
|
255
|
+
const g = Math.max(1, Math.min(gap, d - 1));
|
|
256
|
+
ways.push({ dot: d, gap: g, side: 3 * d + 2 * g });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// A dot five times its gap is a mark whose dots all but touch, and one twice
|
|
260
|
+
// its gap is a loose one; the film's is 2.84. Keep inside that band if any
|
|
261
|
+
// pair can, and among those take the closest size.
|
|
262
|
+
const inBand = ways.filter((w) => w.dot / w.gap >= 2 && w.dot / w.gap <= 4);
|
|
263
|
+
const pick = (inBand.length ? inBand : ways)
|
|
264
|
+
.sort((a, b) => Math.abs(a.side - want) - Math.abs(b.side - want)
|
|
265
|
+
|| Math.abs(a.dot / a.gap - LATTICE.dot / LATTICE.gap) - Math.abs(b.dot / b.gap - LATTICE.dot / LATTICE.gap));
|
|
266
|
+
const { dot, gap, side } = pick[0];
|
|
267
|
+
const pitch = dot + gap;
|
|
268
|
+
const at = [0, 1, 2].map((i) => dot / 2 + i * pitch);
|
|
269
|
+
return { dot, gap, pitch, side, size: side / px, dots: at.flatMap((cy) => at.map((cx) => ({ cx, cy }))) };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Newest first. Something with no moment it became ready (an agent stopped on
|
|
274
|
+
* a yes, right now) sorts as now.
|
|
58
275
|
*/
|
|
59
276
|
export function readyNow(ready, now) {
|
|
60
277
|
return [...(ready ?? [])]
|
|
61
278
|
.sort((a, b) => (Number.isFinite(b.since) ? b.since : now) - (Number.isFinite(a.since) ? a.since : now));
|
|
62
279
|
}
|
|
63
280
|
|
|
64
|
-
/** Whether the tag is on screen right now. */
|
|
65
|
-
export function tagShows({ on, hiddenUntil: until = 0, now, inApp, ready = 0
|
|
281
|
+
/** Whether the tag is on screen right now; `ready` is how many are new. */
|
|
282
|
+
export function tagShows({ on, hiddenUntil: until = 0, now, inApp, ready = 0 }) {
|
|
66
283
|
if (on === false) return false;
|
|
67
284
|
if (until > now) return false;
|
|
68
285
|
// In the app itself the inbox already says all of this, bigger.
|
|
69
286
|
if (inApp) return false;
|
|
70
|
-
return !!tagSays({ ready
|
|
287
|
+
return !!tagSays({ ready });
|
|
71
288
|
}
|
|
72
289
|
|
|
73
290
|
/** How long something has waited, in units. */
|
|
@@ -117,6 +334,38 @@ function inLowerHalf(r, a) { return r.y + r.height / 2 > a.y + a.height / 2; }
|
|
|
117
334
|
* corner stays put, so "2 ready for you" turning into "20 working" does not
|
|
118
335
|
* walk the tag away from the edge you parked it against.
|
|
119
336
|
*/
|
|
337
|
+
/**
|
|
338
|
+
* WHERE THE TAG IS PARKED, SAID IN A WAY THAT SURVIVES A CHANGE OF SCREEN
|
|
339
|
+
* (2026-10-13). Its spot used to be remembered as a point on the whole desktop,
|
|
340
|
+
* so plugging in a monitor, or unplugging one, moved every screen's
|
|
341
|
+
* coordinates underneath it and the tag "jumps around quite a bit". A corner
|
|
342
|
+
* and a gap mean the same thing on any screen: bottom right, 18 points in.
|
|
343
|
+
*/
|
|
344
|
+
export function cornerPlace(bounds, area) {
|
|
345
|
+
const right = inRightHalf(bounds, area);
|
|
346
|
+
const lower = inLowerHalf(bounds, area);
|
|
347
|
+
return {
|
|
348
|
+
corner: `${lower ? 'b' : 't'}${right ? 'r' : 'l'}`,
|
|
349
|
+
dx: Math.round(right ? area.x + area.width - (bounds.x + bounds.width) : bounds.x - area.x),
|
|
350
|
+
dy: Math.round(lower ? area.y + area.height - (bounds.y + bounds.height) : bounds.y - area.y),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** That same place on this screen, wholly on it however small it is. */
|
|
355
|
+
export function spotFor(place, size, area) {
|
|
356
|
+
if (!place?.corner) return null;
|
|
357
|
+
const right = place.corner.endsWith('r');
|
|
358
|
+
const lower = place.corner.startsWith('b');
|
|
359
|
+
// A gap may be negative: that is the tag tucked past the edge, which she
|
|
360
|
+
// asked for and which the resting rule now allows.
|
|
361
|
+
const dx = clamp(Number.isFinite(place.dx) ? place.dx : MARGIN, -EDGE_SLACK, Math.max(0, area.width - size.width));
|
|
362
|
+
const dy = clamp(Number.isFinite(place.dy) ? place.dy : MARGIN, -EDGE_SLACK, Math.max(0, area.height - size.height));
|
|
363
|
+
return {
|
|
364
|
+
x: right ? area.x + area.width - size.width - dx : area.x + dx,
|
|
365
|
+
y: lower ? area.y + area.height - size.height - dy : area.y + dy,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
120
369
|
export function keepCorner(bounds, size, area) {
|
|
121
370
|
const x = inRightHalf(bounds, area) ? bounds.x + bounds.width - size.width : bounds.x;
|
|
122
371
|
const y = inLowerHalf(bounds, area) ? bounds.y + bounds.height - size.height : bounds.y;
|
|
@@ -128,14 +377,20 @@ export function keepCorner(bounds, size, area) {
|
|
|
128
377
|
* screen, so it never runs off an edge, lined up with the tag's outer edge.
|
|
129
378
|
*/
|
|
130
379
|
export function cardPlacement(tag, card, area, gap = 8) {
|
|
131
|
-
let above = inLowerHalf(tag, area);
|
|
132
380
|
const alignRight = inRightHalf(tag, area);
|
|
133
381
|
const x = clamp(alignRight ? tag.x + tag.width - card.width : tag.x, area.x, area.x + area.width - card.width);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
382
|
+
// Which side has the room. The list may never cover the tag: she adjusts the
|
|
383
|
+
// tray's size from this list ("you still need to see the component to be able
|
|
384
|
+
// to make those decisions"), and a list over the thing it is sizing is no
|
|
385
|
+
// use. So the side is picked by which has more room, the list is kept on
|
|
386
|
+
// that side of the tag, and only then is it brought onto the screen — a list
|
|
387
|
+
// taller than its side runs off the screen's edge rather than over the tag.
|
|
388
|
+
const room = { above: tag.y - area.y, below: area.y + area.height - (tag.y + tag.height) };
|
|
389
|
+
let above = inLowerHalf(tag, area);
|
|
390
|
+
if (above && room.above < card.height + gap && room.below > room.above) above = false;
|
|
391
|
+
else if (!above && room.below < card.height + gap && room.above > room.below) above = true;
|
|
392
|
+
const y = above
|
|
393
|
+
? Math.min(tag.y - gap - card.height, Math.max(area.y, tag.y - gap - card.height))
|
|
394
|
+
: Math.max(tag.y + tag.height + gap, Math.min(area.y + area.height - card.height, tag.y + tag.height + gap));
|
|
140
395
|
return { above, alignRight, card: { x, y, width: card.width, height: card.height } };
|
|
141
396
|
}
|
|
@@ -34,6 +34,20 @@ const NARRATED = /^\d{2}:\d{2}:\d{2}\s{2}\S/m;
|
|
|
34
34
|
|
|
35
35
|
const EXITED = /^# exited \((-?\d+)\)/m;
|
|
36
36
|
|
|
37
|
+
// Written after the exit line's time when the person stopped the run (Stop,
|
|
38
|
+
// Z, closing or putting off the thread): that end is not a death to report.
|
|
39
|
+
export const STOPPED_ON_REQUEST = ' · stopped on request';
|
|
40
|
+
const STOPPED = /^# exited \([^)]*\) \S+ · stopped on request$/m;
|
|
41
|
+
|
|
42
|
+
// A tool call, as every engine traces one: `HH:MM:SS [Bash] ls`. Text lines
|
|
43
|
+
// do not count, because the CLI types its own refusals as the agent's words,
|
|
44
|
+
// and neither does OpenCode's `[error]`.
|
|
45
|
+
const TOOL_LINE = /^\d{2}:\d{2}:\d{2}\s{2}\[(?!error\])[^\]\s]+\]/m;
|
|
46
|
+
|
|
47
|
+
export function tracesAToolCall(text) {
|
|
48
|
+
return TOOL_LINE.test(String(text ?? ''));
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
// THE DIAGNOSIS IS ON THE SECOND LINE, NOT THE FIRST, and reading only the
|
|
38
52
|
// prefixed line loses it entirely. What the tool prints arrives as one chunk
|
|
39
53
|
// and is written with one `stderr: ` in front of the whole chunk, so a refusal
|
|
@@ -94,8 +108,9 @@ function errorResult(raw) {
|
|
|
94
108
|
|
|
95
109
|
/**
|
|
96
110
|
* @param {string} text the whole trace file
|
|
97
|
-
* @returns {{everRan: boolean, exitCode: number|null, stderr: string}}
|
|
111
|
+
* @returns {{everRan: boolean, worked: boolean, exitCode: number|null, stoppedOnRequest: boolean, stderr: string}}
|
|
98
112
|
* `stderr` is what the tool said about itself, from either place it says it.
|
|
113
|
+
* `worked` means the agent called at least one tool before it ended.
|
|
99
114
|
* `everRan` false means the CLI never produced a first line: the process
|
|
100
115
|
* started and died before it was a session. `exitCode` null means the trace
|
|
101
116
|
* has no exit line at all, which is what a run killed with the app looks
|
|
@@ -106,7 +121,9 @@ export function readDeadRunTrace(text) {
|
|
|
106
121
|
const exited = EXITED.exec(raw);
|
|
107
122
|
return {
|
|
108
123
|
everRan: NARRATED.test(raw),
|
|
124
|
+
worked: tracesAToolCall(raw),
|
|
109
125
|
exitCode: exited ? Number(exited[1]) : null,
|
|
126
|
+
stoppedOnRequest: STOPPED.test(raw),
|
|
110
127
|
stderr: [stderrChunks(raw), errorResult(raw)].filter(Boolean).join('\n').slice(-1000),
|
|
111
128
|
};
|
|
112
129
|
}
|
|
@@ -122,6 +139,6 @@ export function readDeadRunTrace(text) {
|
|
|
122
139
|
* the supervisor has no business speaking over it.
|
|
123
140
|
*/
|
|
124
141
|
export function traceShowsASilentDeath(trace) {
|
|
125
|
-
if (!trace || trace.exitCode == null) return false;
|
|
142
|
+
if (!trace || trace.exitCode == null || trace.stoppedOnRequest) return false;
|
|
126
143
|
return !trace.everRan || trace.exitCode !== 0;
|
|
127
144
|
}
|