agentbox-app 0.1.13 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -9
- package/bin/agentbox.mjs +23 -1
- package/briefs/message-rules.md +36 -8
- package/briefs/worker.md +46 -23
- package/main/account-discovery.mjs +122 -30
- package/main/account-tooling.mjs +29 -1
- package/main/account-usage.mjs +46 -0
- package/main/agent-sessions.mjs +282 -268
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +12 -2
- package/main/answer-modes.mjs +103 -37
- package/main/approval-prompt-core.mjs +197 -0
- package/main/approval-prompt-server.mjs +4 -138
- package/main/approvals.mjs +101 -297
- package/main/artifact-path.mjs +47 -18
- package/main/claude-input.mjs +48 -4
- package/main/claude-plan.mjs +48 -0
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +18 -40
- package/main/cloud/account.mjs +179 -0
- package/main/cloud/aws.mjs +127 -0
- package/main/cloud/browser-return.mjs +58 -0
- package/main/cloud/clients.mjs +15 -0
- package/main/cloud/codex-cloud.mjs +53 -0
- package/main/cloud/config.mjs +107 -0
- package/main/cloud/docs.mjs +219 -0
- package/main/cloud/exec.mjs +27 -0
- package/main/cloud/host.mjs +165 -0
- package/main/cloud/kit.mjs +80 -0
- package/main/cloud/logins.mjs +543 -0
- package/main/cloud/managed.mjs +115 -0
- package/main/cloud/marker.mjs +104 -0
- package/main/cloud/org.mjs +53 -0
- package/main/cloud/reuse.mjs +208 -0
- package/main/cloud/run-cli.mjs +177 -0
- package/main/cloud/run.mjs +1934 -0
- package/main/cloud/usage.mjs +76 -0
- package/main/cloud-permission.mjs +51 -0
- package/main/codex-account.mjs +78 -0
- package/main/codex-app-server.mjs +276 -409
- package/main/codex-approvals.mjs +148 -433
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +212 -507
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +71 -3
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +17 -3
- package/main/codex.mjs +95 -363
- package/main/config.mjs +11 -1
- package/main/corner-tag.mjs +340 -19
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +232 -282
- package/main/engine-setup.mjs +21 -9
- package/main/feedback.mjs +3 -25
- package/main/folder-pictures.mjs +2 -2
- package/main/freeze-recorder-thread.mjs +86 -0
- package/main/freeze-recorder.mjs +146 -0
- package/main/fresh-user.mjs +20 -9
- package/main/git-change-offthread.mjs +17 -3
- package/main/git-change-thread.mjs +5 -2
- package/main/harnesses/claude.mjs +32 -11
- package/main/harnesses/codex.mjs +9 -2
- package/main/importable-threads.mjs +64 -6
- package/main/ipc.mjs +362 -111
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +199 -84
- package/main/memory-gate-history.mjs +36 -10
- package/main/memory-gate-server.mjs +62 -0
- package/main/memory-gate.mjs +14 -1
- package/main/menu.mjs +4 -2
- package/main/message-priority.mjs +2 -2
- package/main/mic.mjs +38 -0
- package/main/native/liquid-glass/build/liquid-glass.node +0 -0
- package/main/new-chats-watch.mjs +110 -0
- package/main/new-user-window.mjs +25 -0
- package/main/notify.mjs +24 -8
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +29 -9
- package/main/phone-uploads.mjs +476 -0
- package/main/picture-shape.mjs +114 -0
- package/main/quit-sequence.mjs +143 -0
- package/main/refresh-agent-models.mjs +15 -2
- package/main/relay-client.mjs +69 -1
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +12 -41
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +45 -3
- package/main/settings.mjs +220 -44
- package/main/shell-path.mjs +185 -37
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +115 -19
- package/main/source-updater.mjs +18 -10
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/store/home.mjs +87 -232
- package/main/store/paths.mjs +11 -0
- package/main/store/teammates-files.mjs +99 -0
- package/main/store/work-items.mjs +231 -13
- package/main/store.mjs +226 -52
- package/main/supervisor.mjs +3103 -341
- package/main/task-folders.mjs +430 -30
- package/main/task-terminals.mjs +110 -34
- package/main/team/index.mjs +114 -10
- package/main/team/memory-cloud.mjs +183 -1
- package/main/team/room-hours.mjs +48 -0
- package/main/team/room-setup.mjs +31 -0
- package/main/team/rooms-memory.mjs +454 -0
- package/main/team/rooms-shape.mjs +42 -0
- package/main/team/rooms-supabase.mjs +324 -0
- package/main/team/rooms.mjs +575 -0
- package/main/team/session.mjs +96 -26
- package/main/team/supabase-backend.mjs +180 -0
- package/main/team/sync.mjs +109 -1
- package/main/teammates.mjs +580 -0
- package/main/terminal-host.mjs +143 -0
- package/main/terminal-hosts.mjs +261 -0
- package/main/transcript-search.mjs +80 -0
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +90 -219
- package/mcp/core/documents.mjs +3 -1
- package/mcp/core/repeats.mjs +88 -0
- package/mcp/core/teammates.mjs +222 -0
- package/mcp/core/work.mjs +76 -4
- package/mcp/stdio.mjs +24 -11
- package/mcp/tools.mjs +116 -8
- package/package.json +21 -5
- package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
- package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
- package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
- package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
- package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
- package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
- package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
- package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
- package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
- package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
- package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
- package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
- package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
- package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
- package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
- package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
- package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
- package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
- package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
- package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
- package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
- package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
- package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
- package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
- package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
- package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
- package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
- package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
- package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
- package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
- package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
- package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
- package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
- package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
- package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
- package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
- package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
- package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
- package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
- package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
- package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
- package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
- package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
- package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
- package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
- package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
- package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
- package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
- package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
- package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
- package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
- package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
- package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
- package/renderer/dist/corner-tag.html +3 -3
- package/renderer/dist/index.html +3 -3
- package/scripts/after-install.mjs +30 -8
- package/scripts/cloud-shim.pl +57 -0
- package/shared/account-names.d.mts +6 -0
- package/shared/account-names.mjs +15 -0
- package/shared/agent-hours.mjs +136 -0
- package/shared/agent-import.mjs +60 -34
- package/shared/agents.mjs +126 -522
- package/shared/analytics-events.mjs +12 -0
- package/shared/approval-card.mjs +36 -0
- package/shared/bridge-map.mjs +27 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-usage.d.mts +1 -0
- package/shared/claude-usage.mjs +42 -1
- package/shared/cloud-hand-back.d.mts +7 -0
- package/shared/cloud-hand-back.mjs +20 -0
- package/shared/cloud-kit.d.mts +219 -0
- package/shared/cloud-kit.mjs +341 -0
- package/shared/cloud-login-name.mjs +4 -0
- package/shared/cloud-start.mjs +17 -0
- package/shared/cloud-versions.mjs +23 -0
- package/shared/codex-import.mjs +2 -2
- package/shared/codex-modes.d.mts +22 -9
- package/shared/codex-modes.mjs +67 -11
- package/shared/codex-usage.mjs +108 -2
- package/shared/corner-tag.d.mts +23 -1
- package/shared/corner-tag.mjs +284 -29
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +70 -309
- package/shared/feedback.d.mts +7 -0
- package/shared/feedback.mjs +25 -0
- package/shared/first-run-practice.mjs +17 -24
- package/shared/harness-definitions.mjs +8 -0
- package/shared/instruction-defaults.json +2 -2
- package/shared/key-caps.d.mts +14 -0
- package/shared/key-caps.mjs +68 -0
- package/shared/message-edits.d.mts +7 -0
- package/shared/message-edits.mjs +35 -0
- package/shared/move-thread.d.mts +24 -0
- package/shared/move-thread.mjs +112 -0
- package/shared/named-threads.mjs +72 -0
- package/shared/not-shipped.d.mts +22 -0
- package/shared/not-shipped.mjs +89 -0
- package/shared/notify-rules.d.mts +6 -0
- package/shared/notify-rules.mjs +36 -8
- package/shared/phone-files.mjs +28 -0
- package/shared/phone-settings.mjs +118 -0
- package/shared/phone-snapshot.d.mts +16 -0
- package/shared/phone-snapshot.mjs +348 -0
- package/shared/picture-formats.d.mts +1 -0
- package/shared/picture-formats.mjs +13 -0
- package/shared/rank.d.mts +2 -1
- package/shared/rank.mjs +32 -19
- package/shared/repeats.d.mts +8 -0
- package/shared/repeats.mjs +33 -2
- package/shared/room-cards.mjs +167 -0
- package/shared/room-links.d.mts +4 -0
- package/shared/room-links.mjs +21 -0
- package/shared/runs-on.d.mts +3 -3
- package/shared/runs-on.mjs +8 -5
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +14 -6
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +443 -40
- package/shared/team-rules.mjs +9 -2
- package/shared/teammates.d.mts +19 -0
- package/shared/teammates.mjs +756 -0
- package/shared/terminal-focus.d.mts +1 -0
- package/shared/terminal-focus.mjs +13 -0
- package/shared/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +9 -0
- package/shared/usage.mjs +49 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/work-items.mjs +127 -30
- package/shared/work-lines.mjs +92 -244
- package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
- package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
- package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
package/main/config.mjs
CHANGED
|
@@ -34,7 +34,7 @@ const CONFIG_NAME = 'zero.config.json';
|
|
|
34
34
|
*/
|
|
35
35
|
export const DEFAULT_SESSIONS_AT_ONCE = 3;
|
|
36
36
|
|
|
37
|
-
const DEFAULTS = {
|
|
37
|
+
export const DEFAULTS = {
|
|
38
38
|
// ONE ACCOUNT FOLDER PER INSTALL, AND NOBODY ELSE'S.
|
|
39
39
|
//
|
|
40
40
|
// This was a literal id for most of the app's life, which meant every copy
|
|
@@ -76,6 +76,16 @@ const DEFAULTS = {
|
|
|
76
76
|
// commands may run at once; null is Auto, one per 8 GB.
|
|
77
77
|
memoryGate: false,
|
|
78
78
|
memoryGateSlots: null,
|
|
79
|
+
// RUN THE OVERFLOW IN THE CLOUD (docs/cloud/PLAN.md). Null is "when this Mac
|
|
80
|
+
// has been set up for it": nothing happens anywhere until somebody has run
|
|
81
|
+
// scripts/cloud-setup.mjs and an account has signed in from the cloud menu,
|
|
82
|
+
// and before that the app is exactly the app it was. `false` turns it off
|
|
83
|
+
// even then.
|
|
84
|
+
// `maxCloudSessions` is the cloud's own cap, apart from this Mac's. Null is
|
|
85
|
+
// what this Mac can carry: 50 through the shared host (about 11 MB a run),
|
|
86
|
+
// 20 where each run needs a relay of its own (about 110 MB a run).
|
|
87
|
+
cloudRuns: null,
|
|
88
|
+
maxCloudSessions: null,
|
|
79
89
|
// STOP WHAT FINISHED AGENTS LEAVE RUNNING (main/leftovers.mjs). Its own
|
|
80
90
|
// switch, off until somebody turns it on; `cleanupLeftoversSince` is the
|
|
81
91
|
// moment they did, so nothing is stopped sooner than the idle time after it.
|
package/main/corner-tag.mjs
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
// is, so it hands over what is ready and how many are working on
|
|
38
38
|
// `zero:corner-tag`; shared/corner-tag.mjs puts it newest first.
|
|
39
39
|
|
|
40
|
-
import { cardPlacement, hiddenUntil, keepCorner, readyNow, restingSpot, tagShows, HIDE_CHOICES } from '../shared/corner-tag.mjs';
|
|
40
|
+
import { cardPlacement, cornerPlace, heightFromDrag, EDGE_SLACK, hiddenUntil, keepCorner, newSince, notDismissed, readyNow, restingSpot, spotFor, tagScaleOn, tagShows, tagZoom, zoomFor, zoomStep, GRIPS, HIDE_CHOICES } from '../shared/corner-tag.mjs';
|
|
41
41
|
|
|
42
42
|
// How long the pointer may be off both the tag and its list before the list
|
|
43
43
|
// shuts: long enough to cross the gap between them, short enough to feel like
|
|
@@ -52,16 +52,32 @@ export const DRAG_FROM_PX = 6;
|
|
|
52
52
|
const FRAME_MS = 16;
|
|
53
53
|
// A press whose release never came is let go after this long.
|
|
54
54
|
const PRESS_GIVES_UP_MS = 60_000;
|
|
55
|
+
// How often the Mac's glass is asked whether it went light or dark, while the
|
|
56
|
+
// tray is on screen and only then. Drag the tray from a white page onto a dark
|
|
57
|
+
// one and the ink follows within this.
|
|
58
|
+
export const TONE_EVERY_MS = 500;
|
|
59
|
+
// How often the page is asked to measure again while the tray is on screen.
|
|
60
|
+
// The glass is placed from what the page last said, and once (w-93b3a1a796)
|
|
61
|
+
// it was left at an old size by something that was never reproduced; this is
|
|
62
|
+
// the longest that can now last.
|
|
63
|
+
export const MEASURE_EVERY_MS = 2000;
|
|
55
64
|
|
|
56
65
|
const HIDE_LABELS = { '5m': 'Hide for 5 minutes', '30m': 'Hide for 30 minutes', today: 'Hide for the rest of today' };
|
|
57
66
|
const CHANNELS = [
|
|
58
67
|
'zero:corner-tag', 'corner-tag:ready', 'corner-tag:size', 'corner-tag:hover', 'corner-tag:toggle',
|
|
59
68
|
'corner-tag:press', 'corner-tag:release', 'corner-tag:go', 'corner-tag:hide', 'corner-tag:settings', 'corner-tag:menu',
|
|
69
|
+
'corner-tag:clickable', 'corner-tag:resize', 'corner-tag:dismiss',
|
|
60
70
|
];
|
|
61
71
|
|
|
62
72
|
export function createCornerTag({
|
|
63
73
|
BrowserWindow, screen, ipcMain, Menu, powerMonitor, config, saveConfig, mainWindow, load, preload,
|
|
64
|
-
now = () => Date.now(), setTimer = setTimeout, clearTimer = clearTimeout, mac = process.platform === 'darwin',
|
|
74
|
+
count = () => {}, now = () => Date.now(), setTimer = setTimeout, clearTimer = clearTimeout, mac = process.platform === 'darwin',
|
|
75
|
+
// The Mac's own Liquid Glass (main/liquid-glass.mjs), or null, and then the
|
|
76
|
+
// page draws a glass of its own.
|
|
77
|
+
glass = null,
|
|
78
|
+
// What a notification has told you about since you were last at the app,
|
|
79
|
+
// [{ id, at }] (main/notify.mjs). All of it counts as new.
|
|
80
|
+
told = () => [],
|
|
65
81
|
}) {
|
|
66
82
|
const wins = { tag: null, card: null };
|
|
67
83
|
const loaded = { tag: false, card: false };
|
|
@@ -85,37 +101,168 @@ export function createCornerTag({
|
|
|
85
101
|
// leave and come back rather than opening under it.
|
|
86
102
|
let settling = false;
|
|
87
103
|
let gone = false;
|
|
88
|
-
|
|
89
|
-
|
|
104
|
+
// The last time the app's window was in front. Only what started waiting
|
|
105
|
+
// after it is counted (w-93b3a1a796).
|
|
106
|
+
let lookedAt = Number(config.cornerTagLookedAt) || 0;
|
|
107
|
+
// Anything handed over without the moment it started waiting is dated from
|
|
108
|
+
// when this file first saw it, rather than counted new forever.
|
|
109
|
+
const firstSeen = new Map();
|
|
110
|
+
// Threads crossed off the tray, and when (shared/corner-tag.mjs,
|
|
111
|
+
// notDismissed). Kept across a restart, forgotten once a thread leaves the list.
|
|
112
|
+
let dismissed = { ...(config.cornerTagDismissed ?? {}) };
|
|
113
|
+
const toldNow = () => { try { return told() ?? []; } catch { return []; } };
|
|
114
|
+
|
|
115
|
+
const fresh = () => readyNow(notDismissed(newSince(ready, lookedAt, toldNow()), dismissed), now());
|
|
90
116
|
const areas = () => screen.getAllDisplays().map((d) => d.workArea);
|
|
91
117
|
const areaOf = (r) => screen.getDisplayMatching({ x: Math.round(r.x), y: Math.round(r.y), width: Math.max(1, Math.round(r.width)), height: Math.max(1, Math.round(r.height)) }).workArea;
|
|
118
|
+
// THE CLEAR BAND. The tag's window is the glass plus a few points of nothing
|
|
119
|
+
// on every side, so the resize arrows can hang over its corners the way a
|
|
120
|
+
// window's own do. Everything that places the window therefore works against
|
|
121
|
+
// a screen grown by that band, which puts the GLASS where the gap from the
|
|
122
|
+
// edge is measured to, and the list is placed against the glass rather than
|
|
123
|
+
// the window. A click in the band is not the tag's: the page says so and the
|
|
124
|
+
// window lets it through (corner-tag:clickable).
|
|
125
|
+
let frame = 0;
|
|
126
|
+
const framed = (a) => (frame ? { x: a.x - frame, y: a.y - frame, width: a.width + 2 * frame, height: a.height + 2 * frame } : a);
|
|
127
|
+
// Where she may park it: the screen, plus the band, plus the slack that lets
|
|
128
|
+
// her tuck it past the edge (shared/corner-tag.mjs, EDGE_SLACK).
|
|
129
|
+
const parkable = (a) => {
|
|
130
|
+
const f = framed(a);
|
|
131
|
+
return { x: f.x - EDGE_SLACK, y: f.y - EDGE_SLACK, width: f.width + 2 * EDGE_SLACK, height: f.height + 2 * EDGE_SLACK };
|
|
132
|
+
};
|
|
133
|
+
// WHERE THE GLASS IS DRAWN INSIDE THAT WINDOW, as the page measured it. It
|
|
134
|
+
// used to be worked out from the band alone, which is only the truth while
|
|
135
|
+
// the window is exactly the size the page asked for — and a grip drag widens
|
|
136
|
+
// the window first and lets the page catch up, so the last guess of a drag
|
|
137
|
+
// can outlive it. The list then lined up with a window edge that was no
|
|
138
|
+
// longer the glass's: "it is a little too far to the right... it's just the
|
|
139
|
+
// right sides that seem to be improperly aligned" (2026-10-13).
|
|
140
|
+
let inset = null;
|
|
141
|
+
const glassOf = (r) => {
|
|
142
|
+
if (inset) return { x: r.x + inset.x, y: r.y + inset.y, width: inset.width, height: inset.height };
|
|
143
|
+
return frame ? { x: r.x + frame, y: r.y + frame, width: r.width - 2 * frame, height: r.height - 2 * frame } : r;
|
|
144
|
+
};
|
|
92
145
|
const inApp = () => {
|
|
93
146
|
try { return !!mainWindow && !mainWindow.isDestroyed() && mainWindow.isFocused(); } catch { return false; }
|
|
94
147
|
};
|
|
95
148
|
const alive = (part) => !!wins[part] && !wins[part].isDestroyed();
|
|
96
149
|
const rounded = (r) => ({ x: Math.round(r.x), y: Math.round(r.y), width: Math.round(r.width), height: Math.round(r.height) });
|
|
97
150
|
const send = (part, channel, payload) => { if (alive(part) && loaded[part]) wins[part].webContents.send(channel, payload); };
|
|
151
|
+
// Back to 100%, then measure: a zoomed page drew the tag smaller than the
|
|
152
|
+
// size it reported, and the glass stood out past it (w-93b3a1a796). Its own
|
|
153
|
+
// zoom only; the app's window keeps whatever zoom it has.
|
|
154
|
+
const atFull = (part) => { try { if (alive(part)) wins[part].webContents.setZoomFactor?.(1); } catch { /* measured in points anyway */ } };
|
|
155
|
+
const remeasure = (part) => { atFull(part); send(part, 'corner-tag:measure'); };
|
|
156
|
+
// How big the tag draws, from the width of the screen it is resting on. The
|
|
157
|
+
// page multiplies everything by it, reports the size it came out, and this
|
|
158
|
+
// file places that. A tag dragged to another screen is measured again there.
|
|
159
|
+
// ...and how much bigger or smaller than that she has dragged it to be.
|
|
160
|
+
let zoom = tagZoom(config.cornerTagZoom);
|
|
161
|
+
const scaleNow = () => tagScaleOn((tag ? areaOf(tag) : areas()[0] ?? { width: 0 }).width, zoom);
|
|
98
162
|
const sendState = () => {
|
|
99
|
-
const
|
|
163
|
+
const shown = fresh();
|
|
164
|
+
const s = { ready: shown, before: Math.max(0, ready.length - shown.length), working, now: now(), open, scale: scaleNow(), glass: !!glass };
|
|
100
165
|
send('tag', 'corner-tag:state', s);
|
|
101
166
|
send('card', 'corner-tag:state', s);
|
|
102
167
|
};
|
|
103
168
|
const shouldShow = () => tagShows({
|
|
104
|
-
on: config.cornerTag, hiddenUntil: config.cornerTagHiddenUntil ?? 0, now: now(), inApp: inApp(), ready: fresh().length,
|
|
169
|
+
on: config.cornerTag, hiddenUntil: config.cornerTagHiddenUntil ?? 0, now: now(), inApp: inApp(), ready: fresh().length,
|
|
105
170
|
});
|
|
106
171
|
// The list may only appear beside a tag that is on screen and should be.
|
|
107
172
|
// A click that was on its way while the tag hid must not open it alone.
|
|
108
173
|
const tagUp = () => !gone && alive('tag') && wins.tag.isVisible() && shouldShow();
|
|
109
174
|
|
|
175
|
+
// WHICH SCREEN IT BELONGS TO, AND WHERE ON IT. The corner it was parked in
|
|
176
|
+
// and the gap from that corner are what is remembered (shared/corner-tag.mjs,
|
|
177
|
+
// cornerPlace), because a point on the whole desktop stops meaning anything
|
|
178
|
+
// the moment a monitor is plugged in or taken away: every screen's
|
|
179
|
+
// coordinates move underneath it and the tag jumps.
|
|
180
|
+
let place = config.cornerTagPlace ?? null;
|
|
181
|
+
const screenFor = () => {
|
|
182
|
+
const known = tag ?? (config.cornerTagSpot ? { ...config.cornerTagSpot, ...tagSize } : null);
|
|
183
|
+
return known ? areaOf(known) : areas()[0];
|
|
184
|
+
};
|
|
185
|
+
// While a corner is held, the tag is placed from the corner opposite it
|
|
186
|
+
// rather than from its own last position, so the end she is not dragging
|
|
187
|
+
// does not creep across the screen as the size changes.
|
|
188
|
+
const resizeTo = (size) => {
|
|
189
|
+
if (!press?.grip || !tag) return;
|
|
190
|
+
tagSize = { width: Math.max(1, Math.round(size.width)), height: Math.max(1, Math.round(size.height)) };
|
|
191
|
+
const a = press.anchor;
|
|
192
|
+
tag = {
|
|
193
|
+
x: press.corner.endsWith('r') ? a.x : a.x - tagSize.width,
|
|
194
|
+
y: press.corner.startsWith('b') ? a.y : a.y - tagSize.height,
|
|
195
|
+
...tagSize,
|
|
196
|
+
};
|
|
197
|
+
if (alive('tag')) wins.tag.setBounds(rounded(tag));
|
|
198
|
+
};
|
|
199
|
+
|
|
110
200
|
const placeTag = () => {
|
|
111
|
-
const
|
|
201
|
+
const held = place ? spotFor(place, tagSize, framed(screenFor())) : null;
|
|
202
|
+
// A tag that has never been put anywhere starts its margin in from the
|
|
203
|
+
// screen's own edge; one she has parked may keep the slack she left it in.
|
|
204
|
+
const soFar = tag ? { x: tag.x, y: tag.y } : config.cornerTagSpot ?? null;
|
|
205
|
+
const spot = held ?? restingSpot(soFar, tagSize, areas().map(soFar ? parkable : framed));
|
|
112
206
|
tag = { ...spot, ...tagSize };
|
|
113
207
|
if (alive('tag')) wins.tag.setBounds(rounded(tag));
|
|
114
208
|
};
|
|
115
209
|
|
|
116
210
|
const placeCard = () => {
|
|
117
211
|
if (!tag || !cardSize || !alive('card')) return;
|
|
118
|
-
wins.card.setBounds(rounded(cardPlacement(tag, cardSize, areaOf(tag)).card));
|
|
212
|
+
wins.card.setBounds(rounded(cardPlacement(glassOf(tag), cardSize, areaOf(tag)).card));
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/* ------------------------- the Mac's own glass ------------------------- */
|
|
216
|
+
// Where each page draws its glass, and how round, as it last said. The Mac's
|
|
217
|
+
// glass is put exactly there, and the page draws no fill of its own.
|
|
218
|
+
const shape = { tag: null, card: null };
|
|
219
|
+
// Which way the tag's glass went over what is behind it. Only the tag is
|
|
220
|
+
// read: glass the list's size follows the Mac's own dark or light setting
|
|
221
|
+
// rather than the screen, so the list is held to the tag's reading.
|
|
222
|
+
let tone = null;
|
|
223
|
+
const toneSent = { tag: null, card: null };
|
|
224
|
+
let toneTimer = null;
|
|
225
|
+
const shareTone = () => {
|
|
226
|
+
for (const part of ['tag', 'card']) {
|
|
227
|
+
if (!tone || !alive(part) || !loaded[part] || toneSent[part] === tone) continue;
|
|
228
|
+
toneSent[part] = tone;
|
|
229
|
+
send(part, 'corner-tag:tone', tone);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
const readTone = () => {
|
|
233
|
+
if (!glass || !shape.tag || !alive('tag') || !wins.tag.isVisible()) return;
|
|
234
|
+
let read = null;
|
|
235
|
+
try { read = glass.tone(wins.tag); } catch { /* no reading, keep the last */ }
|
|
236
|
+
if ((read !== 'light' && read !== 'dark') || read === tone) return;
|
|
237
|
+
tone = read;
|
|
238
|
+
dress('card');
|
|
239
|
+
shareTone();
|
|
240
|
+
};
|
|
241
|
+
const stopTones = () => { if (toneTimer) { clearTimer(toneTimer); toneTimer = null; } };
|
|
242
|
+
const watchTones = () => {
|
|
243
|
+
if (!glass || toneTimer || gone) return;
|
|
244
|
+
let measuredAt = now();
|
|
245
|
+
const tick = () => {
|
|
246
|
+
toneTimer = null;
|
|
247
|
+
if (gone || !alive('tag') || !wins.tag.isVisible()) return;
|
|
248
|
+
readTone();
|
|
249
|
+
if (now() - measuredAt >= MEASURE_EVERY_MS) {
|
|
250
|
+
measuredAt = now();
|
|
251
|
+
remeasure('tag');
|
|
252
|
+
}
|
|
253
|
+
toneTimer = setTimer(tick, TONE_EVERY_MS);
|
|
254
|
+
};
|
|
255
|
+
toneTimer = setTimer(tick, TONE_EVERY_MS);
|
|
256
|
+
};
|
|
257
|
+
const dress = (part) => {
|
|
258
|
+
if (!glass || !shape[part] || !alive(part)) return;
|
|
259
|
+
// The tag is clear glass, so a light page shows through it the way a dark
|
|
260
|
+
// one does; the regular glass frosts over light pages. The list keeps the
|
|
261
|
+
// regular glass, because it is read.
|
|
262
|
+
const opts = part === 'tag' ? { radius: shape.tag.radius, style: 'clear' }
|
|
263
|
+
: tone ? { radius: shape.card.radius, tone } : { radius: shape.card.radius };
|
|
264
|
+
try { glass.set(wins[part], shape[part].rect, opts); } catch { return; }
|
|
265
|
+
if (part === 'tag') { readTone(); watchTones(); }
|
|
119
266
|
};
|
|
120
267
|
|
|
121
268
|
const make = (part) => {
|
|
@@ -149,6 +296,7 @@ export function createCornerTag({
|
|
|
149
296
|
win.setAlwaysOnTop(true, 'floating');
|
|
150
297
|
try { win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true, skipTransformProcessType: true }); } catch {}
|
|
151
298
|
win.on('closed', () => { wins[part] = null; loaded[part] = false; });
|
|
299
|
+
win.webContents.on('did-finish-load', () => atFull(part));
|
|
152
300
|
// A reload starts the handshake again: the window goes, anything half
|
|
153
301
|
// done with it is let go, and until the page says it is ready nothing is
|
|
154
302
|
// sent to it and it is not shown. The first load is not a reload.
|
|
@@ -156,6 +304,7 @@ export function createCornerTag({
|
|
|
156
304
|
if (!loaded[part]) return;
|
|
157
305
|
loaded[part] = false;
|
|
158
306
|
hovered[part] = false;
|
|
307
|
+
toneSent[part] = null;
|
|
159
308
|
if (part === 'tag') { endPress(true); if (win.isVisible()) win.hide(); }
|
|
160
309
|
closeCard();
|
|
161
310
|
});
|
|
@@ -176,7 +325,11 @@ export function createCornerTag({
|
|
|
176
325
|
}
|
|
177
326
|
|
|
178
327
|
const showCard = () => {
|
|
179
|
-
if (open && tagUp() && loaded.card && cardSize && alive('card') && !wins.card.isVisible())
|
|
328
|
+
if (open && tagUp() && loaded.card && cardSize && alive('card') && !wins.card.isVisible()) {
|
|
329
|
+
wins.card.showInactive();
|
|
330
|
+
remeasure('card');
|
|
331
|
+
readTone();
|
|
332
|
+
}
|
|
180
333
|
};
|
|
181
334
|
|
|
182
335
|
function openCard() {
|
|
@@ -257,6 +410,35 @@ export function createCornerTag({
|
|
|
257
410
|
closeCard();
|
|
258
411
|
send('tag', 'corner-tag:dragging', true);
|
|
259
412
|
}
|
|
413
|
+
// THE GRIP: the cursor sets the height, the page redraws everything from
|
|
414
|
+
// it, and what is remembered is how much bigger or smaller than this
|
|
415
|
+
// screen's automatic size that is.
|
|
416
|
+
if (press.grip) {
|
|
417
|
+
if (!press.moved) return;
|
|
418
|
+
const want = zoomFor(heightFromDrag(press.corner, press.fromHeight, dx, dy), areaOf(tag).width);
|
|
419
|
+
if (want === zoom) return;
|
|
420
|
+
const grew = want > zoom;
|
|
421
|
+
zoom = want;
|
|
422
|
+
sendState();
|
|
423
|
+
// THE WINDOW GROWS BEFORE THE PAGE DOES, never after. The page redraws
|
|
424
|
+
// at the new size and then reports it, a frame later; until that report
|
|
425
|
+
// the window is still the old rectangle, and a tag drawn larger than its
|
|
426
|
+
// window is a tag with its corners cut off, which is what she saw. So a
|
|
427
|
+
// growing drag widens the window on this side of the message straight
|
|
428
|
+
// away, by the same proportion, and the page's own measurement corrects
|
|
429
|
+
// it a frame later. Shrinking waits for the page: a window smaller than
|
|
430
|
+
// what is drawn in it clips, a window larger than it never does.
|
|
431
|
+
if (grew) {
|
|
432
|
+
// The glass the page is about to draw is the scale it was just handed
|
|
433
|
+
// (28 points times it), and everything in it grows together, so the
|
|
434
|
+
// width goes up by the same proportion until the page says otherwise.
|
|
435
|
+
// The window is that glass plus the band on every side.
|
|
436
|
+
const was = { w: Math.max(1, tagSize.width - 2 * frame), h: Math.max(1, tagSize.height - 2 * frame) };
|
|
437
|
+
const h = Math.ceil(28 * scaleNow());
|
|
438
|
+
resizeTo({ width: Math.ceil(was.w * h / was.h) + 2 * frame, height: h + 2 * frame });
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
260
442
|
if (press.moved && alive('tag')) {
|
|
261
443
|
tag = { ...tag, x: press.from.x + dx, y: press.from.y + dy };
|
|
262
444
|
wins.tag.setPosition(Math.round(tag.x), Math.round(tag.y));
|
|
@@ -280,11 +462,31 @@ export function createCornerTag({
|
|
|
280
462
|
const p = press;
|
|
281
463
|
if (!cancelled) follow();
|
|
282
464
|
press = null;
|
|
465
|
+
if (p.grip) {
|
|
466
|
+
// The pointer is still on the tag, so the list waits for it to leave and
|
|
467
|
+
// come back, exactly as it does after a move. A press on a grip that
|
|
468
|
+
// never moved is not a click either: it does not open the app.
|
|
469
|
+
send('tag', 'corner-tag:dragging', false);
|
|
470
|
+
if (!p.moved) return;
|
|
471
|
+
// It has been dragged into a corner of its screen; let it settle back
|
|
472
|
+
// against the edge it is parked at, and remember where that is.
|
|
473
|
+
tag = { ...restingSpot({ x: tag.x, y: tag.y }, tagSize, areas().map(parkable)), ...tagSize };
|
|
474
|
+
if (alive('tag')) wins.tag.setBounds(rounded(tag));
|
|
475
|
+
place = cornerPlace(tag, framed(areaOf(tag)));
|
|
476
|
+
saveConfig(config, { cornerTagZoom: zoom, cornerTagSpot: { x: Math.round(tag.x), y: Math.round(tag.y) }, cornerTagPlace: place });
|
|
477
|
+
if (open) placeCard();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
283
480
|
if (p.moved) {
|
|
284
|
-
tag = { ...restingSpot({ x: tag.x, y: tag.y }, tagSize, areas()), ...tagSize };
|
|
481
|
+
tag = { ...restingSpot({ x: tag.x, y: tag.y }, tagSize, areas().map(parkable)), ...tagSize };
|
|
285
482
|
if (alive('tag')) wins.tag.setBounds(rounded(tag));
|
|
286
|
-
|
|
483
|
+
// Where she left it, said as a corner and a gap so it means the same
|
|
484
|
+
// thing on whatever screen it is on next.
|
|
485
|
+
place = cornerPlace(tag, framed(areaOf(tag)));
|
|
486
|
+
saveConfig(config, { cornerTagSpot: { x: Math.round(tag.x), y: Math.round(tag.y) }, cornerTagPlace: place });
|
|
287
487
|
send('tag', 'corner-tag:dragging', false);
|
|
488
|
+
// Dragged onto another screen, it draws at that screen's size.
|
|
489
|
+
sendState();
|
|
288
490
|
} else if (!cancelled) {
|
|
289
491
|
cancelOpen();
|
|
290
492
|
goTo(null);
|
|
@@ -310,13 +512,21 @@ export function createCornerTag({
|
|
|
310
512
|
// A hide comes back by itself the moment it runs out.
|
|
311
513
|
if (until > now()) wake = setTimer(refresh, until - now() + 50);
|
|
312
514
|
if (!shouldShow()) {
|
|
515
|
+
stopTones();
|
|
313
516
|
if (alive('tag') && wins.tag.isVisible()) { wins.tag.hide(); reset(); }
|
|
314
517
|
else if (open) closeCard();
|
|
315
518
|
return;
|
|
316
519
|
}
|
|
317
520
|
if (!alive('tag')) { make('tag'); return; }
|
|
318
521
|
sendState();
|
|
319
|
-
if (loaded.tag && !wins.tag.isVisible())
|
|
522
|
+
if (loaded.tag && !wins.tag.isVisible()) {
|
|
523
|
+
wins.tag.showInactive();
|
|
524
|
+
// A page that redrew while hidden may never have said its new size, and
|
|
525
|
+
// the glass only moves when it does.
|
|
526
|
+
remeasure('tag');
|
|
527
|
+
readTone();
|
|
528
|
+
watchTones();
|
|
529
|
+
}
|
|
320
530
|
}
|
|
321
531
|
|
|
322
532
|
const bringAppForward = () => {
|
|
@@ -339,6 +549,7 @@ export function createCornerTag({
|
|
|
339
549
|
const until = hiddenUntil(choice, now());
|
|
340
550
|
if (!until) return;
|
|
341
551
|
saveConfig(config, { cornerTagHiddenUntil: until });
|
|
552
|
+
count('tag_hidden', { seconds: Math.round((until - now()) / 1000) });
|
|
342
553
|
refresh();
|
|
343
554
|
};
|
|
344
555
|
|
|
@@ -351,8 +562,20 @@ export function createCornerTag({
|
|
|
351
562
|
|
|
352
563
|
/* ------------------------------ from the app's page ---------------------- */
|
|
353
564
|
ipcMain.handle('zero:corner-tag', (_e, state) => {
|
|
354
|
-
|
|
565
|
+
const given = Array.isArray(state?.ready) ? state.ready : [];
|
|
566
|
+
const ids = new Set(given.map((r) => r.id));
|
|
567
|
+
for (const id of firstSeen.keys()) if (!ids.has(id)) firstSeen.delete(id);
|
|
568
|
+
ready = given.map((r) => {
|
|
569
|
+
if (Number.isFinite(r.since)) return r;
|
|
570
|
+
if (!firstSeen.has(r.id)) firstSeen.set(r.id, now());
|
|
571
|
+
return { ...r, since: firstSeen.get(r.id) };
|
|
572
|
+
});
|
|
355
573
|
working = Number.isFinite(state?.working) ? state.working : 0;
|
|
574
|
+
const kept = Object.fromEntries(Object.entries(dismissed).filter(([id]) => ids.has(id)));
|
|
575
|
+
if (Object.keys(kept).length !== Object.keys(dismissed).length) {
|
|
576
|
+
dismissed = kept;
|
|
577
|
+
try { saveConfig(config, { cornerTagDismissed: dismissed }); } catch { /* kept for this run only */ }
|
|
578
|
+
}
|
|
356
579
|
refresh();
|
|
357
580
|
return null;
|
|
358
581
|
});
|
|
@@ -362,6 +585,7 @@ export function createCornerTag({
|
|
|
362
585
|
const part = partOf(p);
|
|
363
586
|
loaded[part] = true;
|
|
364
587
|
sendState();
|
|
588
|
+
shareTone();
|
|
365
589
|
if (part === 'tag') refresh();
|
|
366
590
|
else if (open) { placeCard(); showCard(); }
|
|
367
591
|
return null;
|
|
@@ -370,17 +594,37 @@ export function createCornerTag({
|
|
|
370
594
|
ipcMain.handle('corner-tag:size', (_e, p) => {
|
|
371
595
|
if (!p || !(p.width > 0) || !(p.height > 0)) return null;
|
|
372
596
|
const size = { width: Math.ceil(p.width), height: Math.ceil(p.height) };
|
|
597
|
+
const radius = (rect) => (Number.isFinite(p.radius) && p.radius >= 0 ? p.radius : rect.height / 2);
|
|
373
598
|
if (partOf(p) === 'card') {
|
|
374
599
|
cardSize = size;
|
|
600
|
+
const rect = { x: 0, y: 0, ...size };
|
|
601
|
+
shape.card = { rect, radius: radius(rect) };
|
|
602
|
+
dress('card');
|
|
375
603
|
if (open) { placeCard(); showCard(); }
|
|
376
604
|
return null;
|
|
377
605
|
}
|
|
606
|
+
if (p.glass && Number.isFinite(p.glass.x) && p.glass.width > 0) {
|
|
607
|
+
inset = {
|
|
608
|
+
x: Math.max(0, Math.round(p.glass.x)), y: Math.max(0, Math.round(p.glass.y)),
|
|
609
|
+
width: Math.round(p.glass.width), height: Math.round(p.glass.height),
|
|
610
|
+
};
|
|
611
|
+
frame = inset.x;
|
|
612
|
+
} else {
|
|
613
|
+
// Never the last rectangle: it belongs to a size the tag no longer is.
|
|
614
|
+
inset = null;
|
|
615
|
+
}
|
|
616
|
+
const rect = inset ?? glassOf({ x: 0, y: 0, ...size });
|
|
617
|
+
shape.tag = { rect, radius: radius(rect) };
|
|
618
|
+
dress('tag');
|
|
619
|
+
// Mid-resize the page's own measurement is the truth: it places the tag
|
|
620
|
+
// from the corner she is not holding.
|
|
621
|
+
if (press?.grip) { resizeTo(size); if (open) placeCard(); return null; }
|
|
378
622
|
tagSize = size;
|
|
379
623
|
// Nothing moves under the pointer while it is dragging.
|
|
380
624
|
if (press?.moved) return null;
|
|
381
625
|
// The first measure places it where she left it. Every one after keeps
|
|
382
626
|
// the edge nearest its corner still as the words change length.
|
|
383
|
-
tag = tag && measured ? keepCorner(tag, tagSize, areaOf(tag)) : null;
|
|
627
|
+
tag = tag && measured ? keepCorner(tag, tagSize, framed(areaOf(tag))) : null;
|
|
384
628
|
measured = true;
|
|
385
629
|
placeTag();
|
|
386
630
|
if (open) placeCard();
|
|
@@ -411,21 +655,87 @@ export function createCornerTag({
|
|
|
411
655
|
ipcMain.handle('corner-tag:press', (_e, at) => {
|
|
412
656
|
const pt = Number.isFinite(at?.x) && Number.isFinite(at?.y) ? { x: at.x, y: at.y } : screen.getCursorScreenPoint?.();
|
|
413
657
|
if (!pt || !tag || !tagUp() || press) return null;
|
|
414
|
-
|
|
658
|
+
const corner = GRIPS.includes(at?.corner) ? at.corner : 'br';
|
|
659
|
+
press = {
|
|
660
|
+
start: pt, from: { x: tag.x, y: tag.y }, moved: false, at: now(), timer: null,
|
|
661
|
+
// A press on a grip resizes instead of moving: the corner she has hold
|
|
662
|
+
// of follows the cursor, and the one opposite it stays exactly where it
|
|
663
|
+
// is, the way a window resizes.
|
|
664
|
+
// THE GLASS, NOT THE WINDOW. The window is the glass plus the band on
|
|
665
|
+
// every side; measuring the window here asked for the band's height
|
|
666
|
+
// again on the first pull of the cursor, and the tag leapt (she met it
|
|
667
|
+
// in the running app: "it jumps to about three times its size").
|
|
668
|
+
grip: !!at?.grip, corner, fromHeight: Math.max(1, tagSize.height - 2 * frame),
|
|
669
|
+
anchor: {
|
|
670
|
+
x: corner.endsWith('r') ? tag.x : tag.x + tag.width,
|
|
671
|
+
y: corner.startsWith('b') ? tag.y : tag.y + tag.height,
|
|
672
|
+
},
|
|
673
|
+
};
|
|
415
674
|
press.timer = setTimer(step, FRAME_MS);
|
|
416
675
|
return null;
|
|
417
676
|
});
|
|
418
677
|
ipcMain.handle('corner-tag:release', (_e, p) => { endPress(!!p?.cancelled); return null; });
|
|
419
678
|
|
|
420
679
|
ipcMain.handle('corner-tag:go', (_e, id) => { goTo(id); return null; });
|
|
680
|
+
// The X on a line of the list: off the tray, and only the tray.
|
|
681
|
+
ipcMain.handle('corner-tag:dismiss', (_e, id) => {
|
|
682
|
+
if (typeof id !== 'string' || !fresh().some((r) => r.id === id)) return null;
|
|
683
|
+
dismissed = { ...dismissed, [id]: now() };
|
|
684
|
+
try { saveConfig(config, { cornerTagDismissed: dismissed }); } catch { /* kept for this run only */ }
|
|
685
|
+
refresh();
|
|
686
|
+
return null;
|
|
687
|
+
});
|
|
421
688
|
ipcMain.handle('corner-tag:hide', (_e, choice) => { hide(choice); return null; });
|
|
422
689
|
ipcMain.handle('corner-tag:settings', () => { openSettings(); return null; });
|
|
423
690
|
|
|
424
691
|
// A right-click on the tag offers the same choices as the list does.
|
|
692
|
+
// One place sets the size, whether it came from the grip or from the menu:
|
|
693
|
+
// the page is told, it redraws and reports what it came out, and that is
|
|
694
|
+
// what gets placed.
|
|
695
|
+
const setZoom = (next) => {
|
|
696
|
+
zoom = tagZoom(next);
|
|
697
|
+
saveConfig(config, { cornerTagZoom: zoom });
|
|
698
|
+
sendState();
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// WHAT IS UNDER THE POINTER IN THE CLEAR BAND IS NOT THE TAG. The window is
|
|
702
|
+
// bigger than the glass now, so every click in that band would otherwise be
|
|
703
|
+
// swallowed by a transparent window floating over somebody's work — the
|
|
704
|
+
// exact fault that took the first drawn shadow out. The page watches the
|
|
705
|
+
// pointer (it keeps getting moves either way, because the window forwards
|
|
706
|
+
// them) and says when the thing under it is the glass or one of its corner
|
|
707
|
+
// arrows; the rest of the time the click goes straight through.
|
|
708
|
+
//
|
|
709
|
+
// It is never switched while a press is live: a press that began on the tag
|
|
710
|
+
// ends on the tag, wherever the cursor has got to.
|
|
711
|
+
let clickable = null;
|
|
712
|
+
const letClicksThrough = (on) => {
|
|
713
|
+
if (!alive('tag') || press) return;
|
|
714
|
+
if (clickable === on) return;
|
|
715
|
+
clickable = on;
|
|
716
|
+
try { wins.tag.setIgnoreMouseEvents?.(!on, { forward: true }); } catch { /* an older Electron: the band stays clickable */ }
|
|
717
|
+
};
|
|
718
|
+
ipcMain.handle('corner-tag:clickable', (_e, p) => { letClicksThrough(!!p?.on); return null; });
|
|
719
|
+
|
|
720
|
+
// A notch bigger or smaller, from the list's foot. The list stays open and
|
|
721
|
+
// is placed again as the tag changes size, because the whole point of
|
|
722
|
+
// sizing it from there is watching the tag while you do it.
|
|
723
|
+
ipcMain.handle('corner-tag:resize', (_e, p) => {
|
|
724
|
+
const by = Number(p?.by) < 0 ? -1 : 1;
|
|
725
|
+
setZoom(zoomStep(zoom, by));
|
|
726
|
+
return null;
|
|
727
|
+
});
|
|
728
|
+
|
|
425
729
|
ipcMain.handle('corner-tag:menu', () => {
|
|
426
730
|
if (!alive('tag')) return null;
|
|
427
731
|
closeCard();
|
|
428
732
|
Menu.buildFromTemplate([
|
|
733
|
+
// Dragging the grip is the way most people will find; this is the way
|
|
734
|
+
// somebody looks second, and the only way back to the automatic size.
|
|
735
|
+
{ label: 'Bigger', click: () => setZoom(zoomStep(zoom, 1)) },
|
|
736
|
+
{ label: 'Smaller', click: () => setZoom(zoomStep(zoom, -1)) },
|
|
737
|
+
{ label: 'Size for this screen', enabled: zoom !== 1, click: () => setZoom(1) },
|
|
738
|
+
{ type: 'separator' },
|
|
429
739
|
...HIDE_CHOICES.map((c) => ({ label: HIDE_LABELS[c.key], click: () => hide(c.key) })),
|
|
430
740
|
{ type: 'separator' },
|
|
431
741
|
{ label: 'Turn off in Settings…', click: openSettings },
|
|
@@ -436,17 +746,27 @@ export function createCornerTag({
|
|
|
436
746
|
/* ------------------------------ from the system -------------------------- */
|
|
437
747
|
// A screen that comes, goes or changes shape: the list shuts and the tag is
|
|
438
748
|
// pulled back onto a screen if it was left hanging off one.
|
|
749
|
+
// A monitor plugged in or taken away: back to the corner it was parked in,
|
|
750
|
+
// the same gap in, on whichever screen now holds it. It used to be clamped
|
|
751
|
+
// from its old desktop point, which is what made it jump.
|
|
439
752
|
const reseat = () => {
|
|
440
753
|
if (gone) return;
|
|
441
754
|
closeCard();
|
|
442
755
|
if (!tag || press) return;
|
|
443
|
-
|
|
444
|
-
|
|
756
|
+
placeTag();
|
|
757
|
+
sendState();
|
|
445
758
|
};
|
|
446
759
|
// After sleep the screens may have changed and a hide may have run out.
|
|
447
760
|
const woke = () => { reseat(); refresh(); };
|
|
448
761
|
|
|
449
|
-
|
|
762
|
+
// Being in the app is looking at everything it holds. The moment you leave
|
|
763
|
+
// is kept, so a restart does not count it all again.
|
|
764
|
+
const onFocus = () => { lookedAt = now(); refresh(); };
|
|
765
|
+
const onBlur = () => {
|
|
766
|
+
lookedAt = now();
|
|
767
|
+
try { saveConfig(config, { cornerTagLookedAt: lookedAt }); } catch { /* kept for this run only */ }
|
|
768
|
+
refresh();
|
|
769
|
+
};
|
|
450
770
|
// The app's window is gone, and the tag goes with it. Left behind, it would
|
|
451
771
|
// hold the app open with nothing behind it and nothing it could open.
|
|
452
772
|
const dispose = () => {
|
|
@@ -456,6 +776,7 @@ export function createCornerTag({
|
|
|
456
776
|
cancelClose();
|
|
457
777
|
if (wake) { clearTimer(wake); wake = null; }
|
|
458
778
|
if (watching) { clearTimer(watching); watching = null; }
|
|
779
|
+
stopTones();
|
|
459
780
|
cancelOpen();
|
|
460
781
|
for (const part of ['tag', 'card']) if (alive(part)) wins[part].destroy();
|
|
461
782
|
for (const ch of CHANNELS) { try { ipcMain.removeHandler?.(ch); } catch {} }
|
|
@@ -470,7 +791,7 @@ export function createCornerTag({
|
|
|
470
791
|
|
|
471
792
|
if (mainWindow) {
|
|
472
793
|
mainWindow.on('focus', onFocus);
|
|
473
|
-
mainWindow.on('blur',
|
|
794
|
+
mainWindow.on('blur', onBlur);
|
|
474
795
|
mainWindow.on('closed', dispose);
|
|
475
796
|
}
|
|
476
797
|
try {
|
package/main/dev-window.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// The change that hurts is one to App.tsx that adds, removes or moves a
|
|
9
9
|
// hook, which is what a branch switch does to it. React Fast Refresh keeps a
|
|
10
10
|
// component's state only while its hooks match, so App is rebuilt from nothing:
|
|
11
|
-
// `snap` is null again, App.tsx draws
|
|
11
|
+
// `snap` is null again, App.tsx draws its boot screen (`<Booting />`), and
|
|
12
12
|
// every child of App is thrown away and remade. The window drops back to the
|
|
13
13
|
// loading page, and an open artifact closes and reopens. The
|
|
14
14
|
// page never navigates, which is why nothing in the dev server's log looked bad
|