feat(ui): render half-block thumbnails for attached images#47
Conversation
- Add internal/ui/imagepreview package: Render() draws low-res thumbnails using Unicode half-blocks (▀) + truecolor/256-color SGR, which survives tmux/zellij (no graphics protocol) - Cache a rendered thumbnail per pending clipboard image in the input component; render once at attach time, never per frame - Fall back to the existing [N image(s) attached] text pill when the terminal lacks truecolor/256-color support - Document Ctrl+V paste, Ctrl+U clear, and the preview in the docs site and README keyboard shortcuts Fixes #46
|
Connected to Huly®: KIT-48 |
|
Warning Review limit reached
More reviews will be available in 3 minutes and 50 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. 📝 WalkthroughWalkthroughThis pull request adds terminal-based image thumbnail previews for pending image attachments using Unicode half-blocks and ANSI color codes. A new ChangesImage Preview Rendering
Sequence Diagram(s)sequenceDiagram
participant User
participant InputComponent
participant Clipboard
participant imagepreview
participant colorprofile
participant Output
User->>InputComponent: Ctrl+V (paste image)
InputComponent->>Clipboard: Read clipboard image bytes
Clipboard-->>InputComponent: Image data, mediaType
InputComponent->>imagepreview: Render(data, mediaType, maxCols, maxRows, bg)
imagepreview->>colorprofile: Env(os.Environ())
colorprofile-->>imagepreview: Terminal color profile
imagepreview-->>InputComponent: ANSI thumbnail string (or empty fallback)
InputComponent->>InputComponent: Append to pendingImages and imageThumbs
InputComponent->>Output: Display thumbnail in View()
User->>InputComponent: Ctrl+U (clear)
InputComponent->>InputComponent: Clear pendingImages and imageThumbs
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/ui/imagepreview/imagepreview.go`:
- Around line 77-80: Before fully decoding the image (the
image.Decode(bytes.NewReader(data)) call that assigns to img), call
image.DecodeConfig(bytes.NewReader(data)) first to get the image bounds; check
the returned Config.Width and Config.Height against a safe maximum (or
reject/trim if too large) and only proceed to image.Decode when within limits
(or perform a controlled downscale step instead). Replace the unconditional
image.Decode call with this guarded flow, using the same data reader
(bytes.NewReader(data)) and preserving error wrapping (fmt.Errorf("decode image:
%w", err)) for both config decode and full decode paths.
In `@internal/ui/input.go`:
- Around line 203-206: Update() is calling s.renderThumbnail(*msg.image)
synchronously which blocks the Bubble Tea event loop; instead, append the image
to s.pendingImages and push a placeholder into s.imageThumbs, then spawn or
enqueue background work to call s.renderThumbnail for that image and send the
result back into the Bubble Tea Update loop (e.g., via a thumbnail-ready message
type or channel) so Update() only does non-blocking enqueue/append and the async
worker updates s.imageThumbs when the thumbnail is ready; modify the handling
around s.pendingImages, s.imageThumbs, Update(), and add a thumbnail-ready
message and background worker to perform decode+resample off the hot path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 66921168-7d61-4db6-9403-f77320f3fe79
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (6)
README.mdgo.modinternal/ui/imagepreview/imagepreview.gointernal/ui/imagepreview/imagepreview_test.gointernal/ui/input.gowww/pages/cli/commands.md
- Render thumbnails asynchronously via a tea.Cmd instead of calling the decode + resample path synchronously inside Update(), which blocked the Bubble Tea event loop - Add thumbnailReadyMsg + an imageGen generation counter so async results land on the correct pendingImages slot and stale renders after a clear/re-attach are discarded - Guard imagepreview.Render against decompression bombs by checking DecodeConfig dimensions against a max before full decode
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/ui/input.go (1)
121-125:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPropagate
imageGenthrough clipboard reads too.Only
thumbnailReadyMsgis generation-scoped right now. Actrl+vstarted just beforectrl+uor submit can still deliver a lateclipboardImageMsgand attach that stale image to the next prompt, which is a real privacy footgun. Tag the clipboard read with the current generation, drop stale results inUpdate, and invalidate in-flight reads onctrl+ueven whenpendingImagesis currently empty.Proposed fix
type clipboardImageMsg struct { + gen int image *core.ImageAttachment err error } @@ - case "ctrl+v": + case "ctrl+v": // Try to read an image from the clipboard asynchronously. - return s, readClipboardImageCmd() + return s, readClipboardImageCmd(s.imageGen) case "ctrl+u": // Clear all pending image attachments. - if len(s.pendingImages) > 0 { - s.pendingImages = nil - s.imageThumbs = nil - s.imageGen++ - return s, nil - } + s.pendingImages = nil + s.imageThumbs = nil + s.imageGen++ + return s, nil @@ case clipboardImageMsg: + if msg.gen != s.imageGen { + return s, nil + } if msg.err != nil { // Silently ignore — no image on clipboard or tool unavailable. return s, nil @@ -func readClipboardImageCmd() tea.Cmd { +func readClipboardImageCmd(gen int) tea.Cmd { return func() tea.Msg { img, err := clipboard.ReadImage() if err != nil { - return clipboardImageMsg{err: err} + return clipboardImageMsg{gen: gen, err: err} } return clipboardImageMsg{ + gen: gen, image: &core.ImageAttachment{ Data: img.Data, MediaType: img.MediaType,Also applies to: 219-237, 291-300
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/ui/input.go` around lines 121 - 125, The clipboardImageMsg needs to carry the current image generation like thumbnailReadyMsg so clipboard reads are generation-scoped: add a generation field to clipboardImageMsg and set it when starting a clipboard read, check and drop any clipboardImageMsg whose generation != current imageGen inside Update (same gating used for thumbnailReadyMsg), and when handling the ctrl+u invalidation path ensure any in-flight clipboard reads are invalidated (clear or bump imageGen) even if pendingImages is empty so late clipboardImageMsg results are ignored and not attached to the next prompt.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/ui/input.go`:
- Around line 579-590: thumbCols wrongly allows a full-size preview for very
small widths because it only clamps when s.width > 6; update the function
(thumbCols) to first handle the narrow-case by returning 0 when s.width <= 6,
then compute cols as the minimum of thumbMaxCols and (s.width - 6), and finally
guard with the existing cols < 1 check so tiny or negative results return 0.
---
Outside diff comments:
In `@internal/ui/input.go`:
- Around line 121-125: The clipboardImageMsg needs to carry the current image
generation like thumbnailReadyMsg so clipboard reads are generation-scoped: add
a generation field to clipboardImageMsg and set it when starting a clipboard
read, check and drop any clipboardImageMsg whose generation != current imageGen
inside Update (same gating used for thumbnailReadyMsg), and when handling the
ctrl+u invalidation path ensure any in-flight clipboard reads are invalidated
(clear or bump imageGen) even if pendingImages is empty so late
clipboardImageMsg results are ignored and not attached to the next prompt.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 373aafaf-727b-46e5-a39f-09e14480f08b
📒 Files selected for processing (3)
internal/ui/imagepreview/imagepreview.gointernal/ui/imagepreview/imagepreview_test.gointernal/ui/input.go
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/ui/imagepreview/imagepreview_test.go
- Return 0 from thumbCols when width <= 6 so a full-size thumbnail is no longer rendered for tiny or uninitialized (width 0) terminals; the caller falls back to the text pill
* fix(ui): show pasted image previews in input and transcript The half-block thumbnail preview added in #47 rendered but was clipped off the bottom of the screen, and submitted images showed only a text badge in the conversation history. - Mark the layout dirty when clipboardImageMsg / thumbnailReadyMsg reach the parent, so distributeHeight re-measures the now-taller input region instead of keeping a stale height that pushed the preview off-screen - Render thumbnail previews in the transcript after a user message, appended as a verbatim ScrollList item (raw ANSI half-blocks would be mangled if folded into the word-wrapped user text block) - Render transcript previews asynchronously via a tea.Cmd so decode + resample never blocks the Bubble Tea event loop - Add regression tests covering the input layout recompute and the transcript preview flow * fix(ui): anchor transcript image preview to its user message - Insert the async thumbnail preview directly after the originating user message (tracked via anchorID) instead of appending, so a streamed assistant reply that lands first no longer pushes the preview out of place - Make the layout regression test deterministic by forcing a truecolor profile, avoiding flakes on low-color CI terminals where the thumbnail would render empty - Add tests for anchored insertion and the unknown-anchor append fallback
Description
Today Kit shows attached clipboard images only as a text placeholder
(
[N image(s) attached] ctrl+u to clear). This PR adds a glanceable,low-resolution thumbnail preview of each pending image, rendered
directly in the input.
Previews are drawn with Unicode upper half-block characters (
▀) plustruecolor/256-color SGR codes — i.e. ordinary styled text, not a graphics
escape protocol. This is deliberate: the Kitty graphics protocol, Sixel,
and iTerm2 inline images are stripped or mangled by terminal multiplexers,
so they are explicitly out of scope. The half-block technique passes
through tmux and zellij untouched.
The thumbnail is rendered once when an image is attached and cached
(never re-rendered per frame), and Kit degrades gracefully: truecolor →
256-color → the existing text pill when the terminal supports neither.
Fixes #46
Type of Change
How It Works
New
internal/ui/imagepreviewpackage exposes:It decodes the image, downscales with
golang.org/x/image/draw(preserving aspect ratio, never upscaling), composites transparent
pixels over
bg, and emits half-block rows where the foreground colorsthe top pixel and the background colors the bottom pixel. Color fidelity
follows the detected
charmbracelet/colorprofileprofile; below ANSI256it returns an empty string so the caller keeps the text pill.
InputComponentcaches one thumbnail per pending image (capped at40×12 cells), rendered at attach time and cleared on submit / Ctrl+U.
The
View()draws each cached thumbnail under the existing[N image(s) attached]indicator.Checklist
go test ./internal/ui/... -racepassesgolangci-lint runreports no issues on the new codeAdditional Information
Files added
internal/ui/imagepreview/imagepreview.go— half-block thumbnail rendererinternal/ui/imagepreview/imagepreview_test.go— unit tests (truecolor,256-color, degradation, decode errors, sizing/aspect ratio, compositing)
Files modified
internal/ui/input.go— thumbnail cache + wiring into attach/clear/submit andView()README.md—Ctrl+V/Ctrl+Urows in the Keyboard Shortcuts tablewww/pages/cli/commands.md— new "Image attachments" sectiongo.mod/go.sum— addgolang.org/x/image; promotecolorprofileto directBackwards compatibility
(no truecolor/256-color), behaviour is unchanged: the text pill is shown
alone. No public SDK (
pkg/kit/) surface changed — the new package isinternal/.Scope note
pendingImages).The
@fileattachment and transcript-history previews suggested in theissue can reuse
imagepreview.Renderas focused follow-ups.Summary by CodeRabbit
@path/to/image.png); MIME auto-detection.