Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function ChatMastraInterface({
workspaceId: _workspaceId,
cwd,
onStartFreshSession,
onRawSnapshotChange,
}: ChatMastraInterfaceProps) {
const { models: availableModels, defaultModel } = useAvailableModels();
const [selectedModel, setSelectedModel] = useState<ModelOption | null>(null);
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MastraMessageContent, { type: "tool_call" }>;
type MastraToolResult = Extract<MastraMessageContent, { type: "tool_result" }>;

interface ChatMastraMessageListProps {
Expand Down Expand Up @@ -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 }) {
Expand Down Expand Up @@ -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(
<Tool key={`${message.id}-tool-${part.id}`}>
<ToolHeader title={part.name} state={state} />
<ToolContent>
<ToolInput input={part.args} />
{result ? (
<ToolOutput
output={result.isError ? undefined : result.result}
errorText={errorText}
/>
) : null}
</ToolContent>
</Tool>,
<MastraToolCallBlock
key={`${message.id}-tool-${part.id}`}
part={toToolPartFromCall({
part,
result,
isStreaming,
})}
/>,
);

// If next sibling is the matched result, skip it.
Expand All @@ -192,20 +195,10 @@ function AssistantMessage({

if (part.type === "tool_result") {
nodes.push(
<Tool key={`${message.id}-tool-result-${part.id}`}>
<ToolHeader
title={part.name}
state={part.isError ? "output-error" : "output-available"}
/>
<ToolContent>
<ToolOutput
output={part.isError ? undefined : part.result}
errorText={
part.isError ? getToolErrorText(part.result) : undefined
}
/>
</ToolContent>
</Tool>,
<MastraToolCallBlock
key={`${message.id}-tool-result-${part.id}`}
part={toToolPartFromResult(part)}
/>,
);
continue;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -6,4 +16,5 @@ export interface ChatMastraInterfaceProps {
created: boolean;
errorMessage?: string;
}>;
onRawSnapshotChange?: (snapshot: ChatMastraRawSnapshot) => void;
}
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -124,6 +127,11 @@ export function ChatMastraPane({
const collections = useCollections();
const ensureSessionRef = useRef(false);
const ensuredRef = useRef<string | null>(null);
const rawSnapshotRef = useRef<ChatMastraRawSnapshot | null>(null);
const [rawSnapshotSessionId, setRawSnapshotSessionId] = useState<
string | null
>(null);
const showDevToolbarActions = env.NODE_ENV === "development";

const { data: workspace } = electronTrpc.workspaces.get.useQuery(
{ id: workspaceId },
Expand Down Expand Up @@ -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 (
<ChatMastraServiceProvider
client={mastraIpcClient}
Expand Down Expand Up @@ -335,6 +375,30 @@ export function ChatMastraPane({
splitOrientation={handlers.splitOrientation}
onSplitPane={handlers.onSplitPane}
onClosePane={handlers.onClosePane}
leadingActions={
showDevToolbarActions ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => {
void handleCopyRawSnapshot();
}}
disabled={
!rawSnapshotRef.current ||
rawSnapshotSessionId !== sessionId
}
className="rounded p-0.5 text-muted-foreground/60 transition-colors hover:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-40"
>
<CopyIcon className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" showArrow={false}>
Copy raw chat JSON (dev)
</TooltipContent>
</Tooltip>
) : null
}
closeHotkeyId="CLOSE_TERMINAL"
/>
</div>
Expand All @@ -345,6 +409,9 @@ export function ChatMastraPane({
workspaceId={workspaceId}
cwd={workspace?.worktreePath ?? ""}
onStartFreshSession={handleStartFreshSession}
onRawSnapshotChange={
showDevToolbarActions ? handleRawSnapshotChange : undefined
}
/>
</BasePaneWindow>
</ChatServiceProvider>
Expand Down
Loading
Loading