feat(desktop): ⌘F finds within the tab holding the window's focus

Merged
DA
7d ago
+1374-36

Dean asked for the standard ⌘F find inside the relevant tabs: plans, agents and subagents, Transcript, Code, Work, and the file viewer.

Summary

  • ⌘F opens a find bar at the top right of the tab holding the window's focus. That is the plan's column (the plan's document, an agent's tab, the draft) or the side panel (Transcript, Code, Work, a subagent, a File tab). It uses the same focus rule ⌘T and ⌘W already follow: the column the pointer or the focus last went into. The Board offers nothing, so Excalidraw keeps its own find. Settings unmounts the plan, so ⌘F there still searches settings. The file tree keeps ⌘F while it is focused; its handler now stops the press so the tab's bar doesn't open over it as well.
  • The bar works like Chrome's, VS Code's, and Slack's. It has a field in the app's search well, a "3 of 12" / "No results" count, ↑ and ↓ icon buttons, and ×. Return and ⇧Return step through matches, and so do ⌘G and ⇧⌘G from anywhere in the column. Escape closes the bar and returns focus to where it was. Text selected in the tab pre-fills the field, and a second ⌘F selects the field's text. The last query is kept, so ⌘F reopens on it. Matching is case-insensitive with no toggles. Choosing another tab or another plan closes the bar, and coming back to the first doesn't reopen it. Tabs are compared with the tab model's own sameTab, so a file named code is not the Code tab. Over a File tab the bar sits just under the file viewer's own bar (#2139), not over its buttons. The bar opened over one column stays open, still tinted, when focus moves to the other column, as an unfocused editor's does. It uses the menu's ground, edge, and menu-enter entrance, and the new --find-* tokens in both appearances.
  • Matches are found in the DOM and painted with the CSS Custom Highlight API (find-matches.ts). Every match gets a tint and the current one a stronger fill. A new search starts at the first match not scrolled above the view of the scroller it sits in, so it starts where you are reading. The current match is scrolled into the middle of the tab's own scroller, not with scrollIntoView (per the pane-motion rule in renderer/CLAUDE.md). Matching runs across a line's inline spans, so a word split over a code line's coloured tokens is still one match, but never from one line or paragraph into the next. A code line's number in the gutter is never matched (data-find-skip, which the bar itself also carries). Nothing is written to the page, so React's tree and session replay's masking are untouched. A MutationObserver re-runs the search when the tab's text changes, as a streaming transcript's does, without moving the current match.
  • Content that isn't in the DOM is still found.
    • Folds (ai-elements/fold.tsx: tool calls' inputs and outputs, reasoning, a finished turn's folded lead, runs of calls). While a bar is open, a closed fold draws its body inside its still-closed <details>, so the platform keeps it hidden and the find still covers it. Stepping to a match opens the folds holding it, the way Chromium's own find opens a <details>. The fold now follows its element's own toggle, so its state stays in step.
    • The File tab's 500-line windows. While a bar is open, the tab reads the remaining windows one after another until the whole file is drawn. withWindow now leaves the run alone when a window it already holds is read twice, so a find read and a scroll read of the same window can't reset what's drawn.
    • Out of scope: the Work tab's older pages that haven't been read yet ("Show earlier work"), and lines the service itself left out ("Earlier lines are not shown"). Find covers what the tab has read, as a browser's find doesn't page an infinite scroll.
  • Shortcuts and menu. FIND is now labelled "Find" (⌘F), with new FIND_NEXT (⌘G) and FIND_PREVIOUS (⇧⌘G) beside it in the shortcut table, so Settings › Shortcuts and the command palette list them. The Edit menu is now written out item by item, keeping the system's items (Undo … Select All, Speech), plus a Find submenu: Find… ⌘F, Find Next ⌘G, Find Previous ⇧⌘G. As every command item does, these only hand the command to the window on a click and leave the keys to the window's keymap.

Rejected alternative

Electron's webContents.findInPage searches the whole window across both panes and can't be scoped to one tab. It also draws its own highlights, which the column's bar couldn't count or step per tab.

Screenshots (headless Chrome, the real DesktopPlans over fixture data)

LightDark
Plan document
Transcript (side panel focused)
File viewer (matches across code tokens)

Verification

  • ./scripts/check.sh exits 0.
  • New tests, each failing without the change:
    • find-in-tab.test.ts (the public FindScope with the window's keymap, CSS.highlights stubbed at the boundary through #testing/find-highlights):
      • ⌘F opens the bar, pre-filled from the selection, and a second ⌘F selects the field's text.
      • Typing counts and tints matches case-insensitively, across inline spans, never across lines.
      • Return steps and wraps, ⇧Return steps back, and ⌘G / ⇧⌘G and the buttons step too.
      • Escape closes the bar, clears the tints, and restores focus without reaching the window's Escape, and ⌘F reopens on the last query.
      • A folded match is found and its fold opened.
      • Streamed text doesn't move the current match.
      • An inactive scope leaves ⌘F unclaimed.
      • Another tab or plan closes the bar, and returning to the first doesn't reopen it.
      • A file named like a fixed tab counts as a different tab.
      • A new search starts at the first match in view of the tab's scroller.
    • desktop-plans.test.ts: ⌘F follows the focused column (document, then the side panel's tab) and is unclaimed over the Board.
    • file-tab.test.ts: an open bar reads the rest of the file and finds a match past the drawn window, never matching a gutter number. A window read twice leaves the run as it was.
    • file-view.test.ts: the tree's ⌘F still takes the caret to its field and the tab's bar stays shut.
    • app-menu.test.ts: Edit keeps the system items and holds the Find submenu with its accelerators.
    • Settings' existing ⌘F test still passes unchanged.
  • CI could not verify this. It is a desktop UI change, and ./scripts/verify.sh was not run: this was written on a Linux VM. LUKE-159 is the manual Mac pass, and a Mac break that lands is caught at the release rehearsal on a tag.

🤖 Generated with Claude Code