Skip to content

fix(web): keep spaced wrapped file paths clickable - #734

Open
Hugo-DDT wants to merge 5 commits into
Ark0N:masterfrom
Hugo-DDT:cursor/spaced-wrapped-file-paths-b495
Open

Hugo-DDT wants to merge 5 commits into
Ark0N:masterfrom
Hugo-DDT:cursor/spaced-wrapped-file-paths-b495

Conversation

@Hugo-DDT

@Hugo-DDT Hugo-DDT commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #733

A parenthesised absolute file path whose name contains a space ((/mnt/data/Kobo/Kobo eReader.conf), including one hard-wrapped between the words) is linked as that whole file. A relative path or a bare spaced filename inside parentheses is left to the ordinary detectors, which still need an explicit path label for a spaced relative path.

.conf is a link extension, the same text the attachment routes already serve. The space a hard wrap consumed stays in the path when the parenthesis starts the broken-off token (or is separated from it by whitespace only), the next token closes it right after the extension, and the spaced name is an absolute path the parenthesised detector takes whole. The clickable cells stay on the rows that hold the name.

Tests: test/spaced-file-path-links.test.ts, test/file-path-link-forms.test.ts, and test/response-viewer-file-links.test.ts.

A parenthesised path with a space in its filename was linked as the
folder prefix. .conf joins the shared link extensions, parentheses
bound the whole path, and a hard wrap between filename words keeps the
separating space and the clickable cells on each row.

Fixes Ark0N#733

Co-authored-by: DDT <1786035110@qq.com>
Copilot AI balanced review requested due to automatic review settings October 28, 2026 12:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@Ark0N

Ark0N commented Oct 28, 2026

Copy link
Copy Markdown
Owner

Thank you for picking this up, Hugo-DDT. This PR makes a parenthesised path with a space in its name, such as (/mnt/data/Kobo/Kobo eReader.conf), link as one file, adds .conf to the link extensions, and keeps the space when such a name is hard-wrapped between its words.

The .conf addition and the absolute-path case inside parentheses are good and I want to keep them (as a bonus, Update(/home/me/proj/My Docs/notes.md) now links whole). Two other parts turn links that work on master into links to files that do not exist, so they need changes before this can merge.

1. Parentheses act as a path label, so prose inside them becomes a path (src/web/public/constants.js:2395-2418)

spacedPathBoundedByParens() falls back to re-running spacedFilePathOnLine() behind a synthesized Path: label, and then to accepting any bare word word file.ext. Neither can tell a filename word from a command or a verb:

Text master links this PR links
(node server.js) server.js node server.js
(updated package.json) package.json updated package.json
(e.g. eslint.config.js) eslint.config.js e.g. eslint.config.js
(default: settings.json) settings.json default: settings.json
(cat notes.md) notes.md cat notes.md
(CI/CD pipeline config.yml) config.yml CI/CD pipeline config.yml

Both surfaces share findFilePathLinks(), so this reaches the terminal and the response viewer. It also contradicts the documented rule that a relative spaced path needs an explicit path label (docs/architecture-invariants.md, which names CI/CD as prose that must defer, and the File-path links section of CLAUDE.md). (Kobo eReader.conf) and (node server.js) have the same shape, so more guards cannot separate them. Please keep only the direct branch (absolute paths, judged by every spacedFilePathOnLine() rule), drop the synthesized-label and bare-filename fallbacks along with their claims in the docs paragraph and the changeset, and add the rows above as negative tests in test/spaced-file-path-links.test.ts.

2. join-space also fires on mid-word cuts, so wrapped paths and URLs gain a space (src/web/public/constants.js:2669, returned at 2654, applied at 2763)

continuationKeepsWordSpace() fires on any unclosed ( anywhere in the stitched text so far, plus a letter on both sides of the break. Claude Code hard-cuts long tokens mid-word (the verbatim BROKEN fixture in test/file-path-link-forms.test.ts is one; it still passes only because that cut falls between two digits). Through the real terminal link provider:

  • Bash(cp /home/me/proj/builds/captures/2026-10-10/ghost1b_gamepl over ay.png /tmp/) now links a folder .../ghost1b_gamepl plus a relative ay.png (master: the whole file).
  • Details (see https://example.com/reports/2026/ghost1b_game over play.png). now links the URL as .../ghost1b_game plus a relative play.png (master: the whole URL).
  • ⎿ BILD (/mnt/build/neon_getaway-mgr-hebel/builds/captures/ghost1b_game over play.png) err=0 now links .../ghost1b_game play.png (master: .../ghost1b_gameplay.png).

So "Existing URL, folder, and mid-token path joins are unchanged" does not hold for a cut between two letters after an open paren. Please scope the rule to the parenthesis that opens the broken-off path itself (not Bash( or any other ( earlier in the stitched text), never apply it to a token containing ://, and pin the Bash and URL shapes above as tests that keep joining without a space.

Even scoped that way, the third shape stays ambiguous: a parenthesised path cut between two letters looks exactly like the Kobo wrap, and both readings are valid paths. Whether fixing the wrapped Kobo case is worth that cost is an open question on my side. If the rule stays, the trade-off needs a sentence in docs/architecture-invariants.md next to the other known trade-offs and a test pinning it.

Smaller

  • src/web/public/constants.js:2392: test trimmed.length > 1000 before trimmed.includes(' '), so deeply nested parentheses with no spaces are not scanned once per level (100,000 nested pairs take 218 ms here against 2.5 ms on master).

Typecheck, lint, format and the link test files all pass locally and CI is green; the problems above are behaviours the current tests do not exercise. Once items 1 and 2 are addressed and the trade-off question is settled, this is close to ready.

Parentheses accept only an absolute spaced path. Join-space applies to the
parenthesis that opens that path, never an earlier one or a :// token.

Co-authored-by: DDT <1786035110@qq.com>
@Ark0N

Ark0N commented Oct 29, 2026

Copy link
Copy Markdown
Owner

Thank you for the quick second round, Hugo-DDT. This PR links a parenthesised absolute path with a space in its name, such as (/mnt/data/Kobo/Kobo eReader.conf), as one file, adds .conf to the link extensions, and keeps the space when such a name is hard-wrapped between its words.

The second commit covers everything from my earlier review: the synthesized-label and bare-filename fallbacks are gone and the six prose rows are pinned as negative tests, join-space only counts the path's own ( and never a :// token, the Bash( and URL shapes are pinned, the mid-letter trade-off has its sentence in docs/architecture-invariants.md and a test, and the length check runs before the space scan. Typecheck, lint, format, the frontend syntax check and the full test suite pass locally.

1. join-space also fires when the parenthesis does not close right after the name (src/web/public/constants.js:2671, in continuationKeepsWordSpace())

The space is kept whenever the path's ( is still open, but parenthesisedSpacedPaths() can only take the spaced name when the inside of the pair is exactly the path. When anything else follows the name inside the parenthesis, or the parenthesis does not close on those rows, the space goes in and the folder and relative detectors split the path. Through terminalPathLine() and findFilePathLinks(), with Saved the capture (/mnt/data/captures/2026-10-10/ghost1b_gamepl as the first row:

Second row master links this PR links
ay.png, 2.1 MB). .../ghost1b_gameplay.png folder .../ghost1b_gamepl + relative ay.png
ay.png and the log). .../ghost1b_gameplay.png folder .../ghost1b_gamepl + relative ay.png
ay.png (no closing paren) .../ghost1b_gameplay.png folder .../ghost1b_gamepl + relative ay.png

This is a different cost from the documented trade-off: there one link goes to the spaced name, here a link that works on master becomes two wrong ones, and the real Kobo wrap gains nothing in these shapes either ((/mnt/data/Kobo/Kobo over eReader.conf and more also splits). Please keep the space only when the continuation token itself closes the parenthesis right after the extension, for example right after the bare line:

if (!/^[^()]*\)[\].,;:!?]*$/.test(headText)) return false;

I ran that on a copy: the three rows above go back to master's join, while the Kobo wrap, the Bash( and URL tests and your ay.png). trade-off test keep their current results. Please add the ay.png, 2.1 MB). row to test/file-path-link-forms.test.ts as a join without a space.

Smaller

  • test/file-path-link-forms.test.ts:384: the new provider test expects openLogViewerWindow for /mnt/data/Kobo/Kobo eReader.conf, but only because the harness stubs _isExternalPreviewPath to ~/ paths (line 283). The session's workingDir is /mnt/build/neon_getaway-mgr-hebel, so in the app the real _isExternalPreviewPath() (src/web/public/panels-ui.js:4186) sends this path to openFilePreview. Please drop the two routing assertions, or put the .conf file under the session's working directory so the expectation matches the app.
  • docs/architecture-invariants.md:412: please state that the ( has to start the broken-off token or be separated from it by whitespace only, as the code comment does. Today (/mnt/data/My Photos/Kobo over eReader.conf) joins as KoboeReader.conf, and the doc sentence reads as if it keeps the space.

Open question

Whether join-space stays at all is still open on my side. A parenthesised path that a program cuts between two letters short of the right edge reads exactly like the Kobo wrap (your ay.png). test), so the rule fixes the wrapped Kobo case and gives up that one. Item 1 and the doc point are what the rule needs if it stays; the routing test applies either way. The parenthesised absolute-path detection and the .conf extension do not depend on that answer.

Join-space now requires the continuation token to close the parenthesis
right after the extension. A parenthesis that does not open the broken-off
token still joins with no space.

Co-authored-by: DDT <1786035110@qq.com>
@Ark0N

Ark0N commented Oct 29, 2026

Copy link
Copy Markdown
Owner

Thank you for the third round, Hugo-DDT. This PR links a parenthesised absolute path with a space in its name, such as (/mnt/data/Kobo/Kobo eReader.conf), as one file, adds .conf to the link extensions, and keeps the space when such a name is hard-wrapped between its words.

The third commit covers every point from my last review: join-space now needs the continuation token itself to close the parenthesis right after the extension, the ay.png, 2.1 MB). row and its two siblings are pinned as joins without a space, the routing assertions are gone from the provider test, and docs/architecture-invariants.md says where the ( has to sit. Typecheck, lint, format, the frontend syntax check and the full test suite (659 files) pass locally, and the new tests fail on master as they should.

1. join-space still fires where the parenthesised detector cannot take the spaced name (src/web/public/constants.js:2681, in continuationKeepsWordSpace(), returned at 2635)

parenthesisedSpacedPaths() only accepts what spacedFilePathOnLine() accepts: an absolute path under a known root, not under bin/, scripts/ or tools/. continuationKeepsWordSpace() does not check that, so for a relative path, a ~/ path or a path under scripts/ it inserts the space, the parenthesised detector then refuses the result, and the folder and relative detectors split it. Through terminalLinkLine() and findFilePathLinks():

Rows master links this PR links
Saved the capture (builds/captures/2026-10-10/ghost1b_gamepl over ay.png). builds/captures/2026-10-10/ghost1b_gameplay.png ay.png only
⎿ BILD (builds/captures/2026-10-10/neon_getaway/ghost1b_game over play.png) err=0 the whole .../ghost1b_gameplay.png play.png only
Saved the capture (~/proj/builds/captures/2026-10-10/ghost1b_gamepl over ay.png). the whole ~/proj/.../ghost1b_gameplay.png folder ~/proj/.../ghost1b_gamepl + ay.png
Ran the helper (/home/me/proj/scripts/capture_gameplay_overv over iew.sh). /home/me/proj/scripts/capture_gameplay_overview.sh folder .../capture_gameplay_overv + iew.sh

The Kobo reading of these shapes gains nothing either ((docs/Kobo over eReader.conf). links only eReader.conf), so in each of them a working link becomes a wrong one. This is the same class as item 1 of my last review, through a different gate. Please keep the space only when the spaced name is one spacedFilePathOnLine() takes whole, for example in place of the last line of continuationKeepsWordSpace():

  if (!/[A-Za-z0-9]$/.test(tail)) return false;
  // Only where parenthesisedSpacedPaths will take the spaced name whole.
  const candidate = `${tail.replace(/^\(/, '')} ${bare}`;
  return spacedFilePathOnLine(candidate)?.path === candidate;

I ran that on a copy: the four rows above go back to master's join, and the Kobo wrap (both rows, cells included), the ( /mnt/... form, the Bash( and URL pins and your ay.png). trade-off test keep their results. All eight link test files (400 tests) pass with it. Please also:

  • add the relative, ~/ and scripts/ rows as joins without a space in test/file-path-link-forms.test.ts, next to the ay.png, 2.1 MB). rows (line 224);
  • add the condition to the 'join-space' sentence in docs/architecture-invariants.md:412 and to the "The space the wrap consumed stays in the path when..." clause in .changeset/parenthesised-spaced-conf-paths.md:5 (only for an absolute path the parenthesised detector takes whole).

Smaller

  • test/spaced-file-path-links.test.ts:465: not.toContain('see the notes.conf') passes for almost any result. The function returns ['notes.conf'] there, so toEqual(['notes.conf']) pins it.
  • The PR description still says parentheses bound relative, ~/ and bare filenames; the changeset is right, so please bring the description in line with it.

Open question

Whether join-space stays at all is still open on my side, and the rows above are part of what it costs. The guard and its tests are what the rule needs if it stays. The parenthesised absolute-path detection and the .conf extension do not depend on that answer.

Join-space now requires the spaced name to be an absolute path
spacedFilePathOnLine takes whole, so a relative, ~/ or scripts/ wrap
joins without a space.

Co-authored-by: DDT <1786035110@qq.com>
@Ark0N

Ark0N commented Oct 30, 2026

Copy link
Copy Markdown
Owner

Thank you for the fourth round, Hugo-DDT. This PR links a parenthesised absolute path with a space in its name, such as (/mnt/data/Kobo/Kobo eReader.conf), as one file, adds .conf to the link extensions, and keeps the space when such a name is hard-wrapped between its words.

The fourth commit covers every point from my last review: continuationKeepsWordSpace() keeps the space only when spacedFilePathOnLine() takes the spaced name whole, the relative, ~/ and scripts/ rows are pinned as joins without a space, the docs paragraph and the changeset state the condition, the notes.conf test uses toEqual, and the PR description matches the changeset. Typecheck, lint, format, the frontend syntax check, the browser-exclusion check and the link test files (14 files, 586 tests) pass locally.

1. A Bash(...) tool header with an absolute executable now links the whole command as one file (src/web/public/constants.js:2394, in spacedPathBoundedByParens())

The inside of every parenthesis pair is judged as an own-line path, and Claude Code prints each shell command as Bash(<command>). A command whose executable is an absolute path outside bin/, scripts/ and tools/, with no flags and a file argument, passes every own-line rule. Through the terminal link provider:

Row master links this PR links
Bash(/tmp/a.out input.txt) input.txt /tmp/a.out input.txt
Bash(/home/me/proj/build/mytool convert input.json) input.json (plus a folder link) the whole command
Bash(/home/me/proj/target/release/app config.toml) config.toml (plus a folder link) the whole command

This is the same class as the earlier rounds (a link that works on master becomes a link to a file that does not exist), reached through the absolute-path branch. One guard that names no tool, which I ran on a copy of parenthesisedSpacedPaths(), right before the hits.push:

// A call-shaped `Name(` wraps a command as often as a path: there, take the
// spaced path only when the space sits in a directory.
const called = open > 0 && /\w/.test(text[open - 1]);
if (hit && called && !hit.path.split(' ').slice(1).some((word) => word.includes('/'))) continue;

It gives the three rows back their master links and keeps Update(/home/me/proj/My Docs/notes.md), the issue's prose form and Program Files (x86), but Write(/home/me/proj/docs/Design Notes.md) and Read(/mnt/data/Kobo/Kobo eReader.conf) then split as on master. Whether that guard or a documented trade-off is the better cost is open on my side. Either way, please add the three rows to the #733 block in test/spaced-file-path-links.test.ts (line 431), pinning whichever behaviour is chosen.

Open question

Whether join-space stays is still open on my side. I measured it: wrapping ten ordinary parenthesised absolute paths after six lead-ins at every width from 40 to 110 columns, short of the terminal edge, 157 of 4,260 renderings turned master's working link into a spaced one ((/home/me/proj/docs/arc over hitecture-invariants.md). links /home/me/proj/docs/arc hitecture-invariants.md), while every rendering where the wrap fell on the space of a spaced filename (230) now links correctly. Rows that reach the right edge are stitched before the rule runs, so they are unaffected either way. The parenthesised absolute-path detection and the .conf extension do not depend on that answer.

Nothing else is outstanding: once the Bash( behaviour and the join-space question are settled, the rest of the PR is ready.

A Name( wrapper takes a spaced path only when the space sits in a
directory. Bash(/tmp/a.out input.txt) links input.txt again, while
Update(/home/me/proj/My Docs/notes.md) stays one path.
@Ark0N

Ark0N commented Nov 1, 2026

Copy link
Copy Markdown
Owner

Thank you for the fifth round, Hugo-DDT. This PR links a parenthesised absolute path with a space in its name, such as (/mnt/data/Kobo/Kobo eReader.conf), as one file, adds .conf to the link extensions, and keeps the space when such a name is hard-wrapped between its words.

The fifth commit covers the Bash( item from my last review: the call-shaped guard is in parenthesisedSpacedPaths() (src/web/public/constants.js:2371), the three Bash( rows and the Update(/home/me/proj/My Docs/notes.md) row are pinned in test/spaced-file-path-links.test.ts, and the docs paragraph and the changeset describe the rule. Typecheck, lint, format, the frontend syntax check, the browser-exclusion check and the full test suite (659 files) pass locally.

Still open on my side

Whether join-space stays is still my decision to make, and so is whether the call-shaped guard is the cost I want for Name(. What each one costs:

  • join-space fixes every wrap that falls on the space of a spaced filename, the issue's (/mnt/data/Kobo/Kobo over eReader.conf). included. It also turns a mid-letter cut of an ordinary parenthesised path, (/home/me/proj/docs/arc over hitecture-invariants.md)., into a link to /home/me/proj/docs/arc hitecture-invariants.md. Without it, the issue's wrapped example joins as /mnt/data/Kobo/KoboeReader.conf.
  • The guard keeps master's links for Bash(/tmp/a.out input.txt). It also means Read(/mnt/data/Kobo/Kobo eReader.conf) still links the folder /mnt/data/Kobo/Kobo (plus eReader.conf), and the docs paragraph at docs/architecture-invariants.md:414 does not yet name that cost the way it names the others.

None of the code needs to change for this round. The one-line parenthesised detection and the .conf extension are in good shape.

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.

File links truncate wrapped filenames containing spaces, including .conf files

3 participants