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 40dae5966..d17c56a37 100644
--- a/components/TasksPage/Run/RunDetails.tsx
+++ b/components/TasksPage/Run/RunDetails.tsx
@@ -2,48 +2,55 @@
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";
interface RunDetailsProps {
runId: string;
data: TaskRunStatus;
+ variant?: "full" | "chat-compact";
}
-export default function RunDetails({ runId, data }: RunDetailsProps) {
+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 (
+
+ );
+ }
+
+ return (
+
+
+ {config.icon}
+
+ {isOnRunPage ? (
+
{displayName}
+ ) : (
+
+ {displayName}
+
+ )}
+
{config.label}
+
+
+
+ {detailsContent}
);
}
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 1e522c846..107129926 100644
--- a/components/VercelChat/MessageParts.tsx
+++ b/components/VercelChat/MessageParts.tsx
@@ -17,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;
@@ -26,9 +27,18 @@ interface MessagePartsProps {
export function MessageParts({ message, mode, setMode }: MessagePartsProps) {
const { status, reload } = useVercelChatContext();
+ 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,
+ -1,
+ );
+
return (
- {message.parts?.map(
+ {orderedParts.map(
(part: UIMessagePart
, partIndex) => {
const { type } = part;
const key = `message-${message.id}-part-${partIndex}`;
@@ -39,10 +49,7 @@ export function MessageParts({ message, mode, setMode }: MessagePartsProps) {
key={key}
className="w-full"
content={part.text}
- isStreaming={
- status === "streaming" &&
- partIndex === message.parts.length - 1
- }
+ isStreaming={status === "streaming" && part === lastOriginalPart}
defaultOpen={true}
/>
);
@@ -56,7 +63,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/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 5e3ed4e9c..92cd332fe 100644
--- a/components/VercelChat/tools/sandbox/RunSandboxCommandResultWithPolling.tsx
+++ b/components/VercelChat/tools/sandbox/RunSandboxCommandResultWithPolling.tsx
@@ -2,14 +2,14 @@
import { useTaskRunStatus } from "@/hooks/useTaskRunStatus";
import RunDetails from "@/components/TasksPage/Run/RunDetails";
-import RunPageSkeleton from "@/components/TasksPage/Run/RunPageSkeleton";
+import CompactRunSkeleton from "./CompactRunSkeleton";
export default function RunSandboxCommandResultWithPolling({ runId }: { runId: string }) {
const { data, isLoading } = useTaskRunStatus(runId);
if (isLoading || !data) {
- return ;
+ return ;
}
- return ;
+ return ;
}