pi-ui-extend 1.0.19 → 1.0.20
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/external/pi-tools-suite/README.md +3 -2
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +23 -3
- package/external/pi-tools-suite/package.json +1 -0
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +22 -1
- package/external/pi-tools-suite/src/async-subagents/lib.ts +1 -0
- package/external/pi-tools-suite/src/todo/state/replay.ts +28 -10
- package/external/pi-tools-suite/src/todo/todo.ts +28 -6
- package/package.json +3 -1
|
@@ -251,8 +251,9 @@ The built-in `browser-qa` role runs on `antigravity/gemini-3-flash-preview`, wit
|
|
|
251
251
|
`openai-codex/gpt-5.4-mini` as its fallback. Its browser workflow is an explicit
|
|
252
252
|
private skill under `src/async-subagents/private-skills/`, outside normal Pi skill
|
|
253
253
|
discovery. The role's first-class `isolatedSkills` setting launches the child with
|
|
254
|
-
`--no-skills` plus
|
|
255
|
-
|
|
254
|
+
`--no-skills` plus the private workflow and the packaged `skills/playwright-cli`
|
|
255
|
+
skill. Both are mandatory even when configuration adds other isolated skills;
|
|
256
|
+
parent and ordinary sub-agent sessions do not discover the private workflow.
|
|
256
257
|
|
|
257
258
|
Keep named dev/staging auth profiles in project `.pi/qa_auth.jsonc` (there is no
|
|
258
259
|
`/qa-auth` command). The private runner supports `form`, `cookie`, `localStorage`,
|
|
@@ -17,6 +17,9 @@ evidence. The role uses `antigravity/gemini-3-flash-preview`, falling back to
|
|
|
17
17
|
Antigravity-backed role unavailable.
|
|
18
18
|
- A type profile may declare `isolatedSkills`. Spawning that profile adds
|
|
19
19
|
`--no-skills` followed by one explicit `--skill` per configured path.
|
|
20
|
+
- The `browser-qa` profile always loads both its private workflow and the
|
|
21
|
+
packaged `skills/playwright-cli` skill. Configuration may append isolated
|
|
22
|
+
skills but cannot remove either mandatory browser skill.
|
|
20
23
|
- Other sub-agent profiles and the parent session must not discover the private
|
|
21
24
|
skill automatically.
|
|
22
25
|
|
|
@@ -66,9 +69,10 @@ evidence. The role uses `antigravity/gemini-3-flash-preview`, falling back to
|
|
|
66
69
|
|
|
67
70
|
## Acceptance criteria
|
|
68
71
|
|
|
69
|
-
1. `browser-qa` resolves to the intended model/fallback
|
|
70
|
-
its isolated child process can register
|
|
71
|
-
|
|
72
|
+
1. `browser-qa` resolves to the intended model/fallback, private workflow, and
|
|
73
|
+
packaged `playwright-cli` skill, and its isolated child process can register
|
|
74
|
+
the configured Antigravity model.
|
|
75
|
+
2. Spawn args contain `--no-skills` and both mandatory explicit skills for this
|
|
72
76
|
profile; ordinary profiles retain existing skill discovery behavior.
|
|
73
77
|
3. Auth profile listing and all error output are redacted; model-authored input
|
|
74
78
|
cannot execute code in the credential-bearing process.
|
|
@@ -79,3 +83,19 @@ evidence. The role uses `antigravity/gemini-3-flash-preview`, falling back to
|
|
|
79
83
|
5. Completed test runs report clickable screenshot, video, and trace links
|
|
80
84
|
whenever those artifacts exist.
|
|
81
85
|
6. Suite tests/typecheck, host checks, and suite sync pass.
|
|
86
|
+
|
|
87
|
+
## Real-browser regression test
|
|
88
|
+
|
|
89
|
+
The repository includes a local mock-page E2E that launches real Chromium and
|
|
90
|
+
asserts PNG screenshots, WebM video, sanitized trace output, and absolute
|
|
91
|
+
path/`file:` URI metadata:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npx playwright install chromium
|
|
95
|
+
npm run test:browser-qa-e2e
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Normal suite tests keep this case skipped; the Publish workflow runs it on
|
|
99
|
+
Linux after installing Chromium. Explicit runs retain the latest artifacts in
|
|
100
|
+
`.pi/qa-runs/browser-qa-e2e/latest/` and print a clickable link for every file.
|
|
101
|
+
Set `BROWSER_QA_KEEP_EVIDENCE=0` to clean up without publishing evidence.
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"smoke:tools": "PI_OFFLINE=1 pi --no-session -p \"ping\"",
|
|
23
23
|
"smoke": "npm run smoke:explicit && npm run smoke:auto && npm run smoke:tools",
|
|
24
24
|
"test": "bun test test",
|
|
25
|
+
"test:browser-qa-e2e": "BROWSER_QA_RUNNER_E2E=1 bun test test/async-subagents/browser-qa-runner.e2e.test.ts",
|
|
25
26
|
"test:async-subagents-e2e": "ASYNC_SUBAGENTS_E2E=1 ASYNC_SUBAGENTS_DEBUG_LOGS=1 ASYNC_SUBAGENTS_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=30 test/async-subagents",
|
|
26
27
|
"test:async-subagents-selection-e2e": "ASYNC_SUBAGENTS_SELECTION_E2E=1 ASYNC_SUBAGENTS_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=30 test/async-subagents/selection-e2e.test.ts",
|
|
27
28
|
"test:prompt-evals:tool-selection": "PROMPT_EVAL_E2E=1 bun test --concurrent --max-concurrency=10 test/tool-selection-e2e.test.ts",
|
|
@@ -288,6 +288,21 @@ export function getBrowserQaSkillPath(): string {
|
|
|
288
288
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "private-skills", "browser-qa", "SKILL.md");
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
export function getPlaywrightCliSkillPath(): string {
|
|
292
|
+
const bundledPath = path.resolve(
|
|
293
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
294
|
+
"../../../../..",
|
|
295
|
+
"skills",
|
|
296
|
+
"playwright-cli",
|
|
297
|
+
"SKILL.md",
|
|
298
|
+
);
|
|
299
|
+
const installedPaths = [
|
|
300
|
+
path.join(os.homedir(), ".agents", "skills", "playwright-cli", "SKILL.md"),
|
|
301
|
+
path.join(os.homedir(), ".pi", "agent", "skills", "playwright-cli", "SKILL.md"),
|
|
302
|
+
];
|
|
303
|
+
return [bundledPath, ...installedPaths].find((candidate) => fs.existsSync(candidate)) ?? bundledPath;
|
|
304
|
+
}
|
|
305
|
+
|
|
291
306
|
export function getSubagentConfigInitTargetPath(cwd: string, env: NodeJS.ProcessEnv = process.env): string {
|
|
292
307
|
return explicitSubagentConfigPath(cwd, env) ?? getDefaultSubagentConfigPath();
|
|
293
308
|
}
|
|
@@ -381,7 +396,7 @@ export function resolveAgentTaskConfig(
|
|
|
381
396
|
return {
|
|
382
397
|
profile,
|
|
383
398
|
extraArgs,
|
|
384
|
-
isolatedSkills:
|
|
399
|
+
isolatedSkills: resolveIsolatedSkills(selectedType, profile),
|
|
385
400
|
fallbackModels,
|
|
386
401
|
retry: resolveRetryConfig(config.retry, profile?.retry),
|
|
387
402
|
maxResultBytes: profile?.maxResultBytes ?? config.maxResultBytes,
|
|
@@ -399,6 +414,12 @@ export function resolveAgentTaskConfig(
|
|
|
399
414
|
};
|
|
400
415
|
}
|
|
401
416
|
|
|
417
|
+
function resolveIsolatedSkills(selectedType: string | undefined, profile: SubagentTypeConfig | undefined): string[] {
|
|
418
|
+
const configured = arrayOfStrings(profile?.isolatedSkills) ?? [];
|
|
419
|
+
if (selectedType !== "browser-qa") return configured;
|
|
420
|
+
return [...new Set([getBrowserQaSkillPath(), getPlaywrightCliSkillPath(), ...configured])];
|
|
421
|
+
}
|
|
422
|
+
|
|
402
423
|
export function resolveSubagentRoutingConfig(config: SubagentConfig): ResolvedSubagentRoutingConfig {
|
|
403
424
|
return { ...DEFAULT_ROUTING_CONFIG, ...(config.routing ?? {}) };
|
|
404
425
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { TaskDetails } from "../tool/types.js";
|
|
2
2
|
import { EMPTY_STATE, type TaskState } from "./state.js";
|
|
3
3
|
|
|
4
|
+
export const TODO_STATE_ENTRY_TYPE = "pi-tools-suite:todo-state";
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Discriminator for `details` envelopes that match the persisted `TaskDetails`
|
|
6
8
|
* shape. Defensive — branch entries from older or corrupt sessions are
|
|
@@ -12,10 +14,29 @@ export function isTaskDetails(value: unknown): value is TaskDetails {
|
|
|
12
14
|
return Array.isArray(v.tasks) && typeof v.nextId === "number";
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
function taskDetailsFromEntry(entry: unknown): TaskDetails | undefined {
|
|
18
|
+
const candidate = entry as {
|
|
19
|
+
type?: string;
|
|
20
|
+
customType?: string;
|
|
21
|
+
data?: unknown;
|
|
22
|
+
message?: { role?: string; toolName?: string; details?: unknown };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (candidate.type === "custom" && candidate.customType === TODO_STATE_ENTRY_TYPE) {
|
|
26
|
+
return isTaskDetails(candidate.data) ? candidate.data : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const message = candidate.message;
|
|
30
|
+
if (candidate.type !== "message" || message?.role !== "toolResult" || message.toolName !== "todo") return undefined;
|
|
31
|
+
return isTaskDetails(message.details) ? message.details : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
/**
|
|
16
|
-
* Walk the current branch in chronological order; the LAST
|
|
17
|
-
*
|
|
18
|
-
* (last-write-wins).
|
|
35
|
+
* Walk the current branch in chronological order; the LAST valid snapshot
|
|
36
|
+
* from either a `todo` tool result or a todo custom state entry wins
|
|
37
|
+
* (last-write-wins). Custom entries persist slash-command mutations, which do
|
|
38
|
+
* not otherwise produce tool results. When no matching entry exists, returns
|
|
39
|
+
* `EMPTY_STATE`.
|
|
19
40
|
*
|
|
20
41
|
* Pure of module state — `index.ts` writes the returned snapshot into the
|
|
21
42
|
* store after this returns. The function explicitly does NOT touch the store
|
|
@@ -24,14 +45,11 @@ export function isTaskDetails(value: unknown): value is TaskDetails {
|
|
|
24
45
|
export function replayFromBranch(ctx: { sessionManager: { getBranch(): Iterable<unknown> } }): TaskState {
|
|
25
46
|
let result: TaskState = { tasks: [...EMPTY_STATE.tasks], nextId: EMPTY_STATE.nextId };
|
|
26
47
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
const msg = e.message;
|
|
30
|
-
if (!msg || msg.role !== "toolResult" || msg.toolName !== "todo") continue;
|
|
31
|
-
if (!isTaskDetails(msg.details)) continue;
|
|
48
|
+
const details = taskDetailsFromEntry(entry);
|
|
49
|
+
if (!details) continue;
|
|
32
50
|
result = {
|
|
33
|
-
tasks:
|
|
34
|
-
nextId:
|
|
51
|
+
tasks: details.tasks.map((t) => ({ ...t })),
|
|
52
|
+
nextId: details.nextId,
|
|
35
53
|
};
|
|
36
54
|
}
|
|
37
55
|
return result;
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
syncPersistedPlan,
|
|
24
24
|
} from "./state/persistence.js";
|
|
25
25
|
import { AUTO_CLEAR_COMPLETED_MESSAGE, autoClearCompletedTodos } from "./state/auto-clear.js";
|
|
26
|
-
import { replayFromBranch } from "./state/replay.js";
|
|
26
|
+
import { replayFromBranch, TODO_STATE_ENTRY_TYPE } from "./state/replay.js";
|
|
27
27
|
import { isTaskBlocked, selectTasksByStatus, selectTodoCounts } from "./state/selectors.js";
|
|
28
28
|
import { applyTaskMutation } from "./state/state-reducer.js";
|
|
29
29
|
import { activateStateScope, commitState, getState, replaceState } from "./state/store.js";
|
|
@@ -98,6 +98,7 @@ interface TodoToolRegistrationOptions extends TodoToolHooks {
|
|
|
98
98
|
|
|
99
99
|
type TodoStateEventContext = { sessionManager?: { getSessionFile?: () => unknown; getSessionId?: () => unknown } };
|
|
100
100
|
type TodoStateEventEmitter = { events?: { emit?: (channel: string, data: unknown) => void } };
|
|
101
|
+
type TodoStateEntryWriter = { appendEntry<T = unknown>(customType: string, data?: T): void };
|
|
101
102
|
|
|
102
103
|
interface TodosCommandOptions {
|
|
103
104
|
status?: TaskStatus;
|
|
@@ -238,6 +239,20 @@ export function publishTodoState(
|
|
|
238
239
|
});
|
|
239
240
|
}
|
|
240
241
|
|
|
242
|
+
function appendTodoStateSnapshot(
|
|
243
|
+
pi: TodoStateEntryWriter,
|
|
244
|
+
action: TaskAction,
|
|
245
|
+
params: Record<string, unknown>,
|
|
246
|
+
): void {
|
|
247
|
+
const state = getState();
|
|
248
|
+
pi.appendEntry(TODO_STATE_ENTRY_TYPE, {
|
|
249
|
+
action,
|
|
250
|
+
params,
|
|
251
|
+
tasks: state.tasks,
|
|
252
|
+
nextId: state.nextId,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
241
256
|
function handlePersistCommand(args: unknown, ctx: { cwd?: string; hasUI?: boolean; ui?: { notify?: (message: string, level?: NotifyLevel) => void } }): boolean {
|
|
242
257
|
const [command, subcommand = "status"] = getCommandTokens(args);
|
|
243
258
|
if (command !== "persist") return false;
|
|
@@ -310,12 +325,13 @@ function handleScopeCommand(
|
|
|
310
325
|
}
|
|
311
326
|
|
|
312
327
|
function clearTodos(
|
|
313
|
-
pi: TodoStateEventEmitter,
|
|
328
|
+
pi: TodoStateEventEmitter & TodoStateEntryWriter,
|
|
314
329
|
ctx: { cwd?: string; hasUI?: boolean; ui?: { notify?: (message: string, level?: NotifyLevel) => void } },
|
|
315
330
|
): void {
|
|
316
331
|
const result = applyTaskMutation(getState(), "clear", { action: "clear" });
|
|
317
332
|
if (result.op.kind !== "clear") return;
|
|
318
333
|
commitState(result.state);
|
|
334
|
+
appendTodoStateSnapshot(pi, "clear", { action: "clear" });
|
|
319
335
|
publishTodoState(pi, ctx, "clear", { action: "clear" });
|
|
320
336
|
const sync = syncPersistedPlan(ctx.cwd, result.state);
|
|
321
337
|
const persistedText = sync?.completed ? `\nProject todo plan removed: ${sync.path}` : "";
|
|
@@ -449,9 +465,12 @@ export function registerTodosCommand(pi: ExtensionAPI): void {
|
|
|
449
465
|
handler: async (args, ctx) => {
|
|
450
466
|
activateTodoStateScope(ctx);
|
|
451
467
|
if (handlePersistCommand(args, ctx)) return;
|
|
452
|
-
if (handleScopeCommand(args, ctx, () =>
|
|
468
|
+
if (handleScopeCommand(args, ctx, () => {
|
|
469
|
+
appendTodoStateSnapshot(pi, "update", { command: "scope" });
|
|
470
|
+
publishTodoState(pi as TodoStateEventEmitter, ctx);
|
|
471
|
+
})) return;
|
|
453
472
|
if (getCommandTokens(args)[0] === "clear") {
|
|
454
|
-
clearTodos(pi
|
|
473
|
+
clearTodos(pi, ctx);
|
|
455
474
|
return;
|
|
456
475
|
}
|
|
457
476
|
if (!ctx.hasUI) {
|
|
@@ -529,7 +548,10 @@ export function registerTodosCommand(pi: ExtensionAPI): void {
|
|
|
529
548
|
description: "Select todo ids to continue from a persisted plan; pending/in_progress items outside the scope become deferred.",
|
|
530
549
|
handler: async (args, ctx) => {
|
|
531
550
|
activateTodoStateScope(ctx);
|
|
532
|
-
handleScopeCommand(`scope ${getCommandText(args)}`, ctx, () =>
|
|
551
|
+
handleScopeCommand(`scope ${getCommandText(args)}`, ctx, () => {
|
|
552
|
+
appendTodoStateSnapshot(pi, "update", { command: "scope" });
|
|
553
|
+
publishTodoState(pi as TodoStateEventEmitter, ctx);
|
|
554
|
+
});
|
|
533
555
|
},
|
|
534
556
|
});
|
|
535
557
|
|
|
@@ -537,7 +559,7 @@ export function registerTodosCommand(pi: ExtensionAPI): void {
|
|
|
537
559
|
description: "Clear all todos on the current branch and remove the persisted project plan if enabled.",
|
|
538
560
|
handler: async (_args, ctx) => {
|
|
539
561
|
activateTodoStateScope(ctx);
|
|
540
|
-
clearTodos(pi
|
|
562
|
+
clearTodos(pi, ctx);
|
|
541
563
|
},
|
|
542
564
|
});
|
|
543
565
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ui-extend",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
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": {
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"test": "mise exec node@24.16.0 -- npm run test:inner",
|
|
59
59
|
"test:inner": "node --import tsx --test \"tests/**/*.test.ts\"",
|
|
60
60
|
"test:tools-suite": "npm --prefix external/pi-tools-suite test",
|
|
61
|
+
"test:browser-qa-e2e": "npm --prefix external/pi-tools-suite run test:browser-qa-e2e",
|
|
61
62
|
"sync:pi-tools-suite": "node scripts/sync-pi-tools-suite.mjs",
|
|
62
63
|
"sync:pi-tools-suite:check": "node scripts/sync-pi-tools-suite.mjs --check",
|
|
63
64
|
"sync:sdk-pin": "node scripts/sync-sdk-pin.mjs",
|
|
@@ -86,6 +87,7 @@
|
|
|
86
87
|
},
|
|
87
88
|
"devDependencies": {
|
|
88
89
|
"@types/node": "24.12.4",
|
|
90
|
+
"playwright": "1.62.1",
|
|
89
91
|
"tsx": "4.22.1",
|
|
90
92
|
"typescript": "5.9.3"
|
|
91
93
|
},
|