Skip to content

Commit 48f8aef

Browse files
authored
feat(desktop): open chat file tools in pane + diffs.com UI for edit tool (#1828)
* feat(desktop): open chat file tools in pane and render edit diffs with diffs.com * fix(desktop): support ast edit diffs and explicit file-open action
1 parent 924cd4f commit 48f8aef

10 files changed

Lines changed: 463 additions & 86 deletions

File tree

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/ChatMastraInterface.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function toErrorMessage(error: unknown): string | null {
4444

4545
export function ChatMastraInterface({
4646
sessionId,
47-
workspaceId: _workspaceId,
47+
workspaceId,
4848
cwd,
4949
onStartFreshSession,
5050
onRawSnapshotChange,
@@ -222,6 +222,8 @@ export function ChatMastraInterface({
222222
messages={mergedMessages}
223223
isRunning={canAbort}
224224
currentMessage={currentMessage ?? null}
225+
workspaceId={workspaceId}
226+
workspaceCwd={cwd}
225227
activeTools={activeTools}
226228
toolInputBuffers={toolInputBuffers}
227229
/>

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/components/ChatMastraMessageList/ChatMastraMessageList.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ interface ChatMastraMessageListProps {
3737
messages: MastraMessage[];
3838
isRunning: boolean;
3939
currentMessage: MastraMessage | null;
40+
workspaceId: string;
41+
workspaceCwd?: string;
4042
activeTools: MastraActiveTools | undefined;
4143
toolInputBuffers: MastraToolInputBuffers | undefined;
4244
}
@@ -225,10 +227,14 @@ function UserMessage({ message }: { message: MastraMessage }) {
225227
function AssistantMessage({
226228
message,
227229
isStreaming,
230+
workspaceId,
231+
workspaceCwd,
228232
previewToolParts = [],
229233
}: {
230234
message: MastraMessage;
231235
isStreaming: boolean;
236+
workspaceId: string;
237+
workspaceCwd?: string;
232238
previewToolParts?: ToolPart[];
233239
}) {
234240
const nodes: ReactNode[] = [];
@@ -287,6 +293,8 @@ function AssistantMessage({
287293
result,
288294
isStreaming,
289295
})}
296+
workspaceId={workspaceId}
297+
workspaceCwd={workspaceCwd}
290298
/>,
291299
);
292300

@@ -303,6 +311,8 @@ function AssistantMessage({
303311
<MastraToolCallBlock
304312
key={`${message.id}-tool-result-${part.id}`}
305313
part={toToolPartFromResult(part)}
314+
workspaceId={workspaceId}
315+
workspaceCwd={workspaceCwd}
306316
/>,
307317
);
308318
continue;
@@ -327,6 +337,8 @@ function AssistantMessage({
327337
<MastraToolCallBlock
328338
key={`${message.id}-tool-preview-${previewPart.toolCallId}`}
329339
part={previewPart}
340+
workspaceId={workspaceId}
341+
workspaceCwd={workspaceCwd}
330342
/>,
331343
);
332344
}
@@ -350,6 +362,8 @@ export function ChatMastraMessageList({
350362
messages,
351363
isRunning,
352364
currentMessage,
365+
workspaceId,
366+
workspaceCwd,
353367
activeTools,
354368
toolInputBuffers,
355369
}: ChatMastraMessageListProps) {
@@ -392,6 +406,8 @@ export function ChatMastraMessageList({
392406
<AssistantMessage
393407
key={message.id}
394408
message={message}
409+
workspaceId={workspaceId}
410+
workspaceCwd={workspaceCwd}
395411
isStreaming={false}
396412
previewToolParts={[]}
397413
/>
@@ -402,6 +418,8 @@ export function ChatMastraMessageList({
402418
<AssistantMessage
403419
key={`current-${currentMessage.id}`}
404420
message={currentMessage}
421+
workspaceId={workspaceId}
422+
workspaceCwd={workspaceCwd}
405423
isStreaming
406424
previewToolParts={previewToolParts}
407425
/>
@@ -428,6 +446,8 @@ export function ChatMastraMessageList({
428446
<MastraToolCallBlock
429447
key={`tool-preview-${part.toolCallId}`}
430448
part={part}
449+
workspaceId={workspaceId}
450+
workspaceCwd={workspaceCwd}
431451
/>
432452
))}
433453
</MessageContent>

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/MastraToolCallBlock.tsx

Lines changed: 148 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { WebFetchTool } from "@superset/ui/ai-elements/web-fetch-tool";
55
import { WebSearchTool } from "@superset/ui/ai-elements/web-search-tool";
66
import { getToolName } from "ai";
77
import { FileIcon, FolderIcon, MessageCircleQuestionIcon } from "lucide-react";
8+
import { useCallback } from "react";
9+
import { useTabsStore } from "renderer/stores/tabs/store";
810
import { READ_ONLY_TOOLS } from "../../constants";
11+
import { normalizeWorkspaceFilePath } from "../../utils/file-paths";
912
import type { ToolPart } from "../../utils/tool-helpers";
1013
import {
1114
getArgs,
@@ -14,24 +17,42 @@ import {
1417
toWsToolState,
1518
} from "../../utils/tool-helpers";
1619
import { ReadOnlyToolCall } from "../ReadOnlyToolCall";
20+
import { EditToolExpandedDiff } from "./components/EditToolExpandedDiff";
1721
import { GenericToolCall } from "./components/GenericToolCall";
1822

1923
interface MastraToolCallBlockProps {
2024
part: ToolPart;
25+
workspaceId?: string;
26+
workspaceCwd?: string;
2127
onAnswer?: (toolCallId: string, answers: Record<string, string>) => void;
2228
}
2329

2430
export function MastraToolCallBlock({
2531
part,
32+
workspaceId,
33+
workspaceCwd,
2634
onAnswer,
2735
}: MastraToolCallBlockProps) {
2836
const args = getArgs(part);
2937
const result = getResult(part);
3038
const state = toWsToolState(part);
3139
const toolName = normalizeToolName(getToolName(part));
40+
const addFileViewerPane = useTabsStore((store) => store.addFileViewerPane);
3241
const toolDisplayName = toolName
3342
.replace("mastra_workspace_", "")
3443
.replaceAll("_", " ");
44+
const openFileInPane = useCallback(
45+
(filePath: string) => {
46+
if (!workspaceId) return;
47+
const normalizedPath = normalizeWorkspaceFilePath({
48+
filePath,
49+
workspaceRoot: workspaceCwd,
50+
});
51+
if (!normalizedPath) return;
52+
addFileViewerPane(workspaceId, { filePath: normalizedPath });
53+
},
54+
[addFileViewerPane, workspaceCwd, workspaceId],
55+
);
3556

3657
const outputObject =
3758
typeof result.output === "object" && result.output !== null
@@ -64,6 +85,17 @@ export function MastraToolCallBlock({
6485
return undefined;
6586
};
6687

88+
const toRecord = (value: unknown): Record<string, unknown> | undefined => {
89+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
90+
return value as Record<string, unknown>;
91+
}
92+
return undefined;
93+
};
94+
95+
const getFilePath = (...values: unknown[]): string => {
96+
return firstText(...values) ?? "";
97+
};
98+
6799
const toNumber = (value: unknown): number | undefined => {
68100
if (typeof value === "number" && Number.isFinite(value)) return value;
69101
if (typeof value === "string" && value.trim().length > 0) {
@@ -151,36 +183,141 @@ export function MastraToolCallBlock({
151183

152184
// --- Write file → FileDiffTool (write mode) ---
153185
if (toolName === "mastra_workspace_write_file") {
154-
const filePath = String(
155-
args.path ?? args.filePath ?? args.relative_workspace_path ?? "",
186+
const filePath = getFilePath(
187+
args.path,
188+
args.filePath,
189+
args.file_path,
190+
args.relative_workspace_path,
191+
args.relativePath,
192+
args.file,
193+
args.filename,
194+
toRecord(args.target)?.path,
156195
);
157196
const content = String(args.content ?? args.data ?? "");
158197
return (
159198
<FileDiffTool
160199
filePath={filePath}
161200
content={content}
162201
isWriteMode
202+
onFilePathClick={openFileInPane}
163203
state={state}
164204
/>
165205
);
166206
}
167207

168208
// --- Edit file → FileDiffTool (diff mode) ---
169-
if (toolName === "mastra_workspace_edit_file") {
170-
const filePath = String(
171-
args.path ?? args.filePath ?? args.relative_workspace_path ?? "",
172-
);
173-
const oldString = String(
174-
args.oldString ?? args.old_string ?? args.old_str ?? "",
209+
if (
210+
toolName === "mastra_workspace_edit_file" ||
211+
toolName === "ast_smart_edit"
212+
) {
213+
const editArgs = toRecord(args.edit);
214+
const filePath = getFilePath(
215+
args.path,
216+
args.filePath,
217+
args.file_path,
218+
args.relative_workspace_path,
219+
args.relativePath,
220+
args.file,
221+
args.filename,
222+
args.file_name,
223+
args.target_file,
224+
args.target_path,
225+
args.targetPath,
226+
editArgs?.path,
227+
editArgs?.filePath,
228+
editArgs?.file_path,
229+
toRecord(args.target)?.path,
175230
);
176-
const newString = String(
177-
args.newString ?? args.new_string ?? args.new_str ?? "",
231+
const oldString =
232+
firstText(
233+
args.oldString,
234+
args.old_string,
235+
args.old_str,
236+
args.oldText,
237+
args.old_text,
238+
args.oldCode,
239+
args.old_code,
240+
args.before,
241+
args.find,
242+
args.search,
243+
args.original,
244+
args.previous,
245+
args.from,
246+
editArgs?.oldString,
247+
editArgs?.old_string,
248+
editArgs?.oldText,
249+
editArgs?.before,
250+
outputObject?.oldString,
251+
outputObject?.old_string,
252+
nestedResultObject?.oldString,
253+
nestedResultObject?.old_string,
254+
) ?? "";
255+
const newString =
256+
firstText(
257+
args.newString,
258+
args.new_string,
259+
args.new_str,
260+
args.newText,
261+
args.new_text,
262+
args.newCode,
263+
args.new_code,
264+
args.after,
265+
args.replace,
266+
args.replacement,
267+
args.updated,
268+
args.to,
269+
editArgs?.newString,
270+
editArgs?.new_string,
271+
editArgs?.newText,
272+
editArgs?.after,
273+
outputObject?.newString,
274+
outputObject?.new_string,
275+
nestedResultObject?.newString,
276+
nestedResultObject?.new_string,
277+
) ?? "";
278+
279+
const structuredPatchValue =
280+
(Array.isArray(result.structuredPatch)
281+
? result.structuredPatch
282+
: Array.isArray(outputObject?.structuredPatch)
283+
? outputObject?.structuredPatch
284+
: Array.isArray(nestedResultObject?.structuredPatch)
285+
? nestedResultObject?.structuredPatch
286+
: undefined) ??
287+
(Array.isArray(result.structured_patch)
288+
? result.structured_patch
289+
: Array.isArray(outputObject?.structured_patch)
290+
? outputObject?.structured_patch
291+
: Array.isArray(nestedResultObject?.structured_patch)
292+
? nestedResultObject?.structured_patch
293+
: undefined);
294+
const structuredPatch = structuredPatchValue?.filter(
295+
(hunk): hunk is { lines: string[] } => {
296+
return Boolean(
297+
typeof hunk === "object" &&
298+
hunk !== null &&
299+
Array.isArray((hunk as { lines?: unknown }).lines),
300+
);
301+
},
178302
);
179303
return (
180304
<FileDiffTool
181305
filePath={filePath}
182306
oldString={oldString}
183307
newString={newString}
308+
structuredPatch={structuredPatch}
309+
onFilePathClick={openFileInPane}
310+
renderExpandedContent={
311+
oldString || newString
312+
? () => (
313+
<EditToolExpandedDiff
314+
filePath={filePath}
315+
oldString={oldString}
316+
newString={newString}
317+
/>
318+
)
319+
: undefined
320+
}
184321
state={state}
185322
/>
186323
);
@@ -247,7 +384,7 @@ export function MastraToolCallBlock({
247384

248385
// --- Read-only exploration tools ---
249386
if (READ_ONLY_TOOLS.has(toolName)) {
250-
return <ReadOnlyToolCall part={part} />;
387+
return <ReadOnlyToolCall part={part} onOpenFileInPane={openFileInPane} />;
251388
}
252389

253390
// --- Destructive workspace tools ---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { LightDiffViewer } from "renderer/screens/main/components/WorkspaceView/ChangesContent/components/LightDiffViewer";
2+
import { useChangesStore } from "renderer/stores/changes";
3+
import type { FileContents } from "shared/changes-types";
4+
5+
interface EditToolExpandedDiffProps {
6+
filePath: string;
7+
oldString: string;
8+
newString: string;
9+
}
10+
11+
export function EditToolExpandedDiff({
12+
filePath,
13+
oldString,
14+
newString,
15+
}: EditToolExpandedDiffProps) {
16+
const viewMode = useChangesStore((state) => state.viewMode);
17+
const hideUnchangedRegions = useChangesStore(
18+
(state) => state.hideUnchangedRegions,
19+
);
20+
21+
const contents: FileContents = {
22+
original: oldString,
23+
modified: newString,
24+
language: "text",
25+
};
26+
27+
return (
28+
<LightDiffViewer
29+
contents={contents}
30+
viewMode={viewMode}
31+
hideUnchangedRegions={hideUnchangedRegions}
32+
filePath={filePath}
33+
/>
34+
);
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { EditToolExpandedDiff } from "./EditToolExpandedDiff";

0 commit comments

Comments
 (0)