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
- func KeyframeFlags(deltas []float64, cut float64) []bool
- func SettleIndex(deltas []float64, quiet float64, stableN int) int
- type ConsoleEntry
- type FrameInfo
- type Manager
- func (m *Manager) Act(ctx context.Context, steps []Step) error
- func (m *Manager) ActiveTake() *Take
- func (m *Manager) AllowHostPort(hostPort string)
- func (m *Manager) CaptureStill(ctx context.Context, opts StillOptions) ([]StillResult, error)
- func (m *Manager) ClearViewport() error
- func (m *Manager) Close()
- func (m *Manager) Console(last int) []ConsoleEntry
- func (m *Manager) EnsureStarted() error
- func (m *Manager) Eval(js string) (string, error)
- func (m *Manager) Open(rawURL string) error
- func (m *Manager) Running() bool
- func (m *Manager) SetAllowedHostPorts(hostPorts []string)
- func (m *Manager) SetHeadless(headless bool) error
- func (m *Manager) SetViewport(width, height int, mobile bool) error
- func (m *Manager) StartRecording(opts RecordOptions) (*Take, error)
- type Options
- type RecordOptions
- type Step
- type StillOptions
- type StillResult
- type Take
- type TakeManifest
- type TakeMark
- type TakeNavigation
Constants ¶
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.
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 ¶
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.
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 ¶
New creates a Manager hung off sessionCtx. Nothing is launched here; the first Open, Act, or EnsureStarted call launches.
func (*Manager) Act ¶
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 ¶
ActiveTake returns the manager's currently recording take, or nil.
func (*Manager) AllowHostPort ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) SetAllowedHostPorts ¶
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 ¶
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 ¶
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 ¶
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"`
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 ¶
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 {
}
TakeNavigation is one navigation observed during a take (timestamp only — transitions are where bugs live, so they are part of the timeline).