~cytrogen/gstack

716e4c934aff10c142a5b4735ad3e919bcb77c2c — Garry Tan a month ago 3ce810f + 1a100a2
Merge pull request #128 from xr843/fix/chain-duplication-and-flush-perf

fix: eliminate duplicate command sets in chain, improve flush perf and type safety
3 files changed, 17 insertions(+), 38 deletions(-)

M browse/src/browser-manager.ts
M browse/src/meta-commands.ts
M browse/src/server.ts
M browse/src/browser-manager.ts => browse/src/browser-manager.ts +10 -10
@@ 15,7 15,7 @@
 *   restores state. Falls back to clean slate on any failure.
 */

import { chromium, type Browser, type BrowserContext, type Page, type Locator } from 'playwright';
import { chromium, type Browser, type BrowserContext, type BrowserContextOptions, type Page, type Locator } from 'playwright';
import { addConsoleEntry, addNetworkEntry, addDialogEntry, networkBuffer, type DialogEntry } from './buffers';

export interface RefEntry {


@@ 57,7 57,7 @@ export class BrowserManager {
      process.exit(1);
    });

    const contextOptions: any = {
    const contextOptions: BrowserContextOptions = {
      viewport: { width: 1280, height: 720 },
    };
    if (this.customUserAgent) {


@@ 282,7 282,7 @@ export class BrowserManager {
    try {
      // 1. Save state from current context
      const savedCookies = await this.context.cookies();
      const savedPages: Array<{ url: string; isActive: boolean; storage: any }> = [];
      const savedPages: Array<{ url: string; isActive: boolean; storage: { localStorage: Record<string, string>; sessionStorage: Record<string, string> } | null }> = [];

      for (const [id, page] of this.pages) {
        const url = page.url();


@@ 308,7 308,7 @@ export class BrowserManager {
      await this.context.close().catch(() => {});

      // 3. Create new context with updated settings
      const contextOptions: any = {
      const contextOptions: BrowserContextOptions = {
        viewport: { width: 1280, height: 720 },
      };
      if (this.customUserAgent) {


@@ 340,15 340,15 @@ export class BrowserManager {
        // 6. Restore storage
        if (saved.storage) {
          try {
            await page.evaluate((s: any) => {
            await page.evaluate((s: { localStorage: Record<string, string>; sessionStorage: Record<string, string> }) => {
              if (s.localStorage) {
                for (const [k, v] of Object.entries(s.localStorage)) {
                  localStorage.setItem(k, v as string);
                  localStorage.setItem(k, v);
                }
              }
              if (s.sessionStorage) {
                for (const [k, v] of Object.entries(s.sessionStorage)) {
                  sessionStorage.setItem(k, v as string);
                  sessionStorage.setItem(k, v);
                }
              }
            }, saved.storage);


@@ 369,13 369,13 @@ export class BrowserManager {
      this.clearRefs();

      return null; // success
    } catch (err: any) {
    } catch (err: unknown) {
      // Fallback: create a clean context + blank tab
      try {
        this.pages.clear();
        if (this.context) await this.context.close().catch(() => {});

        const contextOptions: any = {
        const contextOptions: BrowserContextOptions = {
          viewport: { width: 1280, height: 720 },
        };
        if (this.customUserAgent) {


@@ 387,7 387,7 @@ export class BrowserManager {
      } catch {
        // If even the fallback fails, we're in trouble — but browser is still alive
      }
      return `Context recreation failed: ${err.message}. Browser reset to blank tab.`;
      return `Context recreation failed: ${err instanceof Error ? err.message : String(err)}. Browser reset to blank tab.`;
    }
  }


M browse/src/meta-commands.ts => browse/src/meta-commands.ts +4 -25
@@ 5,6 5,7 @@
import type { BrowserManager } from './browser-manager';
import { handleSnapshot } from './snapshot';
import { getCleanText } from './read-commands';
import { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS } from './commands';
import * as Diff from 'diff';
import * as fs from 'fs';
import * as path from 'path';


@@ 20,28 21,6 @@ function validateOutputPath(filePath: string): void {
  }
}

// Command sets for chain routing (mirrors server.ts — kept local to avoid circular import)
const CHAIN_READ = new Set([
  'text', 'html', 'links', 'forms', 'accessibility',
  'js', 'eval', 'css', 'attrs',
  'console', 'network', 'cookies', 'storage', 'perf',
  'dialog', 'is',
]);
const CHAIN_WRITE = new Set([
  'goto', 'back', 'forward', 'reload',
  'click', 'fill', 'select', 'hover', 'type', 'press', 'scroll', 'wait',
  'viewport', 'cookie', 'cookie-import', 'header', 'useragent',
  'upload', 'dialog-accept', 'dialog-dismiss',
  'cookie-import-browser',
]);
const CHAIN_META = new Set([
  'tabs', 'tab', 'newtab', 'closetab',
  'status', 'stop', 'restart',
  'screenshot', 'pdf', 'responsive',
  'chain', 'diff',
  'url', 'snapshot',
]);

export async function handleMetaCommand(
  command: string,
  args: string[],


@@ 223,9 202,9 @@ export async function handleMetaCommand(
        const [name, ...cmdArgs] = cmd;
        try {
          let result: string;
          if (CHAIN_WRITE.has(name))      result = await handleWriteCommand(name, cmdArgs, bm);
          else if (CHAIN_READ.has(name))  result = await handleReadCommand(name, cmdArgs, bm);
          else if (CHAIN_META.has(name))  result = await handleMetaCommand(name, cmdArgs, bm, shutdown);
          if (WRITE_COMMANDS.has(name))    result = await handleWriteCommand(name, cmdArgs, bm);
          else if (READ_COMMANDS.has(name))  result = await handleReadCommand(name, cmdArgs, bm);
          else if (META_COMMANDS.has(name))  result = await handleMetaCommand(name, cmdArgs, bm, shutdown);
          else throw new Error(`Unknown command: ${name}`);
          results.push(`[${name}] ${result}`);
        } catch (err: any) {

M browse/src/server.ts => browse/src/server.ts +3 -3
@@ 104,7 104,7 @@ async function flushBuffers() {
      const lines = entries.map(e =>
        `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
      ).join('\n') + '\n';
      await Bun.write(CONSOLE_LOG_PATH, (await Bun.file(CONSOLE_LOG_PATH).text().catch(() => '')) + lines);
      fs.appendFileSync(CONSOLE_LOG_PATH, lines);
      lastConsoleFlushed = consoleBuffer.totalAdded;
    }



@@ 115,7 115,7 @@ async function flushBuffers() {
      const lines = entries.map(e =>
        `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} → ${e.status || 'pending'} (${e.duration || '?'}ms, ${e.size || '?'}B)`
      ).join('\n') + '\n';
      await Bun.write(NETWORK_LOG_PATH, (await Bun.file(NETWORK_LOG_PATH).text().catch(() => '')) + lines);
      fs.appendFileSync(NETWORK_LOG_PATH, lines);
      lastNetworkFlushed = networkBuffer.totalAdded;
    }



@@ 126,7 126,7 @@ async function flushBuffers() {
      const lines = entries.map(e =>
        `[${new Date(e.timestamp).toISOString()}] [${e.type}] "${e.message}" → ${e.action}${e.response ? ` "${e.response}"` : ''}`
      ).join('\n') + '\n';
      await Bun.write(DIALOG_LOG_PATH, (await Bun.file(DIALOG_LOG_PATH).text().catch(() => '')) + lines);
      fs.appendFileSync(DIALOG_LOG_PATH, lines);
      lastDialogFlushed = dialogBuffer.totalAdded;
    }
  } catch {