Browsing files without a mouse

Agent Manager · issue #125 (review item U06) · the Files pane, keyboard navigation and access to the operations that already existed · 9 Sep 2026

What was there before

Measured on ef08e84, by tabbing through a mounted Files pane and reading back what had the focus:

The key contract

keywhat it does
↑ ↓previous / next visible row — a collapsed folder's children are not destinations
→collapsed folder: expand it · open folder: step into its first child · file: nothing
←open folder: collapse it · otherwise: the folder this row is in
Home Endfirst / last visible row
Enterfile: preview it · folder: open it as the listing root. While a move is armed, a folder is the destination instead — the same as clicking one.
Esccancel an armed move or an open delete confirmation, and hand the row back
Tabfrom the focused row: that row's own actions, then out of the pane

Moving the focus is not an action. Arrowing over rows opens nothing, writes nothing, and changes neither the folder new files land in nor the destination of a pending move. Enter is the only key that acts.

What it looks like

The Files listing with a folder row focused: an accent ring around the whole row and its three action buttons visible at the right
A row reached with ↓ and expanded with →. The focus ring is the app-wide one ([tabindex]:focus-visible); the row's actions are revealed by focus the same way the sidebar's row actions are.
The same row with its Rename button focused, showing a focus ring on the button
One Tab further: the row's Rename button. Only the focused row's actions are tab stops.
The same focused action in the dark theme
The same in the dark theme.

Focus that survives what the listing does next

Focus is kept by session and path, never by row index — a sort or a refresh reorders every row, and an index would silently mean a different file. It is restored to the row it came from when a preview closes, a rename or a move commits, or a confirmation is cancelled. When the row itself is gone, the focus goes to the next thing in the same folder, else the previous one, else the folder itself.

A late answer never takes the keyboard. A directory read that lands while the operator is in another pane repairs which row is the tab stop and stops there. Moving the browser's focus needs a user action in this pane, and the request expires.

The delete confirmation opens on Cancel, not on Delete: the key that opened it is still on its way up, and a Space release landing on a freshly focused Delete would delete the file with one press.

Four things found on the way

How it was tested

Two suites, both discovered by the normal runner:

The accessibility tree is read from the browser over CDP rather than read back off our own attributes: one tree with a name, eight treeitems named after their files, children announced at level 2, folders carrying expanded and files not.

Thirteen deliberate mutations — remove the key handler, the roles, the roving tabindex, the button guard, the IME guard, the confirmation's focus, the focus restore, the scroll-into-view, the focus-within CSS, the upload's keyboard, and three of the focus-repair rules — each break the suite, and each on the check that names the rule.

npm test in web/: 25 suites passed. npm run build (typecheck + production build): clean. No server change, so no server suite applies.

Chromium only — no screen reader is installed in this container, so the announcements above are what the browser's accessibility tree contains, not a NVDA/VoiceOver transcript.