mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
Merge pull request #665: fix(response-viewer): open markdown links to file paths in the preview, not as a same-origin URL
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
b4275e690e
6 files changed
+129
-5
No files matched your search
@@ -110,6 +110,84 @@ describe('response viewer file-path linkifier', () => {
|
||||
expect(root.querySelector('a')!.getAttribute('href')).toBe('https://example.com/tmp/shot.png');
|
||||
});
|
||||
|
||||
it('turns a markdown link to a path into a preview link, not a same-origin navigation', () => {
|
||||
// Codex cites files as `[label](/abs/path)`: left alone, the href resolved
|
||||
// against Codeman's origin and opened a 404 JSON page in a new tab.
|
||||
const path = '/home/me/llm-bench/runs/crash-20261017/incident-summary.json';
|
||||
const root = linkify(
|
||||
`<p><a href="${path}" target="_blank" rel="noopener noreferrer">Our incident record</a> and ` +
|
||||
`<a href="src/a%20b.ts:42:7">a b</a>, <a href="docs/#L3">docs</a>, <a href="/tmp/renders">renders</a>, ` +
|
||||
`<a href="/tmp/release%3A12">release</a>, <a href="/tmp/a.json?raw=true">raw</a>, <a href="/tmp/">tmp</a></p>`
|
||||
);
|
||||
|
||||
const links = paths(root);
|
||||
expect(links.map((a) => a.getAttribute('data-path'))).toEqual([
|
||||
path,
|
||||
'src/a b.ts',
|
||||
'docs/',
|
||||
'/tmp/renders',
|
||||
// An encoded colon is part of the name, never a line suffix.
|
||||
'/tmp/release:12',
|
||||
// A query names a view of the file, not the file.
|
||||
'/tmp/a.json',
|
||||
'/tmp/',
|
||||
]);
|
||||
for (const a of links) {
|
||||
expect(a.getAttribute('href')).toBe('#');
|
||||
expect(a.hasAttribute('target')).toBe(false);
|
||||
expect(a.hasAttribute('rel')).toBe(false);
|
||||
expect(a.getAttribute('data-linkified')).toBe('1');
|
||||
}
|
||||
expect(links[0].textContent).toBe('Our incident record');
|
||||
// A label, not a path: the second class keeps it in the prose font (styles.css).
|
||||
expect(links.every((a) => a.classList.contains('rv-path-link'))).toBe(true);
|
||||
// A trailing slash and a rooted folder path without one both open the folder grid
|
||||
// (`:` ends a folder match in findFilePathLinks, so `release:12` previews instead).
|
||||
// findFilePathLinks finds no folder in a bare `/tmp/`, so only the trailing-slash
|
||||
// check marks that one.
|
||||
expect(links.map((a) => a.hasAttribute('data-folder'))).toEqual([false, false, true, true, false, false, true]);
|
||||
});
|
||||
|
||||
it('opens a clicked markdown path link in the file preview through the click delegate', () => {
|
||||
const app = Object.create(CodemanApp.prototype) as {
|
||||
activeSessionId: string;
|
||||
openFilePreview: ReturnType<typeof vi.fn>;
|
||||
openFolderGrid: ReturnType<typeof vi.fn>;
|
||||
_linkifyFilePaths(root: unknown): void;
|
||||
_bindResponseViewerInteractions(body: unknown): void;
|
||||
};
|
||||
app.activeSessionId = 'sess-1';
|
||||
app.openFilePreview = vi.fn();
|
||||
app.openFolderGrid = vi.fn();
|
||||
const body = document.createElement('div');
|
||||
body.innerHTML = '<p><a href="/home/me/run/summary.json" target="_blank">Our incident record</a></p>';
|
||||
app._linkifyFilePaths(body);
|
||||
app._bindResponseViewerInteractions(body);
|
||||
|
||||
const click = new dom.window.MouseEvent('click', { bubbles: true, cancelable: true });
|
||||
body.querySelector('a')!.dispatchEvent(click);
|
||||
|
||||
expect(click.defaultPrevented).toBe(true);
|
||||
expect(app.openFilePreview).toHaveBeenCalledWith('/home/me/run/summary.json', 'sess-1');
|
||||
expect(app.openFolderGrid).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps web, mail, protocol-relative and fragment links as they are', () => {
|
||||
const hrefs = ['https://example.com/a.json', 'mailto:me@example.com', '//cdn.example.com/x.js', '#section'];
|
||||
const root = linkify(`<p>${hrefs.map((h) => `<a href="${h}">x</a>`).join(' ')}</p>`);
|
||||
|
||||
expect(paths(root)).toHaveLength(0);
|
||||
expect(Array.from(root.querySelectorAll('a')).map((a) => a.getAttribute('href'))).toEqual(hrefs);
|
||||
});
|
||||
|
||||
it('reads a scheme split by a tab as the URL the browser would open', () => {
|
||||
// The URL parser drops tabs, so this is http://localhost:5173/a, never a relative file.
|
||||
const root = linkify('<p><a href="http	://localhost:5173/a">local</a></p>');
|
||||
|
||||
expect(paths(root)).toHaveLength(0);
|
||||
expect(root.querySelector('a')!.getAttribute('href')).toBe('http\t://localhost:5173/a');
|
||||
});
|
||||
|
||||
it('leaves text with no path untouched', () => {
|
||||
const root = linkify('<p>Ratio 3/4 on 2026/08/16, see the app</p>');
|
||||
|
||||
|
||||
Reference in new issue
Block a user