feat(ui): show inline images in the Details pane #71

Closed
opened 2026-10-08 23:37:13 -05:00 by deadstyle · 2 comments
Owner

Problem

Images in PR and issue bodies can't be seen. With #65 (fetch and decode) and #66 (kitty graphics layer and detection) merged, the Details pane can now draw them inline. Part 3 of #60.

Acceptance criteria

Sources: the PM's answers on #60 unless noted.

  1. In a supported terminal (kitty or Ghostty, auto-detected; tmux needs allow-passthrough on) an image that stands alone in a paragraph of a PR or issue body renders inline in the scrolling Details body, and scrolls and clips with the text. (PM: inline in the scrolling body)
  2. Only images hosted on the connected forge load. Others keep today's 🖼 alt link. (PM answer 1)
  3. Size: fit the pane width, capped at the pane's visible height, keeping aspect ratio; recomputed on resize. (PM answer 2)
  4. Settings has a "Show images" toggle, default on; off shows the 🖼 alt link everywhere. (PM answer 4)
  5. Unsupported terminal, tmux without allow-passthrough, a loading image and a failed image all show the 🖼 alt link, silently: no error message. (PM answers 3, 5, 6)
  6. Images inside text, tables, or HTML <img> (such as Renovate badges) keep the 🖼 alt link.
  7. Quitting or leaving the session deletes the images from the terminal; scrolling the Details pane with an image half in view shows the visible part correctly.
  8. docs/design.md records these behaviors; docs/adr/0014 Consequences notes images now render in supported terminals.

Plan

Decided by Opus (design on #60, ADR 0016 from #65, internal/ui/termimg from #66):

  • markdown.Images(body) []string lists block images (a paragraph whose only content is images, optionally wrapped in a link); markdown.RenderWithImages(body, width, blocks map[string][]string) splices prepared lines in. Render(body, width) is the same call with nil, so the markdown package stays pure and knows nothing about kitty.
  • details keeps per-URL state (loading, ready(id, cols, rows), failed). On sync it issues a load command (core.Service.Image) per unknown block image; imageLoadedMsg leads to tea.Sequence(tea.Raw(termimg.Transmit(...)), placedMsg); only placedMsg bumps an imgGen counter that is part of the mdMemo key. Resize recomputes cols and rows and sends Place. Session close and the toggle going off send Delete for every held ID.
  • termimg.IDs allocates IDs; Support from #66 gates everything.
  • Settings toggle follows the existing Settings pattern (ADR 0012).
  • Wezterm stays out (answers the graphics query, lacks placeholders).

Context

  • #65 delivered forge.AssetReader, core.Service.Image/PeekImage ((image.Image, bool, error)), the 16-entry LRU (cleared by r). #66 delivered termimg and Deps.Detect/App.support.
  • internal/ui/details.go renders via a one-entry mdMemo; image lines must flow through the same memo.
  • Real-terminal check (kitty, Ghostty, tmux with passthrough) is manual; list what was seen in the MR.

Out of scope

HTML <img>, WebP, external hosts, other terminals.

Refs #60

## Problem Images in PR and issue bodies can't be seen. With #65 (fetch and decode) and #66 (kitty graphics layer and detection) merged, the Details pane can now draw them inline. Part 3 of #60. ## Acceptance criteria Sources: the PM's answers on #60 unless noted. 1. In a supported terminal (kitty or Ghostty, auto-detected; tmux needs `allow-passthrough` on) an image that stands alone in a paragraph of a PR or issue body renders inline in the scrolling Details body, and scrolls and clips with the text. *(PM: inline in the scrolling body)* 2. Only images hosted on the connected forge load. Others keep today's `🖼 alt` link. *(PM answer 1)* 3. Size: fit the pane width, capped at the pane's visible height, keeping aspect ratio; recomputed on resize. *(PM answer 2)* 4. Settings has a "Show images" toggle, default on; off shows the `🖼 alt` link everywhere. *(PM answer 4)* 5. Unsupported terminal, tmux without `allow-passthrough`, a loading image and a failed image all show the `🖼 alt` link, silently: no error message. *(PM answers 3, 5, 6)* 6. Images inside text, tables, or HTML `<img>` (such as Renovate badges) keep the `🖼 alt` link. 7. Quitting or leaving the session deletes the images from the terminal; scrolling the Details pane with an image half in view shows the visible part correctly. 8. `docs/design.md` records these behaviors; `docs/adr/0014` Consequences notes images now render in supported terminals. ## Plan Decided by Opus (design on #60, ADR 0016 from #65, `internal/ui/termimg` from #66): - `markdown.Images(body) []string` lists block images (a paragraph whose only content is images, optionally wrapped in a link); `markdown.RenderWithImages(body, width, blocks map[string][]string)` splices prepared lines in. `Render(body, width)` is the same call with nil, so the markdown package stays pure and knows nothing about kitty. - `details` keeps per-URL state (loading, ready(id, cols, rows), failed). On sync it issues a load command (`core.Service.Image`) per unknown block image; `imageLoadedMsg` leads to `tea.Sequence(tea.Raw(termimg.Transmit(...)), placedMsg)`; only `placedMsg` bumps an `imgGen` counter that is part of the `mdMemo` key. Resize recomputes cols and rows and sends `Place`. Session close and the toggle going off send `Delete` for every held ID. - `termimg.IDs` allocates IDs; `Support` from #66 gates everything. - Settings toggle follows the existing Settings pattern (ADR 0012). - Wezterm stays out (answers the graphics query, lacks placeholders). ## Context - #65 delivered `forge.AssetReader`, `core.Service.Image/PeekImage` (`(image.Image, bool, error)`), the 16-entry LRU (cleared by `r`). #66 delivered `termimg` and `Deps.Detect`/`App.support`. - `internal/ui/details.go` renders via a one-entry `mdMemo`; image lines must flow through the same memo. - Real-terminal check (kitty, Ghostty, tmux with passthrough) is manual; list what was seen in the MR. ## Out of scope HTML `<img>`, WebP, external hosts, other terminals. Refs #60
Author
Owner

🤖 Planning #71 turned up three places where the acceptance criteria could be read two ways. I'm going with these readings unless you say otherwise:

  1. Which images draw. Only an image that is the whole content of a top-level paragraph. A lone image inside a list item or quote keeps the 🖼 alt link, because the drawn image is pane-wide and would overflow the indent.
  2. Size. "Fit the pane width" means shrink to fit and never enlarge. A small icon or screenshot keeps its natural size; a large one is scaled down to the pane width, capped at the visible height.
  3. "Leaving the session" means switching to another host. Going back to the host picker or Settings keeps the session (and its images) alive; nothing is visible there either way.

Also: HTML <img> tags (such as Renovate badges) render nothing today, so they stay as they are rather than gaining a link.

🤖 Planning #71 turned up three places where the acceptance criteria could be read two ways. I'm going with these readings unless you say otherwise: 1. **Which images draw.** Only an image that is the whole content of a top-level paragraph. A lone image inside a list item or quote keeps the `🖼 alt` link, because the drawn image is pane-wide and would overflow the indent. 2. **Size.** "Fit the pane width" means shrink to fit and never enlarge. A small icon or screenshot keeps its natural size; a large one is scaled down to the pane width, capped at the visible height. 3. **"Leaving the session"** means switching to another host. Going back to the host picker or Settings keeps the session (and its images) alive; nothing is visible there either way. Also: HTML `<img>` tags (such as Renovate badges) render nothing today, so they stay as they are rather than gaining a link.
Author
Owner

🤖 merged: #78

🤖 merged: https://git.bobparsons.dev/deadstyle/lazyforge/pulls/78
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
deadstyle/lazyforge#71
No description provided.