Files
kit/internal/ui/pending_thumbnail_layout_test.go
Ed Zynda 0313fa03ad fix(ui): show pasted image previews in input and transcript (#48)
* 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
2026-06-04 15:30:47 +03:00

86 lines
2.4 KiB
Go

package ui
import (
"strings"
"testing"
tea "charm.land/bubbletea/v2"
uicore "github.com/mark3labs/kit/internal/ui/core"
)
// drainCmds runs a tea.Cmd chain back through m.Update like the BubbleTea
// event loop, expanding batches, until no further messages are produced.
func drainCmds(t *testing.T, m *AppModel, cmd tea.Cmd) *AppModel {
t.Helper()
queue := []tea.Cmd{cmd}
for i := 0; i < 50 && len(queue) > 0; i++ {
c := queue[0]
queue = queue[1:]
if c == nil {
continue
}
msg := c()
if msg == nil {
continue
}
if batch, ok := msg.(tea.BatchMsg); ok {
queue = append(queue, batch...)
continue
}
updated, nc := m.Update(msg)
m = updated.(*AppModel)
_ = m.View()
if nc != nil {
queue = append(queue, nc)
}
}
return m
}
func measuredInputHeight(m *AppModel) int {
rendered := m.renderInput()
if rendered == "" {
return 0
}
return strings.Count(rendered, "\n") + 1
}
// TestPendingThumbnailTriggersLayoutRecompute is a regression test for the bug
// where a pasted image's async half-block preview rendered but was clipped off
// the bottom of the screen: the thumbnail arrives via thumbnailReadyMsg after
// distributeHeight already measured the input region without it. The parent
// must mark the layout dirty so the (now taller) input is re-measured.
func TestPendingThumbnailTriggersLayoutRecompute(t *testing.T) {
// Force a truecolor profile so imagepreview.Render deterministically
// produces a thumbnail regardless of the CI terminal's color support.
// Without this, a low-color test environment yields an empty preview and
// the glyph / height assertions below would flake.
t.Setenv("TERM", "xterm-256color")
t.Setenv("COLORTERM", "truecolor")
t.Setenv("NO_COLOR", "")
real := NewInputComponent(80, nil)
m, _, _ := newTestAppModel(nil)
m.input = real
m = sendMsg(m, tea.WindowSizeMsg{Width: 80, Height: 24})
heightBefore := measuredInputHeight(m)
updated, cmd := m.Update(clipboardImageMsg{image: &uicore.ImageAttachment{
Data: makeTestPNG(t, 16, 16),
MediaType: "image/png",
}})
m = updated.(*AppModel)
_ = m.View()
m = drainCmds(t, m, cmd)
heightAfter := measuredInputHeight(m)
if heightAfter <= heightBefore {
t.Errorf("input region should grow to fit the thumbnail (before=%d after=%d)", heightBefore, heightAfter)
}
if !strings.Contains(m.View().Content, "▀") {
t.Error("parent View should contain the half-block thumbnail (was clipped or not rendered)")
}
}