Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ Globally search and resume [Claude Code](https://claude.ai/claude-code) conversa
- Delete conversations with confirmation prompt
- Prune bloated conversations losslessly (`ccs prune`)
- Pass flags through to `claude` (e.g., `--plan`)
- Mouse wheel scrolling support

## Installation

Expand Down Expand Up @@ -82,7 +81,6 @@ ccs buyer -- --plan
- `Ctrl+D` - Delete selected conversation (with confirmation)
- `Ctrl+R` - Prune selected conversation - shrink it losslessly (with confirmation)
- `Ctrl+J/K` - Scroll preview
- `Mouse wheel` - Scroll list or preview (context-aware)
- `Ctrl+U` - Clear search
- `Esc` / `Ctrl+C` - Quit

Expand Down
42 changes: 7 additions & 35 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,10 @@ type model struct {
previewScroll int
width int
height int
listHeight int // Calculated list height for mouse detection
listHeight int // Calculated visible list height
selected *Conversation
quitting bool
claudeFlags []string
mouseInPreview bool // Track if mouse is in preview area
confirmDelete bool // Are we in delete confirmation mode?
deleteIndex int // Index of item to delete
confirmPrune bool // Are we in prune confirmation mode?
Expand Down Expand Up @@ -144,43 +143,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
// Calculate list height for mouse detection
// Calculate visible list height
m.listHeight = m.height * 30 / 100
if m.listHeight < 3 {
m.listHeight = 3
}
// Clear so a shrink doesn't leave wider stale rows behind.
return m, tea.ClearScreen

case tea.MouseMsg:
// Determine if mouse is in preview area (below list + separator)
listAreaHeight := 2 + m.listHeight // search line + separator + list
m.mouseInPreview = msg.Y > listAreaHeight

switch msg.Button {
case tea.MouseButtonWheelUp:
if m.mouseInPreview {
m.previewScroll = max(0, m.previewScroll-3)
} else {
if m.cursor > 0 {
m.cursor--
m.previewScroll = 0
}
}
return m, nil
case tea.MouseButtonWheelDown:
if m.mouseInPreview {
m.previewScroll = min(m.previewScroll+3, m.maxPreviewScroll())
} else {
if m.cursor < len(m.filtered)-1 {
m.cursor++
m.previewScroll = 0
}
}
return m, nil
}
return m, nil

case tea.KeyMsg:
// Handle delete confirmation mode
if m.confirmDelete {
Expand Down Expand Up @@ -1314,7 +1284,6 @@ Key bindings:
Ctrl+D Delete conversation (with confirmation)
Ctrl+R Prune conversation - shrink it losslessly (with confirmation)
Ctrl+J/K Scroll preview
Mouse wheel Scroll list or preview (based on position)
Ctrl+U Clear search
Esc, Ctrl+C Quit

Expand Down Expand Up @@ -1432,9 +1401,12 @@ func main() {
os.Exit(1)
}

// Run TUI
// Run TUI. Mouse reporting is intentionally NOT enabled: under a heavy
// frame the terminal emits mouse-wheel reports faster than bubbletea reads
// them, and the fragmented sequences leak into the search box as text.
// Scrolling is keyboard-only (arrows / Ctrl+J/K / PgUp/PgDn).
m := initialModel(items, filterQuery, claudeFlags)
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
p := tea.NewProgram(m, tea.WithAltScreen())

finalModel, err := p.Run()
if err != nil {
Expand Down
54 changes: 0 additions & 54 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1096,65 +1096,11 @@ func TestRenderPreviewLongMultibyteMessageStaysValidUTF8(t *testing.T) {
t.Error("preview of a long multibyte message produced invalid UTF-8")
}
}

func TestUpdateMouseScroll(t *testing.T) {
// Give each conversation enough messages that the preview is scrollable.
msgs := make([]Message, 10)
for i := range msgs {
msgs[i] = Message{Role: "user", Text: "message text line", Ts: "2024-01-15T10:00:00Z"}
}
items := []listItem{
{conv: Conversation{SessionID: "test-1", Messages: msgs}, searchText: "first"},
{conv: Conversation{SessionID: "test-2", Messages: msgs}, searchText: "second"},
{conv: Conversation{SessionID: "test-3", Messages: msgs}, searchText: "third"},
}

m := initialModel(items, "", nil)

// Set window size first to initialize mouse tracking
result, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 30})
m = result.(model)

// Mouse wheel up in list area (Y=5, which should be in list)
m.mouseInPreview = false // Explicitly set for test
result, _ = m.Update(tea.MouseMsg{Button: tea.MouseButtonWheelUp, Y: 5})
m = result.(model)
// Should not move cursor from 0
if m.cursor != 0 {
t.Errorf("wheel up at top should keep cursor at 0, got %d", m.cursor)
}

// Move to second item
result, _ = m.Update(tea.KeyMsg{Type: tea.KeyDown})
m = result.(model)
if m.cursor != 1 {
t.Errorf("should be at cursor 1, got %d", m.cursor)
}

// Mouse wheel down in list area should move cursor forward
m.mouseInPreview = false // Ensure we're scrolling list not preview
result, _ = m.Update(tea.MouseMsg{Button: tea.MouseButtonWheelDown, Y: 5})
m = result.(model)
if m.cursor != 2 {
t.Errorf("wheel down should move to cursor 2, got %d", m.cursor)
}

// Mouse wheel in preview area
m.previewScroll = 0
m.mouseInPreview = true
result, _ = m.Update(tea.MouseMsg{Button: tea.MouseButtonWheelDown, Y: 20})
m = result.(model)
if m.previewScroll != 3 {
t.Errorf("wheel down in preview should scroll preview, got %d", m.previewScroll)
}
}

func TestPreviewScrollClampedToContent(t *testing.T) {
conv := Conversation{SessionID: "s1", Messages: []Message{
{Role: "user", Text: "only message", Ts: "2024-01-15T10:00:00Z"},
}}
m := initialModel([]listItem{{conv: conv}}, "", nil)
m.mouseInPreview = true

maxScroll := m.maxPreviewScroll()

Expand Down
Loading