@dimina-kit/devtools 0.4.0-dev.20260702182435 → 0.4.0-dev.20260703101348

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 (87) hide show
  1. package/dist/main/app/app.js +5 -25
  2. package/dist/main/app/native-overview.d.ts +27 -0
  3. package/dist/main/app/native-overview.js +44 -0
  4. package/dist/main/index.bundle.js +1217 -735
  5. package/dist/main/ipc/bridge-router.d.ts +8 -0
  6. package/dist/main/ipc/bridge-router.js +526 -173
  7. package/dist/main/ipc/project-fs.js +39 -21
  8. package/dist/main/services/automation/handlers/app.js +143 -109
  9. package/dist/main/services/console-forward/index.d.ts +12 -1
  10. package/dist/main/services/console-forward/index.js +99 -1
  11. package/dist/main/services/diagnostics/index.d.ts +49 -0
  12. package/dist/main/services/diagnostics/index.js +61 -0
  13. package/dist/main/services/mcp/tools/simulator-tools.js +118 -97
  14. package/dist/main/services/network-forward/dispatch-batch.d.ts +22 -0
  15. package/dist/main/services/network-forward/dispatch-batch.js +21 -0
  16. package/dist/main/services/network-forward/index.js +37 -20
  17. package/dist/main/services/notifications/renderer-notifier.d.ts +39 -0
  18. package/dist/main/services/notifications/renderer-notifier.js +4 -1
  19. package/dist/main/services/projects/create-project-service.js +54 -34
  20. package/dist/main/services/projects/project-repository.js +2 -1
  21. package/dist/main/services/projects/types.d.ts +1 -32
  22. package/dist/main/services/service-console/console-api.js +36 -27
  23. package/dist/main/services/simulator-temp-files/disk.js +109 -86
  24. package/dist/main/services/views/simulator-session-policy.js +17 -0
  25. package/dist/main/services/workbench-coi-server.js +58 -45
  26. package/dist/main/services/workbench-context.d.ts +10 -0
  27. package/dist/main/services/workspace/workspace-service.js +12 -3
  28. package/dist/main/utils/logger.d.ts +5 -12
  29. package/dist/main/utils/logger.js +5 -45
  30. package/dist/main/windows/service-host-window/create.d.ts +15 -1
  31. package/dist/main/windows/service-host-window/create.js +11 -3
  32. package/dist/preload/instrumentation/wxml-extract.js +94 -62
  33. package/dist/preload/runtime/main-api-runner.d.ts +1 -1
  34. package/dist/preload/shared/api-compat.js +18 -55
  35. package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
  36. package/dist/preload/windows/main.cjs +284 -565
  37. package/dist/preload/windows/main.cjs.map +2 -2
  38. package/dist/preload/windows/simulator.cjs +154 -83
  39. package/dist/preload/windows/simulator.cjs.map +3 -3
  40. package/dist/preload/windows/simulator.js +154 -83
  41. package/dist/render-host/render-inspect.js +63 -52
  42. package/dist/renderer/assets/index-BwzPwCmM.js +49 -0
  43. package/dist/renderer/assets/{input-Cjk0wSEh.js → input-CTb_le7F.js} +2 -2
  44. package/dist/renderer/assets/ipc-transport-BPWIV5hA.css +1 -0
  45. package/dist/renderer/assets/{ipc-transport-OZS-KmOW.js → ipc-transport-Ck5Xa8Tu.js} +2 -2
  46. package/dist/renderer/assets/popover-B1Zu2UQu.js +2 -0
  47. package/dist/renderer/assets/{select-BQDXi5ih.js → select-ozSk5Pt6.js} +2 -2
  48. package/dist/renderer/assets/{settings-tP3Px2KR.js → settings-CNwGOI22.js} +2 -2
  49. package/dist/renderer/assets/settings-api-DMbMp1Eq.js +2 -0
  50. package/dist/renderer/assets/workbenchSettings-Cr4EHo5w.js +8 -0
  51. package/dist/renderer/entries/main/index.html +6 -6
  52. package/dist/renderer/entries/popover/index.html +5 -5
  53. package/dist/renderer/entries/settings/index.html +5 -5
  54. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  55. package/dist/shared/appdata-accumulator.js +53 -48
  56. package/dist/shared/bridge-channels.d.ts +22 -0
  57. package/dist/shared/ipc-channels.d.ts +3 -0
  58. package/dist/shared/ipc-channels.js +9 -0
  59. package/dist/shared/open-in-editor-resource-path.d.ts +56 -0
  60. package/dist/shared/open-in-editor-resource-path.js +133 -0
  61. package/dist/shared/open-in-editor.d.ts +2 -9
  62. package/dist/shared/open-in-editor.js +8 -93
  63. package/dist/shared/request-core.d.ts +84 -0
  64. package/dist/shared/request-core.js +168 -0
  65. package/dist/shared/simulator-api-metadata.d.ts +22 -17
  66. package/dist/shared/simulator-api-metadata.js +36 -0
  67. package/dist/shared/types.d.ts +20 -4
  68. package/dist/simulator/assets/{device-shell-CUl0ILfn.js → device-shell-C-wcFq3Z.js} +2 -2
  69. package/dist/simulator/assets/{jsx-runtime-CDK-o-S0.js → jsx-runtime-BDTY6fEq.js} +2 -2
  70. package/dist/simulator/assets/simulator-DU3-fS3v.js +10 -0
  71. package/dist/simulator/assets/simulator-mini-app-DzfMfnVM.js +2 -0
  72. package/dist/simulator/simulator.html +2 -2
  73. package/dist/vscode-workbench/assets/__vite-browser-external-CXi6Kz9W.js +1 -0
  74. package/dist/vscode-workbench/assets/{dist-CS7SQP3K.js → dist-TpGpmMGi.js} +3 -3
  75. package/dist/vscode-workbench/assets/{iconv-lite-umd-D3q-1-o6.js → iconv-lite-umd-C9tiCAJa.js} +1 -1
  76. package/dist/vscode-workbench/assets/{index-zjigpZ25.js → index-DJ1HyMZ7.js} +20 -22
  77. package/dist/vscode-workbench/assets/{jschardet-Cu4ufeHq.js → jschardet-DE1jV513.js} +1 -1
  78. package/dist/vscode-workbench/index.html +1 -1
  79. package/package.json +6 -6
  80. package/dist/renderer/assets/index-B-Dqb7G0.js +0 -49
  81. package/dist/renderer/assets/ipc-transport-D5dKIti1.css +0 -1
  82. package/dist/renderer/assets/popover-CQVvOe90.js +0 -2
  83. package/dist/renderer/assets/settings-api-B6x2mhaD.js +0 -2
  84. package/dist/renderer/assets/workbenchSettings-Bim9ol9R.js +0 -8
  85. package/dist/simulator/assets/simulator-Dz8iGcgy.js +0 -10
  86. package/dist/simulator/assets/simulator-mini-app-Bt639XL_.js +0 -2
  87. package/dist/vscode-workbench/assets/__vite-browser-external-B0DTNerG.js +0 -1
@@ -468,6 +468,40 @@ function writeFileSync(root, p, content) {
468
468
  nodeFs.closeSync(fd);
469
469
  }
470
470
  }
471
+ /**
472
+ * Reads one directory's entries. Returns `null` (caller skips the directory)
473
+ * for `EACCES`/`ENOENT`/`ENOTDIR` — a directory can disappear or become
474
+ * unreadable between being queued and being walked. Any other error is
475
+ * rethrown.
476
+ */
477
+ async function readDirEntriesSafe(dir) {
478
+ try {
479
+ return await fs.readdir(dir, { withFileTypes: true });
480
+ }
481
+ catch (err) {
482
+ const code = err.code;
483
+ if (code === 'EACCES' || code === 'ENOENT' || code === 'ENOTDIR')
484
+ return null;
485
+ throw err;
486
+ }
487
+ }
488
+ /**
489
+ * Classifies one readdir entry: a non-`SKIP_DIRS` directory is queued for
490
+ * walking; a plain file is recorded as a `safeRoot`-relative POSIX path.
491
+ * Skipped directories and non-file/non-directory entries (symlinks, sockets,
492
+ * etc.) are a no-op.
493
+ */
494
+ function collectListEntry(entry, dir, safeRoot, out, queue) {
495
+ const full = path.join(dir, entry.name);
496
+ if (entry.isDirectory()) {
497
+ if (!SKIP_DIRS.has(entry.name))
498
+ queue.push(full);
499
+ return;
500
+ }
501
+ if (entry.isFile()) {
502
+ out.push(path.relative(safeRoot, full).split(path.sep).join('/'));
503
+ }
504
+ }
471
505
  async function listFiles(root, rootArg) {
472
506
  const safeRoot = await resolveWithinProjectRoot(rootArg, root);
473
507
  const out = [];
@@ -476,29 +510,13 @@ async function listFiles(root, rootArg) {
476
510
  if (out.length >= LIST_FILE_LIMIT)
477
511
  break;
478
512
  const dir = queue.shift();
479
- let entries;
480
- try {
481
- entries = await fs.readdir(dir, { withFileTypes: true });
482
- }
483
- catch (err) {
484
- const code = err.code;
485
- if (code === 'EACCES' || code === 'ENOENT' || code === 'ENOTDIR')
486
- continue;
487
- throw err;
488
- }
489
- for (const e of entries) {
513
+ const entries = await readDirEntriesSafe(dir);
514
+ if (entries === null)
515
+ continue;
516
+ for (const entry of entries) {
490
517
  if (out.length >= LIST_FILE_LIMIT)
491
518
  break;
492
- const full = path.join(dir, e.name);
493
- if (e.isDirectory()) {
494
- if (SKIP_DIRS.has(e.name))
495
- continue;
496
- queue.push(full);
497
- continue;
498
- }
499
- if (!e.isFile())
500
- continue;
501
- out.push(path.relative(safeRoot, full).split(path.sep).join('/'));
519
+ collectListEntry(entry, dir, safeRoot, out, queue);
502
520
  }
503
521
  }
504
522
  return out;
@@ -76,122 +76,141 @@ appHandlers['App.getPageStack'] = async (ctx) => {
76
76
  }
77
77
  return { pageStack };
78
78
  };
79
- appHandlers['App.callWxMethod'] = async (ctx, params) => {
80
- const method = params.method;
81
- const args = params.args || [];
82
- // NATIVE-HOST: the authoritative `wx.*` runs in the hidden service-host
83
- // window (the simulator / render-guest context has no `wx`). Run EVERY method
84
- // there nav so the page stack goes through the real runtime path, and
85
- // non-nav (setNavigationBarTitle / getSystemInfoSync / tabBar APIs / …) so
86
- // they don't fall through to the default-arch eval-in-simulator path below,
87
- // which throws "wx is not defined" under native-host.
88
- if (ctx.bridge?.isNativeHost()) {
89
- const serviceWc = ctx.bridge.getServiceWc();
90
- if (!serviceWc)
91
- throw new Error('Service host not connected');
92
- if (NAV_METHODS.has(method) || method === 'navigateBack') {
93
- const arg = method === 'navigateBack'
94
- ? (args[0] ?? { delta: 1 })
95
- : (args[0] ?? {});
96
- // Capture the active page BEFORE navigating so we can wait for it to
97
- // actually change, instead of blindly sleeping a fixed duration.
98
- const since = ctx.bridge.getActiveBridgeId();
99
- await serviceWc.executeJavaScript(`wx.${method}(${JSON.stringify(arg)})`);
100
- // Wait for the bridge's `activePage` signal (the new page mounted), with a
101
- // timeout floor = the old blind-wait duration. Resolves as soon as the new
102
- // page is active — far faster than the fixed sleep for the common case —
103
- // and still bounded for edges (e.g. switchTab back to an already-mounted
104
- // tab keeps the same bridgeId, so it falls through to the floor, same as
105
- // the old behaviour). See wait-active-page.ts.
106
- const timeoutMs = method === 'navigateBack' ? 1500 : 2000;
107
- await waitForActivePage(ctx.bridge, {
108
- since,
109
- timeoutMs,
110
- onTimeout: () => console.warn(`[automation] ${method}: activePage signal not seen within ${timeoutMs}ms — proceeding on timeout floor`),
111
- });
112
- return { result: undefined };
79
+ /** Nav (incl. navigateBack) on the native-host service-host `wx`, waiting for the page to actually change. */
80
+ async function runNativeHostNav(bridge, serviceWc, method, args) {
81
+ const arg = method === 'navigateBack'
82
+ ? (args[0] ?? { delta: 1 })
83
+ : (args[0] ?? {});
84
+ // Capture the active page BEFORE navigating so we can wait for it to
85
+ // actually change, instead of blindly sleeping a fixed duration.
86
+ const since = bridge.getActiveBridgeId();
87
+ await serviceWc.executeJavaScript(`wx.${method}(${JSON.stringify(arg)})`);
88
+ // Wait for the bridge's `activePage` signal (the new page mounted), with a
89
+ // timeout floor = the old blind-wait duration. Resolves as soon as the new
90
+ // page is active — far faster than the fixed sleep for the common case —
91
+ // and still bounded for edges (e.g. switchTab back to an already-mounted
92
+ // tab keeps the same bridgeId, so it falls through to the floor, same as
93
+ // the old behaviour). See wait-active-page.ts.
94
+ const timeoutMs = method === 'navigateBack' ? 1500 : 2000;
95
+ await waitForActivePage(bridge, {
96
+ since,
97
+ timeoutMs,
98
+ onTimeout: () => console.warn(`[automation] ${method}: activePage signal not seen within ${timeoutMs}ms — proceeding on timeout floor`),
99
+ });
100
+ return { result: undefined };
101
+ }
102
+ /**
103
+ * Non-nav `wx.*` on the native-host service-host, returning its (sync) value.
104
+ * Async methods that report via a success callback return undefined here,
105
+ * same as the default-arch generic path (`runSimulatorGeneric`).
106
+ */
107
+ async function runNativeHostGeneric(serviceWc, method, args) {
108
+ const argsStr = args.map((a) => JSON.stringify(a)).join(', ');
109
+ const result = await serviceWc.executeJavaScript(`
110
+ new Promise((resolve, reject) => {
111
+ try {
112
+ if (typeof wx === 'undefined' || typeof wx[${JSON.stringify(method)}] !== 'function') {
113
+ reject(new Error('wx.' + ${JSON.stringify(method)} + ' is not a function in service host'))
114
+ return
113
115
  }
114
- // Non-nav: invoke on the service-host wx and return its (sync) value. Async
115
- // methods that report via a success callback return undefined here, same as
116
- // the default-arch generic path below.
117
- const argsStr = args.map((a) => JSON.stringify(a)).join(', ');
118
- const result = await serviceWc.executeJavaScript(`
119
- new Promise((resolve, reject) => {
116
+ resolve(wx.${method}(${argsStr}))
117
+ } catch (e) { reject(e && e.message ? e.message : String(e)) }
118
+ })
119
+ `);
120
+ return { result };
121
+ }
122
+ /**
123
+ * NATIVE-HOST: the authoritative `wx.*` runs in the hidden service-host
124
+ * window (the simulator / render-guest context has no `wx`). Run EVERY method
125
+ * there — nav so the page stack goes through the real runtime path, and
126
+ * non-nav (setNavigationBarTitle / getSystemInfoSync / tabBar APIs / …) so
127
+ * they don't fall through to the default-arch eval-in-simulator path, which
128
+ * throws "wx is not defined" under native-host.
129
+ */
130
+ async function runNativeHostWxMethod(bridge, method, args) {
131
+ const serviceWc = bridge.getServiceWc();
132
+ if (!serviceWc)
133
+ throw new Error('Service host not connected');
134
+ if (NAV_METHODS.has(method) || method === 'navigateBack') {
135
+ return runNativeHostNav(bridge, serviceWc, method, args);
136
+ }
137
+ return runNativeHostGeneric(serviceWc, method, args);
138
+ }
139
+ /**
140
+ * Try clicking a matching DOM element for a nav target. We can't use
141
+ * `inIframe` here — tabBar mini-apps keep prior tabs' iframes around
142
+ * (display:none) and `iframes[length-1]` ends up being the hidden,
143
+ * freshly-cached tab rather than the visible one. Pick the visible iframe
144
+ * explicitly.
145
+ */
146
+ async function clickNavTarget(ctx, cleanUrl) {
147
+ return evalInSim(ctx, `(() => {
148
+ const iframes = Array.from(document.querySelectorAll('iframe'))
149
+ const visible = iframes.filter((f) => {
150
+ const cs = window.getComputedStyle(f)
151
+ if (cs.display === 'none' || cs.visibility === 'hidden') return false
152
+ const rect = f.getBoundingClientRect()
153
+ return rect.width > 0 && rect.height > 0
154
+ })
155
+ const target = visible[visible.length - 1] || iframes[iframes.length - 1]
156
+ if (!target || !target.contentDocument) return false
157
+ const path = ${JSON.stringify(cleanUrl)}
158
+ const el = Array.from(target.contentDocument.querySelectorAll('[data-path]'))
159
+ .find((e) => e.getAttribute('data-path') === path)
160
+ if (el) { el.click(); return true }
161
+ return false
162
+ })()`).catch(() => false);
163
+ }
164
+ /**
165
+ * Fallback cascade for navigation methods when no matching DOM element was
166
+ * found to click:
167
+ * 1. iframe wx[method] — page-iframe surface (older arch, where jdimina
168
+ * installed `window.wx` on each page frame).
169
+ * 2. top-window wx[method] — set up by simulator/main.tsx, binds
170
+ * MiniApp.switchTab / navigateTo / etc. Required for switchTab since
171
+ * page-iframe wx (jdimina) doesn't expose it.
172
+ */
173
+ async function invokeNavViaWx(ctx, method, cleanUrl) {
174
+ const apiName = JSON.stringify(method);
175
+ const urlJson = JSON.stringify(cleanUrl);
176
+ await evalInSim(ctx, `(() => {
177
+ const iframes = Array.from(document.querySelectorAll('iframe'))
178
+ for (const f of iframes) {
120
179
  try {
121
- if (typeof wx === 'undefined' || typeof wx[${JSON.stringify(method)}] !== 'function') {
122
- reject(new Error('wx.' + ${JSON.stringify(method)} + ' is not a function in service host'))
180
+ if (f.contentWindow && f.contentWindow.wx && typeof f.contentWindow.wx[${apiName}] === 'function') {
181
+ f.contentWindow.wx[${apiName}]({ url: ${urlJson} })
123
182
  return
124
183
  }
125
- resolve(wx.${method}(${argsStr}))
126
- } catch (e) { reject(e && e.message ? e.message : String(e)) }
127
- })
128
- `);
129
- return { result };
130
- }
131
- // For navigation methods, handle specially via DOM click / wx.* on the iframe
132
- if (['navigateTo', 'redirectTo', 'reLaunch', 'switchTab'].includes(method)) {
133
- const opts = args[0];
134
- const url = opts?.url;
135
- if (url) {
136
- const cleanUrl = url.startsWith('/') ? url : `/${url}`;
137
- // Try clicking a matching DOM element first. We can't use `inIframe`
138
- // here — tabBar mini-apps keep prior tabs' iframes around (display:none)
139
- // and `iframes[length-1]` ends up being the hidden, freshly-cached tab
140
- // rather than the visible one. Pick the visible iframe explicitly.
141
- const clicked = await evalInSim(ctx, `(() => {
142
- const iframes = Array.from(document.querySelectorAll('iframe'))
143
- const visible = iframes.filter((f) => {
144
- const cs = window.getComputedStyle(f)
145
- if (cs.display === 'none' || cs.visibility === 'hidden') return false
146
- const rect = f.getBoundingClientRect()
147
- return rect.width > 0 && rect.height > 0
148
- })
149
- const target = visible[visible.length - 1] || iframes[iframes.length - 1]
150
- if (!target || !target.contentDocument) return false
151
- const path = ${JSON.stringify(cleanUrl)}
152
- const el = Array.from(target.contentDocument.querySelectorAll('[data-path]'))
153
- .find((e) => e.getAttribute('data-path') === path)
154
- if (el) { el.click(); return true }
155
- return false
156
- })()`).catch(() => false);
157
- if (!clicked) {
158
- // Fallback cascade for navigation methods:
159
- // 1. iframe wx[method] — page-iframe surface (older arch, where
160
- // jdimina installed `window.wx` on each page frame).
161
- // 2. top-window wx[method] — set up by simulator/main.tsx, binds
162
- // MiniApp.switchTab / navigateTo / etc. Required for switchTab
163
- // since page-iframe wx (jdimina) doesn't expose it.
164
- const apiName = JSON.stringify(method);
165
- const urlJson = JSON.stringify(cleanUrl);
166
- await evalInSim(ctx, `(() => {
167
- const iframes = Array.from(document.querySelectorAll('iframe'))
168
- for (const f of iframes) {
169
- try {
170
- if (f.contentWindow && f.contentWindow.wx && typeof f.contentWindow.wx[${apiName}] === 'function') {
171
- f.contentWindow.wx[${apiName}]({ url: ${urlJson} })
172
- return
173
- }
174
- } catch (_) {}
175
- }
176
- if (typeof wx !== 'undefined' && wx && typeof wx[${apiName}] === 'function') {
177
- wx[${apiName}]({ url: ${urlJson} })
178
- }
179
- })()`);
180
- }
181
- // Wait for navigation
182
- await new Promise((r) => setTimeout(r, 2000));
183
- }
184
- return { result: undefined };
185
- }
186
- // For navigateBack
187
- if (method === 'navigateBack') {
188
- await evalInSim(ctx, 'history.back()');
189
- await new Promise((r) => setTimeout(r, 1500));
184
+ } catch (_) {}
185
+ }
186
+ if (typeof wx !== 'undefined' && wx && typeof wx[${apiName}] === 'function') {
187
+ wx[${apiName}]({ url: ${urlJson} })
188
+ }
189
+ })()`);
190
+ }
191
+ /** Default-arch nav: handled via DOM click / wx.* on the iframe. */
192
+ async function runSimulatorNav(ctx, method, args) {
193
+ const opts = args[0];
194
+ const url = opts?.url;
195
+ if (!url)
190
196
  return { result: undefined };
197
+ const cleanUrl = url.startsWith('/') ? url : `/${url}`;
198
+ const clicked = await clickNavTarget(ctx, cleanUrl);
199
+ if (!clicked) {
200
+ await invokeNavViaWx(ctx, method, cleanUrl);
191
201
  }
192
- // General wx method call
202
+ // Wait for navigation
203
+ await new Promise((r) => setTimeout(r, 2000));
204
+ return { result: undefined };
205
+ }
206
+ async function runSimulatorNavigateBack(ctx) {
207
+ await evalInSim(ctx, 'history.back()');
208
+ await new Promise((r) => setTimeout(r, 1500));
209
+ return { result: undefined };
210
+ }
211
+ /** General wx method call — async wx methods with success/fail callbacks. */
212
+ async function runSimulatorGeneric(ctx, method, args) {
193
213
  const argsStr = args.map((a) => JSON.stringify(a)).join(', ');
194
- // Async wx methods with success/fail callbacks
195
214
  const result = await evalInSim(ctx, `
196
215
  new Promise((resolve, reject) => {
197
216
  try {
@@ -203,6 +222,21 @@ appHandlers['App.callWxMethod'] = async (ctx, params) => {
203
222
  })
204
223
  `);
205
224
  return { result };
225
+ }
226
+ appHandlers['App.callWxMethod'] = async (ctx, params) => {
227
+ const method = params.method;
228
+ const args = params.args || [];
229
+ const bridge = ctx.bridge;
230
+ if (bridge?.isNativeHost()) {
231
+ return runNativeHostWxMethod(bridge, method, args);
232
+ }
233
+ if (NAV_METHODS.has(method)) {
234
+ return runSimulatorNav(ctx, method, args);
235
+ }
236
+ if (method === 'navigateBack') {
237
+ return runSimulatorNavigateBack(ctx);
238
+ }
239
+ return runSimulatorGeneric(ctx, method, args);
206
240
  };
207
241
  appHandlers['App.callFunction'] = async (ctx, params) => {
208
242
  const fnDecl = params.functionDeclaration;
@@ -1,5 +1,6 @@
1
1
  import { type Disposable } from '@dimina-kit/electron-deck/main';
2
2
  import type { BridgeRouterHandle } from '../../ipc/bridge-router.js';
3
+ import type { DiagnosticsBus } from '../diagnostics/index.js';
3
4
  /**
4
5
  * One console entry posted by a guest preload. Shape mirrors
5
6
  * render-host/preload.cjs + service-host/preload.cjs `emitConsoleLog`:
@@ -31,6 +32,16 @@ export interface ConsoleForwarder extends Disposable {
31
32
  * render→service forward is internal and not exposed as a sink.
32
33
  */
33
34
  subscribe(sink: ConsoleSink): Disposable;
35
+ /**
36
+ * Flush any diagnostic queued for `appSessionId` (plus the global,
37
+ * session-less queue) into that session's now-ready service-host wc.
38
+ * Idempotent: entries are removed from their bucket as they flush, so a
39
+ * repeat call for the same session injects nothing further. Call once the
40
+ * session's service-host webContents has actually navigated/loaded (e.g.
41
+ * from `bootServiceHost`, after `did-finish-load`) — calling it earlier just
42
+ * finds nothing resolvable and leaves the queue intact for a later call.
43
+ */
44
+ notifyServiceHostReady(appSessionId: string): void;
34
45
  }
35
- export declare function createConsoleForwarder(bridge: Pick<BridgeRouterHandle, 'getServiceWc' | 'getServiceWcForBridge'>): ConsoleForwarder;
46
+ export declare function createConsoleForwarder(bridge: Pick<BridgeRouterHandle, 'getServiceWc' | 'getServiceWcForBridge'>, diagnostics?: DiagnosticsBus): ConsoleForwarder;
36
47
  //# sourceMappingURL=index.d.ts.map
@@ -25,9 +25,81 @@ function buildForwardScript(level, args) {
25
25
  // `consoleAPICalled` whose top frame URL === RENDER_FORWARD_SOURCE_URL.
26
26
  return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});console[${JSON.stringify(method)}]('[视图]',...a)}catch(_){}})()\n//# sourceURL=${RENDER_FORWARD_SOURCE_URL}`;
27
27
  }
28
- export function createConsoleForwarder(bridge) {
28
+ /** `Diagnostic.severity` → the literal `console.<method>` call to emit (dot form, not computed — kept greppable/CDP-attributable like a hand-written call site). */
29
+ const DIAGNOSTIC_CONSOLE_CALL = {
30
+ error: 'console.error',
31
+ warn: 'console.warn',
32
+ info: 'console.info',
33
+ };
34
+ /**
35
+ * Build the `executeJavaScript` source that injects one diagnostic into the
36
+ * service host's console, prefixed `[dimina-kit]`. Args ride as a JSON string
37
+ * (same anti-injection shape as `buildForwardScript`) and the sourceURL is the
38
+ * SAME sentinel the render mirror uses, so this line is exempt from
39
+ * service-console's loop-safety skip too.
40
+ */
41
+ function buildDiagnosticScript(severity, message) {
42
+ const call = DIAGNOSTIC_CONSOLE_CALL[severity];
43
+ const argsJson = JSON.stringify([`[dimina-kit] ${message}`]);
44
+ return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});${call}(...a)}catch(_){}})()\n//# sourceURL=${RENDER_FORWARD_SOURCE_URL}`;
45
+ }
46
+ export function createConsoleForwarder(bridge, diagnostics) {
29
47
  const sinks = new Set();
30
48
  const registry = new DisposableRegistry();
49
+ // Diagnostics queued because no live service-host wc could be resolved yet,
50
+ // bucketed by the owning appSessionId; diagnostics with no appSessionId land
51
+ // in `pendingGlobal` instead. `notifyServiceHostReady` drains both into the
52
+ // session that just became ready.
53
+ const pendingBySession = new Map();
54
+ const pendingGlobal = [];
55
+ // Sessions whose service host has finished loading service.html (the caller
56
+ // signals this via `notifyServiceHostReady`). A resolvable wc alone is NOT
57
+ // readiness: the window exists (and is bound) before its spawn navigation
58
+ // lands, and anything injected into that pre-load document is wiped by the
59
+ // navigation — the diagnostic would silently vanish from the Console panel.
60
+ const readySessions = new Set();
61
+ function injectDiagnostic(wc, d) {
62
+ wc.executeJavaScript(buildDiagnosticScript(d.severity, d.message), true).catch(() => { });
63
+ }
64
+ /**
65
+ * Inject a diagnostic into its owning session's service-host console, or
66
+ * queue it until that host is READY (not merely constructed — see
67
+ * `readySessions`). Ownership is strict: a session-owned diagnostic NEVER
68
+ * falls back to the currently-active host — while `handleSpawn` is still
69
+ * running, the new session's host doesn't exist yet and the "active" host
70
+ * is the OUTGOING session's window, so a fallback would land the message in
71
+ * a console about to be destroyed instead of the session it explains. Only
72
+ * session-less diagnostics may use the active host. Never throws — a
73
+ * missing/destroyed/not-yet-ready host is a normal state, not an error.
74
+ */
75
+ function handleDiagnostic(d) {
76
+ if (d.appSessionId) {
77
+ const wc = readySessions.has(d.appSessionId) && bridge.getServiceWcForBridge
78
+ ? bridge.getServiceWcForBridge(d.appSessionId)
79
+ : null;
80
+ if (wc && !wc.isDestroyed()) {
81
+ injectDiagnostic(wc, d);
82
+ return;
83
+ }
84
+ const bucket = pendingBySession.get(d.appSessionId);
85
+ if (bucket)
86
+ bucket.push(d);
87
+ else
88
+ pendingBySession.set(d.appSessionId, [d]);
89
+ return;
90
+ }
91
+ const wc = bridge.getServiceWc();
92
+ if (wc && !wc.isDestroyed()) {
93
+ injectDiagnostic(wc, d);
94
+ return;
95
+ }
96
+ pendingGlobal.push(d);
97
+ }
98
+ if (diagnostics) {
99
+ // replay:true so diagnostics reported before this forwarder existed (early
100
+ // boot, buffered on the bus) are queued/injected here too.
101
+ registry.add(diagnostics.subscribe(handleDiagnostic, { replay: true }));
102
+ }
31
103
  /**
32
104
  * Render→service forward. Resolves the service host for the entry's owning
33
105
  * page (multi-app safe), falling back to the active app's service host when
@@ -79,8 +151,34 @@ export function createConsoleForwarder(bridge) {
79
151
  sinks.add(sink);
80
152
  return registry.add(toDisposable(() => { sinks.delete(sink); }));
81
153
  },
154
+ notifyServiceHostReady(appSessionId) {
155
+ // Readiness is sticky per session: from here on, session-owned
156
+ // diagnostics inject directly instead of queueing.
157
+ readySessions.add(appSessionId);
158
+ // Re-resolve fresh rather than trusting whatever `handleDiagnostic` saw at
159
+ // report time — the caller invokes this exactly when it knows the wc just
160
+ // became live, so that resolution wins even if an earlier snapshot was
161
+ // destroyed/missing.
162
+ const wc = bridge.getServiceWcForBridge ? bridge.getServiceWcForBridge(appSessionId) : null;
163
+ if (!wc)
164
+ return;
165
+ const sessionEntries = pendingBySession.get(appSessionId);
166
+ if (sessionEntries) {
167
+ for (const d of sessionEntries)
168
+ injectDiagnostic(wc, d);
169
+ pendingBySession.delete(appSessionId);
170
+ }
171
+ if (pendingGlobal.length) {
172
+ for (const d of pendingGlobal)
173
+ injectDiagnostic(wc, d);
174
+ pendingGlobal.length = 0;
175
+ }
176
+ },
82
177
  dispose() {
83
178
  sinks.clear();
179
+ pendingBySession.clear();
180
+ pendingGlobal.length = 0;
181
+ readySessions.clear();
84
182
  return registry.disposeAll();
85
183
  },
86
184
  };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Diagnostics bus: the single authoritative channel for developer-facing
3
+ * diagnostics synthesized by main (e.g. "page not found", "logic bundle
4
+ * unreachable") that do not originate from a real `console.*` call the
5
+ * embedded service-host DevTools' CDP capture could observe on its own.
6
+ *
7
+ * Main-synthesized diagnostics used to reach only `ctx.guestConsole.emit`,
8
+ * which fans out to automation subscribers but — for `source:'service'`
9
+ * entries — is never injected into the service host's own console (see
10
+ * `console-forward`'s render-only forward invariant), so they never appeared
11
+ * in the Console panel. This bus is the fix: `report()` is the one place a
12
+ * diagnostic is born, and `console-forward` subscribes to inject each one
13
+ * into the owning session's service-host console (queuing until that
14
+ * session's window is ready — see `notifyServiceHostReady`).
15
+ */
16
+ export type DiagnosticSeverity = 'error' | 'warn' | 'info';
17
+ export interface Diagnostic {
18
+ severity: DiagnosticSeverity;
19
+ code: string;
20
+ message: string;
21
+ appSessionId?: string;
22
+ ts: number;
23
+ }
24
+ export interface DiagnosticsBus {
25
+ /** Record one diagnostic: buffers it, mirrors it to the main-process console, and synchronously notifies every live subscriber. No-op after `dispose()`. */
26
+ report(d: {
27
+ severity: DiagnosticSeverity;
28
+ code: string;
29
+ message: string;
30
+ appSessionId?: string;
31
+ }): void;
32
+ /**
33
+ * Register a sink. With `replay` (default true) the sink is first called,
34
+ * in order, for every buffered diagnostic still held, then for every
35
+ * diagnostic reported from now on; `replay: false` skips the backlog and
36
+ * only delivers diagnostics reported after this call.
37
+ */
38
+ subscribe(sink: (d: Diagnostic) => void, opts?: {
39
+ replay?: boolean;
40
+ }): {
41
+ dispose(): void;
42
+ };
43
+ /** Clears subscribers and the buffer. `report()` becomes a no-op afterward (including the console mirror — dispose means fully off, not "log-only"). */
44
+ dispose(): void;
45
+ }
46
+ export declare function createDiagnosticsBus(opts?: {
47
+ bufferCap?: number;
48
+ }): DiagnosticsBus;
49
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,61 @@
1
+ const DEFAULT_BUFFER_CAP = 200;
2
+ const CONSOLE_METHOD = {
3
+ error: 'error',
4
+ warn: 'warn',
5
+ info: 'info',
6
+ };
7
+ export function createDiagnosticsBus(opts) {
8
+ const bufferCap = opts?.bufferCap && opts.bufferCap > 0 ? opts.bufferCap : DEFAULT_BUFFER_CAP;
9
+ const buffer = [];
10
+ const sinks = new Set();
11
+ let disposed = false;
12
+ return {
13
+ report(d) {
14
+ if (disposed)
15
+ return;
16
+ const entry = { ...d, ts: Date.now() };
17
+ buffer.push(entry);
18
+ // Ring buffer: drop the oldest once over cap. Bounded at `bufferCap` so a
19
+ // burst of synthesized diagnostics can never grow this without limit.
20
+ if (buffer.length > bufferCap)
21
+ buffer.shift();
22
+ // The one guaranteed-visible sink: unlike Console-panel injection (which
23
+ // needs a live, ready service-host wc), the main-process terminal is
24
+ // always there.
25
+ console[CONSOLE_METHOD[entry.severity]](`[dimina-kit:${entry.code}] ${entry.message}`);
26
+ for (const sink of sinks) {
27
+ try {
28
+ sink(entry);
29
+ }
30
+ catch { /* a sink must never break the others */ }
31
+ }
32
+ },
33
+ subscribe(sink, subOpts) {
34
+ const replay = subOpts?.replay ?? true;
35
+ if (replay) {
36
+ for (const entry of buffer) {
37
+ try {
38
+ sink(entry);
39
+ }
40
+ catch { /* isolate a replay throw same as a live one */ }
41
+ }
42
+ }
43
+ sinks.add(sink);
44
+ let released = false;
45
+ return {
46
+ dispose() {
47
+ if (released)
48
+ return;
49
+ released = true;
50
+ sinks.delete(sink);
51
+ },
52
+ };
53
+ },
54
+ dispose() {
55
+ disposed = true;
56
+ sinks.clear();
57
+ buffer.length = 0;
58
+ },
59
+ };
60
+ }
61
+ //# sourceMappingURL=index.js.map