Skip to content

Commit 618b9ac

Browse files
feat(chat): clickable file names on file-related tool call rows
Replace the standalone open-in-pane icon button with a hover-underline treatment directly on the filename. Applies to Read, Check file, Write, Edit, Delete, and Smart Edit tool call rows. Extracts a shared ClickableFilePath component (span[role=button]) that nests safely inside CollapsibleTrigger without invalid nested-button HTML.
1 parent d872a7f commit 618b9ac

4 files changed

Lines changed: 124 additions & 38 deletions

File tree

apps/desktop/src/renderer/components/Chat/ChatInterface/components/ReadOnlyToolCall/ReadOnlyToolCall.tsx

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1+
import { ClickableFilePath } from "@superset/ui/ai-elements/clickable-file-path";
12
import { ReadFileTool } from "@superset/ui/ai-elements/read-file-tool";
23
import { ToolInput, ToolOutput } from "@superset/ui/ai-elements/tool";
34
import { ToolCallRow } from "@superset/ui/ai-elements/tool-call-row";
45
import { getToolName } from "ai";
5-
import {
6-
ExternalLinkIcon,
7-
FileIcon,
8-
FileSearchIcon,
9-
FolderTreeIcon,
10-
SearchIcon,
11-
} from "lucide-react";
6+
import { FileIcon, FileSearchIcon, FolderTreeIcon, SearchIcon } from "lucide-react";
127
import { electronTrpc } from "renderer/lib/electron-trpc";
138
import { detectLanguage } from "shared/detect-language";
149
import type { BundledLanguage } from "shiki";
@@ -157,18 +152,6 @@ export function ReadOnlyToolCall({
157152
const filePath = getWorkspaceToolFilePath({ toolName, args });
158153
const canOpenFile = Boolean(filePath && onOpenFileInPane);
159154

160-
const headerExtra =
161-
canOpenFile && filePath ? (
162-
<button
163-
type="button"
164-
aria-label={`Open ${filePath} in file pane`}
165-
className="mr-1 flex h-6 w-6 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-muted/40 hover:text-foreground"
166-
onClick={() => onOpenFileInPane?.(filePath)}
167-
>
168-
<ExternalLinkIcon className="h-3 w-3" />
169-
</button>
170-
) : undefined;
171-
172155
// Prevent a flash of raw output while the disk read is in flight
173156
if (
174157
isReadFileTool &&
@@ -207,10 +190,22 @@ export function ReadOnlyToolCall({
207190
);
208191
}
209192

193+
// For file-path tools (e.g. file_stat), make the filename clickable.
194+
// Search queries and directory listings stay as plain text.
195+
const descriptionNode =
196+
canOpenFile && filePath && subtitle ? (
197+
<ClickableFilePath
198+
path={filePath}
199+
display={subtitle}
200+
onOpen={() => onOpenFileInPane?.(filePath)}
201+
/>
202+
) : (
203+
subtitle || undefined
204+
);
205+
210206
return (
211207
<ToolCallRow
212-
description={subtitle || undefined}
213-
headerExtra={headerExtra}
208+
description={descriptionNode}
214209
icon={Icon}
215210
isError={isError || displayState === "output-error"}
216211
isPending={isPending}

apps/desktop/src/renderer/components/Chat/components/SubagentInnerToolCall/SubagentInnerToolCall.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { BashTool } from "@superset/ui/ai-elements/bash-tool";
2+
import { ClickableFilePath } from "@superset/ui/ai-elements/clickable-file-path";
23
import { ReadFileTool } from "@superset/ui/ai-elements/read-file-tool";
34
import { ToolCallRow } from "@superset/ui/ai-elements/tool-call-row";
45
import {
@@ -62,6 +63,28 @@ function getToolMeta(toolName: string): ToolMeta {
6263
);
6364
}
6465

66+
/** Tools where the description is a file path (not a search query or URL). */
67+
const FILE_PATH_TOOLS = new Set([
68+
"mastra_workspace_write_file",
69+
"mastra_workspace_edit_file",
70+
"mastra_workspace_file_stat",
71+
"mastra_workspace_delete",
72+
"ast_smart_edit",
73+
]);
74+
75+
function getRawFilePath(
76+
toolName: string,
77+
args: Record<string, unknown>,
78+
): string | null {
79+
if (FILE_PATH_TOOLS.has(toolName)) {
80+
const raw = String(
81+
args.path ?? args.filePath ?? args.file_path ?? args.file ?? "",
82+
);
83+
return raw || null;
84+
}
85+
return null;
86+
}
87+
6588
function getDescription(
6689
toolName: string,
6790
args: Record<string, unknown> | null,
@@ -261,13 +284,33 @@ export function SubagentInnerToolCall({
261284
}
262285
}
263286

287+
// For file-path tools, make the filename in the description clickable.
288+
const rawFilePath = getRawFilePath(normalized, args ?? {});
289+
const resolvedFilePath = rawFilePath
290+
? (normalizeWorkspaceFilePath({
291+
filePath: rawFilePath,
292+
workspaceRoot: workspaceCwd,
293+
}) ?? rawFilePath)
294+
: null;
295+
296+
const descriptionNode =
297+
resolvedFilePath && onOpenFileInPane && description ? (
298+
<ClickableFilePath
299+
path={resolvedFilePath}
300+
display={description}
301+
onOpen={() => onOpenFileInPane(resolvedFilePath)}
302+
/>
303+
) : (
304+
description
305+
);
306+
264307
return (
265308
<ToolCallRow
266309
icon={icon}
267310
isError={isError}
268311
isPending={isPending}
269312
title={label}
270-
description={description}
313+
description={descriptionNode}
271314
>
272315
{hasResult ? (
273316
<div className="pl-2 py-1.5">
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"use client";
2+
3+
export type ClickableFilePathProps = {
4+
/** Full file path, used for the aria-label. */
5+
path: string;
6+
/** Display text. Defaults to the basename of `path`. */
7+
display?: string;
8+
/** When provided, renders as an interactive element that calls this on click. */
9+
onOpen?: () => void;
10+
className?: string;
11+
};
12+
13+
/**
14+
* Displays a file path (or its basename) with a hover highlight when clickable.
15+
*
16+
* Uses `<span role="button">` so it safely nests inside a `<button>` element
17+
* (e.g. CollapsibleTrigger) without producing invalid HTML.
18+
* stopPropagation prevents the outer CollapsibleTrigger from toggling.
19+
*/
20+
export function ClickableFilePath({
21+
path,
22+
display,
23+
onOpen,
24+
className,
25+
}: ClickableFilePathProps) {
26+
const label =
27+
display ?? (path.includes("/") ? (path.split("/").pop() ?? path) : path);
28+
29+
if (!onOpen) {
30+
return <span className={className}>{label}</span>;
31+
}
32+
33+
return (
34+
<span
35+
role="button"
36+
tabIndex={0}
37+
aria-label={`Open ${path} in file pane`}
38+
className={`cursor-pointer underline-offset-2 transition-colors hover:text-foreground hover:underline ${className ?? ""}`}
39+
onClick={(e) => {
40+
e.stopPropagation();
41+
onOpen();
42+
}}
43+
onKeyDown={(e) => {
44+
if (e.key === "Enter" || e.key === " ") {
45+
e.stopPropagation();
46+
onOpen();
47+
}
48+
}}
49+
>
50+
{label}
51+
</span>
52+
);
53+
}

packages/ui/src/components/ai-elements/read-file-tool.tsx

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"use client";
22

3-
import { ExternalLinkIcon, FileIcon } from "lucide-react";
3+
import { FileIcon } from "lucide-react";
44
import type { BundledLanguage } from "shiki";
5+
import { ClickableFilePath } from "./clickable-file-path";
56
import { CodeBlock } from "./code-block";
67
import { ToolCallRow } from "./tool-call-row";
78

@@ -16,7 +17,7 @@ export type ReadFileToolProps = {
1617
language?: BundledLanguage;
1718
isError?: boolean;
1819
isPending?: boolean;
19-
/** When provided, renders an "open in pane" icon button in the header. */
20+
/** When provided, makes the filename clickable to open in pane. */
2021
onOpenInPane?: () => void;
2122
className?: string;
2223
};
@@ -35,22 +36,12 @@ export function ReadFileTool({
3536
onOpenInPane,
3637
className,
3738
}: ReadFileToolProps) {
38-
const headerExtra = onOpenInPane ? (
39-
<button
40-
type="button"
41-
aria-label={`Open ${filename} in file pane`}
42-
className="mr-1 flex h-6 w-6 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-muted/40 hover:text-foreground"
43-
onClick={onOpenInPane}
44-
>
45-
<ExternalLinkIcon className="h-3 w-3" />
46-
</button>
47-
) : undefined;
48-
4939
return (
5040
<ToolCallRow
5141
className={className}
52-
description={filename}
53-
headerExtra={headerExtra}
42+
description={
43+
<ClickableFilePath path={filename} onOpen={onOpenInPane} />
44+
}
5445
icon={FileIcon}
5546
isError={isError}
5647
isPending={isPending}
@@ -59,7 +50,11 @@ export function ReadFileTool({
5950
<div className="py-1.5 pl-2">
6051
<div className="overflow-hidden rounded-md border border-border">
6152
<div className="flex items-center gap-2 border-b border-border bg-muted/50 px-3 py-1.5 font-mono text-xs">
62-
<span className="text-foreground">{filename}</span>
53+
<ClickableFilePath
54+
path={filename}
55+
onOpen={onOpenInPane}
56+
className="text-foreground"
57+
/>
6358
{lineRange && (
6459
<span className="text-muted-foreground">{lineRange}</span>
6560
)}

0 commit comments

Comments
 (0)