diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/ChatMastraInterface.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/ChatMastraInterface.tsx index da446e5d05e..c741dadb5b7 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/ChatMastraInterface.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/ChatMastraInterface.tsx @@ -47,6 +47,7 @@ export function ChatMastraInterface({ workspaceId: _workspaceId, cwd, onStartFreshSession, + onRawSnapshotChange, }: ChatMastraInterfaceProps) { const { models: availableModels, defaultModel } = useAvailableModels(); const [selectedModel, setSelectedModel] = useState(null); @@ -143,6 +144,23 @@ export function ChatMastraInterface({ setSubmitStatus(undefined); }, [isRunning]); + useEffect(() => { + onRawSnapshotChange?.({ + sessionId, + isRunning: canAbort, + currentMessage: currentMessage ?? null, + messages: messages ?? [], + error, + }); + }, [ + canAbort, + currentMessage, + error, + messages, + onRawSnapshotChange, + sessionId, + ]); + const handleSend = useCallback( async (message: PromptInputMessage) => { let text = message.text.trim(); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/components/ChatMastraMessageList/ChatMastraMessageList.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/components/ChatMastraMessageList/ChatMastraMessageList.tsx index 4ea4e7694e0..5a997239255 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/components/ChatMastraMessageList/ChatMastraMessageList.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/components/ChatMastraMessageList/ChatMastraMessageList.tsx @@ -7,24 +7,20 @@ import { } from "@superset/ui/ai-elements/conversation"; import { Message, MessageContent } from "@superset/ui/ai-elements/message"; import { ShimmerLabel } from "@superset/ui/ai-elements/shimmer-label"; -import { - Tool, - ToolContent, - type ToolDisplayState, - ToolHeader, - ToolInput, - ToolOutput, -} from "@superset/ui/ai-elements/tool"; import { FileSearchIcon } from "lucide-react"; import type { ReactNode } from "react"; import { HiMiniChatBubbleLeftRight } from "react-icons/hi2"; +import { MastraToolCallBlock } from "../../../../ChatPane/ChatInterface/components/MastraToolCallBlock"; import { StreamingMessageText } from "../../../../ChatPane/ChatInterface/components/MessagePartsRenderer/components/StreamingMessageText"; import { ReasoningBlock } from "../../../../ChatPane/ChatInterface/components/ReasoningBlock"; +import type { ToolPart } from "../../../../ChatPane/ChatInterface/utils/tool-helpers"; +import { normalizeToolName } from "../../../../ChatPane/ChatInterface/utils/tool-helpers"; type MastraMessage = NonNullable< UseMastraChatDisplayReturn["messages"] >[number]; type MastraMessageContent = MastraMessage["content"][number]; +type MastraToolCall = Extract; type MastraToolResult = Extract; interface ChatMastraMessageListProps { @@ -61,21 +57,38 @@ function findToolResultForCall({ return { result: null, index: -1 }; } -function toToolDisplayState({ +function toToolPartFromCall({ + part, result, isStreaming, }: { + part: MastraToolCall; result: MastraToolResult | null; isStreaming: boolean; -}): ToolDisplayState { - if (result?.isError) return "output-error"; - if (result) return "output-available"; - if (isStreaming) return "input-streaming"; - return "input-available"; +}): ToolPart { + return { + type: `tool-${normalizeToolName(part.name)}` as ToolPart["type"], + toolCallId: part.id, + state: result?.isError + ? "output-error" + : result + ? "output-available" + : isStreaming + ? "input-streaming" + : "input-available", + input: part.args, + ...(result ? { output: result.result } : {}), + } as ToolPart; } -function getToolErrorText(result: unknown): string { - return typeof result === "string" ? result : JSON.stringify(result, null, 2); +function toToolPartFromResult(part: MastraToolResult): ToolPart { + return { + type: `tool-${normalizeToolName(part.name)}` as ToolPart["type"], + toolCallId: part.id, + state: part.isError ? "output-error" : "output-available", + input: {}, + output: part.result, + } as ToolPart; } function UserMessage({ message }: { message: MastraMessage }) { @@ -161,26 +174,16 @@ function AssistantMessage({ toolCallId: part.id, startAt: partIndex + 1, }); - const state = toToolDisplayState({ - result, - isStreaming, - }); - const errorText = - result?.isError === true ? getToolErrorText(result.result) : undefined; nodes.push( - - - - - {result ? ( - - ) : null} - - , + , ); // If next sibling is the matched result, skip it. @@ -192,20 +195,10 @@ function AssistantMessage({ if (part.type === "tool_result") { nodes.push( - - - - - - , + , ); continue; } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/types.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/types.ts index 92ac537113b..7d75dae0c2c 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/types.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraInterface/types.ts @@ -1,3 +1,13 @@ +import type { UseMastraChatDisplayReturn } from "@superset/chat-mastra/client"; + +export interface ChatMastraRawSnapshot { + sessionId: string | null; + isRunning: boolean; + currentMessage: UseMastraChatDisplayReturn["currentMessage"] | null; + messages: UseMastraChatDisplayReturn["messages"]; + error: unknown; +} + export interface ChatMastraInterfaceProps { sessionId: string | null; workspaceId: string; @@ -6,4 +16,5 @@ export interface ChatMastraInterfaceProps { created: boolean; errorMessage?: string; }>; + onRawSnapshotChange?: (snapshot: ChatMastraRawSnapshot) => void; } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraPane.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraPane.tsx index 0b9da7fe8aa..a401fc61e7e 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraPane.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraPane.tsx @@ -1,9 +1,11 @@ import { ChatServiceProvider } from "@superset/chat/client"; import { ChatMastraServiceProvider } from "@superset/chat-mastra/client"; import { toast } from "@superset/ui/sonner"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { eq } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; -import { useCallback, useEffect, useMemo, useRef } from "react"; +import { CopyIcon } from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { MosaicBranch } from "react-mosaic-component"; import { env } from "renderer/env.renderer"; import { apiTrpcClient } from "renderer/lib/api-trpc-client"; @@ -15,6 +17,7 @@ import { useTabsStore } from "renderer/stores/tabs/store"; import { createChatServiceIpcClient } from "../ChatPane/utils/chat-service-client"; import { BasePaneWindow, PaneToolbarActions } from "../components"; import { ChatMastraInterface } from "./ChatMastraInterface"; +import type { ChatMastraRawSnapshot } from "./ChatMastraInterface/types"; import { SessionSelector } from "./components/SessionSelector"; import { createChatMastraServiceIpcClient } from "./utils/chat-mastra-service-client"; import { reportChatMastraError } from "./utils/reportChatMastraError"; @@ -124,6 +127,11 @@ export function ChatMastraPane({ const collections = useCollections(); const ensureSessionRef = useRef(false); const ensuredRef = useRef(null); + const rawSnapshotRef = useRef(null); + const [rawSnapshotSessionId, setRawSnapshotSessionId] = useState< + string | null + >(null); + const showDevToolbarActions = env.NODE_ENV === "development"; const { data: workspace } = electronTrpc.workspaces.get.useQuery( { id: workspaceId }, @@ -304,6 +312,38 @@ export function ChatMastraPane({ [sessions], ); + const handleRawSnapshotChange = useCallback( + (snapshot: ChatMastraRawSnapshot) => { + rawSnapshotRef.current = snapshot; + setRawSnapshotSessionId((previousSessionId) => + previousSessionId === snapshot.sessionId + ? previousSessionId + : snapshot.sessionId, + ); + }, + [], + ); + + const handleCopyRawSnapshot = useCallback(async () => { + const rawSnapshot = rawSnapshotRef.current; + if (!rawSnapshot || rawSnapshot.sessionId !== sessionId) { + toast.error("No raw chat data to copy yet"); + return; + } + + if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) { + toast.error("Clipboard API is unavailable"); + return; + } + + try { + await navigator.clipboard.writeText(JSON.stringify(rawSnapshot, null, 2)); + toast.success("Copied raw chat JSON"); + } catch { + toast.error("Failed to copy raw chat JSON"); + } + }, [sessionId]); + return ( + + + + + Copy raw chat JSON (dev) + + + ) : null + } closeHotkeyId="CLOSE_TERMINAL" /> @@ -345,6 +409,9 @@ export function ChatMastraPane({ workspaceId={workspaceId} cwd={workspace?.worktreePath ?? ""} onStartFreshSession={handleStartFreshSession} + onRawSnapshotChange={ + showDevToolbarActions ? handleRawSnapshotChange : undefined + } /> diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/MastraToolCallBlock.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/MastraToolCallBlock.tsx index 71f2101e3f3..423902b3d02 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/MastraToolCallBlock.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/MastraToolCallBlock.tsx @@ -1,6 +1,5 @@ import { BashTool } from "@superset/ui/ai-elements/bash-tool"; import { FileDiffTool } from "@superset/ui/ai-elements/file-diff-tool"; -import { ToolCall } from "@superset/ui/ai-elements/tool-call"; import { UserQuestionTool } from "@superset/ui/ai-elements/user-question-tool"; import { WebFetchTool } from "@superset/ui/ai-elements/web-fetch-tool"; import { WebSearchTool } from "@superset/ui/ai-elements/web-search-tool"; @@ -8,7 +7,12 @@ import { getToolName } from "ai"; import { FileIcon, FolderIcon, MessageCircleQuestionIcon } from "lucide-react"; import { READ_ONLY_TOOLS } from "../../constants"; import type { ToolPart } from "../../utils/tool-helpers"; -import { getArgs, getResult, toWsToolState } from "../../utils/tool-helpers"; +import { + getArgs, + getResult, + normalizeToolName, + toWsToolState, +} from "../../utils/tool-helpers"; import { ReadOnlyToolCall } from "../ReadOnlyToolCall"; import { GenericToolCall } from "./components/GenericToolCall"; @@ -24,24 +28,116 @@ export function MastraToolCallBlock({ const args = getArgs(part); const result = getResult(part); const state = toWsToolState(part); - const toolName = getToolName(part); + const toolName = normalizeToolName(getToolName(part)); + const toolDisplayName = toolName + .replace("mastra_workspace_", "") + .replaceAll("_", " "); + + const outputObject = + typeof result.output === "object" && result.output !== null + ? (result.output as Record) + : undefined; + const nestedResultObject = + typeof result.result === "object" && result.result !== null + ? (result.result as Record) + : undefined; + + const toText = (value: unknown): string | undefined => { + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + if (Array.isArray(value)) { + const parts = value + .map((item) => (typeof item === "string" ? item : String(item))) + .filter(Boolean); + return parts.length > 0 ? parts.join("\n") : undefined; + } + return undefined; + }; + + const firstText = (...values: unknown[]): string | undefined => { + for (const value of values) { + const text = toText(value); + if (text && text.trim().length > 0) return text; + } + return undefined; + }; + + const toNumber = (value: unknown): number | undefined => { + if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "string" && value.trim().length > 0) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; + } + return undefined; + }; // --- Execute command → BashTool --- if (toolName === "mastra_workspace_execute_command") { - const command = String(args.command ?? args.cmd ?? ""); + const command = String( + args.command ?? + args.cmd ?? + args.command_line ?? + args.commandLine ?? + args.raw ?? + "", + ); const stdout = - result.stdout != null - ? String(result.stdout) - : result.output != null - ? typeof result.output === "string" - ? result.output - : JSON.stringify(result.output, null, 2) - : result.text != null - ? String(result.text) - : undefined; - const stderr = result.stderr != null ? String(result.stderr) : undefined; - const exitCode = - result.exitCode != null ? Number(result.exitCode) : undefined; + firstText( + result.stdout, + result.stdout_text, + result.stdoutText, + outputObject?.stdout, + outputObject?.stdout_text, + outputObject?.stdoutText, + nestedResultObject?.stdout, + nestedResultObject?.stdout_text, + nestedResultObject?.stdoutText, + result.combined_output, + result.combinedOutput, + outputObject?.combined_output, + outputObject?.combinedOutput, + result.output_text, + result.outputText, + result.text, + typeof result.output === "string" ? result.output : undefined, + typeof result.result === "string" ? result.result : undefined, + ) ?? + (typeof result.output === "object" && result.output !== null + ? JSON.stringify(result.output, null, 2) + : undefined); + const stderr = firstText( + result.stderr, + result.stderr_text, + result.stderrText, + outputObject?.stderr, + outputObject?.stderr_text, + outputObject?.stderrText, + nestedResultObject?.stderr, + nestedResultObject?.stderr_text, + nestedResultObject?.stderrText, + result.error, + result.errorText, + outputObject?.error, + outputObject?.errorText, + nestedResultObject?.error, + nestedResultObject?.errorText, + ); + const exitCode = toNumber( + result.exitCode ?? + result.exit_code ?? + result.code ?? + result.status_code ?? + outputObject?.exitCode ?? + outputObject?.exit_code ?? + outputObject?.code ?? + outputObject?.status_code ?? + nestedResultObject?.exitCode ?? + nestedResultObject?.exit_code ?? + nestedResultObject?.code ?? + nestedResultObject?.status_code, + ); return ( | undefined; return ( - ); } @@ -147,48 +237,28 @@ export function MastraToolCallBlock({ ); } - // --- Read-only exploration tools → compact ToolCall --- + // --- Read-only exploration tools --- if (READ_ONLY_TOOLS.has(toolName)) { return ; } - // --- Destructive workspace tools → compact ToolCall --- + // --- Destructive workspace tools --- if (toolName === "mastra_workspace_mkdir") { - const isPending = - part.state !== "output-available" && part.state !== "output-error"; - const subtitle = String(args.path ?? ""); - const shortName = subtitle.includes("/") - ? (subtitle.split("/").pop() ?? subtitle) - : subtitle; return ( - ); } if (toolName === "mastra_workspace_delete") { - const isPending = - part.state !== "output-available" && part.state !== "output-error"; - const subtitle = String(args.path ?? ""); - const shortName = subtitle.includes("/") - ? (subtitle.split("/").pop() ?? subtitle) - : subtitle; return ( - + ); } // --- Fallback: generic tool UI --- - return ; + return ; } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/components/GenericToolCall/GenericToolCall.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/components/GenericToolCall/GenericToolCall.tsx index 2d6f67af5be..037f9e76077 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/components/GenericToolCall/GenericToolCall.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/components/GenericToolCall/GenericToolCall.tsx @@ -1,34 +1,83 @@ +import { ShimmerLabel } from "@superset/ui/ai-elements/shimmer-label"; +import { ToolInput, ToolOutput } from "@superset/ui/ai-elements/tool"; import { - Tool, - ToolContent, - ToolHeader, - ToolInput, - ToolOutput, -} from "@superset/ui/ai-elements/tool"; + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@superset/ui/collapsible"; +import { CheckIcon, Loader2Icon, WrenchIcon, XIcon } from "lucide-react"; +import type { ComponentType } from "react"; +import { useState } from "react"; import type { ToolPart } from "../../../../utils/tool-helpers"; import { getGenericToolCallState } from "./getGenericToolCallState"; type GenericToolCallProps = { part: ToolPart; toolName: string; + icon?: ComponentType<{ className?: string }>; }; -export function GenericToolCall({ part, toolName }: GenericToolCallProps) { +export function GenericToolCall({ + part, + toolName, + icon: Icon = WrenchIcon, +}: GenericToolCallProps) { + const [isOpen, setIsOpen] = useState(false); const { output, isError, displayState, errorText } = getGenericToolCallState(part); + const isPending = + part.state !== "output-available" && part.state !== "output-error"; + const hasDetails = part.input != null || output != null || isError; return ( - - - - {part.input != null && } - {(output != null || isError) && ( - - )} - - + hasDetails && setIsOpen(open)} + open={hasDetails ? isOpen : false} + > + + + + {hasDetails && ( + +
+ {part.input != null && } + {(output != null || isError) && ( + + )} +
+
+ )} +
); } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/MessagePartsRenderer.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/MessagePartsRenderer.tsx index 4d4e8884066..7c8071540fa 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/MessagePartsRenderer.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/MessagePartsRenderer.tsx @@ -15,7 +15,7 @@ import { useTheme } from "renderer/stores"; import { useTabsStore } from "renderer/stores/tabs/store"; import { READ_ONLY_TOOLS } from "../../constants"; import type { ToolPart } from "../../utils/tool-helpers"; -import { getArgs } from "../../utils/tool-helpers"; +import { getArgs, normalizeToolName } from "../../utils/tool-helpers"; import { MastraToolCallBlock } from "../MastraToolCallBlock"; import { ReadOnlyToolCall } from "../ReadOnlyToolCall"; import { ReasoningBlock } from "../ReasoningBlock"; @@ -130,7 +130,7 @@ export function MessagePartsRenderer({ } if (isToolUIPart(part)) { - const toolName = getToolName(part); + const toolName = normalizeToolName(getToolName(part)); // Group consecutive read-only tools into ExploringGroup if (READ_ONLY_TOOLS.has(toolName)) { @@ -139,7 +139,9 @@ export function MessagePartsRenderer({ while ( i < parts.length && isToolUIPart(parts[i]) && - READ_ONLY_TOOLS.has(getToolName(parts[i] as ToolPart)) + READ_ONLY_TOOLS.has( + normalizeToolName(getToolName(parts[i] as ToolPart)), + ) ) { groupParts.push(parts[i] as ToolPart); i++; @@ -162,7 +164,7 @@ export function MessagePartsRenderer({ ); const exploringItems = groupParts.map((p) => { const args = getArgs(p); - const name = getToolName(p); + const name = normalizeToolName(getToolName(p)); let title = "Read"; let subtitle = ""; let icon = FileIcon; @@ -172,7 +174,13 @@ export function MessagePartsRenderer({ p.state !== "output-available" && p.state !== "output-error" ? "Reading" : "Read"; - subtitle = String(args.path ?? args.filePath ?? ""); + subtitle = String( + args.path ?? + args.filePath ?? + args.file_path ?? + args.file ?? + "", + ); icon = FileIcon; break; case "mastra_workspace_list_files": @@ -180,7 +188,15 @@ export function MessagePartsRenderer({ p.state !== "output-available" && p.state !== "output-error" ? "Listing" : "Listed"; - subtitle = String(args.path ?? args.directory ?? ""); + subtitle = String( + args.path ?? + args.directory ?? + args.directoryPath ?? + args.directory_path ?? + args.root ?? + args.cwd ?? + "", + ); icon = FolderTreeIcon; break; case "mastra_workspace_file_stat": @@ -188,7 +204,9 @@ export function MessagePartsRenderer({ p.state !== "output-available" && p.state !== "output-error" ? "Checking" : "Checked"; - subtitle = String(args.path ?? ""); + subtitle = String( + args.path ?? args.file_path ?? args.file ?? "", + ); icon = FileSearchIcon; break; case "mastra_workspace_search": @@ -196,7 +214,14 @@ export function MessagePartsRenderer({ p.state !== "output-available" && p.state !== "output-error" ? "Searching" : "Searched"; - subtitle = String(args.query ?? args.pattern ?? ""); + subtitle = String( + args.query ?? + args.pattern ?? + args.regex ?? + args.substring_pattern ?? + args.text ?? + "", + ); icon = SearchIcon; break; case "mastra_workspace_index": diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ReadOnlyToolCall/ReadOnlyToolCall.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ReadOnlyToolCall/ReadOnlyToolCall.tsx index f6f51ebcb26..bc97f4cbedf 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ReadOnlyToolCall/ReadOnlyToolCall.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ReadOnlyToolCall/ReadOnlyToolCall.tsx @@ -1,48 +1,98 @@ -import { ToolCall } from "@superset/ui/ai-elements/tool-call"; +import { ShimmerLabel } from "@superset/ui/ai-elements/shimmer-label"; +import { ToolInput, ToolOutput } from "@superset/ui/ai-elements/tool"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@superset/ui/collapsible"; import { getToolName } from "ai"; import { + CheckIcon, FileIcon, FileSearchIcon, FolderTreeIcon, + Loader2Icon, SearchIcon, + XIcon, } from "lucide-react"; +import { useState } from "react"; import type { ToolPart } from "../../utils/tool-helpers"; -import { getArgs } from "../../utils/tool-helpers"; +import { + getArgs, + normalizeToolName, + toToolDisplayState, +} from "../../utils/tool-helpers"; + +function stringify(value: unknown): string { + if (typeof value === "string") return value; + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} export function ReadOnlyToolCall({ part }: { part: ToolPart }) { + const [isOpen, setIsOpen] = useState(false); const args = getArgs(part); - const toolName = getToolName(part); + const toolName = normalizeToolName(getToolName(part)); + const output = + "output" in part ? (part as { output?: unknown }).output : undefined; + const outputError = + output != null && typeof output === "object" + ? (output as Record).error + : undefined; + const isError = part.state === "output-error" || outputError !== undefined; const isPending = part.state !== "output-available" && part.state !== "output-error"; + const displayState = toToolDisplayState(part); + const hasDetails = part.input != null || output != null || isError; let title = "Read file"; let subtitle = String(args.path ?? args.filePath ?? args.query ?? ""); - let icon = FileIcon; + let Icon = FileIcon; switch (toolName) { case "mastra_workspace_read_file": title = isPending ? "Reading" : "Read"; - subtitle = String(args.path ?? args.filePath ?? ""); - icon = FileIcon; + subtitle = String( + args.path ?? args.filePath ?? args.file_path ?? args.file ?? "", + ); + Icon = FileIcon; break; case "mastra_workspace_list_files": title = isPending ? "Listing files" : "Listed files"; - subtitle = String(args.path ?? args.directory ?? ""); - icon = FolderTreeIcon; + subtitle = String( + args.path ?? + args.directory ?? + args.directoryPath ?? + args.directory_path ?? + args.root ?? + args.cwd ?? + "", + ); + Icon = FolderTreeIcon; break; case "mastra_workspace_file_stat": - title = isPending ? "Checking" : "Checked"; - subtitle = String(args.path ?? ""); - icon = FileSearchIcon; + title = "Check file"; + subtitle = String(args.path ?? args.file_path ?? args.file ?? ""); + Icon = FileSearchIcon; break; case "mastra_workspace_search": - title = isPending ? "Searching" : "Searched"; - subtitle = String(args.query ?? args.pattern ?? ""); - icon = SearchIcon; + title = "Search"; + subtitle = String( + args.query ?? + args.pattern ?? + args.regex ?? + args.substring_pattern ?? + args.text ?? + "", + ); + Icon = SearchIcon; break; case "mastra_workspace_index": - title = isPending ? "Indexing" : "Indexed"; - icon = SearchIcon; + title = "Index"; + Icon = SearchIcon; break; } @@ -52,12 +102,56 @@ export function ReadOnlyToolCall({ part }: { part: ToolPart }) { } return ( - + hasDetails && setIsOpen(open)} + open={hasDetails ? isOpen : false} + > + + + + {hasDetails && ( + +
+ {part.input != null && } + {(output != null || isError) && ( + + )} +
+
+ )} +
); } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/utils/tool-helpers.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/utils/tool-helpers.ts index db6975739f1..41af5246a34 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/utils/tool-helpers.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/utils/tool-helpers.ts @@ -6,6 +6,28 @@ type ToolPart = Extract; export type { ToolPart }; +const TOOL_NAME_ALIASES: Record = { + execute_command: "mastra_workspace_execute_command", + run_command: "mastra_workspace_execute_command", + run_terminal_cmd: "mastra_workspace_execute_command", + write_file: "mastra_workspace_write_file", + edit_file: "mastra_workspace_edit_file", + read_file: "mastra_workspace_read_file", + view: "mastra_workspace_read_file", + list_files: "mastra_workspace_list_files", + find_files: "mastra_workspace_list_files", + file_stat: "mastra_workspace_file_stat", + search: "mastra_workspace_search", + search_content: "mastra_workspace_search", + index: "mastra_workspace_index", + mkdir: "mastra_workspace_mkdir", + delete: "mastra_workspace_delete", +}; + +export function normalizeToolName(toolName: string): string { + return TOOL_NAME_ALIASES[toolName] ?? toolName; +} + export function toToolDisplayState(part: ToolPart): ToolDisplayState { switch (part.state) { case "input-streaming": diff --git a/packages/ui/src/components/ai-elements/bash-tool.tsx b/packages/ui/src/components/ai-elements/bash-tool.tsx index 2accf441247..ec73e1d2d83 100644 --- a/packages/ui/src/components/ai-elements/bash-tool.tsx +++ b/packages/ui/src/components/ai-elements/bash-tool.tsx @@ -61,10 +61,7 @@ export const BashTool = ({ return ( hasOutput && setIsOutputExpanded(open)} open={hasOutput ? isOutputExpanded : false} > @@ -73,7 +70,7 @@ export const BashTool = ({ className={cn( "flex h-7 w-full items-center justify-between px-2.5 text-left", hasOutput - ? "cursor-pointer transition-colors duration-150 hover:bg-muted/50" + ? "cursor-pointer transition-colors duration-150 hover:bg-muted/30" : "cursor-default", )} disabled={!hasOutput} @@ -94,26 +91,14 @@ export const BashTool = ({ {/* Status */} -
- {!isPending && ( -
- {isSuccess && ( - <> - - Success - - )} - {isError && ( - <> - - Failed - - )} -
- )} -
- {isPending && } -
+
+ {isPending ? ( + + ) : isError ? ( + + ) : isSuccess ? ( + + ) : null}
@@ -121,10 +106,10 @@ export const BashTool = ({ {hasOutput && ( -
+
{/* Command */} {command && (