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
@@ -1,12 +1,5 @@
import { BashTool } from "@superset/ui/ai-elements/bash-tool";
import { FileDiffTool } from "@superset/ui/ai-elements/file-diff-tool";
import {
Tool,
ToolContent,
ToolHeader,
ToolInput,
ToolOutput,
} from "@superset/ui/ai-elements/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";
Expand All @@ -15,13 +8,9 @@ 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,
toToolDisplayState,
toWsToolState,
} from "../../utils/tool-helpers";
import { getArgs, getResult, toWsToolState } from "../../utils/tool-helpers";
import { ReadOnlyToolCall } from "../ReadOnlyToolCall";
import { GenericToolCall } from "./components/GenericToolCall";

interface MastraToolCallBlockProps {
part: ToolPart;
Expand All @@ -40,7 +29,16 @@ export function MastraToolCallBlock({
// --- Execute command → BashTool ---
if (toolName === "mastra_workspace_execute_command") {
const command = String(args.command ?? args.cmd ?? "");
const stdout = result.stdout != null ? String(result.stdout) : undefined;
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;
Expand Down Expand Up @@ -192,28 +190,5 @@ export function MastraToolCallBlock({
}

// --- Fallback: generic tool UI ---
const output =
"output" in part ? (part as { output: unknown }).output : undefined;
const isError = part.state === "output-error";

return (
<Tool>
<ToolHeader title={toolName} state={toToolDisplayState(part)} />
<ToolContent>
{part.input != null && <ToolInput input={part.input} />}
{(output != null || isError) && (
<ToolOutput
output={!isError ? output : undefined}
errorText={
isError
? typeof output === "string"
? output
: JSON.stringify(output)
: undefined
}
/>
)}
</ToolContent>
</Tool>
);
return <GenericToolCall part={part} toolName={toolName} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
Tool,
ToolContent,
ToolHeader,
ToolInput,
ToolOutput,
} from "@superset/ui/ai-elements/tool";
import type { ToolPart } from "../../../../utils/tool-helpers";
import { getGenericToolCallState } from "./getGenericToolCallState";

type GenericToolCallProps = {
part: ToolPart;
toolName: string;
};

export function GenericToolCall({ part, toolName }: GenericToolCallProps) {
const { output, isError, displayState, errorText } =
getGenericToolCallState(part);

return (
<Tool>
<ToolHeader title={toolName} state={displayState} />
<ToolContent>
{part.input != null && <ToolInput input={part.input} />}
{(output != null || isError) && (
<ToolOutput
output={!isError ? output : undefined}
errorText={isError ? errorText : undefined}
/>
)}
</ToolContent>
</Tool>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { ToolDisplayState } from "@superset/ui/ai-elements/tool";
import type { ToolPart } from "../../../../utils/tool-helpers";
import { toToolDisplayState } from "../../../../utils/tool-helpers";

export type GenericToolCallState = {
output: unknown;
isError: boolean;
displayState: ToolDisplayState;
errorText?: string;
};

function stringifyValue(value: unknown): string {
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}

export function getGenericToolCallState(part: ToolPart): GenericToolCallState {
const output =
"output" in part ? (part as { output: unknown }).output : undefined;
const outputObject =
output != null && typeof output === "object"
? (output as Record<string, unknown>)
: undefined;
const outputError = outputObject?.error;
const isOutputError =
outputObject != null && "error" in outputObject && Boolean(outputError);
const isError = part.state === "output-error" || isOutputError;

const baseDisplayState = toToolDisplayState(part);
const displayState =
isOutputError && baseDisplayState === "output-available"
? "output-error"
: baseDisplayState;

let errorText: string | undefined;
if (isError) {
if (typeof output === "string") {
errorText = output;
} else if (typeof outputError === "string") {
errorText = outputError;
} else if (typeof outputObject?.message === "string") {
errorText = outputObject.message;
} else if (outputError !== undefined) {
errorText = stringifyValue(outputError);
}
}

return {
output,
isError,
displayState,
errorText,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { GenericToolCall } from "./GenericToolCall";
export { getGenericToolCallState } from "./getGenericToolCallState";
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
ConversationScrollButton,
} from "@superset/ui/ai-elements/conversation";
import { Message, MessageContent } from "@superset/ui/ai-elements/message";
import { Shimmer } from "@superset/ui/ai-elements/shimmer";
import { ShimmerLabel } from "@superset/ui/ai-elements/shimmer-label";
import type { ChatStatus, UIMessage } from "ai";
import { FileIcon, FileTextIcon, ImageIcon } from "lucide-react";
import { useCallback } from "react";
Expand Down Expand Up @@ -77,6 +77,8 @@ export function MessageList({
messages.map((msg, index) => {
const isLastAssistant =
msg.role === "assistant" && index === messages.length - 1;
const shouldAnimateStreaming =
isLastAssistant && (isStreaming || submitStatus === "submitted");

if (msg.role === "user") {
const textContent = msg.parts
Expand Down Expand Up @@ -144,17 +146,14 @@ export function MessageList({
<Message key={msg.id} from={msg.role}>
<MessageContent>
{isLastAssistant && isThinking && msg.parts.length === 0 ? (
<Shimmer
className="text-sm text-muted-foreground"
duration={1}
>
<ShimmerLabel className="text-sm text-muted-foreground">
Thinking...
</Shimmer>
</ShimmerLabel>
) : (
<MessagePartsRenderer
parts={msg.parts}
isLastAssistant={isLastAssistant}
isStreaming={isStreaming}
isStreaming={shouldAnimateStreaming}
workspaceId={workspaceId}
onAnswer={onAnswer}
/>
Expand All @@ -167,9 +166,9 @@ export function MessageList({
{isThinking && messages[messages.length - 1]?.role === "user" && (
<Message from="assistant">
<MessageContent>
<Shimmer className="text-sm text-muted-foreground" duration={1}>
<ShimmerLabel className="text-sm text-muted-foreground">
Thinking...
</Shimmer>
</ShimmerLabel>
</MessageContent>
</Message>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ExploringGroup } from "@superset/ui/ai-elements/exploring-group";
import { MessageResponse } from "@superset/ui/ai-elements/message";
import type { UIMessage } from "ai";
import { getToolName, isToolUIPart } from "ai";
import {
Expand All @@ -20,6 +19,7 @@ import { getArgs } from "../../utils/tool-helpers";
import { MastraToolCallBlock } from "../MastraToolCallBlock";
import { ReadOnlyToolCall } from "../ReadOnlyToolCall";
import { ReasoningBlock } from "../ReasoningBlock";
import { StreamingMessageText } from "./components/StreamingMessageText";

interface MessagePartsRendererProps {
parts: UIMessage["parts"];
Expand Down Expand Up @@ -96,14 +96,13 @@ export function MessagePartsRenderer({

if (part.type === "text") {
nodes.push(
<MessageResponse
<StreamingMessageText
key={i}
text={part.text}
isAnimating={isLastAssistant && isStreaming}
mermaid={mermaidConfig}
components={components}
>
{part.text}
</MessageResponse>,
/>,
);
i++;
continue;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
MessageResponse,
type MessageResponseProps,
} from "@superset/ui/ai-elements/message";
import { useEffect, useState } from "react";

const STREAM_TEXT_TICK_MS = 16;
const STREAM_TEXT_CHARS_PER_TICK = 2;

interface StreamingMessageTextProps {
text: string;
isAnimating: boolean;
mermaid: MessageResponseProps["mermaid"];
components?: MessageResponseProps["components"];
}

export function StreamingMessageText({
text,
isAnimating,
mermaid,
components,
}: StreamingMessageTextProps) {
const [displayText, setDisplayText] = useState(text);

useEffect(() => {
if (!isAnimating) {
setDisplayText(text);
return;
}

setDisplayText((previous) => (text.startsWith(previous) ? previous : text));

const intervalId = window.setInterval(() => {
setDisplayText((previous) => {
if (previous.length >= text.length) return previous;
const nextLength = Math.min(
text.length,
previous.length + STREAM_TEXT_CHARS_PER_TICK,
);
return text.slice(0, nextLength);
});
}, STREAM_TEXT_TICK_MS);

return () => window.clearInterval(intervalId);
}, [text, isAnimating]);

return (
<MessageResponse
animated={false}
isAnimating={isAnimating}
mermaid={mermaid}
components={components}
>
{displayText}
</MessageResponse>
Comment on lines +47 to +55
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the MessageResponse component definition
fd -t f "MessageResponse" --extension tsx --extension ts

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Search for MessageResponse component definition in the codebase
rg "export.*MessageResponse|function MessageResponse|const MessageResponse" -t ts -t tsx

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

# Look for the streamdownPlugins usage pattern
rg "streamdownPlugins" -t ts -t tsx -A 2 -B 2

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

# Find the StreamingMessageText.tsx file to understand context
fd "StreamingMessageText.tsx" -t f

Repository: superset-sh/superset

Length of output: 271


🏁 Script executed:

# Search for MessageResponse without file type
rg "export.*MessageResponse|function MessageResponse|const MessageResponse" -A 3

Repository: superset-sh/superset

Length of output: 618


🏁 Script executed:

# Search for streamdownPlugins
rg "streamdownPlugins" -A 3 -B 3

Repository: superset-sh/superset

Length of output: 1066


🏁 Script executed:

# Read the StreamingMessageText file to understand context
cat apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/components/StreamingMessageText/StreamingMessageText.tsx

Repository: superset-sh/superset

Length of output: 1348


🏁 Script executed:

# Search for MessageResponse file location
fd "MessageResponse" -t f

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Read the full MessageResponse component to understand the complete behavior
cat packages/ui/src/components/ai-elements/message.tsx

Repository: superset-sh/superset

Length of output: 10689


🏁 Script executed:

# Search for Streamdown component to understand how it handles partial markdown
rg "Streamdown" -B 5 -A 10 | head -100

Repository: superset-sh/superset

Length of output: 8855


🏁 Script executed:

# Look for any test files that might show expected behavior
fd "StreamingMessageText.test" -o fd "message.test" -o fd "streamdown" -i

Repository: superset-sh/superset

Length of output: 187


🏁 Script executed:

# Search for comments or documentation about why plugins are disabled during isAnimating
rg "isAnimating.*plugins|plugins.*isAnimating" -B 3 -A 3

Repository: superset-sh/superset

Length of output: 555


🏁 Script executed:

# Look for any issues or errors related to partial markdown rendering
rg "partial.*markdown|incomplete.*markdown|broken.*fence" -i

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check if there are git history comments about this logic
git log --oneline packages/ui/src/components/ai-elements/message.tsx | head -20

Repository: superset-sh/superset

Length of output: 79


🏁 Script executed:

# Search for mermaid plugin usage context
rg "@streamdown/mermaid" -B 3 -A 3

Repository: superset-sh/superset

Length of output: 3197


🏁 Script executed:

# Search for any other uses of StreamingMessageText to understand the pattern
rg "StreamingMessageText" -B 3 -A 3

Repository: superset-sh/superset

Length of output: 7650


🏁 Script executed:

# Check the Streamdown package docs or types to understand mode="streaming"
rg "mode.*streaming|streaming.*mode" -B 2 -A 2

Repository: superset-sh/superset

Length of output: 901


🏁 Script executed:

# Look for any error handling or edge cases with markdown plugins
rg "mermaid|Streamdown" apps/desktop/src/renderer/components/MarkdownRenderer -B 2 -A 5

Repository: superset-sh/superset

Length of output: 3681


🏁 Script executed:

# Check if there are any related issues or comments about plugin behavior during animation
rg "animated.*plugin|plugin.*animated|isAnimating" -B 2 -A 2

Repository: superset-sh/superset

Length of output: 8234


Pass isAnimating prop through to MessageResponse.

MessageResponse disables plugins during animation with plugins={isAnimating ? undefined : streamdownPlugins} to safely handle partial markdown. By hardcoding isAnimating={false}, the mermaid plugin runs on incomplete markdown during the character-by-character animation, potentially causing rendering artifacts. Thread the actual animation state instead.

🔧 Proposed fix
 return (
   <MessageResponse
     animated={false}
-    isAnimating={false}
+    isAnimating={isAnimating}
     mermaid={mermaid}
     components={components}
   >
     {displayText}
   </MessageResponse>
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<MessageResponse
animated={false}
isAnimating={false}
mermaid={mermaid}
components={components}
>
{displayText}
</MessageResponse>
return (
<MessageResponse
animated={false}
isAnimating={isAnimating}
mermaid={mermaid}
components={components}
>
{displayText}
</MessageResponse>
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/components/StreamingMessageText/StreamingMessageText.tsx`
around lines 47 - 55, StreamingMessageText currently forces MessageResponse to
think it's not animating (isAnimating={false}), which allows plugins like
mermaid to run on partial markdown; update StreamingMessageText to forward the
real animation state instead: locate the StreamingMessageText component and pass
the actual animation flag (e.g., isAnimating or animationInProgress) into
<MessageResponse isAnimating={isAnimating}> (ensure the component either
receives that prop from its parent or computes it from its internal state used
to drive the character-by-character stream) so MessageResponse can disable
streamdownPlugins during animation.

);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { StreamingMessageText } from "./StreamingMessageText";
Loading
Loading