feat(cli): /images opens image URLs from the last response in the browser (#103) - #222
Merged
Conversation
…wser (#103) GitHub issues attach screenshots that `gh issue view` prints as markdown/URLs the terminal can't render. /images scans the most recent turn's output (assistant text + tool results), extracts image URLs, and opens them in the default browser (macOS `open` / else `xdg-open`). - src/images.zig: pure extractImageUrls — markdown images (incl. CommonMark titles and <url> forms), bare URLs ending in an image extension, and GitHub attachment hosts (user-attachments / *.githubusercontent.com) that serve images without one. Order-preserving, de-duplicated, http(s)-only so a model can't smuggle a javascript:/file: scheme. 8 unit tests. - commands_model.zig: the /images handler (before /image's startsWith so it isn't swallowed) + recentTurnText, which serializes the last turn to JSON and scans it (provider-agnostic — URLs survive serialization). The turn boundary uses cleanUserTurn so an image-attachment prompt (array content) still bounds the scan. Transient work runs in a scratch arena freed on return (not the session arena, #124). The browser spawn is capped at 8 tabs; the full list always prints. - oauth.zig: openBrowser made pub. main.zig: GRAFF_NO_BROWSER=1 (headless/SSH) lists URLs instead of spawning. command_catalog.zig: catalog entry. The chat repl points /images at the main session (it runs no tools), like /image /bash. Reviewed adversarially (correctness/integration/resource lenses + per-finding verification); 4 confirmed findings fixed: attachment-prompt boundary, CommonMark title/angle URLs, uncapped tabs, session-arena churn. Tests: src/images.zig 8 unit tests; new scripts/test-pty-images.py drives a mock backend returning two attachment URLs and asserts /images extracts + lists them (GRAFF_NO_BROWSER=1, no real tabs) plus the zero-image path; zig build test green, zig fmt clean, streaming PTY suite green. Closes #103 Co-Authored-By: blackfloofie <265516171+blackfloofie@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
GitHub issues attach screenshots, which
gh issue viewprints as markdown/URLs — but a terminal can't render images, so you see an inert link and no picture:Fix — a
/imagescommandScans the most recent turn's output (assistant text + tool results) for image URLs and opens them in the default browser (macOS
open/ elsexdg-open):(shown with
GRAFF_NO_BROWSER=1, which lists instead of opening — for headless/SSH, and how the test asserts without spawning tabs. Normally it printsopening …and launches them.)How it works
src/images.zig— pureextractImageUrls: markdown(incl. CommonMark"title"and<url>forms), bare URLs ending in an image extension, and GitHub attachment hosts (user-attachments/*.githubusercontent.com) that serve images without one. Order-preserving, de-duplicated, http(s)-only so a model can't smuggle ajavascript:/file:scheme. 8 unit tests.commands_model.zig— the handler (placed before/image'sstartsWithso/imagesisn't swallowed) +recentTurnText, which serializes the last turn to JSON and scans it (provider-agnostic — URLs survive serialization). The turn boundary usescleanUserTurnso an image-attachment prompt (array content) still bounds the scan. Transient work runs in a scratch arena freed on return (not the long-lived session arena, Long-running sessions grow RSS unboundedly — root Agent.arena is never reset (benchmarks/memory.py can't see it) #124). The browser spawn is capped at 8 tabs; the full list always prints.oauth.zig—openBrowsermadepub.main.zig—GRAFF_NO_BROWSER=1.command_catalog.zig— catalog entry (menu/help/completion). The chat repl points/imagesat the main session (it runs no tools), like/image//bash.Review
Ran an adversarial review of the diff (correctness / integration / resource-safety lenses, each finding then independently verified). 7 candidates → 4 confirmed and fixed:
recentTurnTextboundary missed image-attachment prompts (array content) → widened the scan → now usescleanUserTurn./mis-parsed → now cut at whitespace + unwrap angle brackets (+ tests).Tests
src/images.zig: 8 unit tests (markdown/bare/host forms, dedupe, trailing punct, JSON context, title/angle, empty/no-match).scripts/test-pty-images.py: drives a mock backend returning two attachment URLs, asserts/imagesextracts + lists them (GRAFF_NO_BROWSER=1, no real tabs) plus the zero-image path. (This E2E test caught a realstd.json.Stringify-reuse crash during development that the unit tests couldn't.)zig build testgreen,zig fmtclean, streaming PTY suite green.Closes #103