linkify file paths in chat, open via /open endpoint; proxy /open in dev

This commit is contained in:
Levi Neely 2026-04-26 15:54:22 +02:00
parent 1648d99178
commit 1c180de2e4
3 changed files with 31 additions and 1 deletions

View File

@ -50,6 +50,19 @@ function escapeHtml(s: string) {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
} }
function linkifyPaths(html: string, cwd: string): string {
return html.replace(/<code>([^<]+)<\/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 `<code><a class="file-link" href="#" data-fpath="${escapeHtml(fullPath)}">${inner}</a></code>`
})
}
marked.use({ gfm: true, breaks: false, renderer }) marked.use({ gfm: true, breaks: false, renderer })
// Display math: $$...$$ as a block or inline paragraph // 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 handleDiagramClick = (e: MouseEvent) => {
const target = e.target as HTMLElement 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 // Toggle button: switch between rendered and source
const btn = target.closest('.diagram-toggle') as HTMLElement | null const btn = target.closest('.diagram-toggle') as HTMLElement | null
if (btn) { if (btn) {
@ -224,7 +247,7 @@ export function ChatPane({ session, cfg, messages, onSend, onStop, browsePath, b
) : ( ) : (
<div <div
class="md" class="md"
dangerouslySetInnerHTML={{ __html: (marked.parse(msg.content) as string).replace(/^<p>/, '<p><span class="msg-prefix">a: </span>') }} dangerouslySetInnerHTML={{ __html: linkifyPaths((marked.parse(msg.content) as string).replace(/^<p>/, '<p><span class="msg-prefix">a: </span>'), session.cwd) }}
/> />
)} )}
</div> </div>

View File

@ -878,6 +878,12 @@ details[open] > .tool-header::before {
word-break: break-all; word-break: break-all;
color: var(--text); color: var(--text);
} }
.file-link {
cursor: pointer;
text-decoration: underline;
color: var(--accent);
}
.file-link:hover { opacity: 0.8; }
.fs-editor { .fs-editor {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@ -11,6 +11,7 @@ export default defineConfig({
'/t/': 'http://localhost:8080', '/t/': 'http://localhost:8080',
'/sk/': 'http://localhost:8080', '/sk/': 'http://localhost:8080',
'/backends': 'http://localhost:8080', '/backends': 'http://localhost:8080',
'/open': 'http://localhost:8080',
'/openapi.json': 'http://localhost:8080', '/openapi.json': 'http://localhost:8080',
}, },
}, },