agentbox-app 0.1.12 → 0.1.13

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 (214) hide show
  1. package/main/acp.mjs +296 -0
  2. package/main/agent-homes.mjs +158 -0
  3. package/main/agent-sessions.mjs +57 -20
  4. package/main/agent-updates.mjs +9 -22
  5. package/main/analytics.mjs +30 -0
  6. package/main/claude-bin.mjs +45 -311
  7. package/main/claude-input.mjs +9 -3
  8. package/main/claude-launch-env.mjs +54 -0
  9. package/main/claude-models.mjs +39 -7
  10. package/main/claude-plan.mjs +16 -16
  11. package/main/claude-usage.mjs +82 -25
  12. package/main/codex-bin.mjs +51 -249
  13. package/main/codex-session.mjs +38 -3
  14. package/main/codex-threads.mjs +41 -8
  15. package/main/codex-watch.mjs +67 -3
  16. package/main/config.mjs +26 -4
  17. package/main/corner-tag.mjs +489 -0
  18. package/main/crash-report.mjs +26 -3
  19. package/main/engine-bin.mjs +595 -0
  20. package/main/engine-setup.mjs +4 -15
  21. package/main/feedback.mjs +11 -6
  22. package/main/harnesses/claude-stream.mjs +208 -0
  23. package/main/harnesses/claude.mjs +84 -0
  24. package/main/harnesses/codex.mjs +65 -0
  25. package/main/harnesses/common.mjs +78 -0
  26. package/main/harnesses/instructions-file.mjs +116 -0
  27. package/main/harnesses/opencode.mjs +234 -0
  28. package/main/harnesses.mjs +65 -0
  29. package/main/importable-threads.mjs +38 -0
  30. package/main/ipc.mjs +165 -31
  31. package/main/local-app-frames.mjs +82 -0
  32. package/main/main.mjs +100 -4
  33. package/main/memory-gate-history.mjs +51 -12
  34. package/main/memory-gate-server.mjs +67 -11
  35. package/main/memory-gate.mjs +49 -8
  36. package/main/memory-log.mjs +124 -0
  37. package/main/notify.mjs +47 -4
  38. package/main/opencode-server.mjs +271 -0
  39. package/main/opencode.mjs +287 -0
  40. package/main/phone-link.mjs +551 -0
  41. package/main/relay-address.mjs +28 -0
  42. package/main/relay-client.mjs +246 -0
  43. package/main/row-label.mjs +6 -6
  44. package/main/runs-on.mjs +32 -35
  45. package/main/serve.mjs +113 -9
  46. package/main/settings.mjs +148 -10
  47. package/main/shell-path.mjs +86 -0
  48. package/main/ship-queue.mjs +24 -6
  49. package/main/sign-in-files.mjs +3 -7
  50. package/main/source-updater.mjs +27 -1
  51. package/main/start-hold.mjs +73 -0
  52. package/main/store.mjs +1 -1
  53. package/main/supervisor.mjs +832 -428
  54. package/main/task-commands.mjs +13 -17
  55. package/main/task-folders-offthread.mjs +6 -1
  56. package/main/task-folders.mjs +490 -98
  57. package/main/task-terminals.mjs +37 -7
  58. package/main/updater.mjs +71 -9
  59. package/main/worktree-include.mjs +494 -0
  60. package/package.json +10 -1
  61. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
  62. package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
  63. package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
  64. package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
  65. package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
  66. package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
  67. package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
  68. package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
  69. package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
  70. package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
  71. package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
  72. package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
  73. package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
  74. package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
  75. package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
  76. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
  77. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
  78. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
  79. package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
  80. package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
  81. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
  82. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  83. package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
  84. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  85. package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
  86. package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
  87. package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
  88. package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
  89. package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
  90. package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
  91. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
  92. package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
  93. package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
  94. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
  95. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
  96. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
  97. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  98. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
  99. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  100. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
  101. package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
  102. package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
  103. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  104. package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
  105. package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
  106. package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
  107. package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
  108. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
  109. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  110. package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
  111. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
  112. package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
  113. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
  114. package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
  115. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
  116. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
  117. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
  118. package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
  119. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
  120. package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
  121. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
  122. package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
  123. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
  124. package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
  125. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
  126. package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
  127. package/renderer/dist/corner-tag.html +13 -0
  128. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  129. package/renderer/dist/emulator/data/compression/README.md +11 -0
  130. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  131. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  132. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  133. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  134. package/renderer/dist/emulator/data/cores/README.md +22 -0
  135. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  136. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  137. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  138. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  139. package/renderer/dist/emulator/data/cores/package.json +65 -0
  140. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  141. package/renderer/dist/emulator/data/emulator.css +1614 -0
  142. package/renderer/dist/emulator/data/loader.js +170 -0
  143. package/renderer/dist/emulator/data/localization/README.md +63 -0
  144. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  145. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  146. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  147. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  148. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  149. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  150. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  151. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  152. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  153. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  154. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  155. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  156. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  157. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  158. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  159. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  160. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  161. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  162. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  163. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  164. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  165. package/renderer/dist/emulator/data/src/compression.js +142 -0
  166. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  167. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  168. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  169. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  170. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  171. package/renderer/dist/emulator/data/src/storage.js +125 -0
  172. package/renderer/dist/emulator/data/version.json +5 -0
  173. package/renderer/dist/emulator/host.js +297 -0
  174. package/renderer/dist/emulator/index.html +28 -0
  175. package/renderer/dist/emulator/provenance.json +6 -0
  176. package/renderer/dist/index.html +16 -3
  177. package/scripts/approval-server.sh +24 -5
  178. package/scripts/before-publish.mjs +12 -10
  179. package/shared/agent-import.mjs +2 -0
  180. package/shared/agents.d.mts +1 -0
  181. package/shared/bridge-map.mjs +18 -0
  182. package/shared/claude-commands.generated.mjs +6 -4
  183. package/shared/claude-models.generated.mjs +6 -4
  184. package/shared/corner-tag.d.mts +14 -0
  185. package/shared/corner-tag.mjs +141 -0
  186. package/shared/engines.mjs +20 -5
  187. package/shared/harness-definitions.mjs +103 -0
  188. package/shared/notify-rules.d.mts +25 -0
  189. package/shared/notify-rules.mjs +19 -0
  190. package/shared/provider-commands.mjs +10 -2
  191. package/shared/rank.d.mts +2 -0
  192. package/shared/rank.mjs +50 -0
  193. package/shared/relay-address.mjs +6 -0
  194. package/shared/relay-crypto.mjs +165 -0
  195. package/shared/runs-on.d.mts +0 -10
  196. package/shared/runs-on.mjs +25 -49
  197. package/shared/shortcuts.mjs +10 -4
  198. package/shared/spawn-trouble.mjs +57 -6
  199. package/shared/terminal-input.d.mts +1 -0
  200. package/shared/terminal-input.mjs +30 -0
  201. package/shared/terminal-keys.d.mts +1 -0
  202. package/shared/terminal-keys.mjs +16 -0
  203. package/shared/terminal-size.d.mts +4 -0
  204. package/shared/terminal-size.mjs +29 -0
  205. package/shared/usage.d.mts +3 -0
  206. package/shared/usage.mjs +7 -0
  207. package/shared/where-it-runs-from.d.mts +43 -0
  208. package/shared/where-it-runs-from.mjs +155 -0
  209. package/shared/work-items.mjs +25 -1
  210. package/shared/work-lines.mjs +1 -1
  211. package/shared/xterm-invalid-range.d.mts +2 -0
  212. package/shared/xterm-invalid-range.mjs +58 -0
  213. package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
  214. package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
@@ -0,0 +1,489 @@
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, hiddenUntil, keepCorner, readyNow, restingSpot, tagShows, 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
+
56
+ const HIDE_LABELS = { '5m': 'Hide for 5 minutes', '30m': 'Hide for 30 minutes', today: 'Hide for the rest of today' };
57
+ const CHANNELS = [
58
+ 'zero:corner-tag', 'corner-tag:ready', 'corner-tag:size', 'corner-tag:hover', 'corner-tag:toggle',
59
+ 'corner-tag:press', 'corner-tag:release', 'corner-tag:go', 'corner-tag:hide', 'corner-tag:settings', 'corner-tag:menu',
60
+ ];
61
+
62
+ export function createCornerTag({
63
+ BrowserWindow, screen, ipcMain, Menu, powerMonitor, config, saveConfig, mainWindow, load, preload,
64
+ now = () => Date.now(), setTimer = setTimeout, clearTimer = clearTimeout, mac = process.platform === 'darwin',
65
+ }) {
66
+ const wins = { tag: null, card: null };
67
+ const loaded = { tag: false, card: false };
68
+ const hovered = { tag: false, card: false };
69
+ let ready = [];
70
+ let working = 0;
71
+ // The tag's rectangle on screen; its window is exactly this.
72
+ let tag = null;
73
+ let tagSize = { width: 96, height: 26 };
74
+ let measured = false;
75
+ let cardSize = null;
76
+ let open = false;
77
+ // A press on the tag: where the cursor and the tag were when it went down,
78
+ // and whether it has moved far enough to be a drag.
79
+ let press = null;
80
+ let wake = null;
81
+ let closing = null;
82
+ let watching = null;
83
+ let opening = null;
84
+ // After a drag the pointer is still on the tag; the list waits for it to
85
+ // leave and come back rather than opening under it.
86
+ let settling = false;
87
+ let gone = false;
88
+
89
+ const fresh = () => readyNow(ready, now());
90
+ const areas = () => screen.getAllDisplays().map((d) => d.workArea);
91
+ 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;
92
+ const inApp = () => {
93
+ try { return !!mainWindow && !mainWindow.isDestroyed() && mainWindow.isFocused(); } catch { return false; }
94
+ };
95
+ const alive = (part) => !!wins[part] && !wins[part].isDestroyed();
96
+ const rounded = (r) => ({ x: Math.round(r.x), y: Math.round(r.y), width: Math.round(r.width), height: Math.round(r.height) });
97
+ const send = (part, channel, payload) => { if (alive(part) && loaded[part]) wins[part].webContents.send(channel, payload); };
98
+ const sendState = () => {
99
+ const s = { ready: fresh(), working, now: now(), open };
100
+ send('tag', 'corner-tag:state', s);
101
+ send('card', 'corner-tag:state', s);
102
+ };
103
+ const shouldShow = () => tagShows({
104
+ on: config.cornerTag, hiddenUntil: config.cornerTagHiddenUntil ?? 0, now: now(), inApp: inApp(), ready: fresh().length, working,
105
+ });
106
+ // The list may only appear beside a tag that is on screen and should be.
107
+ // A click that was on its way while the tag hid must not open it alone.
108
+ const tagUp = () => !gone && alive('tag') && wins.tag.isVisible() && shouldShow();
109
+
110
+ const placeTag = () => {
111
+ const spot = restingSpot(tag ? { x: tag.x, y: tag.y } : config.cornerTagSpot ?? null, tagSize, areas());
112
+ tag = { ...spot, ...tagSize };
113
+ if (alive('tag')) wins.tag.setBounds(rounded(tag));
114
+ };
115
+
116
+ const placeCard = () => {
117
+ if (!tag || !cardSize || !alive('card')) return;
118
+ wins.card.setBounds(rounded(cardPlacement(tag, cardSize, areaOf(tag)).card));
119
+ };
120
+
121
+ const make = (part) => {
122
+ const win = new BrowserWindow({
123
+ show: false,
124
+ frame: false,
125
+ transparent: true,
126
+ backgroundColor: '#00000000',
127
+ // Clear and unrounded: the page draws the glass and its small corners.
128
+ // The system's frosted material would blur what is behind, but it comes
129
+ // only with the system's own rounder corners or square ones, and the
130
+ // approved tag has neither.
131
+ roundedCorners: false,
132
+ hasShadow: false,
133
+ resizable: false,
134
+ movable: false,
135
+ minimizable: false,
136
+ maximizable: false,
137
+ fullscreenable: false,
138
+ skipTaskbar: true,
139
+ // May take focus, so a press never hands it to the app's main window.
140
+ focusable: true,
141
+ // The first click on a window that is not active still counts.
142
+ acceptFirstMouse: true,
143
+ alwaysOnTop: true,
144
+ // A panel floats over full-screen apps without turning the whole app
145
+ // into a background app, which would take the dock icon away.
146
+ ...(mac ? { type: 'panel' } : {}),
147
+ webPreferences: { preload, contextIsolation: true, nodeIntegration: false, sandbox: true },
148
+ });
149
+ win.setAlwaysOnTop(true, 'floating');
150
+ try { win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true, skipTransformProcessType: true }); } catch {}
151
+ win.on('closed', () => { wins[part] = null; loaded[part] = false; });
152
+ // A reload starts the handshake again: the window goes, anything half
153
+ // done with it is let go, and until the page says it is ready nothing is
154
+ // sent to it and it is not shown. The first load is not a reload.
155
+ win.webContents.on('did-start-loading', () => {
156
+ if (!loaded[part]) return;
157
+ loaded[part] = false;
158
+ hovered[part] = false;
159
+ if (part === 'tag') { endPress(true); if (win.isVisible()) win.hide(); }
160
+ closeCard();
161
+ });
162
+ wins[part] = win;
163
+ if (part === 'tag') placeTag();
164
+ load(win, part);
165
+ return win;
166
+ };
167
+
168
+ const cancelClose = () => { if (closing) { clearTimer(closing); closing = null; } };
169
+
170
+ function closeCard() {
171
+ cancelClose();
172
+ open = false;
173
+ hovered.card = false;
174
+ if (alive('card') && wins.card.isVisible()) wins.card.hide();
175
+ send('tag', 'corner-tag:open', false);
176
+ }
177
+
178
+ const showCard = () => {
179
+ if (open && tagUp() && loaded.card && cardSize && alive('card') && !wins.card.isVisible()) wins.card.showInactive();
180
+ };
181
+
182
+ function openCard() {
183
+ if (!tag || press?.moved || !tagUp()) return;
184
+ open = true;
185
+ send('tag', 'corner-tag:open', true);
186
+ watch();
187
+ reconsider();
188
+ if (!alive('card')) { make('card'); return; }
189
+ sendState();
190
+ placeCard();
191
+ showCard();
192
+ }
193
+
194
+ /* --------------------- where the pointer really is ---------------------- */
195
+ // The page says when the pointer comes and goes, and that is trusted, but a
196
+ // boundary event can go missing, so while the list is open the real cursor
197
+ // is read a few times a second and wins. Nothing is read otherwise.
198
+ 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;
199
+ const drawn = (part) => (alive(part) && wins[part].isVisible() ? wins[part].getBounds() : null);
200
+ const readCursor = () => {
201
+ const pt = screen.getCursorScreenPoint?.();
202
+ if (!pt || press) return false;
203
+ for (const part of ['tag', 'card']) hovered[part] = within(pt, drawn(part));
204
+ return true;
205
+ };
206
+
207
+ function watch() {
208
+ if (watching || gone) return;
209
+ const tick = () => {
210
+ watching = null;
211
+ if (gone) return;
212
+ readCursor();
213
+ reconsider();
214
+ if (open || hovered.tag || hovered.card) watching = setTimer(tick, 200);
215
+ };
216
+ watching = setTimer(tick, 200);
217
+ }
218
+
219
+ // Whether the open list should start, keep or drop its countdown to shutting.
220
+ function reconsider() {
221
+ if (!open || hovered.tag || hovered.card) { cancelClose(); return; }
222
+ if (closing) return;
223
+ closing = setTimer(() => {
224
+ closing = null;
225
+ // The last word is the real cursor's: a pointer that came back without
226
+ // the page noticing keeps the list open.
227
+ readCursor();
228
+ if (!hovered.tag && !hovered.card) closeCard();
229
+ else watch();
230
+ }, CLOSE_AFTER_MS);
231
+ }
232
+
233
+ const cancelOpen = () => { if (opening) { clearTimer(opening); opening = null; } };
234
+
235
+ // The pointer came onto the tag: open the list if it is still there in a
236
+ // moment. The real cursor has the last word, as it does for shutting.
237
+ function meanToOpen() {
238
+ if (open || opening || settling || press) return;
239
+ opening = setTimer(() => {
240
+ opening = null;
241
+ readCursor();
242
+ if (hovered.tag && !settling && !press) openCard();
243
+ }, OPEN_AFTER_MS);
244
+ }
245
+
246
+ /* ------------------------------- the press ------------------------------ */
247
+ // Where the cursor is now, against where it was when the button went down.
248
+ const follow = () => {
249
+ const pt = screen.getCursorScreenPoint?.();
250
+ if (!pt || !press) return;
251
+ const dx = pt.x - press.start.x;
252
+ const dy = pt.y - press.start.y;
253
+ if (!press.moved && Math.hypot(dx, dy) >= DRAG_FROM_PX) {
254
+ press.moved = true;
255
+ cancelOpen();
256
+ settling = true;
257
+ closeCard();
258
+ send('tag', 'corner-tag:dragging', true);
259
+ }
260
+ if (press.moved && alive('tag')) {
261
+ tag = { ...tag, x: press.from.x + dx, y: press.from.y + dy };
262
+ wins.tag.setPosition(Math.round(tag.x), Math.round(tag.y));
263
+ }
264
+ };
265
+
266
+ function step() {
267
+ if (!press || gone) return;
268
+ press.timer = null;
269
+ follow();
270
+ if (now() - press.at > PRESS_GIVES_UP_MS) { endPress(true); return; }
271
+ press.timer = setTimer(step, FRAME_MS);
272
+ }
273
+
274
+ // The button came up (or the press was taken away). A drag lands where the
275
+ // cursor left it and is remembered; a press that never moved is a click.
276
+ function endPress(cancelled) {
277
+ if (!press) return;
278
+ if (press.timer) clearTimer(press.timer);
279
+ press.timer = null;
280
+ const p = press;
281
+ if (!cancelled) follow();
282
+ press = null;
283
+ if (p.moved) {
284
+ tag = { ...restingSpot({ x: tag.x, y: tag.y }, tagSize, areas()), ...tagSize };
285
+ if (alive('tag')) wins.tag.setBounds(rounded(tag));
286
+ saveConfig(config, { cornerTagSpot: { x: Math.round(tag.x), y: Math.round(tag.y) } });
287
+ send('tag', 'corner-tag:dragging', false);
288
+ } else if (!cancelled) {
289
+ cancelOpen();
290
+ goTo(null);
291
+ }
292
+ }
293
+
294
+ // Everything that is not on screen is back where it starts: the list shut,
295
+ // no press half-done, so the tag comes back exactly as it was first drawn.
296
+ const reset = () => {
297
+ endPress(true);
298
+ cancelOpen();
299
+ settling = false;
300
+ closeCard();
301
+ hovered.tag = false;
302
+ send('tag', 'corner-tag:reset', null);
303
+ if (alive('tag') && tag) wins.tag.setBounds(rounded(tag));
304
+ };
305
+
306
+ function refresh() {
307
+ if (gone) return;
308
+ if (wake) { clearTimer(wake); wake = null; }
309
+ const until = config.cornerTagHiddenUntil ?? 0;
310
+ // A hide comes back by itself the moment it runs out.
311
+ if (until > now()) wake = setTimer(refresh, until - now() + 50);
312
+ if (!shouldShow()) {
313
+ if (alive('tag') && wins.tag.isVisible()) { wins.tag.hide(); reset(); }
314
+ else if (open) closeCard();
315
+ return;
316
+ }
317
+ if (!alive('tag')) { make('tag'); return; }
318
+ sendState();
319
+ if (loaded.tag && !wins.tag.isVisible()) wins.tag.showInactive();
320
+ }
321
+
322
+ const bringAppForward = () => {
323
+ if (!mainWindow || mainWindow.isDestroyed()) return false;
324
+ if (mainWindow.isMinimized()) mainWindow.restore();
325
+ mainWindow.show();
326
+ mainWindow.focus();
327
+ return true;
328
+ };
329
+
330
+ // Into the app: on a thread when a line of the list was picked, or just the
331
+ // app as it is when it was the tag.
332
+ function goTo(id) {
333
+ closeCard();
334
+ if (!bringAppForward()) return;
335
+ if (typeof id === 'string' && id) mainWindow.webContents.send('zero:open-item', { id });
336
+ }
337
+
338
+ const hide = (choice) => {
339
+ const until = hiddenUntil(choice, now());
340
+ if (!until) return;
341
+ saveConfig(config, { cornerTagHiddenUntil: until });
342
+ refresh();
343
+ };
344
+
345
+ const openSettings = () => {
346
+ closeCard();
347
+ if (bringAppForward()) mainWindow.webContents.send('zero:open-settings', { pane: 'general' });
348
+ };
349
+
350
+ const partOf = (p) => (p?.part === 'card' ? 'card' : 'tag');
351
+
352
+ /* ------------------------------ from the app's page ---------------------- */
353
+ ipcMain.handle('zero:corner-tag', (_e, state) => {
354
+ ready = Array.isArray(state?.ready) ? state.ready : [];
355
+ working = Number.isFinite(state?.working) ? state.working : 0;
356
+ refresh();
357
+ return null;
358
+ });
359
+
360
+ /* ------------------------------ from the tag's pages --------------------- */
361
+ ipcMain.handle('corner-tag:ready', (_e, p) => {
362
+ const part = partOf(p);
363
+ loaded[part] = true;
364
+ sendState();
365
+ if (part === 'tag') refresh();
366
+ else if (open) { placeCard(); showCard(); }
367
+ return null;
368
+ });
369
+
370
+ ipcMain.handle('corner-tag:size', (_e, p) => {
371
+ if (!p || !(p.width > 0) || !(p.height > 0)) return null;
372
+ const size = { width: Math.ceil(p.width), height: Math.ceil(p.height) };
373
+ if (partOf(p) === 'card') {
374
+ cardSize = size;
375
+ if (open) { placeCard(); showCard(); }
376
+ return null;
377
+ }
378
+ tagSize = size;
379
+ // Nothing moves under the pointer while it is dragging.
380
+ if (press?.moved) return null;
381
+ // The first measure places it where she left it. Every one after keeps
382
+ // the edge nearest its corner still as the words change length.
383
+ tag = tag && measured ? keepCorner(tag, tagSize, areaOf(tag)) : null;
384
+ measured = true;
385
+ placeTag();
386
+ if (open) placeCard();
387
+ return null;
388
+ });
389
+
390
+ ipcMain.handle('corner-tag:hover', (_e, p) => {
391
+ const part = partOf(p);
392
+ hovered[part] = !!p?.inside;
393
+ if (part === 'tag') {
394
+ if (p?.inside) meanToOpen();
395
+ else { settling = false; cancelOpen(); }
396
+ }
397
+ reconsider();
398
+ if (p?.inside) watch();
399
+ return null;
400
+ });
401
+
402
+ ipcMain.handle('corner-tag:toggle', () => {
403
+ if (open) closeCard(); else openCard();
404
+ return null;
405
+ });
406
+
407
+ // `at` is where the button went down, from the page. The message can arrive
408
+ // after the cursor has already moved, and starting from the cursor then put
409
+ // the tag about 30 px off under it. A press on a tag that has since hidden
410
+ // starts nothing.
411
+ ipcMain.handle('corner-tag:press', (_e, at) => {
412
+ const pt = Number.isFinite(at?.x) && Number.isFinite(at?.y) ? { x: at.x, y: at.y } : screen.getCursorScreenPoint?.();
413
+ if (!pt || !tag || !tagUp() || press) return null;
414
+ press = { start: pt, from: { x: tag.x, y: tag.y }, moved: false, at: now(), timer: null };
415
+ press.timer = setTimer(step, FRAME_MS);
416
+ return null;
417
+ });
418
+ ipcMain.handle('corner-tag:release', (_e, p) => { endPress(!!p?.cancelled); return null; });
419
+
420
+ ipcMain.handle('corner-tag:go', (_e, id) => { goTo(id); return null; });
421
+ ipcMain.handle('corner-tag:hide', (_e, choice) => { hide(choice); return null; });
422
+ ipcMain.handle('corner-tag:settings', () => { openSettings(); return null; });
423
+
424
+ // A right-click on the tag offers the same choices as the list does.
425
+ ipcMain.handle('corner-tag:menu', () => {
426
+ if (!alive('tag')) return null;
427
+ closeCard();
428
+ Menu.buildFromTemplate([
429
+ ...HIDE_CHOICES.map((c) => ({ label: HIDE_LABELS[c.key], click: () => hide(c.key) })),
430
+ { type: 'separator' },
431
+ { label: 'Turn off in Settings…', click: openSettings },
432
+ ]).popup({ window: wins.tag });
433
+ return null;
434
+ });
435
+
436
+ /* ------------------------------ from the system -------------------------- */
437
+ // A screen that comes, goes or changes shape: the list shuts and the tag is
438
+ // pulled back onto a screen if it was left hanging off one.
439
+ const reseat = () => {
440
+ if (gone) return;
441
+ closeCard();
442
+ 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));
445
+ };
446
+ // After sleep the screens may have changed and a hide may have run out.
447
+ const woke = () => { reseat(); refresh(); };
448
+
449
+ const onFocus = () => refresh();
450
+ // The app's window is gone, and the tag goes with it. Left behind, it would
451
+ // hold the app open with nothing behind it and nothing it could open.
452
+ const dispose = () => {
453
+ if (gone) return;
454
+ gone = true;
455
+ endPress(true);
456
+ cancelClose();
457
+ if (wake) { clearTimer(wake); wake = null; }
458
+ if (watching) { clearTimer(watching); watching = null; }
459
+ cancelOpen();
460
+ for (const part of ['tag', 'card']) if (alive(part)) wins[part].destroy();
461
+ for (const ch of CHANNELS) { try { ipcMain.removeHandler?.(ch); } catch {} }
462
+ try {
463
+ screen.removeListener?.('display-added', reseat);
464
+ screen.removeListener?.('display-removed', reseat);
465
+ screen.removeListener?.('display-metrics-changed', reseat);
466
+ powerMonitor?.removeListener?.('resume', woke);
467
+ powerMonitor?.removeListener?.('unlock-screen', woke);
468
+ } catch {}
469
+ };
470
+
471
+ if (mainWindow) {
472
+ mainWindow.on('focus', onFocus);
473
+ mainWindow.on('blur', onFocus);
474
+ mainWindow.on('closed', dispose);
475
+ }
476
+ try {
477
+ screen.on('display-added', reseat);
478
+ screen.on('display-removed', reseat);
479
+ screen.on('display-metrics-changed', reseat);
480
+ } catch {}
481
+ try { powerMonitor?.on?.('resume', woke); powerMonitor?.on?.('unlock-screen', woke); } catch {}
482
+
483
+ return {
484
+ refresh,
485
+ dispose,
486
+ // Tests, and nothing else.
487
+ _state: () => ({ ready: fresh(), working, tag, open, visible: alive('tag') && wins.tag.isVisible() }),
488
+ };
489
+ }
@@ -63,15 +63,38 @@ export function report(kind, err, extra = {}) {
63
63
  return write(payload);
64
64
  }
65
65
 
66
+ // THE ONE KIND WHOSE PROCESS DOES NOT SURVIVE BEING REPORTED. `onUncaught`
67
+ // re-throws, by the decision three lines below where it does it, so the main
68
+ // process stops. posthog-node's `capture` only buffers (flushAt 20,
69
+ // flushInterval 10s), and the `will-quit` flush never runs for a process that
70
+ // died, so for this kind the file on disk is the only copy that can reach
71
+ // anybody. It is therefore kept even when the live send said it took the
72
+ // report: one report counted twice is a worse number, and no report at all is
73
+ // a worse app.
74
+ const DIES_REPORTING = new Set(['main-uncaught']);
75
+
66
76
  function write(payload) {
67
77
  try {
68
78
  fs.mkdirSync(ctx.dir, { recursive: true });
69
79
  const file = path.join(ctx.dir, `crash-${payload.ts.replace(/[:.]/g, '-')}-${Math.floor(Math.random() * 1e6)}.json`);
70
80
  fs.writeFileSync(file, JSON.stringify(payload, null, 2));
71
81
  prune();
72
- // No await, no retry, no queue drain here: this runs while the process is
73
- // dying. Delivery is the transport's problem, on the next boot if it has to be.
74
- if (transport) { try { transport(payload); } catch {} }
82
+ // ON DISK FIRST, ALWAYS, because this can run while the process is dying
83
+ // and there is no time to wait on a socket. No await, no retry.
84
+ //
85
+ // AND THEN TAKEN BACK OFF IT IF IT WENT. The queue is for a report nobody
86
+ // has sent yet; a report that HAS been sent and stays in it is sent a
87
+ // second time by the next launch's `drainCrashes`, which is 6 of the 21
88
+ // crash events since the Oct 6 launch being 6 crashes counted twice, and
89
+ // three of them filed against the version that drained them rather than
90
+ // the one they happened on (PostHog, 2026-10-08).
91
+ if (transport) {
92
+ let took = false;
93
+ try { took = transport(payload) !== false; } catch { took = false; }
94
+ if (took && !DIES_REPORTING.has(payload.kind)) {
95
+ try { fs.unlinkSync(file); } catch {}
96
+ }
97
+ }
75
98
  return file;
76
99
  } catch { return null; }
77
100
  }