pi-ui-extend 1.0.37 → 1.0.39
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/README.md +1 -0
- package/dist/app/extensions/extension-ui-controller.js +3 -0
- package/dist/app/rendering/extension-entry-renderer.js +2 -0
- package/dist/bundled-extensions/question/contract.d.ts +3 -0
- package/dist/bundled-extensions/question/contract.js +24 -0
- package/dist/bundled-extensions/question/desktop.d.ts +11 -0
- package/dist/bundled-extensions/question/desktop.js +143 -0
- package/dist/bundled-extensions/question/index.d.ts +1 -0
- package/dist/bundled-extensions/question/index.js +5 -1
- package/dist/bundled-extensions/question/render.js +6 -0
- package/dist/bundled-extensions/question/result.js +71 -9
- package/dist/bundled-extensions/question/tool-description.js +4 -3
- package/dist/bundled-extensions/question/tui.js +127 -12
- package/dist/bundled-extensions/question/types.d.ts +23 -2
- package/dist/tool-renderers/question.js +20 -1
- package/docs/desktop-markdown-media.md +77 -0
- package/docs/desktop-mvp.md +134 -0
- package/docs/desktop-task-manager.md +124 -0
- package/external/pi-tools-suite/package.json +3 -3
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +9 -1
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +41 -2
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +13 -1
- package/external/pi-tools-suite/src/async-subagents/index.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/SKILL.md +65 -22
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +23 -8
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/scripts/browser-qa-runner.mjs +180 -41
- package/external/pi-tools-suite/src/async-subagents/subagent-overlay.ts +1 -1
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +1 -1
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +1 -51
- package/external/pi-tools-suite/src/dcp/debug-log.ts +6 -0
- package/external/pi-tools-suite/src/dcp/index.ts +27 -125
- package/external/pi-tools-suite/src/dcp/prompts.ts +2 -2
- package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +2 -1
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +31 -10
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +6 -7
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +153 -111
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +28 -17
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +61 -27
- package/external/pi-tools-suite/src/dcp/pruner.ts +21 -13
- package/external/pi-tools-suite/src/dcp/state.ts +59 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +4 -0
- package/external/pi-tools-suite/src/lib/rpc-session-state.ts +34 -0
- package/external/pi-tools-suite/src/todo/todo.ts +8 -3
- package/package.json +10 -4
|
@@ -17,6 +17,7 @@ const QA_WORKSPACE_RELATIVE = "browser-qa";
|
|
|
17
17
|
const EVIDENCE_RELATIVE = "evidence";
|
|
18
18
|
const UI_READY_SETTLE_MS = 500;
|
|
19
19
|
const UI_READY_POLL_MS = 100;
|
|
20
|
+
const ACTION_REQUEST_WAIT_MAX_MS = 2_000;
|
|
20
21
|
const FORM_VIDEO_STEP_DELAY_MS = UI_READY_SETTLE_MS;
|
|
21
22
|
const POPUP_VIDEO_SETTLE_MS = 250;
|
|
22
23
|
const CLICK_VIDEO_DELAY_MS = 120;
|
|
@@ -160,7 +161,7 @@ async function main() {
|
|
|
160
161
|
const args = parseArgs(rawArgs);
|
|
161
162
|
const selected = args.profile
|
|
162
163
|
? selectProfile(readAuthConfig(cwd, true, true).profiles, args.profile)
|
|
163
|
-
: createPublicProfile(args.baseUrl);
|
|
164
|
+
: createPublicProfile(args.baseUrl, args.allowOrigin);
|
|
164
165
|
const profileId = selected.id;
|
|
165
166
|
const profile = selected.profile;
|
|
166
167
|
const secrets = collectSecrets(profile.auth);
|
|
@@ -249,6 +250,9 @@ async function runQa({ cwd, agentDir, args, profileId, profile, deadline, progre
|
|
|
249
250
|
runtimeSecrets.push(...collectStorageStateSecrets(initialStorageState));
|
|
250
251
|
await runStage(progress, "trace_start", deadline, () => context.tracing.start({ screenshots: true, snapshots: true, sources: false }), profileId);
|
|
251
252
|
await runStage(progress, "flow_execute", deadline, () => executeFlow({ context, page, baseURL, evidenceDir, flow, allowedOrigins, profileId, secrets: runtimeSecrets, observations, videos }), profileId);
|
|
253
|
+
await runStage(progress, "storage_state_refresh", deadline, async () => {
|
|
254
|
+
if (await refreshRuntimeSecrets(context, runtimeSecrets)) discardRecordedVideos(videos);
|
|
255
|
+
}, profileId);
|
|
252
256
|
await runStage(progress, "secret_exposure_check", deadline, () => assertBrowserDoesNotExposeSecrets(context, page, runtimeSecrets, profileId), profileId);
|
|
253
257
|
await runStage(progress, "final_screenshot", deadline, () => page.screenshot({ path: path.join(evidenceDir, "final.png"), fullPage: true }), profileId);
|
|
254
258
|
} catch (error) {
|
|
@@ -256,15 +260,25 @@ async function runQa({ cwd, agentDir, args, profileId, profile, deadline, progre
|
|
|
256
260
|
failure = error;
|
|
257
261
|
if (page && !page.isClosed() && !error.suppressEvidence) {
|
|
258
262
|
try {
|
|
263
|
+
await runCleanupStage(progress, "failure_storage_state_refresh", async () => {
|
|
264
|
+
if (await refreshRuntimeSecrets(context, runtimeSecrets)) discardRecordedVideos(videos);
|
|
265
|
+
});
|
|
266
|
+
await runCleanupStage(progress, "failure_secret_exposure_check", () => assertBrowserDoesNotExposeSecrets(context, page, runtimeSecrets, profileId));
|
|
259
267
|
await runCleanupStage(progress, "failure_screenshot", () => page.screenshot({ path: path.join(evidenceDir, "failure.png"), fullPage: true }));
|
|
260
|
-
} catch {
|
|
268
|
+
} catch (evidenceError) {
|
|
269
|
+
if (evidenceError instanceof QaStatusError && evidenceError.suppressEvidence) {
|
|
270
|
+
failure = evidenceError;
|
|
271
|
+
outcome = evidenceError.status;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
261
274
|
}
|
|
262
275
|
} finally {
|
|
263
276
|
const cleanupDeadline = Date.now() + CLEANUP_TIMEOUT_MS;
|
|
277
|
+
let discoveredLateSecrets = false;
|
|
264
278
|
if (context) {
|
|
265
279
|
try {
|
|
266
280
|
const finalStorageState = await runCleanupStage(progress, "storage_state_finalize", () => context.storageState(), cleanupDeadline);
|
|
267
|
-
runtimeSecrets
|
|
281
|
+
discoveredLateSecrets = mergeRuntimeSecrets(runtimeSecrets, collectStorageStateSecrets(finalStorageState));
|
|
268
282
|
} catch { /* best effort */ }
|
|
269
283
|
try {
|
|
270
284
|
await runCleanupStage(progress, "trace_stop", () => context.tracing.stop({ path: tracePath }), cleanupDeadline);
|
|
@@ -293,6 +307,14 @@ async function runQa({ cwd, agentDir, args, profileId, profile, deadline, progre
|
|
|
293
307
|
} catch { /* best effort */ }
|
|
294
308
|
}
|
|
295
309
|
await runCleanupStage(progress, "browser_close", () => browser.close(), cleanupDeadline).catch(() => {});
|
|
310
|
+
if (discoveredLateSecrets) {
|
|
311
|
+
removeVisualEvidence(evidenceDir);
|
|
312
|
+
if (!failure) {
|
|
313
|
+
outcome = "failed";
|
|
314
|
+
failure = new QaStatusError("QA_RUN_FAILED", "authentication changed after the final evidence safety check; visual evidence was discarded", 1, profileId);
|
|
315
|
+
failure.suppressEvidence = true;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
296
318
|
}
|
|
297
319
|
try {
|
|
298
320
|
for (const name of fs.readdirSync(evidenceDir).filter((name) => /^download-[A-Za-z0-9._-]+\.bin$/.test(name))) {
|
|
@@ -886,23 +908,29 @@ function authPlaceholderCount(profile) {
|
|
|
886
908
|
)).length;
|
|
887
909
|
}
|
|
888
910
|
|
|
889
|
-
function createPublicProfile(rawBaseUrl) {
|
|
911
|
+
function createPublicProfile(rawBaseUrl, extraAllowedOrigins = []) {
|
|
890
912
|
if (typeof rawBaseUrl !== "string") {
|
|
891
913
|
throw new QaStatusError("QA_RUN_FAILED", "--base-url is required when running without --profile", 1, "public");
|
|
892
914
|
}
|
|
893
915
|
try {
|
|
894
916
|
const url = new URL(rawBaseUrl);
|
|
895
917
|
if (!/^https?:$/.test(url.protocol) || url.username || url.password) throw new Error();
|
|
918
|
+
const allowedOrigins = [url.origin];
|
|
919
|
+
for (const raw of Array.isArray(extraAllowedOrigins) ? extraAllowedOrigins : []) {
|
|
920
|
+
const extra = new URL(raw);
|
|
921
|
+
if (!/^https?:$/.test(extra.protocol) || extra.username || extra.password || extra.href !== `${extra.origin}/`) throw new Error();
|
|
922
|
+
allowedOrigins.push(extra.origin);
|
|
923
|
+
}
|
|
896
924
|
return {
|
|
897
925
|
id: "public",
|
|
898
926
|
profile: {
|
|
899
927
|
baseUrl: url.href,
|
|
900
|
-
allowedOrigins: [
|
|
928
|
+
allowedOrigins: [...new Set(allowedOrigins)],
|
|
901
929
|
auth: { type: "none" },
|
|
902
930
|
},
|
|
903
931
|
};
|
|
904
932
|
} catch {
|
|
905
|
-
throw new QaStatusError("QA_RUN_FAILED", "public --base-url must be
|
|
933
|
+
throw new QaStatusError("QA_RUN_FAILED", "public --base-url and --allow-origin values must be http(s) URLs without credentials; --allow-origin must be an exact origin", 1, "public");
|
|
906
934
|
}
|
|
907
935
|
}
|
|
908
936
|
|
|
@@ -1050,6 +1078,15 @@ async function executeFlow({ context, page, baseURL, evidenceDir, flow, allowedO
|
|
|
1050
1078
|
let capturedPopup;
|
|
1051
1079
|
let unexpectedPopupError;
|
|
1052
1080
|
const readinessTrackers = new Map();
|
|
1081
|
+
let discardFutureVideos = false;
|
|
1082
|
+
const refreshSecrets = async () => {
|
|
1083
|
+
const changed = await refreshRuntimeSecrets(context, secrets);
|
|
1084
|
+
if (changed) {
|
|
1085
|
+
discardFutureVideos = true;
|
|
1086
|
+
discardRecordedVideos(videos);
|
|
1087
|
+
}
|
|
1088
|
+
return changed;
|
|
1089
|
+
};
|
|
1053
1090
|
const readinessFor = (candidate) => {
|
|
1054
1091
|
if (!readinessTrackers.has(candidate)) readinessTrackers.set(candidate, createPageReadinessTracker(candidate));
|
|
1055
1092
|
return readinessTrackers.get(candidate);
|
|
@@ -1078,6 +1115,9 @@ async function executeFlow({ context, page, baseURL, evidenceDir, flow, allowedO
|
|
|
1078
1115
|
}
|
|
1079
1116
|
const { scope, ownerPage } = await resolveStepTarget({ page, popups, step, allowedOrigins, profileId, index });
|
|
1080
1117
|
const locator = step.locator === undefined ? undefined : resolveLocator(scope, step.locator, profileId, index);
|
|
1118
|
+
const readinessTracker = readinessFor(ownerPage);
|
|
1119
|
+
const requestScope = UI_READY_ACTIONS.has(step.action) ? readinessTracker.beginActionScope() : undefined;
|
|
1120
|
+
try {
|
|
1081
1121
|
switch (step.action) {
|
|
1082
1122
|
case "goto": {
|
|
1083
1123
|
const target = typeof step.url === "string" ? step.url : step.path;
|
|
@@ -1149,7 +1189,7 @@ async function executeFlow({ context, page, baseURL, evidenceDir, flow, allowedO
|
|
|
1149
1189
|
[, popup] = await Promise.all([requireLocator(locator, profileId, index).click({ delay: CLICK_VIDEO_DELAY_MS }), popupPromise]);
|
|
1150
1190
|
popup.setDefaultTimeout(timeout);
|
|
1151
1191
|
popup.setDefaultNavigationTimeout(timeout);
|
|
1152
|
-
popupVideo = { name: `video-popup-${step.name}.webm`, video: popup.video(), discard:
|
|
1192
|
+
popupVideo = { name: `video-popup-${step.name}.webm`, video: popup.video(), discard: discardFutureVideos };
|
|
1153
1193
|
videos.push(popupVideo);
|
|
1154
1194
|
await popup.waitForLoadState("domcontentloaded", { timeout });
|
|
1155
1195
|
await waitForUiReady({
|
|
@@ -1234,6 +1274,15 @@ async function executeFlow({ context, page, baseURL, evidenceDir, flow, allowedO
|
|
|
1234
1274
|
const target = requireLocator(locator, profileId, index);
|
|
1235
1275
|
await retryAssertion({
|
|
1236
1276
|
page: ownerPage, timeout, profileId, index, reason: "text assertion failed",
|
|
1277
|
+
check: async () => (await target.isVisible()) && matches(await target.innerText()),
|
|
1278
|
+
});
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
case "assertTextContent": {
|
|
1282
|
+
const matches = expectedStringMatcher(step, profileId, index, "text content");
|
|
1283
|
+
const target = requireLocator(locator, profileId, index);
|
|
1284
|
+
await retryAssertion({
|
|
1285
|
+
page: ownerPage, timeout, profileId, index, reason: "text content assertion failed",
|
|
1237
1286
|
check: async () => matches((await target.textContent()) ?? ""),
|
|
1238
1287
|
});
|
|
1239
1288
|
break;
|
|
@@ -1290,6 +1339,7 @@ async function executeFlow({ context, page, baseURL, evidenceDir, flow, allowedO
|
|
|
1290
1339
|
break;
|
|
1291
1340
|
}
|
|
1292
1341
|
case "authRejectedIf": {
|
|
1342
|
+
if (typeof step.urlIncludes !== "string" && !locator) throw flowError(profileId, index, "authRejectedIf requires urlIncludes or locator");
|
|
1293
1343
|
const urlRejected = typeof step.urlIncludes === "string" && scope.url().includes(step.urlIncludes);
|
|
1294
1344
|
const locatorRejected = locator ? await locator.isVisible().catch(() => false) : false;
|
|
1295
1345
|
if (!urlRejected && !locatorRejected) break;
|
|
@@ -1298,21 +1348,34 @@ async function executeFlow({ context, page, baseURL, evidenceDir, flow, allowedO
|
|
|
1298
1348
|
case "screenshot": {
|
|
1299
1349
|
const name = typeof step.name === "string" ? step.name : `step-${index + 1}`;
|
|
1300
1350
|
if (!isSafeName(name)) throw flowError(profileId, index, "screenshot name is invalid");
|
|
1351
|
+
await refreshSecrets();
|
|
1301
1352
|
await assertBrowserDoesNotExposeSecrets(context, page, secrets, profileId);
|
|
1302
1353
|
await ownerPage.screenshot({ path: path.join(evidenceDir, `${name}.png`), fullPage: step.fullPage !== false });
|
|
1303
1354
|
break;
|
|
1304
1355
|
}
|
|
1305
1356
|
default: throw flowError(profileId, index, `unsupported action: ${step.action}`);
|
|
1306
1357
|
}
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
requestScope?.stopCapture();
|
|
1360
|
+
requestScope?.dispose();
|
|
1361
|
+
throw error;
|
|
1362
|
+
}
|
|
1307
1363
|
if (UI_READY_ACTIONS.has(step.action)) {
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1364
|
+
try {
|
|
1365
|
+
await waitForUiReady({
|
|
1366
|
+
scope,
|
|
1367
|
+
tracker: readinessTracker,
|
|
1368
|
+
requestScope,
|
|
1369
|
+
timeout,
|
|
1370
|
+
failure: () => flowError(profileId, index, "page did not finish loading"),
|
|
1371
|
+
});
|
|
1372
|
+
} finally {
|
|
1373
|
+
requestScope?.stopCapture();
|
|
1374
|
+
requestScope?.dispose();
|
|
1375
|
+
}
|
|
1314
1376
|
}
|
|
1315
1377
|
if (unexpectedPopupError) throw unexpectedPopupError;
|
|
1378
|
+
await refreshSecrets();
|
|
1316
1379
|
await assertBrowserDoesNotExposeSecrets(context, page, secrets, profileId);
|
|
1317
1380
|
}
|
|
1318
1381
|
} finally {
|
|
@@ -1323,15 +1386,36 @@ async function executeFlow({ context, page, baseURL, evidenceDir, flow, allowedO
|
|
|
1323
1386
|
|
|
1324
1387
|
function createPageReadinessTracker(page) {
|
|
1325
1388
|
const pendingRequests = new Set();
|
|
1326
|
-
const
|
|
1327
|
-
const
|
|
1389
|
+
const actionScopes = new Set();
|
|
1390
|
+
const onRequest = (request) => {
|
|
1391
|
+
if (isLongLivedRequest(request)) return;
|
|
1392
|
+
pendingRequests.add(request);
|
|
1393
|
+
for (const scope of actionScopes) {
|
|
1394
|
+
if (scope.capturing) scope.pendingRequests.add(request);
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
const onRequestDone = (request) => {
|
|
1398
|
+
pendingRequests.delete(request);
|
|
1399
|
+
for (const scope of actionScopes) scope.pendingRequests.delete(request);
|
|
1400
|
+
};
|
|
1328
1401
|
page.on("request", onRequest);
|
|
1329
1402
|
page.on("requestfinished", onRequestDone);
|
|
1330
1403
|
page.on("requestfailed", onRequestDone);
|
|
1331
1404
|
return {
|
|
1332
1405
|
page,
|
|
1333
1406
|
pendingRequests,
|
|
1407
|
+
beginActionScope() {
|
|
1408
|
+
const scope = {
|
|
1409
|
+
pendingRequests: new Set(),
|
|
1410
|
+
capturing: true,
|
|
1411
|
+
stopCapture() { scope.capturing = false; },
|
|
1412
|
+
dispose() { actionScopes.delete(scope); },
|
|
1413
|
+
};
|
|
1414
|
+
actionScopes.add(scope);
|
|
1415
|
+
return scope;
|
|
1416
|
+
},
|
|
1334
1417
|
dispose() {
|
|
1418
|
+
actionScopes.clear();
|
|
1335
1419
|
page.off("request", onRequest);
|
|
1336
1420
|
page.off("requestfinished", onRequestDone);
|
|
1337
1421
|
page.off("requestfailed", onRequestDone);
|
|
@@ -1339,8 +1423,18 @@ function createPageReadinessTracker(page) {
|
|
|
1339
1423
|
};
|
|
1340
1424
|
}
|
|
1341
1425
|
|
|
1342
|
-
|
|
1426
|
+
function isLongLivedRequest(request) {
|
|
1427
|
+
try {
|
|
1428
|
+
return ["eventsource", "websocket"].includes(request.resourceType?.());
|
|
1429
|
+
} catch {
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
async function waitForUiReady({ scope, tracker, requestScope = undefined, timeout, failure }) {
|
|
1343
1435
|
const deadline = Date.now() + timeout;
|
|
1436
|
+
const pendingRequests = requestScope?.pendingRequests ?? tracker.pendingRequests;
|
|
1437
|
+
const requestDeadline = Date.now() + Math.min(ACTION_REQUEST_WAIT_MAX_MS, Math.max(0, timeout - UI_READY_SETTLE_MS));
|
|
1344
1438
|
try {
|
|
1345
1439
|
await scope.waitForLoadState("domcontentloaded", { timeout });
|
|
1346
1440
|
} catch {
|
|
@@ -1350,10 +1444,12 @@ async function waitForUiReady({ scope, tracker, timeout, failure }) {
|
|
|
1350
1444
|
const remaining = deadline - Date.now();
|
|
1351
1445
|
if (remaining <= 0) throw failure();
|
|
1352
1446
|
const hasBusyIndicator = await hasVisibleBusyIndicator(scope);
|
|
1353
|
-
|
|
1447
|
+
const requestsStillBlock = pendingRequests.size > 0 && Date.now() < requestDeadline;
|
|
1448
|
+
if (!requestsStillBlock && !hasBusyIndicator) {
|
|
1354
1449
|
await tracker.page.waitForTimeout(Math.min(UI_READY_SETTLE_MS, remaining));
|
|
1355
1450
|
const stillBusy = await hasVisibleBusyIndicator(scope);
|
|
1356
|
-
|
|
1451
|
+
const requestsStillBlockAfterSettle = pendingRequests.size > 0 && Date.now() < requestDeadline;
|
|
1452
|
+
if (!requestsStillBlockAfterSettle && !stillBusy) return;
|
|
1357
1453
|
continue;
|
|
1358
1454
|
}
|
|
1359
1455
|
await tracker.page.waitForTimeout(Math.min(UI_READY_POLL_MS, remaining));
|
|
@@ -1813,13 +1909,20 @@ async function applyFormAuth(page, auth, allowedOrigins, profileId) {
|
|
|
1813
1909
|
const timeout = Math.min(finitePositive(auth.timeoutMs) ?? 15_000, 60_000);
|
|
1814
1910
|
page.setDefaultTimeout(timeout);
|
|
1815
1911
|
page.setDefaultNavigationTimeout(timeout);
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1912
|
+
const loginRequestScope = readinessTracker.beginActionScope();
|
|
1913
|
+
try {
|
|
1914
|
+
await page.goto(auth.loginUrl, { timeout });
|
|
1915
|
+
await waitForUiReady({
|
|
1916
|
+
scope: page,
|
|
1917
|
+
tracker: readinessTracker,
|
|
1918
|
+
requestScope: loginRequestScope,
|
|
1919
|
+
timeout,
|
|
1920
|
+
failure: () => authError(profileId, "form page did not finish loading"),
|
|
1921
|
+
});
|
|
1922
|
+
} finally {
|
|
1923
|
+
loginRequestScope.stopCapture();
|
|
1924
|
+
loginRequestScope.dispose();
|
|
1925
|
+
}
|
|
1823
1926
|
for (const field of auth.fields) {
|
|
1824
1927
|
if (!isObject(field) || typeof field.selector !== "string" || typeof field.value !== "string") {
|
|
1825
1928
|
throw authError(profileId, "form fields must contain selector/value strings");
|
|
@@ -1827,20 +1930,27 @@ async function applyFormAuth(page, auth, allowedOrigins, profileId) {
|
|
|
1827
1930
|
await page.locator(field.selector).fill(field.value);
|
|
1828
1931
|
await page.waitForTimeout(FORM_VIDEO_STEP_DELAY_MS);
|
|
1829
1932
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1933
|
+
const submitRequestScope = readinessTracker.beginActionScope();
|
|
1934
|
+
try {
|
|
1935
|
+
await page.locator(auth.submitSelector).click({ delay: CLICK_VIDEO_DELAY_MS });
|
|
1936
|
+
if (isObject(auth.success) && typeof auth.success.url === "string") await page.waitForURL(auth.success.url, { timeout });
|
|
1937
|
+
if (isObject(auth.success) && typeof auth.success.selector === "string") {
|
|
1938
|
+
await page.locator(auth.success.selector).waitFor({ timeout, state: validLocatorState(auth.success.state) });
|
|
1939
|
+
}
|
|
1940
|
+
if (!isObject(auth.success) || (typeof auth.success.url !== "string" && typeof auth.success.selector !== "string")) {
|
|
1941
|
+
throw authError(profileId, "form success.url or success.selector is required");
|
|
1942
|
+
}
|
|
1943
|
+
await waitForUiReady({
|
|
1944
|
+
scope: page,
|
|
1945
|
+
tracker: readinessTracker,
|
|
1946
|
+
requestScope: submitRequestScope,
|
|
1947
|
+
timeout,
|
|
1948
|
+
failure: () => authError(profileId, "authenticated page did not finish loading"),
|
|
1949
|
+
});
|
|
1950
|
+
} finally {
|
|
1951
|
+
submitRequestScope.stopCapture();
|
|
1952
|
+
submitRequestScope.dispose();
|
|
1837
1953
|
}
|
|
1838
|
-
await waitForUiReady({
|
|
1839
|
-
scope: page,
|
|
1840
|
-
tracker: readinessTracker,
|
|
1841
|
-
timeout,
|
|
1842
|
-
failure: () => authError(profileId, "authenticated page did not finish loading"),
|
|
1843
|
-
});
|
|
1844
1954
|
} catch (error) {
|
|
1845
1955
|
if (error instanceof QaStatusError) throw error;
|
|
1846
1956
|
throw authError(profileId, "form login was rejected or did not reach the configured success condition");
|
|
@@ -1931,7 +2041,10 @@ function cookieAllowed(cookie, allowedOrigins) {
|
|
|
1931
2041
|
if (typeof cookie.url === "string") return isAllowedUrl(cookie.url, allowedOrigins);
|
|
1932
2042
|
if (typeof cookie.domain !== "string") return false;
|
|
1933
2043
|
const domain = cookie.domain.replace(/^\./, "").toLowerCase();
|
|
1934
|
-
return allowedOrigins.some((origin) =>
|
|
2044
|
+
return allowedOrigins.some((origin) => {
|
|
2045
|
+
const host = new URL(origin).hostname.toLowerCase();
|
|
2046
|
+
return host === domain || (domain.includes(".") && host.endsWith(`.${domain}`));
|
|
2047
|
+
});
|
|
1935
2048
|
}
|
|
1936
2049
|
|
|
1937
2050
|
function loadPlaywright(cwd) {
|
|
@@ -1990,13 +2103,18 @@ function findPackageJson(startFile) {
|
|
|
1990
2103
|
}
|
|
1991
2104
|
|
|
1992
2105
|
function parseArgs(values) {
|
|
1993
|
-
const result = {};
|
|
2106
|
+
const result = { allowOrigin: [] };
|
|
1994
2107
|
for (let index = 0; index < values.length; index += 2) {
|
|
1995
2108
|
const flag = values[index];
|
|
1996
2109
|
const value = values[index + 1];
|
|
1997
2110
|
if (!flag?.startsWith("--") || value === undefined) throw new QaStatusError("QA_RUN_FAILED", `invalid argument: ${flag ?? "(missing)"}`, 1);
|
|
1998
2111
|
const key = flag.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
1999
|
-
if (!["profile", "flow", "baseUrl", "runId", "runnerTimeoutMs"].includes(key)) throw new QaStatusError("QA_RUN_FAILED", `unknown option: ${flag}`, 1);
|
|
2112
|
+
if (!["profile", "flow", "baseUrl", "allowOrigin", "runId", "runnerTimeoutMs"].includes(key)) throw new QaStatusError("QA_RUN_FAILED", `unknown option: ${flag}`, 1);
|
|
2113
|
+
if (key === "allowOrigin") {
|
|
2114
|
+
result.allowOrigin.push(value);
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
if (result[key] !== undefined) throw new QaStatusError("QA_RUN_FAILED", `duplicate option: ${flag}`, 1);
|
|
2000
2118
|
result[key] = value;
|
|
2001
2119
|
}
|
|
2002
2120
|
return result;
|
|
@@ -2251,6 +2369,23 @@ function collectStorageStateSecrets(state) {
|
|
|
2251
2369
|
return expandSecretForms(output);
|
|
2252
2370
|
}
|
|
2253
2371
|
|
|
2372
|
+
async function refreshRuntimeSecrets(context, runtimeSecrets) {
|
|
2373
|
+
if (!context || typeof context.storageState !== "function") return false;
|
|
2374
|
+
return mergeRuntimeSecrets(runtimeSecrets, collectStorageStateSecrets(await context.storageState()));
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
function mergeRuntimeSecrets(runtimeSecrets, candidates) {
|
|
2378
|
+
const known = new Set(runtimeSecrets);
|
|
2379
|
+
let added = false;
|
|
2380
|
+
for (const candidate of candidates) {
|
|
2381
|
+
if (!candidate || known.has(candidate)) continue;
|
|
2382
|
+
known.add(candidate);
|
|
2383
|
+
runtimeSecrets.push(candidate);
|
|
2384
|
+
added = true;
|
|
2385
|
+
}
|
|
2386
|
+
return added;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2254
2389
|
function expandSecretForms(values) {
|
|
2255
2390
|
const forms = new Set();
|
|
2256
2391
|
for (const value of values) {
|
|
@@ -2570,6 +2705,10 @@ function removeVisualEvidence(evidenceDir) {
|
|
|
2570
2705
|
}
|
|
2571
2706
|
}
|
|
2572
2707
|
|
|
2708
|
+
function discardRecordedVideos(videos) {
|
|
2709
|
+
for (const entry of videos) entry.discard = true;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2573
2712
|
function writeJsonPrivate(file, value) {
|
|
2574
2713
|
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
2575
2714
|
fs.chmodSync(file, 0o600);
|
|
@@ -23,7 +23,7 @@ export class SubagentOverlay {
|
|
|
23
23
|
if (liveRun.has(agent.id)) continue;
|
|
24
24
|
const agentDir = path.join(runDir, agent.id);
|
|
25
25
|
const agentParentSession = readParentSessionLink(agentDir);
|
|
26
|
-
if (parentSession && agentParentSession
|
|
26
|
+
if (parentSession && (!agentParentSession || !pathsEqual(parentSession, agentParentSession))) continue;
|
|
27
27
|
liveRun.set(agent.id, { runDir, agentId: agent.id, parentSession: agentParentSession, completed: Promise.resolve() });
|
|
28
28
|
}
|
|
29
29
|
if (liveRun.size === 0) this.liveAgents.delete(runDir);
|
|
@@ -116,7 +116,7 @@ export function buildProgrammaticSummary(
|
|
|
116
116
|
return lines.join("\n")
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const SUMMARIZER_SYSTEM_PROMPT = `You summarize a slice of a coding agent's conversation so it can replace the raw messages in context. Produce a dense, continuation-focused summary: preserve user intent, decisions made, files/symbols changed or inspected, exact errors still actionable, verification status, and next steps. Do not infer, invent, or add facts absent from the source; preserve uncertainty instead of filling gaps. Drop full logs, repeated output, and incidental detail. Be concise (roughly 4-10 bullets). Output ONLY the summary text, no preamble.`
|
|
119
|
+
const SUMMARIZER_SYSTEM_PROMPT = `You summarize a slice of a coding agent's conversation so it can replace the raw messages in context. Produce a dense, continuation-focused summary: preserve user intent, decisions made, files/symbols changed or inspected, exact errors still actionable, verification status, and next steps. Preserve exact identifiers and explicit continuity markers verbatim, including uppercase labels before colons; never paraphrase or omit those labels. Do not infer, invent, or add facts absent from the source; preserve uncertainty instead of filling gaps. Drop full logs, repeated output, and incidental detail without quoting or naming the discarded log lines or their markers. Be concise (roughly 4-10 bullets). Output ONLY the summary text, no preamble.`
|
|
120
120
|
|
|
121
121
|
/** Outcome of one summarizer-model attempt, surfaced in DCP debug logs. */
|
|
122
122
|
export interface ModelSummaryAttempt {
|
|
@@ -388,55 +388,6 @@ export function resolveIdToBoundary(
|
|
|
388
388
|
const ts = state.messageIdSnapshot.get(id)
|
|
389
389
|
if (ts !== undefined) return { timestamp: ts }
|
|
390
390
|
|
|
391
|
-
// ── Stale mNNN fallback: direction-only clamp ────────────────────────
|
|
392
|
-
// When compression or pruning removes messages between context passes,
|
|
393
|
-
// positional mNNN IDs shift (e.g. an end boundary m145 becomes m123
|
|
394
|
-
// after 22 messages are removed). Clamp to the closest valid ID, but
|
|
395
|
-
// ONLY in a direction that preserves the range's semantics:
|
|
396
|
-
// - start boundary: clamp upward to the first available ID at or after
|
|
397
|
-
// the requested number. The start must never move backwards into
|
|
398
|
-
// older content; if no such ID exists, the requested start is gone
|
|
399
|
-
// and we cannot safely compress — throw.
|
|
400
|
-
// - end boundary: clamp downward to the last available ID at or before
|
|
401
|
-
// the requested number. The end must never move forwards into newer
|
|
402
|
-
// content; if no such ID exists, throw.
|
|
403
|
-
// The previous implementation fell back to the highest available ID in
|
|
404
|
-
// both "no match" cases, which could clamp e.g. m010..m010 over a
|
|
405
|
-
// snapshot of m001..m003 to a single-message block over m003 — silently
|
|
406
|
-
// compressing the wrong content.
|
|
407
|
-
const mMatch = id.match(/^m(\d+)$/i)
|
|
408
|
-
if (mMatch && state.messageIdSnapshot.size > 0) {
|
|
409
|
-
const requestedNum = parseInt(mMatch[1]!, 10)
|
|
410
|
-
const allIds = sortIds([...state.messageIdSnapshot.keys()])
|
|
411
|
-
const allNums = allIds
|
|
412
|
-
.map((mid) => {
|
|
413
|
-
const n = mid.match(/^m(\d+)$/i)
|
|
414
|
-
return n ? { id: mid, num: parseInt(n[1]!, 10) } : null
|
|
415
|
-
})
|
|
416
|
-
.filter((entry): entry is { id: string; num: number } => entry !== null)
|
|
417
|
-
.sort((a, b) => a.num - b.num)
|
|
418
|
-
|
|
419
|
-
if (allNums.length > 0) {
|
|
420
|
-
let clamped: { id: string; num: number } | undefined
|
|
421
|
-
if (field === "startTimestamp") {
|
|
422
|
-
clamped = allNums.find((entry) => entry.num >= requestedNum)
|
|
423
|
-
} else {
|
|
424
|
-
for (let i = allNums.length - 1; i >= 0; i--) {
|
|
425
|
-
if (allNums[i]!.num <= requestedNum) {
|
|
426
|
-
clamped = allNums[i]
|
|
427
|
-
break
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
if (clamped) {
|
|
432
|
-
const clampedMeta = state.messageMetaSnapshot.get(clamped.id)
|
|
433
|
-
if (clampedMeta) return resolveMetaBoundary(clampedMeta, field, state)
|
|
434
|
-
const clampedTs = state.messageIdSnapshot.get(clamped.id)
|
|
435
|
-
if (clampedTs !== undefined) return { timestamp: clampedTs }
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
391
|
throw unknownCompressionIdError(id, state)
|
|
441
392
|
}
|
|
442
393
|
|
|
@@ -445,8 +396,7 @@ export function resolveIdToBoundary(
|
|
|
445
396
|
* synthetic placeholder for an active compression block (`meta.blockId` is
|
|
446
397
|
* set), resolve to that block's stored boundary so the caller rolls the
|
|
447
398
|
* block up instead of nesting a new block on top of the placeholder. This
|
|
448
|
-
*
|
|
449
|
-
* may itself represent a previously compressed section.
|
|
399
|
+
* A model-visible mNNN may itself represent a previously compressed section.
|
|
450
400
|
*/
|
|
451
401
|
function resolveMetaBoundary(
|
|
452
402
|
meta: MessageIdMeta,
|
|
@@ -127,7 +127,13 @@ export function summarizeDcpState(state: DcpState): Record<string, unknown> {
|
|
|
127
127
|
nextBlockId: state.nextBlockId,
|
|
128
128
|
},
|
|
129
129
|
inactiveBlocksTail: inactiveBlocks,
|
|
130
|
+
persistentMessageIds: state.messageIdsByStableId.size,
|
|
131
|
+
nextMessageId: state.nextMessageId,
|
|
130
132
|
prunedTools: state.prunedToolIds.size,
|
|
133
|
+
automaticPruneCheckpoint: {
|
|
134
|
+
turn: state.lastAutomaticPruneTurn,
|
|
135
|
+
blockId: state.lastAutomaticPruneBlockId,
|
|
136
|
+
},
|
|
131
137
|
providerSeenTools: state.providerSeenToolIds.size,
|
|
132
138
|
consecutiveEmergencyPasses: state.consecutiveIgnoredStrongNudges,
|
|
133
139
|
nudgeAnchors: state.nudgeAnchors.map((anchor) => ({
|