mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-05 17:42:12 +00:00
Feat: MCP UI component (#414)
* init MCP action-step ui component * Update apps/web/src/components/gen-ui/action-step.tsx Co-authored-by: Brace Sproul <braceasproul@gmail.com> * Update apps/web/src/components/thread/messages/ai.tsx Co-authored-by: Brace Sproul <braceasproul@gmail.com> * Update apps/web/src/components/gen-ui/action-step.tsx Co-authored-by: Brace Sproul <braceasproul@gmail.com> * Code Review --------- Co-authored-by: Brace Sproul <braceasproul@gmail.com>
This commit is contained in:
parent
984fc68556
commit
ced98ae029
2 changed files with 119 additions and 2 deletions
|
|
@ -12,6 +12,7 @@ import {
|
||||||
XCircle,
|
XCircle,
|
||||||
Loader2,
|
Loader2,
|
||||||
Globe,
|
Globe,
|
||||||
|
Zap,
|
||||||
FileCode,
|
FileCode,
|
||||||
CloudDownload,
|
CloudDownload,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
@ -101,6 +102,13 @@ type SearchActionProps = BaseActionProps &
|
||||||
errorCode?: number;
|
errorCode?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type McpActionProps = BaseActionProps & {
|
||||||
|
actionType: "mcp";
|
||||||
|
toolName: string;
|
||||||
|
args: Record<string, any>;
|
||||||
|
output?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ActionItemProps =
|
export type ActionItemProps =
|
||||||
| (BaseActionProps & { status: "loading" })
|
| (BaseActionProps & { status: "loading" })
|
||||||
| ShellActionProps
|
| ShellActionProps
|
||||||
|
|
@ -108,6 +116,7 @@ export type ActionItemProps =
|
||||||
| InstallDependenciesActionProps
|
| InstallDependenciesActionProps
|
||||||
| PlannerNotesActionProps
|
| PlannerNotesActionProps
|
||||||
| GetURLContentActionProps
|
| GetURLContentActionProps
|
||||||
|
| McpActionProps
|
||||||
| SearchActionProps;
|
| SearchActionProps;
|
||||||
|
|
||||||
export type ActionStepProps = {
|
export type ActionStepProps = {
|
||||||
|
|
@ -200,6 +209,10 @@ function ActionItem(props: ActionItemProps) {
|
||||||
: "Failed to fetch URL content";
|
: "Failed to fetch URL content";
|
||||||
} else if (props.actionType === "search") {
|
} else if (props.actionType === "search") {
|
||||||
return props.success ? "Search completed" : "Search failed";
|
return props.success ? "Search completed" : "Search failed";
|
||||||
|
} else if (props.actionType === "mcp") {
|
||||||
|
return props.success
|
||||||
|
? `${props.toolName} completed`
|
||||||
|
: `${props.toolName} failed`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,6 +234,10 @@ function ActionItem(props: ActionItemProps) {
|
||||||
return !!props.diff;
|
return !!props.diff;
|
||||||
} else if (props.actionType === "planner_notes") {
|
} else if (props.actionType === "planner_notes") {
|
||||||
return !!(props.notes && props.notes.length > 0);
|
return !!(props.notes && props.notes.length > 0);
|
||||||
|
} else if (props.actionType === "mcp") {
|
||||||
|
const hasArgs = props.args && Object.keys(props.args).length > 0;
|
||||||
|
const hasOutput = !!props.output;
|
||||||
|
return hasArgs || hasOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -267,6 +284,13 @@ function ActionItem(props: ActionItemProps) {
|
||||||
icon={<Globe className={cn(defaultIconStyling)} />}
|
icon={<Globe className={cn(defaultIconStyling)} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (props.actionType === "mcp") {
|
||||||
|
return (
|
||||||
|
<ToolIconWithTooltip
|
||||||
|
toolNamePretty={props.toolName}
|
||||||
|
icon={<Zap className={cn(defaultIconStyling)} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if (props.actionType === "search") {
|
} else if (props.actionType === "search") {
|
||||||
return (
|
return (
|
||||||
<ToolIconWithTooltip
|
<ToolIconWithTooltip
|
||||||
|
|
@ -390,6 +414,14 @@ function ActionItem(props: ActionItemProps) {
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else if (props.actionType === "mcp") {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-foreground/80 text-xs font-normal">
|
||||||
|
{props.toolName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<code className="text-foreground/80 flex items-center text-xs font-normal">
|
<code className="text-foreground/80 flex items-center text-xs font-normal">
|
||||||
|
|
@ -423,6 +455,59 @@ function ActionItem(props: ActionItemProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else if (props.actionType === "mcp") {
|
||||||
|
const hasArgs = props.args && Object.keys(props.args).length > 0;
|
||||||
|
const hasOutput = !!props.output;
|
||||||
|
|
||||||
|
if (!hasArgs && !hasOutput) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-muted overflow-x-auto p-2 dark:bg-gray-900">
|
||||||
|
{hasArgs && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase">
|
||||||
|
Arguments
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted-foreground/5 rounded border p-3">
|
||||||
|
{Object.entries(props.args).map(([key, value]) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="mb-2 last:mb-0"
|
||||||
|
>
|
||||||
|
<div className="text-foreground mb-1 text-xs font-medium">
|
||||||
|
{key}
|
||||||
|
</div>
|
||||||
|
<div className="text-foreground/80 text-xs">
|
||||||
|
{typeof value === "object" ? (
|
||||||
|
<code className="bg-muted rounded px-2 py-1 font-mono text-xs break-all">
|
||||||
|
{JSON.stringify(value, null, 2)}
|
||||||
|
</code>
|
||||||
|
) : (
|
||||||
|
<code className="bg-muted rounded px-2 py-1 font-mono text-xs break-all">
|
||||||
|
{String(value)}
|
||||||
|
</code>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasOutput && (
|
||||||
|
<>
|
||||||
|
<div className="text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase">
|
||||||
|
Output
|
||||||
|
</div>
|
||||||
|
<pre className="text-foreground/90 text-xs font-normal whitespace-pre-wrap">
|
||||||
|
{props.output}
|
||||||
|
</pre>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
} else if (props.actionType === "get_url_content" && props.output) {
|
} else if (props.actionType === "get_url_content" && props.output) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-muted text-foreground/90 overflow-x-auto p-2 dark:bg-gray-900">
|
<div className="bg-muted text-foreground/90 overflow-x-auto p-2 dark:bg-gray-900">
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,20 @@ type ConversationHistorySummaryToolArgs = z.infer<
|
||||||
typeof conversationHistorySummaryTool.schema
|
typeof conversationHistorySummaryTool.schema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
// Helper function to detect MCP tools by checking if tool name is NOT in known tools
|
||||||
|
function isMcpTool(toolName: string): boolean {
|
||||||
|
const knownToolNames = [
|
||||||
|
shellTool.name,
|
||||||
|
applyPatchTool.name,
|
||||||
|
installDependenciesTool.name,
|
||||||
|
plannerNotesTool.name,
|
||||||
|
getURLContentTool.name,
|
||||||
|
openPrTool.name,
|
||||||
|
diagnoseErrorTool.name,
|
||||||
|
];
|
||||||
|
return !knownToolNames.some((t) => t === toolName);
|
||||||
|
}
|
||||||
|
|
||||||
function CustomComponent({
|
function CustomComponent({
|
||||||
message,
|
message,
|
||||||
thread,
|
thread,
|
||||||
|
|
@ -247,6 +261,16 @@ export function mapToolMessageToActionStepProps(
|
||||||
output: getContentString(message.content),
|
output: getContentString(message.content),
|
||||||
reasoningText,
|
reasoningText,
|
||||||
};
|
};
|
||||||
|
} else if (toolCall && isMcpTool(toolCall.name)) {
|
||||||
|
return {
|
||||||
|
actionType: "mcp",
|
||||||
|
status,
|
||||||
|
success,
|
||||||
|
toolName: toolCall.name,
|
||||||
|
args: toolCall.args as Record<string, any>,
|
||||||
|
output: getContentString(message.content),
|
||||||
|
reasoningText,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
status: "loading",
|
status: "loading",
|
||||||
|
|
@ -314,7 +338,8 @@ export function AssistantMessage({
|
||||||
tc.name === searchTool.name ||
|
tc.name === searchTool.name ||
|
||||||
tc.name === installDependenciesTool.name ||
|
tc.name === installDependenciesTool.name ||
|
||||||
tc.name === plannerNotesTool.name ||
|
tc.name === plannerNotesTool.name ||
|
||||||
tc.name === getURLContentTool.name,
|
tc.name === getURLContentTool.name ||
|
||||||
|
isMcpTool(tc.name),
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|
@ -613,7 +638,14 @@ export function AssistantMessage({
|
||||||
output: "",
|
output: "",
|
||||||
} as ActionItemProps;
|
} as ActionItemProps;
|
||||||
} else {
|
} else {
|
||||||
if (isShellTool) {
|
if (isMcpTool(toolCall.name)) {
|
||||||
|
return {
|
||||||
|
actionType: "mcp",
|
||||||
|
status: "generating",
|
||||||
|
toolName: toolCall.name,
|
||||||
|
args: toolCall.args as Record<string, any>,
|
||||||
|
} as ActionItemProps;
|
||||||
|
} else if (isShellTool) {
|
||||||
const args = toolCall.args as ShellToolArgs;
|
const args = toolCall.args as ShellToolArgs;
|
||||||
return {
|
return {
|
||||||
actionType: "shell",
|
actionType: "shell",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue