@@ -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"
@@ -106,6 +107,9 @@ type (
107 // closeDialogMsg is sent to close the current dialog.
108 // closeDialogMsg is sent to close the current dialog.
108 closeDialogMsg struct{}
109 closeDialogMsg struct{}
111+ // copyChatHighlightMsg is sent to copy the current chat highlight to clipboard.
112+ copyChatHighlightMsg struct{}
111// UI represents the main user interface model.
115// UI represents the main user interface model.
@@ -203,6 +207,9 @@ type UI struct {
203 // Todo spinner
207 // Todo spinner
204 todoSpinner spinner.Model
208 todoSpinner spinner.Model
205 todoIsSpinning bool
209 todoIsSpinning bool
211+ // mouse highlighting related state
212+ lastClickTime time.Time
208// New creates a new instance of the [UI] model.
215// New creates a new instance of the [UI] model.
@@ -484,6 +491,8 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
484 m.keyMap.Models.SetHelp("ctrl+m", "models")
491 m.keyMap.Models.SetHelp("ctrl+m", "models")
485 m.keyMap.Editor.Newline.SetHelp("shift+enter", "newline")
492 m.keyMap.Editor.Newline.SetHelp("shift+enter", "newline")
494+ case copyChatHighlightMsg:
495+ cmds = append(cmds, m.copyChatHighlight())
487 case tea.MouseClickMsg:
496 case tea.MouseClickMsg:
488 switch m.state {
497 switch m.state {
489 case uiChat:
498 case uiChat:
@@ -491,7 +500,9 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
491 // Adjust for chat area position
500 // Adjust for chat area position
492 x -= m.layout.main.Min.X
501 x -= m.layout.main.Min.X
493 y -= m.layout.main.Min.Y
502 y -= m.layout.main.Min.Y
494− m.chat.HandleMouseDown(x, y)
503+ if m.chat.HandleMouseDown(x, y) {
504+ m.lastClickTime = time.Now()
497 case tea.MouseMotionMsg:
508 case tea.MouseMotionMsg:
@@ -527,13 +538,22 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
529 case tea.MouseReleaseMsg:
540 case tea.MouseReleaseMsg:
541+ const doubleClickThreshold = 500 * time.Millisecond
530 switch m.state {
543 switch m.state {
531 case uiChat:
544 case uiChat:
532 x, y := msg.X, msg.Y
545 x, y := msg.X, msg.Y
533 // Adjust for chat area position
546 // Adjust for chat area position
534 x -= m.layout.main.Min.X
547 x -= m.layout.main.Min.X
535 y -= m.layout.main.Min.Y
548 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{}
538 case tea.MouseWheelMsg:
558 case tea.MouseWheelMsg:
539 // Pass mouse events to dialogs first if any are open.
559 // 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
2844 return tea.Sequence(cmds...)
2864 return tea.Sequence(cmds...)
2867+func (m *UI) copyChatHighlight() tea.Cmd {
2868+ text := m.chat.HighlighContent()
2869+ return tea.Sequence(
2870+ tea.SetClipboard(text),
2872+ _ = clipboard.WriteAll(text)
2876+ m.chat.ClearMouse()
2879+ uiutil.ReportInfo("Selected text copied to clipboard"),
2847// renderLogo renders the Crush logo with the given styles and dimensions.
2883// renderLogo renders the Crush logo with the given styles and dimensions.
2848func renderLogo(t *styles.Styles, compact bool, width int) string {
2884func renderLogo(t *styles.Styles, compact bool, width int) string {
2849 return logo.Render(version.Version, compact, logo.Opts{
2885 return logo.Render(version.Version, compact, logo.Opts{