browser sessions v2 - frontend (#4514)

Co-authored-by: Benji Visser <benji@093b.org>
This commit is contained in:
Shuchang Zheng
2026-01-22 20:44:56 -08:00
committed by GitHub
parent c4c1e84507
commit 7a86a82107
5 changed files with 68 additions and 32 deletions

View File

@@ -31,12 +31,7 @@ import {
type MessageInExfiltratedEvent,
} from "@/store/useRecordingStore";
import { useSettingsStore } from "@/store/SettingsStore";
import {
environment,
wssBaseUrl,
newWssBaseUrl,
getRuntimeApiKey,
} from "@/util/env";
import { wssBaseUrl, newWssBaseUrl, getRuntimeApiKey } from "@/util/env";
import { copyText } from "@/util/copyText";
import { cn } from "@/util/utils";
@@ -223,7 +218,7 @@ function BrowserStream({
let credentialQueryParam = runtimeApiKey ? `apikey=${runtimeApiKey}` : "";
if (environment !== "local" && credentialGetter) {
if (credentialGetter) {
const token = await credentialGetter();
credentialQueryParam = token ? `token=Bearer ${token}` : "";
}

View File

@@ -30,9 +30,6 @@ type TabName = "stream" | "videos";
function BrowserSession() {
const { browserSessionId } = useParams();
const [hasBrowserSession, setHasBrowserSession] = useState(false);
const [browserSession, setBrowserSession] =
useState<BrowserSessionType | null>(null);
const [activeTab, setActiveTab] = useState<TabName>("stream");
const [isDialogOpen, setIsDialogOpen] = useState(false);
@@ -42,22 +39,17 @@ function BrowserSession() {
queryKey: ["browserSession", browserSessionId],
queryFn: async () => {
const client = await getClient(credentialGetter, "sans-api-v1");
try {
const response = await client.get<BrowserSessionType>(
`/browser_sessions/${browserSessionId}`,
);
setHasBrowserSession(true);
setBrowserSession(response.data);
return response.data;
} catch (error) {
setHasBrowserSession(false);
setBrowserSession(null);
return null;
}
const response = await client.get<BrowserSessionType>(
`/browser_sessions/${browserSessionId}`,
);
return response.data;
},
refetchInterval: (query) =>
query.state.data?.status === "running" ? 5000 : false,
});
const browserSession = query.data;
const closeBrowserSessionMutation = useCloseBrowserSessionMutation({
browserSessionId,
onSuccess: () => {
@@ -76,7 +68,7 @@ function BrowserSession() {
);
}
if (!hasBrowserSession) {
if (query.isError || !browserSession) {
return (
<div className="h-screen w-full gap-4 p-6">
<div className="flex h-full w-full items-center justify-center">
@@ -96,7 +88,20 @@ function BrowserSession() {
<div className="text-xl">Browser Session</div>
{browserSession && (
<div className="ml-auto flex flex-col items-end justify-end overflow-hidden">
<div className="flex items-center justify-end">
<div className="flex items-center justify-end gap-2">
<span
className={`rounded px-2 py-0.5 text-xs font-medium ${
browserSession.status === "running"
? "bg-green-500/20 text-green-500"
: browserSession.status === "completed"
? "bg-blue-500/20 text-blue-500"
: browserSession.status === "failed"
? "bg-red-500/20 text-red-500"
: "bg-gray-500/20 text-gray-500"
}`}
>
{browserSession.status}
</span>
<div className="max-w-[20rem] truncate font-mono text-xs opacity-75">
{browserSession.browser_session_id}
</div>
@@ -141,7 +146,7 @@ function BrowserSession() {
]}
/>
{browserSessionId && (
{browserSessionId && browserSession?.status === "running" && (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button className="ml-auto" variant="secondary">

View File

@@ -3,6 +3,7 @@ import { useParams } from "react-router-dom";
import { getClient } from "@/api/AxiosClient";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { artifactApiBaseUrl } from "@/util/env";
interface Recording {
url: string;
@@ -11,6 +12,16 @@ interface Recording {
modified_at: string;
}
function getRecordingUrl(url: string | null | undefined): string | null {
if (!url) {
return null;
}
if (url.startsWith("file://")) {
return `${artifactApiBaseUrl}/artifact/recording?path=${url.slice(7)}`;
}
return url;
}
function BrowserSessionVideo() {
const { browserSessionId } = useParams();
const credentialGetter = useCredentialGetter();
@@ -31,7 +42,9 @@ function BrowserSessionVideo() {
enabled: !!browserSessionId,
});
const recordings = browserSession?.recordings || [];
const isSessionRunning = browserSession?.status === "running";
// Don't show recordings while session is running - they're incomplete
const recordings = isSessionRunning ? [] : browserSession?.recordings || [];
if (isLoading) {
return (
@@ -59,8 +72,9 @@ function BrowserSessionVideo() {
No recordings available
</div>
<div className="text-sm text-gray-400">
Video recordings will appear here when the browser session is active
and recording
{isSessionRunning
? "Recordings will be available after the session completes"
: "No recordings were created for this session"}
</div>
</div>
</div>
@@ -93,19 +107,19 @@ function BrowserSessionVideo() {
</h3>
</div>
{recording.url ? (
{getRecordingUrl(recording.url) ? (
<div className="w-full">
<video
controls
className="w-full max-w-4xl rounded-lg"
src={recording.url}
src={getRecordingUrl(recording.url)!}
preload="metadata"
>
Your browser does not support the video tag.
</video>
<div className="mt-2 text-xs text-gray-500">
<a
href={recording.url}
href={getRecordingUrl(recording.url)!}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800"

View File

@@ -3,6 +3,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useMutation } from "@tanstack/react-query";
import { getClient } from "@/api/AxiosClient";
import { toast } from "@/components/ui/use-toast";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { BrowserSession } from "@/routes/workflows/types/browserSessionTypes";
import { ProxyLocation } from "@/api/types";
@@ -35,6 +36,26 @@ function useCreateBrowserSessionMutation() {
});
navigate(`/browser-session/${response.data.browser_session_id}`);
},
onError: (error: unknown) => {
let errorMessage =
"Browser session could not be started. Please try again.";
if (error && typeof error === "object") {
const axiosError = error as {
response?: { data?: { detail?: string } };
message?: string;
};
if (axiosError.response?.data?.detail) {
errorMessage = axiosError.response.data.detail;
} else if (axiosError.message) {
errorMessage = axiosError.message;
}
}
toast({
variant: "destructive",
title: "Failed to create browser session",
description: errorMessage,
});
},
});
}

View File

@@ -6,6 +6,7 @@ interface BrowserSession {
runnable_id: string | null;
runnable_type: string | null;
started_at: string | null;
status: string;
timeout: number | null;
vnc_streaming_supported: boolean;
}