11 · Docs — browser tab

The web, as text, one tab over.

The middle pane is a two-tab slot — the office floor by default, the browser behind ctrl+b — and that second tab is a real in-TUI page viewer. Web pages render as navigable text and link rows — no external binary, no headless Chromium, no runtime to install — so it works on every terminal the office runs on. On kitty-capable terminals with Chrome installed the tab can show rendered headless screenshots. The external terminal-browser package is removed; external links open in your system browser.

The text lane

HTML in, readable rows out.

A loaded page paints a ▸ <url> · <title> bar over the rendered body: a title line, bold headings, wrapped paragraphs, bullet rows, a │ b table rows, code rows, and 🖼 <alt> image chips — image bytes are never fetched, so a heavy page stays cheap. Links arrive as text [n] with their URLs indexed in a side map, stable-ordered and deduped by exact URL.

Fetches are disciplined: 10 seconds bounded, 4 MiB capped, and the payload is content-sniffed — HTML only. A PDF or a PNG lands a dim unsupported content type row instead of a parse, and a non-2xx lands a dim error row. The viewer never pretends a failure is a page.

Getting around

Links are a cursor, not a mouse.

↑/↓ (or j/k) move the link cursor — link rows go dim to bright as it lands, auto-scrolled into view. o opens the focused link: a local file rides to the OS browser, an http(s) link navigates in place. e opens an inline URL editor right in the location bar — prefilled with the current URL, enter opens the edit, esc cancels — and O (shift+o) sends the current page to the OS browser. [ and ] walk a 100-page history ring with scroll offsets restored, r reloads in place without duplicating history, and q / esc leaves back to the floor.

The browser lives on the middle pane, in the slot that usually holds the office floor — ctrl+b flips between floor and browser, and q / esc from the browser returns to the floor. The sidebar strip keeps its eight tabs — chat · terminal · agents · board · mail · activity · git · files — with no browser entry and no digit key for it. Idle, the tab shows its starter card: ▸ enter a url · /open <url> · e to edit · o for file.

↑ / ↓move the link cursoroopen the focused linkeedit the URL inline, in the location barOopen the page in the OS browser[ / ]back / forward, 100-page ringrreload in placepgup / pgdnscroll the bodyq / escback to the floor
Opening pages

One slash command, the chat's open key — and the boss.

/open <url> jumps the browser tab to a page: a file:// URL or a bare path reads straight off disk, an http(s) URL fetches — subject to the policy below. From the chat side, the o hotkey on a bubble carrying a verified URL or on-disk path opens that target too, so a link the boss hands you is one keypress from rendered.

The boss can open pages for you outright. On either backend — opencode or Claude Code — an agent can ask the office to open a URL in the browser tab; when it does, the middle slot flips to the browser automatically and a dim transcript note browser: opening <url> (asked by the boss) marks who sent you there. A refused open posts the reason instead of a page.

The boss's reach goes past opening. It can take a screenshot of a page for you — rendered in the tab on kitty-capable terminals, the PNG saved under ~/.theboringfloor/shots/ with the path posted to the transcript — or snapshot a page for itself, the text and links riding back to it as a follow-up message so it can read what it sent you to. And when reading is not enough it can ask to act on a page — click an element, fill a field, evaluate JavaScript — which always routes through your permission prompt first: approve-once only, no standing grant, not even for localhost. Each action drives a fresh page load, and the outcome — the result, the error, or your rejection — goes back to the boss as a follow-up.

Built-in first

Members open; agents direct the office.

Members use /open <url>. Agents use the office's own-line directives instead of launching a browser process themselves:

/open <url> member opens a page

⟦open-browser: URL⟧ agent opens it in the member's tab

⟦browser-screenshot: URL⟧ agent renders a PNG for the member

⟦browser-snapshot: URL⟧ agent reads text and links back

⟦browser-action: URL | click: CSS-SELECTOR⟧ agent acts only after permission

⟦browser-action: URL | fill: CSS-SELECTOR = VALUE⟧ agent fills only after permission

⟦browser-action: URL | eval: JS-EXPRESSION⟧ agent evaluates only after permission

Open, screenshot, and snapshot are read-only. Browser action can click, fill, or evaluate JavaScript, so it is mutating and always permission-gated. The built-in directives work for localhost and external https:// pages. Agents fall back to Chrome, Chromium, Playwright, Puppeteer, or a terminal browser only when the member explicitly asks, or when the built-in path fails and the agent explains why.

The premium lane

Rendered screenshots, with no extra browser package.

On a kitty-capable terminal — kitty or ghostty; tmux and the iTerm2 family stay text — with Chrome installed, the tab shows rendered screenshots of the page: the headless browser runs out-of-process and the pane paints the result under a shot badge and a ▸ headless chromium · <url> strip. The text viewer remains the universal default everywhere else; the premium path is an upgrade the tab resolves live, not a dependency.

Every shot also lands on disk — ~/.theboringfloor/shots/<ts>-<hash>.png, or the temp dir when the office home is overridden — with the path posted to the transcript, so your o-to-open habit works on the file too. On a terminal without kitty graphics the text lane carries a dim screenshot: <path> row in place of the paint, and a failed shot — Chrome absent, navigation refused, timeout — stays text with one dim classified reason row, never a blank pane.

The external terminal-browser package has been removed. The installer no longer downloads it, and legacy opt-in variables cannot enable it. External links use your system browser. The built-in text and screenshot paths remain available.

URL policy

Localhost always. https anywhere. Plain http by flag.

The fetch posture is narrow about plain http and nothing else. file:// URLs and bare paths read off disk. Localhost — localhost, 127.0.0.1, ::1 — is always allowed on either scheme, and https:// opens any host by default. Only plain http:// beyond localhost asks for an explicit unlock: export THEFLOOR_BROWSER_ALLOW_HTTP=1 — read at use time, with no config schema and no brain.json key. A blocked fetch says so in a dim row and tells you the exact flag.

file:// · bare pathsread straight off disklocalhost / 127.0.0.1 / ::1always allowed, either schemehttps://any host by defaultTHEFLOOR_BROWSER_ALLOW_HTTP=1unlocks outbound http
Ceilings

What this doesn't do yet.

  • The text lane is text. No CSS layout, no JavaScript — pages arrive as readable rows. Sites that exist only as a script bundle have nothing to render.
  • Image bytes are never fetched. Images render as 🖼 <alt> chips; the viewer reads markup, not media.
  • HTML only. PDFs, images and other payloads land the dim unsupported content type row instead of a parse.
  • One key route in. The browser isn't part of the sidebar's tab cycle and has no digit key — ctrl+b on the middle pane is the only way in, q / esc the way back to the floor.
  • The premium lane is kitty/ghostty only. tmux and the iTerm2 family stay on the text lane — a protocol boundary, not a preference.