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.
Files changed (278) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/agent-sessions.mjs +282 -268
  9. package/main/agents.mjs +179 -466
  10. package/main/analytics.mjs +12 -2
  11. package/main/answer-modes.mjs +103 -37
  12. package/main/approval-prompt-core.mjs +197 -0
  13. package/main/approval-prompt-server.mjs +4 -138
  14. package/main/approvals.mjs +101 -297
  15. package/main/artifact-path.mjs +47 -18
  16. package/main/claude-input.mjs +48 -4
  17. package/main/claude-plan.mjs +48 -0
  18. package/main/claude-session-open.mjs +28 -0
  19. package/main/claude-usage-process.mjs +40 -0
  20. package/main/claude-usage.mjs +18 -40
  21. package/main/cloud/account.mjs +179 -0
  22. package/main/cloud/aws.mjs +127 -0
  23. package/main/cloud/browser-return.mjs +58 -0
  24. package/main/cloud/clients.mjs +15 -0
  25. package/main/cloud/codex-cloud.mjs +53 -0
  26. package/main/cloud/config.mjs +107 -0
  27. package/main/cloud/docs.mjs +219 -0
  28. package/main/cloud/exec.mjs +27 -0
  29. package/main/cloud/host.mjs +165 -0
  30. package/main/cloud/kit.mjs +80 -0
  31. package/main/cloud/logins.mjs +543 -0
  32. package/main/cloud/managed.mjs +115 -0
  33. package/main/cloud/marker.mjs +104 -0
  34. package/main/cloud/org.mjs +53 -0
  35. package/main/cloud/reuse.mjs +208 -0
  36. package/main/cloud/run-cli.mjs +177 -0
  37. package/main/cloud/run.mjs +1934 -0
  38. package/main/cloud/usage.mjs +76 -0
  39. package/main/cloud-permission.mjs +51 -0
  40. package/main/codex-account.mjs +78 -0
  41. package/main/codex-app-server.mjs +276 -409
  42. package/main/codex-approvals.mjs +148 -433
  43. package/main/codex-models.mjs +20 -2
  44. package/main/codex-session.mjs +212 -507
  45. package/main/codex-threads-worker.mjs +12 -0
  46. package/main/codex-threads.mjs +71 -3
  47. package/main/codex-usage-file.mjs +5 -3
  48. package/main/codex-usage.mjs +56 -7
  49. package/main/codex-watch.mjs +17 -3
  50. package/main/codex.mjs +95 -363
  51. package/main/config.mjs +11 -1
  52. package/main/corner-tag.mjs +340 -19
  53. package/main/dev-window.mjs +1 -1
  54. package/main/engine-bin.mjs +232 -282
  55. package/main/engine-setup.mjs +21 -9
  56. package/main/feedback.mjs +3 -25
  57. package/main/folder-pictures.mjs +2 -2
  58. package/main/freeze-recorder-thread.mjs +86 -0
  59. package/main/freeze-recorder.mjs +146 -0
  60. package/main/fresh-user.mjs +20 -9
  61. package/main/git-change-offthread.mjs +17 -3
  62. package/main/git-change-thread.mjs +5 -2
  63. package/main/harnesses/claude.mjs +32 -11
  64. package/main/harnesses/codex.mjs +9 -2
  65. package/main/importable-threads.mjs +64 -6
  66. package/main/ipc.mjs +362 -111
  67. package/main/leftovers.mjs +117 -25
  68. package/main/liquid-glass.mjs +26 -0
  69. package/main/live-replies.mjs +10 -4
  70. package/main/main-went-red.mjs +330 -0
  71. package/main/main.mjs +199 -84
  72. package/main/memory-gate-history.mjs +36 -10
  73. package/main/memory-gate-server.mjs +62 -0
  74. package/main/memory-gate.mjs +14 -1
  75. package/main/menu.mjs +4 -2
  76. package/main/message-priority.mjs +2 -2
  77. package/main/mic.mjs +38 -0
  78. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  79. package/main/new-chats-watch.mjs +110 -0
  80. package/main/new-user-window.mjs +25 -0
  81. package/main/notify.mjs +24 -8
  82. package/main/phone-alerts.mjs +117 -0
  83. package/main/phone-inbox.mjs +625 -0
  84. package/main/phone-link.mjs +29 -9
  85. package/main/phone-uploads.mjs +476 -0
  86. package/main/picture-shape.mjs +114 -0
  87. package/main/quit-sequence.mjs +143 -0
  88. package/main/refresh-agent-models.mjs +15 -2
  89. package/main/relay-client.mjs +69 -1
  90. package/main/repeats.mjs +10 -4
  91. package/main/row-label.mjs +12 -41
  92. package/main/run-change.mjs +13 -0
  93. package/main/run-ledger.mjs +135 -0
  94. package/main/save-attachments.mjs +41 -0
  95. package/main/serve.mjs +45 -3
  96. package/main/settings.mjs +220 -44
  97. package/main/shell-path.mjs +185 -37
  98. package/main/ship-conflict-brief.mjs +119 -0
  99. package/main/ship-queue.mjs +115 -19
  100. package/main/source-updater.mjs +18 -10
  101. package/main/stage-attachment.mjs +68 -0
  102. package/main/stall-watch.mjs +50 -0
  103. package/main/store/home.mjs +87 -232
  104. package/main/store/paths.mjs +11 -0
  105. package/main/store/teammates-files.mjs +99 -0
  106. package/main/store/work-items.mjs +231 -13
  107. package/main/store.mjs +226 -52
  108. package/main/supervisor.mjs +3103 -341
  109. package/main/task-folders.mjs +430 -30
  110. package/main/task-terminals.mjs +110 -34
  111. package/main/team/index.mjs +114 -10
  112. package/main/team/memory-cloud.mjs +183 -1
  113. package/main/team/room-hours.mjs +48 -0
  114. package/main/team/room-setup.mjs +31 -0
  115. package/main/team/rooms-memory.mjs +454 -0
  116. package/main/team/rooms-shape.mjs +42 -0
  117. package/main/team/rooms-supabase.mjs +324 -0
  118. package/main/team/rooms.mjs +575 -0
  119. package/main/team/session.mjs +96 -26
  120. package/main/team/supabase-backend.mjs +180 -0
  121. package/main/team/sync.mjs +109 -1
  122. package/main/teammates.mjs +580 -0
  123. package/main/terminal-host.mjs +143 -0
  124. package/main/terminal-hosts.mjs +261 -0
  125. package/main/transcript-search.mjs +80 -0
  126. package/main/webview-guard.mjs +45 -0
  127. package/main/worktree-include.mjs +90 -219
  128. package/mcp/core/documents.mjs +3 -1
  129. package/mcp/core/repeats.mjs +88 -0
  130. package/mcp/core/teammates.mjs +222 -0
  131. package/mcp/core/work.mjs +76 -4
  132. package/mcp/stdio.mjs +24 -11
  133. package/mcp/tools.mjs +116 -8
  134. package/package.json +21 -5
  135. package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
  136. package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
  137. package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
  138. package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
  139. package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
  140. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  141. package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
  142. package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
  143. package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
  144. package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
  145. package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
  146. package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
  147. package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
  148. package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
  149. package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
  150. package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
  151. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  152. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  153. package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
  154. package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
  155. package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
  156. package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
  157. package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
  158. package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
  159. package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
  160. package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
  161. package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
  162. package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
  163. package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
  164. package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
  165. package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
  166. package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
  167. package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
  168. package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
  169. package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
  170. package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
  171. package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
  172. package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
  173. package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
  174. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  175. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  176. package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
  177. package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
  178. package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
  179. package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
  180. package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
  181. package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
  182. package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
  183. package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
  184. package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
  185. package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
  186. package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
  187. package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
  188. package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
  189. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  190. package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
  191. package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
  192. package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
  193. package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
  194. package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
  195. package/renderer/dist/corner-tag.html +3 -3
  196. package/renderer/dist/index.html +3 -3
  197. package/scripts/after-install.mjs +30 -8
  198. package/scripts/cloud-shim.pl +57 -0
  199. package/shared/account-names.d.mts +6 -0
  200. package/shared/account-names.mjs +15 -0
  201. package/shared/agent-hours.mjs +136 -0
  202. package/shared/agent-import.mjs +60 -34
  203. package/shared/agents.mjs +126 -522
  204. package/shared/analytics-events.mjs +12 -0
  205. package/shared/approval-card.mjs +36 -0
  206. package/shared/bridge-map.mjs +27 -1
  207. package/shared/claude-code-shape.mjs +6 -0
  208. package/shared/claude-usage.d.mts +1 -0
  209. package/shared/claude-usage.mjs +42 -1
  210. package/shared/cloud-hand-back.d.mts +7 -0
  211. package/shared/cloud-hand-back.mjs +20 -0
  212. package/shared/cloud-kit.d.mts +219 -0
  213. package/shared/cloud-kit.mjs +341 -0
  214. package/shared/cloud-login-name.mjs +4 -0
  215. package/shared/cloud-start.mjs +17 -0
  216. package/shared/cloud-versions.mjs +23 -0
  217. package/shared/codex-import.mjs +2 -2
  218. package/shared/codex-modes.d.mts +22 -9
  219. package/shared/codex-modes.mjs +67 -11
  220. package/shared/codex-usage.mjs +108 -2
  221. package/shared/corner-tag.d.mts +23 -1
  222. package/shared/corner-tag.mjs +284 -29
  223. package/shared/dead-run-trace.mjs +19 -2
  224. package/shared/engines.mjs +70 -309
  225. package/shared/feedback.d.mts +7 -0
  226. package/shared/feedback.mjs +25 -0
  227. package/shared/first-run-practice.mjs +17 -24
  228. package/shared/harness-definitions.mjs +8 -0
  229. package/shared/instruction-defaults.json +2 -2
  230. package/shared/key-caps.d.mts +14 -0
  231. package/shared/key-caps.mjs +68 -0
  232. package/shared/message-edits.d.mts +7 -0
  233. package/shared/message-edits.mjs +35 -0
  234. package/shared/move-thread.d.mts +24 -0
  235. package/shared/move-thread.mjs +112 -0
  236. package/shared/named-threads.mjs +72 -0
  237. package/shared/not-shipped.d.mts +22 -0
  238. package/shared/not-shipped.mjs +89 -0
  239. package/shared/notify-rules.d.mts +6 -0
  240. package/shared/notify-rules.mjs +36 -8
  241. package/shared/phone-files.mjs +28 -0
  242. package/shared/phone-settings.mjs +118 -0
  243. package/shared/phone-snapshot.d.mts +16 -0
  244. package/shared/phone-snapshot.mjs +348 -0
  245. package/shared/picture-formats.d.mts +1 -0
  246. package/shared/picture-formats.mjs +13 -0
  247. package/shared/rank.d.mts +2 -1
  248. package/shared/rank.mjs +32 -19
  249. package/shared/repeats.d.mts +8 -0
  250. package/shared/repeats.mjs +33 -2
  251. package/shared/room-cards.mjs +167 -0
  252. package/shared/room-links.d.mts +4 -0
  253. package/shared/room-links.mjs +21 -0
  254. package/shared/runs-on.d.mts +3 -3
  255. package/shared/runs-on.mjs +8 -5
  256. package/shared/shipped-note.d.mts +4 -0
  257. package/shared/shipped-note.mjs +6 -0
  258. package/shared/shortcuts.mjs +14 -6
  259. package/shared/spawn-trouble.d.mts +4 -0
  260. package/shared/spawn-trouble.mjs +443 -40
  261. package/shared/team-rules.mjs +9 -2
  262. package/shared/teammates.d.mts +19 -0
  263. package/shared/teammates.mjs +756 -0
  264. package/shared/terminal-focus.d.mts +1 -0
  265. package/shared/terminal-focus.mjs +13 -0
  266. package/shared/thread-cards.mjs +4 -1
  267. package/shared/usage.d.mts +9 -0
  268. package/shared/usage.mjs +49 -3
  269. package/shared/websites.d.mts +15 -0
  270. package/shared/websites.mjs +61 -0
  271. package/shared/work-items.mjs +127 -30
  272. package/shared/work-lines.mjs +92 -244
  273. package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
  274. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
  275. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
  276. package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
  277. package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
  278. 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.
@@ -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
- const fresh = () => readyNow(ready, now());
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 s = { ready: fresh(), working, now: now(), open };
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, working,
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 spot = restingSpot(tag ? { x: tag.x, y: tag.y } : config.cornerTagSpot ?? null, tagSize, areas());
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()) wins.card.showInactive();
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
- saveConfig(config, { cornerTagSpot: { x: Math.round(tag.x), y: Math.round(tag.y) } });
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()) wins.tag.showInactive();
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
- ready = Array.isArray(state?.ready) ? state.ready : [];
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
- press = { start: pt, from: { x: tag.x, y: tag.y }, moved: false, at: now(), timer: null };
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
- tag = { ...restingSpot({ x: tag.x, y: tag.y }, tagSize, areas()), ...tagSize };
444
- if (alive('tag')) wins.tag.setBounds(rounded(tag));
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
- const onFocus = () => refresh();
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', onFocus);
794
+ mainWindow.on('blur', onBlur);
474
795
  mainWindow.on('closed', dispose);
475
796
  }
476
797
  try {
@@ -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 `<div className="boot">the app</div>`, and
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