feat(input): unify TUI and web shortcuts, add project cycling and tab moves - #33
Merged
Merged
Conversation
Stepping between project tabs had no chord of its own: the F-keys jump to a tab by number, which does not reach past ten and says nothing about where the neighbours are. The step is sent as a direction, not an index. The handler holds one project, so the wrap needs the tab count and which tab is in front, and both are only known where the tab list is. Switching stays a request: the tab moves when the daemon rebroadcasts the set.
The browser had no way to reach a neighbouring project from the keyboard. The TUI answers with bare F-keys, which the browser and the OS have already claimed, so the web takes the same chord the TUI now uses for stepping instead of copying its key table. The switch goes through the existing selectRepo, so pane clear, active repo persistence and the per-project view restore are the same as a click. The listener captures on document: xterm reads keys from its own textarea below it, so stopping there is what keeps a claimed chord from reaching the PTY, while an unclaimed one is left completely untouched.
Copying the TUI key table into the browser would make the two drift, and the physical keys do not survive the trip: F-keys belong to the browser, Ctrl+F to Find, and Ctrl+Shift+Arrow to word selection inside a field. So the registry names user actions instead of keys, and records for each whether the browser keeps the TUI's meaning, reinterprets it, or has none. Keyboard, help and buttons can then read one table. The classifier and the leader reducer are pure and DOM-free: IME composition, key repeat, exact modifiers and every way a leader must disarm are decided where they can be tested without a browser.
Which project is in front is tracked apart from the order, but while nothing has been focused it falls back to whichever repository is served first — so reordering handed the front tab to a different project. A fresh session started with --repo and a single move was enough: the tab moved and the front moved with the slot instead of with the repository. The fallback is now resolved before the slots move and pinned to what it named, conditionally, so a focus arriving in between still wins. The web viewer's drag-and-drop reorder went through the same operation and had the same defect.
Tab order was reachable only from the browser's drag-and-drop. The TUI could switch among tabs but not rearrange them, and the daemon already owned the whole contract. The keys sit behind the leader, never bare: unmodified characters belong to the PTY, and taking the brackets there would break terminal input. The whole new order is asked for and nothing is rearranged locally, as switching does — the tabs move when the daemon rebroadcasts the set. Neither end wraps; wrapping is what the stepping chord means, and a held key would otherwise shuffle the strip.
The registry and the classifier had no caller. This wires them up and folds the project-cycling chord in, so there is exactly one listener: two of them cannot agree on whether a key was claimed, and the one that loses either eats a keystroke the pane needed or leaks a command into the shell as an escape sequence. Every action binds to the handler its button already calls. Panel commands live below the page, so the panel registers them on a bus, which is also what makes availability answerable — the keyboard and the help sheet read the same answer. The leader clears itself on focus loss, on a dialog, on a project switch and on a reconnect, so the key after it is never swallowed. Pressing it twice sends the literal chord to the pane, as the TUI does.
A shortcut nobody can find is not a feature, and the default leader is the browser's Find key — taking it silently would be worse than not taking it at all. The sheet is generated from the registry, so a new action cannot be added without appearing in it, and it names what the browser reinterprets and what it has no answer for. Every row is a button that runs its action: that is the non-keyboard path for focusing the list and the content pane, which have no other control. Rows unavailable on the current screen say so, from the same answer the keyboard reads. aria-keyshortcuts is emitted only for real chords. A space in it means alternative, so writing a leader sequence there would tell assistive technology that the follow-up key works alone, which it does not; the sequence is in the title and the sheet instead.
The web viewer binds the same commands to different keys, which is a thing a reader has to be told rather than left to discover: the keys the TUI uses are free because the terminal hands them over, and a browser has already spent most of them.
The pin was applied after the catalog was reordered. The watcher reads the session on its own tick, so one landing between the two statements broadcast the new order while the fallback still named the old first tab — the front-tab jump the pinning exists to prevent, adopted by every client and corrected a tick later. Also drops a panic path in the client's order arithmetic: both ends are now bounded by the order just built, so the backward step cannot address past it. And a request that leaves a tab out is explained by what the session actually does with it — omitted repositories are appended, not dropped, as the comments claimed. The new interface had no test that watched a request leave the client at all, so the two added here drive the link against a real daemon and read the tab strip that comes back.
session_link.rs was six lines under the cap, so the next change to it would have breached one. The four free functions there are pure or take only the workspace and touch nothing on the link, which is what lets the tests drive them without a daemon — the split says so.
Both were implemented without being written down: the architecture index still described the leader as the only place app commands live, and the web surface said nothing about where a keystroke is decided or which side owns the leader preference.
Choosing a list from the keyboard reached a weaker setter than the tab buttons do, so the previous list's diff stayed in the content pane, an in-flight pane request could land after the switch, and leaving the log did not reset its snapshot. The claim that a shortcut runs the control the button runs was not true here, and one command had two implementations — the order now lives in one pure function that both callers go through. The leader also stayed armed when the keyboard was switched off. The login screen renders without unmounting the layer, so a session that expired mid-sequence swallowed the first key typed after signing back in.
The pane-swap row was a button that closed the sheet and did nothing: the command arms a second step, and a click has no next key to offer. The registry now records that one action as keyboard-only and the row renders as text, so availability and what a click does agree again. The flag is deliberately narrow — arming a second step is the only reason a row is not a button, and an ordinary action whose row does nothing is a missing handler, which is a bug to fix rather than a state to describe.
The panel's call sites had been compressed onto single lines to keep the file under the cap, against the convention in the rest of it. Restoring the formatting put it over, so the socket and view wiring moves out — the seam the file was already asking for. The move keeps hook call order identical, which matters here because effects run in that order.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Gives the same commands the same meaning in the TUI and the browser, and adds two ways to move between and rearrange projects.
TUI
Ctrl+Shift+Left/Ctrl+Shift+Rightstep to the previous/next project, wrapping. The F-keys jump to a tab by number, which does not reach past ten and says nothing about where the neighbours are.<leader> [/<leader> ]move the active tab one slot, without wrapping — wrapping is what the stepping chord means, and a held key would otherwise shuffle the strip. Bare brackets still belong to the PTY.Web viewer
Ctrl+Fby default) with the same follow-up keys, plus the project chords.fbecomes panel maximize and terminal zoom rather than F11, and redraw, detach and the bareF1–F10tab selection are deliberately unbound.Why it is shaped this way
The physical keys do not survive the trip to a browser: F-keys belong to the browser,
Ctrl+Fto Find,Ctrl+Shift+Arrowto word selection inside a field. Copying the TUI table would have made the two drift and claimed keys the page never receives.There is exactly one keyboard decision point, a capture-phase listener on
document. Two listeners cannot agree on whether a key was claimed, and the one that loses either eats a keystroke the pane needed or leaks a command into the shell as an escape sequence. Because xterm reads keys from its own textarea belowdocument, stopping there is what keeps a claimed chord from reaching the PTY, while an unclaimed one is left completely untouched.Every action binds to the handler its button already calls. Panel commands live below the page, so the panel registers them on a bus — which is also what makes availability answerable from one place, so the keyboard and the sheet agree about what works on the current screen.
Switching and reordering stay requests. Nothing is rearranged locally; the tabs move when the daemon rebroadcasts the set.
Fixes found along the way
--repoand a single move was enough. The web viewer's drag-and-drop reorder went through the same operation and had the same defect.<prefix> l/<prefix> breached a setter that left the previous list's diff in the content pane and did not reset the log snapshot — one command with two implementations, now one pure function both callers go through.ClientMessage::ReorderReposhad no test coverage at all, and nothing watched a request leave the client; both gaps are closed.Notes for review
aria-keyshortcutsis emitted only for real chords. A space in it means alternative, so writing a leader sequence there would tell assistive technology that the follow-up key works alone. The sequence lives in thetitleand the sheet instead.viewer.json— it is not shared session state.Verification
cargo fmt --all --check,cargo build,cargo test(1760 passed),cargo clippy --all-targets --all-features -- -D warnings,npm --prefix viewer-ui test(659 passed),npm --prefix viewer-ui run build, anddocker compose run --rm unix-gate.viewer-ui/distwas rebuilt and a repeat build reproduces it byte for byte.