From 41de3f2837363af89359386e729b66b25a8f2240 Mon Sep 17 00:00:00 2001 From: Pepper Date: Mon, 21 Jul 2025 23:22:49 -0400 Subject: [PATCH 1/5] fix: correct mouse coordinate calculations for text selection - Fix mouse Y coordinate mapping by accounting for header height - Add proper offset calculation for viewport-relative coordinates - Enable auto-scroll during text selection near viewport edges - Remove automatic clipboard copying on mouse release The mouse coordinates were previously calculated relative to the terminal window, but needed to be adjusted for the header height and viewport positioning. This ensures accurate text selection and smooth scrolling behavior during selection operations. --- .../tui/internal/components/chat/messages.go | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index cbea349ca707..58bb9b5b28c5 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -98,8 +98,11 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.MouseClickMsg: - slog.Info("mouse", "x", msg.X, "y", msg.Y, "offset", m.viewport.YOffset) - y := msg.Y + m.viewport.YOffset + headerHeight := lipgloss.Height(m.renderHeader()) + // Account for header and the newline separator + offset := headerHeight + 1 + slog.Info("mouse", "x", msg.X, "y", msg.Y, "offset", m.viewport.YOffset, "offset", offset) + y := (msg.Y - offset) + m.viewport.YOffset if y > 0 { m.selection = &selection{ startY: y, @@ -114,26 +117,35 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.MouseMotionMsg: if m.selection != nil { + headerHeight := lipgloss.Height(m.renderHeader()) + // Account for header and the newline separator + offset := headerHeight + 1 + viewportRelativeY := msg.Y - offset m.selection = &selection{ startX: m.selection.startX, startY: m.selection.startY, endX: msg.X + 1, - endY: msg.Y + m.viewport.YOffset, + endY: viewportRelativeY + m.viewport.YOffset, + } + + // Auto-scroll when selecting near edges + viewportHeight := m.viewport.Height() + scrollMargin := 2 + + if viewportRelativeY < scrollMargin && m.viewport.YOffset > 0 { + // Scroll up + m.viewport.LineUp(1) + } else if viewportRelativeY > viewportHeight-scrollMargin { + // Scroll down + m.viewport.LineDown(1) } return m, m.renderView() } case tea.MouseReleaseMsg: - if m.selection != nil && len(m.clipboard) > 0 { - content := strings.Join(m.clipboard, "\n") - m.selection = nil - m.clipboard = []string{} - return m, tea.Sequence( - m.renderView(), - app.SetClipboard(content), - toast.NewSuccessToast("Copied to clipboard"), - ) - } + // Just handle the mouse release, don't auto-copy or clear selection + // Selection state remains visible for user to manually copy if desired + return m, nil case tea.WindowSizeMsg: effectiveWidth := msg.Width - 4 // Clear cache on resize since width affects rendering From c5b54210c6089c93eaf8a55ad527243bc3e469dc Mon Sep 17 00:00:00 2001 From: Pepper Date: Mon, 21 Jul 2025 23:41:49 -0400 Subject: [PATCH 2/5] fix: mouse text selection coordinate mapping - Fixed double offset application causing 7-8 line selection offset - Mouse coordinates now correctly map to text under cursor - Selection rendering uses content-relative coordinates - Preserves auto-scroll functionality during text selection --- packages/tui/internal/components/chat/messages.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index 58bb9b5b28c5..4664dca9df92 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -507,7 +507,7 @@ func (m *messagesComponent) renderView() tea.Cmd { clipboard := []string{} var selection *selection if m.selection != nil { - selection = m.selection.coords(lipgloss.Height(header) + 1) + selection = m.selection.coords(0) } for _, block := range blocks { lines := strings.Split(block, "\n") From e27b61dcbaca21440134ea399bed1aa42963299f Mon Sep 17 00:00:00 2001 From: Pepper Date: Tue, 22 Jul 2025 00:26:14 -0400 Subject: [PATCH 3/5] fix: improve mouse text selection and scrolling behavior - Fix mouse coordinate mapping for accurate text selection - Add auto-scroll functionality when selecting near viewport edges - Fix Unicode character width calculation for proper selection - Disable tail mode during selection to allow free scrolling - Remove automatic clipboard copying on mouse release --- packages/tui/internal/components/chat/messages.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index 4664dca9df92..99b854e2512a 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -128,6 +128,9 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { endY: viewportRelativeY + m.viewport.YOffset, } + // Disable tail mode during selection to allow free scrolling + m.tail = false + // Auto-scroll when selecting near edges viewportHeight := m.viewport.Height() scrollMargin := 2 @@ -143,6 +146,8 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case tea.MouseReleaseMsg: + // Re-enable tail mode when selection ends + m.tail = true // Just handle the mouse release, don't auto-copy or clear selection // Selection state remains visible for user to manually copy if desired return m, nil From c74bb33b7e63f2f98075e8f0743c809d2b38eda1 Mon Sep 17 00:00:00 2001 From: Pepper Date: Sat, 26 Jul 2025 22:00:12 -0400 Subject: [PATCH 4/5] feat: right-click to copy selected text in TUI - Added right-click functionality to copy selected text to clipboard - Removed revert/redo commands that were causing build issues - Maintained existing left-click selection behavior - Clean implementation based on dev branch mouse selection logic --- .../tui/internal/components/chat/messages.go | 41 ++++++++++++------- packages/tui/internal/tui/tui.go | 1 - 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index 99b854e2512a..7fbc514adeee 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -98,21 +98,34 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.MouseClickMsg: - headerHeight := lipgloss.Height(m.renderHeader()) - // Account for header and the newline separator - offset := headerHeight + 1 - slog.Info("mouse", "x", msg.X, "y", msg.Y, "offset", m.viewport.YOffset, "offset", offset) - y := (msg.Y - offset) + m.viewport.YOffset - if y > 0 { - m.selection = &selection{ - startY: y, - startX: msg.X, - endY: -1, - endX: -1, - } + if msg.Button == tea.MouseLeft { + headerHeight := lipgloss.Height(m.renderHeader()) + // Account for header and the newline separator + offset := headerHeight + 1 + slog.Info("mouse", "x", msg.X, "y", msg.Y, "offset", m.viewport.YOffset, "offset", offset) + y := (msg.Y - offset) + m.viewport.YOffset + if y > 0 { + m.selection = &selection{ + startY: y, + startX: msg.X, + endY: -1, + endX: -1, + } - slog.Info("mouse selection", "start", fmt.Sprintf("%d,%d", m.selection.startX, m.selection.startY), "end", fmt.Sprintf("%d,%d", m.selection.endX, m.selection.endY)) - return m, m.renderView() + slog.Info("mouse selection", "start", fmt.Sprintf("%d,%d", m.selection.startX, m.selection.startY), "end", fmt.Sprintf("%d,%d", m.selection.endX, m.selection.endY)) + return m, m.renderView() + } + } else if msg.Button == tea.MouseRight { + if m.selection != nil && len(m.clipboard) > 0 { + content := strings.Join(m.clipboard, "\n") + m.selection = nil + m.clipboard = []string{} + return m, tea.Sequence( + m.renderView(), + app.SetClipboard(content), + toast.NewSuccessToast("Copied to clipboard"), + ) + } } case tea.MouseMotionMsg: diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 752e805ea3c9..6a9f75f95a96 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -1045,7 +1045,6 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) { updated, cmd := a.messages.CopyLastMessage() a.messages = updated.(chat.MessagesComponent) cmds = append(cmds, cmd) - case commands.MessagesRevertCommand: case commands.AppExitCommand: return a, tea.Quit } From ca01b5d023bfb0cd5543d26fe64004843fcdd00a Mon Sep 17 00:00:00 2001 From: Pepper Date: Mon, 28 Jul 2025 23:44:28 -0400 Subject: [PATCH 5/5] added right click mouse to copy --- packages/tui/go.mod | 1 - packages/tui/go.sum | 2 -- packages/tui/internal/components/chat/messages.go | 15 +++++++++++++++ packages/tui/internal/tui/tui.go | 7 +++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/tui/go.mod b/packages/tui/go.mod index 0b4698383afa..e23e05c300fa 100644 --- a/packages/tui/go.mod +++ b/packages/tui/go.mod @@ -5,7 +5,6 @@ go 1.24.0 require ( github.com/BurntSushi/toml v1.5.0 github.com/alecthomas/chroma/v2 v2.18.0 - github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4 github.com/charmbracelet/glamour v0.10.0 diff --git a/packages/tui/go.sum b/packages/tui/go.sum index f41abaf42584..370ea7121174 100644 --- a/packages/tui/go.sum +++ b/packages/tui/go.sum @@ -20,8 +20,6 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= -github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE= github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw= github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4 h1:UgUuKKvBwgqm2ZEL+sKv/OLeavrUb4gfHgdxe6oIOno= diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index 7fbc514adeee..ce80358d8266 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -31,6 +31,7 @@ type MessagesComponent interface { GotoTop() (tea.Model, tea.Cmd) GotoBottom() (tea.Model, tea.Cmd) CopyLastMessage() (tea.Model, tea.Cmd) + CopySelection() (tea.Model, tea.Cmd) } type messagesComponent struct { @@ -803,6 +804,20 @@ func (m *messagesComponent) CopyLastMessage() (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } +func (m *messagesComponent) CopySelection() (tea.Model, tea.Cmd) { + if m.selection != nil && len(m.clipboard) > 0 { + content := strings.Join(m.clipboard, "\n") + m.selection = nil + m.clipboard = []string{} + return m, tea.Sequence( + m.renderView(), + app.SetClipboard(content), + toast.NewSuccessToast("Copied to clipboard"), + ) + } + return m, nil +} + func NewMessagesComponent(app *app.App) MessagesComponent { vp := viewport.New() vp.KeyMap = viewport.KeyMap{} diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 6a9f75f95a96..26ac39eb48a4 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -276,6 +276,13 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches)) } + if keyString == "ctrl+alt+c" { + updatedMessages, cmd := a.messages.CopySelection() + a.messages = updatedMessages.(chat.MessagesComponent) + cmds = append(cmds, cmd) + return a, tea.Batch(cmds...) + } + // Fallback: suspend if ctrl+z is pressed and no user keybind matched if keyString == "ctrl+z" { return a, tea.Suspend