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:
Codeman maintainer committed 2026-10-22 19:23:17 +02:00
commit b4275e690e
6 files changed
+129 -5

No files matched your search

+36
View File
@@ -2967,6 +2967,12 @@ class CodemanApp {
* output. Subtrees already inside an `<a>` are skipped so an autolinked URL
* is never re-cut, and the anchor's textContent is the path verbatim, so
* "copy code" still yields exactly what the agent printed.
*
* Before that walk it also rewrites an existing anchor whose href is a path
* (no scheme, not `//host`, not a bare `#fragment`) into the same `a.rv-path`
* shape, on purpose WITHOUT `FILE_PATH_LINK_PATTERN`'s root and extension
* filter: the agent chose that href as a link target, and left alone it opens
* as a same-origin URL (a 404 page in a new tab).
*/
_linkifyFilePaths(root) {
if (!root || typeof document === 'undefined') return;
@@ -2974,6 +2980,36 @@ class CodemanApp {
// out of the middle of rendering a message.
if (typeof findFilePathLinks !== 'function') return;
// A markdown link whose destination is a path (`[report](/home/me/run/x.json)`,
// Codex's usual way of citing a file) resolved against Codeman's own origin
// and opened a 404 in a new tab. Give it the linkified-path shape instead;
// links with a scheme or `//host` keep their own behaviour.
for (const a of root.querySelectorAll('a[href]')) {
// The URL parser drops tabs and newlines, so `http&#9;://` is still http.
const href = (a.getAttribute('href') || '').replace(/[\t\n\r]/g, '');
if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith('//')) continue;
// `#L42`, `?plain=1` and a `:42:7` suffix name a spot in the file, not the
// file. The suffix is cut before decoding, so an encoded `%3A` stays a name.
let path = href.split('#')[0].split('?')[0].replace(/:\d+(?::\d+)?$/, '');
try {
path = decodeURIComponent(path);
} catch {
/* malformed escape: keep the path as written */
}
// A bare `#fragment` (and every `.rv-path`, whose href is `#`) ends up empty.
if (!path) continue;
// rv-path-link keeps the label in the prose font (styles.css): it is a label,
// not a path, and break-all would split it mid-word on a phone.
a.className = 'rv-path rv-path-link';
a.dataset.path = path;
a.dataset.linkified = '1';
if (path.endsWith('/') || findFilePathLinks(path).some((f) => f.folder && f.path === path)) a.dataset.folder = '1';
a.title = path;
a.setAttribute('href', '#');
a.removeAttribute('target');
a.removeAttribute('rel');
}
// Collect first: replacing a node while the walker is positioned on it
// invalidates the traversal.
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
+8
View File
@@ -14961,6 +14961,14 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
cursor: pointer;
}
/* A markdown link whose target is a path (`[Our incident record](/tmp/x.json)`)
takes the rv-path shape for the click delegate, but its text is a label, not a
path: keep it in the prose font and never break it mid-word. */
.rv-text a.rv-path.rv-path-link {
font: inherit;
word-break: normal;
}
/* Tables — scroll wrapper keeps table proper while allowing horizontal overflow */
.rv-table-wrap {
margin: 1em 0;