@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
@@ -11,6 +11,111 @@
11
11
  */
12
12
  import { z } from 'zod';
13
13
  import { getClient } from '../target-manager.js';
14
+ function ok(text) {
15
+ return { content: [{ type: 'text', text }] };
16
+ }
17
+ function err(msg) {
18
+ return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
19
+ }
20
+ async function clickAt(c, x, y) {
21
+ await c.Input.dispatchMouseEvent({ type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
22
+ await c.Input.dispatchMouseEvent({ type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
23
+ }
24
+ async function tapCoord(c, x, y) {
25
+ if (typeof x !== 'number' || typeof y !== 'number')
26
+ return err('tap_coord requires x and y');
27
+ await clickAt(c, x, y);
28
+ return ok(`Tapped at (${x}, ${y})`);
29
+ }
30
+ /**
31
+ * In-page expression evaluated by `tap_selector`: locates the nth match,
32
+ * scrolls it into view, and reports its center point (or a reason it can't be
33
+ * tapped) so the caller can dispatch a real mouse click at that point.
34
+ */
35
+ function buildTapSelectorExpression(selector, index) {
36
+ return `(() => {
37
+ const payload = ${JSON.stringify({ selector, index })}
38
+ try {
39
+ const matches = Array.from(document.querySelectorAll(payload.selector))
40
+ if (matches.length === 0) {
41
+ return { ok: false, reason: 'no_match', message: \`selector matched no elements: \${payload.selector}\` }
42
+ }
43
+ if (payload.index < 0 || payload.index >= matches.length) {
44
+ return { ok: false, reason: 'out_of_range', message: \`nth \${payload.index} out of range (matches: \${matches.length})\` }
45
+ }
46
+ const element = matches[payload.index]
47
+ element.scrollIntoView({ block: 'center', inline: 'center' })
48
+ const rect = element.getBoundingClientRect()
49
+ if (rect.width === 0 || rect.height === 0) {
50
+ return { ok: false, reason: 'not_visible', message: \`selector matched an element, but it is not visible or rendered (zero rect): \${payload.selector}[\${payload.index}]\` }
51
+ }
52
+ return {
53
+ ok: true,
54
+ selector: payload.selector,
55
+ index: payload.index,
56
+ x: rect.left + rect.width / 2,
57
+ y: rect.top + rect.height / 2,
58
+ rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
59
+ }
60
+ } catch (error) {
61
+ const message = error instanceof Error ? error.message : String(error)
62
+ return { ok: false, reason: 'selector_error', message: \`invalid selector: \${payload.selector} (\${message})\` }
63
+ }
64
+ })()`;
65
+ }
66
+ async function tapSelector(c, selector, nth) {
67
+ if (!selector)
68
+ return err('tap_selector requires selector');
69
+ const index = nth ?? 0;
70
+ const expression = buildTapSelectorExpression(selector, index);
71
+ const result = await c.Runtime.evaluate({ expression, returnByValue: true, awaitPromise: true });
72
+ const value = result.result?.value;
73
+ if (!value)
74
+ return err(`selector evaluation returned no result: ${selector}[${index}]`);
75
+ if (!value.ok)
76
+ return err(value.message);
77
+ await clickAt(c, value.x, value.y);
78
+ return ok(`Tapped selector ${value.selector}[${value.index}] at (${value.x}, ${value.y}) within rect (${value.rect.left}, ${value.rect.top}, ${value.rect.width} x ${value.rect.height})`);
79
+ }
80
+ async function typeText(c, selector, nth, text) {
81
+ if (!selector || typeof text !== 'string')
82
+ return err('type requires selector and text');
83
+ const { root } = await c.DOM.getDocument({ depth: 0 });
84
+ const { nodeIds } = await c.DOM.querySelectorAll({ nodeId: root.nodeId, selector });
85
+ if (!nodeIds || nodeIds.length === 0)
86
+ return err(`selector matched no elements: ${selector}`);
87
+ const index = nth ?? 0;
88
+ if (index < 0 || index >= nodeIds.length)
89
+ return err(`nth ${index} out of range (matches: ${nodeIds.length})`);
90
+ await c.DOM.focus({ nodeId: nodeIds[index] });
91
+ try {
92
+ await c.Input.insertText({ text });
93
+ }
94
+ catch {
95
+ for (const ch of text) {
96
+ await c.Input.dispatchKeyEvent({ type: 'char', text: ch });
97
+ }
98
+ }
99
+ return ok(`Typed ${text.length} char(s) into ${selector}[${index}]`);
100
+ }
101
+ async function scrollAt(c, x, y, deltaX, deltaY) {
102
+ if (typeof x !== 'number' || typeof y !== 'number' || typeof deltaX !== 'number' || typeof deltaY !== 'number') {
103
+ return err('scroll requires x, y, deltaX, deltaY');
104
+ }
105
+ await c.Input.dispatchMouseEvent({ type: 'mouseWheel', x, y, deltaX, deltaY });
106
+ return ok(`Scrolled at (${x}, ${y}) by (${deltaX}, ${deltaY})`);
107
+ }
108
+ async function dispatchKey(c, key) {
109
+ if (!key)
110
+ return err('key requires key');
111
+ const isSingleChar = key.length === 1;
112
+ const down = { type: 'keyDown', key, code: key };
113
+ if (isSingleChar)
114
+ down.text = key;
115
+ await c.Input.dispatchKeyEvent(down);
116
+ await c.Input.dispatchKeyEvent({ type: 'keyUp', key, code: key });
117
+ return ok(`Dispatched key ${key}`);
118
+ }
14
119
  export function registerSimulatorTools(server) {
15
120
  server.tool('simulator_navigate', 'Navigate the simulator to a URL, or reload the current page', {
16
121
  url: z.string().optional().describe('URL to navigate to'),
@@ -40,104 +145,20 @@ export function registerSimulatorTools(server) {
40
145
  deltaY: z.number().optional().describe('Vertical wheel delta for `scroll`'),
41
146
  }, async ({ action, x, y, selector, nth, text, key, deltaX, deltaY }) => {
42
147
  const c = getClient('simulator');
43
- const err = (msg) => ({ content: [{ type: 'text', text: `Error: ${msg}` }], isError: true });
44
- const clickAt = async (cx, cy) => {
45
- await c.Input.dispatchMouseEvent({ type: 'mousePressed', x: cx, y: cy, button: 'left', clickCount: 1 });
46
- await c.Input.dispatchMouseEvent({ type: 'mouseReleased', x: cx, y: cy, button: 'left', clickCount: 1 });
47
- };
48
- if (action === 'tap_coord') {
49
- if (typeof x !== 'number' || typeof y !== 'number')
50
- return err('tap_coord requires x and y');
51
- await clickAt(x, y);
52
- return { content: [{ type: 'text', text: `Tapped at (${x}, ${y})` }] };
53
- }
54
- if (action === 'tap_selector') {
55
- if (!selector)
56
- return err('tap_selector requires selector');
57
- const index = nth ?? 0;
58
- const expression = `(() => {
59
- const payload = ${JSON.stringify({ selector, index })}
60
- try {
61
- const matches = Array.from(document.querySelectorAll(payload.selector))
62
- if (matches.length === 0) {
63
- return { ok: false, reason: 'no_match', message: \`selector matched no elements: \${payload.selector}\` }
64
- }
65
- if (payload.index < 0 || payload.index >= matches.length) {
66
- return { ok: false, reason: 'out_of_range', message: \`nth \${payload.index} out of range (matches: \${matches.length})\` }
67
- }
68
- const element = matches[payload.index]
69
- element.scrollIntoView({ block: 'center', inline: 'center' })
70
- const rect = element.getBoundingClientRect()
71
- if (rect.width === 0 || rect.height === 0) {
72
- return { ok: false, reason: 'not_visible', message: \`selector matched an element, but it is not visible or rendered (zero rect): \${payload.selector}[\${payload.index}]\` }
73
- }
74
- return {
75
- ok: true,
76
- selector: payload.selector,
77
- index: payload.index,
78
- x: rect.left + rect.width / 2,
79
- y: rect.top + rect.height / 2,
80
- rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
81
- }
82
- } catch (error) {
83
- const message = error instanceof Error ? error.message : String(error)
84
- return { ok: false, reason: 'selector_error', message: \`invalid selector: \${payload.selector} (\${message})\` }
85
- }
86
- })()`;
87
- const result = await c.Runtime.evaluate({ expression, returnByValue: true, awaitPromise: true });
88
- const value = result.result?.value;
89
- if (!value)
90
- return err(`selector evaluation returned no result: ${selector}[${index}]`);
91
- if (!value.ok)
92
- return err(value.message);
93
- await clickAt(value.x, value.y);
94
- return {
95
- content: [{
96
- type: 'text',
97
- text: `Tapped selector ${value.selector}[${value.index}] at (${value.x}, ${value.y}) within rect (${value.rect.left}, ${value.rect.top}, ${value.rect.width} x ${value.rect.height})`,
98
- }],
99
- };
100
- }
101
- if (action === 'type') {
102
- if (!selector || typeof text !== 'string')
103
- return err('type requires selector and text');
104
- const { root } = await c.DOM.getDocument({ depth: 0 });
105
- const { nodeIds } = await c.DOM.querySelectorAll({ nodeId: root.nodeId, selector });
106
- if (!nodeIds || nodeIds.length === 0)
107
- return err(`selector matched no elements: ${selector}`);
108
- const index = nth ?? 0;
109
- if (index < 0 || index >= nodeIds.length)
110
- return err(`nth ${index} out of range (matches: ${nodeIds.length})`);
111
- await c.DOM.focus({ nodeId: nodeIds[index] });
112
- try {
113
- await c.Input.insertText({ text });
114
- }
115
- catch {
116
- for (const ch of text) {
117
- await c.Input.dispatchKeyEvent({ type: 'char', text: ch });
118
- }
119
- }
120
- return { content: [{ type: 'text', text: `Typed ${text.length} char(s) into ${selector}[${index}]` }] };
121
- }
122
- if (action === 'scroll') {
123
- if (typeof x !== 'number' || typeof y !== 'number' || typeof deltaX !== 'number' || typeof deltaY !== 'number') {
124
- return err('scroll requires x, y, deltaX, deltaY');
125
- }
126
- await c.Input.dispatchMouseEvent({ type: 'mouseWheel', x, y, deltaX, deltaY });
127
- return { content: [{ type: 'text', text: `Scrolled at (${x}, ${y}) by (${deltaX}, ${deltaY})` }] };
128
- }
129
- if (action === 'key') {
130
- if (!key)
131
- return err('key requires key');
132
- const isSingleChar = key.length === 1;
133
- const down = { type: 'keyDown', key, code: key };
134
- if (isSingleChar)
135
- down.text = key;
136
- await c.Input.dispatchKeyEvent(down);
137
- await c.Input.dispatchKeyEvent({ type: 'keyUp', key, code: key });
138
- return { content: [{ type: 'text', text: `Dispatched key ${key}` }] };
148
+ switch (action) {
149
+ case 'tap_coord':
150
+ return tapCoord(c, x, y);
151
+ case 'tap_selector':
152
+ return tapSelector(c, selector, nth);
153
+ case 'type':
154
+ return typeText(c, selector, nth, text);
155
+ case 'scroll':
156
+ return scrollAt(c, x, y, deltaX, deltaY);
157
+ case 'key':
158
+ return dispatchKey(c, key);
159
+ default:
160
+ return err(`unknown action: ${String(action)}`);
139
161
  }
140
- return err(`unknown action: ${action}`);
141
162
  });
142
163
  }
143
164
  //# sourceMappingURL=simulator-tools.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Pure batch-packing for the native dispatch queue: greedily pack queued CDP
3
+ * messages up to `maxBatchChars` so one `executeJavaScript` call stays sized,
4
+ * pulling any oversized message (over `maxSingleChars`) into `chunked` for the
5
+ * separate chunked transport instead.
6
+ *
7
+ * An oversized message reached while `batch` is still empty is queued into
8
+ * `chunked` immediately and packing continues past it, so several oversized
9
+ * messages in a row are all pulled out in one pass. One reached once `batch`
10
+ * already has items stops packing there, leaving it (and everything after) in
11
+ * `remaining` for the next flush — this preserves delivery order across passes.
12
+ */
13
+ export interface PackedDispatchBatch {
14
+ /** Messages to send in one `executeJavaScript` batch dispatch. */
15
+ batch: string[];
16
+ /** Oversized messages to dispatch individually via the chunked transport. */
17
+ chunked: string[];
18
+ /** Whatever the queue had left after this pass. */
19
+ remaining: string[];
20
+ }
21
+ export declare function packDispatchBatch(queue: readonly string[], maxSingleChars: number, maxBatchChars: number): PackedDispatchBatch;
22
+ //# sourceMappingURL=dispatch-batch.d.ts.map
@@ -0,0 +1,21 @@
1
+ export function packDispatchBatch(queue, maxSingleChars, maxBatchChars) {
2
+ const batch = [];
3
+ const chunked = [];
4
+ let batchChars = 0;
5
+ let i = 0;
6
+ for (; i < queue.length; i++) {
7
+ const msg = queue[i];
8
+ if (msg.length > maxSingleChars) {
9
+ if (batch.length > 0)
10
+ break;
11
+ chunked.push(msg);
12
+ continue;
13
+ }
14
+ if (batch.length > 0 && batchChars + msg.length > maxBatchChars)
15
+ break;
16
+ batch.push(msg);
17
+ batchChars += msg.length;
18
+ }
19
+ return { batch, chunked, remaining: queue.slice(i) };
20
+ }
21
+ //# sourceMappingURL=dispatch-batch.js.map
@@ -1,5 +1,6 @@
1
1
  import { DisposableRegistry, toDisposable } from '@dimina-kit/electron-deck/main';
2
2
  import { isFrontendSettled } from '../views/inject-when-ready.js';
3
+ import { packDispatchBatch } from './dispatch-batch.js';
3
4
  // ── requestId namespacing (pure, testable) ──────────────────────────────────
4
5
  /**
5
6
  * CDP events whose `params.requestId` we rewrite into the virtual namespace.
@@ -252,6 +253,16 @@ export function createNetworkForwarder(bridge) {
252
253
  // degrade, dropped if we go ready (so a request shows in exactly one sink).
253
254
  let probeConsoleBuffer = [];
254
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;
255
266
  // ── Batched native dispatch into the DevTools front-end ───────────────────
256
267
  // Queued raw CDP messages (already namespaced + JSON-stringified) awaiting a
257
268
  // microtask flush — coalescing many events into one executeJavaScript avoids
@@ -268,6 +279,7 @@ export function createNetworkForwarder(bridge) {
268
279
  if (sink === 'probing')
269
280
  return;
270
281
  sink = 'probing';
282
+ probeDeadline = Date.now() + DEVTOOLS_READY_TIMEOUT_MS;
271
283
  if (readyTimeoutTimer)
272
284
  clearTimeout(readyTimeoutTimer);
273
285
  readyTimeoutTimer = setTimeout(() => {
@@ -280,6 +292,7 @@ export function createNetworkForwarder(bridge) {
280
292
  /** Native path confirmed live: console buffer is moot, drop it. */
281
293
  function markReady() {
282
294
  sink = 'ready';
295
+ probeDeadline = null;
283
296
  if (readyTimeoutTimer) {
284
297
  clearTimeout(readyTimeoutTimer);
285
298
  readyTimeoutTimer = null;
@@ -298,6 +311,7 @@ export function createNetworkForwarder(bridge) {
298
311
  */
299
312
  function degradeToConsole() {
300
313
  sink = 'degraded';
314
+ probeDeadline = null;
301
315
  dispatchQueue = [];
302
316
  if (readyTimeoutTimer) {
303
317
  clearTimeout(readyTimeoutTimer);
@@ -375,26 +389,10 @@ export function createNetworkForwarder(bridge) {
375
389
  if (sink === 'idle')
376
390
  beginProbing();
377
391
  // Pack greedily up to MAX_BATCH_CHARS so one executeJavaScript stays sized;
378
- // oversized single messages go via the chunked transport.
379
- const batch = [];
380
- let batchChars = 0;
381
- let i = 0;
382
- for (; i < dispatchQueue.length; i++) {
383
- const msg = dispatchQueue[i];
384
- if (msg.length > MAX_SINGLE_DISPATCH_CHARS) {
385
- // Flush whatever's accumulated first to preserve ordering, then chunk.
386
- if (batch.length > 0)
387
- break;
388
- dispatchChunked(wc, msg);
389
- continue;
390
- }
391
- if (batch.length > 0 && batchChars + msg.length > MAX_BATCH_CHARS)
392
- break;
393
- batch.push(msg);
394
- batchChars += msg.length;
395
- }
396
- // Everything up to i was either batched or chunk-dispatched; keep the rest.
397
- const remaining = dispatchQueue.slice(i);
392
+ // oversized single messages go via the chunked transport (see packDispatchBatch).
393
+ const { batch, chunked, remaining } = packDispatchBatch(dispatchQueue, MAX_SINGLE_DISPATCH_CHARS, MAX_BATCH_CHARS);
394
+ for (const msg of chunked)
395
+ dispatchChunked(wc, msg);
398
396
  if (batch.length === 0) {
399
397
  // Only chunked messages were processed this turn; continue with the rest.
400
398
  dispatchQueue = remaining;
@@ -459,10 +457,21 @@ export function createNetworkForwarder(bridge) {
459
457
  function scheduleReadyRetry() {
460
458
  if (readyRetryTimer || sink === 'ready' || sink === 'degraded')
461
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
+ }
462
467
  readyRetryTimer = setTimeout(() => {
463
468
  readyRetryTimer = null;
464
469
  if (sink === 'degraded')
465
470
  return;
471
+ if (probeDeadline !== null && Date.now() >= probeDeadline) {
472
+ degradeToConsole();
473
+ return;
474
+ }
466
475
  if (dispatchQueue.length > 0)
467
476
  scheduleFlush();
468
477
  }, READY_RETRY_MS);
@@ -519,6 +528,7 @@ export function createNetworkForwarder(bridge) {
519
528
  // A new simulator attach restarts the native sink: 'idle' until the next
520
529
  // event re-probes the (possibly already-set) host.
521
530
  sink = 'idle';
531
+ probeDeadline = null;
522
532
  if (readyRetryTimer) {
523
533
  clearTimeout(readyRetryTimer);
524
534
  readyRetryTimer = null;
@@ -707,6 +717,7 @@ export function createNetworkForwarder(bridge) {
707
717
  clearTimeout(readyRetryTimer);
708
718
  readyRetryTimer = null;
709
719
  }
720
+ probeDeadline = null;
710
721
  // Records buffered while probing the OLD host are stale — drop, don't flush
711
722
  // (their native copies were already queued; on a host swap we restart clean).
712
723
  probeConsoleBuffer = [];
@@ -747,6 +758,12 @@ export function createNetworkForwarder(bridge) {
747
758
  catch { /* gone */ }
748
759
  });
749
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';
750
767
  beginProbing();
751
768
  if (dispatchQueue.length > 0)
752
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
  },
@@ -15,55 +15,67 @@
15
15
  import fs from 'node:fs';
16
16
  import path from 'node:path';
17
17
  const DEFAULT_TEMPLATE_ID = 'blank';
18
- export async function createProject(input, ctx) {
19
- // 1. name validation
20
- const name = (input.name ?? '').trim();
18
+ /** Non-empty-after-trim `input.name`, or throws. */
19
+ function resolveName(rawName) {
20
+ const name = (rawName ?? '').trim();
21
21
  if (name.length === 0) {
22
22
  throw new Error('Project name cannot be empty');
23
23
  }
24
- // 2. path validation: does-not-exist OR exists+empty.
25
- const target = input.path;
24
+ return name;
25
+ }
26
+ /** `input.path` does-not-exist OR exists+empty, or throws. */
27
+ function validateTargetPath(target) {
26
28
  if (!target || typeof target !== 'string') {
27
29
  throw new Error('Project path is required');
28
30
  }
29
- if (fs.existsSync(target)) {
30
- const stat = fs.statSync(target);
31
- if (!stat.isDirectory()) {
32
- throw new Error(`Project path exists and is not a directory: ${target}`);
33
- }
34
- const entries = fs.readdirSync(target);
35
- if (entries.length > 0) {
36
- throw new Error(`Project path is not empty (refusing to overwrite): ${target}`);
37
- }
31
+ if (!fs.existsSync(target))
32
+ return;
33
+ const stat = fs.statSync(target);
34
+ if (!stat.isDirectory()) {
35
+ throw new Error(`Project path exists and is not a directory: ${target}`);
36
+ }
37
+ const entries = fs.readdirSync(target);
38
+ if (entries.length > 0) {
39
+ throw new Error(`Project path is not empty (refusing to overwrite): ${target}`);
38
40
  }
39
- // 3. template lookup (default 'blank')
40
- const templateId = input.templateId ?? DEFAULT_TEMPLATE_ID;
41
- const template = ctx.templates.find((t) => t.id === templateId);
41
+ }
42
+ /** Look up `templateId` (default 'blank') and confirm it can materialise a project. */
43
+ function resolveTemplate(templates, templateId) {
44
+ const id = templateId ?? DEFAULT_TEMPLATE_ID;
45
+ const template = templates.find((t) => t.id === id);
42
46
  if (!template) {
43
- throw new Error(`Template not found: ${templateId}`);
47
+ throw new Error(`Template not found: ${id}`);
44
48
  }
45
49
  if (!template.source && !template.generate) {
46
- throw new Error(`Template '${templateId}' has neither a source directory nor a generate function`);
50
+ throw new Error(`Template '${id}' has neither a source directory nor a generate function`);
47
51
  }
48
- // 4. materialise: ensure target exists, then copy or generate.
52
+ return template;
53
+ }
54
+ /** Ensure `target` exists, then copy the template's `source` tree or run its `generate`. */
55
+ async function materializeTemplate(target, template, name) {
49
56
  fs.mkdirSync(target, { recursive: true });
50
57
  if (template.generate) {
51
58
  await template.generate(target, { name });
59
+ return;
52
60
  }
53
- else if (template.source) {
54
- if (!fs.existsSync(template.source.path)) {
55
- throw new Error(`Template source missing on disk: ${template.source.path}`);
56
- }
57
- // `recursive: true` makes cpSync mirror the entire tree (files,
58
- // subdirs, symlinks). `force: true` mirrors over an empty target.
59
- fs.cpSync(template.source.path, target, {
60
- recursive: true,
61
- force: true,
62
- });
61
+ if (!template.source)
62
+ return;
63
+ if (!fs.existsSync(template.source.path)) {
64
+ throw new Error(`Template source missing on disk: ${template.source.path}`);
63
65
  }
64
- // 5. rewrite project.config.json projectname (best-effort: create the file
65
- // if the template didn't ship one gives the user a working starting
66
- // point even for tiny generators).
66
+ // `recursive: true` makes cpSync mirror the entire tree (files,
67
+ // subdirs, symlinks). `force: true` mirrors over an empty target.
68
+ fs.cpSync(template.source.path, target, {
69
+ recursive: true,
70
+ force: true,
71
+ });
72
+ }
73
+ /**
74
+ * Rewrite `<target>/project.config.json`'s `projectname` (best-effort: create
75
+ * the file if the template didn't ship one — gives the user a working
76
+ * starting point even for tiny generators).
77
+ */
78
+ function writeProjectConfig(target, name) {
67
79
  const cfgPath = path.join(target, 'project.config.json');
68
80
  let cfg = {};
69
81
  if (fs.existsSync(cfgPath)) {
@@ -76,7 +88,15 @@ export async function createProject(input, ctx) {
76
88
  }
77
89
  cfg.projectname = name;
78
90
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
79
- // 6. register with provider — this is what makes the new project appear
91
+ }
92
+ export async function createProject(input, ctx) {
93
+ const name = resolveName(input.name);
94
+ const target = input.path;
95
+ validateTargetPath(target);
96
+ const template = resolveTemplate(ctx.templates, input.templateId);
97
+ await materializeTemplate(target, template, name);
98
+ writeProjectConfig(target, name);
99
+ // register with provider — this is what makes the new project appear
80
100
  // in the list immediately after the dialog closes.
81
101
  const created = await ctx.projectsProvider.addProject(target);
82
102
  return created;
@@ -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
  }
@@ -7,6 +7,7 @@
7
7
  import type { CompileConfig } from '../../../shared/types.js';
8
8
  import type { Project } from './project-repository.js';
9
9
  export type { Project };
10
+ export type { ProjectTemplate, CreateProjectInput } from '../../../shared/types.js';
10
11
  /**
11
12
  * Default compile config returned by WorkspaceService when the injected
12
13
  * provider omits `getCompileConfig`, or when no record exists for a path.
@@ -81,38 +82,6 @@ export interface ProjectsProvider {
81
82
  */
82
83
  getThumbnail?(dirPath: string): string | null | Promise<string | null>;
83
84
  }
84
- /**
85
- * A template the user can pick from in the "新建项目" dialog. Exactly one of
86
- * `source` or `generate` should be supplied; if both are omitted, the
87
- * service refuses to materialise the template.
88
- */
89
- export interface ProjectTemplate {
90
- /** Stable identifier. Used by host whitelists and the `templateId` field of CreateProjectInput. */
91
- id: string;
92
- /** Human-readable label shown in the dialog. */
93
- name: string;
94
- description?: string;
95
- icon?: string;
96
- /** Copy-tree source. `path` must be absolute. */
97
- source?: {
98
- type: 'directory';
99
- path: string;
100
- };
101
- /** Programmatic generator. `target` is the absolute destination directory. */
102
- generate?: (target: string, opts: {
103
- name: string;
104
- }) => Promise<void>;
105
- }
106
- /**
107
- * Payload returned from the create-project dialog. The service uses this to
108
- * scaffold disk content and to register the project with the provider.
109
- */
110
- export interface CreateProjectInput {
111
- name: string;
112
- path: string;
113
- templateId?: string;
114
- extra?: Record<string, unknown>;
115
- }
116
85
  /** Built-in template policy. */
117
86
  export type BuiltinTemplatesMode = 'all' | 'none' | readonly string[];
118
87
  //# sourceMappingURL=types.d.ts.map