@dimina-kit/devtools 0.4.0-dev.20260703051110 → 0.4.0-dev.20260706064107

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 (58) hide show
  1. package/dist/main/app/app.js +6 -0
  2. package/dist/main/index.bundle.js +445 -49
  3. package/dist/main/ipc/bridge-router.d.ts +8 -0
  4. package/dist/main/ipc/bridge-router.js +352 -38
  5. package/dist/main/services/compile-standby.d.ts +26 -0
  6. package/dist/main/services/compile-standby.js +57 -0
  7. package/dist/main/services/console-forward/index.d.ts +12 -1
  8. package/dist/main/services/console-forward/index.js +99 -1
  9. package/dist/main/services/diagnostics/index.d.ts +49 -0
  10. package/dist/main/services/diagnostics/index.js +61 -0
  11. package/dist/main/services/network-forward/index.js +32 -0
  12. package/dist/main/services/notifications/renderer-notifier.d.ts +39 -0
  13. package/dist/main/services/notifications/renderer-notifier.js +4 -1
  14. package/dist/main/services/projects/project-repository.js +2 -1
  15. package/dist/main/services/views/simulator-session-policy.js +17 -0
  16. package/dist/main/services/workbench-context.d.ts +10 -0
  17. package/dist/main/services/workspace/workspace-service.js +12 -3
  18. package/dist/main/windows/service-host-window/create.d.ts +15 -1
  19. package/dist/main/windows/service-host-window/create.js +11 -3
  20. package/dist/preload/shared/api-compat.js +18 -55
  21. package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
  22. package/dist/preload/windows/main.cjs.map +1 -1
  23. package/dist/preload/windows/simulator.cjs +117 -44
  24. package/dist/preload/windows/simulator.cjs.map +3 -3
  25. package/dist/preload/windows/simulator.js +117 -44
  26. package/dist/renderer/assets/index-BwzPwCmM.js +49 -0
  27. package/dist/renderer/assets/{input-Cjk0wSEh.js → input-CTb_le7F.js} +2 -2
  28. package/dist/renderer/assets/ipc-transport-BPWIV5hA.css +1 -0
  29. package/dist/renderer/assets/{ipc-transport-OZS-KmOW.js → ipc-transport-Ck5Xa8Tu.js} +2 -2
  30. package/dist/renderer/assets/popover-B1Zu2UQu.js +2 -0
  31. package/dist/renderer/assets/{select-BQDXi5ih.js → select-ozSk5Pt6.js} +2 -2
  32. package/dist/renderer/assets/{settings-tP3Px2KR.js → settings-CNwGOI22.js} +2 -2
  33. package/dist/renderer/assets/settings-api-DMbMp1Eq.js +2 -0
  34. package/dist/renderer/assets/{workbenchSettings-COhuFF-i.js → workbenchSettings-Cr4EHo5w.js} +2 -2
  35. package/dist/renderer/entries/main/index.html +6 -6
  36. package/dist/renderer/entries/popover/index.html +5 -5
  37. package/dist/renderer/entries/settings/index.html +5 -5
  38. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  39. package/dist/shared/bridge-channels.d.ts +22 -0
  40. package/dist/shared/ipc-channels.d.ts +3 -0
  41. package/dist/shared/ipc-channels.js +9 -0
  42. package/dist/shared/request-core.d.ts +84 -0
  43. package/dist/shared/request-core.js +168 -0
  44. package/dist/shared/simulator-api-metadata.d.ts +22 -17
  45. package/dist/shared/simulator-api-metadata.js +36 -0
  46. package/dist/shared/types.d.ts +7 -4
  47. package/dist/simulator/assets/{device-shell-CiLAPa0I.js → device-shell-C-wcFq3Z.js} +2 -2
  48. package/dist/simulator/assets/{jsx-runtime-CDK-o-S0.js → jsx-runtime-BDTY6fEq.js} +2 -2
  49. package/dist/simulator/assets/simulator-DU3-fS3v.js +10 -0
  50. package/dist/simulator/assets/simulator-mini-app-DzfMfnVM.js +2 -0
  51. package/dist/simulator/simulator.html +2 -2
  52. package/package.json +5 -5
  53. package/dist/renderer/assets/index-D-ksyN1p.js +0 -49
  54. package/dist/renderer/assets/ipc-transport-D5dKIti1.css +0 -1
  55. package/dist/renderer/assets/popover-CQVvOe90.js +0 -2
  56. package/dist/renderer/assets/settings-api-B6x2mhaD.js +0 -2
  57. package/dist/simulator/assets/simulator-Dvnxry3y.js +0 -10
  58. package/dist/simulator/assets/simulator-mini-app-Bt639XL_.js +0 -2
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Wires devkit's warm-standby compile worker into the devtools runtime.
3
+ *
4
+ * While no project is open, devkit keeps one project-agnostic compile worker
5
+ * forked + prewarmed; every openProject adopts it so the first compile skips
6
+ * fork + compiler import (~1.6s). This module owns exactly the glue devtools
7
+ * needs on top: turn the accelerator on at boot, mirror its lifecycle events
8
+ * onto the diagnostics bus (a user-machine bug report then carries the whole
9
+ * standby story), and shut it down with the context.
10
+ *
11
+ * Failure-transparent by design: if devkit is missing or predates the standby
12
+ * API, the accelerator silently stays off — compilation itself is untouched
13
+ * (the adapter falls back to devkit's normal cold-fork path, or the host runs
14
+ * its own adapter entirely).
15
+ */
16
+ import type { DiagnosticsBus } from './diagnostics/index.js';
17
+ export interface CompileStandbyDeps {
18
+ /** Test hook: replaces the dynamic import('@dimina-kit/devkit'). */
19
+ loadDevkit?: () => Promise<unknown>;
20
+ }
21
+ export declare function setupCompileWorkerStandby(ctx: {
22
+ diagnostics?: DiagnosticsBus;
23
+ }, deps?: CompileStandbyDeps): {
24
+ dispose: () => Promise<void>;
25
+ };
26
+ //# sourceMappingURL=compile-standby.d.ts.map
@@ -0,0 +1,57 @@
1
+ const EVENT_SEVERITY = {
2
+ 'spawned': 'info',
3
+ 'prewarmed': 'info',
4
+ 'adopted': 'info',
5
+ 'died': 'warn',
6
+ 'health-check-failed': 'warn',
7
+ 'degraded': 'error',
8
+ };
9
+ export function setupCompileWorkerStandby(ctx, deps = {}) {
10
+ const loadDevkit = deps.loadDevkit ?? (() => import('@dimina-kit/devkit'));
11
+ let manager = null;
12
+ let disposed = false;
13
+ const forward = (ev) => {
14
+ try {
15
+ const detail = [
16
+ `compile standby ${ev.type}`,
17
+ typeof ev.pid === 'number' ? `pid=${ev.pid}` : null,
18
+ ev.reason ? `— ${ev.reason}` : null,
19
+ ].filter(Boolean).join(' ');
20
+ ctx.diagnostics?.report({
21
+ severity: EVENT_SEVERITY[ev.type] ?? 'info',
22
+ code: 'compile-standby',
23
+ message: detail,
24
+ });
25
+ }
26
+ catch {
27
+ // telemetry must never break the standby
28
+ }
29
+ };
30
+ void (async () => {
31
+ let enable;
32
+ try {
33
+ const devkit = await loadDevkit();
34
+ enable = devkit.enableCompileWorkerStandby;
35
+ }
36
+ catch {
37
+ // devkit unavailable — the accelerator quietly stays off.
38
+ return;
39
+ }
40
+ if (typeof enable !== 'function')
41
+ return;
42
+ manager = enable({ onEvent: forward });
43
+ // A teardown that raced the async load wins: the manager is disposed the
44
+ // moment it exists, so a quitting app can never leak a fresh spare.
45
+ if (disposed)
46
+ void manager.dispose();
47
+ })();
48
+ return {
49
+ dispose: async () => {
50
+ if (disposed)
51
+ return;
52
+ disposed = true;
53
+ await manager?.dispose();
54
+ },
55
+ };
56
+ }
57
+ //# sourceMappingURL=compile-standby.js.map
@@ -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
@@ -253,6 +253,16 @@ export function createNetworkForwarder(bridge) {
253
253
  // degrade, dropped if we go ready (so a request shows in exactly one sink).
254
254
  let probeConsoleBuffer = [];
255
255
  let readyTimeoutTimer = null;
256
+ // Wall-clock deadline for the CURRENT probe, set once when 'probing' begins.
257
+ // scheduleReadyRetry() re-checks this on every retry so the retry chain is
258
+ // itself authoritative on giving up — it does not depend on winning a race
259
+ // against readyTimeoutTimer firing first (two timers due at the same virtual
260
+ // instant have no guaranteed firing order under fake timers, which let a
261
+ // never-ready host's retry chain outlive the nominal timeout in CI: an
262
+ // "Aborting after running 10000 timers" abort in `network-forward`'s
263
+ // ready-timeout test). readyTimeoutTimer stays as a backstop for hosts that
264
+ // never retry at all (e.g. the queue drains before the deadline).
265
+ let probeDeadline = null;
256
266
  // ── Batched native dispatch into the DevTools front-end ───────────────────
257
267
  // Queued raw CDP messages (already namespaced + JSON-stringified) awaiting a
258
268
  // microtask flush — coalescing many events into one executeJavaScript avoids
@@ -269,6 +279,7 @@ export function createNetworkForwarder(bridge) {
269
279
  if (sink === 'probing')
270
280
  return;
271
281
  sink = 'probing';
282
+ probeDeadline = Date.now() + DEVTOOLS_READY_TIMEOUT_MS;
272
283
  if (readyTimeoutTimer)
273
284
  clearTimeout(readyTimeoutTimer);
274
285
  readyTimeoutTimer = setTimeout(() => {
@@ -281,6 +292,7 @@ export function createNetworkForwarder(bridge) {
281
292
  /** Native path confirmed live: console buffer is moot, drop it. */
282
293
  function markReady() {
283
294
  sink = 'ready';
295
+ probeDeadline = null;
284
296
  if (readyTimeoutTimer) {
285
297
  clearTimeout(readyTimeoutTimer);
286
298
  readyTimeoutTimer = null;
@@ -299,6 +311,7 @@ export function createNetworkForwarder(bridge) {
299
311
  */
300
312
  function degradeToConsole() {
301
313
  sink = 'degraded';
314
+ probeDeadline = null;
302
315
  dispatchQueue = [];
303
316
  if (readyTimeoutTimer) {
304
317
  clearTimeout(readyTimeoutTimer);
@@ -444,10 +457,21 @@ export function createNetworkForwarder(bridge) {
444
457
  function scheduleReadyRetry() {
445
458
  if (readyRetryTimer || sink === 'ready' || sink === 'degraded')
446
459
  return;
460
+ // Self-terminate on the same deadline readyTimeoutTimer enforces, instead of
461
+ // trusting that timer to win a same-instant race against this one (see the
462
+ // comment on `probeDeadline`'s declaration).
463
+ if (probeDeadline !== null && Date.now() >= probeDeadline) {
464
+ degradeToConsole();
465
+ return;
466
+ }
447
467
  readyRetryTimer = setTimeout(() => {
448
468
  readyRetryTimer = null;
449
469
  if (sink === 'degraded')
450
470
  return;
471
+ if (probeDeadline !== null && Date.now() >= probeDeadline) {
472
+ degradeToConsole();
473
+ return;
474
+ }
451
475
  if (dispatchQueue.length > 0)
452
476
  scheduleFlush();
453
477
  }, READY_RETRY_MS);
@@ -504,6 +528,7 @@ export function createNetworkForwarder(bridge) {
504
528
  // A new simulator attach restarts the native sink: 'idle' until the next
505
529
  // event re-probes the (possibly already-set) host.
506
530
  sink = 'idle';
531
+ probeDeadline = null;
507
532
  if (readyRetryTimer) {
508
533
  clearTimeout(readyRetryTimer);
509
534
  readyRetryTimer = null;
@@ -692,6 +717,7 @@ export function createNetworkForwarder(bridge) {
692
717
  clearTimeout(readyRetryTimer);
693
718
  readyRetryTimer = null;
694
719
  }
720
+ probeDeadline = null;
695
721
  // Records buffered while probing the OLD host are stale — drop, don't flush
696
722
  // (their native copies were already queued; on a host swap we restart clean).
697
723
  probeConsoleBuffer = [];
@@ -732,6 +758,12 @@ export function createNetworkForwarder(bridge) {
732
758
  catch { /* gone */ }
733
759
  });
734
760
  }
761
+ // A host swap while still 'probing' the OLD host must not inherit its
762
+ // window: beginProbing()'s `sink === 'probing'` guard would otherwise skip
763
+ // (re-)arming readyTimeoutTimer/probeDeadline for the NEW host, leaving it
764
+ // probing forever with no timeout. Force through 'idle' so beginProbing()
765
+ // always arms a fresh window for whichever host is now current.
766
+ sink = 'idle';
735
767
  beginProbing();
736
768
  if (dispatchQueue.length > 0)
737
769
  scheduleFlush();
@@ -11,6 +11,43 @@ export interface ProjectStatusPayload {
11
11
  message: string;
12
12
  /** True when the status update is emitted by the file-watcher rebuild loop. */
13
13
  hotReload?: boolean;
14
+ /** Freshly-read page list, carried on a hot-reload status so the launch dropdown stays current. Absent when the read failed or wasn't attempted. */
15
+ pages?: string[];
16
+ /** Present (`'dead'`) once the project's file watcher has stopped — saves no longer trigger an automatic rebuild. */
17
+ watcher?: 'dead';
18
+ }
19
+ /**
20
+ * Payload for the `session:runtimeStatus` push — the post-compile SESSION
21
+ * lifecycle main tracks per `appSessionId` in bridge-router (`AppSession`
22
+ * itself is main-private; this is the public projection). A successful
23
+ * compile only means the resource tree exists; it says nothing about whether
24
+ * the simulator actually booted — this closes that gap.
25
+ */
26
+ export interface SessionRuntimeStatusPayload {
27
+ appId: string;
28
+ phase: 'launching' | 'running' | 'launch-failed' | 'crashed';
29
+ /**
30
+ * Machine-readable cause, present on `'launch-failed'` and `'crashed'`:
31
+ * `'timeout'` — the launch watchdog (`LAUNCH_TIMEOUT_MS`) expired before the
32
+ * root page reported `domReady`; `'logic-bundle-unreachable'` — the
33
+ * compiled `logic.js` could not be fetched/executed;
34
+ * `'service-host-navigation-failed'` — the service-host window failed to
35
+ * navigate to its spawn URL; `'service-host-crashed'` — the service host's
36
+ * renderer process was gone. Plain `string` (not a literal union) so an
37
+ * unrecognized future code degrades gracefully instead of a type error.
38
+ */
39
+ code?: string;
40
+ /** Human-readable detail, mirroring the short form of the matching `ctx.diagnostics` report. */
41
+ reason?: string;
42
+ /**
43
+ * Present on `'launching'` when `handleSpawn`'s page-mount gate
44
+ * (`resolveRootPagePath`) substituted the resolved page for the request —
45
+ * i.e. `SpawnResult.pageFallbackApplied` was true for this spawn.
46
+ */
47
+ pageFallback?: {
48
+ requested: string;
49
+ resolved: string;
50
+ };
14
51
  }
15
52
  /**
16
53
  * Payload for the `project:compileLog` push — one filtered dmcc log line.
@@ -51,6 +88,8 @@ export interface WorkbenchSettingsInitPayload {
51
88
  export interface RendererNotifier {
52
89
  /** Broadcast project compile status transitions to the main renderer. */
53
90
  projectStatus(payload: ProjectStatusPayload): void;
91
+ /** Broadcast a session's post-compile runtime lifecycle to the main renderer. */
92
+ sessionRuntimeStatus(payload: SessionRuntimeStatusPayload): void;
54
93
  /** Push one per-line dmcc compile-log entry to the main renderer. */
55
94
  compileLog(payload: CompileLogPayload): void;
56
95
  /** Ask the main renderer to navigate back to its landing screen. */
@@ -1,4 +1,4 @@
1
- import { ProjectChannel, WindowChannel, SettingsChannel, PopoverChannel, WorkbenchSettingsChannel, EditorChannel, ViewChannel, } from '../../../shared/ipc-channels.js';
1
+ import { ProjectChannel, SessionChannel, WindowChannel, SettingsChannel, PopoverChannel, WorkbenchSettingsChannel, EditorChannel, ViewChannel, } from '../../../shared/ipc-channels.js';
2
2
  /** Safely resolve a WebContents, skipping destroyed / missing targets. */
3
3
  function liveWebContents(wc) {
4
4
  if (!wc)
@@ -24,6 +24,9 @@ export function createRendererNotifier(ctx) {
24
24
  projectStatus(payload) {
25
25
  sendToMain(ProjectChannel.Status, payload);
26
26
  },
27
+ sessionRuntimeStatus(payload) {
28
+ sendToMain(SessionChannel.RuntimeStatus, payload);
29
+ },
27
30
  compileLog(payload) {
28
31
  sendToMain(ProjectChannel.CompileLog, payload);
29
32
  },
@@ -103,7 +103,8 @@ export function getProjectPages(dirPath) {
103
103
  entryPagePath: appJson.entryPagePath || appJson.pages?.[0] || '',
104
104
  };
105
105
  }
106
- catch {
106
+ catch (err) {
107
+ log.warn(`Failed to read project pages from ${appJsonPath}`, err);
107
108
  return { pages: [], entryPagePath: '' };
108
109
  }
109
110
  }
@@ -19,6 +19,12 @@ import { session } from 'electron';
19
19
  import { toDisposable } from '@dimina-kit/electron-deck/main';
20
20
  import { getSimulatorServicewechatReferer } from '../simulator/referer.js';
21
21
  import { registerMiniappSessionConfigurator, SHARED_MINIAPP_PARTITION, } from './miniapp-partition.js';
22
+ /** The response headers this policy owns outright (compared lowercase). */
23
+ const CORS_OVERRIDE_HEADERS = new Set([
24
+ 'access-control-allow-origin',
25
+ 'access-control-allow-headers',
26
+ 'access-control-allow-methods',
27
+ ]);
22
28
  /** Apply the simulator runtime's referer + CORS webRequest policy to one
23
29
  * session. Each session installs its own listeners (a webRequest listener is
24
30
  * per-session), so this runs once per partition. */
@@ -35,6 +41,17 @@ function applySimulatorWebRequestPolicy(simulatorSession) {
35
41
  // CORS for the native render/service hosts to fetch compiled app resources
36
42
  // cross-origin. (The COOP/COEP cross-origin-isolation headers were only for
37
43
  // the removed default-path SharedArrayBuffer sync Worker — dropped.)
44
+ //
45
+ // This policy is the single authority for these three headers: any value
46
+ // the server itself sent must be REPLACED, not shadowed. responseHeaders
47
+ // keys keep the server's original casing, so assigning the lowercase key
48
+ // beside an existing `Access-Control-Allow-Origin` puts BOTH on the wire —
49
+ // Chromium then rejects the response for carrying multiple ACAO values
50
+ // (`*, *`), breaking every fetch against a CORS-compliant backend.
51
+ for (const key of Object.keys(headers)) {
52
+ if (CORS_OVERRIDE_HEADERS.has(key.toLowerCase()))
53
+ delete headers[key];
54
+ }
38
55
  headers['access-control-allow-origin'] = ['*'];
39
56
  headers['access-control-allow-headers'] = ['*'];
40
57
  headers['access-control-allow-methods'] = ['*'];
@@ -2,6 +2,7 @@ import type { BrowserWindow } from 'electron';
2
2
  import type { CompilationAdapter, CustomFileTypes, WorkbenchConfig } from '../../shared/types.js';
3
3
  import type { BridgeRouterHandle } from '../ipc/bridge-router.js';
4
4
  import type { ConsoleForwarder } from './console-forward/index.js';
5
+ import type { DiagnosticsBus } from './diagnostics/index.js';
5
6
  import type { NetworkForwarder } from './network-forward/index.js';
6
7
  import type { AppDataTap } from './simulator-appdata/index.js';
7
8
  import type { StorageApi } from './simulator-storage/index.js';
@@ -167,6 +168,15 @@ export interface WorkbenchContext {
167
168
  * installed.
168
169
  */
169
170
  consoleForwarder?: ConsoleForwarder;
171
+ /**
172
+ * Authoritative diagnostics bus for main-synthesized developer diagnostics
173
+ * (page-not-found, logic-bundle-unreachable, service-host-navigation-failed,
174
+ * …), set by `installBridgeRouter`. `consoleForwarder` subscribes to it so
175
+ * every diagnostic also surfaces in the owning session's embedded DevTools
176
+ * Console panel, not just the main-process log. Undefined until the bridge
177
+ * router is installed.
178
+ */
179
+ diagnostics?: DiagnosticsBus;
170
180
  /**
171
181
  * Native-host network forwarder, set in app bootstrap. Attaches the CDP
172
182
  * debugger to the simulator WCV (where `wx.request`/`downloadFile`/`uploadFile`
@@ -38,8 +38,8 @@ export function createWorkspaceService(ctx) {
38
38
  // wins request tokens, so a close mid-open can't disposeAll() the views the open
39
39
  // is building or desync currentSession vs the bridge appSessions. See op-lock.ts.
40
40
  const opLock = createOpLock();
41
- function sendStatus(status, message, hotReload) {
42
- ctx.notify.projectStatus(hotReload ? { status, message, hotReload: true } : { status, message });
41
+ function sendStatus(status, message, hotReload, pages, watcherDead) {
42
+ ctx.notify.projectStatus({ status, message, ...(hotReload ? { hotReload: true } : {}), ...(pages ? { pages } : {}), ...(watcherDead ? { watcher: 'dead' } : {}) });
43
43
  }
44
44
  function bestEffort(label, fn) {
45
45
  try {
@@ -127,8 +127,17 @@ export function createWorkspaceService(ctx) {
127
127
  sourcemap: true,
128
128
  fileTypes: ctx.fileTypes,
129
129
  watch: compile.watch,
130
- onRebuild: () => sendStatus('ready', '编译完成,已重启', true),
130
+ onRebuild: () => {
131
+ // getProjectPages never throws — failure yields the empty-pages
132
+ // shape. Empty means "read failed or degenerate project" (a valid
133
+ // app.json names ≥1 page) and is withheld so the renderer keeps its
134
+ // previous dropdown instead of blanking; the push is never blocked.
135
+ const { pages } = repo.getProjectPages(projectPath);
136
+ sendStatus('ready', '编译完成,已重启', true, pages.length ? pages : undefined);
137
+ },
131
138
  onBuildError: (err) => sendStatus('error', String(err)),
139
+ // Watcher died mid-session (EMFILE, permission loss, …): non-fatal, so 'ready' stays but `watcher: 'dead'` flags that saves no longer auto-rebuild.
140
+ onWatcherError: () => sendStatus('ready', '文件监听已停止,保存不再触发自动编译', false, undefined, true),
132
141
  onLog: (entry) => {
133
142
  if (sessionGeneration !== logGeneration)
134
143
  return;
@@ -28,6 +28,13 @@ export interface ServiceHostWindowOptions {
28
28
  * which page logic referencing `qd.*` throws `ReferenceError: qd is not defined`.
29
29
  */
30
30
  apiNamespaces?: string[];
31
+ /**
32
+ * Observes a spawn-navigation `loadURL` rejection (NOT encoded into the
33
+ * spawn URL). Without it the fresh-window path swallows the failure entirely
34
+ * — `did-finish-load` never fires, the session hangs, and the only signal is
35
+ * the launch watchdog's late timeout instead of the real cause.
36
+ */
37
+ onLoadFailed?: (err: unknown) => void;
31
38
  }
32
39
  /**
33
40
  * Construct an (un-navigated) service-host BrowserWindow. Split out from
@@ -52,8 +59,15 @@ export declare function buildServiceHostSpawnUrl(opts: ServiceHostWindowOptions)
52
59
  * preload re-evaluates on this navigation, rebuilding `__diminaSpawnContext`.
53
60
  * Returns the load promise (resolves on did-finish-load). Attaches the dev-only
54
61
  * detached-DevTools hook, matching the original behavior.
62
+ *
63
+ * A `loadURL` rejection is swallowed — the pooled-window recycle path expects
64
+ * a failed navigation not to reject the caller — but `opts.onLoadFailed`, when
65
+ * given, still observes the original rejection reason (routed by the caller
66
+ * into the diagnostics bus) before this resolves.
55
67
  */
56
- export declare function navigateServiceHost(win: BrowserWindow, url: string): Promise<void>;
68
+ export declare function navigateServiceHost(win: BrowserWindow, url: string, opts?: {
69
+ onLoadFailed?: (err: unknown) => void;
70
+ }): Promise<void>;
57
71
  /**
58
72
  * The spec a `ServiceHostPool` uses to pre-warm service-host windows. Keeps the
59
73
  * pooled window byte-equivalent to `constructServiceHostWindow`'s output, and
@@ -72,9 +72,17 @@ export function buildServiceHostSpawnUrl(opts) {
72
72
  * preload re-evaluates on this navigation, rebuilding `__diminaSpawnContext`.
73
73
  * Returns the load promise (resolves on did-finish-load). Attaches the dev-only
74
74
  * detached-DevTools hook, matching the original behavior.
75
+ *
76
+ * A `loadURL` rejection is swallowed — the pooled-window recycle path expects
77
+ * a failed navigation not to reject the caller — but `opts.onLoadFailed`, when
78
+ * given, still observes the original rejection reason (routed by the caller
79
+ * into the diagnostics bus) before this resolves.
75
80
  */
76
- export function navigateServiceHost(win, url) {
77
- const loaded = Promise.resolve(win.loadURL(url)).then(() => undefined, () => undefined);
81
+ export function navigateServiceHost(win, url, opts) {
82
+ const loaded = Promise.resolve(win.loadURL(url)).then(() => undefined, (err) => {
83
+ opts?.onLoadFailed?.(err);
84
+ return undefined;
85
+ });
78
86
  if (!app.isPackaged && process.env.NODE_ENV !== 'test') {
79
87
  // Recycled pooled windows get navigated repeatedly. A `once` hook that never
80
88
  // fires (the spawn was disposed before its service.html settled) stays on the
@@ -141,7 +149,7 @@ export function createServiceHostWindow(opts) {
141
149
  // so its logic-layer localStorage/cookies are shared with the project's render
142
150
  // side only, never with other projects.
143
151
  const win = constructServiceHostWindow({ appId: opts.appId, partition: miniappPartition(opts.appId, opts.projectPath) });
144
- void navigateServiceHost(win, buildServiceHostSpawnUrl(opts));
152
+ void navigateServiceHost(win, buildServiceHostSpawnUrl(opts), { onLoadFailed: opts.onLoadFailed });
145
153
  return win;
146
154
  }
147
155
  //# sourceMappingURL=create.js.map