Integrate CopilotChatActionsProvider into ChatContainer and enhance RunAgent and RunBlock tools with ChatCredentialsSetup for improved credential management and user interaction.

This commit is contained in:
abhi1992002
2026-02-03 14:38:19 +05:30
parent ea9f289647
commit 640b894405
12 changed files with 927 additions and 59 deletions

View File

@@ -6,6 +6,7 @@ import { ChatInput } from "@/components/contextual/Chat/components/ChatInput/Cha
import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat";
import { useState } from "react";
import { parseAsString, useQueryState } from "nuqs";
import { CopilotChatActionsProvider } from "../CopilotChatActionsProvider/CopilotChatActionsProvider";
export interface ChatContainerProps {
messages: UIMessage<unknown, UIDataTypes, UITools>[];
@@ -45,34 +46,36 @@ export const ChatContainer = ({
}
return (
<div className="mx-auto h-full w-full max-w-3xl pb-6">
<div className="flex h-full flex-col">
{sessionId ? (
<ChatMessagesContainer
messages={messages}
status={status}
error={error}
handleSubmit={handleMessageSubmit}
input={input}
setInput={setInput}
/>
) : (
<EmptySession
isCreating={isCreating}
onCreateSession={createSession}
/>
)}
<div className="relative px-3 pt-2">
<div className="pointer-events-none absolute top-[-18px] z-10 h-6 w-full bg-gradient-to-b from-transparent to-[#f8f8f9]" />
<ChatInput
onSend={onSend}
disabled={status === "streaming" || !sessionId}
isStreaming={status === "streaming"}
onStop={() => {}}
placeholder="You can search or just ask"
/>
<CopilotChatActionsProvider onSend={onSend}>
<div className="mx-auto h-full w-full max-w-3xl pb-6">
<div className="flex h-full flex-col">
{sessionId ? (
<ChatMessagesContainer
messages={messages}
status={status}
error={error}
handleSubmit={handleMessageSubmit}
input={input}
setInput={setInput}
/>
) : (
<EmptySession
isCreating={isCreating}
onCreateSession={createSession}
/>
)}
<div className="relative px-3 pt-2">
<div className="pointer-events-none absolute top-[-18px] z-10 h-6 w-full bg-gradient-to-b from-transparent to-[#f8f8f9]" />
<ChatInput
onSend={onSend}
disabled={status === "streaming" || !sessionId}
isStreaming={status === "streaming"}
onStop={() => {}}
placeholder="You can search or just ask"
/>
</div>
</div>
</div>
</div>
</CopilotChatActionsProvider>
);
};

View File

@@ -17,6 +17,8 @@ import { SearchDocsTool } from "../../tools/SearchDocs/SearchDocs";
import { RunBlockTool } from "../../tools/RunBlock/RunBlock";
import { RunAgentTool } from "../../tools/RunAgent/RunAgent";
import { ViewAgentOutputTool } from "../../tools/ViewAgentOutput/ViewAgentOutput";
import { CreateAgentTool } from "../../tools/CreateAgent/CreateAgent";
import { EditAgentTool } from "../../tools/EditAgent/EditAgent";
interface ChatMessagesContainerProps {
messages: UIMessage<unknown, UIDataTypes, UITools>[];
@@ -90,12 +92,27 @@ export const ChatMessagesContainer = ({
/>
);
case "tool-run_agent":
case "tool-schedule_agent":
return (
<RunAgentTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-create_agent":
return (
<CreateAgentTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-edit_agent":
return (
<EditAgentTool
key={`${message.id}-${i}`}
part={part as ToolUIPart}
/>
);
case "tool-view_agent_output":
return (
<ViewAgentOutputTool

View File

@@ -0,0 +1,16 @@
"use client";
import { CopilotChatActionsContext } from "./useCopilotChatActions";
interface Props {
onSend: (message: string) => void;
children: React.ReactNode;
}
export function CopilotChatActionsProvider({ onSend, children }: Props) {
return (
<CopilotChatActionsContext.Provider value={{ onSend }}>
{children}
</CopilotChatActionsContext.Provider>
);
}

View File

@@ -0,0 +1,23 @@
"use client";
import { createContext, useContext } from "react";
interface CopilotChatActions {
onSend: (message: string) => void;
}
const CopilotChatActionsContext = createContext<CopilotChatActions | null>(
null,
);
export function useCopilotChatActions(): CopilotChatActions {
const ctx = useContext(CopilotChatActionsContext);
if (!ctx) {
throw new Error(
"useCopilotChatActions must be used within CopilotChatActionsProvider",
);
}
return ctx;
}
export { CopilotChatActionsContext };

View File

@@ -0,0 +1,189 @@
"use client";
import type { ToolUIPart } from "ai";
import Link from "next/link";
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
import { ClarificationQuestionsWidget } from "@/components/contextual/Chat/components/ClarificationQuestionsWidget/ClarificationQuestionsWidget";
import {
formatMaybeJson,
getAnimationText,
getCreateAgentToolOutput,
StateIcon,
truncateText,
type CreateAgentToolOutput,
} from "./helpers";
export interface CreateAgentToolPart {
type: string;
toolCallId: string;
state: ToolUIPart["state"];
input?: unknown;
output?: unknown;
}
interface Props {
part: CreateAgentToolPart;
}
function getAccordionMeta(output: CreateAgentToolOutput): {
badgeText: string;
title: string;
description?: string;
} {
if (output.type === "agent_saved") {
return { badgeText: "Create agent", title: output.agent_name };
}
if (output.type === "agent_preview") {
return {
badgeText: "Create agent",
title: output.agent_name,
description: `${output.node_count} block${output.node_count === 1 ? "" : "s"}`,
};
}
if (output.type === "clarification_needed") {
return {
badgeText: "Create agent",
title: "Needs clarification",
description: `${output.questions.length} question${output.questions.length === 1 ? "" : "s"}`,
};
}
if (
output.type === "operation_started" ||
output.type === "operation_pending" ||
output.type === "operation_in_progress"
) {
return { badgeText: "Create agent", title: "Creating agent" };
}
return { badgeText: "Create agent", title: "Error" };
}
export function CreateAgentTool({ part }: Props) {
const text = getAnimationText(part);
const { onSend } = useCopilotChatActions();
const output = getCreateAgentToolOutput(part);
const hasExpandableContent =
part.state === "output-available" &&
!!output &&
(output.type === "operation_started" ||
output.type === "operation_pending" ||
output.type === "operation_in_progress" ||
output.type === "agent_preview" ||
output.type === "agent_saved" ||
output.type === "clarification_needed" ||
output.type === "error");
function handleClarificationAnswers(answers: Record<string, string>) {
const contextMessage = Object.entries(answers)
.map(([keyword, answer]) => `${keyword}: ${answer}`)
.join("\n");
onSend(
`I have the answers to your questions:\n\n${contextMessage}\n\nPlease proceed with creating the agent.`,
);
}
return (
<div className="py-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<StateIcon state={part.state} />
<MorphingTextAnimation text={text} />
</div>
{hasExpandableContent && output && (
<ToolAccordion
{...getAccordionMeta(output)}
defaultExpanded={output.type === "clarification_needed"}
>
{(output.type === "operation_started" ||
output.type === "operation_pending") && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<p className="text-xs text-muted-foreground">
Operation: {output.operation_id}
</p>
<p className="text-xs italic text-muted-foreground">
Check your library in a few minutes.
</p>
</div>
)}
{output.type === "operation_in_progress" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<p className="text-xs italic text-muted-foreground">
Please wait for the current operation to finish.
</p>
</div>
)}
{output.type === "agent_saved" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<div className="flex flex-wrap gap-2">
<Link
href={output.library_agent_link}
className="text-xs font-medium text-purple-600 hover:text-purple-700"
>
Open in library
</Link>
<Link
href={output.agent_page_link}
className="text-xs font-medium text-purple-600 hover:text-purple-700"
>
Open in builder
</Link>
</div>
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{truncateText(
formatMaybeJson({ agent_id: output.agent_id }),
800,
)}
</pre>
</div>
)}
{output.type === "agent_preview" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
{output.description?.trim() && (
<p className="text-xs text-muted-foreground">
{output.description}
</p>
)}
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{truncateText(formatMaybeJson(output.agent_json), 1600)}
</pre>
</div>
)}
{output.type === "clarification_needed" && (
<ClarificationQuestionsWidget
questions={output.questions}
message={output.message}
onSubmitAnswers={handleClarificationAnswers}
/>
)}
{output.type === "error" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
{output.error && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{formatMaybeJson(output.error)}
</pre>
)}
{output.details && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{formatMaybeJson(output.details)}
</pre>
)}
</div>
)}
</ToolAccordion>
)}
</div>
);
}

View File

@@ -0,0 +1,168 @@
import type { ToolUIPart } from "ai";
import {
CheckCircleIcon,
CircleNotchIcon,
XCircleIcon,
} from "@phosphor-icons/react";
export interface ClarifyingQuestion {
question: string;
keyword: string;
example?: string;
}
export interface OperationStartedOutput {
type: "operation_started";
message: string;
session_id?: string;
operation_id: string;
tool_name: string;
}
export interface OperationPendingOutput {
type: "operation_pending";
message: string;
session_id?: string;
operation_id: string;
tool_name: string;
}
export interface OperationInProgressOutput {
type: "operation_in_progress";
message: string;
session_id?: string;
tool_call_id: string;
}
export interface AgentPreviewOutput {
type: "agent_preview";
message: string;
session_id?: string;
agent_json: Record<string, unknown>;
agent_name: string;
description: string;
node_count: number;
link_count: number;
}
export interface AgentSavedOutput {
type: "agent_saved";
message: string;
session_id?: string;
agent_id: string;
agent_name: string;
library_agent_id: string;
library_agent_link: string;
agent_page_link: string;
}
export interface ClarificationNeededOutput {
type: "clarification_needed";
message: string;
session_id?: string;
questions: ClarifyingQuestion[];
}
export interface ErrorOutput {
type: "error";
message: string;
session_id?: string;
error?: string | null;
details?: Record<string, unknown> | null;
}
export type CreateAgentToolOutput =
| OperationStartedOutput
| OperationPendingOutput
| OperationInProgressOutput
| AgentPreviewOutput
| AgentSavedOutput
| ClarificationNeededOutput
| ErrorOutput;
function parseOutput(output: unknown): CreateAgentToolOutput | null {
if (!output) return null;
if (typeof output === "string") {
const trimmed = output.trim();
if (!trimmed) return null;
try {
return JSON.parse(trimmed) as CreateAgentToolOutput;
} catch {
return null;
}
}
if (typeof output === "object") return output as CreateAgentToolOutput;
return null;
}
export function getCreateAgentToolOutput(
part: unknown,
): CreateAgentToolOutput | null {
if (!part || typeof part !== "object") return null;
return parseOutput((part as { output?: unknown }).output);
}
export function getAnimationText(part: {
state: ToolUIPart["state"];
input?: unknown;
output?: unknown;
}): string {
switch (part.state) {
case "input-streaming":
return "Creating agent";
case "input-available":
return "Generating agent workflow";
case "output-available": {
const output = parseOutput(part.output);
if (!output) return "Agent created";
if (output.type === "operation_started") return "Agent creation started";
if (output.type === "operation_pending")
return "Agent creation in progress";
if (output.type === "operation_in_progress")
return "Agent creation already in progress";
if (output.type === "agent_saved") return `Saved: ${output.agent_name}`;
if (output.type === "agent_preview")
return `Preview: ${output.agent_name}`;
if (output.type === "clarification_needed") return "Needs clarification";
return "Error creating agent";
}
case "output-error":
return "Error creating agent";
default:
return "Processing";
}
}
export function StateIcon({ state }: { state: ToolUIPart["state"] }) {
switch (state) {
case "input-streaming":
case "input-available":
return (
<CircleNotchIcon
className="h-4 w-4 animate-spin text-muted-foreground"
weight="bold"
/>
);
case "output-available":
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
case "output-error":
return <XCircleIcon className="h-4 w-4 text-red-500" />;
default:
return null;
}
}
export function formatMaybeJson(value: unknown): string {
if (typeof value === "string") return value;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
export function truncateText(text: string, maxChars: number): string {
const trimmed = text.trim();
if (trimmed.length <= maxChars) return trimmed;
return `${trimmed.slice(0, maxChars).trimEnd()}`;
}

View File

@@ -0,0 +1,189 @@
"use client";
import type { ToolUIPart } from "ai";
import Link from "next/link";
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
import { ClarificationQuestionsWidget } from "@/components/contextual/Chat/components/ClarificationQuestionsWidget/ClarificationQuestionsWidget";
import {
formatMaybeJson,
getAnimationText,
getEditAgentToolOutput,
StateIcon,
truncateText,
type EditAgentToolOutput,
} from "./helpers";
export interface EditAgentToolPart {
type: string;
toolCallId: string;
state: ToolUIPart["state"];
input?: unknown;
output?: unknown;
}
interface Props {
part: EditAgentToolPart;
}
function getAccordionMeta(output: EditAgentToolOutput): {
badgeText: string;
title: string;
description?: string;
} {
if (output.type === "agent_saved") {
return { badgeText: "Edit agent", title: output.agent_name };
}
if (output.type === "agent_preview") {
return {
badgeText: "Edit agent",
title: output.agent_name,
description: `${output.node_count} block${output.node_count === 1 ? "" : "s"}`,
};
}
if (output.type === "clarification_needed") {
return {
badgeText: "Edit agent",
title: "Needs clarification",
description: `${output.questions.length} question${output.questions.length === 1 ? "" : "s"}`,
};
}
if (
output.type === "operation_started" ||
output.type === "operation_pending" ||
output.type === "operation_in_progress"
) {
return { badgeText: "Edit agent", title: "Editing agent" };
}
return { badgeText: "Edit agent", title: "Error" };
}
export function EditAgentTool({ part }: Props) {
const text = getAnimationText(part);
const { onSend } = useCopilotChatActions();
const output = getEditAgentToolOutput(part);
const hasExpandableContent =
part.state === "output-available" &&
!!output &&
(output.type === "operation_started" ||
output.type === "operation_pending" ||
output.type === "operation_in_progress" ||
output.type === "agent_preview" ||
output.type === "agent_saved" ||
output.type === "clarification_needed" ||
output.type === "error");
function handleClarificationAnswers(answers: Record<string, string>) {
const contextMessage = Object.entries(answers)
.map(([keyword, answer]) => `${keyword}: ${answer}`)
.join("\n");
onSend(
`I have the answers to your questions:\n\n${contextMessage}\n\nPlease proceed with editing the agent.`,
);
}
return (
<div className="py-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<StateIcon state={part.state} />
<MorphingTextAnimation text={text} />
</div>
{hasExpandableContent && output && (
<ToolAccordion
{...getAccordionMeta(output)}
defaultExpanded={output.type === "clarification_needed"}
>
{(output.type === "operation_started" ||
output.type === "operation_pending") && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<p className="text-xs text-muted-foreground">
Operation: {output.operation_id}
</p>
<p className="text-xs italic text-muted-foreground">
Check your library in a few minutes.
</p>
</div>
)}
{output.type === "operation_in_progress" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<p className="text-xs italic text-muted-foreground">
Please wait for the current operation to finish.
</p>
</div>
)}
{output.type === "agent_saved" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
<div className="flex flex-wrap gap-2">
<Link
href={output.library_agent_link}
className="text-xs font-medium text-purple-600 hover:text-purple-700"
>
Open in library
</Link>
<Link
href={output.agent_page_link}
className="text-xs font-medium text-purple-600 hover:text-purple-700"
>
Open in builder
</Link>
</div>
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{truncateText(
formatMaybeJson({ agent_id: output.agent_id }),
800,
)}
</pre>
</div>
)}
{output.type === "agent_preview" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
{output.description?.trim() && (
<p className="text-xs text-muted-foreground">
{output.description}
</p>
)}
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{truncateText(formatMaybeJson(output.agent_json), 1600)}
</pre>
</div>
)}
{output.type === "clarification_needed" && (
<ClarificationQuestionsWidget
questions={output.questions}
message={output.message}
onSubmitAnswers={handleClarificationAnswers}
/>
)}
{output.type === "error" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
{output.error && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{formatMaybeJson(output.error)}
</pre>
)}
{output.details && (
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
{formatMaybeJson(output.details)}
</pre>
)}
</div>
)}
</ToolAccordion>
)}
</div>
);
}

View File

@@ -0,0 +1,168 @@
import type { ToolUIPart } from "ai";
import {
CheckCircleIcon,
CircleNotchIcon,
XCircleIcon,
} from "@phosphor-icons/react";
export interface ClarifyingQuestion {
question: string;
keyword: string;
example?: string;
}
export interface OperationStartedOutput {
type: "operation_started";
message: string;
session_id?: string;
operation_id: string;
tool_name: string;
}
export interface OperationPendingOutput {
type: "operation_pending";
message: string;
session_id?: string;
operation_id: string;
tool_name: string;
}
export interface OperationInProgressOutput {
type: "operation_in_progress";
message: string;
session_id?: string;
tool_call_id: string;
}
export interface AgentPreviewOutput {
type: "agent_preview";
message: string;
session_id?: string;
agent_json: Record<string, unknown>;
agent_name: string;
description: string;
node_count: number;
link_count: number;
}
export interface AgentSavedOutput {
type: "agent_saved";
message: string;
session_id?: string;
agent_id: string;
agent_name: string;
library_agent_id: string;
library_agent_link: string;
agent_page_link: string;
}
export interface ClarificationNeededOutput {
type: "clarification_needed";
message: string;
session_id?: string;
questions: ClarifyingQuestion[];
}
export interface ErrorOutput {
type: "error";
message: string;
session_id?: string;
error?: string | null;
details?: Record<string, unknown> | null;
}
export type EditAgentToolOutput =
| OperationStartedOutput
| OperationPendingOutput
| OperationInProgressOutput
| AgentPreviewOutput
| AgentSavedOutput
| ClarificationNeededOutput
| ErrorOutput;
function parseOutput(output: unknown): EditAgentToolOutput | null {
if (!output) return null;
if (typeof output === "string") {
const trimmed = output.trim();
if (!trimmed) return null;
try {
return JSON.parse(trimmed) as EditAgentToolOutput;
} catch {
return null;
}
}
if (typeof output === "object") return output as EditAgentToolOutput;
return null;
}
export function getEditAgentToolOutput(
part: unknown,
): EditAgentToolOutput | null {
if (!part || typeof part !== "object") return null;
return parseOutput((part as { output?: unknown }).output);
}
export function getAnimationText(part: {
state: ToolUIPart["state"];
input?: unknown;
output?: unknown;
}): string {
switch (part.state) {
case "input-streaming":
return "Editing agent";
case "input-available":
return "Updating agent workflow";
case "output-available": {
const output = parseOutput(part.output);
if (!output) return "Agent updated";
if (output.type === "operation_started") return "Agent update started";
if (output.type === "operation_pending")
return "Agent update in progress";
if (output.type === "operation_in_progress")
return "Agent update already in progress";
if (output.type === "agent_saved") return `Saved: ${output.agent_name}`;
if (output.type === "agent_preview")
return `Preview: ${output.agent_name}`;
if (output.type === "clarification_needed") return "Needs clarification";
return "Error editing agent";
}
case "output-error":
return "Error editing agent";
default:
return "Processing";
}
}
export function StateIcon({ state }: { state: ToolUIPart["state"] }) {
switch (state) {
case "input-streaming":
case "input-available":
return (
<CircleNotchIcon
className="h-4 w-4 animate-spin text-muted-foreground"
weight="bold"
/>
);
case "output-available":
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
case "output-error":
return <XCircleIcon className="h-4 w-4 text-red-500" />;
default:
return null;
}
}
export function formatMaybeJson(value: unknown): string {
if (typeof value === "string") return value;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
export function truncateText(text: string, maxChars: number): string {
const trimmed = text.trim();
if (trimmed.length <= maxChars) return trimmed;
return `${trimmed.slice(0, maxChars).trimEnd()}`;
}

View File

@@ -4,6 +4,8 @@ import type { ToolUIPart } from "ai";
import Link from "next/link";
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
import { ChatCredentialsSetup } from "@/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup";
import {
formatMaybeJson,
getAnimationText,
@@ -37,6 +39,14 @@ function getAccordionMeta(output: RunAgentToolOutput): {
};
}
if (output.type === "agent_details") {
return {
badgeText: "Run agent",
title: output.agent.name,
description: "Inputs required",
};
}
if (output.type === "setup_requirements") {
const missingCredsCount = Object.keys(
output.setup_info.user_readiness.missing_credentials ?? {},
@@ -60,16 +70,24 @@ function getAccordionMeta(output: RunAgentToolOutput): {
export function RunAgentTool({ part }: Props) {
const text = getAnimationText(part);
const { onSend } = useCopilotChatActions();
const output = getRunAgentToolOutput(part);
const hasExpandableContent =
part.state === "output-available" &&
!!output &&
(output.type === "execution_started" ||
output.type === "agent_details" ||
output.type === "setup_requirements" ||
output.type === "need_login" ||
output.type === "error");
function handleAllCredentialsComplete() {
onSend(
"I've configured the required credentials. Please check if everything is ready and proceed with running the agent.",
);
}
return (
<div className="py-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
@@ -78,7 +96,13 @@ export function RunAgentTool({ part }: Props) {
</div>
{hasExpandableContent && output && (
<ToolAccordion {...getAccordionMeta(output)}>
<ToolAccordion
{...getAccordionMeta(output)}
defaultExpanded={
output.type === "setup_requirements" ||
output.type === "agent_details"
}
>
{output.type === "execution_started" && (
<div className="grid gap-2">
<div className="rounded-2xl border bg-background p-3">
@@ -107,6 +131,28 @@ export function RunAgentTool({ part }: Props) {
</div>
)}
{output.type === "agent_details" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
{output.agent.description?.trim() && (
<p className="text-xs text-muted-foreground">
{output.agent.description}
</p>
)}
<div className="rounded-2xl border bg-background p-3">
<p className="text-xs font-medium text-foreground">Inputs</p>
<p className="mt-1 text-xs text-muted-foreground">
Provide required inputs in chat, or ask to run with defaults.
</p>
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
{formatMaybeJson(output.agent.inputs)}
</pre>
</div>
</div>
)}
{output.type === "setup_requirements" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
@@ -114,21 +160,24 @@ export function RunAgentTool({ part }: Props) {
{Object.keys(
output.setup_info.user_readiness.missing_credentials ?? {},
).length > 0 && (
<div className="rounded-2xl border bg-background p-3">
<p className="text-xs font-medium text-foreground">
Missing credentials
</p>
<ul className="mt-2 list-disc space-y-1 pl-5 text-xs text-muted-foreground">
{Object.entries(
output.setup_info.user_readiness.missing_credentials ??
{},
).map(([field, cred]) => (
<li key={field}>
{cred.title} ({cred.provider})
</li>
))}
</ul>
</div>
<ChatCredentialsSetup
credentials={Object.values(
output.setup_info.user_readiness.missing_credentials ?? {},
).map((cred) => ({
provider: cred.provider,
providerName:
cred.provider_name ?? cred.provider.replace(/_/g, " "),
credentialTypes: (cred.types ?? [cred.type]) as Array<
"api_key" | "oauth2" | "user_password" | "host_scoped"
>,
title: cred.title,
scopes: cred.scopes,
}))}
agentName={output.setup_info.agent_name}
message={output.message}
onAllCredentialsComplete={handleAllCredentialsComplete}
onCancel={() => {}}
/>
)}
{output.setup_info.requirements.inputs?.length > 0 && (

View File

@@ -18,8 +18,11 @@ export interface RunAgentInput {
export interface CredentialsMeta {
id: string;
provider: string;
provider_name?: string;
type: string;
types?: string[];
title: string;
scopes?: string[];
}
export interface SetupInfo {
@@ -78,9 +81,31 @@ export interface NeedLoginOutput {
session_id?: string;
}
export interface AgentDetailsOutput {
type: "agent_details";
message: string;
session_id?: string;
agent: {
id: string;
name: string;
description: string;
inputs: Record<string, unknown>;
credentials: CredentialsMeta[];
execution_options?: {
manual?: boolean;
scheduled?: boolean;
webhook?: boolean;
};
};
user_authenticated?: boolean;
graph_id?: string | null;
graph_version?: number | null;
}
export type RunAgentToolOutput =
| SetupRequirementsOutput
| ExecutionStartedOutput
| AgentDetailsOutput
| NeedLoginOutput
| ErrorOutput;
@@ -143,6 +168,9 @@ export function getAnimationText(part: {
if (output.type === "execution_started") {
return `Started: ${output.graph_name}`;
}
if (output.type === "agent_details") {
return `Agent inputs: ${output.agent.name}`;
}
if (output.type === "setup_requirements") {
return `Needs setup: ${output.setup_info.agent_name}`;
}

View File

@@ -3,6 +3,8 @@
import type { ToolUIPart } from "ai";
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
import { ChatCredentialsSetup } from "@/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup";
import {
formatMaybeJson,
getAnimationText,
@@ -59,6 +61,7 @@ function getAccordionMeta(output: RunBlockToolOutput): {
export function RunBlockTool({ part }: Props) {
const text = getAnimationText(part);
const { onSend } = useCopilotChatActions();
const output = getRunBlockToolOutput(part);
const hasExpandableContent =
@@ -68,6 +71,12 @@ export function RunBlockTool({ part }: Props) {
output.type === "setup_requirements" ||
output.type === "error");
function handleAllCredentialsComplete() {
onSend(
"I've configured the required credentials. Please re-run the block now.",
);
}
return (
<div className="py-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
@@ -76,7 +85,10 @@ export function RunBlockTool({ part }: Props) {
</div>
{hasExpandableContent && output && (
<ToolAccordion {...getAccordionMeta(output)}>
<ToolAccordion
{...getAccordionMeta(output)}
defaultExpanded={output.type === "setup_requirements"}
>
{output.type === "block_output" && (
<div className="grid gap-2">
<p className="text-sm text-foreground">{output.message}</p>
@@ -106,21 +118,24 @@ export function RunBlockTool({ part }: Props) {
{Object.keys(
output.setup_info.user_readiness.missing_credentials ?? {},
).length > 0 && (
<div className="rounded-2xl border bg-background p-3">
<p className="text-xs font-medium text-foreground">
Missing credentials
</p>
<ul className="mt-2 list-disc space-y-1 pl-5 text-xs text-muted-foreground">
{Object.entries(
output.setup_info.user_readiness.missing_credentials ??
{},
).map(([field, cred]) => (
<li key={field}>
{cred.title} ({cred.provider})
</li>
))}
</ul>
</div>
<ChatCredentialsSetup
credentials={Object.values(
output.setup_info.user_readiness.missing_credentials ?? {},
).map((cred) => ({
provider: cred.provider,
providerName:
cred.provider_name ?? cred.provider.replace(/_/g, " "),
credentialTypes: (cred.types ?? [cred.type]) as Array<
"api_key" | "oauth2" | "user_password" | "host_scoped"
>,
title: cred.title,
scopes: cred.scopes,
}))}
agentName={output.setup_info.agent_name}
message={output.message}
onAllCredentialsComplete={handleAllCredentialsComplete}
onCancel={() => {}}
/>
)}
{output.setup_info.requirements.inputs?.length > 0 && (

View File

@@ -13,8 +13,11 @@ export interface RunBlockInput {
export interface CredentialsMeta {
id: string;
provider: string;
provider_name?: string;
type: string;
types?: string[];
title: string;
scopes?: string[];
}
export interface SetupInfo {