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.
- package/README.md +12 -5
- package/dist/loader.js +0 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +84 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/dist/modes/interactive/theme/theme.js +949 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/node_modules/cliui/CHANGELOG.md +121 -0
- package/node_modules/color-convert/CHANGELOG.md +54 -0
- package/node_modules/esprima/ChangeLog +235 -0
- package/node_modules/mz/HISTORY.md +66 -0
- package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
- package/node_modules/source-map/CHANGELOG.md +301 -0
- package/node_modules/thenify/History.md +11 -0
- package/node_modules/thenify-all/History.md +11 -0
- package/node_modules/y18n/CHANGELOG.md +100 -0
- package/node_modules/yargs/CHANGELOG.md +88 -0
- package/node_modules/yargs-parser/CHANGELOG.md +263 -0
- package/package.json +5 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/src/resources/extensions/browser-tools/capture.ts +165 -0
- package/src/resources/extensions/browser-tools/evaluate-helpers.ts +184 -0
- package/src/resources/extensions/browser-tools/index.ts +47 -4985
- package/src/resources/extensions/browser-tools/lifecycle.ts +265 -0
- package/src/resources/extensions/browser-tools/package.json +5 -1
- package/src/resources/extensions/browser-tools/refs.ts +264 -0
- package/src/resources/extensions/browser-tools/settle.ts +197 -0
- package/src/resources/extensions/browser-tools/state.ts +408 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +652 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +614 -0
- package/src/resources/extensions/browser-tools/tools/assertions.ts +342 -0
- package/src/resources/extensions/browser-tools/tools/forms.ts +801 -0
- package/src/resources/extensions/browser-tools/tools/inspection.ts +492 -0
- package/src/resources/extensions/browser-tools/tools/intent.ts +614 -0
- package/src/resources/extensions/browser-tools/tools/interaction.ts +865 -0
- package/src/resources/extensions/browser-tools/tools/navigation.ts +232 -0
- package/src/resources/extensions/browser-tools/tools/pages.ts +303 -0
- package/src/resources/extensions/browser-tools/tools/refs.ts +541 -0
- package/src/resources/extensions/browser-tools/tools/screenshot.ts +83 -0
- package/src/resources/extensions/browser-tools/tools/session.ts +400 -0
- package/src/resources/extensions/browser-tools/tools/wait.ts +247 -0
- package/src/resources/extensions/browser-tools/utils.ts +660 -0
- package/src/resources/extensions/gsd/git-service.ts +3 -0
- package/src/resources/extensions/shared/interview-ui.ts +1 -1
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser-tools — Node-side utility functions
|
|
3
|
+
*
|
|
4
|
+
* All functions that were helpers in index.ts but run in Node (not browser).
|
|
5
|
+
* They import state accessors from ./state.ts — never raw module-level variables.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Frame, Page } from "playwright";
|
|
9
|
+
import { mkdir, stat, writeFile, copyFile } from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_MAX_BYTES,
|
|
13
|
+
DEFAULT_MAX_LINES,
|
|
14
|
+
truncateHead,
|
|
15
|
+
} from "@gsd/pi-coding-agent";
|
|
16
|
+
import {
|
|
17
|
+
beginAction,
|
|
18
|
+
finishAction,
|
|
19
|
+
findAction,
|
|
20
|
+
toActionParamsSummary,
|
|
21
|
+
registryListPages,
|
|
22
|
+
} from "./core.js";
|
|
23
|
+
import {
|
|
24
|
+
ARTIFACT_ROOT,
|
|
25
|
+
getActiveFrame,
|
|
26
|
+
getActiveTraceSession,
|
|
27
|
+
getConsoleLogs,
|
|
28
|
+
getDialogLogs,
|
|
29
|
+
getHarState,
|
|
30
|
+
getNetworkLogs,
|
|
31
|
+
getSessionArtifactDir,
|
|
32
|
+
getSessionStartedAt,
|
|
33
|
+
setSessionArtifactDir,
|
|
34
|
+
setSessionStartedAt,
|
|
35
|
+
pageRegistry,
|
|
36
|
+
actionTimeline,
|
|
37
|
+
getPendingCriticalRequestsByPage,
|
|
38
|
+
getLastActionBeforeState,
|
|
39
|
+
getLastActionAfterState,
|
|
40
|
+
setLastActionBeforeState,
|
|
41
|
+
setLastActionAfterState,
|
|
42
|
+
type ConsoleEntry,
|
|
43
|
+
type NetworkEntry,
|
|
44
|
+
type CompactPageState,
|
|
45
|
+
type CompactSelectorState,
|
|
46
|
+
type ClickTargetStateSnapshot,
|
|
47
|
+
type VerificationCheck,
|
|
48
|
+
type VerificationResult,
|
|
49
|
+
type BrowserAssertionCheckInput,
|
|
50
|
+
type AdaptiveSettleOptions,
|
|
51
|
+
type AdaptiveSettleDetails,
|
|
52
|
+
type ParsedRefSpec,
|
|
53
|
+
} from "./state.js";
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Text truncation
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
export function truncateText(text: string): string {
|
|
60
|
+
const result = truncateHead(text, {
|
|
61
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
62
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
63
|
+
});
|
|
64
|
+
if (result.truncated) {
|
|
65
|
+
return (
|
|
66
|
+
result.content +
|
|
67
|
+
`\n\n[Output truncated: ${result.outputLines}/${result.totalLines} lines shown]`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return result.content;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Artifact helpers
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
export function formatArtifactTimestamp(timestamp: number): string {
|
|
78
|
+
return new Date(timestamp).toISOString().replace(/[:.]/g, "-");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function ensureDir(dirPath: string): Promise<string> {
|
|
82
|
+
await mkdir(dirPath, { recursive: true });
|
|
83
|
+
return dirPath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function writeArtifactFile(
|
|
87
|
+
filePath: string,
|
|
88
|
+
content: string | Uint8Array,
|
|
89
|
+
): Promise<{ path: string; bytes: number }> {
|
|
90
|
+
await ensureDir(path.dirname(filePath));
|
|
91
|
+
await writeFile(filePath, content);
|
|
92
|
+
const fileStat = await stat(filePath);
|
|
93
|
+
return { path: filePath, bytes: fileStat.size };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function copyArtifactFile(
|
|
97
|
+
sourcePath: string,
|
|
98
|
+
destinationPath: string,
|
|
99
|
+
): Promise<{ path: string; bytes: number }> {
|
|
100
|
+
await ensureDir(path.dirname(destinationPath));
|
|
101
|
+
await copyFile(sourcePath, destinationPath);
|
|
102
|
+
const fileStat = await stat(destinationPath);
|
|
103
|
+
return { path: destinationPath, bytes: fileStat.size };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function ensureSessionStartedAt(): number {
|
|
107
|
+
let t = getSessionStartedAt();
|
|
108
|
+
if (!t) {
|
|
109
|
+
t = Date.now();
|
|
110
|
+
setSessionStartedAt(t);
|
|
111
|
+
}
|
|
112
|
+
return t;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function ensureSessionArtifactDir(): Promise<string> {
|
|
116
|
+
const existing = getSessionArtifactDir();
|
|
117
|
+
if (existing) {
|
|
118
|
+
await ensureDir(existing);
|
|
119
|
+
return existing;
|
|
120
|
+
}
|
|
121
|
+
const startedAt = ensureSessionStartedAt();
|
|
122
|
+
const dir = path.join(ARTIFACT_ROOT, `${formatArtifactTimestamp(startedAt)}-session`);
|
|
123
|
+
setSessionArtifactDir(dir);
|
|
124
|
+
await ensureDir(dir);
|
|
125
|
+
return dir;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function buildSessionArtifactPath(filename: string): string {
|
|
129
|
+
const dir = getSessionArtifactDir();
|
|
130
|
+
if (!dir) {
|
|
131
|
+
throw new Error("browser session artifact directory is not initialized");
|
|
132
|
+
}
|
|
133
|
+
return path.join(dir, filename);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getActivePageMetadata() {
|
|
137
|
+
const registry = pageRegistry;
|
|
138
|
+
const activeEntry =
|
|
139
|
+
registry.activePageId !== null
|
|
140
|
+
? registry.pages.find((entry: any) => entry.id === registry.activePageId) ?? null
|
|
141
|
+
: null;
|
|
142
|
+
return {
|
|
143
|
+
id: activeEntry?.id ?? null,
|
|
144
|
+
title: activeEntry?.title ?? "",
|
|
145
|
+
url: activeEntry?.url ?? "",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function getActiveFrameMetadata() {
|
|
150
|
+
const frame = getActiveFrame();
|
|
151
|
+
if (!frame) {
|
|
152
|
+
return { name: null, url: null };
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
name: frame.name() || null,
|
|
156
|
+
url: frame.url() || null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function getSessionArtifactMetadata() {
|
|
161
|
+
return {
|
|
162
|
+
artifactRoot: ARTIFACT_ROOT,
|
|
163
|
+
sessionStartedAt: getSessionStartedAt(),
|
|
164
|
+
sessionArtifactDir: getSessionArtifactDir(),
|
|
165
|
+
activeTraceSession: getActiveTraceSession(),
|
|
166
|
+
harState: { ...getHarState() },
|
|
167
|
+
activePage: getActivePageMetadata(),
|
|
168
|
+
activeFrame: getActiveFrameMetadata(),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function sanitizeArtifactName(value: string, fallback: string): string {
|
|
173
|
+
const sanitized = value
|
|
174
|
+
.trim()
|
|
175
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
176
|
+
.replace(/^-+|-+$/g, "");
|
|
177
|
+
return sanitized || fallback;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Page helpers
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* getLivePagesSnapshot requires ensureBrowser (circular) — it will be
|
|
186
|
+
* wired in via ToolDeps. This is a factory that takes ensureBrowser.
|
|
187
|
+
*/
|
|
188
|
+
export function createGetLivePagesSnapshot(
|
|
189
|
+
ensureBrowser: () => Promise<{ page: Page }>,
|
|
190
|
+
) {
|
|
191
|
+
return async function getLivePagesSnapshot() {
|
|
192
|
+
await ensureBrowser();
|
|
193
|
+
for (const entry of pageRegistry.pages) {
|
|
194
|
+
try {
|
|
195
|
+
entry.title = await entry.page.title();
|
|
196
|
+
entry.url = entry.page.url();
|
|
197
|
+
} catch {
|
|
198
|
+
// Page may have been closed between snapshots.
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return registryListPages(pageRegistry);
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function resolveAccessibilityScope(
|
|
206
|
+
selector?: string,
|
|
207
|
+
): Promise<{ selector?: string; scope: string; source: string }> {
|
|
208
|
+
if (selector?.trim()) {
|
|
209
|
+
return {
|
|
210
|
+
selector: selector.trim(),
|
|
211
|
+
scope: `selector:${selector.trim()}`,
|
|
212
|
+
source: "explicit_selector",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const frame = getActiveFrame();
|
|
216
|
+
// We need getActiveTarget for dialog check, but that requires page access.
|
|
217
|
+
// For non-frame scoping, the caller must handle dialog detection separately
|
|
218
|
+
// if needed. Here we handle the frame case and fall through to full_page.
|
|
219
|
+
if (frame) {
|
|
220
|
+
return {
|
|
221
|
+
selector: "body",
|
|
222
|
+
scope: frame.name()
|
|
223
|
+
? `active frame:${frame.name()}`
|
|
224
|
+
: "active frame",
|
|
225
|
+
source: "active_frame",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return { selector: "body", scope: "full page", source: "full_page" };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* captureAccessibilityMarkdown — needs access to the active target.
|
|
233
|
+
* Accepts the target (Page | Frame) so it doesn't need to pull from state.
|
|
234
|
+
*/
|
|
235
|
+
export async function captureAccessibilityMarkdown(
|
|
236
|
+
target: Page | Frame,
|
|
237
|
+
selector?: string,
|
|
238
|
+
): Promise<{ snapshot: string; scope: string; source: string }> {
|
|
239
|
+
const scopeInfo = await resolveAccessibilityScope(selector);
|
|
240
|
+
const locator = target.locator(scopeInfo.selector ?? "body").first();
|
|
241
|
+
const snapshot = await locator.ariaSnapshot();
|
|
242
|
+
return { snapshot, scope: scopeInfo.scope, source: scopeInfo.source };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Critical request tracking
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
export function isCriticalResourceType(resourceType: string): boolean {
|
|
250
|
+
return resourceType === "document" || resourceType === "fetch" || resourceType === "xhr";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function updatePendingCriticalRequests(p: Page, delta: number): void {
|
|
254
|
+
const map = getPendingCriticalRequestsByPage();
|
|
255
|
+
const current = map.get(p) ?? 0;
|
|
256
|
+
map.set(p, Math.max(0, current + delta));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function getPendingCriticalRequests(p: Page): number {
|
|
260
|
+
return getPendingCriticalRequestsByPage().get(p) ?? 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Verification helpers
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
export function verificationFromChecks(
|
|
268
|
+
checks: VerificationCheck[],
|
|
269
|
+
retryHint?: string,
|
|
270
|
+
): VerificationResult {
|
|
271
|
+
const passedChecks = checks
|
|
272
|
+
.filter((check) => check.passed)
|
|
273
|
+
.map((check) => check.name);
|
|
274
|
+
const verified = passedChecks.length > 0;
|
|
275
|
+
return {
|
|
276
|
+
verified,
|
|
277
|
+
checks,
|
|
278
|
+
verificationSummary: verified
|
|
279
|
+
? `PASS (${passedChecks.join(", ")})`
|
|
280
|
+
: "SOFT-FAIL (no observable state change)",
|
|
281
|
+
retryHint: verified ? undefined : retryHint,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function verificationLine(verification: VerificationResult): string {
|
|
286
|
+
return `Verification: ${verification.verificationSummary}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// Assertion helpers
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
export async function collectAssertionState(
|
|
294
|
+
p: Page,
|
|
295
|
+
checks: BrowserAssertionCheckInput[],
|
|
296
|
+
captureCompactPageState: (
|
|
297
|
+
p: Page,
|
|
298
|
+
options?: { selectors?: string[]; includeBodyText?: boolean; target?: Page | Frame },
|
|
299
|
+
) => Promise<CompactPageState>,
|
|
300
|
+
target?: Page | Frame,
|
|
301
|
+
): Promise<{
|
|
302
|
+
url: string;
|
|
303
|
+
title: string;
|
|
304
|
+
bodyText: string;
|
|
305
|
+
focus: string;
|
|
306
|
+
selectorStates: Record<string, CompactSelectorState>;
|
|
307
|
+
consoleEntries: ConsoleEntry[];
|
|
308
|
+
networkEntries: NetworkEntry[];
|
|
309
|
+
allConsoleEntries: ConsoleEntry[];
|
|
310
|
+
allNetworkEntries: NetworkEntry[];
|
|
311
|
+
actionTimeline: typeof actionTimeline;
|
|
312
|
+
}> {
|
|
313
|
+
const selectors = checks
|
|
314
|
+
.map((check) => check.selector)
|
|
315
|
+
.filter((value): value is string => !!value);
|
|
316
|
+
const compactState = await captureCompactPageState(p, {
|
|
317
|
+
selectors,
|
|
318
|
+
includeBodyText: true,
|
|
319
|
+
target,
|
|
320
|
+
});
|
|
321
|
+
const sinceActionId = checks.reduce<number | undefined>((max, check) => {
|
|
322
|
+
if (check.sinceActionId === undefined) return max;
|
|
323
|
+
if (max === undefined) return check.sinceActionId;
|
|
324
|
+
return Math.max(max, check.sinceActionId);
|
|
325
|
+
}, undefined);
|
|
326
|
+
return {
|
|
327
|
+
url: compactState.url,
|
|
328
|
+
title: compactState.title,
|
|
329
|
+
bodyText: compactState.bodyText,
|
|
330
|
+
focus: compactState.focus,
|
|
331
|
+
selectorStates: compactState.selectorStates,
|
|
332
|
+
consoleEntries: getConsoleEntriesSince(sinceActionId),
|
|
333
|
+
networkEntries: getNetworkEntriesSince(sinceActionId),
|
|
334
|
+
allConsoleEntries: getConsoleLogs(),
|
|
335
|
+
allNetworkEntries: getNetworkLogs(),
|
|
336
|
+
actionTimeline,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function formatAssertionText(
|
|
341
|
+
result: ReturnType<typeof import("./core.js").evaluateAssertionChecks>,
|
|
342
|
+
): string {
|
|
343
|
+
const lines = [result.summary];
|
|
344
|
+
for (const check of result.checks.slice(0, 8)) {
|
|
345
|
+
lines.push(
|
|
346
|
+
`- ${check.passed ? "PASS" : "FAIL"} ${check.name}: expected ${JSON.stringify(check.expected)}, got ${JSON.stringify(check.actual)}`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
lines.push(`Hint: ${result.agentHint}`);
|
|
350
|
+
return lines.join("\n");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function formatDiffText(
|
|
354
|
+
diff: ReturnType<typeof import("./core.js").diffCompactStates>,
|
|
355
|
+
): string {
|
|
356
|
+
const lines = [diff.summary];
|
|
357
|
+
for (const change of diff.changes.slice(0, 8)) {
|
|
358
|
+
lines.push(
|
|
359
|
+
`- ${change.type}: ${JSON.stringify(change.before ?? null)} → ${JSON.stringify(change.after ?? null)}`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return lines.join("\n");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// URL / dialog helpers
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
export function getUrlHash(url: string): string {
|
|
370
|
+
try {
|
|
371
|
+
return new URL(url).hash || "";
|
|
372
|
+
} catch {
|
|
373
|
+
return "";
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function countOpenDialogs(target: Page | Frame): Promise<number> {
|
|
378
|
+
try {
|
|
379
|
+
return await target.evaluate(() =>
|
|
380
|
+
document.querySelectorAll('[role="dialog"]:not([hidden]),dialog[open]')
|
|
381
|
+
.length,
|
|
382
|
+
);
|
|
383
|
+
} catch {
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Click / input helpers
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
export async function captureClickTargetState(
|
|
393
|
+
target: Page | Frame,
|
|
394
|
+
selector: string,
|
|
395
|
+
): Promise<ClickTargetStateSnapshot> {
|
|
396
|
+
try {
|
|
397
|
+
return await target.evaluate((sel) => {
|
|
398
|
+
const el = document.querySelector(sel) as HTMLElement | null;
|
|
399
|
+
if (!el) {
|
|
400
|
+
return {
|
|
401
|
+
exists: false,
|
|
402
|
+
ariaExpanded: null,
|
|
403
|
+
ariaPressed: null,
|
|
404
|
+
ariaSelected: null,
|
|
405
|
+
open: null,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
exists: true,
|
|
410
|
+
ariaExpanded: el.getAttribute("aria-expanded"),
|
|
411
|
+
ariaPressed: el.getAttribute("aria-pressed"),
|
|
412
|
+
ariaSelected: el.getAttribute("aria-selected"),
|
|
413
|
+
open:
|
|
414
|
+
el instanceof HTMLDialogElement
|
|
415
|
+
? el.open
|
|
416
|
+
: el.getAttribute("open") !== null,
|
|
417
|
+
};
|
|
418
|
+
}, selector);
|
|
419
|
+
} catch {
|
|
420
|
+
return {
|
|
421
|
+
exists: false,
|
|
422
|
+
ariaExpanded: null,
|
|
423
|
+
ariaPressed: null,
|
|
424
|
+
ariaSelected: null,
|
|
425
|
+
open: null,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export async function readInputLikeValue(
|
|
431
|
+
target: Page | Frame,
|
|
432
|
+
selector?: string,
|
|
433
|
+
): Promise<string | null> {
|
|
434
|
+
try {
|
|
435
|
+
return await target.evaluate((sel) => {
|
|
436
|
+
const resolveTarget = (): Element | null => {
|
|
437
|
+
if (sel) return document.querySelector(sel);
|
|
438
|
+
const active = document.activeElement;
|
|
439
|
+
if (
|
|
440
|
+
!active ||
|
|
441
|
+
active === document.body ||
|
|
442
|
+
active === document.documentElement
|
|
443
|
+
)
|
|
444
|
+
return null;
|
|
445
|
+
return active;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const target = resolveTarget();
|
|
449
|
+
if (!target) return null;
|
|
450
|
+
if (
|
|
451
|
+
target instanceof HTMLInputElement ||
|
|
452
|
+
target instanceof HTMLTextAreaElement
|
|
453
|
+
) {
|
|
454
|
+
return target.value;
|
|
455
|
+
}
|
|
456
|
+
if (target instanceof HTMLSelectElement) {
|
|
457
|
+
return target.value;
|
|
458
|
+
}
|
|
459
|
+
if ((target as HTMLElement).isContentEditable) {
|
|
460
|
+
return (target.textContent ?? "").trim();
|
|
461
|
+
}
|
|
462
|
+
return (target as HTMLElement).getAttribute("value");
|
|
463
|
+
}, selector);
|
|
464
|
+
} catch {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function firstErrorLine(err: unknown): string {
|
|
470
|
+
const message =
|
|
471
|
+
typeof err === "object" && err && "message" in err
|
|
472
|
+
? String((err as { message?: unknown }).message ?? "")
|
|
473
|
+
: String(err ?? "unknown error");
|
|
474
|
+
return message.split("\n")[0] || "unknown error";
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
// Action tracking
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
|
|
481
|
+
export function beginTrackedAction(
|
|
482
|
+
tool: string,
|
|
483
|
+
params: unknown,
|
|
484
|
+
beforeUrl: string,
|
|
485
|
+
) {
|
|
486
|
+
return beginAction(actionTimeline, {
|
|
487
|
+
tool,
|
|
488
|
+
paramsSummary: toActionParamsSummary(params),
|
|
489
|
+
beforeUrl,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function finishTrackedAction(
|
|
494
|
+
actionId: number,
|
|
495
|
+
updates: {
|
|
496
|
+
status: "success" | "error";
|
|
497
|
+
afterUrl?: string;
|
|
498
|
+
verificationSummary?: string;
|
|
499
|
+
warningSummary?: string;
|
|
500
|
+
diffSummary?: string;
|
|
501
|
+
changed?: boolean;
|
|
502
|
+
error?: string;
|
|
503
|
+
beforeState?: CompactPageState;
|
|
504
|
+
afterState?: CompactPageState;
|
|
505
|
+
},
|
|
506
|
+
) {
|
|
507
|
+
return finishAction(actionTimeline, actionId, updates);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function getSinceTimestamp(sinceActionId?: number): number {
|
|
511
|
+
if (!sinceActionId) return 0;
|
|
512
|
+
const action = findAction(actionTimeline, sinceActionId);
|
|
513
|
+
if (!action) return 0;
|
|
514
|
+
return action.startedAt ?? 0;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function getConsoleEntriesSince(sinceActionId?: number): ConsoleEntry[] {
|
|
518
|
+
const since = getSinceTimestamp(sinceActionId);
|
|
519
|
+
return getConsoleLogs().filter((entry) => entry.timestamp >= since);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function getNetworkEntriesSince(sinceActionId?: number): NetworkEntry[] {
|
|
523
|
+
const since = getSinceTimestamp(sinceActionId);
|
|
524
|
+
return getNetworkLogs().filter((entry) => entry.timestamp >= since);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
528
|
+
// Error summary
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
|
|
531
|
+
export function getRecentErrors(pageUrl: string): string {
|
|
532
|
+
const parts: string[] = [];
|
|
533
|
+
const now = Date.now();
|
|
534
|
+
const since = now - 12_000;
|
|
535
|
+
|
|
536
|
+
const toOrigin = (url: string): string | null => {
|
|
537
|
+
try {
|
|
538
|
+
return new URL(url).origin;
|
|
539
|
+
} catch {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
const pageOrigin = toOrigin(pageUrl);
|
|
544
|
+
const sameOrigin = (url: string): boolean =>
|
|
545
|
+
!pageOrigin || toOrigin(url) === pageOrigin;
|
|
546
|
+
|
|
547
|
+
const summarize = (items: string[], max: number): string[] => {
|
|
548
|
+
const counts = new Map<string, number>();
|
|
549
|
+
const order: string[] = [];
|
|
550
|
+
for (const item of items) {
|
|
551
|
+
if (!counts.has(item)) order.push(item);
|
|
552
|
+
counts.set(item, (counts.get(item) ?? 0) + 1);
|
|
553
|
+
}
|
|
554
|
+
return order.slice(0, max).map((item) => {
|
|
555
|
+
const count = counts.get(item) ?? 1;
|
|
556
|
+
return count > 1 ? `${item} (x${count})` : item;
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const consoleLogs = getConsoleLogs();
|
|
561
|
+
const jsWarnings = consoleLogs
|
|
562
|
+
.filter(
|
|
563
|
+
(e) =>
|
|
564
|
+
(e.type === "error" || e.type === "pageerror") &&
|
|
565
|
+
e.timestamp >= since &&
|
|
566
|
+
sameOrigin(e.url),
|
|
567
|
+
)
|
|
568
|
+
.map((e) => e.text.slice(0, 120));
|
|
569
|
+
if (jsWarnings.length > 0) {
|
|
570
|
+
parts.push("JS: " + summarize(jsWarnings, 2).join(" | "));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const actionableStatus = new Set([401, 403, 404, 408, 409, 422, 429]);
|
|
574
|
+
const actionableTypes = new Set(["document", "fetch", "xhr", "script"]);
|
|
575
|
+
const networkLogs = getNetworkLogs();
|
|
576
|
+
const netWarnings = networkLogs
|
|
577
|
+
.filter((e) => e.timestamp >= since && sameOrigin(e.url))
|
|
578
|
+
.filter((e) => {
|
|
579
|
+
if (e.failed) return actionableTypes.has(e.resourceType);
|
|
580
|
+
if (e.status === null) return false;
|
|
581
|
+
if (e.status >= 500) return true;
|
|
582
|
+
return (
|
|
583
|
+
actionableStatus.has(e.status) &&
|
|
584
|
+
actionableTypes.has(e.resourceType)
|
|
585
|
+
);
|
|
586
|
+
})
|
|
587
|
+
.map((e) => {
|
|
588
|
+
if (e.failed) return `${e.method} ${e.resourceType} FAILED`;
|
|
589
|
+
return `${e.method} ${e.resourceType} ${e.status}`;
|
|
590
|
+
});
|
|
591
|
+
if (netWarnings.length > 0) {
|
|
592
|
+
parts.push("Network: " + summarize(netWarnings, 2).join(" | "));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const dialogLogs = getDialogLogs();
|
|
596
|
+
const dialogWarnings = dialogLogs
|
|
597
|
+
.filter((e) => e.timestamp >= since && sameOrigin(e.url))
|
|
598
|
+
.map((e) => `${e.type}: ${e.message.slice(0, 80)}`);
|
|
599
|
+
if (dialogWarnings.length > 0) {
|
|
600
|
+
parts.push("Dialogs: " + summarize(dialogWarnings, 1).join(" | "));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (parts.length === 0) return "";
|
|
604
|
+
return `\n\nWarnings: ${parts.join("; ")}\nUse browser_get_console_logs/browser_get_network_logs for full diagnostics.`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ---------------------------------------------------------------------------
|
|
608
|
+
// Ref helpers (parsing / formatting — no browser evaluate)
|
|
609
|
+
// ---------------------------------------------------------------------------
|
|
610
|
+
|
|
611
|
+
export function parseRef(input: string): ParsedRefSpec {
|
|
612
|
+
const trimmed = input.trim().toLowerCase();
|
|
613
|
+
const token = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
614
|
+
const versioned = token.match(/^v(\d+):(e\d+)$/);
|
|
615
|
+
if (versioned) {
|
|
616
|
+
const version = parseInt(versioned[1], 10);
|
|
617
|
+
const key = versioned[2];
|
|
618
|
+
return { key, version, display: `@v${version}:${key}` };
|
|
619
|
+
}
|
|
620
|
+
return { key: token, version: null, display: `@${token}` };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function formatVersionedRef(version: number, key: string): string {
|
|
624
|
+
return `@v${version}:${key}`;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function staleRefGuidance(refDisplay: string, reason: string): string {
|
|
628
|
+
return `Ref ${refDisplay} could not be resolved (${reason}). The ref is likely stale after DOM/navigation changes. Call browser_snapshot_refs again to refresh refs.`;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// ---------------------------------------------------------------------------
|
|
632
|
+
// Compact state summary formatting
|
|
633
|
+
// ---------------------------------------------------------------------------
|
|
634
|
+
|
|
635
|
+
export function formatCompactStateSummary(state: CompactPageState): string {
|
|
636
|
+
const lines: string[] = [];
|
|
637
|
+
lines.push(`Title: ${state.title}`);
|
|
638
|
+
lines.push(`URL: ${state.url}`);
|
|
639
|
+
lines.push(
|
|
640
|
+
`Elements: ${state.counts.landmarks} landmarks, ${state.counts.buttons} buttons, ${state.counts.links} links, ${state.counts.inputs} inputs`,
|
|
641
|
+
);
|
|
642
|
+
if (state.headings.length > 0) {
|
|
643
|
+
lines.push(
|
|
644
|
+
"Headings: " +
|
|
645
|
+
state.headings
|
|
646
|
+
.map((text, index) => `H${index + 1} \"${text}\"`)
|
|
647
|
+
.join(", "),
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (state.focus) {
|
|
651
|
+
lines.push(`Focused: ${state.focus}`);
|
|
652
|
+
}
|
|
653
|
+
if (state.dialog.title) {
|
|
654
|
+
lines.push(`Active dialog: "${state.dialog.title}"`);
|
|
655
|
+
}
|
|
656
|
+
lines.push(
|
|
657
|
+
"Use browser_find for targeted discovery, browser_assert for verification, or browser_get_accessibility_tree for full detail.",
|
|
658
|
+
);
|
|
659
|
+
return lines.join("\n");
|
|
660
|
+
}
|
|
@@ -487,6 +487,9 @@ export class GitServiceImpl {
|
|
|
487
487
|
commitType, milestoneId, sliceId, sliceTitle, mainBranch, branch,
|
|
488
488
|
);
|
|
489
489
|
|
|
490
|
+
// Pull latest main before merging to avoid conflicts from remote changes
|
|
491
|
+
this.git(["pull", "--rebase", "origin", mainBranch], { allowFailure: true });
|
|
492
|
+
|
|
490
493
|
// Squash merge — abort cleanly on conflict so the working tree is never
|
|
491
494
|
// left in a half-merged state (see: merge-bug-fix).
|
|
492
495
|
try {
|
|
@@ -235,7 +235,7 @@ export async function showInterviewRound(
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
function saveEditorToState() {
|
|
238
|
-
states[currentIdx].notes = getEditor().
|
|
238
|
+
states[currentIdx].notes = getEditor().getExpandedText().trim();
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
function loadStateToEditor() {
|