diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index df092a630..5831a0069 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -755,7 +755,21 @@ export default function NewChatPage() { } } catch (error) { if (error instanceof Error && error.name === "AbortError") { - // Request was cancelled + // Request was cancelled by user - persist partial response if any content was received + const hasContent = contentParts.some( + (part) => + (part.type === "text" && part.text.length > 0) || + (part.type === "tool-call" && TOOLS_WITH_UI.has(part.toolName)) + ); + if (hasContent && currentThreadId) { + const partialContent = buildContentForPersistence(); + appendMessage(currentThreadId, { + role: "assistant", + content: partialContent, + }).catch((err) => + console.error("Failed to persist partial assistant message:", err) + ); + } return; } console.error("[NewChatPage] Chat error:", error); diff --git a/surfsense_web/components/tool-ui/plan/plan.tsx b/surfsense_web/components/tool-ui/plan/plan.tsx index a520ea416..472cc41b8 100644 --- a/surfsense_web/components/tool-ui/plan/plan.tsx +++ b/surfsense_web/components/tool-ui/plan/plan.tsx @@ -30,19 +30,27 @@ import type { PlanTodo, TodoStatus } from "./schema"; interface StatusIconProps { status: TodoStatus; className?: string; + /** When false, in_progress items show as static (no spinner) */ + isStreaming?: boolean; } -const StatusIcon: FC = ({ status, className }) => { +const StatusIcon: FC = ({ status, className, isStreaming = true }) => { const baseClass = cn("size-4 shrink-0", className); switch (status) { case "completed": return ; case "in_progress": + // Only animate the spinner if we're actively streaming + // When streaming is stopped, show as a static dashed circle return ( ); case "cancelled": @@ -59,18 +67,21 @@ const StatusIcon: FC = ({ status, className }) => { interface TodoItemProps { todo: PlanTodo; + /** When false, in_progress items show as static (no spinner/pulse) */ + isStreaming?: boolean; } -const TodoItem: FC = ({ todo }) => { +const TodoItem: FC = ({ todo, isStreaming = true }) => { const isStrikethrough = todo.status === "completed" || todo.status === "cancelled"; - const isShimmer = todo.status === "in_progress"; + // Only show pulse animation if streaming and in progress + const isShimmer = todo.status === "in_progress" && isStreaming; if (todo.description) { return (
- + = ({ todo }) => { return (
- + void; @@ -129,6 +142,7 @@ export const Plan: FC = ({ todos, maxVisibleTodos = 4, showProgress = true, + isStreaming = true, responseActions, className, onResponseAction, @@ -176,7 +190,7 @@ export const Plan: FC = ({ return ( {items.map((todo) => ( - + ))} ); @@ -185,7 +199,7 @@ export const Plan: FC = ({ return (
{items.map((todo) => ( - + ))}
); diff --git a/surfsense_web/components/tool-ui/write-todos.tsx b/surfsense_web/components/tool-ui/write-todos.tsx index a8a7eaf43..6d8e0446b 100644 --- a/surfsense_web/components/tool-ui/write-todos.tsx +++ b/surfsense_web/components/tool-ui/write-todos.tsx @@ -1,6 +1,6 @@ "use client"; -import { makeAssistantToolUI } from "@assistant-ui/react"; +import { makeAssistantToolUI, useAssistantState } from "@assistant-ui/react"; import { useAtomValue, useSetAtom } from "jotai"; import { Loader2 } from "lucide-react"; import { useEffect, useMemo } from "react"; @@ -91,6 +91,10 @@ export const WriteTodosToolUI = makeAssistantToolUI thread.isRunning); // Get the plan data (from result or args) const planData = result || transformArgsToResult(args); @@ -140,7 +144,7 @@ export const WriteTodosToolUI = makeAssistantToolUI - +
); @@ -159,17 +163,15 @@ export const WriteTodosToolUI = makeAssistantToolUI - +
); @@ -178,7 +180,8 @@ export const WriteTodosToolUI = makeAssistantToolUI; @@ -188,7 +191,7 @@ export const WriteTodosToolUI = makeAssistantToolUI - + );