feat: add clickable file paths to agent tool outputs and inline code#13465
feat: add clickable file paths to agent tool outputs and inline code#13465
Conversation
Make file paths in agent tool outputs (Edit, Write, Read, Glob, Grep, MultiEdit, NotebookEdit) and inline code blocks interactive. Clicking opens the file; a dropdown menu offers "Reveal in Finder" and "Open in Editor" (VS Code, Cursor, Zed) integration. - Add ClickableFilePath component with editor dropdown - Detect absolute file paths in inline code blocks - Add i18n keys for file operations across all locales - Add unit tests for ClickableFilePath and CodeBlock detection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
EurFelux
left a comment
There was a problem hiding this comment.
Nice feature! The clickable file paths across all agent tool outputs are a solid developer workflow improvement. The implementation is clean and well-tested. A few observations:
Key items:
- Code duplication —
getEditorIconand theopenInEditorURL construction pattern are duplicated fromOpenExternalAppButton.tsx. Consider extracting into a shared utility. - GrepTool regex —
/^(\/[^:]+)(:.*)?$/is quite permissive and could match non-path lines. A tighter pattern would reduce false positives. - NotebookEditTool fallback — Minor inconsistency in the ternary fallback vs other tools in this PR.
- Accessibility — The clickable
<span>could benefit from keyboard navigation support (tabIndex,onKeyDown).
Overall, this looks good! The conservative inline code path regex is a sensible tradeoff, and the test coverage is appreciated.
| const getEditorIcon = (app: ExternalAppInfo) => { | ||
| switch (app.id) { | ||
| case 'vscode': | ||
| return <VSCodeIcon className="size-4" /> | ||
| case 'cursor': | ||
| return <CursorIcon className="size-4" /> | ||
| case 'zed': | ||
| return <ZedIcon className="size-4" /> | ||
| } |
There was a problem hiding this comment.
Code duplication: getEditorIcon is duplicated here and in OpenExternalAppButton.tsx (line 12-21). Consider extracting it into a shared utility, e.g., under @renderer/components/Icons/ or a shared hooks module.
This also applies to the openInEditor logic pattern (lines 35-41) which shares the same URL construction approach as OpenExternalAppButton.tsx (lines 40-54), but without the error handling (logger.error + window.toast.error) that the existing implementation has for unsupported editors.
| <div>{truncatedOutput}</div> | ||
| <div> | ||
| {truncatedOutput?.split('\n').map((line, i) => { | ||
| const match = line.match(/^(\/[^:]+)(:.*)?$/) |
There was a problem hiding this comment.
Regex over-matching: The pattern /^(\/[^:]+)(:.*)?$/ will match any line starting with /, which could include error messages like /usr/bin/bash: No such file or directory.
Also, for content output mode, grep lines look like /path/to/file:42:matched content — the (:.*)? capture will grab :42:matched content as a single string, losing the structure. This is probably fine for display purposes, but worth noting.
A slightly tighter pattern like /^(\/[\w./@-]+[^:])(:.*)?$/ could reduce false positives while still matching most real paths.
| <ToolHeader toolName={AgentToolsType.NotebookEdit} variant="collapse-label" showStatus={false} /> | ||
| <Tag color="blue">{input?.notebook_path}</Tag> | ||
| <Tag color="blue"> | ||
| {input?.notebook_path ? <ClickableFilePath path={input.notebook_path} /> : input?.notebook_path} |
There was a problem hiding this comment.
Redundant fallback: The ternary fallback input?.notebook_path (when falsy) will render undefined, which is harmless but semantically odd. Other tools in this PR (e.g., EditTool, WriteTool) use undefined explicitly as the fallback:
// Consistent with other tools:
{input?.notebook_path ? <ClickableFilePath path={input.notebook_path} /> : undefined}nit: Minor inconsistency, but would be good to align with the pattern used in sibling components.
| <span | ||
| onClick={handleOpen} | ||
| className="cursor-pointer hover:underline" | ||
| style={{ color: 'var(--color-link)', wordBreak: 'break-all' }}> | ||
| {displayName ?? path} | ||
| </span> |
There was a problem hiding this comment.
Accessibility: This clickable <span> lacks keyboard support. Users who navigate with keyboard (Tab + Enter) won't be able to open files. Consider adding:
<span
role="link"
tabIndex={0}
onClick={handleOpen}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleOpen(e as any) }}
className="cursor-pointer hover:underline"
style={{ color: 'var(--color-link)', wordBreak: 'break-all' }}>Or alternatively, use a <button> styled as a link. This is a nit for now — just worth keeping in mind for a11y.
- Extract getEditorIcon and buildEditorUrl into shared editorUtils - Reuse shared utility in both ClickableFilePath and OpenExternalAppButton - Tighten GrepTool regex to reduce false positives on non-path lines - Fix NotebookEditTool redundant fallback to use undefined explicitly - Add keyboard accessibility (role="link", tabIndex, Enter/Space) - Make dropdown trigger more visible (larger font, hover background) - Add keyboard accessibility tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>

What this PR does
Before this PR:
File paths displayed in agent tool outputs (Edit, Write, Read, Glob, Grep, MultiEdit, NotebookEdit) and inline code blocks were plain, non-interactive text.
After this PR:
File paths are clickable — clicking opens the file directly. An ellipsis dropdown menu provides additional actions: "Reveal in Finder" and "Open in Editor" (VS Code, Cursor, Zed).
Why we need it and why it was done in this way
Clickable file paths improve developer workflow by allowing quick navigation to files referenced in agent tool outputs without manual copy-paste.
The following tradeoffs were made:
ClickableFilePathcomponent is used across all agent tools for consistency./^\/[\w.-]+(?:\/[\w.-]+)+$/) to avoid false positives on non-path inline code. This means some edge-case paths (e.g., with spaces or@) won't be detected in inline code, but tool-output paths are always clickable since they come from structured input.The following alternatives were considered:
Breaking changes
None.
Special notes for your reviewer
ClickableFilePathcomponent reuses existinguseExternalAppshook andwindow.api.fileIPC APIs.open_file,open_file_error,file_not_found, andreveal_in_finderacross all locale files.'Finder'string on macOS was replaced witht()for proper i18n compliance.openPath/showInFoldernow return Promises to avoid TypeError on.catch().Checklist
/gh-pr-review,gh pr diff, or GitHub UI) before requesting review from othersRelease note