feat(app,api): inline terminal image gallery for detected paths#241
feat(app,api): inline terminal image gallery for detected paths#241skulidropek merged 16 commits intoProverCoderAI:mainfrom
Conversation
Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: ProverCoderAI#240
Add a pure module that scans terminal output for absolute image paths (png, jpg, jpeg, gif, webp), strips ANSI escape sequences, and deduplicates results. This is the first step toward rendering inline image previews for paths that appear in the terminal.
- Use String.fromCodePoint and uppercase hex literals - Use String.raw for regex sources containing backslashes - Switch test paths from /tmp to /var/data to satisfy publicly-writable-directories rule
Pure validation module that produces a TerminalImageFetchPlan from a candidate absolute container path. Rejects empty, relative, whitespace, control-character, parent-traversal, and unsupported-extension inputs and returns the matched media type for valid paths.
GET /projects/by-key/:projectKey/terminal-sessions/:sessionId/image?path=... GET /projects/:projectId/terminal-sessions/:sessionId/image?path=... Streams the bytes of an image file from the project container so the web terminal can render image paths inline. Validates the requested path with planTerminalImageFetch, runs docker exec -u dev cat -- <path>, enforces a 10 MiB cap, and surfaces missing files as 404 and oversize as 400.
Wires the terminal output image-path detector into a thumbnail strip under the terminal viewport. Each detected absolute path is fetched via the project terminal-sessions image endpoint and shown as a clickable thumbnail (opens in a new tab). The list is deduplicated and capped at the most recent 20 paths per session and is reset when the active session changes.
The Effect-TS lint config forbids `as` expressions outside src/core/axioms.ts. Type the supported-extensions list with an explicit `ReadonlyArray<string>` annotation instead of an `as const` literal.
The PR is no longer a placeholder draft; the placeholder file added at branch creation can be deleted.
Working session summaryPR #241 is now marked ready for review with an updated description that documents CI parity with main and explains the cancelled Final state:
This summary was automatically extracted from the AI working session output. |
🤖 Solution Draft LogThis log file contains the complete execution trace of the AI solution draft process. 💰 Cost: $26.513267📊 Context and tokens usage:Claude Opus 4.7: (6 sub-sessions)
Total: (33.0K new + 592.5K cache writes + 37.0M cache reads) input tokens, 166.3K output tokens, $26.513267 cost 🤖 Models used:
📎 Log file uploaded as Gist (11305KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
🔄 Auto-restart 1/5Detected uncommitted changes from previous run. Starting new session to review and commit or discard them. Uncommitted files: Auto-restart will stop after changes are committed or discarded, or after 4 more iterations. Please wait until working session will end and give your feedback. |
🔄 Auto-restart 1/5 LogThis log file contains the complete execution trace of the AI solution draft process. 💰 Cost: $0.328605📊 Context and tokens usage:
Total: (15 new + 31.1K cache writes + 167.9K cache reads) input tokens, 2.0K output tokens, $0.328605 cost 🤖 Models used:
📎 Log file uploaded as Gist (11632KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
Working session summaryAll three PR comments are from the prior session (status updates and the auto-restart notice), no new human review comments. The 3 failing checks (Lint, Lint Effect-TS, Test) on the latest run are documented as pre-existing parity with main. The uncommitted ci-logs directory has been cleaned up — it contained empty debug log files from the prior session that aren't part of the solution. The working tree is now clean and the PR remains in its prior ready state. Cleaned up the leftover empty This summary was automatically extracted from the AI working session output. |
🔄 Auto-restart triggered (iteration 1)Reason: CI failures detected; Merge conflicts Starting new session to address the issues. Auto-restart-until-mergeable mode is active. This run will stop after 5 restart iterations. |
# Conflicts: # packages/app/src/web/panel-terminal.tsx
Reuse resolveTerminalApiOriginUrl from terminal.ts for image fetch URL resolution to eliminate the duplicate origin builder, and merge the image fetch URL test cases into terminal.test.ts so they share the existing mock setup. Extract a same-origin location stub helper to keep individual test bodies short. Also extract attachGlobalResizeListeners out of mountTerminalSession to stay under the max-lines-per-function threshold.
🔄 Auto-restart-until-mergeable Log (iteration 1)This log file contains the complete execution trace of the AI solution draft process. 💰 Cost: $6.353594📊 Context and tokens usage:Claude Opus 4.7: (2 sub-sessions)
Total: (1.9K new + 162.1K cache writes + 8.9M cache reads) input tokens, 36.0K output tokens, $6.353594 cost 🤖 Models used:
📎 Log file uploaded as Gist (14658KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
Working session summaryI'll wait on the running background watchers instead. They'll notify when checks finish. This summary was automatically extracted from the AI working session output. |
🔄 Auto-restart triggered (iteration 2)Reason: Uncommitted changes detected Starting new session to address the issues. Auto-restart-until-mergeable mode is active. This run will stop after 5 restart iterations. |
🔄 Auto-restart-until-mergeable Log (iteration 2)This log file contains the complete execution trace of the AI solution draft process. 💰 Cost: $1.619063📊 Context and tokens usage:
Total: (55 new + 71.5K cache writes + 2.0M cache reads) input tokens, 7.1K output tokens, $1.619063 cost 🤖 Models used:
📎 Log file uploaded as Gist (15634KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
Working session summaryPR is SummaryPR #241 is now fully green and ready to merge:
PR: #241 — This summary was automatically extracted from the AI working session output. |
✅ Ready to mergeThis pull request is now ready to be merged:
Monitored by hive-mind with --auto-restart-until-mergeable flag |
|
Screenshots for verification (commit af2c01f): Terminal image gallery after printing an absolute PNG path: Fetched image opened from the gallery link: |
|
Inline terminal image preview verification (commit 21f42cc): The terminal now renders an image preview directly under the printed image path, and the path itself opens the full image. Full image opened from the clickable terminal path: |
|
Compact inline terminal image preview verification (commit c2f11c6): The terminal printed a real 960x540 PNG path, but the inline preview is capped to a small thumbnail box (verified at 96x56 container, image rendered at 86x46). The full image remains available from the image URL/path. Full image view: |


Summary
Closes #240.
When the terminal stream prints an absolute path that looks like an image file (e.g.
/var/data/screenshot.png), the path is detected and a thumbnail strip below the terminal renders the image inline. Each thumbnail is clickable and opens the full-resolution image in a new tab.The image bytes are served by a new authenticated
terminal-sessionsimage endpoint that runsdocker exec cat <path>inside the same project container the terminal is attached to, with strict path validation and a 10 MB cap.Changes
Backend (
packages/api)services/terminal-image-fetch-core.ts— pure path validator (planTerminalImageFetch): rejects relative paths, traversal, NUL bytes, and non-image extensions; enforcesterminalImageFetchMaxBytes = 10 * 1024 * 1024.services/terminal-sessions.ts— addsreadProjectTerminalImagewhich streams the file viadocker execfor the project's container.http.ts— addswithProjectTerminalImagesrouter exposing:GET /projects/by-key/:projectKey/terminal-sessions/:sessionId/image?path=<abs>GET /projects/:projectId/terminal-sessions/:sessionId/image?path=<abs>Responses use
HttpServerResponse.uint8Arraywith the detected MIME type.Frontend (
packages/app)web/terminal-image-paths.ts— detects absolute image paths in terminal output (strips ANSI CSI/OSC sequences first, matches.png|.jpg|.jpeg|.gif|.webp).web/terminal-image-url.ts— derives the image fetch URL fromsession.websocketPathby replacing the/wssuffix with/imageand appending thepathquery, honoringresolveApiBaseUrl()for both absolute and same-origin proxy bases.web/terminal-image-gallery-core.ts— pure list merge: dedupes by path, caps at the 20 most recent entries, and returns the same reference when nothing new arrives (so React can skip re-renders).web/terminal-panel-runtime-{types,core,runtime}.ts— threads an optionalonImagePathscallback through the lifecycle. When the server sends anoutputmessage, the runtime extracts any new image paths and forwards them to the panel.web/panel-terminal.tsx— owns the gallery state, resets on session change, builds entries viaresolveTerminalImageFetchUrl, and renders the thumbnail strip beneath the terminal body.Tests
New unit tests, all running under
vitest:tests/docker-git/terminal-image-paths.test.ts— strips ANSI before matching; finds paths in mixed output.tests/docker-git/terminal-image-url.test.ts—/ws→/imagesuffix swap; absolute URL composition with encodedpathquery; same-origin proxy mode.tests/docker-git/terminal-image-gallery-core.test.ts— append, dedupe, identity preservation when no additions, FIFO trim, and theterminalImageGalleryLimitconstant.tests/docker-git/terminal-image-fetch-core.test.ts(api) — accept/reject matrix forplanTerminalImageFetch.CI parity with main
Lint,Lint Effect-TS, andTestare red on this PR and onmain(run 25343558501) with the same error sets:Lint Effect-TS: 8 errors at exactly the same lines on both branches (Casting is only allowed in src/core/axioms.tsat8:65,9:17,22:27,56:27,282:79— all in pre-existing files).Test: 48 errors (8 TypeScript, 40 ESLint) on both branches; the failing files (tests/docker-git/terminal.test.ts,tests/docker-git/app-ready-create.test.ts) are unchanged by this PR.Lint: 14 distinct error patterns, identical between main and this branch; theTotal: 39 vs 38headline difference is line-number drift inside pre-existing nested-ternary warnings inpanel-terminal.tsx, not a new violation.E2E (Login context)was cancelled at the 40-minute job timeout while still runningapt-get install containerd(Azure mirror slowness). My code never ran in that job; the other five E2E checks (Local package CLI, OpenCode, Clone cache, Clone auto-open SSH, Runtime volumes + SSH) all pass.Test plan
bunx tsc --noEmit(clean)bunx vitest runinpackages/app(47 files / 236 tests pass, including the 19 new terminal-image tests)bunx vitest runinpackages/api(image-fetch core suite passes)mainis also green here; failing checks have identical pre-existing errors