Skip to content

Commit 5023a0b

Browse files
committed
Render inline image previews
1 parent 7f8774a commit 5023a0b

3 files changed

Lines changed: 88 additions & 11 deletions

File tree

pi-coding-agent-render.el

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
(require 'pi-coding-agent-table)
4545
(require 'cl-lib)
4646
(require 'ansi-color)
47+
(require 'image)
4748

4849
;; Forward references for functions in other modules
4950
(declare-function pi-coding-agent-compact "pi-coding-agent-menu" (&optional custom-instructions))
@@ -1742,21 +1743,85 @@ streaming state changed."
17421743
event-type
17431744
event-content-index))))
17441745

1746+
(defun pi-coding-agent--image-type-from-mime (mime-type)
1747+
"Return an Emacs image type symbol for MIME-TYPE."
1748+
(pcase mime-type
1749+
("image/png" 'png)
1750+
((or "image/jpeg" "image/jpg") 'jpeg)
1751+
("image/gif" 'gif)
1752+
("image/webp" 'webp)
1753+
("image/svg+xml" 'svg)))
1754+
1755+
(defun pi-coding-agent--image-block-display (block)
1756+
"Return an image display object for image content BLOCK, or nil."
1757+
(when (display-images-p)
1758+
(let* ((mime-type (plist-get block :mimeType))
1759+
(image-type (pi-coding-agent--image-type-from-mime mime-type))
1760+
(data (plist-get block :data)))
1761+
(when (and image-type
1762+
(image-type-available-p image-type)
1763+
data)
1764+
(condition-case nil
1765+
(create-image (base64-decode-string data) image-type t
1766+
:max-width pi-coding-agent-image-preview-max-width)
1767+
(error nil))))))
1768+
1769+
(defun pi-coding-agent--render-image-content-block (block)
1770+
"Return a display string for image content BLOCK."
1771+
(let ((fallback (format "[image%s]"
1772+
(if-let* ((mime-type (or (plist-get block :mimeType)
1773+
(plist-get block :mime-type))))
1774+
(format ": %s" mime-type)
1775+
""))))
1776+
(if-let* ((image (pi-coding-agent--image-block-display block)))
1777+
(propertize fallback
1778+
'display image
1779+
'rear-nonsticky t
1780+
'help-echo fallback)
1781+
fallback)))
1782+
1783+
(defun pi-coding-agent--file-image-display (path)
1784+
"Return an image display object for local image PATH, or nil."
1785+
(when (and (stringp path)
1786+
(file-readable-p path)
1787+
(display-images-p)
1788+
(image-supported-file-p path))
1789+
(condition-case nil
1790+
(create-image path nil nil
1791+
:max-width pi-coding-agent-image-preview-max-width)
1792+
(error nil))))
1793+
1794+
(defun pi-coding-agent--render-file-image (path)
1795+
"Return a display string for local image PATH."
1796+
(let* ((mime-type (when-let* ((image-type (image-supported-file-p path)))
1797+
(pcase image-type
1798+
('svg "image/svg+xml")
1799+
(_ (format "image/%s" image-type)))))
1800+
(fallback (format "[image%s]"
1801+
(if mime-type (format ": %s" mime-type) ""))))
1802+
(if-let* ((image (pi-coding-agent--file-image-display path)))
1803+
(propertize fallback
1804+
'display image
1805+
'rear-nonsticky t
1806+
'help-echo fallback)
1807+
fallback)))
1808+
17451809
(defun pi-coding-agent--extract-text-from-content (content-blocks)
1746-
"Extract text from CONTENT-BLOCKS vector efficiently.
1747-
Returns the concatenated text from all text blocks.
1748-
Optimized for the common case of a single text block."
1810+
"Extract displayable content from CONTENT-BLOCKS vector.
1811+
Text blocks are concatenated as text; image blocks are rendered inline when
1812+
Emacs can display them, otherwise a textual placeholder is used."
17491813
(if (and (vectorp content-blocks) (> (length content-blocks) 0))
17501814
(let ((first-block (aref content-blocks 0)))
17511815
(if (and (= (length content-blocks) 1)
17521816
(equal (plist-get first-block :type) "text"))
17531817
;; Fast path: single text block (common case)
17541818
(or (plist-get first-block :text) "")
1755-
;; Slow path: multiple blocks, need to filter and concat
1819+
;; Slow path: multiple blocks, render supported block types.
17561820
(mapconcat (lambda (c)
1757-
(if (equal (plist-get c :type) "text")
1758-
(or (plist-get c :text) "")
1759-
""))
1821+
(pcase (plist-get c :type)
1822+
("text" (or (plist-get c :text) ""))
1823+
("image" (concat "\n" (pi-coding-agent--render-image-content-block c) "\n"))
1824+
(_ "")))
17601825
content-blocks "")))
17611826
""))
17621827

@@ -1924,12 +1989,18 @@ if none exists, render the result at point without a live overlay."
19241989
(is-edit-diff (and (equal tool-name "edit")
19251990
(not is-error)
19261991
(plist-get details :diff)))
1992+
(image-read-content
1993+
(when (equal tool-name "read")
1994+
(when-let* ((path (pi-coding-agent--tool-path args))
1995+
((pi-coding-agent--file-image-display path)))
1996+
(concat raw-output "\n" (pi-coding-agent--render-file-image path)))))
19271997
(display-content
19281998
(ansi-color-filter-apply
19291999
(pcase tool-name
19302000
("edit" (or (plist-get details :diff) raw-output))
19312001
("write" (or (plist-get args :content) raw-output))
1932-
((or "bash" "read") raw-output)
2002+
("bash" raw-output)
2003+
("read" (or image-read-content raw-output))
19332004
(_ (if-let* ((details-json
19342005
(pi-coding-agent--pretty-print-json details)))
19352006
(concat raw-output "\n\n"
@@ -1944,7 +2015,8 @@ if none exists, render the result at point without a live overlay."
19442015
(truncation (pi-coding-agent--truncate-to-visual-lines
19452016
display-content preview-limit width))
19462017
(hidden-count (plist-get truncation :hidden-lines))
1947-
(needs-collapse (> hidden-count 0))
2018+
(needs-collapse (and (not image-read-content)
2019+
(> hidden-count 0)))
19482020
(inhibit-read-only t))
19492021
(pi-coding-agent--with-scroll-preservation
19502022
(save-excursion

pi-coding-agent-ui.el

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ For example:
189189
:value-type sexp)))
190190
:group 'pi-coding-agent)
191191

192+
(defcustom pi-coding-agent-image-preview-max-width 900
193+
"Maximum width in pixels for inline image previews."
194+
:type 'integer
195+
:group 'pi-coding-agent)
196+
192197
(defcustom pi-coding-agent-quit-without-confirmation nil
193198
"Whether `pi-coding-agent-quit' skips confirmation for a live process.
194199
When non-nil, quitting a session never asks whether a running pi process

test/pi-coding-agent-render-test.el

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5151,12 +5151,12 @@ a slot, so downstream consumers that skip blanks still get N content lines."
51515151
"hello world"))))
51525152

51535153
(ert-deftest pi-coding-agent-test-extract-text-from-content-multiple-blocks ()
5154-
"Extract-text-from-content concatenates multiple text blocks."
5154+
"Extract-text-from-content renders mixed text and image blocks."
51555155
(let ((blocks [(:type "text" :text "hello ")
51565156
(:type "image" :data "...")
51575157
(:type "text" :text "world")]))
51585158
(should (equal (pi-coding-agent--extract-text-from-content blocks)
5159-
"hello world"))))
5159+
"hello \n[image]\nworld"))))
51605160

51615161
(ert-deftest pi-coding-agent-test-extract-text-from-content-empty ()
51625162
"Extract-text-from-content handles empty input."

0 commit comments

Comments
 (0)