import type { OpenGeniBrowserClient } from "@opengeni/sdk/browser"; import { isTransientRequestError } from "@opengeni/sdk"; import type { Session, SessionEvent, SessionVisibility, SessionVisibilityWork, } from "@opengeni/sdk"; import { useNavigate } from "../../lib/navigation"; import { ChevronDownIcon, CircleAlertIcon, CopyPlusIcon, Loader2Icon, LockKeyholeIcon, UsersIcon, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "../../lib/toast"; import { Button } from "../../../ui/button"; import { ConfirmDialog } from "../../../ui/confirm-dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../../ui/dropdown-menu"; import { Notice } from "../../../ui/notice"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../../ui/tooltip"; import { ProblemPanel } from "../common"; import { useAppContext } from "../../app-context"; import { isApiErrorStatus } from "../../lib/app-api"; import { isPersonalWorkspace } from "../../lib/managed-self-context"; import { workspaceSessionsPath } from "../../lib/routes"; import { SessionUnavailableRoute } from "../../routes/session-unavailable"; import { classifySessionTenancyFailure, isCurrentSessionTenancyTarget, isStoppingSessionWorkError, normalizeSessionVisibilityWork, privateSessionSandboxNote, retryableSessionTenancyReconciliationFailure, sessionVisibilityWorkIsEmpty, sessionVisibilityWorkStopsWork, sessionVisibilityWorkSummary, type SessionTenancyTarget, } from "../../lib/session-tenancy"; import { sessionTenancyOperationController, type SessionTenancyOperationController, type SessionTenancyOperationScope, } from "../../lib/session-tenancy-operation-controller"; import { runCurrentTransitionInvocation, type WorkspaceTransitionIdentity, } from "../../lib/workspace-transition"; type Confirmation = | { kind: "visibility"; visibility: SessionVisibility } | { kind: "fork"; visibility: SessionVisibility }; /** * What a change to Only me would stop or drop, read before anything is shown. * Nothing in the way: the change just happens. `unavailable` (an older server * or a failed read) opens the plain confirmation; the server answers * `active_work` with the same summary if something is in the way. */ type VisibilityChangePreview = | { status: "ready"; work: SessionVisibilityWork } | { status: "unavailable" }; /** Each request already waits for the stop to settle; a few more cover a slow worker. */ const STOPPING_WORK_RETRIES = 3; export default function SessionRouteAuxiliary( props: | { session: Session; events: SessionEvent[]; workspaceId?: never | undefined; sessionId?: never | undefined; } | { workspaceId: string; sessionId: string; loadError: unknown; /** Retry the session read now (it also keeps retrying in the background). */ onRetry?: (() => void) | undefined; session?: never | undefined; events?: never | undefined; }, ) { return props.session ? ( ) : isTransientRequestError(props.loadError) ? ( // The session read has failed transiently for a while (an outage outlasting // a rolling deploy). The read keeps retrying and the page opens the session // on its own when it succeeds. Try again ) : undefined } /> ) : !isApiErrorStatus(props.loadError, 404) ? ( Back to sessions } /> ) : ( ); } /** Route adapter kept inside the activation-gated lazy chunk. */ /** * A private fork lands in the SOURCE workspace, so it is the same product * decision migration 0323 makes for a private create: an organization whose * owner has not enabled private sessions gets no private destination in a * shared workspace. Migration 0336's fork lifecycle routine fails that closed * with SQLSTATE 55000, and this hook keeps the dialog from offering a choice * the database will refuse. A personal workspace is exempt, exactly as on the create path, * and an unknown answer fails closed rather than offering Private optimistically. */ function usePrivateForkCapability(options: { client: OpenGeniBrowserClient; workspaceId: string; personalWorkspace: boolean; managedSession: boolean; }): boolean { const { client, workspaceId, personalWorkspace, managedSession } = options; const [canForkPrivately, setCanForkPrivately] = useState(personalWorkspace); useEffect(() => { if (personalWorkspace) { setCanForkPrivately(true); return; } setCanForkPrivately(false); if (!managedSession) return; let current = true; void client .getSessionTenancyCreateCapabilities(workspaceId) .then((capabilities) => { if (current) setCanForkPrivately(capabilities.canCreatePrivate === true); }) .catch(() => { if (current) setCanForkPrivately(false); }); return () => { current = false; }; }, [client, managedSession, personalWorkspace, workspaceId]); return canForkPrivately; } export function SessionTenancyRouteControl({ session, events, sourceEventId, onForkClose, }: { session: Session; events: SessionEvent[]; sourceEventId?: string | undefined; onForkClose?: (() => void) | undefined; }) { const context = useAppContext(); const navigate = useNavigate(); const workspace = context.workspaces.find((candidate) => candidate.id === session.workspaceId); const personalWorkspace = isPersonalWorkspace(workspace ?? null, context.managedSelfContext); const managedSession = context.clientConfig.auth.mode === "managedSession" && context.authSession !== null; const canForkPrivately = usePrivateForkCapability({ client: context.client, workspaceId: session.workspaceId, personalWorkspace, managedSession, }); const transition = context.captureWorkspaceInvocation(session.workspaceId); if (!transition) return null; const scopeLabel = personalWorkspace ? `${workspace?.name ?? "this workspace"} Personal workspace` : (workspace?.name ?? "this workspace"); return ( void navigate({ to: "/workspaces/$workspaceId/sessions/$sessionId", params: { workspaceId, sessionId }, }) } /> ); } export function SessionTenancyControl({ sourceEventId, onForkClose, session, events, client, managedSession, canForkPrivately, scopeLabel, captureWorkspaceInvocation, ownsWorkspaceInvocation, operationController, operationScope, onOpenSession, }: { session: Session; events?: SessionEvent[] | undefined; sourceEventId?: string | undefined; onForkClose?: (() => void) | undefined; client: OpenGeniBrowserClient; managedSession: boolean; /** False also covers "not yet known": a private fork is never offered on a * guess, because the database refuses it under the 0323 product decision. */ canForkPrivately: boolean; scopeLabel: string; captureWorkspaceInvocation: (workspaceId: string) => WorkspaceTransitionIdentity | null; ownsWorkspaceInvocation: (workspaceId: string, accepted: WorkspaceTransitionIdentity) => boolean; operationController: SessionTenancyOperationController; operationScope: SessionTenancyOperationScope; onRefreshSession?: (() => Promise) | undefined; onOpenSession: (workspaceId: string, sessionId: string) => void; }) { const target = useMemo( () => ({ workspaceId: session.workspaceId, sessionId: session.id }), [session.id, session.workspaceId], ); const targetRef = useRef(target); targetRef.current = target; const mountedRef = useRef(false); const operationSequenceRef = useRef(0); const visibilityEventSequenceRef = useRef(0); const operationSnapshot = operationController.snapshot(operationScope); const pendingVisibility = operationSnapshot.visibility; const pendingFork = operationSnapshot.fork; const [override, setOverride] = useState(session.tenancy); const [confirmation, setConfirmation] = useState(() => sourceEventId ? { kind: "fork", visibility: operationSnapshot.fork?.visibility ?? (canForkPrivately ? (session.tenancy?.visibility ?? "workspace") : "workspace"), } : null, ); const [busy, setBusy] = useState(false); const [failure, setFailure] = useState(null); const [visibilityPreview, setVisibilityPreview] = useState(null); const [announcement, setAnnouncement] = useState(""); const [, setControllerRevision] = useState(0); const accessTriggerRef = useRef(null); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); useEffect(() => { operationSequenceRef.current += 1; setBusy(false); setFailure(null); if (!sourceEventId) setConfirmation(null); }, [session.id, session.workspaceId, sourceEventId]); useEffect(() => { if (!session.tenancy) { setOverride(undefined); return; } setOverride((current) => !current || session.tenancy!.authorityEpoch >= current.authorityEpoch ? session.tenancy : current, ); }, [session.tenancy]); useEffect(() => { if ( pendingVisibility && (!session.tenancy || session.tenancy.authorityEpoch > pendingVisibility.expectedAuthorityEpoch) ) { operationController.settleVisibility(operationScope, pendingVisibility); setControllerRevision((current) => current + 1); } }, [operationController, operationScope, pendingVisibility, session.tenancy]); const displayedTenancy = override; const mayManage = Boolean(displayedTenancy?.ownedByCurrentUser && managedSession); const mayFork = Boolean( displayedTenancy && managedSession && (displayedTenancy.visibility === "workspace" || displayedTenancy.ownedByCurrentUser), ); const isCurrentInvocation = useCallback( ( acceptedTarget: SessionTenancyTarget, acceptedTransition: WorkspaceTransitionIdentity, operationSequence: number, ) => mountedRef.current && operationSequenceRef.current === operationSequence && isCurrentSessionTenancyTarget(targetRef.current, acceptedTarget) && ownsWorkspaceInvocation(acceptedTarget.workspaceId, acceptedTransition), [ownsWorkspaceInvocation], ); const reconcileSource = useCallback( async ( acceptedTarget: SessionTenancyTarget, acceptedTransition: WorkspaceTransitionIdentity, operationSequence: number, ): Promise => { const result = await runCurrentTransitionInvocation({ isCurrent: () => isCurrentInvocation(acceptedTarget, acceptedTransition, operationSequence), request: async () => await client.getSession(acceptedTarget.workspaceId, acceptedTarget.sessionId, { fresh: true, }), }); if (result.status === "stale") return null; setOverride(result.value.tenancy); return result.value; }, [client, isCurrentInvocation], ); const latestVisibilitySequence = events?.reduce( (latest, event) => event.type === "session.visibility.changed" ? Math.max(latest, event.sequence) : latest, 0, ); useEffect(() => { if ( !latestVisibilitySequence || latestVisibilitySequence <= visibilityEventSequenceRef.current ) { return; } visibilityEventSequenceRef.current = latestVisibilitySequence; const acceptedTransition = captureWorkspaceInvocation(target.workspaceId); if (!acceptedTransition) return; let cancelled = false; void client .getSession(target.workspaceId, target.sessionId, { fresh: true }) .then((current) => { if ( cancelled || !mountedRef.current || !isCurrentSessionTenancyTarget(targetRef.current, target) || !ownsWorkspaceInvocation(target.workspaceId, acceptedTransition) ) { return; } setOverride(current.tenancy); const retained = operationController.snapshot(operationScope).visibility; if ( retained && (!current.tenancy || current.tenancy.authorityEpoch > retained.expectedAuthorityEpoch) ) { operationController.settleVisibility(operationScope, retained); setControllerRevision((revision) => revision + 1); } setAnnouncement( !current.tenancy ? "Session access controls are no longer available." : current.tenancy.visibility === "private" ? "Session is private to you." : `Session is visible to ${scopeLabel}.`, ); }) .catch(() => undefined); return () => { cancelled = true; }; }, [ captureWorkspaceInvocation, client, latestVisibilitySequence, operationController, operationScope, ownsWorkspaceInvocation, scopeLabel, target, ]); const changeVisibility = useCallback( async (visibility: SessionVisibility, stopActiveWork: boolean): Promise => { if (!displayedTenancy || !mayManage || visibility === displayedTenancy.visibility) { return true; } const acceptedTransition = captureWorkspaceInvocation(target.workspaceId); if (!acceptedTransition) return true; const operationSequence = operationSequenceRef.current + 1; operationSequenceRef.current = operationSequence; const attempt = operationController.prepareVisibility( operationScope, { visibility, expectedAuthorityEpoch: displayedTenancy.authorityEpoch }, () => crypto.randomUUID(), ); setBusy(true); setFailure(null); let receiptConfirmed = false; try { const result = await runCurrentTransitionInvocation({ isCurrent: () => isCurrentInvocation(target, acceptedTransition, operationSequence), request: async () => { for (let retry = 0; ; retry += 1) { try { return await client.updateSessionVisibility(target.workspaceId, target.sessionId, { visibility: attempt.visibility, expectedAuthorityEpoch: attempt.expectedAuthorityEpoch, idempotencyKey: attempt.idempotencyKey, ...(stopActiveWork ? { stopActiveWork: true } : {}), }); } catch (error) { // Same key, same consent: the server resumes the stop it began. if ( retry < STOPPING_WORK_RETRIES && isStoppingSessionWorkError(error) && isCurrentInvocation(target, acceptedTransition, operationSequence) ) { continue; } throw error; } } }, }); if (result.status === "stale") return true; receiptConfirmed = true; if (result.value.replay) { const authoritative = await reconcileSource( target, acceptedTransition, operationSequence, ); if (!authoritative) return true; operationController.settleVisibility(operationScope, attempt); if (authoritative.tenancy) { const message = authoritative.tenancy.visibility === "private" ? "Session is private to you." : `Session is visible to ${scopeLabel}.`; setAnnouncement(message); toast.info("Session access refreshed", { description: message }); } else { const message = "Session access controls are no longer available."; setAnnouncement(message); toast.info("Session access refreshed", { description: message }); } return true; } else { setOverride({ ...displayedTenancy, visibility: result.value.visibility, authorityEpoch: result.value.authorityEpoch, }); } operationController.settleVisibility(operationScope, attempt); const message = result.value.visibility === "private" ? "Session is private to you." : `Session is visible to ${scopeLabel}.`; setAnnouncement(message); toast.success("Session access updated", { description: message }); return true; } catch (error) { if (!isCurrentInvocation(target, acceptedTransition, operationSequence)) return true; const classified = classifySessionTenancyFailure(error); if (classified.kind === "active_work" && classified.work) { // Something appeared after the preview: show what it is and ask once. setVisibilityPreview({ status: "ready", work: classified.work }); setConfirmation({ kind: "visibility", visibility }); } let authoritative: Session | null = null; if (classified.reconcile) { authoritative = await reconcileSource( target, acceptedTransition, operationSequence, ).catch(() => null); } if (!isCurrentInvocation(target, acceptedTransition, operationSequence)) return true; if ( authoritative && (!authoritative.tenancy || authoritative.tenancy.authorityEpoch > attempt.expectedAuthorityEpoch) ) { operationController.settleVisibility(operationScope, attempt); if (classified.kind === "epoch_conflict") { setFailure(classified.message); setAnnouncement(classified.message); return true; } const message = !authoritative.tenancy ? "Session access controls are no longer available." : authoritative.tenancy.visibility === "private" ? "Session is private to you." : `Session is visible to ${scopeLabel}.`; setAnnouncement(message); toast.info("Session access refreshed", { description: message }); return true; } const retainAttempt = classified.retainAttempt || (receiptConfirmed && retryableSessionTenancyReconciliationFailure(error)); if (!retainAttempt) { operationController.settleVisibility(operationScope, attempt); } setFailure(classified.message); setAnnouncement(classified.message); return !retainAttempt; } finally { if (isCurrentInvocation(target, acceptedTransition, operationSequence)) setBusy(false); } }, [ captureWorkspaceInvocation, client, displayedTenancy, isCurrentInvocation, mayManage, operationController, operationScope, reconcileSource, scopeLabel, target, ], ); /** * Sharing always asks. Limiting to me reads what it would stop or drop first: * nothing in the way just makes the change; anything else is listed in one * confirmation whose single button does it all. */ const startVisibilityChange = useCallback( async (visibility: SessionVisibility) => { setFailure(null); setVisibilityPreview(null); if (visibility !== "private") { setConfirmation({ kind: "visibility", visibility }); return; } const operationSequence = operationSequenceRef.current; setBusy(true); let preview: VisibilityChangePreview; try { const result = await client.previewSessionVisibilityChange( target.workspaceId, target.sessionId, visibility, ); const work = normalizeSessionVisibilityWork(result.work); preview = work ? { status: "ready", work } : { status: "unavailable" }; } catch { preview = { status: "unavailable" }; } if (!mountedRef.current || operationSequenceRef.current !== operationSequence) return; setBusy(false); if (preview.status === "ready" && sessionVisibilityWorkIsEmpty(preview.work)) { await changeVisibility(visibility, false); return; } setVisibilityPreview(preview); setConfirmation({ kind: "visibility", visibility }); }, [changeVisibility, client, target], ); const forkSession = useCallback( async (visibility: SessionVisibility): Promise => { if (!displayedTenancy || !mayFork) return true; if (sourceEventId && pendingFork && pendingFork.sourceEventId !== sourceEventId) { setFailure( "A previous fork still needs a retry. Resolve it from the session menu before choosing another message.", ); return false; } const acceptedTransition = captureWorkspaceInvocation(target.workspaceId); if (!acceptedTransition) return true; const operationSequence = operationSequenceRef.current + 1; operationSequenceRef.current = operationSequence; const workspaceSharedAcknowledged = displayedTenancy.visibility === "private" && visibility === "workspace"; const attempt = operationController.prepareFork( operationScope, { visibility, workspaceSharedAcknowledged, ...(sourceEventId ? { sourceEventId } : {}) }, () => crypto.randomUUID(), ); setBusy(true); setFailure(null); let receiptConfirmed = false; try { const result = await runCurrentTransitionInvocation({ isCurrent: () => isCurrentInvocation(target, acceptedTransition, operationSequence), request: async () => await client.forkSession(target.workspaceId, target.sessionId, { ...(attempt.sourceEventId ? { sourceEventId: attempt.sourceEventId } : {}), visibility: attempt.visibility, workspaceSharedAcknowledged: attempt.workspaceSharedAcknowledged, idempotencyKey: attempt.idempotencyKey, }), }); if (result.status === "stale") return true; receiptConfirmed = true; const destination = await runCurrentTransitionInvocation({ isCurrent: () => isCurrentInvocation(target, acceptedTransition, operationSequence), request: async () => await client.getSession(result.value.workspaceId, result.value.sessionId, { fresh: true, }), }); if (destination.status === "stale") return true; if ( destination.value.workspaceId !== target.workspaceId || destination.value.tenancy?.visibility !== result.value.visibility || !destination.value.tenancy.ownedByCurrentUser ) { throw new Error("The fork receipt did not resolve to the expected owned session."); } operationController.settleFork(operationScope, attempt); const description = result.value.visibility === "private" ? "The independent copy is private to you." : `The independent copy is visible to ${scopeLabel}.`; setAnnouncement(description); toast.success(result.value.replay ? "Session fork recovered" : "Session fork created", { description, }); onOpenSession(result.value.workspaceId, result.value.sessionId); return true; } catch (error) { if (!isCurrentInvocation(target, acceptedTransition, operationSequence)) return true; const classified = classifySessionTenancyFailure(error); const retainAttempt = classified.retainAttempt || (receiptConfirmed && retryableSessionTenancyReconciliationFailure(error)); if (!retainAttempt) operationController.settleFork(operationScope, attempt); const message = classified.message; setFailure(message); setAnnouncement(message); return sourceEventId ? false : !retainAttempt; } finally { if (isCurrentInvocation(target, acceptedTransition, operationSequence)) setBusy(false); } }, [ captureWorkspaceInvocation, client, displayedTenancy, isCurrentInvocation, mayFork, sourceEventId, pendingFork, onOpenSession, operationController, operationScope, scopeLabel, target, ], ); if (!displayedTenancy) return null; const privateSession = displayedTenancy.visibility === "private"; const alternateVisibility: SessionVisibility = privateSession ? "workspace" : "private"; const retryingVisibility = pendingVisibility?.visibility === alternateVisibility && pendingVisibility.expectedAuthorityEpoch === displayedTenancy.authorityEpoch; const visibilityAction = privateSession ? "Share this session with workspace…" : "Limit this session to me"; const readyPreview = visibilityPreview?.status === "ready" ? visibilityPreview : null; // What the one confirmation also stops or drops; null when nothing is listed. const workToStop = readyPreview && !sessionVisibilityWorkIsEmpty(readyPreview.work) ? readyPreview.work : null; const stopsWork = workToStop !== null && sessionVisibilityWorkStopsWork(workToStop); const visibilityConfirmLabel = privateSession ? retryingVisibility ? "Retry share with workspace" : "Share with workspace" : stopsWork ? "Stop work and limit to me" : retryingVisibility ? "Retry limit to me" : "Limit to me"; const stateLabel = privateSession ? "Private" : "Workspace"; const stateDescription = privateSession ? "Only you can open this session." : `Visible to people in ${scopeLabel}.`; const retryingFork = pendingFork !== null; const stateChip = ( // Phones show just the icon (a 44px target, no chip chrome); the labelled // chip with its chevron returns from sm. A narrow title bar (a small app // window beside the sidebar) also gets just the icon, so the title keeps // its room. {privateSession ? ( ); return (
{mayFork ? (

{stateLabel} session

{stateDescription}

{mayManage ? ( <> void startVisibilityChange(alternateVisibility)} > {privateSession ? : } {retryingVisibility ? `Retry: ${visibilityAction}` : visibilityAction} ) : null} setConfirmation({ kind: "fork", visibility: pendingFork?.visibility ?? (canForkPrivately ? displayedTenancy.visibility : "workspace"), }) } > {retryingFork ? "Retry session fork" : "Fork session"} {failure ? ( <>
Access change needs attention. {failure}
) : null}
) : ( {stateChip} {stateDescription} )} {announcement}
{ if (!open) { setConfirmation(null); setVisibilityPreview(null); onForkClose?.(); } }} title={ confirmation?.kind === "fork" ? confirmation.visibility === "workspace" ? `Fork a workspace-visible copy?` : "Fork a private copy?" : confirmation?.visibility === "workspace" ? `Share this session with ${scopeLabel}?` : "Limit this session to you?" } description={ (pendingFork ? pendingFork.sourceEventId : sourceEventId) ? `Copy the conversation through the selected message${confirmation?.visibility === "workspace" ? ` into a session visible to people in ${scopeLabel}` : " into a session visible only to you"}. Later messages are excluded. Live credentials, tools, goals, and processes are not copied.` : confirmation?.kind === "fork" && displayedTenancy.visibility === "private" && confirmation.visibility === "workspace" ? `This private session's complete conversation will be copied into a new session visible to people in ${scopeLabel}. Live credentials, connections, tools, goals, and processes are not copied.` : confirmation?.kind === "fork" ? confirmation.visibility === "workspace" ? `Create an independent copy visible to people in ${scopeLabel}. Live credentials, connections, tools, goals, and processes are not copied.` : "Create an independent copy visible only to you. Live credentials, connections, tools, goals, and processes are not copied." : confirmation?.visibility === "workspace" ? "People who can access this workspace will be able to open and continue the session. Current work keeps running with its existing identity and connections." : `Only you will be able to open the session.${privateSessionSandboxNote(session.sandboxBackend)}` } confirmLabel={ confirmation?.kind === "fork" ? retryingFork ? "Retry fork" : confirmation.visibility === "workspace" ? "Fork for workspace" : "Fork privately" : visibilityConfirmLabel } pendingLabel={ confirmation?.kind === "visibility" && stopsWork ? "Stopping work…" : "Working…" } // A fork only widens exposure when a private source is copied into the // workspace. Changing an existing session to workspace visibility always // does, and making it private destroys the work and access it drops. // A shared-to-shared fork exposes nothing new. destructive={ confirmation?.kind === "fork" ? displayedTenancy.visibility === "private" && confirmation.visibility === "workspace" : confirmation?.visibility === "workspace" || workToStop !== null } cancelAutoFocus restoreFocusRef={sourceEventId ? undefined : accessTriggerRef} onConfirm={() => { if (!confirmation) return true; return confirmation.kind === "visibility" ? changeVisibility(confirmation.visibility, workToStop !== null) : forkSession(confirmation.visibility); }} > {confirmation?.kind === "visibility" && workToStop ? (
    {sessionVisibilityWorkSummary(workToStop).map((line) => (
  • {line}
  • ))}
) : null} {confirmation?.kind === "fork" && !retryingFork ? (
{canForkPrivately ? ( ) : null}
) : null} {failure ? ( {failure} ) : null}
); }