Browser Find-in-Page — Delivery Notes
⌘F did nothing useful on a browser tab: Pulsar’s
find-and-replacesearches aTextEditorbuffer, and a live web page has no buffer. This shipped a real browser find driven by Electron’s native<webview>.findInPage(), surfaced as an independent draggable pane item and styled from the business design tokens. The decision record is ADR-0009; this note is the delivery write-up — what shipped, the false starts, and the gotchas.
Status
Implemented and verified live. Branch feat/browser-find (in both tranquil-browser and tranquil-client); uncommitted at time of writing.
| Piece | State |
|---|---|
BrowserFindItem — draggable pane item, native <webview> find | ✅ |
⌘F routing (workspace keymap + abortKeyBinding, webview keyHandler) | ✅ |
| ⌘F menu-accelerator leak suppressed (interceptor + eager activation) | ✅ |
| ⌘G / ⇧⌘G next/previous; clickable search icon | ✅ |
| Design-token styling (both business themes) + terminal find palette themed | ✅ |
Palette entries: find, toggle-url-bar, go-back, go-forward (focus-independent) | ✅ |
| Stale-highlight clearing on tab switch (flicker-free on click) | ✅ |
The behavior wanted
“⌘F on a browser tab → find text on the page, like any browser.” Pulsar can’t do it: find-and-replace only searches a TextEditor, its extension hooks require a real editor, and it’s
third-party core we never modify. Electron’s <webview> element, though, exposes a native find on
the host side — findInPage(text, opts), stopFindInPage(action), and the found-in-page event —
which highlights in the live page with no guest injection or IPC. That’s the engine.
What shipped
A single owned pane item, tranquil-browser/lib/browser-find-item.js:
- An independent, draggable workspace item (
BrowserFindItem) — plain DOM viagetElement(),getDefaultLocation() → "right",getAllowedLocations()= all docks + center, a search icon, and input + counter + previous/next/close controls. Registered with an opener + singleton;showFind()opens it in the right dock (or focuses the existing one). Drag it anywhere. - Targets the active browser’s
<webview>—getCenter().getActivePaneItem()(with alastBrowserfallback), callingwv.findInPage(...)and listening forfound-in-pageto update the “N of M” counter. - Styled from the design tokens — geometry in
tranquil-browser/styles/tranquil-browser.less, colors in the business themes’tranquil-browser.less(@app-bg,@border-strong,@accent,@text-*). See the token gotcha below.
The false starts (and why they were wrong)
- A fixed
addBottomPanelstrip. First cut. It didn’t fit the theme, sat below the cmd+j dock rather than in it, and its icons didn’t render. Replaced by the pane item. - Opening Find as a sibling tab in the browser’s own pane. A pane shows one tab at a time, so
Find hid the browser — and hiding a
<webview>detaches it, which both blanked the page you were searching and threwThe WebView must be attached to the DOM…. Find must live in a separate pane (the dock), so the browser stays visible. - Reusing
find-and-replaceviagetEmbeddedTextEditor. It needs a realTextEditorbuffer, so it can’t search or highlight a live DOM. Dead end.
Gotchas worth remembering
URI scheme collision. The browser’s own URL opener claims any URI starting with tranquil-browser:, so atom.workspace.open("tranquil-browser://find") was turned into a blank
browser tab (a webview loading a bogus URL — the source of an empty “find” tab and a webview crash).
Fixed by using a distinct scheme: tranquil-find://find.
The ⌘F menu-accelerator leak. Electron menu accelerators fire even when a <webview> has focus,
so the “Find in Buffer” menu item still dispatched find-and-replace:show onto browser tabs — showing
the (inert) editor find bar on top of ours. Fixes:
- a workspace-level
tranquil-browser:findkeymap that beatsfind-and-replace’s.platform-darwinbinding and callsabortKeyBinding()for non-browser items (editor find still works); - the webview
keyHandlerhandles the guest-focused case (a focused page swallows ⌘F, forwarded over the existing key relay); - an
onDidDispatchinterceptor that hidesfind-and-replace’s panel on browser tabs; and - eagerly activating
find-and-replaceat startup (it’s lazily loaded viaactivationCommands), so the first ⌘F’s show + our hide land in the same tick — no flash.
Electron’s findNext contract is counter-intuitive. findInPage(text, { findNext: true }) selects/advances the active match and reports the count; findNext: false only highlights matches
without selecting one, so found-in-page never reports an ordinal and the counter stayed empty. We
pass findNext: true always (as the reference electron-find does) — Electron treats a changed
query as a new search and an unchanged query as “advance”, so one flag covers the first find and
stepping (⌘G / ⇧⌘G).
@ui-variables are default-Atom grays. The active UI theme (tranquil-business-*) doesn’t remap
Atom’s @ui-variables; it styles components directly with the design tokens in colors.less. So
styling the find pane (or the terminal find palette) with @ui-variables looked wrong. Colors live in
the business themes using the real tokens; the geometry stays in tranquil-browser.
Clearing highlights across display:none tabs. Pulsar hides an inactive pane item with display:none (pane-element.js hideItemView), and Electron refuses findInPage/stopFindInPage on a hidden webview — so a tab keeps its find highlight when you switch
away, and there’s no will-change-active-pane-item event to clear it before it hides. The only
moment the outgoing tab is still visible/clearable is the tab’s mousedown (fires before the pane
switches). So:
- a capture-phase
mousedownon the tab bar clears the current tab’s highlight pre-switch → tab-click switches are flicker-free; - an on-arrival clear (in
observeActivePaneItem) is the fallback for non-click switches (Ctrl+Tab) — those still show a one-frame flash, since there’s no pre-visible signal.
Also swept up
- ⌘G / ⇧⌘G bound (scoped to the find pane) to
tranquil-browser:find-next/:find-previous; the search icon is clickable and runs the find like Enter. - Command-palette visibility fix.
toggle-url-bar,go-back,go-forwardwere registered on narrow selectors (.tranquil-browser [webview]), so they only appeared in the palette while a browser view was focused. Moved toatom-workspacescope (targeting the center’s active browser) so they’re always listed. - Terminal find palette themed. The
terminalpackage paints its find bar with@tool-panel-background-color(default-Atom light gray → unreadable on the dark UI); repainted from the business palette in the themes’terminal.less(theme stylesheets outrank package ones, no!importantneeded for the background).
Files changed
| Repo / file | Change |
|---|---|
tranquil-browser/lib/browser-find-item.js | New — BrowserFindItem pane item + native find engine |
tranquil-browser/lib/tranquil-browser.js | Opener + singleton, showFind, ⌘F command + abortKeyBinding, onDidDispatch interceptor, eager-activate find-and-replace, workspace-scoped toggle-url-bar/go-back/go-forward |
tranquil-browser/lib/tranquil-browser-view.js | keyHandler case "f"; removed dead #find input and the per-view nav-command registrations |
tranquil-browser/keymaps/tranquil-browser.json | ⌘F/Ctrl-F → find; ⌘G/⇧⌘G → next/previous (scoped to the find pane) |
tranquil-browser/styles/tranquil-browser.less | Find-pane layout/geometry; removed the old bottom-bar + dead .find-bc styles |
tranquil-browser/lib/browser-find-bar.js, resources/electron-find/, on-find IPC | Deleted — the old bottom-panel bar and the dormant/broken guest find widget |
| `tranquil-business-dark | light/styles/tranquil-browser.less` |
| `tranquil-business-dark | light/styles/terminal.less` |
How it was verified
Live in a running window: ⌘F opens the Find pane in the right dock (opening the dock if closed);
type + Enter highlights all matches in the page and shows “1 of N”; ⌘G / ⇧⌘G step through; the search
icon runs the find; the pane drags to other panes/docks; the palette lists Tranquil Browser: Find (and the nav commands regardless of focus); the first ⌘F after a reload no longer flashes the editor
find bar; the terminal find palette reads correctly in tranquil-business-dark and -light; and
switching browser tabs by click clears the previous tab’s highlight with no flicker.