diff --git a/.github/workflows/make-pdf-gate.yml b/.github/workflows/make-pdf-gate.yml index 69c4d7eb14..2abf3d10f0 100644 --- a/.github/workflows/make-pdf-gate.yml +++ b/.github/workflows/make-pdf-gate.yml @@ -74,7 +74,7 @@ jobs: - name: ad-hoc codesign (Apple Silicon) if: matrix.os == 'macos-latest' run: | - for bin in browse/dist/browse browse/dist/find-browse design/dist/design make-pdf/dist/pdf; do + for bin in browse/dist/browse design/dist/design make-pdf/dist/pdf; do codesign --remove-signature "$bin" 2>/dev/null || true codesign -s - -f "$bin" || true done diff --git a/.github/workflows/windows-setup-e2e.yml b/.github/workflows/windows-setup-e2e.yml index 310566ce37..edf8f28631 100644 --- a/.github/workflows/windows-setup-e2e.yml +++ b/.github/workflows/windows-setup-e2e.yml @@ -23,7 +23,6 @@ on: - 'setup' - 'runtime/**' - 'browse/src/cli.ts' - - 'browse/src/find-browse.ts' - 'bin/gstack-paths' - '.github/workflows/windows-setup-e2e.yml' workflow_dispatch: diff --git a/BROWSER.md b/BROWSER.md index affa0447d1..072fa8581c 100644 --- a/BROWSER.md +++ b/BROWSER.md @@ -1258,7 +1258,6 @@ browse/ │ ├── error-handling.ts # safeUnlink / safeKill / isProcessAlive │ ├── platform.ts # OS detection (macOS, Linux, Windows) │ ├── telemetry.ts # Anonymous opt-in usage telemetry -│ ├── find-browse.ts # Locate running daemon or bootstrap │ └── config.ts # Config resolution (env / files) ├── test/ # Integration tests + HTML fixtures └── dist/ diff --git a/browse/bin/find-browse b/browse/bin/find-browse deleted file mode 100755 index 8f441b499c..0000000000 --- a/browse/bin/find-browse +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# Shim: delegates to compiled find-browse binary, falls back to basic discovery. -# The compiled binary handles git root detection for workspace-local installs. -DIR="$(cd "$(dirname "$0")/.." && pwd)/dist" -if test -x "$DIR/find-browse"; then - exec "$DIR/find-browse" "$@" -fi -# Fallback: basic discovery with priority chain -ROOT=$(git rev-parse --show-toplevel 2>/dev/null) -for MARKER in .codex .agents .claude; do - if [ -n "$ROOT" ] && test -x "$ROOT/$MARKER/skills/gstack/browse/dist/browse"; then - echo "$ROOT/$MARKER/skills/gstack/browse/dist/browse" - exit 0 - fi - if test -x "$HOME/$MARKER/skills/gstack/browse/dist/browse"; then - echo "$HOME/$MARKER/skills/gstack/browse/dist/browse" - exit 0 - fi -done -echo "ERROR: browse binary not found. Run: cd && ./setup" >&2 -exit 1 diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index 5cdfbb8ece..4b51a49cc4 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -230,7 +230,10 @@ export class BrowserManager { // ─── Watch Mode ───────────────────────────────────────── private watching = false; public watchInterval: ReturnType | null = null; - private watchSnapshots: string[] = []; + // watch stop only ever shows the most recent snapshot, so we keep just that + // plus a count instead of retaining every 5s snapshot (~126 MB/hr of dead strings). + private lastWatchSnapshot: string | null = null; + private watchSnapshotCount = 0; private watchStartTime: number = 0; // ─── Headed State ──────────────────────────────────────── @@ -299,25 +302,29 @@ export class BrowserManager { startWatch(): void { this.watching = true; - this.watchSnapshots = []; + this.lastWatchSnapshot = null; + this.watchSnapshotCount = 0; this.watchStartTime = Date.now(); } - stopWatch(): { snapshots: string[]; duration: number } { + stopWatch(): { count: number; last: string | null; duration: number } { this.watching = false; if (this.watchInterval) { clearInterval(this.watchInterval); this.watchInterval = null; } - const snapshots = this.watchSnapshots; + const count = this.watchSnapshotCount; + const last = this.lastWatchSnapshot; const duration = Date.now() - this.watchStartTime; - this.watchSnapshots = []; + this.lastWatchSnapshot = null; + this.watchSnapshotCount = 0; this.watchStartTime = 0; - return { snapshots, duration }; + return { count, last, duration }; } addWatchSnapshot(snapshot: string): void { - this.watchSnapshots.push(snapshot); + this.lastWatchSnapshot = snapshot; + this.watchSnapshotCount++; } /** diff --git a/browse/src/commands.ts b/browse/src/commands.ts index 73bc9ab1bf..0e8383e6f8 100644 --- a/browse/src/commands.ts +++ b/browse/src/commands.ts @@ -10,6 +10,13 @@ * Zero side effects. Safe to import from build scripts and tests. */ +// Implicit post-action settle window (click/fill/select and end-of-chain). +// networkidle resolves instantly on quiet pages, so this only caps the wait on +// pages holding a connection open (polling, analytics, websockets) — where the +// old 2000ms made every interaction feel slow. Explicit `wait --networkidle` +// stays available for a full wait. +export const NETWORK_SETTLE_MS = 500; + export const READ_COMMANDS = new Set([ 'text', 'html', 'links', 'forms', 'accessibility', 'js', 'eval', 'css', 'attrs', diff --git a/browse/src/find-browse.ts b/browse/src/find-browse.ts deleted file mode 100644 index ab9f6a54d2..0000000000 --- a/browse/src/find-browse.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * find-browse — locate the gstack browse binary. - * - * Compiled to browse/dist/find-browse (standalone binary, no bun runtime needed). - * Outputs the absolute path to the browse binary on stdout, or exits 1 if not found. - */ - -import { accessSync, constants } from 'fs'; -import { join } from 'path'; -import { homedir } from 'os'; - -// ─── Binary Discovery ─────────────────────────────────────────── - -function getGitRoot(): string | null { - try { - const proc = Bun.spawnSync(['git', 'rev-parse', '--show-toplevel'], { - stdout: 'pipe', - stderr: 'pipe', - }); - if (proc.exitCode !== 0) return null; - return proc.stdout.toString().trim(); - } catch { - return null; - } -} - -// Probe a path for executability. accessSync(X_OK) checks the executable -// bit on Linux/macOS and degrades to an existence check on Windows (no -// true execute bit). Mirrors make-pdf/src/browseClient.ts:159 / -// make-pdf/src/pdftotext.ts:117. -function isExecutable(p: string): boolean { - try { - accessSync(p, constants.X_OK); - return true; - } catch { - return false; - } -} - -// Resolve a bare binary path to the actual file on disk. On Windows, `bun -// build --compile` appends `.exe` to the output filename, so `browse` on -// disk is actually `browse.exe`. After a bare-path probe, try the Windows -// extensions. Linux/macOS behavior is unchanged. Mirrors the helper in -// make-pdf/src/browseClient.ts:89 and make-pdf/src/pdftotext.ts:52. -function findExecutable(base: string): string | null { - if (isExecutable(base)) return base; - if (process.platform === 'win32') { - for (const ext of ['.exe', '.cmd', '.bat']) { - const withExt = base + ext; - if (isExecutable(withExt)) return withExt; - } - } - return null; -} - -export function locateBinary(): string | null { - const root = getGitRoot(); - const home = homedir(); - const markers = ['.codex', '.agents', '.claude']; - - // Workspace-local takes priority (for development) - if (root) { - for (const m of markers) { - const local = join(root, m, 'skills', 'gstack', 'browse', 'dist', 'browse'); - const found = findExecutable(local); - if (found) return found; - } - - // Source-checkout fallback (no installed skill layout — the binary - // lives directly at /browse/dist/browse[.exe]). Hit by: - // - gstack repo dev workflow before `./setup` runs - // - the windows-setup-e2e.yml CI workflow which builds binaries - // in place but never installs them under a marker dir - // - make-pdf consumers running from a sibling source checkout - const sourceCheckout = join(root, 'browse', 'dist', 'browse'); - const sourceFound = findExecutable(sourceCheckout); - if (sourceFound) return sourceFound; - } - - // Global fallback - for (const m of markers) { - const global = join(home, m, 'skills', 'gstack', 'browse', 'dist', 'browse'); - const found = findExecutable(global); - if (found) return found; - } - - return null; -} - -// ─── Main ─────────────────────────────────────────────────────── - -function main() { - const bin = locateBinary(); - if (!bin) { - process.stderr.write('ERROR: browse binary not found. Run: cd && ./setup\n'); - process.exit(1); - } - - console.log(bin); -} - -// Only run main() when this module is the entry point. Without this guard, -// any test that imports `locateBinary` from this file would have main() fire -// at module-load time, calling process.exit(1) when no compiled binary -// exists — killing the test process before any test runs. Surfaced on the -// windows-free-tests CI lane where the runner has no compiled browse -// binary (intentional — that lane only builds server-node.mjs). -if (import.meta.main) { - main(); -} diff --git a/browse/src/meta-commands.ts b/browse/src/meta-commands.ts index 4cd2964922..8bc3b4c1a5 100644 --- a/browse/src/meta-commands.ts +++ b/browse/src/meta-commands.ts @@ -5,7 +5,7 @@ import type { BrowserManager } from './browser-manager'; import { handleSnapshot } from './snapshot'; import { getCleanText } from './read-commands'; -import { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS, PAGE_CONTENT_COMMANDS, wrapUntrustedContent, canonicalizeCommand } from './commands'; +import { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS, PAGE_CONTENT_COMMANDS, wrapUntrustedContent, canonicalizeCommand, NETWORK_SETTLE_MS } from './commands'; import { handleDomainSkillCommand } from './domain-skill-commands'; import { handleSkillCommand } from './browser-skill-commands'; import { validateNavigationUrl } from './url-validation'; @@ -706,9 +706,9 @@ export async function handleMetaCommand( } } - // Wait for network to settle after write commands before returning + // Settle window after the last write in a chain (see NETWORK_SETTLE_MS). if (lastWasWrite) { - await bm.getPage().waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {}); + await bm.getPage().waitForLoadState('networkidle', { timeout: NETWORK_SETTLE_MS }).catch(() => {}); } return results.join('\n\n'); @@ -842,11 +842,11 @@ export async function handleMetaCommand( if (!bm.isWatching()) return 'Not currently watching.'; const result = bm.stopWatch(); const durationSec = Math.round(result.duration / 1000); - const lastSnapshot = result.snapshots.length > 0 - ? wrapUntrustedContent(result.snapshots[result.snapshots.length - 1], bm.getCurrentUrl()) + const lastSnapshot = result.last + ? wrapUntrustedContent(result.last, bm.getCurrentUrl()) : '(none)'; return [ - `WATCH STOPPED (${durationSec}s, ${result.snapshots.length} snapshots)`, + `WATCH STOPPED (${durationSec}s, ${result.count} snapshots)`, '', 'Last snapshot:', lastSnapshot, diff --git a/browse/src/server.ts b/browse/src/server.ts index ca7ef19eb6..7b49156a16 100644 --- a/browse/src/server.ts +++ b/browse/src/server.ts @@ -1154,16 +1154,23 @@ async function handleCommandInternalImpl( }); // Start periodic snapshot interval when watch mode begins if (command === 'watch' && args[0] !== 'stop' && browserManager.isWatching()) { + // A full interactive snapshot can take >1s on large pages; guard against + // overlapping runs piling up when a snapshot outlasts the 5s interval. + let snapshotInFlight = false; const watchInterval = setInterval(async () => { if (!browserManager.isWatching()) { clearInterval(watchInterval); return; } + if (snapshotInFlight) return; + snapshotInFlight = true; try { const snapshot = await handleSnapshot(['-i'], browserManager.getActiveSession()); browserManager.addWatchSnapshot(snapshot); } catch { // Page may be navigating — skip this snapshot + } finally { + snapshotInFlight = false; } }, 5000); browserManager.watchInterval = watchInterval; diff --git a/browse/src/write-commands.ts b/browse/src/write-commands.ts index 626ba8794b..f2cb341550 100644 --- a/browse/src/write-commands.ts +++ b/browse/src/write-commands.ts @@ -19,6 +19,7 @@ import { TEMP_DIR, isPathWithin } from './platform'; import { SAFE_DIRECTORIES } from './path-security'; import { modifyStyle, undoModification, resetModifications, getModificationHistory } from './cdp-inspector'; import { withCdpSession } from './cdp-bridge'; +import { NETWORK_SETTLE_MS } from './commands'; /** * Aggressive page cleanup selectors and heuristics. @@ -374,8 +375,11 @@ export async function handleWriteCommand( } throw err; } - // Wait for network to settle (catches XHR/fetch triggered by clicks) - await page.waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {}); + // Settle window for XHR/fetch triggered by the click. Idle pages resolve + // instantly; only busy/polling pages ever wait. Capped low so analytics- + // heavy apps that never reach networkidle don't tax every action 2s. + // Explicit `wait --networkidle` stays available when a full wait is needed. + await page.waitForLoadState('networkidle', { timeout: NETWORK_SETTLE_MS }).catch(() => {}); return `Clicked ${selector} → now at ${page.url()}`; } @@ -389,8 +393,8 @@ export async function handleWriteCommand( } else { await target.locator(resolved.selector).fill(value, { timeout: 5000 }); } - // Wait for network to settle (form validation XHRs) - await page.waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {}); + // Settle window for form-validation XHRs (see NETWORK_SETTLE_MS note above). + await page.waitForLoadState('networkidle', { timeout: NETWORK_SETTLE_MS }).catch(() => {}); return `Filled ${selector}`; } @@ -404,8 +408,8 @@ export async function handleWriteCommand( } else { await target.locator(resolved.selector).selectOption(value, { timeout: 5000 }); } - // Wait for network to settle (dropdown-triggered requests) - await page.waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {}); + // Settle window for dropdown-triggered requests (see NETWORK_SETTLE_MS note above). + await page.waitForLoadState('networkidle', { timeout: NETWORK_SETTLE_MS }).catch(() => {}); return `Selected "${value}" in ${selector}`; } diff --git a/browse/test/find-browse.test.ts b/browse/test/find-browse.test.ts deleted file mode 100644 index 333e09acd8..0000000000 --- a/browse/test/find-browse.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Tests for find-browse binary locator. - */ - -import { describe, test, expect } from 'bun:test'; -import { locateBinary } from '../src/find-browse'; -import { existsSync } from 'fs'; - -describe('locateBinary', () => { - test('returns null when no binary exists at known paths', () => { - // This test depends on the test environment — if a real binary exists at - // ~/.claude/skills/gstack/browse/dist/browse, it will find it. - // We mainly test that the function doesn't throw. - const result = locateBinary(); - expect(result === null || typeof result === 'string').toBe(true); - }); - - test('returns string path when binary exists', () => { - const result = locateBinary(); - if (result !== null) { - expect(existsSync(result)).toBe(true); - } - }); - - test('priority chain checks .codex, .agents, .claude markers', () => { - // Verify the source code implements the correct priority order. - // We read the function source to confirm the markers array order. - const src = require('fs').readFileSync(require('path').join(__dirname, '../src/find-browse.ts'), 'utf-8'); - // The markers array should list .codex first, then .agents, then .claude - const markersMatch = src.match(/const markers = \[([^\]]+)\]/); - expect(markersMatch).not.toBeNull(); - const markers = markersMatch![1]; - const codexIdx = markers.indexOf('.codex'); - const agentsIdx = markers.indexOf('.agents'); - const claudeIdx = markers.indexOf('.claude'); - // All three must be present - expect(codexIdx).toBeGreaterThanOrEqual(0); - expect(agentsIdx).toBeGreaterThanOrEqual(0); - expect(claudeIdx).toBeGreaterThanOrEqual(0); - // .codex before .agents before .claude - expect(codexIdx).toBeLessThan(agentsIdx); - expect(agentsIdx).toBeLessThan(claudeIdx); - }); - - test('function signature accepts no arguments', () => { - // locateBinary should be callable with no arguments - expect(typeof locateBinary).toBe('function'); - expect(locateBinary.length).toBe(0); - }); - - test('source-checkout fallback resolves /browse/dist/browse[.exe]', () => { - // The windows-setup-e2e.yml workflow builds binaries directly under - // browse/dist/ (no .claude/skills/gstack/ install layout). find-browse - // must resolve those — otherwise every fresh build that hasn't run - // ./setup yet looks broken. Static pin so a future refactor that - // drops the source-checkout branch trips this test. - const src = require('fs').readFileSync(require('path').join(__dirname, '../src/find-browse.ts'), 'utf-8'); - expect(src).toContain('Source-checkout fallback'); - expect(src).toContain("join(root, 'browse', 'dist', 'browse')"); - }); -}); diff --git a/browse/test/watch.test.ts b/browse/test/watch.test.ts index 7e03ced704..38144e5bb0 100644 --- a/browse/test/watch.test.ts +++ b/browse/test/watch.test.ts @@ -4,6 +4,9 @@ * Pure unit tests — no browser needed. Just instantiate BrowserManager * and test the watch state methods (startWatch, stopWatch, addWatchSnapshot, * isWatching). + * + * watch stop only ever displayed the latest snapshot, so the manager keeps + * just the last snapshot + a count rather than retaining every 5s capture. */ import { describe, test, expect } from 'bun:test'; @@ -21,7 +24,7 @@ describe('watch mode — state machine', () => { expect(bm.isWatching()).toBe(true); }); - test('stopWatch clears isWatching and returns snapshots', () => { + test('stopWatch clears isWatching and returns count + last snapshot', () => { const bm = new BrowserManager(); bm.startWatch(); bm.addWatchSnapshot('snapshot-1'); @@ -29,8 +32,8 @@ describe('watch mode — state machine', () => { const result = bm.stopWatch(); expect(bm.isWatching()).toBe(false); - expect(result.snapshots).toEqual(['snapshot-1', 'snapshot-2']); - expect(result.snapshots.length).toBe(2); + expect(result.count).toBe(2); + expect(result.last).toBe('snapshot-2'); }); test('stopWatch returns correct duration (approximately)', async () => { @@ -47,7 +50,7 @@ describe('watch mode — state machine', () => { expect(result.duration).toBeLessThan(5000); }); - test('addWatchSnapshot stores snapshots', () => { + test('addWatchSnapshot only retains the latest snapshot + count', () => { const bm = new BrowserManager(); bm.startWatch(); @@ -56,25 +59,25 @@ describe('watch mode — state machine', () => { bm.addWatchSnapshot('page C content'); const result = bm.stopWatch(); - expect(result.snapshots.length).toBe(3); - expect(result.snapshots[0]).toBe('page A content'); - expect(result.snapshots[1]).toBe('page B content'); - expect(result.snapshots[2]).toBe('page C content'); + expect(result.count).toBe(3); + expect(result.last).toBe('page C content'); }); - test('stopWatch resets snapshots for next cycle', () => { + test('stopWatch resets state for next cycle', () => { const bm = new BrowserManager(); // First cycle bm.startWatch(); bm.addWatchSnapshot('first-cycle-snapshot'); const result1 = bm.stopWatch(); - expect(result1.snapshots.length).toBe(1); + expect(result1.count).toBe(1); + expect(result1.last).toBe('first-cycle-snapshot'); // Second cycle — should start fresh bm.startWatch(); const result2 = bm.stopWatch(); - expect(result2.snapshots.length).toBe(0); + expect(result2.count).toBe(0); + expect(result2.last).toBeNull(); }); test('multiple start/stop cycles work correctly', () => { @@ -86,7 +89,8 @@ describe('watch mode — state machine', () => { bm.addWatchSnapshot('snap-1'); const r1 = bm.stopWatch(); expect(bm.isWatching()).toBe(false); - expect(r1.snapshots).toEqual(['snap-1']); + expect(r1.count).toBe(1); + expect(r1.last).toBe('snap-1'); // Cycle 2 bm.startWatch(); @@ -95,14 +99,16 @@ describe('watch mode — state machine', () => { bm.addWatchSnapshot('snap-2b'); const r2 = bm.stopWatch(); expect(bm.isWatching()).toBe(false); - expect(r2.snapshots).toEqual(['snap-2a', 'snap-2b']); + expect(r2.count).toBe(2); + expect(r2.last).toBe('snap-2b'); // Cycle 3 — no snapshots added bm.startWatch(); expect(bm.isWatching()).toBe(true); const r3 = bm.stopWatch(); expect(bm.isWatching()).toBe(false); - expect(r3.snapshots).toEqual([]); + expect(r3.count).toBe(0); + expect(r3.last).toBeNull(); }); test('stopWatch clears watchInterval if set', () => { @@ -122,7 +128,8 @@ describe('watch mode — state machine', () => { // Calling stopWatch without startWatch should not throw const result = bm.stopWatch(); - expect(result.snapshots).toEqual([]); + expect(result.count).toBe(0); + expect(result.last).toBeNull(); expect(result.duration).toBeLessThanOrEqual(Date.now()); // duration = now - 0 expect(bm.isWatching()).toBe(false); }); diff --git a/runtime/install.js b/runtime/install.js index 9046a2a6f0..00b067a237 100644 --- a/runtime/install.js +++ b/runtime/install.js @@ -264,7 +264,6 @@ export const DEFAULT_RUNTIME_BUNDLE = Object.freeze([ ...RUNTIME_HELPER_INTERNALS.map((target) => entry(target, undefined, true)), ...RUNTIME_HELPER_DEPENDENCIES.map((target) => entry(target)), entry(platformBinary("browse/dist/browse"), "core", true), - entry(platformBinary("browse/dist/find-browse"), "core", true), entry("browse/dist/server-node.mjs", "core"), entry("browse/dist/bun-polyfill.cjs", "core"), entry("browse/dist/.version", "core"), diff --git a/scripts/build.sh b/scripts/build.sh index 0678a052c0..a5bbb78293 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -34,12 +34,11 @@ esac "$BUN_CMD" run vendor:xterm "$BUN_CMD" build --compile browse/src/cli.ts --outfile browse/dist/browse -"$BUN_CMD" build --compile browse/src/find-browse.ts --outfile browse/dist/find-browse "$BUN_CMD" build --compile design/src/cli.ts --outfile design/dist/design "$BUN_CMD" build --compile make-pdf/src/cli.ts --outfile make-pdf/dist/pdf bash browse/scripts/build-node-server.sh bash scripts/write-version-files.sh browse/dist/.version design/dist/.version make-pdf/dist/.version -chmod +x browse/dist/browse browse/dist/find-browse design/dist/design make-pdf/dist/pdf +chmod +x browse/dist/browse design/dist/design make-pdf/dist/pdf if [ "$RUNTIME_ONLY" -eq 0 ]; then "$BUN_CMD" run gen:gstack2 "$BUN_CMD" run gen:skill-docs --host all diff --git a/scripts/gstack2/browser-provider-contract.ts b/scripts/gstack2/browser-provider-contract.ts index dca3e93959..a28f32bf59 100644 --- a/scripts/gstack2/browser-provider-contract.ts +++ b/scripts/gstack2/browser-provider-contract.ts @@ -4,11 +4,12 @@ export type BrowserProviderKind = | 'native-agent' | 'native-plugin' | 'native-mcp' + | 'dedicated-ai-browser' | 'extension-only' | 'no-native-automation'; export interface BrowserProviderContract { - id: 'claude' | 'codex' | 'gemini' | 'cursor' | 'github-copilot' | 'openclaw' | 'kimi' | 'pi'; + id: 'claude' | 'codex' | 'gemini' | 'cursor' | 'github-copilot' | 'openclaw' | 'kimi' | 'pi' | 'aside'; label: string; kind: BrowserProviderKind; setup: readonly string[]; @@ -145,6 +146,22 @@ export const BROWSER_PROVIDER_CONTRACTS: readonly BrowserProviderContract[] = [ ], unavailable: 'Offer GStack local browser, evidence-limited continuation, or deferral; never recommend or install a third-party Pi browser package automatically.', }, + { + id: 'aside', + label: 'Aside AI browser', + kind: 'dedicated-ai-browser', + setup: [ + 'Explain that Aside is a standalone AI browser driven from the terminal by the `aside` CLI, running the user\'s real browser with their logged-in accounts and cookies. `aside repl` is a persistent Playwright-compatible JavaScript surface for deterministic control and evidence; `aside exec` delegates a whole task to Aside\'s own agent; `aside mcp` exposes the same REPL over MCP.', + 'Confirm the `aside` CLI is callable in this session (for example `aside --version`) and that the user has an authenticated Aside account. Never sign in, attach an account, or drive the user\'s logged-in sessions without their explicit request; use neutral public pages and localhost for QA.', + 'Prefer `aside repl` for QA: it returns structured evidence (accessibility snapshot, screenshots, console logs, network) rather than a prose summary. Reserve `aside exec` for autonomous multi-step tasks the user explicitly delegates.', + ], + readiness: [ + 'The `aside` CLI is callable and reports a version, and a `aside repl` session can `openTab` and return a structured `snapshot`.', + 'Console evidence is available via `await page.console.logs()` (level, message, timestamp), and network evidence via `page.cdp` (attach the page target, `Network.enable`, then `requestWillBeSent`/`responseReceived` give URL, method, and status).', + 'The common local readiness journey completes end to end through `aside repl`: navigate, read, interact, capture console and network, and release the tab.', + ], + unavailable: 'Offer GStack local browser or defer; never treat the presence of the `aside` binary as readiness, and never drive the user\'s logged-in accounts without an explicit request.', + }, ] as const; export function renderBrowserProviderContract(): string { diff --git a/scripts/gstack2/runtime-install-smoke.sh b/scripts/gstack2/runtime-install-smoke.sh index e987da746b..fbbd9a62dd 100755 --- a/scripts/gstack2/runtime-install-smoke.sh +++ b/scripts/gstack2/runtime-install-smoke.sh @@ -28,7 +28,6 @@ cp -R "$SOURCE/." "$REPO/" rm -rf "$REPO/node_modules" rm -f \ "$REPO/browse/dist/browse" "$REPO/browse/dist/browse.exe" \ - "$REPO/browse/dist/find-browse" "$REPO/browse/dist/find-browse.exe" \ "$REPO/design/dist/design" "$REPO/design/dist/design.exe" \ "$REPO/make-pdf/dist/pdf" "$REPO/make-pdf/dist/pdf.exe" diff --git a/skills/debug/references/BROWSER-PROVIDERS.md b/skills/debug/references/BROWSER-PROVIDERS.md index 9f0e998c29..d05d967609 100644 --- a/skills/debug/references/BROWSER-PROVIDERS.md +++ b/skills/debug/references/BROWSER-PROVIDERS.md @@ -183,6 +183,24 @@ Readiness evidence: If unavailable: Offer GStack local browser, evidence-limited continuation, or deferral; never recommend or install a third-party Pi browser package automatically. +## Aside AI browser + +Classification: `dedicated-ai-browser` + +Setup: + +1. Explain that Aside is a standalone AI browser driven from the terminal by the `aside` CLI, running the user's real browser with their logged-in accounts and cookies. `aside repl` is a persistent Playwright-compatible JavaScript surface for deterministic control and evidence; `aside exec` delegates a whole task to Aside's own agent; `aside mcp` exposes the same REPL over MCP. +2. Confirm the `aside` CLI is callable in this session (for example `aside --version`) and that the user has an authenticated Aside account. Never sign in, attach an account, or drive the user's logged-in sessions without their explicit request; use neutral public pages and localhost for QA. +3. Prefer `aside repl` for QA: it returns structured evidence (accessibility snapshot, screenshots, console logs, network) rather than a prose summary. Reserve `aside exec` for autonomous multi-step tasks the user explicitly delegates. + +Readiness evidence: + +- The `aside` CLI is callable and reports a version, and a `aside repl` session can `openTab` and return a structured `snapshot`. +- Console evidence is available via `await page.console.logs()` (level, message, timestamp), and network evidence via `page.cdp` (attach the page target, `Network.enable`, then `requestWillBeSent`/`responseReceived` give URL, method, and status). +- The common local readiness journey completes end to end through `aside repl`: navigate, read, interact, capture console and network, and release the tab. + +If unavailable: Offer GStack local browser or defer; never treat the presence of the `aside` binary as readiness, and never drive the user's logged-in accounts without an explicit request. + ## GStack local browser fallback GStack's existing local Chromium/Playwright implementation remains the only bundled browser backend. Follow `RUNTIME.md` for preview and separate install consent, run `gstack doctor`, then run the same readiness journey with the `browse` launcher. Do not run `./setup`, add a cloud browser, remote provider, alternate local backend, or personal-profile attachment as a fallback. diff --git a/skills/design/references/BROWSER-PROVIDERS.md b/skills/design/references/BROWSER-PROVIDERS.md index 9f0e998c29..d05d967609 100644 --- a/skills/design/references/BROWSER-PROVIDERS.md +++ b/skills/design/references/BROWSER-PROVIDERS.md @@ -183,6 +183,24 @@ Readiness evidence: If unavailable: Offer GStack local browser, evidence-limited continuation, or deferral; never recommend or install a third-party Pi browser package automatically. +## Aside AI browser + +Classification: `dedicated-ai-browser` + +Setup: + +1. Explain that Aside is a standalone AI browser driven from the terminal by the `aside` CLI, running the user's real browser with their logged-in accounts and cookies. `aside repl` is a persistent Playwright-compatible JavaScript surface for deterministic control and evidence; `aside exec` delegates a whole task to Aside's own agent; `aside mcp` exposes the same REPL over MCP. +2. Confirm the `aside` CLI is callable in this session (for example `aside --version`) and that the user has an authenticated Aside account. Never sign in, attach an account, or drive the user's logged-in sessions without their explicit request; use neutral public pages and localhost for QA. +3. Prefer `aside repl` for QA: it returns structured evidence (accessibility snapshot, screenshots, console logs, network) rather than a prose summary. Reserve `aside exec` for autonomous multi-step tasks the user explicitly delegates. + +Readiness evidence: + +- The `aside` CLI is callable and reports a version, and a `aside repl` session can `openTab` and return a structured `snapshot`. +- Console evidence is available via `await page.console.logs()` (level, message, timestamp), and network evidence via `page.cdp` (attach the page target, `Network.enable`, then `requestWillBeSent`/`responseReceived` give URL, method, and status). +- The common local readiness journey completes end to end through `aside repl`: navigate, read, interact, capture console and network, and release the tab. + +If unavailable: Offer GStack local browser or defer; never treat the presence of the `aside` binary as readiness, and never drive the user's logged-in accounts without an explicit request. + ## GStack local browser fallback GStack's existing local Chromium/Playwright implementation remains the only bundled browser backend. Follow `RUNTIME.md` for preview and separate install consent, run `gstack doctor`, then run the same readiness journey with the `browse` launcher. Do not run `./setup`, add a cloud browser, remote provider, alternate local backend, or personal-profile attachment as a fallback. diff --git a/skills/plan/references/BROWSER-PROVIDERS.md b/skills/plan/references/BROWSER-PROVIDERS.md index 9f0e998c29..d05d967609 100644 --- a/skills/plan/references/BROWSER-PROVIDERS.md +++ b/skills/plan/references/BROWSER-PROVIDERS.md @@ -183,6 +183,24 @@ Readiness evidence: If unavailable: Offer GStack local browser, evidence-limited continuation, or deferral; never recommend or install a third-party Pi browser package automatically. +## Aside AI browser + +Classification: `dedicated-ai-browser` + +Setup: + +1. Explain that Aside is a standalone AI browser driven from the terminal by the `aside` CLI, running the user's real browser with their logged-in accounts and cookies. `aside repl` is a persistent Playwright-compatible JavaScript surface for deterministic control and evidence; `aside exec` delegates a whole task to Aside's own agent; `aside mcp` exposes the same REPL over MCP. +2. Confirm the `aside` CLI is callable in this session (for example `aside --version`) and that the user has an authenticated Aside account. Never sign in, attach an account, or drive the user's logged-in sessions without their explicit request; use neutral public pages and localhost for QA. +3. Prefer `aside repl` for QA: it returns structured evidence (accessibility snapshot, screenshots, console logs, network) rather than a prose summary. Reserve `aside exec` for autonomous multi-step tasks the user explicitly delegates. + +Readiness evidence: + +- The `aside` CLI is callable and reports a version, and a `aside repl` session can `openTab` and return a structured `snapshot`. +- Console evidence is available via `await page.console.logs()` (level, message, timestamp), and network evidence via `page.cdp` (attach the page target, `Network.enable`, then `requestWillBeSent`/`responseReceived` give URL, method, and status). +- The common local readiness journey completes end to end through `aside repl`: navigate, read, interact, capture console and network, and release the tab. + +If unavailable: Offer GStack local browser or defer; never treat the presence of the `aside` binary as readiness, and never drive the user's logged-in accounts without an explicit request. + ## GStack local browser fallback GStack's existing local Chromium/Playwright implementation remains the only bundled browser backend. Follow `RUNTIME.md` for preview and separate install consent, run `gstack doctor`, then run the same readiness journey with the `browse` launcher. Do not run `./setup`, add a cloud browser, remote provider, alternate local backend, or personal-profile attachment as a fallback. diff --git a/skills/qa/references/BROWSER-PROVIDERS.md b/skills/qa/references/BROWSER-PROVIDERS.md index 9f0e998c29..d05d967609 100644 --- a/skills/qa/references/BROWSER-PROVIDERS.md +++ b/skills/qa/references/BROWSER-PROVIDERS.md @@ -183,6 +183,24 @@ Readiness evidence: If unavailable: Offer GStack local browser, evidence-limited continuation, or deferral; never recommend or install a third-party Pi browser package automatically. +## Aside AI browser + +Classification: `dedicated-ai-browser` + +Setup: + +1. Explain that Aside is a standalone AI browser driven from the terminal by the `aside` CLI, running the user's real browser with their logged-in accounts and cookies. `aside repl` is a persistent Playwright-compatible JavaScript surface for deterministic control and evidence; `aside exec` delegates a whole task to Aside's own agent; `aside mcp` exposes the same REPL over MCP. +2. Confirm the `aside` CLI is callable in this session (for example `aside --version`) and that the user has an authenticated Aside account. Never sign in, attach an account, or drive the user's logged-in sessions without their explicit request; use neutral public pages and localhost for QA. +3. Prefer `aside repl` for QA: it returns structured evidence (accessibility snapshot, screenshots, console logs, network) rather than a prose summary. Reserve `aside exec` for autonomous multi-step tasks the user explicitly delegates. + +Readiness evidence: + +- The `aside` CLI is callable and reports a version, and a `aside repl` session can `openTab` and return a structured `snapshot`. +- Console evidence is available via `await page.console.logs()` (level, message, timestamp), and network evidence via `page.cdp` (attach the page target, `Network.enable`, then `requestWillBeSent`/`responseReceived` give URL, method, and status). +- The common local readiness journey completes end to end through `aside repl`: navigate, read, interact, capture console and network, and release the tab. + +If unavailable: Offer GStack local browser or defer; never treat the presence of the `aside` binary as readiness, and never drive the user's logged-in accounts without an explicit request. + ## GStack local browser fallback GStack's existing local Chromium/Playwright implementation remains the only bundled browser backend. Follow `RUNTIME.md` for preview and separate install consent, run `gstack doctor`, then run the same readiness journey with the `browse` launcher. Do not run `./setup`, add a cloud browser, remote provider, alternate local backend, or personal-profile attachment as a fallback. diff --git a/skills/review/references/BROWSER-PROVIDERS.md b/skills/review/references/BROWSER-PROVIDERS.md index 9f0e998c29..d05d967609 100644 --- a/skills/review/references/BROWSER-PROVIDERS.md +++ b/skills/review/references/BROWSER-PROVIDERS.md @@ -183,6 +183,24 @@ Readiness evidence: If unavailable: Offer GStack local browser, evidence-limited continuation, or deferral; never recommend or install a third-party Pi browser package automatically. +## Aside AI browser + +Classification: `dedicated-ai-browser` + +Setup: + +1. Explain that Aside is a standalone AI browser driven from the terminal by the `aside` CLI, running the user's real browser with their logged-in accounts and cookies. `aside repl` is a persistent Playwright-compatible JavaScript surface for deterministic control and evidence; `aside exec` delegates a whole task to Aside's own agent; `aside mcp` exposes the same REPL over MCP. +2. Confirm the `aside` CLI is callable in this session (for example `aside --version`) and that the user has an authenticated Aside account. Never sign in, attach an account, or drive the user's logged-in sessions without their explicit request; use neutral public pages and localhost for QA. +3. Prefer `aside repl` for QA: it returns structured evidence (accessibility snapshot, screenshots, console logs, network) rather than a prose summary. Reserve `aside exec` for autonomous multi-step tasks the user explicitly delegates. + +Readiness evidence: + +- The `aside` CLI is callable and reports a version, and a `aside repl` session can `openTab` and return a structured `snapshot`. +- Console evidence is available via `await page.console.logs()` (level, message, timestamp), and network evidence via `page.cdp` (attach the page target, `Network.enable`, then `requestWillBeSent`/`responseReceived` give URL, method, and status). +- The common local readiness journey completes end to end through `aside repl`: navigate, read, interact, capture console and network, and release the tab. + +If unavailable: Offer GStack local browser or defer; never treat the presence of the `aside` binary as readiness, and never drive the user's logged-in accounts without an explicit request. + ## GStack local browser fallback GStack's existing local Chromium/Playwright implementation remains the only bundled browser backend. Follow `RUNTIME.md` for preview and separate install consent, run `gstack doctor`, then run the same readiness journey with the `browse` launcher. Do not run `./setup`, add a cloud browser, remote provider, alternate local backend, or personal-profile attachment as a fallback. diff --git a/skills/ship/references/BROWSER-PROVIDERS.md b/skills/ship/references/BROWSER-PROVIDERS.md index 9f0e998c29..d05d967609 100644 --- a/skills/ship/references/BROWSER-PROVIDERS.md +++ b/skills/ship/references/BROWSER-PROVIDERS.md @@ -183,6 +183,24 @@ Readiness evidence: If unavailable: Offer GStack local browser, evidence-limited continuation, or deferral; never recommend or install a third-party Pi browser package automatically. +## Aside AI browser + +Classification: `dedicated-ai-browser` + +Setup: + +1. Explain that Aside is a standalone AI browser driven from the terminal by the `aside` CLI, running the user's real browser with their logged-in accounts and cookies. `aside repl` is a persistent Playwright-compatible JavaScript surface for deterministic control and evidence; `aside exec` delegates a whole task to Aside's own agent; `aside mcp` exposes the same REPL over MCP. +2. Confirm the `aside` CLI is callable in this session (for example `aside --version`) and that the user has an authenticated Aside account. Never sign in, attach an account, or drive the user's logged-in sessions without their explicit request; use neutral public pages and localhost for QA. +3. Prefer `aside repl` for QA: it returns structured evidence (accessibility snapshot, screenshots, console logs, network) rather than a prose summary. Reserve `aside exec` for autonomous multi-step tasks the user explicitly delegates. + +Readiness evidence: + +- The `aside` CLI is callable and reports a version, and a `aside repl` session can `openTab` and return a structured `snapshot`. +- Console evidence is available via `await page.console.logs()` (level, message, timestamp), and network evidence via `page.cdp` (attach the page target, `Network.enable`, then `requestWillBeSent`/`responseReceived` give URL, method, and status). +- The common local readiness journey completes end to end through `aside repl`: navigate, read, interact, capture console and network, and release the tab. + +If unavailable: Offer GStack local browser or defer; never treat the presence of the `aside` binary as readiness, and never drive the user's logged-in accounts without an explicit request. + ## GStack local browser fallback GStack's existing local Chromium/Playwright implementation remains the only bundled browser backend. Follow `RUNTIME.md` for preview and separate install consent, run `gstack doctor`, then run the same readiness journey with the `browse` launcher. Do not run `./setup`, add a cloud browser, remote provider, alternate local backend, or personal-profile attachment as a fallback. diff --git a/test/gstack2-browser-provider-setup.test.ts b/test/gstack2-browser-provider-setup.test.ts index e1109b776c..4cd59dd805 100644 --- a/test/gstack2-browser-provider-setup.test.ts +++ b/test/gstack2-browser-provider-setup.test.ts @@ -16,7 +16,7 @@ afterEach(async () => { }); describe("browser provider setup contract", () => { - test("defines every verified host plus Gemini without inventing Kimi or Pi core automation", () => { + test("defines every verified host plus Gemini and the Aside AI browser without inventing Kimi or Pi core automation", () => { expect(BROWSER_PROVIDER_CONTRACTS.map((provider) => provider.id)).toEqual([ "claude", "codex", @@ -26,6 +26,7 @@ describe("browser provider setup contract", () => { "openclaw", "kimi", "pi", + "aside", ]); expect(BROWSER_PROVIDER_CONTRACTS.map((provider) => provider.kind)).toEqual([ "native-extension", @@ -36,6 +37,7 @@ describe("browser provider setup contract", () => { "native-plugin", "no-native-automation", "extension-only", + "dedicated-ai-browser", ]); const rendered = renderBrowserProviderContract(); @@ -48,6 +50,8 @@ describe("browser provider setup contract", () => { expect(rendered).toContain("OpenClaw includes a browser plugin"); expect(rendered).toContain("Do not describe `kimi web` as browser automation"); expect(rendered).toContain("Pi intentionally has no core interactive browser tool"); + expect(rendered).toContain("Aside is a standalone AI browser driven from the terminal by the `aside` CLI"); + expect(rendered).toContain("await page.console.logs()"); expect(rendered).toContain("never silently add a browser MCP or alternate backend"); expect(rendered).toContain("GStack's existing local Chromium/Playwright implementation remains the only bundled browser backend"); expect(rendered).toContain("Do not run `./setup`"); diff --git a/test/helpers/e2e-helpers.ts b/test/helpers/e2e-helpers.ts index 32510f13ac..2ef07ebd18 100644 --- a/test/helpers/e2e-helpers.ts +++ b/test/helpers/e2e-helpers.ts @@ -103,7 +103,7 @@ export function copyDirSync(src: string, dest: string) { } /** - * Set up browse shims (binary symlink, find-browse, remote-slug) in a tmpDir. + * Set up browse shims (binary symlink, remote-slug) in a tmpDir. */ export function setupBrowseShims(dir: string) { // Symlink browse binary @@ -113,14 +113,8 @@ export function setupBrowseShims(dir: string) { fs.symlinkSync(browseBin, path.join(binDir, 'browse')); } - // find-browse shim const findBrowseDir = path.join(dir, 'browse', 'bin'); fs.mkdirSync(findBrowseDir, { recursive: true }); - fs.writeFileSync( - path.join(findBrowseDir, 'find-browse'), - `#!/bin/bash\necho "${browseBin}"\n`, - { mode: 0o755 }, - ); // remote-slug shim (returns test-project) fs.writeFileSync( diff --git a/test/skill-e2e.test.ts b/test/skill-e2e.test.ts index 312a609cca..a167dd2aea 100644 --- a/test/skill-e2e.test.ts +++ b/test/skill-e2e.test.ts @@ -107,7 +107,7 @@ function copyDirSync(src: string, dest: string) { } /** - * Set up browse shims (binary symlink, find-browse, remote-slug) in a tmpDir. + * Set up browse shims (binary symlink, remote-slug) in a tmpDir. */ function setupBrowseShims(dir: string) { // Symlink browse binary @@ -117,14 +117,8 @@ function setupBrowseShims(dir: string) { fs.symlinkSync(browseBin, path.join(binDir, 'browse')); } - // find-browse shim const findBrowseDir = path.join(dir, 'browse', 'bin'); fs.mkdirSync(findBrowseDir, { recursive: true }); - fs.writeFileSync( - path.join(findBrowseDir, 'find-browse'), - `#!/bin/bash\necho "${browseBin}"\n`, - { mode: 0o755 }, - ); // remote-slug shim (returns test-project) fs.writeFileSync(