testdriverai 7.8.0-test.50 → 7.8.0-test.51
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.
- package/agent/lib/sandbox.js +35 -35
- package/ai/skills/testdriver-find/SKILL.md +14 -20
- package/docs/v7/find.mdx +14 -20
- package/package.json +1 -1
- package/sdk.js +3 -3
package/agent/lib/sandbox.js
CHANGED
|
@@ -29,7 +29,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
29
29
|
this._lastConnectParams = null;
|
|
30
30
|
this._teamId = null;
|
|
31
31
|
this._sandboxId = null;
|
|
32
|
-
this._disconnectedAt = null; // tracks when
|
|
32
|
+
this._disconnectedAt = null; // tracks when Realtime connection dropped (for timeout extension on reconnect)
|
|
33
33
|
|
|
34
34
|
// Rate limiting state for Ably publishes (Ably limits to 50 msg/sec per connection)
|
|
35
35
|
this._publishLastTime = 0;
|
|
@@ -85,21 +85,21 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
85
85
|
suspendedRetryTimeout: 15000, // retry from suspended every 15s (default 30s)
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
logger.log(`[
|
|
88
|
+
logger.log(`[realtime] Connecting as sdk-${this._sandboxId}...`);
|
|
89
89
|
|
|
90
90
|
await new Promise(function (resolve, reject) {
|
|
91
91
|
self._ably.connection.on("connected", resolve);
|
|
92
92
|
self._ably.connection.on("failed", function () {
|
|
93
|
-
reject(new Error("
|
|
93
|
+
reject(new Error("Realtime connection failed"));
|
|
94
94
|
});
|
|
95
95
|
setTimeout(function () {
|
|
96
|
-
reject(new Error("
|
|
96
|
+
reject(new Error("Realtime connection timeout"));
|
|
97
97
|
}, 30000);
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
this._sessionChannel = this._ably.channels.get(channelName);
|
|
101
101
|
|
|
102
|
-
logger.log(`[
|
|
102
|
+
logger.log(`[realtime] Channel initialized: ${channelName}`);
|
|
103
103
|
|
|
104
104
|
// Enter presence on the session channel so the API can count connected SDK clients
|
|
105
105
|
try {
|
|
@@ -107,7 +107,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
107
107
|
sandboxId: this._sandboxId,
|
|
108
108
|
connectedAt: Date.now(),
|
|
109
109
|
});
|
|
110
|
-
logger.log(`[
|
|
110
|
+
logger.log(`[realtime] Entered presence on session channel (sandbox=${this._sandboxId})`);
|
|
111
111
|
} catch (e) {
|
|
112
112
|
// Non-fatal — presence is used for concurrency counting, not critical path
|
|
113
113
|
logger.warn("Failed to enter presence on session channel: " + (e.message || e));
|
|
@@ -118,7 +118,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
118
118
|
var message = msg.data;
|
|
119
119
|
if (!message) return;
|
|
120
120
|
|
|
121
|
-
logger.log(`[
|
|
121
|
+
logger.log(`[realtime] Received response: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
|
|
122
122
|
|
|
123
123
|
if (message.type === "sandbox.progress") {
|
|
124
124
|
emitter.emit(events.sandbox.progress, {
|
|
@@ -178,7 +178,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
178
178
|
}).join(', ')
|
|
179
179
|
: 'none';
|
|
180
180
|
logger.warn(
|
|
181
|
-
'[
|
|
181
|
+
'[realtime] No pending promise for requestId=' + (message.requestId || 'null') +
|
|
182
182
|
' | response type=' + (message.type || 'unknown') +
|
|
183
183
|
' | error=' + (message.error ? (message.errorMessage || 'true') : 'false') +
|
|
184
184
|
' | currently pending: [' + pendingSummary + ']'
|
|
@@ -193,7 +193,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
193
193
|
? ((Date.now() - pendingEntry.startTime) / 1000).toFixed(1) + 's'
|
|
194
194
|
: '?';
|
|
195
195
|
logger.warn(
|
|
196
|
-
'[
|
|
196
|
+
'[realtime] Promise REJECTED: requestId=' + message.requestId +
|
|
197
197
|
' | type=' + (pendingMessage ? pendingMessage.type : 'unknown') +
|
|
198
198
|
' | age=' + pendingAge +
|
|
199
199
|
' | error=' + (message.errorMessage || 'Sandbox error')
|
|
@@ -213,7 +213,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
213
213
|
? ((Date.now() - resolveEntry.startTime) / 1000).toFixed(1) + 's'
|
|
214
214
|
: '?';
|
|
215
215
|
logger.log(
|
|
216
|
-
'[
|
|
216
|
+
'[realtime] Promise RESOLVED: requestId=' + message.requestId +
|
|
217
217
|
' | type=' + (resolveEntry.message ? resolveEntry.message.type : 'unknown') +
|
|
218
218
|
' | age=' + resolveAge
|
|
219
219
|
);
|
|
@@ -241,7 +241,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
241
241
|
this._onFileMsg = function (msg) {
|
|
242
242
|
var message = msg.data;
|
|
243
243
|
if (!message) return;
|
|
244
|
-
logger.log(`[
|
|
244
|
+
logger.log(`[realtime] Received file: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
|
|
245
245
|
if (message.requestId && self.ps[message.requestId]) {
|
|
246
246
|
emitter.emit(events.sandbox.received);
|
|
247
247
|
self.ps[message.requestId].resolve(message);
|
|
@@ -260,7 +260,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
260
260
|
const chState = this._sessionChannel ? this._sessionChannel.state : 'null';
|
|
261
261
|
const pendingIds = Object.keys(this.ps);
|
|
262
262
|
const pending = pendingIds.length;
|
|
263
|
-
logger.log(`[
|
|
263
|
+
logger.log(`[realtime][stats] connection=${connState} | sandbox=${this._sandboxId} | pending=${pending} | channel=${chState}`);
|
|
264
264
|
if (pending > 0) {
|
|
265
265
|
const now = Date.now();
|
|
266
266
|
for (const rid of pendingIds) {
|
|
@@ -268,20 +268,20 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
268
268
|
if (!entry) continue;
|
|
269
269
|
const type = entry.message ? entry.message.type : 'unknown';
|
|
270
270
|
const ageSec = ((now - (entry.startTime || now)) / 1000).toFixed(1);
|
|
271
|
-
logger.log(`[
|
|
271
|
+
logger.log(`[realtime][stats] pending: requestId=${rid} | type=${type} | age=${ageSec}s`);
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
274
|
}, 10000);
|
|
275
275
|
if (this._statsInterval.unref) this._statsInterval.unref();
|
|
276
276
|
|
|
277
277
|
this._ably.connection.on("disconnected", function () {
|
|
278
|
-
logger.log("[
|
|
278
|
+
logger.log("[realtime] Connection: disconnected - will auto-reconnect");
|
|
279
279
|
self._disconnectedAt = Date.now();
|
|
280
280
|
});
|
|
281
281
|
|
|
282
282
|
this._ably.connection.on("connected", function () {
|
|
283
283
|
// Log reconnection so the user knows the blip was recovered
|
|
284
|
-
logger.log("[
|
|
284
|
+
logger.log("[realtime] Connection: reconnected");
|
|
285
285
|
// Extend any pending command timeouts by the disconnection duration so
|
|
286
286
|
// commands whose timer was counting down while the connection was down
|
|
287
287
|
// don't get incorrectly timed out.
|
|
@@ -291,7 +291,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
291
291
|
var pendingIds = Object.keys(self.ps);
|
|
292
292
|
if (pendingIds.length > 0) {
|
|
293
293
|
logger.log(
|
|
294
|
-
'[
|
|
294
|
+
'[realtime] Extending ' + pendingIds.length + ' pending timeout(s) by ' +
|
|
295
295
|
disconnectionDurationMs + 'ms after disconnection'
|
|
296
296
|
);
|
|
297
297
|
for (var i = 0; i < pendingIds.length; i++) {
|
|
@@ -305,14 +305,14 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
305
305
|
});
|
|
306
306
|
|
|
307
307
|
this._ably.connection.on("suspended", function () {
|
|
308
|
-
logger.warn("[
|
|
308
|
+
logger.warn("[realtime] Connection: suspended - connection lost for extended period, will keep retrying");
|
|
309
309
|
});
|
|
310
310
|
|
|
311
311
|
this._ably.connection.on("failed", function () {
|
|
312
|
-
logger.error("[
|
|
312
|
+
logger.error("[realtime] Connection: failed");
|
|
313
313
|
self.apiSocketConnected = false;
|
|
314
314
|
self.instanceSocketConnected = false;
|
|
315
|
-
emitter.emit(events.error.sandbox, "
|
|
315
|
+
emitter.emit(events.error.sandbox, "Realtime connection failed");
|
|
316
316
|
});
|
|
317
317
|
|
|
318
318
|
// ─── Channel discontinuity detection ──────────────────────────────
|
|
@@ -326,7 +326,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
326
326
|
var reasonMsg = reason ? (reason.message || reason.code || String(reason)) : '';
|
|
327
327
|
|
|
328
328
|
if (current === 'attached' && stateChange.resumed === false && previous === 'attached') {
|
|
329
|
-
logger.warn('[
|
|
329
|
+
logger.warn('[realtime] Channel DISCONTINUITY detected (resumed=false)' + (reasonMsg ? ' — ' + reasonMsg : ''));
|
|
330
330
|
emitter.emit(events.sandbox.progress, {
|
|
331
331
|
step: 'discontinuity',
|
|
332
332
|
message: 'Recovering missed messages after connection interruption...',
|
|
@@ -353,7 +353,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
353
353
|
var entry = subs[i];
|
|
354
354
|
if (!entry.sub) continue;
|
|
355
355
|
try {
|
|
356
|
-
logger.log('[
|
|
356
|
+
logger.log('[realtime] Discontinuity recovery: fetching historyBeforeSubscribe for ' + entry.name + '...');
|
|
357
357
|
var page = await entry.sub.historyBeforeSubscribe({ limit: 100 });
|
|
358
358
|
var recovered = 0;
|
|
359
359
|
while (page) {
|
|
@@ -363,25 +363,25 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
363
363
|
recovered++;
|
|
364
364
|
try {
|
|
365
365
|
if (entry.handler) {
|
|
366
|
-
logger.log('[
|
|
366
|
+
logger.log('[realtime] Replaying recovered ' + entry.name + ' message (requestId=' + (page.items[j].data && page.items[j].data.requestId || 'none') + ')');
|
|
367
367
|
entry.handler(page.items[j]);
|
|
368
368
|
}
|
|
369
369
|
} catch (replayErr) {
|
|
370
|
-
logger.error('[
|
|
370
|
+
logger.error('[realtime] Error replaying recovered message: ' + (replayErr.message || replayErr));
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
page = page.hasNext() ? await page.next() : null;
|
|
374
374
|
}
|
|
375
375
|
totalRecovered += recovered;
|
|
376
|
-
logger.log('[
|
|
376
|
+
logger.log('[realtime] Discontinuity recovery: replayed ' + recovered + ' ' + entry.name + ' message(s) from gap');
|
|
377
377
|
} catch (err) {
|
|
378
|
-
logger.error('[
|
|
378
|
+
logger.error('[realtime] Discontinuity recovery failed for ' + entry.name + ': ' + (err.message || err));
|
|
379
379
|
}
|
|
380
380
|
}
|
|
381
381
|
if (totalRecovered > 0) {
|
|
382
|
-
logger.warn('[
|
|
382
|
+
logger.warn('[realtime] Recovered and replayed ' + totalRecovered + ' message(s) that were missed during connection interruption');
|
|
383
383
|
} else {
|
|
384
|
-
logger.log('[
|
|
384
|
+
logger.log('[realtime] Discontinuity recovery: no missed messages found');
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
387
|
|
|
@@ -809,7 +809,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
809
809
|
function onFailed() {
|
|
810
810
|
clearTimeout(timer);
|
|
811
811
|
self._ably.connection.off("connected", onConnected);
|
|
812
|
-
reject(new Error("
|
|
812
|
+
reject(new Error("Realtime connection failed while waiting to send"));
|
|
813
813
|
}
|
|
814
814
|
self._ably.connection.once("connected", onConnected);
|
|
815
815
|
self._ably.connection.once("failed", onFailed);
|
|
@@ -881,7 +881,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
881
881
|
return rid + '(' + (e && e.message ? e.message.type : '?') + ', ' + age + ')';
|
|
882
882
|
}).join(', ');
|
|
883
883
|
logger.error(
|
|
884
|
-
'[
|
|
884
|
+
'[realtime] Promise TIMEOUT: requestId=' + requestId +
|
|
885
885
|
' | type=' + message.type +
|
|
886
886
|
' | timeout=' + timeout + 'ms' +
|
|
887
887
|
' | all pending: [' + pendingSummary + ']'
|
|
@@ -930,7 +930,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
930
930
|
timeoutId = setTimeout(timeoutFn, newRemaining);
|
|
931
931
|
if (timeoutId.unref) timeoutId.unref();
|
|
932
932
|
logger.log(
|
|
933
|
-
'[
|
|
933
|
+
'[realtime] Extended timeout for requestId=' + requestId +
|
|
934
934
|
' by ' + disconnectionDurationMs + 'ms (new remaining: ' + Math.round(newRemaining / 1000) + 's)'
|
|
935
935
|
);
|
|
936
936
|
},
|
|
@@ -1002,7 +1002,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1002
1002
|
}
|
|
1003
1003
|
|
|
1004
1004
|
return channel.publish(eventName, message).then(function () {
|
|
1005
|
-
logger.log(`[
|
|
1005
|
+
logger.log(`[realtime] Published: channel=${channel.name.split(':').pop()}, event=${eventName}, type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
|
|
1006
1006
|
});
|
|
1007
1007
|
}
|
|
1008
1008
|
|
|
@@ -1132,7 +1132,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1132
1132
|
// Send end-session control message to runner before disconnecting
|
|
1133
1133
|
if (this._sessionChannel && this._ably?.connection?.state === 'connected') {
|
|
1134
1134
|
try {
|
|
1135
|
-
logger.log('[
|
|
1135
|
+
logger.log('[realtime] Publishing control: type=end-session');
|
|
1136
1136
|
await this._sessionChannel.publish('control', { type: 'end-session' });
|
|
1137
1137
|
} catch (e) {
|
|
1138
1138
|
// Ignore - best effort
|
|
@@ -1142,7 +1142,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1142
1142
|
// Leave presence on session channel
|
|
1143
1143
|
if (this._sessionChannel) {
|
|
1144
1144
|
try {
|
|
1145
|
-
logger.log('[
|
|
1145
|
+
logger.log('[realtime] Leaving presence on session channel');
|
|
1146
1146
|
await this._sessionChannel.presence.leave();
|
|
1147
1147
|
} catch (e) {
|
|
1148
1148
|
// ignore - best effort, Ably will auto-leave on disconnect
|
|
@@ -1150,7 +1150,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1150
1150
|
}
|
|
1151
1151
|
|
|
1152
1152
|
try {
|
|
1153
|
-
logger.log('[
|
|
1153
|
+
logger.log('[realtime] Detaching session channel');
|
|
1154
1154
|
if (this._sessionChannel) {
|
|
1155
1155
|
await this._sessionChannel.detach();
|
|
1156
1156
|
}
|
|
@@ -1160,7 +1160,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
1160
1160
|
|
|
1161
1161
|
if (this._ably) {
|
|
1162
1162
|
try {
|
|
1163
|
-
logger.log('[
|
|
1163
|
+
logger.log('[realtime] Closing Realtime connection');
|
|
1164
1164
|
this._ably.close();
|
|
1165
1165
|
} catch (e) {
|
|
1166
1166
|
/* ignore */
|
|
@@ -49,8 +49,8 @@ const element = await testdriver.find(description, options)
|
|
|
49
49
|
- `"any"` — No wrapping, uses the description as-is (default behavior)
|
|
50
50
|
</ParamField>
|
|
51
51
|
|
|
52
|
-
<ParamField path="zoom" type="boolean" default={
|
|
53
|
-
|
|
52
|
+
<ParamField path="zoom" type="boolean" default={true}>
|
|
53
|
+
Two-phase zoom mode for better precision in crowded UIs with many similar elements. Enabled by default.
|
|
54
54
|
</ParamField>
|
|
55
55
|
|
|
56
56
|
<ParamField path="ai" type="object">
|
|
@@ -332,14 +332,19 @@ The `timeout` option:
|
|
|
332
332
|
- Returns the element (check `element.found()` if not throwing on failure)
|
|
333
333
|
- Set to `0` to disable polling and make a single attempt
|
|
334
334
|
|
|
335
|
-
## Zoom Mode
|
|
335
|
+
## Zoom Mode
|
|
336
336
|
|
|
337
|
-
|
|
337
|
+
Zoom mode is **enabled by default**. It uses a two-phase approach for better precision when locating elements, especially in crowded UIs with many similar elements.
|
|
338
|
+
|
|
339
|
+
To disable zoom for a specific find call, pass `zoom: false`:
|
|
338
340
|
|
|
339
341
|
```javascript
|
|
340
|
-
//
|
|
341
|
-
const extensionsBtn = await testdriver.find('extensions puzzle icon in Chrome toolbar'
|
|
342
|
+
// Zoom is on by default — no option needed
|
|
343
|
+
const extensionsBtn = await testdriver.find('extensions puzzle icon in Chrome toolbar');
|
|
342
344
|
await extensionsBtn.click();
|
|
345
|
+
|
|
346
|
+
// Disable zoom for a specific call if needed
|
|
347
|
+
const largeButton = await testdriver.find('big hero button', { zoom: false });
|
|
343
348
|
```
|
|
344
349
|
|
|
345
350
|
### How Zoom Mode Works
|
|
@@ -352,22 +357,11 @@ await extensionsBtn.click();
|
|
|
352
357
|
This two-phase approach gives the AI a higher-resolution view of the target area, improving accuracy when multiple similar elements are close together.
|
|
353
358
|
|
|
354
359
|
<Tip>
|
|
355
|
-
|
|
356
|
-
-
|
|
357
|
-
-
|
|
358
|
-
- Targeting elements in dense UI areas
|
|
359
|
-
- The default locate is clicking the wrong similar element
|
|
360
|
-
- You get an AI verification rejection like "The crosshair is located in the empty space of the browser's tab bar/title bar area" — this means the initial locate was imprecise and zoom will help the AI pinpoint the correct element
|
|
360
|
+
You may want to disable zoom with `zoom: false` when:
|
|
361
|
+
- Targeting large, isolated elements where the extra precision isn't needed
|
|
362
|
+
- You want to speed up find calls in simple UIs
|
|
361
363
|
</Tip>
|
|
362
364
|
|
|
363
|
-
```javascript
|
|
364
|
-
// Without zoom - may click wrong icon in toolbar
|
|
365
|
-
const icon = await testdriver.find('settings icon');
|
|
366
|
-
|
|
367
|
-
// With zoom - better precision for crowded areas
|
|
368
|
-
const icon = await testdriver.find('settings icon', { zoom: true });
|
|
369
|
-
```
|
|
370
|
-
|
|
371
365
|
## Cache Options
|
|
372
366
|
|
|
373
367
|
Control caching behavior to optimize performance, especially when using dynamic variables in prompts.
|
package/docs/v7/find.mdx
CHANGED
|
@@ -50,8 +50,8 @@ const element = await testdriver.find(description, options)
|
|
|
50
50
|
- `"any"` — No wrapping, uses the description as-is (default behavior)
|
|
51
51
|
</ParamField>
|
|
52
52
|
|
|
53
|
-
<ParamField path="zoom" type="boolean" default={
|
|
54
|
-
|
|
53
|
+
<ParamField path="zoom" type="boolean" default={true}>
|
|
54
|
+
Two-phase zoom mode for better precision in crowded UIs with many similar elements. Enabled by default.
|
|
55
55
|
</ParamField>
|
|
56
56
|
|
|
57
57
|
<ParamField path="ai" type="object">
|
|
@@ -333,14 +333,19 @@ The `timeout` option:
|
|
|
333
333
|
- Returns the element (check `element.found()` if not throwing on failure)
|
|
334
334
|
- Set to `0` to disable polling and make a single attempt
|
|
335
335
|
|
|
336
|
-
## Zoom Mode
|
|
336
|
+
## Zoom Mode
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
Zoom mode is **enabled by default**. It uses a two-phase approach for better precision when locating elements, especially in crowded UIs with many similar elements.
|
|
339
|
+
|
|
340
|
+
To disable zoom for a specific find call, pass `zoom: false`:
|
|
339
341
|
|
|
340
342
|
```javascript
|
|
341
|
-
//
|
|
342
|
-
const extensionsBtn = await testdriver.find('extensions puzzle icon in Chrome toolbar'
|
|
343
|
+
// Zoom is on by default — no option needed
|
|
344
|
+
const extensionsBtn = await testdriver.find('extensions puzzle icon in Chrome toolbar');
|
|
343
345
|
await extensionsBtn.click();
|
|
346
|
+
|
|
347
|
+
// Disable zoom for a specific call if needed
|
|
348
|
+
const largeButton = await testdriver.find('big hero button', { zoom: false });
|
|
344
349
|
```
|
|
345
350
|
|
|
346
351
|
### How Zoom Mode Works
|
|
@@ -353,22 +358,11 @@ await extensionsBtn.click();
|
|
|
353
358
|
This two-phase approach gives the AI a higher-resolution view of the target area, improving accuracy when multiple similar elements are close together.
|
|
354
359
|
|
|
355
360
|
<Tip>
|
|
356
|
-
|
|
357
|
-
-
|
|
358
|
-
-
|
|
359
|
-
- Targeting elements in dense UI areas
|
|
360
|
-
- The default locate is clicking the wrong similar element
|
|
361
|
-
- You get an AI verification rejection like "The crosshair is located in the empty space of the browser's tab bar/title bar area" — this means the initial locate was imprecise and zoom will help the AI pinpoint the correct element
|
|
361
|
+
You may want to disable zoom with `zoom: false` when:
|
|
362
|
+
- Targeting large, isolated elements where the extra precision isn't needed
|
|
363
|
+
- You want to speed up find calls in simple UIs
|
|
362
364
|
</Tip>
|
|
363
365
|
|
|
364
|
-
```javascript
|
|
365
|
-
// Without zoom - may click wrong icon in toolbar
|
|
366
|
-
const icon = await testdriver.find('settings icon');
|
|
367
|
-
|
|
368
|
-
// With zoom - better precision for crowded areas
|
|
369
|
-
const icon = await testdriver.find('settings icon', { zoom: true });
|
|
370
|
-
```
|
|
371
|
-
|
|
372
366
|
## Cache Options
|
|
373
367
|
|
|
374
368
|
Control caching behavior to optimize performance, especially when using dynamic variables in prompts.
|
package/package.json
CHANGED
package/sdk.js
CHANGED
|
@@ -481,7 +481,7 @@ class Element {
|
|
|
481
481
|
let cacheKey = null;
|
|
482
482
|
let cacheThreshold = null;
|
|
483
483
|
let perCommandThresholds = null; // Per-command { screen, element } override
|
|
484
|
-
let zoom =
|
|
484
|
+
let zoom = true; // Default to enabled
|
|
485
485
|
let perCommandAi = null; // Per-command AI config override
|
|
486
486
|
|
|
487
487
|
let minConfidence = null; // Minimum confidence threshold
|
|
@@ -494,8 +494,8 @@ class Element {
|
|
|
494
494
|
// New: options is an object with cacheKey and/or cacheThreshold
|
|
495
495
|
cacheKey = options.cacheKey || null;
|
|
496
496
|
cacheThreshold = options.cacheThreshold ?? null;
|
|
497
|
-
// zoom defaults to
|
|
498
|
-
zoom = options.zoom
|
|
497
|
+
// zoom defaults to true unless explicitly set to false
|
|
498
|
+
zoom = options.zoom !== false;
|
|
499
499
|
// Minimum confidence threshold: fail find if AI confidence is below this value
|
|
500
500
|
minConfidence = options.confidence ?? null;
|
|
501
501
|
// Element type hint for prompt wrapping
|