import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQueryClient } from '@tanstack/react-query'; import { createBrowserSessionProfile, startBrowserSessionLogin, saveBrowserSession, cancelBrowserSession, type BrowserSessionProfile, } from '../../api'; import { usePictureInPicture } from '../../lib/usePictureInPicture.js'; import { PipButton } from '../browser/PipButton.js'; type Phase = 'form' | 'logging-in' | 'saving' | 'done' | 'error'; interface Props { existingProfile?: BrowserSessionProfile | null; onClose: () => void; /** When set, the profile is created under this space (`?spaceId=…`) and the * space-scoped react-query key is invalidated on save. Omit for personal. */ spaceId?: string; } export function AddBrowserSessionDialog({ existingProfile, onClose, spaceId }: Props) { const { t } = useTranslation('userfolder'); const qc = useQueryClient(); const [phase, setPhase] = useState('form'); const [label, setLabel] = useState(existingProfile?.label ?? ''); const [startUrl, setStartUrl] = useState(existingProfile?.startUrl ?? ''); const [loggedInSelector, setLoggedInSelector] = useState(existingProfile?.loggedInSelector ?? ''); const [loginUrl, setLoginUrl] = useState(existingProfile?.loginUrlPatterns?.[0] ?? ''); const [profileId, setProfileId] = useState(existingProfile?.id ?? null); const [sessionId, setSessionId] = useState(null); const [novncPath, setNovncPath] = useState(null); const [error, setError] = useState(null); const pip = usePictureInPicture(novncPath, label ? t('browserSessions.dialog.novncTitle', { label }) : t('browserSessions.dialog.novncTitleNoLabel')); async function startLogin() { setError(null); try { let pid = profileId; if (!pid) { const created = await createBrowserSessionProfile({ label, startUrl, matchPatterns: [], storageOrigins: [new URL(startUrl).origin], loggedInSelector: loggedInSelector || undefined, loginUrlPatterns: loginUrl ? [loginUrl] : [], }, spaceId); pid = created.id; setProfileId(pid); } const r = await startBrowserSessionLogin(pid); setSessionId(r.sessionId); setNovncPath(r.novncPath); setPhase('logging-in'); } catch (e) { setError((e as Error).message); setPhase('error'); } } async function saveNow() { if (!profileId || !sessionId) return; setPhase('saving'); try { await saveBrowserSession(profileId, sessionId); qc.invalidateQueries({ queryKey: ['browser-session-profiles'] }); if (spaceId) qc.invalidateQueries({ queryKey: ['space-browser-sessions', spaceId] }); setPhase('done'); setTimeout(onClose, 800); } catch (e) { setError((e as Error).message); setPhase('error'); } } async function cancel() { if (profileId && sessionId) await cancelBrowserSession(profileId, sessionId).catch(() => {}); onClose(); } // The remote noVNC content is rendered at the Xvfb native resolution // (1280x720, see src/engine/browser-session.ts). At the form-phase 640px // dialog width the iframe scales down to ~50%, which feels cramped while // the user is actually logging in. Expand the dialog to ~1320x860 once we // enter the login phase so the iframe can show 1:1. const inLogin = phase === 'logging-in'; const dialogSize = inLogin ? 'w-[1320px] h-[860px] max-w-[95vw] max-h-[95vh]' : 'w-[640px] max-w-[95vw] max-h-[90vh]'; return (

{existingProfile ? t('browserSessions.dialog.reLoginTitle', { label: existingProfile.label }) : t('browserSessions.dialog.addTitle')}

{phase === 'form' && (
setLabel(e.target.value)} disabled={!!existingProfile} placeholder="My Twitter" className="w-full h-8 px-2 text-xs border border-hairline rounded-md disabled:bg-slate-50 disabled:text-slate-500" />
setStartUrl(e.target.value)} placeholder="https://twitter.com/home" className="w-full h-8 px-2 text-xs border border-hairline rounded-md" />
setLoggedInSelector(e.target.value)} placeholder='[data-testid="primaryColumn"]' className="w-full h-8 px-2 text-xs border border-hairline rounded-md" />
setLoginUrl(e.target.value)} placeholder="https://twitter.com/i/flow/login**" className="w-full h-8 px-2 text-xs border border-hairline rounded-md" />
{error &&
{error}
}
)} {phase === 'logging-in' && novncPath && (
{pip.isOpen ? (
{t('browserSessions.dialog.pipShown')}
) : (