Commit Graph

12 Commits

Author SHA1 Message Date
Ed Zynda 30f2bc243d fix(ui): correct mouse selection drift with extension widgets
- Match View() and getItemAndLineAtY() row counts for empty items so
  streaming-reasoning placeholders no longer offset hit-testing by one
  row each (exposed when extension widgets like subagent-monitor shrink
  the scrollback).
- Honor IsLineInRange's endCol=-1 'to end of line' sentinel in
  HighlightLine and ExtractText so the start row of a multi-line drag
  actually renders highlighted and is included in clipboard copies.
- Add regression tests for both invariants in scrolllist and selection.
2026-05-16 13:48:51 +03:00
Ed Zynda a322dfc59a fix(ui): eliminate mouse copy-selection drift during streaming
- Lock viewport scroll while a drag-select is active so highlighted
  content stays under the cursor (SetItems, appendStreamingChunk,
  MouseWheelDown all now honor IsMouseDown).
- HandleMouseDrag defensively clears autoScroll on every update so a
  racy re-enable can't shift the row mid-drag.
- Recompute scrollback yOffset/viewport height on each mouse event
  via currentScrollbackBounds() instead of relying on stale values
  cached during the previous View() pass.
- Account for canceling/ctrlCPressedOnce warning rows in
  distributeHeight and mark layoutDirty when those flags toggle so
  the height budget and mouse origin stay in sync.
- Add ScrollList regression tests covering the three invariants.
2026-05-15 13:30:57 +03:00
Ed Zynda d2e23295b6 perf(ui): cache item heights in ScrollList to eliminate redundant renders
- Add heightCache map to ScrollList, keyed by item ID, avoiding
  repeated Render() calls purely to count lines
- Rewrite GotoBottom() to walk backwards from the end in O(visible)
  instead of two full O(N) forward passes over all items
- Replace all height-only Render() calls in clampOffset(), AtBottom(),
  ScrollBy(), and ScrollPercent() with cached itemHeight() lookups
- Invalidate cache on width changes (SetWidth) and item mutations
  (AppendChunk, AppendStdout/Stderr via InvalidateItemHeight)
- Refresh cache entries in View() from authoritative renders
2026-04-23 12:03:44 +03:00
Ed Zynda 8e3cfeede5 fix(ui): correct mouse selection Y-offset for reasoning blocks
The getItemAndLineAtY() method was using item.Height() which returns 0
for reasoning blocks (StreamingMessageItem with role='reasoning') because
their render cache is intentionally never populated (they include a live
duration timer).

This caused all items below a reasoning block to have incorrect Y
coordinates — clicking on the reasoning text would highlight the
assistant text below it instead.

Two fixes:
1. getItemAndLineAtY() now uses renderedHeight() which calls Render()
   and counts lines — matching exactly what View() does. This is the
   single source of truth for item height during hit-testing.

2. StreamingMessageItem.Height() now falls back to Render(0) when
   cachedRender is empty, fixing the same issue for other callers
   (GotoBottom, ScrollBy, clampOffset, etc.).
2026-04-01 18:15:04 +03:00
Ed Zynda 4fa5775974 feat(ui): implement character-level mouse text selection and copy
Implement crush-style mouse text selection with character-level precision,
replacing the previously disabled stub implementation.

Architecture:
- New selection package (internal/ui/selection/) handles all coordinate
  math, word boundary detection, and cell-level ANSI text manipulation
- ScrollList upgraded with proper mouse down/drag/up flow supporting
  single click (character drag), double click (word), triple click (line)
- Model.go wires BubbleTea mouse events through to ScrollList with
  proper viewport Y-offset adjustment for the scrollback area

Key features:
- Character-level selection using ultraviolet ScreenBuffer for ANSI-aware
  cell parsing — correctly handles styled text, emoji, CJK wide chars
- Word selection via UAX#29 Unicode segmentation (clipperhouse/uax29)
- Display-width-aware columns via clipperhouse/displaywidth (not bytes)
- Dual clipboard: OSC 52 (remote terminals) + native (atotto/clipboard)
- Multi-click detection with 400ms threshold and 2px tolerance
- Mouse event throttling via existing MouseModeCellMotion
- Selection cleared on any keypress for clean UX

Dependencies (all already indirect in go.mod):
- github.com/charmbracelet/ultraviolet (ScreenBuffer, cell manipulation)
- github.com/charmbracelet/x/ansi (ANSI strip, StringWidth)
- github.com/clipperhouse/displaywidth (grapheme display width)
- github.com/clipperhouse/uax29/v2 (Unicode word segmentation)
2026-04-01 18:05:48 +03:00
Ed Zynda 28d2de8f39 Phase 1: Reorganize UI leaf utilities into subpackages
Moved leaf utility files to subpackages for better organization:
- events.go -> core/ (core message types)
- clipboard.go -> clipboard/ (clipboard operations)
- commands.go -> commands/ (slash commands)
- file_processor.go -> fileutil/ (file attachment processing)
- preferences.go -> prefs/ (theme/model preferences)
- enhanced_styles.go, styles.go, themes.go -> style/ (theming system)

Added exports.go to re-export commonly used types for backward
compatibility. External importers can still use ui.XXX without
changes.

All tests pass, basic smoke test successful.
2026-04-01 13:54:10 +03:00
Ed Zynda 8159431ce4 Prevent scrolling past bottom of content in ScrollList
Enhanced clampOffset() to detect when the viewport has scrolled past the
bottom of the content (would show empty space) and automatically reposition
to show the last line of content at the bottom of the viewport.

This prevents the 'floating' effect where multiple PgDn or scroll down
operations would push content off the top while showing blank space below.

The clamping logic:
1. Calculates total content height
2. If content fits in viewport, forces position to top
3. Otherwise, checks if remaining content < viewport height
4. If so, repositions to show exactly the last line at viewport bottom

Also updated clampOffset to use rendered height calculation (handles
non-cached items like reasoning blocks) instead of cached Height().
2026-03-31 18:56:18 +03:00
Ed Zynda 9f9f265fb3 Fix autoscroll for streaming messages (iteratr pattern)
Root cause: GotoBottom() was calculating heights using Height() which returns
0 for non-cached items. Reasoning blocks never cache renders due to live
duration updates, causing incorrect scroll calculations during reasoning →
assistant transitions.

Fix: Calculate heights directly from rendered strings instead of relying on
cached Height() values. This ensures accurate scroll positioning for all
message types.

Changes:
- ScrollList.GotoBottom(): Render items and calculate height from string
- ScrollList.AtBottom(): Same pattern for bottom detection
- appendStreamingChunk(): Call GotoBottom() directly for existing messages
- refreshContent(): Remove redundant GotoBottom() (handled by SetItems)

Tested with 'explore this repo' prompt - autoscroll now works correctly
throughout reasoning and assistant streaming phases.
2026-03-31 18:53:18 +03:00
Ed Zynda 290c5a4774 chore: disable select/copy functionality but keep plumbing
Disable the mouse selection and keyboard copy features while keeping
all the supporting code infrastructure:

- Comment out MouseClickMsg, MouseMotionMsg, MouseReleaseMsg handlers
- Comment out keyboard shortcuts (c/y keys) for copying
- Keep all ScrollList selection tracking code
- Keep clipboard utilities (clipboard.go)
- Keep highlighting functions in scrolllist.go

This allows the features to be easily re-enabled later while keeping
the codebase clean for now.
2026-03-31 16:29:01 +03:00
Ed Zynda 287d60c31e feat: add visual selection highlighting with theme colors
Implement visual feedback for text selection in the scrollback:

- Add isLineInSelection() to check if a line is within the current selection
- Add applyHighlight() using the theme's Highlight color for selected lines
- Add applyFocusIndicator() using MutedBorder for focused items
- Update View() to apply highlighting during rendering
- Add getItemAndLineAtY() for precise mouse position tracking
- Track both item index and line index within item for selection

Selection highlighting uses the user's selected theme colors for
consistent visual feedback across all themes.
2026-03-31 16:23:46 +03:00
Ed Zynda 3d45d98895 feat: add crush-style copy+paste support
Implement mouse selection and keyboard copy functionality following
crush's patterns:

- Add clipboard.go with dual-write clipboard support (OSC 52 + system)
- Add CopySelection tracking to ScrollList for text selection
- Implement HandleMouseDown/HandleMouseDrag/HandleMouseUp methods
- Add keyboard shortcuts (c/y) for copying messages
- Mouse click+drag to select text, auto-copy on release
- Toast notifications for copy feedback

Note: Full text extraction from selection requires additional work to
properly extract raw text from styled message content.
2026-03-31 16:19:58 +03:00
Ed Zynda db4be4f9a2 feat: implement full alt screen mode with in-memory scrollback
Add ScrollList component for viewport-based message history with lazy
rendering and offset-based scrolling. Implement MessageItem system for
user, assistant, tool, system, and error messages with pre-rendered
styled content from MessageRenderer.

Key changes:
- ScrollList: height-constrained viewport with itemGap support, padding
  to ensure fixed height for sticky bottom layout
- MessageItem implementations with preRendered content from MessageRenderer
- refreshContent() pattern for efficient ScrollList updates
- Mouse wheel scrolling (3 lines per tick) with auto-scroll behavior
- All message types (user, assistant, tool, system, error, extension)
  properly added to in-memory scrollback
- PgUp/PgDn/Alt+Home/Alt+End keybindings for navigation
- Removed tea.Println() calls for alt screen compatibility
- Sticky bottom layout: input, separator, status bar fixed at bottom

Files added:
- internal/ui/scrolllist.go (ScrollList component)
- internal/ui/message_items.go (MessageItem implementations)

Files modified:
- internal/ui/model.go (main integration)
- internal/ui/*.go (alt screen config for components)
2026-03-31 16:12:30 +03:00