@dimina-kit/devtools 0.4.0-dev.20260718143821 → 0.4.0-dev.20260728063215

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 (68) hide show
  1. package/dist/main/app/app.js +67 -1
  2. package/dist/main/app/lifecycle.js +9 -0
  3. package/dist/main/index.bundle.js +928 -162
  4. package/dist/main/ipc/bridge-router.d.ts +36 -0
  5. package/dist/main/ipc/bridge-router.js +56 -0
  6. package/dist/main/ipc/index.d.ts +1 -0
  7. package/dist/main/ipc/index.js +1 -0
  8. package/dist/main/ipc/internal-devtools.d.ts +15 -0
  9. package/dist/main/ipc/internal-devtools.js +9 -0
  10. package/dist/main/services/compile-standby.js +4 -0
  11. package/dist/main/services/console-forward/cdp-inject.d.ts +18 -0
  12. package/dist/main/services/console-forward/cdp-inject.js +30 -0
  13. package/dist/main/services/console-forward/global-console-mirror.d.ts +33 -0
  14. package/dist/main/services/console-forward/global-console-mirror.js +71 -0
  15. package/dist/main/services/console-forward/global-diagnostics-mirror.d.ts +33 -0
  16. package/dist/main/services/console-forward/global-diagnostics-mirror.js +73 -0
  17. package/dist/main/services/console-forward/index.d.ts +15 -1
  18. package/dist/main/services/console-forward/index.js +42 -2
  19. package/dist/main/services/console-forward/internal-log.d.ts +11 -0
  20. package/dist/main/services/console-forward/internal-log.js +23 -0
  21. package/dist/main/services/console-forward/open-gated-relay.d.ts +67 -0
  22. package/dist/main/services/console-forward/open-gated-relay.js +55 -0
  23. package/dist/main/services/diagnostics/index.d.ts +10 -0
  24. package/dist/main/services/elements-forward/index.js +14 -96
  25. package/dist/main/services/network-forward/frontend-dispatch.d.ts +64 -0
  26. package/dist/main/services/network-forward/frontend-dispatch.js +113 -8
  27. package/dist/main/services/network-forward/global-body-gate.d.ts +46 -0
  28. package/dist/main/services/network-forward/global-body-gate.js +93 -0
  29. package/dist/main/services/network-forward/global-mirror-test-fixtures.d.ts +48 -0
  30. package/dist/main/services/network-forward/global-mirror-test-fixtures.js +96 -0
  31. package/dist/main/services/network-forward/index.d.ts +31 -0
  32. package/dist/main/services/network-forward/index.js +256 -8
  33. package/dist/main/services/network-forward/user-facing.d.ts +21 -0
  34. package/dist/main/services/network-forward/user-facing.js +50 -0
  35. package/dist/main/services/views/console-filter.d.ts +145 -0
  36. package/dist/main/services/views/console-filter.js +233 -0
  37. package/dist/main/services/views/frontend-bootstrap-gate.d.ts +59 -0
  38. package/dist/main/services/views/frontend-bootstrap-gate.js +117 -0
  39. package/dist/main/services/views/native-simulator-devtools-host.js +138 -30
  40. package/dist/main/services/views/view-manager-devtools-host-test-fixtures.d.ts +76 -0
  41. package/dist/main/services/views/view-manager-devtools-host-test-fixtures.js +182 -0
  42. package/dist/main/services/workbench-context.d.ts +10 -0
  43. package/dist/main/utils/theme.d.ts +10 -6
  44. package/dist/main/utils/theme.js +10 -6
  45. package/dist/main/windows/internal-devtools-window/index.d.ts +66 -0
  46. package/dist/main/windows/internal-devtools-window/index.js +160 -0
  47. package/dist/native-host/render/render.js +89 -73
  48. package/dist/native-host/service/service.js +2 -2
  49. package/dist/preload/windows/host-toolbar-runtime.cjs.map +1 -1
  50. package/dist/preload/windows/simulator.cjs.map +1 -1
  51. package/dist/renderer/assets/index-M5NDq_vi.js +49 -0
  52. package/dist/renderer/assets/{input-c2OcrrZy.js → input-dsgeTmVX.js} +2 -2
  53. package/dist/renderer/assets/ipc-transport-BgFdT9bT.js +9 -0
  54. package/dist/renderer/assets/{popover-kv2qspWz.js → popover-Disfu1cx.js} +2 -2
  55. package/dist/renderer/assets/{select-DKqzMtTh.js → select-DAw1hzkl.js} +2 -2
  56. package/dist/renderer/assets/{settings-dJNKTGr8.js → settings-M_8GFW2M.js} +2 -2
  57. package/dist/renderer/assets/settings-api-DFR5eDl_.js +2 -0
  58. package/dist/renderer/assets/{workbenchSettings-DQ_VP5-W.js → workbenchSettings-CxdXeoOu.js} +2 -2
  59. package/dist/renderer/entries/main/index.html +5 -5
  60. package/dist/renderer/entries/popover/index.html +4 -4
  61. package/dist/renderer/entries/settings/index.html +4 -4
  62. package/dist/renderer/entries/workbench-settings/index.html +3 -3
  63. package/dist/shared/ipc-channels.d.ts +3 -0
  64. package/dist/shared/ipc-channels.js +6 -0
  65. package/package.json +8 -8
  66. package/dist/renderer/assets/index-DfDIvgvK.js +0 -49
  67. package/dist/renderer/assets/ipc-transport-CuDJ07aE.js +0 -9
  68. package/dist/renderer/assets/settings-api-qpXKDRXt.js +0 -2
@@ -33,6 +33,33 @@ export interface RenderEvent {
33
33
  * Lets the current-page panel push the route without a separate lookup. */
34
34
  pagePath?: string;
35
35
  }
36
+ /**
37
+ * Fires once per app session, the moment its hidden SERVICE HOST window's
38
+ * `service.html` document has `did-finish-load`'d — i.e. `serviceWcId` is
39
+ * GUARANTEED resolvable via `webContents.fromId(serviceWcId)` right now. This
40
+ * is document readiness, NOT mini-app runtime readiness: `injectLogicBundle`
41
+ * and the service's own `serviceResourceLoaded` handshake both happen AFTER
42
+ * this fires (see `bootServiceHost`) — a consumer that needs "the mini-app
43
+ * has actually booted" must not treat this as that signal.
44
+ *
45
+ * Exists so a consumer that needs to act on the service host wc THE MOMENT it
46
+ * becomes resolvable (e.g. attaching the right-panel DevTools front-end) has
47
+ * a real event to react to instead of polling `getServiceWc(appId)` on a
48
+ * fixed retry budget and silently giving up if that budget is exhausted
49
+ * before the wc resolves under load — the bug this event was added to fix
50
+ * (see `native-simulator-devtools-host.ts`'s `onServiceHostReady` subscriber).
51
+ *
52
+ * `appId`-keyed resolution (`getServiceWc(appId)`) is NOT equivalent to this
53
+ * event during same-app respawn overlap — `getServiceWc` returns the MOST
54
+ * RECENT live session for that appId, which may not be the specific session
55
+ * that just fired this event. Consumers that must target the EXACT session
56
+ * should resolve `serviceWcId` directly rather than re-resolving by `appId`.
57
+ */
58
+ export interface ServiceHostReadyEvent {
59
+ appId: string;
60
+ appSessionId: string;
61
+ serviceWcId: number;
62
+ }
36
63
  /**
37
64
  * Point-in-time counts of every resource class RouterState owns. Leak coverage
38
65
  * (unit + e2e) asserts EXACT equality of this ledger around a churn cycle —
@@ -75,8 +102,17 @@ export interface BridgeRouterHandle {
75
102
  * before the first PAGE_STACK signal. Optional: only the native-host bridge
76
103
  * provides it (the default path derives the stack from the simulator URL). */
77
104
  getPageStack?(appId?: string): PageStackEntry[] | null;
105
+ /** The current (or named) app session's resource-server baseUrl (serves
106
+ * that session's framework js/css), or null when no matching session
107
+ * exists. Per-session, never a global singleton — see AppSession.resourceBaseUrl. */
108
+ getResourceBaseUrl?(appId?: string): string | null;
78
109
  /** Subscribe to render-side activity (domReady / active-page change). */
79
110
  onRenderEvent(listener: (event: RenderEvent) => void): () => void;
111
+ /** Subscribe to a service host's `did-finish-load` readiness (see
112
+ * `ServiceHostReadyEvent`'s doc comment). A subscriber registering after
113
+ * the still-live session's event already fired gets a missed-signal
114
+ * catch-up — it never needs to poll. */
115
+ onServiceHostReady(listener: (event: ServiceHostReadyEvent) => void): () => void;
80
116
  /** The currently-selected device (renderer toolbar), or null pre-selection. */
81
117
  getDevice(): NativeDeviceInfo | null;
82
118
  /** Cache the selected device + push DEVICE_CHANGE to the live simulator WC(s). */
@@ -166,6 +166,8 @@ export function installBridgeRouter(ctx) {
166
166
  pendingApiCalls: new Map(),
167
167
  pool: null,
168
168
  emitRenderEvent: () => { },
169
+ emitServiceHostReady: () => { },
170
+ lastServiceHostReady: null,
169
171
  connections: ctx.connections,
170
172
  debugTap: createDebugTap({ enabled: resolveDebugTapEnabled() }),
171
173
  appLifecycle: createAppLifecycleController(),
@@ -237,6 +239,22 @@ export function installBridgeRouter(ctx) {
237
239
  }
238
240
  };
239
241
  state.emitRenderEvent = emitRenderEvent;
242
+ // Subscribers to `ServiceHostReadyEvent` (see its doc comment). Separate
243
+ // from `renderEventListeners`: this is session-scoped (appSessionId +
244
+ // serviceWcId), not page/bridgeId-scoped like `RenderEvent`.
245
+ const serviceHostReadyListeners = new Set();
246
+ const emitServiceHostReady = (event) => {
247
+ state.lastServiceHostReady = event;
248
+ for (const listener of serviceHostReadyListeners) {
249
+ try {
250
+ listener(event);
251
+ }
252
+ catch (error) {
253
+ console.warn('[bridge-router] service-host-ready listener threw:', error);
254
+ }
255
+ }
256
+ };
257
+ state.emitServiceHostReady = emitServiceHostReady;
240
258
  // Expose a thin accessor over RouterState so other main services (storage,
241
259
  // automation, appdata) can resolve live render/service WebContents without
242
260
  // owning router state. Getters resolve fresh — the pre-warm pool can swap
@@ -269,6 +287,10 @@ export function installBridgeRouter(ctx) {
269
287
  const ap = resolveCurrentApp(state, ctx, appId);
270
288
  return ap?.pageStack ?? null;
271
289
  },
290
+ getResourceBaseUrl: (appId) => {
291
+ const ap = resolveCurrentApp(state, ctx, appId);
292
+ return ap?.resourceBaseUrl ?? null;
293
+ },
272
294
  getActiveRenderWc: (appId) => {
273
295
  const ap = resolveCurrentApp(state, ctx, appId);
274
296
  if (!ap)
@@ -280,6 +302,35 @@ export function installBridgeRouter(ctx) {
280
302
  renderEventListeners.add(listener);
281
303
  return () => renderEventListeners.delete(listener);
282
304
  },
305
+ onServiceHostReady: (listener) => {
306
+ serviceHostReadyListeners.add(listener);
307
+ // Missed-signal catch-up (mirrors host-toolbar-port-channel.ts's
308
+ // `onReady`): a subscriber registering AFTER the session it cares
309
+ // about already booted must not wait forever for an event that
310
+ // already happened. Scheduled on a microtask (never synchronous —
311
+ // matches the same-tick-dispose/re-attach safety the host-toolbar
312
+ // pattern documents) and RE-VALIDATED at fire time: the session must
313
+ // still be live and still bound to the SAME serviceWcId the event
314
+ // captured, so a session that respawned/closed between registration
315
+ // and the microtask can't resurrect a stale attach.
316
+ const candidate = state.lastServiceHostReady;
317
+ if (candidate) {
318
+ queueMicrotask(() => {
319
+ if (!serviceHostReadyListeners.has(listener))
320
+ return;
321
+ const ap = state.appSessions.get(candidate.appSessionId);
322
+ if (!ap || ap.serviceWc.isDestroyed() || ap.serviceWc.id !== candidate.serviceWcId)
323
+ return;
324
+ try {
325
+ listener(candidate);
326
+ }
327
+ catch (error) {
328
+ console.warn('[bridge-router] service-host-ready catch-up listener threw:', error);
329
+ }
330
+ });
331
+ }
332
+ return () => serviceHostReadyListeners.delete(listener);
333
+ },
283
334
  getDevice: () => currentDevice,
284
335
  setDevice: (device) => {
285
336
  currentDevice = device;
@@ -970,6 +1021,11 @@ async function bootServiceHost(state, ap, ctx) {
970
1021
  // any diagnostic queued for this session (or the global bucket) into its now
971
1022
  // resolvable console. Safe to call even when nothing is queued.
972
1023
  ctx.consoleForwarder?.notifyServiceHostReady?.(ap.appSessionId);
1024
+ // Same authoritative moment, exposed as a general subscribable event (see
1025
+ // `ServiceHostReadyEvent`'s doc comment) — other main-process consumers
1026
+ // (the right-panel DevTools attach) need this exact signal too and must
1027
+ // not poll `getServiceWc` on a fixed retry budget for it.
1028
+ state.emitServiceHostReady({ appId: ap.appId, appSessionId: ap.appSessionId, serviceWcId: ap.serviceWc.id });
973
1029
  ap.logicInjected = await injectLogicBundle(ap);
974
1030
  if (!ap.logicInjected) {
975
1031
  // The compiled logic.js never executed, so `modDefine` registered nothing.
@@ -4,5 +4,6 @@ export { registerPopoverIpc, popoverModule } from './popover.js';
4
4
  export { registerProjectsIpc, projectsModule } from './projects.js';
5
5
  export { registerSessionIpc, sessionModule } from './session.js';
6
6
  export { registerSettingsIpc, settingsModule } from './settings.js';
7
+ export { registerInternalDevtoolsIpc } from './internal-devtools.js';
7
8
  export { simulatorModule } from './simulator-module.js';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -4,5 +4,6 @@ export { registerPopoverIpc, popoverModule } from './popover.js';
4
4
  export { registerProjectsIpc, projectsModule } from './projects.js';
5
5
  export { registerSessionIpc, sessionModule } from './session.js';
6
6
  export { registerSettingsIpc, settingsModule } from './settings.js';
7
+ export { registerInternalDevtoolsIpc } from './internal-devtools.js';
7
8
  export { simulatorModule } from './simulator-module.js';
8
9
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,15 @@
1
+ import type { Disposable } from '@dimina-kit/electron-deck/main';
2
+ import { type SenderPolicy } from '../utils/ipc-registry.js';
3
+ import type { InternalDevtoolsWindow } from '../windows/internal-devtools-window/index.js';
4
+ /**
5
+ * Standalone internal (app-wide) DevTools debug window. The simulator
6
+ * toolbar's "debug" button opens (or focuses) it via the window controller —
7
+ * a module-local narrow deps interface, not `WorkbenchContext`, per the
8
+ * shrink-only WorkbenchContext-import gate (see eslint.config.js).
9
+ */
10
+ export interface InternalDevtoolsIpcDeps {
11
+ internalDevtoolsWindow?: InternalDevtoolsWindow;
12
+ senderPolicy: SenderPolicy;
13
+ }
14
+ export declare function registerInternalDevtoolsIpc(ctx: InternalDevtoolsIpcDeps): Disposable;
15
+ //# sourceMappingURL=internal-devtools.d.ts.map
@@ -0,0 +1,9 @@
1
+ import { InternalDevtoolsChannel } from '../../shared/ipc-channels.js';
2
+ import { IpcRegistry } from '../utils/ipc-registry.js';
3
+ export function registerInternalDevtoolsIpc(ctx) {
4
+ return new IpcRegistry(ctx.senderPolicy)
5
+ .handle(InternalDevtoolsChannel.Open, () => {
6
+ ctx.internalDevtoolsWindow?.open();
7
+ });
8
+ }
9
+ //# sourceMappingURL=internal-devtools.js.map
@@ -21,6 +21,10 @@ export function setupCompileWorkerStandby(ctx, deps = {}) {
21
21
  severity: EVENT_SEVERITY[ev.type] ?? 'info',
22
22
  code: 'compile-standby',
23
23
  message: detail,
24
+ // Devtools-tooling-only state, unrelated to the inspected mini-program
25
+ // — must never reach the right-panel (user-facing) Console panel; see
26
+ // console-forward/index.ts's audience gate and global-diagnostics-mirror.ts.
27
+ audience: 'internal',
24
28
  });
25
29
  }
26
30
  catch {
@@ -0,0 +1,18 @@
1
+ import type { WebContents } from 'electron';
2
+ import type { CdpSessionBroker } from '../cdp-session/index.js';
3
+ /**
4
+ * Shared CDP injection primitive for the global mirrors (console +
5
+ * diagnostics): evaluate `script` in `target`'s realm over the broker's
6
+ * debugger session — never `target.executeJavaScript`, whose internal RPC
7
+ * hangs forever on a wc that is simultaneously the inspected side of
8
+ * `setDevToolsWebContents` AND attached by an external CDP client (see
9
+ * global-console-mirror.ts for the full mechanism and the real-machine
10
+ * repro).
11
+ *
12
+ * Resolves true only on a clean evaluation. `exceptionDetails`, a rejected
13
+ * send, and an unacquirable session (destroyed wc / debugger exclusively
14
+ * held elsewhere) all report false — createOpenGatedRelay then leaves the
15
+ * entry retryable instead of marking it done.
16
+ */
17
+ export declare function injectViaCdp(broker: CdpSessionBroker, target: WebContents, script: string, warnTag: string): boolean | Promise<boolean>;
18
+ //# sourceMappingURL=cdp-inject.d.ts.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared CDP injection primitive for the global mirrors (console +
3
+ * diagnostics): evaluate `script` in `target`'s realm over the broker's
4
+ * debugger session — never `target.executeJavaScript`, whose internal RPC
5
+ * hangs forever on a wc that is simultaneously the inspected side of
6
+ * `setDevToolsWebContents` AND attached by an external CDP client (see
7
+ * global-console-mirror.ts for the full mechanism and the real-machine
8
+ * repro).
9
+ *
10
+ * Resolves true only on a clean evaluation. `exceptionDetails`, a rejected
11
+ * send, and an unacquirable session (destroyed wc / debugger exclusively
12
+ * held elsewhere) all report false — createOpenGatedRelay then leaves the
13
+ * entry retryable instead of marking it done.
14
+ */
15
+ export function injectViaCdp(broker, target, script, warnTag) {
16
+ const lease = broker.acquire(target);
17
+ if (!lease)
18
+ return false;
19
+ return lease.send('Runtime.evaluate', { expression: script }).then((result) => {
20
+ if (result?.exceptionDetails) {
21
+ console.warn(`[${warnTag}] Runtime.evaluate reported an exception, will retry on next reopen`);
22
+ return false;
23
+ }
24
+ return true;
25
+ }, (err) => {
26
+ console.warn(`[${warnTag}] injection failed, will retry on next reopen:`, err instanceof Error ? err.message : String(err));
27
+ return false;
28
+ });
29
+ }
30
+ //# sourceMappingURL=cdp-inject.js.map
@@ -0,0 +1,33 @@
1
+ import type { WebContents } from 'electron';
2
+ import type { Disposable } from '@dimina-kit/electron-deck/main';
3
+ import type { ConsoleForwarder } from './index.js';
4
+ import type { CdpSessionBroker } from '../cdp-session/index.js';
5
+ /**
6
+ * Mirror EVERY guest console entry (both service + render layers,
7
+ * UNFILTERED — no isInternalLogMessage gating, see that module's doc for
8
+ * why this mirror deliberately skips it) into `target`'s own console, but
9
+ * ONLY while the standalone internal DevTools window is open.
10
+ *
11
+ * The subscription lifecycle is gated by `onHostChanged` (non-null hostWc =
12
+ * window just opened/rebuilt, null = just closed) rather than subscribing
13
+ * once for the whole app lifetime: each open re-subscribes to `forwarder`
14
+ * with `{replay:true}`, draining its CURRENT history buffer into `target`
15
+ * before continuing live, and each close disposes that subscription. This
16
+ * is what makes opening (or reopening) the window always show recent
17
+ * history — subscribing once at construction time (this module's earlier
18
+ * design) captured the replay burst before any window could possibly be
19
+ * open to receive it, silently losing it forever. `target` is the fixed
20
+ * INSPECTED side (mainWindow.webContents, per
21
+ * internal-devtools-window.ts's setDevToolsWebContents relationship) —
22
+ * never the hostWc the signal carries; injecting into the front-end host
23
+ * page would only ever reach that page's own invisible console.
24
+ *
25
+ * Reopen-dedup is handled by `createOpenGatedRelay` (real-machine e2e
26
+ * confirmed a naive replay-on-every-open double-injects everything already
27
+ * shown once, since Chromium's own console storage survives a DevTools
28
+ * close — see that module's doc comment).
29
+ */
30
+ export declare function createGlobalConsoleMirror(forwarder: Pick<ConsoleForwarder, 'subscribe'>, target: WebContents, onHostChanged: (handler: (hostWc: WebContents | null) => void) => () => void, opts: {
31
+ broker: CdpSessionBroker;
32
+ }): Disposable;
33
+ //# sourceMappingURL=global-console-mirror.d.ts.map
@@ -0,0 +1,71 @@
1
+ import { isFrontendSettled } from '../views/inject-when-ready.js';
2
+ import { createOpenGatedRelay } from './open-gated-relay.js';
3
+ import { injectViaCdp } from './cdp-inject.js';
4
+ /** Console levels safe to re-emit; anything else maps to 'log' — mirrors
5
+ * console-forward/index.ts's FORWARDABLE_LEVELS. */
6
+ const FORWARDABLE_LEVELS = new Set(['log', 'warn', 'error', 'info', 'debug']);
7
+ /**
8
+ * Build the `executeJavaScript` source that re-emits one guest console entry
9
+ * into the target wc's own console, tagged with its source layer. Args ride
10
+ * as a JSON string and are re-parsed target-side — data, never code — same
11
+ * discipline as console-forward/index.ts's buildForwardScript.
12
+ */
13
+ function buildMirrorScript(entry) {
14
+ const method = FORWARDABLE_LEVELS.has(entry.level ?? '') ? entry.level : 'log';
15
+ const tag = entry.source === 'render' ? '[render]' : '[service]';
16
+ const argsJson = JSON.stringify(entry.args ?? []);
17
+ return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});console[${JSON.stringify(method)}](${JSON.stringify(tag)},...a)}catch(_){}})()`;
18
+ }
19
+ /**
20
+ * Mirror EVERY guest console entry (both service + render layers,
21
+ * UNFILTERED — no isInternalLogMessage gating, see that module's doc for
22
+ * why this mirror deliberately skips it) into `target`'s own console, but
23
+ * ONLY while the standalone internal DevTools window is open.
24
+ *
25
+ * The subscription lifecycle is gated by `onHostChanged` (non-null hostWc =
26
+ * window just opened/rebuilt, null = just closed) rather than subscribing
27
+ * once for the whole app lifetime: each open re-subscribes to `forwarder`
28
+ * with `{replay:true}`, draining its CURRENT history buffer into `target`
29
+ * before continuing live, and each close disposes that subscription. This
30
+ * is what makes opening (or reopening) the window always show recent
31
+ * history — subscribing once at construction time (this module's earlier
32
+ * design) captured the replay burst before any window could possibly be
33
+ * open to receive it, silently losing it forever. `target` is the fixed
34
+ * INSPECTED side (mainWindow.webContents, per
35
+ * internal-devtools-window.ts's setDevToolsWebContents relationship) —
36
+ * never the hostWc the signal carries; injecting into the front-end host
37
+ * page would only ever reach that page's own invisible console.
38
+ *
39
+ * Reopen-dedup is handled by `createOpenGatedRelay` (real-machine e2e
40
+ * confirmed a naive replay-on-every-open double-injects everything already
41
+ * shown once, since Chromium's own console storage survives a DevTools
42
+ * close — see that module's doc comment).
43
+ */
44
+ export function createGlobalConsoleMirror(forwarder, target, onHostChanged, opts) {
45
+ function inject(entry) {
46
+ if (target.isDestroyed())
47
+ return false;
48
+ if (!isFrontendSettled(target))
49
+ return false;
50
+ let script;
51
+ try {
52
+ script = buildMirrorScript(entry);
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ // Transport is CDP `Runtime.evaluate` over the shared debugger-session
58
+ // broker — NEVER `target.executeJavaScript`: Electron's internal
59
+ // ExecuteJavaScript RPC hangs FOREVER on a wc that is simultaneously the
60
+ // inspected side of `setDevToolsWebContents` (the standalone debug
61
+ // window) AND attached by an external CDP client (remote-debugging-port,
62
+ // which bootstrap's setupCdpPort enables by default when unpackaged and
63
+ // whenever cdp/mcp settings are on) — real-machine repro. The debugger
64
+ // channel is unaffected. Report the real outcome (not fire-and-forget) —
65
+ // createOpenGatedRelay only marks an entry permanently "injected" on a
66
+ // confirmed `true`, and its inject timeout bounds any residual hang.
67
+ return injectViaCdp(opts.broker, target, script, 'global-console-mirror');
68
+ }
69
+ return createOpenGatedRelay(onHostChanged, (sink, opts2) => forwarder.subscribe(sink, opts2), inject);
70
+ }
71
+ //# sourceMappingURL=global-console-mirror.js.map
@@ -0,0 +1,33 @@
1
+ import type { WebContents } from 'electron';
2
+ import type { Disposable } from '@dimina-kit/electron-deck/main';
3
+ import type { DiagnosticsBus } from '../diagnostics/index.js';
4
+ import type { CdpSessionBroker } from '../cdp-session/index.js';
5
+ /**
6
+ * Mirror EVERY diagnostic (both `audience:'user'` and `audience:'internal'`,
7
+ * UNFILTERED — the same "see everything" contract `createGlobalConsoleMirror`
8
+ * applies to guest console entries) into `target`'s own console, but ONLY
9
+ * while the standalone internal DevTools window is open.
10
+ *
11
+ * The subscription lifecycle is gated by `onHostChanged` (non-null hostWc =
12
+ * window just opened/rebuilt, null = just closed) rather than subscribing
13
+ * once for the whole app lifetime: each open re-subscribes to `diagnostics`
14
+ * with `{replay:true}`, draining its CURRENT buffer into `target` before
15
+ * continuing live, and each close disposes that subscription. This is what
16
+ * makes opening (or reopening) the window always show history — subscribing
17
+ * once at construction time (this module's earlier design) captured the
18
+ * replay burst before any window could possibly be open to receive it
19
+ * (construction happens at app boot; the window is user-opened, always
20
+ * later), silently losing it forever — the exact reported bug (e.g. the
21
+ * earliest compile-standby events at boot never showing up). `target` is the
22
+ * fixed INSPECTED side (mainWindow.webContents) — never the hostWc the
23
+ * signal carries.
24
+ *
25
+ * Reopen-dedup is handled by `createOpenGatedRelay` (real-machine e2e
26
+ * confirmed a naive replay-on-every-open double-injects everything already
27
+ * shown once, since Chromium's own console storage survives a DevTools
28
+ * close — see that module's doc comment).
29
+ */
30
+ export declare function createGlobalDiagnosticsMirror(diagnostics: Pick<DiagnosticsBus, 'subscribe'>, target: WebContents, onHostChanged: (handler: (hostWc: WebContents | null) => void) => () => void, opts: {
31
+ broker: CdpSessionBroker;
32
+ }): Disposable;
33
+ //# sourceMappingURL=global-diagnostics-mirror.d.ts.map
@@ -0,0 +1,73 @@
1
+ import { isFrontendSettled } from '../views/inject-when-ready.js';
2
+ import { createOpenGatedRelay } from './open-gated-relay.js';
3
+ import { injectViaCdp } from './cdp-inject.js';
4
+ /** `Diagnostic.severity` → the literal `console.<method>` call — same mapping
5
+ * console-forward/index.ts's DIAGNOSTIC_CONSOLE_CALL uses (kept local since
6
+ * that constant isn't exported; both must stay in sync with `Diagnostic`). */
7
+ const DIAGNOSTIC_CONSOLE_CALL = {
8
+ error: 'console.error',
9
+ warn: 'console.warn',
10
+ info: 'console.info',
11
+ };
12
+ /**
13
+ * Build the `executeJavaScript` source that injects one diagnostic into the
14
+ * target wc's own console, prefixed `[dimina-kit] ` — same shape as
15
+ * console-forward/index.ts's `buildDiagnosticScript`, minus that function's
16
+ * `RENDER_FORWARD_SOURCE_URL` loop-safety sentinel: that sentinel exists to
17
+ * stop the service-host's OWN CDP capture from re-broadcasting the injected
18
+ * line, and nothing here re-captures `mainWindow.webContents`'s console.
19
+ */
20
+ function buildMirrorScript(severity, message) {
21
+ const call = DIAGNOSTIC_CONSOLE_CALL[severity];
22
+ const argsJson = JSON.stringify([`[dimina-kit] ${message}`]);
23
+ return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});${call}(...a)}catch(_){}})()`;
24
+ }
25
+ /**
26
+ * Mirror EVERY diagnostic (both `audience:'user'` and `audience:'internal'`,
27
+ * UNFILTERED — the same "see everything" contract `createGlobalConsoleMirror`
28
+ * applies to guest console entries) into `target`'s own console, but ONLY
29
+ * while the standalone internal DevTools window is open.
30
+ *
31
+ * The subscription lifecycle is gated by `onHostChanged` (non-null hostWc =
32
+ * window just opened/rebuilt, null = just closed) rather than subscribing
33
+ * once for the whole app lifetime: each open re-subscribes to `diagnostics`
34
+ * with `{replay:true}`, draining its CURRENT buffer into `target` before
35
+ * continuing live, and each close disposes that subscription. This is what
36
+ * makes opening (or reopening) the window always show history — subscribing
37
+ * once at construction time (this module's earlier design) captured the
38
+ * replay burst before any window could possibly be open to receive it
39
+ * (construction happens at app boot; the window is user-opened, always
40
+ * later), silently losing it forever — the exact reported bug (e.g. the
41
+ * earliest compile-standby events at boot never showing up). `target` is the
42
+ * fixed INSPECTED side (mainWindow.webContents) — never the hostWc the
43
+ * signal carries.
44
+ *
45
+ * Reopen-dedup is handled by `createOpenGatedRelay` (real-machine e2e
46
+ * confirmed a naive replay-on-every-open double-injects everything already
47
+ * shown once, since Chromium's own console storage survives a DevTools
48
+ * close — see that module's doc comment).
49
+ */
50
+ export function createGlobalDiagnosticsMirror(diagnostics, target, onHostChanged, opts) {
51
+ function inject(d) {
52
+ if (target.isDestroyed())
53
+ return false;
54
+ if (!isFrontendSettled(target))
55
+ return false;
56
+ let script;
57
+ try {
58
+ script = buildMirrorScript(d.severity, d.message);
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ // Transport is CDP `Runtime.evaluate` over the shared debugger-session
64
+ // broker — never `target.executeJavaScript`, whose internal RPC hangs
65
+ // forever under the setDevToolsWebContents + external-CDP-client double
66
+ // attach (see cdp-inject.ts / global-console-mirror.ts; same target,
67
+ // same failure). Report the real outcome — createOpenGatedRelay only
68
+ // marks an entry permanently "injected" on a confirmed `true`.
69
+ return injectViaCdp(opts.broker, target, script, 'global-diagnostics-mirror');
70
+ }
71
+ return createOpenGatedRelay(onHostChanged, (sink, opts2) => diagnostics.subscribe(sink, opts2), inject);
72
+ }
73
+ //# sourceMappingURL=global-diagnostics-mirror.js.map
@@ -30,8 +30,22 @@ export interface ConsoleForwarder extends Disposable {
30
30
  * Register an external sink (e.g. automation WS broadcast). Returns a
31
31
  * Disposable that unregisters it. Sinks see EVERY entry (both layers) — the
32
32
  * render→service forward is internal and not exposed as a sink.
33
+ *
34
+ * A bounded history buffer (200 entries, oldest dropped first — mirrors
35
+ * `DiagnosticsBus`'s `DEFAULT_BUFFER_CAP` ring-buffer style) records every
36
+ * entry regardless of whether anyone is subscribed. `opts.replay` (default
37
+ * `false`, UNLIKE `DiagnosticsBus` whose default is `true`) opts a new
38
+ * subscriber into draining that buffer, in order, before receiving live
39
+ * entries. Default stays non-replaying because the existing automation
40
+ * subscriber (`services/automation/index.ts`) wants only entries from the
41
+ * moment it subscribes — a history dump on an existing WS connection would
42
+ * be a surprise duplicate broadcast. `{replay:true}` exists for the global
43
+ * console mirror, whose whole point is to catch up a standalone debug
44
+ * window opened well after boot.
33
45
  */
34
- subscribe(sink: ConsoleSink): Disposable;
46
+ subscribe(sink: ConsoleSink, opts?: {
47
+ replay?: boolean;
48
+ }): Disposable;
35
49
  /**
36
50
  * Flush any diagnostic queued for `appSessionId` (plus the global,
37
51
  * session-less queue) into that session's now-ready service-host wc.
@@ -1,5 +1,6 @@
1
1
  import { DisposableRegistry, toDisposable } from '@dimina-kit/electron-deck/main';
2
2
  import { RENDER_FORWARD_SOURCE_URL } from '../service-console/console-api.js';
3
+ import { isInternalLogMessage } from './internal-log.js';
3
4
  /** Console levels we re-emit into the service host. Anything else maps to 'log'. */
4
5
  const FORWARDABLE_LEVELS = new Set(['log', 'warn', 'error', 'info', 'debug']);
5
6
  /**
@@ -46,6 +47,12 @@ function buildDiagnosticScript(severity, message) {
46
47
  export function createConsoleForwarder(bridge, diagnostics) {
47
48
  const sinks = new Set();
48
49
  const registry = new DisposableRegistry();
50
+ // Bounded history ring buffer backing `subscribe(sink, {replay:true})` —
51
+ // see the interface doc comment above. Every entry is recorded regardless
52
+ // of whether anyone is subscribed, so a replaying subscriber that arrives
53
+ // long after boot (the standalone global debug window) still catches up.
54
+ const CONSOLE_BUFFER_CAP = 200;
55
+ const buffer = [];
49
56
  // Diagnostics queued because no live service-host wc could be resolved yet,
50
57
  // bucketed by the owning appSessionId; diagnostics with no appSessionId land
51
58
  // in `pendingGlobal` instead. `notifyServiceHostReady` drains both into the
@@ -73,6 +80,16 @@ export function createConsoleForwarder(bridge, diagnostics) {
73
80
  * missing/destroyed/not-yet-ready host is a normal state, not an error.
74
81
  */
75
82
  function handleDiagnostic(d) {
83
+ // Internal-audience diagnostics (e.g. compile-standby's warm-pool
84
+ // lifecycle) are devtools-tooling-only state — never inject them into a
85
+ // project's service host (the right-panel CDP is natively attached
86
+ // there), and never queue them either: queuing exists so a diagnostic
87
+ // survives until ITS session's host becomes resolvable, which is
88
+ // meaningless for something that must never reach any service host.
89
+ // (See global-diagnostics-mirror.ts for where these actually surface —
90
+ // the independent, unfiltered "debug the whole app" panel.)
91
+ if (d.audience === 'internal')
92
+ return;
76
93
  if (d.appSessionId) {
77
94
  const wc = readySessions.has(d.appSessionId) && bridge.getServiceWcForBridge
78
95
  ? bridge.getServiceWcForBridge(d.appSessionId)
@@ -105,9 +122,20 @@ export function createConsoleForwarder(bridge, diagnostics) {
105
122
  * page (multi-app safe), falling back to the active app's service host when
106
123
  * the bridgeId is unknown — that's the host the embedded DevTools inspects.
107
124
  * No-op on a destroyed/missing host so we never write to a torn-down wc (pool
108
- * reuse / session swap can swap the host out under us).
125
+ * reuse / session swap can swap the host out under us). Skips dimina
126
+ * framework-internal render-layer log lines (`isInternalLogMessage`) —
127
+ * those must never reach the project's right-panel console at all (the
128
+ * global console mirror still sees them via the `sinks` broadcast in
129
+ * `emit()`, which runs before this function). This is the source-level fix
130
+ * for the render half of the noise console-filter.ts's front-end text
131
+ * filter used to paper over; the service-layer half (`[service]` lines,
132
+ * native CDP-observed, no interception point available — see
133
+ * service-host/preload.cjs's "console.* is deliberately NOT patched"
134
+ * comment) still needs that filter.
109
135
  */
110
136
  function forwardRenderToServiceHost(entry) {
137
+ if (isInternalLogMessage(entry))
138
+ return;
111
139
  let wc = null;
112
140
  if (entry.bridgeId && bridge.getServiceWcForBridge) {
113
141
  wc = bridge.getServiceWcForBridge(entry.bridgeId);
@@ -135,6 +163,9 @@ export function createConsoleForwarder(bridge, diagnostics) {
135
163
  // (render only — see loop-safety invariant in the module header).
136
164
  emit(raw) {
137
165
  const entry = (raw ?? {});
166
+ buffer.push(entry);
167
+ if (buffer.length > CONSOLE_BUFFER_CAP)
168
+ buffer.shift();
138
169
  for (const sink of sinks) {
139
170
  try {
140
171
  sink(entry);
@@ -147,7 +178,15 @@ export function createConsoleForwarder(bridge, diagnostics) {
147
178
  if (entry.source === 'render')
148
179
  forwardRenderToServiceHost(entry);
149
180
  },
150
- subscribe(sink) {
181
+ subscribe(sink, opts) {
182
+ if (opts?.replay) {
183
+ for (const entry of buffer) {
184
+ try {
185
+ sink(entry);
186
+ }
187
+ catch { /* isolate a replay throw same as a live one */ }
188
+ }
189
+ }
151
190
  sinks.add(sink);
152
191
  return registry.add(toDisposable(() => { sinks.delete(sink); }));
153
192
  },
@@ -176,6 +215,7 @@ export function createConsoleForwarder(bridge, diagnostics) {
176
215
  },
177
216
  dispose() {
178
217
  sinks.clear();
218
+ buffer.length = 0;
179
219
  pendingBySession.clear();
180
220
  pendingGlobal.length = 0;
181
221
  readySessions.clear();
@@ -0,0 +1,11 @@
1
+ import type { GuestConsoleEntry } from './index.js';
2
+ /**
3
+ * Judges whether a guest console entry is dimina framework-internal output
4
+ * (a bridge/lifecycle log the framework itself prints) rather than the
5
+ * mini-app author's own business `console.*` call. Only the FIRST arg is
6
+ * inspected — framework logs always lead with the literal tag; a
7
+ * business log that happens to mention the tag later in its args must not
8
+ * be swept up.
9
+ */
10
+ export declare function isInternalLogMessage(entry: GuestConsoleEntry): boolean;
11
+ //# sourceMappingURL=internal-log.d.ts.map
@@ -0,0 +1,23 @@
1
+ /** dimina framework internal-log first-arg prefixes, from the framework
2
+ * source literals (dimina/fe/packages/service/src/core/message.js:26 and
3
+ * siblings use `'[service]'`; dimina/fe/packages/render/src/core/message.js:18
4
+ * and siblings use `'[system]'`). */
5
+ const SERVICE_PREFIX = '[service]';
6
+ const RENDER_PREFIX = '[system]';
7
+ /**
8
+ * Judges whether a guest console entry is dimina framework-internal output
9
+ * (a bridge/lifecycle log the framework itself prints) rather than the
10
+ * mini-app author's own business `console.*` call. Only the FIRST arg is
11
+ * inspected — framework logs always lead with the literal tag; a
12
+ * business log that happens to mention the tag later in its args must not
13
+ * be swept up.
14
+ */
15
+ export function isInternalLogMessage(entry) {
16
+ const first = entry.args?.[0];
17
+ if (typeof first !== 'string')
18
+ return false;
19
+ if (first === RENDER_PREFIX)
20
+ return true;
21
+ return first === SERVICE_PREFIX || first.startsWith(`${SERVICE_PREFIX} `);
22
+ }
23
+ //# sourceMappingURL=internal-log.js.map