feat(desktop): one bar across the file viewer: back and forward, breadcrumbs, GitHub, a ⋯ of file actions and view settings, and the tree's toggle

Merged
DA
7d ago
+1259-284

Part 3 of Dean's file viewer polish; parts 1 (#2125) and 2 (#2131) are merged. #2140 (stacked on this) restyles the breadcrumb dropdowns to match the tree, and a follow-up makes Copy contents and Download read the file's raw text.

Summary

One bar across the whole file viewer, the tree's column included. The tree no longer has a bar of its own; its top row is the search field from part 1. Left to right:

[← →] [acme/relay › apps › web › server › plans-app.ts] … [GitHub] [⋯] [collapse]

  • Back / Forward walk the files this File tab showed.
    • They are drawn as the sidebar's own pair: icon-button, ArrowLeftIcon/ArrowRightIcon, a "Back"/"Forward" tooltip, and dimmed rather than hidden when a direction is empty, so nothing shifts.
    • The history lives on the tab's OpenFile (back/forward, use-side-panel.ts). Browsing to a file pushes the previous one and clears Forward; each step restores that file's lit lines.
    • If the target file already has a tab of its own, that tab is chosen instead, keeping one tab per file.
    • No chord is bound: ⌘[ / ⌘] stay the window's navigation.
  • Breadcrumbs: each segment is the button. Pressing it lists the folder it stands in, with itself marked, as VS Code and Xcode do; the repository crumb lists the root. Choosing a file opens it, ⌘ in a new tab. Choosing a folder reveals it in the tree, opening the tree if needed. The chevrons between segments are plain aria-hidden glyphs, not buttons.
  • GitHub: only the GitHub mark, as an icon-button with the tooltip Open on GitHub. It opens the file on the branch it was read from, its lit lines marked, and is dimmed until the file is read. The branch name chip is gone.
  • ⋯ (File actions) uses the window's standard ActionMenu, with an icon on each item:
    • Copy file name, Copy file contents, Download file, a separator, then Line numbers ✓ and Word wrap ✓.
    • Both copies report through the shared copy toast (useCopyToast).
    • The contents are read whole only when asked, one 500-line window at a time, so Copy and Download carry the whole file, not just the window drawn.
    • Contents and Download are dimmed over a file that was refused or is still loading.
    • ActionMenu gains checkable items (MenuAction.checked). They render as role="menuitemcheckbox" with aria-checked and a check at the item's end, and the menu's arrow keys reach them.
  • Download uses a new act, window.saveFile {name, text}; no save or export act existed before.
    • Main shows dialog.showSaveDialog over the asking panel, writes the file, and answers whether it was saved. A cancelled dialog shows no toast; a save shows File saved.
    • The act is refused from any window that is not a panel, and for a name with a folder in it.
    • window.copyText's bound is raised to the larger of a plan document and a file: the service reads files of up to 1 MB.
  • Line numbers and Word wrap are kept in renderer storage (luke.file-view, file-view-settings.ts, the same pattern as the sidebar's fold) and apply to every File tab at once. Wrap off scrolls the lines sideways together. Both default to on, which matches what the tab drew before.
  • Collapse sits at the far right, using the side panel's own toggle glyph (SidePanelIcon), unmirrored because the tree is docked on the right, as the panel is. It has the panel toggle's pattern: a Hide files / Show files tooltip and aria-expanded.
  • Copy contents and Download refuse a file with a line the service cut. The service carries each line cut to 400 characters (SHOWN_CODE_BOUNDS.MAX_LINE_CHARS). A file with any line at that bound is refused with a toast pointing to GitHub rather than copied or saved short. Lines are joined with \n. The follow-up PR replaces this with a raw read, one bounded call that returns the untrimmed text.
  • The old heading's range label ("Lines 8–12") is dropped, since Dean's layout has no slot for it; the lit lines stay highlighted in the file.

Bundle

renderer.js is 739,254 gz against a ceiling of 770,501. file-tree.js is unchanged by this PR (71,256 gz on the stack). No budget move.

Screenshots (headless Chrome, fixture data)

LightDark
The bar
⋯ menu

Verification

  • ./scripts/check.sh exits 0.
  • New tests, each failing without the change:
    • planning/file-view.test.ts, through FileView:
      • the bar's controls in order, with no second bar for the tree
      • a segment press lists its folder with itself marked; the chevrons are not buttons; choosing a file navigates and choosing a folder reveals it
      • Back and Forward walk the history and are dimmed at its ends
      • GitHub opens the file at the branch with its lit lines, and is dimmed over a refused file
      • the ⋯ copies the name and the whole 1,200-line text across three windows, downloads through window.saveFile, and dims the text actions over a refused file
      • Line numbers and Word wrap redraw every File tab and are kept in storage
    • planning/use-side-panel.test.ts: Back and Forward walk the tab's history with its lit lines; browsing clears Forward; a step to a file with its own tab chooses that tab.
    • Review fixes, each with a test:
      • the fallback snippet follows the view settings
      • a snippet reopening a tab keeps its history
      • a step back to a file whose tab another plan took moves that tab here
      • a crumb's stale listing opens nothing
      • a file with a cut line is not copied or saved
    • main/ipc/window-surface.test.ts: a panel's save is written where the dialog chose; a cancelled dialog answers not saved; a non-panel window, or a name with a folder, is refused.
  • 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. A Mac break that lands is caught at the release rehearsal on a tag (LUKE-159).

🤖 Generated with Claude Code