Skip to content

TUI menus: arrow keys should wrap (cycle) on short menus, matching Tab behavior #39

Closed

Description

@subinium

Problem

The short menu screens — ActionSelect, ResumeSelect, AgentSelect, PermissionSelect, plus the new-session mode picker — accept Up/Down for navigation but stop at the first/last item. To get from the bottom item back to the top you have to hit Up 5–6 times.

Worse, the same screens already wrap on Tab / Shift-Tab but not on the arrow keys, so the behavior is inconsistent within a single screen.

Code: the inconsistency, in one place

src/tui/mod.rs ui_action_select (L1095–1111):

// Tab / Shift-Tab: wrap with modulo
app.action_index = (app.action_index + action_count - 1) % action_count;   // Shift-Tab
...
app.action_index = (app.action_index + 1) % action_count;                  // Tab

// Up / Down: bounded, no wrap
} else if (ui.key_code(slt::KeyCode::Up) || ...) && app.action_index > 0 {
    app.action_index -= 1;
} else if (ui.key_code(slt::KeyCode::Down) || ...) && app.action_index < action_count - 1 {
    app.action_index += 1;
}

Tab wraps, Up/Down doesn't. Same screen, same conceptual action ("next/prev menu item"), two different behaviors. Same shape repeats in ui_resume_select, ui_agent_select, and the new-session picker.

Why wrap is the right default for these screens

These are all short, fixed menus (typically 3–6 items: Resume / Open / Cd / Pin / Delete / Back, or the resume-mode list, or the installed-agent list). On a 6-item menu, hitting Up at item 1 to jump to item 6 is the standard expectation — fzf, vim :menu, every TUI launcher I can think of behaves this way. The Tab path already commits to that model; the arrow path just hasn't caught up.

The browse list (main session list) is a different story — it can be hundreds of rows and wrap-around there would feel disorienting (you scroll past the bottom and end up at the top with no clear "new page" cue). Keep wrap off in Browse / GroupedBrowse / BulkDelete. Add wrap only on the short-menu screens.

Fix sketch

Replace the bounded Up/Down branches in each menu screen with the same modulo math the Tab path already uses:

if (ui.key_code(slt::KeyCode::Up) || ui.key_mod('p', CONTROL) || ui.key_mod('k', CONTROL))
    && action_count > 0
{
    app.action_index = (app.action_index + action_count - 1) % action_count;
}
if (ui.key_code(slt::KeyCode::Down) || ui.key_mod('n', CONTROL) || ui.key_mod('j', CONTROL))
    && action_count > 0
{
    app.action_index = (app.action_index + 1) % action_count;
}

Apply identically to ui_resume_select (resume_mode_index / resume_mode_options.len()), ui_agent_select (agent_index / installed_agents().len()), ui_permission_select, and the new-session mode picker (mode_index / mode_options.len()).

Out of scope

  • Browse / GroupedBrowse / BulkDelete: long lists, keep current bounded behavior. (If a user wants j/k wrap on Browse later, that's a separate UX call.)

Severity

Low. UX papercut. But it's hit on every action menu — Resume / Delete / Open are routed through ActionSelect — so it shows up several times per session.

Activity

  1. subinium commented on May 4, 2026

    @subinium
    OwnerAuthor

    Fixed in #42 (v0.11.1). ActionSelect / AgentSelect / PermissionSelect / ResumeSelect arrow keys now cycle-wrap (first → last on Up, last → first on Down), matching the existing Tab/BackTab wrap behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions