✨ feat(tui): sidebar stashes mode toggled by s (#34)#166
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new “stashes” sidebar mode in the TUI, toggled via the rebindable keymap (s by default), so users can view git stash list for the selected worktree alongside the existing commits-focused sidebar behavior.
Changes:
- Introduces
SidebarMode(Commits/Stashes) with mode-cycling, cache keying by(path, mode), and status updates when toggled. - Adds
worktree::git_stash_list()+StashEntryparsing helper and integrates stash rendering into the sidebar UI. - Updates keymap/help overlay wiring and expands test coverage across worktree integration + TUI state/dispatch.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/worktree.rs |
Adds stash list parsing helper (git_stash_list) and StashEntry model. |
src/tui/state/sidebar.rs |
Adds SidebarMode, mode toggle logic, and updates sidebar cache key shape. |
src/tui/ui.rs |
Updates sidebar rendering to be mode-aware and adds stash-mode rendering. |
src/tui/app.rs |
Wires an app-level cycle_sidebar_mode() orchestrator + status message. |
src/tui/mod.rs |
Dispatches the new keymap action ToggleSidebarMode to the app handler. |
src/tui/keymap.rs |
Adds toggle_sidebar_mode action slug and default s binding. |
tests/worktree_integration.rs |
Adds integration tests for empty/parsing/limit behavior of stash listing. |
tests/tui_state_sidebar_tests.rs |
Adds sidebar mode default + cycling + cache invalidation tests. |
tests/tui_chord_tests.rs |
Adds dispatch and help-overlay coverage for the new action/binding. |
tests/tui_app_tests.rs |
Updates tests for new sidebar cache key and updated build_sidebar_sections signature. |
CHANGELOG.md |
Documents the new sidebar mode + keybinding and cache behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+642
to
+648
| /// Shell out to `git status --short` inside `path` and return raw stdout. | ||
| /// Used by the TUI sidebar to preview the working-tree state. | ||
| /// One row of `git stash list` (issue #34). Surfaced by the sidebar | ||
| /// in stashes mode. Kept deliberately minimal — `ref_name` so the user | ||
| /// can copy `stash@{N}` to the status bar, `subject` so they can tell | ||
| /// which stash is which. Per-file diff numbers (`+/-`) live in a | ||
| /// follow-up — the v1 contract is just "name + subject". |
Comment on lines
+674
to
+678
| let output = Command::new("git") | ||
| .arg("-C") | ||
| .arg(path) | ||
| .args(["stash", "list", "--pretty=format:%gd\x1f%s"]) | ||
| .output() |
Comment on lines
+462
to
+466
| // Stashes view (issue #34). `working_tree` is left empty because | ||
| // the stashed lines already carry the per-stash file summary; a | ||
| // separate `git status` block would only duplicate the user's | ||
| // current dirty state which has nothing to do with the stash | ||
| // contents they're auditing. |
Comment on lines
+462
to
+471
| // Stashes view (issue #34). `working_tree` is left empty because | ||
| // the stashed lines already carry the per-stash file summary; a | ||
| // separate `git status` block would only duplicate the user's | ||
| // current dirty state which has nothing to do with the stash | ||
| // contents they're auditing. | ||
| SidebarMode::Stashes => stash_lines(w, STASHES_DISPLAY_LIMIT), | ||
| }; | ||
| let working_tree = match mode { | ||
| SidebarMode::Commits => working_tree_lines(w), | ||
| SidebarMode::Stashes => Vec::new(), |
Comment on lines
+375
to
+381
| fn render_section( | ||
| f: &mut Frame, | ||
| area: Rect, | ||
| title: &'static str, | ||
| // Title is `Into<String>` so callers can pass either a static | ||
| // literal (`" Worktree "`) or a runtime-formatted label | ||
| // (` Recent Commits — commits ` after the issue #34 mode toggle). | ||
| title: impl Into<String>, |
Comment on lines
+139
to
+141
| assert!( | ||
| row.contains('s'), | ||
| "expected the default `s` binding to appear, got: {row}" |
9 tasks
Cycle the Details panel between two preview modes:
- `commits` (default, pre-existing) — `git log --oneline -n 10` +
`git status --short`. Unchanged behaviour.
- `stashes` — `git stash list` rendered one row per stash (yellow
`stash@{N}` ref + subject), capped at `STASHES_DISPLAY_LIMIT`
(currently 10). Working-tree section is hidden in this mode
because the stashed diff and the current dirty state answer
different questions.
The toggle is wired through the rebindable keymap as
`Action::ToggleSidebarMode` (`s` by default), so users who want it
on another key can edit `[tui.keys]`:
[tui.keys]
toggle_sidebar_mode = ["Ctrl+s"]
`SidebarState` gains `mode: SidebarMode` and `cycle_mode()` which
flips the mode, resets the scroll to 0 (the new content has its own
length), and invalidates the cache. The cache key changes from
`PathBuf` to `(PathBuf, SidebarMode)` so toggling re-shells the
right git command rather than serving stale content from the
previous mode.
New `worktree::git_stash_list(path, limit) -> Vec<StashEntry>`
helper. Uses `%gd\x1f%s` so subjects with spaces / colons round-trip
safely; empty stash list is `Ok(Vec::new())` (not an error) so the
sidebar can distinguish "(no stashes)" from a real failure.
The bottom panel hint adapts to the active mode: the historical
`i of N` counter in commits mode, `Enter: copy stash@{N} to status`
in stashes mode.
Tests:
- `tests/tui_state_sidebar_tests.rs` — default mode, cycle,
scroll reset on cycle, cache keyed by (path, mode).
- `tests/worktree_integration.rs` — `git_stash_list` happy path
(empty / two stashes / limit).
- `tests/tui_chord_tests.rs::s_dispatches_toggle_sidebar_mode` —
end-to-end keymap dispatch.
- `tests/tui_chord_tests.rs::help_overlay_lists_toggle_sidebar_mode`
— help overlay surfaces the new row with its default binding.
Stacked on feat/#87-tui-keymap; the new `Action::ToggleSidebarMode`
variant lands cleanly on top of the keymap macro introduced there.
Refs #34
Six nits from Copilot's review, addressed together because they
all touch the stashes mode surface:
1. **Misplaced rustdoc** (src/worktree.rs:642): inserting
`StashEntry` between `git_status_short` and its rustdoc
re-attached the docs to `StashEntry`, leaving `git_status_short`
undocumented. Restored the rustdoc above its function.
2. **Full stash output read before clamp** (src/worktree.rs:678):
`git_stash_list` applied `limit` client-side after stdout was
fully read, so a repo with hundreds of stashes still produced
the full list just to be truncated. Pass `-n <limit>` (a
`git log` flag `stash list` forwards through) so git itself
stops at the cap.
3. **Stale comment about per-stash file summary** (src/tui/ui.rs:466):
the comment claimed "the stashed lines already carry the
per-stash file summary", but `stash_lines` only renders
`<ref> <subject>`. Rewrote to explain why `working_tree` is
empty (user's dirty state ≠ stashed contents) and flag the
per-stash `+/-` summary as follow-up.
4. **Empty Working Tree block in stashes mode** (src/tui/ui.rs:471):
`working_tree = Vec::new()` still produced a fixed 2-line
bordered section with no content, leaving a void in the
sidebar. Collapsed the constraint to 0 and skipped
`render_section` when the section is empty.
5. **Per-frame title alloc** (src/tui/ui.rs:381): `impl Into<String>`
forced every static-literal title (` Worktree ` / ` Issue / PR `
/ ` Working Tree `) through `String::from(&str)` on every render
frame. Changed to `impl Into<Line<'static>>` so static slices
pass through to ratatui zero-copy while dynamic `String` titles
still move in.
6. **Ineffective `s` binding assertion** (tests/tui_chord_tests.rs:141):
`row.contains('s')` passed trivially because the description
carries "stashes" / "sidebar". Tightened to `row.starts_with(" s ")`
so the test fails on a binding-column regression rather than
passing because of the label.
Refs PR #166 review comments by Copilot.
ad7517d to
a4fda8f
Compare
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.
Description
Implements issue #34 — sidebar gains a
stashesmode toggled bysthat shows
git stash listfor the selected worktree, alongside theexisting
commitsmode (git log --oneline+git status --short).Stacked on #87 (feat/#87-tui-keymap, PR #165) because the
sbinding ships through the rebindable keymap as a new
Action::ToggleSidebarMode. The PR will need a rebase / re-targetto
devonce #165 merges; the diff itself doesn't touch any filethat #165 doesn't already touch, so the rebase is mechanical.
Closes #34
Type of change
Changes
SidebarMode { Commits, Stashes }enum onSidebarStatewithcycle_mode(). Default isCommitsso pre-[Feature]: Sidebar stashes view (toggle with 's') #34 behaviour is preserved verbatim until the user pressess.PathBufto(PathBuf, SidebarMode)so toggling re-shells the right git command rather than serving stale content for the other mode.worktree::StashEntry { ref_name, subject }+worktree::git_stash_list(path, limit)helper. Uses%gd\x1f%s(ASCII unit separator) so subjects containing colons / spaces round-trip cleanly.build_sidebar_sections) now takes aSidebarModeand dispatches to eitherrecent_commits_linesor the newstash_lines. Title shows the mode (Recent Commits — commits/Stashes — stashes); bottom hint switches toEnter: copy stash@{N} to statusin stashes mode.Action::ToggleSidebarMode(slugtoggle_sidebar_mode), default bindings, wired through the View::List dispatcher.?) gets a new rows cycle sidebar mode (commits / stashes)— drives off the resolved keymap like every other row.Tests
cargo testpasses locallycargo fmt --checkpassescargo clippy -- -D warningspassestests/New tests:
tests/tui_state_sidebar_tests.rs::default_mode_is_commits— pre-[Feature]: Sidebar stashes view (toggle with 's') #34 contract preserved.tests/tui_state_sidebar_tests.rs::cycle_mode_flips_commits_to_stashes_and_back— toggle invariant.tests/tui_state_sidebar_tests.rs::cycle_mode_resets_scroll— fresh content gets fresh viewport.tests/tui_state_sidebar_tests.rs::cache_is_keyed_by_path_and_mode— toggling invalidates the cache.tests/worktree_integration.rs::git_stash_list_empty_returns_empty_vec— fresh repo edge case.tests/worktree_integration.rs::git_stash_list_parses_canonical_output— 2-stash LIFO order + subject preservation.tests/worktree_integration.rs::git_stash_list_respects_limit— preview cap honoured.tests/tui_chord_tests.rs::s_dispatches_toggle_sidebar_mode— end-to-end keymap dispatch.tests/tui_chord_tests.rs::help_overlay_lists_toggle_sidebar_mode— help row surfaces with default binding.Existing
tui_state_sidebar_tests,tui_app_tests, andworktree_integrationupdated for the new cache key shape +build_sidebar_sectionssignature; all 17 sidebar-state + 173 tui-app + 38 worktree-integration tests stay green.Screenshots / TUI captures
Checklist
<type>/#<issue>-<description>## [Unreleased]unwrap()on user-facing pathsprintln!in TUI render codeNotes for reviewers
feat/#87-tui-keymap, notdev. Once ✨ feat(tui): configurable keymap with chord support (#87) #165 merges, I'll retarget this PR todev(the diff is purely additive on top of [Feature]: Configurable TUI keymap ([tui.keys] + gwm tui keys) #87's keymap macro, no conflict expected).git stash apply <ref>in the surrounding shell.ref_name + subject; addinggit diff-tree --numstatper stash would need another shell-out per row and the issue called it out as "optional".son a different key andtoggle_sidebar(v) can be repurposed via[tui.keys].