browser

package
v4.0.0-beta.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package browser wraps the go-rod automation library into the small surface the CLI dev session needs: one managed browser process, one long-lived tab, an origin allowlist, and a small set of scripted action primitives.

The browser is launched through rod's leakless wrapper (github.com/ysmood/ leakless): a watcher process babysits the child, so when the CLI dies without running its cleanup, leakless reaps the browser anyway — no Chrome process is ever left behind. Every call routes through the same kept-alive page; the browser is closed after an idle period and relaunched transparently on the next use.

An origin allowlist guards every navigation: only loopback hosts on the configured dev-server ports may be opened; anything else is refused before the browser is even touched.

This package produces no user-facing output by design; callers report failures to their own channels.

console.go — page-console retention for the managed browser.

Every launch subscribes the kept-alive tab to Runtime.consoleAPICalled and retains the error and warning messages in a bounded ring, so the audit's console-errors plane and the logs tool can report what the page's JavaScript said after the fact. The ring is session history, not per-browser state: it survives idle closes and headless toggles, and only the subscription is re-armed per launch.

eval.go — the JS-read seam the dev session's MCP surface needs: audit and probe run read-style expressions inside the kept-alive page. This is the only place outside this package that reaches the page's JS context; the expression contract ("() => …" returning a serializable value) keeps the surface read-shaped.

Index

Constants

View Source
const (
	// DefaultIdleAfter is how long the browser stays up with no activity
	// before it is closed. The next call relaunches it.
	DefaultIdleAfter = 5 * time.Minute

	// DefaultStepTimeout bounds each individual scripted action step.
	DefaultStepTimeout = 15 * time.Second

	// DefaultUserDataDir keeps cookies and localStorage across relaunches so
	// session state survives a headless toggle or an idle shutdown. It is
	// relative to the CLI's working directory, which is the user's project
	// root in a dev session.
	DefaultUserDataDir = ".gothicCli/mcp/state/browser-profile"

	// SettleQuiet is the quiet window WaitStable requires before a navigation
	// counts as settled.
	SettleQuiet = 200 * time.Millisecond
)

Defaults for the options a caller may override.

View Source
const (
	// DefaultCaptureDir is where stills and takes land when the caller gives
	// no directory. Relative to the CLI process working directory (the user's
	// project root in a dev session), beside the other .gothicCli state.
	DefaultCaptureDir = ".gothicCli/mcp"

	// DefaultRecordMaxDuration is the auto-stop budget for a take. A take
	// never runs longer than this without an explicit Stop.
	DefaultRecordMaxDuration = 60 * time.Second

	// DefaultRecordMaxFrames is the frame ceiling: beyond it the OLDEST
	// frames are dropped so a long busy take stays bounded on disk.
	DefaultRecordMaxFrames = 1200

	// DefaultRecordQuality is the JPEG quality for screencast frames.
	DefaultRecordQuality = 80

	// DefaultStillMaxRects caps the DOM manifest element count.
	DefaultStillMaxRects = 400

	// FrameCutDelta is the mean per-channel pixel delta (0..255) above which
	// a frame counts as a scene cut (a keyframe).
	FrameCutDelta = 8.0

	// FrameQuietDelta is the mean per-channel pixel delta below which a frame
	// counts as visually stable.
	FrameQuietDelta = 2.0

	// FrameStableRun is how many consecutive stable frames end a take
	// (settle detection).
	FrameStableRun = 4

	// FrameQuietWindow is the no-frame settle window: with no new frames
	// arriving for this long (the compositor emits nothing while the page is
	// static) the take counts as settled. Frame-based settle (FrameStableRun
	// consecutive stable frames) handles pages that repaint steadily.
	FrameQuietWindow = 1500 * time.Millisecond
)

Capture defaults and locked thresholds.

Variables

This section is empty.

Functions

func KeyframeFlags

func KeyframeFlags(deltas []float64, cut float64) []bool

KeyframeFlags marks the frames that start a new visual scene: the first frame, and any frame whose delta from the previous one reaches cut. Pure; exercised against fixture delta sequences.

func SettleIndex

func SettleIndex(deltas []float64, quiet float64, stableN int) int

SettleIndex returns the index of the first frame whose trailing run of stableN consecutive deltas below quiet completes — the point where a take ends. -1 when the sequence never settles.

Types

type ConsoleEntry

type ConsoleEntry struct {
	Type string `json:"type"` // "error" or "warning"
	Text string `json:"text"`
	// TimestampMs is epoch milliseconds; CDP's Runtime.Timestamp counts
	// seconds since the epoch (mirrors the screencast frame timestamps), so
	// ordering across entries is stable but the field is wall-clock, not
	// relative to the page's time origin.
	TimestampMs int64 `json:"timestamp_ms"`
}

ConsoleEntry is one retained console message from the managed page.

type FrameInfo

type FrameInfo struct {
	File     string  `json:"file"`     // frame_NNNNN.jpg inside the take dir
	T        int64   `json:"t"`        // epoch ms (CDP frame swap timestamp when present)
	Delta    float64 `json:"delta"`    // mean pixel delta vs the previous frame
	Keyframe bool    `json:"keyframe"` // scene cut (delta >= FrameCutDelta)
	ScrollX  float64 `json:"scroll_x,omitempty"`
	ScrollY  float64 `json:"scroll_y,omitempty"`
	W        int     `json:"w"`
	H        int     `json:"h"`
}

FrameInfo is one screencast frame in a take manifest.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager owns the managed browser lifecycle: launch, the single kept-alive tab, the idle close, and shutdown. It is safe for concurrent use; all calls serialize on one mutex so scripted actions from concurrent clients run in order.

func New

func New(sessionCtx context.Context, opts Options) *Manager

New creates a Manager hung off sessionCtx. Nothing is launched here; the first Open, Act, or EnsureStarted call launches.

func (*Manager) Act

func (m *Manager) Act(ctx context.Context, steps []Step) error

Act runs steps in order over the single kept-alive page, launching the browser first if needed. Each step is bounded by the step timeout and the whole run by ctx. The first failure aborts the run and is reported with the failing step's index and action.

func (*Manager) ActiveTake

func (m *Manager) ActiveTake() *Take

ActiveTake returns the manager's currently recording take, or nil.

func (*Manager) AllowHostPort

func (m *Manager) AllowHostPort(hostPort string)

AllowHostPort adds one host:port pair to the origin allowlist.

func (*Manager) CaptureStill

func (m *Manager) CaptureStill(ctx context.Context, opts StillOptions) ([]StillResult, error)

CaptureStill navigates (optionally) and screenshots the compositor once per requested viewport width, writing <png> and a DOM manifest next to it. The manifest is produced by ONE page.Eval and matches the shape cli/internal/pagemap consumes; it additionally carries the URL and the viewport size (extra keys pagemap ignores).

func (*Manager) ClearViewport

func (m *Manager) ClearViewport() error

ClearViewport lifts a pinned override and restores the browser's own window size. With no pin and no running page it is a clean no-op; with a pin but no running page the pin is dropped so a later launch starts unpinned.

func (*Manager) Close

func (m *Manager) Close()

Close tears the browser down and marks the manager unusable. Safe to call more than once; concurrent calls serialize on the same mutex.

func (*Manager) Console

func (m *Manager) Console(last int) []ConsoleEntry

Console returns the retained console messages, newest first. last <= 0 returns the whole ring (at most consoleRingCap entries).

func (*Manager) EnsureStarted

func (m *Manager) EnsureStarted() error

EnsureStarted launches the browser and tab without navigating anywhere. The dev command calls it at session start so the first tool call never pays the launch latency.

func (*Manager) Eval

func (m *Manager) Eval(js string) (string, error)

Eval runs one rod-style JS function expression ("() => …") in the kept-alive page and returns its value serialized to a string. Promise results are awaited by rod's Eval shortcut. Like every other entry point, it launches the browser first if needed and re-arms the idle timer.

func (*Manager) Open

func (m *Manager) Open(rawURL string) error

Open navigates the kept-alive tab to rawURL, launching the browser first if needed. The allowlist gates the URL before anything is launched.

func (*Manager) Running

func (m *Manager) Running() bool

Running reports whether the browser is currently up.

func (*Manager) SetAllowedHostPorts

func (m *Manager) SetAllowedHostPorts(hostPorts []string)

SetAllowedHostPorts replaces the origin allowlist with host:port pairs such as "localhost:3000". Ports not on the list are refused; non-local hosts are always refused regardless of the list.

func (*Manager) SetHeadless

func (m *Manager) SetHeadless(headless bool) error

SetHeadless switches the managed browser between headless and headful.

Chrome cannot change this at runtime: the flag belongs to the process, so toggling restarts the browser. The profile directory (UserDataDir) is reused, which is the documented restart contract: cookies, localStorage, and session state all survive the toggle. Only the running processes are torn down and relaunched; the allowlist and options are kept.

If the browser is not currently running, the toggle just updates the options the next launch will use.

func (*Manager) SetViewport

func (m *Manager) SetViewport(width, height int, mobile bool) error

SetViewport pins a device-metrics override on the managed page until ClearViewport: the agent keeps the browser in one configuration (say a 375px mobile viewport) across navigations, probes, and captures. CDP's override outlives navigations within the target; the manager re-applies it after any relaunch. A non-positive height keeps the page's current height (CDP replaces both dimensions at once, so the pin must carry an explicit pair).

func (*Manager) StartRecording

func (m *Manager) StartRecording(opts RecordOptions) (*Take, error)

StartRecording begins a screencast take. A take already running is closed automatically (its manifest keeps a warning), which is the double-start valve. The take cleans up after itself when the dev session context ends or the browser closes — a take can never outlive its manager.

type Options

type Options struct {
	// Headless runs the browser without a window. Toggling it relaunches the
	// profile in the other mode; see SetHeadless.
	Headless bool

	// AllowDownload lets the manager download rod's pinned-revision Chromium
	// when launcher.LookPath finds no installed browser. The download lands
	// under the CLI cache dir (see browserDownloadDir). Tests leave this
	// false so a machine without a browser skips instead of downloading.
	AllowDownload bool

	// UserDataDir for the browser profile, keeping cookies across relaunches.
	// Empty takes DefaultUserDataDir (relative to the process working
	// directory, which for the CLI is the user's project root).
	UserDataDir string

	// IdleAfter overrides DefaultIdleAfter when positive.
	IdleAfter time.Duration

	// StepTimeout overrides DefaultStepTimeout when positive.
	StepTimeout time.Duration

	// BinPath forces a specific browser executable, bypassing
	// launcher.LookPath and downloads. A test seam; production leaves it
	// empty.
	BinPath string
}

Options configures a Manager. Callers use NewOptions to get the production defaults (headless on, download off).

func NewOptions

func NewOptions() Options

NewOptions returns the production defaults: headless on.

type RecordOptions

type RecordOptions struct {
	// BaseDir under which rec_<id>/ is created. Empty takes DefaultCaptureDir.
	BaseDir string

	// MaxDuration is the auto-stop budget. <=0 takes the 60s default.
	MaxDuration time.Duration

	// MaxFrames is the ceiling; beyond it the oldest frames are dropped.
	// <=0 takes the 1200 default.
	MaxFrames int

	// Quality is the JPEG quality for frames. <=0 takes 80.
	Quality int

	// MaxWidth/MaxHeight optionally cap the screencast frame size.
	MaxWidth, MaxHeight int

	// Marks pulls the newest act marks from the dev bus ring (newest first).
	// The recorder keeps everything not yet consumed. Nil means no marks.
	Marks func() []TakeMark
}

RecordOptions configures a recorded take.

type Step

type Step struct {
	// Action is one of: "goto", "click", "fill", "hover", "scroll", "key",
	// "select". Unknown actions error.
	Action string
	// Selector is the CSS selector the action targets. Required by click,
	// fill, hover, and select; ignored by goto, scroll, and key.
	Selector string
	// Value is the payload for the action:
	//   fill   — the text to type into the element (replaces existing content)
	//   goto   — the URL (allowlist-gated)
	//   key    — key name(s), e.g. "Enter Escape Tab ArrowDown a"
	//   select — the option value(s) to select, comma-separated
	//   click, hover, scroll — scroll takes it (see scrollLocked), others unused
	Value string
}

Step is one scripted action primitive over the kept-alive page. Actions address elements by CSS selector.

type StillOptions

type StillOptions struct {
	// Widths is one viewport width per still. Empty captures only the
	// current viewport. Heights are inherited from the current viewport.
	Widths []int

	// URL navigates there first (allowlist-gated). Empty captures the page
	// the managed tab already shows.
	URL string

	// Dir is the output directory. Empty takes DefaultCaptureDir.
	Dir string
}

StillOptions configures a stills capture.

type StillResult

type StillResult struct {
	Width        int        `json:"width"`
	Height       int        `json:"height"`
	PNGPath      string     `json:"png"`
	ManifestPath string     `json:"manifest"`
	URL          string     `json:"url"`
	Scroll       [2]float64 `json:"scroll"`
}

StillResult is one still that landed on disk.

type Take

type Take struct {
	ID  string
	Dir string
	// contains filtered or unexported fields
}

Take is one active (or finished) recording. The manifest is readable after the take's Done channel closes.

func (*Take) Done

func (t *Take) Done() <-chan struct{}

Done exposes the take's completion channel.

func (*Take) Stop

func (t *Take) Stop(discard bool) (TakeManifest, error)

Stop finalizes the take and returns its timeline manifest. With discard, the take directory is deleted and no frames are returned.

func (*Take) Wait

func (t *Take) Wait() (TakeManifest, error)

Wait blocks until the take finalizes on its own (settle, auto-stop, session end or browser close) and returns the manifest.

type TakeManifest

type TakeManifest struct {
	ID          string           `json:"id"`
	URL         string           `json:"url"`
	StartedAt   int64            `json:"started_at"`
	EndedAt     int64            `json:"ended_at"`
	DurationMs  int64            `json:"duration_ms"`
	EndReason   string           `json:"end_reason"`
	Settled     bool             `json:"settled"`
	Dropped     int              `json:"dropped"`
	Frames      []FrameInfo      `json:"frames"`
	Keyframes   []string         `json:"keyframes"`
	Marks       []TakeMark       `json:"marks"`
	Network     []networkEvent   `json:"network"`
	Navigations []TakeNavigation `json:"navigations"`
	Warnings    []string         `json:"warnings,omitempty"`
	Discarded   bool             `json:"discarded,omitempty"`
}

TakeManifest is the timeline manifest written next to a take's frames.

type TakeMark

type TakeMark struct {
	T       int64          `json:"t"`
	Seq     int64          `json:"seq"`
	Event   string         `json:"event"`
	Dir     string         `json:"dir"`
	Payload map[string]any `json:"payload"`
}

TakeMark is one act marker (or settle signal) the injected dev script observed, pulled from the dev bus ring. Shape mirrors the script's kind:"mark" records; t is the epoch-ms timestamp that makes the marker contract ("something happened 220ms after my click") computable.

func MarksFromRecords

func MarksFromRecords(records []map[string]any) []TakeMark

MarksFromRecords filters dev-bus records down to their kind:"mark" entries and decodes them into TakeMarks. This is the adapter the dev session wires between the proxy's bus ring and RecordOptions.Marks.

type TakeNavigation

type TakeNavigation struct {
	T int64 `json:"t"`
}

TakeNavigation is one navigation observed during a take (timestamp only — transitions are where bugs live, so they are part of the timeline).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL