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

View File

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

View File

@@ -3,6 +3,7 @@ import { useParams } from "react-router-dom";
import { getClient } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { artifactApiBaseUrl } from "@/util/env";
interface Recording { interface Recording {
url: string; url: string;
@@ -11,6 +12,16 @@ interface Recording {
modified_at: string; 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() { function BrowserSessionVideo() {
const { browserSessionId } = useParams(); const { browserSessionId } = useParams();
const credentialGetter = useCredentialGetter(); const credentialGetter = useCredentialGetter();
@@ -31,7 +42,9 @@ function BrowserSessionVideo() {
enabled: !!browserSessionId, 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) { if (isLoading) {
return ( return (
@@ -59,8 +72,9 @@ function BrowserSessionVideo() {
No recordings available No recordings available
</div> </div>
<div className="text-sm text-gray-400"> <div className="text-sm text-gray-400">
Video recordings will appear here when the browser session is active {isSessionRunning
and recording ? "Recordings will be available after the session completes"
: "No recordings were created for this session"}
</div> </div>
</div> </div>
</div> </div>
@@ -93,19 +107,19 @@ function BrowserSessionVideo() {
</h3> </h3>
</div> </div>
{recording.url ? ( {getRecordingUrl(recording.url) ? (
<div className="w-full"> <div className="w-full">
<video <video
controls controls
className="w-full max-w-4xl rounded-lg" className="w-full max-w-4xl rounded-lg"
src={recording.url} src={getRecordingUrl(recording.url)!}
preload="metadata" preload="metadata"
> >
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
<div className="mt-2 text-xs text-gray-500"> <div className="mt-2 text-xs text-gray-500">
<a <a
href={recording.url} href={getRecordingUrl(recording.url)!}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800" 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 { useMutation } from "@tanstack/react-query";
import { getClient } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { toast } from "@/components/ui/use-toast";
import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { BrowserSession } from "@/routes/workflows/types/browserSessionTypes"; import { BrowserSession } from "@/routes/workflows/types/browserSessionTypes";
import { ProxyLocation } from "@/api/types"; import { ProxyLocation } from "@/api/types";
@@ -35,6 +36,26 @@ function useCreateBrowserSessionMutation() {
}); });
navigate(`/browser-session/${response.data.browser_session_id}`); 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_id: string | null;
runnable_type: string | null; runnable_type: string | null;
started_at: string | null; started_at: string | null;
status: string;
timeout: number | null; timeout: number | null;
vnc_streaming_supported: boolean; vnc_streaming_supported: boolean;
} }