pi-ui-extend 1.0.31 → 1.0.35

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/dist/app/app.js CHANGED
@@ -533,7 +533,6 @@ export class PiUiExtendApp {
533
533
  getThinkingMenuItems: (query) => this.menuItems.getThinkingMenuItems(query),
534
534
  awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
535
535
  modelRef: (model) => this.menuItems.modelRef(model),
536
- getFavoriteScopedModels: () => this.menuItems.getFavoriteScopedModels(),
537
536
  setSessionStatus: (session) => this.setSessionStatus(session),
538
537
  queueUserMessage: (text) => {
539
538
  this.queuedMessages.deferUserMessage(this.queuedMessages.createSubmittedUserMessage(text, text, []));
@@ -1,7 +1,7 @@
1
1
  import type { AgentSession, AgentSessionRuntime, SessionInfo } from "@earendil-works/pi-coding-agent";
2
2
  import type { SessionSearchResult } from "../session/session-search.js";
3
3
  import type { AppRequestHistory } from "../session/request-history.js";
4
- import type { ActivePopupMenu, AppOptions, Entry, ModelMenuValue, PixMenuItem, PixMenuOptions, PopupMenuPlacement, ScopedSessionModel, SessionModel, ThinkingMenuValue } from "../types.js";
4
+ import type { ActivePopupMenu, AppOptions, Entry, ModelMenuValue, PixMenuItem, PixMenuOptions, PopupMenuPlacement, SessionModel, ThinkingMenuValue } from "../types.js";
5
5
  import type { ToastNotifier } from "../../ui.js";
6
6
  export type DirectPopupMenu = Exclude<ActivePopupMenu, "slash">;
7
7
  export type CommandControllerHost = {
@@ -27,7 +27,6 @@ export type CommandControllerHost = {
27
27
  getModelMenuItems(query: string): readonly PixMenuItem<ModelMenuValue>[];
28
28
  getThinkingMenuItems(query: string): readonly PixMenuItem<ThinkingMenuValue>[];
29
29
  modelRef(model: SessionModel): string;
30
- getFavoriteScopedModels(): ScopedSessionModel[];
31
30
  setSessionStatus(session: AgentSession | undefined): void;
32
31
  queueUserMessage(text: string): void;
33
32
  resetSessionView(): void;
@@ -18,7 +18,7 @@ export class ModelCommandActions {
18
18
  const enabledModels = settings.getEnabledModels();
19
19
  const scopedModelText = enabledModels && enabledModels.length > 0
20
20
  ? enabledModels.map((model) => ` - ${model}`).join("\n")
21
- : " - default favorites";
21
+ : " - all available models (no scope)";
22
22
  const text = [
23
23
  "Settings summary",
24
24
  `cwd: ${runtime.cwd}`,
@@ -191,7 +191,7 @@ export class ModelCommandActions {
191
191
  else if (sessionModels.length > 0)
192
192
  scopedModelLines = sessionModels;
193
193
  else
194
- scopedModelLines = [" - default favorites"];
194
+ scopedModelLines = [" - all available models (no scope)"];
195
195
  this.host.addEntry({
196
196
  id: createId("system"),
197
197
  kind: "system",
@@ -200,7 +200,7 @@ export class ModelCommandActions {
200
200
  ...scopedModelLines,
201
201
  "",
202
202
  "Usage: /scoped-models <provider/model[:thinking]> [...more]",
203
- "Use /scoped-models reset to restore the default favorites.",
203
+ "Use /scoped-models reset to use all available models.",
204
204
  ].join("\n"),
205
205
  });
206
206
  this.host.setSessionStatus(runtime.session);
@@ -208,8 +208,8 @@ export class ModelCommandActions {
208
208
  }
209
209
  if (["reset", "default", "clear"].includes(value.toLowerCase())) {
210
210
  runtime.services.settingsManager.setEnabledModels(undefined);
211
- runtime.session.setScopedModels(this.host.getFavoriteScopedModels());
212
- this.host.addEntry({ id: createId("system"), kind: "system", text: "Scoped models reset to default favorites." });
211
+ runtime.session.setScopedModels([]);
212
+ this.host.addEntry({ id: createId("system"), kind: "system", text: "Model scope reset to all available models." });
213
213
  this.host.setSessionStatus(runtime.session);
214
214
  return;
215
215
  }
@@ -1,7 +1,6 @@
1
1
  import type { ResolvedToolRule } from "../config.js";
2
2
  export declare const THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
3
3
  export declare const THINKING_MENU_MAX_ROWS: number;
4
- export declare const PI_FAVORITE_MODEL_REFS: readonly ["amazon-bedrock/us.anthropic.claude-opus-4-6-v1", "anthropic/claude-opus-4-8", "openai/gpt-5.4", "azure-openai-responses/gpt-5.4", "openai-codex/gpt-5.6-sol", "deepseek/deepseek-v4-pro", "google/gemini-3.1-pro-preview", "google-vertex/gemini-3.1-pro-preview", "github-copilot/gpt-5.4", "openrouter/moonshotai/kimi-k2.6", "vercel-ai-gateway/zai/glm-5.2", "xai/grok-4.5", "groq/openai/gpt-oss-120b", "cerebras/gpt-oss-120b", "zai/glm-5.3", "mistral/devstral-medium-latest", "minimax/MiniMax-M2.7", "minimax-cn/MiniMax-M2.7", "moonshotai/kimi-k2.6", "moonshotai-cn/kimi-k2.6", "huggingface/moonshotai/Kimi-K2.6", "fireworks/accounts/fireworks/models/kimi-k2p6", "together/moonshotai/Kimi-K2.6", "opencode/kimi-k2.6", "opencode-go/kimi-k2.6", "kimi-coding/kimi-for-coding", "cloudflare-workers-ai/@cf/moonshotai/kimi-k2.6", "cloudflare-ai-gateway/workers-ai/@cf/moonshotai/kimi-k2.6", "xiaomi/mimo-v2.5-pro", "xiaomi-token-plan-cn/mimo-v2.5-pro", "xiaomi-token-plan-ams/mimo-v2.5-pro", "xiaomi-token-plan-sgp/mimo-v2.5-pro"];
5
4
  export declare const SLASH_COMMAND_MENU_MAX_ROWS = 6;
6
5
  export declare const RESUME_MENU_MAX_ROWS = 20;
7
6
  export declare const RESUME_MENU_INITIAL_SESSION_ROWS = 30;
@@ -1,40 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
3
3
  export const THINKING_MENU_MAX_ROWS = THINKING_LEVELS.length + 1;
4
- export const PI_FAVORITE_MODEL_REFS = [
5
- "amazon-bedrock/us.anthropic.claude-opus-4-6-v1",
6
- "anthropic/claude-opus-4-8",
7
- "openai/gpt-5.4",
8
- "azure-openai-responses/gpt-5.4",
9
- "openai-codex/gpt-5.6-sol",
10
- "deepseek/deepseek-v4-pro",
11
- "google/gemini-3.1-pro-preview",
12
- "google-vertex/gemini-3.1-pro-preview",
13
- "github-copilot/gpt-5.4",
14
- "openrouter/moonshotai/kimi-k2.6",
15
- "vercel-ai-gateway/zai/glm-5.2",
16
- "xai/grok-4.5",
17
- "groq/openai/gpt-oss-120b",
18
- "cerebras/gpt-oss-120b",
19
- "zai/glm-5.3",
20
- "mistral/devstral-medium-latest",
21
- "minimax/MiniMax-M2.7",
22
- "minimax-cn/MiniMax-M2.7",
23
- "moonshotai/kimi-k2.6",
24
- "moonshotai-cn/kimi-k2.6",
25
- "huggingface/moonshotai/Kimi-K2.6",
26
- "fireworks/accounts/fireworks/models/kimi-k2p6",
27
- "together/moonshotai/Kimi-K2.6",
28
- "opencode/kimi-k2.6",
29
- "opencode-go/kimi-k2.6",
30
- "kimi-coding/kimi-for-coding",
31
- "cloudflare-workers-ai/@cf/moonshotai/kimi-k2.6",
32
- "cloudflare-ai-gateway/workers-ai/@cf/moonshotai/kimi-k2.6",
33
- "xiaomi/mimo-v2.5-pro",
34
- "xiaomi-token-plan-cn/mimo-v2.5-pro",
35
- "xiaomi-token-plan-ams/mimo-v2.5-pro",
36
- "xiaomi-token-plan-sgp/mimo-v2.5-pro",
37
- ];
38
4
  export const SLASH_COMMAND_MENU_MAX_ROWS = 6;
39
5
  export const RESUME_MENU_MAX_ROWS = 20;
40
6
  export const RESUME_MENU_INITIAL_SESSION_ROWS = 30;
@@ -1,6 +1,6 @@
1
1
  import type { AgentSessionRuntime, SessionInfo } from "@earendil-works/pi-coding-agent";
2
2
  import type { PopupMenuItem } from "../../ui.js";
3
- import type { Entry, ModelMenuValue, QueueMessageMenuValue, ResumeMenuValue, ScopedSessionModel, SessionModel, SlashCommand, ThinkingMenuValue, UserMessageJumpMenuValue, UserMessageMenuValue } from "../types.js";
3
+ import type { Entry, ModelMenuValue, QueueMessageMenuValue, ResumeMenuValue, SessionModel, SlashCommand, ThinkingMenuValue, UserMessageJumpMenuValue, UserMessageMenuValue } from "../types.js";
4
4
  export type AppMenuItemsControllerHost = {
5
5
  runtime(): AgentSessionRuntime | undefined;
6
6
  getBuiltinSlashCommands(): readonly SlashCommand[];
@@ -18,7 +18,6 @@ export declare class AppMenuItemsController {
18
18
  getSlashCommandMatches(query: string, limit?: number): import("../../fuzzy.js").FuzzyMatch<SlashCommand>[];
19
19
  getSlashCommandMenuItems(query: string): PopupMenuItem<SlashCommand>[];
20
20
  modelRef(model: SessionModel): string;
21
- getFavoriteScopedModels(): ScopedSessionModel[];
22
21
  getModelMenuItems(query: string): PopupMenuItem<ModelMenuValue>[];
23
22
  getThinkingMenuItems(query: string): PopupMenuItem<ThinkingMenuValue>[];
24
23
  getUserMessageMenuItems(): PopupMenuItem<UserMessageMenuValue>[];
@@ -30,7 +29,6 @@ export declare class AppMenuItemsController {
30
29
  private getResumeMenuItemsLoader;
31
30
  private getAllSlashCommands;
32
31
  private isCurrentModel;
33
- private resolveScopedModelRefs;
34
32
  private getModelMenuModels;
35
33
  private thinkingLevelDescription;
36
34
  }
@@ -1,8 +1,7 @@
1
1
  import { resolve } from "node:path";
2
2
  import { fuzzySearch } from "../../fuzzy.js";
3
- import { PI_FAVORITE_MODEL_REFS, THINKING_LEVELS } from "../constants.js";
3
+ import { THINKING_LEVELS } from "../constants.js";
4
4
  import { APP_ICONS } from "../icons.js";
5
- import { parseScopedModelRef } from "../model/model-ref.js";
6
5
  import { buildUserMessageJumpItems, createSessionInfoMenuItemsLoader, filterUserMessageJumpItems } from "./popup-menu-controller.js";
7
6
  import { getResourceSlashCommands, getSlashCommandMatches, parseSlashInput } from "../commands/slash-commands.js";
8
7
  import { isRecord } from "../guards.js";
@@ -38,11 +37,6 @@ export class AppMenuItemsController {
38
37
  modelRef(model) {
39
38
  return `${model.provider}/${model.id}`;
40
39
  }
41
- getFavoriteScopedModels() {
42
- const configuredRefs = this.host.runtime()?.services.settingsManager.getEnabledModels();
43
- const refs = configuredRefs && configuredRefs.length > 0 ? configuredRefs : PI_FAVORITE_MODEL_REFS;
44
- return this.resolveScopedModelRefs(refs);
45
- }
46
40
  getModelMenuItems(query) {
47
41
  const models = [...this.getModelMenuModels()].sort((left, right) => {
48
42
  const leftCurrent = this.isCurrentModel(left);
@@ -172,33 +166,17 @@ export class AppMenuItemsController {
172
166
  const current = this.host.runtime()?.session.model;
173
167
  return current?.provider === model.provider && current.id === model.id;
174
168
  }
175
- resolveScopedModelRefs(modelRefs) {
176
- const modelRuntime = this.host.runtime()?.services.modelRuntime;
177
- if (!modelRuntime)
178
- return [];
179
- const scopedModels = [];
180
- for (const modelRef of modelRefs) {
181
- const parsed = parseScopedModelRef(modelRef);
182
- if (!parsed)
183
- continue;
184
- const model = modelRuntime.getModel(parsed.provider, parsed.modelId);
185
- if (!model)
186
- continue;
187
- scopedModels.push({
188
- model,
189
- ...(parsed.thinkingLevel === undefined ? {} : { thinkingLevel: parsed.thinkingLevel }),
190
- });
191
- }
192
- return scopedModels;
193
- }
194
169
  getModelMenuModels() {
195
- const session = this.host.runtime()?.session;
196
- const scopedModels = session?.scopedModels.length ? session.scopedModels : this.getFavoriteScopedModels();
197
- if (!scopedModels.length)
170
+ const runtime = this.host.runtime();
171
+ if (!runtime)
198
172
  return [];
199
- const modelRuntime = this.host.runtime()?.services.modelRuntime;
200
- return scopedModels.map((scoped) => {
201
- const refreshed = modelRuntime?.getModel(scoped.model.provider, scoped.model.id);
173
+ const { session } = runtime;
174
+ const modelRuntime = runtime.services.modelRuntime;
175
+ if (!session.scopedModels.length) {
176
+ return [...modelRuntime.getAvailableSnapshot()];
177
+ }
178
+ return session.scopedModels.map((scoped) => {
179
+ const refreshed = modelRuntime.getModel(scoped.model.provider, scoped.model.id);
202
180
  return (refreshed ?? scoped.model);
203
181
  });
204
182
  }
@@ -101,7 +101,7 @@ export class PopupMenuRenderer {
101
101
  const visibleItems = menu.visibleItems();
102
102
  if (!this.hasPopupActionItems(menu.items)) {
103
103
  lines.push({
104
- text: this.host.session ? " No matching favorite models" : " Model menu unavailable",
104
+ text: this.host.session ? " No matching available models" : " Model menu unavailable",
105
105
  variant: "muted",
106
106
  });
107
107
  }
@@ -5,7 +5,6 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, SessionManager, } from "@earendil-works/pi-coding-agent";
7
7
  import { loadPixConfig, resolveDefaultModelRef } from "../config.js";
8
- import { PI_FAVORITE_MODEL_REFS } from "./constants.js";
9
8
  import { isThinkingLevel, parseModelRef, parseScopedModelRef } from "./model/model-ref.js";
10
9
  import { openLazySessionManager } from "./session/lazy-session-manager.js";
11
10
  const BUNDLED_QUESTION_EXTENSION_NAME = "question";
@@ -267,8 +266,7 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
267
266
  throw new Error(`Model not found: ${parsedModel.provider}/${parsedModel.modelId}`);
268
267
  }
269
268
  const enabledModelRefs = services.settingsManager.getEnabledModels();
270
- const favoriteModelRefs = enabledModelRefs && enabledModelRefs.length > 0 ? enabledModelRefs : PI_FAVORITE_MODEL_REFS;
271
- const scopedModels = favoriteModelRefs.flatMap((modelRef) => {
269
+ const scopedModels = (enabledModelRefs ?? []).flatMap((modelRef) => {
272
270
  const scoped = parseScopedModelRef(modelRef);
273
271
  if (!scoped)
274
272
  return [];
@@ -2,6 +2,11 @@ import { existsSync } from "node:fs";
2
2
  import { spawn } from "node:child_process";
3
3
  import { isAbsolute, posix, win32 } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ const MEDIA_EXTENSIONS = new Set([
6
+ ".3g2", ".3gp", ".aac", ".aiff", ".alac", ".apng", ".avi", ".avif", ".bmp", ".flac", ".gif", ".heic", ".heif",
7
+ ".ico", ".jpeg", ".jpg", ".m4a", ".m4v", ".mkv", ".mov", ".mp3", ".mp4", ".mpeg", ".mpg", ".oga", ".ogg",
8
+ ".ogv", ".opus", ".png", ".svg", ".tif", ".tiff", ".wav", ".webm", ".webp", ".wma", ".wmv",
9
+ ]);
5
10
  let deps = { existsSync, env: process.env, platform: process.platform, spawn };
6
11
  export function setFileLinkOpenerTestDeps(overrides) {
7
12
  const previous = deps;
@@ -16,9 +21,11 @@ export function openFileLink(link) {
16
21
  const filePath = link.filePath ?? filePathFromUrl(link.url);
17
22
  if (!filePath)
18
23
  return false;
19
- const editorLaunch = preferredEditorLaunch(filePath, link.line, link.column);
20
- if (editorLaunch && trySpawnCandidates(editorLaunch.candidates, editorLaunch.args))
21
- return true;
24
+ if (isZedTerminal(deps.env) && !isMediaFile(filePath)) {
25
+ const openedInZed = trySpawnCandidates(zedCommandCandidates(), [zedTarget(filePath, link.line, link.column)]);
26
+ if (openedInZed)
27
+ return true;
28
+ }
22
29
  return openPathWithSystemViewer(filePath);
23
30
  }
24
31
  function isWebUrl(url) {
@@ -39,45 +46,12 @@ function zedTarget(filePath, line, column) {
39
46
  return filePath;
40
47
  return column === undefined ? `${filePath}:${line}` : `${filePath}:${line}:${column}`;
41
48
  }
42
- function gotoTarget(filePath, line, column) {
43
- if (line === undefined)
44
- return filePath;
45
- return column === undefined ? `${filePath}:${line}` : `${filePath}:${line}:${column}`;
46
- }
47
- function preferredEditorLaunch(filePath, line, column) {
48
- switch (detectEditor(deps.env)) {
49
- case "cursor":
50
- return { args: ["--goto", gotoTarget(filePath, line, column)], candidates: commandCandidates(deps.env.CURSOR_CLI, "cursor") };
51
- case "jetbrains":
52
- return {
53
- args: jetbrainsTargetArgs(filePath, line),
54
- candidates: commandCandidates(deps.env.JETBRAINS_IDE_CLI, "idea", "idea64", "webstorm", "webstorm64", "pycharm", "pycharm64", "goland", "goland64", "clion", "clion64", "phpstorm", "phpstorm64", "rubymine", "rubymine64", "rider", "rider64"),
55
- };
56
- case "vscode":
57
- return { args: ["--goto", gotoTarget(filePath, line, column)], candidates: commandCandidates(deps.env.VSCODE_CLI, "code", "code-insiders") };
58
- case "windsurf":
59
- return { args: ["--goto", gotoTarget(filePath, line, column)], candidates: commandCandidates(deps.env.WINDSURF_CLI, "windsurf") };
60
- case "zed":
61
- return { args: [zedTarget(filePath, line, column)], candidates: zedCommandCandidates() };
62
- default:
63
- return undefined;
64
- }
49
+ function isZedTerminal(env) {
50
+ return env.TERM_PROGRAM?.trim().toLowerCase() === "zed" || Boolean(env.ZED_CLI);
65
51
  }
66
- function detectEditor(env) {
67
- const termProgram = env.TERM_PROGRAM?.trim().toLowerCase();
68
- const terminalEmulator = env.TERMINAL_EMULATOR?.trim().toLowerCase();
69
- const terminalProvider = env.TERMINAL_PROVIDER?.trim().toLowerCase();
70
- if (termProgram === "cursor" || env.CURSOR_TRACE_ID || env.CURSOR_TRACE)
71
- return "cursor";
72
- if (termProgram === "windsurf")
73
- return "windsurf";
74
- if (termProgram === "zed" || env.ZED_CLI)
75
- return "zed";
76
- if (termProgram === "vscode" || env.VSCODE_IPC_HOOK_CLI || env.VSCODE_GIT_IPC_HANDLE)
77
- return "vscode";
78
- if (terminalEmulator?.includes("jetbrains") || terminalProvider === "jetbrains")
79
- return "jetbrains";
80
- return undefined;
52
+ function isMediaFile(filePath) {
53
+ const pathApi = deps.platform === "win32" ? win32 : posix;
54
+ return MEDIA_EXTENSIONS.has(pathApi.extname(filePath).toLowerCase());
81
55
  }
82
56
  function zedCommandCandidates() {
83
57
  const candidates = [deps.env.ZED_CLI, "zed", "zeditor"];
@@ -85,14 +59,6 @@ function zedCommandCandidates() {
85
59
  candidates.push("/opt/homebrew/bin/zed", "/usr/local/bin/zed");
86
60
  return candidates.filter((candidate) => Boolean(candidate));
87
61
  }
88
- function commandCandidates(primary, ...rest) {
89
- return [primary, ...rest].filter((candidate) => Boolean(candidate));
90
- }
91
- function jetbrainsTargetArgs(filePath, line) {
92
- if (line === undefined)
93
- return [filePath];
94
- return ["--line", `${line}`, filePath];
95
- }
96
62
  function trySpawnCandidates(candidates, args) {
97
63
  for (const command of candidates) {
98
64
  if (!canRunCommand(command))
@@ -4,7 +4,7 @@ Local all-in-one Pi extension package.
4
4
 
5
5
  This package keeps shared Pi tools as ordinary source folders under `src/` and registers them through one entrypoint.
6
6
 
7
- - `src/coding-discipline` — injects a deduplicated silent-mode and quality-discipline block at the very top of the main-session per-turn system prompt for GLM main-session models only (`isGlmModel`) immediately before the LLM request; non-GLM models are left untouched; disabled for async sub-agents
7
+ - `src/coding-discipline` — injects a deduplicated silent-mode and quality-discipline block at the very top of the main-session per-turn system prompt for GLM main-session models only (`isGlmModel`) immediately before the LLM request; text-only GLM models get the `lookup` bridge while vision-capable `zai/glm-5.3-flash` inspects images directly; non-GLM models are left untouched; disabled for async sub-agents
8
8
  - `src/credential-firewall` — opt-in secret firewall for high-confidence outbound/session credential redaction; disabled by default
9
9
  - `src/ast-grep` — `ast_grep` / `ast_apply`
10
10
  - `src/async-subagents` — `subagents` tool and sub-agent slash commands, including oh-my-openagent-style `/ultrawork` (`/ulw`) and `/hyperplan` orchestration prompts, plus config-defined sub-agent model/thinking/args presets selected via `/subagent-preset` from `asyncSubagents` in `~/.config/pi/pi-tools-suite.jsonc`; includes the `frontend` profile for Gemini-friendly UI/UX and visual frontend work and the `oracle` profile for cross-provider second opinions; enforces a 30-minute per-agent execution timeout, project-wide `maxConcurrent` queueing, optional retry/backoff, and `result.json` structured metadata/chaining fields next to raw `result.md`; stores project-local run files and a registry under `.pi/subagents/` so result/status collection can recover after compaction or reload while the main session remains alive
@@ -247,8 +247,8 @@ 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 `openai-codex/gpt-5.6-luna`, with
251
- `antigravity/gemini-3-flash-preview` and then `zai/glm-5.3` as fallbacks. Its browser
250
+ The built-in `browser-qa` role runs on `zai/glm-5.3-flash`, with
251
+ `openai-codex/gpt-5.6-luna` as its fallback. Its browser
252
252
  workflow is an explicit private skill under `src/async-subagents/private-skills/`,
253
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
@@ -316,9 +316,17 @@ parent to ask the user for an update and rerun. See
316
316
  for complete profile shapes and `references/qa-flow.example.jsonc` beside it for
317
317
  the declarative, non-executable QA action/assertion format.
318
318
 
319
+ Browser QA videos automatically visualize pointer interactions. Clicks and
320
+ double-clicks show a transient cursor and pulse. Native drag/drop is replayed
321
+ for 450 ms with a large orange cursor, progressively drawn high-contrast path,
322
+ and green drop marker. The isolated, pointer-transparent layer is installed for
323
+ the whole browser context, including same-origin frames, declared popups, and
324
+ form-auth submission, and clears during the normal post-action stable interval
325
+ so screenshots and assertions remain state-focused.
326
+
319
327
  Async-subagents also injects a lightweight oh-my-openagent-style system-prompt strategy by model: non-GPT parents get `parallel-first`, an orchestration-first hint that favors ultrawork/subagents for broad work, while GPT-like parents get `deep-work`, a direct deep-worker hint that uses subagents only when clearly useful. Explicit custom system prompts (`--system-prompt`, `SYSTEM.md`, custom templates) are respected and skip this injection by default. Disable it with `PI_AGENT_STRATEGY=off`; force a strategy with `PI_AGENT_STRATEGY=parallel-first` or `PI_AGENT_STRATEGY=deep-work`; set `PI_AGENT_STRATEGY_WITH_CUSTOM_PROMPT=1` to append it even when a custom prompt is present.
320
328
 
321
- For blind-model screenshot/image inspection, use the main-session `coding-discipline` lookup tool. Async-subagents still supports `imagePaths` on tasks when a broader delegated track genuinely needs images, but it no longer ships a dedicated `vision` role. Dynamic provider capabilities can be missing or stale after switching models, so blind parent models can still be configured explicitly with case-insensitive `*` masks under `asyncSubagents.vision.blindModelPatterns` in `~/.config/pi/pi-tools-suite.jsonc`; this keeps guidance honest, not a sub-agent role.
329
+ For blind-model screenshot/image inspection, use the main-session `coding-discipline` lookup tool; the bundled default uses vision-capable `zai/glm-5.3-flash`. Async-subagents still supports `imagePaths` on tasks when a broader delegated track genuinely needs images, but it no longer ships a dedicated `vision` role. Dynamic provider capabilities can be missing or stale after switching models, so blind parent models can still be configured explicitly with case-insensitive `*` masks under `asyncSubagents.vision.blindModelPatterns` in `~/.config/pi/pi-tools-suite.jsonc`; do not include `zai/glm-5.3-flash` because it accepts image input. This keeps guidance honest, not a sub-agent role.
322
330
 
323
331
  When a task omits `subagentType`, async-subagents asks a lightweight router model to choose one configured type for each task from the task text/scope and the `types.<name>.description` metadata. Explicit task `subagentType` still wins. Keep type descriptions short, literal, and distinct because they are inserted into the router prompt for a small model. Router settings live under `asyncSubagents.routing` (`enabled`, `model`, `maxTaskChars`, `maxTokens`, `maxRetries`, `timeoutMs`, `debug`); the default router model is `zai/glm-4.5-air`. If the router is disabled, unavailable, aborted, or returns invalid JSON, omitted types fall back to `defaultType`.
324
332
 
@@ -337,10 +345,11 @@ Example shared async-subagents config section:
337
345
  },
338
346
  "presets": {
339
347
  "cheap": {
340
- "description": "Use GLM for text/code roles.",
348
+ "description": "Use GLM by role, including GLM-5.3 Flash for multimodal work.",
341
349
  "types": {
342
350
  "quick": { "model": "zai/glm-5.3", "thinking": "off" },
343
- "frontend": { "model": "antigravity/gemini-3-flash-preview", "fallbackModels": ["zai/glm-5.3"], "thinking": "medium" },
351
+ "frontend": { "model": "zai/glm-5.3-flash", "thinking": "medium" },
352
+ "browser-qa": { "model": "zai/glm-5.3-flash", "fallbackModels": ["openai-codex/gpt-5.6-luna"], "thinking": "low" },
344
353
  "review": { "model": "zai/glm-5.3", "thinking": "high" }
345
354
  }
346
355
  }
@@ -4,8 +4,8 @@
4
4
 
5
5
  Provide a cheap, fast `browser-qa` async-subagent that reproduces browser bugs
6
6
  and proves fixes with deterministic assertions plus screenshot, video, and trace
7
- evidence. The role uses `openai-codex/gpt-5.6-luna`, falling back to
8
- `antigravity/gemini-3-flash-preview` and then `zai/glm-5.3`.
7
+ evidence. The role uses `zai/glm-5.3-flash`, falling back to
8
+ `openai-codex/gpt-5.6-luna`.
9
9
 
10
10
  ## Private skill isolation
11
11
 
@@ -73,6 +73,20 @@ evidence. The role uses `openai-codex/gpt-5.6-luna`, falling back to
73
73
  screenshots, video finalization, and redacted result output. Before retaining
74
74
  a trace it removes network/non-image resource entries, redacts configured and
75
75
  runtime storage credentials, and verifies those values are absent.
76
+ - After every navigation or visible interaction, the runner waits for DOM
77
+ readiness, completion of requests started by the action, and disappearance of
78
+ common visible busy/spinner/skeleton markers. It requires a 500 ms stable
79
+ interval before the next action so recordings remain readable; a page that
80
+ stays busy through the flow timeout fails closed instead of being tested as a
81
+ loading shell. App-specific readiness still requires an explicit declarative
82
+ wait/assertion in the authored flow.
83
+ - Before any page is created, the runner installs a context-wide, isolated
84
+ interaction visualizer. Recorded clicks/double-clicks show a transient cursor
85
+ and pulse. Native drag/drop is replayed for 450 ms with a large orange cursor,
86
+ progressively drawn high-contrast path, and green drop marker. It also covers
87
+ same-origin frames, declared popups, and form-auth submission. The layer is
88
+ accessibility-hidden, pointer-transparent, never cancels application events,
89
+ and its bounded animations clear within the post-action stable interval.
76
90
  - Success and post-launch failure results include typed artifact groups. Every
77
91
  item has an absolute filesystem path and a `file:` URI; the sub-agent must
78
92
  present each item as a clickable Markdown link instead of reporting only the
@@ -112,7 +126,7 @@ evidence. The role uses `openai-codex/gpt-5.6-luna`, falling back to
112
126
 
113
127
  1. `browser-qa` resolves to the intended model/fallback and its self-contained
114
128
  private workflow, and its isolated child process can register the configured
115
- Antigravity model.
129
+ model provider.
116
130
  2. Spawn args contain `--no-skills` and the mandatory private skill for this
117
131
  profile; ordinary profiles retain existing skill discovery behavior.
118
132
  3. Auth profile listing and all error output are redacted; model-authored input
@@ -123,12 +137,16 @@ evidence. The role uses `openai-codex/gpt-5.6-luna`, falling back to
123
137
  flows, and successful redacted evidence creation.
124
138
  5. Browser QA flows/evidence live only inside the owning sub-agent directory;
125
139
  deleting the run removes them while persistent auth config/state remains.
126
- 6. Completed test runs report clickable screenshot, video, and trace links
140
+ 6. Runner tests prove that network activity and visible loading indicators are
141
+ awaited, persistent loading fails the flow, visible actions retain a stable
142
+ 500 ms video interval, and context-wide click/drag video visualization is
143
+ installed with bounded click pacing.
144
+ 7. Completed test runs report clickable screenshot, video, and trace links
127
145
  whenever those artifacts exist.
128
- 7. Timeout tests identify the last browser stage, launcher progress remains
146
+ 8. Timeout tests identify the last browser stage, launcher progress remains
129
147
  available when full RPC logging is disabled, and process-tree tests prove a
130
148
  descendant is terminated without signalling unrelated processes.
131
- 8. Suite tests/typecheck, host checks, and suite sync pass.
149
+ 9. Suite tests/typecheck, host checks, and suite sync pass.
132
150
 
133
151
  ## Real-browser regression test
134
152
 
@@ -44,9 +44,9 @@
44
44
  "vscode-languageserver-protocol": "^3.17.5"
45
45
  },
46
46
  "peerDependencies": {
47
- "@earendil-works/pi-ai": "0.84.3",
48
- "@earendil-works/pi-coding-agent": "0.84.3",
49
- "@earendil-works/pi-tui": "0.84.3",
47
+ "@earendil-works/pi-ai": "0.84.4",
48
+ "@earendil-works/pi-coding-agent": "0.84.4",
49
+ "@earendil-works/pi-tui": "0.84.4",
50
50
  "typebox": "*"
51
51
  },
52
52
  "devDependencies": {
@@ -5,7 +5,7 @@
5
5
  // Global async-subagents routing.
6
6
  // Rule of thumb:
7
7
  // - GLM: cheap/fast discovery, grep-style scanning, docs/test inventory.
8
- // - Gemini: frontend/UI/UX, visual/product polish, and multimodal-heavy analysis.
8
+ // - GLM-5.3 Flash: frontend/UI/UX, visual/product polish, and multimodal-heavy analysis.
9
9
  // - GPT/Claude: review/audit, deep reasoning, tests, and implementation/refactoring.
10
10
  // Override exact model IDs here if your pi provider uses different names.
11
11
  "defaultType": "quick",
@@ -34,7 +34,11 @@
34
34
  // screenshot inspection should use lookup; this only keeps guidance honest.
35
35
  // Matching is case-insensitive and supports `*` wildcards.
36
36
  "vision": {
37
- "blindModelPatterns": ["zai/glm*", "glm*", "*/glm*"]
37
+ "blindModelPatterns": [
38
+ "zai/glm-4.5*", "glm-4.5*", "*/glm-4.5*",
39
+ "zai/glm-5-turbo*", "glm-5-turbo*", "*/glm-5-turbo*",
40
+ "zai/glm-5.3", "glm-5.3", "*/glm-5.3"
41
+ ]
38
42
  },
39
43
 
40
44
  // Optional per-type prompt customization (leave commented until needed):
@@ -109,14 +113,14 @@
109
113
 
110
114
  "presets": {
111
115
  "cheap": {
112
- "description": "Use cheap GLM/Gemini Flash models for text/code roles.",
116
+ "description": "Use GLM models by role, including GLM-5.3 Flash for multimodal work.",
113
117
  "types": {
114
118
  "quick": { "model": "zai/glm-4.5-air", "thinking": "off" },
115
119
  "scan": { "model": "zai/glm-4.5-air", "thinking": "off" },
116
120
  "research": { "model": "zai/glm-5-turbo", "thinking": "low" },
117
121
  "docs": { "model": "zai/glm-4.5-air", "thinking": "low" },
118
- "frontend": { "model": "antigravity/gemini-3-flash-preview", "fallbackModels": ["zai/glm-5.3"], "thinking": "medium" },
119
- "browser-qa": { "model": "openai-codex/gpt-5.6-luna", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "low" },
122
+ "frontend": { "model": "zai/glm-5.3-flash", "thinking": "medium" },
123
+ "browser-qa": { "model": "zai/glm-5.3-flash", "fallbackModels": ["openai-codex/gpt-5.6-luna"], "thinking": "low" },
120
124
  "tests": { "model": "zai/glm-5-turbo", "thinking": "medium" },
121
125
  "review": { "model": "zai/glm-5.3", "thinking": "high" },
122
126
  "implement": { "model": "zai/glm-5.3", "thinking": "high" },
@@ -132,7 +136,7 @@
132
136
  "research": { "model": "openai-codex/gpt-5.6-terra", "fallbackModels": ["zai/glm-5-turbo"], "thinking": "low" },
133
137
  "docs": { "model": "openai-codex/gpt-5.6-luna", "fallbackModels": ["zai/glm-4.5-air"], "thinking": "low" },
134
138
  "frontend": { "model": "openai-codex/gpt-5.6-terra", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "medium" },
135
- "browser-qa": { "model": "openai-codex/gpt-5.6-luna", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "low" },
139
+ "browser-qa": { "model": "zai/glm-5.3-flash", "fallbackModels": ["openai-codex/gpt-5.6-luna"], "thinking": "low" },
136
140
  "tests": { "model": "openai-codex/gpt-5.6-terra", "fallbackModels": ["zai/glm-5-turbo"], "thinking": "medium" },
137
141
  "review": { "model": "openai-codex/gpt-5.6-sol", "fallbackModels": ["zai/glm-5.3"], "thinking": "high" },
138
142
  "implement": { "model": "openai-codex/gpt-5.6-sol", "fallbackModels": ["zai/glm-5.3"], "thinking": "high" },
@@ -148,7 +152,7 @@
148
152
  "research": { "model": "antigravity/gemini-3.1-pro-preview", "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-5-turbo"], "thinking": "medium" },
149
153
  "docs": { "model": "antigravity/gemini-2.5-flash", "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-4.5-air"], "thinking": "medium" },
150
154
  "frontend": { "model": "antigravity/gemini-3.1-pro-preview-customtools", "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-5.3"], "thinking": "low" },
151
- "browser-qa": { "model": "openai-codex/gpt-5.6-luna", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "low" },
155
+ "browser-qa": { "model": "zai/glm-5.3-flash", "fallbackModels": ["openai-codex/gpt-5.6-luna"], "thinking": "low" },
152
156
  "tests": { "model": "antigravity/antigravity-claude-sonnet-4-6", "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-5-turbo"], "thinking": "high" },
153
157
  "review": { "model": "antigravity/antigravity-claude-sonnet-4-6", "fallbackModels": ["openai-codex/gpt-5.6-sol", "zai/glm-5.3"], "thinking": "high" },
154
158
  "implement": { "model": "openai-codex/gpt-5.6-sol", "fallbackModels": ["zai/glm-5.3"], "thinking": "high" },
@@ -203,8 +207,8 @@
203
207
 
204
208
  "browser-qa": {
205
209
  "description": "Use for browser-based visual QA: reproduce UI bugs and verify fixes with deterministic assertions, screenshots, video, and traces.",
206
- "model": "openai-codex/gpt-5.6-luna",
207
- "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"],
210
+ "model": "zai/glm-5.3-flash",
211
+ "fallbackModels": ["openai-codex/gpt-5.6-luna"],
208
212
  "thinking": "low",
209
213
  "timeoutMs": 120000,
210
214
  "tools": ["read", "grep", "bash"]
@@ -215,8 +215,8 @@ const BUILTIN_CONFIG: SubagentConfig = {
215
215
  },
216
216
  "browser-qa": {
217
217
  description: "Use for browser-based visual QA: reproduce UI bugs and verify fixes with deterministic assertions, screenshots, video, and traces.",
218
- model: "openai-codex/gpt-5.6-luna",
219
- fallbackModels: ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"],
218
+ model: "zai/glm-5.3-flash",
219
+ fallbackModels: ["openai-codex/gpt-5.6-luna"],
220
220
  thinking: "low",
221
221
  timeoutMs: 120_000,
222
222
  tools: ["read", "grep", "bash"],
@@ -88,15 +88,18 @@ export function spawnAgent(
88
88
  // detached subprocesses do not depend on persisted local pi settings.
89
89
  const persistSessions = shouldPersistSubagentSessions();
90
90
  const sessionDir = persistSessions ? getAgentSessionDir(agentDir) : undefined;
91
+ const forwardedExtraArgs = options.isolatedSkills?.length ? withoutSkillArgs(extraArgs) : extraArgs;
91
92
  if (sessionDir) fs.mkdirSync(sessionDir, { recursive: true });
92
93
  const piArgs: string[] = ["--mode", "rpc"];
93
94
  if (sessionDir) piArgs.push("--session-dir", sessionDir);
94
95
  else piArgs.push("--no-session");
95
96
  piArgs.push("--no-extensions");
96
97
  piArgs.push("--extension", getModelToolsExtensionPath());
97
- // `--no-extensions` keeps sub-agents isolated, but the suite-owned provider
98
- // is infrastructure: always restore it, regardless of the selected model.
99
- piArgs.push("--extension", getAntigravityAuthExtensionPath());
98
+ // Preserve `--no-extensions` unless this invocation explicitly selects an
99
+ // Antigravity model. Environment/default models do not opt the provider in.
100
+ if (usesAntigravityModel(task.model, forwardedExtraArgs)) {
101
+ piArgs.push("--extension", getAntigravityAuthExtensionPath());
102
+ }
100
103
  if (options.isolatedSkills && options.isolatedSkills.length > 0) {
101
104
  piArgs.push("--no-skills");
102
105
  for (const skillPath of options.isolatedSkills) piArgs.push("--skill", skillPath);
@@ -111,7 +114,7 @@ export function spawnAgent(
111
114
  if (task.thinking) piArgs.push("--thinking", task.thinking);
112
115
 
113
116
  // User-supplied extra args (e.g. --thinking high)
114
- piArgs.push(...(options.isolatedSkills?.length ? withoutSkillArgs(extraArgs) : extraArgs));
117
+ piArgs.push(...forwardedExtraArgs);
115
118
  // Keep recursive/interactive parent-only tools disabled even if explicit
116
119
  // sub-agent extraArgs load additional extensions or override --tools.
117
120
  piArgs.push("--extension", getSubagentToolGuardExtensionPath());
@@ -470,6 +473,22 @@ function withoutSkillArgs(args: string[]): string[] {
470
473
  return filtered;
471
474
  }
472
475
 
476
+ function usesAntigravityModel(taskModel: string | undefined, extraArgs: string[]): boolean {
477
+ let model = taskModel?.trim() || undefined;
478
+ for (let index = 0; index < extraArgs.length; index += 1) {
479
+ const arg = extraArgs[index];
480
+ if (arg === "--model" || arg === "-m") {
481
+ model = extraArgs[index + 1]?.trim() || undefined;
482
+ index += 1;
483
+ continue;
484
+ }
485
+ if (arg.startsWith("--model=")) {
486
+ model = arg.slice("--model=".length).trim() || undefined;
487
+ }
488
+ }
489
+ return Boolean(model?.startsWith("antigravity/") && model.length > "antigravity/".length);
490
+ }
491
+
473
492
  function getModelToolsExtensionPath(): string {
474
493
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "model-tools", "index.ts");
475
494
  }