Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/server/localBrowseUi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest'
import { decodeBrowsePath, toBrowseHref, toEditHref } from './localBrowseUi'

describe('decodeBrowsePath', () => {
it('removes the browse-route slash before Windows drive paths', () => {
expect(decodeBrowsePath('/C:/Users/Nulled/video.mp4', 'win32')).toBe('C:/Users/Nulled/video.mp4')
expect(decodeBrowsePath('/%43%3A/Users/Nulled/file.ps1', 'win32')).toBe('C:/Users/Nulled/file.ps1')
})

it('preserves Unix, UNC, and already normalized paths', () => {
expect(decodeBrowsePath('/home/codex/file.txt', 'linux')).toBe('/home/codex/file.txt')
expect(decodeBrowsePath('//server/share/file.txt', 'win32')).toBe('//server/share/file.txt')
expect(decodeBrowsePath('C:/Users/Nulled/file.txt', 'win32')).toBe('C:/Users/Nulled/file.txt')
})

it('leaves malformed URL encoding usable for the normal validation path', () => {
expect(decodeBrowsePath('/tmp/100%/file.txt', 'linux')).toBe('/tmp/100%/file.txt')
})
})

describe('local browse route hrefs', () => {
it('adds the route separator and converts Windows path separators', () => {
const path = String.raw`C:\Users\Nulled\Documents\invasion\artifacts\autonomous-weaponry\mammoth-v1`
expect(toBrowseHref(path)).toBe(
'/codex-local-browse/C:/Users/Nulled/Documents/invasion/artifacts/autonomous-weaponry/mammoth-v1',
)
expect(toEditHref(String.raw`C:\Users\Nulled\file.ps1`)).toBe(
'/codex-local-edit/C:/Users/Nulled/file.ps1',
)
})

it('preserves Unix and UNC absolute paths', () => {
expect(toBrowseHref('/home/codex/folder')).toBe('/codex-local-browse/home/codex/folder')
expect(toBrowseHref(String.raw`\\server\share\folder`)).toBe('/codex-local-browse//server/share/folder')
})

it('keeps project picker query parameters encoded', () => {
expect(toBrowseHref(String.raw`C:\Users\Nulled\Parent Folder`, 'My Project')).toBe(
'/codex-local-browse/C:/Users/Nulled/Parent%20Folder?newProjectName=My%20Project',
)
})
})
29 changes: 22 additions & 7 deletions src/server/localBrowseUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,23 @@ export function normalizeLocalPath(rawPath: string): string {
return trimmed
}

export function decodeBrowsePath(rawPath: string): string {
export function decodeBrowsePath(rawPath: string, platform: NodeJS.Platform = process.platform): string {
if (!rawPath) return ''
let decoded: string
try {
return decodeURIComponent(rawPath)
decoded = decodeURIComponent(rawPath)
} catch {
return rawPath
decoded = rawPath
}

// Browse URLs keep an absolute-path slash after the route prefix. On Windows,
// that turns `C:/path` into `/C:/path`, which Node resolves as `C:\C:\path`.
// Remove only that synthetic slash; Unix and UNC absolute paths stay intact.
if (platform === 'win32' && /^\/[A-Za-z]:[\\/]/u.test(decoded)) {
return decoded.slice(1)
}
Comment on lines +81 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Windows browse hrefs broken 🐞 Bug ≡ Correctness

On Windows, decodeBrowsePath now returns drive paths as C:/... (no leading /), but
toBrowseHref/toEditHref concatenate paths directly after
/codex-local-browse//codex-local-edit without inserting a separator slash. This can produce URLs
like /codex-local-browseC:%5CUsers%5Cfile that won’t match the server routes (expecting
/codex-local-browse/*path), breaking directory navigation/back/edit links on Windows.
Agent Prompt
## Issue description
`decodeBrowsePath()` now correctly normalizes Windows drive-letter paths from `/C:/...` to `C:/...`, but URL generation (`toBrowseHref` / `toEditHref`) assumes the path already begins with `/` and concatenates it directly after the route prefix. On Windows this yields malformed URLs (missing the route separator `/`) that don’t match `/codex-local-browse/*path` and `/codex-local-edit/*path`.

## Issue Context
- Server routes require `/codex-local-browse/<path>` and `/codex-local-edit/<path>`.
- After this PR, browse/edit link generation must not rely on the filesystem path having a leading `/`.

## Fix Focus Areas
- Update `toBrowseHref` and `toEditHref` to always insert exactly one `/` after the route prefix, regardless of whether `pathValue` starts with `/`.
- Normalize Windows separators for URL paths (e.g., replace `\\` with `/`) before encoding, so generated hrefs are stable and readable.
- Add a small unit test by factoring URL building into an exported helper (or otherwise testing via existing exported functions) to cover Windows drive paths.

### Code locations
- src/server/localBrowseUi.ts[144-154]
- src/server/localBrowseUi.ts[253-269]
- src/server/httpServer.ts[152-178]
- src/server/localBrowseUi.test.ts[1-19]

## Suggested implementation sketch
- In `toBrowseHref` / `toEditHref`:
  - `const urlPath = pathValue.replace(/\\/gu, '/')`
  - `const encoded = encodeURI(urlPath)`
  - `const suffix = encoded.startsWith('/') ? encoded.slice(1) : encoded`
  - `return `/codex-local-browse/${suffix}${query}`` (and similarly for edit)
- Add tests that assert a Windows input like `C:\\Users\\Me\\file.txt` (and `C:/Users/Me/file.txt`) yields `/codex-local-browse/C:/Users/Me/file.txt` (or equivalent encoding) and matches the route prefix with `/codex-local-browse/`.

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


return decoded
}

export function isTextEditablePath(pathValue: string): boolean {
Expand Down Expand Up @@ -131,16 +141,21 @@ function normalizeNewProjectName(value: string): string {
return value.trim().replace(/[\\/]+/gu, '').trim()
}

function toBrowseHref(pathValue: string, newProjectName = ''): string {
function normalizeLocalRoutePath(pathValue: string): string {
const normalized = pathValue.replace(/\\/gu, '/')
return normalized.startsWith('/') ? normalized : `/${normalized}`
}

export function toBrowseHref(pathValue: string, newProjectName = ''): string {
const normalizedName = normalizeNewProjectName(newProjectName)
const query = normalizedName ? `?newProjectName=${encodeURIComponent(normalizedName)}` : ''
return `/codex-local-browse${encodeURI(pathValue)}${query}`
return `/codex-local-browse${encodeURI(normalizeLocalRoutePath(pathValue))}${query}`
}

function toEditHref(pathValue: string, newProjectName = ''): string {
export function toEditHref(pathValue: string, newProjectName = ''): string {
const normalizedName = normalizeNewProjectName(newProjectName)
const query = normalizedName ? `?newProjectName=${encodeURIComponent(normalizedName)}` : ''
return `/codex-local-edit${encodeURI(pathValue)}${query}`
return `/codex-local-edit${encodeURI(normalizeLocalRoutePath(pathValue))}${query}`
Comment on lines +144 to +158

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo files around localBrowseUi:"
fd -a 'localBrowseUi\.(ts|js|tsx)$' . || true

file="$(fd 'localBrowseUi\.(ts|js|tsx)$' . | head -n1 || true)"
if [ -n "${file:-}" ]; then
  echo "FILE=$file"
  wc -l "$file"
  sed -n '1,240p' "$file" | cat -n
fi

echo
echo "Search usages of toBrowseHref/toEditHref/export:"
rg -n "toBrowseHref|toEditHref|normalizeLocalRoutePath|codex-local-(browse|edit)" . || true

echo
echo "Node browser URL behavior:"
node - <<'JS'
const paths = ['/tmp/report#1.txt', 'tmp/a?b.txt', '/tmp/a/b#b.txt?query=1'];
for (const pathValue of paths) {
  const normalized = pathValue.replace(/\\/g, '/')
  const href = `/codex-local-browse${encodeURI(normalized.startsWith('/') ? normalized : `/${normalized}`)}`
  const u = new URL(`http://example.com${href}`)
  console.log({ pathValue, href, pathname: u.pathname, search: u.search, hash: u.hash })
}
console.log("imports localBrowseUi?:", (() => { try { import('./src/server/localBrowseUi.ts').then(()=>{}, e=>console.log('async import:', e.message)); import('./src/server/localBrowseUi.js').then(()=>{}, e=>console.log('cjs async import:', e.message)); } catch(e){ console.log('top import error', e.message) } })());
JS

Repository: friuns2/codex-mobile

Length of output: 17011


Encode filesystem path segments before constructing the route URL.

encodeURI leaves ? and # unescaped. A filename like report#1.txt becomes /codex-local-browse/tmp/report#1.txt; the browser treats #1.txt as a fragment, so /codex-local-browse/*path receives only /tmp/report. Encode each normalized path segment with encodeURIComponent while preserving / separators, and add regression coverage for # and ? filenames.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/localBrowseUi.ts` around lines 144 - 158, Update
normalizeLocalRoutePath, toBrowseHref, and toEditHref so each normalized
filesystem path segment is encoded with encodeURIComponent while preserving
slash separators; do not rely on encodeURI, which leaves # and ? unescaped. Add
regression coverage verifying filenames containing # and ? remain part of the
routed path.

}

function escapeForInlineScriptString(value: string): string {
Expand Down
1 change: 1 addition & 0 deletions tests/chat-composer-rendering/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Return to the [manual test index](../../tests.md).
| [Feature: Inline thread image payloads are rewritten to renderable local file URLs](inline-thread-image-payloads-are-rewritten-to-renderable-local-file-urls.md) |
| [Feature: Markdown file links with spaces and parentheses in path](markdown-file-links-with-spaces-and-parentheses-in-path.md) |
| [Feature: Markdown link with backticked label renders as file link](markdown-link-with-backticked-label-renders-as-file-link.md) |
| [Feature: Windows absolute file links open through local browse](windows-absolute-file-links-open-local-browse.md) |
| [Feature: Backticked bare filenames render as file links](backticked-bare-filenames-render-as-file-links.md) |
| [Feature: Lazy message rendering (windowed conversation)](lazy-message-rendering-windowed-conversation.md) |
| [Assistant generated image rendering](assistant-generated-image-rendering.md) |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
### Feature: Windows absolute file links open through local browse

## Prerequisites

- Run CodexApp on Windows.
- Open TestChat with a project that contains an existing text file and MP4 file on a drive-letter path.

## Steps

1. Send a message containing Markdown links to `C:/path/to/file.ps1` and `C:/path/to/video.mp4`.
2. Inspect both rendered links and confirm their `href`, title, and visible text preserve the complete drive-letter paths.
3. Open the text-file link and confirm the request returns the existing file instead of a 404 response.
4. Open the MP4 link and seek within the video.
5. Inspect the MP4 request and confirm byte-range requests return `206 Partial Content` with `Accept-Ranges: bytes`.
6. Open a directory through local browse and click a nested folder plus the parent (`..`) link.
Comment on lines +8 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Missing testchat hrefok checks 📘 Rule violation ☼ Reliability

This PR changes file-link/local-browse behavior, but the new manual test entry does not include the
mandatory TestChat validation outputs (hrefOk, titleOk, textOk) nor the required saved
screenshot under output/playwright/testchat-<feature>-cjs.png. Without those standardized checks
and evidence, regressions in chat parsing/link rendering may go undetected.
Agent Prompt
## Issue description
The manual verification entry for Windows absolute file links does not include the mandatory TestChat validation outputs required for link rendering changes: a unique marker, explicit `hrefOk`/`titleOk`/`textOk` assertions, and a screenshot saved under `output/playwright/testchat-<feature>-cjs.png`.

## Issue Context
This PR modifies browse/edit link generation and browse-path decoding, which is within the scope of chat parsing/link rendering changes that require standardized TestChat validation evidence.

## Fix Focus Areas
- tests/chat-composer-rendering/windows-absolute-file-links-open-local-browse.md[8-23]

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


## Expected Results

- Windows drive paths are decoded as `C:/...`, not `/C:/...` or `C:\\C:\\...`.
- Existing files return successfully through `/codex-local-browse/C:/...`.
- MP4 files use the correct media content type and support browser range requests.
- Directory, parent, and edit links use `/codex-local-browse/C:/...` or `/codex-local-edit/C:/...` with forward slashes and a separator after the route prefix.
- Unix absolute paths and UNC paths retain their existing behavior.
Comment on lines +8 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Missing light/dark verification step 📘 Rule violation ☼ Reliability

The added manual test case for the changed link/browse UI does not require validation in both light
and dark themes. This increases the risk of theme-specific regressions going unnoticed.
Agent Prompt
## Issue description
The manual test entry for the Windows local-browse link behavior does not include explicit verification in both light and dark themes.

## Issue Context
PR Compliance requires changed UI surfaces to be validated in both themes to prevent dark-mode regressions.

## Fix Focus Areas
- tests/chat-composer-rendering/windows-absolute-file-links-open-local-browse.md[8-23]

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


## Rollback / Cleanup

- Close the opened file and video tabs. No files are modified by this test.