Detailed changes
  
  
    
    @@ -10,16 +10,16 @@ require (
 	github.com/anthropics/anthropic-sdk-go v1.6.2
 	github.com/atotto/clipboard v0.1.4
 	github.com/aymanbagabas/go-udiff v0.3.1
-	github.com/bmatcuk/doublestar/v4 v4.9.0
+	github.com/bmatcuk/doublestar/v4 v4.9.1
 	github.com/charlievieth/fastwalk v1.0.11
 	github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5
-	github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250730165737-56ff7146d52d
-	github.com/charmbracelet/catwalk v0.4.5
+	github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250805190305-70e94a2e0b2d
+	github.com/charmbracelet/catwalk v0.4.6
 	github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674
 	github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
 	github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250721205738-ea66aa652ee0
 	github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706
-	github.com/charmbracelet/x/ansi v0.9.3
+	github.com/charmbracelet/x/ansi v0.10.0
 	github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3
 	github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a
 	github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
@@ -75,7 +75,7 @@ require (
 	github.com/bahlo/generic-list-go v0.2.0 // indirect
 	github.com/buger/jsonparser v1.1.1 // indirect
 	github.com/charmbracelet/colorprofile v0.3.1 // indirect
-	github.com/charmbracelet/ultraviolet v0.0.0-20250731212901-76da584cc9a5
+	github.com/charmbracelet/ultraviolet v0.0.0-20250805154935-01be9d7ef65d
 	github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect
 	github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef
 	github.com/charmbracelet/x/term v0.2.1
  
  
  
    
    @@ -68,18 +68,18 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
 github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
 github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
-github.com/bmatcuk/doublestar/v4 v4.9.0 h1:DBvuZxjdKkRP/dr4GVV4w2fnmrk5Hxc90T51LZjv0JA=
-github.com/bmatcuk/doublestar/v4 v4.9.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
+github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
 github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8=
 github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5 h1:GTcMIfDQJKyNKS+xVt7GkNIwz+tBuQtIuiP50WpzNgs=
 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
-github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250730165737-56ff7146d52d h1:YMXLZHSo8DjytVY/b5dK8LDuyQsVUmBK3ydQMpu2Ui4=
-github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250730165737-56ff7146d52d/go.mod h1:XIQ1qQfRph6Z5o2EikCydjumo0oDInQySRHuPATzbZc=
-github.com/charmbracelet/catwalk v0.4.5 h1:Kv3PadDe8IF8gpcYTfAJdCee5Bv4HufvtNT61FXtq5g=
-github.com/charmbracelet/catwalk v0.4.5/go.mod h1:WnKgNPmQHuMyk7GtwAQwl+ezHusfH40IvzML2qwUGwc=
+github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250805190305-70e94a2e0b2d h1:1C2whi5rgs+APtsQDE4riA+W5neYaMNY+Y+5o6J/rDU=
+github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250805190305-70e94a2e0b2d/go.mod h1:XIQ1qQfRph6Z5o2EikCydjumo0oDInQySRHuPATzbZc=
+github.com/charmbracelet/catwalk v0.4.6 h1:Y0JDq5V4agK8oO3lKC/hhInrYXePGwZPNo8I1Lk08jc=
+github.com/charmbracelet/catwalk v0.4.6/go.mod h1:WnKgNPmQHuMyk7GtwAQwl+ezHusfH40IvzML2qwUGwc=
 github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
 github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
 github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674 h1:+Cz+VfxD5DO+JT1LlswXWhre0HYLj6l2HW8HVGfMuC0=
@@ -90,10 +90,10 @@ github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250721205738-ea66aa652ee0
 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250721205738-ea66aa652ee0/go.mod h1:XIuqKpZTUXtVyeyiN1k9Tc/U7EzfaDnVc34feFHfBws=
 github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE=
 github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM=
-github.com/charmbracelet/ultraviolet v0.0.0-20250731212901-76da584cc9a5 h1:FrEzjuUbVbGd8UtZBfK8mf/IA4ExT2i3/fi+SEOv2eM=
-github.com/charmbracelet/ultraviolet v0.0.0-20250731212901-76da584cc9a5/go.mod h1:XrrgNFfXLrFAyd9DUmrqVc3yQFVv8Uk+okj4PsNNzpc=
-github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
-github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
+github.com/charmbracelet/ultraviolet v0.0.0-20250805154935-01be9d7ef65d h1:miSXsyi0ARm35O+DulTdaLYoUioWdGkoBfFoIhdqpCA=
+github.com/charmbracelet/ultraviolet v0.0.0-20250805154935-01be9d7ef65d/go.mod h1:XrrgNFfXLrFAyd9DUmrqVc3yQFVv8Uk+okj4PsNNzpc=
+github.com/charmbracelet/x/ansi v0.10.0 h1:jnOP9pFxY6/gw5nYjkpi6f17K0P/sN4fqT0Y1ioaORI=
+github.com/charmbracelet/x/ansi v0.10.0/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME=
 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3 h1:1xwHZg6eMZ9Wv5TE1UGub6ARubyOd1Lo5kPUI/6VL50=
  
  
  
    
    @@ -2,7 +2,6 @@ package chat
 
 import (
 	"context"
-	"fmt"
 	"time"
 
 	"github.com/atotto/clipboard"
@@ -45,7 +44,7 @@ type MessageListCmp interface {
 	SetSession(session.Session) tea.Cmd
 	GoToBottom() tea.Cmd
 	GetSelectedText() string
-	CopySelectedText() tea.Cmd
+	CopySelectedText(bool) tea.Cmd
 }
 
 // messageListCmp implements MessageListCmp, providing a virtualized list
@@ -96,6 +95,10 @@ func (m *messageListCmp) Init() tea.Cmd {
 // Update handles incoming messages and updates the component state.
 func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
+	case tea.KeyPressMsg:
+		if key.Matches(msg, messages.CopyKey) && m.listCmp.HasSelection() {
+			return m, m.CopySelectedText(true)
+		}
 	case tea.MouseClickMsg:
 		x := msg.X - 1 // Adjust for padding
 		y := msg.Y - 1 // Adjust for padding
@@ -128,11 +131,10 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		if msg.Button == tea.MouseLeft {
 			if x < 0 || y < 0 || x >= m.width-2 || y >= m.height-1 {
 				m.listCmp.SelectionStop()
-				return m, m.CopySelectedText()
+			} else {
+				m.listCmp.EndSelection(x, y)
+				m.listCmp.SelectionStop()
 			}
-			m.listCmp.EndSelection(x, y)
-			m.listCmp.SelectionStop()
-			return m, m.CopySelectedText()
 		}
 		return m, nil
 	case pubsub.Event[permission.PermissionNotification]:
@@ -155,13 +157,11 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		u, cmd := m.listCmp.Update(msg)
 		m.listCmp = u.(list.List[list.Item])
 		return m, cmd
-	default:
-		var cmds []tea.Cmd
-		u, cmd := m.listCmp.Update(msg)
-		m.listCmp = u.(list.List[list.Item])
-		cmds = append(cmds, cmd)
-		return m, tea.Batch(cmds...)
 	}
+
+	u, cmd := m.listCmp.Update(msg)
+	m.listCmp = u.(list.List[list.Item])
+	return m, cmd
 }
 
 // View renders the message list or an initial screen if empty.
@@ -654,24 +654,51 @@ func (m *messageListCmp) handleMouseClick(x, y int) tea.Cmd {
 	return nil
 }
 
+// SelectionClear clears the current selection in the list component.
+func (m *messageListCmp) SelectionClear() tea.Cmd {
+	m.listCmp.SelectionClear()
+	m.previousSelected = ""
+	m.lastClickX, m.lastClickY = 0, 0
+	m.clickCount = 0
+	return nil
+}
+
+// HasSelection checks if there is a selection in the list component.
+func (m *messageListCmp) HasSelection() bool {
+	return m.listCmp.HasSelection()
+}
+
 // GetSelectedText returns the currently selected text from the list component.
 func (m *messageListCmp) GetSelectedText() string {
 	return m.listCmp.GetSelectedText(3) // 3 padding for the left border/padding
 }
 
-// CopySelectedText copies the currently selected text to the clipboard.
-func (m *messageListCmp) CopySelectedText() tea.Cmd {
-	return nil
+// CopySelectedText copies the currently selected text to the clipboard. When
+// clear is true, it clears the selection after copying.
+func (m *messageListCmp) CopySelectedText(clear bool) tea.Cmd {
+	if !m.listCmp.HasSelection() {
+		return nil
+	}
+
 	selectedText := m.GetSelectedText()
 	if selectedText == "" {
 		return util.ReportInfo("No text selected")
 	}
 
-	err := clipboard.WriteAll(selectedText)
-	if err != nil {
-		return util.ReportError(fmt.Errorf("failed to copy selected text to clipboard: %w", err))
+	if clear {
+		defer func() { m.SelectionClear() }()
 	}
-	return util.ReportInfo("Selected text copied to clipboard")
+
+	return tea.Sequence(
+		// We use both OSC 52 and native clipboard for compatibility with different
+		// terminal emulators and environments.
+		tea.SetClipboard(selectedText),
+		func() tea.Msg {
+			_ = clipboard.WriteAll(selectedText)
+			return nil
+		},
+		util.ReportInfo("Selected text copied to clipboard"),
+	)
 }
 
 // abs returns the absolute value of an integer.
  
  
  
    
    @@ -25,7 +25,8 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/util"
 )
 
-var copyKey = key.NewBinding(key.WithKeys("c", "y", "C", "Y"), key.WithHelp("c/y", "copy"))
+// CopyKey is the key binding for copying message content to the clipboard.
+var CopyKey = key.NewBinding(key.WithKeys("c", "y", "C", "Y"), key.WithHelp("c/y", "copy"))
 
 // MessageCmp defines the interface for message components in the chat interface.
 // It combines standard UI model interfaces with message-specific functionality.
@@ -99,12 +100,15 @@ func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			return m, cmd
 		}
 	case tea.KeyPressMsg:
-		if key.Matches(msg, copyKey) {
-			err := clipboard.WriteAll(m.message.Content().Text)
-			if err != nil {
-				return m, util.ReportError(fmt.Errorf("failed to copy message content to clipboard: %w", err))
-			}
-			return m, util.ReportInfo("Message copied to clipboard")
+		if key.Matches(msg, CopyKey) {
+			return m, tea.Sequence(
+				tea.SetClipboard(m.message.Content().Text),
+				func() tea.Msg {
+					_ = clipboard.WriteAll(m.message.Content().Text)
+					return nil
+				},
+				util.ReportInfo("Message copied to clipboard"),
+			)
 		}
 	}
 	return m, nil
  
  
  
    
    @@ -165,7 +165,7 @@ func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		}
 		return m, tea.Batch(cmds...)
 	case tea.KeyPressMsg:
-		if key.Matches(msg, copyKey) {
+		if key.Matches(msg, CopyKey) {
 			return m, m.copyTool()
 		}
 	}
@@ -198,11 +198,14 @@ func (m *toolCallCmp) SetCancelled() {
 
 func (m *toolCallCmp) copyTool() tea.Cmd {
 	content := m.formatToolForCopy()
-	err := clipboard.WriteAll(content)
-	if err != nil {
-		return util.ReportError(fmt.Errorf("failed to copy tool content to clipboard: %w", err))
-	}
-	return util.ReportInfo("Tool content copied to clipboard")
+	return tea.Sequence(
+		tea.SetClipboard(content),
+		func() tea.Msg {
+			_ = clipboard.WriteAll(content)
+			return nil
+		},
+		util.ReportInfo("Tool content copied to clipboard"),
+	)
 }
 
 func (m *toolCallCmp) formatToolForCopy() string {
  
  
  
    
    @@ -42,7 +42,7 @@ func DefaultKeyMap() KeyMap {
 			key.WithHelp("s", "allow session"),
 		),
 		Deny: key.NewBinding(
-			key.WithKeys("d", "D", "ctrl+d"),
+			key.WithKeys("d", "D", "ctrl+d", "esc"),
 			key.WithHelp("d", "deny"),
 		),
 		Select: key.NewBinding(
  
  
  
    
    @@ -68,7 +68,7 @@ func RenderMCPList(opts RenderOptions) []string {
 			case agent.MCPStateConnected:
 				icon = t.ItemOnlineIcon
 				if state.ToolCount > 0 {
-					extraContent = t.S().Subtle.Render(fmt.Sprintf("(%d tools)", state.ToolCount))
+					extraContent = t.S().Subtle.Render(fmt.Sprintf("%d tools", state.ToolCount))
 				}
 			case agent.MCPStateError:
 				icon = t.ItemErrorIcon
  
  
  
    
    @@ -56,6 +56,7 @@ type List[T Item] interface {
 	SelectWord(col, line int)
 	SelectParagraph(col, line int)
 	GetSelectedText(paddingLeft int) string
+	HasSelection() bool
 }
 
 type direction int
@@ -286,30 +287,10 @@ func (l *list[T]) handleMouseWheel(msg tea.MouseWheelMsg) (tea.Model, tea.Cmd) {
 	return l, cmd
 }
 
-// View implements List.
-func (l *list[T]) View() string {
-	if l.height <= 0 || l.width <= 0 {
-		return ""
-	}
+// selectionView renders the highlighted selection in the view and returns it
+// as a string. If textOnly is true, it won't render any styles.
+func (l *list[T]) selectionView(view string, textOnly bool) string {
 	t := styles.CurrentTheme()
-	view := l.rendered
-	lines := strings.Split(view, "\n")
-
-	start, end := l.viewPosition()
-	viewStart := max(0, start)
-	viewEnd := min(len(lines), end+1)
-	lines = lines[viewStart:viewEnd]
-	if l.resize {
-		return strings.Join(lines, "\n")
-	}
-	view = t.S().Base.
-		Height(l.height).
-		Width(l.width).
-		Render(strings.Join(lines, "\n"))
-
-	if !l.hasSelection() {
-		return view
-	}
 	area := uv.Rect(0, 0, l.width, l.height)
 	scr := uv.NewScreenBuffer(area.Dx(), area.Dy())
 	uv.NewStyledString(view).Draw(scr, area)
@@ -397,6 +378,8 @@ func (l *list[T]) View() string {
 		lineTextBounds[y] = bounds
 	}
 
+	var selectedText strings.Builder
+
 	// Second pass: apply selection highlighting
 	for y := range scr.Height() {
 		selBounds := lineSelections[y]
@@ -406,6 +389,11 @@ func (l *list[T]) View() string {
 
 		textBounds := lineTextBounds[y]
 		if textBounds.start < 0 {
+			if textOnly {
+				// We don't want to get rid of all empty lines in text-only mode
+				selectedText.WriteByte('\n')
+			}
+
 			continue // No text on this line
 		}
 
@@ -421,16 +409,63 @@ func (l *list[T]) View() string {
 
 			cellStr := cell.String()
 			if len(cellStr) > 0 && !specialChars[cellStr] {
+				if textOnly {
+					// Collect selected text without styles
+					selectedText.WriteString(cell.String())
+					continue
+				}
+
+				// Text selection styling, which is a Lip Gloss style. We must
+				// extract the values to use in a UV style, below.
+				ts := t.TextSelection
+
 				cell = cell.Clone()
-				cell.Style = cell.Style.Background(t.BgOverlay).Foreground(t.White)
+				cell.Style = cell.Style.Background(ts.GetBackground()).Foreground(ts.GetForeground())
 				scr.SetCell(x, y, cell)
 			}
 		}
+
+		if textOnly {
+			// Make sure we add a newline after each line of selected text
+			selectedText.WriteByte('\n')
+		}
+	}
+
+	if textOnly {
+		return strings.TrimSpace(selectedText.String())
 	}
 
 	return scr.Render()
 }
 
+// View implements List.
+func (l *list[T]) View() string {
+	if l.height <= 0 || l.width <= 0 {
+		return ""
+	}
+	t := styles.CurrentTheme()
+	view := l.rendered
+	lines := strings.Split(view, "\n")
+
+	start, end := l.viewPosition()
+	viewStart := max(0, start)
+	viewEnd := min(len(lines), end+1)
+	lines = lines[viewStart:viewEnd]
+	if l.resize {
+		return strings.Join(lines, "\n")
+	}
+	view = t.S().Base.
+		Height(l.height).
+		Width(l.width).
+		Render(strings.Join(lines, "\n"))
+
+	if !l.hasSelection() {
+		return view
+	}
+
+	return l.selectionView(view, false)
+}
+
 func (l *list[T]) viewPosition() (int, int) {
 	start, end := 0, 0
 	renderedLines := lipgloss.Height(l.rendered) - 1
@@ -1374,69 +1409,16 @@ func (l *list[T]) SelectParagraph(col, line int) {
 	l.selectionActive = false // Not actively selecting, just selected
 }
 
+// HasSelection returns whether there is an active selection.
+func (l *list[T]) HasSelection() bool {
+	return l.hasSelection()
+}
+
 // GetSelectedText returns the currently selected text.
 func (l *list[T]) GetSelectedText(paddingLeft int) string {
-	return ""
-	// if !l.hasSelection() {
-	// 	return ""
-	// }
-	//
-	// startLine := l.selectionStartLine
-	// endLine := l.selectionEndLine
-	// startCol := l.selectionStartCol
-	// endCol := l.selectionEndCol
-	//
-	// if l.direction == DirectionBackward {
-	// 	startLine = (lipgloss.Height(l.rendered) - 1) - startLine
-	// 	endLine = (lipgloss.Height(l.rendered) - 1) - endLine
-	// }
-	//
-	// if l.offset > 0 {
-	// 	if l.direction == DirectionBackward {
-	// 		startLine += l.offset
-	// 		endLine += l.offset
-	// 	} else {
-	// 		startLine -= l.offset
-	// 		endLine -= l.offset
-	// 	}
-	// }
-	//
-	// lines := strings.Split(l.rendered, "\n")
-	//
-	// if startLine < 0 || endLine < 0 || startLine >= len(lines) || endLine >= len(lines) {
-	// 	return ""
-	// }
-	//
-	// var result strings.Builder
-	// for i := range lines {
-	// 	lines[i] = ansi.Strip(lines[i])
-	// 	for _, icon := range styles.SelectionIgnoreIcons {
-	// 		lines[i] = strings.ReplaceAll(lines[i], icon, " ")
-	// 	}
-	//
-	// 	if i == startLine {
-	// 		if startCol < 0 || startCol >= len(lines[i]) {
-	// 			startCol = 0
-	// 		}
-	// 		if startCol < paddingLeft {
-	// 			startCol = paddingLeft
-	// 		}
-	// 		if i != endLine {
-	// 			endCol = len(lines[i])
-	// 		}
-	// 		result.WriteString(strings.TrimRightFunc(lines[i][startCol:endCol], unicode.IsSpace))
-	// 	} else if i > startLine && i < endLine {
-	// 		result.WriteString(strings.TrimRightFunc(lines[i][paddingLeft:], unicode.IsSpace))
-	// 	} else if i == endLine {
-	// 		if endCol < 0 || endCol >= len(lines[i]) {
-	// 			endCol = len(lines[i])
-	// 		}
-	// 		if endCol < paddingLeft {
-	// 			endCol = paddingLeft
-	// 		}
-	// 		result.WriteString(strings.TrimRightFunc(lines[i][paddingLeft:endCol], unicode.IsSpace))
-	// 	}
-	// }
-	//
-	// return result.String()
+	if !l.hasSelection() {
+		return ""
+	}
+
+	return l.selectionView(l.View(), true)
 }
  
  
  
    
    @@ -19,6 +19,7 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/components/chat"
 	"github.com/charmbracelet/crush/internal/tui/components/chat/editor"
 	"github.com/charmbracelet/crush/internal/tui/components/chat/header"
+	"github.com/charmbracelet/crush/internal/tui/components/chat/messages"
 	"github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
 	"github.com/charmbracelet/crush/internal/tui/components/chat/splash"
 	"github.com/charmbracelet/crush/internal/tui/components/completions"
@@ -172,10 +173,18 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		}
 		return p, nil
 	case tea.MouseClickMsg:
+		if p.isMouseOverChat(msg.X, msg.Y) {
+			p.focusedPane = PanelTypeChat
+			p.chat.Focus()
+			p.editor.Blur()
+		} else {
+			p.focusedPane = PanelTypeEditor
+			p.editor.Focus()
+			p.chat.Blur()
+		}
 		u, cmd := p.chat.Update(msg)
 		p.chat = u.(chat.MessageListCmp)
 		return p, cmd
-		return p, nil
 	case tea.MouseMotionMsg:
 		if msg.Button == tea.MouseLeft {
 			u, cmd := p.chat.Update(msg)
@@ -857,10 +866,7 @@ func (p *chatPage) Help() help.KeyMap {
 					key.WithKeys("up", "down"),
 					key.WithHelp("↑↓", "scroll"),
 				),
-				key.NewBinding(
-					key.WithKeys("c", "y"),
-					key.WithHelp("c/y", "copy"),
-				),
+				messages.CopyKey,
 			)
 			fullList = append(fullList,
 				[]key.Binding{
  
  
  
    
    @@ -5,9 +5,9 @@ import (
 	"github.com/charmbracelet/x/exp/charmtone"
 )
 
-func NewCrushTheme() *Theme {
+func NewCharmtoneTheme() *Theme {
 	t := &Theme{
-		Name:   "crush",
+		Name:   "charmtone",
 		IsDark: true,
 
 		Primary:   charmtone.Charple,
@@ -56,6 +56,9 @@ func NewCrushTheme() *Theme {
 		Cherry:   charmtone.Cherry,
 	}
 
+	// Text selection.
+	t.TextSelection = lipgloss.NewStyle().Foreground(charmtone.Salt).Background(charmtone.Charple)
+
 	// LSP and MCP status.
 	t.ItemOfflineIcon = lipgloss.NewStyle().Foreground(charmtone.Squid).SetString("●")
 	t.ItemBusyIcon = t.ItemOfflineIcon.Foreground(charmtone.Citron)
  
  
  
    
    @@ -74,6 +74,9 @@ type Theme struct {
 	RedLight color.Color
 	Cherry   color.Color
 
+	// Text selection.
+	TextSelection lipgloss.Style
+
 	// LSP and MCP status indicators.
 	ItemOfflineIcon lipgloss.Style
 	ItemBusyIcon    lipgloss.Style
@@ -491,26 +494,26 @@ func SetDefaultManager(m *Manager) {
 
 func DefaultManager() *Manager {
 	if defaultManager == nil {
-		defaultManager = NewManager("crush")
+		defaultManager = NewManager()
 	}
 	return defaultManager
 }
 
 func CurrentTheme() *Theme {
 	if defaultManager == nil {
-		defaultManager = NewManager("crush")
+		defaultManager = NewManager()
 	}
 	return defaultManager.Current()
 }
 
-func NewManager(defaultTheme string) *Manager {
+func NewManager() *Manager {
 	m := &Manager{
 		themes: make(map[string]*Theme),
 	}
 
-	m.Register(NewCrushTheme())
-
-	m.current = m.themes[defaultTheme]
+	t := NewCharmtoneTheme() // default theme
+	m.Register(t)
+	m.current = m.themes[t.Name]
 
 	return m
 }