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

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 (50) 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 +826 -690
  5. package/dist/main/ipc/bridge-router.js +180 -141
  6. package/dist/main/ipc/project-fs.js +39 -21
  7. package/dist/main/services/automation/handlers/app.js +143 -109
  8. package/dist/main/services/mcp/tools/simulator-tools.js +118 -97
  9. package/dist/main/services/network-forward/dispatch-batch.d.ts +22 -0
  10. package/dist/main/services/network-forward/dispatch-batch.js +21 -0
  11. package/dist/main/services/network-forward/index.js +5 -20
  12. package/dist/main/services/projects/create-project-service.js +54 -34
  13. package/dist/main/services/projects/types.d.ts +1 -32
  14. package/dist/main/services/service-console/console-api.js +36 -27
  15. package/dist/main/services/simulator-temp-files/disk.js +109 -86
  16. package/dist/main/services/workbench-coi-server.js +58 -45
  17. package/dist/main/utils/logger.d.ts +5 -12
  18. package/dist/main/utils/logger.js +5 -45
  19. package/dist/preload/instrumentation/wxml-extract.js +94 -62
  20. package/dist/preload/runtime/main-api-runner.d.ts +1 -1
  21. package/dist/preload/windows/main.cjs +284 -565
  22. package/dist/preload/windows/main.cjs.map +2 -2
  23. package/dist/preload/windows/simulator.cjs +37 -39
  24. package/dist/preload/windows/simulator.cjs.map +2 -2
  25. package/dist/preload/windows/simulator.js +37 -39
  26. package/dist/render-host/render-inspect.js +63 -52
  27. package/dist/renderer/assets/index-D-ksyN1p.js +49 -0
  28. package/dist/renderer/assets/workbenchSettings-COhuFF-i.js +8 -0
  29. package/dist/renderer/entries/main/index.html +1 -1
  30. package/dist/renderer/entries/workbench-settings/index.html +1 -1
  31. package/dist/shared/appdata-accumulator.js +53 -48
  32. package/dist/shared/open-in-editor-resource-path.d.ts +56 -0
  33. package/dist/shared/open-in-editor-resource-path.js +133 -0
  34. package/dist/shared/open-in-editor.d.ts +2 -9
  35. package/dist/shared/open-in-editor.js +8 -93
  36. package/dist/shared/types.d.ts +13 -0
  37. package/dist/simulator/assets/{device-shell-CUl0ILfn.js → device-shell-CiLAPa0I.js} +2 -2
  38. package/dist/simulator/assets/simulator-Dvnxry3y.js +10 -0
  39. package/dist/simulator/simulator.html +1 -1
  40. package/dist/vscode-workbench/assets/__vite-browser-external-CXi6Kz9W.js +1 -0
  41. package/dist/vscode-workbench/assets/{dist-CS7SQP3K.js → dist-TpGpmMGi.js} +3 -3
  42. package/dist/vscode-workbench/assets/{iconv-lite-umd-D3q-1-o6.js → iconv-lite-umd-C9tiCAJa.js} +1 -1
  43. package/dist/vscode-workbench/assets/{index-zjigpZ25.js → index-DJ1HyMZ7.js} +20 -22
  44. package/dist/vscode-workbench/assets/{jschardet-Cu4ufeHq.js → jschardet-DE1jV513.js} +1 -1
  45. package/dist/vscode-workbench/index.html +1 -1
  46. package/package.json +5 -5
  47. package/dist/renderer/assets/index-B-Dqb7G0.js +0 -49
  48. package/dist/renderer/assets/workbenchSettings-Bim9ol9R.js +0 -8
  49. package/dist/simulator/assets/simulator-Dz8iGcgy.js +0 -10
  50. package/dist/vscode-workbench/assets/__vite-browser-external-B0DTNerG.js +0 -1
@@ -76,122 +76,141 @@ appHandlers['App.getPageStack'] = async (ctx) => {
76
76
  }
77
77
  return { pageStack };
78
78
  };
79
- appHandlers['App.callWxMethod'] = async (ctx, params) => {
80
- const method = params.method;
81
- const args = params.args || [];
82
- // NATIVE-HOST: the authoritative `wx.*` runs in the hidden service-host
83
- // window (the simulator / render-guest context has no `wx`). Run EVERY method
84
- // there nav so the page stack goes through the real runtime path, and
85
- // non-nav (setNavigationBarTitle / getSystemInfoSync / tabBar APIs / …) so
86
- // they don't fall through to the default-arch eval-in-simulator path below,
87
- // which throws "wx is not defined" under native-host.
88
- if (ctx.bridge?.isNativeHost()) {
89
- const serviceWc = ctx.bridge.getServiceWc();
90
- if (!serviceWc)
91
- throw new Error('Service host not connected');
92
- if (NAV_METHODS.has(method) || method === 'navigateBack') {
93
- const arg = method === 'navigateBack'
94
- ? (args[0] ?? { delta: 1 })
95
- : (args[0] ?? {});
96
- // Capture the active page BEFORE navigating so we can wait for it to
97
- // actually change, instead of blindly sleeping a fixed duration.
98
- const since = ctx.bridge.getActiveBridgeId();
99
- await serviceWc.executeJavaScript(`wx.${method}(${JSON.stringify(arg)})`);
100
- // Wait for the bridge's `activePage` signal (the new page mounted), with a
101
- // timeout floor = the old blind-wait duration. Resolves as soon as the new
102
- // page is active — far faster than the fixed sleep for the common case —
103
- // and still bounded for edges (e.g. switchTab back to an already-mounted
104
- // tab keeps the same bridgeId, so it falls through to the floor, same as
105
- // the old behaviour). See wait-active-page.ts.
106
- const timeoutMs = method === 'navigateBack' ? 1500 : 2000;
107
- await waitForActivePage(ctx.bridge, {
108
- since,
109
- timeoutMs,
110
- onTimeout: () => console.warn(`[automation] ${method}: activePage signal not seen within ${timeoutMs}ms — proceeding on timeout floor`),
111
- });
112
- return { result: undefined };
79
+ /** Nav (incl. navigateBack) on the native-host service-host `wx`, waiting for the page to actually change. */
80
+ async function runNativeHostNav(bridge, serviceWc, method, args) {
81
+ const arg = method === 'navigateBack'
82
+ ? (args[0] ?? { delta: 1 })
83
+ : (args[0] ?? {});
84
+ // Capture the active page BEFORE navigating so we can wait for it to
85
+ // actually change, instead of blindly sleeping a fixed duration.
86
+ const since = bridge.getActiveBridgeId();
87
+ await serviceWc.executeJavaScript(`wx.${method}(${JSON.stringify(arg)})`);
88
+ // Wait for the bridge's `activePage` signal (the new page mounted), with a
89
+ // timeout floor = the old blind-wait duration. Resolves as soon as the new
90
+ // page is active — far faster than the fixed sleep for the common case —
91
+ // and still bounded for edges (e.g. switchTab back to an already-mounted
92
+ // tab keeps the same bridgeId, so it falls through to the floor, same as
93
+ // the old behaviour). See wait-active-page.ts.
94
+ const timeoutMs = method === 'navigateBack' ? 1500 : 2000;
95
+ await waitForActivePage(bridge, {
96
+ since,
97
+ timeoutMs,
98
+ onTimeout: () => console.warn(`[automation] ${method}: activePage signal not seen within ${timeoutMs}ms — proceeding on timeout floor`),
99
+ });
100
+ return { result: undefined };
101
+ }
102
+ /**
103
+ * Non-nav `wx.*` on the native-host service-host, returning its (sync) value.
104
+ * Async methods that report via a success callback return undefined here,
105
+ * same as the default-arch generic path (`runSimulatorGeneric`).
106
+ */
107
+ async function runNativeHostGeneric(serviceWc, method, args) {
108
+ const argsStr = args.map((a) => JSON.stringify(a)).join(', ');
109
+ const result = await serviceWc.executeJavaScript(`
110
+ new Promise((resolve, reject) => {
111
+ try {
112
+ if (typeof wx === 'undefined' || typeof wx[${JSON.stringify(method)}] !== 'function') {
113
+ reject(new Error('wx.' + ${JSON.stringify(method)} + ' is not a function in service host'))
114
+ return
113
115
  }
114
- // Non-nav: invoke on the service-host wx and return its (sync) value. Async
115
- // methods that report via a success callback return undefined here, same as
116
- // the default-arch generic path below.
117
- const argsStr = args.map((a) => JSON.stringify(a)).join(', ');
118
- const result = await serviceWc.executeJavaScript(`
119
- new Promise((resolve, reject) => {
116
+ resolve(wx.${method}(${argsStr}))
117
+ } catch (e) { reject(e && e.message ? e.message : String(e)) }
118
+ })
119
+ `);
120
+ return { result };
121
+ }
122
+ /**
123
+ * NATIVE-HOST: the authoritative `wx.*` runs in the hidden service-host
124
+ * window (the simulator / render-guest context has no `wx`). Run EVERY method
125
+ * there — nav so the page stack goes through the real runtime path, and
126
+ * non-nav (setNavigationBarTitle / getSystemInfoSync / tabBar APIs / …) so
127
+ * they don't fall through to the default-arch eval-in-simulator path, which
128
+ * throws "wx is not defined" under native-host.
129
+ */
130
+ async function runNativeHostWxMethod(bridge, method, args) {
131
+ const serviceWc = bridge.getServiceWc();
132
+ if (!serviceWc)
133
+ throw new Error('Service host not connected');
134
+ if (NAV_METHODS.has(method) || method === 'navigateBack') {
135
+ return runNativeHostNav(bridge, serviceWc, method, args);
136
+ }
137
+ return runNativeHostGeneric(serviceWc, method, args);
138
+ }
139
+ /**
140
+ * Try clicking a matching DOM element for a nav target. We can't use
141
+ * `inIframe` here — tabBar mini-apps keep prior tabs' iframes around
142
+ * (display:none) and `iframes[length-1]` ends up being the hidden,
143
+ * freshly-cached tab rather than the visible one. Pick the visible iframe
144
+ * explicitly.
145
+ */
146
+ async function clickNavTarget(ctx, cleanUrl) {
147
+ return evalInSim(ctx, `(() => {
148
+ const iframes = Array.from(document.querySelectorAll('iframe'))
149
+ const visible = iframes.filter((f) => {
150
+ const cs = window.getComputedStyle(f)
151
+ if (cs.display === 'none' || cs.visibility === 'hidden') return false
152
+ const rect = f.getBoundingClientRect()
153
+ return rect.width > 0 && rect.height > 0
154
+ })
155
+ const target = visible[visible.length - 1] || iframes[iframes.length - 1]
156
+ if (!target || !target.contentDocument) return false
157
+ const path = ${JSON.stringify(cleanUrl)}
158
+ const el = Array.from(target.contentDocument.querySelectorAll('[data-path]'))
159
+ .find((e) => e.getAttribute('data-path') === path)
160
+ if (el) { el.click(); return true }
161
+ return false
162
+ })()`).catch(() => false);
163
+ }
164
+ /**
165
+ * Fallback cascade for navigation methods when no matching DOM element was
166
+ * found to click:
167
+ * 1. iframe wx[method] — page-iframe surface (older arch, where jdimina
168
+ * installed `window.wx` on each page frame).
169
+ * 2. top-window wx[method] — set up by simulator/main.tsx, binds
170
+ * MiniApp.switchTab / navigateTo / etc. Required for switchTab since
171
+ * page-iframe wx (jdimina) doesn't expose it.
172
+ */
173
+ async function invokeNavViaWx(ctx, method, cleanUrl) {
174
+ const apiName = JSON.stringify(method);
175
+ const urlJson = JSON.stringify(cleanUrl);
176
+ await evalInSim(ctx, `(() => {
177
+ const iframes = Array.from(document.querySelectorAll('iframe'))
178
+ for (const f of iframes) {
120
179
  try {
121
- if (typeof wx === 'undefined' || typeof wx[${JSON.stringify(method)}] !== 'function') {
122
- reject(new Error('wx.' + ${JSON.stringify(method)} + ' is not a function in service host'))
180
+ if (f.contentWindow && f.contentWindow.wx && typeof f.contentWindow.wx[${apiName}] === 'function') {
181
+ f.contentWindow.wx[${apiName}]({ url: ${urlJson} })
123
182
  return
124
183
  }
125
- resolve(wx.${method}(${argsStr}))
126
- } catch (e) { reject(e && e.message ? e.message : String(e)) }
127
- })
128
- `);
129
- return { result };
130
- }
131
- // For navigation methods, handle specially via DOM click / wx.* on the iframe
132
- if (['navigateTo', 'redirectTo', 'reLaunch', 'switchTab'].includes(method)) {
133
- const opts = args[0];
134
- const url = opts?.url;
135
- if (url) {
136
- const cleanUrl = url.startsWith('/') ? url : `/${url}`;
137
- // Try clicking a matching DOM element first. We can't use `inIframe`
138
- // here — tabBar mini-apps keep prior tabs' iframes around (display:none)
139
- // and `iframes[length-1]` ends up being the hidden, freshly-cached tab
140
- // rather than the visible one. Pick the visible iframe explicitly.
141
- const clicked = await evalInSim(ctx, `(() => {
142
- const iframes = Array.from(document.querySelectorAll('iframe'))
143
- const visible = iframes.filter((f) => {
144
- const cs = window.getComputedStyle(f)
145
- if (cs.display === 'none' || cs.visibility === 'hidden') return false
146
- const rect = f.getBoundingClientRect()
147
- return rect.width > 0 && rect.height > 0
148
- })
149
- const target = visible[visible.length - 1] || iframes[iframes.length - 1]
150
- if (!target || !target.contentDocument) return false
151
- const path = ${JSON.stringify(cleanUrl)}
152
- const el = Array.from(target.contentDocument.querySelectorAll('[data-path]'))
153
- .find((e) => e.getAttribute('data-path') === path)
154
- if (el) { el.click(); return true }
155
- return false
156
- })()`).catch(() => false);
157
- if (!clicked) {
158
- // Fallback cascade for navigation methods:
159
- // 1. iframe wx[method] — page-iframe surface (older arch, where
160
- // jdimina installed `window.wx` on each page frame).
161
- // 2. top-window wx[method] — set up by simulator/main.tsx, binds
162
- // MiniApp.switchTab / navigateTo / etc. Required for switchTab
163
- // since page-iframe wx (jdimina) doesn't expose it.
164
- const apiName = JSON.stringify(method);
165
- const urlJson = JSON.stringify(cleanUrl);
166
- await evalInSim(ctx, `(() => {
167
- const iframes = Array.from(document.querySelectorAll('iframe'))
168
- for (const f of iframes) {
169
- try {
170
- if (f.contentWindow && f.contentWindow.wx && typeof f.contentWindow.wx[${apiName}] === 'function') {
171
- f.contentWindow.wx[${apiName}]({ url: ${urlJson} })
172
- return
173
- }
174
- } catch (_) {}
175
- }
176
- if (typeof wx !== 'undefined' && wx && typeof wx[${apiName}] === 'function') {
177
- wx[${apiName}]({ url: ${urlJson} })
178
- }
179
- })()`);
180
- }
181
- // Wait for navigation
182
- await new Promise((r) => setTimeout(r, 2000));
183
- }
184
- return { result: undefined };
185
- }
186
- // For navigateBack
187
- if (method === 'navigateBack') {
188
- await evalInSim(ctx, 'history.back()');
189
- await new Promise((r) => setTimeout(r, 1500));
184
+ } catch (_) {}
185
+ }
186
+ if (typeof wx !== 'undefined' && wx && typeof wx[${apiName}] === 'function') {
187
+ wx[${apiName}]({ url: ${urlJson} })
188
+ }
189
+ })()`);
190
+ }
191
+ /** Default-arch nav: handled via DOM click / wx.* on the iframe. */
192
+ async function runSimulatorNav(ctx, method, args) {
193
+ const opts = args[0];
194
+ const url = opts?.url;
195
+ if (!url)
190
196
  return { result: undefined };
197
+ const cleanUrl = url.startsWith('/') ? url : `/${url}`;
198
+ const clicked = await clickNavTarget(ctx, cleanUrl);
199
+ if (!clicked) {
200
+ await invokeNavViaWx(ctx, method, cleanUrl);
191
201
  }
192
- // General wx method call
202
+ // Wait for navigation
203
+ await new Promise((r) => setTimeout(r, 2000));
204
+ return { result: undefined };
205
+ }
206
+ async function runSimulatorNavigateBack(ctx) {
207
+ await evalInSim(ctx, 'history.back()');
208
+ await new Promise((r) => setTimeout(r, 1500));
209
+ return { result: undefined };
210
+ }
211
+ /** General wx method call — async wx methods with success/fail callbacks. */
212
+ async function runSimulatorGeneric(ctx, method, args) {
193
213
  const argsStr = args.map((a) => JSON.stringify(a)).join(', ');
194
- // Async wx methods with success/fail callbacks
195
214
  const result = await evalInSim(ctx, `
196
215
  new Promise((resolve, reject) => {
197
216
  try {
@@ -203,6 +222,21 @@ appHandlers['App.callWxMethod'] = async (ctx, params) => {
203
222
  })
204
223
  `);
205
224
  return { result };
225
+ }
226
+ appHandlers['App.callWxMethod'] = async (ctx, params) => {
227
+ const method = params.method;
228
+ const args = params.args || [];
229
+ const bridge = ctx.bridge;
230
+ if (bridge?.isNativeHost()) {
231
+ return runNativeHostWxMethod(bridge, method, args);
232
+ }
233
+ if (NAV_METHODS.has(method)) {
234
+ return runSimulatorNav(ctx, method, args);
235
+ }
236
+ if (method === 'navigateBack') {
237
+ return runSimulatorNavigateBack(ctx);
238
+ }
239
+ return runSimulatorGeneric(ctx, method, args);
206
240
  };
207
241
  appHandlers['App.callFunction'] = async (ctx, params) => {
208
242
  const fnDecl = params.functionDeclaration;
@@ -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.
@@ -375,26 +376,10 @@ export function createNetworkForwarder(bridge) {
375
376
  if (sink === 'idle')
376
377
  beginProbing();
377
378
  // 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);
379
+ // oversized single messages go via the chunked transport (see packDispatchBatch).
380
+ const { batch, chunked, remaining } = packDispatchBatch(dispatchQueue, MAX_SINGLE_DISPATCH_CHARS, MAX_BATCH_CHARS);
381
+ for (const msg of chunked)
382
+ dispatchChunked(wc, msg);
398
383
  if (batch.length === 0) {
399
384
  // Only chunked messages were processed this turn; continue with the rest.
400
385
  dispatchQueue = remaining;