Skip to content

Conversation

@sotte
Copy link
Contributor

@sotte sotte commented Dec 31, 2025

This is a follow up to #42
and it got messy quickly :) Partially due to me not dully understanding bubbletea, partially also just getting the flow right-ish. Still not quite happy.

@hmans please try it out. If we move forward with this, we have to clean this up quite a bit.

Everything from c4f6332 on is relevant.

image

sotte and others added 30 commits December 28, 2025 19:37
Single-character codes for compact list display:
- Types: M(ilestone), E(pic), B(ug), F(eature), T(ask)
- Statuses: D(raft), T(odo), I(n-progress), C(ompleted), S(crapped)

Refs: beans-t0tv
- Type column: 3 chars (M/E/B/F/T)
- Status column: 3 chars (D/T/I/C/S)
- Updated CalculateResponsiveColumns base width calculation (40 -> 20)
- Updated tree headers to use "T" and "S" instead of "TYPE" and "STATUS"
- Frees up ~20 chars per row for title

Refs: beans-t0tv
Lightweight component for two-column layout right pane:
- Shows bean ID, title, status, type, priority
- Renders markdown body (truncated to fit)
- Shows 'No bean selected' when empty

Refs: beans-t0tv
- TwoColumnMinWidth: 120 columns
- LeftPaneWidth: 55 characters
- isTwoColumnMode() helper method

Refs: beans-t0tv
- Add preview previewModel field to App struct
- Initialize preview in New() with newPreviewModel(nil, 0, 0)

Refs: beans-t0tv
- View() checks isTwoColumnMode() before rendering
- renderTwoColumnView() composes list + preview horizontally
- ViewConstrained() renders list with constrained width
- Falls back to single-column for narrow terminals

Refs: beans-t0tv

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- cursorChangedMsg emitted when list cursor moves
- App updates preview on cursor change
- Preview initialized when beans are loaded

Refs: beans-t0tv
- Update preview dimensions on window resize
- Handle empty list state in preview
- Add 'enter' shortcut to help overlay

Refs: beans-t0tv
- Footer now app-global, spanning full terminal width
- Right pane capped at 80 chars max (left pane gets remaining space)
- Preview height properly constrained to prevent overflow
- Detail view linked beans show full type/status names

Refs: beans-m3mq, beans-tbtr
When markdown content caused line wrapping within the preview pane,
lipgloss rendered more lines than expected. Our truncation was cutting
off the bottom border. Now we preserve the bottom border when truncating.

Also fixed height calculation in ViewConstrained() - was using -4
(for footer) instead of -2 (no footer in two-column mode).

Refs: beans-t0tv
Show full type/status names (e.g., "feature", "in-progress") when
terminal is ≥120 columns wide, single-letter abbreviations (F, I)
when space is tight.

- Add UseFullTypeStatus to ResponsiveColumns struct
- Set flag and wider column widths when width >= 120
- Pass setting to RenderBeanRow via list delegate

Refs: beans-vn93
- Enter/Backspace for left↔right pane focus
- Tab for links↔body within detail pane
- Border color indicates focus state
- Single-column mode shows one pane at a time
- Delete preview.go, use detailModel in right pane

Refs: beans-pn6z
Design decisions:
- Granular view states (viewListFocused, viewDetailLinksFocused, viewDetailBodyFocused)
- Enter/Backspace for list↔detail navigation
- Tab for links↔body within detail
- Backspace navigates history first, then returns to list
- Only q quits, esc is for cancel/clear only
- Border color indicates focus (primary=focused, muted=unfocused)
- Footer changes based on focused area
- Edit shortcuts (p,s,t,P,b,e,y) work from all focus states
- Pickers return to exact focus state they were opened from
- Delete preview.go, use detailModel in right pane

Refs: beans-pn6z
Explains why we chose:
- Granular view states over bool (picker return)
- Backspace for nav, Esc for cancel (consistent meaning)
- Keep history stack (blocking jumps far in list)
- Keep all linked beans (needed in narrow mode)
- Only q quits (Esc does too much already)
- Edit shortcuts in detail (less friction)
- Border color for focus (simplest option)

Refs: beans-pn6z
10 tasks created:
1. Update view states to granular focus states
2. Delete preview.go and update App struct
3. Add focus parameter to list border rendering
4. Update detail.go to accept focus parameters
5. Update keyboard routing for granular focus states
6. Update View() for two-column rendering with focus
7. Update history navigation for unified view
8. Remove Esc as quit, clean up keyboard handling
9. Integrate detail model with App keyboard routing
10. Testing and polish

Refs: beans-pn6z
Fills gap in original plan:
- beansLoadedMsg creates initial detailModel
- beansChangedMsg checks new view states
- WindowSizeMsg updates detail dimensions
- Empty list handling

Refs: beans-pn6z
Updates:
- Task 2: Explicit code blocks to comment out
- Task 4: List all 7 newDetailModel call sites explicitly
- Task 6: Add moveCursorToBean helper definition (was in Task 8)
- Task 7: Complete renderDetailBodyFooter code, add statusMessage handling
- Task 8: Consolidate with Task 9 (beans-oms6 now scrapped)
- Task 9: Marked as consolidated into Task 8

Refs: beans-pn6z
- Fix viewList/viewDetail references to use new granular focus states
- Delete preview.go (no longer needed)
- Remove preview field from App struct
- Comment out preview references (will be replaced with detail)

Refs: beans-pn6z
Border color changes based on focus state:
- Primary (cyan) when focused
- Muted (gray) when not focused

Refs: beans-pn6z
- Add linksFocused/bodyFocused parameters to detailModel
- Border colors controlled by focus params
- Remove internal Tab handling (App controls focus via viewState)

Refs: beans-pn6z
- beansLoadedMsg creates initial detailModel
- beansChangedMsg checks new view states
- WindowSizeMsg updates detail dimensions
- Handle nil bean gracefully (empty list)

Refs: beans-pn6z
- Enter from list focuses detail (links if present, else body)
- Tab toggles between links and body in detail
- Backspace navigates history then returns to list
- q quits from any state (except when filtering)
- ? opens help from list/detail states
- Changed history to store bean IDs instead of detailModels
- Added moveCursorToBean helper method for navigation
- Removed old backToListMsg handling (now handled directly)

Refs: beans-pn6z
- Render both panes with focus-dependent border colors
- Footer changes based on which area is focused
- Narrow mode shows single pane at full width

Refs: beans-pn6z
- Enable cursorChangedMsg handler to recreate detail when cursor moves
- Update selectBeanMsg to move cursor and rely on cursorChangedMsg
- Simplify backspace handler to rely on cursorChangedMsg for detail updates
- Auto-switch to bodyFocused when navigating to bean with no links

Refs: beans-pn6z, beans-7vrp
- Detail handles scrolling, link navigation, edit shortcuts only
- App handles focus switching (Tab), navigation (Backspace), quit (q)
- Footer rendered by App, not detail
- Esc is for cancel/clear only (list)

Refs: beans-pn6z
- Use correct pane width instead of full terminal width
- Preserve links/body focus state after editing from detail view

Refs: beans-pn6z
- Width() sets content width, border adds 2 - use Width(m.width-2) for bordered boxes
- Remove incorrect +1 adjustments for newline separators in calculateHeaderHeight()
- Add teatest golden file tests for layout verification

Refs: beans-pn6z
- Remove DEBUG_TUI environment variable checks and /tmp file logging
- Remove unused os import from detail.go

Refs: beans-pn6z
Replace magic number 2 with named constant for border width/height calculations.

Refs: beans-pn6z
- Pass inner width (minus border) to linkDelegate instead of full pane width
- Remove extra -8 padding from title width calculation
- Use unicode ellipsis (…) instead of three dots (...) to save 2 chars

Refs: beans-ck61
- Backspace always returns to list view immediately and clears history
- Escape navigates history stack; when empty, returns to list
- Update footer to show "esc back" instead of "backspace back"
- Update tests and golden files

Refs: beans-svx7
Escape in list view was quitting the app because:
1. Bubbles list has default Quit/ForceQuit key bindings
2. Escape was being forwarded to bubbles even when not handled

Now escape/backspace return early instead of forwarding to bubbles.

Refs: beans-qqbx
When pressing Enter on a linked bean, the TUI now:
- Moves the list cursor to that bean
- Updates the detail view to show the selected bean
- Switches focus to the list view for continued navigation

moveCursorToBean() now returns a tea.Cmd that emits cursorChangedMsg,
ensuring the detail view is properly updated.

Refs: beans-d8ez
- Update key bindings table: backspace returns to list, esc navigates history
- Update History Navigation section to reflect actual behavior
- Update footer documentation: esc back instead of backspace back
- Update Design Rationale for backspace/escape semantics
- Move selectBeanMsg from list.go to tui.go (consistent with other cross-component messages)

Refs: beans-u0md
Update completed task beans and add new draft/idea beans for future work.
@sotte
Copy link
Contributor Author

sotte commented Dec 31, 2025

By Claude, in collab with Stefan.

TUI Design

Layout

Two-column layout with responsive behavior:

┌─────────────────────────────┐┌─────────────────────────────┐
│ Beans                       ││ Bean Title                  │
│                             ││ beans-xxxx  ● in-progress   │
│ ▸ beans-1234  ● todo  task  │├─────────────────────────────┤
│   beans-5678  ● done  bug   ││ Linked Beans                │
│   └─ beans-9abc ...         ││ ▸ Parent:     beans-parent  │
│                             ││   Blocked by: beans-other   │
│                             │├─────────────────────────────┤
│                             ││ ## Description              │
│                             ││                             │
│                             ││ Markdown body rendered here │
│                             ││                             │
└─────────────────────────────┘└─────────────────────────────┘
 space select · enter view · c create · / filter · ? help · q quit

Wide terminal (≥120 cols): Both panes visible, focus determines which receives input.

Narrow terminal (<120 cols): Single pane visible. List by default, detail when focused.

Navigation Workflow

Entering and Exiting Detail View

LIST ──enter──► DETAIL ──backspace──► LIST
  • From list: enter focuses detail (links section if present, otherwise body)
  • From detail: backspace returns to list

Switching Within Detail View

LINKS ◄──tab──► BODY
  • tab toggles between links and body sections

Following Linked Beans

DETAIL ──enter on link──► LIST (cursor on linked bean)
  • In links section, enter jumps to the linked bean
  • Pushes current bean to history stack
  • Focus returns to list for continued navigation

History Navigation

DETAIL ──esc──► previous bean (or LIST if history empty)
  • esc pops from history and navigates back
  • When history is empty, returns to list
  • backspace always returns to list immediately (clears history)

View States

viewListFocused        // List pane has focus
viewDetailLinksFocused // Detail pane, links section has focus
viewDetailBodyFocused  // Detail pane, body section has focus
viewTagPicker          // Tag filter picker (full screen)
viewParentPicker       // Parent picker modal
viewStatusPicker       // Status picker modal
viewTypePicker         // Type picker modal
viewBlockingPicker     // Blocking picker modal
viewPriorityPicker     // Priority picker modal
viewCreateModal        // Create bean modal
viewHelpOverlay        // Help overlay

Key Bindings

Key List Links Body
enter Focus detail Follow link -
backspace - Return to list Return to list
esc Clear selection/filter History back, then list History back, then list
tab - Switch to body Switch to links
j/k Navigate Navigate Scroll
/ Filter Filter -
space Toggle select - -
c Create bean - -
p Set parent Set parent Set parent
s Set status Set status Set status
t Set type Set type Set type
P Set priority Set priority Set priority
b Set blocking Set blocking Set blocking
e Open editor Open editor Open editor
y Copy ID Copy ID Copy ID
g t Filter by tag - -
? Help Help Help
q Quit Quit Quit

Focus Indication

Border color indicates focus:

  • Cyan (primary): Focused section receives keyboard input
  • Gray (muted): Visible but not focused

Applied to: list pane, links section, body section.

History Navigation

When following a linked bean (Enter in links section):

  1. Push current bean ID to history stack
  2. Move list cursor to target bean
  3. Detail pane updates automatically
  4. Switch to list focus

When pressing Escape in detail:

  1. If history not empty → pop and navigate to previous bean
  2. If history empty → return to list focus

When pressing Backspace in detail:

  • Always return to list immediately, clear history

Detail Pane Structure

┌─────────────────────────────┐
│ Title                       │  ← Header (always visible)
│ ID  Status  Tags            │
├─────────────────────────────┤
│ Linked Beans                │  ← Links section (if any links exist)
│ ▸ Parent:     beans-xxxx    │     Filterable list
│   Blocking:   beans-yyyy    │     Max 5 visible, scrollable
│   Blocked by: beans-zzzz    │
├─────────────────────────────┤
│                             │  ← Body section (scrollable viewport)
│ Markdown content            │     Rendered with glamour
│                             │
└─────────────────────────────┘

List Pane Structure

┌─────────────────────────────┐
│ Beans [tag: filter]         │  ← Title (shows active filter)
│                             │
│ ▸ beans-1234  ● todo  task  │  ← Tree view with hierarchy
│   beans-5678  ✓ done  bug   │     Status/type colored
│   └─ beans-9abc  ...        │     Priority indicator if set
│                             │
└─────────────────────────────┘

Features:

  • Tree view showing parent/child hierarchy
  • Filterable (/ key)
  • Multi-select (space to toggle, batch operations)
  • Tag filtering (g t chord)

Picker/Modal Behavior

When opening a picker:

  1. Save current viewState to previousState
  2. Show picker as modal over current view
  3. On close, restore previousState

This preserves exact focus location - returning from a picker opened in viewDetailLinksFocused returns to viewDetailLinksFocused.

Message Flow

Cross-component messages in tui.go:

  • cursorChangedMsg - List cursor moved, update detail pane
  • selectBeanMsg - Navigate to a bean (from link following)
  • beansChangedMsg - File watcher detected changes, refresh
  • copyBeanIDMsg - Copy ID(s) to clipboard

Component messages stay in their files:

  • beansLoadedMsg in list.go
  • backToListMsg in detail.go
  • *SelectedMsg, open*Msg, close*Msg in picker files

File Structure

internal/tui/
├── tui.go              # App model, routing, view composition
├── list.go             # List pane model
├── detail.go           # Detail pane model
├── styles.go           # Shared styles
├── keys.go             # Key binding definitions
├── help.go             # Help overlay
├── modal.go            # Modal rendering utilities
├── createmodal.go      # Create bean modal
├── tagpicker.go        # Tag filter picker
├── parentpicker.go     # Parent picker
├── statuspicker.go     # Status picker
├── typepicker.go       # Type picker
├── prioritypicker.go   # Priority picker
└── blockingpicker.go   # Blocking relationship picker

Constants

TwoColumnMinWidth = 120  // Threshold for two-column mode
RightPaneMaxWidth = 80   // Max detail pane width
borderSize = 2           // Width/height added by borders

@hmans
Copy link
Owner

hmans commented Jan 2, 2026

Just a quick heads-up that I've seen these PRs -- been busy with New Year's stuff and will continue to do so today. I'll take a look at these this weekend.

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