From bad20d018ee92330b37ce8e88ca47b797f4f89b1 Mon Sep 17 00:00:00 2001 From: Avi Gaba Date: Mon, 30 Mar 2026 13:54:37 +0530 Subject: [PATCH 1/4] Move sandbox run details to compact end-of-message card --- components/TasksPage/Run/RunDetails.tsx | 178 ++++++++++++++++-- components/VercelChat/MessageParts.tsx | 51 ++++- .../RunSandboxCommandResultWithPolling.tsx | 20 +- 3 files changed, 222 insertions(+), 27 deletions(-) diff --git a/components/TasksPage/Run/RunDetails.tsx b/components/TasksPage/Run/RunDetails.tsx index 40dae5966..c7e1c21cc 100644 --- a/components/TasksPage/Run/RunDetails.tsx +++ b/components/TasksPage/Run/RunDetails.tsx @@ -2,8 +2,15 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useEffect, useState } from "react"; +import { ChevronDown } from "lucide-react"; import type { TaskRunStatus } from "@/lib/tasks/getTaskRunStatus"; import { getTaskDisplayName } from "@/lib/tasks/getTaskDisplayName"; +import { formatTimestamp } from "@/lib/tasks/formatTimestamp"; +import { formatDuration } from "@/lib/tasks/formatDuration"; +import { useElapsedMs } from "@/hooks/useElapsedMs"; +import { cn } from "@/lib/utils"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { ERROR_STATUSES, STATUS_CONFIG, FALLBACK_CONFIG } from "./statusConfig"; import RunLogsList from "./RunLogsList"; import RunTimeline from "./RunTimeline"; @@ -14,36 +21,75 @@ import AccountIdDisplay from "@/components/ArtistSetting/AccountIdDisplay"; interface RunDetailsProps { runId: string; data: TaskRunStatus; + variant?: "full" | "chat-compact"; } -export default function RunDetails({ runId, data }: RunDetailsProps) { +const TERMINAL_STATUSES = new Set([ + "COMPLETED", + "FAILED", + "CRASHED", + "CANCELED", + "SYSTEM_FAILURE", + "INTERRUPTED", +]); + +const STATUS_BADGE_CLASSES: Record = { + COMPLETED: + "border-green-200 bg-green-50 text-green-700 dark:border-green-900/60 dark:bg-green-950/50 dark:text-green-300", + FAILED: + "border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300", + CRASHED: + "border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300", + SYSTEM_FAILURE: + "border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300", + INTERRUPTED: + "border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300", + CANCELED: + "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", + EXECUTING: + "border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900/60 dark:bg-yellow-950/50 dark:text-yellow-300", + REATTEMPTING: + "border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900/60 dark:bg-yellow-950/50 dark:text-yellow-300", + QUEUED: + "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", + DELAYED: + "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", + FROZEN: + "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", + PENDING_VERSION: + "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", +}; + +export default function RunDetails({ + runId, + data, + variant = "full", +}: RunDetailsProps) { const config = STATUS_CONFIG[data.status] ?? FALLBACK_CONFIG; const logs = data.metadata?.logs ?? []; const currentStep = data.metadata?.currentStep; const pathname = usePathname(); const isOnRunPage = pathname === `/tasks/${runId}`; const displayName = getTaskDisplayName(data.taskIdentifier); + const displayDuration = useElapsedMs(data.startedAt, data.durationMs); + const isTerminal = TERMINAL_STATUSES.has(data.status); + const [isOpen, setIsOpen] = useState(!isTerminal); + const badgeClassName = + STATUS_BADGE_CLASSES[data.status] ?? + "border-border bg-muted/60 text-muted-foreground"; + const summaryText = + ERROR_STATUSES.has(data.status) && data.error?.message + ? data.error.message + : currentStep; - return ( -
-
- {config.icon} -
- {isOnRunPage ? ( -

{displayName}

- ) : ( - - {displayName} - - )} -

{config.label}

-
-
+ useEffect(() => { + if (!isTerminal) { + setIsOpen(true); + } + }, [isTerminal]); + const detailsContent = ( + <>
+ + ); + + if (variant === "chat-compact") { + return ( + + + + + +
+
+

+ Run Details +

+ + Open full run + +
+ {detailsContent} +
+
+
+ ); + } + + return ( +
+
+ {config.icon} +
+ {isOnRunPage ? ( +

{displayName}

+ ) : ( + + {displayName} + + )} +

{config.label}

+
+
+ + {detailsContent}
); } diff --git a/components/VercelChat/MessageParts.tsx b/components/VercelChat/MessageParts.tsx index 1e522c846..6265bf104 100644 --- a/components/VercelChat/MessageParts.tsx +++ b/components/VercelChat/MessageParts.tsx @@ -5,6 +5,7 @@ import { UIMessagePart, UIDataTypes, UITools, + getToolOrDynamicToolName, } from "ai"; import { Dispatch, SetStateAction } from "react"; import { cn } from "@/lib/utils"; @@ -24,11 +25,54 @@ interface MessagePartsProps { setMode: Dispatch>; } +const DEFERRED_SANDBOX_TOOL_NAMES = new Set([ + "get_task_run_status", + "prompt_sandbox", +]); + +function isDeferredSandboxResultPart( + part: UIMessagePart, +) { + if (!isToolOrDynamicToolUIPart(part)) { + return false; + } + + return ( + (part as ToolUIPart).state === "output-available" && + DEFERRED_SANDBOX_TOOL_NAMES.has(getToolOrDynamicToolName(part)) + ); +} + +function getOrderedMessageParts( + parts: UIMessagePart[], +) { + const regularParts: UIMessagePart[] = []; + const deferredSandboxParts: UIMessagePart[] = []; + + for (const part of parts) { + if (isDeferredSandboxResultPart(part)) { + deferredSandboxParts.push(part); + continue; + } + + regularParts.push(part); + } + + return [...regularParts, ...deferredSandboxParts]; +} + export function MessageParts({ message, mode, setMode }: MessagePartsProps) { const { status, reload } = useVercelChatContext(); + const orderedParts = getOrderedMessageParts(message.parts ?? []); + const lastTextPartIndex = orderedParts.reduce( + (lastIndex, part, partIndex) => + part.type === "text" ? partIndex : lastIndex, + -1, + ); + return (
- {message.parts?.map( + {orderedParts.map( (part: UIMessagePart, partIndex) => { const { type } = part; const key = `message-${message.id}-part-${partIndex}`; @@ -40,8 +84,7 @@ export function MessageParts({ message, mode, setMode }: MessagePartsProps) { className="w-full" content={part.text} isStreaming={ - status === "streaming" && - partIndex === message.parts.length - 1 + status === "streaming" && partIndex === orderedParts.length - 1 } defaultOpen={true} /> @@ -56,7 +99,7 @@ export function MessageParts({ message, mode, setMode }: MessagePartsProps) { const isLastMessage = message.role === "assistant" && status !== "streaming" && - partIndex === message.parts.length - 1; + partIndex === lastTextPartIndex; if (mode === "view") { return ( diff --git a/components/VercelChat/tools/sandbox/RunSandboxCommandResultWithPolling.tsx b/components/VercelChat/tools/sandbox/RunSandboxCommandResultWithPolling.tsx index 5e3ed4e9c..9b3f1f141 100644 --- a/components/VercelChat/tools/sandbox/RunSandboxCommandResultWithPolling.tsx +++ b/components/VercelChat/tools/sandbox/RunSandboxCommandResultWithPolling.tsx @@ -2,14 +2,28 @@ import { useTaskRunStatus } from "@/hooks/useTaskRunStatus"; import RunDetails from "@/components/TasksPage/Run/RunDetails"; -import RunPageSkeleton from "@/components/TasksPage/Run/RunPageSkeleton"; + +function CompactRunSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+ ); +} export default function RunSandboxCommandResultWithPolling({ runId }: { runId: string }) { const { data, isLoading } = useTaskRunStatus(runId); if (isLoading || !data) { - return ; + return ; } - return ; + return ; } From e4cb0711869aebf8172fa72676b476ecd9876376 Mon Sep 17 00:00:00 2001 From: Avi Gaba Date: Mon, 30 Mar 2026 14:37:21 +0530 Subject: [PATCH 2/4] Preserve reasoning streaming state after part reordering --- components/VercelChat/MessageParts.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/VercelChat/MessageParts.tsx b/components/VercelChat/MessageParts.tsx index 6265bf104..03d3f9dea 100644 --- a/components/VercelChat/MessageParts.tsx +++ b/components/VercelChat/MessageParts.tsx @@ -63,7 +63,9 @@ function getOrderedMessageParts( export function MessageParts({ message, mode, setMode }: MessagePartsProps) { const { status, reload } = useVercelChatContext(); - const orderedParts = getOrderedMessageParts(message.parts ?? []); + const originalParts = message.parts ?? []; + const orderedParts = getOrderedMessageParts(originalParts); + const lastOriginalPart = originalParts[originalParts.length - 1]; const lastTextPartIndex = orderedParts.reduce( (lastIndex, part, partIndex) => part.type === "text" ? partIndex : lastIndex, @@ -83,9 +85,7 @@ export function MessageParts({ message, mode, setMode }: MessagePartsProps) { key={key} className="w-full" content={part.text} - isStreaming={ - status === "streaming" && partIndex === orderedParts.length - 1 - } + isStreaming={status === "streaming" && part === lastOriginalPart} defaultOpen={true} /> ); From d1e2d3a4c3993d5067991b252e2c558cd88756f5 Mon Sep 17 00:00:00 2001 From: Avi Gaba Date: Tue, 31 Mar 2026 02:05:46 +0530 Subject: [PATCH 3/4] Extract sandbox chat helpers and components --- .../TasksPage/Run/CompactRunDetails.tsx | 104 ++++++++++++++++ components/TasksPage/Run/RunDetails.tsx | 117 +++--------------- .../TasksPage/Run/runDetailsConstants.ts | 35 ++++++ components/VercelChat/MessageParts.tsx | 38 +----- .../VercelChat/getOrderedMessageParts.ts | 20 +++ .../VercelChat/isDeferredSandboxResultPart.ts | 26 ++++ .../tools/sandbox/CompactRunSkeleton.tsx | 14 +++ .../RunSandboxCommandResultWithPolling.tsx | 16 +-- 8 files changed, 215 insertions(+), 155 deletions(-) create mode 100644 components/TasksPage/Run/CompactRunDetails.tsx create mode 100644 components/TasksPage/Run/runDetailsConstants.ts create mode 100644 components/VercelChat/getOrderedMessageParts.ts create mode 100644 components/VercelChat/isDeferredSandboxResultPart.ts create mode 100644 components/VercelChat/tools/sandbox/CompactRunSkeleton.tsx diff --git a/components/TasksPage/Run/CompactRunDetails.tsx b/components/TasksPage/Run/CompactRunDetails.tsx new file mode 100644 index 000000000..8ddc4c044 --- /dev/null +++ b/components/TasksPage/Run/CompactRunDetails.tsx @@ -0,0 +1,104 @@ +"use client"; + +import Link from "next/link"; +import type { ReactNode } from "react"; +import { ChevronDown } from "lucide-react"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import type { TaskRunStatus } from "@/lib/tasks/getTaskRunStatus"; +import { formatTimestamp } from "@/lib/tasks/formatTimestamp"; +import { formatDuration } from "@/lib/tasks/formatDuration"; +import { cn } from "@/lib/utils"; + +interface CompactRunDetailsProps { + runId: string; + displayName: string; + displayDuration: number | null; + statusLabel: string; + statusIcon: ReactNode; + badgeClassName: string; + summaryText?: string; + isOpen: boolean; + onOpenChange: (open: boolean) => void; + detailsContent: ReactNode; + data: TaskRunStatus; +} + +export default function CompactRunDetails({ + runId, + displayName, + displayDuration, + statusLabel, + statusIcon, + badgeClassName, + summaryText, + isOpen, + onOpenChange, + detailsContent, + data, +}: CompactRunDetailsProps) { + return ( + + + + + +
+
+

+ Run Details +

+ + Open full run + +
+ {detailsContent} +
+
+
+ ); +} diff --git a/components/TasksPage/Run/RunDetails.tsx b/components/TasksPage/Run/RunDetails.tsx index c7e1c21cc..d17c56a37 100644 --- a/components/TasksPage/Run/RunDetails.tsx +++ b/components/TasksPage/Run/RunDetails.tsx @@ -3,20 +3,17 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; -import { ChevronDown } from "lucide-react"; import type { TaskRunStatus } from "@/lib/tasks/getTaskRunStatus"; import { getTaskDisplayName } from "@/lib/tasks/getTaskDisplayName"; -import { formatTimestamp } from "@/lib/tasks/formatTimestamp"; -import { formatDuration } from "@/lib/tasks/formatDuration"; import { useElapsedMs } from "@/hooks/useElapsedMs"; -import { cn } from "@/lib/utils"; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { ERROR_STATUSES, STATUS_CONFIG, FALLBACK_CONFIG } from "./statusConfig"; import RunLogsList from "./RunLogsList"; import RunTimeline from "./RunTimeline"; import RunOutput from "./RunOutput"; import RunErrorDetails from "./RunErrorDetails"; import AccountIdDisplay from "@/components/ArtistSetting/AccountIdDisplay"; +import { STATUS_BADGE_CLASSES, TERMINAL_STATUSES } from "./runDetailsConstants"; +import CompactRunDetails from "./CompactRunDetails"; interface RunDetailsProps { runId: string; @@ -24,42 +21,6 @@ interface RunDetailsProps { variant?: "full" | "chat-compact"; } -const TERMINAL_STATUSES = new Set([ - "COMPLETED", - "FAILED", - "CRASHED", - "CANCELED", - "SYSTEM_FAILURE", - "INTERRUPTED", -]); - -const STATUS_BADGE_CLASSES: Record = { - COMPLETED: - "border-green-200 bg-green-50 text-green-700 dark:border-green-900/60 dark:bg-green-950/50 dark:text-green-300", - FAILED: - "border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300", - CRASHED: - "border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300", - SYSTEM_FAILURE: - "border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300", - INTERRUPTED: - "border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300", - CANCELED: - "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", - EXECUTING: - "border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900/60 dark:bg-yellow-950/50 dark:text-yellow-300", - REATTEMPTING: - "border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900/60 dark:bg-yellow-950/50 dark:text-yellow-300", - QUEUED: - "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", - DELAYED: - "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", - FROZEN: - "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", - PENDING_VERSION: - "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", -}; - export default function RunDetails({ runId, data, @@ -129,69 +90,19 @@ export default function RunDetails({ if (variant === "chat-compact") { return ( - - - - - -
-
-

- Run Details -

- - Open full run - -
- {detailsContent} -
-
-
+ detailsContent={detailsContent} + data={data} + /> ); } diff --git a/components/TasksPage/Run/runDetailsConstants.ts b/components/TasksPage/Run/runDetailsConstants.ts new file mode 100644 index 000000000..35c60153d --- /dev/null +++ b/components/TasksPage/Run/runDetailsConstants.ts @@ -0,0 +1,35 @@ +export const TERMINAL_STATUSES = new Set([ + "COMPLETED", + "FAILED", + "CRASHED", + "CANCELED", + "SYSTEM_FAILURE", + "INTERRUPTED", +]); + +export const STATUS_BADGE_CLASSES: Record = { + COMPLETED: + "border-green-200 bg-green-50 text-green-700 dark:border-green-900/60 dark:bg-green-950/50 dark:text-green-300", + FAILED: + "border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300", + CRASHED: + "border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300", + SYSTEM_FAILURE: + "border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300", + INTERRUPTED: + "border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/50 dark:text-red-300", + CANCELED: + "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", + EXECUTING: + "border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900/60 dark:bg-yellow-950/50 dark:text-yellow-300", + REATTEMPTING: + "border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900/60 dark:bg-yellow-950/50 dark:text-yellow-300", + QUEUED: + "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", + DELAYED: + "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", + FROZEN: + "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", + PENDING_VERSION: + "border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300", +}; diff --git a/components/VercelChat/MessageParts.tsx b/components/VercelChat/MessageParts.tsx index 03d3f9dea..107129926 100644 --- a/components/VercelChat/MessageParts.tsx +++ b/components/VercelChat/MessageParts.tsx @@ -5,7 +5,6 @@ import { UIMessagePart, UIDataTypes, UITools, - getToolOrDynamicToolName, } from "ai"; import { Dispatch, SetStateAction } from "react"; import { cn } from "@/lib/utils"; @@ -18,6 +17,7 @@ import { Actions, Action } from "@/components/actions"; import { RefreshCcwIcon, Pencil } from "lucide-react"; import CopyAction from "./CopyAction"; import { useVercelChatContext } from "@/providers/VercelChatProvider"; +import { getOrderedMessageParts } from "./getOrderedMessageParts"; interface MessagePartsProps { message: UIMessage; @@ -25,42 +25,6 @@ interface MessagePartsProps { setMode: Dispatch>; } -const DEFERRED_SANDBOX_TOOL_NAMES = new Set([ - "get_task_run_status", - "prompt_sandbox", -]); - -function isDeferredSandboxResultPart( - part: UIMessagePart, -) { - if (!isToolOrDynamicToolUIPart(part)) { - return false; - } - - return ( - (part as ToolUIPart).state === "output-available" && - DEFERRED_SANDBOX_TOOL_NAMES.has(getToolOrDynamicToolName(part)) - ); -} - -function getOrderedMessageParts( - parts: UIMessagePart[], -) { - const regularParts: UIMessagePart[] = []; - const deferredSandboxParts: UIMessagePart[] = []; - - for (const part of parts) { - if (isDeferredSandboxResultPart(part)) { - deferredSandboxParts.push(part); - continue; - } - - regularParts.push(part); - } - - return [...regularParts, ...deferredSandboxParts]; -} - export function MessageParts({ message, mode, setMode }: MessagePartsProps) { const { status, reload } = useVercelChatContext(); const originalParts = message.parts ?? []; diff --git a/components/VercelChat/getOrderedMessageParts.ts b/components/VercelChat/getOrderedMessageParts.ts new file mode 100644 index 000000000..8f9ce239b --- /dev/null +++ b/components/VercelChat/getOrderedMessageParts.ts @@ -0,0 +1,20 @@ +import { UIDataTypes, UIMessagePart, UITools } from "ai"; +import { isDeferredSandboxResultPart } from "./isDeferredSandboxResultPart"; + +export function getOrderedMessageParts( + parts: UIMessagePart[], +) { + const regularParts: UIMessagePart[] = []; + const deferredSandboxParts: UIMessagePart[] = []; + + for (const part of parts) { + if (isDeferredSandboxResultPart(part)) { + deferredSandboxParts.push(part); + continue; + } + + regularParts.push(part); + } + + return [...regularParts, ...deferredSandboxParts]; +} diff --git a/components/VercelChat/isDeferredSandboxResultPart.ts b/components/VercelChat/isDeferredSandboxResultPart.ts new file mode 100644 index 000000000..53db49e7e --- /dev/null +++ b/components/VercelChat/isDeferredSandboxResultPart.ts @@ -0,0 +1,26 @@ +import { + ToolUIPart, + UIDataTypes, + UIMessagePart, + UITools, + getToolOrDynamicToolName, + isToolOrDynamicToolUIPart, +} from "ai"; + +const DEFERRED_SANDBOX_TOOL_NAMES = new Set([ + "get_task_run_status", + "prompt_sandbox", +]); + +export function isDeferredSandboxResultPart( + part: UIMessagePart, +) { + if (!isToolOrDynamicToolUIPart(part)) { + return false; + } + + return ( + (part as ToolUIPart).state === "output-available" && + DEFERRED_SANDBOX_TOOL_NAMES.has(getToolOrDynamicToolName(part)) + ); +} diff --git a/components/VercelChat/tools/sandbox/CompactRunSkeleton.tsx b/components/VercelChat/tools/sandbox/CompactRunSkeleton.tsx new file mode 100644 index 000000000..cf16ce359 --- /dev/null +++ b/components/VercelChat/tools/sandbox/CompactRunSkeleton.tsx @@ -0,0 +1,14 @@ +export default function CompactRunSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/components/VercelChat/tools/sandbox/RunSandboxCommandResultWithPolling.tsx b/components/VercelChat/tools/sandbox/RunSandboxCommandResultWithPolling.tsx index 9b3f1f141..92cd332fe 100644 --- a/components/VercelChat/tools/sandbox/RunSandboxCommandResultWithPolling.tsx +++ b/components/VercelChat/tools/sandbox/RunSandboxCommandResultWithPolling.tsx @@ -2,21 +2,7 @@ import { useTaskRunStatus } from "@/hooks/useTaskRunStatus"; import RunDetails from "@/components/TasksPage/Run/RunDetails"; - -function CompactRunSkeleton() { - return ( -
-
-
-
-
-
-
-
-
-
- ); -} +import CompactRunSkeleton from "./CompactRunSkeleton"; export default function RunSandboxCommandResultWithPolling({ runId }: { runId: string }) { const { data, isLoading } = useTaskRunStatus(runId); From 122528a20a2f40f135ddd2e2e128e2d4af654efb Mon Sep 17 00:00:00 2001 From: Avi Gaba Date: Tue, 31 Mar 2026 14:09:45 +0530 Subject: [PATCH 4/4] Refactor sandbox chat run card ownership --- .../TasksPage/Run/CompactRunDetails.tsx | 7 +- components/TasksPage/Run/RunDetails.tsx | 98 +---------- .../TasksPage/Run/RunDetailsContent.tsx | 61 +++++++ components/VercelChat/MessageParts.tsx | 159 +++++++++--------- .../VercelChat/getOrderedMessageParts.ts | 20 --- .../tools/sandbox/ChatSandboxRunDetails.tsx | 48 ++++++ .../tools/sandbox/CompactRunSkeleton.tsx | 5 +- .../RunSandboxCommandResultWithPolling.tsx | 4 +- hooks/useCompactRunDisclosure.ts | 17 ++ lib/vercel/getOrderedMessageParts.ts | 27 +++ .../vercel}/isDeferredSandboxResultPart.ts | 0 11 files changed, 245 insertions(+), 201 deletions(-) create mode 100644 components/TasksPage/Run/RunDetailsContent.tsx delete mode 100644 components/VercelChat/getOrderedMessageParts.ts create mode 100644 components/VercelChat/tools/sandbox/ChatSandboxRunDetails.tsx create mode 100644 hooks/useCompactRunDisclosure.ts create mode 100644 lib/vercel/getOrderedMessageParts.ts rename {components/VercelChat => lib/vercel}/isDeferredSandboxResultPart.ts (100%) diff --git a/components/TasksPage/Run/CompactRunDetails.tsx b/components/TasksPage/Run/CompactRunDetails.tsx index 8ddc4c044..6361466c2 100644 --- a/components/TasksPage/Run/CompactRunDetails.tsx +++ b/components/TasksPage/Run/CompactRunDetails.tsx @@ -2,14 +2,14 @@ import Link from "next/link"; import type { ReactNode } from "react"; -import { ChevronDown } from "lucide-react"; +import { ChevronDownIcon } from "@radix-ui/react-icons"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import type { TaskRunStatus } from "@/lib/tasks/getTaskRunStatus"; import { formatTimestamp } from "@/lib/tasks/formatTimestamp"; import { formatDuration } from "@/lib/tasks/formatDuration"; import { cn } from "@/lib/utils"; -interface CompactRunDetailsProps { +export interface CompactRunDetailsProps { runId: string; displayName: string; displayDuration: number | null; @@ -74,7 +74,7 @@ export default function CompactRunDetails({ )}
- Open full run diff --git a/components/TasksPage/Run/RunDetails.tsx b/components/TasksPage/Run/RunDetails.tsx index d17c56a37..42a9af1f5 100644 --- a/components/TasksPage/Run/RunDetails.tsx +++ b/components/TasksPage/Run/RunDetails.tsx @@ -2,109 +2,21 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; -import { useEffect, useState } from "react"; import type { TaskRunStatus } from "@/lib/tasks/getTaskRunStatus"; import { getTaskDisplayName } from "@/lib/tasks/getTaskDisplayName"; -import { useElapsedMs } from "@/hooks/useElapsedMs"; -import { ERROR_STATUSES, STATUS_CONFIG, FALLBACK_CONFIG } from "./statusConfig"; -import RunLogsList from "./RunLogsList"; -import RunTimeline from "./RunTimeline"; -import RunOutput from "./RunOutput"; -import RunErrorDetails from "./RunErrorDetails"; -import AccountIdDisplay from "@/components/ArtistSetting/AccountIdDisplay"; -import { STATUS_BADGE_CLASSES, TERMINAL_STATUSES } from "./runDetailsConstants"; -import CompactRunDetails from "./CompactRunDetails"; +import { STATUS_CONFIG, FALLBACK_CONFIG } from "./statusConfig"; +import RunDetailsContent from "./RunDetailsContent"; -interface RunDetailsProps { +export interface RunDetailsProps { runId: string; data: TaskRunStatus; - variant?: "full" | "chat-compact"; } -export default function RunDetails({ - runId, - data, - variant = "full", -}: RunDetailsProps) { +export default function RunDetails({ runId, data }: RunDetailsProps) { const config = STATUS_CONFIG[data.status] ?? FALLBACK_CONFIG; - const logs = data.metadata?.logs ?? []; - const currentStep = data.metadata?.currentStep; const pathname = usePathname(); const isOnRunPage = pathname === `/tasks/${runId}`; const displayName = getTaskDisplayName(data.taskIdentifier); - const displayDuration = useElapsedMs(data.startedAt, data.durationMs); - const isTerminal = TERMINAL_STATUSES.has(data.status); - const [isOpen, setIsOpen] = useState(!isTerminal); - const badgeClassName = - STATUS_BADGE_CLASSES[data.status] ?? - "border-border bg-muted/60 text-muted-foreground"; - const summaryText = - ERROR_STATUSES.has(data.status) && data.error?.message - ? data.error.message - : currentStep; - - useEffect(() => { - if (!isTerminal) { - setIsOpen(true); - } - }, [isTerminal]); - - const detailsContent = ( - <> - - - {currentStep && ( -
-

- Current Step -

-

{currentStep}

-
- )} - - {data.status === "COMPLETED" && data.output !== undefined && ( - - )} - -
-

- Activity Log -

- -
- - {ERROR_STATUSES.has(data.status) && data.error && ( - - )} - -
- -
- - ); - - if (variant === "chat-compact") { - return ( - - ); - } return (
@@ -126,7 +38,7 @@ export default function RunDetails({
- {detailsContent} +
); } diff --git a/components/TasksPage/Run/RunDetailsContent.tsx b/components/TasksPage/Run/RunDetailsContent.tsx new file mode 100644 index 000000000..ca17ac6cc --- /dev/null +++ b/components/TasksPage/Run/RunDetailsContent.tsx @@ -0,0 +1,61 @@ +"use client"; + +import type { TaskRunStatus } from "@/lib/tasks/getTaskRunStatus"; +import { ERROR_STATUSES } from "./statusConfig"; +import RunLogsList from "./RunLogsList"; +import RunTimeline from "./RunTimeline"; +import RunOutput from "./RunOutput"; +import RunErrorDetails from "./RunErrorDetails"; +import AccountIdDisplay from "@/components/ArtistSetting/AccountIdDisplay"; + +export interface RunDetailsContentProps { + runId: string; + data: TaskRunStatus; +} + +export default function RunDetailsContent({ + runId, + data, +}: RunDetailsContentProps) { + const logs = data.metadata?.logs ?? []; + const currentStep = data.metadata?.currentStep; + + return ( + <> + + + {currentStep && ( +
+

+ Current Step +

+

{currentStep}

+
+ )} + + {data.status === "COMPLETED" && data.output !== undefined && ( + + )} + +
+

+ Activity Log +

+ +
+ + {ERROR_STATUSES.has(data.status) && data.error && ( + + )} + +
+ +
+ + ); +} diff --git a/components/VercelChat/MessageParts.tsx b/components/VercelChat/MessageParts.tsx index 107129926..5df635d0f 100644 --- a/components/VercelChat/MessageParts.tsx +++ b/components/VercelChat/MessageParts.tsx @@ -2,9 +2,6 @@ import { ToolUIPart, UIMessage, isToolOrDynamicToolUIPart, - UIMessagePart, - UIDataTypes, - UITools, } from "ai"; import { Dispatch, SetStateAction } from "react"; import { cn } from "@/lib/utils"; @@ -17,7 +14,7 @@ import { Actions, Action } from "@/components/actions"; import { RefreshCcwIcon, Pencil } from "lucide-react"; import CopyAction from "./CopyAction"; import { useVercelChatContext } from "@/providers/VercelChatProvider"; -import { getOrderedMessageParts } from "./getOrderedMessageParts"; +import { getOrderedMessageParts } from "@/lib/vercel/getOrderedMessageParts"; interface MessagePartsProps { message: UIMessage; @@ -30,98 +27,96 @@ export function MessageParts({ message, mode, setMode }: MessagePartsProps) { const originalParts = message.parts ?? []; const orderedParts = getOrderedMessageParts(originalParts); const lastOriginalPart = originalParts[originalParts.length - 1]; - const lastTextPartIndex = orderedParts.reduce( - (lastIndex, part, partIndex) => - part.type === "text" ? partIndex : lastIndex, + const lastTextOriginalIndex = orderedParts.reduce( + (lastIndex, orderedPart) => + orderedPart.part.type === "text" ? orderedPart.originalIndex : lastIndex, -1, ); return (
- {orderedParts.map( - (part: UIMessagePart, partIndex) => { - const { type } = part; - const key = `message-${message.id}-part-${partIndex}`; + {orderedParts.map(({ originalIndex, part }) => { + const { type } = part; + const key = `message-${message.id}-part-${originalIndex}`; - if (type === "reasoning") { - return ( - - ); - } + if (type === "reasoning") { + return ( + + ); + } - if (type === "file") { - return ; - } + if (type === "file") { + return ; + } - if (type === "text") { - const isLastMessage = - message.role === "assistant" && - status !== "streaming" && - partIndex === lastTextPartIndex; + if (type === "text") { + const isLastMessage = + message.role === "assistant" && + status !== "streaming" && + originalIndex === lastTextOriginalIndex; - if (mode === "view") { - return ( -
- - - {message.role === "user" && ( - setMode("edit")} - label="Edit" - tooltip="Edit message" - > - - - )} - {isLastMessage && ( - reload()} - label="Retry" - tooltip="Regenerate this response" - > - - - )} - - -
- ); - } + if (mode === "view") { + return ( +
+ + + {message.role === "user" && ( + setMode("edit")} + label="Edit" + tooltip="Edit message" + > + + + )} + {isLastMessage && ( + reload()} + label="Retry" + tooltip="Regenerate this response" + > + + + )} + + +
+ ); + } - if (mode === "edit") { - return ( - - ); - } + if (mode === "edit") { + return ( + + ); } + } - if (isToolOrDynamicToolUIPart(part)) { - const { state } = part as ToolUIPart; - if (state !== "output-available") { - return getToolCallComponent(part as ToolUIPart); - } else { - return getToolResultComponent(part as ToolUIPart); - } + if (isToolOrDynamicToolUIPart(part)) { + const { state } = part as ToolUIPart; + if (state !== "output-available") { + return getToolCallComponent(part as ToolUIPart); + } else { + return getToolResultComponent(part as ToolUIPart); } } - )} + })}
); } diff --git a/components/VercelChat/getOrderedMessageParts.ts b/components/VercelChat/getOrderedMessageParts.ts deleted file mode 100644 index 8f9ce239b..000000000 --- a/components/VercelChat/getOrderedMessageParts.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { UIDataTypes, UIMessagePart, UITools } from "ai"; -import { isDeferredSandboxResultPart } from "./isDeferredSandboxResultPart"; - -export function getOrderedMessageParts( - parts: UIMessagePart[], -) { - const regularParts: UIMessagePart[] = []; - const deferredSandboxParts: UIMessagePart[] = []; - - for (const part of parts) { - if (isDeferredSandboxResultPart(part)) { - deferredSandboxParts.push(part); - continue; - } - - regularParts.push(part); - } - - return [...regularParts, ...deferredSandboxParts]; -} diff --git a/components/VercelChat/tools/sandbox/ChatSandboxRunDetails.tsx b/components/VercelChat/tools/sandbox/ChatSandboxRunDetails.tsx new file mode 100644 index 000000000..87555d023 --- /dev/null +++ b/components/VercelChat/tools/sandbox/ChatSandboxRunDetails.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useElapsedMs } from "@/hooks/useElapsedMs"; +import { useCompactRunDisclosure } from "@/hooks/useCompactRunDisclosure"; +import type { TaskRunStatus } from "@/lib/tasks/getTaskRunStatus"; +import { getTaskDisplayName } from "@/lib/tasks/getTaskDisplayName"; +import { ERROR_STATUSES, FALLBACK_CONFIG, STATUS_CONFIG } from "@/components/TasksPage/Run/statusConfig"; +import { STATUS_BADGE_CLASSES } from "@/components/TasksPage/Run/runDetailsConstants"; +import CompactRunDetails from "@/components/TasksPage/Run/CompactRunDetails"; +import RunDetailsContent from "@/components/TasksPage/Run/RunDetailsContent"; + +export interface ChatSandboxRunDetailsProps { + runId: string; + data: TaskRunStatus; +} + +export default function ChatSandboxRunDetails({ + runId, + data, +}: ChatSandboxRunDetailsProps) { + const config = STATUS_CONFIG[data.status] ?? FALLBACK_CONFIG; + const displayName = getTaskDisplayName(data.taskIdentifier); + const displayDuration = useElapsedMs(data.startedAt, data.durationMs); + const { isOpen, setIsOpen } = useCompactRunDisclosure(data.status); + const badgeClassName = + STATUS_BADGE_CLASSES[data.status] ?? + "border-border bg-muted/60 text-muted-foreground"; + const summaryText = + ERROR_STATUSES.has(data.status) && data.error?.message + ? data.error.message + : data.metadata?.currentStep; + + return ( + } + data={data} + /> + ); +} diff --git a/components/VercelChat/tools/sandbox/CompactRunSkeleton.tsx b/components/VercelChat/tools/sandbox/CompactRunSkeleton.tsx index cf16ce359..c3d5ac847 100644 --- a/components/VercelChat/tools/sandbox/CompactRunSkeleton.tsx +++ b/components/VercelChat/tools/sandbox/CompactRunSkeleton.tsx @@ -1,6 +1,9 @@ export default function CompactRunSkeleton() { return ( -
+