@dimina-kit/devtools 0.4.0-dev.20260828161241 → 0.4.0-dev.20260907055341

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 (277) hide show
  1. package/README.md +52 -729
  2. package/dist/main/app/app.d.ts +27 -7
  3. package/dist/main/app/app.js +285 -681
  4. package/dist/main/app/editor-view.d.ts +21 -0
  5. package/dist/main/app/editor-view.js +71 -0
  6. package/dist/main/app/global-mirrors.d.ts +23 -0
  7. package/dist/main/app/global-mirrors.js +37 -0
  8. package/dist/main/app/host-sidebar-default.d.ts +19 -0
  9. package/dist/main/app/host-sidebar-default.js +44 -0
  10. package/dist/main/app/menu-setup.d.ts +13 -0
  11. package/dist/main/app/menu-setup.js +49 -0
  12. package/dist/main/app/open-project-ipc.d.ts +13 -0
  13. package/dist/main/app/open-project-ipc.js +21 -0
  14. package/dist/main/app/project-list-surface.d.ts +31 -0
  15. package/dist/main/app/project-list-surface.js +20 -0
  16. package/dist/main/app/project-window.d.ts +36 -0
  17. package/dist/main/app/project-window.js +93 -0
  18. package/dist/main/app/servers.d.ts +17 -0
  19. package/dist/main/app/servers.js +60 -0
  20. package/dist/main/app/ui-extension-targets.d.ts +42 -0
  21. package/dist/main/app/ui-extension-targets.js +101 -0
  22. package/dist/main/app/window-close-reveal.harness.d.ts +34 -0
  23. package/dist/main/app/window-close-reveal.harness.js +65 -0
  24. package/dist/main/app/window-events.d.ts +15 -0
  25. package/dist/main/app/window-events.js +45 -0
  26. package/dist/main/app/window-runtime-services.d.ts +41 -0
  27. package/dist/main/app/window-runtime-services.js +178 -0
  28. package/dist/main/app/workbench-modules.d.ts +24 -0
  29. package/dist/main/app/workbench-modules.js +51 -0
  30. package/dist/main/app/workbench-window.d.ts +85 -0
  31. package/dist/main/app/workbench-window.js +310 -0
  32. package/dist/main/index.bundle.js +10669 -9096
  33. package/dist/main/ipc/app.d.ts +4 -1
  34. package/dist/main/ipc/app.js +4 -3
  35. package/dist/main/ipc/device-picker.d.ts +16 -0
  36. package/dist/main/ipc/device-picker.js +27 -0
  37. package/dist/main/ipc/index.d.ts +1 -0
  38. package/dist/main/ipc/index.js +1 -0
  39. package/dist/main/ipc/internal-devtools.d.ts +2 -1
  40. package/dist/main/ipc/internal-devtools.js +4 -3
  41. package/dist/main/ipc/popover.d.ts +4 -1
  42. package/dist/main/ipc/popover.js +25 -9
  43. package/dist/main/ipc/project-create.d.ts +2 -1
  44. package/dist/main/ipc/project-create.js +6 -5
  45. package/dist/main/ipc/project-fs.d.ts +3 -1
  46. package/dist/main/ipc/project-fs.js +17 -17
  47. package/dist/main/ipc/projects.d.ts +3 -3
  48. package/dist/main/ipc/projects.js +13 -12
  49. package/dist/main/ipc/session.d.ts +4 -1
  50. package/dist/main/ipc/session.js +16 -11
  51. package/dist/main/ipc/settings.d.ts +4 -1
  52. package/dist/main/ipc/settings.js +12 -19
  53. package/dist/main/ipc/simulator-module.d.ts +5 -0
  54. package/dist/main/ipc/simulator-module.js +9 -5
  55. package/dist/main/ipc/simulator.d.ts +4 -1
  56. package/dist/main/ipc/simulator.js +12 -18
  57. package/dist/main/ipc/tooltip.d.ts +2 -1
  58. package/dist/main/ipc/tooltip.js +8 -7
  59. package/dist/main/ipc/views.d.ts +4 -1
  60. package/dist/main/ipc/views.js +20 -9
  61. package/dist/main/runtime/devtools-backend-before-quit.harness.d.ts +2 -20
  62. package/dist/main/runtime/devtools-backend-before-quit.harness.js +14 -283
  63. package/dist/main/runtime/devtools-backend.js +14 -3
  64. package/dist/main/runtime/devtools-runtime-mock.harness.d.ts +21 -0
  65. package/dist/main/runtime/devtools-runtime-mock.harness.js +313 -0
  66. package/dist/main/services/app-services.d.ts +62 -0
  67. package/dist/main/services/app-services.js +64 -0
  68. package/dist/main/services/automation/connection-target.d.ts +36 -0
  69. package/dist/main/services/automation/connection-target.js +29 -0
  70. package/dist/main/services/automation/console-bridge.d.ts +28 -0
  71. package/dist/main/services/automation/console-bridge.js +91 -0
  72. package/dist/main/services/automation/handlers/app.js +4 -4
  73. package/dist/main/services/automation/handlers/element.js +55 -21
  74. package/dist/main/services/automation/index.d.ts +12 -1
  75. package/dist/main/services/automation/index.js +54 -133
  76. package/dist/main/services/default-adapter.js +1 -1
  77. package/dist/main/services/fs-bridge-protocol.d.ts +61 -0
  78. package/dist/main/services/fs-bridge-protocol.js +34 -0
  79. package/dist/main/services/fs-watch-sse.d.ts +8 -1
  80. package/dist/main/services/fs-watch-sse.js +19 -5
  81. package/dist/main/services/mcp/connection-owner.d.ts +38 -0
  82. package/dist/main/services/mcp/connection-owner.js +79 -0
  83. package/dist/main/services/mcp/index.d.ts +3 -1
  84. package/dist/main/services/mcp/index.js +4 -3
  85. package/dist/main/services/mcp/opened-project.d.ts +88 -0
  86. package/dist/main/services/mcp/opened-project.js +60 -0
  87. package/dist/main/services/mcp/target-manager.d.ts +71 -23
  88. package/dist/main/services/mcp/target-manager.js +269 -142
  89. package/dist/main/services/mcp/target-selection.d.ts +47 -0
  90. package/dist/main/services/mcp/target-selection.js +71 -0
  91. package/dist/main/services/mcp/tools/context-tools.js +20 -5
  92. package/dist/main/services/mcp/tools/project-tools.d.ts +29 -6
  93. package/dist/main/services/mcp/tools/project-tools.js +37 -25
  94. package/dist/main/services/module.d.ts +13 -5
  95. package/dist/main/services/notifications/renderer-notifier.d.ts +28 -4
  96. package/dist/main/services/notifications/renderer-notifier.js +15 -3
  97. package/dist/main/services/projects/local-provider.js +2 -0
  98. package/dist/main/services/projects/project-repository.d.ts +31 -1
  99. package/dist/main/services/projects/project-repository.js +144 -20
  100. package/dist/main/services/projects/types.d.ts +31 -8
  101. package/dist/main/services/safe-area/index.d.ts +19 -5
  102. package/dist/main/services/safe-area/index.js +38 -13
  103. package/dist/main/services/simulator/ui-extensions.d.ts +8 -0
  104. package/dist/main/services/simulator/ui-extensions.js +16 -6
  105. package/dist/main/services/simulator-storage/index.d.ts +12 -1
  106. package/dist/main/services/simulator-storage/index.js +22 -7
  107. package/dist/main/services/simulator-storage/window-scope.d.ts +20 -0
  108. package/dist/main/services/simulator-storage/window-scope.js +20 -0
  109. package/dist/main/services/views/host-slot-port-channel.d.ts +20 -3
  110. package/dist/main/services/views/host-slot-port-channel.js +33 -5
  111. package/dist/main/services/views/managed-web-contents-view.js +21 -1
  112. package/dist/main/services/views/native-simulator-view.js +13 -16
  113. package/dist/main/services/views/overlay-panels-view.d.ts +13 -0
  114. package/dist/main/services/views/overlay-panels-view.js +31 -0
  115. package/dist/main/services/views/view-manager.d.ts +1 -1
  116. package/dist/main/services/views/view-manager.js +3 -0
  117. package/dist/main/services/window-contexts/context-router.d.ts +36 -0
  118. package/dist/main/services/window-contexts/context-router.js +55 -0
  119. package/dist/main/services/workbench-coi-host.d.ts +52 -0
  120. package/dist/main/services/workbench-coi-host.js +227 -0
  121. package/dist/main/services/workbench-coi-server.js +75 -133
  122. package/dist/main/services/workbench-coi-shutdown.d.ts +40 -0
  123. package/dist/main/services/workbench-coi-shutdown.js +69 -0
  124. package/dist/main/services/workbench-coi-static.d.ts +23 -0
  125. package/dist/main/services/workbench-coi-static.js +87 -0
  126. package/dist/main/services/workbench-context.d.ts +27 -3
  127. package/dist/main/services/workbench-context.js +10 -5
  128. package/dist/main/services/workspace/compile-mode-adapter.d.ts +18 -0
  129. package/dist/main/services/workspace/compile-mode-adapter.js +57 -0
  130. package/dist/main/services/workspace/compile-mode-store.d.ts +25 -0
  131. package/dist/main/services/workspace/compile-mode-store.js +71 -0
  132. package/dist/main/services/workspace/workspace-compile-modes.d.ts +59 -0
  133. package/dist/main/services/workspace/workspace-compile-modes.js +93 -0
  134. package/dist/main/services/workspace/workspace-referer.d.ts +17 -0
  135. package/dist/main/services/workspace/workspace-referer.js +26 -0
  136. package/dist/main/services/workspace/workspace-service.d.ts +4 -0
  137. package/dist/main/services/workspace/workspace-service.js +51 -84
  138. package/dist/main/services/workspace/workspace-thumbnails.d.ts +22 -0
  139. package/dist/main/services/workspace/workspace-thumbnails.js +67 -0
  140. package/dist/main/utils/ipc-context-source.d.ts +40 -0
  141. package/dist/main/utils/ipc-context-source.js +23 -0
  142. package/dist/main/utils/ipc-registry.d.ts +42 -36
  143. package/dist/main/utils/ipc-registry.js +117 -50
  144. package/dist/main/utils/sender-policy.d.ts +9 -0
  145. package/dist/main/utils/sender-policy.js +26 -3
  146. package/dist/main/utils/theme.d.ts +3 -2
  147. package/dist/main/utils/theme.js +3 -2
  148. package/dist/main/windows/main-window/create.d.ts +16 -0
  149. package/dist/main/windows/main-window/create.js +19 -11
  150. package/dist/main/windows/main-window/events.d.ts +1 -1
  151. package/dist/main/windows/main-window/events.js +26 -13
  152. package/dist/main/windows/main-window/index.d.ts +1 -1
  153. package/dist/main/windows/main-window/index.js +1 -1
  154. package/dist/native-host/common/common.js +94 -73
  155. package/dist/native-host/render/render.js +4079 -3171
  156. package/dist/native-host/service/service.js +2 -2
  157. package/dist/preload/runtime/native-host.d.ts +4 -0
  158. package/dist/preload/runtime/temp-files.js +9 -2
  159. package/dist/preload/runtime/wait-for-slot-root.d.ts +7 -4
  160. package/dist/preload/runtime/wait-for-slot-root.js +36 -8
  161. package/dist/preload/windows/host-dialog-runtime.cjs +10 -3
  162. package/dist/preload/windows/host-dialog-runtime.cjs.map +2 -2
  163. package/dist/preload/windows/host-sidebar-runtime.cjs +10 -3
  164. package/dist/preload/windows/host-sidebar-runtime.cjs.map +2 -2
  165. package/dist/preload/windows/host-toolbar-runtime.cjs +10 -3
  166. package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
  167. package/dist/preload/windows/main.cjs +181 -4
  168. package/dist/preload/windows/main.cjs.map +4 -4
  169. package/dist/preload/windows/simulator.cjs +10 -0
  170. package/dist/preload/windows/simulator.cjs.map +2 -2
  171. package/dist/preload/windows/simulator.js +10 -0
  172. package/dist/render-host/pageFrame.html +1 -1
  173. package/dist/render-host/preload.cjs +16 -4
  174. package/dist/renderer/assets/button-CB2MWjHY.js +2 -0
  175. package/dist/renderer/assets/constants-CkoGPQTS.js +2 -0
  176. package/dist/renderer/assets/constants-zQZ2-y21.js +2 -0
  177. package/dist/renderer/assets/{createLucideIcon-Bqi8hpdi.js → createLucideIcon-cZ9FlZZw.js} +2 -2
  178. package/dist/renderer/assets/devicePicker-H2eIO4e1.js +2 -0
  179. package/dist/renderer/assets/dialog-pHzEA8Vx.js +46 -0
  180. package/dist/renderer/assets/dist-B5HADirS.js +2 -0
  181. package/dist/renderer/assets/{dist-DKWdE1TK.js → dist-DIFlRMKe.js} +2 -2
  182. package/dist/renderer/assets/hostSidebarDefault-DRWh6OCt.js +2 -0
  183. package/dist/renderer/assets/index-CEL_XFge.js +2 -0
  184. package/dist/renderer/assets/{input-Ct3alvkq.js → input-CAObw95L.js} +2 -2
  185. package/dist/renderer/assets/ipc-channels-overlays-gIPUFnqc.js +2 -0
  186. package/dist/renderer/assets/jsx-runtime-DICEQH_0.css +1 -0
  187. package/dist/renderer/assets/{jsx-runtime-DGLHmmOq.js → jsx-runtime-DIw5wnYl.js} +1 -1
  188. package/dist/renderer/assets/plus-BQ9u5EbL.js +2 -0
  189. package/dist/renderer/assets/popover-Cw7G2Ctp.js +2 -0
  190. package/dist/renderer/assets/presets-DMZOb9ay.js +2 -0
  191. package/dist/renderer/assets/project-api-YTOr_Yvo.js +2 -0
  192. package/dist/renderer/assets/{projectCreateDialog-Bbjz75bP.js → projectCreateDialog-BmLqiYxn.js} +2 -2
  193. package/dist/renderer/assets/search-BMvuYF48.js +2 -0
  194. package/dist/renderer/assets/select-nFP95TxM.js +2 -0
  195. package/dist/renderer/assets/settings-CnpVfSWt.js +2 -0
  196. package/dist/renderer/assets/settings-api-Ccsf-Nsj.js +2 -0
  197. package/dist/renderer/assets/settings-tab-bar-CAzrv32m.js +2 -0
  198. package/dist/renderer/assets/smartphone-Bb2GdIKy.js +2 -0
  199. package/dist/renderer/assets/tabs-CPKN3Mmq.js +2 -0
  200. package/dist/renderer/assets/{tooltip-B_-_YVbw.js → tooltip-D6AxXRk5.js} +2 -2
  201. package/dist/renderer/assets/types-Bh0hA7El.js +2 -0
  202. package/dist/renderer/assets/updateDialog-CwndJ0Bl.js +2 -0
  203. package/dist/renderer/assets/{utils-kfQov6Pa.js → utils-sEB5OZEQ.js} +2 -2
  204. package/dist/renderer/assets/view-api-B-2zPBR5.js +2 -0
  205. package/dist/renderer/assets/view-ids-BfaDVV5P.js +2 -0
  206. package/dist/renderer/assets/workbench-BLJ0wKsg.js +5 -0
  207. package/dist/renderer/assets/{workbenchSettings-CV4LVA9b.js → workbenchSettings-YD0Ctbm_.js} +3 -3
  208. package/dist/renderer/entries/device-picker/index.html +28 -0
  209. package/dist/renderer/entries/host-sidebar-default/index.html +6 -5
  210. package/dist/renderer/entries/main/index.html +16 -17
  211. package/dist/renderer/entries/popover/index.html +16 -10
  212. package/dist/renderer/entries/project-create-dialog/index.html +12 -12
  213. package/dist/renderer/entries/settings/index.html +10 -10
  214. package/dist/renderer/entries/tooltip/index.html +5 -5
  215. package/dist/renderer/entries/update-dialog/index.html +10 -10
  216. package/dist/renderer/entries/workbench/index.html +49 -0
  217. package/dist/renderer/entries/workbench-settings/index.html +12 -11
  218. package/dist/service-host/host-env-update.cjs +23 -0
  219. package/dist/service-host/preload.cjs +24 -49
  220. package/dist/service-host/service-message-queue.cjs +71 -0
  221. package/dist/service-host/sync-impls/system-info.js +31 -9
  222. package/dist/shared/compile-mode-state.d.ts +73 -0
  223. package/dist/shared/compile-mode-state.js +100 -0
  224. package/dist/shared/compile-modes.d.ts +152 -0
  225. package/dist/shared/compile-modes.js +237 -0
  226. package/dist/shared/ipc-channels-overlays.d.ts +8 -2
  227. package/dist/shared/ipc-channels-overlays.js +19 -2
  228. package/dist/shared/ipc-channels.d.ts +5 -10
  229. package/dist/shared/ipc-channels.js +17 -14
  230. package/dist/shared/ipc-schemas.d.ts +76 -24
  231. package/dist/shared/ipc-schemas.js +55 -10
  232. package/dist/shared/project-window-types.d.ts +42 -0
  233. package/dist/shared/project-window-types.js +2 -0
  234. package/dist/shared/types.d.ts +43 -17
  235. package/dist/shared/view-ids.d.ts +1 -0
  236. package/dist/shared/view-ids.js +3 -1
  237. package/dist/simulator/assets/bridge-channels-B3JAmRfl.js +2 -0
  238. package/dist/simulator/assets/device-shell-ATEhHV2_.css +1 -0
  239. package/dist/simulator/assets/device-shell-pxnZZp10.js +431 -0
  240. package/dist/simulator/assets/simulator-Dwp7lnvu.js +10 -0
  241. package/dist/simulator/assets/simulator-mini-app-DFo7fhi5.js +2 -0
  242. package/dist/simulator/assets/simulator-ui-B2ddJB1R.js +2 -0
  243. package/dist/simulator/simulator.html +3 -3
  244. package/dist/simulator/temp-files.d.ts +6 -0
  245. package/dist/simulator/temp-files.js +46 -4
  246. package/dist/simulator/types.d.ts +7 -0
  247. package/dist/vscode-workbench/assets/__vite-browser-external-JIkWewFf.js +1 -0
  248. package/dist/vscode-workbench/assets/{dist-CMfmHflW.js → dist-CXG-u1VB.js} +3 -3
  249. package/dist/vscode-workbench/assets/{iconv-lite-umd-Bx8CgaZq.js → iconv-lite-umd-Dxx9Qyb4.js} +1 -1
  250. package/dist/vscode-workbench/assets/{index-CdG6YWhW.js → index-6VJMsaWj.js} +460 -460
  251. package/dist/vscode-workbench/assets/{jschardet-M0qgd2lN.js → jschardet-Ba3D16vt.js} +1 -1
  252. package/dist/vscode-workbench/index.html +1 -1
  253. package/package.json +11 -8
  254. package/dist/renderer/assets/button-D5Q2wkXq.js +0 -2
  255. package/dist/renderer/assets/constants-D7DfVqMp.js +0 -2
  256. package/dist/renderer/assets/constants-D8zsejFq.js +0 -2
  257. package/dist/renderer/assets/dialog-e1TecTOY.js +0 -46
  258. package/dist/renderer/assets/hostSidebarDefault-B3yLfy8j.js +0 -2
  259. package/dist/renderer/assets/index-CsPD68N_.js +0 -5
  260. package/dist/renderer/assets/ipc-channels-overlays-LsDzGAtV.js +0 -2
  261. package/dist/renderer/assets/jsx-runtime-2vXHouwr.css +0 -1
  262. package/dist/renderer/assets/popover-Dix5cm_b.js +0 -2
  263. package/dist/renderer/assets/project-api-k7Oq9iaq.js +0 -2
  264. package/dist/renderer/assets/select-acGb0AmJ.js +0 -2
  265. package/dist/renderer/assets/settings-CQrTMr43.js +0 -2
  266. package/dist/renderer/assets/settings-api-CrUQ9rYV.js +0 -2
  267. package/dist/renderer/assets/settings-tab-bar-Cw1kVIf2.js +0 -2
  268. package/dist/renderer/assets/types-CsEKtFyr.js +0 -2
  269. package/dist/renderer/assets/updateDialog-NsF8223A.js +0 -2
  270. package/dist/renderer/assets/view-api-CLn3rcQE.js +0 -2
  271. package/dist/simulator/assets/bridge-channels-zhsumfky.js +0 -2
  272. package/dist/simulator/assets/device-shell-CmtM1QxP.js +0 -2
  273. package/dist/simulator/assets/device-shell-iFZo1igL.css +0 -1
  274. package/dist/simulator/assets/simulator-CIobn3Mh.js +0 -10
  275. package/dist/simulator/assets/simulator-mini-app-CRe6n9br.js +0 -2
  276. package/dist/simulator/assets/simulator-ui-C_cP602X.js +0 -2
  277. package/dist/vscode-workbench/assets/__vite-browser-external-Bc1vizTf.js +0 -1
@@ -11,82 +11,108 @@
11
11
  */
12
12
  import CDP from 'chrome-remote-interface';
13
13
  import { DEFAULT_CDP_PORT } from '../../../shared/constants.js';
14
- import { DMB_PAGEFRAME_DOC_NAME } from '../../../shared/dmb-resource-url.js';
15
- const SIMULATOR_URL_PATTERN = 'localhost:7788';
16
- // Native-host: the real mini-app page runs in a nested render-host <webview>
17
- // guest whose CDP target URL carries the render frame + the page's bridgeId.
18
- // Import the shared reserved doc name rather than hand-writing a second
19
- // literal that duplication is exactly what let this pattern drift out of
20
- // sync with the actual document URL shape (dmb-resource-url.ts).
21
- const RENDER_GUEST_PATTERN = DMB_PAGEFRAME_DOC_NAME;
14
+ import { connectionOwner } from './connection-owner.js';
15
+ import { selectSimulatorTarget, selectWorkbenchTarget } from './target-selection.js';
16
+ // Re-exported so existing `./target-manager.js` imports keep working the
17
+ // candidate-ranking rules live in target-selection.ts, but callers reach them
18
+ // through the connection manager that uses them.
19
+ export { selectSimulatorTarget, selectWorkbenchTarget } from './target-selection.js';
22
20
  const MAX_BUFFER = 500;
23
21
  const RECONNECT_INTERVAL_MS = 3000;
22
+ // How long a burst of focus changes is allowed to settle before the live
23
+ // connections are re-aimed.
24
+ const ACTIVE_WINDOW_SETTLE_MS = 150;
24
25
  let cdpPort = DEFAULT_CDP_PORT;
25
- // Native-host mode: the simulator target is the active render-host <webview>
26
- // guest (pageFrame.html?...bridgeId=<id>) rather than the localhost:7788 shell.
27
- let nativeHostMode = false;
28
- // The visible page's bridgeId, pushed from the bridge-router render events.
29
- // `selectSimulatorTarget` prefers the guest matching this id so MCP follows
30
- // the active page across navigation/tab switches.
31
- let activeBridgeId = null;
32
- let nativeOverviewProvider = null;
33
- const targets = {
34
- simulator: { client: null, connected: false, timer: null, consoleLogs: [], networkRequests: [] },
35
- workbench: { client: null, connected: false, timer: null, consoleLogs: [], networkRequests: [] },
36
- };
37
- export function setCdpPort(port) {
38
- cdpPort = port;
26
+ const windowFacts = new Map();
27
+ let resolveActiveOwner = () => null;
28
+ /**
29
+ * Point MCP at the window the user is working in. Assembled once by the app;
30
+ * `owner` is the opaque token a window registered itself under. Resolving to
31
+ * null means no project window is open, which is the answer MCP reports before
32
+ * the first project is opened.
33
+ */
34
+ export function setActiveMcpWindowResolver(resolve) {
35
+ resolveActiveOwner = resolve;
39
36
  }
40
- export function setNativeHost(enabled) {
41
- nativeHostMode = enabled;
37
+ export function registerMcpWindow(owner, facts) {
38
+ windowFacts.set(owner, facts);
39
+ return {
40
+ facts,
41
+ dispose: () => {
42
+ // Only this window's record: other project windows may still be open and
43
+ // their native-host state has to survive this teardown.
44
+ if (windowFacts.get(owner) === facts)
45
+ windowFacts.delete(owner);
46
+ },
47
+ };
48
+ }
49
+ /** The facts of the project window MCP currently drives, or null when none is open. */
50
+ export function activeMcpWindow() {
51
+ const owner = resolveActiveOwner();
52
+ return (owner !== null && windowFacts.get(owner)) || null;
42
53
  }
43
54
  /**
44
- * Update the active-page bridgeId so the `simulator` target follows the
45
- * currently visible render guest. No-op when unchanged. When native-host is
46
- * on and the simulator target is already connected, fire-and-forget a
47
- * reconnect so MCP re-points at the new active page.
55
+ * Record the render guest now visible in `owner`'s window. Re-points the live
56
+ * simulator connection only when that window is the one MCP drives — a
57
+ * navigation in a background project window must not steal the target.
48
58
  */
49
- export function setActiveBridgeId(id) {
50
- if (id === activeBridgeId)
59
+ export function noteActiveBridgeId(owner, id) {
60
+ const facts = windowFacts.get(owner);
61
+ if (!facts || facts.activeBridgeId === id)
51
62
  return;
52
- activeBridgeId = id;
53
- if (nativeHostMode && targets.simulator.connected) {
63
+ facts.activeBridgeId = id;
64
+ if (facts.nativeHost && resolveActiveOwner() === owner && targets.simulator.connected) {
54
65
  void connectTarget('simulator');
55
66
  }
56
67
  }
57
- export function setNativeOverviewProvider(provider) {
58
- nativeOverviewProvider = provider;
59
- }
60
- export function getNativeOverviewProvider() {
61
- return nativeOverviewProvider;
62
- }
63
68
  /**
64
- * Resolve which CDP target the `simulator` MCP tools should drive.
69
+ * The active project window changed. A connection that is already established
70
+ * was aimed at whichever window was active when it was made, and nothing
71
+ * re-read that afterwards — so without this the MCP tools keep reporting and
72
+ * driving the project the user has left. Only connections bound to a different
73
+ * window are re-aimed; the rest are left alone.
65
74
  *
66
- * Default (non-native) path: the localhost:7788 simulator shell identical
67
- * to the original behavior; `activeBridgeId` is ignored.
68
- *
69
- * Native-host path: the active render-host <webview> guest
70
- * (pageFrame.html?...bridgeId=<id>), preferring the guest matching
71
- * `activeBridgeId`, then any pageFrame guest, then degrading to the shell.
75
+ * Coalesced on purpose: clicking across windows produces a burst of these, and
76
+ * re-resolving once the burst settles means focus that returns to the window a
77
+ * connection already holds costs nothing.
72
78
  */
73
- export function selectSimulatorTarget(targets, opts) {
74
- if (!opts.nativeHost) {
75
- return targets.find((t) => t.url?.includes(SIMULATOR_URL_PATTERN));
76
- }
77
- // 1) Active-bridge guest takes priority over list order.
78
- if (opts.activeBridgeId !== null) {
79
- const bridgeMatch = `bridgeId=${opts.activeBridgeId}`;
80
- const active = targets.find((t) => t.url?.includes(RENDER_GUEST_PATTERN) && t.url.includes(bridgeMatch));
81
- if (active)
82
- return active;
83
- }
84
- // 2) Any render guest (no active match / no active bridge).
85
- const anyGuest = targets.find((t) => t.url?.includes(RENDER_GUEST_PATTERN));
86
- if (anyGuest)
87
- return anyGuest;
88
- // 3) Degrade to the localhost:7788 shell when no render guest exists yet.
89
- return targets.find((t) => t.url?.includes(SIMULATOR_URL_PATTERN));
79
+ export function noteActiveMcpWindowChanged() {
80
+ if (repointTimer)
81
+ return;
82
+ repointTimer = setTimeout(() => {
83
+ repointTimer = null;
84
+ const owner = resolveActiveOwner();
85
+ // A window whose facts are already gone is on its way out: its pages are
86
+ // about to be destroyed, so connecting into them would only produce a
87
+ // client that immediately disconnects. The reconnect timer picks up
88
+ // whichever window replaces it.
89
+ if (owner !== null && !windowFacts.has(owner))
90
+ return;
91
+ for (const kind of Object.keys(targets)) {
92
+ if (!targets[kind].connected || targets[kind].owner === owner)
93
+ continue;
94
+ void connectTarget(kind);
95
+ }
96
+ }, ACTIVE_WINDOW_SETTLE_MS);
97
+ }
98
+ // The two CDP connections are process-wide on purpose: MCP exposes one
99
+ // `simulator` and one `workbench` target with no window argument, so both
100
+ // always mean "the project the user is working in". Which window that is comes
101
+ // from `activeMcpWindow()` on every (re)connect, so the pair follows the
102
+ // active window rather than belonging to any one of them.
103
+ const targets = {
104
+ simulator: { client: null, connected: false, owner: null, timer: null, consoleLogs: [], networkRequests: [], bufferOwner: null },
105
+ workbench: { client: null, connected: false, owner: null, timer: null, consoleLogs: [], networkRequests: [], bufferOwner: null },
106
+ };
107
+ /** Monotonic per target: only the newest connect attempt may publish itself. */
108
+ const connectGeneration = { simulator: 0, workbench: 0 };
109
+ let repointTimer = null;
110
+ export function setCdpPort(port) {
111
+ cdpPort = port;
112
+ }
113
+ /** The cross-process overview reader of the window MCP drives, if it has one. */
114
+ export function getNativeOverviewProvider() {
115
+ return activeMcpWindow()?.nativeOverviewProvider ?? null;
90
116
  }
91
117
  export function getTargetState(kind) {
92
118
  return targets[kind];
@@ -100,27 +126,150 @@ async function listCdpTargets() {
100
126
  }
101
127
  }
102
128
  export { listCdpTargets as listTargets };
129
+ /**
130
+ * `selectWorkbenchTarget` already matches on the active window's exact
131
+ * `projectPath`, so no candidate ranking is blind to ownership there — the
132
+ * match is exact or nothing, with nothing left for a pre-filter to change.
133
+ */
134
+ function findWorkbenchTarget(allTargets) {
135
+ const active = activeMcpWindow();
136
+ return selectWorkbenchTarget(allTargets, { projectPath: active ? active.projectPath : null });
137
+ }
138
+ /**
139
+ * `selectSimulatorTarget` ranks candidates by a fixed priority order with no
140
+ * idea which window each one belongs to, and its "any guest" tier in
141
+ * particular has no way to tell a stray guest left over from another window
142
+ * apart from a legitimate one.
143
+ *
144
+ * Once the active window has an `activeBridgeId` of its own, it is known to
145
+ * be genuinely on a native page and merely waiting for that specific guest
146
+ * target to show up in the CDP list — connecting to whatever guest IS present
147
+ * and letting the post-connect ownership check below reject a wrong one is a
148
+ * reasonable, existing way to close that timing gap, so ranking stays
149
+ * unfiltered in that case.
150
+ *
151
+ * Before any page is known for this window (`activeBridgeId === null`, which
152
+ * is also always true for non-native windows), though, "any guest present"
153
+ * carries no signal it belongs to THIS window at all — so ranking is
154
+ * restricted to targets `connectionOwner` attributes to the active window,
155
+ * falling back to the raw list only when none of them qualify (the same
156
+ * retry-and-reject cadence as the unfiltered path, for a candidate list that
157
+ * belongs to no window MCP can currently claim).
158
+ */
159
+ function findSimulatorTarget(allTargets) {
160
+ const active = activeMcpWindow();
161
+ const opts = { nativeHost: active?.nativeHost ?? false, activeBridgeId: active?.activeBridgeId ?? null };
162
+ if (opts.activeBridgeId !== null)
163
+ return selectSimulatorTarget(allTargets, opts);
164
+ const intended = resolveActiveOwner();
165
+ const owned = allTargets.filter((t) => connectionOwner('simulator', t.url, intended, windowFacts).onTarget);
166
+ return selectSimulatorTarget(owned, opts) ?? selectSimulatorTarget(allTargets, opts);
167
+ }
103
168
  function findTarget(allTargets, kind) {
104
- if (kind === 'simulator') {
105
- return selectSimulatorTarget(allTargets, { nativeHost: nativeHostMode, activeBridgeId });
169
+ return kind === 'simulator' ? findSimulatorTarget(allTargets) : findWorkbenchTarget(allTargets);
170
+ }
171
+ /**
172
+ * Console and network events into this target's ring buffers, which is all
173
+ * these subscriptions do — they hold no view of which window is connected, so
174
+ * they outlive nothing and need no generation check of their own.
175
+ */
176
+ function subscribeBuffers(client, state) {
177
+ const pending = new Map();
178
+ const pushNetworkRequest = (entry) => {
179
+ state.networkRequests.push(entry);
180
+ if (state.networkRequests.length > MAX_BUFFER)
181
+ state.networkRequests = state.networkRequests.slice(-MAX_BUFFER);
182
+ };
183
+ const pushConsoleLog = (entry) => {
184
+ state.consoleLogs.push(entry);
185
+ if (state.consoleLogs.length > MAX_BUFFER)
186
+ state.consoleLogs = state.consoleLogs.slice(-MAX_BUFFER);
187
+ };
188
+ client.Runtime.on('consoleAPICalled', (params) => {
189
+ pushConsoleLog({
190
+ level: params.type,
191
+ text: params.args.map((a) => String(a.value ?? a.description ?? JSON.stringify(a))).join(' '),
192
+ timestamp: new Date(params.timestamp).toISOString(),
193
+ });
194
+ });
195
+ client.Console.on('messageAdded', (params) => {
196
+ const msg = params.message;
197
+ pushConsoleLog({ level: msg.level, text: msg.text, timestamp: new Date().toISOString() });
198
+ });
199
+ client.Network.on('requestWillBeSent', (params) => {
200
+ pending.set(params.requestId, { url: params.request.url, method: params.request.method, timestamp: params.timestamp });
201
+ });
202
+ client.Network.on('responseReceived', (params) => {
203
+ const req = pending.get(params.requestId);
204
+ if (!req)
205
+ return;
206
+ pending.delete(params.requestId);
207
+ pushNetworkRequest({
208
+ url: req.url, method: req.method,
209
+ status: params.response.status, mimeType: params.response.mimeType,
210
+ responseSize: params.response.encodedDataLength || 0,
211
+ timing: params.response.timing
212
+ ? { requestTime: params.response.timing.requestTime, receiveHeadersEnd: params.response.timing.receiveHeadersEnd }
213
+ : null,
214
+ });
215
+ });
216
+ client.Network.on('loadingFailed', (params) => {
217
+ const req = pending.get(params.requestId);
218
+ if (!req)
219
+ return;
220
+ pending.delete(params.requestId);
221
+ pushNetworkRequest({ url: req.url, method: req.method, status: 0, mimeType: '', responseSize: 0, timing: null });
222
+ });
223
+ }
224
+ /** Closing a client is never the reason an attempt fails. */
225
+ async function closeQuietly(client) {
226
+ if (!client)
227
+ return;
228
+ try {
229
+ await client.close();
230
+ }
231
+ catch { }
232
+ }
233
+ /** No usable target right now: publish nothing and keep retrying. */
234
+ function loseTarget(kind) {
235
+ const state = targets[kind];
236
+ state.client = null;
237
+ state.connected = false;
238
+ state.owner = null;
239
+ scheduleReconnect(kind);
240
+ }
241
+ /** Publish a connection that survived the race, on the window it reached. */
242
+ function publishConnection(kind, client, owner) {
243
+ const state = targets[kind];
244
+ state.client = client;
245
+ state.connected = true;
246
+ state.owner = owner;
247
+ if (state.timer) {
248
+ clearTimeout(state.timer);
249
+ state.timer = null;
106
250
  }
107
- // workbench main window: page target whose URL contains index.html (renderer)
108
- return allTargets.find((t) => t.type === 'page' && t.url?.includes('entries/main/index.html') && !t.url?.includes(SIMULATOR_URL_PATTERN));
109
251
  }
110
252
  export async function connectTarget(kind) {
111
253
  const state = targets[kind];
254
+ // Establishing a connection is a long chain of awaits, and the reason to
255
+ // establish it — which window is active, which page it shows — can change
256
+ // while that chain runs. An attempt overtaken by a newer one must publish
257
+ // nothing, close the client it opened, and schedule no work.
258
+ const generation = (connectGeneration[kind] += 1);
259
+ const superseded = () => connectGeneration[kind] !== generation;
112
260
  if (state.client) {
113
- try {
114
- await state.client.close();
115
- }
116
- catch { }
261
+ const previous = state.client;
117
262
  state.client = null;
263
+ await closeQuietly(previous);
264
+ if (superseded())
265
+ return;
118
266
  }
119
267
  const allTargets = await listCdpTargets();
120
268
  const target = findTarget(allTargets, kind);
269
+ if (superseded())
270
+ return;
121
271
  if (!target) {
122
- state.connected = false;
123
- scheduleReconnect(kind);
272
+ loseTarget(kind);
124
273
  return;
125
274
  }
126
275
  let client = null;
@@ -133,83 +282,54 @@ export async function connectTarget(kind) {
133
282
  client.Network.enable(),
134
283
  client.Console.enable(),
135
284
  ]);
136
- // Network buffer
137
- const pending = new Map();
138
- const pushNetworkRequest = (entry) => {
139
- state.networkRequests.push(entry);
140
- if (state.networkRequests.length > MAX_BUFFER)
141
- state.networkRequests = state.networkRequests.slice(-MAX_BUFFER);
142
- };
143
- // Console buffer
144
- client.Runtime.on('consoleAPICalled', (params) => {
145
- const entry = {
146
- level: params.type,
147
- text: params.args.map((a) => String(a.value ?? a.description ?? JSON.stringify(a))).join(' '),
148
- timestamp: new Date(params.timestamp).toISOString(),
149
- };
150
- state.consoleLogs.push(entry);
151
- if (state.consoleLogs.length > MAX_BUFFER)
152
- state.consoleLogs = state.consoleLogs.slice(-MAX_BUFFER);
153
- });
154
- client.Console.on('messageAdded', (params) => {
155
- const msg = params.message;
156
- state.consoleLogs.push({ level: msg.level, text: msg.text, timestamp: new Date().toISOString() });
157
- if (state.consoleLogs.length > MAX_BUFFER)
158
- state.consoleLogs = state.consoleLogs.slice(-MAX_BUFFER);
159
- });
160
- client.Network.on('requestWillBeSent', (params) => {
161
- pending.set(params.requestId, { url: params.request.url, method: params.request.method, timestamp: params.timestamp });
162
- });
163
- client.Network.on('responseReceived', (params) => {
164
- const req = pending.get(params.requestId);
165
- if (!req)
166
- return;
167
- pending.delete(params.requestId);
168
- pushNetworkRequest({
169
- url: req.url, method: req.method,
170
- status: params.response.status, mimeType: params.response.mimeType,
171
- responseSize: params.response.encodedDataLength || 0,
172
- timing: params.response.timing
173
- ? { requestTime: params.response.timing.requestTime, receiveHeadersEnd: params.response.timing.receiveHeadersEnd }
174
- : null,
175
- });
176
- });
177
- client.Network.on('loadingFailed', (params) => {
178
- const req = pending.get(params.requestId);
179
- if (!req)
285
+ // Everything below subscribes or publishes, so an attempt that lost the
286
+ // race stops here and leaves nothing behind but its own closed client.
287
+ if (superseded()) {
288
+ await closeQuietly(client);
289
+ return;
290
+ }
291
+ // Which window this target belongs to is resolved here, not when the
292
+ // attempt started: an attempt that outlived a window switch has to be
293
+ // judged against the window the user is in now. MCP exposes ONE client per
294
+ // kind, so a target belonging to another window leaves nothing behind at
295
+ // all — buffers wired to it would mix that window's console and network
296
+ // events into the ones the eventual right connection appends to, and
297
+ // `connected` would hand it out meanwhile. The retry cadence is the only
298
+ // thing that carries over: the exact target may still be loading.
299
+ const binding = connectionOwner(kind, target.url, resolveActiveOwner(), windowFacts);
300
+ if (!binding.onTarget) {
301
+ await closeQuietly(client);
302
+ if (superseded())
180
303
  return;
181
- pending.delete(params.requestId);
182
- pushNetworkRequest({
183
- url: req.url,
184
- method: req.method,
185
- status: 0,
186
- mimeType: '',
187
- responseSize: 0,
188
- timing: null,
189
- });
190
- });
304
+ loseTarget(kind);
305
+ return;
306
+ }
307
+ // Buffers are keyed to the owner they were collected for, not to the
308
+ // kind slot: a reconnect landing on the SAME owner (a transient CDP drop)
309
+ // must keep what was already buffered, but re-aiming at a different
310
+ // window must not let that window inherit the previous owner's entries.
311
+ if (state.bufferOwner !== binding.owner) {
312
+ state.consoleLogs = [];
313
+ state.networkRequests = [];
314
+ state.bufferOwner = binding.owner;
315
+ }
316
+ subscribeBuffers(client, state);
191
317
  client.on('disconnect', () => {
318
+ // A superseded client is closed on purpose; only the live connection
319
+ // going away means MCP has lost its target.
320
+ if (superseded())
321
+ return;
192
322
  state.connected = false;
193
323
  state.client = null;
194
324
  scheduleReconnect(kind);
195
325
  });
196
- state.client = client;
197
- state.connected = true;
198
- if (state.timer) {
199
- clearTimeout(state.timer);
200
- state.timer = null;
201
- }
326
+ publishConnection(kind, client, binding.owner);
202
327
  }
203
328
  catch {
204
- if (client) {
205
- try {
206
- await client.close();
207
- }
208
- catch { }
209
- }
210
- state.client = null;
211
- state.connected = false;
212
- scheduleReconnect(kind);
329
+ await closeQuietly(client);
330
+ if (superseded())
331
+ return;
332
+ loseTarget(kind);
213
333
  }
214
334
  }
215
335
  function scheduleReconnect(kind) {
@@ -223,10 +343,17 @@ function scheduleReconnect(kind) {
223
343
  }
224
344
  export function getClient(kind) {
225
345
  const state = targets[kind];
346
+ const label = kind === 'simulator' ? '模拟器' : '主窗口';
226
347
  if (!state.connected || !state.client) {
227
- const label = kind === 'simulator' ? '模拟器' : '主窗口';
228
348
  throw new Error(`未连接到${label}。请确保 dimina-devtools 正在以开发模式运行。`);
229
349
  }
350
+ // A focus change re-aims the live connections only once the burst settles,
351
+ // and during that gap the client still belongs to the window the user left.
352
+ // Answering from it would report and drive the wrong project, so the caller
353
+ // waits for the re-aim instead.
354
+ if (state.owner !== resolveActiveOwner()) {
355
+ throw new Error(`${label}连接属于已切换的项目窗口,正在重新连接,请稍后重试。`);
356
+ }
230
357
  return state.client;
231
358
  }
232
359
  //# sourceMappingURL=target-manager.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Pure candidate-ranking rules for the two MCP CDP targets.
3
+ *
4
+ * Split out of target-manager.ts: these functions only rank a raw CDP.List
5
+ * result by URL shape and carry no connection state of their own, so they can
6
+ * be pinned with direct unit tests independent of the connection lifecycle.
7
+ */
8
+ /**
9
+ * Resolve which CDP target the `simulator` MCP tools should drive.
10
+ *
11
+ * Default (non-native) path: the localhost:7788 simulator shell — identical
12
+ * to the original behavior; `activeBridgeId` is ignored.
13
+ *
14
+ * Native-host path: the active render-host <webview> guest
15
+ * (pageFrame.html?...bridgeId=<id>), preferring the guest matching
16
+ * `activeBridgeId`, then any pageFrame guest, then degrading to the shell.
17
+ */
18
+ export declare function selectSimulatorTarget<T extends {
19
+ url?: string;
20
+ type?: string;
21
+ }>(targets: T[], opts: {
22
+ nativeHost: boolean;
23
+ activeBridgeId: string | null;
24
+ }): T | undefined;
25
+ /**
26
+ * Resolve which CDP target the `workbench` MCP tools should drive.
27
+ *
28
+ * `projectPath` is the active project window's path, fixed when that window
29
+ * opened; null when no project window is open, and then the project list is
30
+ * the only workbench surface.
31
+ *
32
+ * A project window names its own renderer from the moment it exists, so the
33
+ * match is exact or nothing: another project's workbench page, or the project
34
+ * list standing in for a project that IS open, would leave every workbench
35
+ * tool answering for the wrong window with nothing to correct it.
36
+ *
37
+ * Matching is on the renderer ENTRY, never on "the URL mentions the project
38
+ * directory" — the service-host window carries the same directory in its own
39
+ * `pkgRoot` query and would win a substring match.
40
+ */
41
+ export declare function selectWorkbenchTarget<T extends {
42
+ url?: string;
43
+ type?: string;
44
+ }>(candidates: T[], opts: {
45
+ projectPath: string | null;
46
+ }): T | undefined;
47
+ //# sourceMappingURL=target-selection.d.ts.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Pure candidate-ranking rules for the two MCP CDP targets.
3
+ *
4
+ * Split out of target-manager.ts: these functions only rank a raw CDP.List
5
+ * result by URL shape and carry no connection state of their own, so they can
6
+ * be pinned with direct unit tests independent of the connection lifecycle.
7
+ */
8
+ import { DMB_PAGEFRAME_DOC_NAME } from '../../../shared/dmb-resource-url.js';
9
+ import { targetQuery } from './connection-owner.js';
10
+ const SIMULATOR_URL_PATTERN = 'localhost:7788';
11
+ // The two renderer entries MCP can drive: a project's workbench window and the
12
+ // always-present project list.
13
+ const WORKBENCH_ENTRY = 'entries/workbench/index.html';
14
+ const PROJECT_LIST_ENTRY = 'entries/main/index.html';
15
+ // Native-host: the real mini-app page runs in a nested render-host <webview>
16
+ // guest whose CDP target URL carries the render frame + the page's bridgeId.
17
+ // Import the shared reserved doc name rather than hand-writing a second
18
+ // literal — that duplication is exactly what let this pattern drift out of
19
+ // sync with the actual document URL shape (dmb-resource-url.ts).
20
+ const RENDER_GUEST_PATTERN = DMB_PAGEFRAME_DOC_NAME;
21
+ /**
22
+ * Resolve which CDP target the `simulator` MCP tools should drive.
23
+ *
24
+ * Default (non-native) path: the localhost:7788 simulator shell — identical
25
+ * to the original behavior; `activeBridgeId` is ignored.
26
+ *
27
+ * Native-host path: the active render-host <webview> guest
28
+ * (pageFrame.html?...bridgeId=<id>), preferring the guest matching
29
+ * `activeBridgeId`, then any pageFrame guest, then degrading to the shell.
30
+ */
31
+ export function selectSimulatorTarget(targets, opts) {
32
+ if (!opts.nativeHost) {
33
+ return targets.find((t) => t.url?.includes(SIMULATOR_URL_PATTERN));
34
+ }
35
+ // 1) Active-bridge guest takes priority over list order.
36
+ if (opts.activeBridgeId !== null) {
37
+ const bridgeMatch = `bridgeId=${opts.activeBridgeId}`;
38
+ const active = targets.find((t) => t.url?.includes(RENDER_GUEST_PATTERN) && t.url.includes(bridgeMatch));
39
+ if (active)
40
+ return active;
41
+ }
42
+ // 2) Any render guest (no active match / no active bridge).
43
+ const anyGuest = targets.find((t) => t.url?.includes(RENDER_GUEST_PATTERN));
44
+ if (anyGuest)
45
+ return anyGuest;
46
+ // 3) Degrade to the localhost:7788 shell when no render guest exists yet.
47
+ return targets.find((t) => t.url?.includes(SIMULATOR_URL_PATTERN));
48
+ }
49
+ /**
50
+ * Resolve which CDP target the `workbench` MCP tools should drive.
51
+ *
52
+ * `projectPath` is the active project window's path, fixed when that window
53
+ * opened; null when no project window is open, and then the project list is
54
+ * the only workbench surface.
55
+ *
56
+ * A project window names its own renderer from the moment it exists, so the
57
+ * match is exact or nothing: another project's workbench page, or the project
58
+ * list standing in for a project that IS open, would leave every workbench
59
+ * tool answering for the wrong window with nothing to correct it.
60
+ *
61
+ * Matching is on the renderer ENTRY, never on "the URL mentions the project
62
+ * directory" — the service-host window carries the same directory in its own
63
+ * `pkgRoot` query and would win a substring match.
64
+ */
65
+ export function selectWorkbenchTarget(candidates, opts) {
66
+ const pages = candidates.filter((t) => t.type === 'page' && !t.url?.includes(SIMULATOR_URL_PATTERN));
67
+ if (opts.projectPath === null)
68
+ return pages.find((t) => t.url?.includes(PROJECT_LIST_ENTRY));
69
+ return pages.find((t) => t.url?.includes(WORKBENCH_ENTRY) && targetQuery(t.url, 'path') === opts.projectPath);
70
+ }
71
+ //# sourceMappingURL=target-selection.js.map
@@ -136,8 +136,21 @@ export function registerContextTools(server) {
136
136
  return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
137
137
  }
138
138
  let result;
139
+ // Snapshot the provider AND the console/network summaries in the same
140
+ // tick as `getClient`: the provider reads the active window live off
141
+ // `resolveActiveOwner()`, and the buffers are process-wide and get
142
+ // cleared and re-owned the moment a reconnect lands on another window.
143
+ // Both awaits below (the CDP probe, the native provider) are windows the
144
+ // user can switch projects during; reading either after them would mix
145
+ // this window's probe with whichever window is active by then.
146
+ let nativeOverviewProvider;
147
+ let consoleSum;
148
+ let networkSum;
139
149
  try {
140
150
  const c = getClient('simulator');
151
+ nativeOverviewProvider = getNativeOverviewProvider();
152
+ consoleSum = summarizeConsole('simulator');
153
+ networkSum = summarizeNetwork('simulator');
141
154
  result = await c.Runtime.evaluate({ expression: SIMULATOR_PROBE_EXPR, returnByValue: true });
142
155
  }
143
156
  catch {
@@ -160,7 +173,6 @@ export function registerContextTools(server) {
160
173
  isError: true,
161
174
  };
162
175
  }
163
- const nativeOverviewProvider = getNativeOverviewProvider();
164
176
  if (nativeOverviewProvider) {
165
177
  try {
166
178
  const nativeOverview = await nativeOverviewProvider();
@@ -182,8 +194,6 @@ export function registerContextTools(server) {
182
194
  hints.push('no active miniapp page (empty page stack)');
183
195
  if (probe.storageCount > MAX_KEYS)
184
196
  hints.push(`storageKeys truncated to first ${MAX_KEYS} of ${probe.storageCount}`);
185
- const consoleSum = summarizeConsole('simulator');
186
- const networkSum = summarizeNetwork('simulator');
187
197
  const payload = {
188
198
  connected: true,
189
199
  url: probe.url,
@@ -213,8 +223,15 @@ export function registerContextTools(server) {
213
223
  return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
214
224
  }
215
225
  let result;
226
+ // Same snapshot rule as the simulator overview: the buffers belong to
227
+ // the connection `getClient` just vouched for, so they are read before
228
+ // the probe's await, not after it.
229
+ let consoleSum;
230
+ let networkSum;
216
231
  try {
217
232
  const c = getClient('workbench');
233
+ consoleSum = summarizeConsole('workbench');
234
+ networkSum = summarizeNetwork('workbench');
218
235
  result = await c.Runtime.evaluate({ expression: WORKBENCH_PROBE_EXPR, returnByValue: true });
219
236
  }
220
237
  catch {
@@ -237,8 +254,6 @@ export function registerContextTools(server) {
237
254
  isError: true,
238
255
  };
239
256
  }
240
- const consoleSum = summarizeConsole('workbench');
241
- const networkSum = summarizeNetwork('workbench');
242
257
  const payload = {
243
258
  connected: true,
244
259
  url: probe.url,