agentbox-app 0.1.12 → 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 (370) 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/acp.mjs +296 -0
  9. package/main/agent-homes.mjs +158 -0
  10. package/main/agent-sessions.mjs +311 -260
  11. package/main/agent-updates.mjs +9 -22
  12. package/main/agents.mjs +179 -466
  13. package/main/analytics.mjs +42 -2
  14. package/main/answer-modes.mjs +103 -37
  15. package/main/approval-prompt-core.mjs +197 -0
  16. package/main/approval-prompt-server.mjs +4 -138
  17. package/main/approvals.mjs +101 -297
  18. package/main/artifact-path.mjs +47 -18
  19. package/main/claude-bin.mjs +45 -311
  20. package/main/claude-input.mjs +57 -7
  21. package/main/claude-launch-env.mjs +54 -0
  22. package/main/claude-models.mjs +39 -7
  23. package/main/claude-plan.mjs +64 -16
  24. package/main/claude-session-open.mjs +28 -0
  25. package/main/claude-usage-process.mjs +40 -0
  26. package/main/claude-usage.mjs +94 -59
  27. package/main/cloud/account.mjs +179 -0
  28. package/main/cloud/aws.mjs +127 -0
  29. package/main/cloud/browser-return.mjs +58 -0
  30. package/main/cloud/clients.mjs +15 -0
  31. package/main/cloud/codex-cloud.mjs +53 -0
  32. package/main/cloud/config.mjs +107 -0
  33. package/main/cloud/docs.mjs +219 -0
  34. package/main/cloud/exec.mjs +27 -0
  35. package/main/cloud/host.mjs +165 -0
  36. package/main/cloud/kit.mjs +80 -0
  37. package/main/cloud/logins.mjs +543 -0
  38. package/main/cloud/managed.mjs +115 -0
  39. package/main/cloud/marker.mjs +104 -0
  40. package/main/cloud/org.mjs +53 -0
  41. package/main/cloud/reuse.mjs +208 -0
  42. package/main/cloud/run-cli.mjs +177 -0
  43. package/main/cloud/run.mjs +1934 -0
  44. package/main/cloud/usage.mjs +76 -0
  45. package/main/cloud-permission.mjs +51 -0
  46. package/main/codex-account.mjs +78 -0
  47. package/main/codex-app-server.mjs +276 -409
  48. package/main/codex-approvals.mjs +148 -433
  49. package/main/codex-bin.mjs +51 -249
  50. package/main/codex-models.mjs +20 -2
  51. package/main/codex-session.mjs +246 -506
  52. package/main/codex-threads-worker.mjs +12 -0
  53. package/main/codex-threads.mjs +112 -11
  54. package/main/codex-usage-file.mjs +5 -3
  55. package/main/codex-usage.mjs +56 -7
  56. package/main/codex-watch.mjs +83 -5
  57. package/main/codex.mjs +95 -363
  58. package/main/config.mjs +37 -5
  59. package/main/corner-tag.mjs +810 -0
  60. package/main/crash-report.mjs +26 -3
  61. package/main/dev-window.mjs +1 -1
  62. package/main/engine-bin.mjs +545 -0
  63. package/main/engine-setup.mjs +25 -24
  64. package/main/feedback.mjs +14 -31
  65. package/main/folder-pictures.mjs +2 -2
  66. package/main/freeze-recorder-thread.mjs +86 -0
  67. package/main/freeze-recorder.mjs +146 -0
  68. package/main/fresh-user.mjs +20 -9
  69. package/main/git-change-offthread.mjs +17 -3
  70. package/main/git-change-thread.mjs +5 -2
  71. package/main/harnesses/claude-stream.mjs +208 -0
  72. package/main/harnesses/claude.mjs +105 -0
  73. package/main/harnesses/codex.mjs +72 -0
  74. package/main/harnesses/common.mjs +78 -0
  75. package/main/harnesses/instructions-file.mjs +116 -0
  76. package/main/harnesses/opencode.mjs +234 -0
  77. package/main/harnesses.mjs +65 -0
  78. package/main/importable-threads.mjs +96 -0
  79. package/main/ipc.mjs +520 -135
  80. package/main/leftovers.mjs +117 -25
  81. package/main/liquid-glass.mjs +26 -0
  82. package/main/live-replies.mjs +10 -4
  83. package/main/local-app-frames.mjs +82 -0
  84. package/main/main-went-red.mjs +330 -0
  85. package/main/main.mjs +248 -37
  86. package/main/memory-gate-history.mjs +85 -20
  87. package/main/memory-gate-server.mjs +129 -11
  88. package/main/memory-gate.mjs +62 -8
  89. package/main/memory-log.mjs +124 -0
  90. package/main/menu.mjs +4 -2
  91. package/main/message-priority.mjs +2 -2
  92. package/main/mic.mjs +38 -0
  93. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  94. package/main/new-chats-watch.mjs +110 -0
  95. package/main/new-user-window.mjs +25 -0
  96. package/main/notify.mjs +64 -5
  97. package/main/opencode-server.mjs +271 -0
  98. package/main/opencode.mjs +287 -0
  99. package/main/phone-alerts.mjs +117 -0
  100. package/main/phone-inbox.mjs +625 -0
  101. package/main/phone-link.mjs +571 -0
  102. package/main/phone-uploads.mjs +476 -0
  103. package/main/picture-shape.mjs +114 -0
  104. package/main/quit-sequence.mjs +143 -0
  105. package/main/refresh-agent-models.mjs +15 -2
  106. package/main/relay-address.mjs +28 -0
  107. package/main/relay-client.mjs +314 -0
  108. package/main/repeats.mjs +10 -4
  109. package/main/row-label.mjs +18 -47
  110. package/main/run-change.mjs +13 -0
  111. package/main/run-ledger.mjs +135 -0
  112. package/main/runs-on.mjs +32 -35
  113. package/main/save-attachments.mjs +41 -0
  114. package/main/serve.mjs +155 -9
  115. package/main/settings.mjs +364 -50
  116. package/main/shell-path.mjs +234 -0
  117. package/main/ship-conflict-brief.mjs +119 -0
  118. package/main/ship-queue.mjs +134 -20
  119. package/main/sign-in-files.mjs +3 -7
  120. package/main/source-updater.mjs +45 -11
  121. package/main/stage-attachment.mjs +68 -0
  122. package/main/stall-watch.mjs +50 -0
  123. package/main/start-hold.mjs +73 -0
  124. package/main/store/home.mjs +87 -232
  125. package/main/store/paths.mjs +11 -0
  126. package/main/store/teammates-files.mjs +99 -0
  127. package/main/store/work-items.mjs +231 -13
  128. package/main/store.mjs +227 -53
  129. package/main/supervisor.mjs +3865 -699
  130. package/main/task-commands.mjs +13 -17
  131. package/main/task-folders-offthread.mjs +6 -1
  132. package/main/task-folders.mjs +897 -105
  133. package/main/task-terminals.mjs +147 -41
  134. package/main/team/index.mjs +114 -10
  135. package/main/team/memory-cloud.mjs +183 -1
  136. package/main/team/room-hours.mjs +48 -0
  137. package/main/team/room-setup.mjs +31 -0
  138. package/main/team/rooms-memory.mjs +454 -0
  139. package/main/team/rooms-shape.mjs +42 -0
  140. package/main/team/rooms-supabase.mjs +324 -0
  141. package/main/team/rooms.mjs +575 -0
  142. package/main/team/session.mjs +96 -26
  143. package/main/team/supabase-backend.mjs +180 -0
  144. package/main/team/sync.mjs +109 -1
  145. package/main/teammates.mjs +580 -0
  146. package/main/terminal-host.mjs +143 -0
  147. package/main/terminal-hosts.mjs +261 -0
  148. package/main/transcript-search.mjs +80 -0
  149. package/main/updater.mjs +71 -9
  150. package/main/webview-guard.mjs +45 -0
  151. package/main/worktree-include.mjs +365 -0
  152. package/mcp/core/documents.mjs +3 -1
  153. package/mcp/core/repeats.mjs +88 -0
  154. package/mcp/core/teammates.mjs +222 -0
  155. package/mcp/core/work.mjs +76 -4
  156. package/mcp/stdio.mjs +24 -11
  157. package/mcp/tools.mjs +116 -8
  158. package/package.json +30 -5
  159. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-CoWB6YUK.js +1 -0
  160. package/renderer/dist/assets/arc-Ch0NrCul.js +1 -0
  161. package/renderer/dist/assets/architectureDiagram-47P4ROYG-CXz9M5tQ.js +36 -0
  162. package/renderer/dist/assets/blockDiagram-E7TT5TSB-DchGUuHH.js +140 -0
  163. package/renderer/dist/assets/c4Diagram-CRA5TL53-Cip-zNyr.js +38 -0
  164. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  165. package/renderer/dist/assets/chunk-2Q5K7J3B-BoXid_2r.js +1 -0
  166. package/renderer/dist/assets/chunk-5VM5RSS4-Cpez1rRz.js +15 -0
  167. package/renderer/dist/assets/chunk-7TKQ45FW-DKA84YkE.js +1 -0
  168. package/renderer/dist/assets/chunk-AW2ZBBNX-Dg-SumPG.js +88 -0
  169. package/renderer/dist/assets/chunk-FVRAUYC3-06TCvPm1.js +2 -0
  170. package/renderer/dist/assets/chunk-HTAEGDNF-CMEBy1Td.js +1 -0
  171. package/renderer/dist/assets/chunk-JWPE2WC7-BJ2ObmV-.js +1 -0
  172. package/renderer/dist/assets/chunk-KQW6MTUR-CGWOYXUE.js +1 -0
  173. package/renderer/dist/assets/chunk-XXDRQBXY-CiZp07XC.js +1 -0
  174. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-DJNIX719.js +217 -0
  175. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  176. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  177. package/renderer/dist/assets/cose-bilkent-JH36ORCC-CbkyZOUH.js +1 -0
  178. package/renderer/dist/assets/cynefin-EF2NZ3EQ-78iOroZO.js +141 -0
  179. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-GcCopMs7.js +62 -0
  180. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  181. package/renderer/dist/assets/dagre-W4DXFKR2-DHLlXLkz.js +4 -0
  182. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  183. package/renderer/dist/assets/diagram-2UJZ2QOL-DuZSUHcH.js +200 -0
  184. package/renderer/dist/assets/diagram-OVF4WLC6-Ckxu3cqH.js +24 -0
  185. package/renderer/dist/assets/diagram-PFPMY2P6-CsTw9Ycn.js +30 -0
  186. package/renderer/dist/assets/diagram-UMYRVEAY-CTXd8qVg.js +24 -0
  187. package/renderer/dist/assets/diagram-YEKJPTXX-CJs1T_9m.js +41 -0
  188. package/renderer/dist/assets/diagram-ZIFT7M5P-CvjI23mf.js +3 -0
  189. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-d_IeAzOJ.js +1 -0
  190. package/renderer/dist/assets/elk-IJKZMXRS-zo9KFP3N.js +27 -0
  191. package/renderer/dist/assets/erDiagram-O2IAPWRE-DqfYf5Dt.js +99 -0
  192. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-DAWsMLmo.js +213 -0
  193. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-4ml_bhwR.js +292 -0
  194. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-B_WcqSId.js +106 -0
  195. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  196. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-CI-jYqzc.js +2 -0
  197. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  198. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-B56VWRlV.js +70 -0
  199. package/renderer/dist/assets/journeyDiagram-COXZFDF6-D1-p09Lr.js +139 -0
  200. package/renderer/dist/assets/kanban-definition-P3RFRI5V-Cv3w7JoN.js +89 -0
  201. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  202. package/renderer/dist/assets/linear-C35V2Spi.js +1 -0
  203. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  204. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  205. package/renderer/dist/assets/mermaid.core-BzGT5Vo9.js +314 -0
  206. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-BZUfuymC.js +96 -0
  207. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  208. package/renderer/dist/assets/pegDiagram-BGZESJAR-DFh9XNGe.js +1 -0
  209. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-C65tPVsy.js +39 -0
  210. package/renderer/dist/assets/product-name-CSNW6d7s.js +40 -0
  211. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-Cushj_nk.js +7 -0
  212. package/renderer/dist/assets/railroadDiagram-SM67HX2B-DZGebGx5.js +1 -0
  213. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-DpAB0MNo.js +84 -0
  214. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-D-29rHra.js +40 -0
  215. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-FfOQAZol.js +169 -0
  216. package/renderer/dist/assets/sizeCapture-INFHLROL-C_n6fDBY.js +1 -0
  217. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-BZjenzJG.js +291 -0
  218. package/renderer/dist/assets/swimlanes-N4OXWK64-DxaMU9vX.js +1 -0
  219. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  220. package/renderer/dist/assets/timeline-definition-YOQAKGHF-DTg5HJKz.js +120 -0
  221. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-Bc3MZDsl.js +370 -0
  222. package/renderer/dist/assets/vennDiagram-BLWOH2XV-GIVGk9Id.js +34 -0
  223. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-NmJnbQOg.js +78 -0
  224. package/renderer/dist/assets/xychartDiagram-TAQBALBS-p-kfGfnW.js +7 -0
  225. package/renderer/dist/corner-tag.html +13 -0
  226. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  227. package/renderer/dist/emulator/data/compression/README.md +11 -0
  228. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  229. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  230. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  231. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  232. package/renderer/dist/emulator/data/cores/README.md +22 -0
  233. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  234. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  235. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  236. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  237. package/renderer/dist/emulator/data/cores/package.json +65 -0
  238. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  239. package/renderer/dist/emulator/data/emulator.css +1614 -0
  240. package/renderer/dist/emulator/data/loader.js +170 -0
  241. package/renderer/dist/emulator/data/localization/README.md +63 -0
  242. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  243. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  244. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  245. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  246. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  247. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  248. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  249. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  250. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  251. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  252. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  253. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  254. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  255. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  256. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  257. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  258. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  259. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  260. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  261. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  262. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  263. package/renderer/dist/emulator/data/src/compression.js +142 -0
  264. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  265. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  266. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  267. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  268. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  269. package/renderer/dist/emulator/data/src/storage.js +125 -0
  270. package/renderer/dist/emulator/data/version.json +5 -0
  271. package/renderer/dist/emulator/host.js +297 -0
  272. package/renderer/dist/emulator/index.html +28 -0
  273. package/renderer/dist/emulator/provenance.json +6 -0
  274. package/renderer/dist/index.html +16 -3
  275. package/scripts/after-install.mjs +30 -8
  276. package/scripts/approval-server.sh +24 -5
  277. package/scripts/before-publish.mjs +12 -10
  278. package/scripts/cloud-shim.pl +57 -0
  279. package/shared/account-names.d.mts +6 -0
  280. package/shared/account-names.mjs +15 -0
  281. package/shared/agent-hours.mjs +136 -0
  282. package/shared/agent-import.mjs +62 -34
  283. package/shared/agents.d.mts +1 -0
  284. package/shared/agents.mjs +126 -522
  285. package/shared/analytics-events.mjs +12 -0
  286. package/shared/approval-card.mjs +36 -0
  287. package/shared/bridge-map.mjs +45 -1
  288. package/shared/claude-code-shape.mjs +6 -0
  289. package/shared/claude-commands.generated.mjs +6 -4
  290. package/shared/claude-models.generated.mjs +6 -4
  291. package/shared/claude-usage.d.mts +1 -0
  292. package/shared/claude-usage.mjs +42 -1
  293. package/shared/cloud-hand-back.d.mts +7 -0
  294. package/shared/cloud-hand-back.mjs +20 -0
  295. package/shared/cloud-kit.d.mts +219 -0
  296. package/shared/cloud-kit.mjs +341 -0
  297. package/shared/cloud-login-name.mjs +4 -0
  298. package/shared/cloud-start.mjs +17 -0
  299. package/shared/cloud-versions.mjs +23 -0
  300. package/shared/codex-import.mjs +2 -2
  301. package/shared/codex-modes.d.mts +22 -9
  302. package/shared/codex-modes.mjs +67 -11
  303. package/shared/codex-usage.mjs +108 -2
  304. package/shared/corner-tag.d.mts +36 -0
  305. package/shared/corner-tag.mjs +396 -0
  306. package/shared/dead-run-trace.mjs +19 -2
  307. package/shared/engines.mjs +76 -300
  308. package/shared/feedback.d.mts +7 -0
  309. package/shared/feedback.mjs +25 -0
  310. package/shared/first-run-practice.mjs +17 -24
  311. package/shared/harness-definitions.mjs +111 -0
  312. package/shared/instruction-defaults.json +2 -2
  313. package/shared/key-caps.d.mts +14 -0
  314. package/shared/key-caps.mjs +68 -0
  315. package/shared/message-edits.d.mts +7 -0
  316. package/shared/message-edits.mjs +35 -0
  317. package/shared/move-thread.d.mts +24 -0
  318. package/shared/move-thread.mjs +112 -0
  319. package/shared/named-threads.mjs +72 -0
  320. package/shared/not-shipped.d.mts +22 -0
  321. package/shared/not-shipped.mjs +89 -0
  322. package/shared/notify-rules.d.mts +31 -0
  323. package/shared/notify-rules.mjs +55 -8
  324. package/shared/phone-files.mjs +28 -0
  325. package/shared/phone-settings.mjs +118 -0
  326. package/shared/phone-snapshot.d.mts +16 -0
  327. package/shared/phone-snapshot.mjs +348 -0
  328. package/shared/picture-formats.d.mts +1 -0
  329. package/shared/picture-formats.mjs +13 -0
  330. package/shared/provider-commands.mjs +10 -2
  331. package/shared/rank.d.mts +4 -1
  332. package/shared/rank.mjs +82 -19
  333. package/shared/relay-address.mjs +6 -0
  334. package/shared/relay-crypto.mjs +165 -0
  335. package/shared/repeats.d.mts +8 -0
  336. package/shared/repeats.mjs +33 -2
  337. package/shared/room-cards.mjs +167 -0
  338. package/shared/room-links.d.mts +4 -0
  339. package/shared/room-links.mjs +21 -0
  340. package/shared/runs-on.d.mts +3 -13
  341. package/shared/runs-on.mjs +31 -52
  342. package/shared/shipped-note.d.mts +4 -0
  343. package/shared/shipped-note.mjs +6 -0
  344. package/shared/shortcuts.mjs +24 -10
  345. package/shared/spawn-trouble.d.mts +4 -0
  346. package/shared/spawn-trouble.mjs +484 -30
  347. package/shared/team-rules.mjs +9 -2
  348. package/shared/teammates.d.mts +19 -0
  349. package/shared/teammates.mjs +756 -0
  350. package/shared/terminal-focus.d.mts +1 -0
  351. package/shared/terminal-focus.mjs +13 -0
  352. package/shared/terminal-input.d.mts +1 -0
  353. package/shared/terminal-input.mjs +30 -0
  354. package/shared/terminal-keys.d.mts +1 -0
  355. package/shared/terminal-keys.mjs +16 -0
  356. package/shared/terminal-size.d.mts +4 -0
  357. package/shared/terminal-size.mjs +29 -0
  358. package/shared/thread-cards.mjs +4 -1
  359. package/shared/usage.d.mts +12 -0
  360. package/shared/usage.mjs +56 -3
  361. package/shared/websites.d.mts +15 -0
  362. package/shared/websites.mjs +61 -0
  363. package/shared/where-it-runs-from.d.mts +43 -0
  364. package/shared/where-it-runs-from.mjs +155 -0
  365. package/shared/work-items.mjs +150 -29
  366. package/shared/work-lines.mjs +93 -245
  367. package/shared/xterm-invalid-range.d.mts +2 -0
  368. package/shared/xterm-invalid-range.mjs +58 -0
  369. package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
  370. package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
@@ -0,0 +1,810 @@
1
+ // THE CORNER TAG'S WINDOWS (w-dafae58a23). shared/corner-tag.mjs decides what
2
+ // it says, when it shows and where it sits; this file is the live half: the
3
+ // tag's own small window, the list that opens beside it, the screens they sit
4
+ // on, and her choices about them.
5
+ //
6
+ // What the windows have to get right, each learned from the drawings, from
7
+ // real-Electron runs, from four reviews by Codex, and from real mouse events
8
+ // posted at the real windows (2026-10-08), which is where the last three of
9
+ // these were found:
10
+ //
11
+ // - A PRESS ON THE TAG MUST NOT BRING THE APP FORWARD. It did, on every
12
+ // floating-window setting tried: macOS makes the app active on a press, and
13
+ // a window that may not take focus hands it to the app's main window, which
14
+ // comes to the front. So the tag and its list MAY take focus. A press makes
15
+ // the tag the active window and the app's window stays exactly where it was.
16
+ // - EACH WINDOW IS EXACTLY ITS PILL OR ITS LIST. The first version padded the
17
+ // windows for a drawn shadow and let clicks fall through the padding, and
18
+ // switching that on and off under the pointer told the page the pointer had
19
+ // left, so a real first click fell straight through to the app beneath.
20
+ // With no margin there is nothing to fall through. The page draws the
21
+ // glass, its small corners and its edge. The window has no system shadow:
22
+ // with one, macOS also drew its own window outline (a black hairline and a
23
+ // light band) around the glass, a strong border no picture had shown.
24
+ // - A DRAG FOLLOWS THE REAL CURSOR. The page only says when the button went
25
+ // down and up; between the two, this process reads the cursor itself, so a
26
+ // drag never depends on the page being sent every move.
27
+ // - POINTING OPENS THE LIST, A CLICK OPENS THE APP (her ask, 2026-10-08: with
28
+ // 16 waiting she wants to go straight in, not click through the list). The
29
+ // pointer has to rest on the tag a moment, so passing over the corner opens
30
+ // nothing, and after a drag it has to leave and come back. The list is in
31
+ // its own window so the tag never moves. Off both for a moment and it shuts.
32
+ // - IT SHOWS OVER FULL-SCREEN APPS, IT HIDES FOR A WHILE AND COMES BACK, it is
33
+ // off for good only in Settings, it steps aside while she is in the app, and
34
+ // it goes when the app's window goes.
35
+ //
36
+ // What the tag counts is not decided here. Only the page knows what the inbox
37
+ // is, so it hands over what is ready and how many are working on
38
+ // `zero:corner-tag`; shared/corner-tag.mjs puts it newest first.
39
+
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
+
42
+ // How long the pointer may be off both the tag and its list before the list
43
+ // shuts: long enough to cross the gap between them, short enough to feel like
44
+ // it follows the pointer.
45
+ export const CLOSE_AFTER_MS = 450;
46
+ // How long the pointer rests on the tag before the list opens: long enough
47
+ // that crossing the corner on the way to something else opens nothing.
48
+ export const OPEN_AFTER_MS = 220;
49
+ // Far enough that a click with a shaky hand is still a click.
50
+ export const DRAG_FROM_PX = 6;
51
+ // How often a drag reads the cursor: once a frame.
52
+ const FRAME_MS = 16;
53
+ // A press whose release never came is let go after this long.
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;
64
+
65
+ const HIDE_LABELS = { '5m': 'Hide for 5 minutes', '30m': 'Hide for 30 minutes', today: 'Hide for the rest of today' };
66
+ const CHANNELS = [
67
+ 'zero:corner-tag', 'corner-tag:ready', 'corner-tag:size', 'corner-tag:hover', 'corner-tag:toggle',
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',
70
+ ];
71
+
72
+ export function createCornerTag({
73
+ BrowserWindow, screen, ipcMain, Menu, powerMonitor, config, saveConfig, mainWindow, load, preload,
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 = () => [],
81
+ }) {
82
+ const wins = { tag: null, card: null };
83
+ const loaded = { tag: false, card: false };
84
+ const hovered = { tag: false, card: false };
85
+ let ready = [];
86
+ let working = 0;
87
+ // The tag's rectangle on screen; its window is exactly this.
88
+ let tag = null;
89
+ let tagSize = { width: 96, height: 26 };
90
+ let measured = false;
91
+ let cardSize = null;
92
+ let open = false;
93
+ // A press on the tag: where the cursor and the tag were when it went down,
94
+ // and whether it has moved far enough to be a drag.
95
+ let press = null;
96
+ let wake = null;
97
+ let closing = null;
98
+ let watching = null;
99
+ let opening = null;
100
+ // After a drag the pointer is still on the tag; the list waits for it to
101
+ // leave and come back rather than opening under it.
102
+ let settling = false;
103
+ let gone = false;
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());
116
+ const areas = () => screen.getAllDisplays().map((d) => d.workArea);
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
+ };
145
+ const inApp = () => {
146
+ try { return !!mainWindow && !mainWindow.isDestroyed() && mainWindow.isFocused(); } catch { return false; }
147
+ };
148
+ const alive = (part) => !!wins[part] && !wins[part].isDestroyed();
149
+ const rounded = (r) => ({ x: Math.round(r.x), y: Math.round(r.y), width: Math.round(r.width), height: Math.round(r.height) });
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);
162
+ const sendState = () => {
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 };
165
+ send('tag', 'corner-tag:state', s);
166
+ send('card', 'corner-tag:state', s);
167
+ };
168
+ const shouldShow = () => tagShows({
169
+ on: config.cornerTag, hiddenUntil: config.cornerTagHiddenUntil ?? 0, now: now(), inApp: inApp(), ready: fresh().length,
170
+ });
171
+ // The list may only appear beside a tag that is on screen and should be.
172
+ // A click that was on its way while the tag hid must not open it alone.
173
+ const tagUp = () => !gone && alive('tag') && wins.tag.isVisible() && shouldShow();
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
+
200
+ const placeTag = () => {
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));
206
+ tag = { ...spot, ...tagSize };
207
+ if (alive('tag')) wins.tag.setBounds(rounded(tag));
208
+ };
209
+
210
+ const placeCard = () => {
211
+ if (!tag || !cardSize || !alive('card')) return;
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(); }
266
+ };
267
+
268
+ const make = (part) => {
269
+ const win = new BrowserWindow({
270
+ show: false,
271
+ frame: false,
272
+ transparent: true,
273
+ backgroundColor: '#00000000',
274
+ // Clear and unrounded: the page draws the glass and its small corners.
275
+ // The system's frosted material would blur what is behind, but it comes
276
+ // only with the system's own rounder corners or square ones, and the
277
+ // approved tag has neither.
278
+ roundedCorners: false,
279
+ hasShadow: false,
280
+ resizable: false,
281
+ movable: false,
282
+ minimizable: false,
283
+ maximizable: false,
284
+ fullscreenable: false,
285
+ skipTaskbar: true,
286
+ // May take focus, so a press never hands it to the app's main window.
287
+ focusable: true,
288
+ // The first click on a window that is not active still counts.
289
+ acceptFirstMouse: true,
290
+ alwaysOnTop: true,
291
+ // A panel floats over full-screen apps without turning the whole app
292
+ // into a background app, which would take the dock icon away.
293
+ ...(mac ? { type: 'panel' } : {}),
294
+ webPreferences: { preload, contextIsolation: true, nodeIntegration: false, sandbox: true },
295
+ });
296
+ win.setAlwaysOnTop(true, 'floating');
297
+ try { win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true, skipTransformProcessType: true }); } catch {}
298
+ win.on('closed', () => { wins[part] = null; loaded[part] = false; });
299
+ win.webContents.on('did-finish-load', () => atFull(part));
300
+ // A reload starts the handshake again: the window goes, anything half
301
+ // done with it is let go, and until the page says it is ready nothing is
302
+ // sent to it and it is not shown. The first load is not a reload.
303
+ win.webContents.on('did-start-loading', () => {
304
+ if (!loaded[part]) return;
305
+ loaded[part] = false;
306
+ hovered[part] = false;
307
+ toneSent[part] = null;
308
+ if (part === 'tag') { endPress(true); if (win.isVisible()) win.hide(); }
309
+ closeCard();
310
+ });
311
+ wins[part] = win;
312
+ if (part === 'tag') placeTag();
313
+ load(win, part);
314
+ return win;
315
+ };
316
+
317
+ const cancelClose = () => { if (closing) { clearTimer(closing); closing = null; } };
318
+
319
+ function closeCard() {
320
+ cancelClose();
321
+ open = false;
322
+ hovered.card = false;
323
+ if (alive('card') && wins.card.isVisible()) wins.card.hide();
324
+ send('tag', 'corner-tag:open', false);
325
+ }
326
+
327
+ const showCard = () => {
328
+ if (open && tagUp() && loaded.card && cardSize && alive('card') && !wins.card.isVisible()) {
329
+ wins.card.showInactive();
330
+ remeasure('card');
331
+ readTone();
332
+ }
333
+ };
334
+
335
+ function openCard() {
336
+ if (!tag || press?.moved || !tagUp()) return;
337
+ open = true;
338
+ send('tag', 'corner-tag:open', true);
339
+ watch();
340
+ reconsider();
341
+ if (!alive('card')) { make('card'); return; }
342
+ sendState();
343
+ placeCard();
344
+ showCard();
345
+ }
346
+
347
+ /* --------------------- where the pointer really is ---------------------- */
348
+ // The page says when the pointer comes and goes, and that is trusted, but a
349
+ // boundary event can go missing, so while the list is open the real cursor
350
+ // is read a few times a second and wins. Nothing is read otherwise.
351
+ const within = (pt, r) => !!r && pt.x >= r.x && pt.x < r.x + r.width && pt.y >= r.y && pt.y < r.y + r.height;
352
+ const drawn = (part) => (alive(part) && wins[part].isVisible() ? wins[part].getBounds() : null);
353
+ const readCursor = () => {
354
+ const pt = screen.getCursorScreenPoint?.();
355
+ if (!pt || press) return false;
356
+ for (const part of ['tag', 'card']) hovered[part] = within(pt, drawn(part));
357
+ return true;
358
+ };
359
+
360
+ function watch() {
361
+ if (watching || gone) return;
362
+ const tick = () => {
363
+ watching = null;
364
+ if (gone) return;
365
+ readCursor();
366
+ reconsider();
367
+ if (open || hovered.tag || hovered.card) watching = setTimer(tick, 200);
368
+ };
369
+ watching = setTimer(tick, 200);
370
+ }
371
+
372
+ // Whether the open list should start, keep or drop its countdown to shutting.
373
+ function reconsider() {
374
+ if (!open || hovered.tag || hovered.card) { cancelClose(); return; }
375
+ if (closing) return;
376
+ closing = setTimer(() => {
377
+ closing = null;
378
+ // The last word is the real cursor's: a pointer that came back without
379
+ // the page noticing keeps the list open.
380
+ readCursor();
381
+ if (!hovered.tag && !hovered.card) closeCard();
382
+ else watch();
383
+ }, CLOSE_AFTER_MS);
384
+ }
385
+
386
+ const cancelOpen = () => { if (opening) { clearTimer(opening); opening = null; } };
387
+
388
+ // The pointer came onto the tag: open the list if it is still there in a
389
+ // moment. The real cursor has the last word, as it does for shutting.
390
+ function meanToOpen() {
391
+ if (open || opening || settling || press) return;
392
+ opening = setTimer(() => {
393
+ opening = null;
394
+ readCursor();
395
+ if (hovered.tag && !settling && !press) openCard();
396
+ }, OPEN_AFTER_MS);
397
+ }
398
+
399
+ /* ------------------------------- the press ------------------------------ */
400
+ // Where the cursor is now, against where it was when the button went down.
401
+ const follow = () => {
402
+ const pt = screen.getCursorScreenPoint?.();
403
+ if (!pt || !press) return;
404
+ const dx = pt.x - press.start.x;
405
+ const dy = pt.y - press.start.y;
406
+ if (!press.moved && Math.hypot(dx, dy) >= DRAG_FROM_PX) {
407
+ press.moved = true;
408
+ cancelOpen();
409
+ settling = true;
410
+ closeCard();
411
+ send('tag', 'corner-tag:dragging', true);
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
+ }
442
+ if (press.moved && alive('tag')) {
443
+ tag = { ...tag, x: press.from.x + dx, y: press.from.y + dy };
444
+ wins.tag.setPosition(Math.round(tag.x), Math.round(tag.y));
445
+ }
446
+ };
447
+
448
+ function step() {
449
+ if (!press || gone) return;
450
+ press.timer = null;
451
+ follow();
452
+ if (now() - press.at > PRESS_GIVES_UP_MS) { endPress(true); return; }
453
+ press.timer = setTimer(step, FRAME_MS);
454
+ }
455
+
456
+ // The button came up (or the press was taken away). A drag lands where the
457
+ // cursor left it and is remembered; a press that never moved is a click.
458
+ function endPress(cancelled) {
459
+ if (!press) return;
460
+ if (press.timer) clearTimer(press.timer);
461
+ press.timer = null;
462
+ const p = press;
463
+ if (!cancelled) follow();
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
+ }
480
+ if (p.moved) {
481
+ tag = { ...restingSpot({ x: tag.x, y: tag.y }, tagSize, areas().map(parkable)), ...tagSize };
482
+ if (alive('tag')) wins.tag.setBounds(rounded(tag));
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 });
487
+ send('tag', 'corner-tag:dragging', false);
488
+ // Dragged onto another screen, it draws at that screen's size.
489
+ sendState();
490
+ } else if (!cancelled) {
491
+ cancelOpen();
492
+ goTo(null);
493
+ }
494
+ }
495
+
496
+ // Everything that is not on screen is back where it starts: the list shut,
497
+ // no press half-done, so the tag comes back exactly as it was first drawn.
498
+ const reset = () => {
499
+ endPress(true);
500
+ cancelOpen();
501
+ settling = false;
502
+ closeCard();
503
+ hovered.tag = false;
504
+ send('tag', 'corner-tag:reset', null);
505
+ if (alive('tag') && tag) wins.tag.setBounds(rounded(tag));
506
+ };
507
+
508
+ function refresh() {
509
+ if (gone) return;
510
+ if (wake) { clearTimer(wake); wake = null; }
511
+ const until = config.cornerTagHiddenUntil ?? 0;
512
+ // A hide comes back by itself the moment it runs out.
513
+ if (until > now()) wake = setTimer(refresh, until - now() + 50);
514
+ if (!shouldShow()) {
515
+ stopTones();
516
+ if (alive('tag') && wins.tag.isVisible()) { wins.tag.hide(); reset(); }
517
+ else if (open) closeCard();
518
+ return;
519
+ }
520
+ if (!alive('tag')) { make('tag'); return; }
521
+ sendState();
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
+ }
530
+ }
531
+
532
+ const bringAppForward = () => {
533
+ if (!mainWindow || mainWindow.isDestroyed()) return false;
534
+ if (mainWindow.isMinimized()) mainWindow.restore();
535
+ mainWindow.show();
536
+ mainWindow.focus();
537
+ return true;
538
+ };
539
+
540
+ // Into the app: on a thread when a line of the list was picked, or just the
541
+ // app as it is when it was the tag.
542
+ function goTo(id) {
543
+ closeCard();
544
+ if (!bringAppForward()) return;
545
+ if (typeof id === 'string' && id) mainWindow.webContents.send('zero:open-item', { id });
546
+ }
547
+
548
+ const hide = (choice) => {
549
+ const until = hiddenUntil(choice, now());
550
+ if (!until) return;
551
+ saveConfig(config, { cornerTagHiddenUntil: until });
552
+ count('tag_hidden', { seconds: Math.round((until - now()) / 1000) });
553
+ refresh();
554
+ };
555
+
556
+ const openSettings = () => {
557
+ closeCard();
558
+ if (bringAppForward()) mainWindow.webContents.send('zero:open-settings', { pane: 'general' });
559
+ };
560
+
561
+ const partOf = (p) => (p?.part === 'card' ? 'card' : 'tag');
562
+
563
+ /* ------------------------------ from the app's page ---------------------- */
564
+ ipcMain.handle('zero:corner-tag', (_e, state) => {
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
+ });
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
+ }
579
+ refresh();
580
+ return null;
581
+ });
582
+
583
+ /* ------------------------------ from the tag's pages --------------------- */
584
+ ipcMain.handle('corner-tag:ready', (_e, p) => {
585
+ const part = partOf(p);
586
+ loaded[part] = true;
587
+ sendState();
588
+ shareTone();
589
+ if (part === 'tag') refresh();
590
+ else if (open) { placeCard(); showCard(); }
591
+ return null;
592
+ });
593
+
594
+ ipcMain.handle('corner-tag:size', (_e, p) => {
595
+ if (!p || !(p.width > 0) || !(p.height > 0)) return null;
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);
598
+ if (partOf(p) === 'card') {
599
+ cardSize = size;
600
+ const rect = { x: 0, y: 0, ...size };
601
+ shape.card = { rect, radius: radius(rect) };
602
+ dress('card');
603
+ if (open) { placeCard(); showCard(); }
604
+ return null;
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; }
622
+ tagSize = size;
623
+ // Nothing moves under the pointer while it is dragging.
624
+ if (press?.moved) return null;
625
+ // The first measure places it where she left it. Every one after keeps
626
+ // the edge nearest its corner still as the words change length.
627
+ tag = tag && measured ? keepCorner(tag, tagSize, framed(areaOf(tag))) : null;
628
+ measured = true;
629
+ placeTag();
630
+ if (open) placeCard();
631
+ return null;
632
+ });
633
+
634
+ ipcMain.handle('corner-tag:hover', (_e, p) => {
635
+ const part = partOf(p);
636
+ hovered[part] = !!p?.inside;
637
+ if (part === 'tag') {
638
+ if (p?.inside) meanToOpen();
639
+ else { settling = false; cancelOpen(); }
640
+ }
641
+ reconsider();
642
+ if (p?.inside) watch();
643
+ return null;
644
+ });
645
+
646
+ ipcMain.handle('corner-tag:toggle', () => {
647
+ if (open) closeCard(); else openCard();
648
+ return null;
649
+ });
650
+
651
+ // `at` is where the button went down, from the page. The message can arrive
652
+ // after the cursor has already moved, and starting from the cursor then put
653
+ // the tag about 30 px off under it. A press on a tag that has since hidden
654
+ // starts nothing.
655
+ ipcMain.handle('corner-tag:press', (_e, at) => {
656
+ const pt = Number.isFinite(at?.x) && Number.isFinite(at?.y) ? { x: at.x, y: at.y } : screen.getCursorScreenPoint?.();
657
+ if (!pt || !tag || !tagUp() || press) return 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
+ };
674
+ press.timer = setTimer(step, FRAME_MS);
675
+ return null;
676
+ });
677
+ ipcMain.handle('corner-tag:release', (_e, p) => { endPress(!!p?.cancelled); return null; });
678
+
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
+ });
688
+ ipcMain.handle('corner-tag:hide', (_e, choice) => { hide(choice); return null; });
689
+ ipcMain.handle('corner-tag:settings', () => { openSettings(); return null; });
690
+
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
+
729
+ ipcMain.handle('corner-tag:menu', () => {
730
+ if (!alive('tag')) return null;
731
+ closeCard();
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' },
739
+ ...HIDE_CHOICES.map((c) => ({ label: HIDE_LABELS[c.key], click: () => hide(c.key) })),
740
+ { type: 'separator' },
741
+ { label: 'Turn off in Settings…', click: openSettings },
742
+ ]).popup({ window: wins.tag });
743
+ return null;
744
+ });
745
+
746
+ /* ------------------------------ from the system -------------------------- */
747
+ // A screen that comes, goes or changes shape: the list shuts and the tag is
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.
752
+ const reseat = () => {
753
+ if (gone) return;
754
+ closeCard();
755
+ if (!tag || press) return;
756
+ placeTag();
757
+ sendState();
758
+ };
759
+ // After sleep the screens may have changed and a hide may have run out.
760
+ const woke = () => { reseat(); refresh(); };
761
+
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
+ };
770
+ // The app's window is gone, and the tag goes with it. Left behind, it would
771
+ // hold the app open with nothing behind it and nothing it could open.
772
+ const dispose = () => {
773
+ if (gone) return;
774
+ gone = true;
775
+ endPress(true);
776
+ cancelClose();
777
+ if (wake) { clearTimer(wake); wake = null; }
778
+ if (watching) { clearTimer(watching); watching = null; }
779
+ stopTones();
780
+ cancelOpen();
781
+ for (const part of ['tag', 'card']) if (alive(part)) wins[part].destroy();
782
+ for (const ch of CHANNELS) { try { ipcMain.removeHandler?.(ch); } catch {} }
783
+ try {
784
+ screen.removeListener?.('display-added', reseat);
785
+ screen.removeListener?.('display-removed', reseat);
786
+ screen.removeListener?.('display-metrics-changed', reseat);
787
+ powerMonitor?.removeListener?.('resume', woke);
788
+ powerMonitor?.removeListener?.('unlock-screen', woke);
789
+ } catch {}
790
+ };
791
+
792
+ if (mainWindow) {
793
+ mainWindow.on('focus', onFocus);
794
+ mainWindow.on('blur', onBlur);
795
+ mainWindow.on('closed', dispose);
796
+ }
797
+ try {
798
+ screen.on('display-added', reseat);
799
+ screen.on('display-removed', reseat);
800
+ screen.on('display-metrics-changed', reseat);
801
+ } catch {}
802
+ try { powerMonitor?.on?.('resume', woke); powerMonitor?.on?.('unlock-screen', woke); } catch {}
803
+
804
+ return {
805
+ refresh,
806
+ dispose,
807
+ // Tests, and nothing else.
808
+ _state: () => ({ ready: fresh(), working, tag, open, visible: alive('tag') && wins.tag.isVisible() }),
809
+ };
810
+ }