Skip to content

✨ feat(tui): sidebar stashes mode toggled by s (#34)#166

Merged
kbrdn1 merged 3 commits into
devfrom
feat/#34-sidebar-stashes
May 26, 2026
Merged

✨ feat(tui): sidebar stashes mode toggled by s (#34)#166
kbrdn1 merged 3 commits into
devfrom
feat/#34-sidebar-stashes

Conversation

@kbrdn1
Copy link
Copy Markdown
Owner

@kbrdn1 kbrdn1 commented May 26, 2026

Description

Implements issue #34 — sidebar gains a stashes mode toggled by s
that shows git stash list for the selected worktree, alongside the
existing commits mode (git log --oneline + git status --short).

Stacked on #87 (feat/#87-tui-keymap, PR #165) because the s
binding ships through the rebindable keymap as a new
Action::ToggleSidebarMode. The PR will need a rebase / re-target
to dev once #165 merges; the diff itself doesn't touch any file
that #165 doesn't already touch, so the rebase is mechanical.

Closes #34

Type of change

  • ✨ Feature (new functionality)

Changes

  • SidebarMode { Commits, Stashes } enum on SidebarState with cycle_mode(). Default is Commits so pre-[Feature]: Sidebar stashes view (toggle with 's') #34 behaviour is preserved verbatim until the user presses s.
  • Cache key changes from PathBuf to (PathBuf, SidebarMode) so toggling re-shells the right git command rather than serving stale content for the other mode.
  • New 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.
  • Sidebar renderer (build_sidebar_sections) now takes a SidebarMode and dispatches to either recent_commits_lines or the new stash_lines. Title shows the mode (Recent Commits — commits / Stashes — stashes); bottom hint switches to Enter: copy stash@{N} to status in stashes mode.
  • New Action::ToggleSidebarMode (slug toggle_sidebar_mode), default binding s, wired through the View::List dispatcher.
  • Help overlay (?) gets a new row s cycle sidebar mode (commits / stashes) — drives off the resolved keymap like every other row.

Tests

  • cargo test passes locally
  • cargo fmt --check passes
  • cargo clippy -- -D warnings passes
  • New tests added under tests/

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, and worktree_integration updated for the new cache key shape + build_sidebar_sections signature; all 17 sidebar-state + 173 tui-app + 38 worktree-integration tests stay green.

Screenshots / TUI captures

Press `s` to toggle:

┌─ Stashes — stashes ─────────────────┐
│ stash@{0}  WIP on feat/auth: tweak  │
│ stash@{1}  WIP on docs: refactor    │
│                                     │
│ Enter: copy stash@{N} to status     │
└─────────────────────────────────────┘

`?` help overlay row:
  s             cycle sidebar mode (commits / stashes)

`gwm tui keys` row:
  toggle_sidebar_mode  s    default

Checklist

  • Branch follows <type>/#<issue>-<description>
  • Commits follow Gitmoji + Conventional Commits
  • CHANGELOG.md updated under ## [Unreleased]
  • No unwrap() on user-facing paths
  • No println! in TUI render code

Notes for reviewers

  • Base branch is feat/#87-tui-keymap, not dev. Once ✨ feat(tui): configurable keymap with chord support (#87) #165 merges, I'll retarget this PR to dev (the diff is purely additive on top of [Feature]: Configurable TUI keymap ([tui.keys] + gwm tui keys) #87's keymap macro, no conflict expected).
  • No stash-apply from the TUI per the issue's scope note — that's lazygit's territory. We surface the ref name so the user can paste it into git stash apply <ref> in the surrounding shell.
  • Per-stash file +/- counts are deferred to a follow-up. The v1 contract is ref_name + subject; adding git diff-tree --numstat per stash would need another shell-out per row and the issue called it out as "optional".
  • Mode is per-session, not persisted — matches the issue note ("low ROI to remember between launches"). If a user reliably wants stashes by default, the keymap can be rebound to put s on a different key and toggle_sidebar (v) can be repurposed via [tui.keys].

Copilot AI review requested due to automatic review settings May 26, 2026 18:50
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() + StashEntry parsing 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 thread src/worktree.rs Outdated
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 thread src/worktree.rs
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 thread src/tui/ui.rs Outdated
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 thread src/tui/ui.rs Outdated
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 thread src/tui/ui.rs Outdated
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 thread tests/tui_chord_tests.rs Outdated
Comment on lines +139 to +141
assert!(
row.contains('s'),
"expected the default `s` binding to appear, got: {row}"
kbrdn1 added 3 commits May 26, 2026 22:22
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.
@kbrdn1 kbrdn1 force-pushed the feat/#34-sidebar-stashes branch from ad7517d to a4fda8f Compare May 26, 2026 20:27
@kbrdn1 kbrdn1 changed the base branch from feat/#87-tui-keymap to dev May 26, 2026 20:59
@kbrdn1 kbrdn1 closed this May 26, 2026
@kbrdn1 kbrdn1 reopened this May 26, 2026
@kbrdn1 kbrdn1 merged commit 88b7017 into dev May 26, 2026
9 checks passed
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.

2 participants