From 1c180de2e4c90a797db0f3fb70474b06d536c842 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Sun, 26 Apr 2026 15:54:22 +0200 Subject: [PATCH] linkify file paths in chat, open via /open endpoint; proxy /open in dev --- src/components/ChatPane.tsx | 25 ++++++++++++++++++++++++- src/style.css | 6 ++++++ vite.config.ts | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/components/ChatPane.tsx b/src/components/ChatPane.tsx index 27a30b5..7750951 100644 --- a/src/components/ChatPane.tsx +++ b/src/components/ChatPane.tsx @@ -50,6 +50,19 @@ function escapeHtml(s: string) { return s.replace(/&/g, '&').replace(//g, '>') } +function linkifyPaths(html: string, cwd: string): string { + return html.replace(/([^<]+)<\/code>/g, (_match, inner: string) => { + // Match: has a file extension, or contains a / + if (!/\.\w+/.test(inner) && !inner.includes('/')) return _match + // Exclude things that are clearly not paths (spaces, parens, etc.) + if (/[\s(){}[\]|;]/.test(inner)) return _match + const filePart = inner.split(':')[0] + const href = filePart.startsWith('/') ? `file://${filePart}` : `file://${cwd}/${filePart}` + const fullPath = filePart.startsWith('/') ? filePart : cwd + '/' + filePart + return `${inner}` + }) +} + marked.use({ gfm: true, breaks: false, renderer }) // Display math: $$...$$ as a block or inline paragraph @@ -132,6 +145,16 @@ export function ChatPane({ session, cfg, messages, onSend, onStop, browsePath, b const handleDiagramClick = (e: MouseEvent) => { const target = e.target as HTMLElement + // File path links — open in editor + const link = target.closest('.file-link') as HTMLElement | null + if (link) { + e.preventDefault() + e.stopPropagation() + const fp = link.getAttribute('data-fpath') + if (fp) fetch('/open?path=' + encodeURIComponent(fp)) + return + } + // Toggle button: switch between rendered and source const btn = target.closest('.diagram-toggle') as HTMLElement | null if (btn) { @@ -224,7 +247,7 @@ export function ChatPane({ session, cfg, messages, onSend, onStop, browsePath, b ) : (
/, '

a: ') }} + dangerouslySetInnerHTML={{ __html: linkifyPaths((marked.parse(msg.content) as string).replace(/^

/, '

a: '), session.cwd) }} /> )}

diff --git a/src/style.css b/src/style.css index faaef55..2279dbd 100644 --- a/src/style.css +++ b/src/style.css @@ -878,6 +878,12 @@ details[open] > .tool-header::before { word-break: break-all; color: var(--text); } +.file-link { + cursor: pointer; + text-decoration: underline; + color: var(--accent); +} +.file-link:hover { opacity: 0.8; } .fs-editor { width: 100%; height: 100%; diff --git a/vite.config.ts b/vite.config.ts index cf6331f..d13024b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ '/t/': 'http://localhost:8080', '/sk/': 'http://localhost:8080', '/backends': 'http://localhost:8080', + '/open': 'http://localhost:8080', '/openapi.json': 'http://localhost:8080', }, },