@dimina-kit/devtools 0.3.1-dev.20260513123738 → 0.3.1-dev.20260515105504

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 (109) hide show
  1. package/README.md +103 -41
  2. package/dist/main/api.d.ts +6 -2
  3. package/dist/main/api.js +5 -2
  4. package/dist/main/app/app.d.ts +1 -1
  5. package/dist/main/app/app.js +71 -36
  6. package/dist/main/app/launch.d.ts +2 -2
  7. package/dist/main/app/launch.js +6 -3
  8. package/dist/main/index.bundle.js +1074 -289
  9. package/dist/main/ipc/app.d.ts +2 -1
  10. package/dist/main/ipc/app.js +5 -4
  11. package/dist/main/ipc/index.d.ts +5 -4
  12. package/dist/main/ipc/index.js +5 -4
  13. package/dist/main/ipc/panels.d.ts +2 -1
  14. package/dist/main/ipc/panels.js +14 -9
  15. package/dist/main/ipc/popover.d.ts +4 -1
  16. package/dist/main/ipc/popover.js +15 -5
  17. package/dist/main/ipc/projects.d.ts +4 -1
  18. package/dist/main/ipc/projects.js +42 -5
  19. package/dist/main/ipc/session.d.ts +4 -1
  20. package/dist/main/ipc/session.js +26 -6
  21. package/dist/main/ipc/settings.d.ts +4 -1
  22. package/dist/main/ipc/settings.js +30 -17
  23. package/dist/main/ipc/simulator-module.d.ts +8 -0
  24. package/dist/main/ipc/simulator-module.js +19 -0
  25. package/dist/main/ipc/simulator.d.ts +2 -1
  26. package/dist/main/ipc/simulator.js +29 -15
  27. package/dist/main/ipc/toolbar.d.ts +2 -1
  28. package/dist/main/ipc/toolbar.js +3 -2
  29. package/dist/main/services/automation/index.js +88 -13
  30. package/dist/main/services/mcp/server.d.ts +2 -1
  31. package/dist/main/services/mcp/server.js +5 -0
  32. package/dist/main/services/module.d.ts +18 -0
  33. package/dist/main/services/module.js +2 -0
  34. package/dist/main/services/notifications/renderer-notifier.d.ts +2 -0
  35. package/dist/main/services/projects/project-repository.d.ts +1 -0
  36. package/dist/main/services/projects/project-repository.js +3 -0
  37. package/dist/main/services/settings/index.d.ts +4 -0
  38. package/dist/main/services/settings/index.js +12 -1
  39. package/dist/main/services/simulator/custom-apis.d.ts +10 -0
  40. package/dist/main/services/simulator/custom-apis.js +26 -0
  41. package/dist/main/services/simulator-storage/index.d.ts +22 -1
  42. package/dist/main/services/simulator-storage/index.js +365 -98
  43. package/dist/main/services/update/update-manager.d.ts +16 -1
  44. package/dist/main/services/update/update-manager.js +18 -15
  45. package/dist/main/services/views/view-manager.d.ts +4 -0
  46. package/dist/main/services/views/view-manager.js +33 -2
  47. package/dist/main/services/window-service.d.ts +24 -0
  48. package/dist/main/services/window-service.js +26 -0
  49. package/dist/main/services/workbench-context.d.ts +15 -0
  50. package/dist/main/services/workbench-context.js +6 -0
  51. package/dist/main/services/workspace/workspace-service.d.ts +1 -0
  52. package/dist/main/services/workspace/workspace-service.js +7 -3
  53. package/dist/main/simulator-apis.d.ts +23 -0
  54. package/dist/main/simulator-apis.js +24 -0
  55. package/dist/main/utils/disposable.d.ts +13 -0
  56. package/dist/main/utils/disposable.js +60 -0
  57. package/dist/main/utils/ipc-registry.d.ts +39 -0
  58. package/dist/main/utils/ipc-registry.js +109 -0
  59. package/dist/main/utils/ipc-schema.d.ts +28 -0
  60. package/dist/main/utils/ipc-schema.js +38 -0
  61. package/dist/main/utils/paths.d.ts +6 -0
  62. package/dist/main/utils/paths.js +6 -0
  63. package/dist/main/utils/sender-policy.d.ts +23 -0
  64. package/dist/main/utils/sender-policy.js +42 -0
  65. package/dist/main/windows/main-window/create.d.ts +6 -0
  66. package/dist/main/windows/main-window/create.js +65 -9
  67. package/dist/main/windows/main-window/events.d.ts +2 -1
  68. package/dist/main/windows/main-window/events.js +25 -2
  69. package/dist/main/windows/navigation-hardening.d.ts +24 -0
  70. package/dist/main/windows/navigation-hardening.js +55 -0
  71. package/dist/main/windows/settings-window/create.js +11 -1
  72. package/dist/preload/index.d.ts +17 -0
  73. package/dist/preload/index.js +17 -0
  74. package/dist/preload/instrumentation/app-data.js +178 -19
  75. package/dist/preload/runtime/custom-apis.d.ts +6 -0
  76. package/dist/preload/runtime/custom-apis.js +18 -0
  77. package/dist/preload/runtime/host.d.ts +1 -0
  78. package/dist/preload/runtime/host.js +13 -0
  79. package/dist/preload/windows/main.cjs +47 -0
  80. package/dist/preload/windows/main.cjs.map +7 -0
  81. package/dist/preload/windows/simulator.js +174 -18
  82. package/dist/renderer/assets/index-DW_0thr-.js +46 -0
  83. package/dist/renderer/assets/{input-DxfYnzX_.js → input-C5M5Wxn6.js} +2 -2
  84. package/dist/renderer/assets/{ipc-transport-ClIvDcYY.js → ipc-transport-CUsl-fS2.js} +2 -2
  85. package/dist/renderer/assets/ipc-transport-CltRuvhi.css +1 -0
  86. package/dist/renderer/assets/{popover-CtXcBJtM.js → popover-BSBPVO0o.js} +2 -2
  87. package/dist/renderer/assets/{select-DMfp49f1.js → select-B1tF3UAc.js} +2 -2
  88. package/dist/renderer/assets/{settings-DteGraEU.js → settings-7w54H6p-.js} +2 -2
  89. package/dist/renderer/assets/settings-api-BjJalghB.js +2 -0
  90. package/dist/renderer/assets/workbenchSettings-q2_-nc8t.js +8 -0
  91. package/dist/renderer/entries/main/index.html +12 -5
  92. package/dist/renderer/entries/popover/index.html +7 -5
  93. package/dist/renderer/entries/settings/index.html +7 -5
  94. package/dist/renderer/entries/workbench-settings/index.html +6 -4
  95. package/dist/shared/ipc-channels.d.ts +22 -0
  96. package/dist/shared/ipc-channels.js +23 -0
  97. package/dist/shared/ipc-schemas.d.ts +113 -0
  98. package/dist/shared/ipc-schemas.js +128 -0
  99. package/dist/shared/types.d.ts +2 -0
  100. package/dist/simulator/assets/simulator-DL6kOCq-.js +9 -0
  101. package/dist/simulator/simulator.html +1 -1
  102. package/package.json +11 -27
  103. package/dist/preload/windows/main.d.ts +0 -6
  104. package/dist/preload/windows/main.js +0 -6
  105. package/dist/renderer/assets/index-D1p6crtf.js +0 -46
  106. package/dist/renderer/assets/ipc-transport-EiWxu_hG.css +0 -1
  107. package/dist/renderer/assets/settings-api-CY84UMpN.js +0 -2
  108. package/dist/renderer/assets/workbenchSettings-DjELLnSJ.js +0 -8
  109. package/dist/simulator/assets/simulator-C1eHXnuq.js +0 -9
@@ -15,10 +15,10 @@
15
15
  * If a user opens DevTools on the simulator the debugger detaches and
16
16
  * events stop flowing until they close it.
17
17
  */
18
- import { app, ipcMain, webContents as wcStatic } from 'electron';
18
+ import { app, webContents as wcStatic } from 'electron';
19
19
  import { SimulatorElementChannel, SimulatorStorageChannel, } from '../../../shared/ipc-channels.js';
20
- let attachedWc = null;
21
- let hostWc = null;
20
+ import { DisposableRegistry } from '../../utils/disposable.js';
21
+ import { IpcRegistry } from '../../utils/ipc-registry.js';
22
22
  function isSimulatorWebview(wc) {
23
23
  if (wc.isDestroyed())
24
24
  return false;
@@ -26,87 +26,381 @@ function isSimulatorWebview(wc) {
26
26
  return false;
27
27
  return wc.getURL().includes('simulator.html');
28
28
  }
29
- function detach() {
30
- if (!attachedWc)
31
- return;
32
- const wc = attachedWc;
33
- attachedWc = null;
29
+ function safeOff(target, event, fn) {
34
30
  try {
35
- if (!wc.isDestroyed() && wc.debugger.isAttached()) {
36
- wc.debugger.detach();
37
- }
31
+ if (target.isDestroyed?.())
32
+ return;
33
+ target.removeListener(event, fn);
38
34
  }
39
35
  catch {
40
36
  // best-effort
41
37
  }
42
38
  }
43
- async function attachToSim(wc) {
44
- if (attachedWc === wc)
45
- return;
46
- if (wc.isDestroyed())
47
- return;
48
- detach();
49
- try {
50
- if (!wc.debugger.isAttached()) {
51
- wc.debugger.attach('1.3');
52
- }
53
- await wc.debugger.sendCommand('DOMStorage.enable');
54
- wc.debugger.on('message', onCdpMessage);
55
- wc.debugger.on('detach', () => {
56
- if (attachedWc === wc)
57
- attachedWc = null;
58
- });
59
- wc.once('destroyed', () => {
60
- if (attachedWc === wc)
61
- attachedWc = null;
62
- });
63
- attachedWc = wc;
39
+ export function setupSimulatorStorage(host, options) {
40
+ const registry = new DisposableRegistry();
41
+ const getActiveAppId = options.getActiveAppId;
42
+ let attachedWc = null;
43
+ let attachDisposables = null;
44
+ /**
45
+ * Cached origin discovered via `executeJavaScript('location.origin')` on
46
+ * first use. The simulator <webview> loads `simulator.html` from a stable
47
+ * scheme/host/port for the lifetime of the CDP attach, so caching avoids an
48
+ * extra IPC round-trip per write call. Cleared on detach.
49
+ */
50
+ let cachedOrigin = null;
51
+ /** Active appId prefix (e.g. `wx123_`), or null when no session is active. */
52
+ function activePrefix() {
53
+ const appId = getActiveAppId();
54
+ return appId ? `${appId}_` : null;
64
55
  }
65
- catch (e) {
66
- console.warn('[storage-watcher] attach failed:', e.message);
56
+ function detachFromSim() {
57
+ if (!attachedWc)
58
+ return;
59
+ const wc = attachedWc;
60
+ attachedWc = null;
61
+ cachedOrigin = null;
62
+ const ad = attachDisposables;
63
+ attachDisposables = null;
64
+ if (ad)
65
+ void ad.disposeAll().catch(() => { });
66
+ try {
67
+ if (!wc.isDestroyed() && wc.debugger.isAttached()) {
68
+ wc.debugger.detach();
69
+ }
70
+ }
71
+ catch {
72
+ // best-effort
73
+ }
67
74
  }
68
- }
69
- function onCdpMessage(_event, method, params) {
70
- if (!hostWc || hostWc.isDestroyed())
71
- return;
72
- const p = params;
73
- let evt;
74
- switch (method) {
75
- case 'DOMStorage.domStorageItemAdded':
76
- evt = { type: 'added', key: String(p.key), newValue: String(p.newValue ?? '') };
77
- break;
78
- case 'DOMStorage.domStorageItemUpdated':
79
- evt = {
80
- type: 'updated',
81
- key: String(p.key),
82
- oldValue: String(p.oldValue ?? ''),
83
- newValue: String(p.newValue ?? ''),
75
+ async function attachToSim(wc) {
76
+ if (attachedWc === wc)
77
+ return;
78
+ if (wc.isDestroyed())
79
+ return;
80
+ detachFromSim();
81
+ try {
82
+ if (!wc.debugger.isAttached()) {
83
+ wc.debugger.attach('1.3');
84
+ }
85
+ await wc.debugger.sendCommand('DOMStorage.enable');
86
+ const attach = new DisposableRegistry();
87
+ attachDisposables = attach;
88
+ const onMessage = (_event, method, params) => forwardCdpMessage(method, params);
89
+ wc.debugger.on('message', onMessage);
90
+ attach.add(() => safeOff(wc.debugger, 'message', onMessage));
91
+ const onDetach = () => {
92
+ if (attachedWc === wc)
93
+ attachedWc = null;
84
94
  };
85
- break;
86
- case 'DOMStorage.domStorageItemRemoved':
87
- evt = { type: 'removed', key: String(p.key) };
88
- break;
89
- case 'DOMStorage.domStorageItemsCleared':
90
- evt = { type: 'cleared' };
91
- break;
92
- default:
95
+ wc.debugger.on('detach', onDetach);
96
+ attach.add(() => safeOff(wc.debugger, 'detach', onDetach));
97
+ const onDestroyed = () => {
98
+ // When the attached wc is destroyed, the lingering attachDisposables
99
+ // hold listener refs to a wc that will never emit again. Dispose the
100
+ // registry here so debugger/wc listeners are removed deterministically
101
+ // and a subsequent attachToSim() starts from a clean slate.
102
+ if (attachedWc === wc) {
103
+ attachedWc = null;
104
+ const ad = attachDisposables;
105
+ attachDisposables = null;
106
+ if (ad)
107
+ void ad.disposeAll().catch(() => { });
108
+ }
109
+ };
110
+ wc.once('destroyed', onDestroyed);
111
+ attach.add(() => safeOff(wc, 'destroyed', onDestroyed));
112
+ attachedWc = wc;
113
+ }
114
+ catch (e) {
115
+ console.warn('[storage-watcher] attach failed:', e.message);
116
+ }
117
+ }
118
+ function forwardCdpMessage(method, params) {
119
+ if (host.isDestroyed())
93
120
  return;
121
+ const p = params;
122
+ const prefix = activePrefix();
123
+ let evt;
124
+ switch (method) {
125
+ case 'DOMStorage.domStorageItemAdded': {
126
+ const key = String(p.key);
127
+ if (prefix && !key.startsWith(prefix))
128
+ return;
129
+ evt = { type: 'added', key, newValue: String(p.newValue ?? '') };
130
+ break;
131
+ }
132
+ case 'DOMStorage.domStorageItemUpdated': {
133
+ const key = String(p.key);
134
+ if (prefix && !key.startsWith(prefix))
135
+ return;
136
+ evt = {
137
+ type: 'updated',
138
+ key,
139
+ oldValue: String(p.oldValue ?? ''),
140
+ newValue: String(p.newValue ?? ''),
141
+ };
142
+ break;
143
+ }
144
+ case 'DOMStorage.domStorageItemRemoved': {
145
+ const key = String(p.key);
146
+ if (prefix && !key.startsWith(prefix))
147
+ return;
148
+ evt = { type: 'removed', key };
149
+ break;
150
+ }
151
+ case 'DOMStorage.domStorageItemsCleared':
152
+ // Forwarded unfiltered: this CDP event only fires on a true
153
+ // origin-wide `localStorage.clear()`. The dimina runtime's
154
+ // `wx.clearStorageSync` removes keys one-by-one (see
155
+ // `simulator-api-storage.ts`), so prefix-scoped clears arrive as
156
+ // a stream of `domStorageItemRemoved` events that the filter above
157
+ // handles. Any `cleared` we see here is genuinely a full-origin
158
+ // wipe (e.g. from DevTools console) and should propagate.
159
+ evt = { type: 'cleared' };
160
+ break;
161
+ default:
162
+ return;
163
+ }
164
+ host.send(SimulatorStorageChannel.Event, evt);
94
165
  }
95
- hostWc.send(SimulatorStorageChannel.Event, evt);
96
- }
97
- async function getSnapshot() {
98
- if (!attachedWc || attachedWc.isDestroyed())
99
- return [];
100
- try {
101
- const origin = await attachedWc.executeJavaScript('location.origin');
102
- const result = (await attachedWc.debugger.sendCommand('DOMStorage.getDOMStorageItems', {
103
- storageId: { securityOrigin: origin, isLocalStorage: true },
104
- }));
105
- return result.entries.map(([key, value]) => ({ key, value }));
166
+ /**
167
+ * Lazy-attach guard.
168
+ *
169
+ * The `attachToSim` call inside `web-contents-created` only fires after the
170
+ * simulator webview emits `did-finish-load`. UI affordances (the storage
171
+ * panel's "+ 新增", inline edit, delete, clear) are reachable as soon as
172
+ * the right-pane tab renders — which can happen BEFORE that load completes.
173
+ * Without this hook, a fast click bounces off `!attachedWc` and the write
174
+ * IPC returns `{ ok:false, error:'simulator not attached' }` silently,
175
+ * leaving the user (and the e2e suite) staring at a no-op.
176
+ *
177
+ * Behaviour:
178
+ * - If a wc is already attached, this is a fast no-op.
179
+ * - If no wc is attached, scan `webContents.getAllWebContents()` for a
180
+ * simulator webview and `await attachToSim()` on the first match.
181
+ * - If no simulator exists at all, leave `attachedWc` null so the
182
+ * caller's existing `not attached` error branch fires — preserving
183
+ * the documented contract exercised by `Set returns ok:false when
184
+ * no simulator is attached`.
185
+ */
186
+ async function ensureAttached() {
187
+ if (attachedWc && !attachedWc.isDestroyed())
188
+ return;
189
+ const sim = findSimulatorWebContents();
190
+ if (!sim)
191
+ return;
192
+ await attachToSim(sim);
106
193
  }
107
- catch {
108
- return [];
194
+ async function getStorageId() {
195
+ if (!attachedWc || attachedWc.isDestroyed())
196
+ return null;
197
+ if (!cachedOrigin) {
198
+ try {
199
+ cachedOrigin = String(await attachedWc.executeJavaScript('location.origin'));
200
+ }
201
+ catch {
202
+ return null;
203
+ }
204
+ }
205
+ return { securityOrigin: cachedOrigin, isLocalStorage: true };
206
+ }
207
+ /**
208
+ * Push a storage event synthesised from the write path itself, in addition
209
+ * to whatever CDP forwards via `DOMStorage.domStorageItem*`. The CDP
210
+ * tracker has a small registration window after `DOMStorage.enable`
211
+ * during which the first write's notification can be dropped — the e2e
212
+ * `add a new entry via the footer form` case races exactly this gap when
213
+ * the storage tab is clicked before the simulator finishes loading and
214
+ * `ensureAttached()` performs the first attach. Renderer state reducers
215
+ * are idempotent on the same key, so an extra push from here cannot
216
+ * duplicate rows even when CDP also delivers.
217
+ *
218
+ * Active-prefix filtering mirrors `forwardCdpMessage` so the panel's
219
+ * per-appId filter behaves identically whether the event came from CDP
220
+ * or from this synthesised push.
221
+ */
222
+ function pushSyntheticEvent(evt) {
223
+ if (host.isDestroyed())
224
+ return;
225
+ const prefix = activePrefix();
226
+ if (prefix && !evt.key.startsWith(prefix))
227
+ return;
228
+ host.send(SimulatorStorageChannel.Event, evt);
229
+ }
230
+ async function setItem(key, value) {
231
+ await ensureAttached();
232
+ if (!attachedWc || attachedWc.isDestroyed()) {
233
+ return { ok: false, error: 'simulator not attached' };
234
+ }
235
+ const storageId = await getStorageId();
236
+ if (!storageId)
237
+ return { ok: false, error: 'failed to resolve simulator origin' };
238
+ try {
239
+ await attachedWc.debugger.sendCommand('DOMStorage.setDOMStorageItem', {
240
+ storageId,
241
+ key,
242
+ value,
243
+ });
244
+ pushSyntheticEvent({ type: 'added', key, newValue: value });
245
+ return { ok: true };
246
+ }
247
+ catch (e) {
248
+ return { ok: false, error: e.message };
249
+ }
250
+ }
251
+ async function removeItem(key) {
252
+ await ensureAttached();
253
+ if (!attachedWc || attachedWc.isDestroyed()) {
254
+ return { ok: false, error: 'simulator not attached' };
255
+ }
256
+ const storageId = await getStorageId();
257
+ if (!storageId)
258
+ return { ok: false, error: 'failed to resolve simulator origin' };
259
+ try {
260
+ await attachedWc.debugger.sendCommand('DOMStorage.removeDOMStorageItem', {
261
+ storageId,
262
+ key,
263
+ });
264
+ pushSyntheticEvent({ type: 'removed', key });
265
+ return { ok: true };
266
+ }
267
+ catch (e) {
268
+ return { ok: false, error: e.message };
269
+ }
270
+ }
271
+ // Scoped clear: when an appId is active, removes only its prefixed keys so
272
+ // we don't nuke other projects' state sharing the simulator partition.
273
+ // Falls back to origin-wide `DOMStorage.clear` only when no active appId is
274
+ // set (matches the snapshot filter's documented null-fallback).
275
+ async function clearScoped() {
276
+ await ensureAttached();
277
+ if (!attachedWc || attachedWc.isDestroyed()) {
278
+ return { ok: false, error: 'simulator not attached' };
279
+ }
280
+ const storageId = await getStorageId();
281
+ if (!storageId)
282
+ return { ok: false, error: 'failed to resolve simulator origin' };
283
+ const prefix = activePrefix();
284
+ try {
285
+ if (!prefix) {
286
+ await attachedWc.debugger.sendCommand('DOMStorage.clear', { storageId });
287
+ return { ok: true };
288
+ }
289
+ const result = (await attachedWc.debugger.sendCommand('DOMStorage.getDOMStorageItems', {
290
+ storageId,
291
+ }));
292
+ for (const [key] of result.entries) {
293
+ if (!key.startsWith(prefix))
294
+ continue;
295
+ await attachedWc.debugger.sendCommand('DOMStorage.removeDOMStorageItem', {
296
+ storageId,
297
+ key,
298
+ });
299
+ }
300
+ return { ok: true };
301
+ }
302
+ catch (e) {
303
+ return { ok: false, error: e.message };
304
+ }
305
+ }
306
+ // Origin-wide clear regardless of the active appId — wipes every project's
307
+ // state in the shared simulator partition. The renderer guards this with a
308
+ // confirm dialog; we don't second-guess here.
309
+ async function clearAll() {
310
+ await ensureAttached();
311
+ if (!attachedWc || attachedWc.isDestroyed()) {
312
+ return { ok: false, error: 'simulator not attached' };
313
+ }
314
+ const storageId = await getStorageId();
315
+ if (!storageId)
316
+ return { ok: false, error: 'failed to resolve simulator origin' };
317
+ try {
318
+ await attachedWc.debugger.sendCommand('DOMStorage.clear', { storageId });
319
+ return { ok: true };
320
+ }
321
+ catch (e) {
322
+ return { ok: false, error: e.message };
323
+ }
324
+ }
325
+ async function getSnapshot() {
326
+ await ensureAttached();
327
+ if (!attachedWc || attachedWc.isDestroyed())
328
+ return [];
329
+ try {
330
+ const storageId = await getStorageId();
331
+ if (!storageId)
332
+ return [];
333
+ const result = (await attachedWc.debugger.sendCommand('DOMStorage.getDOMStorageItems', {
334
+ storageId,
335
+ }));
336
+ const prefix = activePrefix();
337
+ const filtered = prefix
338
+ ? result.entries.filter(([key]) => key.startsWith(prefix))
339
+ : result.entries;
340
+ // Keys are returned with their `${appId}_` prefix intact; stripping is
341
+ // a follow-up so the panel UI can stay compatible with the raw keys.
342
+ return filtered.map(([key, value]) => ({ key, value }));
343
+ }
344
+ catch {
345
+ return [];
346
+ }
347
+ }
348
+ // Attach to any simulator webview that already exists (project may have
349
+ // been opened before this function runs).
350
+ for (const wc of wcStatic.getAllWebContents()) {
351
+ if (isSimulatorWebview(wc)) {
352
+ void attachToSim(wc);
353
+ break;
354
+ }
109
355
  }
356
+ // Catch new simulator webviews on creation. did-finish-load is the right
357
+ // moment because getURL() may still be 'about:blank' at creation.
358
+ // Track each per-wc listener so it can be removed on storage dispose,
359
+ // and also self-clean when the wc is destroyed.
360
+ const wcSubs = new Map();
361
+ const onWcCreated = (_event, wc) => {
362
+ const sub = new DisposableRegistry();
363
+ wcSubs.set(wc, sub);
364
+ const onFinishLoad = () => {
365
+ if (isSimulatorWebview(wc))
366
+ void attachToSim(wc);
367
+ };
368
+ wc.on('did-finish-load', onFinishLoad);
369
+ sub.add(() => safeOff(wc, 'did-finish-load', onFinishLoad));
370
+ wc.once('destroyed', () => {
371
+ void sub.disposeAll().catch(() => { });
372
+ wcSubs.delete(wc);
373
+ });
374
+ };
375
+ app.on('web-contents-created', onWcCreated);
376
+ registry.add(() => {
377
+ app.removeListener('web-contents-created', onWcCreated);
378
+ });
379
+ registry.add(async () => {
380
+ const subs = Array.from(wcSubs.values());
381
+ wcSubs.clear();
382
+ for (const sub of subs) {
383
+ await sub.disposeAll().catch(() => { });
384
+ }
385
+ });
386
+ // IPC handlers — gated by the workbench SenderPolicy when provided so this
387
+ // module routes through the same C-stage sender white-list as every other
388
+ // workbench-built-in (main-window renderer + overlay views only). Without
389
+ // a policy (unit tests with mocked electron) the registry is a transparent
390
+ // pass-through and still owns the removeHandler lifecycle.
391
+ const ipc = new IpcRegistry(options.senderPolicy);
392
+ ipc.handle(SimulatorStorageChannel.GetSnapshot, () => getSnapshot());
393
+ ipc.handle(SimulatorStorageChannel.GetActivePrefix, () => activePrefix() ?? '');
394
+ ipc.handle(SimulatorStorageChannel.Set, (_event, payload) => setItem(String(payload?.key ?? ''), String(payload?.value ?? '')));
395
+ ipc.handle(SimulatorStorageChannel.Remove, (_event, payload) => removeItem(String(payload?.key ?? '')));
396
+ ipc.handle(SimulatorStorageChannel.Clear, () => clearScoped());
397
+ ipc.handle(SimulatorStorageChannel.ClearAll, () => clearAll());
398
+ ipc.handle(SimulatorElementChannel.Inspect, (_event, sid) => inspectElement(sid));
399
+ ipc.handle(SimulatorElementChannel.Clear, () => clearElementInspection());
400
+ registry.add(ipc);
401
+ // Detach active CDP session last
402
+ registry.add(() => detachFromSim());
403
+ return registry;
110
404
  }
111
405
  // Element inspection delegates to the simulator preload's __simulatorData
112
406
  // bridge via executeJavaScript. Looked up independently from `attachedWc` so
@@ -145,31 +439,4 @@ async function clearElementInspection() {
145
439
  // best-effort
146
440
  }
147
441
  }
148
- export function setupSimulatorStorage(host) {
149
- hostWc = host;
150
- // Attach to any simulator webview that already exists (project may have
151
- // been opened before this function runs).
152
- for (const wc of wcStatic.getAllWebContents()) {
153
- if (isSimulatorWebview(wc)) {
154
- void attachToSim(wc);
155
- break;
156
- }
157
- }
158
- // Catch new simulator webviews on creation. did-finish-load is the right
159
- // moment because getURL() may still be 'about:blank' at creation.
160
- app.on('web-contents-created', (_event, wc) => {
161
- wc.on('did-finish-load', () => {
162
- if (isSimulatorWebview(wc)) {
163
- void attachToSim(wc);
164
- }
165
- });
166
- wc.on('did-navigate-in-page', () => {
167
- // Project switched but same webview — attach already correct, debugger
168
- // session survives in-page navigations. No-op.
169
- });
170
- });
171
- ipcMain.handle(SimulatorStorageChannel.GetSnapshot, getSnapshot);
172
- ipcMain.handle(SimulatorElementChannel.Inspect, (_event, sid) => inspectElement(sid));
173
- ipcMain.handle(SimulatorElementChannel.Clear, clearElementInspection);
174
- }
175
442
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,6 @@
1
1
  import { BrowserWindow } from 'electron';
2
2
  import type { UpdateChecker, UpdateInfo } from '../../../shared/types.js';
3
+ import { type SenderPolicy } from '../../utils/ipc-registry.js';
3
4
  export interface UpdateManagerOptions {
4
5
  checker: UpdateChecker;
5
6
  mainWindow: BrowserWindow;
@@ -9,6 +10,13 @@ export interface UpdateManagerOptions {
9
10
  initialDelay?: number;
10
11
  /** Override the version string passed to the checker. Default: app.getVersion() */
11
12
  getCurrentVersion?: () => string;
13
+ /**
14
+ * Optional sender gate. When provided, IpcRegistry rejects invocations
15
+ * whose `event.sender` is not whitelisted (same C-stage policy used by
16
+ * every other workbench-built-in). Omitted in unit tests that mock
17
+ * electron without a real WebContents.
18
+ */
19
+ senderPolicy?: SenderPolicy;
12
20
  }
13
21
  export declare class UpdateManager {
14
22
  private checker;
@@ -18,6 +26,7 @@ export declare class UpdateManager {
18
26
  private downloadedPath;
19
27
  private timer;
20
28
  private initialTimer;
29
+ private ipc;
21
30
  constructor(opts: UpdateManagerOptions);
22
31
  private registerIpc;
23
32
  check(): Promise<{
@@ -30,7 +39,13 @@ export declare class UpdateManager {
30
39
  error?: string;
31
40
  }>;
32
41
  install(): void;
33
- dispose(): void;
42
+ /**
43
+ * Tear down timers + IPC handlers. Returns a Promise so callers that need
44
+ * a settled cleanup (e.g. workbench registry shutdown, the unit-test
45
+ * lifecycle suite) can await it; calling without await is also safe since
46
+ * the timer clears + handler removals are scheduled synchronously.
47
+ */
48
+ dispose(): Promise<void>;
34
49
  private startPeriodicCheck;
35
50
  private checkAndNotify;
36
51
  }
@@ -1,4 +1,6 @@
1
- import { app, ipcMain, shell } from 'electron';
1
+ import { app, shell } from 'electron';
2
+ import { UpdateChannel } from '../../../shared/ipc-channels.js';
3
+ import { IpcRegistry } from '../../utils/ipc-registry.js';
2
4
  export class UpdateManager {
3
5
  checker;
4
6
  mainWindow;
@@ -7,23 +9,20 @@ export class UpdateManager {
7
9
  downloadedPath = null;
8
10
  timer = null;
9
11
  initialTimer = null;
12
+ ipc;
10
13
  constructor(opts) {
11
14
  this.checker = opts.checker;
12
15
  this.mainWindow = opts.mainWindow;
13
16
  this.getCurrentVersion = opts.getCurrentVersion ?? (() => app.getVersion());
17
+ this.ipc = new IpcRegistry(opts.senderPolicy);
14
18
  this.registerIpc();
15
19
  this.startPeriodicCheck(opts.checkInterval ?? 60 * 60 * 1000, opts.initialDelay ?? 5000);
16
20
  }
17
21
  registerIpc() {
18
- ipcMain.handle('updates:check', async () => {
19
- return this.check();
20
- });
21
- ipcMain.handle('updates:download', async () => {
22
- return this.download();
23
- });
24
- ipcMain.handle('updates:install', async () => {
25
- this.install();
26
- });
22
+ this.ipc
23
+ .handle(UpdateChannel.Check, async () => this.check())
24
+ .handle(UpdateChannel.Download, async () => this.download())
25
+ .handle(UpdateChannel.Install, async () => this.install());
27
26
  }
28
27
  async check() {
29
28
  try {
@@ -47,7 +46,7 @@ export class UpdateManager {
47
46
  }
48
47
  try {
49
48
  const filePath = await this.checker.downloadUpdate(this.latestUpdate, (percent) => {
50
- this.mainWindow.webContents.send('updates:downloadProgress', { percent });
49
+ this.mainWindow.webContents.send(UpdateChannel.DownloadProgress, { percent });
51
50
  });
52
51
  this.downloadedPath = filePath;
53
52
  return { success: true, filePath };
@@ -63,6 +62,12 @@ export class UpdateManager {
63
62
  app.quit();
64
63
  }
65
64
  }
65
+ /**
66
+ * Tear down timers + IPC handlers. Returns a Promise so callers that need
67
+ * a settled cleanup (e.g. workbench registry shutdown, the unit-test
68
+ * lifecycle suite) can await it; calling without await is also safe since
69
+ * the timer clears + handler removals are scheduled synchronously.
70
+ */
66
71
  dispose() {
67
72
  if (this.timer) {
68
73
  clearInterval(this.timer);
@@ -72,9 +77,7 @@ export class UpdateManager {
72
77
  clearTimeout(this.initialTimer);
73
78
  this.initialTimer = null;
74
79
  }
75
- ipcMain.removeHandler('updates:check');
76
- ipcMain.removeHandler('updates:download');
77
- ipcMain.removeHandler('updates:install');
80
+ return this.ipc.dispose();
78
81
  }
79
82
  startPeriodicCheck(interval, initialDelay) {
80
83
  this.initialTimer = setTimeout(() => void this.checkAndNotify(), initialDelay);
@@ -83,7 +86,7 @@ export class UpdateManager {
83
86
  async checkAndNotify() {
84
87
  const result = await this.check();
85
88
  if (result.hasUpdate && result.info) {
86
- this.mainWindow.webContents.send('updates:available', result.info);
89
+ this.mainWindow.webContents.send(UpdateChannel.Available, result.info);
87
90
  }
88
91
  }
89
92
  }
@@ -68,6 +68,10 @@ export interface ViewManager {
68
68
  hasSimulatorView(): boolean;
69
69
  /** Return the settings overlay's WebContents (for renderer-notifier). */
70
70
  getSettingsWebContents(): WebContents | null;
71
+ /** Return the webContents ID of the settings overlay if alive, else null. */
72
+ getSettingsWebContentsId(): number | null;
73
+ /** Return the webContents ID of the popover overlay if alive, else null. */
74
+ getPopoverWebContentsId(): number | null;
71
75
  /** Update lastSimWidth; reposition simulator + settings if they are added. */
72
76
  resize(simWidth: number): void;
73
77
  /** Show or hide the simulator overlay based on visibility flag. */