diff --git a/lib/hive/tui/bubble_model.rb b/lib/hive/tui/bubble_model.rb index d42249e34..d80f99500 100644 --- a/lib/hive/tui/bubble_model.rb +++ b/lib/hive/tui/bubble_model.rb @@ -2944,8 +2944,8 @@ module Hive # Reserve a 1-cell right margin across every section so no row # ever lands a glyph in the terminal's last column. Header and # footer strips are width-aware — the fixed hint footer was - # 75 chars regardless of terminal width and overflowed at - # cols<76 before this clamp. + # 79 chars regardless of terminal width and overflowed at + # cols<80 before this clamp. usable = [ cols - 1, 1 ].max sections = [ header_strip(usable) ] sections << stalled_banner(usable) if stalled? @@ -3002,7 +3002,7 @@ module Hive # Default footer — context-aware key hints + flash decay (the # status line). v1 had this in Views::Grid#status_line; lifted here # so the panes stay layout-only. `usable_width` clamps the line - # so the fixed 75-char hint string doesn't overflow narrow + # so the fixed 79-char hint string doesn't overflow narrow # terminals (e.g. cols=70 used to wrap onto a second visible row). def default_footer(usable_width = nil) if @hive_model.flash_active? @@ -3017,7 +3017,7 @@ module Hive end def footer_hint - "[Tab] switch [Enter] action [n] new [/] filter [?] help [q] quit" + "[Tab] switch [Enter] action [n] new [/] filter [?] help [i] idea [q] quit" end # Compute pane widths and join horizontally. Left pane is clamped diff --git a/lib/hive/tui/styles.rb b/lib/hive/tui/styles.rb index 10d9a20d4..a4e179538 100644 --- a/lib/hive/tui/styles.rb +++ b/lib/hive/tui/styles.rb @@ -107,7 +107,7 @@ module Hive # render loop is showing a snapshot older than the staleness threshold. STALLED = Lipgloss::Style.new.foreground(color(:yellow)).reverse(true) - # Hint footer ("[?] help [/] filter [q] quit") — faint default-fg so + # Hint footer (see `BubbleModel#footer_hint`) — faint default-fg so # it recedes against the active grid content. HINT = Lipgloss::Style.new.faint(true) diff --git a/test/unit/tui/bubble_model_test.rb b/test/unit/tui/bubble_model_test.rb index bfc3e63b9..3fd67e81f 100644 --- a/test/unit/tui/bubble_model_test.rb +++ b/test/unit/tui/bubble_model_test.rb @@ -600,31 +600,58 @@ class HiveTuiBubbleModelTest < Minitest::Test assert_includes out, "Tasks ·", "tasks pane title must render" assert_includes out, "[Tab] switch", "default footer hints must appear" assert_includes out, "[Enter] action", "Enter footer hint must describe contextual behavior" + assert_includes out, "[i] idea", "the idea-preview hint must appear in the footer legend" refute_includes out, "[Enter] open", "Enter is not only an open action" end - def test_default_footer_hint_omits_o_at_70_col_budget - # Plan R6: `[o] open` is included in the footer only if it fits - # the 70-col budget without wrapping or pushing primary actions - # onto a second line. At 70 cols the current hint string is - # already ~69 chars; adding ten more (separator + "[o] open") - # would exceed the budget. We rely on the `?` overlay for - # discoverability instead. This test pins that decision so a - # future contributor doesn't silently re-add the hint and break - # 70-col rendering. + def test_footer_hint_pins_primary_hints_and_79_char_budget + # Plan R6: `[o] open` and `[s] steer` are secondary gestures + # documented in the `?` overlay only. The footer carries the primary + # hints: Tab / Enter / n / filter / help / i (idea preview) / q. + # `[i] idea` was promoted to a primary hint because the binding was + # already merged (PR #100) but invisible in the legend; its 10 chars + # pushed the footer literal from 69 to 79 chars. At cols < 80 the + # rightmost hints are truncated (see the narrow-terminal test), but + # `q` still works and every binding stays documented in `?`. hint = @model.send(:footer_hint) - assert_equal "[Tab] switch [Enter] action [n] new [/] filter [?] help [q] quit", + assert_equal "[Tab] switch [Enter] action [n] new [/] filter [?] help [i] idea [q] quit", hint, - "footer hint must remain the pre-`o` literal; `o` is documented in `?` only" + "footer hint must be the exact primary-hints literal" refute_includes hint, "[o] open", - "70-col budget can't absorb `[o] open` alongside primary hints" + "`[o] open` stays in the `?` overlay, not the footer" + refute_includes hint, "[s] steer", + "`[s] steer` stays in the `?` overlay, not the footer" # Width guard: pin the actual character count so a future contributor # who adds a hint and (correctly) bumps the literal above also has to - # acknowledge they're spending bytes against the 70-col budget. If + # acknowledge they're spending bytes against the terminal budget. If # this assertion fires alongside an updated literal, the contributor - # MUST verify default_footer truncation behavior at cols == 70. - assert hint.length <= 70, - "footer hint must fit the 70-col budget without truncation; got #{hint.length} chars" + # MUST verify default_footer truncation behavior (see + # test_default_footer_truncates_single_line_at_70_cols). + assert_equal 79, hint.length, + "footer hint length is pinned at 79; changing it is a conscious budget decision" + end + + def test_default_footer_truncates_single_line_at_70_cols + # At cols == 70 the usable footer width is cols - 1 = 69, below the + # 79-char hint literal. `default_footer` must clamp to a single row + # (no wrap onto a second visible row) via `Views::Format.truncate`, + # cutting from the right: the 69-cell cut lands inside `[i] idea` + # (the strip ends `… [i] ide…`), dropping `[q] quit` plus the + # trailing `a` of `idea`. + # `q` still quits and every binding is documented in the `?` overlay. + narrow = Hive::Tui::BubbleModel.new( + hive_model: Hive::Tui::Model.initial.with(cols: 70), + dispatch: @dispatch + ) + footer = narrow.send(:default_footer, 69) + refute_includes footer, "\n", + "footer must stay a single strip row; no wrap to a second line" + assert_includes footer, "…", + "truncation ellipsis must mark the cut at 70 cols" + assert_includes footer, "[i] ide", + "the 69-cell cut must land inside `[i] idea` (strip ends `[i] ide…`)" + refute_includes footer, "[q] quit", + "[q] quit is the first entry cut on narrow terminals; q still works and is in `?`" end def test_grid_mode_collapses_to_single_pane_below_min_cols diff --git a/wiki/commands/tui.md b/wiki/commands/tui.md index f46a05044..61ea0529b 100644 --- a/wiki/commands/tui.md +++ b/wiki/commands/tui.md @@ -28,7 +28,7 @@ The legacy curses backend was removed in plan #003 U11. `HIVE_TUI_BACKEND=curses │ myapp │ ⚠ oauth-… 6-review Needs recovery 1h │ │ appcrawl │ │ ├─────────────────┴────────────────────────────────────────────────────────┤ -│ Footer: [Tab] switch [Enter] action [n] new [/] filter [?] help [q]│ +│ Footer: [Tab] switch [Enter] action [n] new [/] filter [?] help [i]…│ └──────────────────────────────────────────────────────────────────────────┘ ``` @@ -65,6 +65,7 @@ Pane focus is keyboard-only; the focused pane border is bright cyan, the inactiv | `a` | run `hive archive` | | `Enter` | from left pane: focus right pane. From right pane: perform the row's contextual action: input editor on `needs_input` (completed brainstorm answer rounds auto-run; plan rows auto-advance to `develop` or auto-revise on user feedback), log tail on `agent_running` (and on `error` rows still in a kill-class auto-heal window), red-status detail on selected review-recovery and non-kill-class `error` rows, direct retry/browse for the legacy review-stale exceptions, and suggested-command dispatch for ready rows | | `o` | open the focused row's hive-state task folder in `$VISUAL` / `$EDITOR` / `vi` for read-only browsing — no marker change, no workflow dispatch. Distinct from `Enter` (workflow-contextual) and the verb keys (subprocess dispatch). Useful for revisiting investigation outputs in `9-done` (or any stage). | +| `i` | preview the focused task's source `idea.md` `original_text` in a read-only bottom strip (right-pane focus required); any key dismisses back to the grid | | `s` | steer the focused task manually: open the configured `execute.agent` in the feature worktree with every existing stage folder for that slug passed as agent context, mark the row `MANUAL_STEERING`, and archive the slug under `archived-manual/` when the agent exits | | `n` | open the new-idea flow; if scope is `★ All projects`, first show a project picker, then submit with `hive new ""` against the chosen concrete project | | `/` | open filter prompt | diff --git a/wiki/log.md b/wiki/log.md index 7c8465bc6..395428d4a 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -1855,3 +1855,10 @@ chruby and RVM are intentionally not handled — they modify PATH per-shell and **Refreshed pages:** - [[testing]] — documented the CI foreground/daemonization coverage pitfall and reload-safe enum caveat. + +## [2026-08-13T00:00:00Z] tui — surface `[i] idea` in the grid footer legend + +**Action:** Added the always-visible `[i] idea` legend entry to `BubbleModel#footer_hint`. The `i` key already worked (PR #100 — read-only bottom-strip preview of the focused card's `idea.md` `original_text` via `KeyMap#grid_message` → `Messages::OpenIdeaPreview` → `Views::IdeaPreview`), but the footer legend omitted it, so the binding was invisible in the grid. The footer literal now reads `[Tab] switch [Enter] action [n] new [/] filter [?] help [i] idea [q] quit` (69 → 79 chars); `[o] open` and `[s] steer` remain `?`-overlay-only. The pinned footer test was updated to the new literal, asserts the 79-char budget explicitly, and gained a narrow-terminal test proving `default_footer` clamps to a single truncated row at cols=70 (dropping `[q] quit` first). + +**Refreshed pages:** +- [[commands/tui]] — footer diagram + keybindings table now include `i`.