gsd-pi 2.7.1 → 2.8.0

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.
Files changed (53) hide show
  1. package/README.md +12 -5
  2. package/dist/loader.js +0 -0
  3. package/dist/modes/interactive/theme/dark.json +85 -0
  4. package/dist/modes/interactive/theme/light.json +84 -0
  5. package/dist/modes/interactive/theme/theme-schema.json +335 -0
  6. package/dist/modes/interactive/theme/theme.d.ts +78 -0
  7. package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  8. package/dist/modes/interactive/theme/theme.js +949 -0
  9. package/dist/modes/interactive/theme/theme.js.map +1 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  11. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
  12. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  13. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  14. package/node_modules/cliui/CHANGELOG.md +121 -0
  15. package/node_modules/color-convert/CHANGELOG.md +54 -0
  16. package/node_modules/esprima/ChangeLog +235 -0
  17. package/node_modules/mz/HISTORY.md +66 -0
  18. package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
  19. package/node_modules/source-map/CHANGELOG.md +301 -0
  20. package/node_modules/thenify/History.md +11 -0
  21. package/node_modules/thenify-all/History.md +11 -0
  22. package/node_modules/y18n/CHANGELOG.md +100 -0
  23. package/node_modules/yargs/CHANGELOG.md +88 -0
  24. package/node_modules/yargs-parser/CHANGELOG.md +263 -0
  25. package/package.json +5 -2
  26. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  27. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
  28. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  29. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  30. package/src/resources/extensions/browser-tools/capture.ts +165 -0
  31. package/src/resources/extensions/browser-tools/evaluate-helpers.ts +184 -0
  32. package/src/resources/extensions/browser-tools/index.ts +47 -4985
  33. package/src/resources/extensions/browser-tools/lifecycle.ts +265 -0
  34. package/src/resources/extensions/browser-tools/package.json +5 -1
  35. package/src/resources/extensions/browser-tools/refs.ts +264 -0
  36. package/src/resources/extensions/browser-tools/settle.ts +197 -0
  37. package/src/resources/extensions/browser-tools/state.ts +408 -0
  38. package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +652 -0
  39. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +614 -0
  40. package/src/resources/extensions/browser-tools/tools/assertions.ts +342 -0
  41. package/src/resources/extensions/browser-tools/tools/forms.ts +801 -0
  42. package/src/resources/extensions/browser-tools/tools/inspection.ts +492 -0
  43. package/src/resources/extensions/browser-tools/tools/intent.ts +614 -0
  44. package/src/resources/extensions/browser-tools/tools/interaction.ts +865 -0
  45. package/src/resources/extensions/browser-tools/tools/navigation.ts +232 -0
  46. package/src/resources/extensions/browser-tools/tools/pages.ts +303 -0
  47. package/src/resources/extensions/browser-tools/tools/refs.ts +541 -0
  48. package/src/resources/extensions/browser-tools/tools/screenshot.ts +83 -0
  49. package/src/resources/extensions/browser-tools/tools/session.ts +400 -0
  50. package/src/resources/extensions/browser-tools/tools/wait.ts +247 -0
  51. package/src/resources/extensions/browser-tools/utils.ts +660 -0
  52. package/src/resources/extensions/gsd/git-service.ts +3 -0
  53. package/src/resources/extensions/shared/interview-ui.ts +1 -1
@@ -0,0 +1,342 @@
1
+ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { StringEnum } from "@gsd/pi-ai";
4
+ import {
5
+ diffCompactStates,
6
+ evaluateAssertionChecks,
7
+ findAction,
8
+ runBatchSteps,
9
+ validateWaitParams,
10
+ createRegionStableScript,
11
+ parseThreshold,
12
+ includesNeedle,
13
+ } from "../core.js";
14
+ import type { ToolDeps, CompactPageState } from "../state.js";
15
+ import {
16
+ getConsoleLogs,
17
+ getCurrentRefMap,
18
+ getLastActionBeforeState,
19
+ getLastActionAfterState,
20
+ setLastActionBeforeState,
21
+ setLastActionAfterState,
22
+ getActionTimeline,
23
+ } from "../state.js";
24
+
25
+ export function registerAssertionTools(pi: ExtensionAPI, deps: ToolDeps): void {
26
+ // -------------------------------------------------------------------------
27
+ // browser_assert
28
+ // -------------------------------------------------------------------------
29
+ pi.registerTool({
30
+ name: "browser_assert",
31
+ label: "Browser Assert",
32
+ description:
33
+ "Run one or more explicit browser assertions and return structured PASS/FAIL results. Prefer this for verification instead of inferring success from prose summaries.",
34
+ promptGuidelines: [
35
+ "Prefer browser_assert for browser verification instead of inferring success from summaries.",
36
+ "When finishing UI work, explicit browser assertions should usually be the final verification step.",
37
+ "Use checks for URL, text, selector state, value, and browser diagnostics whenever those signals are available.",
38
+ ],
39
+ parameters: Type.Object({
40
+ checks: Type.Array(
41
+ Type.Object({
42
+ kind: Type.String({ description: "Assertion kind, e.g. url_contains, text_visible, selector_visible, value_equals, no_console_errors, no_failed_requests, request_url_seen, response_status, console_message_matches, network_count, console_count, no_console_errors_since, no_failed_requests_since" }),
43
+ selector: Type.Optional(Type.String()),
44
+ text: Type.Optional(Type.String()),
45
+ value: Type.Optional(Type.String()),
46
+ checked: Type.Optional(Type.Boolean()),
47
+ sinceActionId: Type.Optional(Type.Number()),
48
+ })
49
+ ),
50
+ }),
51
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
52
+ try {
53
+ const { page: p } = await deps.ensureBrowser();
54
+ const target = deps.getActiveTarget();
55
+ const state = await deps.collectAssertionState(p, params.checks, target);
56
+ const result = evaluateAssertionChecks({ checks: params.checks, state });
57
+ return {
58
+ content: [{ type: "text", text: `Browser assert\n\n${deps.formatAssertionText(result)}` }],
59
+ details: { ...result, url: state.url, title: state.title },
60
+ isError: !result.verified,
61
+ };
62
+ } catch (err: any) {
63
+ return {
64
+ content: [{ type: "text", text: `Browser assert failed: ${err.message}` }],
65
+ details: { error: err.message },
66
+ isError: true,
67
+ };
68
+ }
69
+ },
70
+ });
71
+
72
+ // -------------------------------------------------------------------------
73
+ // browser_diff
74
+ // -------------------------------------------------------------------------
75
+ pi.registerTool({
76
+ name: "browser_diff",
77
+ label: "Browser Diff",
78
+ description:
79
+ "Report meaningful browser-state changes. By default compares the current page to the most recent tracked action state. Use this to understand what changed after a click, submit, or navigation.",
80
+ promptGuidelines: [
81
+ "Use browser_diff after ambiguous or high-impact actions when you need to know what changed.",
82
+ "Prefer browser_diff over requesting a broad new page inspection when the question is change detection.",
83
+ ],
84
+ parameters: Type.Object({
85
+ sinceActionId: Type.Optional(Type.Number({ description: "Optional action id to diff against. Uses that action's stored after-state when available." })),
86
+ }),
87
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
88
+ try {
89
+ const { page: p } = await deps.ensureBrowser();
90
+ const target = deps.getActiveTarget();
91
+ const current = await deps.captureCompactPageState(p, { includeBodyText: true, target });
92
+ let baseline: CompactPageState | null = null;
93
+ if (params.sinceActionId) {
94
+ const actionTimeline = getActionTimeline();
95
+ const action = findAction(actionTimeline, params.sinceActionId) as { afterState?: CompactPageState } | null;
96
+ baseline = action?.afterState ?? null;
97
+ }
98
+ if (!baseline) {
99
+ baseline = getLastActionAfterState() ?? getLastActionBeforeState();
100
+ }
101
+ if (!baseline) {
102
+ return {
103
+ content: [{ type: "text", text: "Browser diff unavailable: no prior tracked browser state exists yet." }],
104
+ details: { changed: false, changes: [], summary: "No prior tracked state" },
105
+ isError: true,
106
+ };
107
+ }
108
+ const diff = diffCompactStates(baseline, current);
109
+ return {
110
+ content: [{ type: "text", text: `Browser diff\n\n${deps.formatDiffText(diff)}` }],
111
+ details: diff,
112
+ };
113
+ } catch (err: any) {
114
+ return {
115
+ content: [{ type: "text", text: `Browser diff failed: ${err.message}` }],
116
+ details: { error: err.message },
117
+ isError: true,
118
+ };
119
+ }
120
+ },
121
+ });
122
+
123
+ // -------------------------------------------------------------------------
124
+ // browser_batch
125
+ // -------------------------------------------------------------------------
126
+ pi.registerTool({
127
+ name: "browser_batch",
128
+ label: "Browser Batch",
129
+ description:
130
+ "Execute multiple explicit browser steps in one call. Prefer this for obvious action sequences like click → type → wait → assert to reduce round trips and token usage.",
131
+ promptGuidelines: [
132
+ "If the next 2-5 browser actions are obvious and low-risk, prefer browser_batch over multiple tiny browser calls.",
133
+ "Use browser_batch for explicit sequences like click → type → submit → wait → assert.",
134
+ "Keep browser_batch steps explicit; do not use it as a speculative planner.",
135
+ ],
136
+ parameters: Type.Object({
137
+ steps: Type.Array(
138
+ Type.Object({
139
+ action: StringEnum(["navigate", "click", "type", "key_press", "wait_for", "assert", "click_ref", "fill_ref"] as const),
140
+ selector: Type.Optional(Type.String()),
141
+ text: Type.Optional(Type.String()),
142
+ url: Type.Optional(Type.String()),
143
+ key: Type.Optional(Type.String()),
144
+ condition: Type.Optional(Type.String()),
145
+ value: Type.Optional(Type.String()),
146
+ threshold: Type.Optional(Type.String()),
147
+ timeout: Type.Optional(Type.Number()),
148
+ clearFirst: Type.Optional(Type.Boolean()),
149
+ submit: Type.Optional(Type.Boolean()),
150
+ ref: Type.Optional(Type.String()),
151
+ checks: Type.Optional(Type.Array(Type.Object({
152
+ kind: Type.String({ description: "Assertion kind, e.g. url_contains, text_visible, selector_visible, value_equals, no_console_errors, no_failed_requests, request_url_seen, response_status, console_message_matches, network_count, console_count, no_console_errors_since, no_failed_requests_since" }),
153
+ selector: Type.Optional(Type.String()),
154
+ text: Type.Optional(Type.String()),
155
+ value: Type.Optional(Type.String()),
156
+ checked: Type.Optional(Type.Boolean()),
157
+ sinceActionId: Type.Optional(Type.Number()),
158
+ }))),
159
+ })
160
+ ),
161
+ stopOnFailure: Type.Optional(Type.Boolean({ description: "Stop after the first failing step (default: true)." })),
162
+ finalSummaryOnly: Type.Optional(Type.Boolean({ description: "Return only the compact final batch summary in content while keeping step results in details." })),
163
+ }),
164
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
165
+ let actionId: number | null = null;
166
+ let beforeState: CompactPageState | null = null;
167
+ try {
168
+ const { page: p } = await deps.ensureBrowser();
169
+ const target = deps.getActiveTarget();
170
+ beforeState = await deps.captureCompactPageState(p, { includeBodyText: true, target });
171
+ actionId = deps.beginTrackedAction("browser_batch", params, beforeState.url).id;
172
+ const executeStep = async (step: any, index: number) => {
173
+ const stepTarget = deps.getActiveTarget();
174
+ try {
175
+ switch (step.action) {
176
+ case "navigate": {
177
+ await p.goto(step.url, { waitUntil: "domcontentloaded", timeout: 30000 });
178
+ await p.waitForLoadState("networkidle", { timeout: 5000 }).catch(() => {});
179
+ return { ok: true, action: step.action, url: p.url() };
180
+ }
181
+ case "click": {
182
+ await stepTarget.locator(step.selector).first().click({ timeout: step.timeout ?? 8000 });
183
+ await deps.settleAfterActionAdaptive(p);
184
+ return { ok: true, action: step.action, selector: step.selector, url: p.url() };
185
+ }
186
+ case "type": {
187
+ if (step.clearFirst) {
188
+ await stepTarget.locator(step.selector).first().fill("");
189
+ }
190
+ await stepTarget.locator(step.selector).first().fill(step.text ?? "", { timeout: step.timeout ?? 8000 });
191
+ if (step.submit) await p.keyboard.press("Enter");
192
+ await deps.settleAfterActionAdaptive(p);
193
+ return { ok: true, action: step.action, selector: step.selector, text: step.text };
194
+ }
195
+ case "key_press": {
196
+ await p.keyboard.press(step.key);
197
+ await deps.settleAfterActionAdaptive(p, { checkFocusStability: true });
198
+ return { ok: true, action: step.action, key: step.key };
199
+ }
200
+ case "wait_for": {
201
+ const timeout = step.timeout ?? 10000;
202
+ const waitValidation = validateWaitParams({ condition: step.condition, value: step.value, threshold: step.threshold });
203
+ if (waitValidation) throw new Error(waitValidation.error);
204
+
205
+ if (step.condition === "selector_visible") await stepTarget.waitForSelector(step.value, { state: "visible", timeout });
206
+ else if (step.condition === "selector_hidden") await stepTarget.waitForSelector(step.value, { state: "hidden", timeout });
207
+ else if (step.condition === "url_contains") await p.waitForURL((url) => url.toString().includes(step.value), { timeout });
208
+ else if (step.condition === "network_idle") await p.waitForLoadState("networkidle", { timeout });
209
+ else if (step.condition === "delay") await new Promise((resolve) => setTimeout(resolve, parseInt(step.value ?? "1000", 10)));
210
+ else if (step.condition === "text_visible") {
211
+ await stepTarget.waitForFunction(
212
+ (needle: string) => (document.body?.innerText ?? "").toLowerCase().includes(needle.toLowerCase()),
213
+ step.value!,
214
+ { timeout }
215
+ );
216
+ }
217
+ else if (step.condition === "text_hidden") {
218
+ await stepTarget.waitForFunction(
219
+ (needle: string) => !(document.body?.innerText ?? "").toLowerCase().includes(needle.toLowerCase()),
220
+ step.value!,
221
+ { timeout }
222
+ );
223
+ }
224
+ else if (step.condition === "request_completed") {
225
+ await deps.getActivePage().waitForResponse(
226
+ (resp: any) => resp.url().includes(step.value!),
227
+ { timeout }
228
+ );
229
+ }
230
+ else if (step.condition === "console_message") {
231
+ const needle = step.value!;
232
+ const startTime = Date.now();
233
+ let found = false;
234
+ while (Date.now() - startTime < timeout) {
235
+ if (getConsoleLogs().find((entry) => includesNeedle(entry.text, needle))) { found = true; break; }
236
+ await new Promise((resolve) => setTimeout(resolve, 100));
237
+ }
238
+ if (!found) throw new Error(`Timed out waiting for console message matching "${needle}" (${timeout}ms)`);
239
+ }
240
+ else if (step.condition === "element_count") {
241
+ const threshold = parseThreshold(step.threshold ?? ">=1");
242
+ if (!threshold) throw new Error(`element_count threshold is malformed: "${step.threshold}"`);
243
+ const selector = step.value!;
244
+ const op = threshold.op;
245
+ const n = threshold.n;
246
+ await stepTarget.waitForFunction(
247
+ ({ selector, op, n }: { selector: string; op: string; n: number }) => {
248
+ const count = document.querySelectorAll(selector).length;
249
+ switch (op) {
250
+ case ">=": return count >= n;
251
+ case "<=": return count <= n;
252
+ case "==": return count === n;
253
+ case ">": return count > n;
254
+ case "<": return count < n;
255
+ default: return false;
256
+ }
257
+ },
258
+ { selector, op, n },
259
+ { timeout }
260
+ );
261
+ }
262
+ else if (step.condition === "region_stable") {
263
+ const script = createRegionStableScript(step.value!);
264
+ await stepTarget.waitForFunction(script, undefined, { timeout, polling: 200 });
265
+ }
266
+ else throw new Error(`Unsupported wait condition: ${step.condition}`);
267
+ return { ok: true, action: step.action, condition: step.condition, value: step.value };
268
+ }
269
+ case "assert": {
270
+ const state = await deps.collectAssertionState(p, step.checks ?? [], stepTarget);
271
+ const assertion = evaluateAssertionChecks({ checks: step.checks ?? [], state });
272
+ return { ok: assertion.verified, action: step.action, summary: assertion.summary, assertion };
273
+ }
274
+ case "click_ref": {
275
+ const parsedRef = deps.parseRef(step.ref);
276
+ const currentRefMap = getCurrentRefMap();
277
+ const node = currentRefMap[parsedRef.key];
278
+ if (!node) throw new Error(`Unknown ref: ${step.ref}`);
279
+ const resolved = await deps.resolveRefTarget(stepTarget, node);
280
+ if (!resolved.ok) throw new Error(resolved.reason);
281
+ await stepTarget.locator(resolved.selector).first().click({ timeout: step.timeout ?? 8000 });
282
+ await deps.settleAfterActionAdaptive(p);
283
+ return { ok: true, action: step.action, ref: step.ref };
284
+ }
285
+ case "fill_ref": {
286
+ const parsedRef = deps.parseRef(step.ref);
287
+ const currentRefMap = getCurrentRefMap();
288
+ const node = currentRefMap[parsedRef.key];
289
+ if (!node) throw new Error(`Unknown ref: ${step.ref}`);
290
+ const resolved = await deps.resolveRefTarget(stepTarget, node);
291
+ if (!resolved.ok) throw new Error(resolved.reason);
292
+ if (step.clearFirst) await stepTarget.locator(resolved.selector).first().fill("");
293
+ await stepTarget.locator(resolved.selector).first().fill(step.text ?? "", { timeout: step.timeout ?? 8000 });
294
+ if (step.submit) await p.keyboard.press("Enter");
295
+ await deps.settleAfterActionAdaptive(p);
296
+ return { ok: true, action: step.action, ref: step.ref, text: step.text };
297
+ }
298
+ default:
299
+ throw new Error(`Unsupported batch action: ${step.action}`);
300
+ }
301
+ } catch (err: any) {
302
+ return { ok: false, action: step.action, index, message: err.message };
303
+ }
304
+ };
305
+ const run = await runBatchSteps({
306
+ steps: params.steps,
307
+ executeStep,
308
+ stopOnFailure: params.stopOnFailure !== false,
309
+ });
310
+ const batchEndTarget = deps.getActiveTarget();
311
+ const afterState = await deps.captureCompactPageState(p, { includeBodyText: true, target: batchEndTarget });
312
+ const diff = diffCompactStates(beforeState!, afterState);
313
+ setLastActionBeforeState(beforeState!);
314
+ setLastActionAfterState(afterState);
315
+ deps.finishTrackedAction(actionId!, {
316
+ status: run.ok ? "success" : "error",
317
+ afterUrl: afterState.url,
318
+ diffSummary: diff.summary,
319
+ changed: diff.changed,
320
+ error: run.ok ? undefined : run.summary,
321
+ beforeState: beforeState!,
322
+ afterState,
323
+ });
324
+ const summary = `${run.summary}\n${run.stepResults.map((step: any, index: number) => `- ${index + 1}. ${step.action}: ${step.ok ? "PASS" : "FAIL"}${step.message ? ` (${step.message})` : ""}`).join("\n")}`;
325
+ return {
326
+ content: [{ type: "text", text: params.finalSummaryOnly ? run.summary : `Browser batch\nAction: ${actionId}\n\n${summary}\n\nDiff:\n${deps.formatDiffText(diff)}` }],
327
+ details: { actionId, diff, ...run },
328
+ isError: !run.ok,
329
+ };
330
+ } catch (err: any) {
331
+ if (actionId !== null) {
332
+ deps.finishTrackedAction(actionId, { status: "error", afterUrl: deps.getActivePageOrNull()?.url() ?? "", error: err.message, beforeState: beforeState ?? undefined });
333
+ }
334
+ return {
335
+ content: [{ type: "text", text: `Browser batch failed: ${err.message}` }],
336
+ details: { error: err.message, actionId },
337
+ isError: true,
338
+ };
339
+ }
340
+ },
341
+ });
342
+ }