Skip to content

feat(sidebar): keep the cursor when a row disappears [6]#153

Draft
AbysmalBiscuit wants to merge 15 commits into
mathix420:masterfrom
AbysmalBiscuit:feat/sidebar-focus-preservation
Draft

feat(sidebar): keep the cursor when a row disappears [6]#153
AbysmalBiscuit wants to merge 15 commits into
mathix420:masterfrom
AbysmalBiscuit:feat/sidebar-focus-preservation

Conversation

@AbysmalBiscuit

Copy link
Copy Markdown
Contributor

DO NOT MERGE

When a projects-sidebar row holding the cursor stops being rendered, the cursor no longer drops to row 0. A row hidden by a filter or a collapse climbs to its nearest visible ancestor and returns when the filter widens; a deleted row slides to a sibling, bounded by its parent.

The mechanism is an observing reconciler: sidebar_focus.rs diffs a TreeSnapshot of the sidebar between frames and repairs the cursor from the diff, rather than every mutation site having to remember to fix focus itself. That is why after_filter_changed and the inline close-navigation dispatch are gone — deletion and filtering now report what happened and let the reconciler decide.

Config

[ui]
sidebar_focus = "preserve"  # default

"follow" additionally moves the terminal to a delete landing that has a live session.

Behavior change worth a decision

preserve is the default, and there is no value that restores the old drop-to-first-row behavior. If you would rather this ship opt-in, say so and I will add a third value and flip the default — the reconciler is already gated on the config, so it is a small change.

Stacked

This branch is stacked; against master it carries three prerequisite commits that are their own PRs:

Commits PR
bca668e7, 154e43aa — sidebar search confirm/cancel #138
66236260 — keep worktrees when unreachable #145 (cherry-pick)

Only the twelve commits 9c6ddbda..3c6de3bc are new here. Reviewing per-commit, or after those two land, is much easier than reading the combined diff.

Testing

cargo test -p alacritree: 565 passing. New coverage includes snapshot/model agreement, climb and slide behavior, anchor return, and steady_state.rs, which gates the unchanged-frame cost so the per-frame snapshot cannot silently start allocating.

Not yet done: the manual GUI passes for the deferred-navigation paths. Worth doing before this leaves draft.

Known edge

An anchor can be retired as a false positive when a structural removal changes the active session while you are mid-filter with a climbed cursor — you lose the "return to the filtered-out row" convenience for that one interaction. No cursor or terminal corruption; the visible cursor still repairs correctly.

AbysmalBiscuit and others added 15 commits July 25, 2026 10:24
Search-mode Enter/Esc were hardcoded in panel_filter and could not be
rebound, and Enter never scrolled the activated row into view.

Add three search-scoped actions — SidebarSearchConfirm (Enter),
SidebarSearchCancel (Esc), and SidebarSearchCancelToTerminal
(Shift+Esc) — dispatched through the keyboard binding system from the
focused panel's nav pass. handle_shortcuts drops search-scoped actions
so the terminal's own Enter/Esc still reach the PTY. Confirm scrolls the
acted-on row into view via a focus-independent pending target, resolving
a now-hidden child to its owning project header. Cancel returns the
cursor to the seed row and preserves the s/a toggle filters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Confirming a fuzzy search ran the row's full activation: a project
header toggled its expansion, and a worktree or session switched
workspace and moved focus to the terminal. Search is how you find a
row, so acting on it in the same keystroke is surprising — and for a
header destructive, collapsing the project you had just gone looking
for.

Confirm now only leaves search, puts the cursor on the highlighted row
and scrolls it in, keeping focus in the sidebar. Activation stays a
second, explicit Enter in browsing mode. That makes confirm the twin of
cancel, so both share one landing helper per panel, and the git panel
follows the same rule instead of opening a diff.

Search lists matched worktrees and sessions whatever their project's
expanded flag says, so a child under a collapsed project vanished the
moment the query cleared. Confirm expands that project first —
expand-only, and never for a header, which is what keeps selecting a
project from behaving as a toggle.

Dropping activation also drops the reason for the focus-independent
pending scroll target, which existed only because activation focused
the terminal and nulled the paint-time cursor. The ordinary cursor-move
scroll suffices once its moved flag is forced rather than keyed on
cursor identity: restoring the unfiltered list can move the same row
far off-screen. That also fixes git cancel, which could leave a
surviving row unscrolled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A failed wsl.exe round trip returned Project::placeholder, the same
single pseudo-worktree a non-git folder gets, so refresh and
poll_project_refreshes copied it over the real worktree list. One
transient WSL outage was indistinguishable from "every worktree in this
project was deleted", and the sidebar collapsed to a single row until a
later refresh happened to succeed.

Discovery now returns Discovered, pairing the project with whether the
backend could vouch for the answer, and callers adopt through
Project::apply, which drops a non-authoritative result. A distro that
answers "not a repository" is still authoritative; only a round trip
that never landed, or came back without even a main checkout, is not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assembles the sidebar_focus arena from live app state: a free
build_sidebar_snapshot() that lockstep-merges live (workspace, id)
session pairs against the projected rows, plus AlacritreeApp helpers
(session_pairs, session_inputs, sidebar_snapshot) that feed it.

Model membership comes from live sessions, never from the listed-rows
projection, so a workspace's last session never reads as deleted just
because it fell below the sidebar's listing threshold, and a session
whose project was removed stays in the model as a detached node
instead of vanishing.

Adds the sidebar_rows_cache field the next task wires into paint.
The helper is called only from the snapshot test module, so a non-test
build sees it as dead code. Gating it #[cfg(test)] keeps it honestly
test-only and clears the warning.
@AbysmalBiscuit
AbysmalBiscuit force-pushed the feat/sidebar-focus-preservation branch from 3c6de3b to ae881c8 Compare July 25, 2026 08:32
@AbysmalBiscuit AbysmalBiscuit changed the title feat(sidebar): keep the cursor when a row disappears feat(sidebar): keep the cursor when a row disappears [6] Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant