Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions components/TasksPage/Run/RunLogsList.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
"use client";

import { useEffect, useRef } from "react";
import { useAutoScroll } from "@/hooks/useAutoScroll";

interface RunLogsListProps {
logs: string[];
}

export default function RunLogsList({ logs }: RunLogsListProps) {
const bottomRef = useRef<HTMLDivElement>(null);

useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [logs.length]);
const containerRef = useAutoScroll<HTMLDivElement>(logs.length);

if (logs.length === 0) {
return (
Expand All @@ -22,13 +18,12 @@ export default function RunLogsList({ logs }: RunLogsListProps) {
}

return (
<div className="max-h-80 overflow-y-auto rounded-md border bg-muted/30 p-3 font-mono text-sm">
<div ref={containerRef} className="max-h-80 overflow-y-auto rounded-md border bg-muted/30 p-3 font-mono text-sm">
{logs.map((log, i) => (
<p key={i} className="py-0.5 text-muted-foreground">
{log}
</p>
))}
<div ref={bottomRef} />
</div>
);
}
14 changes: 14 additions & 0 deletions components/VercelChat/ToolComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ import GetChatsResult, {
import RunPageSkeleton from "@/components/TasksPage/Run/RunPageSkeleton";
import SandboxCreatedResult from "./tools/sandbox/SandboxCreatedResult";
import RunSandboxCommandResultWithPolling from "./tools/sandbox/RunSandboxCommandResultWithPolling";
import PromptSandboxStreamProgress from "./tools/sandbox/PromptSandboxStreamProgress";
import type { SandboxStreamProgress } from "@/lib/sandboxes/sandboxStreamTypes";

type CallToolResult = {
content: TextContent[];
Expand Down Expand Up @@ -320,6 +322,12 @@ export function getToolCallComponent(part: ToolUIPart) {
<RunPageSkeleton />
</div>
);
} else if (toolName === "prompt_sandbox") {
return (
<div key={toolCallId}>
<PromptSandboxStreamProgress progress={{ status: "booting", output: "" }} />
</div>
);
}

// Default for other tools
Expand Down Expand Up @@ -615,6 +623,12 @@ export function getToolResultComponent(part: ToolUIPart | DynamicToolUIPart) {
<RunSandboxCommandResultWithPolling runId={runId} />
</div>
);
} else if (toolName === "prompt_sandbox") {
return (
<div key={toolCallId}>
<PromptSandboxStreamProgress progress={result as SandboxStreamProgress} />
</div>
);
}

// Default generic result for other tools
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from "react";
import { Loader, CheckCircle, XCircle } from "lucide-react";
import type { SandboxStreamProgress } from "@/lib/sandboxes/sandboxStreamTypes";
import { useAutoScroll } from "@/hooks/useAutoScroll";

interface PromptSandboxStreamProgressProps {
progress: SandboxStreamProgress;
}

const PromptSandboxStreamProgress: React.FC<
PromptSandboxStreamProgressProps
> = ({ progress }) => {
const preRef = useAutoScroll<HTMLPreElement>(progress.output);

if (progress.status === "booting") {
return (
<div className="flex items-center gap-2 text-sm text-muted-foreground py-2">
<Loader className="h-3 w-3 animate-spin" />
<span>Starting sandbox...</span>
</div>
);
}

if (progress.status === "streaming") {
return (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader className="h-3 w-3 animate-spin" />
<span>Running in sandbox...</span>
</div>
<div className="border border-border dark:border-zinc-800 rounded-lg bg-zinc-950 overflow-hidden">
<pre
ref={preRef}
className="p-3 text-xs text-zinc-300 font-mono whitespace-pre-wrap max-h-80 overflow-y-auto"
>
{progress.output || "Waiting for output..."}
</pre>
</div>
</div>
);
}

if (progress.status === "complete") {
const hasError = progress.exitCode !== undefined && progress.exitCode !== 0;

return (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
{hasError ? (
<XCircle className="h-3 w-3 text-red-500" />
) : (
<CheckCircle className="h-3 w-3 text-green-500" />
)}
<span>
{hasError
? `Sandbox exited with code ${progress.exitCode}`
: "Sandbox complete"}
</span>
</div>
<div className="border border-border dark:border-zinc-800 rounded-lg bg-zinc-950 overflow-hidden">
<pre className="p-3 text-xs text-zinc-300 font-mono whitespace-pre-wrap max-h-80 overflow-y-auto">
{progress.output || "(no output)"}
</pre>
</div>
</div>
);
}

if (progress.status === "error") {
return (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-red-500">
<XCircle className="h-3 w-3" />
<span>Sandbox error</span>
</div>
<div className="border border-red-800/30 rounded-lg bg-red-950/20 overflow-hidden">
<pre className="p-3 text-xs text-red-400 font-mono whitespace-pre-wrap max-h-40 overflow-y-auto">
{progress.stderr || progress.output || "Unknown error"}
</pre>
</div>
</div>
);
}

return null;
};

export default PromptSandboxStreamProgress;
17 changes: 17 additions & 0 deletions hooks/useAutoScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect, useRef } from "react";

/**
* Returns a ref to attach to a scrollable container.
* Automatically scrolls to the bottom whenever the dependency changes.
*/
export function useAutoScroll<T extends HTMLElement>(dep: unknown) {
const ref = useRef<T>(null);

useEffect(() => {
if (ref.current) {
ref.current.scrollTop = ref.current.scrollHeight;
}
}, [dep]);

return ref;
}
10 changes: 10 additions & 0 deletions lib/sandboxes/sandboxStreamTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type SandboxStreamStatus = "booting" | "streaming" | "complete" | "error";

export interface SandboxStreamProgress {
status: SandboxStreamStatus;
sandboxId?: string;
output: string;
stderr?: string;
exitCode?: number;
created?: boolean;
}