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,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().getText().trim();
238
+ states[currentIdx].notes = getEditor().getExpandedText().trim();
239
239
  }
240
240
 
241
241
  function loadStateToEditor() {