Skip to content

Commit cc44b43

Browse files
authored
Merge pull request #122 from Artawower/master
Support pasting images from clipboard via yank-media (emacs 29+)
2 parents 65d2d2c + 6f8d5ef commit cc44b43

File tree

3 files changed

+152
-32
lines changed

3 files changed

+152
-32
lines changed

eca-chat-media.el

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
;;; eca-chat-media.el --- Media handling for ECA chat -*- lexical-binding: t; -*-
2+
;; Copyright (C) 2025 Eric Dallo
3+
;;
4+
;; SPDX-License-Identifier: Apache-2.0
5+
;;
6+
;; This file is not part of GNU Emacs.
7+
;;
8+
;;; Commentary:
9+
;;
10+
;; Media handling for ECA chat: saving screenshots, mime type mapping,
11+
;; and yank-media handlers.
12+
;;
13+
;;; Code:
14+
15+
(require 'f)
16+
(require 'eca-util)
17+
18+
(eval-when-compile
19+
(require 'eca-chat))
20+
21+
(declare-function eca-session "eca")
22+
(declare-function eca-assert-session-running "eca")
23+
(declare-function eca-chat--get-last-buffer "eca-chat")
24+
(declare-function eca-chat--context->str "eca-chat")
25+
(declare-function eca-chat--insert-prompt "eca-chat")
26+
(declare-function eca-chat--select-window "eca-chat")
27+
28+
(defconst eca-chat-media--mime-extension-map
29+
'(("image/png" . "png")
30+
("image/x-png" . "png")
31+
("image/jpeg" . "jpg")
32+
("image/jpg" . "jpg")
33+
("image/gif" . "gif")
34+
("image/webp" . "webp")
35+
("image/heic" . "heic")
36+
("image/heif" . "heif")
37+
("image/svg+xml" . "svg"))
38+
"Mapping of mime types to screenshot file extensions.")
39+
40+
(defun eca-chat-media--extension-for-type (type)
41+
"Return file extension (without dot) for mime TYPE.
42+
TYPE can be a string or symbol."
43+
(let* ((type-str (if (symbolp type) (symbol-name type) type))
44+
(clean (and type-str (string-trim type-str))))
45+
(or (cdr (assoc-string clean eca-chat-media--mime-extension-map t))
46+
(when clean
47+
(let* ((parts (split-string clean "/"))
48+
(raw-subtype (cadr parts))
49+
(subtype (car (split-string (or raw-subtype "") "\\+"))))
50+
(unless (string-empty-p subtype)
51+
subtype)))
52+
"png")))
53+
54+
(defun eca-chat-media--yank-image-handler (type data)
55+
"Handler for yank-media to insert images from clipboard.
56+
TYPE is the MIME type (e.g., 'image/png').
57+
DATA is the binary image data as a string."
58+
(when-let* ((session (eca-session))
59+
(chat-buffer (eca-chat--get-last-buffer session))
60+
(extension (eca-chat-media--extension-for-type type))
61+
(output-path (make-temp-file "eca-screenshot-" nil (concat "." extension))))
62+
(condition-case err
63+
(progn
64+
(with-temp-file output-path
65+
(set-buffer-multibyte nil)
66+
(insert data))
67+
(when (f-exists? output-path)
68+
(eca-chat--with-current-buffer chat-buffer
69+
(let ((context (list :type "file" :path output-path)))
70+
(eca-chat--insert-prompt (concat (eca-chat--context->str context 'static) " "))
71+
(eca-chat--select-window)
72+
(goto-char (line-end-position)))
73+
(eca-info "Image yanked and added to prompt: %s"
74+
(file-size-human-readable (file-attribute-size (file-attributes output-path)))))))
75+
(error
76+
(eca-error "Failed to save yanked image: %s" (error-message-string err))))))
77+
78+
;;;###autoload
79+
(defun eca-chat-media-yank-screenshot ()
80+
"Yank image from clipboard and add to context.
81+
Uses native `yank-media'. Requires Emacs 29+."
82+
(interactive)
83+
(unless (fboundp 'yank-media)
84+
(user-error "Screenshot yanking requires Emacs 29+ with yank-media support"))
85+
(let* ((session (eca-session))
86+
(chat-buffer (eca-chat--get-last-buffer session)))
87+
(eca-assert-session-running session)
88+
(unless chat-buffer
89+
(user-error "Open an ECA chat buffer before yanking a screenshot"))
90+
(eca-chat--select-window)
91+
(yank-media)))
92+
93+
(provide 'eca-chat-media)
94+
;;; eca-chat-media.el ends here

eca-chat.el

Lines changed: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@
4040
Can be `'left', `'right', `'top', or `'bottom'. This setting will only
4141
be used when `eca-chat-use-side-window' is non-nil."
4242
:type '(choice (const :tag "Left" left)
43-
(const :tag "Right" right)
44-
(const :tag "Top" top)
45-
(const :tag "Bottom" bottom))
43+
(const :tag "Right" right)
44+
(const :tag "Top" top)
45+
(const :tag "Bottom" bottom))
4646
:group 'eca)
4747

4848
(defcustom eca-chat-window-width 0.40
@@ -170,14 +170,14 @@ Must be a valid model supported by server, check `eca-chat-select-model`."
170170
(defcustom eca-chat-diff-tool 'smerge
171171
"Select the method for displaying file-change diffs in ECA chat."
172172
:type '(choice (const :tag "Side-by-side Ediff" ediff)
173-
(const :tag "Merge-style Smerge" smerge))
173+
(const :tag "Merge-style Smerge" smerge))
174174
:group 'eca)
175175

176176
(defcustom eca-chat-tool-call-prepare-throttle 'smart
177177
"Throttle strategy for handling `toolCallPrepare` events.
178178
Possible values: `all` or `smart` (default)."
179179
:type '(choice (const :tag "Process all updates" all)
180-
(const :tag "Smart throttle" smart))
180+
(const :tag "Smart throttle" smart))
181181
:group 'eca)
182182

183183
(defcustom eca-chat-tool-call-prepare-update-interval 5
@@ -404,6 +404,7 @@ Must be a positive integer."
404404
(defvar eca-chat--last-known-model nil)
405405
(defvar eca-chat--last-known-behavior nil)
406406

407+
407408
(defun eca-chat-new-buffer-name (session)
408409
"Return the chat buffer name for SESSION."
409410
(format "<eca-chat:%s:%s>" (eca--session-id session) eca-chat--new-chat-id))
@@ -441,6 +442,7 @@ Must be a positive integer."
441442
(define-key map (kbd "C-c <up>") #'eca-chat-go-to-prev-expandable-block)
442443
(define-key map (kbd "C-c <down>") #'eca-chat-go-to-next-expandable-block)
443444
(define-key map (kbd "C-c <tab>") #'eca-chat-toggle-expandable-block)
445+
(define-key map (kbd "C-c C-y") #'eca-chat-media-yank-screenshot)
444446
map)
445447
"Keymap used by `eca-chat-mode'.")
446448

@@ -851,12 +853,28 @@ the prompt/context line."
851853
(goto-char prompt-start)
852854
(string-trim (buffer-substring (point) (point-max))))))
853855

856+
(defun eca-chat--extract-contexts-from-prompt ()
857+
"Extract contexts from prompt text properties.
858+
Returns a list of context plists found in the prompt field."
859+
(when-let ((prompt-start (eca-chat--prompt-field-start-point)))
860+
(let ((contexts '())
861+
(pos prompt-start)
862+
(end (point-max)))
863+
(while (< pos end)
864+
(when-let ((context (get-text-property pos 'eca-chat-context-item)))
865+
(unless (member context contexts)
866+
(push context contexts)))
867+
(setq pos (next-single-property-change pos 'eca-chat-context-item nil end)))
868+
(nreverse contexts))))
869+
854870
(defun eca-chat--send-prompt (session prompt)
855871
"Send PROMPT to server for SESSION."
856872
(when eca-chat--closed
857873
(user-error (eca-error "This chat is closed")))
858874
(let* ((prompt-start (eca-chat--prompt-field-start-point))
859-
(refined-contexts (-map #'eca-chat--refine-context eca-chat--context)))
875+
(prompt-contexts (eca-chat--extract-contexts-from-prompt))
876+
(refined-contexts (-map #'eca-chat--refine-context
877+
(append eca-chat--context prompt-contexts))))
860878
(when (seq-empty-p eca-chat--history) (eca-chat--clear))
861879
(add-to-list 'eca-chat--history prompt)
862880
(setq eca-chat--history-index -1)
@@ -1289,14 +1307,16 @@ If STATIC? return strs with no dynamic values."
12891307
(-let* (((&plist :type type) context)
12901308
(context-str
12911309
(pcase type
1292-
("file" (propertize (concat eca-chat-context-prefix
1293-
(eca-chat--context-presentable-path (plist-get context :path))
1294-
(-when-let ((&plist :start start :end end) (plist-get context :linesRange))
1295-
(format "(%d-%d)" start end)))
1296-
'eca-chat-expanded-item-str (concat eca-chat-context-prefix (plist-get context :path)
1297-
(-when-let ((&plist :start start :end end) (plist-get context :linesRange))
1298-
(format ":L%d-L%d" start end)))
1299-
'font-lock-face 'eca-chat-context-file-face))
1310+
("file" (let ((path (plist-get context :path))
1311+
(lines-range (plist-get context :linesRange)))
1312+
(propertize (concat eca-chat-context-prefix
1313+
(eca-chat--context-presentable-path path)
1314+
(-when-let ((&plist :start start :end end) lines-range)
1315+
(format "(%d-%d)" start end)))
1316+
'eca-chat-expanded-item-str (concat eca-chat-context-prefix path
1317+
(-when-let ((&plist :start start :end end) lines-range)
1318+
(format ":L%d-L%d" start end)))
1319+
'font-lock-face 'eca-chat-context-file-face)))
13001320
("directory" (propertize (concat eca-chat-context-prefix (eca-chat--context-presentable-path (plist-get context :path)))
13011321
'eca-chat-expanded-item-str (concat eca-chat-context-prefix (plist-get context :path))
13021322
'font-lock-face 'eca-chat-context-file-face))
@@ -1329,7 +1349,7 @@ If STATIC? return strs with no dynamic values."
13291349
")"))
13301350
'eca-chat-expanded-item-str (concat eca-chat-context-prefix "cursor")
13311351
'font-lock-face 'eca-chat-context-cursor-face))
1332-
(_ (concat eca-chat-context-prefix "unkown:" type)))))
1352+
(_ (concat eca-chat-context-prefix "unknown:" type)))))
13331353
(propertize context-str
13341354
'eca-chat-item-type 'context
13351355
'eca-chat-item-str-length (length context-str)
@@ -1468,20 +1488,22 @@ Add text property to prompt text to match context."
14681488

14691489
(defun eca-chat--context-to-completion (context)
14701490
"Convert CONTEXT to a completion item."
1471-
(let ((raw-label (pcase (plist-get context :type)
1472-
("file" (f-filename (plist-get context :path)))
1473-
("directory" (f-filename (plist-get context :path)))
1474-
("repoMap" "repoMap")
1475-
("cursor" "cursor")
1476-
("mcpResource" (concat (plist-get context :server) ":" (plist-get context :name)))
1477-
(_ (concat "Unknown - " (plist-get context :type)))))
1478-
(face (pcase (plist-get context :type)
1479-
("file" 'eca-chat-context-file-face)
1480-
("directory" 'eca-chat-context-file-face)
1481-
("repoMap" 'eca-chat-context-repo-map-face)
1482-
("cursor" 'eca-chat-context-cursor-face)
1483-
("mcpResource" 'eca-chat-context-mcp-resource-face)
1484-
(_ nil))))
1491+
(let* ((ctx-type (plist-get context :type))
1492+
(ctx-path (plist-get context :path))
1493+
(raw-label (pcase ctx-type
1494+
("file" (f-filename ctx-path))
1495+
("directory" (f-filename ctx-path))
1496+
("repoMap" "repoMap")
1497+
("cursor" "cursor")
1498+
("mcpResource" (concat (plist-get context :server) ":" (plist-get context :name)))
1499+
(_ (concat "Unknown - " ctx-type))))
1500+
(face (pcase ctx-type
1501+
("file" 'eca-chat-context-file-face)
1502+
("directory" 'eca-chat-context-file-face)
1503+
("repoMap" 'eca-chat-context-repo-map-face)
1504+
("cursor" 'eca-chat-context-cursor-face)
1505+
("mcpResource" 'eca-chat-context-mcp-resource-face)
1506+
(_ nil))))
14851507
(propertize raw-label
14861508
'eca-chat-completion-item context
14871509
'face face)))
@@ -1671,6 +1693,8 @@ CHILD, NAME, DOCSTRING and BODY are passed down."
16711693
(setq-local eca-chat--history-index -1)
16721694

16731695
;; Show diff blocks in markdown-mode with colors.
1696+
(when (fboundp 'yank-media-handler)
1697+
(yank-media-handler "image/.*" #'eca-chat-media--yank-image-handler))
16741698
(setq-local markdown-fontify-code-blocks-natively t)
16751699
;; Enable gfm-view-mode-like rendering without read-only
16761700
(setq-local markdown-hide-markup t)
@@ -1839,8 +1863,8 @@ Calls CB with the resulting message."
18391863
(cond
18401864
((eq action 'metadata)
18411865
'(metadata (category . eca-capf)
1842-
(display-sort-function . identity)
1843-
(cycle-sort-function . identity)))
1866+
(display-sort-function . identity)
1867+
(cycle-sort-function . identity)))
18441868
((eq (car-safe action) 'boundaries) nil)
18451869
(t
18461870
(complete-with-action action (funcall candidates-fn) probe pred))))
@@ -2718,4 +2742,5 @@ Returns selected message plist or nil if no messages or cancelled."
27182742
(eca-info (format "Saved chat to '%s'" file)))))
27192743

27202744
(provide 'eca-chat)
2745+
(require 'eca-chat-media)
27212746
;;; eca-chat.el ends here

eca-util.el

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ Inheirits BASE-MAP."
223223
("s" "Add to system prompt" eca-chat-add-context-to-system-prompt)
224224
("u" "Add to user prompt" eca-chat-add-context-to-user-prompt)
225225
("d" "Drop from system prompt" eca-chat-drop-context-from-system-prompt)
226-
("A" "Accept next pending tool call" eca-chat-tool-call-accept-next)]
226+
("A" "Accept next pending tool call" eca-chat-tool-call-accept-next)
227+
("y" "Yank screenshot" eca-chat-media-yank-screenshot)]
227228

228229
["Navigation"
229230
("N h" "Message history" eca-chat-timeline)

0 commit comments

Comments
 (0)