@@ -23,6 +23,7 @@ import (
2323 "charm.land/bubbles/v2/textarea"
2424 tea "charm.land/bubbletea/v2"
2525 "charm.land/lipgloss/v2"
26+ "github.com/atotto/clipboard"
2627 "github.com/charmbracelet/catwalk/pkg/catwalk"
2728 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
2829 "github.com/charmbracelet/crush/internal/app"
@@ -106,6 +107,9 @@ type (
106107
107108 // closeDialogMsg is sent to close the current dialog.
108109 closeDialogMsg struct{}
110+
111+ // copyChatHighlightMsg is sent to copy the current chat highlight to clipboard.
112+ copyChatHighlightMsg struct{}
109113)
110114
111115// UI represents the main user interface model.
@@ -203,6 +207,9 @@ type UI struct {
203207 // Todo spinner
204208 todoSpinner spinner.Model
205209 todoIsSpinning bool
210+
211+ // mouse highlighting related state
212+ lastClickTime time.Time
206213}
207214
208215// New creates a new instance of the [UI] model.
@@ -484,6 +491,8 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
484491 m.keyMap.Models.SetHelp("ctrl+m", "models")
485492 m.keyMap.Editor.Newline.SetHelp("shift+enter", "newline")
486493 }
494+ case copyChatHighlightMsg:
495+ cmds = append(cmds, m.copyChatHighlight())
487496 case tea.MouseClickMsg:
488497 switch m.state {
489498 case uiChat:
@@ -491,7 +500,9 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
491500 // Adjust for chat area position
492501 x -= m.layout.main.Min.X
493502 y -= m.layout.main.Min.Y
494− m.chat.HandleMouseDown(x, y)
503+ if m.chat.HandleMouseDown(x, y) {
504+ m.lastClickTime = time.Now()
505+ }
495506 }
496507
497508 case tea.MouseMotionMsg:
@@ -527,13 +538,22 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
527538 }
528539
529540 case tea.MouseReleaseMsg:
541+ const doubleClickThreshold = 500 * time.Millisecond
542+
530543 switch m.state {
531544 case uiChat:
532545 x, y := msg.X, msg.Y
533546 // Adjust for chat area position
534547 x -= m.layout.main.Min.X
535548 y -= m.layout.main.Min.Y
536− m.chat.HandleMouseUp(x, y)
549+ if m.chat.HandleMouseUp(x, y) {
550+ cmds = append(cmds, tea.Tick(doubleClickThreshold, func(t time.Time) tea.Msg {
551+ if time.Since(m.lastClickTime) >= doubleClickThreshold {
552+ return copyChatHighlightMsg{}
553+ }
554+ return nil
555+ }))
556+ }
537557 }
538558 case tea.MouseWheelMsg:
539559 // Pass mouse events to dialogs first if any are open.
@@ -2844,6 +2864,22 @@ func (m *UI) runMCPPrompt(clientID, promptID string, arguments map[string]string
28442864 return tea.Sequence(cmds...)
28452865}
28462866
2867+func (m *UI) copyChatHighlight() tea.Cmd {
2868+ text := m.chat.HighlighContent()
2869+ return tea.Sequence(
2870+ tea.SetClipboard(text),
2871+ func() tea.Msg {
2872+ _ = clipboard.WriteAll(text)
2873+ return nil
2874+ },
2875+ func() tea.Msg {
2876+ m.chat.ClearMouse()
2877+ return nil
2878+ },
2879+ uiutil.ReportInfo("Selected text copied to clipboard"),
2880+ )
2881+}
2882+
28472883// renderLogo renders the Crush logo with the given styles and dimensions.
28482884func renderLogo(t *styles.Styles, compact bool, width int) string {
28492885 return logo.Render(version.Version, compact, logo.Opts{