Skip to content

feat(slash): /resume picker + mid-session in-place re-init#70

Merged
hakula139 merged 39 commits intomainfrom
feat/resume-picker
May 8, 2026
Merged

feat(slash): /resume picker + mid-session in-place re-init#70
hakula139 merged 39 commits intomainfrom
feat/resume-picker

Conversation

@hakula139
Copy link
Copy Markdown
Owner

Summary

/resume (alias /continue) opens an in-place session picker — type to filter by id, title, or project, Tab toggles current-project ↔ all-projects, Enter swaps the live session in place. /resume <id-prefix> resolves directly. Adds a generic SearchableList<T> modal primitive so future searchable pickers share the chrome, plus a --limit N cap (default 30, --limit 0 unbounded) on ox --list so long-running projects don't dump hundreds of rows on every invocation.

Design decisions

  • Mid-session re-init via roll_into, not process exec. Mirrors the existing roll-for-/clear shape: load + sanitize the target FIRST so a missing-file or empty-after-sanitize error leaves the live session untouched, then snapshot + clear + restore the file tracker, swap the handle, finalize the old session. The TUI keeps running — chat repopulates from the resumed transcript via the same chat.load_history(...) call the App constructor uses, so launch-time and mid-session resume share one population path.
  • Bare opens picker, typed resolves directly. Aligns with /theme, /model, /effort. Bare /resume is ReadOnly (safe mid-turn) and opens the modal; /resume <id-prefix> is Mutating (deferred mid-turn) and short-circuits to UserAction::Resume. The picker excludes the live session from typed-prefix matches — resuming yourself is a no-op.
  • Generic SearchableList<T: SearchableItem> primitive. Sibling to ListPicker<T: PickerItem>: substring filter on a composite haystack, scrollable viewport with PageUp / PageDown, replaceable item set so a scope toggle reloads without rebuilding the modal. Concrete pickers own submit semantics — primitive stays callback-free.
  • Typed-prefix scope widens automatically. Match in current project first; widen to all projects on no-match so users don't have to type --all. Ambiguous prefixes list candidates with their 8-char ids.
  • AgentEvent::SessionResumed carries the full payload. Avoids a round-trip — the agent loop loads + sanitizes, then pushes id, title, messages, tool_metadata into one event the App applies atomically (clear chat + load history + reset pending state + finalize idle).
  • Listing API gains list_paged(limit, all) -> ListPage { sessions, total }. total is the count before truncation so renderers can show ... and N more without re-walking the directory. list() / list_all() stay as thin wrappers. The ox --list cap defaults to 30; the footer tells you how to widen with --limit N or disable with --limit 0.
  • Test seed helper in session::store. seed_test_session(...) is pub(crate) so cross-module tests (slash / handle) can stand up sessions without exposing the entry module's Entry::Title / Entry::Summary shapes. roll_into gains tests that pin the swap-on-success / no-swap-on-failure invariants.

Changes

File Description
slash/resume.rs New: ResumeCmd (alias continue) + ResumePicker modal — SessionRow flattens SessionInfo into the haystack + display columns; resolve_prefix matches in-project first, widens on no match.
tui/modal/searchable_list.rs New: SearchableList<T: SearchableItem> — substring filter, scrollable viewport, replaceable items.
session/handle.rs Add roll_into + RollIntoOutcome: load-target-first, then snapshot + swap + finalize-old; tests cover happy path, target-load failure, tracker-snapshot swap.
session/store.rs Add seed_test_session cross-module test helper.
session.rs Promote entry to pub(crate) so SessionInfo is visible to slash::resume.
agent/event.rs New: UserAction::Resume { session_id }, AgentEvent::SessionResumed { id, title, messages, tool_metadata }.
agent.rs Catch UserAction::Resume in the mid-turn unreachable arm with a warn-log.
main.rs Agent loop: handle UserAction::Resume → call roll_into → emit SessionResumed; failure leaves the session unchanged and surfaces an Error event.
tui/app.rs New apply_session_resumed helper for AgentEvent::SessionResumed; route UserAction::Resume through apply_action_locally like Clear.
tui/modal.rs Wire the new searchable_list submodule.
slash.rs, slash/registry.rs Register ResumeCmd in BUILT_INS (alphabetical).
tui/components/input/snapshots/... Updated baselines: /resume row added, alphabetical sort displaces /status past MAX_VISIBLE_ROWS.
docs/guide/slash-commands.md, docs/guide/sessions.md, docs/roadmap.md, README.md, CLAUDE.md Document /resume (alias /continue), the --list --limit cap, and the searchable_list primitive.

Test plan

  • cargo fmt --all --check
  • cargo clippy --all-targets -- -D warnings — zero warnings
  • cargo test — 1782 passed
  • cargo llvm-cov --ignore-filename-regex 'main\.rs'slash/resume.rs 94.4%, tui/modal/searchable_list.rs 96.7%, session/handle.rs 99.8%
  • pnpm lint, pnpm spellcheck — clean
  • Manual: bare /resume opens picker; type to filter; Tab toggles scope; Enter resumes; chat repopulates from target's transcript
  • Manual: /resume <id-prefix> jumps directly; ambiguous / unknown prefixes show recovery hint
  • Manual: /resume mid-turn — picker opens (ReadOnly); /resume <id> mid-turn refuses (Mutating gate)
  • Manual: ox --list shows 30 most-recent rows + ... and N more footer; --limit 0 shows everything; --limit 100 raises the cap for one run

hakula139 added 3 commits May 8, 2026 01:32
Surveys /resume / --resume / --continue UX across Claude Code,
Codex, and opencode; lays out the oxide-code design (bare /resume
opens a searchable picker; /resume <id-prefix> mirrors `ox -c
<prefix>`; mid-session re-init via roll_into; SearchableList
primitive sibling to ListPicker).
Refactors `SessionStore::list` / `list_all` to call a new
`list_paged(limit, all)` that pre-stat-and-sorts directory entries
before paying the per-file JSONL parse cost. Returns a `ListPage`
carrying the post-truncation rows and the pre-truncation total so
the renderer can append a "... and N more" footer.

Caps `ox --list` at 30 rows by default; `--limit N` widens, and
`--limit 0` opts back into the previous unbounded behavior for
piping. Old `list()` / `list_all()` stay as one-line wrappers so
existing callers (resolver, tests) don't churn.
Adds `/resume` (alias `/continue`) — a searchable session picker that
swaps the live session in place via a new `roll_into` mirror of `roll`.
Bare opens the picker (substring filter, Tab toggles current-project ↔
all-projects); `/resume <id-prefix>` resolves directly. Introduces a
generic `SearchableList<T>` modal primitive so future filtered pickers
share the chrome. Old session is finalized after the swap so the live
session stays clean if the target fails to load.
@hakula139 hakula139 added the enhancement New feature or request label May 7, 2026
@hakula139 hakula139 self-assigned this May 7, 2026
@hakula139 hakula139 added the enhancement New feature or request label May 7, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 7, 2026

hakula139 added 23 commits May 8, 2026 02:44
…ghten width math

Review-driven fixes for the /resume picker:

- Filter the live session out of the picker rows AND short-circuit roll_into
  when target == live id, so neither path can race the open append-writer.
- Classify both forms of /resume as Mutating; bare picker also waits for idle
  so the agent loop never drops the submitted Resume action.
- Drop the dead key_hint / select_by_hint / HINT_WIDTH surface from
  SearchableList; the resume picker doesn't dispatch numeric mnemonics and
  the gutter was a permanent 3-col leak.
- Tighten title-budget arithmetic: use unicode display width on the project
  column (CJK paths no longer overflow), name the +2 magic separator, cap
  project rendering coherently with the budget.
- Re-export SessionInfo / TitleInfo via session::store instead of widening
  the entry module to pub(crate).
- Replace RollIntoOutcome's wasted ResumedSession.handle clone with a
  payload-only struct (messages / title / tool_metadata / finalize_failure).
- Move session_write_error AFTER SessionResumed so chat.clear_history
  doesn't wipe the freshly-pushed error; promote dropped-event log to error
  level. Disable input on UserAction::Resume to avoid the wipe-while-typing
  window.
- Doc fixes: SessionResumed / UserAction::Resume / RollIntoOutcome /
  seed_test_session / total field / resolve_prefix; CLAUDE.md crate tree
  drops internal-name leaks.
- Tests: apply_session_resumed, typed-arg success path, picker filters live
  session, Tab widens scope and preserves query, full-id match,
  tracker invariance under load failure, self-resume rejection,
  empty-visible navigation guards, footer plural / all-projects branches.
…ware footer

- consolidate ambiguity formatter in `match_in_scope` so the typed-arg
  path stays in lockstep with `ox -c <prefix>`
- empty-list Enter stays open instead of silently dismissing (Cancelled
  → Consumed) so users can Tab the scope or Esc out explicitly
- surface `list_paged` errors inline in the footer instead of warn-log
  + empty list — failures must not look like "no sessions found"
- footer reflects substring filter as "X / Y matching" so a typed query
  doesn't mask the actual scope size
- defensive `modals.clear()` in `apply_session_resumed` for future
  nested overlays above the picker
apply_session_resumed silently dropped pending_prompts; user-typed
Enter-committed work disappeared with no UI signal. Push a system
message into the resumed chat naming the discarded count so the loss
is visible.
The previous code reused session_write_error for the OLD session's
finalize failure, which formatted as "Session write failed: ...". A
user reading that in the resumed UI would think the *current* writer
broke. Route through AgentEvent::Error with an explicit "Previous
session failed to finalize" prefix so the source is unambiguous.

Also fix the misleading "Move once" comment over a clone — the code
clones; the comment claimed otherwise.
restore_verified silently dropped drifted snapshots; the user's next
Edit would reject with "must Read before Edit" and they wouldn't
understand why. Return the dropped paths from restore_verified, plumb
them through RollIntoOutcome, and surface a count + 3-path preview as
an AgentEvent::Error after SessionResumed.

CLI --continue path warn-logs the drift; mid-session Resume routes
through the sink so the user sees it in chat. Extracted apply_resume
helper to keep agent_loop_task under the line cap.
CLAUDE.md forbids `pub use` re-exports that obscure where items are
defined. Promote `entry` to `pub(crate)` and have callers import
directly from `session::entry::{SessionInfo, TitleInfo}`. Also drops
the workaround comment that was admitting the re-export was a hack.
The field duplicated session_id[..8] and required from_info to keep
two strings in sync. Compute the prefix at render time so the
invariant becomes structural. Drops the redundant haystack slot
(every prefix char is already a session_id substring).
Public fields permitted constructing `ListPage { sessions: vec![full;
N], total: 0 }` — an inverted state. Make fields private, add new()
with a debug-asserted invariant, accessor pair, into_sessions for the
move case, and Default for empty fallbacks. The picker's error path
now reads `ListPage::default()` instead of a literal.
ResumePicker re-queried `UtcOffset::current_local_offset` on open —
documented as unsound on multi-threaded Unix runtimes (race on
localtime_r post-spawn). Move the OnceLock cache out of main.rs
into util::time so the picker reads the same value the CLI listing
uses, captured once before tokio spawns workers.
A title-gen task spawned for a session that subsequently finalizes
(via /clear or /resume) hits actor-gone when its Haiku response
finally returns. The old code routed that as `Session write failed:
...` into the sink — which, for /resume, lands in the *resumed*
session's UI. Add `SessionHandle::is_actor_alive` and have title-gen
warn-log post-finalize failures instead of surfacing them.
- store.rs: list_paged tests now sit after list_all (production:
  list → list_all → list_paged)
- resume.rs: ID_WIDTH declared before TIMESTAMP_WIDTH (render paints
  the id slot first); footer_text + render-load-error tests merged
  into the ResumePicker section so production-method tests aren't
  scattered after resolve_prefix tests
- searchable_list.rs: replace_items tests precede select_next /
  page_down (production order); navigation_on_empty_visible_set moved
  into page_down/page_up section as the cross-method edge case;
  replace_items_resets_cursor_and_reapplies_filter now asserts the
  cursor reset documented on the function
…n const

- searchable_list: fold the four `if visible.is_empty() { return; }`
  prefixes through a `nonzero_visible_len()` helper; recompute_visible
  separates the empty-needle fast path so the common case skips per-row
  to_lowercase allocation; drop the misleading
  `Vec::with_capacity(area.height)` hint
- slash/resume: promote `FIXED_PREFIX_WIDTH` to a const mirroring
  list_view.rs:16; `match_in_scope` uses pop() instead of
  into_iter().next(); with_isolated_xdg drops the redundant
  PathBuf clone
Apply comment-analyzer's per-line verdicts:
- resume.rs: drop ID_WIDTH name-restating doc, the verbose render-row
  paragraph, the "hex session ids" overclaim in resolve_prefix doc,
  the dead "Most-recent-first" test comment, and the test-name-
  restating "Pin the success branch" / "Two projects" comments
- searchable_list.rs: tighten module doc, trait/struct/field docs;
  drop trait-name-restating "One row in a SearchableList"
- store.rs: collapse the four-line collect_paths_all_projects doc
- handle.rs: trim RollIntoOutcome doc to the load-bearing contract
- app.rs: drop dead `_ = pending_call_id;` and the unused let-binding
The original draft was generated from the implementation plan and documented
shape that didn't ship: ReadOnly classify on bare, complete_arg, date-grouped
rows, numeric mnemonics, four-method SearchableItem, nested RollIntoOutcome,
messages_for_chat. Rewrites against the shipped surface and shrinks the doc
to the same length band as modals.md and commands.md so the trio reads as one
voice.
The bar painted a static `▏` glyph after either the placeholder text or
the typed query, which placed the visual cursor at the wrong column when
the query was empty (sitting at the end of the placeholder hint instead
of the prompt) and gave it a non-blinking shape inconsistent with the
input panel. Drop the painted glyph and call `frame.set_cursor_position`
so the terminal's native cursor anchors at the insertion point — same
pattern the input panel uses, so shape and blink match the user's
terminal config.
…ware project column

Title-prominent layout adopted from Claude Code: each session paints a
bold title row above a dim metadata row carrying id-prefix + relative
time. The cwd column drops out when the picker scope is `current
project` — every visible row shares it, so painting it would be visual
noise and would confuse the substring filter; it appears only after Tab
widens to all projects. Relative time (`3s ago`, `2h ago`, `5d ago`,
ISO date for >30d) reads more naturally in a recency-sorted list than
the absolute timestamp.

`SearchableItem` now returns `Vec<Line>` and declares a `row_height()`
constant so the list primitive can size its viewport for items wider
than one terminal row. The cursor gutter only paints on the first line
of a multi-row item so the layout still aligns.
Welcome screen advertised every other slash command but skipped /resume,
and the tip pool only mentioned the CLI form (\`ox --continue\`). Add the
slash form to both so the picker is discoverable from a fresh empty
session, not just from the help index.

Snapshots regenerated — adding entries shifts the seeded-shuffle output
to land on different starters / tips for the fixture seeds.
Three real production paths the existing suite missed:

- util::time::{init_local_offset, local_offset} now under test for
  idempotency + stable-cache contract (the OnceLock layer).
- title_generator's alive-session writer-failure branch — the
  counterpart to the post-finalize-silent test. New `acks_with_failure`
  handle helper enables it.
- App::apply_action_locally's `UserAction::Resume` arm — pin that input
  is gated between forward and SessionResumed so a typed prompt can't
  slip into chat just before `clear_history` wipes it.
…er empty

ratatui_textarea reserves the first column as a phantom-cursor cell, so the
placeholder text starts one column right of the textarea origin. Previously
the visible cursor sat on the phantom slot, leaving a one-column gap before
the placeholder text. Shift the cursor by 1 when the buffer is empty so it
anchors on the placeholder's first letter, matching the searchable-list
search bar.
hakula139 added 13 commits May 8, 2026 22:53
…and new tests

Reduce codecov patch-coverage gaps left by the resume picker and adjacent
modules: tighten control flow so the early-return arm is reachable from
tests, and pin previously untested error-paths.

- file_tracker: collapse the two `Ok` arms in `restore_verified` into one
  `inspect_err` chain so the metadata-failure branch shares coverage with
  the happy path.
- session/store: replace the `match` in directory iteration with an
  `inspect_err`/`ok`/`map` chain; widen `test_project_dir` to `pub(crate)`
  so resume picker tests can exercise the `list_paged` Err arm.
- session/handle/testing: slim `acks_with_failure` into a focused
  `acks_append_ai_title_with_failure` (only the variant the title-gen test
  needs); rename the call site.
- slash/resume: render footer guard becomes an early-return so the
  `< 2` rows path is no longer dead in measurement; convert two `match`
  arms to `let-else` for the same reason; eager-bind the footer string
  in the filter test so both assertion sides see the same value.
- New tests: render at `height=1` (footer drop), reload sets
  `load_error` + zeroes `total` when `list_paged` fails, and a
  searchable-list area-too-short cursor-placement guard.
…m time

Iteration on the /resume picker driven by review feedback. The picker
metadata row now surfaces session weight (`N msgs`) and the git branch
the session was started on; the relative-time format swaps short codes
(`5d ago`) for long-form (`5 days ago`) with proper singular/plural.
Color and copy alignments make the picker feel native alongside /model.

- session/entry: extend `Entry::Header` and `SessionInfo` with an
  optional `git_branch`. `serde(default, skip_serializing_if = ...)`
  preserves backward compatibility with sessions written by previous
  builds.
- session/state: capture `HEAD` via `git rev-parse --abbrev-ref HEAD`
  when minting a new session header. Tests are insulated from the host
  repo's branch name via a `cfg!(test)` guard.
- session/store: thread `git_branch` through `read_session_info`.
- slash/resume:
  - `SessionRow` gains `message_count` + `git_branch`; metadata row
    appends `· N msgs` and `· branch` when present (singular/plural via
    a `pluralize` helper).
  - `format_relative_time` rewritten in long-form (`1 day ago`,
    `5 days ago`); negative deltas (clock skew) collapse to
    `0 seconds ago` so the singular/plural axis stays sane.
  - Cursor row now uses `text + bold` to match /model — the `>` gutter
    already carries the accent color.
  - `PICKER_DESCRIPTION` drops the redundant Tab hint (already shown in
    the footer).
- main: alphabetize CLI fields; drop "(legacy behavior)" from
  `--limit` description.
- tui/modal/searchable_list: capitalize "Type to filter".
- session/list_view: extend test fixture to cover the new field.
… in review

- file_tracker::restore_verified now logs distinct warnings for each
  drop reason: stat-failure (file gone or unreadable) vs (mtime, size)
  drift. Previously both arms collapsed into a silent push to `dropped`,
  hiding why the agent had to re-Read.
- session::store::find_session_path now logs a warning for read-dir
  iteration errors instead of silently `continue`-ing — sessions in a
  flapping project subdir would otherwise vanish without trace.
- session::store::read_session_info bails with a clear "empty file"
  message when the first line is blank; prior path produced a confusing
  serde "invalid header line" error.
- slash::resume::format_relative_time switches to `expect` for the
  static format-description path. Previously the impossible failure
  arm fell back to "unknown", masking the invariant.
…hape

P1 review nits:

- file_tracker::record_modify_after_write warn-logs stat failures so
  subsequent Edit calls aren't surprised by a stale snapshot.
- session::store::read_session_info now logs when mtime is unreadable
  and falls back to created_at, instead of swallowing the error.
- main: bump session-rolled and config-changed event-drop logs from
  `warn` to `error` — both leave the TUI desynced from client state, so
  they should stand out in the log.
- util::time::init_local_offset warn-logs when localtime resolution
  fails (UTC fallback was previously silent).
- session::title_generator: flip the dead-actor branch to early-return
  for a flatter happy path.
- tui::modal::searchable_list: page_up uses the same `let-else` shape
  as page_down for symmetry.
- slash::resume::SessionRow: meta row now respects the same `TITLE_FLOOR`
  width floor as the title row so a tiny terminal still surfaces the id
  prefix instead of an empty line.
- main::apply_resume: drop the redundant `messages_for_event` clone in
  favor of `messages.clone_from(&outcome.messages)`; the original
  `outcome.messages` then moves into `SessionResumed` directly.
- .cspell/words.txt sorted case-insensitively so future inserts don't
  fight the diff.
The previous fix added `+1` to the cursor X when `is_empty()` was true,
on the theory that ratatui_textarea reserves column 0 as a phantom slot
and the placeholder text begins at column 1. In practice the cursor at
`textarea_area.x + 0` already lands on the placeholder's first character
— the prior visual that looked like "cursor before the letter" was just
the cursor highlighting the dim placeholder cell.

Adding `+1` overshot by one cell, parking the cursor on the second
letter of the placeholder (`s` of `Ask anything...`). Drop the shift.
…ment

The input panel and the modal search bar both ended with the same
`cursor_x = raw_x.min(area.right().saturating_sub(1))` clamp followed by
`Frame::set_cursor_position`. Lift the pattern into a shared
`tui::cursor::place_clamped` helper so future input surfaces don't
re-derive (or worse, drop) the right-edge clamp.

- New `tui/cursor.rs` with the helper + tests covering within-area,
  past-right-edge, and offset-area cases.
- Input panel and `SearchableList::place_terminal_cursor` switch to the
  helper. Behavior is identical.
Both selected and unselected rows previously used `theme.text()`
(differing only by `BOLD` on the cursor row), so titles and metadata
read at the same weight and the eye had to hunt for the cursor.

Three-tier hierarchy now:

- Cursor row title: `text + bold`
- Other titles: `text + DIM` modifier — softer than full text but still
  brighter than metadata's `theme.dim()` foreground
- Metadata: unchanged (`theme.dim()`)

Drops the previous "matches /model" comment — /model deliberately keeps
all rows at full text and relies on the `>` gutter alone, but /resume
benefits from the extra middle tier because each row is two lines tall.
…offset lookups

The two production paths flagged by codecov as low-coverage shared the
same shape: a single function did both an impure system call and the
parsing of its result, with the parsing arms unreachable from tests
that couldn't synthesize the system-call output.

Split each into a thin shell-out + a pure parser, then unit-test the
parser directly with synthetic inputs. The shell-out itself is now
covered too — `current_git_branch` runs against a temp git repo
fixture, and the cfg(test) skip moves to the call site so snapshot
tests still get `git_branch: None` without hiding the production body.

- session::state: extract `parse_git_branch(success, stdout)`. New
  tests cover failed exit, trailing newline, detached HEAD (`HEAD`
  literal), empty / whitespace stdout, invalid UTF-8, and a real
  `git init -b fixture-branch` round-trip (skipped if `git` isn't on
  PATH).
- util::time: extract `resolve_offset(Result)`. Tests cover the Ok and
  the `IndeterminateOffset` Err arms — the warn-and-fallback path was
  previously unreachable from tests because the host's TZ database
  determined which arm fired.
- CLAUDE.md crate tree: add the new `tui/cursor.rs` entry from the
  preceding refactor.
…verride

Add a sketch under .claude/plans/ and surface in the deferred slash list.
The design captures the title-generator race window (manual must beat the
background AI title), the persistence shape (TitleSource enum on the
Title entry), and the v1 scope (current session only — picker-driven
rename overlaps with the deferred delete UX).
…ext+DIM

Dimming `text` with `Modifier::DIM` keeps the row in the same color
family as the metadata's `dim` foreground, so titles + metadata read at
roughly the same weight and the row is harder to scan. Switch unselected
titles to `theme.muted()` so the picker rides the theme's pre-existing
three-tier `text` / `muted` / `dim` hierarchy: selected stays
`text + bold`, metadata stays `dim`, and the middle tier is now visibly
distinct.

Add a contrast assertion so a future regression that flattens any pair
of tiers fails loudly.
CLAUDE.md says test sections must mirror the production function order
in the same file, and within each section follow happy → variants →
edge / error.

- slash/resume.rs: split the catch-all `// ── ResumePicker ──` umbrella
  into per-method subsections (`new`, `reload`, `submit`, `footer_text`,
  `Modal::render`, `Modal::handle_key`) and group the previously
  scattered tests under the right one. Renamed
  `picker_filters_out_live_session_to_block_self_resume` to
  `new_filters_out_…` so it cluster-sorts with its function.
- session/state.rs: hoist `// ── current_git_branch ──` and
  `// ── parse_git_branch ──` (recently added) up next to `flush_entries`
  to follow the production order
  `flush_entries → current_git_branch → parse_git_branch → format_current_dir`.
  Reorder parse_git_branch tests so the happy-path case leads.
- tui/modal/searchable_list.rs: move
  `place_terminal_cursor_skips_when_area_is_shorter_than_search_row_offset`
  to the end of the `// ── render ──` section — it is the edge case,
  and CLAUDE.md puts edges last.

No assertion or test-body changes — pure rearrangement. Suite stays
green at 1817 tests.
Three small cleanups in line with CLAUDE.md test-suite guidance
(prefer concise + merge tests covering the same path) and the global
"comment the why, not the change" rule:

- searchable_list.rs: hoist `use ratatui::Terminal/TestBackend` to the
  top of the test module (was repeated inside five tests). Merge the
  two `render_anchors_terminal_cursor_*` tests into one
  `render_places_cursor_at_prompt_plus_visible_query_width` that
  exercises both the empty-query and typed-query cursor positions in a
  single render setup. Drop the "Regression: bare picker used to paint
  a `▏` glyph..." comment — it narrates the historical fix, which
  belongs in git history, not the source tree.
- state.rs: collapse five one-liner `parse_git_branch_*` tests into a
  single table-style test. The fn is a 4-branch parser; one test with
  six asserts reads more clearly than five 3-line tests.
@hakula139 hakula139 merged commit 5fc9d8e into main May 8, 2026
4 checks passed
@hakula139 hakula139 deleted the feat/resume-picker branch May 8, 2026 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant