mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +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,
|
||||
Loader2,
|
||||
Globe,
|
||||
Zap,
|
||||
FileCode,
|
||||
CloudDownload,
|
||||
} from "lucide-react";
|
||||
|
|
@ -101,6 +102,13 @@ type SearchActionProps = BaseActionProps &
|
|||
errorCode?: number;
|
||||
};
|
||||
|
||||
type McpActionProps = BaseActionProps & {
|
||||
actionType: "mcp";
|
||||
toolName: string;
|
||||
args: Record<string, any>;
|
||||
output?: string;
|
||||
};
|
||||
|
||||
export type ActionItemProps =
|
||||
| (BaseActionProps & { status: "loading" })
|
||||
| ShellActionProps
|
||||
|
|
@ -108,6 +116,7 @@ export type ActionItemProps =
|
|||
| InstallDependenciesActionProps
|
||||
| PlannerNotesActionProps
|
||||
| GetURLContentActionProps
|
||||
| McpActionProps
|
||||
| SearchActionProps;
|
||||
|
||||
export type ActionStepProps = {
|
||||
|
|
@ -200,6 +209,10 @@ function ActionItem(props: ActionItemProps) {
|
|||
: "Failed to fetch URL content";
|
||||
} else if (props.actionType === "search") {
|
||||
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;
|
||||
} else if (props.actionType === "planner_notes") {
|
||||
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;
|
||||
|
|
@ -267,6 +284,13 @@ function ActionItem(props: ActionItemProps) {
|
|||
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") {
|
||||
return (
|
||||
<ToolIconWithTooltip
|
||||
|
|
@ -390,6 +414,14 @@ function ActionItem(props: ActionItemProps) {
|
|||
</code>
|
||||
</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 {
|
||||
return (
|
||||
<code className="text-foreground/80 flex items-center text-xs font-normal">
|
||||
|
|
@ -423,6 +455,59 @@ function ActionItem(props: ActionItemProps) {
|
|||
)}
|
||||
</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) {
|
||||
return (
|
||||
<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
|
||||
>;
|
||||
|
||||
// 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({
|
||||
message,
|
||||
thread,
|
||||
|
|
@ -247,6 +261,16 @@ export function mapToolMessageToActionStepProps(
|
|||
output: getContentString(message.content),
|
||||
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 {
|
||||
status: "loading",
|
||||
|
|
@ -314,7 +338,8 @@ export function AssistantMessage({
|
|||
tc.name === searchTool.name ||
|
||||
tc.name === installDependenciesTool.name ||
|
||||
tc.name === plannerNotesTool.name ||
|
||||
tc.name === getURLContentTool.name,
|
||||
tc.name === getURLContentTool.name ||
|
||||
isMcpTool(tc.name),
|
||||
)
|
||||
: [];
|
||||
|
||||
|
|
@ -613,7 +638,14 @@ export function AssistantMessage({
|
|||
output: "",
|
||||
} as ActionItemProps;
|
||||
} 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;
|
||||
return {
|
||||
actionType: "shell",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue