(null);
- const { urlSessionId, setUrlSessionId } = useCopilotSessionId();
- const setIsStreaming = useCopilotStore((s) => s.setIsStreaming);
- const isCreating = useCopilotStore((s) => s.isCreatingSession);
- const setIsCreating = useCopilotStore((s) => s.setIsCreatingSession);
+ const {
+ sessionId,
+ setSessionId,
+ hydratedMessages,
+ isLoadingSession,
+ createSession,
+ isCreatingSession,
+ } = useChatSession();
- const greetingName = getGreetingName(user);
- const quickActions = getQuickActions();
+ const breakpoint = useBreakpoint();
+ const isMobile =
+ breakpoint === "base" || breakpoint === "sm" || breakpoint === "md";
- const hasSession = Boolean(urlSessionId);
- const initialPrompt = urlSessionId
- ? getInitialPrompt(urlSessionId)
- : undefined;
+ const transport = useMemo(
+ () =>
+ sessionId
+ ? new DefaultChatTransport({
+ api: `/api/chat/sessions/${sessionId}/stream`,
+ prepareSendMessagesRequest: ({ messages }) => {
+ const last = messages[messages.length - 1];
+ return {
+ body: {
+ message: (
+ last.parts?.map((p) => (p.type === "text" ? p.text : "")) ??
+ []
+ ).join(""),
+ is_user_message: last.role === "user",
+ context: null,
+ },
+ };
+ },
+ })
+ : null,
+ [sessionId],
+ );
+
+ const { messages, sendMessage, stop, status, error, setMessages } = useChat({
+ id: sessionId ?? undefined,
+ transport: transport ?? undefined,
+ });
useEffect(() => {
- if (isLoggedIn) completeStep("VISIT_COPILOT");
- }, [completeStep, isLoggedIn]);
+ if (!hydratedMessages || hydratedMessages.length === 0) return;
+ setMessages((prev) => {
+ if (prev.length >= hydratedMessages.length) return prev;
+ return hydratedMessages;
+ });
+ }, [hydratedMessages, setMessages]);
- async function startChatWithPrompt(prompt: string) {
- if (!prompt?.trim()) return;
- if (isCreating) return;
+ // Clear messages when session is null
+ useEffect(() => {
+ if (!sessionId) setMessages([]);
+ }, [sessionId, setMessages]);
- const trimmedPrompt = prompt.trim();
- setIsCreating(true);
+ useEffect(() => {
+ if (!sessionId || !pendingMessage) return;
+ const msg = pendingMessage;
+ setPendingMessage(null);
+ sendMessage({ text: msg });
+ }, [sessionId, pendingMessage, sendMessage]);
- try {
- const sessionResponse = await postV2CreateSession({
- body: JSON.stringify({}),
- });
+ async function onSend(message: string) {
+ const trimmed = message.trim();
+ if (!trimmed) return;
- if (sessionResponse.status !== 200 || !sessionResponse.data?.id) {
- throw new Error("Failed to create session");
- }
-
- const sessionId = sessionResponse.data.id;
- setInitialPrompt(sessionId, trimmedPrompt);
-
- await queryClient.invalidateQueries({
- queryKey: getGetV2ListSessionsQueryKey(),
- });
-
- await setUrlSessionId(sessionId, { shallow: true });
- } catch (error) {
- console.error("[CopilotPage] Failed to start chat:", error);
- toast({ title: "Failed to start chat", variant: "destructive" });
- Sentry.captureException(error);
- } finally {
- setIsCreating(false);
+ if (sessionId) {
+ sendMessage({ text: trimmed });
+ return;
}
+
+ setPendingMessage(trimmed);
+ await createSession();
}
- function handleQuickAction(action: string) {
- startChatWithPrompt(action);
+ const { data: sessionsResponse, isLoading: isLoadingSessions } =
+ useGetV2ListSessions(
+ { limit: 50 },
+ { query: { enabled: !isUserLoading && isLoggedIn } },
+ );
+
+ const sessions =
+ sessionsResponse?.status === 200 ? sessionsResponse.data.sessions : [];
+
+ function handleOpenDrawer() {
+ setIsDrawerOpen(true);
}
- function handleSessionNotFound() {
- router.replace("/copilot");
+ function handleCloseDrawer() {
+ setIsDrawerOpen(false);
}
- function handleStreamingChange(isStreamingValue: boolean) {
- setIsStreaming(isStreamingValue);
+ function handleDrawerOpenChange(open: boolean) {
+ setIsDrawerOpen(open);
+ }
+
+ function handleSelectSession(id: string) {
+ setSessionId(id);
+ if (isMobile) setIsDrawerOpen(false);
+ }
+
+ function handleNewChat() {
+ setSessionId(null);
+ if (isMobile) setIsDrawerOpen(false);
}
return {
- state: {
- greetingName,
- quickActions,
- isLoading: isUserLoading,
- hasSession,
- initialPrompt,
- },
- handlers: {
- handleQuickAction,
- startChatWithPrompt,
- handleSessionNotFound,
- handleStreamingChange,
- },
+ sessionId,
+ messages,
+ status,
+ error,
+ stop,
+ isLoadingSession,
+ isCreatingSession,
+ isUserLoading,
+ isLoggedIn,
+ createSession,
+ onSend,
+ // Mobile drawer
+ isMobile,
+ isDrawerOpen,
+ sessions,
+ isLoadingSessions,
+ handleOpenDrawer,
+ handleCloseDrawer,
+ handleDrawerOpenChange,
+ handleSelectSession,
+ handleNewChat,
};
}
-
-function getInitialPrompt(sessionId: string): string | undefined {
- try {
- const prompts = JSON.parse(
- sessionStorage.get(SessionKey.CHAT_INITIAL_PROMPTS) || "{}",
- );
- return prompts[sessionId];
- } catch {
- return undefined;
- }
-}
-
-function setInitialPrompt(sessionId: string, prompt: string): void {
- try {
- const prompts = JSON.parse(
- sessionStorage.get(SessionKey.CHAT_INITIAL_PROMPTS) || "{}",
- );
- prompts[sessionId] = prompt;
- sessionStorage.set(
- SessionKey.CHAT_INITIAL_PROMPTS,
- JSON.stringify(prompts),
- );
- } catch {
- // Ignore storage errors
- }
-}
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/RunDetailHeader/RunDetailHeader.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/RunDetailHeader/RunDetailHeader.tsx
index 3d04234bb3..4e98c70de5 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/RunDetailHeader/RunDetailHeader.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/RunDetailHeader/RunDetailHeader.tsx
@@ -2,7 +2,7 @@ import { GraphExecution } from "@/app/api/__generated__/models/graphExecution";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Text } from "@/components/atoms/Text/Text";
import { ClockClockwiseIcon } from "@phosphor-icons/react";
-import moment from "moment";
+import { formatDistanceToNow, formatDistanceStrict } from "date-fns";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
import { RunStatusBadge } from "../SelectedRunView/components/RunStatusBadge";
@@ -43,7 +43,10 @@ export function RunDetailHeader({ agent, run, scheduleRecurrence }: Props) {
{run ? (
- Started {moment(run.started_at).fromNow()}
+ Started{" "}
+ {run.started_at
+ ? formatDistanceToNow(run.started_at, { addSuffix: true })
+ : "—"}
|
@@ -62,7 +65,7 @@ export function RunDetailHeader({ agent, run, scheduleRecurrence }: Props) {
|
Duration:{" "}
- {moment.duration(run.stats.duration, "seconds").humanize()}
+ {formatDistanceStrict(0, run.stats.duration * 1000)}
>
)}
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/ScheduleListItem.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/ScheduleListItem.tsx
index a389fb4fc8..1ad40fcef4 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/ScheduleListItem.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/ScheduleListItem.tsx
@@ -3,7 +3,7 @@
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { ClockClockwiseIcon } from "@phosphor-icons/react";
-import moment from "moment";
+import { formatDistanceToNow } from "date-fns";
import { IconWrapper } from "./IconWrapper";
import { ScheduleActionsDropdown } from "./ScheduleActionsDropdown";
import { SidebarItemCard } from "./SidebarItemCard";
@@ -26,7 +26,9 @@ export function ScheduleListItem({
return (
}
title={template.name}
- description={moment(template.updated_at).fromNow()}
+ description={formatDistanceToNow(template.updated_at, {
+ addSuffix: true,
+ })}
onClick={onClick}
selected={selected}
actions={
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/TriggerListItem.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/TriggerListItem.tsx
index 4c399e640a..c7b0473a9b 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/TriggerListItem.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/TriggerListItem.tsx
@@ -3,7 +3,7 @@
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { LightningIcon } from "@phosphor-icons/react";
-import moment from "moment";
+import { formatDistanceToNow } from "date-fns";
import { IconWrapper } from "./IconWrapper";
import { SidebarItemCard } from "./SidebarItemCard";
import { TriggerActionsDropdown } from "./TriggerActionsDropdown";
@@ -31,7 +31,7 @@ export function TriggerListItem({
}
title={trigger.name}
- description={moment(trigger.updated_at).fromNow()}
+ description={formatDistanceToNow(trigger.updated_at, { addSuffix: true })}
onClick={onClick}
selected={selected}
actions={
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx
index f51c44c243..eb5224c958 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx
@@ -1,5 +1,5 @@
"use client";
-import moment from "moment";
+import { format, formatDistanceToNow, formatDistanceStrict } from "date-fns";
import React, { useCallback, useMemo, useEffect } from "react";
import {
@@ -90,13 +90,15 @@ export function AgentRunDetailsView({
},
{
label: "Started",
- value: `${moment(run.started_at).fromNow()}, ${moment(run.started_at).format("HH:mm")}`,
+ value: run.started_at
+ ? `${formatDistanceToNow(run.started_at, { addSuffix: true })}, ${format(run.started_at, "HH:mm")}`
+ : "—",
},
...(run.stats
? [
{
label: "Duration",
- value: moment.duration(run.stats.duration, "seconds").humanize(),
+ value: formatDistanceStrict(0, run.stats.duration * 1000),
},
{ label: "Steps", value: run.stats.node_exec_count },
{ label: "Cost", value: formatCredits(run.stats.cost) },
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-summary-card.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-summary-card.tsx
index 423495878b..6f7d7865bc 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-summary-card.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-summary-card.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import moment from "moment";
+import { formatDistanceToNow, isPast } from "date-fns";
import { cn } from "@/lib/utils";
@@ -118,10 +118,10 @@ export function AgentRunSummaryCard({
{timestamp && (
- {moment(timestamp).isBefore() ? "Ran" : "Runs in"}{" "}
- {moment(timestamp).fromNow()}
+ {isPast(timestamp) ? "Ran" : "Runs in"}{" "}
+ {formatDistanceToNow(timestamp, { addSuffix: true })}
)}
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx
index 74b8e9874c..efeb654ed5 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx
@@ -1,4 +1,4 @@
-import { debounce } from "lodash";
+import debounce from "lodash/debounce";
import { useCallback, useEffect } from "react";
interface Props {
diff --git a/autogpt_platform/frontend/src/app/(platform)/monitoring/components/AgentFlowList.tsx b/autogpt_platform/frontend/src/app/(platform)/monitoring/components/AgentFlowList.tsx
index badba61bf1..a306275f10 100644
--- a/autogpt_platform/frontend/src/app/(platform)/monitoring/components/AgentFlowList.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/monitoring/components/AgentFlowList.tsx
@@ -30,7 +30,7 @@ import {
TableHeader,
TableRow,
} from "@/components/__legacy__/ui/table";
-import moment from "moment/moment";
+import { formatDistanceToNow } from "date-fns";
import { DialogTitle } from "@/components/__legacy__/ui/dialog";
import { AgentImportForm } from "./AgentImportForm";
@@ -161,8 +161,12 @@ export const AgentFlowList = ({
(!lastRun ? (
) : (
-
- {moment(lastRun.started_at).fromNow()}
+
+ {lastRun.started_at
+ ? formatDistanceToNow(lastRun.started_at, {
+ addSuffix: true,
+ })
+ : "—"}
))}
diff --git a/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunInfo.tsx b/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunInfo.tsx
index e619c2fd3d..0fdf39e8d9 100644
--- a/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunInfo.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunInfo.tsx
@@ -10,7 +10,7 @@ import Link from "next/link";
import { Button, buttonVariants } from "@/components/__legacy__/ui/button";
import { IconSquare } from "@/components/__legacy__/ui/icons";
import { ExitIcon, Pencil2Icon } from "@radix-ui/react-icons";
-import moment from "moment/moment";
+import { format } from "date-fns";
import { FlowRunStatusBadge } from "@/app/(platform)/monitoring/components/FlowRunStatusBadge";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import RunnerOutputUI, {
@@ -111,11 +111,15 @@ export const FlowRunInfo: React.FC<
Started: {" "}
- {moment(execution.started_at).format("YYYY-MM-DD HH:mm:ss")}
+ {execution.started_at
+ ? format(execution.started_at, "yyyy-MM-dd HH:mm:ss")
+ : "—"}
Finished: {" "}
- {moment(execution.ended_at).format("YYYY-MM-DD HH:mm:ss")}
+ {execution.ended_at
+ ? format(execution.ended_at, "yyyy-MM-dd HH:mm:ss")
+ : "—"}
{execution.stats && (
diff --git a/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunsList.tsx b/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunsList.tsx
index a99d9309b5..c4a2a40447 100644
--- a/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunsList.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunsList.tsx
@@ -14,7 +14,7 @@ import {
TableHeader,
TableRow,
} from "@/components/__legacy__/ui/table";
-import moment from "moment/moment";
+import { format } from "date-fns";
import { FlowRunStatusBadge } from "@/app/(platform)/monitoring/components/FlowRunStatusBadge";
import { TextRenderer } from "../../../../components/__legacy__/ui/render";
@@ -59,7 +59,9 @@ export const FlowRunsList: React.FC<{
/>
- {moment(execution.started_at).format("HH:mm")}
+ {execution.started_at
+ ? format(execution.started_at, "HH:mm")
+ : "—"}
{
- const now = moment();
- const time = moment(unixTime);
- return now.diff(time, "hours") < 24
- ? time.format("HH:mm")
- : time.format("YYYY-MM-DD HH:mm");
+ const now = new Date();
+ const time = new Date(unixTime);
+ return differenceInHours(now, time) < 24
+ ? format(time, "HH:mm")
+ : format(time, "yyyy-MM-dd HH:mm");
}}
name="Time"
scale="time"
@@ -79,7 +79,9 @@ export const FlowRunsTimeline = ({
Started: {" "}
- {moment(data.started_at).format("YYYY-MM-DD HH:mm:ss")}
+ {data.started_at
+ ? format(data.started_at, "yyyy-MM-dd HH:mm:ss")
+ : "—"}
{data.stats && (
diff --git a/autogpt_platform/frontend/src/app/api/chat/sessions/[sessionId]/stream/route.ts b/autogpt_platform/frontend/src/app/api/chat/sessions/[sessionId]/stream/route.ts
index d63eed0ca2..6facf80c58 100644
--- a/autogpt_platform/frontend/src/app/api/chat/sessions/[sessionId]/stream/route.ts
+++ b/autogpt_platform/frontend/src/app/api/chat/sessions/[sessionId]/stream/route.ts
@@ -88,39 +88,27 @@ export async function POST(
}
/**
- * Legacy GET endpoint for backward compatibility
+ * Resume an active stream for a session.
+ *
+ * Called by the AI SDK's `useChat(resume: true)` on page load.
+ * Proxies to the backend which checks for an active stream and either
+ * replays it (200 + SSE) or returns 204 No Content.
*/
export async function GET(
- request: NextRequest,
+ _request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> },
) {
const { sessionId } = await params;
- const searchParams = request.nextUrl.searchParams;
- const message = searchParams.get("message");
- const isUserMessage = searchParams.get("is_user_message");
-
- if (!message) {
- return new Response("Missing message parameter", { status: 400 });
- }
try {
- // Get auth token from server-side session
const token = await getServerAuthToken();
- // Build backend URL
const backendUrl = environment.getAGPTServerBaseUrl();
const streamUrl = new URL(
`/api/chat/sessions/${sessionId}/stream`,
backendUrl,
);
- streamUrl.searchParams.set("message", message);
- // Pass is_user_message parameter if provided
- if (isUserMessage !== null) {
- streamUrl.searchParams.set("is_user_message", isUserMessage);
- }
-
- // Forward request to backend with auth header
const headers: Record = {
Accept: "text/event-stream",
"Cache-Control": "no-cache",
@@ -136,6 +124,11 @@ export async function GET(
headers,
});
+ // 204 = no active stream to resume
+ if (response.status === 204) {
+ return new Response(null, { status: 204 });
+ }
+
if (!response.ok) {
const error = await response.text();
return new Response(error, {
@@ -144,17 +137,17 @@ export async function GET(
});
}
- // Return the SSE stream directly
return new Response(response.body, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
+ "x-vercel-ai-ui-message-stream": "v1",
},
});
} catch (error) {
- console.error("SSE proxy error:", error);
+ console.error("Resume stream proxy error:", error);
return new Response(
JSON.stringify({
error: "Failed to connect to chat service",
diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json
index ccf5ad3e34..172419d27e 100644
--- a/autogpt_platform/frontend/src/app/api/openapi.json
+++ b/autogpt_platform/frontend/src/app/api/openapi.json
@@ -1018,6 +1018,58 @@
}
}
},
+ "/api/chat/schema/tool-responses": {
+ "get": {
+ "tags": ["v2", "chat", "chat"],
+ "summary": "[Dummy] Tool response type export for codegen",
+ "description": "This endpoint is not meant to be called. It exists solely to expose tool response models in the OpenAPI schema for frontend codegen.",
+ "operationId": "getV2[dummy] tool response type export for codegen",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ { "$ref": "#/components/schemas/AgentsFoundResponse" },
+ { "$ref": "#/components/schemas/NoResultsResponse" },
+ { "$ref": "#/components/schemas/AgentDetailsResponse" },
+ {
+ "$ref": "#/components/schemas/SetupRequirementsResponse"
+ },
+ { "$ref": "#/components/schemas/ExecutionStartedResponse" },
+ { "$ref": "#/components/schemas/NeedLoginResponse" },
+ { "$ref": "#/components/schemas/ErrorResponse" },
+ {
+ "$ref": "#/components/schemas/InputValidationErrorResponse"
+ },
+ { "$ref": "#/components/schemas/AgentOutputResponse" },
+ {
+ "$ref": "#/components/schemas/UnderstandingUpdatedResponse"
+ },
+ { "$ref": "#/components/schemas/AgentPreviewResponse" },
+ { "$ref": "#/components/schemas/AgentSavedResponse" },
+ {
+ "$ref": "#/components/schemas/ClarificationNeededResponse"
+ },
+ { "$ref": "#/components/schemas/BlockListResponse" },
+ { "$ref": "#/components/schemas/BlockOutputResponse" },
+ { "$ref": "#/components/schemas/DocSearchResultsResponse" },
+ { "$ref": "#/components/schemas/DocPageResponse" },
+ { "$ref": "#/components/schemas/OperationStartedResponse" },
+ { "$ref": "#/components/schemas/OperationPendingResponse" },
+ {
+ "$ref": "#/components/schemas/OperationInProgressResponse"
+ }
+ ],
+ "title": "Response Getv2[Dummy] Tool Response Type Export For Codegen"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/api/chat/sessions": {
"get": {
"tags": ["v2", "chat", "chat"],
@@ -1182,9 +1234,9 @@
"/api/chat/sessions/{session_id}/stream": {
"get": {
"tags": ["v2", "chat", "chat"],
- "summary": "Stream Chat Get",
- "description": "Stream chat responses for a session (GET - legacy endpoint).\n\nStreams the AI/completion responses in real time over Server-Sent Events (SSE), including:\n - Text fragments as they are generated\n - Tool call UI elements (if invoked)\n - Tool execution results\n\nArgs:\n session_id: The chat session identifier to associate with the streamed messages.\n message: The user's new message to process.\n user_id: Optional authenticated user ID.\n is_user_message: Whether the message is a user message.\nReturns:\n StreamingResponse: SSE-formatted response chunks.",
- "operationId": "getV2StreamChatGet",
+ "summary": "Resume Session Stream",
+ "description": "Resume an active stream for a session.\n\nCalled by the AI SDK's ``useChat(resume: true)`` on page load.\nChecks for an active (in-progress) task on the session and either replays\nthe full SSE stream or returns 204 No Content if nothing is running.\n\nArgs:\n session_id: The chat session identifier.\n user_id: Optional authenticated user ID.\n\nReturns:\n StreamingResponse (SSE) when an active stream exists,\n or 204 No Content when there is nothing to resume.",
+ "operationId": "getV2ResumeSessionStream",
"security": [{ "HTTPBearerJWT": [] }],
"parameters": [
{
@@ -1192,27 +1244,6 @@
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Session Id" }
- },
- {
- "name": "message",
- "in": "query",
- "required": true,
- "schema": {
- "type": "string",
- "minLength": 1,
- "maxLength": 10000,
- "title": "Message"
- }
- },
- {
- "name": "is_user_message",
- "in": "query",
- "required": false,
- "schema": {
- "type": "boolean",
- "default": true,
- "title": "Is User Message"
- }
}
],
"responses": {
@@ -6358,6 +6389,75 @@
"required": ["new_balance", "transaction_key"],
"title": "AddUserCreditsResponse"
},
+ "AgentDetails": {
+ "properties": {
+ "id": { "type": "string", "title": "Id" },
+ "name": { "type": "string", "title": "Name" },
+ "description": { "type": "string", "title": "Description" },
+ "in_library": {
+ "type": "boolean",
+ "title": "In Library",
+ "default": false
+ },
+ "inputs": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Inputs",
+ "default": {}
+ },
+ "credentials": {
+ "items": { "$ref": "#/components/schemas/CredentialsMetaInput" },
+ "type": "array",
+ "title": "Credentials",
+ "default": []
+ },
+ "execution_options": {
+ "$ref": "#/components/schemas/ExecutionOptions"
+ },
+ "trigger_info": {
+ "anyOf": [
+ { "additionalProperties": true, "type": "object" },
+ { "type": "null" }
+ ],
+ "title": "Trigger Info"
+ }
+ },
+ "type": "object",
+ "required": ["id", "name", "description"],
+ "title": "AgentDetails",
+ "description": "Detailed agent information."
+ },
+ "AgentDetailsResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "agent_details"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "agent": { "$ref": "#/components/schemas/AgentDetails" },
+ "user_authenticated": {
+ "type": "boolean",
+ "title": "User Authenticated",
+ "default": false
+ },
+ "graph_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Graph Id"
+ },
+ "graph_version": {
+ "anyOf": [{ "type": "integer" }, { "type": "null" }],
+ "title": "Graph Version"
+ }
+ },
+ "type": "object",
+ "required": ["message", "agent"],
+ "title": "AgentDetailsResponse",
+ "description": "Response for get_details action."
+ },
"AgentExecutionStatus": {
"type": "string",
"enum": [
@@ -6371,6 +6471,224 @@
],
"title": "AgentExecutionStatus"
},
+ "AgentInfo": {
+ "properties": {
+ "id": { "type": "string", "title": "Id" },
+ "name": { "type": "string", "title": "Name" },
+ "description": { "type": "string", "title": "Description" },
+ "source": {
+ "type": "string",
+ "title": "Source",
+ "description": "marketplace or library"
+ },
+ "in_library": {
+ "type": "boolean",
+ "title": "In Library",
+ "default": false
+ },
+ "creator": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Creator"
+ },
+ "category": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Category"
+ },
+ "rating": {
+ "anyOf": [{ "type": "number" }, { "type": "null" }],
+ "title": "Rating"
+ },
+ "runs": {
+ "anyOf": [{ "type": "integer" }, { "type": "null" }],
+ "title": "Runs"
+ },
+ "is_featured": {
+ "anyOf": [{ "type": "boolean" }, { "type": "null" }],
+ "title": "Is Featured"
+ },
+ "status": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Status"
+ },
+ "can_access_graph": {
+ "anyOf": [{ "type": "boolean" }, { "type": "null" }],
+ "title": "Can Access Graph"
+ },
+ "has_external_trigger": {
+ "anyOf": [{ "type": "boolean" }, { "type": "null" }],
+ "title": "Has External Trigger"
+ },
+ "new_output": {
+ "anyOf": [{ "type": "boolean" }, { "type": "null" }],
+ "title": "New Output"
+ },
+ "graph_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Graph Id"
+ },
+ "inputs": {
+ "anyOf": [
+ { "additionalProperties": true, "type": "object" },
+ { "type": "null" }
+ ],
+ "title": "Inputs",
+ "description": "Input schema for the agent, including field names, types, and defaults"
+ }
+ },
+ "type": "object",
+ "required": ["id", "name", "description", "source"],
+ "title": "AgentInfo",
+ "description": "Information about an agent."
+ },
+ "AgentOutputResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "agent_output"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "agent_name": { "type": "string", "title": "Agent Name" },
+ "agent_id": { "type": "string", "title": "Agent Id" },
+ "library_agent_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Library Agent Id"
+ },
+ "library_agent_link": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Library Agent Link"
+ },
+ "execution": {
+ "anyOf": [
+ { "$ref": "#/components/schemas/ExecutionOutputInfo" },
+ { "type": "null" }
+ ]
+ },
+ "available_executions": {
+ "anyOf": [
+ {
+ "items": { "additionalProperties": true, "type": "object" },
+ "type": "array"
+ },
+ { "type": "null" }
+ ],
+ "title": "Available Executions"
+ },
+ "total_executions": {
+ "type": "integer",
+ "title": "Total Executions",
+ "default": 0
+ }
+ },
+ "type": "object",
+ "required": ["message", "agent_name", "agent_id"],
+ "title": "AgentOutputResponse",
+ "description": "Response for agent_output tool."
+ },
+ "AgentPreviewResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "agent_preview"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "agent_json": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Agent Json"
+ },
+ "agent_name": { "type": "string", "title": "Agent Name" },
+ "description": { "type": "string", "title": "Description" },
+ "node_count": { "type": "integer", "title": "Node Count" },
+ "link_count": {
+ "type": "integer",
+ "title": "Link Count",
+ "default": 0
+ }
+ },
+ "type": "object",
+ "required": [
+ "message",
+ "agent_json",
+ "agent_name",
+ "description",
+ "node_count"
+ ],
+ "title": "AgentPreviewResponse",
+ "description": "Response for previewing a generated agent before saving."
+ },
+ "AgentSavedResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "agent_saved"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "agent_id": { "type": "string", "title": "Agent Id" },
+ "agent_name": { "type": "string", "title": "Agent Name" },
+ "library_agent_id": { "type": "string", "title": "Library Agent Id" },
+ "library_agent_link": {
+ "type": "string",
+ "title": "Library Agent Link"
+ },
+ "agent_page_link": { "type": "string", "title": "Agent Page Link" }
+ },
+ "type": "object",
+ "required": [
+ "message",
+ "agent_id",
+ "agent_name",
+ "library_agent_id",
+ "library_agent_link",
+ "agent_page_link"
+ ],
+ "title": "AgentSavedResponse",
+ "description": "Response when an agent is saved to the library."
+ },
+ "AgentsFoundResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "agents_found"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "title": {
+ "type": "string",
+ "title": "Title",
+ "default": "Available Agents"
+ },
+ "agents": {
+ "items": { "$ref": "#/components/schemas/AgentInfo" },
+ "type": "array",
+ "title": "Agents"
+ },
+ "count": { "type": "integer", "title": "Count" },
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "default": "agents_found"
+ }
+ },
+ "type": "object",
+ "required": ["message", "agents", "count"],
+ "title": "AgentsFoundResponse",
+ "description": "Response for find_agent tool."
+ },
"ApiResponse": {
"properties": {
"answer": { "type": "string", "title": "Answer" },
@@ -6691,6 +7009,120 @@
],
"title": "BlockInfo"
},
+ "BlockInfoSummary": {
+ "properties": {
+ "id": { "type": "string", "title": "Id" },
+ "name": { "type": "string", "title": "Name" },
+ "description": { "type": "string", "title": "Description" },
+ "categories": {
+ "items": { "type": "string" },
+ "type": "array",
+ "title": "Categories"
+ },
+ "input_schema": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Input Schema"
+ },
+ "output_schema": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Output Schema"
+ },
+ "required_inputs": {
+ "items": { "$ref": "#/components/schemas/BlockInputFieldInfo" },
+ "type": "array",
+ "title": "Required Inputs",
+ "description": "List of required input fields for this block"
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "description",
+ "categories",
+ "input_schema",
+ "output_schema"
+ ],
+ "title": "BlockInfoSummary",
+ "description": "Summary of a block for search results."
+ },
+ "BlockInputFieldInfo": {
+ "properties": {
+ "name": { "type": "string", "title": "Name" },
+ "type": { "type": "string", "title": "Type" },
+ "description": {
+ "type": "string",
+ "title": "Description",
+ "default": ""
+ },
+ "required": {
+ "type": "boolean",
+ "title": "Required",
+ "default": false
+ },
+ "default": { "anyOf": [{}, { "type": "null" }], "title": "Default" }
+ },
+ "type": "object",
+ "required": ["name", "type"],
+ "title": "BlockInputFieldInfo",
+ "description": "Information about a block input field."
+ },
+ "BlockListResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "block_list"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "blocks": {
+ "items": { "$ref": "#/components/schemas/BlockInfoSummary" },
+ "type": "array",
+ "title": "Blocks"
+ },
+ "count": { "type": "integer", "title": "Count" },
+ "query": { "type": "string", "title": "Query" },
+ "usage_hint": {
+ "type": "string",
+ "title": "Usage Hint",
+ "default": "To execute a block, call run_block with block_id set to the block's 'id' field and input_data containing the required fields from input_schema."
+ }
+ },
+ "type": "object",
+ "required": ["message", "blocks", "count", "query"],
+ "title": "BlockListResponse",
+ "description": "Response for find_block tool."
+ },
+ "BlockOutputResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "block_output"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "block_id": { "type": "string", "title": "Block Id" },
+ "block_name": { "type": "string", "title": "Block Name" },
+ "outputs": {
+ "additionalProperties": { "items": {}, "type": "array" },
+ "type": "object",
+ "title": "Outputs"
+ },
+ "success": { "type": "boolean", "title": "Success", "default": true }
+ },
+ "type": "object",
+ "required": ["message", "block_id", "block_name", "outputs"],
+ "title": "BlockOutputResponse",
+ "description": "Response for run_block tool."
+ },
"BlockResponse": {
"properties": {
"blocks": {
@@ -6937,6 +7369,42 @@
"required": ["query", "conversation_history", "message_id"],
"title": "ChatRequest"
},
+ "ClarificationNeededResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "clarification_needed"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "questions": {
+ "items": { "$ref": "#/components/schemas/ClarifyingQuestion" },
+ "type": "array",
+ "title": "Questions"
+ }
+ },
+ "type": "object",
+ "required": ["message"],
+ "title": "ClarificationNeededResponse",
+ "description": "Response when the LLM needs more information from the user."
+ },
+ "ClarifyingQuestion": {
+ "properties": {
+ "question": { "type": "string", "title": "Question" },
+ "keyword": { "type": "string", "title": "Keyword" },
+ "example": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Example"
+ }
+ },
+ "type": "object",
+ "required": ["question", "keyword"],
+ "title": "ClarifyingQuestion",
+ "description": "A question that needs user clarification."
+ },
"CountResponse": {
"properties": {
"all_blocks": { "type": "integer", "title": "All Blocks" },
@@ -7195,6 +7663,71 @@
"required": ["version_counts"],
"title": "DeleteGraphResponse"
},
+ "DocPageResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "doc_page"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "title": { "type": "string", "title": "Title" },
+ "path": { "type": "string", "title": "Path" },
+ "content": { "type": "string", "title": "Content" },
+ "doc_url": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Doc Url"
+ }
+ },
+ "type": "object",
+ "required": ["message", "title", "path", "content"],
+ "title": "DocPageResponse",
+ "description": "Response for get_doc_page tool."
+ },
+ "DocSearchResult": {
+ "properties": {
+ "title": { "type": "string", "title": "Title" },
+ "path": { "type": "string", "title": "Path" },
+ "section": { "type": "string", "title": "Section" },
+ "snippet": { "type": "string", "title": "Snippet" },
+ "score": { "type": "number", "title": "Score" },
+ "doc_url": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Doc Url"
+ }
+ },
+ "type": "object",
+ "required": ["title", "path", "section", "snippet", "score"],
+ "title": "DocSearchResult",
+ "description": "A single documentation search result."
+ },
+ "DocSearchResultsResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "doc_search_results"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "results": {
+ "items": { "$ref": "#/components/schemas/DocSearchResult" },
+ "type": "array",
+ "title": "Results"
+ },
+ "count": { "type": "integer", "title": "Count" },
+ "query": { "type": "string", "title": "Query" }
+ },
+ "type": "object",
+ "required": ["message", "results", "count", "query"],
+ "title": "DocSearchResultsResponse",
+ "description": "Response for search_docs tool."
+ },
"Document": {
"properties": {
"url": { "type": "string", "title": "Url" },
@@ -7204,6 +7737,34 @@
"required": ["url", "relevance_score"],
"title": "Document"
},
+ "ErrorResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "error"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "error": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Error"
+ },
+ "details": {
+ "anyOf": [
+ { "additionalProperties": true, "type": "object" },
+ { "type": "null" }
+ ],
+ "title": "Details"
+ }
+ },
+ "type": "object",
+ "required": ["message"],
+ "title": "ErrorResponse",
+ "description": "Response for errors."
+ },
"ExecutionAnalyticsConfig": {
"properties": {
"available_models": {
@@ -7380,6 +7941,85 @@
],
"title": "ExecutionAnalyticsResult"
},
+ "ExecutionOptions": {
+ "properties": {
+ "manual": { "type": "boolean", "title": "Manual", "default": true },
+ "scheduled": {
+ "type": "boolean",
+ "title": "Scheduled",
+ "default": true
+ },
+ "webhook": { "type": "boolean", "title": "Webhook", "default": false }
+ },
+ "type": "object",
+ "title": "ExecutionOptions",
+ "description": "Available execution options for an agent."
+ },
+ "ExecutionOutputInfo": {
+ "properties": {
+ "execution_id": { "type": "string", "title": "Execution Id" },
+ "status": { "type": "string", "title": "Status" },
+ "started_at": {
+ "anyOf": [
+ { "type": "string", "format": "date-time" },
+ { "type": "null" }
+ ],
+ "title": "Started At"
+ },
+ "ended_at": {
+ "anyOf": [
+ { "type": "string", "format": "date-time" },
+ { "type": "null" }
+ ],
+ "title": "Ended At"
+ },
+ "outputs": {
+ "additionalProperties": { "items": {}, "type": "array" },
+ "type": "object",
+ "title": "Outputs"
+ },
+ "inputs_summary": {
+ "anyOf": [
+ { "additionalProperties": true, "type": "object" },
+ { "type": "null" }
+ ],
+ "title": "Inputs Summary"
+ }
+ },
+ "type": "object",
+ "required": ["execution_id", "status", "outputs"],
+ "title": "ExecutionOutputInfo",
+ "description": "Summary of a single execution's outputs."
+ },
+ "ExecutionStartedResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "execution_started"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "execution_id": { "type": "string", "title": "Execution Id" },
+ "graph_id": { "type": "string", "title": "Graph Id" },
+ "graph_name": { "type": "string", "title": "Graph Name" },
+ "library_agent_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Library Agent Id"
+ },
+ "library_agent_link": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Library Agent Link"
+ },
+ "status": { "type": "string", "title": "Status", "default": "QUEUED" }
+ },
+ "type": "object",
+ "required": ["message", "execution_id", "graph_id", "graph_name"],
+ "title": "ExecutionStartedResponse",
+ "description": "Response for run/schedule actions."
+ },
"Graph": {
"properties": {
"id": { "type": "string", "title": "Id" },
@@ -8131,6 +8771,43 @@
"required": ["provider", "host"],
"title": "HostScopedCredentials"
},
+ "InputValidationErrorResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "input_validation_error"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "unrecognized_fields": {
+ "items": { "type": "string" },
+ "type": "array",
+ "title": "Unrecognized Fields",
+ "description": "List of input field names that were not recognized"
+ },
+ "inputs": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Inputs",
+ "description": "The agent's valid input schema for reference"
+ },
+ "graph_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Graph Id"
+ },
+ "graph_version": {
+ "anyOf": [{ "type": "integer" }, { "type": "null" }],
+ "title": "Graph Version"
+ }
+ },
+ "type": "object",
+ "required": ["message", "unrecognized_fields", "inputs"],
+ "title": "InputValidationErrorResponse",
+ "description": "Response when run_agent receives unknown input fields."
+ },
"LibraryAgent": {
"properties": {
"id": { "type": "string", "title": "Id" },
@@ -8649,6 +9326,54 @@
"required": ["agents", "pagination"],
"title": "MyAgentsResponse"
},
+ "NeedLoginResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "need_login"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "agent_info": {
+ "anyOf": [
+ { "additionalProperties": true, "type": "object" },
+ { "type": "null" }
+ ],
+ "title": "Agent Info"
+ }
+ },
+ "type": "object",
+ "required": ["message"],
+ "title": "NeedLoginResponse",
+ "description": "Response when login is needed."
+ },
+ "NoResultsResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "no_results"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "suggestions": {
+ "items": { "type": "string" },
+ "type": "array",
+ "title": "Suggestions",
+ "default": []
+ },
+ "name": { "type": "string", "title": "Name", "default": "no_results" }
+ },
+ "type": "object",
+ "required": ["message"],
+ "title": "NoResultsResponse",
+ "description": "Response when no agents found."
+ },
"Node": {
"properties": {
"id": { "type": "string", "title": "Id" },
@@ -9058,6 +9783,66 @@
"title": "OperationCompleteRequest",
"description": "Request model for external completion webhook."
},
+ "OperationInProgressResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "operation_in_progress"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "tool_call_id": { "type": "string", "title": "Tool Call Id" }
+ },
+ "type": "object",
+ "required": ["message", "tool_call_id"],
+ "title": "OperationInProgressResponse",
+ "description": "Response when an operation is already in progress.\n\nReturned for idempotency when the same tool_call_id is requested again\nwhile the background task is still running."
+ },
+ "OperationPendingResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "operation_pending"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "operation_id": { "type": "string", "title": "Operation Id" },
+ "tool_name": { "type": "string", "title": "Tool Name" }
+ },
+ "type": "object",
+ "required": ["message", "operation_id", "tool_name"],
+ "title": "OperationPendingResponse",
+ "description": "Response stored in chat history while a long-running operation is executing.\n\nThis is persisted to the database so users see a pending state when they\nrefresh before the operation completes."
+ },
+ "OperationStartedResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "operation_started"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "operation_id": { "type": "string", "title": "Operation Id" },
+ "tool_name": { "type": "string", "title": "Tool Name" },
+ "task_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Task Id"
+ }
+ },
+ "type": "object",
+ "required": ["message", "operation_id", "tool_name"],
+ "title": "OperationStartedResponse",
+ "description": "Response when a long-running operation has been started in the background.\n\nThis is returned immediately to the client while the operation continues\nto execute. The user can close the tab and check back later.\n\nThe task_id can be used to reconnect to the SSE stream via\nGET /chat/tasks/{task_id}/stream?last_idx=0"
+ },
"Pagination": {
"properties": {
"total_items": {
@@ -9689,6 +10474,38 @@
"required": ["credit_amount"],
"title": "RequestTopUp"
},
+ "ResponseType": {
+ "type": "string",
+ "enum": [
+ "agents_found",
+ "agent_details",
+ "setup_requirements",
+ "execution_started",
+ "need_login",
+ "error",
+ "no_results",
+ "agent_output",
+ "understanding_updated",
+ "agent_preview",
+ "agent_saved",
+ "clarification_needed",
+ "block_list",
+ "block_output",
+ "doc_search_results",
+ "doc_page",
+ "workspace_file_list",
+ "workspace_file_content",
+ "workspace_file_metadata",
+ "workspace_file_written",
+ "workspace_file_deleted",
+ "operation_started",
+ "operation_pending",
+ "operation_in_progress",
+ "input_validation_error"
+ ],
+ "title": "ResponseType",
+ "description": "Types of tool responses."
+ },
"ReviewItem": {
"properties": {
"node_exec_id": {
@@ -9952,6 +10769,48 @@
"required": ["active_graph_version"],
"title": "SetGraphActiveVersion"
},
+ "SetupInfo": {
+ "properties": {
+ "agent_id": { "type": "string", "title": "Agent Id" },
+ "agent_name": { "type": "string", "title": "Agent Name" },
+ "requirements": {
+ "additionalProperties": { "items": {}, "type": "array" },
+ "type": "object",
+ "title": "Requirements"
+ },
+ "user_readiness": { "$ref": "#/components/schemas/UserReadiness" }
+ },
+ "type": "object",
+ "required": ["agent_id", "agent_name"],
+ "title": "SetupInfo",
+ "description": "Complete setup information."
+ },
+ "SetupRequirementsResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "setup_requirements"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "setup_info": { "$ref": "#/components/schemas/SetupInfo" },
+ "graph_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Graph Id"
+ },
+ "graph_version": {
+ "anyOf": [{ "type": "integer" }, { "type": "null" }],
+ "title": "Graph Version"
+ }
+ },
+ "type": "object",
+ "required": ["message", "setup_info"],
+ "title": "SetupRequirementsResponse",
+ "description": "Response for validate action."
+ },
"ShareRequest": {
"properties": {},
"type": "object",
@@ -11348,6 +12207,33 @@
"required": ["name", "graph_id", "graph_version", "trigger_config"],
"title": "TriggeredPresetSetupRequest"
},
+ "UnderstandingUpdatedResponse": {
+ "properties": {
+ "type": {
+ "$ref": "#/components/schemas/ResponseType",
+ "default": "understanding_updated"
+ },
+ "message": { "type": "string", "title": "Message" },
+ "session_id": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Session Id"
+ },
+ "updated_fields": {
+ "items": { "type": "string" },
+ "type": "array",
+ "title": "Updated Fields"
+ },
+ "current_understanding": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Current Understanding"
+ }
+ },
+ "type": "object",
+ "required": ["message"],
+ "title": "UnderstandingUpdatedResponse",
+ "description": "Response for add_understanding tool."
+ },
"UnifiedSearchResponse": {
"properties": {
"results": {
@@ -12226,6 +13112,29 @@
"required": ["provider", "username", "password"],
"title": "UserPasswordCredentials"
},
+ "UserReadiness": {
+ "properties": {
+ "has_all_credentials": {
+ "type": "boolean",
+ "title": "Has All Credentials",
+ "default": false
+ },
+ "missing_credentials": {
+ "additionalProperties": true,
+ "type": "object",
+ "title": "Missing Credentials",
+ "default": {}
+ },
+ "ready_to_run": {
+ "type": "boolean",
+ "title": "Ready To Run",
+ "default": false
+ }
+ },
+ "type": "object",
+ "title": "UserReadiness",
+ "description": "User readiness status."
+ },
"UserTransaction": {
"properties": {
"transaction_key": {
diff --git a/autogpt_platform/frontend/src/app/globals.css b/autogpt_platform/frontend/src/app/globals.css
index 1f782f753b..dd1d17cde7 100644
--- a/autogpt_platform/frontend/src/app/globals.css
+++ b/autogpt_platform/frontend/src/app/globals.css
@@ -1,6 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+@source "../node_modules/streamdown/dist/*.js";
@layer base {
:root {
@@ -29,6 +30,14 @@
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
@@ -56,6 +65,14 @@
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 224.3 76.3% 48%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
* {
diff --git a/autogpt_platform/frontend/src/components/ai-elements/conversation.tsx b/autogpt_platform/frontend/src/components/ai-elements/conversation.tsx
new file mode 100644
index 0000000000..92e940c715
--- /dev/null
+++ b/autogpt_platform/frontend/src/components/ai-elements/conversation.tsx
@@ -0,0 +1,109 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { scrollbarStyles } from "@/components/styles/scrollbars";
+import { cn } from "@/lib/utils";
+import { ArrowDownIcon } from "lucide-react";
+import type { ComponentProps } from "react";
+import { useCallback } from "react";
+import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
+
+export type ConversationProps = ComponentProps;
+
+export const Conversation = ({ className, ...props }: ConversationProps) => (
+
+);
+
+export type ConversationContentProps = ComponentProps<
+ typeof StickToBottom.Content
+>;
+
+export const ConversationContent = ({
+ className,
+ ...props
+}: ConversationContentProps) => (
+
+);
+
+export type ConversationEmptyStateProps = ComponentProps<"div"> & {
+ title?: string;
+ description?: string;
+ icon?: React.ReactNode;
+};
+
+export const ConversationEmptyState = ({
+ className,
+ title = "No messages yet",
+ description = "Start a conversation to see messages here",
+ icon,
+ children,
+ ...props
+}: ConversationEmptyStateProps) => (
+
+ {children ?? (
+ <>
+ {icon && (
+
{icon}
+ )}
+
+
{title}
+ {description && (
+
+ {description}
+
+ )}
+
+ >
+ )}
+
+);
+
+export type ConversationScrollButtonProps = ComponentProps;
+
+export const ConversationScrollButton = ({
+ className,
+ ...props
+}: ConversationScrollButtonProps) => {
+ const { isAtBottom, scrollToBottom } = useStickToBottomContext();
+
+ const handleScrollToBottom = useCallback(() => {
+ scrollToBottom();
+ }, [scrollToBottom]);
+
+ return (
+ !isAtBottom && (
+
+
+
+ )
+ );
+};
diff --git a/autogpt_platform/frontend/src/components/ai-elements/message.tsx b/autogpt_platform/frontend/src/components/ai-elements/message.tsx
new file mode 100644
index 0000000000..5cc330e57c
--- /dev/null
+++ b/autogpt_platform/frontend/src/components/ai-elements/message.tsx
@@ -0,0 +1,338 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { cn } from "@/lib/utils";
+import { cjk } from "@streamdown/cjk";
+import { code } from "@streamdown/code";
+import { math } from "@streamdown/math";
+import { mermaid } from "@streamdown/mermaid";
+import type { UIMessage } from "ai";
+import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
+import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
+import { createContext, memo, useContext, useEffect, useState } from "react";
+import { Streamdown } from "streamdown";
+
+export type MessageProps = HTMLAttributes & {
+ from: UIMessage["role"];
+};
+
+export const Message = ({ className, from, ...props }: MessageProps) => (
+
+);
+
+export type MessageContentProps = HTMLAttributes;
+
+export const MessageContent = ({
+ children,
+ className,
+ ...props
+}: MessageContentProps) => (
+
+ {children}
+
+);
+
+export type MessageActionsProps = ComponentProps<"div">;
+
+export const MessageActions = ({
+ className,
+ children,
+ ...props
+}: MessageActionsProps) => (
+
+ {children}
+
+);
+
+export type MessageActionProps = ComponentProps & {
+ tooltip?: string;
+ label?: string;
+};
+
+export const MessageAction = ({
+ tooltip,
+ children,
+ label,
+ variant = "ghost",
+ size = "icon-sm",
+ ...props
+}: MessageActionProps) => {
+ const button = (
+
+ {children}
+ {label || tooltip}
+
+ );
+
+ if (tooltip) {
+ return (
+
+
+ {button}
+
+ {tooltip}
+
+
+
+ );
+ }
+
+ return button;
+};
+
+interface MessageBranchContextType {
+ currentBranch: number;
+ totalBranches: number;
+ goToPrevious: () => void;
+ goToNext: () => void;
+ branches: ReactElement[];
+ setBranches: (branches: ReactElement[]) => void;
+}
+
+const MessageBranchContext = createContext(
+ null,
+);
+
+const useMessageBranch = () => {
+ const context = useContext(MessageBranchContext);
+
+ if (!context) {
+ throw new Error("MessageBranch components must be used within");
+ }
+
+ return context;
+};
+
+export type MessageBranchProps = HTMLAttributes & {
+ defaultBranch?: number;
+ onBranchChange?: (branchIndex: number) => void;
+};
+
+export const MessageBranch = ({
+ defaultBranch = 0,
+ onBranchChange,
+ className,
+ ...props
+}: MessageBranchProps) => {
+ const [currentBranch, setCurrentBranch] = useState(defaultBranch);
+ const [branches, setBranches] = useState([]);
+
+ const handleBranchChange = (newBranch: number) => {
+ setCurrentBranch(newBranch);
+ onBranchChange?.(newBranch);
+ };
+
+ const goToPrevious = () => {
+ const newBranch =
+ currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
+ handleBranchChange(newBranch);
+ };
+
+ const goToNext = () => {
+ const newBranch =
+ currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
+ handleBranchChange(newBranch);
+ };
+
+ const contextValue: MessageBranchContextType = {
+ currentBranch,
+ totalBranches: branches.length,
+ goToPrevious,
+ goToNext,
+ branches,
+ setBranches,
+ };
+
+ return (
+
+ div]:pb-0", className)}
+ {...props}
+ />
+
+ );
+};
+
+export type MessageBranchContentProps = HTMLAttributes
;
+
+export const MessageBranchContent = ({
+ children,
+ ...props
+}: MessageBranchContentProps) => {
+ const { currentBranch, setBranches, branches } = useMessageBranch();
+ const childrenArray = Array.isArray(children) ? children : [children];
+
+ // Use useEffect to update branches when they change
+ useEffect(() => {
+ if (branches.length !== childrenArray.length) {
+ setBranches(childrenArray);
+ }
+ }, [childrenArray, branches, setBranches]);
+
+ return childrenArray.map((branch, index) => (
+ div]:pb-0",
+ index === currentBranch ? "block" : "hidden",
+ )}
+ key={branch.key}
+ {...props}
+ >
+ {branch}
+
+ ));
+};
+
+export type MessageBranchSelectorProps = HTMLAttributes & {
+ from: UIMessage["role"];
+};
+
+export const MessageBranchSelector = ({
+ className,
+ from: _from,
+ ...props
+}: MessageBranchSelectorProps) => {
+ const { totalBranches } = useMessageBranch();
+
+ // Don't render if there's only one branch
+ if (totalBranches <= 1) {
+ return null;
+ }
+
+ return (
+ *:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",
+ className,
+ )}
+ orientation="horizontal"
+ {...props}
+ />
+ );
+};
+
+export type MessageBranchPreviousProps = ComponentProps;
+
+export const MessageBranchPrevious = ({
+ children,
+ ...props
+}: MessageBranchPreviousProps) => {
+ const { goToPrevious, totalBranches } = useMessageBranch();
+
+ return (
+
+ {children ?? }
+
+ );
+};
+
+export type MessageBranchNextProps = ComponentProps;
+
+export const MessageBranchNext = ({
+ children,
+ ...props
+}: MessageBranchNextProps) => {
+ const { goToNext, totalBranches } = useMessageBranch();
+
+ return (
+
+ {children ?? }
+
+ );
+};
+
+export type MessageBranchPageProps = HTMLAttributes;
+
+export const MessageBranchPage = ({
+ className,
+ ...props
+}: MessageBranchPageProps) => {
+ const { currentBranch, totalBranches } = useMessageBranch();
+
+ return (
+
+ {currentBranch + 1} of {totalBranches}
+
+ );
+};
+
+export type MessageResponseProps = ComponentProps;
+
+export const MessageResponse = memo(
+ ({ className, ...props }: MessageResponseProps) => (
+ *:first-child]:mt-0 [&>*:last-child]:mb-0 [&_pre]:!bg-white",
+ className,
+ )}
+ plugins={{ code, mermaid, math, cjk }}
+ {...props}
+ />
+ ),
+ (prevProps, nextProps) => prevProps.children === nextProps.children,
+);
+
+MessageResponse.displayName = "MessageResponse";
+
+export type MessageToolbarProps = ComponentProps<"div">;
+
+export const MessageToolbar = ({
+ className,
+ children,
+ ...props
+}: MessageToolbarProps) => (
+
+ {children}
+
+);
diff --git a/autogpt_platform/frontend/src/components/atoms/OverflowText/OverflowText.tsx b/autogpt_platform/frontend/src/components/atoms/OverflowText/OverflowText.tsx
index efc345f79c..b118cc5aa0 100644
--- a/autogpt_platform/frontend/src/components/atoms/OverflowText/OverflowText.tsx
+++ b/autogpt_platform/frontend/src/components/atoms/OverflowText/OverflowText.tsx
@@ -77,7 +77,7 @@ export function OverflowText(props: Props) {
"block min-w-0 overflow-hidden text-ellipsis whitespace-nowrap",
)}
>
-
+
{value}
diff --git a/autogpt_platform/frontend/src/components/atoms/Text/Text.tsx b/autogpt_platform/frontend/src/components/atoms/Text/Text.tsx
index 8bae184e5b..86c39b6436 100644
--- a/autogpt_platform/frontend/src/components/atoms/Text/Text.tsx
+++ b/autogpt_platform/frontend/src/components/atoms/Text/Text.tsx
@@ -1,4 +1,5 @@
import React from "react";
+import { cn } from "@/lib/utils";
import { As, Variant, variantElementMap, variants } from "./helpers";
type CustomProps = {
@@ -22,7 +23,7 @@ export function Text({
}: TextProps) {
const variantClasses = variants[size || variant] || variants.body;
const Element = outerAs || variantElementMap[variant];
- const combinedClassName = `${variantClasses} ${className}`.trim();
+ const combinedClassName = cn(variantClasses, className);
return React.createElement(
Element,
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/Chat.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/Chat.tsx
deleted file mode 100644
index da454150bf..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/Chat.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-"use client";
-
-import { useCopilotSessionId } from "@/app/(platform)/copilot/useCopilotSessionId";
-import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-import { useEffect, useRef } from "react";
-import { ChatContainer } from "./components/ChatContainer/ChatContainer";
-import { ChatErrorState } from "./components/ChatErrorState/ChatErrorState";
-import { useChat } from "./useChat";
-
-export interface ChatProps {
- className?: string;
- initialPrompt?: string;
- onSessionNotFound?: () => void;
- onStreamingChange?: (isStreaming: boolean) => void;
-}
-
-export function Chat({
- className,
- initialPrompt,
- onSessionNotFound,
- onStreamingChange,
-}: ChatProps) {
- const { urlSessionId } = useCopilotSessionId();
- const hasHandledNotFoundRef = useRef(false);
- const {
- session,
- messages,
- isLoading,
- isCreating,
- error,
- isSessionNotFound,
- sessionId,
- createSession,
- showLoader,
- startPollingForOperation,
- } = useChat({ urlSessionId });
-
- // Extract active stream info for reconnection
- const activeStream = (
- session as {
- active_stream?: {
- task_id: string;
- last_message_id: string;
- operation_id: string;
- tool_name: string;
- };
- }
- )?.active_stream;
-
- useEffect(() => {
- if (!onSessionNotFound) return;
- if (!urlSessionId) return;
- if (!isSessionNotFound || isLoading || isCreating) return;
- if (hasHandledNotFoundRef.current) return;
- hasHandledNotFoundRef.current = true;
- onSessionNotFound();
- }, [
- onSessionNotFound,
- urlSessionId,
- isSessionNotFound,
- isLoading,
- isCreating,
- ]);
-
- const shouldShowLoader = showLoader && (isLoading || isCreating);
-
- return (
-
- {/* Main Content */}
-
- {/* Loading State */}
- {shouldShowLoader && (
-
-
-
-
- Loading your chat...
-
-
-
- )}
-
- {/* Error State */}
- {error && !isLoading && (
-
- )}
-
- {/* Session Content */}
- {sessionId && !isLoading && !error && (
-
- )}
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/SSE_RECONNECTION.md b/autogpt_platform/frontend/src/components/contextual/Chat/SSE_RECONNECTION.md
deleted file mode 100644
index 9e78679f4e..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/SSE_RECONNECTION.md
+++ /dev/null
@@ -1,159 +0,0 @@
-# SSE Reconnection Contract for Long-Running Operations
-
-This document describes the client-side contract for handling SSE (Server-Sent Events) disconnections and reconnecting to long-running background tasks.
-
-## Overview
-
-When a user triggers a long-running operation (like agent generation), the backend:
-
-1. Spawns a background task that survives SSE disconnections
-2. Returns an `operation_started` response with a `task_id`
-3. Stores stream messages in Redis Streams for replay
-
-Clients can reconnect to the task stream at any time to receive missed messages.
-
-## Client-Side Flow
-
-### 1. Receiving Operation Started
-
-When you receive an `operation_started` tool response:
-
-```typescript
-// The response includes a task_id for reconnection
-{
- type: "operation_started",
- tool_name: "generate_agent",
- operation_id: "uuid-...",
- task_id: "task-uuid-...", // <-- Store this for reconnection
- message: "Operation started. You can close this tab."
-}
-```
-
-### 2. Storing Task Info
-
-Use the chat store to track the active task:
-
-```typescript
-import { useChatStore } from "./chat-store";
-
-// When operation_started is received:
-useChatStore.getState().setActiveTask(sessionId, {
- taskId: response.task_id,
- operationId: response.operation_id,
- toolName: response.tool_name,
- lastMessageId: "0",
-});
-```
-
-### 3. Reconnecting to a Task
-
-To reconnect (e.g., after page refresh or tab reopen):
-
-```typescript
-const { reconnectToTask, getActiveTask } = useChatStore.getState();
-
-// Check if there's an active task for this session
-const activeTask = getActiveTask(sessionId);
-
-if (activeTask) {
- // Reconnect to the task stream
- await reconnectToTask(
- sessionId,
- activeTask.taskId,
- activeTask.lastMessageId, // Resume from last position
- (chunk) => {
- // Handle incoming chunks
- console.log("Received chunk:", chunk);
- },
- );
-}
-```
-
-### 4. Tracking Message Position
-
-To enable precise replay, update the last message ID as chunks arrive:
-
-```typescript
-const { updateTaskLastMessageId } = useChatStore.getState();
-
-function handleChunk(chunk: StreamChunk) {
- // If chunk has an index/id, track it
- if (chunk.idx !== undefined) {
- updateTaskLastMessageId(sessionId, String(chunk.idx));
- }
-}
-```
-
-## API Endpoints
-
-### Task Stream Reconnection
-
-```
-GET /api/chat/tasks/{taskId}/stream?last_message_id={idx}
-```
-
-- `taskId`: The task ID from `operation_started`
-- `last_message_id`: Last received message index (default: "0" for full replay)
-
-Returns: SSE stream of missed messages + live updates
-
-## Chunk Types
-
-The reconnected stream follows the same Vercel AI SDK protocol:
-
-| Type | Description |
-| ----------------------- | ----------------------- |
-| `start` | Message lifecycle start |
-| `text-delta` | Streaming text content |
-| `text-end` | Text block completed |
-| `tool-output-available` | Tool result available |
-| `finish` | Stream completed |
-| `error` | Error occurred |
-
-## Error Handling
-
-If reconnection fails:
-
-1. Check if task still exists (may have expired - default TTL: 1 hour)
-2. Fall back to polling the session for final state
-3. Show appropriate UI message to user
-
-## Persistence Considerations
-
-For robust reconnection across browser restarts:
-
-```typescript
-// Store in localStorage/sessionStorage
-const ACTIVE_TASKS_KEY = "chat_active_tasks";
-
-function persistActiveTask(sessionId: string, task: ActiveTaskInfo) {
- const tasks = JSON.parse(localStorage.getItem(ACTIVE_TASKS_KEY) || "{}");
- tasks[sessionId] = task;
- localStorage.setItem(ACTIVE_TASKS_KEY, JSON.stringify(tasks));
-}
-
-function loadPersistedTasks(): Record {
- return JSON.parse(localStorage.getItem(ACTIVE_TASKS_KEY) || "{}");
-}
-```
-
-## Backend Configuration
-
-The following backend settings affect reconnection behavior:
-
-| Setting | Default | Description |
-| ------------------- | ------- | ---------------------------------- |
-| `stream_ttl` | 3600s | How long streams are kept in Redis |
-| `stream_max_length` | 1000 | Max messages per stream |
-
-## Testing
-
-To test reconnection locally:
-
-1. Start a long-running operation (e.g., agent generation)
-2. Note the `task_id` from the `operation_started` response
-3. Close the browser tab
-4. Reopen and call `reconnectToTask` with the saved `task_id`
-5. Verify that missed messages are replayed
-
-See the main README for full local development setup.
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/chat-constants.ts b/autogpt_platform/frontend/src/components/contextual/Chat/chat-constants.ts
deleted file mode 100644
index 8802de2155..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/chat-constants.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Constants for the chat system.
- *
- * Centralizes magic strings and values used across chat components.
- */
-
-// LocalStorage keys
-export const STORAGE_KEY_ACTIVE_TASKS = "chat_active_tasks";
-
-// Redis Stream IDs
-export const INITIAL_MESSAGE_ID = "0";
-export const INITIAL_STREAM_ID = "0-0";
-
-// TTL values (in milliseconds)
-export const COMPLETED_STREAM_TTL_MS = 5 * 60 * 1000; // 5 minutes
-export const ACTIVE_TASK_TTL_MS = 60 * 60 * 1000; // 1 hour
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/chat-store.ts b/autogpt_platform/frontend/src/components/contextual/Chat/chat-store.ts
deleted file mode 100644
index 3083f65d2c..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/chat-store.ts
+++ /dev/null
@@ -1,501 +0,0 @@
-"use client";
-
-import { create } from "zustand";
-import {
- ACTIVE_TASK_TTL_MS,
- COMPLETED_STREAM_TTL_MS,
- INITIAL_STREAM_ID,
- STORAGE_KEY_ACTIVE_TASKS,
-} from "./chat-constants";
-import type {
- ActiveStream,
- StreamChunk,
- StreamCompleteCallback,
- StreamResult,
- StreamStatus,
-} from "./chat-types";
-import { executeStream, executeTaskReconnect } from "./stream-executor";
-
-export interface ActiveTaskInfo {
- taskId: string;
- sessionId: string;
- operationId: string;
- toolName: string;
- lastMessageId: string;
- startedAt: number;
-}
-
-/** Load active tasks from localStorage */
-function loadPersistedTasks(): Map {
- if (typeof window === "undefined") return new Map();
- try {
- const stored = localStorage.getItem(STORAGE_KEY_ACTIVE_TASKS);
- if (!stored) return new Map();
- const parsed = JSON.parse(stored) as Record;
- const now = Date.now();
- const tasks = new Map();
- // Filter out expired tasks
- for (const [sessionId, task] of Object.entries(parsed)) {
- if (now - task.startedAt < ACTIVE_TASK_TTL_MS) {
- tasks.set(sessionId, task);
- }
- }
- return tasks;
- } catch {
- return new Map();
- }
-}
-
-/** Save active tasks to localStorage */
-function persistTasks(tasks: Map): void {
- if (typeof window === "undefined") return;
- try {
- const obj: Record = {};
- for (const [sessionId, task] of tasks) {
- obj[sessionId] = task;
- }
- localStorage.setItem(STORAGE_KEY_ACTIVE_TASKS, JSON.stringify(obj));
- } catch {
- // Ignore storage errors
- }
-}
-
-interface ChatStoreState {
- activeStreams: Map;
- completedStreams: Map;
- activeSessions: Set;
- streamCompleteCallbacks: Set;
- /** Active tasks for SSE reconnection - keyed by sessionId */
- activeTasks: Map;
-}
-
-interface ChatStoreActions {
- startStream: (
- sessionId: string,
- message: string,
- isUserMessage: boolean,
- context?: { url: string; content: string },
- onChunk?: (chunk: StreamChunk) => void,
- ) => Promise;
- stopStream: (sessionId: string) => void;
- subscribeToStream: (
- sessionId: string,
- onChunk: (chunk: StreamChunk) => void,
- skipReplay?: boolean,
- ) => () => void;
- getStreamStatus: (sessionId: string) => StreamStatus;
- getCompletedStream: (sessionId: string) => StreamResult | undefined;
- clearCompletedStream: (sessionId: string) => void;
- isStreaming: (sessionId: string) => boolean;
- registerActiveSession: (sessionId: string) => void;
- unregisterActiveSession: (sessionId: string) => void;
- isSessionActive: (sessionId: string) => boolean;
- onStreamComplete: (callback: StreamCompleteCallback) => () => void;
- /** Track active task for SSE reconnection */
- setActiveTask: (
- sessionId: string,
- taskInfo: Omit,
- ) => void;
- /** Get active task for a session */
- getActiveTask: (sessionId: string) => ActiveTaskInfo | undefined;
- /** Clear active task when operation completes */
- clearActiveTask: (sessionId: string) => void;
- /** Reconnect to an existing task stream */
- reconnectToTask: (
- sessionId: string,
- taskId: string,
- lastMessageId?: string,
- onChunk?: (chunk: StreamChunk) => void,
- ) => Promise;
- /** Update last message ID for a task (for tracking replay position) */
- updateTaskLastMessageId: (sessionId: string, lastMessageId: string) => void;
-}
-
-type ChatStore = ChatStoreState & ChatStoreActions;
-
-function notifyStreamComplete(
- callbacks: Set,
- sessionId: string,
-) {
- for (const callback of callbacks) {
- try {
- callback(sessionId);
- } catch (err) {
- console.warn("[ChatStore] Stream complete callback error:", err);
- }
- }
-}
-
-function cleanupExpiredStreams(
- completedStreams: Map,
-): Map {
- const now = Date.now();
- const cleaned = new Map(completedStreams);
- for (const [sessionId, result] of cleaned) {
- if (now - result.completedAt > COMPLETED_STREAM_TTL_MS) {
- cleaned.delete(sessionId);
- }
- }
- return cleaned;
-}
-
-/**
- * Finalize a stream by moving it from activeStreams to completedStreams.
- * Also handles cleanup and notifications.
- */
-function finalizeStream(
- sessionId: string,
- stream: ActiveStream,
- onChunk: ((chunk: StreamChunk) => void) | undefined,
- get: () => ChatStoreState & ChatStoreActions,
- set: (state: Partial) => void,
-): void {
- if (onChunk) stream.onChunkCallbacks.delete(onChunk);
-
- if (stream.status !== "streaming") {
- const currentState = get();
- const finalActiveStreams = new Map(currentState.activeStreams);
- let finalCompletedStreams = new Map(currentState.completedStreams);
-
- const storedStream = finalActiveStreams.get(sessionId);
- if (storedStream === stream) {
- const result: StreamResult = {
- sessionId,
- status: stream.status,
- chunks: stream.chunks,
- completedAt: Date.now(),
- error: stream.error,
- };
- finalCompletedStreams.set(sessionId, result);
- finalActiveStreams.delete(sessionId);
- finalCompletedStreams = cleanupExpiredStreams(finalCompletedStreams);
- set({
- activeStreams: finalActiveStreams,
- completedStreams: finalCompletedStreams,
- });
-
- if (stream.status === "completed" || stream.status === "error") {
- notifyStreamComplete(currentState.streamCompleteCallbacks, sessionId);
- }
- }
- }
-}
-
-/**
- * Clean up an existing stream for a session and move it to completed streams.
- * Returns updated maps for both active and completed streams.
- */
-function cleanupExistingStream(
- sessionId: string,
- activeStreams: Map,
- completedStreams: Map,
- callbacks: Set,
-): {
- activeStreams: Map;
- completedStreams: Map;
-} {
- const newActiveStreams = new Map(activeStreams);
- let newCompletedStreams = new Map(completedStreams);
-
- const existingStream = newActiveStreams.get(sessionId);
- if (existingStream) {
- existingStream.abortController.abort();
- const normalizedStatus =
- existingStream.status === "streaming"
- ? "completed"
- : existingStream.status;
- const result: StreamResult = {
- sessionId,
- status: normalizedStatus,
- chunks: existingStream.chunks,
- completedAt: Date.now(),
- error: existingStream.error,
- };
- newCompletedStreams.set(sessionId, result);
- newActiveStreams.delete(sessionId);
- newCompletedStreams = cleanupExpiredStreams(newCompletedStreams);
- if (normalizedStatus === "completed" || normalizedStatus === "error") {
- notifyStreamComplete(callbacks, sessionId);
- }
- }
-
- return {
- activeStreams: newActiveStreams,
- completedStreams: newCompletedStreams,
- };
-}
-
-/**
- * Create a new active stream with initial state.
- */
-function createActiveStream(
- sessionId: string,
- onChunk?: (chunk: StreamChunk) => void,
-): ActiveStream {
- const abortController = new AbortController();
- const initialCallbacks = new Set<(chunk: StreamChunk) => void>();
- if (onChunk) initialCallbacks.add(onChunk);
-
- return {
- sessionId,
- abortController,
- status: "streaming",
- startedAt: Date.now(),
- chunks: [],
- onChunkCallbacks: initialCallbacks,
- };
-}
-
-export const useChatStore = create((set, get) => ({
- activeStreams: new Map(),
- completedStreams: new Map(),
- activeSessions: new Set(),
- streamCompleteCallbacks: new Set(),
- activeTasks: loadPersistedTasks(),
-
- startStream: async function startStream(
- sessionId,
- message,
- isUserMessage,
- context,
- onChunk,
- ) {
- const state = get();
- const callbacks = state.streamCompleteCallbacks;
-
- // Clean up any existing stream for this session
- const {
- activeStreams: newActiveStreams,
- completedStreams: newCompletedStreams,
- } = cleanupExistingStream(
- sessionId,
- state.activeStreams,
- state.completedStreams,
- callbacks,
- );
-
- // Create new stream
- const stream = createActiveStream(sessionId, onChunk);
- newActiveStreams.set(sessionId, stream);
- set({
- activeStreams: newActiveStreams,
- completedStreams: newCompletedStreams,
- });
-
- try {
- await executeStream(stream, message, isUserMessage, context);
- } finally {
- finalizeStream(sessionId, stream, onChunk, get, set);
- }
- },
-
- stopStream: function stopStream(sessionId) {
- const state = get();
- const stream = state.activeStreams.get(sessionId);
- if (!stream) return;
-
- stream.abortController.abort();
- stream.status = "completed";
-
- const newActiveStreams = new Map(state.activeStreams);
- let newCompletedStreams = new Map(state.completedStreams);
-
- const result: StreamResult = {
- sessionId,
- status: stream.status,
- chunks: stream.chunks,
- completedAt: Date.now(),
- error: stream.error,
- };
- newCompletedStreams.set(sessionId, result);
- newActiveStreams.delete(sessionId);
- newCompletedStreams = cleanupExpiredStreams(newCompletedStreams);
-
- set({
- activeStreams: newActiveStreams,
- completedStreams: newCompletedStreams,
- });
-
- notifyStreamComplete(state.streamCompleteCallbacks, sessionId);
- },
-
- subscribeToStream: function subscribeToStream(
- sessionId,
- onChunk,
- skipReplay = false,
- ) {
- const state = get();
- const stream = state.activeStreams.get(sessionId);
-
- if (stream) {
- if (!skipReplay) {
- for (const chunk of stream.chunks) {
- onChunk(chunk);
- }
- }
-
- stream.onChunkCallbacks.add(onChunk);
-
- return function unsubscribe() {
- stream.onChunkCallbacks.delete(onChunk);
- };
- }
-
- return function noop() {};
- },
-
- getStreamStatus: function getStreamStatus(sessionId) {
- const { activeStreams, completedStreams } = get();
-
- const active = activeStreams.get(sessionId);
- if (active) return active.status;
-
- const completed = completedStreams.get(sessionId);
- if (completed) return completed.status;
-
- return "idle";
- },
-
- getCompletedStream: function getCompletedStream(sessionId) {
- return get().completedStreams.get(sessionId);
- },
-
- clearCompletedStream: function clearCompletedStream(sessionId) {
- const state = get();
- if (!state.completedStreams.has(sessionId)) return;
-
- const newCompletedStreams = new Map(state.completedStreams);
- newCompletedStreams.delete(sessionId);
- set({ completedStreams: newCompletedStreams });
- },
-
- isStreaming: function isStreaming(sessionId) {
- const stream = get().activeStreams.get(sessionId);
- return stream?.status === "streaming";
- },
-
- registerActiveSession: function registerActiveSession(sessionId) {
- const state = get();
- if (state.activeSessions.has(sessionId)) return;
-
- const newActiveSessions = new Set(state.activeSessions);
- newActiveSessions.add(sessionId);
- set({ activeSessions: newActiveSessions });
- },
-
- unregisterActiveSession: function unregisterActiveSession(sessionId) {
- const state = get();
- if (!state.activeSessions.has(sessionId)) return;
-
- const newActiveSessions = new Set(state.activeSessions);
- newActiveSessions.delete(sessionId);
- set({ activeSessions: newActiveSessions });
- },
-
- isSessionActive: function isSessionActive(sessionId) {
- return get().activeSessions.has(sessionId);
- },
-
- onStreamComplete: function onStreamComplete(callback) {
- const state = get();
- const newCallbacks = new Set(state.streamCompleteCallbacks);
- newCallbacks.add(callback);
- set({ streamCompleteCallbacks: newCallbacks });
-
- return function unsubscribe() {
- const currentState = get();
- const cleanedCallbacks = new Set(currentState.streamCompleteCallbacks);
- cleanedCallbacks.delete(callback);
- set({ streamCompleteCallbacks: cleanedCallbacks });
- };
- },
-
- setActiveTask: function setActiveTask(sessionId, taskInfo) {
- const state = get();
- const newActiveTasks = new Map(state.activeTasks);
- newActiveTasks.set(sessionId, {
- ...taskInfo,
- sessionId,
- startedAt: Date.now(),
- });
- set({ activeTasks: newActiveTasks });
- persistTasks(newActiveTasks);
- },
-
- getActiveTask: function getActiveTask(sessionId) {
- return get().activeTasks.get(sessionId);
- },
-
- clearActiveTask: function clearActiveTask(sessionId) {
- const state = get();
- if (!state.activeTasks.has(sessionId)) return;
-
- const newActiveTasks = new Map(state.activeTasks);
- newActiveTasks.delete(sessionId);
- set({ activeTasks: newActiveTasks });
- persistTasks(newActiveTasks);
- },
-
- reconnectToTask: async function reconnectToTask(
- sessionId,
- taskId,
- lastMessageId = INITIAL_STREAM_ID,
- onChunk,
- ) {
- const state = get();
- const callbacks = state.streamCompleteCallbacks;
-
- // Clean up any existing stream for this session
- const {
- activeStreams: newActiveStreams,
- completedStreams: newCompletedStreams,
- } = cleanupExistingStream(
- sessionId,
- state.activeStreams,
- state.completedStreams,
- callbacks,
- );
-
- // Create new stream for reconnection
- const stream = createActiveStream(sessionId, onChunk);
- newActiveStreams.set(sessionId, stream);
- set({
- activeStreams: newActiveStreams,
- completedStreams: newCompletedStreams,
- });
-
- try {
- await executeTaskReconnect(stream, taskId, lastMessageId);
- } finally {
- finalizeStream(sessionId, stream, onChunk, get, set);
-
- // Clear active task on completion
- if (stream.status === "completed" || stream.status === "error") {
- const taskState = get();
- if (taskState.activeTasks.has(sessionId)) {
- const newActiveTasks = new Map(taskState.activeTasks);
- newActiveTasks.delete(sessionId);
- set({ activeTasks: newActiveTasks });
- persistTasks(newActiveTasks);
- }
- }
- }
- },
-
- updateTaskLastMessageId: function updateTaskLastMessageId(
- sessionId,
- lastMessageId,
- ) {
- const state = get();
- const task = state.activeTasks.get(sessionId);
- if (!task) return;
-
- const newActiveTasks = new Map(state.activeTasks);
- newActiveTasks.set(sessionId, {
- ...task,
- lastMessageId,
- });
- set({ activeTasks: newActiveTasks });
- persistTasks(newActiveTasks);
- },
-}));
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/chat-types.ts b/autogpt_platform/frontend/src/components/contextual/Chat/chat-types.ts
deleted file mode 100644
index 34813e17fe..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/chat-types.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-import type { ToolArguments, ToolResult } from "@/types/chat";
-
-export type StreamStatus = "idle" | "streaming" | "completed" | "error";
-
-export interface StreamChunk {
- type:
- | "stream_start"
- | "text_chunk"
- | "text_ended"
- | "tool_call"
- | "tool_call_start"
- | "tool_response"
- | "login_needed"
- | "need_login"
- | "credentials_needed"
- | "error"
- | "usage"
- | "stream_end";
- taskId?: string;
- timestamp?: string;
- content?: string;
- message?: string;
- code?: string;
- details?: Record;
- tool_id?: string;
- tool_name?: string;
- arguments?: ToolArguments;
- result?: ToolResult;
- success?: boolean;
- idx?: number;
- session_id?: string;
- agent_info?: {
- graph_id: string;
- name: string;
- trigger_type: string;
- };
- provider?: string;
- provider_name?: string;
- credential_type?: string;
- scopes?: string[];
- title?: string;
- [key: string]: unknown;
-}
-
-export type VercelStreamChunk =
- | { type: "start"; messageId: string; taskId?: string }
- | { type: "finish" }
- | { type: "text-start"; id: string }
- | { type: "text-delta"; id: string; delta: string }
- | { type: "text-end"; id: string }
- | { type: "tool-input-start"; toolCallId: string; toolName: string }
- | {
- type: "tool-input-available";
- toolCallId: string;
- toolName: string;
- input: Record;
- }
- | {
- type: "tool-output-available";
- toolCallId: string;
- toolName?: string;
- output: unknown;
- success?: boolean;
- }
- | {
- type: "usage";
- promptTokens: number;
- completionTokens: number;
- totalTokens: number;
- }
- | {
- type: "error";
- errorText: string;
- code?: string;
- details?: Record;
- };
-
-export interface ActiveStream {
- sessionId: string;
- abortController: AbortController;
- status: StreamStatus;
- startedAt: number;
- chunks: StreamChunk[];
- error?: Error;
- onChunkCallbacks: Set<(chunk: StreamChunk) => void>;
-}
-
-export interface StreamResult {
- sessionId: string;
- status: StreamStatus;
- chunks: StreamChunk[];
- completedAt: number;
- error?: Error;
-}
-
-export type StreamCompleteCallback = (sessionId: string) => void;
-
-// Type guards for message types
-
-/**
- * Check if a message has a toolId property.
- */
-export function hasToolId(
- msg: T,
-): msg is T & { toolId: string } {
- return (
- "toolId" in msg &&
- typeof (msg as Record).toolId === "string"
- );
-}
-
-/**
- * Check if a message has an operationId property.
- */
-export function hasOperationId(
- msg: T,
-): msg is T & { operationId: string } {
- return (
- "operationId" in msg &&
- typeof (msg as Record).operationId === "string"
- );
-}
-
-/**
- * Check if a message has a toolCallId property.
- */
-export function hasToolCallId(
- msg: T,
-): msg is T & { toolCallId: string } {
- return (
- "toolCallId" in msg &&
- typeof (msg as Record).toolCallId === "string"
- );
-}
-
-/**
- * Check if a message is an operation message type.
- */
-export function isOperationMessage(
- msg: T,
-): msg is T & {
- type: "operation_started" | "operation_pending" | "operation_in_progress";
-} {
- return (
- msg.type === "operation_started" ||
- msg.type === "operation_pending" ||
- msg.type === "operation_in_progress"
- );
-}
-
-/**
- * Get the tool ID from a message if available.
- * Checks toolId, operationId, and toolCallId properties.
- */
-export function getToolIdFromMessage(
- msg: T,
-): string | undefined {
- const record = msg as Record;
- if (typeof record.toolId === "string") return record.toolId;
- if (typeof record.operationId === "string") return record.operationId;
- if (typeof record.toolCallId === "string") return record.toolCallId;
- return undefined;
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/AIChatBubble/AIChatBubble.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/AIChatBubble/AIChatBubble.tsx
deleted file mode 100644
index f5d56fcb15..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/AIChatBubble/AIChatBubble.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { cn } from "@/lib/utils";
-import { ReactNode } from "react";
-
-export interface AIChatBubbleProps {
- children: ReactNode;
- className?: string;
-}
-
-export function AIChatBubble({ children, className }: AIChatBubbleProps) {
- return (
-
- {children}
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentCarouselMessage/AgentCarouselMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentCarouselMessage/AgentCarouselMessage.tsx
deleted file mode 100644
index 582b24de5e..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentCarouselMessage/AgentCarouselMessage.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import { Button } from "@/components/atoms/Button/Button";
-import { Card } from "@/components/atoms/Card/Card";
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-import { ArrowRight, List, Robot } from "@phosphor-icons/react";
-import Image from "next/image";
-
-export interface Agent {
- id: string;
- name: string;
- description: string;
- version?: number;
- image_url?: string;
-}
-
-export interface AgentCarouselMessageProps {
- agents: Agent[];
- totalCount?: number;
- onSelectAgent?: (agentId: string) => void;
- className?: string;
-}
-
-export function AgentCarouselMessage({
- agents,
- totalCount,
- onSelectAgent,
- className,
-}: AgentCarouselMessageProps) {
- const displayCount = totalCount ?? agents.length;
-
- return (
-
- {/* Header */}
-
-
-
-
-
-
- Found {displayCount} {displayCount === 1 ? "Agent" : "Agents"}
-
-
- Select an agent to view details or run it
-
-
-
-
- {/* Agent Cards */}
-
- {agents.map((agent) => (
-
-
-
- {agent.image_url ? (
-
- ) : (
-
-
-
- )}
-
-
-
-
- {agent.name}
-
- {agent.version && (
-
- v{agent.version}
-
- )}
-
-
- {agent.description}
-
- {onSelectAgent && (
-
onSelectAgent(agent.id)}
- variant="ghost"
- className="mt-2 flex items-center gap-1 p-0 text-sm text-purple-600 hover:text-purple-800"
- >
- View details
-
-
- )}
-
-
-
- ))}
-
-
- {totalCount && totalCount > agents.length && (
-
- Showing {agents.length} of {totalCount} results
-
- )}
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/AgentInputsSetup.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/AgentInputsSetup.tsx
deleted file mode 100644
index 3ef71eca09..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/AgentInputsSetup.tsx
+++ /dev/null
@@ -1,246 +0,0 @@
-"use client";
-
-import { Button } from "@/components/atoms/Button/Button";
-import { Card } from "@/components/atoms/Card/Card";
-import { Text } from "@/components/atoms/Text/Text";
-import { CredentialsInput } from "@/components/contextual/CredentialsInput/CredentialsInput";
-import { RunAgentInputs } from "@/components/contextual/RunAgentInputs/RunAgentInputs";
-
-import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
-import {
- BlockIOCredentialsSubSchema,
- BlockIOSubSchema,
-} from "@/lib/autogpt-server-api/types";
-import { cn, isEmpty } from "@/lib/utils";
-import { PlayIcon, WarningIcon } from "@phosphor-icons/react";
-import { useMemo } from "react";
-import { useAgentInputsSetup } from "./useAgentInputsSetup";
-
-type LibraryAgentInputSchemaProperties = LibraryAgent["input_schema"] extends {
- properties: infer P;
-}
- ? P extends Record
- ? P
- : Record
- : Record;
-
-type LibraryAgentCredentialsInputSchemaProperties =
- LibraryAgent["credentials_input_schema"] extends {
- properties: infer P;
- }
- ? P extends Record
- ? P
- : Record
- : Record;
-
-interface Props {
- agentName?: string;
- inputSchema: LibraryAgentInputSchemaProperties | Record;
- credentialsSchema?:
- | LibraryAgentCredentialsInputSchemaProperties
- | Record;
- message: string;
- requiredFields?: string[];
- onRun: (
- inputs: Record,
- credentials: Record,
- ) => void;
- onCancel?: () => void;
- className?: string;
-}
-
-export function AgentInputsSetup({
- agentName,
- inputSchema,
- credentialsSchema,
- message,
- requiredFields,
- onRun,
- onCancel,
- className,
-}: Props) {
- const { inputValues, setInputValue, credentialsValues, setCredentialsValue } =
- useAgentInputsSetup();
-
- const inputSchemaObj = useMemo(() => {
- if (!inputSchema) return { properties: {}, required: [] };
- if ("properties" in inputSchema && "type" in inputSchema) {
- return inputSchema as {
- properties: Record;
- required?: string[];
- };
- }
- return { properties: inputSchema as Record, required: [] };
- }, [inputSchema]);
-
- const credentialsSchemaObj = useMemo(() => {
- if (!credentialsSchema) return { properties: {}, required: [] };
- if ("properties" in credentialsSchema && "type" in credentialsSchema) {
- return credentialsSchema as {
- properties: Record;
- required?: string[];
- };
- }
- return {
- properties: credentialsSchema as Record,
- required: [],
- };
- }, [credentialsSchema]);
-
- const agentInputFields = useMemo(() => {
- const properties = inputSchemaObj.properties || {};
- return Object.fromEntries(
- Object.entries(properties).filter(
- ([_, subSchema]: [string, any]) => !subSchema.hidden,
- ),
- );
- }, [inputSchemaObj]);
-
- const agentCredentialsInputFields = useMemo(() => {
- return credentialsSchemaObj.properties || {};
- }, [credentialsSchemaObj]);
-
- const inputFields = Object.entries(agentInputFields);
- const credentialFields = Object.entries(agentCredentialsInputFields);
-
- const defaultsFromSchema = useMemo(() => {
- const defaults: Record = {};
- Object.entries(agentInputFields).forEach(([key, schema]) => {
- if ("default" in schema && schema.default !== undefined) {
- defaults[key] = schema.default;
- }
- });
- return defaults;
- }, [agentInputFields]);
-
- const defaultsFromCredentialsSchema = useMemo(() => {
- const defaults: Record = {};
- Object.entries(agentCredentialsInputFields).forEach(([key, schema]) => {
- if ("default" in schema && schema.default !== undefined) {
- defaults[key] = schema.default;
- }
- });
- return defaults;
- }, [agentCredentialsInputFields]);
-
- const mergedInputValues = useMemo(() => {
- return { ...defaultsFromSchema, ...inputValues };
- }, [defaultsFromSchema, inputValues]);
-
- const mergedCredentialsValues = useMemo(() => {
- return { ...defaultsFromCredentialsSchema, ...credentialsValues };
- }, [defaultsFromCredentialsSchema, credentialsValues]);
-
- const allRequiredInputsAreSet = useMemo(() => {
- const requiredInputs = new Set(
- requiredFields || (inputSchemaObj.required as string[]) || [],
- );
- const nonEmptyInputs = new Set(
- Object.keys(mergedInputValues).filter(
- (k) => !isEmpty(mergedInputValues[k]),
- ),
- );
- const missing = [...requiredInputs].filter(
- (input) => !nonEmptyInputs.has(input),
- );
- return missing.length === 0;
- }, [inputSchemaObj.required, mergedInputValues, requiredFields]);
-
- const allCredentialsAreSet = useMemo(() => {
- const requiredCredentials = new Set(
- (credentialsSchemaObj.required as string[]) || [],
- );
- if (requiredCredentials.size === 0) {
- return true;
- }
- const missing = [...requiredCredentials].filter((key) => {
- const cred = mergedCredentialsValues[key];
- return !cred || !cred.id;
- });
- return missing.length === 0;
- }, [credentialsSchemaObj.required, mergedCredentialsValues]);
-
- const canRun = allRequiredInputsAreSet && allCredentialsAreSet;
-
- function handleRun() {
- if (canRun) {
- onRun(mergedInputValues, mergedCredentialsValues);
- }
- }
-
- return (
-
-
-
-
-
-
-
- {agentName ? `Configure ${agentName}` : "Agent Configuration"}
-
-
- {message}
-
-
- {inputFields.length > 0 && (
-
- {inputFields.map(([key, inputSubSchema]) => (
- setInputValue(key, value)}
- />
- ))}
-
- )}
-
- {credentialFields.length > 0 && (
-
- {credentialFields.map(([key, schema]) => {
- const requiredCredentials = new Set(
- (credentialsSchemaObj.required as string[]) || [],
- );
- return (
-
- setCredentialsValue(key, value)
- }
- siblingInputs={mergedInputValues}
- isOptional={!requiredCredentials.has(key)}
- />
- );
- })}
-
- )}
-
-
-
-
- Run Agent
-
- {onCancel && (
-
- Cancel
-
- )}
-
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/useAgentInputsSetup.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/useAgentInputsSetup.ts
deleted file mode 100644
index e36a3f3c5d..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/useAgentInputsSetup.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
-import { useState } from "react";
-
-export function useAgentInputsSetup() {
- const [inputValues, setInputValues] = useState>({});
- const [credentialsValues, setCredentialsValues] = useState<
- Record
- >({});
-
- function setInputValue(key: string, value: any) {
- setInputValues((prev) => ({
- ...prev,
- [key]: value,
- }));
- }
-
- function setCredentialsValue(key: string, value?: CredentialsMetaInput) {
- if (value) {
- setCredentialsValues((prev) => ({
- ...prev,
- [key]: value,
- }));
- } else {
- setCredentialsValues((prev) => {
- const next = { ...prev };
- delete next[key];
- return next;
- });
- }
- }
-
- return {
- inputValues,
- setInputValue,
- credentialsValues,
- setCredentialsValue,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/AuthPromptWidget/AuthPromptWidget.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/AuthPromptWidget/AuthPromptWidget.tsx
deleted file mode 100644
index b2cf92ec56..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/AuthPromptWidget/AuthPromptWidget.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-"use client";
-
-import { Button } from "@/components/atoms/Button/Button";
-import { cn } from "@/lib/utils";
-import { ShieldIcon, SignInIcon, UserPlusIcon } from "@phosphor-icons/react";
-import { useRouter } from "next/navigation";
-
-export interface AuthPromptWidgetProps {
- message: string;
- sessionId: string;
- agentInfo?: {
- graph_id: string;
- name: string;
- trigger_type: string;
- };
- returnUrl?: string;
- className?: string;
-}
-
-export function AuthPromptWidget({
- message,
- sessionId,
- agentInfo,
- returnUrl = "/copilot/chat",
- className,
-}: AuthPromptWidgetProps) {
- const router = useRouter();
-
- function handleSignIn() {
- if (typeof window !== "undefined") {
- localStorage.setItem("pending_chat_session", sessionId);
- if (agentInfo) {
- localStorage.setItem("pending_agent_setup", JSON.stringify(agentInfo));
- }
- }
- const returnUrlWithSession = `${returnUrl}?session_id=${sessionId}`;
- const encodedReturnUrl = encodeURIComponent(returnUrlWithSession);
- router.push(`/login?returnUrl=${encodedReturnUrl}`);
- }
-
- function handleSignUp() {
- if (typeof window !== "undefined") {
- localStorage.setItem("pending_chat_session", sessionId);
- if (agentInfo) {
- localStorage.setItem("pending_agent_setup", JSON.stringify(agentInfo));
- }
- }
- const returnUrlWithSession = `${returnUrl}?session_id=${sessionId}`;
- const encodedReturnUrl = encodeURIComponent(returnUrlWithSession);
- router.push(`/signup?returnUrl=${encodedReturnUrl}`);
- }
-
- return (
-
-
-
-
-
-
-
-
- Authentication Required
-
-
- Sign in to set up and manage agents
-
-
-
-
-
-
{message}
- {agentInfo && (
-
-
- Ready to set up:{" "}
- {agentInfo.name}
-
-
- Type:{" "}
- {agentInfo.trigger_type}
-
-
- )}
-
-
-
-
-
- Sign In
-
-
-
- Create Account
-
-
-
-
- Your chat session will be preserved after signing in
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/ChatContainer.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/ChatContainer.tsx
deleted file mode 100644
index fbf2d5d143..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/ChatContainer.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
-import { Button } from "@/components/atoms/Button/Button";
-import { Text } from "@/components/atoms/Text/Text";
-import { Dialog } from "@/components/molecules/Dialog/Dialog";
-import { cn } from "@/lib/utils";
-import { GlobeHemisphereEastIcon } from "@phosphor-icons/react";
-import { useEffect } from "react";
-import { ChatInput } from "../ChatInput/ChatInput";
-import { MessageList } from "../MessageList/MessageList";
-import { useChatContainer } from "./useChatContainer";
-
-export interface ChatContainerProps {
- sessionId: string | null;
- initialMessages: SessionDetailResponse["messages"];
- initialPrompt?: string;
- className?: string;
- onStreamingChange?: (isStreaming: boolean) => void;
- onOperationStarted?: () => void;
- /** Active stream info from the server for reconnection */
- activeStream?: {
- taskId: string;
- lastMessageId: string;
- operationId: string;
- toolName: string;
- };
-}
-
-export function ChatContainer({
- sessionId,
- initialMessages,
- initialPrompt,
- className,
- onStreamingChange,
- onOperationStarted,
- activeStream,
-}: ChatContainerProps) {
- const {
- messages,
- streamingChunks,
- isStreaming,
- stopStreaming,
- isRegionBlockedModalOpen,
- sendMessageWithContext,
- handleRegionModalOpenChange,
- handleRegionModalClose,
- } = useChatContainer({
- sessionId,
- initialMessages,
- initialPrompt,
- onOperationStarted,
- activeStream,
- });
-
- useEffect(() => {
- onStreamingChange?.(isStreaming);
- }, [isStreaming, onStreamingChange]);
-
- return (
-
-
-
-
- Service unavailable
-
-
- }
- controlled={{
- isOpen: isRegionBlockedModalOpen,
- set: handleRegionModalOpenChange,
- }}
- onClose={handleRegionModalClose}
- styling={{ maxWidth: 550, width: "100%", minWidth: "auto" }}
- >
-
-
-
- The Autogpt AI model is not available in your region or your
- connection is blocking it. Please try again with a different
- connection.
-
-
-
- Got it
-
-
-
-
-
- {/* Messages - Scrollable */}
-
-
- {/* Input - Fixed at bottom */}
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/createStreamEventDispatcher.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/createStreamEventDispatcher.ts
deleted file mode 100644
index af3b3329b7..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/createStreamEventDispatcher.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { toast } from "sonner";
-import type { StreamChunk } from "../../chat-types";
-import type { HandlerDependencies } from "./handlers";
-import {
- getErrorDisplayMessage,
- handleError,
- handleLoginNeeded,
- handleStreamEnd,
- handleTextChunk,
- handleTextEnded,
- handleToolCallStart,
- handleToolResponse,
- isRegionBlockedError,
-} from "./handlers";
-
-export function createStreamEventDispatcher(
- deps: HandlerDependencies,
-): (chunk: StreamChunk) => void {
- return function dispatchStreamEvent(chunk: StreamChunk): void {
- if (
- chunk.type === "text_chunk" ||
- chunk.type === "tool_call_start" ||
- chunk.type === "tool_response" ||
- chunk.type === "login_needed" ||
- chunk.type === "need_login" ||
- chunk.type === "error"
- ) {
- deps.hasResponseRef.current = true;
- }
-
- switch (chunk.type) {
- case "stream_start":
- // Store task ID for SSE reconnection
- if (chunk.taskId && deps.onActiveTaskStarted) {
- deps.onActiveTaskStarted({
- taskId: chunk.taskId,
- operationId: chunk.taskId,
- toolName: "chat",
- toolCallId: "chat_stream",
- });
- }
- break;
-
- case "text_chunk":
- handleTextChunk(chunk, deps);
- break;
-
- case "text_ended":
- handleTextEnded(chunk, deps);
- break;
-
- case "tool_call_start":
- handleToolCallStart(chunk, deps);
- break;
-
- case "tool_response":
- handleToolResponse(chunk, deps);
- break;
-
- case "login_needed":
- case "need_login":
- handleLoginNeeded(chunk, deps);
- break;
-
- case "stream_end":
- // Note: "finish" type from backend gets normalized to "stream_end" by normalizeStreamChunk
- handleStreamEnd(chunk, deps);
- break;
-
- case "error":
- const isRegionBlocked = isRegionBlockedError(chunk);
- handleError(chunk, deps);
- // Show toast at dispatcher level to avoid circular dependencies
- if (!isRegionBlocked) {
- toast.error("Chat Error", {
- description: getErrorDisplayMessage(chunk),
- });
- }
- break;
-
- case "usage":
- // TODO: Handle usage for display
- break;
-
- default:
- console.warn("Unknown stream chunk type:", chunk);
- }
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/handlers.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/handlers.ts
deleted file mode 100644
index 5aec5b9818..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/handlers.ts
+++ /dev/null
@@ -1,362 +0,0 @@
-import type { Dispatch, MutableRefObject, SetStateAction } from "react";
-import { StreamChunk } from "../../useChatStream";
-import type { ChatMessageData } from "../ChatMessage/useChatMessage";
-import {
- extractCredentialsNeeded,
- extractInputsNeeded,
- parseToolResponse,
-} from "./helpers";
-
-function isToolCallMessage(
- message: ChatMessageData,
-): message is Extract {
- return message.type === "tool_call";
-}
-
-export interface HandlerDependencies {
- setHasTextChunks: Dispatch>;
- setStreamingChunks: Dispatch>;
- streamingChunksRef: MutableRefObject;
- hasResponseRef: MutableRefObject;
- textFinalizedRef: MutableRefObject;
- streamEndedRef: MutableRefObject;
- setMessages: Dispatch>;
- setIsStreamingInitiated: Dispatch>;
- setIsRegionBlockedModalOpen: Dispatch>;
- sessionId: string;
- onOperationStarted?: () => void;
- onActiveTaskStarted?: (taskInfo: {
- taskId: string;
- operationId: string;
- toolName: string;
- toolCallId: string;
- }) => void;
-}
-
-export function isRegionBlockedError(chunk: StreamChunk): boolean {
- if (chunk.code === "MODEL_NOT_AVAILABLE_REGION") return true;
- const message = chunk.message || chunk.content;
- if (typeof message !== "string") return false;
- return message.toLowerCase().includes("not available in your region");
-}
-
-export function getUserFriendlyErrorMessage(
- code: string | undefined,
-): string | undefined {
- switch (code) {
- case "TASK_EXPIRED":
- return "This operation has expired. Please try again.";
- case "TASK_NOT_FOUND":
- return "Could not find the requested operation.";
- case "ACCESS_DENIED":
- return "You do not have access to this operation.";
- case "QUEUE_OVERFLOW":
- return "Connection was interrupted. Please refresh to continue.";
- case "MODEL_NOT_AVAILABLE_REGION":
- return "This model is not available in your region.";
- default:
- return undefined;
- }
-}
-
-export function handleTextChunk(chunk: StreamChunk, deps: HandlerDependencies) {
- if (!chunk.content) return;
- deps.setHasTextChunks(true);
- deps.setStreamingChunks((prev) => {
- const updated = [...prev, chunk.content!];
- deps.streamingChunksRef.current = updated;
- return updated;
- });
-}
-
-export function handleTextEnded(
- _chunk: StreamChunk,
- deps: HandlerDependencies,
-) {
- if (deps.textFinalizedRef.current) {
- return;
- }
-
- const completedText = deps.streamingChunksRef.current.join("");
- if (completedText.trim()) {
- deps.textFinalizedRef.current = true;
-
- deps.setMessages((prev) => {
- const exists = prev.some(
- (msg) =>
- msg.type === "message" &&
- msg.role === "assistant" &&
- msg.content === completedText,
- );
- if (exists) return prev;
-
- const assistantMessage: ChatMessageData = {
- type: "message",
- role: "assistant",
- content: completedText,
- timestamp: new Date(),
- };
- return [...prev, assistantMessage];
- });
- }
- deps.setStreamingChunks([]);
- deps.streamingChunksRef.current = [];
- deps.setHasTextChunks(false);
-}
-
-export function handleToolCallStart(
- chunk: StreamChunk,
- deps: HandlerDependencies,
-) {
- // Use deterministic fallback instead of Date.now() to ensure same ID on replay
- const toolId =
- chunk.tool_id ||
- `tool-${deps.sessionId}-${chunk.idx ?? "unknown"}-${chunk.tool_name || "unknown"}`;
-
- const toolCallMessage: Extract = {
- type: "tool_call",
- toolId,
- toolName: chunk.tool_name || "Executing",
- arguments: chunk.arguments || {},
- timestamp: new Date(),
- };
-
- function updateToolCallMessages(prev: ChatMessageData[]) {
- const existingIndex = prev.findIndex(function findToolCallIndex(msg) {
- return isToolCallMessage(msg) && msg.toolId === toolCallMessage.toolId;
- });
- if (existingIndex === -1) {
- return [...prev, toolCallMessage];
- }
- const nextMessages = [...prev];
- const existing = nextMessages[existingIndex];
- if (!isToolCallMessage(existing)) return prev;
- const nextArguments =
- toolCallMessage.arguments &&
- Object.keys(toolCallMessage.arguments).length > 0
- ? toolCallMessage.arguments
- : existing.arguments;
- nextMessages[existingIndex] = {
- ...existing,
- toolName: toolCallMessage.toolName || existing.toolName,
- arguments: nextArguments,
- timestamp: toolCallMessage.timestamp,
- };
- return nextMessages;
- }
-
- deps.setMessages(updateToolCallMessages);
-}
-
-const TOOL_RESPONSE_TYPES = new Set([
- "tool_response",
- "operation_started",
- "operation_pending",
- "operation_in_progress",
- "execution_started",
- "agent_carousel",
- "clarification_needed",
-]);
-
-function hasResponseForTool(
- messages: ChatMessageData[],
- toolId: string,
-): boolean {
- return messages.some((msg) => {
- if (!TOOL_RESPONSE_TYPES.has(msg.type)) return false;
- const msgToolId =
- (msg as { toolId?: string }).toolId ||
- (msg as { toolCallId?: string }).toolCallId;
- return msgToolId === toolId;
- });
-}
-
-export function handleToolResponse(
- chunk: StreamChunk,
- deps: HandlerDependencies,
-) {
- let toolName = chunk.tool_name || "unknown";
- if (!chunk.tool_name || chunk.tool_name === "unknown") {
- deps.setMessages((prev) => {
- const matchingToolCall = [...prev]
- .reverse()
- .find(
- (msg) => msg.type === "tool_call" && msg.toolId === chunk.tool_id,
- );
- if (matchingToolCall && matchingToolCall.type === "tool_call") {
- toolName = matchingToolCall.toolName;
- }
- return prev;
- });
- }
- const responseMessage = parseToolResponse(
- chunk.result!,
- chunk.tool_id!,
- toolName,
- new Date(),
- );
- if (!responseMessage) {
- let parsedResult: Record | null = null;
- try {
- parsedResult =
- typeof chunk.result === "string"
- ? JSON.parse(chunk.result)
- : (chunk.result as Record);
- } catch {
- parsedResult = null;
- }
- if (
- (chunk.tool_name === "run_agent" || chunk.tool_name === "run_block") &&
- chunk.success &&
- parsedResult?.type === "setup_requirements"
- ) {
- const inputsMessage = extractInputsNeeded(parsedResult, chunk.tool_name);
- if (inputsMessage) {
- deps.setMessages((prev) => {
- // Check for duplicate inputs_needed message
- const exists = prev.some((msg) => msg.type === "inputs_needed");
- if (exists) return prev;
- return [...prev, inputsMessage];
- });
- }
- const credentialsMessage = extractCredentialsNeeded(
- parsedResult,
- chunk.tool_name,
- );
- if (credentialsMessage) {
- deps.setMessages((prev) => {
- // Check for duplicate credentials_needed message
- const exists = prev.some((msg) => msg.type === "credentials_needed");
- if (exists) return prev;
- return [...prev, credentialsMessage];
- });
- }
- }
- return;
- }
- if (responseMessage.type === "operation_started") {
- deps.onOperationStarted?.();
- const taskId = (responseMessage as { taskId?: string }).taskId;
- if (taskId && deps.onActiveTaskStarted) {
- deps.onActiveTaskStarted({
- taskId,
- operationId:
- (responseMessage as { operationId?: string }).operationId || "",
- toolName: (responseMessage as { toolName?: string }).toolName || "",
- toolCallId: (responseMessage as { toolId?: string }).toolId || "",
- });
- }
- }
-
- deps.setMessages((prev) => {
- const toolCallIndex = prev.findIndex(
- (msg) => msg.type === "tool_call" && msg.toolId === chunk.tool_id,
- );
- if (hasResponseForTool(prev, chunk.tool_id!)) {
- return prev;
- }
- if (toolCallIndex !== -1) {
- const newMessages = [...prev];
- newMessages.splice(toolCallIndex + 1, 0, responseMessage);
- return newMessages;
- }
- return [...prev, responseMessage];
- });
-}
-
-export function handleLoginNeeded(
- chunk: StreamChunk,
- deps: HandlerDependencies,
-) {
- const loginNeededMessage: ChatMessageData = {
- type: "login_needed",
- toolName: "login_needed",
- message: chunk.message || "Please sign in to use chat and agent features",
- sessionId: chunk.session_id || deps.sessionId,
- agentInfo: chunk.agent_info,
- timestamp: new Date(),
- };
- deps.setMessages((prev) => {
- // Check for duplicate login_needed message
- const exists = prev.some((msg) => msg.type === "login_needed");
- if (exists) return prev;
- return [...prev, loginNeededMessage];
- });
-}
-
-export function handleStreamEnd(
- _chunk: StreamChunk,
- deps: HandlerDependencies,
-) {
- if (deps.streamEndedRef.current) {
- return;
- }
- deps.streamEndedRef.current = true;
-
- const completedContent = deps.streamingChunksRef.current.join("");
- if (!completedContent.trim() && !deps.hasResponseRef.current) {
- deps.setMessages((prev) => {
- const exists = prev.some(
- (msg) =>
- msg.type === "message" &&
- msg.role === "assistant" &&
- msg.content === "No response received. Please try again.",
- );
- if (exists) return prev;
- return [
- ...prev,
- {
- type: "message",
- role: "assistant",
- content: "No response received. Please try again.",
- timestamp: new Date(),
- },
- ];
- });
- }
- if (completedContent.trim() && !deps.textFinalizedRef.current) {
- deps.textFinalizedRef.current = true;
-
- deps.setMessages((prev) => {
- const exists = prev.some(
- (msg) =>
- msg.type === "message" &&
- msg.role === "assistant" &&
- msg.content === completedContent,
- );
- if (exists) return prev;
-
- const assistantMessage: ChatMessageData = {
- type: "message",
- role: "assistant",
- content: completedContent,
- timestamp: new Date(),
- };
- return [...prev, assistantMessage];
- });
- }
- deps.setStreamingChunks([]);
- deps.streamingChunksRef.current = [];
- deps.setHasTextChunks(false);
- deps.setIsStreamingInitiated(false);
-}
-
-export function handleError(chunk: StreamChunk, deps: HandlerDependencies) {
- if (isRegionBlockedError(chunk)) {
- deps.setIsRegionBlockedModalOpen(true);
- }
- deps.setIsStreamingInitiated(false);
- deps.setHasTextChunks(false);
- deps.setStreamingChunks([]);
- deps.streamingChunksRef.current = [];
- deps.textFinalizedRef.current = false;
- deps.streamEndedRef.current = true;
-}
-
-export function getErrorDisplayMessage(chunk: StreamChunk): string {
- const friendlyMessage = getUserFriendlyErrorMessage(chunk.code);
- if (friendlyMessage) {
- return friendlyMessage;
- }
- return chunk.message || chunk.content || "An error occurred";
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/helpers.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/helpers.ts
deleted file mode 100644
index f1e94cea17..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/helpers.ts
+++ /dev/null
@@ -1,607 +0,0 @@
-import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
-import { SessionKey, sessionStorage } from "@/services/storage/session-storage";
-import type { ToolResult } from "@/types/chat";
-import type { ChatMessageData } from "../ChatMessage/useChatMessage";
-
-export function processInitialMessages(
- initialMessages: SessionDetailResponse["messages"],
-): ChatMessageData[] {
- const processedMessages: ChatMessageData[] = [];
- const toolCallMap = new Map();
-
- for (const msg of initialMessages) {
- if (!isValidMessage(msg)) {
- console.warn("Invalid message structure from backend:", msg);
- continue;
- }
-
- let content = String(msg.content || "");
- const role = String(msg.role || "assistant").toLowerCase();
- const toolCalls = msg.tool_calls;
- const timestamp = msg.timestamp
- ? new Date(msg.timestamp as string)
- : undefined;
-
- if (role === "user") {
- content = removePageContext(content);
- if (!content.trim()) continue;
- processedMessages.push({
- type: "message",
- role: "user",
- content,
- timestamp,
- });
- continue;
- }
-
- if (role === "assistant") {
- content = content
- .replace(/[\s\S]*?<\/thinking>/gi, "")
- .replace(/[\s\S]*?<\/internal_reasoning>/gi, "")
- .trim();
-
- if (toolCalls && isToolCallArray(toolCalls) && toolCalls.length > 0) {
- for (const toolCall of toolCalls) {
- const toolName = toolCall.function.name;
- const toolId = toolCall.id;
- toolCallMap.set(toolId, toolName);
-
- try {
- const args = JSON.parse(toolCall.function.arguments || "{}");
- processedMessages.push({
- type: "tool_call",
- toolId,
- toolName,
- arguments: args,
- timestamp,
- });
- } catch (err) {
- console.warn("Failed to parse tool call arguments:", err);
- processedMessages.push({
- type: "tool_call",
- toolId,
- toolName,
- arguments: {},
- timestamp,
- });
- }
- }
- if (content.trim()) {
- processedMessages.push({
- type: "message",
- role: "assistant",
- content,
- timestamp,
- });
- }
- } else if (content.trim()) {
- processedMessages.push({
- type: "message",
- role: "assistant",
- content,
- timestamp,
- });
- }
- continue;
- }
-
- if (role === "tool") {
- const toolCallId = (msg.tool_call_id as string) || "";
- const toolName = toolCallMap.get(toolCallId) || "unknown";
- const toolResponse = parseToolResponse(
- content,
- toolCallId,
- toolName,
- timestamp,
- );
- if (toolResponse) {
- processedMessages.push(toolResponse);
- }
- continue;
- }
-
- if (content.trim()) {
- processedMessages.push({
- type: "message",
- role: role as "user" | "assistant" | "system",
- content,
- timestamp,
- });
- }
- }
-
- return processedMessages;
-}
-
-export function hasSentInitialPrompt(sessionId: string): boolean {
- try {
- const sent = JSON.parse(
- sessionStorage.get(SessionKey.CHAT_SENT_INITIAL_PROMPTS) || "{}",
- );
- return sent[sessionId] === true;
- } catch {
- return false;
- }
-}
-
-export function markInitialPromptSent(sessionId: string): void {
- try {
- const sent = JSON.parse(
- sessionStorage.get(SessionKey.CHAT_SENT_INITIAL_PROMPTS) || "{}",
- );
- sent[sessionId] = true;
- sessionStorage.set(
- SessionKey.CHAT_SENT_INITIAL_PROMPTS,
- JSON.stringify(sent),
- );
- } catch {
- // Ignore storage errors
- }
-}
-
-export function removePageContext(content: string): string {
- // Remove "Page URL: ..." pattern at start of line (case insensitive, handles various formats)
- let cleaned = content.replace(/^\s*Page URL:\s*[^\n\r]*/gim, "");
-
- // Find "User Message:" marker at start of line to preserve the actual user message
- const userMessageMatch = cleaned.match(/^\s*User Message:\s*([\s\S]*)$/im);
- if (userMessageMatch) {
- // If we found "User Message:", extract everything after it
- cleaned = userMessageMatch[1];
- } else {
- // If no "User Message:" marker, remove "Page Content:" and everything after it at start of line
- cleaned = cleaned.replace(/^\s*Page Content:[\s\S]*$/gim, "");
- }
-
- // Clean up extra whitespace and newlines
- cleaned = cleaned.replace(/\n\s*\n\s*\n+/g, "\n\n").trim();
- return cleaned;
-}
-
-export function createUserMessage(content: string): ChatMessageData {
- return {
- type: "message",
- role: "user",
- content,
- timestamp: new Date(),
- };
-}
-
-export function filterAuthMessages(
- messages: ChatMessageData[],
-): ChatMessageData[] {
- return messages.filter(
- (msg) => msg.type !== "credentials_needed" && msg.type !== "login_needed",
- );
-}
-
-export function isValidMessage(msg: unknown): msg is Record {
- if (typeof msg !== "object" || msg === null) {
- return false;
- }
- const m = msg as Record;
- if (typeof m.role !== "string") {
- return false;
- }
- if (m.content !== undefined && typeof m.content !== "string") {
- return false;
- }
- return true;
-}
-
-export function isToolCallArray(value: unknown): value is Array<{
- id: string;
- type: string;
- function: { name: string; arguments: string };
-}> {
- if (!Array.isArray(value)) {
- return false;
- }
- return value.every(
- (item) =>
- typeof item === "object" &&
- item !== null &&
- "id" in item &&
- typeof item.id === "string" &&
- "type" in item &&
- typeof item.type === "string" &&
- "function" in item &&
- typeof item.function === "object" &&
- item.function !== null &&
- "name" in item.function &&
- typeof item.function.name === "string" &&
- "arguments" in item.function &&
- typeof item.function.arguments === "string",
- );
-}
-
-export function isAgentArray(value: unknown): value is Array<{
- id: string;
- name: string;
- description: string;
- version?: number;
- image_url?: string;
-}> {
- if (!Array.isArray(value)) {
- return false;
- }
- return value.every(
- (item) =>
- typeof item === "object" &&
- item !== null &&
- "id" in item &&
- typeof item.id === "string" &&
- "name" in item &&
- typeof item.name === "string" &&
- "description" in item &&
- typeof item.description === "string" &&
- (!("version" in item) || typeof item.version === "number") &&
- (!("image_url" in item) || typeof item.image_url === "string"),
- );
-}
-
-export function extractJsonFromErrorMessage(
- message: string,
-): Record | null {
- try {
- const start = message.indexOf("{");
- if (start === -1) {
- return null;
- }
- let depth = 0;
- let end = -1;
- for (let i = start; i < message.length; i++) {
- const ch = message[i];
- if (ch === "{") {
- depth++;
- } else if (ch === "}") {
- depth--;
- if (depth === 0) {
- end = i;
- break;
- }
- }
- }
- if (end === -1) {
- return null;
- }
- const jsonStr = message.slice(start, end + 1);
- return JSON.parse(jsonStr) as Record;
- } catch {
- return null;
- }
-}
-
-export function parseToolResponse(
- result: ToolResult,
- toolId: string,
- toolName: string,
- timestamp?: Date,
-): ChatMessageData | null {
- let parsedResult: Record | null = null;
- try {
- parsedResult =
- typeof result === "string"
- ? JSON.parse(result)
- : (result as Record);
- } catch {
- parsedResult = null;
- }
- if (parsedResult && typeof parsedResult === "object") {
- const responseType = parsedResult.type as string | undefined;
- if (responseType === "no_results") {
- return {
- type: "tool_response",
- toolId,
- toolName,
- result: (parsedResult.message as string) || "No results found",
- success: true,
- timestamp: timestamp || new Date(),
- };
- }
- if (responseType === "agent_carousel") {
- const agentsData = parsedResult.agents;
- if (isAgentArray(agentsData)) {
- return {
- type: "agent_carousel",
- toolId,
- toolName: "agent_carousel",
- agents: agentsData,
- totalCount: parsedResult.total_count as number | undefined,
- timestamp: timestamp || new Date(),
- };
- } else {
- console.warn("Invalid agents array in agent_carousel response");
- }
- }
- if (responseType === "execution_started") {
- return {
- type: "execution_started",
- toolId,
- toolName: "execution_started",
- executionId: (parsedResult.execution_id as string) || "",
- agentName: (parsedResult.graph_name as string) || undefined,
- message: parsedResult.message as string | undefined,
- libraryAgentLink: parsedResult.library_agent_link as string | undefined,
- timestamp: timestamp || new Date(),
- };
- }
- if (responseType === "clarification_needed") {
- return {
- type: "clarification_needed",
- toolName,
- questions:
- (parsedResult.questions as Array<{
- question: string;
- keyword: string;
- example?: string;
- }>) || [],
- message:
- (parsedResult.message as string) ||
- "I need more information to proceed.",
- sessionId: (parsedResult.session_id as string) || "",
- timestamp: timestamp || new Date(),
- };
- }
- if (responseType === "operation_started") {
- return {
- type: "operation_started",
- toolName: (parsedResult.tool_name as string) || toolName,
- toolId,
- operationId: (parsedResult.operation_id as string) || "",
- taskId: (parsedResult.task_id as string) || undefined, // For SSE reconnection
- message:
- (parsedResult.message as string) ||
- "Operation started. You can close this tab.",
- timestamp: timestamp || new Date(),
- };
- }
- if (responseType === "operation_pending") {
- return {
- type: "operation_pending",
- toolName: (parsedResult.tool_name as string) || toolName,
- toolId,
- operationId: (parsedResult.operation_id as string) || "",
- message:
- (parsedResult.message as string) ||
- "Operation in progress. Please wait...",
- timestamp: timestamp || new Date(),
- };
- }
- if (responseType === "operation_in_progress") {
- return {
- type: "operation_in_progress",
- toolName: (parsedResult.tool_name as string) || toolName,
- toolCallId: (parsedResult.tool_call_id as string) || toolId,
- message:
- (parsedResult.message as string) ||
- "Operation already in progress. Please wait...",
- timestamp: timestamp || new Date(),
- };
- }
- if (responseType === "need_login") {
- return {
- type: "login_needed",
- toolName: "login_needed",
- message:
- (parsedResult.message as string) ||
- "Please sign in to use chat and agent features",
- sessionId: (parsedResult.session_id as string) || "",
- agentInfo: parsedResult.agent_info as
- | {
- graph_id: string;
- name: string;
- trigger_type: string;
- }
- | undefined,
- timestamp: timestamp || new Date(),
- };
- }
- if (responseType === "setup_requirements") {
- return null;
- }
- if (responseType === "understanding_updated") {
- return {
- type: "tool_response",
- toolId,
- toolName,
- result: (parsedResult || result) as ToolResult,
- success: true,
- timestamp: timestamp || new Date(),
- };
- }
- }
- return {
- type: "tool_response",
- toolId,
- toolName,
- result: parsedResult ? (parsedResult as ToolResult) : result,
- success: true,
- timestamp: timestamp || new Date(),
- };
-}
-
-export function isUserReadiness(
- value: unknown,
-): value is { missing_credentials?: Record } {
- return (
- typeof value === "object" &&
- value !== null &&
- (!("missing_credentials" in value) ||
- typeof (value as any).missing_credentials === "object")
- );
-}
-
-export function isMissingCredentials(
- value: unknown,
-): value is Record> {
- if (typeof value !== "object" || value === null) {
- return false;
- }
- return Object.values(value).every((v) => typeof v === "object" && v !== null);
-}
-
-export function isSetupInfo(value: unknown): value is {
- user_readiness?: Record;
- agent_name?: string;
-} {
- return (
- typeof value === "object" &&
- value !== null &&
- (!("user_readiness" in value) ||
- typeof (value as any).user_readiness === "object") &&
- (!("agent_name" in value) || typeof (value as any).agent_name === "string")
- );
-}
-
-export function extractCredentialsNeeded(
- parsedResult: Record,
- toolName: string = "run_agent",
-): ChatMessageData | null {
- try {
- const setupInfo = parsedResult?.setup_info as
- | Record
- | undefined;
- const userReadiness = setupInfo?.user_readiness as
- | Record
- | undefined;
- const missingCreds = userReadiness?.missing_credentials as
- | Record>
- | undefined;
- if (missingCreds && Object.keys(missingCreds).length > 0) {
- const agentName = (setupInfo?.agent_name as string) || "this block";
- const credentials = Object.values(missingCreds).map((credInfo) => {
- // Normalize to array at boundary - prefer 'types' array, fall back to single 'type'
- const typesArray = credInfo.types as
- | Array<"api_key" | "oauth2" | "user_password" | "host_scoped">
- | undefined;
- const singleType =
- (credInfo.type as
- | "api_key"
- | "oauth2"
- | "user_password"
- | "host_scoped"
- | undefined) || "api_key";
- const credentialTypes =
- typesArray && typesArray.length > 0 ? typesArray : [singleType];
-
- return {
- provider: (credInfo.provider as string) || "unknown",
- providerName:
- (credInfo.provider_name as string) ||
- (credInfo.provider as string) ||
- "Unknown Provider",
- credentialTypes,
- title:
- (credInfo.title as string) ||
- `${(credInfo.provider_name as string) || (credInfo.provider as string)} credentials`,
- scopes: credInfo.scopes as string[] | undefined,
- };
- });
- return {
- type: "credentials_needed",
- toolName,
- credentials,
- message: `To run ${agentName}, you need to add ${credentials.length === 1 ? "credentials" : `${credentials.length} credentials`}.`,
- agentName,
- timestamp: new Date(),
- };
- }
- return null;
- } catch (err) {
- console.error("Failed to extract credentials from setup info:", err);
- return null;
- }
-}
-
-export function extractInputsNeeded(
- parsedResult: Record,
- toolName: string = "run_agent",
-): ChatMessageData | null {
- try {
- const setupInfo = parsedResult?.setup_info as
- | Record
- | undefined;
- const requirements = setupInfo?.requirements as
- | Record
- | undefined;
- const inputs = requirements?.inputs as
- | Array>
- | undefined;
- const credentials = requirements?.credentials as
- | Array>
- | undefined;
-
- if (!inputs || inputs.length === 0) {
- return null;
- }
-
- const agentName = (setupInfo?.agent_name as string) || "this agent";
- const agentId = parsedResult?.graph_id as string | undefined;
- const graphVersion = parsedResult?.graph_version as number | undefined;
-
- const properties: Record = {};
- const requiredProps: string[] = [];
- inputs.forEach((input) => {
- const name = input.name as string;
- if (name) {
- properties[name] = {
- title: input.name as string,
- description: (input.description as string) || "",
- type: (input.type as string) || "string",
- default: input.default,
- enum: input.options,
- format: input.format,
- };
- if ((input.required as boolean) === true) {
- requiredProps.push(name);
- }
- }
- });
-
- const inputSchema: Record = {
- type: "object",
- properties,
- };
- if (requiredProps.length > 0) {
- inputSchema.required = requiredProps;
- }
-
- const credentialsSchema: Record = {};
- if (credentials && credentials.length > 0) {
- credentials.forEach((cred) => {
- const id = cred.id as string;
- if (id) {
- const credentialTypes = Array.isArray(cred.types)
- ? cred.types
- : [(cred.type as string) || "api_key"];
- credentialsSchema[id] = {
- type: "object",
- properties: {},
- credentials_provider: [cred.provider as string],
- credentials_types: credentialTypes,
- credentials_scopes: cred.scopes as string[] | undefined,
- };
- }
- });
- }
-
- return {
- type: "inputs_needed",
- toolName,
- agentName,
- agentId,
- graphVersion,
- inputSchema,
- credentialsSchema:
- Object.keys(credentialsSchema).length > 0
- ? credentialsSchema
- : undefined,
- message: `Please provide the required inputs to run ${agentName}.`,
- timestamp: new Date(),
- };
- } catch (err) {
- console.error("Failed to extract inputs from setup info:", err);
- return null;
- }
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.ts
deleted file mode 100644
index 248383df42..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.ts
+++ /dev/null
@@ -1,517 +0,0 @@
-import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
-import { useEffect, useMemo, useRef, useState } from "react";
-import { INITIAL_STREAM_ID } from "../../chat-constants";
-import { useChatStore } from "../../chat-store";
-import { toast } from "sonner";
-import { useChatStream } from "../../useChatStream";
-import { usePageContext } from "../../usePageContext";
-import type { ChatMessageData } from "../ChatMessage/useChatMessage";
-import {
- getToolIdFromMessage,
- hasToolId,
- isOperationMessage,
- type StreamChunk,
-} from "../../chat-types";
-import { createStreamEventDispatcher } from "./createStreamEventDispatcher";
-import {
- createUserMessage,
- filterAuthMessages,
- hasSentInitialPrompt,
- markInitialPromptSent,
- processInitialMessages,
-} from "./helpers";
-
-const TOOL_RESULT_TYPES = new Set([
- "tool_response",
- "agent_carousel",
- "execution_started",
- "clarification_needed",
-]);
-
-// Helper to generate deduplication key for a message
-function getMessageKey(msg: ChatMessageData): string {
- if (msg.type === "message") {
- // Don't include timestamp - dedupe by role + content only
- // This handles the case where local and server timestamps differ
- // Server messages are authoritative, so duplicates from local state are filtered
- return `msg:${msg.role}:${msg.content}`;
- } else if (msg.type === "tool_call") {
- return `toolcall:${msg.toolId}`;
- } else if (TOOL_RESULT_TYPES.has(msg.type)) {
- // Unified key for all tool result types - same toolId with different types
- // (tool_response vs agent_carousel) should deduplicate to the same key
- const toolId = getToolIdFromMessage(msg);
- // If no toolId, fall back to content-based key to avoid empty key collisions
- if (!toolId) {
- return `toolresult:content:${JSON.stringify(msg).slice(0, 200)}`;
- }
- return `toolresult:${toolId}`;
- } else if (isOperationMessage(msg)) {
- const toolId = getToolIdFromMessage(msg) || "";
- return `op:${toolId}:${msg.toolName}`;
- } else {
- return `${msg.type}:${JSON.stringify(msg).slice(0, 100)}`;
- }
-}
-
-interface Args {
- sessionId: string | null;
- initialMessages: SessionDetailResponse["messages"];
- initialPrompt?: string;
- onOperationStarted?: () => void;
- /** Active stream info from the server for reconnection */
- activeStream?: {
- taskId: string;
- lastMessageId: string;
- operationId: string;
- toolName: string;
- };
-}
-
-export function useChatContainer({
- sessionId,
- initialMessages,
- initialPrompt,
- onOperationStarted,
- activeStream,
-}: Args) {
- const [messages, setMessages] = useState([]);
- const [streamingChunks, setStreamingChunks] = useState([]);
- const [hasTextChunks, setHasTextChunks] = useState(false);
- const [isStreamingInitiated, setIsStreamingInitiated] = useState(false);
- const [isRegionBlockedModalOpen, setIsRegionBlockedModalOpen] =
- useState(false);
- const hasResponseRef = useRef(false);
- const streamingChunksRef = useRef([]);
- const textFinalizedRef = useRef(false);
- const streamEndedRef = useRef(false);
- const previousSessionIdRef = useRef(null);
- const {
- error,
- sendMessage: sendStreamMessage,
- stopStreaming,
- } = useChatStream();
- const activeStreams = useChatStore((s) => s.activeStreams);
- const subscribeToStream = useChatStore((s) => s.subscribeToStream);
- const setActiveTask = useChatStore((s) => s.setActiveTask);
- const getActiveTask = useChatStore((s) => s.getActiveTask);
- const reconnectToTask = useChatStore((s) => s.reconnectToTask);
- const isStreaming = isStreamingInitiated || hasTextChunks;
- // Track whether we've already connected to this activeStream to avoid duplicate connections
- const connectedActiveStreamRef = useRef(null);
- // Track if component is mounted to prevent state updates after unmount
- const isMountedRef = useRef(true);
- // Track current dispatcher to prevent multiple dispatchers from adding messages
- const currentDispatcherIdRef = useRef(0);
-
- // Set mounted flag - reset on every mount, cleanup on unmount
- useEffect(function trackMountedState() {
- isMountedRef.current = true;
- return function cleanup() {
- isMountedRef.current = false;
- };
- }, []);
-
- // Callback to store active task info for SSE reconnection
- function handleActiveTaskStarted(taskInfo: {
- taskId: string;
- operationId: string;
- toolName: string;
- toolCallId: string;
- }) {
- if (!sessionId) return;
- setActiveTask(sessionId, {
- taskId: taskInfo.taskId,
- operationId: taskInfo.operationId,
- toolName: taskInfo.toolName,
- lastMessageId: INITIAL_STREAM_ID,
- });
- }
-
- // Create dispatcher for stream events - stable reference for current sessionId
- // Each dispatcher gets a unique ID to prevent stale dispatchers from updating state
- function createDispatcher() {
- if (!sessionId) return () => {};
- // Increment dispatcher ID - only the most recent dispatcher should update state
- const dispatcherId = ++currentDispatcherIdRef.current;
-
- const baseDispatcher = createStreamEventDispatcher({
- setHasTextChunks,
- setStreamingChunks,
- streamingChunksRef,
- hasResponseRef,
- textFinalizedRef,
- streamEndedRef,
- setMessages,
- setIsRegionBlockedModalOpen,
- sessionId,
- setIsStreamingInitiated,
- onOperationStarted,
- onActiveTaskStarted: handleActiveTaskStarted,
- });
-
- // Wrap dispatcher to check if it's still the current one
- return function guardedDispatcher(chunk: StreamChunk) {
- // Skip if component unmounted or this is a stale dispatcher
- if (!isMountedRef.current) {
- return;
- }
- if (dispatcherId !== currentDispatcherIdRef.current) {
- return;
- }
- baseDispatcher(chunk);
- };
- }
-
- useEffect(
- function handleSessionChange() {
- const isSessionChange = sessionId !== previousSessionIdRef.current;
-
- // Handle session change - reset state
- if (isSessionChange) {
- const prevSession = previousSessionIdRef.current;
- if (prevSession) {
- stopStreaming(prevSession);
- }
- previousSessionIdRef.current = sessionId;
- connectedActiveStreamRef.current = null;
- setMessages([]);
- setStreamingChunks([]);
- streamingChunksRef.current = [];
- setHasTextChunks(false);
- setIsStreamingInitiated(false);
- hasResponseRef.current = false;
- textFinalizedRef.current = false;
- streamEndedRef.current = false;
- }
-
- if (!sessionId) return;
-
- // Priority 1: Check if server told us there's an active stream (most authoritative)
- if (activeStream) {
- const streamKey = `${sessionId}:${activeStream.taskId}`;
-
- if (connectedActiveStreamRef.current === streamKey) {
- return;
- }
-
- // Skip if there's already an active stream for this session in the store
- const existingStream = activeStreams.get(sessionId);
- if (existingStream && existingStream.status === "streaming") {
- connectedActiveStreamRef.current = streamKey;
- return;
- }
-
- connectedActiveStreamRef.current = streamKey;
-
- // Clear all state before reconnection to prevent duplicates
- // Server's initialMessages is authoritative; local state will be rebuilt from SSE replay
- setMessages([]);
- setStreamingChunks([]);
- streamingChunksRef.current = [];
- setHasTextChunks(false);
- textFinalizedRef.current = false;
- streamEndedRef.current = false;
- hasResponseRef.current = false;
-
- setIsStreamingInitiated(true);
- setActiveTask(sessionId, {
- taskId: activeStream.taskId,
- operationId: activeStream.operationId,
- toolName: activeStream.toolName,
- lastMessageId: activeStream.lastMessageId,
- });
- reconnectToTask(
- sessionId,
- activeStream.taskId,
- activeStream.lastMessageId,
- createDispatcher(),
- );
- // Don't return cleanup here - the guarded dispatcher handles stale events
- // and the stream will complete naturally. Cleanup would prematurely stop
- // the stream when effect re-runs due to activeStreams changing.
- return;
- }
-
- // Only check localStorage/in-memory on session change
- if (!isSessionChange) return;
-
- // Priority 2: Check localStorage for active task
- const activeTask = getActiveTask(sessionId);
- if (activeTask) {
- // Clear all state before reconnection to prevent duplicates
- // Server's initialMessages is authoritative; local state will be rebuilt from SSE replay
- setMessages([]);
- setStreamingChunks([]);
- streamingChunksRef.current = [];
- setHasTextChunks(false);
- textFinalizedRef.current = false;
- streamEndedRef.current = false;
- hasResponseRef.current = false;
-
- setIsStreamingInitiated(true);
- reconnectToTask(
- sessionId,
- activeTask.taskId,
- activeTask.lastMessageId,
- createDispatcher(),
- );
- // Don't return cleanup here - the guarded dispatcher handles stale events
- return;
- }
-
- // Priority 3: Check for an in-memory active stream (same-tab scenario)
- const inMemoryStream = activeStreams.get(sessionId);
- if (!inMemoryStream || inMemoryStream.status !== "streaming") {
- return;
- }
-
- setIsStreamingInitiated(true);
- const skipReplay = initialMessages.length > 0;
- return subscribeToStream(sessionId, createDispatcher(), skipReplay);
- },
- [
- sessionId,
- stopStreaming,
- activeStreams,
- subscribeToStream,
- onOperationStarted,
- getActiveTask,
- reconnectToTask,
- activeStream,
- setActiveTask,
- ],
- );
-
- // Collect toolIds from completed tool results in initialMessages
- // Used to filter out operation messages when their results arrive
- const completedToolIds = useMemo(() => {
- const processedInitial = processInitialMessages(initialMessages);
- const ids = new Set();
- for (const msg of processedInitial) {
- if (
- msg.type === "tool_response" ||
- msg.type === "agent_carousel" ||
- msg.type === "execution_started"
- ) {
- const toolId = hasToolId(msg) ? msg.toolId : undefined;
- if (toolId) {
- ids.add(toolId);
- }
- }
- }
- return ids;
- }, [initialMessages]);
-
- // Clean up local operation messages when their completed results arrive from polling
- // This effect runs when completedToolIds changes (i.e., when polling brings new results)
- useEffect(
- function cleanupCompletedOperations() {
- if (completedToolIds.size === 0) return;
-
- setMessages((prev) => {
- const filtered = prev.filter((msg) => {
- if (isOperationMessage(msg)) {
- const toolId = getToolIdFromMessage(msg);
- if (toolId && completedToolIds.has(toolId)) {
- return false; // Remove - operation completed
- }
- }
- return true;
- });
- // Only update state if something was actually filtered
- return filtered.length === prev.length ? prev : filtered;
- });
- },
- [completedToolIds],
- );
-
- // Combine initial messages from backend with local streaming messages,
- // Server messages maintain correct order; only append truly new local messages
- const allMessages = useMemo(() => {
- const processedInitial = processInitialMessages(initialMessages);
-
- // Build a set of keys from server messages for deduplication
- const serverKeys = new Set();
- for (const msg of processedInitial) {
- serverKeys.add(getMessageKey(msg));
- }
-
- // Filter local messages: remove duplicates and completed operation messages
- const newLocalMessages = messages.filter((msg) => {
- // Remove operation messages for completed tools
- if (isOperationMessage(msg)) {
- const toolId = getToolIdFromMessage(msg);
- if (toolId && completedToolIds.has(toolId)) {
- return false;
- }
- }
- // Remove messages that already exist in server data
- const key = getMessageKey(msg);
- return !serverKeys.has(key);
- });
-
- // Server messages first (correct order), then new local messages
- const combined = [...processedInitial, ...newLocalMessages];
-
- // Post-processing: Remove duplicate assistant messages that can occur during
- // race conditions (e.g., rapid screen switching during SSE reconnection).
- // Two assistant messages are considered duplicates if:
- // - They are both text messages with role "assistant"
- // - One message's content starts with the other's content (partial vs complete)
- // - Or they have very similar content (>80% overlap at the start)
- const deduplicated: ChatMessageData[] = [];
- for (let i = 0; i < combined.length; i++) {
- const current = combined[i];
-
- // Check if this is an assistant text message
- if (current.type !== "message" || current.role !== "assistant") {
- deduplicated.push(current);
- continue;
- }
-
- // Look for duplicate assistant messages in the rest of the array
- let dominated = false;
- for (let j = 0; j < combined.length; j++) {
- if (i === j) continue;
- const other = combined[j];
- if (other.type !== "message" || other.role !== "assistant") continue;
-
- const currentContent = current.content || "";
- const otherContent = other.content || "";
-
- // Skip empty messages
- if (!currentContent.trim() || !otherContent.trim()) continue;
-
- // Check if current is a prefix of other (current is incomplete version)
- if (
- otherContent.length > currentContent.length &&
- otherContent.startsWith(currentContent.slice(0, 100))
- ) {
- // Current is a shorter/incomplete version of other - skip it
- dominated = true;
- break;
- }
-
- // Check if messages are nearly identical (within a small difference)
- // This catches cases where content differs only slightly
- const minLen = Math.min(currentContent.length, otherContent.length);
- const compareLen = Math.min(minLen, 200); // Compare first 200 chars
- if (
- compareLen > 50 &&
- currentContent.slice(0, compareLen) ===
- otherContent.slice(0, compareLen)
- ) {
- // Same prefix - keep the longer one
- if (otherContent.length > currentContent.length) {
- dominated = true;
- break;
- }
- }
- }
-
- if (!dominated) {
- deduplicated.push(current);
- }
- }
-
- return deduplicated;
- }, [initialMessages, messages, completedToolIds]);
-
- async function sendMessage(
- content: string,
- isUserMessage: boolean = true,
- context?: { url: string; content: string },
- ) {
- if (!sessionId) return;
-
- setIsRegionBlockedModalOpen(false);
- if (isUserMessage) {
- const userMessage = createUserMessage(content);
- setMessages((prev) => [...filterAuthMessages(prev), userMessage]);
- } else {
- setMessages((prev) => filterAuthMessages(prev));
- }
- setStreamingChunks([]);
- streamingChunksRef.current = [];
- setHasTextChunks(false);
- setIsStreamingInitiated(true);
- hasResponseRef.current = false;
- textFinalizedRef.current = false;
- streamEndedRef.current = false;
-
- try {
- await sendStreamMessage(
- sessionId,
- content,
- createDispatcher(),
- isUserMessage,
- context,
- );
- } catch (err) {
- setIsStreamingInitiated(false);
- if (err instanceof Error && err.name === "AbortError") return;
-
- const errorMessage =
- err instanceof Error ? err.message : "Failed to send message";
- toast.error("Failed to send message", {
- description: errorMessage,
- });
- }
- }
-
- function handleStopStreaming() {
- stopStreaming();
- setStreamingChunks([]);
- streamingChunksRef.current = [];
- setHasTextChunks(false);
- setIsStreamingInitiated(false);
- }
-
- const { capturePageContext } = usePageContext();
- const sendMessageRef = useRef(sendMessage);
- sendMessageRef.current = sendMessage;
-
- useEffect(
- function handleInitialPrompt() {
- if (!initialPrompt || !sessionId) return;
- if (initialMessages.length > 0) return;
- if (hasSentInitialPrompt(sessionId)) return;
-
- markInitialPromptSent(sessionId);
- const context = capturePageContext();
- sendMessageRef.current(initialPrompt, true, context);
- },
- [initialPrompt, sessionId, initialMessages.length, capturePageContext],
- );
-
- async function sendMessageWithContext(
- content: string,
- isUserMessage: boolean = true,
- ) {
- const context = capturePageContext();
- await sendMessage(content, isUserMessage, context);
- }
-
- function handleRegionModalOpenChange(open: boolean) {
- setIsRegionBlockedModalOpen(open);
- }
-
- function handleRegionModalClose() {
- setIsRegionBlockedModalOpen(false);
- }
-
- return {
- messages: allMessages,
- streamingChunks,
- isStreaming,
- error,
- isRegionBlockedModalOpen,
- setIsRegionBlockedModalOpen,
- sendMessageWithContext,
- handleRegionModalOpenChange,
- handleRegionModalClose,
- sendMessage,
- stopStreaming: handleStopStreaming,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup.tsx
deleted file mode 100644
index f0dfadd1f7..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import { Text } from "@/components/atoms/Text/Text";
-import { CredentialsInput } from "@/components/contextual/CredentialsInput/CredentialsInput";
-import type { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
-import { cn } from "@/lib/utils";
-import { CheckIcon, RobotIcon, WarningIcon } from "@phosphor-icons/react";
-import { useEffect, useRef } from "react";
-import { useChatCredentialsSetup } from "./useChatCredentialsSetup";
-
-export interface CredentialInfo {
- provider: string;
- providerName: string;
- credentialTypes: Array<
- "api_key" | "oauth2" | "user_password" | "host_scoped"
- >;
- title: string;
- scopes?: string[];
-}
-
-interface Props {
- credentials: CredentialInfo[];
- agentName?: string;
- message: string;
- onAllCredentialsComplete: () => void;
- onCancel: () => void;
- className?: string;
-}
-
-function createSchemaFromCredentialInfo(
- credential: CredentialInfo,
-): BlockIOCredentialsSubSchema {
- return {
- type: "object",
- properties: {},
- credentials_provider: [credential.provider],
- credentials_types: credential.credentialTypes,
- credentials_scopes: credential.scopes,
- discriminator: undefined,
- discriminator_mapping: undefined,
- discriminator_values: undefined,
- };
-}
-
-export function ChatCredentialsSetup({
- credentials,
- agentName: _agentName,
- message,
- onAllCredentialsComplete,
- onCancel: _onCancel,
-}: Props) {
- const { selectedCredentials, isAllComplete, handleCredentialSelect } =
- useChatCredentialsSetup(credentials);
-
- // Track if we've already called completion to prevent double calls
- const hasCalledCompleteRef = useRef(false);
-
- // Reset the completion flag when credentials change (new credential setup flow)
- useEffect(
- function resetCompletionFlag() {
- hasCalledCompleteRef.current = false;
- },
- [credentials],
- );
-
- // Auto-call completion when all credentials are configured
- useEffect(
- function autoCompleteWhenReady() {
- if (isAllComplete && !hasCalledCompleteRef.current) {
- hasCalledCompleteRef.current = true;
- onAllCredentialsComplete();
- }
- },
- [isAllComplete, onAllCredentialsComplete],
- );
-
- return (
-
-
-
-
-
-
-
-
-
-
- Credentials Required
-
-
- {message}
-
-
-
-
- {credentials.map((cred, index) => {
- const schema = createSchemaFromCredentialInfo(cred);
- const isSelected = !!selectedCredentials[cred.provider];
-
- return (
-
-
- {isSelected ? (
-
- ) : (
-
- )}
-
- {cred.providerName}
-
-
-
-
- handleCredentialSelect(cred.provider, credMeta)
- }
- />
-
- );
- })}
-
-
-
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/useChatCredentialsSetup.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/useChatCredentialsSetup.ts
deleted file mode 100644
index 6b4b26e834..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/useChatCredentialsSetup.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useState, useMemo } from "react";
-import type { CredentialInfo } from "./ChatCredentialsSetup";
-import type { CredentialsMetaInput } from "@/lib/autogpt-server-api";
-
-export function useChatCredentialsSetup(credentials: CredentialInfo[]) {
- const [selectedCredentials, setSelectedCredentials] = useState<
- Record
- >({});
-
- // Check if all credentials are configured
- const isAllComplete = useMemo(
- function checkAllComplete() {
- if (credentials.length === 0) return false;
- return credentials.every((cred) => selectedCredentials[cred.provider]);
- },
- [credentials, selectedCredentials],
- );
-
- function handleCredentialSelect(
- provider: string,
- credential?: CredentialsMetaInput,
- ) {
- if (credential) {
- setSelectedCredentials((prev) => ({
- ...prev,
- [provider]: credential,
- }));
- }
- }
-
- return {
- selectedCredentials,
- isAllComplete,
- handleCredentialSelect,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatErrorState/ChatErrorState.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatErrorState/ChatErrorState.tsx
deleted file mode 100644
index bac13d1b0c..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatErrorState/ChatErrorState.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from "react";
-import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
-import { cn } from "@/lib/utils";
-
-export interface ChatErrorStateProps {
- error: Error;
- onRetry?: () => void;
- className?: string;
-}
-
-export function ChatErrorState({
- error,
- onRetry,
- className,
-}: ChatErrorStateProps) {
- return (
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatLoader/ChatLoader.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatLoader/ChatLoader.tsx
deleted file mode 100644
index 76cee8dbae..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatLoader/ChatLoader.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export function ChatLoader() {
- return (
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatLoadingState/ChatLoadingState.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatLoadingState/ChatLoadingState.tsx
deleted file mode 100644
index c0cdb33c50..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatLoadingState/ChatLoadingState.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
-import { cn } from "@/lib/utils";
-
-export interface ChatLoadingStateProps {
- message?: string;
- className?: string;
-}
-
-export function ChatLoadingState({ className }: ChatLoadingStateProps) {
- return (
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatMessage/ChatMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatMessage/ChatMessage.tsx
deleted file mode 100644
index 44dae40eb4..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatMessage/ChatMessage.tsx
+++ /dev/null
@@ -1,448 +0,0 @@
-"use client";
-
-import { Button } from "@/components/atoms/Button/Button";
-import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
-import { cn } from "@/lib/utils";
-import {
- ArrowsClockwiseIcon,
- CheckCircleIcon,
- CheckIcon,
-} from "@phosphor-icons/react";
-import { useRouter } from "next/navigation";
-import { useCallback, useState } from "react";
-import { AgentCarouselMessage } from "../AgentCarouselMessage/AgentCarouselMessage";
-import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
-import { AuthPromptWidget } from "../AuthPromptWidget/AuthPromptWidget";
-import { ChatCredentialsSetup } from "../ChatCredentialsSetup/ChatCredentialsSetup";
-import { ClarificationQuestionsWidget } from "../ClarificationQuestionsWidget/ClarificationQuestionsWidget";
-import { ExecutionStartedMessage } from "../ExecutionStartedMessage/ExecutionStartedMessage";
-import { PendingOperationWidget } from "../PendingOperationWidget/PendingOperationWidget";
-import { MarkdownContent } from "../MarkdownContent/MarkdownContent";
-import { NoResultsMessage } from "../NoResultsMessage/NoResultsMessage";
-import { ToolCallMessage } from "../ToolCallMessage/ToolCallMessage";
-import { ToolResponseMessage } from "../ToolResponseMessage/ToolResponseMessage";
-import { UserChatBubble } from "../UserChatBubble/UserChatBubble";
-import { useChatMessage, type ChatMessageData } from "./useChatMessage";
-
-function stripInternalReasoning(content: string): string {
- const cleaned = content.replace(
- /[\s\S]*?<\/internal_reasoning>/gi,
- "",
- );
- return cleaned.replace(/\n{3,}/g, "\n\n").trim();
-}
-
-function getDisplayContent(message: ChatMessageData, isUser: boolean): string {
- if (message.type !== "message") return "";
- if (isUser) return message.content;
- return stripInternalReasoning(message.content);
-}
-
-export interface ChatMessageProps {
- message: ChatMessageData;
- messages?: ChatMessageData[];
- index?: number;
- isStreaming?: boolean;
- className?: string;
- onDismissLogin?: () => void;
- onDismissCredentials?: () => void;
- onSendMessage?: (content: string, isUserMessage?: boolean) => void;
- agentOutput?: ChatMessageData;
- isFinalMessage?: boolean;
-}
-
-export function ChatMessage({
- message,
- messages = [],
- index = -1,
- isStreaming = false,
- className,
- onDismissCredentials,
- onSendMessage,
- agentOutput,
- isFinalMessage = true,
-}: ChatMessageProps) {
- const { user } = useSupabase();
- const router = useRouter();
- const [copied, setCopied] = useState(false);
- const {
- isUser,
- isToolCall,
- isToolResponse,
- isLoginNeeded,
- isCredentialsNeeded,
- isClarificationNeeded,
- isOperationStarted,
- isOperationPending,
- isOperationInProgress,
- } = useChatMessage(message);
- const displayContent = getDisplayContent(message, isUser);
-
- const handleAllCredentialsComplete = useCallback(
- function handleAllCredentialsComplete() {
- // Send a user message that explicitly asks to retry the setup
- // This ensures the LLM calls get_required_setup_info again and proceeds with execution
- if (onSendMessage) {
- onSendMessage(
- "I've configured the required credentials. Please check if everything is ready and proceed with setting up the agent.",
- );
- }
- // Optionally dismiss the credentials prompt
- if (onDismissCredentials) {
- onDismissCredentials();
- }
- },
- [onSendMessage, onDismissCredentials],
- );
-
- function handleCancelCredentials() {
- // Dismiss the credentials prompt
- if (onDismissCredentials) {
- onDismissCredentials();
- }
- }
-
- const handleCopy = useCallback(
- async function handleCopy() {
- if (message.type !== "message") return;
- if (!displayContent) return;
-
- try {
- await navigator.clipboard.writeText(displayContent);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- } catch (error) {
- console.error("Failed to copy:", error);
- }
- },
- [displayContent, message],
- );
-
- const handleTryAgain = useCallback(() => {
- if (message.type !== "message" || !onSendMessage) return;
- onSendMessage(message.content, message.role === "user");
- }, [message, onSendMessage]);
-
- const handleViewExecution = useCallback(() => {
- if (message.type === "execution_started" && message.libraryAgentLink) {
- router.push(message.libraryAgentLink);
- }
- }, [message, router]);
-
- // Render credentials needed messages
- if (isCredentialsNeeded && message.type === "credentials_needed") {
- return (
-
- );
- }
-
- if (isClarificationNeeded && message.type === "clarification_needed") {
- const hasUserReplyAfter =
- index >= 0 &&
- messages
- .slice(index + 1)
- .some((m) => m.type === "message" && m.role === "user");
-
- const handleClarificationAnswers = (answers: Record) => {
- if (onSendMessage) {
- // Iterate over questions (preserves original order) instead of answers
- const contextMessage = message.questions
- .map((q) => {
- const answer = answers[q.keyword] || "";
- return `> ${q.question}\n\n${answer}`;
- })
- .join("\n\n");
-
- onSendMessage(
- `**Here are my answers:**\n\n${contextMessage}\n\nPlease proceed with creating the agent.`,
- );
- }
- };
-
- return (
-
- );
- }
-
- // Render login needed messages
- if (isLoginNeeded && message.type === "login_needed") {
- // If user is already logged in, show success message instead of auth prompt
- if (user) {
- return (
-
-
-
-
-
-
-
-
-
- Successfully Authenticated
-
-
- You're now signed in and ready to continue
-
-
-
-
-
-
- );
- }
-
- // Show auth prompt if not logged in
- return (
-
- );
- }
-
- // Render tool call messages
- if (isToolCall && message.type === "tool_call") {
- // Check if this tool call is currently streaming
- // A tool call is streaming if:
- // 1. isStreaming is true
- // 2. This is the last tool_call message
- // 3. There's no tool_response for this tool call yet
- const isToolCallStreaming =
- isStreaming &&
- index >= 0 &&
- (() => {
- // Find the last tool_call index
- let lastToolCallIndex = -1;
- for (let i = messages.length - 1; i >= 0; i--) {
- if (messages[i].type === "tool_call") {
- lastToolCallIndex = i;
- break;
- }
- }
- // Check if this is the last tool_call and there's no response yet
- if (index === lastToolCallIndex) {
- // Check if there's a tool_response for this tool call
- const hasResponse = messages
- .slice(index + 1)
- .some(
- (msg) =>
- msg.type === "tool_response" && msg.toolId === message.toolId,
- );
- return !hasResponse;
- }
- return false;
- })();
-
- return (
-
-
-
- );
- }
-
- // Render no_results messages - use dedicated component, not ToolResponseMessage
- if (message.type === "no_results") {
- return (
-
-
-
- );
- }
-
- // Render agent_carousel messages - use dedicated component, not ToolResponseMessage
- if (message.type === "agent_carousel") {
- return (
-
- );
- }
-
- // Render execution_started messages - use dedicated component, not ToolResponseMessage
- if (message.type === "execution_started") {
- return (
-
-
-
- );
- }
-
- // Render operation_started messages (long-running background operations)
- if (isOperationStarted && message.type === "operation_started") {
- return (
-
- );
- }
-
- // Render operation_pending messages (operations in progress when refreshing)
- if (isOperationPending && message.type === "operation_pending") {
- return (
-
- );
- }
-
- // Render operation_in_progress messages (duplicate request while operation running)
- if (isOperationInProgress && message.type === "operation_in_progress") {
- return (
-
- );
- }
-
- // Render tool response messages (but skip agent_output if it's being rendered inside assistant message)
- if (isToolResponse && message.type === "tool_response") {
- return (
-
-
-
- );
- }
-
- // Render regular chat messages
- if (message.type === "message") {
- return (
-
-
-
- {isUser ? (
-
-
-
- ) : (
-
-
- {agentOutput && agentOutput.type === "tool_response" && (
-
-
-
- )}
-
- )}
-
- {isUser && onSendMessage && (
-
-
-
- )}
- {!isUser && isFinalMessage && !isStreaming && (
-
- {copied ? (
-
- ) : (
-
-
-
-
- )}
-
- )}
-
-
-
-
- );
- }
-
- // Fallback for unknown message types
- return null;
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatMessage/useChatMessage.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatMessage/useChatMessage.ts
deleted file mode 100644
index 6809497a93..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatMessage/useChatMessage.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-import type { ToolArguments, ToolResult } from "@/types/chat";
-import { formatDistanceToNow } from "date-fns";
-
-export type ChatMessageData =
- | {
- type: "message";
- role: "user" | "assistant" | "system";
- content: string;
- timestamp?: string | Date;
- }
- | {
- type: "tool_call";
- toolId: string;
- toolName: string;
- arguments?: ToolArguments;
- timestamp?: string | Date;
- }
- | {
- type: "tool_response";
- toolId: string;
- toolName: string;
- result: ToolResult;
- success?: boolean;
- timestamp?: string | Date;
- }
- | {
- type: "login_needed";
- toolName: string;
- message: string;
- sessionId: string;
- agentInfo?: {
- graph_id: string;
- name: string;
- trigger_type: string;
- };
- timestamp?: string | Date;
- }
- | {
- type: "credentials_needed";
- toolName: string;
- credentials: Array<{
- provider: string;
- providerName: string;
- credentialTypes: Array<
- "api_key" | "oauth2" | "user_password" | "host_scoped"
- >;
- title: string;
- scopes?: string[];
- }>;
- message: string;
- agentName?: string;
- timestamp?: string | Date;
- }
- | {
- type: "no_results";
- toolName: string;
- message: string;
- suggestions?: string[];
- sessionId?: string;
- timestamp?: string | Date;
- }
- | {
- type: "agent_carousel";
- toolId: string;
- toolName: string;
- agents: Array<{
- id: string;
- name: string;
- description: string;
- version?: number;
- image_url?: string;
- }>;
- totalCount?: number;
- timestamp?: string | Date;
- }
- | {
- type: "execution_started";
- toolId: string;
- toolName: string;
- executionId: string;
- agentName?: string;
- message?: string;
- libraryAgentLink?: string;
- timestamp?: string | Date;
- }
- | {
- type: "inputs_needed";
- toolName: string;
- agentName?: string;
- agentId?: string;
- graphVersion?: number;
- inputSchema: Record;
- credentialsSchema?: Record;
- message: string;
- timestamp?: string | Date;
- }
- | {
- type: "clarification_needed";
- toolName: string;
- questions: Array<{
- question: string;
- keyword: string;
- example?: string;
- }>;
- message: string;
- sessionId: string;
- timestamp?: string | Date;
- }
- | {
- type: "operation_started";
- toolName: string;
- toolId: string;
- operationId: string;
- taskId?: string; // For SSE reconnection
- message: string;
- timestamp?: string | Date;
- }
- | {
- type: "operation_pending";
- toolName: string;
- toolId: string;
- operationId: string;
- message: string;
- timestamp?: string | Date;
- }
- | {
- type: "operation_in_progress";
- toolName: string;
- toolCallId: string;
- message: string;
- timestamp?: string | Date;
- };
-
-export function useChatMessage(message: ChatMessageData) {
- const formattedTimestamp = message.timestamp
- ? formatDistanceToNow(new Date(message.timestamp), { addSuffix: true })
- : "Just now";
-
- return {
- formattedTimestamp,
- isUser: message.type === "message" && message.role === "user",
- isAssistant: message.type === "message" && message.role === "assistant",
- isSystem: message.type === "message" && message.role === "system",
- isToolCall: message.type === "tool_call",
- isToolResponse: message.type === "tool_response",
- isLoginNeeded: message.type === "login_needed",
- isCredentialsNeeded: message.type === "credentials_needed",
- isNoResults: message.type === "no_results",
- isAgentCarousel: message.type === "agent_carousel",
- isExecutionStarted: message.type === "execution_started",
- isInputsNeeded: message.type === "inputs_needed",
- isClarificationNeeded: message.type === "clarification_needed",
- isOperationStarted: message.type === "operation_started",
- isOperationPending: message.type === "operation_pending",
- isOperationInProgress: message.type === "operation_in_progress",
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ExecutionStartedMessage/ExecutionStartedMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ExecutionStartedMessage/ExecutionStartedMessage.tsx
deleted file mode 100644
index 1ac3b440e0..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ExecutionStartedMessage/ExecutionStartedMessage.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { Button } from "@/components/atoms/Button/Button";
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-import { ArrowSquareOut, CheckCircle, Play } from "@phosphor-icons/react";
-
-export interface ExecutionStartedMessageProps {
- executionId: string;
- agentName?: string;
- message?: string;
- onViewExecution?: () => void;
- className?: string;
-}
-
-export function ExecutionStartedMessage({
- executionId,
- agentName,
- message = "Agent execution started successfully",
- onViewExecution,
- className,
-}: ExecutionStartedMessageProps) {
- return (
-
- {/* Icon & Header */}
-
-
-
-
-
-
- Execution Started
-
-
- {message}
-
-
-
-
- {/* Details */}
-
-
- {agentName && (
-
-
- Agent:
-
-
- {agentName}
-
-
- )}
-
-
- Execution ID:
-
-
- {executionId.slice(0, 16)}...
-
-
-
-
-
- {/* Action Buttons */}
- {onViewExecution && (
-
- )}
-
-
-
-
- Your agent is now running. You can monitor its progress in the monitor
- page.
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MarkdownContent/MarkdownContent.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/MarkdownContent/MarkdownContent.tsx
deleted file mode 100644
index ecadbe938b..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MarkdownContent/MarkdownContent.tsx
+++ /dev/null
@@ -1,349 +0,0 @@
-"use client";
-
-import { getGetWorkspaceDownloadFileByIdUrl } from "@/app/api/__generated__/endpoints/workspace/workspace";
-import { cn } from "@/lib/utils";
-import { EyeSlash } from "@phosphor-icons/react";
-import React, { useState } from "react";
-import ReactMarkdown from "react-markdown";
-import remarkGfm from "remark-gfm";
-
-interface MarkdownContentProps {
- content: string;
- className?: string;
-}
-
-interface CodeProps extends React.HTMLAttributes {
- children?: React.ReactNode;
- className?: string;
-}
-
-interface ListProps extends React.HTMLAttributes {
- children?: React.ReactNode;
- className?: string;
-}
-
-interface ListItemProps extends React.HTMLAttributes {
- children?: React.ReactNode;
- className?: string;
-}
-
-interface InputProps extends React.InputHTMLAttributes {
- type?: string;
-}
-
-/**
- * Converts a workspace:// URL to a proxy URL that routes through Next.js to the backend.
- * workspace://abc123 -> /api/proxy/api/workspace/files/abc123/download
- *
- * Uses the generated API URL helper and routes through the Next.js proxy
- * which handles authentication and proper backend routing.
- */
-/**
- * URL transformer for ReactMarkdown.
- * Converts workspace:// URLs to proxy URLs that route through Next.js to the backend.
- * workspace://abc123 -> /api/proxy/api/workspace/files/abc123/download
- *
- * This is needed because ReactMarkdown sanitizes URLs and only allows
- * http, https, mailto, and tel protocols by default.
- */
-function resolveWorkspaceUrl(src: string): string {
- if (src.startsWith("workspace://")) {
- // Strip MIME type fragment if present (e.g., workspace://abc123#video/mp4 → abc123)
- const withoutPrefix = src.replace("workspace://", "");
- const fileId = withoutPrefix.split("#")[0];
- // Use the generated API URL helper to get the correct path
- const apiPath = getGetWorkspaceDownloadFileByIdUrl(fileId);
- // Route through the Next.js proxy (same pattern as customMutator for client-side)
- return `/api/proxy${apiPath}`;
- }
- return src;
-}
-
-/**
- * Check if the image URL is a workspace file (AI cannot see these yet).
- * After URL transformation, workspace files have URLs like /api/proxy/api/workspace/files/...
- */
-function isWorkspaceImage(src: string | undefined): boolean {
- return src?.includes("/workspace/files/") ?? false;
-}
-
-/**
- * Renders a workspace video with controls and an optional "AI cannot see" badge.
- */
-function WorkspaceVideo({
- src,
- aiCannotSee,
-}: {
- src: string;
- aiCannotSee: boolean;
-}) {
- return (
-
-
-
- Your browser does not support the video tag.
-
- {aiCannotSee && (
-
-
- AI cannot see this video
-
- )}
-
- );
-}
-
-/**
- * Custom image component that shows an indicator when the AI cannot see the image.
- * Also handles the "video:" alt-text prefix convention to render elements.
- * For workspace files with unknown types, falls back to if fails.
- * Note: src is already transformed by urlTransform, so workspace:// is now /api/workspace/...
- */
-function MarkdownImage(props: Record) {
- const src = props.src as string | undefined;
- const alt = props.alt as string | undefined;
- const [imgFailed, setImgFailed] = useState(false);
-
- const aiCannotSee = isWorkspaceImage(src);
-
- // If no src, show a placeholder
- if (!src) {
- return (
-
- [Image: {alt || "missing src"}]
-
- );
- }
-
- // Detect video: prefix in alt text (set by formatOutputValue in helpers.ts)
- if (alt?.startsWith("video:")) {
- return ;
- }
-
- // If the failed to load and this is a workspace file, try as video.
- // This handles generic output keys like "file_out" where the MIME type
- // isn't known from the key name alone.
- if (imgFailed && aiCannotSee) {
- return ;
- }
-
- return (
-
- {/* eslint-disable-next-line @next/next/no-img-element */}
- {
- if (aiCannotSee) setImgFailed(true);
- }}
- />
- {aiCannotSee && (
-
-
- AI cannot see this image
-
- )}
-
- );
-}
-
-export function MarkdownContent({ content, className }: MarkdownContentProps) {
- return (
-
-
{
- const isInline = !className?.includes("language-");
- if (isInline) {
- return (
-
- {children}
-
- );
- }
- return (
-
- {children}
-
- );
- },
- pre: ({ children, ...props }) => (
-
- {children}
-
- ),
- a: ({ children, href, ...props }) => (
-
- {children}
-
- ),
- strong: ({ children, ...props }) => (
-
- {children}
-
- ),
- em: ({ children, ...props }) => (
-
- {children}
-
- ),
- del: ({ children, ...props }) => (
-
- {children}
-
- ),
- ul: ({ children, ...props }: ListProps) => (
-
- ),
- ol: ({ children, ...props }) => (
-
- {children}
-
- ),
- li: ({ children, ...props }: ListItemProps) => (
-
- {children}
-
- ),
- input: ({ ...props }: InputProps) => {
- if (props.type === "checkbox") {
- return (
-
- );
- }
- return ;
- },
- blockquote: ({ children, ...props }) => (
-
- {children}
-
- ),
- h1: ({ children, ...props }) => (
-
- {children}
-
- ),
- h2: ({ children, ...props }) => (
-
- {children}
-
- ),
- h3: ({ children, ...props }) => (
-
- {children}
-
- ),
- h4: ({ children, ...props }) => (
-
- {children}
-
- ),
- h5: ({ children, ...props }) => (
-
- {children}
-
- ),
- h6: ({ children, ...props }) => (
-
- {children}
-
- ),
- p: ({ children, ...props }) => (
-
- {children}
-
- ),
- hr: ({ ...props }) => (
-
- ),
- table: ({ children, ...props }) => (
-
- ),
- th: ({ children, ...props }) => (
-
- {children}
-
- ),
- td: ({ children, ...props }) => (
-
- {children}
-
- ),
- img: ({ src, alt, ...props }) => (
-
- ),
- }}
- >
- {content}
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageBubble/MessageBubble.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageBubble/MessageBubble.tsx
deleted file mode 100644
index 70d5d135a1..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageBubble/MessageBubble.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { cn } from "@/lib/utils";
-import { ReactNode } from "react";
-
-export interface MessageBubbleProps {
- children: ReactNode;
- variant: "user" | "assistant";
- className?: string;
-}
-
-export function MessageBubble({
- children,
- variant,
- className,
-}: MessageBubbleProps) {
- const userTheme = {
- bg: "bg-purple-100",
- border: "border-purple-100",
- text: "text-slate-900",
- };
-
- const assistantTheme = {
- bg: "bg-slate-50/20",
- border: "border-slate-100",
- gradient: "from-slate-200/20 via-slate-300/10 to-transparent",
- text: "text-slate-900",
- };
-
- const theme = variant === "user" ? userTheme : assistantTheme;
-
- return (
-
- {/* Gradient flare background */}
-
-
- {children}
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/MessageList.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/MessageList.tsx
deleted file mode 100644
index d8478f1e82..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/MessageList.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-"use client";
-
-import { cn } from "@/lib/utils";
-import type { ChatMessageData } from "../ChatMessage/useChatMessage";
-import { StreamingMessage } from "../StreamingMessage/StreamingMessage";
-import { ThinkingMessage } from "../ThinkingMessage/ThinkingMessage";
-import { LastToolResponse } from "./components/LastToolResponse/LastToolResponse";
-import { MessageItem } from "./components/MessageItem/MessageItem";
-import { findLastMessageIndex, shouldSkipAgentOutput } from "./helpers";
-import { useMessageList } from "./useMessageList";
-
-export interface MessageListProps {
- messages: ChatMessageData[];
- streamingChunks?: string[];
- isStreaming?: boolean;
- className?: string;
- onStreamComplete?: () => void;
- onSendMessage?: (content: string) => void;
-}
-
-export function MessageList({
- messages,
- streamingChunks = [],
- isStreaming = false,
- className,
- onStreamComplete,
- onSendMessage,
-}: MessageListProps) {
- const { messagesEndRef, messagesContainerRef } = useMessageList({
- messageCount: messages.length,
- isStreaming,
- });
-
- return (
-
- {/* Top fade shadow */}
-
-
-
-
- {/* Render all persisted messages */}
- {(() => {
- const lastAssistantMessageIndex = findLastMessageIndex(
- messages,
- (msg) => msg.type === "message" && msg.role === "assistant",
- );
-
- const lastToolResponseIndex = findLastMessageIndex(
- messages,
- (msg) => msg.type === "tool_response",
- );
-
- return messages.map((message, index) => {
- // Skip agent_output tool_responses that should be rendered inside assistant messages
- if (shouldSkipAgentOutput(message, messages[index - 1])) {
- return null;
- }
-
- // Render last tool_response as AIChatBubble
- if (
- message.type === "tool_response" &&
- index === lastToolResponseIndex
- ) {
- return (
-
- );
- }
-
- return (
-
- );
- });
- })()}
-
- {/* Render thinking message when streaming but no chunks yet */}
- {isStreaming && streamingChunks.length === 0 &&
}
-
- {/* Render streaming message if active */}
- {isStreaming && streamingChunks.length > 0 && (
-
- )}
-
- {/* Invisible div to scroll to */}
-
-
-
-
- {/* Bottom fade shadow */}
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/LastToolResponse/LastToolResponse.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/LastToolResponse/LastToolResponse.tsx
deleted file mode 100644
index 7c5a75bec5..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/LastToolResponse/LastToolResponse.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { ChatMessageData } from "../../../ChatMessage/useChatMessage";
-import { ToolResponseMessage } from "../../../ToolResponseMessage/ToolResponseMessage";
-import { shouldSkipAgentOutput } from "../../helpers";
-
-export interface LastToolResponseProps {
- message: ChatMessageData;
- prevMessage: ChatMessageData | undefined;
- onSendMessage?: (content: string) => void;
-}
-
-export function LastToolResponse({
- message,
- prevMessage,
- onSendMessage,
-}: LastToolResponseProps) {
- if (message.type !== "tool_response") return null;
-
- if (shouldSkipAgentOutput(message, prevMessage)) return null;
-
- return (
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/MessageItem.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/MessageItem.tsx
deleted file mode 100644
index d455633b66..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/MessageItem.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { ChatMessage } from "../../../ChatMessage/ChatMessage";
-import type { ChatMessageData } from "../../../ChatMessage/useChatMessage";
-import { useMessageItem } from "./useMessageItem";
-
-export interface MessageItemProps {
- message: ChatMessageData;
- messages: ChatMessageData[];
- index: number;
- lastAssistantMessageIndex: number;
- isStreaming?: boolean;
- onSendMessage?: (content: string) => void;
-}
-
-export function MessageItem({
- message,
- messages,
- index,
- lastAssistantMessageIndex,
- isStreaming = false,
- onSendMessage,
-}: MessageItemProps) {
- const { messageToRender, agentOutput, isFinalMessage } = useMessageItem({
- message,
- messages,
- index,
- lastAssistantMessageIndex,
- });
-
- return (
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/useMessageItem.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/useMessageItem.ts
deleted file mode 100644
index 65c2e02cc8..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/useMessageItem.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import type { ChatMessageData } from "../../../ChatMessage/useChatMessage";
-import { isAgentOutputResult, isToolOutputPattern } from "../../helpers";
-
-export interface UseMessageItemArgs {
- message: ChatMessageData;
- messages: ChatMessageData[];
- index: number;
- lastAssistantMessageIndex: number;
-}
-
-export function useMessageItem({
- message,
- messages,
- index,
- lastAssistantMessageIndex,
-}: UseMessageItemArgs) {
- let agentOutput: ChatMessageData | undefined;
- let messageToRender: ChatMessageData = message;
-
- // Check if assistant message follows a tool_call and looks like a tool output
- if (message.type === "message" && message.role === "assistant") {
- const prevMessage = messages[index - 1];
-
- // Check if next message is an agent_output tool_response to include in current assistant message
- const nextMessage = messages[index + 1];
- if (
- nextMessage &&
- nextMessage.type === "tool_response" &&
- nextMessage.result
- ) {
- if (isAgentOutputResult(nextMessage.result)) {
- agentOutput = nextMessage;
- }
- }
-
- // Only convert to tool_response if it follows a tool_call AND looks like a tool output
- if (prevMessage && prevMessage.type === "tool_call") {
- if (isToolOutputPattern(message.content)) {
- // Convert this message to a tool_response format for rendering
- messageToRender = {
- type: "tool_response",
- toolId: prevMessage.toolId,
- toolName: prevMessage.toolName,
- result: message.content,
- success: true,
- timestamp: message.timestamp,
- } as ChatMessageData;
- }
- }
- }
-
- const isFinalMessage =
- messageToRender.type !== "message" ||
- messageToRender.role !== "assistant" ||
- index === lastAssistantMessageIndex;
-
- return {
- messageToRender,
- agentOutput,
- isFinalMessage,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/helpers.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/helpers.ts
deleted file mode 100644
index f6731c66c7..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/helpers.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import type { ChatMessageData } from "../ChatMessage/useChatMessage";
-
-export function parseToolResult(
- result: unknown,
-): Record | null {
- try {
- return typeof result === "string"
- ? JSON.parse(result)
- : (result as Record);
- } catch {
- return null;
- }
-}
-
-export function isAgentOutputResult(result: unknown): boolean {
- const parsed = parseToolResult(result);
- return parsed?.type === "agent_output";
-}
-
-export function isToolOutputPattern(content: string): boolean {
- const normalizedContent = content.toLowerCase().trim();
-
- return (
- normalizedContent.startsWith("no agents found") ||
- normalizedContent.startsWith("no results found") ||
- normalizedContent.includes("no agents found matching") ||
- !!normalizedContent.match(/^no \w+ found/i) ||
- (content.length < 150 && normalizedContent.includes("try different")) ||
- (content.length < 200 &&
- !normalizedContent.includes("i'll") &&
- !normalizedContent.includes("let me") &&
- !normalizedContent.includes("i can") &&
- !normalizedContent.includes("i will"))
- );
-}
-
-export function formatToolResultValue(result: unknown): string {
- return typeof result === "string"
- ? result
- : result
- ? JSON.stringify(result, null, 2)
- : "";
-}
-
-export function findLastMessageIndex(
- messages: ChatMessageData[],
- predicate: (msg: ChatMessageData) => boolean,
-): number {
- for (let i = messages.length - 1; i >= 0; i--) {
- if (predicate(messages[i])) return i;
- }
- return -1;
-}
-
-export function shouldSkipAgentOutput(
- message: ChatMessageData,
- prevMessage: ChatMessageData | undefined,
-): boolean {
- if (message.type !== "tool_response" || !message.result) return false;
-
- const isAgentOutput = isAgentOutputResult(message.result);
- return (
- isAgentOutput &&
- !!prevMessage &&
- prevMessage.type === "message" &&
- prevMessage.role === "assistant"
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/useMessageList.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/useMessageList.ts
deleted file mode 100644
index 3dcc75df3c..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/useMessageList.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { useEffect, useRef, useCallback } from "react";
-
-interface UseMessageListArgs {
- messageCount: number;
- isStreaming: boolean;
-}
-
-export function useMessageList({
- messageCount,
- isStreaming,
-}: UseMessageListArgs) {
- const messagesEndRef = useRef(null);
- const messagesContainerRef = useRef(null);
-
- const scrollToBottom = useCallback(() => {
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
- }, []);
-
- useEffect(() => {
- scrollToBottom();
- }, [messageCount, isStreaming, scrollToBottom]);
-
- return {
- messagesEndRef,
- messagesContainerRef,
- scrollToBottom,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/NoResultsMessage/NoResultsMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/NoResultsMessage/NoResultsMessage.tsx
deleted file mode 100644
index b6adc8b93c..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/NoResultsMessage/NoResultsMessage.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-import { MagnifyingGlass, X } from "@phosphor-icons/react";
-
-export interface NoResultsMessageProps {
- message: string;
- suggestions?: string[];
- className?: string;
-}
-
-export function NoResultsMessage({
- message,
- suggestions = [],
- className,
-}: NoResultsMessageProps) {
- return (
-
- {/* Icon */}
-
-
- {/* Content */}
-
-
- No Results Found
-
-
- {message}
-
-
-
- {/* Suggestions */}
- {suggestions.length > 0 && (
-
-
- Try these suggestions:
-
-
- {suggestions.map((suggestion, index) => (
-
- •
- {suggestion}
-
- ))}
-
-
- )}
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/PendingOperationWidget/PendingOperationWidget.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/PendingOperationWidget/PendingOperationWidget.tsx
deleted file mode 100644
index 6cfea7f327..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/PendingOperationWidget/PendingOperationWidget.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-"use client";
-
-import { Card } from "@/components/atoms/Card/Card";
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-import { CircleNotch, CheckCircle, XCircle } from "@phosphor-icons/react";
-
-type OperationStatus =
- | "pending"
- | "started"
- | "in_progress"
- | "completed"
- | "error";
-
-interface Props {
- status: OperationStatus;
- message: string;
- toolName?: string;
- className?: string;
-}
-
-function getOperationTitle(toolName?: string): string {
- if (!toolName) return "Operation";
- // Convert tool name to human-readable format
- // e.g., "create_agent" -> "Creating Agent", "edit_agent" -> "Editing Agent"
- if (toolName === "create_agent") return "Creating Agent";
- if (toolName === "edit_agent") return "Editing Agent";
- // Default: capitalize and format tool name
- return toolName
- .split("_")
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
- .join(" ");
-}
-
-export function PendingOperationWidget({
- status,
- message,
- toolName,
- className,
-}: Props) {
- const isPending =
- status === "pending" || status === "started" || status === "in_progress";
- const isCompleted = status === "completed";
- const isError = status === "error";
-
- const operationTitle = getOperationTitle(toolName);
-
- return (
-
-
-
-
- {isPending && (
-
- )}
- {isCompleted && (
-
- )}
- {isError && (
-
- )}
-
-
-
-
-
-
-
- {isPending && operationTitle}
- {isCompleted && `${operationTitle} Complete`}
- {isError && `${operationTitle} Failed`}
-
-
- {message}
-
-
-
- {isPending && (
-
- Check your library in a few minutes.
-
- )}
-
- {toolName && (
-
- Tool: {toolName}
-
- )}
-
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/QuickActionsWelcome/QuickActionsWelcome.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/QuickActionsWelcome/QuickActionsWelcome.tsx
deleted file mode 100644
index dd76fd9fb6..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/QuickActionsWelcome/QuickActionsWelcome.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-"use client";
-
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-
-export interface QuickActionsWelcomeProps {
- title: string;
- description: string;
- actions: string[];
- onActionClick: (action: string) => void;
- disabled?: boolean;
- className?: string;
-}
-
-export function QuickActionsWelcome({
- title,
- description,
- actions,
- onActionClick,
- disabled = false,
- className,
-}: QuickActionsWelcomeProps) {
- return (
-
-
-
-
- {title}
-
-
- {description}
-
-
-
- {actions.map((action) => {
- // Use slate theme for all cards
- const theme = {
- bg: "bg-slate-50/10",
- border: "border-slate-100",
- hoverBg: "hover:bg-slate-50/20",
- hoverBorder: "hover:border-slate-200",
- gradient: "from-slate-200/20 via-slate-300/10 to-transparent",
- text: "text-slate-900",
- hoverText: "group-hover:text-slate-900",
- };
-
- return (
-
onActionClick(action)}
- disabled={disabled}
- className={cn(
- "group relative overflow-hidden rounded-xl border p-5 text-left backdrop-blur-xl",
- "transition-all duration-200",
- theme.bg,
- theme.border,
- theme.hoverBg,
- theme.hoverBorder,
- "hover:shadow-sm",
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/50 focus-visible:ring-offset-2",
- "disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:shadow-none",
- )}
- >
- {/* Gradient flare background */}
-
-
-
- {action}
-
-
- );
- })}
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/SessionsDrawer/SessionsDrawer.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/SessionsDrawer/SessionsDrawer.tsx
deleted file mode 100644
index ee2896f1e2..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/SessionsDrawer/SessionsDrawer.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-"use client";
-
-import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat";
-import { Text } from "@/components/atoms/Text/Text";
-import { scrollbarStyles } from "@/components/styles/scrollbars";
-import { cn } from "@/lib/utils";
-import { X } from "@phosphor-icons/react";
-import { formatDistanceToNow } from "date-fns";
-import { Drawer } from "vaul";
-
-interface SessionsDrawerProps {
- isOpen: boolean;
- onClose: () => void;
- onSelectSession: (sessionId: string) => void;
- currentSessionId?: string | null;
-}
-
-export function SessionsDrawer({
- isOpen,
- onClose,
- onSelectSession,
- currentSessionId,
-}: SessionsDrawerProps) {
- const { data, isLoading } = useGetV2ListSessions(
- { limit: 100 },
- {
- query: {
- enabled: isOpen,
- },
- },
- );
-
- const sessions =
- data?.status === 200
- ? data.data.sessions.filter((session) => {
- // Filter out sessions without messages (sessions that were never updated)
- // If updated_at equals created_at, the session was created but never had messages
- return session.updated_at !== session.created_at;
- })
- : [];
-
- function handleSelectSession(sessionId: string) {
- onSelectSession(sessionId);
- onClose();
- }
-
- return (
- !open && onClose()}
- direction="right"
- >
-
-
-
-
-
-
- Chat Sessions
-
-
-
-
-
-
-
-
- {isLoading ? (
-
-
- Loading sessions...
-
-
- ) : sessions.length === 0 ? (
-
-
- You don't have previously started chats
-
-
- ) : (
-
- {sessions.map((session) => {
- const isActive = session.id === currentSessionId;
- const updatedAt = session.updated_at
- ? formatDistanceToNow(new Date(session.updated_at), {
- addSuffix: true,
- })
- : "";
-
- return (
-
handleSelectSession(session.id)}
- className={cn(
- "w-full rounded-lg border p-3 text-left transition-colors",
- isActive
- ? "border-indigo-500 bg-zinc-50"
- : "border-zinc-200 bg-zinc-100/50 hover:border-zinc-300 hover:bg-zinc-50",
- )}
- >
-
-
- {session.title || "Untitled Chat"}
-
-
- {session.id.slice(0, 8)}...
- {updatedAt && • }
- {updatedAt}
-
-
-
- );
- })}
-
- )}
-
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/StreamingMessage/StreamingMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/StreamingMessage/StreamingMessage.tsx
deleted file mode 100644
index 5b1f8b1617..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/StreamingMessage/StreamingMessage.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { cn } from "@/lib/utils";
-import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
-import { MarkdownContent } from "../MarkdownContent/MarkdownContent";
-import { useStreamingMessage } from "./useStreamingMessage";
-
-export interface StreamingMessageProps {
- chunks: string[];
- className?: string;
- onComplete?: () => void;
-}
-
-export function StreamingMessage({
- chunks,
- className,
- onComplete,
-}: StreamingMessageProps) {
- const { displayText } = useStreamingMessage({ chunks, onComplete });
-
- return (
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/StreamingMessage/useStreamingMessage.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/StreamingMessage/useStreamingMessage.ts
deleted file mode 100644
index 5203762151..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/StreamingMessage/useStreamingMessage.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { useEffect, useState } from "react";
-
-interface UseStreamingMessageArgs {
- chunks: string[];
- onComplete?: () => void;
-}
-
-export function useStreamingMessage({
- chunks,
- onComplete,
-}: UseStreamingMessageArgs) {
- const [isComplete, _setIsComplete] = useState(false);
- const displayText = chunks.join("");
-
- useEffect(() => {
- if (isComplete && onComplete) {
- onComplete();
- }
- }, [isComplete, onComplete]);
-
- return {
- displayText,
- isComplete,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ThinkingMessage/ThinkingMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ThinkingMessage/ThinkingMessage.tsx
deleted file mode 100644
index 2202705e65..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ThinkingMessage/ThinkingMessage.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import { Progress } from "@/components/atoms/Progress/Progress";
-import { cn } from "@/lib/utils";
-import { useEffect, useRef, useState } from "react";
-import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
-import { useAsymptoticProgress } from "../ToolCallMessage/useAsymptoticProgress";
-
-export interface ThinkingMessageProps {
- className?: string;
-}
-
-export function ThinkingMessage({ className }: ThinkingMessageProps) {
- const [showSlowLoader, setShowSlowLoader] = useState(false);
- const [showCoffeeMessage, setShowCoffeeMessage] = useState(false);
- const timerRef = useRef(null);
- const coffeeTimerRef = useRef(null);
- const progress = useAsymptoticProgress(showCoffeeMessage);
-
- useEffect(() => {
- if (timerRef.current === null) {
- timerRef.current = setTimeout(() => {
- setShowSlowLoader(true);
- }, 8000);
- }
-
- if (coffeeTimerRef.current === null) {
- coffeeTimerRef.current = setTimeout(() => {
- setShowCoffeeMessage(true);
- }, 10000);
- }
-
- return () => {
- if (timerRef.current) {
- clearTimeout(timerRef.current);
- timerRef.current = null;
- }
- if (coffeeTimerRef.current) {
- clearTimeout(coffeeTimerRef.current);
- coffeeTimerRef.current = null;
- }
- };
- }, []);
-
- return (
-
-
-
-
-
- {showCoffeeMessage ? (
-
-
-
- Working on it...
- {Math.round(progress)}%
-
-
-
-
- This could take a few minutes, grab a coffee ☕️
-
-
- ) : showSlowLoader ? (
-
- Taking a bit more time...
-
- ) : (
-
- Thinking...
-
- )}
-
-
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolCallMessage/ToolCallMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolCallMessage/ToolCallMessage.tsx
deleted file mode 100644
index 67aa975f15..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolCallMessage/ToolCallMessage.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-import type { ToolArguments } from "@/types/chat";
-import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
-import {
- formatToolArguments,
- getToolActionPhrase,
- getToolIcon,
-} from "./helpers";
-
-export interface ToolCallMessageProps {
- toolId?: string;
- toolName: string;
- arguments?: ToolArguments;
- isStreaming?: boolean;
- className?: string;
-}
-
-export function ToolCallMessage({
- toolName,
- arguments: toolArguments,
- isStreaming = false,
- className,
-}: ToolCallMessageProps) {
- const actionPhrase = getToolActionPhrase(toolName);
- const argumentsText = formatToolArguments(toolName, toolArguments);
- const displayText = `${actionPhrase}${argumentsText}`;
- const IconComponent = getToolIcon(toolName);
-
- return (
-
-
-
-
- {displayText}
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolCallMessage/helpers.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolCallMessage/helpers.ts
deleted file mode 100644
index 2f9f50b455..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolCallMessage/helpers.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-import type { ToolArguments } from "@/types/chat";
-import {
- BrainIcon,
- EyeIcon,
- FileMagnifyingGlassIcon,
- FileTextIcon,
- MagnifyingGlassIcon,
- PackageIcon,
- PencilLineIcon,
- PlayIcon,
- PlusIcon,
- SquaresFourIcon,
- type Icon,
-} from "@phosphor-icons/react";
-
-/**
- * Maps internal tool names to human-friendly action phrases (present continuous).
- * Used for tool call messages to indicate what action is currently happening.
- *
- * @param toolName - The internal tool name from the backend
- * @returns A human-friendly action phrase in present continuous tense
- */
-export function getToolActionPhrase(toolName: string): string {
- const normalizedName = toolName.trim();
- if (!normalizedName) return "Executing";
- if (normalizedName.toLowerCase().startsWith("executing")) {
- return normalizedName;
- }
- if (normalizedName.toLowerCase() === "unknown") return "Executing";
- const toolActionPhrases: Record = {
- add_understanding: "Updating your business information",
- create_agent: "Creating a new agent",
- edit_agent: "Editing the agent",
- find_agent: "Looking for agents in the marketplace",
- find_block: "Searching for blocks",
- find_library_agent: "Looking for library agents",
- run_agent: "Running the agent",
- run_block: "Running the block",
- view_agent_output: "Retrieving agent output",
- search_docs: "Searching documentation",
- get_doc_page: "Loading documentation page",
- agent_carousel: "Looking for agents in the marketplace",
- execution_started: "Running the agent",
- get_required_setup_info: "Getting setup requirements",
- schedule_agent: "Scheduling the agent to run",
- };
-
- // Return mapped phrase or generate human-friendly fallback
- return (
- toolActionPhrases[toolName] ||
- toolName
- .replace(/_/g, " ")
- .replace(/\b\w/g, (l) => l.toUpperCase())
- .replace(/^/, "Executing ")
- );
-}
-
-/**
- * Formats tool call arguments into user-friendly text.
- * Handles different tool types and formats their arguments nicely.
- *
- * @param toolName - The tool name
- * @param args - The tool arguments
- * @returns Formatted user-friendly text to append to action phrase
- */
-export function formatToolArguments(
- toolName: string,
- args: ToolArguments | undefined,
-): string {
- if (!args || Object.keys(args).length === 0) {
- return "";
- }
-
- switch (toolName) {
- case "find_agent":
- case "find_library_agent":
- case "agent_carousel":
- if (args.query) {
- return ` matching "${args.query as string}"`;
- }
- break;
-
- case "find_block":
- if (args.query) {
- return ` matching "${args.query as string}"`;
- }
- break;
-
- case "search_docs":
- if (args.query) {
- return ` for "${args.query as string}"`;
- }
- break;
-
- case "get_doc_page":
- if (args.path) {
- return ` "${args.path as string}"`;
- }
- break;
-
- case "run_agent":
- if (args.username_agent_slug) {
- return ` "${args.username_agent_slug as string}"`;
- }
- if (args.library_agent_id) {
- return ` (library agent)`;
- }
- break;
-
- case "run_block":
- if (args.block_id) {
- return ` "${args.block_id as string}"`;
- }
- break;
-
- case "view_agent_output":
- if (args.library_agent_id) {
- return ` (library agent)`;
- }
- if (args.username_agent_slug) {
- return ` "${args.username_agent_slug as string}"`;
- }
- break;
-
- case "create_agent":
- case "edit_agent":
- if (args.name) {
- return ` "${args.name as string}"`;
- }
- break;
-
- case "add_understanding":
- const understandingFields = Object.entries(args)
- .filter(
- ([_, value]) => value !== null && value !== undefined && value !== "",
- )
- .map(([key, value]) => {
- if (key === "user_name" && typeof value === "string") {
- return `for ${value}`;
- }
- if (typeof value === "string") {
- return `${key}: ${value}`;
- }
- if (Array.isArray(value) && value.length > 0) {
- return `${key}: ${value.slice(0, 2).join(", ")}${value.length > 2 ? ` (+${value.length - 2} more)` : ""}`;
- }
- return key;
- });
- if (understandingFields.length > 0) {
- return ` ${understandingFields[0]}`;
- }
- break;
- }
-
- return "";
-}
-
-/**
- * Maps tool names to their corresponding Phosphor icon components.
- *
- * @param toolName - The tool name from the backend
- * @returns The Icon component for the tool
- */
-export function getToolIcon(toolName: string): Icon {
- const iconMap: Record = {
- add_understanding: BrainIcon,
- create_agent: PlusIcon,
- edit_agent: PencilLineIcon,
- find_agent: SquaresFourIcon,
- find_library_agent: MagnifyingGlassIcon,
- find_block: PackageIcon,
- run_agent: PlayIcon,
- run_block: PlayIcon,
- view_agent_output: EyeIcon,
- search_docs: FileMagnifyingGlassIcon,
- get_doc_page: FileTextIcon,
- agent_carousel: MagnifyingGlassIcon,
- execution_started: PlayIcon,
- get_required_setup_info: SquaresFourIcon,
- schedule_agent: PlayIcon,
- };
-
- return iconMap[toolName] || SquaresFourIcon;
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/AgentCreatedPrompt.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/AgentCreatedPrompt.tsx
deleted file mode 100644
index 8494452eea..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/AgentCreatedPrompt.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-"use client";
-
-import { useGetV2GetLibraryAgent } from "@/app/api/__generated__/endpoints/library/library";
-import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
-import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
-import { RunAgentModal } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/RunAgentModal";
-import { Button } from "@/components/atoms/Button/Button";
-import { Text } from "@/components/atoms/Text/Text";
-import {
- CheckCircleIcon,
- PencilLineIcon,
- PlayIcon,
-} from "@phosphor-icons/react";
-import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
-
-interface Props {
- agentName: string;
- libraryAgentId: string;
- onSendMessage?: (content: string) => void;
-}
-
-export function AgentCreatedPrompt({
- agentName,
- libraryAgentId,
- onSendMessage,
-}: Props) {
- // Fetch library agent eagerly so modal is ready when user clicks
- const { data: libraryAgentResponse, isLoading } = useGetV2GetLibraryAgent(
- libraryAgentId,
- {
- query: {
- enabled: !!libraryAgentId,
- },
- },
- );
-
- const libraryAgent =
- libraryAgentResponse?.status === 200 ? libraryAgentResponse.data : null;
-
- function handleRunWithPlaceholders() {
- onSendMessage?.(
- `Run the agent "${agentName}" with placeholder/example values so I can test it.`,
- );
- }
-
- function handleRunCreated(execution: GraphExecutionMeta) {
- onSendMessage?.(
- `I've started the agent "${agentName}". The execution ID is ${execution.id}. Please monitor its progress and let me know when it completes.`,
- );
- }
-
- function handleScheduleCreated(schedule: GraphExecutionJobInfo) {
- const scheduleInfo = schedule.cron
- ? `with cron schedule "${schedule.cron}"`
- : "to run on the specified schedule";
- onSendMessage?.(
- `I've scheduled the agent "${agentName}" ${scheduleInfo}. The schedule ID is ${schedule.id}.`,
- );
- }
-
- return (
-
-
-
-
-
-
-
-
- Agent Created Successfully
-
-
- "{agentName}" is ready to test
-
-
-
-
-
-
- Ready to test?
-
-
-
-
- Run with example values
-
- {libraryAgent ? (
-
-
- Run with my inputs
-
- }
- agent={libraryAgent}
- onRunCreated={handleRunCreated}
- onScheduleCreated={handleScheduleCreated}
- />
- ) : (
-
-
- Run with my inputs
-
- )}
-
-
- or just ask me
-
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/ToolResponseMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/ToolResponseMessage.tsx
deleted file mode 100644
index 53d5f1ef96..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/ToolResponseMessage.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-import type { ToolResult } from "@/types/chat";
-import { WarningCircleIcon } from "@phosphor-icons/react";
-import { AgentCreatedPrompt } from "./AgentCreatedPrompt";
-import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
-import { MarkdownContent } from "../MarkdownContent/MarkdownContent";
-import {
- formatToolResponse,
- getErrorMessage,
- isAgentSavedResponse,
- isErrorResponse,
-} from "./helpers";
-
-export interface ToolResponseMessageProps {
- toolId?: string;
- toolName: string;
- result?: ToolResult;
- success?: boolean;
- className?: string;
- onSendMessage?: (content: string) => void;
-}
-
-export function ToolResponseMessage({
- toolId: _toolId,
- toolName,
- result,
- success: _success,
- className,
- onSendMessage,
-}: ToolResponseMessageProps) {
- if (isErrorResponse(result)) {
- const errorMessage = getErrorMessage(result);
- return (
-
-
-
-
- {errorMessage}
-
-
-
- );
- }
-
- // Check for agent_saved response - show special prompt
- const agentSavedData = isAgentSavedResponse(result);
- if (agentSavedData.isSaved) {
- return (
-
- );
- }
-
- const formattedText = formatToolResponse(result, toolName);
-
- return (
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/helpers.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/helpers.ts
deleted file mode 100644
index 63da171f54..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/helpers.ts
+++ /dev/null
@@ -1,461 +0,0 @@
-function stripInternalReasoning(content: string): string {
- return content
- .replace(/[\s\S]*?<\/internal_reasoning>/gi, "")
- .replace(/[\s\S]*?<\/thinking>/gi, "")
- .replace(/\n{3,}/g, "\n\n")
- .trim();
-}
-
-export interface AgentSavedData {
- isSaved: boolean;
- agentName: string;
- agentId: string;
- libraryAgentId: string;
- libraryAgentLink: string;
-}
-
-export function isAgentSavedResponse(result: unknown): AgentSavedData {
- if (typeof result !== "object" || result === null) {
- return {
- isSaved: false,
- agentName: "",
- agentId: "",
- libraryAgentId: "",
- libraryAgentLink: "",
- };
- }
- const response = result as Record;
- if (response.type === "agent_saved") {
- return {
- isSaved: true,
- agentName: (response.agent_name as string) || "Agent",
- agentId: (response.agent_id as string) || "",
- libraryAgentId: (response.library_agent_id as string) || "",
- libraryAgentLink: (response.library_agent_link as string) || "",
- };
- }
- return {
- isSaved: false,
- agentName: "",
- agentId: "",
- libraryAgentId: "",
- libraryAgentLink: "",
- };
-}
-
-export function isErrorResponse(result: unknown): boolean {
- if (typeof result === "string") {
- const lower = result.toLowerCase();
- return (
- lower.startsWith("error:") ||
- lower.includes("not found") ||
- lower.includes("does not exist") ||
- lower.includes("failed to") ||
- lower.includes("unable to")
- );
- }
- if (typeof result === "object" && result !== null) {
- const response = result as Record;
- return response.type === "error" || response.error !== undefined;
- }
- return false;
-}
-
-export function getErrorMessage(result: unknown): string {
- if (typeof result === "string") {
- return stripInternalReasoning(result.replace(/^error:\s*/i, ""));
- }
- if (typeof result === "object" && result !== null) {
- const response = result as Record;
- if (response.message)
- return stripInternalReasoning(String(response.message));
- if (response.error) return stripInternalReasoning(String(response.error));
- }
- return "An error occurred";
-}
-
-/**
- * Check if a value is a workspace file reference.
- * Format: workspace://{fileId} or workspace://{fileId}#{mimeType}
- */
-function isWorkspaceRef(value: unknown): value is string {
- return typeof value === "string" && value.startsWith("workspace://");
-}
-
-/**
- * Extract MIME type from a workspace reference fragment.
- * e.g., "workspace://abc123#video/mp4" → "video/mp4"
- * Returns undefined if no fragment is present.
- */
-function getWorkspaceMimeType(value: string): string | undefined {
- const hashIndex = value.indexOf("#");
- if (hashIndex === -1) return undefined;
- return value.slice(hashIndex + 1) || undefined;
-}
-
-/**
- * Determine the media category of a workspace ref or data URI.
- * Uses the MIME type fragment on workspace refs when available,
- * falls back to output key keyword matching for older refs without it.
- */
-function getMediaCategory(
- value: string,
- outputKey?: string,
-): "video" | "image" | "audio" | "unknown" {
- // Data URIs carry their own MIME type
- if (value.startsWith("data:video/")) return "video";
- if (value.startsWith("data:image/")) return "image";
- if (value.startsWith("data:audio/")) return "audio";
-
- // Workspace refs: prefer MIME type fragment
- if (isWorkspaceRef(value)) {
- const mime = getWorkspaceMimeType(value);
- if (mime) {
- if (mime.startsWith("video/")) return "video";
- if (mime.startsWith("image/")) return "image";
- if (mime.startsWith("audio/")) return "audio";
- return "unknown";
- }
-
- // Fallback: keyword matching on output key for older refs without fragment
- if (outputKey) {
- const lowerKey = outputKey.toLowerCase();
-
- const videoKeywords = [
- "video",
- "mp4",
- "mov",
- "avi",
- "webm",
- "movie",
- "clip",
- ];
- if (videoKeywords.some((kw) => lowerKey.includes(kw))) return "video";
-
- const imageKeywords = [
- "image",
- "img",
- "photo",
- "picture",
- "thumbnail",
- "avatar",
- "icon",
- "screenshot",
- ];
- if (imageKeywords.some((kw) => lowerKey.includes(kw))) return "image";
- }
-
- // Default to image for backward compatibility
- return "image";
- }
-
- return "unknown";
-}
-
-/**
- * Format a single output value, converting workspace refs to markdown images/videos.
- * Videos use a "video:" alt-text prefix so the MarkdownContent renderer can
- * distinguish them from images and render a element.
- */
-function formatOutputValue(value: unknown, outputKey?: string): string {
- if (typeof value === "string") {
- const category = getMediaCategory(value, outputKey);
-
- if (category === "video") {
- // Format with "video:" prefix so MarkdownContent renders
- return ``;
- }
-
- if (category === "image") {
- return ``;
- }
-
- // For audio, unknown workspace refs, data URIs, etc. - return as-is
- return value;
- }
-
- if (Array.isArray(value)) {
- return value
- .map((item, idx) => formatOutputValue(item, `${outputKey}_${idx}`))
- .join("\n\n");
- }
-
- if (typeof value === "object" && value !== null) {
- return JSON.stringify(value, null, 2);
- }
-
- return String(value);
-}
-
-function getToolCompletionPhrase(toolName: string): string {
- const toolCompletionPhrases: Record = {
- add_understanding: "Updated your business information",
- create_agent: "Created the agent",
- edit_agent: "Updated the agent",
- find_agent: "Found agents in the marketplace",
- find_block: "Found blocks",
- find_library_agent: "Found library agents",
- run_agent: "Started agent execution",
- run_block: "Executed the block",
- view_agent_output: "Retrieved agent output",
- search_docs: "Found documentation",
- get_doc_page: "Loaded documentation page",
- };
-
- // Return mapped phrase or generate human-friendly fallback
- return (
- toolCompletionPhrases[toolName] ||
- `Completed ${toolName.replace(/_/g, " ").replace("...", "")}`
- );
-}
-
-export function formatToolResponse(result: unknown, toolName: string): string {
- if (typeof result === "string") {
- const trimmed = result.trim();
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
- try {
- const parsed = JSON.parse(trimmed);
- return formatToolResponse(parsed, toolName);
- } catch {
- return stripInternalReasoning(trimmed);
- }
- }
- return stripInternalReasoning(result);
- }
-
- if (typeof result !== "object" || result === null) {
- return String(result);
- }
-
- const response = result as Record;
-
- // Handle different response types
- const responseType = response.type as string | undefined;
-
- if (!responseType) {
- if (response.message) {
- return String(response.message);
- }
- return getToolCompletionPhrase(toolName);
- }
-
- switch (responseType) {
- case "agents_found":
- const agents = (response.agents as Array<{ name: string }>) || [];
- const count =
- typeof response.count === "number" && !isNaN(response.count)
- ? response.count
- : agents.length;
- if (count === 0) {
- return "No agents found matching your search.";
- }
- return `Found ${count} agent${count === 1 ? "" : "s"}: ${agents
- .slice(0, 3)
- .map((a) => a.name)
- .join(", ")}${count > 3 ? ` and ${count - 3} more` : ""}`;
-
- case "agent_details":
- const agent = response.agent as { name: string; description?: string };
- if (agent) {
- return `Agent: ${agent.name}${agent.description ? `\n\n${agent.description}` : ""}`;
- }
- break;
-
- case "block_list":
- const blocks = (response.blocks as Array<{ name: string }>) || [];
- const blockCount =
- typeof response.count === "number" && !isNaN(response.count)
- ? response.count
- : blocks.length;
- if (blockCount === 0) {
- return "No blocks found matching your search.";
- }
- return `Found ${blockCount} block${blockCount === 1 ? "" : "s"}: ${blocks
- .slice(0, 3)
- .map((b) => b.name)
- .join(", ")}${blockCount > 3 ? ` and ${blockCount - 3} more` : ""}`;
-
- case "block_output":
- const blockName = (response.block_name as string) || "Block";
- const outputs = response.outputs as Record | undefined;
- if (outputs && Object.keys(outputs).length > 0) {
- const formattedOutputs: string[] = [];
-
- for (const [key, values] of Object.entries(outputs)) {
- if (!Array.isArray(values) || values.length === 0) continue;
-
- // Format each value in the output array
- for (const value of values) {
- const formatted = formatOutputValue(value, key);
- if (formatted) {
- formattedOutputs.push(formatted);
- }
- }
- }
-
- if (formattedOutputs.length > 0) {
- return `${blockName} executed successfully.\n\n${formattedOutputs.join("\n\n")}`;
- }
- return `${blockName} executed successfully.`;
- }
- return `${blockName} executed successfully.`;
-
- case "doc_search_results":
- const docResults = (response.results as Array<{ title: string }>) || [];
- const docCount =
- typeof response.count === "number" && !isNaN(response.count)
- ? response.count
- : docResults.length;
- if (docCount === 0) {
- return "No documentation found matching your search.";
- }
- return `Found ${docCount} documentation result${docCount === 1 ? "" : "s"}: ${docResults
- .slice(0, 3)
- .map((r) => r.title)
- .join(", ")}${docCount > 3 ? ` and ${docCount - 3} more` : ""}`;
-
- case "doc_page":
- const title = (response.title as string) || "Documentation";
- const content = (response.content as string) || "";
- if (content) {
- const preview = content.substring(0, 200).trim();
- return `${title}\n\n${preview}${content.length > 200 ? "..." : ""}`;
- }
- return title;
-
- case "understanding_updated":
- const currentUnderstanding = response.current_understanding as
- | Record
- | undefined;
- const fields = (response.updated_fields as string[]) || [];
-
- if (response.message && typeof response.message === "string") {
- let message = response.message;
- if (currentUnderstanding) {
- for (const [key, value] of Object.entries(currentUnderstanding)) {
- if (value !== null && value !== undefined && value !== "") {
- const placeholder = key;
- const actualValue = String(value);
- message = message.replace(
- new RegExp(`\\b${placeholder}\\b`, "g"),
- actualValue,
- );
- }
- }
- }
- return message;
- }
-
- if (
- currentUnderstanding &&
- Object.keys(currentUnderstanding).length > 0
- ) {
- const understandingEntries = Object.entries(currentUnderstanding)
- .filter(
- ([_, value]) =>
- value !== null && value !== undefined && value !== "",
- )
- .map(([key, value]) => {
- if (key === "user_name" && typeof value === "string") {
- return `Updated information for ${value}`;
- }
- return `${key}: ${String(value)}`;
- });
- if (understandingEntries.length > 0) {
- return understandingEntries[0];
- }
- }
- if (fields.length > 0) {
- return `Updated business information: ${fields.join(", ")}`;
- }
- return "Updated your business information.";
-
- case "agent_saved":
- const agentName = (response.agent_name as string) || "Agent";
- return `Successfully saved "${agentName}" to your library.`;
-
- case "agent_preview":
- const previewAgentName = (response.agent_name as string) || "Agent";
- const nodeCount = (response.node_count as number) || 0;
- const linkCount = (response.link_count as number) || 0;
- const description = (response.description as string) || "";
- let previewText = `Preview: "${previewAgentName}"`;
- if (description) {
- previewText += `\n\n${description}`;
- }
- previewText += `\n\n${nodeCount} node${nodeCount === 1 ? "" : "s"}, ${linkCount} link${linkCount === 1 ? "" : "s"}`;
- return previewText;
-
- case "clarification_needed":
- const questions =
- (response.questions as Array<{ question: string }>) || [];
- if (questions.length === 0) {
- return response.message
- ? String(response.message)
- : "I need more information to proceed.";
- }
- if (questions.length === 1) {
- return questions[0].question;
- }
- return `I need clarification on ${questions.length} points:\n\n${questions
- .map((q, i) => `${i + 1}. ${q.question}`)
- .join("\n")}`;
-
- case "agent_output":
- if (response.message) {
- return String(response.message);
- }
- const outputAgentName = (response.agent_name as string) || "Agent";
- const execution = response.execution as
- | {
- status?: string;
- outputs?: Record;
- }
- | undefined;
- if (execution) {
- const status = execution.status || "completed";
- const outputs = execution.outputs || {};
- const outputKeys = Object.keys(outputs);
- if (outputKeys.length > 0) {
- return `${outputAgentName} execution ${status}. Outputs: ${outputKeys.join(", ")}`;
- }
- return `${outputAgentName} execution ${status}.`;
- }
- return `${outputAgentName} output retrieved.`;
-
- case "execution_started":
- const execAgentName = (response.graph_name as string) || "Agent";
- if (response.message) {
- return String(response.message);
- }
- return `Started execution of "${execAgentName}".`;
-
- case "error":
- const errorMsg =
- (response.message as string) || response.error || "An error occurred";
- return stripInternalReasoning(String(errorMsg));
-
- case "no_results":
- const suggestions = (response.suggestions as string[]) || [];
- let noResultsText = (response.message as string) || "No results found.";
- if (suggestions.length > 0) {
- noResultsText += `\n\nSuggestions: ${suggestions.join(", ")}`;
- }
- return noResultsText;
-
- default:
- // Try to extract a message field
- if (response.message) {
- return String(response.message);
- }
- // Fallback: try to stringify nicely
- if (Object.keys(response).length === 0) {
- return getToolCompletionPhrase(toolName);
- }
- // If we have a response object but no recognized type, try to format it nicely
- // Don't return raw JSON - return a completion phrase instead
- return getToolCompletionPhrase(toolName);
- }
-
- return getToolCompletionPhrase(toolName);
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/UserChatBubble/UserChatBubble.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/UserChatBubble/UserChatBubble.tsx
deleted file mode 100644
index 39a6cb36ad..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/UserChatBubble/UserChatBubble.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { cn } from "@/lib/utils";
-import { ReactNode } from "react";
-
-export interface UserChatBubbleProps {
- children: ReactNode;
- className?: string;
-}
-
-export function UserChatBubble({ children, className }: UserChatBubbleProps) {
- return (
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/stream-executor.ts b/autogpt_platform/frontend/src/components/contextual/Chat/stream-executor.ts
deleted file mode 100644
index 8f4c8f9fec..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/stream-executor.ts
+++ /dev/null
@@ -1,255 +0,0 @@
-import { INITIAL_STREAM_ID } from "./chat-constants";
-import type {
- ActiveStream,
- StreamChunk,
- VercelStreamChunk,
-} from "./chat-types";
-import {
- INITIAL_RETRY_DELAY,
- MAX_RETRIES,
- normalizeStreamChunk,
- parseSSELine,
-} from "./stream-utils";
-
-function notifySubscribers(
- stream: ActiveStream,
- chunk: StreamChunk,
- skipStore = false,
-) {
- if (!skipStore) {
- stream.chunks.push(chunk);
- }
- for (const callback of stream.onChunkCallbacks) {
- try {
- callback(chunk);
- } catch (err) {
- console.warn("[StreamExecutor] Subscriber callback error:", err);
- }
- }
-}
-
-interface StreamExecutionOptions {
- stream: ActiveStream;
- mode: "new" | "reconnect";
- message?: string;
- isUserMessage?: boolean;
- context?: { url: string; content: string };
- taskId?: string;
- lastMessageId?: string;
- retryCount?: number;
-}
-
-async function executeStreamInternal(
- options: StreamExecutionOptions,
-): Promise {
- const {
- stream,
- mode,
- message,
- isUserMessage,
- context,
- taskId,
- lastMessageId = INITIAL_STREAM_ID,
- retryCount = 0,
- } = options;
-
- const { sessionId, abortController } = stream;
- const isReconnect = mode === "reconnect";
-
- if (isReconnect) {
- if (!taskId) {
- throw new Error("taskId is required for reconnect mode");
- }
- if (lastMessageId === null || lastMessageId === undefined) {
- throw new Error("lastMessageId is required for reconnect mode");
- }
- } else {
- if (!message) {
- throw new Error("message is required for new stream mode");
- }
- if (isUserMessage === undefined) {
- throw new Error("isUserMessage is required for new stream mode");
- }
- }
-
- try {
- let url: string;
- let fetchOptions: RequestInit;
-
- if (isReconnect) {
- url = `/api/chat/tasks/${taskId}/stream?last_message_id=${encodeURIComponent(lastMessageId)}`;
- fetchOptions = {
- method: "GET",
- headers: {
- Accept: "text/event-stream",
- },
- signal: abortController.signal,
- };
- } else {
- url = `/api/chat/sessions/${sessionId}/stream`;
- fetchOptions = {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Accept: "text/event-stream",
- },
- body: JSON.stringify({
- message,
- is_user_message: isUserMessage,
- context: context || null,
- }),
- signal: abortController.signal,
- };
- }
-
- const response = await fetch(url, fetchOptions);
-
- if (!response.ok) {
- const errorText = await response.text();
- let errorCode: string | undefined;
- let errorMessage = errorText || `HTTP ${response.status}`;
- try {
- const parsed = JSON.parse(errorText);
- if (parsed.detail) {
- const detail =
- typeof parsed.detail === "string"
- ? parsed.detail
- : parsed.detail.message || JSON.stringify(parsed.detail);
- errorMessage = detail;
- errorCode =
- typeof parsed.detail === "object" ? parsed.detail.code : undefined;
- }
- } catch {}
-
- const isPermanentError =
- isReconnect &&
- (response.status === 404 ||
- response.status === 403 ||
- response.status === 410);
-
- const error = new Error(errorMessage) as Error & {
- status?: number;
- isPermanent?: boolean;
- taskErrorCode?: string;
- };
- error.status = response.status;
- error.isPermanent = isPermanentError;
- error.taskErrorCode = errorCode;
- throw error;
- }
-
- if (!response.body) {
- throw new Error("Response body is null");
- }
-
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let buffer = "";
-
- while (true) {
- const { done, value } = await reader.read();
-
- if (done) {
- notifySubscribers(stream, { type: "stream_end" });
- stream.status = "completed";
- return;
- }
-
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split("\n");
- buffer = lines.pop() || "";
-
- for (const line of lines) {
- const data = parseSSELine(line);
- if (data !== null) {
- if (data === "[DONE]") {
- notifySubscribers(stream, { type: "stream_end" });
- stream.status = "completed";
- return;
- }
-
- try {
- const rawChunk = JSON.parse(data) as
- | StreamChunk
- | VercelStreamChunk;
- const chunk = normalizeStreamChunk(rawChunk);
- if (!chunk) continue;
-
- notifySubscribers(stream, chunk);
-
- if (chunk.type === "stream_end") {
- stream.status = "completed";
- return;
- }
-
- if (chunk.type === "error") {
- stream.status = "error";
- stream.error = new Error(
- chunk.message || chunk.content || "Stream error",
- );
- return;
- }
- } catch {}
- }
- }
- }
- } catch (err) {
- if (err instanceof Error && err.name === "AbortError") {
- notifySubscribers(stream, { type: "stream_end" });
- stream.status = "completed";
- return;
- }
-
- const isPermanentError =
- err instanceof Error &&
- (err as Error & { isPermanent?: boolean }).isPermanent;
-
- if (!isPermanentError && retryCount < MAX_RETRIES) {
- const retryDelay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount);
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
- return executeStreamInternal({
- ...options,
- retryCount: retryCount + 1,
- });
- }
-
- stream.status = "error";
- stream.error = err instanceof Error ? err : new Error("Stream failed");
- notifySubscribers(stream, {
- type: "error",
- message: stream.error.message,
- });
- }
-}
-
-export async function executeStream(
- stream: ActiveStream,
- message: string,
- isUserMessage: boolean,
- context?: { url: string; content: string },
- retryCount: number = 0,
-): Promise {
- return executeStreamInternal({
- stream,
- mode: "new",
- message,
- isUserMessage,
- context,
- retryCount,
- });
-}
-
-export async function executeTaskReconnect(
- stream: ActiveStream,
- taskId: string,
- lastMessageId: string = INITIAL_STREAM_ID,
- retryCount: number = 0,
-): Promise {
- return executeStreamInternal({
- stream,
- mode: "reconnect",
- taskId,
- lastMessageId,
- retryCount,
- });
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/stream-utils.ts b/autogpt_platform/frontend/src/components/contextual/Chat/stream-utils.ts
deleted file mode 100644
index 253e47b874..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/stream-utils.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import type { ToolArguments, ToolResult } from "@/types/chat";
-import type { StreamChunk, VercelStreamChunk } from "./chat-types";
-
-const LEGACY_STREAM_TYPES = new Set([
- "text_chunk",
- "text_ended",
- "tool_call",
- "tool_call_start",
- "tool_response",
- "login_needed",
- "need_login",
- "credentials_needed",
- "error",
- "usage",
- "stream_end",
-]);
-
-export function isLegacyStreamChunk(
- chunk: StreamChunk | VercelStreamChunk,
-): chunk is StreamChunk {
- return LEGACY_STREAM_TYPES.has(chunk.type as StreamChunk["type"]);
-}
-
-export function normalizeStreamChunk(
- chunk: StreamChunk | VercelStreamChunk,
-): StreamChunk | null {
- if (isLegacyStreamChunk(chunk)) return chunk;
-
- switch (chunk.type) {
- case "text-delta":
- // Vercel AI SDK sends "delta" for text content
- return { type: "text_chunk", content: chunk.delta };
- case "text-end":
- return { type: "text_ended" };
- case "tool-input-available":
- return {
- type: "tool_call_start",
- tool_id: chunk.toolCallId,
- tool_name: chunk.toolName,
- arguments: chunk.input as ToolArguments,
- };
- case "tool-output-available":
- return {
- type: "tool_response",
- tool_id: chunk.toolCallId,
- tool_name: chunk.toolName,
- result: chunk.output as ToolResult,
- success: chunk.success ?? true,
- };
- case "usage":
- return {
- type: "usage",
- promptTokens: chunk.promptTokens,
- completionTokens: chunk.completionTokens,
- totalTokens: chunk.totalTokens,
- };
- case "error":
- return {
- type: "error",
- message: chunk.errorText,
- code: chunk.code,
- details: chunk.details,
- };
- case "finish":
- return { type: "stream_end" };
- case "start":
- // Start event with optional taskId for reconnection
- return chunk.taskId
- ? { type: "stream_start", taskId: chunk.taskId }
- : null;
- case "text-start":
- return null;
- case "tool-input-start":
- return {
- type: "tool_call_start",
- tool_id: chunk.toolCallId,
- tool_name: chunk.toolName,
- arguments: {},
- };
- }
-}
-
-export const MAX_RETRIES = 3;
-export const INITIAL_RETRY_DELAY = 1000;
-
-export function parseSSELine(line: string): string | null {
- if (line.startsWith("data: ")) return line.slice(6);
- return null;
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/useChat.ts b/autogpt_platform/frontend/src/components/contextual/Chat/useChat.ts
deleted file mode 100644
index 124301abc4..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/useChat.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-"use client";
-
-import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
-import { useEffect, useRef, useState } from "react";
-import { useChatSession } from "./useChatSession";
-import { useChatStream } from "./useChatStream";
-
-interface UseChatArgs {
- urlSessionId?: string | null;
-}
-
-export function useChat({ urlSessionId }: UseChatArgs = {}) {
- const hasClaimedSessionRef = useRef(false);
- const { user } = useSupabase();
- const { sendMessage: sendStreamMessage } = useChatStream();
- const [showLoader, setShowLoader] = useState(false);
- const {
- session,
- sessionId: sessionIdFromHook,
- messages,
- isLoading,
- isCreating,
- error,
- isSessionNotFound,
- createSession,
- claimSession,
- clearSession: clearSessionBase,
- loadSession,
- startPollingForOperation,
- } = useChatSession({
- urlSessionId,
- autoCreate: false,
- });
-
- useEffect(
- function autoClaimSession() {
- if (
- session &&
- !session.user_id &&
- user &&
- !hasClaimedSessionRef.current &&
- !isLoading &&
- sessionIdFromHook
- ) {
- hasClaimedSessionRef.current = true;
- claimSession(sessionIdFromHook)
- .then(() => {
- sendStreamMessage(
- sessionIdFromHook,
- "User has successfully logged in.",
- () => {},
- false,
- ).catch(() => {});
- })
- .catch(() => {
- hasClaimedSessionRef.current = false;
- });
- }
- },
- [
- session,
- user,
- isLoading,
- sessionIdFromHook,
- claimSession,
- sendStreamMessage,
- ],
- );
-
- useEffect(
- function showLoaderWithDelay() {
- if (isLoading || isCreating) {
- const timer = setTimeout(() => setShowLoader(true), 300);
- return () => clearTimeout(timer);
- }
- setShowLoader(false);
- },
- [isLoading, isCreating],
- );
-
- function clearSession() {
- clearSessionBase();
- hasClaimedSessionRef.current = false;
- }
-
- return {
- session,
- messages,
- isLoading,
- isCreating,
- error,
- isSessionNotFound,
- createSession,
- clearSession,
- loadSession,
- sessionId: sessionIdFromHook,
- showLoader,
- startPollingForOperation,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/useChatSession.ts b/autogpt_platform/frontend/src/components/contextual/Chat/useChatSession.ts
deleted file mode 100644
index 936a49936c..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/useChatSession.ts
+++ /dev/null
@@ -1,385 +0,0 @@
-import {
- getGetV2GetSessionQueryKey,
- getGetV2GetSessionQueryOptions,
- getGetV2ListSessionsQueryKey,
- postV2CreateSession,
- useGetV2GetSession,
- usePatchV2SessionAssignUser,
- usePostV2CreateSession,
-} from "@/app/api/__generated__/endpoints/chat/chat";
-import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
-import { okData } from "@/app/api/helpers";
-import { isValidUUID } from "@/lib/utils";
-import { useQueryClient } from "@tanstack/react-query";
-import { useEffect, useMemo, useRef, useState } from "react";
-import { toast } from "sonner";
-
-interface UseChatSessionArgs {
- urlSessionId?: string | null;
- autoCreate?: boolean;
-}
-
-export function useChatSession({
- urlSessionId,
- autoCreate = false,
-}: UseChatSessionArgs = {}) {
- const queryClient = useQueryClient();
- const [sessionId, setSessionId] = useState(null);
- const [error, setError] = useState(null);
- const justCreatedSessionIdRef = useRef(null);
-
- useEffect(() => {
- if (urlSessionId) {
- if (!isValidUUID(urlSessionId)) {
- console.error("Invalid session ID format:", urlSessionId);
- toast.error("Invalid session ID", {
- description:
- "The session ID in the URL is not valid. Starting a new session...",
- });
- setSessionId(null);
- return;
- }
- setSessionId(urlSessionId);
- } else if (autoCreate) {
- setSessionId(null);
- } else {
- setSessionId(null);
- }
- }, [urlSessionId, autoCreate]);
-
- const { isPending: isCreating, error: createError } =
- usePostV2CreateSession();
-
- const {
- data: sessionData,
- isLoading: isLoadingSession,
- error: loadError,
- refetch,
- } = useGetV2GetSession(sessionId || "", {
- query: {
- enabled: !!sessionId,
- select: okData,
- staleTime: 0,
- retry: shouldRetrySessionLoad,
- retryDelay: getSessionRetryDelay,
- },
- });
-
- const { mutateAsync: claimSessionMutation } = usePatchV2SessionAssignUser();
-
- const session = useMemo(() => {
- if (sessionData) return sessionData;
-
- if (sessionId && justCreatedSessionIdRef.current === sessionId) {
- return {
- id: sessionId,
- user_id: null,
- messages: [],
- created_at: new Date().toISOString(),
- updated_at: new Date().toISOString(),
- } as SessionDetailResponse;
- }
- return null;
- }, [sessionData, sessionId]);
-
- const messages = session?.messages || [];
- const isLoading = isCreating || isLoadingSession;
-
- useEffect(() => {
- if (createError) {
- setError(
- createError instanceof Error
- ? createError
- : new Error("Failed to create session"),
- );
- } else if (loadError) {
- setError(
- loadError instanceof Error
- ? loadError
- : new Error("Failed to load session"),
- );
- } else {
- setError(null);
- }
- }, [createError, loadError]);
-
- // Track if we should be polling (set by external callers when they receive operation_started via SSE)
- const [forcePolling, setForcePolling] = useState(false);
- // Track if we've seen server acknowledge the pending operation (to avoid clearing forcePolling prematurely)
- const hasSeenServerPendingRef = useRef(false);
-
- // Check if there are any pending operations in the messages
- // Must check all operation types: operation_pending, operation_started, operation_in_progress
- const hasPendingOperationsFromServer = useMemo(() => {
- if (!messages || messages.length === 0) return false;
- const pendingTypes = new Set([
- "operation_pending",
- "operation_in_progress",
- "operation_started",
- ]);
- return messages.some((msg) => {
- if (msg.role !== "tool" || !msg.content) return false;
- try {
- const content =
- typeof msg.content === "string"
- ? JSON.parse(msg.content)
- : msg.content;
- return pendingTypes.has(content?.type);
- } catch {
- return false;
- }
- });
- }, [messages]);
-
- // Track when server has acknowledged the pending operation
- useEffect(() => {
- if (hasPendingOperationsFromServer) {
- hasSeenServerPendingRef.current = true;
- }
- }, [hasPendingOperationsFromServer]);
-
- // Combined: poll if server has pending ops OR if we received operation_started via SSE
- const hasPendingOperations = hasPendingOperationsFromServer || forcePolling;
-
- // Clear forcePolling only after server has acknowledged AND completed the operation
- useEffect(() => {
- if (
- forcePolling &&
- !hasPendingOperationsFromServer &&
- hasSeenServerPendingRef.current
- ) {
- // Server acknowledged the operation and it's now complete
- setForcePolling(false);
- hasSeenServerPendingRef.current = false;
- }
- }, [forcePolling, hasPendingOperationsFromServer]);
-
- // Function to trigger polling (called when operation_started is received via SSE)
- function startPollingForOperation() {
- setForcePolling(true);
- hasSeenServerPendingRef.current = false; // Reset for new operation
- }
-
- // Refresh sessions list when a pending operation completes
- // (hasPendingOperations transitions from true to false)
- const prevHasPendingOperationsRef = useRef(hasPendingOperations);
- useEffect(
- function refreshSessionsListOnOperationComplete() {
- const wasHasPending = prevHasPendingOperationsRef.current;
- prevHasPendingOperationsRef.current = hasPendingOperations;
-
- // Only invalidate when transitioning from pending to not pending
- if (wasHasPending && !hasPendingOperations && sessionId) {
- queryClient.invalidateQueries({
- queryKey: getGetV2ListSessionsQueryKey(),
- });
- }
- },
- [hasPendingOperations, sessionId, queryClient],
- );
-
- // Poll for updates when there are pending operations
- // Backoff: 2s, 4s, 6s, 8s, 10s, ... up to 30s max
- const pollAttemptRef = useRef(0);
- const hasPendingOperationsRef = useRef(hasPendingOperations);
- hasPendingOperationsRef.current = hasPendingOperations;
-
- useEffect(
- function pollForPendingOperations() {
- if (!sessionId || !hasPendingOperations) {
- pollAttemptRef.current = 0;
- return;
- }
-
- let cancelled = false;
- let timeoutId: ReturnType | null = null;
-
- function schedule() {
- // 2s, 4s, 6s, 8s, 10s, ... 30s (max)
- const delay = Math.min((pollAttemptRef.current + 1) * 2000, 30000);
- timeoutId = setTimeout(async () => {
- if (cancelled) return;
- pollAttemptRef.current += 1;
- try {
- await refetch();
- } catch (err) {
- console.error("[useChatSession] Poll failed:", err);
- } finally {
- if (!cancelled && hasPendingOperationsRef.current) {
- schedule();
- }
- }
- }, delay);
- }
-
- schedule();
-
- return () => {
- cancelled = true;
- if (timeoutId) clearTimeout(timeoutId);
- };
- },
- [sessionId, hasPendingOperations, refetch],
- );
-
- async function createSession() {
- try {
- setError(null);
- const response = await postV2CreateSession({
- body: JSON.stringify({}),
- });
- if (response.status !== 200) {
- throw new Error("Failed to create session");
- }
- const newSessionId = response.data.id;
- setSessionId(newSessionId);
- justCreatedSessionIdRef.current = newSessionId;
- setTimeout(() => {
- if (justCreatedSessionIdRef.current === newSessionId) {
- justCreatedSessionIdRef.current = null;
- }
- }, 10000);
- return newSessionId;
- } catch (err) {
- const error =
- err instanceof Error ? err : new Error("Failed to create session");
- setError(error);
- toast.error("Failed to create chat session", {
- description: error.message,
- });
- throw error;
- }
- }
-
- async function loadSession(id: string) {
- try {
- setError(null);
- // Invalidate the query cache for this session to force a fresh fetch
- await queryClient.invalidateQueries({
- queryKey: getGetV2GetSessionQueryKey(id),
- });
- // Set sessionId after invalidation to ensure the hook refetches
- setSessionId(id);
- // Force fetch with fresh data (bypass cache)
- const queryOptions = getGetV2GetSessionQueryOptions(id, {
- query: {
- staleTime: 0, // Force fresh fetch
- retry: shouldRetrySessionLoad,
- retryDelay: getSessionRetryDelay,
- },
- });
- const result = await queryClient.fetchQuery(queryOptions);
- if (!result || ("status" in result && result.status !== 200)) {
- console.warn("Session not found on server");
- setSessionId(null);
- throw new Error("Session not found");
- }
- } catch (err) {
- const error =
- err instanceof Error ? err : new Error("Failed to load session");
- setError(error);
- throw error;
- }
- }
-
- async function refreshSession() {
- if (!sessionId) return;
- try {
- setError(null);
- await refetch();
- } catch (err) {
- const error =
- err instanceof Error ? err : new Error("Failed to refresh session");
- setError(error);
- throw error;
- }
- }
-
- async function claimSession(id: string) {
- try {
- setError(null);
- await claimSessionMutation({ sessionId: id });
- if (justCreatedSessionIdRef.current === id) {
- justCreatedSessionIdRef.current = null;
- }
- await queryClient.invalidateQueries({
- queryKey: getGetV2GetSessionQueryKey(id),
- });
- await refetch();
- toast.success("Session claimed successfully", {
- description: "Your chat history has been saved to your account",
- });
- } catch (err: unknown) {
- const error =
- err instanceof Error ? err : new Error("Failed to claim session");
- const is404 =
- (typeof err === "object" &&
- err !== null &&
- "status" in err &&
- err.status === 404) ||
- (typeof err === "object" &&
- err !== null &&
- "response" in err &&
- typeof err.response === "object" &&
- err.response !== null &&
- "status" in err.response &&
- err.response.status === 404);
- if (!is404) {
- setError(error);
- toast.error("Failed to claim session", {
- description: error.message || "Unable to claim session",
- });
- }
- throw error;
- }
- }
-
- function clearSession() {
- setSessionId(null);
- setError(null);
- justCreatedSessionIdRef.current = null;
- }
-
- return {
- session,
- sessionId,
- messages,
- isLoading,
- isCreating,
- error,
- isSessionNotFound: isNotFoundError(loadError),
- hasPendingOperations,
- createSession,
- loadSession,
- refreshSession,
- claimSession,
- clearSession,
- startPollingForOperation,
- };
-}
-
-function isNotFoundError(error: unknown): boolean {
- if (!error || typeof error !== "object") return false;
- if ("status" in error && error.status === 404) return true;
- if (
- "response" in error &&
- typeof error.response === "object" &&
- error.response !== null &&
- "status" in error.response &&
- error.response.status === 404
- ) {
- return true;
- }
- return false;
-}
-
-function shouldRetrySessionLoad(failureCount: number, error: unknown): boolean {
- if (!isNotFoundError(error)) return false;
- return failureCount <= 2;
-}
-
-function getSessionRetryDelay(attemptIndex: number): number {
- if (attemptIndex === 0) return 3000;
- if (attemptIndex === 1) return 5000;
- return 0;
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/useChatStream.ts b/autogpt_platform/frontend/src/components/contextual/Chat/useChatStream.ts
deleted file mode 100644
index 5a9f637457..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/useChatStream.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-"use client";
-
-import { useEffect, useRef, useState } from "react";
-import { toast } from "sonner";
-import { useChatStore } from "./chat-store";
-import type { StreamChunk } from "./chat-types";
-
-export type { StreamChunk } from "./chat-types";
-
-export function useChatStream() {
- const [isStreaming, setIsStreaming] = useState(false);
- const [error, setError] = useState(null);
- const currentSessionIdRef = useRef(null);
- const onChunkCallbackRef = useRef<((chunk: StreamChunk) => void) | null>(
- null,
- );
-
- const stopStream = useChatStore((s) => s.stopStream);
- const unregisterActiveSession = useChatStore(
- (s) => s.unregisterActiveSession,
- );
- const isSessionActive = useChatStore((s) => s.isSessionActive);
- const onStreamComplete = useChatStore((s) => s.onStreamComplete);
- const getCompletedStream = useChatStore((s) => s.getCompletedStream);
- const registerActiveSession = useChatStore((s) => s.registerActiveSession);
- const startStream = useChatStore((s) => s.startStream);
- const getStreamStatus = useChatStore((s) => s.getStreamStatus);
-
- function stopStreaming(sessionId?: string) {
- const targetSession = sessionId || currentSessionIdRef.current;
- if (targetSession) {
- stopStream(targetSession);
- unregisterActiveSession(targetSession);
- }
- setIsStreaming(false);
- }
-
- useEffect(() => {
- return function cleanup() {
- const sessionId = currentSessionIdRef.current;
- if (sessionId && !isSessionActive(sessionId)) {
- stopStream(sessionId);
- }
- currentSessionIdRef.current = null;
- onChunkCallbackRef.current = null;
- };
- }, []);
-
- useEffect(() => {
- const unsubscribe = onStreamComplete(
- function handleStreamComplete(completedSessionId) {
- if (completedSessionId !== currentSessionIdRef.current) return;
-
- setIsStreaming(false);
- const completed = getCompletedStream(completedSessionId);
- if (completed?.error) {
- setError(completed.error);
- }
- unregisterActiveSession(completedSessionId);
- },
- );
-
- return unsubscribe;
- }, []);
-
- async function sendMessage(
- sessionId: string,
- message: string,
- onChunk: (chunk: StreamChunk) => void,
- isUserMessage: boolean = true,
- context?: { url: string; content: string },
- ) {
- const previousSessionId = currentSessionIdRef.current;
- if (previousSessionId && previousSessionId !== sessionId) {
- stopStreaming(previousSessionId);
- }
-
- currentSessionIdRef.current = sessionId;
- onChunkCallbackRef.current = onChunk;
- setIsStreaming(true);
- setError(null);
-
- registerActiveSession(sessionId);
-
- try {
- await startStream(sessionId, message, isUserMessage, context, onChunk);
-
- const status = getStreamStatus(sessionId);
- if (status === "error") {
- const completed = getCompletedStream(sessionId);
- if (completed?.error) {
- setError(completed.error);
- toast.error("Connection Failed", {
- description: "Unable to connect to chat service. Please try again.",
- });
- throw completed.error;
- }
- }
- } catch (err) {
- const streamError =
- err instanceof Error ? err : new Error("Failed to start stream");
- setError(streamError);
- throw streamError;
- } finally {
- setIsStreaming(false);
- }
- }
-
- return {
- isStreaming,
- error,
- sendMessage,
- stopStreaming,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/usePageContext.ts b/autogpt_platform/frontend/src/components/contextual/Chat/usePageContext.ts
deleted file mode 100644
index c567422a5c..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/usePageContext.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { useCallback } from "react";
-
-export interface PageContext {
- url: string;
- content: string;
-}
-
-const MAX_CONTENT_CHARS = 10000;
-
-/**
- * Hook to capture the current page context (URL + full page content)
- * Privacy-hardened: removes sensitive inputs and enforces content size limits
- */
-export function usePageContext() {
- const capturePageContext = useCallback((): PageContext => {
- if (typeof window === "undefined" || typeof document === "undefined") {
- return { url: "", content: "" };
- }
-
- const url = window.location.href;
-
- // Clone document to avoid modifying the original
- const clone = document.cloneNode(true) as Document;
-
- // Remove script, style, and noscript elements
- const scripts = clone.querySelectorAll("script, style, noscript");
- scripts.forEach((el) => el.remove());
-
- // Remove sensitive elements and their content
- const sensitiveSelectors = [
- "input",
- "textarea",
- "[contenteditable]",
- 'input[type="password"]',
- 'input[type="email"]',
- 'input[type="tel"]',
- 'input[type="search"]',
- 'input[type="hidden"]',
- "form",
- "[data-sensitive]",
- "[data-sensitive='true']",
- ];
-
- sensitiveSelectors.forEach((selector) => {
- const elements = clone.querySelectorAll(selector);
- elements.forEach((el) => {
- // For form elements, remove the entire element
- if (el.tagName === "FORM") {
- el.remove();
- } else {
- // For inputs and textareas, clear their values but keep the element structure
- if (
- el instanceof HTMLInputElement ||
- el instanceof HTMLTextAreaElement
- ) {
- el.value = "";
- el.textContent = "";
- } else {
- // For other sensitive elements, remove them entirely
- el.remove();
- }
- }
- });
- });
-
- // Strip any remaining input values that might have been missed
- const allInputs = clone.querySelectorAll("input, textarea");
- allInputs.forEach((el) => {
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
- el.value = "";
- el.textContent = "";
- }
- });
-
- // Get text content from body
- const body = clone.body;
- const content = body?.textContent || body?.innerText || "";
-
- // Clean up whitespace
- let cleanedContent = content
- .replace(/\s+/g, " ")
- .replace(/\n\s*\n/g, "\n")
- .trim();
-
- // Enforce maximum content size
- if (cleanedContent.length > MAX_CONTENT_CHARS) {
- cleanedContent =
- cleanedContent.substring(0, MAX_CONTENT_CHARS) + "... [truncated]";
- }
-
- return {
- url,
- content: cleanedContent,
- };
- }, []);
-
- return { capturePageContext };
-}
diff --git a/autogpt_platform/frontend/src/components/layout/Navbar/Navbar.tsx b/autogpt_platform/frontend/src/components/layout/Navbar/Navbar.tsx
index 9997c4a51c..ce5992b4be 100644
--- a/autogpt_platform/frontend/src/components/layout/Navbar/Navbar.tsx
+++ b/autogpt_platform/frontend/src/components/layout/Navbar/Navbar.tsx
@@ -62,7 +62,7 @@ export function Navbar() {
) : null}
{/* Left section */}
diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/FormRenderer.tsx b/autogpt_platform/frontend/src/components/renderers/InputRenderer/FormRenderer.tsx
index fc388cc343..c3a20d8cd2 100644
--- a/autogpt_platform/frontend/src/components/renderers/InputRenderer/FormRenderer.tsx
+++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/FormRenderer.tsx
@@ -30,8 +30,6 @@ export const FormRenderer = ({
return generateUiSchemaForCustomFields(preprocessedSchema, uiSchema);
}, [preprocessedSchema, uiSchema]);
- console.log("preprocessedSchema", preprocessedSchema);
-
return (