diff --git a/apps/standalone-chat/src/features/agent-chat/components/ChatInterface.tsx b/apps/standalone-chat/src/features/agent-chat/components/ChatInterface.tsx index 70380e9f..7f89d23a 100644 --- a/apps/standalone-chat/src/features/agent-chat/components/ChatInterface.tsx +++ b/apps/standalone-chat/src/features/agent-chat/components/ChatInterface.tsx @@ -19,7 +19,6 @@ import { Clock, Circle, FileIcon, - Mail, } from "lucide-react"; import { ChatMessage } from "./ChatMessage"; import type { TodoItem, ToolCall } from "../types"; @@ -32,11 +31,8 @@ import { v4 as uuidv4 } from "uuid"; import { useChatContext } from "../providers/ChatProvider"; import { useQueryState } from "nuqs"; import { cn } from "@/lib/utils"; -import { ThreadActionsView } from "./interrupted-actions"; import { useStickToBottom } from "use-stick-to-bottom"; import { FilesPopover } from "./TasksFilesSidebar"; -import useInterruptedActions from "./interrupted-actions/hooks/use-interrupted-actions"; -import { HumanResponseWithEdits } from "../types/inbox"; interface ChatInterfaceProps { assistant: Assistant | null; @@ -137,10 +133,11 @@ export const ChatInterface = React.memo( ); const { + stream, messages, todos, files, - email, + ui, setFiles, isLoading, isThreadLoading, @@ -152,10 +149,6 @@ export const ChatInterface = React.memo( stopStream, } = useChatContext(); - const actions = useInterruptedActions({ - interrupt, - }); - const submitDisabled = isLoading || !assistant; const handleSubmit = useCallback( @@ -165,13 +158,6 @@ export const ChatInterface = React.memo( } if (submitDisabled) return; - if (interrupt) { - actions.handleSubmit(e); - actions.resetState(); - setInput(""); - return; - } - const messageText = input.trim(); if (!messageText || isLoading) return; if (debugMode) { @@ -195,8 +181,6 @@ export const ChatInterface = React.memo( setInput, runSingleStep, submitDisabled, - actions, - interrupt, ], ); @@ -432,36 +416,6 @@ export const ChatInterface = React.memo( const handleInputChange = (e: React.ChangeEvent) => { setInput(e.target.value); - - if (interrupt) { - actions.setSelectedSubmitType("response"); - actions.setHasAddedResponse(true); - - actions.setHumanResponse((prev) => { - const newResponse: HumanResponseWithEdits = { - type: "response", - args: e.target.value, - }; - - if (prev.find((p) => p.type === newResponse.type)) { - return prev.map((p) => { - if (p.type === newResponse.type) { - if (p.acceptAllowed) { - return { - ...newResponse, - acceptAllowed: true, - editsMade: !!e.target.value, - }; - } - return newResponse; - } - return p; - }); - } else { - throw new Error("No human response found for string response"); - } - }); - } }; return ( @@ -471,32 +425,33 @@ export const ChatInterface = React.memo( ref={scrollRef} >
{isThreadLoading ? ( skeleton ) : ( <> - {processedMessages.map((data, index) => ( - - ))} - {interrupt && ( - - )} + {processedMessages.map((data, index) => { + const messageUi = ui?.filter( + (u: any) => u.metadata?.message_id === data.message.id + ); + return ( + + ); + })} {interrupt && debugMode && (
diff --git a/apps/standalone-chat/src/features/agent-chat/components/ChatMessage.tsx b/apps/standalone-chat/src/features/agent-chat/components/ChatMessage.tsx index bb6f8c35..c32acc2e 100644 --- a/apps/standalone-chat/src/features/agent-chat/components/ChatMessage.tsx +++ b/apps/standalone-chat/src/features/agent-chat/components/ChatMessage.tsx @@ -23,6 +23,8 @@ interface ChatMessageProps { isLastMessage?: boolean; isLoading?: boolean; interrupt?: Interrupt; + ui?: any[]; + stream?: any; } export const ChatMessage = React.memo( @@ -35,6 +37,8 @@ export const ChatMessage = React.memo( isLastMessage, isLoading, interrupt, + ui, + stream, }) => { const isUser = message.type === "human"; const isAIMessage = message.type === "ai"; @@ -88,8 +92,8 @@ export const ChatMessage = React.memo( >
{(hasContent || debugMode) && ( @@ -125,20 +129,21 @@ export const ChatMessage = React.memo(
)} {hasToolCalls && ( -
+
{toolCalls.map((toolCall: ToolCall, idx, arr) => { if (toolCall.name === "task") return null; - // Don't show tool call if it's the interrupted tool call. - if ( - idx === arr.length - 1 && - toolCall.name === interruptTitle - ) { - return null; - } + const uiComponent = ui?.find( + (u) => u.metadata?.tool_call_id === toolCall.id + ); + const isInterrupted = + idx === arr.length - 1 && toolCall.name === interruptTitle && isLastMessage; return ( ); })} diff --git a/apps/standalone-chat/src/features/agent-chat/components/FileViewDialog.tsx b/apps/standalone-chat/src/features/agent-chat/components/FileViewDialog.tsx index f73a25a2..e0a882f1 100644 --- a/apps/standalone-chat/src/features/agent-chat/components/FileViewDialog.tsx +++ b/apps/standalone-chat/src/features/agent-chat/components/FileViewDialog.tsx @@ -57,8 +57,8 @@ export const FileViewDialog = React.memo<{ editDisabled: boolean; }>(({ file, onSaveFile, onClose, editDisabled }) => { const [isEditingMode, setIsEditingMode] = useState(file === null); - const [fileName, setFileName] = useState(file?.path || ""); - const [fileContent, setFileContent] = useState(file?.content || ""); + const [fileName, setFileName] = useState(String(file?.path || "")); + const [fileContent, setFileContent] = useState(String(file?.content || "")); const fileUpdate = useSWRMutation( { kind: "files-update", fileName, fileContent }, @@ -73,13 +73,14 @@ export const FileViewDialog = React.memo<{ ); useEffect(() => { - setFileName(file?.path || ""); - setFileContent(file?.content || ""); + setFileName(String(file?.path || "")); + setFileContent(String(file?.content || "")); setIsEditingMode(file === null); }, [file]); const fileExtension = useMemo(() => { - return fileName.split(".").pop()?.toLowerCase() || ""; + const fileNameStr = String(fileName || ""); + return fileNameStr.split(".").pop()?.toLowerCase() || ""; }, [fileName]); const isMarkdown = useMemo(() => { @@ -118,8 +119,8 @@ export const FileViewDialog = React.memo<{ if (file === null) { onClose(); } else { - setFileName(file.path); - setFileContent(file.content); + setFileName(String(file.path)); + setFileContent(String(file.content)); setIsEditingMode(false); } }, [file, onClose]); diff --git a/apps/standalone-chat/src/features/agent-chat/components/TasksFilesSidebar.tsx b/apps/standalone-chat/src/features/agent-chat/components/TasksFilesSidebar.tsx index 35cbe536..96a983ae 100644 --- a/apps/standalone-chat/src/features/agent-chat/components/TasksFilesSidebar.tsx +++ b/apps/standalone-chat/src/features/agent-chat/components/TasksFilesSidebar.tsx @@ -49,24 +49,43 @@ export function FilesPopover({
) : (
- {Object.keys(files).map((file) => ( - - ))} + } else { + fileContent = String(rawContent || ""); + } + + return ( + + ); + })}
)} diff --git a/apps/standalone-chat/src/features/agent-chat/components/ToolCallBox.tsx b/apps/standalone-chat/src/features/agent-chat/components/ToolCallBox.tsx index 0bdce2fd..9eafebf1 100644 --- a/apps/standalone-chat/src/features/agent-chat/components/ToolCallBox.tsx +++ b/apps/standalone-chat/src/features/agent-chat/components/ToolCallBox.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useMemo, useCallback } from "react"; +import React, { useState, useMemo, useCallback, useEffect } from "react"; import { ChevronDown, ChevronUp, @@ -13,12 +13,16 @@ import { import { Button } from "@/components/ui/button"; import { ToolCall } from "../types"; import { cn } from "@/lib/utils"; +import { LoadExternalComponent } from "@langchain/langgraph-sdk/react-ui"; interface ToolCallBoxProps { toolCall: ToolCall; + uiComponent?: any; + stream?: any; + isInterrupted?: boolean; } -export const ToolCallBox = React.memo(({ toolCall }) => { +export const ToolCallBox = React.memo(({ toolCall, uiComponent, stream, isInterrupted }) => { const [isExpanded, setIsExpanded] = useState(false); const [expandedArgs, setExpandedArgs] = useState>({}); @@ -33,7 +37,7 @@ export const ToolCallBox = React.memo(({ toolCall }) => { parsedArgs = { raw: toolArgs }; } const toolResult = toolCall.result || null; - const toolStatus = toolCall.status || "completed"; + const toolStatus = isInterrupted ? "interrupted" : (toolCall.status || "completed"); return { name: toolName, @@ -41,7 +45,7 @@ export const ToolCallBox = React.memo(({ toolCall }) => { result: toolResult, status: toolStatus, }; - }, [toolCall]); + }, [toolCall, isInterrupted]); const statusIcon = useMemo(() => { switch (status) { @@ -91,10 +95,17 @@ export const ToolCallBox = React.memo(({ toolCall }) => { const hasContent = result || Object.keys(args).length > 0; + // Auto-expand when status is interrupted + useEffect(() => { + if (status === "interrupted" && hasContent) { + setIsExpanded(true); + } + }, [status, hasContent]); + return (
@@ -103,7 +114,7 @@ export const ToolCallBox = React.memo(({ toolCall }) => { size="sm" onClick={toggleExpanded} className={cn( - "flex w-full items-center justify-between gap-2 border-none px-2 py-2 text-left shadow-none outline-none disabled:cursor-default", + "flex w-full items-center justify-between gap-2 border-none px-2 py-2 text-left shadow-none outline-none disabled:cursor-default focus-visible:ring-0 focus-visible:ring-offset-0", )} disabled={!hasContent} > @@ -131,59 +142,73 @@ export const ToolCallBox = React.memo(({ toolCall }) => { {isExpanded && hasContent && (
- {Object.keys(args).length > 0 && ( + {uiComponent && stream ? (
-

- Arguments -

-
- {Object.entries(args).map(([key, value]) => ( -
- - {expandedArgs[key] && ( -
-
-                          {typeof value === "string"
-                            ? value
-                            : JSON.stringify(value, null, 2)}
-                        
+ +
+ ) : ( + <> + {Object.keys(args).length > 0 && ( +
+

+ Arguments +

+
+ {Object.entries(args).map(([key, value]) => ( +
+ + {expandedArgs[key] && ( +
+
+                              {typeof value === "string"
+                                ? value
+                                : JSON.stringify(value, null, 2)}
+                            
+
+ )}
- )} + ))}
- ))} -
-
- )} - {result && ( -
-

- Result -

-
-                {typeof result === "string"
-                  ? result
-                  : JSON.stringify(result, null, 2)}
-              
-
+
+ )} + {result && ( +
+

+ Result +

+
+                    {typeof result === "string"
+                      ? result
+                      : JSON.stringify(result, null, 2)}
+                  
+
+ )} + )}
)} diff --git a/apps/standalone-chat/src/features/agent-chat/components/interrupted-actions/index.tsx b/apps/standalone-chat/src/features/agent-chat/components/interrupted-actions/index.tsx index f3289772..1d8d6247 100644 --- a/apps/standalone-chat/src/features/agent-chat/components/interrupted-actions/index.tsx +++ b/apps/standalone-chat/src/features/agent-chat/components/interrupted-actions/index.tsx @@ -38,7 +38,6 @@ export function ThreadActionsView({
{threadId && }
- {/* Actions */}