@@ -24,6 +24,7 @@ import (
24 tea "charm.land/bubbletea/v2"
24 tea "charm.land/bubbletea/v2"
25 "charm.land/catwalk/pkg/catwalk"
25 "charm.land/catwalk/pkg/catwalk"
26 "charm.land/lipgloss/v2"
26 "charm.land/lipgloss/v2"
27+ nativeclipboard "github.com/aymanbagabas/go-nativeclipboard"
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"
29 "github.com/charmbracelet/crush/internal/commands"
30 "github.com/charmbracelet/crush/internal/commands"
@@ -1549,6 +1550,9 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
1549 cmds = append(cmds, cmd)
1550 cmds = append(cmds, cmd)
1553+ case key.Matches(msg, m.keyMap.Editor.PasteImage):
1554+ cmds = append(cmds, m.pasteImageFromClipboard)
1552 case key.Matches(msg, m.keyMap.Editor.SendMessage):
1556 case key.Matches(msg, m.keyMap.Editor.SendMessage):
1553 value := m.textarea.Value()
1557 value := m.textarea.Value()
1554 if before, ok := strings.CutSuffix(value, "\\"); ok {
1558 if before, ok := strings.CutSuffix(value, "\\"); ok {
@@ -2081,6 +2085,7 @@ func (m *UI) FullHelp() [][]key.Binding {
2081 []key.Binding{
2085 []key.Binding{
2082 k.Editor.Newline,
2086 k.Editor.Newline,
2083 k.Editor.AddImage,
2087 k.Editor.AddImage,
2088+ k.Editor.PasteImage,
2084 k.Editor.MentionFile,
2089 k.Editor.MentionFile,
2085 k.Editor.OpenEditor,
2090 k.Editor.OpenEditor,
@@ -2129,6 +2134,7 @@ func (m *UI) FullHelp() [][]key.Binding {
2129 []key.Binding{
2134 []key.Binding{
2130 k.Editor.Newline,
2135 k.Editor.Newline,
2131 k.Editor.AddImage,
2136 k.Editor.AddImage,
2137+ k.Editor.PasteImage,
2132 k.Editor.MentionFile,
2138 k.Editor.MentionFile,
2133 k.Editor.OpenEditor,
2139 k.Editor.OpenEditor,
@@ -3061,6 +3067,80 @@ func (m *UI) handleFilePathPaste(path string) tea.Cmd {
3070+// pasteImageFromClipboard reads image data from the system clipboard and
3071+// creates an attachment. If no image data is found, it falls back to
3072+// interpreting clipboard text as a file path.
3073+func (m *UI) pasteImageFromClipboard() tea.Msg {
3074+ imageData, err := nativeclipboard.Image.Read()
3075+ if int64(len(imageData)) > common.MaxAttachmentSize {
3076+ return util.InfoMsg{
3077+ Type: util.InfoTypeError,
3078+ Msg: "File too large, max 5MB",
3081+ name := fmt.Sprintf("paste_%d.png", m.pasteIdx())
3083+ return message.Attachment{
3086+ MimeType: mimeOf(imageData),
3087+ Content: imageData,
3091+ textData, textErr := nativeclipboard.Text.Read()
3092+ if textErr != nil || len(textData) == 0 {
3093+ return util.NewInfoMsg("Clipboard is empty or does not contain an image")
3096+ path := strings.TrimSpace(string(textData))
3097+ path = strings.ReplaceAll(path, "\\ ", " ")
3098+ if _, statErr := os.Stat(path); statErr != nil {
3099+ return util.NewInfoMsg("Clipboard does not contain an image or valid file path")
3102+ lowerPath := strings.ToLower(path)
3104+ for _, ext := range common.AllowedImageTypes {
3105+ if strings.HasSuffix(lowerPath, ext) {
3111+ return util.NewInfoMsg("File type is not a supported image format")
3114+ fileInfo, statErr := os.Stat(path)
3115+ if statErr != nil {
3116+ return util.InfoMsg{
3117+ Type: util.InfoTypeError,
3118+ Msg: fmt.Sprintf("Unable to read file: %v", statErr),
3121+ if fileInfo.Size() > common.MaxAttachmentSize {
3122+ return util.InfoMsg{
3123+ Type: util.InfoTypeError,
3124+ Msg: "File too large, max 5MB",
3128+ content, readErr := os.ReadFile(path)
3129+ if readErr != nil {
3130+ return util.InfoMsg{
3131+ Type: util.InfoTypeError,
3132+ Msg: fmt.Sprintf("Unable to read file: %v", readErr),
3136+ return message.Attachment{
3138+ FileName: filepath.Base(path),
3139+ MimeType: mimeOf(content),
3064var pasteRE = regexp.MustCompile(`paste_(\d+).txt`)
3144var pasteRE = regexp.MustCompile(`paste_(\d+).txt`)
3066func (m *UI) pasteIdx() int {
3146func (m *UI) pasteIdx() int {