Skip to content

UI preview image#339

Merged
wonderwhy-er merged 12 commits into
wonderwhy-er:mainfrom
edgarsskore:ui-preview-image
Feb 16, 2026
Merged

UI preview image#339
wonderwhy-er merged 12 commits into
wonderwhy-er:mainfrom
edgarsskore:ui-preview-image

Conversation

@edgarsskore

@edgarsskore edgarsskore commented Feb 16, 2026

Copy link
Copy Markdown
Collaborator

User description

  • Added image preview support for read_file using structuredContent (fileType: image, imageData, mimeType).
  • Updated the preview UI to render allowlisted image MIME types, including SVG.
  • Unified image transport by keeping content text-only for all images (better host compatibility).

CodeAnt-AI Description

Add image previews and absolute file paths for file preview UI

What Changed

  • File previews now return absolute file paths (expands ~ and resolves relative paths) so "Open in folder" works reliably across platforms.
  • Image files are delivered in the structured preview payload (imageData + mimeType) instead of as an inline image content item; the legacy content array keeps a short text summary only.
  • The preview UI now renders allowlisted image MIME types (including SVG) from structured content, shows a clear notice when an image format is not supported or image data is missing, and disables the "Copy source" action for images.
  • MIME types are normalized and checked against an allowlist; unit tests were added to validate image allowlist, MIME normalization, and image preview contract.

Impact

✅ Preview images render in the widget from structured payloads
✅ Open in folder works with ~/ and relative paths
✅ Clearer behavior for unsupported/missing image previews

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • New Features

    • Image file preview support (PNG, JPEG, GIF, WebP, BMP, SVG) and image-specific preview mode
    • "Open in folder" action for file previews
  • Improvements

    • Improved local path resolution for consistent previews and open-in-folder behavior
    • More robust copy-to-clipboard flow and minor accessibility/labeling tweaks
    • Refined image display UI with centered layout and size constraints
  • Tests

    • Added image/SVG preview tests and MIME-type validation tests

edgarsskore and others added 11 commits February 11, 2026 23:32
Use a PowerShell encoded invocation that passes the selected file path as a single explorer argument, preventing cmd metacharacter injection while preserving /select behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
Store absolute file paths in structured content so the "Open in folder"
button works correctly with relative paths (~, ./, etc). Previously,
paths like ~/file.txt or ./file.txt would fail because:
- Tilde is not expanded inside shell single quotes
- Relative paths depend on the MCP server's current working directory

Co-authored-by: Cursor <cursoragent@cursor.com>
@codeant-ai

codeant-ai Bot commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai

coderabbitai Bot commented Feb 16, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds image preview support and absolute local-path resolution: filesystem handler expands home and resolves absolute paths, types gain imageData/mimeType, UI app and toolbar render image previews via data URLs, new image MIME utilities added, styles and tests updated for SVG/image handling.

Changes

Cohort / File(s) Summary
Filesystem handler
src/handlers/filesystem-handlers.ts
Add home expansion and absolute-path resolution; compute resolvedFilePath for local reads and populate structuredContent.fileName/filePath; move image bytes/mime into structuredContent and mark image fileType.
Types & Preview file type
src/types.ts, src/ui/file-preview/shared/preview-file-types.ts, src/ui/file-preview/src/types.ts
Add imageData?: string and mimeType?: string to FilePreviewStructuredContent; add 'image' to PreviewFileType union; remove UI-local PreviewStructuredContent.
Preview UI app
src/ui/file-preview/src/app.ts
Switch to FilePreviewStructuredContent payload; add image rendering path (renderImageBody) that validates MIME, builds data URLs, and delegates image previews; update copy/open/load handlers and renderApp signature.
Toolbar
src/ui/file-preview/src/components/toolbar.ts
Accept FilePreviewStructuredContent and new canOpenInFolder flag; surface “Open in folder” button and related state (disabled/title) for local resolved paths.
Image utilities & tests
src/ui/file-preview/src/image-preview.ts, test/test-file-preview-image-runtime.js, test/test-file-handlers.js
New allowlist, normalize, and validator for image MIME types; add runtime tests for MIME normalization/allowlist; extend file handler tests for SVG and imageData/mimeType assertions.
Styles
src/ui/styles/apps/file-preview.css
Add .image-content and .image-preview styles for centered image display and adjust code viewer overflow behavior.
Package manifest
package.json
Minor manifest change (lines changed recorded).

Sequence Diagram

sequenceDiagram
    participant User
    participant Handler as Filesystem Handler
    participant App as File Preview App
    participant UI as Renderer/DOM

    User->>Handler: Request file preview
    Handler->>Handler: expandHome() & resolveAbsolutePath()
    Handler->>Handler: detect file type (image/pdf/text/html)
    alt Local image
        Handler->>Handler: read bytes, normalize mime, encode imageData
        Handler->>App: return FilePreviewStructuredContent (fileType: image, imageData, mimeType, resolved filePath)
    else Remote URL or other
        Handler->>App: return structuredContent (url or text/html/unsupported)
    end

    App->>App: branch on payload.fileType
    alt image
        App->>App: validate mime with isAllowedImageMimeType()
        App->>UI: render <img> with data URL from imageData
    else other
        App->>UI: render markdown/text/html or unsupported view
    end

    UI->>User: display preview and toolbar (copy/open actions)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • wonderwhy-er
  • serg33v

Poem

🐰
I hopped through paths both near and far,
Found homes expanded, files on par.
I chewed the MIME and stitched the bytes,
Now images glow in data lights.
Hooray — the preview sees each star! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'UI preview image' is vague and generic, using non-descriptive terms that don't clearly convey the specific changes made in the changeset. Consider a more descriptive title that summarizes the main change, such as 'Add image preview support with MIME type validation and path resolution' or 'Enable image rendering in file preview widget with absolute path handling'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Feb 16, 2026
@codeant-ai

codeant-ai Bot commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • Possible Crash
    inferFilePill calls payload.filePath.toLowerCase() assuming filePath exists. If structured payloads are missing filePath (or it's undefined), this will throw. Add a defensive guard or default to an empty string.

  • Path mismatch
    The handler computes resolvedFilePath (expanded/absolute) for use in structuredContent but continues to call readFile with the original parsed.path. This can cause reading failures or inconsistent behavior (e.g. when paths use ~) because the actual read uses a different path than what the UI "Open in folder" or structuredContent points to.

  • XSS
    The toolbar builds HTML strings using file metadata (fileName, filePath) without escaping. If a file's name/path contain HTML or quotes, they can break attributes or introduce XSS when this HTML is injected. Verify metadata is always treated as plain text and escape before interpolation.

  • Image MIME handling
    renderImageBody calls normalizeImageMimeType(payload.mimeType) without verifying that payload.mimeType is present. If mimeType is undefined or malformed, normalization or subsequent checks could behave unexpectedly. Also ensure imageData is validated (base64, size) before building a data: URI to avoid very large in-memory payloads or unexpected content.

  • Home expansion differences
    A new expandHome implementation broadens behavior (checks any filePath.startsWith('~')) compared to the existing helper in tools/filesystem.ts (which checks for '~/' or '~'). This may incorrectly expand inputs like ~username/... or ~something and creates duplication/inconsistency between modules.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/handlers/filesystem-handlers.ts`:
- Around line 38-43: The expandHome function erroneously expands paths like
"~otheruser/file"; restrict expansion to only the standalone "~" and the
current-user prefix by checking filePath === '~' or that it starts with "~/" or
"~" + path.sep, then replace only that leading "~" with os.homedir(); update the
logic inside expandHome (refer to function name expandHome and use path and os
imports) to perform this stricter check and return the original filePath
unchanged for "~user/..." forms.

In `@test/test-file-handlers.js`:
- Around line 344-354: The SVG test block is missing assertions for
structuredContent.filePath and the nested _meta.ui.resourceUri like the image
test; update the SVG assertions after calling handleReadFile (svgResult) to
assert svgResult.structuredContent.filePath equals the expected
FILE_PREVIEW_RESOURCE_URI/path value and assert svgResult._meta.ui &&
svgResult._meta.ui.resourceUri (or svgResult._meta['ui/resourceUri']) equals
FILE_PREVIEW_RESOURCE_URI, using the same assertion style (assert.strictEqual /
assert.ok) as the image test to keep parity with handleReadFile output
validation.
🧹 Nitpick comments (3)
src/ui/file-preview/src/components/toolbar.ts (1)

8-24: inferFilePill doesn't have an explicit case for fileType === 'image' — it falls through to extension-based labeling.

This works fine in practice (e.g., .png → "PNG", .svg → "SVG"), but if an image file has no extension or a long one, the fallback extension.slice(0, 4).toUpperCase() might produce an unclear label. Consider whether an explicit 'image' case (e.g., label: "IMG") would be more consistent with the markdown/html branches.

src/ui/file-preview/src/app.ts (2)

253-258: Minor: cleanedContent is computed but unused for image payloads.

stripReadStatusLine on line 254 runs for image file types even though the result is never used (image branch returns early on line 257). Consider moving the computation below the image check.

♻️ Suggested reorder
 function renderBody(payload: FilePreviewStructuredContent, htmlMode: HtmlPreviewMode, startLine = 1): { html: string; notice?: string } {
-    const cleanedContent = stripReadStatusLine(payload.content);
-
     if (payload.fileType === 'image') {
         return renderImageBody(payload);
     }
 
+    const cleanedContent = stripReadStatusLine(payload.content);
+
     if (payload.fileType === 'unsupported') {

642-654: Footer label shows line count for image files, which may be confusing.

For image payloads, lineCount is computed from payload.content (the text description), so the footer reads something like "IMAGE • 1 LINE". This might confuse users who expect image metadata (e.g., dimensions) rather than a description line count. Consider suppressing the line count for image file types.

♻️ Suggested change
     const footerLabel = range?.isPartial
         ? `${escapeHtml(fileTypeLabel)} • LINES ${range.fromLine}–${range.toLine} OF ${range.totalLines}`
-        : `${escapeHtml(fileTypeLabel)} • ${lineCount} LINE${lineCount !== 1 ? 'S' : ''}`;
+        : payload.fileType === 'image'
+            ? escapeHtml(fileTypeLabel)
+            : `${escapeHtml(fileTypeLabel)} • ${lineCount} LINE${lineCount !== 1 ? 'S' : ''}`;

Comment thread src/handlers/filesystem-handlers.ts
Comment on lines +344 to +354
const svgResult = await handleReadFile({ path: SVG_FILE });
assert.ok(Array.isArray(svgResult.content), 'SVG result should include content array');
assert.ok(!svgResult.content.some((item) => item.type === 'image'), 'SVG result should avoid image content item for host compatibility');
assert.ok(svgResult.structuredContent, 'SVG should include structuredContent');
assert.strictEqual(svgResult.structuredContent.fileType, 'image', 'SVG should map to image preview state');
assert.strictEqual(svgResult.structuredContent.mimeType, 'image/svg+xml', 'SVG structured payload should include SVG mimeType');
assert.strictEqual(typeof svgResult.structuredContent.imageData, 'string', 'SVG structured payload should include imageData');
assert.ok(svgResult.structuredContent.imageData.length > 0, 'SVG structured payload should include non-empty imageData');
assert.strictEqual(svgResult._meta['ui/resourceUri'], FILE_PREVIEW_RESOURCE_URI, 'SVG should include ui/resourceUri');
assert.strictEqual(svgResult._meta['openai/widgetAccessible'], true, 'SVG should enable widget accessibility');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

SVG test block is missing filePath and nested ui.resourceUri assertions that the image block includes.

The image test (lines 339–342) validates structuredContent.filePath and _meta.ui.resourceUri, but the SVG block omits both. This creates an inconsistency and could mask regressions in the SVG path.

Suggested additions
   assert.ok(svgResult.structuredContent.imageData.length > 0, 'SVG structured payload should include non-empty imageData');
+  assert.strictEqual(svgResult.structuredContent.filePath, SVG_FILE, 'SVG file path should be present');
   assert.strictEqual(svgResult._meta['ui/resourceUri'], FILE_PREVIEW_RESOURCE_URI, 'SVG should include ui/resourceUri');
+  assert.strictEqual(svgResult._meta.ui.resourceUri, FILE_PREVIEW_RESOURCE_URI, 'SVG should include preview resource URI');
   assert.strictEqual(svgResult._meta['openai/widgetAccessible'], true, 'SVG should enable widget accessibility');
🤖 Prompt for AI Agents
In `@test/test-file-handlers.js` around lines 344 - 354, The SVG test block is
missing assertions for structuredContent.filePath and the nested
_meta.ui.resourceUri like the image test; update the SVG assertions after
calling handleReadFile (svgResult) to assert
svgResult.structuredContent.filePath equals the expected
FILE_PREVIEW_RESOURCE_URI/path value and assert svgResult._meta.ui &&
svgResult._meta.ui.resourceUri (or svgResult._meta['ui/resourceUri']) equals
FILE_PREVIEW_RESOURCE_URI, using the same assertion style (assert.strictEqual /
assert.ok) as the image test to keep parity with handleReadFile output
validation.

}
}

if (import.meta.url === `file://${process.argv[1]}`) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: The equality check between import.meta.url and file://${process.argv[1]} will almost never be true because import.meta.url is an absolute file:///... URL while process.argv[1] is typically a plain path (often relative), so the tests will not auto-run when the file is executed directly with Node. You should normalize process.argv[1] to a file:// URL in the same way Node does (e.g., resolving it against the current working directory) before comparing it to import.meta.url. [logic error]

Severity Level: Major ⚠️
- ⚠️ Direct CLI execution of runtime tests does nothing.
- ⚠️ Developers may think tests ran when they did not.
- ⚠️ Automated scripts invoking this file directly skip tests.
Suggested change
if (import.meta.url === `file://${process.argv[1]}`) {
if (process.argv[1] && import.meta.url === new URL(process.argv[1], `file://${process.cwd()}/`).href) {
Steps of Reproduction ✅
1. From the project root, run the test file directly with Node: `node
test/test-file-preview-image-runtime.js` (this file is
`test/test-file-preview-image-runtime.js:1-66`).

2. At module evaluation, Node sets `import.meta.url` to an absolute file URL like
`file:///ABS_PATH/test/test-file-preview-image-runtime.js` (see bottom guard at
`test/test-file-preview-image-runtime.js:59-66`).

3. In the same process, `process.argv[1]` is the path argument passed to Node, typically a
plain (often relative) path such as `test/test-file-preview-image-runtime.js`, so the
template literal produces `file://test/test-file-preview-image-runtime.js`.

4. Because `import.meta.url` (`file:///ABS_PATH/...`) and ``file://${process.argv[1]}``
(`file://test/...`) differ, the condition `import.meta.url ===
\`file://${process.argv[1]}\`` at line 59 is false, the `runTests()` block is never
executed, and the runtime tests do not auto-run when the script is invoked directly.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** test/test-file-preview-image-runtime.js
**Line:** 59:59
**Comment:**
	*Logic Error: The equality check between `import.meta.url` and ``file://${process.argv[1]}`` will almost never be true because `import.meta.url` is an absolute `file:///...` URL while `process.argv[1]` is typically a plain path (often relative), so the tests will not auto-run when the file is executed directly with Node. You should normalize `process.argv[1]` to a `file://` URL in the same way Node does (e.g., resolving it against the current working directory) before comparing it to `import.meta.url`.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

@codeant-ai

codeant-ai Bot commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

CodeAnt AI finished reviewing your PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/handlers/filesystem-handlers.ts (1)

154-154: Minor path inconsistency: imageSummary uses raw parsed.path while structuredContent uses resolvedFilePath.

For consistency, consider using resolvedFilePath in the summary string as well (same applies to the PDF summary on line 133). Currently a user could see ~/photo.png in the text content but /home/user/photo.png in the structured metadata.

Suggested fix
-            const imageSummary = `Image file: ${parsed.path} (${fileResult.mimeType})\n`;
+            const imageSummary = `Image file: ${resolvedFilePath} (${fileResult.mimeType})\n`;

And for the PDF case (line 133):

-                    text: `PDF file: ${parsed.path}${author}${title} (${meta?.totalPages} pages) \n`
+                    text: `PDF file: ${resolvedFilePath}${author}${title} (${meta?.totalPages} pages) \n`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants