The web, as text, one tab over.
The left 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 upgrades to rendered headless screenshots; an older embedded zenbu terminal-browser lane survives behind an explicit opt-in flag; everywhere else, text is the feature, not the fallback.
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.
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 left 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 seven tabs — chat · terminal · agents · board · mail · activity · git — 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.
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 left 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.
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.
Rendered screenshots by default; the embedded browser is opt-in.
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 older EMBEDDED lane — zenbu's terminal-browser, a real Chromium app living inside the pane — is retained but off by default. Opt in explicitly: install the binary (re-run the office installer with --with-terminal-browser) and export THEBORINGOFFICE_ZENBU_LANE=1. Then the tab embeds the live page at the pane's exact pixel size under a top strip ▸ zenbu terminal-browser · <url> and a zenbu badge.
The fallback is honest both ways: a non-zero or instant (<300ms) exit drops back to the text viewer with the URL state kept and a dim zenbu exited (<code>) — falling back to text mode note; a clean exit just returns to text quietly. Two kill-switches force the text lane — THEBORINGOFFICE_TERMINAL_BROWSER_OFF=1 or THEBORINGOFFICE_NO_TERMINAL_BROWSER=1 — and they win over the opt-in flag.
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 THEBORINGOFFICE_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.
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 typerow 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+bon the left pane is the only way in,q/escthe 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.
- The embedded lane is off by default. The zenbu
terminal-browserembed is opt-in (THEBORINGOFFICE_ZENBU_LANE=1+ the binary onPATH); the default premium path is headless screenshots.
