Skip to content

Improve local file link validation and add inline text file preview/edit UX#53

Merged
friuns2 merged 1 commit intofriuns2:mainfrom
marmeladema:improved-file-browser
Apr 14, 2026
Merged

Improve local file link validation and add inline text file preview/edit UX#53
friuns2 merged 1 commit intofriuns2:mainfrom
marmeladema:improved-file-browser

Conversation

@marmeladema
Copy link
Copy Markdown
Contributor

@marmeladema marmeladema commented Apr 14, 2026

This PR improves the local file browsing experience in chat messages and the built-in local file browser.

It addresses two core UX issues:

  • path-like text in messages was often turned into clickable file links even when the file did not exist on the host, leading users to dead-end File not found pages
  • text/code files often opened as downloads instead of an in-browser preview, which made quick inspection unnecessarily awkward

With this change:

  • only real local paths become clickable file links
  • missing paths remain plain text
  • markdown file references only linkify when the target exists
  • unresolved markdown references remain readable and preserve context
  • text/code files open in an inline preview instead of forcing a download
  • preview and edit pages support a broader set of languages
  • preview has a plain-text fallback if Ace fails to load
  • preview fills the available viewport height below the toolbar
  • dev and prod local-browse behavior are aligned

Problems addressed

False-positive local file links

Previously, the chat renderer would eagerly convert many path-like strings into browse links even when they did not point to a real file on the current machine.

This produced misleading UI for cases such as:

  • nonexistent paths
  • command-like strings such as some/file.rs --flag
  • markdown file references whose target path did not exist

The result was a poor interaction loop: users clicked what looked like a valid local file link and landed on a File not found response.

This PR changes file-link rendering to depend on server-side path existence checks.

Source files opening as downloads

Valid text/code files were often handled as downloads rather than opening in a useful in-browser view.

That behavior is inconvenient for common workflows like:

  • quickly inspecting code referenced in a conversation
  • checking config files
  • browsing source files before deciding whether to edit them

This PR adds a proper inline text preview page for text-like files, with actions for Raw, Download, and Edit.

Weak fallback behavior

There were also a couple of failure modes that degraded poorly:

  • unresolved markdown file references could lose useful context
  • preview pages depended on Ace loading successfully, with no good content fallback if the CDN script failed

This PR keeps unresolved references readable and adds a plain-text fallback preview.

Dev/prod drift

The Vite dev server and the main HTTP server did not fully match for local browsing behavior, which creates review and maintenance risk.

This PR aligns the relevant local-path probe, raw-file, browse, and edit behaviors in both environments.

Changes

1. Add server-backed local path probing

Introduced a local path probe endpoint and client-side probe flow so the UI can confirm whether candidate file paths actually exist before rendering them as links.

After this change:

  • existing local paths render as links
  • nonexistent paths stay plain text
  • command fragments that do not map to a valid path stay plain text

2. Preserve readable fallback text for unresolved markdown file references

Adjusted file-segment rendering so unresolved markdown file references do not collapse to incomplete or misleading text.

For unresolved references, the UI now preserves both:

  • the human-readable label
  • the original target path

Example:

[missing report](/tmp/example/missing.rs)

will remain readable as plain text rather than becoming a broken local file link.

3. Add inline text preview pages for local files

Text-like files now open in an HTML preview page instead of forcing immediate browser download behavior.

The preview page includes:

  • syntax-highlighted rendering via Ace
  • Raw action
  • Download action
  • Edit action
  • file metadata in the toolbar

Non-text files continue to use download-oriented behavior.

4. Add plain-text fallback when Ace is unavailable

If Ace fails to load, the preview page still renders the file contents in a plain <pre> block.

This preserves a useful read-only experience even when the CDN is blocked or unavailable.

5. Expand language coverage for preview/edit

Preview and edit pages now support a broader set of text/code file types, including:

  • JavaScript
  • TypeScript
  • Vue SFCs
  • Rust
  • Python
  • Lua
  • JSON
  • YAML
  • Markdown
  • TOML
  • C / C++
  • Bazel / BUILD-family files
  • plain text

Additional JS/TS module variants are also covered:

  • .mjs
  • .cjs
  • .mts
  • .cts

6. Make preview fill remaining viewport height

The preview container no longer stops at a short fixed-height region. It now stretches to fill the remaining viewport height below the toolbar, which makes code browsing significantly more usable.

7. Align Vite dev middleware with main server behavior

Updated Vite dev-server route handling to match the main server for:

  • POST /codex-api/local-paths/probe
  • /codex-local-file
  • /codex-local-browse/*path
  • /codex-local-edit/*path

This avoids fixing the feature in production while leaving development behavior inconsistent.

User-visible behavior after this PR

  • Real local file paths in messages become clickable.
  • Nonexistent file-like text does not become a link.
  • Markdown file references only become links when the target exists.
  • Missing markdown file references remain readable and preserve both label and target.
  • Text/code files open in-browser with syntax highlighting.
  • Raw serves plain text directly.
  • Download forces file download.
  • Edit opens the text editor for supported text-like files.
  • Preview still works in plain text when Ace does not load.
  • Preview uses the available screen height more effectively.

Testing

Verified with:

./node_modules/.bin/vue-tsc --noEmit
./node_modules/.bin/vite build
./node_modules/.bin/tsup

Manual test coverage was also updated in tests.md for:

  • valid vs invalid path linkification
  • unresolved markdown file references
  • inline preview behavior
  • preview fallback behavior when syntax highlighting is unavailable
  • language coverage across preview/edit
  • full-height preview layout

Why this is useful

This change makes local file references in chat much more trustworthy and much easier to use.

Before this PR:

  • many path-like strings became misleading broken links
  • valid source files often opened in an inconvenient way
  • preview behavior was more fragile than it should be

After this PR:

  • file links are based on server truth
  • source files are easy to inspect inline
  • failure modes degrade gracefully
  • local browse behavior is consistent across dev and prod

@qodo-code-review
Copy link
Copy Markdown

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Review Summary by Qodo

Improve local file link validation and add inline text file preview

✨ Enhancement 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Add server-backed local path probing to validate file existence before linkifying
• Create inline text preview pages for text/code files with syntax highlighting
• Expand language support for preview/edit to include Rust, Vue, Lua, Bazel, C/C++
• Preserve readable fallback text for unresolved markdown file references
• Align local file browsing behavior between dev and production environments
Diagram
flowchart LR
  A["Chat Message Text"] -->|"Parse file paths"| B["Collect Path Candidates"]
  B -->|"Probe server"| C["Local Path Probe Endpoint"]
  C -->|"Check existence"| D["File System"]
  D -->|"Return results"| E["Update Link State"]
  E -->|"Render conditionally"| F["Linkified or Plain Text"]
  G["Browse File Link"] -->|"Check file type"| H["Text File?"]
  H -->|"Yes"| I["Text Preview Page"]
  H -->|"No"| J["Download"]
  I -->|"Load Ace Editor"| K["Syntax Highlighted View"]
  K -->|"Fallback"| L["Plain Text Rendering"]
  I -->|"Actions"| M["Raw / Download / Edit"]
Loading

Grey Divider

File Changes

1. src/api/codexGateway.ts ✨ Enhancement +49/-0

Add local path probing API client

• Add LocalPathProbeEntry type to represent path existence check results
• Implement probeLocalPaths() function to query server for file existence
• Normalize and deduplicate paths before sending probe requests

src/api/codexGateway.ts


2. src/server/httpServer.ts ✨ Enhancement +115/-16

Add path probing endpoint and text preview support

• Add /codex-api/local-paths/probe POST endpoint for server-side path validation
• Refactor /codex-local-file to support text preview and download modes
• Add text preview rendering via createTextPreviewHtml() in browse endpoint
• Add validation to /codex-local-edit to ensure only text files are editable
• Implement readBooleanQueryFlag() helper for query parameter parsing
• Align dev and prod behavior for local file browsing

src/server/httpServer.ts


3. src/server/localBrowseUi.ts ✨ Enhancement +278/-35

Expand language support and add text preview page

• Replace simple extension set with comprehensive language configuration maps
• Add LocalFileLanguageConfig and LocalTextFileMetadata types
• Implement getLanguageConfigForPath() with filename and extension matching
• Add getLocalTextFileMetadata() to determine file language and size
• Create createTextPreviewHtml() with Ace editor and plain-text fallback
• Enhance createTextEditorHtml() with preview/raw/download actions and metadata
• Add toRawFileHref() helper for raw file and download links
• Expand language support to 30+ file types including Rust, Vue, Lua, Bazel, C/C++
• Implement file size limit (1 MiB) for inline preview display

src/server/localBrowseUi.ts


View more (3)
4. vite.config.ts ✨ Enhancement +143/-11

Add path probing and preview to dev server

• Import new preview and metadata functions from localBrowseUi
• Add /codex-api/local-paths/probe middleware for dev server
• Implement text preview rendering in dev /codex-local-browse handler
• Add readBooleanQueryFlag() helper for consistent query parsing
• Align dev server behavior with production HTTP server

vite.config.ts


5. src/components/content/ThreadConversation.vue ✨ Enhancement +264/-76

Add client-side path probing and conditional link rendering

• Add confirmedBrowseUrl() function that only returns URLs for probed existing paths
• Implement resolveAbsoluteLocalPath() to normalize and resolve file paths
• Add collectInlineLocalPathCandidates() to extract paths from message blocks
• Implement ensureLocalPathProbeResults() to batch-probe paths with caching
• Add fileLinkFallbackText() to preserve readable text for unresolved references
• Update file segment rendering to use conditional links based on probe results
• Add computed property visibleInlineLocalPathCandidates to track paths in view
• Add watcher to trigger path probing when visible messages change
• Store probe results and timestamps for TTL-based cache invalidation

src/components/content/ThreadConversation.vue


6. tests.md 🧪 Tests +41/-0

Add manual test cases for file browser feature

• Add comprehensive test steps for local path validation and text preview feature
• Document expected behavior for linkification, preview rendering, and fallbacks
• Include verification steps for syntax highlighting across multiple languages
• Add viewport height verification for preview panel layout

tests.md


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Apr 14, 2026

Code Review by Qodo

🐞 Bugs (3)   📘 Rule violations (0)   📎 Requirement gaps (0)
🐞\ ≡ Correctness (1) ☼ Reliability (1) ➹ Performance (1)

Grey Divider


Action required

1. Download missing filename 🐞
Description
/codex-local-file sets Content-Disposition to just "attachment" when download=1, so downloaded files
will not preserve the original basename and may save as "codex-local-file". This breaks the
user-facing Download action from the new preview/editor toolbars.
Code

src/server/httpServer.ts[R189-195]

+      if (wantsDownload) {
+        res.setHeader('Content-Disposition', 'attachment')
+        res.sendFile(localPath, { dotfiles: 'allow' }, (error) => {
+          if (!error) return
+          if (!res.headersSent) res.status(404).json({ error: 'File not found.' })
+        })
+        return
Evidence
The server explicitly sets an attachment disposition without filename, and the new preview UI
generates Download links that hit this endpoint, so the browser relies on this header to name the
saved file.

src/server/httpServer.ts[169-196]
src/server/localBrowseUi.ts[318-332]
src/server/localBrowseUi.ts[239-245]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Downloads initiated via `/codex-local-file?download=1` respond with `Content-Disposition: attachment` without a `filename=...`, so browsers commonly save the file with a generic name (e.g. `codex-local-file`) instead of the real basename.

## Issue Context
The new text preview/editor pages expose a **Download** action that routes through `/codex-local-file`.

## Fix Focus Areas
- src/server/httpServer.ts[189-195]

## Suggested fix
- Use `res.download(localPath, basename(localPath), ...)` (or set `Content-Disposition: attachment; filename="..."`) so the downloaded file name matches the actual local file.
- Keep the existing `dotfiles: 'allow'` behavior and error callback behavior consistent with the other branches.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Text raw buffers file 🐞
Description
For text-like files, /codex-local-file reads the full file into memory with readFile('utf8') before
responding, which can cause high memory usage and slow responses for large files. This is
inconsistent with the inline preview’s explicit 1 MiB cap and can make the raw endpoint a
performance footgun.
Code

src/server/httpServer.ts[R198-202]

+      if (textMetadata) {
+        const content = await readFile(localPath, 'utf8')
+        res.status(200).type('text/plain; charset=utf-8').send(content)
+        return
+      }
Evidence
The handler uses readFile(..., 'utf8') to build the response body for any file considered
"text-like" by getLocalTextFileMetadata. Separately, the preview HTML intentionally disables
embedding content above MAX_INLINE_PREVIEW_BYTES, indicating large text files are expected and
should be handled carefully.

src/server/httpServer.ts[179-202]
src/server/localBrowseUi.ts[96-114]
src/server/localBrowseUi.ts[484-499]
src/server/localBrowseUi.ts[191-211]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `/codex-local-file` handler fully buffers text-like files (`await readFile(localPath, 'utf8')`) before sending them. Large text files can cause memory spikes and slow responses.

## Issue Context
The preview UI already has a `MAX_INLINE_PREVIEW_BYTES` cap, but the raw endpoint has no size guard and currently buffers the entire file.

## Fix Focus Areas
- src/server/httpServer.ts[198-202]

## Suggested fix
- Replace `readFile(..., 'utf8')` with streaming (e.g. `createReadStream(localPath, { encoding: 'utf8' })`) and pipe to the response.
- Keep `Content-Type: text/plain; charset=utf-8`.
- Optionally add a hard size cap (or use a separate, higher cap) and return `413` with a clear error if exceeded.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. 404 masks real errors 🐞
Description
The /codex-local-file handler maps any thrown error to a 404 "File not found", including permission
errors and other IO failures. This misleads users and makes diagnosing real failures significantly
harder.
Code

src/server/httpServer.ts[R209-211]

+    } catch {
+      res.status(404).json({ error: 'File not found.' })
+    }
Evidence
The try block wraps stat/readFile/sendFile work, but the catch does not inspect error codes and
always returns 404. Errors like EACCES would be incorrectly reported as missing files.

src/server/httpServer.ts[179-211]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`/codex-local-file` catches all errors and responds with `404 { error: 'File not found.' }`, even when the file exists but cannot be read (e.g., permissions) or when other IO errors occur.

## Issue Context
The catch currently wraps stat/read and send logic, so it can swallow different failure modes.

## Fix Focus Areas
- src/server/httpServer.ts[179-211]

## Suggested fix
- Narrow the catch handling by inspecting `error` as `NodeJS.ErrnoException`:
 - `ENOENT` -> 404
 - `EACCES`/`EPERM` -> 403
 - otherwise -> 500 with a generic message
- If you switch to streaming for text responses, also attach an `error` handler to the stream and map statuses similarly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@marmeladema marmeladema force-pushed the improved-file-browser branch from 7027fd2 to eb8bce7 Compare April 14, 2026 10:28
@friuns2 friuns2 merged commit e5b7af5 into friuns2:main Apr 14, 2026
@marmeladema marmeladema deleted the improved-file-browser branch April 14, 2026 14:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants