Skip to content

feat(ui): render half-block thumbnails for attached images#47

Merged
ezynda3 merged 3 commits into
masterfrom
feat/46-image-thumbnail-preview
Jun 4, 2026
Merged

feat(ui): render half-block thumbnails for attached images#47
ezynda3 merged 3 commits into
masterfrom
feat/46-image-thumbnail-preview

Conversation

@ezynda3
Copy link
Copy Markdown
Contributor

@ezynda3 ezynda3 commented Jun 4, 2026

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 () plus
truecolor/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

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Refactor / chore

How It Works

  • New internal/ui/imagepreview package exposes:

    func Render(data []byte, mediaType string, maxCols, maxRows int, bg color.Color) (string, error)

    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 colors
    the top pixel and the background colors the bottom pixel. Color fidelity
    follows the detected charmbracelet/colorprofile profile; below ANSI256
    it returns an empty string so the caller keeps the text pill.

  • InputComponent caches one thumbnail per pending image (capped at
    40×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

  • Code follows the project style guidelines
  • Self-reviewed the changes
  • Added unit tests for the new behaviour
  • Updated documentation (README + docs site)
  • go test ./internal/ui/... -race passes
  • golangci-lint run reports no issues on the new code

Additional Information

Files added

  • internal/ui/imagepreview/imagepreview.go — half-block thumbnail renderer
  • internal/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 and View()
  • README.mdCtrl+V / Ctrl+U rows in the Keyboard Shortcuts table
  • www/pages/cli/commands.md — new "Image attachments" section
  • go.mod / go.sum — add golang.org/x/image; promote colorprofile to direct

Backwards compatibility

  • Fully backwards compatible. When the terminal cannot display a preview
    (no truecolor/256-color), behaviour is unchanged: the text pill is shown
    alone. No public SDK (pkg/kit/) surface changed — the new package is
    internal/.

Scope note

  • This wires the issue's primary surface (clipboard pendingImages).
    The @file attachment and transcript-history previews suggested in the
    issue can reuse imagepreview.Render as focused follow-ups.

Summary by CodeRabbit

  • New Features
    • Paste images from clipboard with inline terminal thumbnail previews (Ctrl+V).
    • Attach images via file path syntax (@path/to/image.png); MIME auto-detection.
    • Clear all pending image attachments (Ctrl+U). Pending attachments are shown as thumbnails and cleared on submit.
  • Documentation
    • Added image attachments docs covering shortcuts, preview behavior, and color/truecolor fallback.
  • Tests
    • Added tests covering preview rendering, bounds/size guards, compositing, and edge cases.

- 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
@mark-iii-labs-huly
Copy link
Copy Markdown

Connected to Huly®: KIT-48

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

Warning

Review limit reached

@ezynda3, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e201a700-b0e2-46d5-8f79-dff1aeaf0cdd

📥 Commits

Reviewing files that changed from the base of the PR and between f6a48f4 and d27eccc.

📒 Files selected for processing (1)
  • internal/ui/input.go
📝 Walkthrough

Walkthrough

This pull request adds terminal-based image thumbnail previews for pending image attachments using Unicode half-blocks and ANSI color codes. A new imagepreview package renders scaled image thumbnails as colored text, integrated into InputComponent with per-image thumbnail caching. Keyboard shortcuts (Ctrl+V paste, Ctrl+U clear) and documentation are updated.

Changes

Image Preview Rendering

Layer / File(s) Summary
Image preview rendering engine
internal/ui/imagepreview/imagepreview.go
imagepreview.Render decodes image bytes, validates dimensions, scales to terminal cell constraints, composites transparent pixels over background, and generates ANSI-colored half-block strings using truecolor (24-bit) or 256-color SGR modes with helper functions for fitting, compositing, and palette/index mapping.
Dependency declarations
go.mod
Added github.com/charmbracelet/colorprofile v0.4.3 and golang.org/x/image v0.41.0.
InputComponent thumbnail caching and rendering
internal/ui/input.go
InputComponent caches per-image thumbnail strings (imageThumbs) aligned to pendingImages, uses imageGen to invalidate stale async renders, enqueues renderThumbnailCmd when capacity allows, and updates View to emit cached thumbnails per pending image; clearing/submission nils the cache and increments imageGen.
User-facing documentation
README.md, www/pages/cli/commands.md
Adds keyboard shortcuts (Ctrl+V paste image, Ctrl+U clear attachments) and CLI docs describing image attachments, thumbnail preview behavior and fallbacks, and file-based attachment syntax with MIME auto-detection.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 With half-blocks stacked and colors bright,
Images peek through tmux's night,
No graphics protocol could survive,
But text-based thumbnails keep previews alive!
Cached once and rendered with care,
Terminal beauty, multiplexer-fair. 🎨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: adding half-block thumbnail rendering for attached images, which matches the primary objective of the changeset.
Linked Issues check ✅ Passed The PR implementation fully addresses all coding requirements from issue #46: half-block thumbnail rendering with truecolor/256-color/fallback support, aspect ratio preservation, decompression guards, async rendering with generation counters, and integration into the input component for clipboard and file attachments.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the linked issue scope: imagepreview package implementation, input component integration, README and CLI documentation updates, and required go.mod dependencies (colorprofile, golang.org/x/image).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/46-image-thumbnail-preview

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between ae722d5 and 8c7b007.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (6)
  • README.md
  • go.mod
  • internal/ui/imagepreview/imagepreview.go
  • internal/ui/imagepreview/imagepreview_test.go
  • internal/ui/input.go
  • www/pages/cli/commands.md

Comment thread internal/ui/imagepreview/imagepreview.go
Comment thread internal/ui/input.go
- 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
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Propagate imageGen through clipboard reads too.

Only thumbnailReadyMsg is generation-scoped right now. A ctrl+v started just before ctrl+u or submit can still deliver a late clipboardImageMsg and 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 in Update, and invalidate in-flight reads on ctrl+u even when pendingImages is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8c7b007 and f6a48f4.

📒 Files selected for processing (3)
  • internal/ui/imagepreview/imagepreview.go
  • internal/ui/imagepreview/imagepreview_test.go
  • internal/ui/input.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/ui/imagepreview/imagepreview_test.go

Comment thread internal/ui/input.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
@ezynda3 ezynda3 merged commit d27022b into master Jun 4, 2026
3 checks passed
ezynda3 added a commit that referenced this pull request Jun 4, 2026
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: render low-res image previews for attachments (tmux/zellij-safe)

1 participant