@@ -23,6 +23,7 @@ import (
23 "charm.land/bubbles/v2/textarea"
23 "charm.land/bubbles/v2/textarea"
24 tea "charm.land/bubbletea/v2"
24 tea "charm.land/bubbletea/v2"
25 "charm.land/lipgloss/v2"
25 "charm.land/lipgloss/v2"
26+ "github.com/atotto/clipboard"
26 "github.com/charmbracelet/catwalk/pkg/catwalk"
27 "github.com/charmbracelet/catwalk/pkg/catwalk"
27 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
28 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
28 "github.com/charmbracelet/crush/internal/app"
29 "github.com/charmbracelet/crush/internal/app"
@@ -103,6 +104,9 @@ type (
104 // closeDialogMsg is sent to close the current dialog.
105 // closeDialogMsg is sent to close the current dialog.
105 closeDialogMsg struct{}
106 closeDialogMsg struct{}
108+ // copyChatHighlightMsg is sent to copy the current chat highlight to clipboard.
109+ copyChatHighlightMsg struct{}
108// UI represents the main user interface model.
112// UI represents the main user interface model.
@@ -200,6 +204,9 @@ type UI struct {
200 // Todo spinner
204 // Todo spinner
201 todoSpinner spinner.Model
205 todoSpinner spinner.Model
202 todoIsSpinning bool
206 todoIsSpinning bool
208+ // mouse highlighting related state
209+ lastClickTime time.Time
205// New creates a new instance of the [UI] model.
212// New creates a new instance of the [UI] model.
@@ -481,6 +488,8 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
481 m.keyMap.Models.SetHelp("ctrl+m", "models")
488 m.keyMap.Models.SetHelp("ctrl+m", "models")
482 m.keyMap.Editor.Newline.SetHelp("shift+enter", "newline")
489 m.keyMap.Editor.Newline.SetHelp("shift+enter", "newline")
491+ case copyChatHighlightMsg:
492+ cmds = append(cmds, m.copyChatHighlight())
484 case tea.MouseClickMsg:
493 case tea.MouseClickMsg:
485 switch m.state {
494 switch m.state {
486 case uiChat:
495 case uiChat:
@@ -488,7 +497,9 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
488 // Adjust for chat area position
497 // Adjust for chat area position
489 x -= m.layout.main.Min.X
498 x -= m.layout.main.Min.X
490 y -= m.layout.main.Min.Y
499 y -= m.layout.main.Min.Y
491− m.chat.HandleMouseDown(x, y)
500+ if m.chat.HandleMouseDown(x, y) {
501+ m.lastClickTime = time.Now()
494 case tea.MouseMotionMsg:
505 case tea.MouseMotionMsg:
@@ -524,13 +535,22 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
526 case tea.MouseReleaseMsg:
537 case tea.MouseReleaseMsg:
538+ const doubleClickThreshold = 500 * time.Millisecond
527 switch m.state {
540 switch m.state {
528 case uiChat:
541 case uiChat:
529 x, y := msg.X, msg.Y
542 x, y := msg.X, msg.Y
530 // Adjust for chat area position
543 // Adjust for chat area position
531 x -= m.layout.main.Min.X
544 x -= m.layout.main.Min.X
532 y -= m.layout.main.Min.Y
545 y -= m.layout.main.Min.Y
533− m.chat.HandleMouseUp(x, y)
546+ if m.chat.HandleMouseUp(x, y) {
547+ cmds = append(cmds, tea.Tick(doubleClickThreshold, func(t time.Time) tea.Msg {
548+ if time.Since(m.lastClickTime) >= doubleClickThreshold {
549+ return copyChatHighlightMsg{}
535 case tea.MouseWheelMsg:
555 case tea.MouseWheelMsg:
536 // Pass mouse events to dialogs first if any are open.
556 // Pass mouse events to dialogs first if any are open.
@@ -2842,6 +2862,22 @@ func (m *UI) runMCPPrompt(clientID, promptID string, arguments map[string]string
2842 return tea.Sequence(cmds...)
2862 return tea.Sequence(cmds...)
2865+func (m *UI) copyChatHighlight() tea.Cmd {
2866+ text := m.chat.HighlighContent()
2867+ return tea.Sequence(
2868+ tea.SetClipboard(text),
2870+ _ = clipboard.WriteAll(text)
2874+ m.chat.ClearMouse()
2877+ uiutil.ReportInfo("Selected text copied to clipboard"),
2845// renderLogo renders the Crush logo with the given styles and dimensions.
2881// renderLogo renders the Crush logo with the given styles and dimensions.
2846func renderLogo(t *styles.Styles, compact bool, width int) string {
2882func renderLogo(t *styles.Styles, compact bool, width int) string {
2847 return logo.Render(version.Version, compact, logo.Opts{
2883 return logo.Render(version.Version, compact, logo.Opts{