Problem
In the Studio Chat panel, AI-generated markdown messages frequently reference workspace files, e.g.:
The assembly script is at [assemble_full_fig2.py](assemble_full_fig2.py), and the delivery notes
are in [FIG2_FULL_NINE_PANEL_DELIVERY.md](FIG2_FULL_NINE_PANEL_DELIVERY.md).
Currently, renderer.link renders all [text](href) uniformly as <a href="..." target="_blank">. Clicking these links causes the browser to open a new tab with a URL like:
http://127.0.0.1:20999/home/werner/DeepScientist/quests/001/.ds/worktrees/.../assemble_full_fig2.py
The web server has no matching route, so the request 404s. The links are effectively dead.
Code trail
| Location |
Role |
src/ui/src/lib/plugins/ai-manus/lib/markdown.ts → renderer.link |
Renders all markdown links as <a href="..." target="_blank"> |
src/ui/src/lib/plugins/ai-manus/AiManusChatView.tsx L1468, L8343–8377 |
Already has useOpenFile() → findNodeByPath + openFileInTab for file navigation |
src/ui/src/components/workspace/QuestWorkspaceSurface.tsx L4650–4679 |
Same — complete file-tree-click-to-open implementation |
Key observation: the file navigation infrastructure already exists (findNodeByPath + openFileInTab); the markdown rendering layer simply does not hook into it.
Proposed implementation
Layer 1 — Link classification
Introduce a utility function that categorizes an href into one of three types:
| Type |
Condition |
Behavior |
| Workspace file |
Relative path, or absolute path rooted under the quest worktree |
findNodeByPath + openFileInTab |
| Internal page |
Same-origin URL |
SPA navigation |
| External link |
Any other URL (http/https) |
Keep current behavior — target="_blank" |
function classifyHref(href: string, questRoot: string):
| { type: 'workspace-file'; relativePath: string }
| { type: 'internal'; url: string }
| { type: 'external'; url: string }
Layer 2 — Click interception
Since renderer.link returns a static HTML string (not a React component), direct React event binding is not possible. Two approaches:
Approach A — Event delegation (recommended, minimal diff)
Add a delegated onClick handler on the Chat message container:
const handleLinkClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
const anchor = (e.target as HTMLElement).closest('a[href]')
if (!anchor) return
const href = anchor.getAttribute('href') ?? ''
const classified = classifyHref(href, questRoot)
if (classified.type === 'workspace-file') {
e.preventDefault()
const node = findNodeByPath(classified.relativePath)
if (node) openFileInTab(node)
}
// external and internal links fall through to default behavior
}, [questRoot, findNodeByPath, openFileInTab])
Approach B — Custom React component for <a> tags
Switch markdown rendering from dangerouslySetInnerHTML to react-markdown's component mapping, supplying a custom React component for a. Larger diff but better type safety.
Layer 3 — Mark workspace links at render time (optional enhancement)
Add data-* attributes in renderer.link for workspace file hrefs, so the event delegate can classify links without re-parsing:
renderer.link = ({ href, title, text }: Tokens.Link) => {
const safeHref = href ?? ''
const titleAttr = title ? ` title="${title}"` : ''
const classified = classifyHref(safeHref, questRoot)
if (classified.type === 'workspace-file') {
return `<a href="${safeHref}" data-ws-path="${escapeHtml(classified.relativePath)}" class="ds-file-link"${titleAttr}>${text}</a>`
}
return `<a href="${safeHref}" target="_blank" rel="noopener noreferrer"${titleAttr}>${text}</a>`
}
This also removes target="_blank" from workspace links, preventing an empty tab from opening on click.
Layer 4 — Backend file serving route (optional enhancement)
Register a file serving endpoint (e.g. GET /api/v1/quests/:questId/files/*) so that workspace file links also work when right-click / middle-click opened in a new tab (preview or download). Lower priority than the frontend interception.
Concrete minimal implementation (~15 lines, 2 files)
The existing code already provides all the building blocks. The assistant message container at ChatMessage.tsx L784 already has an onClick={handleAssistantClick} handler that dispatches to handleMarkdownAction and handleCitationClick. The onFileClick prop pattern is already established. The fix threads a new onFileLinkClick callback through the same path.
File 1: src/ui/src/lib/plugins/ai-manus/components/ChatMessage.tsx
1a. Add prop (L52, inside ChatMessageProps):
onFileClick?: (fileId: string) => void
onFileLinkClick?: (path: string) => void // ← new
1b. Destructure it (around L267 where other props are destructured):
1c. Extend handleAssistantClick (L623–629) — add file link interception before existing handlers:
const handleAssistantClick = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
// --- file link interception (new) ---
const anchor = (event.target as HTMLElement).closest('a[href]')
if (anchor) {
const href = anchor.getAttribute('href') ?? ''
if (href && !href.startsWith('http') && !href.startsWith('mailto:')) {
event.preventDefault()
onFileLinkClick?.(href)
return
}
}
// --- existing handlers ---
if (handleMarkdownAction(event)) return
handleCitationClick(event)
},
[handleCitationClick, handleMarkdownAction, onFileLinkClick]
)
File 2: src/ui/src/lib/plugins/ai-manus/AiManusChatView.tsx
2a. Pass the callback (L8709, where <ChatMessage> is rendered):
onFileLinkClick={async (path) => {
const node = findNodeByPath(path)
if (node) {
await openFileInTab(node, { customData: projectId ? { projectId } : undefined })
}
}}
findNodeByPath and openFileInTab are already in scope (L1468–1470).
Why this works
handleAssistantClick already fires on every click inside the assistant message div (L784).
closest('a[href]') catches clicks on the link text, nested <code> spans, etc.
- Non-URL hrefs (relative paths, absolute filesystem paths) are the only ones that don't start with
http or mailto:, which is exactly the set of workspace file references.
- External links (
http://...) fall through to the existing target="_blank" behavior unchanged.
Suggested priority
- P0 — The minimal implementation above (~15 lines across 2 files): intercept non-URL hrefs in
handleAssistantClick, route to findNodeByPath + openFileInTab
- P1 — Add
data-ws-path marker and remove target="_blank" in renderer.link for workspace paths (Layer 3 above) to avoid the brief blank-tab flash
- P1 — Register a backend file serving route for new-tab / direct-URL access (Layer 4 above)
- P2 — Visual differentiation: style workspace file links differently from external links (e.g. file icon prefix)
Environment
- DeepScientist v1.5.17
- Ubuntu, Chrome / Firefox
Proposed solution
See above.
User-facing impact
See above.
Alternatives considered
No response
Expected scope
No response
Compatibility or migration concerns
No response
Problem
In the Studio Chat panel, AI-generated markdown messages frequently reference workspace files, e.g.:
Currently,
renderer.linkrenders all[text](href)uniformly as<a href="..." target="_blank">. Clicking these links causes the browser to open a new tab with a URL like:The web server has no matching route, so the request 404s. The links are effectively dead.
Code trail
src/ui/src/lib/plugins/ai-manus/lib/markdown.ts→renderer.link<a href="..." target="_blank">src/ui/src/lib/plugins/ai-manus/AiManusChatView.tsxL1468, L8343–8377useOpenFile()→findNodeByPath+openFileInTabfor file navigationsrc/ui/src/components/workspace/QuestWorkspaceSurface.tsxL4650–4679Key observation: the file navigation infrastructure already exists (
findNodeByPath+openFileInTab); the markdown rendering layer simply does not hook into it.Proposed implementation
Layer 1 — Link classification
Introduce a utility function that categorizes an href into one of three types:
findNodeByPath+openFileInTabtarget="_blank"Layer 2 — Click interception
Since
renderer.linkreturns a static HTML string (not a React component), direct React event binding is not possible. Two approaches:Approach A — Event delegation (recommended, minimal diff)
Add a delegated
onClickhandler on the Chat message container:Approach B — Custom React component for
<a>tagsSwitch markdown rendering from
dangerouslySetInnerHTMLtoreact-markdown's component mapping, supplying a custom React component fora. Larger diff but better type safety.Layer 3 — Mark workspace links at render time (optional enhancement)
Add
data-*attributes inrenderer.linkfor workspace file hrefs, so the event delegate can classify links without re-parsing:This also removes
target="_blank"from workspace links, preventing an empty tab from opening on click.Layer 4 — Backend file serving route (optional enhancement)
Register a file serving endpoint (e.g.
GET /api/v1/quests/:questId/files/*) so that workspace file links also work when right-click / middle-click opened in a new tab (preview or download). Lower priority than the frontend interception.Concrete minimal implementation (~15 lines, 2 files)
The existing code already provides all the building blocks. The assistant message container at
ChatMessage.tsxL784 already has anonClick={handleAssistantClick}handler that dispatches tohandleMarkdownActionandhandleCitationClick. TheonFileClickprop pattern is already established. The fix threads a newonFileLinkClickcallback through the same path.File 1:
src/ui/src/lib/plugins/ai-manus/components/ChatMessage.tsx1a. Add prop (L52, inside
ChatMessageProps):1b. Destructure it (around L267 where other props are destructured):
1c. Extend
handleAssistantClick(L623–629) — add file link interception before existing handlers:File 2:
src/ui/src/lib/plugins/ai-manus/AiManusChatView.tsx2a. Pass the callback (L8709, where
<ChatMessage>is rendered):findNodeByPathandopenFileInTabare already in scope (L1468–1470).Why this works
handleAssistantClickalready fires on every click inside the assistant message div (L784).closest('a[href]')catches clicks on the link text, nested<code>spans, etc.httpormailto:, which is exactly the set of workspace file references.http://...) fall through to the existingtarget="_blank"behavior unchanged.Suggested priority
handleAssistantClick, route tofindNodeByPath+openFileInTabdata-ws-pathmarker and removetarget="_blank"inrenderer.linkfor workspace paths (Layer 3 above) to avoid the brief blank-tab flashEnvironment
Proposed solution
See above.
User-facing impact
See above.
Alternatives considered
No response
Expected scope
No response
Compatibility or migration concerns
No response