pi-ui-extend 1.0.21 → 1.0.22

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.
@@ -247,10 +247,10 @@ For an oh-my-openagent-style workflow, run `/ultrawork` or `/ulw` to ask the par
247
247
 
248
248
  ### Private browser QA and project auth
249
249
 
250
- The built-in `browser-qa` role runs on `antigravity/gemini-3-flash-preview`, with
251
- `openai-codex/gpt-5.4-mini` as its fallback. Its browser workflow is an explicit
252
- private skill under `src/async-subagents/private-skills/`, outside normal Pi skill
253
- discovery. The role's first-class `isolatedSkills` setting launches the child with
250
+ The built-in `browser-qa` role runs on `openai-codex/gpt-5.4-mini`, with
251
+ `antigravity/gemini-3-flash-preview` and then `zai/glm-5.3` as fallbacks. Its browser
252
+ workflow is an explicit private skill under `src/async-subagents/private-skills/`,
253
+ outside normal Pi skill discovery. The role's first-class `isolatedSkills` setting launches the child with
254
254
  `--no-skills` plus one self-contained private workflow. It bundles the relevant
255
255
  scenario-design, locator, waiting, assertion, evidence, and cleanup guidance next
256
256
  to its trusted runner, so browser QA does not depend on a separately installed
@@ -288,10 +288,13 @@ blocked. Example:
288
288
  }
289
289
  ```
290
290
 
291
- Do not place credential values in prompts, QA flows, shell arguments, reports,
292
- or evidence. The helper reads JSONC internally, emits only redacted statuses, and
293
- caches generated storage state under `.pi/qa-auth-state`. The launcher provides
294
- each browser QA process with its own
291
+ Do not place credential values in prompts, QA flows, shell arguments, or reports.
292
+ The helper reads JSONC internally and emits only redacted statuses. For form auth,
293
+ video recording begins on the login page and captures the field-filling and submit
294
+ sequence; password inputs remain browser-masked, but the private video may show
295
+ other visible login identifiers and must be treated as sensitive evidence. Tracing
296
+ starts only after login succeeds and is sanitized before retention. The launcher
297
+ provides each browser QA process with its own
295
298
  `.pi/subagents/<run>/<agent-id>/browser-qa/` workspace. Declarative flows,
296
299
  screenshots, video, sanitized traces, and result manifests stay there, so normal
297
300
  session shutdown or `subagents cleanup` removes them with the run directory.
@@ -116,7 +116,7 @@
116
116
  "research": { "model": "zai/glm-5-turbo", "thinking": "low" },
117
117
  "docs": { "model": "zai/glm-4.5-air", "thinking": "low" },
118
118
  "frontend": { "model": "antigravity/gemini-3-flash-preview", "fallbackModels": ["zai/glm-5.3"], "thinking": "medium" },
119
- "browser-qa": { "model": "antigravity/gemini-3-flash-preview", "fallbackModels": ["openai-codex/gpt-5.4-mini"], "thinking": "medium" },
119
+ "browser-qa": { "model": "openai-codex/gpt-5.4-mini", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "medium" },
120
120
  "tests": { "model": "zai/glm-5-turbo", "thinking": "medium" },
121
121
  "review": { "model": "zai/glm-5.3", "thinking": "high" },
122
122
  "implement": { "model": "zai/glm-5.3", "thinking": "high" },
@@ -132,7 +132,7 @@
132
132
  "research": { "model": "openai-codex/gpt-5.6-terra", "fallbackModels": ["zai/glm-5-turbo"], "thinking": "low" },
133
133
  "docs": { "model": "openai-codex/gpt-5.6-luna", "fallbackModels": ["zai/glm-4.5-air"], "thinking": "low" },
134
134
  "frontend": { "model": "openai-codex/gpt-5.6-terra", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "medium" },
135
- "browser-qa": { "model": "antigravity/gemini-3-flash-preview", "fallbackModels": ["openai-codex/gpt-5.4-mini"], "thinking": "medium" },
135
+ "browser-qa": { "model": "openai-codex/gpt-5.4-mini", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "medium" },
136
136
  "tests": { "model": "openai-codex/gpt-5.6-terra", "fallbackModels": ["zai/glm-5-turbo"], "thinking": "medium" },
137
137
  "review": { "model": "openai-codex/gpt-5.6-sol", "fallbackModels": ["zai/glm-5.3"], "thinking": "high" },
138
138
  "implement": { "model": "openai-codex/gpt-5.6-sol", "fallbackModels": ["zai/glm-5.3"], "thinking": "high" },
@@ -148,7 +148,7 @@
148
148
  "research": { "model": "antigravity/gemini-3.1-pro-preview", "fallbackModels": ["openai-codex/gpt-5.4-mini", "zai/glm-5-turbo"], "thinking": "medium" },
149
149
  "docs": { "model": "antigravity/gemini-2.5-flash", "fallbackModels": ["openai-codex/gpt-5.3-codex-spark", "zai/glm-4.5-air"], "thinking": "medium" },
150
150
  "frontend": { "model": "antigravity/gemini-3.1-pro-preview-customtools", "fallbackModels": ["openai-codex/gpt-5.4-mini", "zai/glm-5.3"], "thinking": "low" },
151
- "browser-qa": { "model": "antigravity/gemini-3-flash-preview", "fallbackModels": ["openai-codex/gpt-5.4-mini"], "thinking": "medium" },
151
+ "browser-qa": { "model": "openai-codex/gpt-5.4-mini", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "medium" },
152
152
  "tests": { "model": "antigravity/antigravity-claude-sonnet-4-6", "fallbackModels": ["openai-codex/gpt-5.4-mini", "zai/glm-5-turbo"], "thinking": "high" },
153
153
  "review": { "model": "antigravity/antigravity-claude-sonnet-4-6", "fallbackModels": ["openai-codex/gpt-5.6-sol", "zai/glm-5.3"], "thinking": "high" },
154
154
  "implement": { "model": "openai-codex/gpt-5.6-sol", "fallbackModels": ["zai/glm-5.3"], "thinking": "high" },
@@ -199,8 +199,8 @@
199
199
 
200
200
  "browser-qa": {
201
201
  "description": "Use for browser-based visual QA: reproduce UI bugs and verify fixes with deterministic assertions, screenshots, video, and traces.",
202
- "model": "antigravity/gemini-3-flash-preview",
203
- "fallbackModels": ["openai-codex/gpt-5.4-mini"],
202
+ "model": "openai-codex/gpt-5.4-mini",
203
+ "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"],
204
204
  "thinking": "medium",
205
205
  "tools": ["read", "grep", "bash"]
206
206
  },
@@ -207,8 +207,8 @@ const BUILTIN_CONFIG: SubagentConfig = {
207
207
  },
208
208
  "browser-qa": {
209
209
  description: "Use for browser-based visual QA: reproduce UI bugs and verify fixes with deterministic assertions, screenshots, video, and traces.",
210
- model: "antigravity/gemini-3-flash-preview",
211
- fallbackModels: ["openai-codex/gpt-5.4-mini"],
210
+ model: "openai-codex/gpt-5.4-mini",
211
+ fallbackModels: ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"],
212
212
  thinking: "medium",
213
213
  tools: ["read", "grep", "bash"],
214
214
  isolatedSkills: [getBrowserQaSkillPath()],
@@ -101,8 +101,10 @@ isolated browser context and exclusive evidence directory; the runner closes
101
101
  all owned browser resources on success and failure. Flows, screenshots, video,
102
102
  sanitized traces, and runner result manifests remain under
103
103
  `$PI_SUBAGENT_AGENT_DIR/browser-qa/` so normal sub-agent shutdown or cleanup
104
- deletes them with the run directory. `.pi/qa_auth.jsonc` and reusable
105
- `.pi/qa-auth-state/` remain project-local and persistent.
104
+ deletes them with the run directory. For form auth, recording starts on the login
105
+ page and includes field filling and submission; password inputs remain masked,
106
+ but the private video may show visible login identifiers. Tracing starts only
107
+ after login succeeds so credentials are not captured in the trace.
106
108
 
107
109
  ## Credentials and blocked runs
108
110
 
@@ -55,13 +55,16 @@ stale credentials into an explicit update request instead of misreporting a
55
55
  product regression.
56
56
 
57
57
  Do not encode credentials, tokens, storage values, or login form secrets in the
58
- flow. The trusted runner applies the selected profile internally and removes
59
- secret-bearing evidence if it detects disclosure.
58
+ flow. The trusted runner applies the selected profile internally. For form auth,
59
+ video starts on the login page and includes field filling and submission; password
60
+ inputs remain masked, but visible identifiers can appear, so treat the video as
61
+ sensitive private evidence. Tracing starts only after login succeeds and is
62
+ sanitized before retention.
60
63
 
61
64
  ## Evidence strategy
62
65
 
63
- The runner always attempts a final or failure screenshot, records video, and
64
- creates a sanitized trace once the browser launches. Add named `screenshot`
66
+ The runner always attempts a final or failure screenshot, records video from the
67
+ first page, and creates a sanitized post-auth trace. Add named `screenshot`
65
68
  steps only at states that materially help explain the result—for example before
66
69
  and after a destructive interaction, or when a transient success message is
67
70
  the oracle.
@@ -102,5 +105,5 @@ result manifest inside `$PI_SUBAGENT_AGENT_DIR/browser-qa/`. The launcher owns
102
105
  that path and the runner validates it before opening a browser. Do not override
103
106
  the environment path or copy evidence into shared `.pi/qa-runs`/`.pi/qa-flows`
104
107
  directories: the agent-local workspace is intentionally removed by the normal
105
- sub-agent shutdown and cleanup lifecycle. Authentication config and reusable
106
- auth state are separate persistent inputs and stay under project `.pi/`.
108
+ sub-agent shutdown and cleanup lifecycle. Authentication config remains a
109
+ separate persistent input under project `.pi/`.
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { createHash } from "node:crypto";
4
3
  import fs from "node:fs";
5
4
  import path from "node:path";
6
5
  import { createRequire } from "node:module";
@@ -9,10 +8,10 @@ import { parse as parseJsonc, printParseErrorCode } from "jsonc-parser";
9
8
  import { strFromU8, strToU8, unzipSync, zipSync } from "../vendor/fflate.mjs";
10
9
 
11
10
  const CONFIG_RELATIVE = ".pi/qa_auth.jsonc";
12
- const STATE_RELATIVE = path.join(".pi", "qa-auth-state");
13
11
  const SUBAGENT_AGENT_DIR_ENV = "PI_SUBAGENT_AGENT_DIR";
14
12
  const QA_WORKSPACE_RELATIVE = "browser-qa";
15
13
  const EVIDENCE_RELATIVE = "evidence";
14
+ const FORM_VIDEO_STEP_DELAY_MS = 250;
16
15
  const EXIT_AUTH_UPDATE_REQUIRED = 42;
17
16
  const EXIT_PROFILE_REQUIRED = 43;
18
17
  const PROFILE_ID = /^[A-Za-z0-9._-]+$/;
@@ -112,7 +111,7 @@ async function runQa({ cwd, agentDir, args, profileId, profile }) {
112
111
  const runtimeSecrets = collectSecrets(profile.auth);
113
112
  const tracePath = path.join(evidenceDir, "trace.zip");
114
113
  try {
115
- const contextOptions = await contextOptionsForAuth({ cwd, profileId, profile, allowedOrigins, browser });
114
+ const contextOptions = await contextOptionsForAuth({ cwd, profileId, profile, allowedOrigins });
116
115
  context = await browser.newContext({
117
116
  ...contextOptions,
118
117
  baseURL,
@@ -122,10 +121,11 @@ async function runQa({ cwd, agentDir, args, profileId, profile }) {
122
121
  });
123
122
  await installOriginGuard(context, allowedOrigins, profile.auth);
124
123
  await applyContextAuth(context, profile.auth, allowedOrigins, baseURL, profileId);
125
- runtimeSecrets.push(...collectStorageStateSecrets(await context.storageState()));
126
- await context.tracing.start({ screenshots: true, snapshots: true, sources: false });
127
124
  page = await context.newPage();
128
125
  video = page.video();
126
+ await applyFormAuth(page, profile.auth, allowedOrigins, profileId);
127
+ runtimeSecrets.push(...collectStorageStateSecrets(await context.storageState()));
128
+ await context.tracing.start({ screenshots: true, snapshots: true, sources: false });
129
129
  await executeFlow({ page, context, baseURL, evidenceDir, flow, allowedOrigins, profileId, secrets: runtimeSecrets });
130
130
  await assertPageDoesNotExposeSecrets(page, runtimeSecrets, profileId);
131
131
  await page.screenshot({ path: path.join(evidenceDir, "final.png"), fullPage: true });
@@ -465,7 +465,7 @@ function isStringArray(value) {
465
465
  return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string");
466
466
  }
467
467
 
468
- async function contextOptionsForAuth({ cwd, profileId, profile, allowedOrigins, browser }) {
468
+ async function contextOptionsForAuth({ cwd, profileId, profile, allowedOrigins }) {
469
469
  const auth = profile.auth;
470
470
  if (auth.type === "storageState") {
471
471
  if (typeof auth.path !== "string") throw authError(profileId, "storageState path is missing");
@@ -473,16 +473,7 @@ async function contextOptionsForAuth({ cwd, profileId, profile, allowedOrigins,
473
473
  return { storageState: filteredStorageState(statePath, allowedOrigins, profileId) };
474
474
  }
475
475
  if (auth.type === "form") {
476
- const stateDirectory = path.join(cwd, STATE_RELATIVE);
477
- const statePath = path.join(stateDirectory, `${profileId}-${authCacheKey(auth, allowedOrigins)}.json`);
478
- if (!fs.existsSync(statePath)) {
479
- ensurePrivateDirectory(cwd, stateDirectory);
480
- for (const name of fs.readdirSync(stateDirectory)) {
481
- if (name.startsWith(`${profileId}-`) && name.endsWith(".json")) fs.rmSync(path.join(stateDirectory, name), { force: true });
482
- }
483
- await createFormState({ statePath, auth, allowedOrigins, browser, profileId });
484
- }
485
- return { storageState: filteredStorageState(statePath, allowedOrigins, profileId) };
476
+ return {};
486
477
  }
487
478
  if (!["cookie", "localStorage", "sessionStorage", "bearer"].includes(auth.type)) {
488
479
  throw authError(profileId, `unsupported auth type: ${auth.type}`);
@@ -490,39 +481,36 @@ async function contextOptionsForAuth({ cwd, profileId, profile, allowedOrigins,
490
481
  return {};
491
482
  }
492
483
 
493
- async function createFormState({ statePath, auth, allowedOrigins, browser, profileId }) {
484
+ async function applyFormAuth(page, auth, allowedOrigins, profileId) {
485
+ if (auth.type !== "form") return;
494
486
  if (typeof auth.loginUrl !== "string" || !isAllowedUrl(auth.loginUrl, allowedOrigins)) {
495
487
  throw authError(profileId, "form loginUrl is missing or outside allowedOrigins");
496
488
  }
497
489
  if (!Array.isArray(auth.fields) || auth.fields.length === 0 || typeof auth.submitSelector !== "string") {
498
490
  throw authError(profileId, "form fields or submitSelector are missing");
499
491
  }
500
- const context = await browser.newContext();
501
492
  try {
502
- await installOriginGuard(context, allowedOrigins, auth);
503
- const page = await context.newPage();
504
- await page.goto(auth.loginUrl);
493
+ const timeout = Math.min(finitePositive(auth.timeoutMs) ?? 15_000, 60_000);
494
+ page.setDefaultTimeout(timeout);
495
+ page.setDefaultNavigationTimeout(timeout);
496
+ await page.goto(auth.loginUrl, { timeout });
497
+ await page.waitForTimeout(FORM_VIDEO_STEP_DELAY_MS);
505
498
  for (const field of auth.fields) {
506
499
  if (!isObject(field) || typeof field.selector !== "string" || typeof field.value !== "string") {
507
500
  throw authError(profileId, "form fields must contain selector/value strings");
508
501
  }
509
502
  await page.locator(field.selector).fill(field.value);
503
+ await page.waitForTimeout(FORM_VIDEO_STEP_DELAY_MS);
510
504
  }
511
505
  await page.locator(auth.submitSelector).click();
512
- const timeout = finitePositive(auth.timeoutMs) ?? 15_000;
513
506
  if (isObject(auth.success) && typeof auth.success.url === "string") await page.waitForURL(auth.success.url, { timeout });
514
507
  if (isObject(auth.success) && typeof auth.success.selector === "string") await page.locator(auth.success.selector).waitFor({ timeout });
515
508
  if (!isObject(auth.success) || (typeof auth.success.url !== "string" && typeof auth.success.selector !== "string")) {
516
509
  throw authError(profileId, "form success.url or success.selector is required");
517
510
  }
518
- fs.mkdirSync(path.dirname(statePath), { recursive: true, mode: 0o700 });
519
- await context.storageState({ path: statePath });
520
- fs.chmodSync(statePath, 0o600);
521
511
  } catch (error) {
522
512
  if (error instanceof QaStatusError) throw error;
523
513
  throw authError(profileId, "form login was rejected or did not reach the configured success condition");
524
- } finally {
525
- await context.close().catch(() => {});
526
514
  }
527
515
  }
528
516
 
@@ -550,10 +538,6 @@ async function applyContextAuth(context, auth, allowedOrigins, baseURL, profileI
550
538
  }
551
539
  }
552
540
 
553
- function authCacheKey(auth, allowedOrigins) {
554
- return createHash("sha256").update(JSON.stringify({ auth, allowedOrigins })).digest("hex").slice(0, 16);
555
- }
556
-
557
541
  async function installOriginGuard(context, allowedOrigins, auth) {
558
542
  await context.route("**/*", async (route) => {
559
543
  const request = route.request();
@@ -890,10 +874,6 @@ function createExclusivePrivateDirectory(cwd, target) {
890
874
  createPrivateDirectory(cwd, target, true);
891
875
  }
892
876
 
893
- function ensurePrivateDirectory(cwd, target) {
894
- createPrivateDirectory(cwd, target, false);
895
- }
896
-
897
877
  function createPrivateDirectory(cwd, target, exclusive) {
898
878
  const root = fs.realpathSync(cwd);
899
879
  const resolved = path.resolve(target);
@@ -300,6 +300,13 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
300
300
  "When no mockup exists, choose a clear aesthetic direction and explain it briefly. Verify with targeted build/lint/tests or screenshot-relevant checks when possible."
301
301
  ]
302
302
  },
303
+ "browser-qa": {
304
+ "description": "Use for browser-based visual QA: reproduce UI bugs and verify fixes with deterministic assertions, screenshots, video, and traces.",
305
+ "model": "openai-codex/gpt-5.4-mini",
306
+ "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"],
307
+ "thinking": "medium",
308
+ "tools": ["read", "grep", "bash"]
309
+ },
303
310
  "tests": {
304
311
  "description": "Use for tests: locate coverage, find gaps, run/check targeted test commands, diagnose failing tests.",
305
312
  "model": "zai/glm-5-turbo",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "description": "Pix: a workspace-first terminal UI for Pi with tabs, readable tool activity, voice input, and bundled agent tools.",
5
5
  "private": false,
6
6
  "repository": {