fix: apply the review items for #687 at merge

- constants.js:2296,2301 spacedFilePathOnLine: refuse a first word ending in `$`
  and an earlier word with no letter or digit (other than a bare `-`), so a
  shell prompt showing an absolute working directory (`/mnt/c/proj ❯ code
  notes.md`, `/home/me/proj$ cat notes.md`) keeps master's links; the six rows
  from the review are pinned, plus OneDrive, `Program Files (x86)` and
  `Docs - Copy` still linking whole.
- constants.js:2312: the leading strip is now `/^(?:~?\/|(?:\.\.?\/)+)/`, so
  `Saved to: ../My Docs/plan.md` links whole; an interior `..` still links
  nothing. Tests for the detector and the terminal provider.
- app.js:3032 _linkifyFilePaths: carry any leading bold label and let
  spacedFilePathOnLine() judge it, and give a text node that follows another
  inline element a placeholder word, so `**Note:** /home/me/proj/out
  report.pdf` and ``Use `--config` /home/me/proj/cfg data.json`` split as in
  the terminal; a spaced path inside inline code still links whole. The
  now-unused label-pattern guard is gone.
- JSDoc (findFilePathLinks, SPACED_FILE_PATH_LABEL_PATTERN,
  spacedFilePathOnLine) and the spaced-path paragraph in
  docs/architecture-invariants.md updated to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-27 01:34:15 +01:00
1 parent df9f93d3ee
commit e52cc60c7c
5 files changed
+91 -18

No files matched your search

+22
View File
@@ -121,6 +121,28 @@ describe('response viewer file-path linkifier', () => {
expect(paths(linkify(renderChat(markdown))).map((a) => a.getAttribute('data-path'))).toEqual(expected);
});
// The terminal splits these lines (`Note:` is not a path label, and the path does
// not start the line), so the viewer must split them too.
it.each([
['**Note:** /home/me/proj/out report.pdf', ['/home/me/proj/out', 'report.pdf']],
['Use `--config` /home/me/proj/cfg data.json', ['/home/me/proj/cfg', 'data.json']],
])('splits a spaced candidate the terminal splits: %s', (markdown, expected) => {
const html = renderChat(markdown);
const original = document.createElement('div');
original.innerHTML = html;
const root = linkify(html);
expect(paths(root).map((a) => a.getAttribute('data-path'))).toEqual(expected);
expect(root.textContent).toBe(original.textContent);
});
it('still links a spaced path inside inline code whole, after another inline element too', () => {
const root = linkify(renderChat('Use `--config` `/home/me/My Docs/cfg data.json`'));
const links = paths(root);
expect(links.map((a) => a.getAttribute('data-path'))).toEqual(['/home/me/My Docs/cfg data.json']);
expect(links[0].closest('code')).not.toBeNull();
});
it('does not inherit a bold label from earlier prose or another block', () => {
for (const markdown of [
'See **Saved to:** 03 Attachments/Build SIPOC Design/Build-SIPOC.pptx',
+38
View File
@@ -326,6 +326,37 @@ describe('spaced file paths: shapes that keep master links', () => {
expect(paths('Saved to: ./My Docs/plan.md')).toEqual(['./My Docs/plan.md']);
expect(paths('Saved to: ./notes.md')).toEqual(['./notes.md']);
});
it('reads a leading run of ../ as parent directories, not segments', () => {
expect(paths('Saved to: ../My Docs/plan.md')).toEqual(['../My Docs/plan.md']);
expect(paths('Saved to: ../../My Docs/plan.md')).toEqual(['../../My Docs/plan.md']);
expect(paths('Saved to: ../notes.md')).toEqual(['../notes.md']);
// An interior `..` is still a segment, so the span links nothing.
expect(paths('Saved to: My Docs/../plan.md')).toEqual([]);
expect(paths('Saved to: ../My Docs/../x/plan.md')).toEqual([]);
});
// A shell prompt that prints its working directory in full (WSL's `/mnt/c/...` is
// outside `$HOME`): a word with no letter or digit, or a first word ending in `$`,
// is never part of a file name, so every earlier command in the scrollback keeps
// master's links.
it.each([
['/mnt/c/proj ❯ code notes.md', ['/mnt/c/proj', true], ['notes.md']],
['/home/me/proj $ cat notes.md', ['/home/me/proj', true], ['notes.md']],
['/home/me/proj$ cat notes.md', ['/home/me/proj', true], ['notes.md']],
['/home/me/proj ❯ git add src/a.ts', ['/home/me/proj', true], ['src/a.ts']],
['/mnt/c/Users/me/OneDrive - Contoso/proj ❯ code notes.md', ['/mnt/c/Users/me/OneDrive', true], ['notes.md']],
['/home/me/proj/out ✓ report.pdf', ['/home/me/proj/out', true], ['report.pdf']],
] as Array<[string, ...Array<[string, true?]>]>)('keeps a shell prompt out of the path: %s', (line, ...links) => {
expect(findFilePathLinks(line)).toEqual(masterLinks(line, ...links));
});
it.each([LONG, '/mnt/c/Program Files (x86)/app/readme.txt', '/mnt/c/Users/me/Docs - Copy/a.docx'])(
'keeps a name whose words hold a letter or digit, or a bare -, whole: %s',
(line) => {
expect(paths(line)).toEqual([line]);
}
);
});
describe('terminal link provider with spaced paths', () => {
@@ -383,6 +414,13 @@ describe('terminal link provider with spaced paths', () => {
expect(opened).toEqual([`/workspace/project/${rel}`]);
});
it('links a labelled spaced path under ../ and opens it outside the workspace', () => {
const { links, opened } = linksFor('Saved to: ../My Docs/plan.md');
expect(links.map((l) => l.text)).toEqual(['../My Docs/plan.md']);
links[0].activate(null, links[0].text);
expect(opened).toEqual(['/workspace/My Docs/plan.md']);
});
it('links nothing for an unlinkable spaced path', () => {
expect(linksFor('/etc/secret files/passwords.txt').links).toEqual([]);
});