@dimina-kit/devtools 0.4.0-dev.20260625063832 → 0.4.0-dev.20260629114208

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 (64) hide show
  1. package/dist/main/app/app.js +2 -0
  2. package/dist/main/index.bundle.js +230 -144
  3. package/dist/main/ipc/bridge-router.d.ts +21 -0
  4. package/dist/main/ipc/bridge-router.js +63 -3
  5. package/dist/main/services/automation/index.js +0 -1
  6. package/dist/main/services/elements-forward/index.js +81 -72
  7. package/dist/main/services/mcp/target-manager.d.ts +3 -3
  8. package/dist/main/services/views/miniapp-partition.d.ts +18 -8
  9. package/dist/main/services/views/miniapp-partition.js +34 -13
  10. package/dist/main/services/views/view-manager.js +59 -36
  11. package/dist/main/services/workbench-coi-server.d.ts +7 -0
  12. package/dist/main/services/workbench-coi-server.js +6 -0
  13. package/dist/main/services/workbench-context.d.ts +4 -2
  14. package/dist/main/services/workbench-context.js +1 -0
  15. package/dist/main/services/workspace/op-lock.d.ts +26 -0
  16. package/dist/main/services/workspace/op-lock.js +23 -0
  17. package/dist/main/services/workspace/workspace-service.d.ts +7 -0
  18. package/dist/main/services/workspace/workspace-service.js +197 -103
  19. package/dist/main/windows/service-host-window/create.d.ts +5 -1
  20. package/dist/main/windows/service-host-window/create.js +6 -3
  21. package/dist/native-host/render/render.js +485 -449
  22. package/dist/native-host/service/service.js +1 -1
  23. package/dist/renderer/assets/index-BMwmmaSp.js +46 -0
  24. package/dist/renderer/entries/main/index.html +1 -1
  25. package/dist/shared/types.d.ts +15 -0
  26. package/dist/vscode-workbench/assets/__vite-browser-external-B0DTNerG.js +1 -0
  27. package/dist/vscode-workbench/assets/__vite-browser-external-BuLjVMfN.js +1 -0
  28. package/dist/vscode-workbench/assets/__vite-browser-external-thkx7I19.js +1 -0
  29. package/dist/vscode-workbench/assets/{dist-SFZ2HNEz.js → dist-CS7SQP3K.js} +4 -4
  30. package/dist/vscode-workbench/assets/dist-VSgdjUlW.js +8 -0
  31. package/dist/vscode-workbench/assets/editor.worker-DmMg51ee.js +41 -0
  32. package/dist/vscode-workbench/assets/extensionHost.worker-BMpl1cpB.js +207 -0
  33. package/dist/vscode-workbench/assets/{iconv-lite-umd-izd5l6fa.js → iconv-lite-umd-D3q-1-o6.js} +1 -1
  34. package/dist/vscode-workbench/assets/{iconv-lite-umd-COfjYshr.js → iconv-lite-umd-Jn9RTrsv.js} +2 -2
  35. package/dist/vscode-workbench/assets/index-zjigpZ25.js +2842 -0
  36. package/dist/vscode-workbench/assets/{jschardet-Bf-VMK4F.js → jschardet-CjCVc2Gw.js} +1 -1
  37. package/dist/vscode-workbench/assets/{jschardet-Dh5TyKsQ.js → jschardet-Cu4ufeHq.js} +1 -1
  38. package/dist/vscode-workbench/assets/{layout.contribution.darwin-cz2ttHTW.js → layout.contribution.darwin-CoH1BZtz.js} +1 -1
  39. package/dist/vscode-workbench/assets/{layout.contribution.linux-fEtdzbcY.js → layout.contribution.linux-Di2FF-CG.js} +1 -1
  40. package/dist/vscode-workbench/assets/{layout.contribution.win-BFyOKI8c.js → layout.contribution.win-moiNStil.js} +1 -1
  41. package/dist/vscode-workbench/assets/{main-Bh8TI5LP.js → main-DzHL45xo.js} +4 -4
  42. package/dist/vscode-workbench/assets/{main2-Bds9Fhfn.js → main2-CWVd-Hyp.js} +1 -1
  43. package/dist/vscode-workbench/assets/{worker-D9ti6A0r.js → worker-CRTb-Arc.js} +7 -7
  44. package/dist/vscode-workbench/assets/{worker-DQtXDXIZ.js → worker-CVB8WPuM.js} +2 -2
  45. package/dist/vscode-workbench/index.html +1 -4
  46. package/package.json +7 -37
  47. package/dist/renderer/assets/index-6XUSo6Su.js +0 -46
  48. package/dist/vscode-workbench/assets/__vite-browser-external-BUwiolNy.js +0 -1
  49. package/dist/vscode-workbench/assets/__vite-browser-external-CmG9GjjO.js +0 -1
  50. package/dist/vscode-workbench/assets/__vite-browser-external-DGJPTKLg.js +0 -1
  51. package/dist/vscode-workbench/assets/_commonjsHelpers-qcUHfnrg.js +0 -1
  52. package/dist/vscode-workbench/assets/chunk-B3K2TuZy.js +0 -1
  53. package/dist/vscode-workbench/assets/chunk-DECur_0Z.js +0 -1
  54. package/dist/vscode-workbench/assets/dist-tesJ1Kd2.js +0 -8
  55. package/dist/vscode-workbench/assets/editor.worker-BC1qOyXG.js +0 -41
  56. package/dist/vscode-workbench/assets/extension.api-BWA2aPAU.js +0 -1
  57. package/dist/vscode-workbench/assets/extensionHost.worker-Dvu35anN.js +0 -207
  58. package/dist/vscode-workbench/assets/index-CQ5xP0pp.js +0 -1656
  59. package/dist/vscode-workbench/assets/localExtensionHost-Ccz7QUdA.js +0 -1188
  60. package/dist/vscode-workbench/assets/main-BDgdQmvs.js +0 -4
  61. package/dist/vscode-workbench/assets/main2-DwfDVleQ.js +0 -1
  62. package/dist/vscode-workbench/assets/preload-helper-xBbMyY7u.js +0 -1
  63. /package/dist/vscode-workbench/assets/{_.contribution-C9aS_G-o.js → _.contribution-B46wcGKQ.js} +0 -0
  64. /package/dist/vscode-workbench/assets/{_commonjsHelpers-BPKu-ps5.js → _commonjsHelpers-DMmVv2OA.js} +0 -0
@@ -119,11 +119,27 @@ function resolveCurrentApp(state, ctx, appId) {
119
119
  if (match)
120
120
  return match;
121
121
  }
122
- // Maps preserve insertion order; the last entry is the most recent spawn.
122
+ // During a project close the workspace nulls its session BEFORE the bridge app
123
+ // session is torn down (disposeSession runs before disposeAll). Resolving here
124
+ // would hand a consumer the closing project's dying guest, so refuse to guess
125
+ // while a close is in flight.
126
+ if (ctx.workspace?.isClosing?.())
127
+ return undefined;
128
+ // No appId hint and no workspace session to disambiguate. Picking by appId is
129
+ // impossible, so fall back to the most-recent spawn — but ONLY when every live
130
+ // session belongs to the same app. Same-appId multiples are a respawn/reopen
131
+ // in progress where the newest is the live one (insertion order = spawn
132
+ // order). Multiple DISTINCT appIds mean a previous project is mid-teardown
133
+ // alongside the new one; any pick is a guess that can resolve the WRONG
134
+ // project's content (the screenshot/inspect-stale-app bug class), so prefer
135
+ // null over a wrong guess.
123
136
  let last;
124
- for (const ap of state.appSessions.values())
137
+ const distinctAppIds = new Set();
138
+ for (const ap of state.appSessions.values()) {
125
139
  last = ap;
126
- return last;
140
+ distinctAppIds.add(ap.appId);
141
+ }
142
+ return distinctAppIds.size <= 1 ? last : undefined;
127
143
  }
128
144
  export function installBridgeRouter(ctx) {
129
145
  const state = {
@@ -262,6 +278,22 @@ export function installBridgeRouter(ctx) {
262
278
  }
263
279
  }
264
280
  },
281
+ disposeSessionsForSimulator: (simulatorWcId) => {
282
+ // Snapshot ids first: disposeAppSession mutates state.appSessions.
283
+ const ids = [];
284
+ for (const [id, ap] of state.appSessions) {
285
+ if (ap.simulatorWc.id === simulatorWcId)
286
+ ids.push(id);
287
+ }
288
+ // disposeAppSession's synchronous prefix clears every map + closes the
289
+ // render guests + the service window, so the bridge is clean the moment
290
+ // this is called. The joined promise resolves after the async tail
291
+ // (pool.release / resourceServer.close) so a caller can sequence a reopen
292
+ // after full teardown. disposeAppSession logs those tail failures
293
+ // internally, so this resolves rather than rejecting on them — it is a
294
+ // completion signal, not an error channel.
295
+ return Promise.all(ids.map((id) => disposeAppSession(state, id))).then(() => { });
296
+ },
265
297
  debugTap: state.debugTap,
266
298
  };
267
299
  ctx.bridge = bridgeHandle;
@@ -506,6 +538,13 @@ async function handleSpawn(state, ctx, event, opts) {
506
538
  serviceWindow = createServiceHostWindow({
507
539
  bridgeId,
508
540
  appId,
541
+ // Same (appId, projectPath) pair the simulator WCV uses, so this project's
542
+ // service host and render guests land on ONE partition while a different
543
+ // project with the same appId is isolated. Use the path captured at spawn
544
+ // ENTRY (workspaceProjectPath), not a re-read: a project switch during the
545
+ // awaits above would otherwise give the service host a different path than
546
+ // the simulator WCV was built with, splitting the partition.
547
+ projectPath: workspaceProjectPath || undefined,
509
548
  pagePath,
510
549
  pkgRoot,
511
550
  root,
@@ -585,6 +624,17 @@ async function handleSpawn(state, ctx, event, opts) {
585
624
  void disposeAppSession(state, appSessionId);
586
625
  };
587
626
  simulatorWc.once('destroyed', onSimulatorDestroyed);
627
+ // The simulator WCV may have been torn down DURING this spawn's awaits
628
+ // (loadAppConfig / resource server / service-host creation) — e.g. a project
629
+ // close/switch that ran while we were mid-flight. In that case its 'destroyed'
630
+ // event already fired and the once() above will never run, so the freshly
631
+ // registered session would be a zombie owned by a dead simulator (the
632
+ // teardown's disposeSessionsForSimulator snapshot taken before appSessions.set
633
+ // could not have included it). Dispose now — idempotent with the graceful
634
+ // path — so a closed/superseded simulator can't leave a resurrected session.
635
+ if (simulatorWc.isDestroyed()) {
636
+ void disposeAppSession(state, appSessionId);
637
+ }
588
638
  if (usedPool) {
589
639
  // A pooled/fallback window passes through about:blank (warm load or the
590
640
  // fallback's initial load). Boot only once the REAL service.html navigation
@@ -1232,6 +1282,16 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
1232
1282
  for (const page of ap.pages.values()) {
1233
1283
  if (page.renderWc && !page.renderWc.isDestroyed()) {
1234
1284
  state.wcIdToBridgeId.delete(page.renderWc.id);
1285
+ // The render-host <webview> guest is otherwise reclaimed only when its
1286
+ // host simulator WCV is destroyed, and that cascade is async. Close it
1287
+ // here so a project-close / respawn tears the guest down deterministically
1288
+ // rather than leaving the outgoing project's guest alive to be
1289
+ // screenshotted or re-resolved by the next project. Idempotent with the
1290
+ // cascade (guarded on isDestroyed).
1291
+ try {
1292
+ page.renderWc.close();
1293
+ }
1294
+ catch { /* guest already gone */ }
1235
1295
  }
1236
1296
  state.pageSessions.delete(page.bridgeId);
1237
1297
  }
@@ -91,7 +91,6 @@ export async function startAutomationServer(ctx, port = 0) {
91
91
  if (ipcMessageHostHandler) {
92
92
  try {
93
93
  if (!attachedSim.isDestroyed()) {
94
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
94
  ;
96
95
  attachedSim.removeListener('ipc-message-host', ipcMessageHostHandler);
97
96
  }
@@ -132,6 +132,24 @@ export function buildElementsHookScript() {
132
132
  return 'partial';
133
133
  }catch(e){ return 'error:' + (e && e.message); }})()`;
134
134
  }
135
+ /**
136
+ * One reconcile tick run in the front-end realm: idempotently (re)install the
137
+ * hook AND drain the outbound queue in a SINGLE round-trip. The front-end's
138
+ * `globalThis` is wiped on every devtools (re)load (service-host pool swap on a
139
+ * hot-reload respawn re-opens DevTools and re-bootstraps the front-end), so the
140
+ * hook must be re-asserted continuously — not once on `dom-ready`. Returns
141
+ * `{ status, batch }`: `status` is `'installed'` the tick we (re)wrap a freshly
142
+ * loaded front-end (the caller re-primes + re-pulls so Elements snaps back to the
143
+ * render guest), `'already'` once it is in place, `'partial'` while the embedder
144
+ * global has not appeared yet. `batch` is the drained command queue.
145
+ */
146
+ function buildReconcileScript() {
147
+ return `(function(){`
148
+ + `var status; try { status = ${buildElementsHookScript()}; } catch(e){ status = 'error:'+(e&&e.message); }`
149
+ + `var batch = []; try { if (globalThis.__diminaElementsOutbound) batch = globalThis.__diminaElementsOutbound.splice(0); } catch(_){}`
150
+ + `return { status: status, batch: batch };`
151
+ + `})()`;
152
+ }
135
153
  // ── main → front-end dispatch (mirrored from network-forward/index.ts) ────────
136
154
  // network-forward keeps these file-private and the task forbids touching it, so
137
155
  // they're re-implemented here. Keep the chunk contract in sync if that changes.
@@ -171,14 +189,10 @@ function buildDocumentUpdatedScript() {
171
189
  const msg = JSON.stringify({ method: 'DOM.documentUpdated', params: {} });
172
190
  return buildDispatchScript(msg);
173
191
  }
192
+ // Reconcile cadence: each tick re-asserts the hook (idempotent) and drains the
193
+ // outbound queue. Fast enough that a freshly reloaded front-end is re-hooked
194
+ // within a tick, cheap because the install script short-circuits to 'already'.
174
195
  const DRAIN_INTERVAL_MS = 150;
175
- /** Bounded retry for the front-end hook install (front-end boots asynchronously). */
176
- // Per-load install poll window. Must outlast the gap between a devtools
177
- // front-end `dom-ready` and `InspectorFrontendHost` becoming available on a
178
- // cold/slow open (pool cold-start). 200×50ms = 10s — generous, and each future
179
- // front-end load re-arms a fresh window anyway (see the `dom-ready` listener).
180
- const INSTALL_POLL_TRIES = 200;
181
- const INSTALL_POLL_INTERVAL_MS = 50;
182
196
  /**
183
197
  * Install Elements forwarding on a DevTools front-end host wc. Returns a disposer
184
198
  * (`stop`) that clears timers, unsubscribes render events, detaches ONLY the
@@ -192,8 +206,16 @@ const INSTALL_POLL_INTERVAL_MS = 50;
192
206
  export function installElementsForward(deps) {
193
207
  const { devtoolsWc, bridge } = deps;
194
208
  let disposed = false;
195
- let installTimer = null;
196
- let drainTimer = null;
209
+ // A single self-healing loop drives BOTH hook (re)install and outbound drain;
210
+ // it never self-terminates so a front-end reload (respawn) is re-hooked.
211
+ let reconcileTimer = null;
212
+ // Teardown that can only be wired AFTER the subscriptions below exist (render
213
+ // events, the dom-ready listener, the connection 'closed' sub). `stop()` runs it
214
+ // so EVERY teardown entry point — internal wc-destroy detection, the connection
215
+ // 'closed'/'destroyed' handler, and the returned disposer — does the SAME full,
216
+ // idempotent cleanup. Without this, a wc-destroy that reaches `stop()` directly
217
+ // would leave the render-event subscription (and dom-ready listener) dangling.
218
+ let lateCleanup = null;
197
219
  // Staleness is keyed on the CURRENT active render guest, resolved fresh from the
198
220
  // bridge per check — NOT a generation snapshot. An event/response is honoured
199
221
  // only while its originating guest is still the active one, so switching away
@@ -212,13 +234,9 @@ export function installElementsForward(deps) {
212
234
  const selfAttached = new Set();
213
235
  const stop = () => {
214
236
  disposed = true;
215
- if (installTimer) {
216
- clearInterval(installTimer);
217
- installTimer = null;
218
- }
219
- if (drainTimer) {
220
- clearInterval(drainTimer);
221
- drainTimer = null;
237
+ if (reconcileTimer) {
238
+ clearInterval(reconcileTimer);
239
+ reconcileTimer = null;
222
240
  }
223
241
  for (const cleanup of wiredGuests.values()) {
224
242
  try {
@@ -228,6 +246,15 @@ export function installElementsForward(deps) {
228
246
  }
229
247
  wiredGuests.clear();
230
248
  detachSelfAttached();
249
+ // Run the late-wired teardown once (render events, dom-ready, 'closed' sub).
250
+ if (lateCleanup) {
251
+ const c = lateCleanup;
252
+ lateCleanup = null;
253
+ try {
254
+ c();
255
+ }
256
+ catch { /* already gone */ }
257
+ }
231
258
  };
232
259
  /** Detach every session we own; leave safe-area's alone. */
233
260
  function detachSelfAttached() {
@@ -538,91 +565,73 @@ export function installElementsForward(deps) {
538
565
  pushDocumentUpdated();
539
566
  };
540
567
  const unsubscribeRenderEvents = bridge.onRenderEvent(onRenderEvent);
541
- // ── startup: install hook (retry until embedder exists), then drain ─────────
568
+ // ── self-healing reconcile loop: (re)install hook + drain, every tick ───────
542
569
  const onReady = () => {
543
570
  if (disposed)
544
571
  return;
545
- // Re-arm safe: a devtools front-end (re)load re-runs this, so clear any
546
- // in-flight timers from a prior attempt before starting fresh ones without
547
- // this a reload would stack intervals (leak) and double-drain.
548
- if (installTimer) {
549
- clearInterval(installTimer);
550
- installTimer = null;
551
- }
552
- if (drainTimer) {
553
- clearInterval(drainTimer);
554
- drainTimer = null;
555
- }
556
- let tries = 0;
557
- installTimer = setInterval(() => {
558
- tries++;
572
+ // Re-arm safe: a devtools front-end (re)load re-runs this, so clear any prior
573
+ // loop before starting a fresh one without this a reload would stack
574
+ // intervals (leak) and double-drain.
575
+ if (reconcileTimer) {
576
+ clearInterval(reconcileTimer);
577
+ reconcileTimer = null;
578
+ }
579
+ // ONE loop, never self-terminating: each tick idempotently (re)installs the
580
+ // hook AND drains the outbound queue in a single round-trip. The earlier
581
+ // design stopped its install poll on first success and re-armed ONLY via
582
+ // `dom-ready` — but a service-host pool swap on a hot-reload respawn re-points
583
+ // the front-end without a reliable `dom-ready`, so the hook (which lives in
584
+ // the front-end `globalThis` and is wiped on every reload) stayed uninstalled
585
+ // and Elements fell back to the natively-inspected service host. Re-asserting
586
+ // the hook every tick makes re-establishment independent of any single event.
587
+ // The tick we freshly (re)wrap a reloaded front-end (`status === 'installed'`)
588
+ // we re-prime the active guest and nudge a re-pull so the panel snaps back to
589
+ // the render guest. We deliberately do NOT force showView('elements') — the
590
+ // panel stays on the user-preferred Console default; the eager DOM pull (and
591
+ // any later Elements click) is already routed to render.
592
+ reconcileTimer = setInterval(() => {
559
593
  if (disposed || devtoolsWc.isDestroyed()) {
560
594
  stop();
561
595
  return;
562
596
  }
563
597
  devtoolsWc
564
- .executeJavaScript(buildElementsHookScript())
565
- .then((status) => {
598
+ .executeJavaScript(buildReconcileScript())
599
+ .then((res) => {
566
600
  if (disposed)
567
601
  return;
568
- if (status === 'installed' || status === 'already') {
569
- if (installTimer) {
570
- clearInterval(installTimer);
571
- installTimer = null;
572
- }
573
- // Prime the current guest, then nudge the front-end to (re-)pull DOM
574
- // so the bootstrap document.getDocument is routed at the render guest.
575
- // We deliberately do NOT force showView('elements') — the panel stays
576
- // on the user-preferred Console default; the eager DOM pull (and any
577
- // later Elements click) is already routed to render.
602
+ const r = (res ?? {});
603
+ if (r.status === 'installed') {
578
604
  const wc = activeRenderWc();
579
605
  if (wc)
580
606
  primeGuest(wc);
581
607
  pushDocumentUpdated();
582
608
  }
609
+ handleOutbound(r.batch);
583
610
  })
584
611
  .catch(() => { });
585
- if (tries > INSTALL_POLL_TRIES && installTimer) {
586
- clearInterval(installTimer);
587
- installTimer = null;
588
- }
589
- }, INSTALL_POLL_INTERVAL_MS);
590
- // Poll-drain the outbound command queue.
591
- drainTimer = setInterval(() => {
592
- if (disposed || devtoolsWc.isDestroyed()) {
593
- stop();
594
- return;
595
- }
596
- devtoolsWc
597
- .executeJavaScript('(globalThis.__diminaElementsOutbound ? __diminaElementsOutbound.splice(0) : [])')
598
- .then(handleOutbound)
599
- .catch(() => { });
600
612
  }, DRAIN_INTERVAL_MS);
601
613
  };
602
- // Re-install on EVERY devtools front-end load, not once. The hook lives in the
603
- // front-end's `globalThis` (`__diminaElementsHookInstalled`), so any front-end
604
- // (re)load wipes it most importantly a service-host pool SWAP, which re-opens
605
- // DevTools (`pointNativeDevtoolsAtServiceWc`) and re-bootstraps the front-end.
606
- // A single-shot `once('dom-ready')` left the hook uninstalled after such a
607
- // reload, so Elements silently fell back to the native service-host DOM. A
608
- // PERSISTENT listener re-runs the (idempotent — the sentinel returns 'already')
609
- // install on each load; the immediate call covers an already-loaded wc and the
610
- // first `openDevTools` whose `dom-ready` may have fired before this ran.
614
+ // `dom-ready` re-arms the loop promptly on a front-end (re)load; the loop itself
615
+ // is the durable self-healer (it re-asserts the hook every tick regardless), so
616
+ // a missed `dom-ready` no longer leaves Elements stuck on the service host.
611
617
  devtoolsWc.on('dom-ready', onReady);
612
618
  onReady();
613
619
  let devtoolsClosedSub;
614
620
  try {
615
621
  // Devtools front-end wc is never pool-reset → use `'closed'` (fires only on
616
- // real destroy). `on('closed')` dispose() removes WITHOUT firing, so the
617
- // returned teardown releases it then calls `stop()` directly — no stale
618
- // `stop` disposers accumulate across re-install on a surviving devtoolsWc.
622
+ // real destroy). `on('closed')` dispose() removes WITHOUT firing, so `stop()`
623
+ // (which now disposes it) can be the sole teardown — no stale disposers
624
+ // accumulate across re-install on a surviving devtoolsWc.
619
625
  if (deps.connections)
620
626
  devtoolsClosedSub = deps.connections.acquire(devtoolsWc).on('closed', stop);
621
627
  else
622
628
  devtoolsWc.once('destroyed', stop);
623
629
  }
624
630
  catch { /* fake/minimal wc */ }
625
- return () => {
631
+ // Now that every subscription exists, route them through `stop()` so a teardown
632
+ // from ANY entry point (returned disposer, 'closed'/'destroyed' handler, or the
633
+ // reconcile tick noticing the wc is gone) does the identical full cleanup.
634
+ lateCleanup = () => {
626
635
  try {
627
636
  unsubscribeRenderEvents();
628
637
  }
@@ -635,7 +644,7 @@ export function installElementsForward(deps) {
635
644
  devtoolsWc.removeListener('dom-ready', onReady);
636
645
  }
637
646
  catch { /* already gone */ }
638
- stop();
639
647
  };
648
+ return stop;
640
649
  }
641
650
  //# sourceMappingURL=index.js.map
@@ -36,9 +36,9 @@ export interface NativeOverview {
36
36
  }
37
37
  export type NativeOverviewProvider = () => Promise<NativeOverview>;
38
38
  interface TargetState {
39
- client: any;
39
+ client: CDP.Client | null;
40
40
  connected: boolean;
41
- timer: any;
41
+ timer: ReturnType<typeof setTimeout> | null;
42
42
  consoleLogs: ConsoleLogEntry[];
43
43
  networkRequests: NetworkRequestEntry[];
44
44
  }
@@ -74,5 +74,5 @@ export declare function getTargetState(kind: TargetKind): TargetState;
74
74
  declare function listCdpTargets(): Promise<CDP.Target[]>;
75
75
  export { listCdpTargets as listTargets };
76
76
  export declare function connectTarget(kind: TargetKind): Promise<void>;
77
- export declare function getClient(kind: TargetKind): any;
77
+ export declare function getClient(kind: TargetKind): CDP.Client;
78
78
  //# sourceMappingURL=target-manager.d.ts.map
@@ -8,12 +8,15 @@
8
8
  * visible to (and clobbered by) project B; two projects open at once cross-
9
9
  * contaminate and "clear storage" on one nukes the other.
10
10
  *
11
- * The fix derives a STABLE per-project partition from the project identity (the
12
- * miniapp `appId`, which flows in via the simulator URL `?appId=` and the spawn
13
- * path's `appId`). Same project same partition (storage survives a relaunch);
14
- * different projects different partitions (no cross-contamination). The shape
15
- * mirrors the IDE state of the art (WeChat's static partition, ByteDance's
16
- * `persist:miniapp-<id>` dynamic partition).
11
+ * The fix derives a STABLE per-project partition from the project identity:
12
+ * the miniapp `appId` (which flows in via the simulator URL `?appId=` and the
13
+ * spawn path's `appId`) PLUS the project path. appId alone is insufficient
14
+ * it comes from the project manifest, so two DIFFERENT project paths can declare
15
+ * the SAME appId and would otherwise share a partition; folding the path in
16
+ * isolates them. Same project (same appId + path) → same partition (storage
17
+ * survives a relaunch); different projects → different partitions (no cross-
18
+ * contamination). The shape mirrors the IDE state of the art (WeChat's static
19
+ * partition, ByteDance's `persist:miniapp-<id>` dynamic partition).
17
20
  *
18
21
  * The protocol handlers (`difile://`, `dmb-resource`) and webRequest policies
19
22
  * (referer / CORS) that the simulator runtime needs were installed once on the
@@ -25,6 +28,13 @@
25
28
  * Partition CLEANUP (reclaiming on-disk `persist:` data) is intentionally NOT
26
29
  * done here — leaving the data on disk is the whole point of a `persist:`
27
30
  * partition (cache/storage survives a relaunch). That is a separate concern.
31
+ *
32
+ * MIGRATION (intentional one-time reset): folding the project path into the key
33
+ * means a project that previously ran on the appId-only `persist:miniapp-<appId>`
34
+ * now runs on `persist:miniapp-<appId>-p<hash>` and starts with empty
35
+ * cookies/localStorage/cache. Old data is NOT migrated — a best-effort copy
36
+ * could carry already-cross-contaminated state into the newly isolated space,
37
+ * which defeats the isolation. The reset is accepted as a one-time cost.
28
38
  */
29
39
  import type { Session } from 'electron';
30
40
  /** The legacy single-session partition. Still used by the pre-warm pool (which
@@ -42,14 +52,14 @@ export declare const SHARED_MINIAPP_PARTITION = "persist:simulator";
42
52
  * same key (so storage survives a relaunch); different `appId`s yield different
43
53
  * keys.
44
54
  */
45
- export declare function miniappPartitionKey(appId: string): string;
55
+ export declare function miniappPartitionKey(appId: string, projectPath?: string | null): string;
46
56
  /**
47
57
  * The Electron session partition for a project. Returns the SHARED partition
48
58
  * when `appId` is empty/unknown (so the runtime still has a session to load on,
49
59
  * matching the legacy behavior) and a `persist:miniapp-<key>` partition
50
60
  * otherwise.
51
61
  */
52
- export declare function miniappPartition(appId: string | null | undefined): string;
62
+ export declare function miniappPartition(appId: string | null | undefined, projectPath?: string | null): string;
53
63
  type SessionConfigurator = (sess: Session, partition: string) => void;
54
64
  /**
55
65
  * Register a configurator that runs against every miniapp partition session.
@@ -8,12 +8,15 @@
8
8
  * visible to (and clobbered by) project B; two projects open at once cross-
9
9
  * contaminate and "clear storage" on one nukes the other.
10
10
  *
11
- * The fix derives a STABLE per-project partition from the project identity (the
12
- * miniapp `appId`, which flows in via the simulator URL `?appId=` and the spawn
13
- * path's `appId`). Same project same partition (storage survives a relaunch);
14
- * different projects different partitions (no cross-contamination). The shape
15
- * mirrors the IDE state of the art (WeChat's static partition, ByteDance's
16
- * `persist:miniapp-<id>` dynamic partition).
11
+ * The fix derives a STABLE per-project partition from the project identity:
12
+ * the miniapp `appId` (which flows in via the simulator URL `?appId=` and the
13
+ * spawn path's `appId`) PLUS the project path. appId alone is insufficient
14
+ * it comes from the project manifest, so two DIFFERENT project paths can declare
15
+ * the SAME appId and would otherwise share a partition; folding the path in
16
+ * isolates them. Same project (same appId + path) → same partition (storage
17
+ * survives a relaunch); different projects → different partitions (no cross-
18
+ * contamination). The shape mirrors the IDE state of the art (WeChat's static
19
+ * partition, ByteDance's `persist:miniapp-<id>` dynamic partition).
17
20
  *
18
21
  * The protocol handlers (`difile://`, `dmb-resource`) and webRequest policies
19
22
  * (referer / CORS) that the simulator runtime needs were installed once on the
@@ -25,6 +28,13 @@
25
28
  * Partition CLEANUP (reclaiming on-disk `persist:` data) is intentionally NOT
26
29
  * done here — leaving the data on disk is the whole point of a `persist:`
27
30
  * partition (cache/storage survives a relaunch). That is a separate concern.
31
+ *
32
+ * MIGRATION (intentional one-time reset): folding the project path into the key
33
+ * means a project that previously ran on the appId-only `persist:miniapp-<appId>`
34
+ * now runs on `persist:miniapp-<appId>-p<hash>` and starts with empty
35
+ * cookies/localStorage/cache. Old data is NOT migrated — a best-effort copy
36
+ * could carry already-cross-contaminated state into the newly isolated space,
37
+ * which defeats the isolation. The reset is accepted as a one-time cost.
28
38
  */
29
39
  import * as electron from 'electron';
30
40
  /** The legacy single-session partition. Still used by the pre-warm pool (which
@@ -43,15 +53,26 @@ const PARTITION_PREFIX = 'persist:miniapp-';
43
53
  * same key (so storage survives a relaunch); different `appId`s yield different
44
54
  * keys.
45
55
  */
46
- export function miniappPartitionKey(appId) {
56
+ export function miniappPartitionKey(appId, projectPath) {
47
57
  const safe = appId.replace(/[^A-Za-z0-9_-]/g, '');
48
58
  // Folding may have dropped distinguishing characters; if the input was not
49
59
  // already fully safe, append a deterministic hash of the RAW appId so two
50
60
  // distinct appIds can never alias onto the same key.
51
- if (safe === appId && safe.length > 0)
52
- return safe;
53
- const hash = djb2(appId).toString(36);
54
- return safe.length > 0 ? `${safe}-${hash}` : hash;
61
+ const base = safe === appId && safe.length > 0
62
+ ? safe
63
+ : safe.length > 0
64
+ ? `${safe}-${djb2(appId).toString(36)}`
65
+ : djb2(appId).toString(36);
66
+ // `appId` comes from the project manifest, not the path — two DIFFERENT
67
+ // project paths can declare the SAME appId. Fold the project path into the key
68
+ // so those projects get isolated partitions (no storage/cache cross-talk),
69
+ // while the same path always yields the same suffix (a relaunch reuses its
70
+ // storage). Path characters aren't partition-safe, so always hash. Omitting
71
+ // projectPath preserves the legacy appId-only key (pre-warm pool / callers
72
+ // that have no project identity).
73
+ if (!projectPath)
74
+ return base;
75
+ return `${base}-p${djb2(projectPath).toString(36)}`;
55
76
  }
56
77
  /**
57
78
  * The Electron session partition for a project. Returns the SHARED partition
@@ -59,10 +80,10 @@ export function miniappPartitionKey(appId) {
59
80
  * matching the legacy behavior) and a `persist:miniapp-<key>` partition
60
81
  * otherwise.
61
82
  */
62
- export function miniappPartition(appId) {
83
+ export function miniappPartition(appId, projectPath) {
63
84
  if (!appId)
64
85
  return SHARED_MINIAPP_PARTITION;
65
- return `${PARTITION_PREFIX}${miniappPartitionKey(appId)}`;
86
+ return `${PARTITION_PREFIX}${miniappPartitionKey(appId, projectPath)}`;
66
87
  }
67
88
  /** Small, stable string hash (djb2). Not cryptographic — only needs to be
68
89
  * deterministic and collision-resistant enough to disambiguate appIds. */
@@ -877,13 +877,22 @@ export function createViewManager(ctx) {
877
877
  stopElementsForward?.();
878
878
  }
879
879
  catch { /* prior disposer already gone */ }
880
- stopElementsForward = installElementsForward({ devtoolsWc, bridge: ctx.bridge, connections: ctx.connections });
880
+ // Capture THIS instance's disposer in the destroy handler — NOT the mutable
881
+ // `stopElementsForward`. A respawn creates a new simulatorView whose old
882
+ // devtools host wc is destroyed AFTER the new instance is already installed;
883
+ // a handler closing over `stopElementsForward` would then dispose the CURRENT
884
+ // instance (its reconcile loop never gets to install the hook, so Elements
885
+ // falls back to the natively-inspected service host). Stop only this wc's own
886
+ // forward, and clear the module pointer only while it still points here.
887
+ const thisForward = installElementsForward({ devtoolsWc, bridge: ctx.bridge, connections: ctx.connections });
888
+ stopElementsForward = thisForward;
881
889
  devtoolsWc.once('destroyed', () => {
882
890
  try {
883
- stopElementsForward?.();
891
+ thisForward();
884
892
  }
885
893
  catch { /* already stopped */ }
886
- stopElementsForward = null;
894
+ if (stopElementsForward === thisForward)
895
+ stopElementsForward = null;
887
896
  });
888
897
  }
889
898
  devtoolsWc.once('dom-ready', () => {
@@ -934,6 +943,40 @@ export function createViewManager(ctx) {
934
943
  nativeCustomApiBridgeHandler = null;
935
944
  }
936
945
  }
946
+ /**
947
+ * The single teardown path for the live native-host simulator WCV, shared by
948
+ * relaunch (attachNativeSimulator replacing the view) and project close
949
+ * (detachSimulator). Detaches the view from the window, then synchronously
950
+ * clears its bridge sessions (render guests + service host + mappings) BEFORE
951
+ * the WCV's own async close(), so the next project never re-resolves,
952
+ * re-renders, or screenshots the outgoing guest. The sync prefix clears the
953
+ * maps immediately; the async tail (pool / resource-server release) is observed
954
+ * so a rejection is logged rather than swallowed. The simulatorWc 'destroyed'
955
+ * hook in bridge-router stays as an idempotent fallback. No-op when no view is
956
+ * live; idempotent (disposeSessionsForSimulator early-returns once a session is
957
+ * gone), so the eager teardown here and the 'destroyed' fallback never
958
+ * double-dispose. `label` only tags the diagnostic on the async-tail failure.
959
+ */
960
+ function tearDownNativeSimulatorView(label) {
961
+ if (!nativeSimulatorView)
962
+ return;
963
+ if (nativeSimulatorViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
964
+ try {
965
+ ctx.windows.mainWindow.contentView.removeChildView(nativeSimulatorView);
966
+ }
967
+ catch { /* already removed */ }
968
+ }
969
+ try {
970
+ if (!nativeSimulatorView.webContents.isDestroyed()) {
971
+ ctx.bridge?.disposeSessionsForSimulator?.(nativeSimulatorView.webContents.id)
972
+ ?.catch((err) => console.warn(`[view-manager] dispose sessions (${label}) failed:`, err));
973
+ nativeSimulatorView.webContents.close();
974
+ }
975
+ }
976
+ catch { /* ignore */ }
977
+ nativeSimulatorView = null;
978
+ nativeSimulatorViewAdded = false;
979
+ }
937
980
  /**
938
981
  * NATIVE-HOST ONLY. The native simulator is a top-level WebContentsView with
939
982
  * NO embedder renderer, so the simulator-side `__diminaCustomApis` bridge
@@ -996,23 +1039,12 @@ export function createViewManager(ctx) {
996
1039
  // that generation cancelled, so this cannot schedule a stale capture.
997
1040
  settleNativeSimulatorReady?.();
998
1041
  settleNativeSimulatorReady = null;
999
- // Tear down any previous native simulator view (relaunch / re-open).
1042
+ // Tear down any previous native simulator view (relaunch / re-open) through
1043
+ // the shared single teardown path. detachNativeCustomApiBridge stays here
1044
+ // (the bridge dispatcher is re-installed per attach below).
1000
1045
  if (nativeSimulatorView) {
1001
1046
  detachNativeCustomApiBridge();
1002
- if (nativeSimulatorViewAdded) {
1003
- try {
1004
- ctx.windows.mainWindow.contentView.removeChildView(nativeSimulatorView);
1005
- }
1006
- catch { /* already removed */ }
1007
- nativeSimulatorViewAdded = false;
1008
- }
1009
- try {
1010
- if (!nativeSimulatorView.webContents.isDestroyed()) {
1011
- nativeSimulatorView.webContents.close();
1012
- }
1013
- }
1014
- catch { /* ignore */ }
1015
- nativeSimulatorView = null;
1047
+ tearDownNativeSimulatorView('relaunch');
1016
1048
  }
1017
1049
  nativeSimulatorProjectPath = null;
1018
1050
  const ready = new Promise((resolve) => {
@@ -1025,7 +1057,11 @@ export function createViewManager(ctx) {
1025
1057
  // (protocol handlers + CORS/referer policy) before any project content loads
1026
1058
  // on it — idempotent per partition.
1027
1059
  const route = parseRoute(simulatorUrl);
1028
- const partition = miniappPartition(route?.appId);
1060
+ // Include the project path so two projects that declare the same appId at
1061
+ // different paths get isolated partitions. The service-host window for THIS
1062
+ // project derives its partition from the same (appId, projectPath) pair so
1063
+ // render guests + service host still share one session.
1064
+ const partition = miniappPartition(route?.appId, ctx.workspace?.getProjectPath());
1029
1065
  configureMiniappSession(partition);
1030
1066
  // The simulator preload is a CJS bundle; webPreferences.preload obeys the
1031
1067
  // `.js` + "type":"module" ESM rule (require would be undefined), so hand the
@@ -1221,23 +1257,10 @@ export function createViewManager(ctx) {
1221
1257
  // back to the console.
1222
1258
  ctx.networkForward?.detachSimulator();
1223
1259
  ctx.networkForward?.setDevtoolsHost(null);
1224
- // Native-host simulator content view (no-op in the default path).
1225
- if (nativeSimulatorView) {
1226
- if (nativeSimulatorViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
1227
- try {
1228
- ctx.windows.mainWindow.contentView.removeChildView(nativeSimulatorView);
1229
- }
1230
- catch { /* already removed */ }
1231
- }
1232
- try {
1233
- if (!nativeSimulatorView.webContents.isDestroyed()) {
1234
- nativeSimulatorView.webContents.close();
1235
- }
1236
- }
1237
- catch { /* ignore */ }
1238
- nativeSimulatorView = null;
1239
- nativeSimulatorViewAdded = false;
1240
- }
1260
+ // Native-host simulator content view (no-op in the default path) — same
1261
+ // single teardown path as relaunch. detachNativeCustomApiBridge was already
1262
+ // called above.
1263
+ tearDownNativeSimulatorView('close');
1241
1264
  hidePopover();
1242
1265
  // Drop the settings view too — the previous detachAllViews() did.
1243
1266
  destroyViewInternal(settingsView);
@@ -14,6 +14,13 @@ export interface WorkbenchCoiServerOptions {
14
14
  * manifest the workbench reads at boot. Omit for none.
15
15
  */
16
16
  extensionsDir?: string;
17
+ /**
18
+ * Reads the host's custom file types (e.g. `.qdml`/`.qdss`/`.qds`), served at
19
+ * `/__filetypes` so the workbench maps them to `files.associations`. Read
20
+ * per-request so a project switch takes effect on the next editor boot. Omit
21
+ * for none (built-in `wx*` associations only).
22
+ */
23
+ getFileTypes?: () => unknown;
17
24
  }
18
25
  export declare function startWorkbenchCoiServer(options: WorkbenchCoiServerOptions): Promise<WorkbenchCoiServer>;
19
26
  //# sourceMappingURL=workbench-coi-server.d.ts.map