import { useAppApi } from "../lib/app-api"; import { useSessionUiFeature } from "../lib/session-ui-features"; import { initialWorkspaceMachineCompute } from "../lib/workspace-default-machine"; import { t } from "../lib/console-text"; import { ANALYTICS_COLLECTION_ENABLED_EVENT } from "../lib/analytics-consent"; import { useWorkspaceRigs } from "../lib/use-workspace-rigs"; import { useRepositoryCatalogRefresh } from "../lib/use-follow-up-repositories"; import { useGitHubAppConnectLauncher } from "../components/github-app-connect-launcher"; import { openGitHubInstallationSettings } from "../lib/github-app-connect"; import { captureAnalyticsEvent } from "../lib/analytics-observer"; import { useWorkspaceMachines } from "../lib/use-workspace-machines"; import { useSessionListHintRefresh } from "../lib/use-session-list-hint-refresh"; import { useOpenGeni } from "../../session-context"; import { addsConnectorOutsideDefaults, changedConnectorExclusions, defaultConnectorSelection, newSessionConnectorCustomizeState, } from "../lib/composer-connectors"; // The sessions index: the centered "Start a session" composer. The form is // organised top-down — (A) message + model/tools/repos pills → (B) WHERE SHOULD // THIS RUN? (when machines exist) → (C) rig or machine fields. // Goals are set by the agent (or later surfaces), not at create. // // "Where should this run?" is a first-class segmented control with two kinds: // Managed Sandbox (ephemeral, platform-owned — clones repos, injects env) vs // Connected Machine (a user-owned enrolled machine — its own checkout & git // auth, a working folder, no clone, no env injection). The kind gates the band // below it; invalid states ("clone my repo onto a machine") are unreachable by // construction. // // The Connected Machine path is opt-in when the deployment owns a managed // sandbox. On a selfhosted-primary deployment it is the only truthful default: // the composer selects an online machine and remains blocked when none exists. import { FILE_ONLY_MESSAGE_TEXT, type ComposerState } from "../../hooks/use-composer"; import { LightboxProvider } from "../../timeline"; import { ModelMark } from "../../components/model-mark"; import { useChannels } from "../../hooks/use-channels"; import { useWorkspaceSessions } from "../../hooks/use-workspace-sessions"; import { resolveWorkspaceSessionToolDefaults, stableJson } from "@opengeni/contracts"; import { MACHINES_COMPOSER_POLL_MS, type MachineView } from "../../machines"; import { NewSessionRealtimeControl, prestartSessionRealtime, useRealtimeModelSelection, } from "../../realtime"; import { OpenGeniApiError, type NewSessionSelectionHistory, type Rig, type SessionRealtimeModel, } from "@opengeni/sdk"; import { Link, type PageHost, useNavigate, usePage } from "../lib/navigation"; import { ChevronDownIcon, CreditCardIcon, FolderIcon, LockIcon, PlusIcon } from "lucide-react"; import { createElement, lazy, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import { toast } from "../lib/toast"; import { BillingClassMark } from "../components/billing-class-mark"; import { ChannelCreateDialog } from "../components/rail/channel-create-dialog"; import { ConsoleComposer, useDraftAttachments } from "../components/Composer"; import { NewSessionStarters } from "../components/new-session-starters"; import { NewSessionDraftSyncNotice } from "../components/new-session-draft-sync-notice"; import { WorkspaceComposerPlus as ComposerMobilePlus } from "../components/workspace-composer-plus"; import { RunsOnMenuBody, RunsOnNotice, UsageLimitMenuBody, VisibilityMenuBody, hasRunsOnChoices, runsOnAttention, hasVisibilityChoice, runsOnSummary, visibilitySummary, } from "../components/session/new-session-settings-menu"; import { NO_SESSION_USAGE_LIMIT, parseSessionUsageLimitDraft, sessionUsageLimitCapability, sessionUsageLimitDraftSummary, type SessionUsageLimitDraft, } from "../lib/session-usage-limit"; import { ModelPicker, type SessionToolSelection } from "../components/pickers"; import { RepositoryContextMenuBody, type RepositoryContextPickerProps, } from "../components/repository-picker"; import { Button } from "../../ui/button"; import { sessionDisplayTitle } from "../lib/session-rename"; import { DropdownMenu, DropdownMenuCheck, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, } from "../../ui/dropdown-menu"; import { Notice } from "../../ui/notice"; import { CreditRefusalNotice } from "../components/credits/credit-refusal-notice"; import { coveredModelChoice, creditRefusalGap, freeCreditGap } from "../lib/free-credit-gap"; import { isPaymentRequiredError } from "../lib/model-access"; import { isTransientServiceFailure, OPENGENI_UPDATING_NOTICE } from "../lib/transient-retry"; import { useConnectionAccounts } from "../components/capabilities/use-connection-accounts"; import { StatusDot } from "../../ui/status-dot"; import { useAppContext, useLatestCallback } from "../app-context"; import { useBrowserAccountBridgeBlocker } from "../lib/browser-account-bridge"; import { EMPTY_COMPOSER_LAUNCH, composerLaunchSearchKey, modelProvidedAfterLaunch, type ComposerLaunchSearch, } from "../lib/composer-launch"; import { FOCUS_CREATE_COMPOSER_EVENT, type CreateComposerFocusIntent, } from "../lib/create-composer-focus"; import type { RepoDraft } from "../lib/session-tools"; import { composerFallbackModel } from "../lib/model-access-onboarding"; import { useForgetLastSession } from "../lib/last-session-memory"; import { isMachineComputeSelectable, resolveSelectableMachineSandboxId, } from "../lib/machine-selectability"; import { effortOptionsForModel, findPickerRow, modelUsesCredits, runnableLatencyModesForModel, type PickerModelRow, } from "../lib/model-policy"; import { isPersonalWorkspace } from "../lib/managed-self-context"; import { attachManualRepository } from "../lib/manual-repositories"; import { hasAccountPermission, hasWorkspacePermission } from "../lib/permissions"; import { isPersonalAttachmentConflict, newSessionFixedResourceCatalogFailed, newSessionPersonalResourceAttachment, personalResourceSelectionIdentityKey, reconcileNewSessionFixedResources, recoverNewSessionPersonalResourceAttachment, resolvePersonalResourceOwnerScope, } from "../lib/personal-resource-attachments"; import { recentSessionModelPresentation, recentSessionStatus, recentSessionsForHome, relativeTimeLabel, sessionRepoLabel, } from "../../session-list-model"; import { signupStarterSet } from "../lib/signup-starter-set"; import { useWorkspaceModelCatalog, type WorkspaceModelCatalogState, } from "../lib/use-workspace-model-catalog"; import { resolveWorkspaceAgentDefaults } from "@opengeni/contracts"; import { ComposerCapabilitiesChip } from "../components/composer-capabilities-chip"; import { useVaults } from "../../hooks/use-vaults"; import { SessionSecretsMenuBody, draftSecretsRows, sessionSecretsSummary, } from "../components/secrets/session-secrets-panel"; import { capabilityAvailability, capabilityOn, capabilitySummary, draftFromRequest, requestFromDraft, workspaceAgentDefaultsDraft, type AgentCapabilityDraft, } from "../lib/agent-capabilities"; import { emptySessionDraft, fitToolPolicyToAgentCapabilities, isSessionDraftComputeReady, newSessionCreateVisibility, newSessionDraftOptionsFromSessionDraft, rememberedMachineFolder, sessionDraftFromNewSessionDraftOptions, submissionFromSessionDraft, type SessionDraft, } from "../lib/session-create"; import { clientFirstPartyMcpToolPolicy, firstPartySessionToolOptionsFor, selectableSessionMcpServerIds, unavailableSessionMcpServerIds, newSessionDraftToolPolicy, rehydrateRepositoryResources, repositorySelectionFromResources, } from "../lib/session-tools"; import { useNewSessionDraft, type NewSessionDraftEditable } from "../lib/use-new-session-draft"; import { userErrorText } from "../../ui/api-error"; import { newSessionCreateSnapshot, runNewSessionRouteSubmission, type CreatedSessionRouteAuthority, } from "./sessions-index-submission"; import { hydratedNewSessionProjectProvenancePresent, initialNewSessionProjectLaunchIntent, newSessionProjectSelection, nextFocusedNewSessionProjectLaunchIntent, nextNewSessionProjectLaunchIntent, resolveAmbientNewSessionProjectChannelId, resolveHydratedNewSessionProjectSelection, } from "./sessions-index-hydration"; import type { Channel, SandboxBackend, Session } from "../types"; import { useUiEventTarget } from "../lib/ui-events"; const useCommitSynchronousEffect = typeof window === "undefined" ? useEffect : useLayoutEffect; const EmptyCreditsNotice = lazy(() => import("../components/credit-required-prompt").then((module) => ({ default: module.EmptyCreditsNotice, })), ); const CreditTopupPrompt = lazy(() => import("../components/credit-required-prompt").then((module) => ({ default: module.CreditRequiredPrompt, })), ); export function SessionsIndexRoute({ workspaceId, launch = EMPTY_COMPOSER_LAUNCH, }: { workspaceId: string; launch?: ComposerLaunchSearch | undefined; }) { const { accessKeyVersion, accessContext } = useAppContext(); useForgetLastSession(accessContext.subjectId, workspaceId); return ( ); } function SessionsIndexRouteContent({ workspaceId, launch, }: { workspaceId: string; launch: ComposerLaunchSearch; }) { const uiEvents = useUiEventTarget("window"); const context = useAppContext(); const api = useAppApi(); const page = usePage(); const starters = useSessionUiFeature("starters"); const connectionAccounts = useConnectionAccounts( context.client, { id: "new-session", workspaceId, selectedIds: [...context.selectedCapabilityToolIds], }, context.workspaceCapabilityCatalog, context.accessContext === null ? null : hasWorkspacePermission(context.accessContext, workspaceId, "connections:read"), ); const repositoryCatalogRefresh = useRepositoryCatalogRefresh(workspaceId, context); // Hosted here, outside the repository menu, so the menu closing when the // authorization popup opens does not unmount GitHub App setup. const githubAppConnect = useGitHubAppConnectLauncher(workspaceId); const firstPartyMcpToolPolicy = useMemo( () => clientFirstPartyMcpToolPolicy(context.clientConfig), [context.clientConfig], ); const workspace = context.workspaces.find((candidate) => candidate.id === workspaceId) ?? null; const configuredToolDefaults = useMemo( () => resolveWorkspaceSessionToolDefaults(workspace?.settings), [workspace?.settings], ); const defaultFirstPartyMcpTools = useMemo( () => configuredToolDefaults?.firstPartyMcpTools?.filter((tool) => firstPartyMcpToolPolicy.allowed.includes(tool), ) ?? firstPartyMcpToolPolicy.default, [configuredToolDefaults, firstPartyMcpToolPolicy], ); const defaultSandboxBackend = context.clientConfig.defaultSandboxBackend; const firstPartyToolOptions = useMemo( () => firstPartySessionToolOptionsFor(firstPartyMcpToolPolicy.allowed), [firstPartyMcpToolPolicy], ); const navigate = useNavigate(); const modelCatalog = useWorkspaceModelCatalog(workspaceId); const channelsQuery = useChannels({ pollIntervalMs: 60_000 }); const launchChannelId = launch.channelId === "default" ? null : launch.channelId; const [selectedChannelId, setSelectedChannelId] = useState( launchChannelId ?? null, ); const selectedChannelIdRef = useRef(selectedChannelId); const setSelectedProjectChannelId = useCallback((channelId: string | null) => { selectedChannelIdRef.current = channelId; setSelectedChannelId(channelId); }, []); const [selectionHistory, setSelectionHistory] = useState({ projects: [], }); const [projectProvenancePresent, setProjectProvenancePresent] = useState( launchChannelId !== undefined, ); const remoteDraftHydratedRef = useRef(false); const previousLaunchChannelIdRef = useRef(launchChannelId); const launchProjectIntentRef = useRef(initialNewSessionProjectLaunchIntent(launchChannelId)); const recentChannelId = selectionHistory.projects[0]?.channelId ?? null; const [projectDialogOpen, setProjectDialogOpen] = useState(false); const [projectNameDraft, setProjectNameDraft] = useState(""); const { resetSessionView } = context; const [message, setMessage] = useState(""); const [creditTopupOpen, setCreditTopupOpen] = useState(false); const [draft, setDraft] = useState(() => emptySessionDraft(defaultFirstPartyMcpTools, defaultSandboxBackend), ); const personalWorkspace = isPersonalWorkspace(workspace, context.managedSelfContext); const [capabilitiesOpenRequest, setCapabilitiesOpenRequest] = useState< { panel: "capabilities"; nonce: number } | undefined >(undefined); // "+" > Capabilities: the workspace's defaults, or this chat's own choice. const agentAvailability = useMemo( () => capabilityAvailability(context.clientConfig.agentConfig), [context.clientConfig.agentConfig], ); const workspaceAgentDraft = useMemo( () => workspaceAgentDefaultsDraft({ capabilities: resolveWorkspaceAgentDefaults(workspace?.settings)?.capabilities, legacyHumanInputOff: workspace?.settings.agentHumanInputEnabled === false, }), [workspace?.settings], ); const composerAgentCapabilities = { customized: draft.agentCapabilities !== undefined, draft: draft.agentCapabilities !== undefined ? draftFromRequest(draft.agentCapabilities) : workspaceAgentDraft, availability: agentAvailability, onCustomizedChange: (customized: boolean) => setDraft((current) => { if (!customized) { const { agentCapabilities: _dropped, ...rest } = current; return rest; } return { ...current, agentCapabilities: requestFromDraft(workspaceAgentDraft, agentAvailability), }; }), onChange: (next: AgentCapabilityDraft) => setDraft((current) => ({ ...current, agentCapabilities: requestFromDraft(next, agentAvailability), })), }; // "+" > Secrets: which vaults this chat may use. Shown only while its agent // can use secrets (the workspace default or this chat's own capabilities). const draftSecretsOn = agentAvailability.isAvailable("secrets") && capabilityOn(composerAgentCapabilities.draft.values, "secrets"); const canListSecrets = hasWorkspacePermission(context.accessContext, workspaceId, "secrets:list") || hasWorkspacePermission(context.accessContext, workspaceId, "variable-sets:list"); const draftVaults = useVaults({ client: context.client, workspaceId, enabled: canListSecrets && draftSecretsOn, }); const draftVaultPolicy = draft.vaultPolicy ?? { mode: "all" as const }; const draftVaultRows = draftVaults.vaults ? draftSecretsRows(draftVaults.vaults, draftVaultPolicy) : null; const attachments = useDraftAttachments( workspaceId, personalWorkspace || draft.visibility === "private" ? "personal" : "workspace", ); const fixedResourceCatalogEnabled = draft.compute.kind === "sandbox"; const canUseRigs = hasWorkspacePermission(context.accessContext, workspaceId, "rigs:use"); const rigs = useWorkspaceRigs({ enabled: fixedResourceCatalogEnabled && canUseRigs, }); const [tenancyCapabilities, setTenancyCapabilities] = useState<{ activated: boolean; canCreatePrivate: boolean; reason: "available" | "not_activated" | "managed_session_required" | "unavailable"; defaultVisibility?: "private" | "workspace" | undefined; } | null>(null); const tenancyCapabilityGeneration = useRef(0); // Whether the person picked who can see this new chat. Until they do, it // starts with the organization's default. const visibilityChosenRef = useRef(false); useEffect(() => { const generation = ++tenancyCapabilityGeneration.current; visibilityChosenRef.current = false; setTenancyCapabilities(null); void context.client .getSessionTenancyCreateCapabilities(workspaceId) .then((capabilities) => { if (tenancyCapabilityGeneration.current !== generation) return; setTenancyCapabilities(capabilities); if (!capabilities.canCreatePrivate) { setDraft((current) => current.visibility === "private" ? { ...current, visibility: "workspace" } : current, ); } else if (capabilities.defaultVisibility === "private" && !visibilityChosenRef.current) { setDraft((current) => current.visibility === "private" ? current : { ...current, visibility: "private" }, ); } }) .catch(() => { if (tenancyCapabilityGeneration.current !== generation) return; setTenancyCapabilities({ activated: false, canCreatePrivate: false, reason: "unavailable", }); setDraft((current) => current.visibility === "private" ? { ...current, visibility: "workspace" } : current, ); }); }, [context.client, personalWorkspace, workspaceId]); const createVisibility = newSessionCreateVisibility( personalWorkspace, draft.visibility, tenancyCapabilities?.canCreatePrivate === true, ); const personalOwnerScope = resolvePersonalResourceOwnerScope({ authMode: context.clientConfig.auth.mode, authSession: context.authSession, accessSubjectId: context.accessContext.subjectId, managedSelfContext: context.managedSelfContext, workspace, }); const personalResourcesAvailable = personalOwnerScope !== null && (personalWorkspace || tenancyCapabilities?.activated === true); const personalResourceEligibilitySettled = personalWorkspace || personalOwnerScope === null || tenancyCapabilities !== null; // Sandbox Environments configure the managed sandbox; a deployment without // one has nothing for them to apply to. const noManagedSandbox = defaultSandboxBackend === "none"; const selectableRigs = noManagedSandbox ? [] : rigs.rigs.filter((rig) => rig.scope !== "user" || personalResourcesAvailable); const selectedRig = selectableRigs.find((candidate) => candidate.id === draft.rigId); const selectableRigIdsKey = selectableRigs.map((rig) => rig.id).join("\u0000"); const fixedResourceSelection = reconcileNewSessionFixedResources({ selectedRigId: draft.rigId, selectableRigIds: fixedResourceCatalogEnabled ? selectableRigs.map((rig) => rig.id) : draft.rigId ? [draft.rigId] : [], rigsSettled: !fixedResourceCatalogEnabled || (personalResourceEligibilitySettled && !rigs.loading && rigs.error === null), }); const fixedResourceCatalogError = draft.compute.kind === "sandbox" && newSessionFixedResourceCatalogFailed({ selectedRigId: draft.rigId, selectionResolved: fixedResourceSelection.selectionResolved, rigCatalogFailed: rigs.error !== null, }); useEffect(() => { setDraft((current) => { if (current.compute.kind !== "sandbox") return current; const reconciled = reconcileNewSessionFixedResources({ selectedRigId: current.rigId, selectableRigIds: selectableRigIdsKey ? selectableRigIdsKey.split("\u0000") : [], rigsSettled: personalResourceEligibilitySettled && !rigs.loading && rigs.error === null, }); return reconciled.rigId === current.rigId ? current : { ...current, rigId: reconciled.rigId }; }); }, [ draft.rigId, personalResourceEligibilitySettled, rigs.error, rigs.loading, selectableRigIdsKey, ]); const [fleetPollMs, setFleetPollMs] = useState(undefined); const fleet = useWorkspaceMachines({ pollIntervalMs: fleetPollMs }); const machines = fleet.machines.filter((machine) => machine.kind === "selfhosted"); // "+" > Runs on: where the new chat runs and on which environment or folder. const runsOnChoices = { draft, machines, rigs: selectableRigs, workspaceDefaultRigId: workspace?.defaultRigId ?? null, selfhostedPrimary: defaultSandboxBackend === "selfhosted", noManagedSandbox, // A 404 means Connected Machines are off here, not a failure. fleetLoadFailed: fleet.error != null && !(fleet.error instanceof OpenGeniApiError && fleet.error.status === 404), selectedChannelId, selectionHistory, }; const fleetEmpty = machines.length === 0; const fleetLoadFailed = fleet.error != null && !(fleet.error instanceof OpenGeniApiError && fleet.error.status === 404); useEffect(() => { if (fleet.loading) return; setFleetPollMs(!fleetEmpty || fleetLoadFailed ? MACHINES_COMPOSER_POLL_MS : undefined); }, [fleet.loading, fleetEmpty, fleetLoadFailed]); const selectedMachineSandboxId = draft.compute.kind === "machine" ? draft.compute.sandboxId : null; const selectedMachine = selectedMachineSandboxId ? (machines.find((machine) => machine.sandboxId === selectedMachineSandboxId) ?? null) : null; const personalMachineSelected = selectedMachine?.scope === "user"; const selectedPersonalResourceNames = draft.compute.kind === "sandbox" ? selectedRig?.scope === "user" ? [selectedRig.name] : [] : personalMachineSelected && selectedMachine ? [selectedMachine.name] : []; const personalResourceSelectionKey = personalResourceSelectionIdentityKey({ rigId: selectedRig?.scope === "user" ? selectedRig.id : null, connectedMachineId: personalMachineSelected && selectedMachine ? selectedMachine.enrollmentId : null, }); const selectedPersonalResourceCount = personalResourceSelectionKey ? personalResourceSelectionKey.split("\u0000").length : 0; const [personalResourceCatalogRefreshPending, setPersonalResourceCatalogRefreshPending] = useState(false); const personalResourceCatalogRefreshGeneration = useRef(0); const personalResourceAttachment = newSessionPersonalResourceAttachment({ personalResourceCount: selectedPersonalResourceCount, visibility: createVisibility, }); const refreshPersonalResourceCatalogs = useLatestCallback(async (): Promise => { const generation = ++personalResourceCatalogRefreshGeneration.current; setPersonalResourceCatalogRefreshPending(true); try { if (canUseRigs) await rigs.refresh(); } finally { if (personalResourceCatalogRefreshGeneration.current === generation) { setPersonalResourceCatalogRefreshPending(false); } } }); const recoverPersonalResourceAttachment = useLatestCallback( (error: unknown, attemptedInput: Parameters[1]): void => { if (!isPersonalAttachmentConflict(error, attemptedInput)) return; void recoverNewSessionPersonalResourceAttachment({ error, attemptedInput, refreshCatalogs: refreshPersonalResourceCatalogs, }); }, ); const [toolSelectionExplicit, setToolSelectionExplicit] = useState(false); // Whether the person chose the composer's model policy. False follows the // server-resolved new-chat default; undefined means an older server that // does not report the marker, so none is sent back. const [modelProvided, setModelProvided] = useState(undefined); const [connectorCustomizing, setConnectorCustomizing] = useState(false); const [connectorExclusions, setConnectorExclusions] = useState([]); const followWorkspaceConnectors = () => { connectionAccounts.resetEmptyChoices(); setConnectorCustomizing(false); setToolSelectionExplicit(false); setConnectorExclusions([]); context.setSelectedCapabilityToolIds( defaultConnectorSelection(context.workspaceDefaultToolIds, []), ); }; const changeConnectorSelection = (selection: SessionToolSelection) => { // Choosing accounts is also an explicit connector customization. Apply it // in this event, rather than waiting for the header switch to re-render. if (!connectorCustomizing) setConnectorCustomizing(true); if ( !toolSelectionExplicit && addsConnectorOutsideDefaults( context.selectedCapabilityToolIds, selection.mcpServerIds, context.workspaceDefaultToolIds, ) ) { setToolSelectionExplicit(true); } else if (!toolSelectionExplicit) { setConnectorExclusions((current) => changedConnectorExclusions( current, context.selectedCapabilityToolIds, selection.mcpServerIds, ), ); } context.setSelectedCapabilityToolIds(selection.mcpServerIds); }; const [submitting, setSubmitting] = useState(false); // The new chat's usage limit: local to this composer (never the synced // draft or a remembered default), and only offered when the server can // enforce it. const usageLimitCapability = sessionUsageLimitCapability(context.clientConfig); const [usageLimitDraft, setUsageLimitDraft] = useState(NO_SESSION_USAGE_LIMIT); const [usageLimitSendBlocked, setUsageLimitSendBlocked] = useState(false); // The API refused to start this chat because no credit pays for its model. const [createCreditRefusal, setCreateCreditRefusal] = useState<{ workspaceId: string; model: string; message: string; } | null>(null); const [createdSessionAuthority, setCreatedSessionAuthority] = useState(null); const composerRegionRef = useRef(null); // 0 = no explicit request (mount uses ConsoleComposer autoFocus). >0 = same-route // new-session / shortcut asked us to put the caret back in the create composer. const [createComposerFocusGen, setCreateComposerFocusGen] = useState(0); const selectProject = useLatestCallback((channelId: string | null, explicit = true) => { const previousChannelId = selectedChannelIdRef.current; if (explicit) setProjectProvenancePresent(true); setSelectedProjectChannelId(channelId); setDraft((current) => { const selection = newSessionProjectSelection( selectionHistory, channelId, { channelId: previousChannelId, compute: current.compute }, defaultSandboxBackend, ); return selection.compute === current.compute ? current : { ...current, compute: selection.compute }; }); }); const setExplicitComputeDraft = useLatestCallback((nextDraft: SessionDraft) => { setProjectProvenancePresent(true); setDraft( nextDraft.compute.kind === "sandbox" && !nextDraft.compute.backend && defaultSandboxBackend !== "selfhosted" ? { ...nextDraft, compute: { ...nextDraft.compute, backend: defaultSandboxBackend ?? "", }, } : nextDraft, ); }); useEffect(() => { resetSessionView(); }, [resetSessionView, workspaceId]); // Folder-launch links preselect their exact destination, including Default, // while the ordinary New session entry starts in Recents. Commit this intent // before pending draft continuations can run; keeping it in an effect also // preserves render purity and the useEffect server fallback preserves SSR. useCommitSynchronousEffect(() => { const previousLaunchChannelId = previousLaunchChannelIdRef.current; previousLaunchChannelIdRef.current = launchChannelId; launchProjectIntentRef.current = nextNewSessionProjectLaunchIntent( launchProjectIntentRef.current, previousLaunchChannelId, launchChannelId, ); const channelId = resolveAmbientNewSessionProjectChannelId({ launchChannelId, previousLaunchChannelId, recentChannelId, remoteDraftHydrated: remoteDraftHydratedRef.current, }); if (channelId === undefined) return; if (launchChannelId === undefined && previousLaunchChannelId !== undefined) { setProjectProvenancePresent(false); } selectProject(channelId, launchChannelId !== undefined); }, [launchChannelId, recentChannelId, selectProject]); useEffect(() => { if ( selectedChannelId !== null && !channelsQuery.loading && !channelsQuery.channels.some((channel) => channel.id === selectedChannelId) ) { selectProject(null, false); } }, [channelsQuery.channels, channelsQuery.loading, selectedChannelId, selectProject]); const createProject = useCallback(async () => { const name = projectNameDraft.trim(); if (!name) return; const project = await channelsQuery.create({ name }); if (!project) { toast.error("Couldn't create the project. The name may already be in use."); return; } selectProject(project.id); setProjectDialogOpen(false); setProjectNameDraft(""); }, [channelsQuery, projectNameDraft, selectProject]); useEffect(() => { const onRequest = (event: Event) => { const requestedChannelId = (event as CustomEvent).detail ?.channelId; launchProjectIntentRef.current = nextFocusedNewSessionProjectLaunchIntent( launchProjectIntentRef.current, requestedChannelId, ); if (requestedChannelId !== undefined) { selectProject(requestedChannelId); } else if (remoteDraftHydratedRef.current) { setProjectProvenancePresent(false); selectProject(recentChannelId, false); } setCreateComposerFocusGen((current) => current + 1); }; uiEvents.addEventListener(FOCUS_CREATE_COMPOSER_EVENT, onRequest); return () => uiEvents.removeEventListener(FOCUS_CREATE_COMPOSER_EVENT, onRequest); }, [recentChannelId, selectProject, uiEvents]); const computeReady = isSessionDraftComputeReady(draft) && (draft.compute.kind !== "machine" || (!fleet.loading && selectedMachine !== null && isMachineComputeSelectable(selectedMachine.state))); const persistedToolPolicy = useMemo( () => fitToolPolicyToAgentCapabilities( newSessionDraftToolPolicy({ selectedMcpServerIds: context.selectedCapabilityToolIds, workspaceDefaultMcpServerIds: context.workspaceDefaultToolIds, catalogReady: context.workspaceMcpCatalogReady, customizing: connectorCustomizing, explicit: toolSelectionExplicit, ...(!toolSelectionExplicit ? { excludedMcpServerIds: connectorExclusions } : {}), }), draft.agentCapabilities, ), [ context.selectedCapabilityToolIds, context.workspaceMcpCatalogReady, context.workspaceDefaultToolIds, toolSelectionExplicit, connectorCustomizing, connectorExclusions, draft.agentCapabilities, ], ); const persistedValue = useMemo( () => ({ text: message, resources: [ ...(draft.compute.kind === "machine" ? [] : context.currentResources), ...attachments.readyResources, ], tools: persistedToolPolicy.tools, toolsProvided: persistedToolPolicy.toolsProvided, model: context.model, reasoningEffort: context.reasoningEffort, latencyMode: context.latencyMode, ...(modelProvided !== undefined ? { modelProvided } : {}), ...(projectProvenancePresent ? { selectedProjectChannelId: selectedChannelId } : {}), options: { ...newSessionDraftOptionsFromSessionDraft( draft, defaultFirstPartyMcpTools, createVisibility, ), ...(persistedToolPolicy.excludedMcpServerIds !== undefined ? { excludedMcpServerIds: persistedToolPolicy.excludedMcpServerIds } : {}), }, }), [ attachments.readyResources, context.model, context.latencyMode, context.reasoningEffort, context.currentResources, draft, defaultFirstPartyMcpTools, message, modelProvided, createVisibility, persistedToolPolicy, projectProvenancePresent, selectedChannelId, ], ); useEffect(() => { if ( context.selectedPersonalGitHubRepoIds.size > 0 && !context.personalGitHubAuthority && context.personalGitHubCatalogReady ) { void context.ensurePersonalGitHubAuthority(workspaceId); } }, [ context, context.ensurePersonalGitHubAuthority, context.personalGitHubAuthority, context.personalGitHubCatalogReady, context.selectedPersonalGitHubRepoIds.size, workspaceId, ]); const hydrateResources = useLatestCallback((resources: NewSessionDraftEditable["resources"]) => rehydrateRepositoryResources(resources, context.githubRepos, { catalogReady: context.githubCatalogReady, personalRepositories: context.personalGitHubRepositories, personalCatalogReady: context.personalGitHubCatalogReady, }), ); const setModel = context.setModel; const setReasoningEffort = context.setReasoningEffort; const setLatencyMode = context.setLatencyMode; const setSelectedCapabilityToolIds = context.setSelectedCapabilityToolIds; const setManualRepos = context.setManualRepos; const setSelectedRepoIds = context.setSelectedRepoIds; const setSelectedRepoRefs = context.setSelectedRepoRefs; const setSelectedPersonalGitHubRepoIds = context.setSelectedPersonalGitHubRepoIds; const setSelectedPersonalGitHubRepoRefs = context.setSelectedPersonalGitHubRepoRefs; const githubRepos = context.githubRepos; const workspaceDefaultToolIdsForHydration = context.workspaceDefaultToolIds; const applyRemoteDraft = useCallback( (remote: NewSessionDraftEditable, history: NewSessionSelectionHistory) => { setMessage(remote.text); const restored = sessionDraftFromNewSessionDraftOptions( remote.options, defaultFirstPartyMcpTools, defaultSandboxBackend, ); const projectSelection = resolveHydratedNewSessionProjectSelection({ launchIntent: launchProjectIntentRef.current, remote, history, restoredCompute: restored.compute, defaultSandboxBackend, }); // Fence the ambient Recents effect before installing history. That state // update changes recentChannelId, but must not replace this hydrated // explicit/persisted selection (or the legacy fallback resolved above). remoteDraftHydratedRef.current = true; setSelectionHistory(history); setProjectProvenancePresent( hydratedNewSessionProjectProvenancePresent(launchProjectIntentRef.current, remote), ); setSelectedProjectChannelId(projectSelection.channelId); const compute = initialWorkspaceMachineCompute({ settings: workspace?.settings, options: remote.options, fallback: projectSelection.compute, backend: defaultSandboxBackend, }); setDraft({ ...restored, compute }); setModel(remote.model); setReasoningEffort(remote.reasoningEffort); setLatencyMode(remote.latencyMode); setModelProvided(remote.modelProvided); const customize = newSessionConnectorCustomizeState({ toolsProvided: remote.toolsProvided, tools: remote.tools, excludedMcpServerIds: remote.options.excludedMcpServerIds, }); setConnectorCustomizing(customize.customizing); setToolSelectionExplicit(customize.explicit); setConnectorExclusions(remote.options.excludedMcpServerIds ?? []); const selected = new Set( customize.explicit ? remote.tools.map((tool) => tool.id) : defaultConnectorSelection( workspaceDefaultToolIdsForHydration, customize.customizing ? (remote.options.excludedMcpServerIds ?? []) : [], ), ); setSelectedCapabilityToolIds(selectableSessionMcpServerIds(selected)); const repositorySelection = repositorySelectionFromResources(remote.resources, githubRepos); setManualRepos(repositorySelection.manualRepos); setSelectedRepoIds(repositorySelection.selectedRepoIds); setSelectedRepoRefs(repositorySelection.selectedRepoRefs); setSelectedPersonalGitHubRepoIds(repositorySelection.selectedPersonalRepoIds); setSelectedPersonalGitHubRepoRefs(repositorySelection.selectedPersonalRepoRefs); }, [ setManualRepos, setModel, setLatencyMode, setReasoningEffort, setSelectedCapabilityToolIds, setSelectedRepoIds, setSelectedRepoRefs, setSelectedPersonalGitHubRepoIds, setSelectedPersonalGitHubRepoRefs, githubRepos, defaultFirstPartyMcpTools, defaultSandboxBackend, setSelectedProjectChannelId, workspaceDefaultToolIdsForHydration, workspace?.settings, ], ); const newSessionDraft = useNewSessionDraft({ workspaceId, client: context.client, value: persistedValue, onApplyRemote: applyRemoteDraft, restoreReadyFiles: attachments.restoreReadyFiles, hydrateResources, suspendAutosave: submitting, // Establish the passive baseline only after effective visibility settles. // Otherwise a late Personal-workspace capability response turns hydration // into an autosave, racing navigation and sibling drafts without a user edit. // Failed capability reads settle to the existing unavailable fallback too. // GitHub remains optional and must not keep the composer unsendable. resourceHydrationReady: context.workspaceMcpCatalogReady && tenancyCapabilities !== null, }); useEffect(() => { if (newSessionDraft.loading || !context.workspaceMcpCatalogReady || toolSelectionExplicit) return; const next = defaultConnectorSelection( context.workspaceDefaultToolIds, connectorCustomizing ? connectorExclusions : [], ); setSelectedCapabilityToolIds((current) => current.size === next.size && [...next].every((id) => current.has(id)) ? current : next, ); }, [ newSessionDraft.loading, context.workspaceMcpCatalogReady, context.workspaceDefaultToolIds, setSelectedCapabilityToolIds, connectorCustomizing, connectorExclusions, toolSelectionExplicit, ]); const busy = context.busy || submitting; const privateCreateUnavailable = (personalWorkspace && tenancyCapabilities === null) || (!personalWorkspace && draft.visibility === "private" && tenancyCapabilities?.canCreatePrivate !== true); const selectedPolicyRow = findPickerRow(modelCatalog.rows, context.model); // A refused start names its model until the next start or a dismissal; before // any Send, an uncovered model with free credit left for others does the same. const refusedCreate = createCreditRefusal?.workspaceId === workspaceId ? createCreditRefusal : null; const creditCoverageOptions = { selectedModel: context.model, preferredModel: modelCatalog.defaultSelection?.model, }; const creditGap = refusedCreate ? creditRefusalGap(modelCatalog.rows, refusedCreate.model, creditCoverageOptions) : freeCreditGap(modelCatalog.rows, context.model, creditCoverageOptions); const noteCreditRefusal = (error: Error, request: { model?: string | undefined }): boolean => { if (!isPaymentRequiredError(error)) return false; setCreateCreditRefusal({ workspaceId, model: request.model ?? context.model, message: error.message, }); // Re-read funding so the notice and picker name what the credit covers now. void modelCatalog.refresh(); return true; }; const chooseCoveredModel = (id: string) => { const row = findPickerRow(modelCatalog.rows, id); const choice = row ? coveredModelChoice(row, context.reasoningEffort, context.latencyMode) : null; if (!choice) return; setModelProvided(true); context.setModel(choice.model); context.setReasoningEffort(choice.effort); if (choice.latencyMode !== null) context.setLatencyMode(choice.latencyMode); }; const newSessionPolicyValid = Boolean( selectedPolicyRow?.selectable && effortOptionsForModel(selectedPolicyRow.catalog).includes(context.reasoningEffort) && (context.latencyMode === "standard" || runnableLatencyModesForModel(selectedPolicyRow.catalog).includes(context.latencyMode)), ); const noRunnableModel = !modelCatalog.loading && modelCatalog.rows.length > 0 && !modelCatalog.rows.some((row) => row.selectable); const newSessionPolicyError = !modelCatalog.loading && !newSessionPolicyValid && !noRunnableModel && selectedPolicyRow?.selectable ? "Choose a supported model, reasoning level, and speed." : null; useEffect(() => { if (modelCatalog.loading || newSessionDraft.loading) return; if (findPickerRow(modelCatalog.rows, context.model)?.selectable) return; // The server-resolved default comes first (saved workspace default, then a // connected subscription, then credits, then the deployment default); the // client ranking only covers a default that is itself unavailable. const resolvedDefault = modelCatalog.defaultSelection; const next = composerFallbackModel({ models: modelCatalog.models, rows: modelCatalog.rows, defaultSelection: resolvedDefault, }); if (!next || next.id === context.model) return; setModel(next.id); setReasoningEffort(next.effort); // An automatic replacement is not the person's choice. setModelProvided((current) => (current === undefined ? current : false)); }, [ context.model, modelCatalog.defaultSelection, modelCatalog.loading, modelCatalog.models, modelCatalog.rows, newSessionDraft.loading, setModel, setReasoningEffort, ]); const codexConnected = modelCatalog.models.some( (candidate) => candidate.provider === "codex-subscription" && candidate.credentialReadiness.status === "ready", ); const startBlocker = modelCatalog.loading || newSessionDraft.loading ? null : modelCatalog.error ? "model_catalog_unavailable" : !newSessionPolicyValid ? !modelCatalog.rows.some((row) => row.selectable) ? "no_model_connected" : selectedPolicyRow && selectedPolicyRow.billingClass !== "opengeni_credits" && selectedPolicyRow.catalog.credentialReadiness.status !== "ready" ? "selected_model_not_connected" : "model_policy_unavailable" : privateCreateUnavailable ? "private_session_unavailable" : attachments.hasUnresolved ? "attachments_pending" : !computeReady ? "compute_unavailable" : null; useEffect(() => { const record = () => { if (startBlocker) captureAnalyticsEvent("session_start_blocker_viewed", { workspace_id: workspaceId, reason: startBlocker, }); }; record(); window.addEventListener(ANALYTICS_COLLECTION_ENABLED_EVENT, record); return () => window.removeEventListener(ANALYTICS_COLLECTION_ENABLED_EVENT, record); }, [startBlocker, workspaceId]); // Shared with the bar start control and the mobile “+ → Voice model” panel. const voiceSelection = useRealtimeModelSelection({ client: context.client, workspaceId, codexConnected, }); useEffect(() => { if (createComposerFocusGen === 0 || newSessionDraft.loading) return; const textarea = composerRegionRef.current?.querySelector("textarea"); if (!textarea || textarea.disabled) return; textarea.focus(); }, [createComposerFocusGen, newSessionDraft.loading]); const submitNewSession = useLatestCallback( async ( realtimeModel: SessionRealtimeModel | null, policy?: Pick, ): Promise => { if (startBlocker) captureAnalyticsEvent("session_start_blocked", { workspace_id: workspaceId, reason: startBlocker, }); const hasTypedText = message.trim().length > 0; const text = hasTypedText ? message : attachments.readyResources.length > 0 ? FILE_ONLY_MESSAGE_TEXT(t) : ""; if ( busy || !context.workspaceMcpCatalogReady || newSessionDraft.loading || !newSessionPolicyValid || privateCreateUnavailable || personalResourceCatalogRefreshPending || (!realtimeModel && createdSessionAuthority === null && (connectionAccounts.loading || connectionAccounts.error !== null || connectionAccounts.requiresAccountChoice)) || (createdSessionAuthority === null && !fixedResourceSelection.selectionResolved) ) return false; if (createdSessionAuthority === null) { const unavailable = unavailableSessionMcpServerIds( context.selectedCapabilityToolIds, context.toolMcpServers, context.workspaceMcpCatalogLoadedSuccessfully, ); if (unavailable.length > 0) { const removed = new Set(unavailable); context.setSelectedCapabilityToolIds( (current) => new Set([...current].filter((id) => !removed.has(id))), ); setConnectorExclusions((current) => [...new Set([...current, ...unavailable])]); toast.error("Some selected tools are no longer available", { description: "Removed them from this draft. Your message is still here; review the tools and send again.", }); return false; } } if (realtimeModel && personalMachineSelected) { toast.error("Voice can't start on a personal Connected Machine", { description: "Start the session with a message first so Opengeni can attach the machine to an accepted turn.", }); return false; } if ( createdSessionAuthority === null && ((!text && !realtimeModel) || attachments.hasUnresolved || !computeReady) ) { return false; } // A chosen limit must be a valid amount; an older server never gets one. const usageLimit = usageLimitCapability === "enabled" ? parseSessionUsageLimitDraft(usageLimitDraft) : null; if (createdSessionAuthority === null && usageLimit && !usageLimit.ok) { setUsageLimitSendBlocked(true); toast.error("Check the usage limit", { description: `${usageLimit.error} Change it under + > Usage limit.`, }); return false; } const usageLimitMicros = usageLimit?.ok ? usageLimit.limitMicros : null; const model = policy?.model ?? persistedValue.model; const reasoningEffort = policy?.effort ?? persistedValue.reasoningEffort; const latencyMode = policy?.latency ?? persistedValue.latencyMode; // One Send always refers to one visible snapshot, even if a sibling edit // forces a draft save or create retry while the user continues typing. const visibleSignature = stableJson(persistedValue); const submittedSnapshot = newSessionCreateSnapshot( persistedValue, realtimeModel ? persistedValue.text : text, { model, reasoningEffort, latencyMode, }, ); const preserveNewerLocalDraft = async () => { if (!newSessionDraft.isCurrentSignature(visibleSignature)) { // A definitive create failure must not leave text typed during the // attempt unsaved after a retry persisted the clicked snapshot. await (realtimeModel ? newSessionDraft.flush() : newSessionDraft.flushForSend()); } }; setSubmitting(true); try { return await runNewSessionRouteSubmission({ authority: createdSessionAuthority, onAuthorityChange: setCreatedSessionAuthority, create: async () => { // Voice launch is realtime-only: never turn composer text/files into // an initial message. Persist the draft so a pending autosave is not // lost on navigate, but do not consume it — text stays for later. if (realtimeModel) { for (let attempt = 0; attempt < 3; attempt += 1) { // Voice launch does not consume the draft. Save local edits if // possible, but never rebase an old voice draft over a sibling's // newer unsent message merely to start a realtime session. const flushed = await newSessionDraft.flush(); if (!flushed) { reportDraftSaveFailure(newSessionDraft); return null; } const submission = submissionFromSessionDraft( draft, defaultFirstPartyMcpTools, personalResourceAttachment.intent, ); let draftConflict = false; let outcomeUnknown = false; const created = await context.startSession( workspaceId, { text: "", resources: [], tools: submittedSnapshot.tools, model, reasoningEffort, latencyMode, ...submission.extras, }, { targetSandboxId: submission.options.targetSandboxId, workingDir: submission.options.workingDir, channelId: selectedChannelId, omitWorkspaceResources: submission.omitWorkspaceResources, installedSkillIds: launch.skillCapabilityId ? [launch.skillCapabilityId] : undefined, startMode: "realtime", expectedNewSessionDraftRevision: flushed.revision, newSessionDraftToolPolicy: persistedToolPolicy, agentLearning: draft.agentLearning, usageLimitMicros, visibility: newSessionCreateVisibility( personalWorkspace, submission.options.visibility ?? "workspace", tenancyCapabilities?.canCreatePrivate === true, ), onFailure: ({ error, request, outcomeUnknown: uncertain }) => { draftConflict = newSessionDraft.captureConflict(error); outcomeUnknown = uncertain; recoverPersonalResourceAttachment(error, request); connectionAccounts.onSelectionError(error); // A brief outage shows the updating notice, not a toast. return ( draftConflict || newSessionDraft.reportUnavailable(error) || noteCreditRefusal(error, request) ); }, }, ); if (!created) { if (draftConflict) continue; if (!outcomeUnknown) await preserveNewerLocalDraft(); return null; } setUsageLimitDraft(NO_SESSION_USAGE_LIMIT); setCreateCreditRefusal(null); setUsageLimitSendBlocked(false); return { sessionId: created.id, settleDraft: async () => true, }; } await preserveNewerLocalDraft(); newSessionDraft.clearError(); toast.error("Couldn't start voice", { description: DRAFT_CHANGED_DURING_SEND_TEXT, }); return null; } const submittedResources = submittedSnapshot.resources; for (let attempt = 0; attempt < 3; attempt += 1) { const flushed = await newSessionDraft.flushForSend(submittedSnapshot); if (!flushed) { reportDraftSaveFailure(newSessionDraft); return null; } const submission = submissionFromSessionDraft( draft, defaultFirstPartyMcpTools, personalResourceAttachment.intent, ); let draftConflict = false; let outcomeUnknown = false; const created = await context.startSession( workspaceId, { text, resources: submittedResources, tools: submittedSnapshot.tools, model, reasoningEffort, latencyMode, ...submission.extras, connectionAccounts: connectionAccounts.selections, }, { targetSandboxId: submission.options.targetSandboxId, workingDir: submission.options.workingDir, channelId: selectedChannelId, omitWorkspaceResources: submission.omitWorkspaceResources, installedSkillIds: launch.skillCapabilityId ? [launch.skillCapabilityId] : undefined, expectedNewSessionDraftRevision: flushed.revision, newSessionDraftToolPolicy: persistedToolPolicy, agentLearning: draft.agentLearning, usageLimitMicros, visibility: newSessionCreateVisibility( personalWorkspace, submission.options.visibility ?? "workspace", tenancyCapabilities?.canCreatePrivate === true, ), onFailure: ({ error, request, outcomeUnknown: uncertain }) => { draftConflict = newSessionDraft.captureConflict(error); outcomeUnknown = uncertain; recoverPersonalResourceAttachment(error, request); connectionAccounts.onSelectionError(error); // A brief outage shows the updating notice, not a toast. // An unconfirmed create keeps its idempotency key, so the // person's next Send cannot start a second session. return ( draftConflict || newSessionDraft.reportUnavailable(error) || noteCreditRefusal(error, request) ); }, }, ); if (!created) { if (draftConflict) continue; if (!outcomeUnknown) await preserveNewerLocalDraft(); return null; } // The limit belongs to the chat just created; the next starts with none. setUsageLimitDraft(NO_SESSION_USAGE_LIMIT); setCreateCreditRefusal(null); setUsageLimitSendBlocked(false); return { sessionId: created.id, settleDraft: async () => { const acknowledged = await newSessionDraft.acknowledgeConsumed( flushed, visibleSignature, ); if ( acknowledged?.kind === "consumed" && newSessionDraft.isCurrentSignature(visibleSignature) ) { setMessage(""); visibilityChosenRef.current = false; setDraft({ ...emptySessionDraft(defaultFirstPartyMcpTools, defaultSandboxBackend), visibility: tenancyCapabilities?.canCreatePrivate === true && tenancyCapabilities.defaultVisibility === "private" ? "private" : "workspace", }); attachments.removeReadyFiles( submittedResources.flatMap((resource) => resource.kind === "file" ? [resource.fileId] : [], ), ); } else if ( acknowledged?.kind !== "preserved" || !newSessionDraft.isCurrentSignature(acknowledged.flushed.signature) ) { // The message was already accepted. If there is no newer // local edit, leave a sibling's later draft untouched. if (!acknowledged && newSessionDraft.isCurrentSignature(visibleSignature)) { return true; } const preserved = await newSessionDraft.flushForSend(); if (!preserved || !newSessionDraft.isCurrentSignature(preserved.signature)) { return false; } } return true; }, }; } await preserveNewerLocalDraft(); // Each attempt saved the draft before the create refused it, so the // draft is not unsaved: clear that notice and resume autosave. newSessionDraft.clearError(); toast.error("Couldn't send", { description: DRAFT_CHANGED_DURING_SEND_TEXT, }); return null; }, navigate: async (sessionId) => { // Voice connects while the session page loads; the page adopts // the running call (`?realtime=` still selects its model there). const sessionPath = `/workspaces/${workspaceId}/sessions/${sessionId}`; const abandonVoice = realtimeModel ? prestartSessionRealtime({ client: api.voiceRealtimeClient( context.client, () => new URL(sessionPath, page.origin()).href, ), workspaceId, sessionId, model: realtimeModel, language: voiceSelection.languageFor(realtimeModel), voiceSettings: voiceSelection.voiceSettingsFor(realtimeModel), }) : null; try { await navigate({ to: "/workspaces/$workspaceId/sessions/$sessionId", params: { workspaceId, sessionId }, search: realtimeModel ? { realtime: realtimeModel } : {}, }); } catch (error) { abandonVoice?.(); throw error; } }, }); } finally { setSubmitting(false); } }, ); // URL launch: ?model=&effort=&latency= prefill the composer; +?realtime= also // creates a realtime-first session and autostarts voice on the session page. // Wait for the durable new-session draft so remote hydrate cannot stomp the // URL policy after we apply it. const launchModel = launch.model; const launchEffort = launch.effort; const launchLatency = launch.latency; const launchRealtime = launch.realtime; const launchFollowDefault = launch.followDefault === true; const launchSkillCapabilityId = launch.skillCapabilityId; const launchKey = composerLaunchSearchKey(launch); const handledLaunchKeyRef = useRef(null); useEffect(() => { if (!launchKey || handledLaunchKeyRef.current === launchKey) return; if (newSessionDraft.loading) return; if (launchModel) setModel(launchModel); if (launchEffort) setReasoningEffort(launchEffort); if (launchLatency) setLatencyMode(launchLatency); // A checkout return carries the credits default and keeps following the // default; any other launch policy is the person's choice. setModelProvided((current) => modelProvidedAfterLaunch( { ...(launchModel ? { model: launchModel } : {}), ...(launchEffort ? { effort: launchEffort } : {}), ...(launchLatency ? { latency: launchLatency } : {}), ...(launchFollowDefault ? { followDefault: true } : {}), }, current, ), ); if (!launchRealtime) { handledLaunchKeyRef.current = launchKey; void navigate({ to: "/workspaces/$workspaceId/sessions", params: { workspaceId }, search: { ...(launch.channelId ? { channelId: launch.channelId } : {}), ...(launchSkillCapabilityId ? { skillCapabilityId: launchSkillCapabilityId } : {}), }, replace: true, }); return; } if ( busy || !computeReady || !newSessionPolicyValid || !context.workspaceMcpCatalogReady || attachments.hasUnresolved ) { return; } handledLaunchKeyRef.current = launchKey; void submitNewSession(launchRealtime, { model: launchModel, effort: launchEffort, latency: launchLatency, }).then((ok) => { if (!ok) handledLaunchKeyRef.current = null; }); }, [ attachments.hasUnresolved, busy, computeReady, context.workspaceMcpCatalogReady, launchEffort, launchFollowDefault, launchLatency, launchModel, launchRealtime, launchSkillCapabilityId, launchKey, launch.channelId, navigate, newSessionDraft.loading, newSessionPolicyValid, setLatencyMode, setModel, setReasoningEffort, submitNewSession, workspaceId, ]); // The session does not exist yet, so this surface cannot use `useComposer` // (that hook sends to a session). It still renders the package ChatComposer // by implementing the same `ComposerState` contract over session creation. const createComposer: ComposerState = { value: message, setValue: setMessage, hasDraftContent: () => message.length > 0 || attachments.attachments.length > 0, sending: busy, // Mirrors useComposer's gate: a ready attachment with no typed draft is a // sendable file-only message (the API requires non-empty text, so send() // substitutes FILE_ONLY_MESSAGE_TEXT). canSend: (createdSessionAuthority !== null || message.trim().length > 0 || attachments.readyResources.length > 0) && !busy && !newSessionDraft.loading && newSessionPolicyValid && !personalResourceCatalogRefreshPending && (createdSessionAuthority !== null || (!connectionAccounts.loading && connectionAccounts.error === null && !connectionAccounts.requiresAccountChoice)) && (createdSessionAuthority !== null || fixedResourceSelection.selectionResolved) && (createdSessionAuthority !== null || (!attachments.hasUnresolved && computeReady)), pause: async () => {}, pausing: false, resume: async () => {}, resumeScope: async () => {}, resuming: false, draft: null, draftRevision: newSessionDraft.revision, draftLoading: newSessionDraft.loading, draftSaving: newSessionDraft.saving, // A stale autosaved draft must not block or distract from explicit Send. draftConflict: null, policy: { model: context.model, reasoningEffort: context.reasoningEffort, latencyMode: context.latencyMode, }, setModel: (model) => { setModelProvided(true); context.setModel(model); }, setReasoningEffort: (effort) => { setModelProvided(true); context.setReasoningEffort(effort); }, setLatencyMode: (latencyMode) => { setModelProvided(true); context.setLatencyMode(latencyMode); }, draftPersistence: "disabled", applyDraft: () => {}, reloadDraft: newSessionDraft.reload, resolveDraftConflict: newSessionDraft.resolveConflict, restoredResources: [], removeRestoredResource: () => {}, // A brief outage is explained once by the updating notice below the // composer, never as a red error with a request reference. error: newSessionDraft.unavailable || newSessionDraft.conflict ? null : newSessionDraft.error, clearError: newSessionDraft.clearError, send: async () => await submitNewSession(null), steer: async () => { const text = message.trim() || (attachments.readyResources.length > 0 ? FILE_ONLY_MESSAGE_TEXT(t) : ""); if (!text || busy || attachments.hasUnresolved || !computeReady || !newSessionPolicyValid) { return false; } return await createComposer.send(); }, }; useBrowserAccountBridgeBlocker(`new-session-composer:${workspaceId}`, () => { if (attachments.hasUnresolved) { return { id: "ignored", label: "A file upload is not settled", detail: "Wait for the upload or remove it before changing accounts.", }; } if (busy || submitting || newSessionDraft.saving) { return { id: "ignored", label: "A new-session mutation is still running", detail: "Wait for the current save or session creation to finish.", }; } return createComposer.hasDraftContent() ? { id: "ignored", label: "The new-session composer has an unsent draft", detail: "Continuing clears the account-bound draft.", } : null; }); // A transient outage is covered by the updating notice instead. const accountsFailure = connectionAccounts.error !== null && !connectionAccounts.unavailable; return createElement( LightboxProvider, null, // The canvas parent is overflow-hidden, so this route owns its scrolling — // without it the page clips (recent sessions were unreachable below the fold).
{githubAppConnect.element}

What should the agent do?

{context.clientConfig.billingMode === "stripe" && workspace?.accountId && hasAccountPermission(context.accessContext, workspace.accountId, "billing:manage") ? ( <> ) : null}
{launchSkillCapabilityId ? (
This installed Skill will be frozen onto this session only. Other workspace sessions will not receive it.
) : null} {creditGap ? (
setCreateCreditRefusal(null) } : {})} />
) : modelUsesCredits(selectedPolicyRow?.catalog) && hasAccountPermission( context.accessContext, workspace?.accountId ?? "", "billing:read", ) ? (
) : null}
void connectionAccounts.refresh(), disabled: busy || newSessionDraft.loading, }, }} menuSide="bottom" agentCapabilities={{ ...composerAgentCapabilities, disabled: busy || newSessionDraft.loading, }} draftChatSettings={{ workspaceId, scope: createVisibility === "private" || personalWorkspace ? "personal" : "workspace", value: draft.agentLearning ?? {}, onChange: (agentLearning) => setDraft((current) => ({ ...current, agentLearning })), }} workspaceId={workspaceId} disabled={busy || newSessionDraft.loading} fileUploadsEnabled={context.clientConfig.fileUploads.enabled === true} servers={context.toolMcpServers} firstPartyTools={firstPartyToolOptions} selection={{ mcpServerIds: context.selectedCapabilityToolIds, firstPartyToolIds: draft.firstPartyMcpTools, }} toolsDisabled={busy || newSessionDraft.loading} connectorCustomizing={connectorCustomizing} onConnectorCustomizingChange={(next) => { if (next) setConnectorCustomizing(true); else followWorkspaceConnectors(); }} onToolSelectionChange={(selection) => { changeConnectorSelection(selection); }} {...(hasRunsOnChoices(runsOnChoices) ? { runsOn: { summary: runsOnSummary(runsOnChoices), disabled: busy || newSessionDraft.loading, panel: ( void fleet.refresh()} /> ), }, } : {})} {...(hasVisibilityChoice({ personalWorkspace, canCreatePrivate: tenancyCapabilities?.canCreatePrivate === true, }) ? { visibility: { summary: visibilitySummary(draft.visibility), disabled: busy || newSessionDraft.loading, panel: ( { visibilityChosenRef.current = true; setDraft((current) => ({ ...current, visibility, })); }} /> ), }, } : {})} {...(usageLimitCapability === "enabled" ? { usageLimit: { summary: sessionUsageLimitDraftSummary(usageLimitDraft), disabled: busy || newSessionDraft.loading, panel: ( ), }, } : {})} {...(draft.compute.kind === "sandbox" ? { repositories: { selectedCount: context.selectedRepoIds.size + context.selectedPersonalGitHubRepoIds.size + context.manualRepos.filter((repo) => repo.url.trim().length > 0).length, disabled: busy || newSessionDraft.loading, panel: ( ), }, } : {})} {...(draftSecretsOn && canListSecrets ? { secrets: { summary: sessionSecretsSummary(draftVaultPolicy, draftVaultRows), disabled: busy || newSessionDraft.loading, panel: ( void draftVaults.refresh()} onPolicyChange={(policy) => setDraft((current) => { const { vaultPolicy: _previous, ...rest } = current; return policy.mode === "all" ? rest : { ...rest, vaultPolicy: policy }; }) } onManage={() => void navigate({ to: "/workspaces/$workspaceId/secrets", params: { workspaceId }, }) } /> ), }, } : {})} /> {composerAgentCapabilities.customized ? ( setCapabilitiesOpenRequest((current) => ({ panel: "capabilities", nonce: (current?.nonce ?? 0) + 1, })) } /> ) : null} } controls={
file.status !== "failed" && file.contentType.startsWith("image/"), )} modelCatalog={modelCatalog} policyError={newSessionPolicyError} disabled={busy || newSessionDraft.loading} workspaceId={workspaceId} onPolicyChosen={() => setModelProvided(true)} />
} actions={ <> await submitNewSession(model)} /> } header={ setProjectDialogOpen(true)} /> } /> {personalWorkspace ? : null} {newSessionDraft.conflict ? : null} {/* A deploy or restart makes Opengeni unreachable for a few seconds. Reads retry quietly first; if it lasts longer, this one calm line replaces every error, the message stays in the composer, and the hooks reconnect on their own so Send works again. */} {newSessionDraft.unavailable || connectionAccounts.unavailable ? (
{OPENGENI_UPDATING_NOTICE(t)}
) : null} {/* Accounts load quietly with the composer: only a problem shows here, never a loading line behind an open menu. */} {accountsFailure || connectionAccounts.accountChoiceMessage ? (
void connectionAccounts.refresh()} > Retry ) : undefined } > {accountsFailure ? connectionAccounts.accessDenied ? connectionAccounts.error : "Couldn't send your message. Try again." : connectionAccounts.accountChoiceMessage}
) : null} void refreshPersonalResourceCatalogs(), }} selectedChannelId={selectedChannelId} selectionHistory={selectionHistory} />
{starters ? ( { setMessage(prompt); composerRegionRef.current?.querySelector("textarea")?.focus({ preventScroll: true }); }} /> ) : null}
{ setProjectDialogOpen(open); if (!open) setProjectNameDraft(""); }} onSubmit={() => void createProject()} />
, ); } /** Send kept losing to a newer draft saved elsewhere (another tab or window). */ const DRAFT_CHANGED_DURING_SEND_TEXT = "Your message is saved, but this draft kept changing in another tab or window. Check it and send again."; /** A draft that didn't save: the message is kept, then what to do. */ function draftSaveFailureText(draft: { conflict: Error | null; error: Error | null }): string { if (draft.conflict || !draft.error) return "Your message is still here. Try again."; return `Your message is still here. ${userErrorText(draft.error)}`; } /** A failed pre-send draft save: a brief outage shows the updating notice instead of a toast. */ function reportDraftSaveFailure(draft: { conflict: Error | null; currentError: () => Error | null; }): void { const error = draft.currentError(); if (isTransientServiceFailure(error)) return; toast.error("Couldn't save the draft", { description: draftSaveFailureText({ conflict: draft.conflict, error }), }); } // ── Recent sessions — the quiet main-canvas browser the rail can't be (D4.2) ── // A calm section below the composer: the most recent sessions as compact rows // (status, title, provider mark + catalog model label, relative time). Reuses // the rail's session list + the workspace model catalog so labels/marks match // the model picker — never raw wire ids as the primary display. function RecentSessions({ workspaceId }: { workspaceId: string }) { const { workspaceSessionListEvent } = useOpenGeni(); const hintsAvailable = workspaceSessionListEvent?.workspaceId === workspaceId && workspaceSessionListEvent.available; const { sessions, pinned, refresh, error } = useWorkspaceSessions({ limit: 12, // Top-level conversations: sub-agents open from their parent and would // otherwise fill this short page in a workspace that runs many of them. parentSessionId: null, pollIntervalMs: hintsAvailable ? undefined : 30_000, }); useSessionListHintRefresh(refresh, error); const modelCatalog = useWorkspaceModelCatalog(workspaceId); // Pins are server-authoritative and sit above running and recency rows. const recent = useMemo(() => recentSessionsForHome(t, sessions, pinned, 6), [pinned, sessions]); if (recent.length === 0) { return null; } return (

Recent sessions

{/* flex-col, not grid: a grid auto track grows to a nowrap row's full min-content width, defeating truncate and overflowing the page. */}
    {recent.map((session) => ( ))}
); } function RecentSessionRow({ workspaceId, session, catalogRows, }: { workspaceId: string; session: Session; catalogRows: readonly PickerModelRow[]; }) { const title = sessionDisplayTitle(session); const model = recentSessionModelPresentation(session.model, catalogRows); const repo = sessionRepoLabel(session); const metaBits = [model.label, repo].filter(Boolean); const status = recentSessionStatus(session); return (
  • {title} {metaBits.length > 0 ? ( } /> {metaBits.join(" · ")} ) : null} {relativeTimeLabel(t, session.updatedAt)}
  • ); } // Setup selections sit above the prompt so the footer remains an action row. // Repo stays out of the compute band so that band only shows when rigs / // machines exist. Tools and repos live under “+” at every width. function SessionSetupStrip({ disabled, channels, selectedChannelId, onChannelChange, onCreateProject, }: { disabled: boolean; channels: Channel[]; selectedChannelId: string | null; onChannelChange: (channelId: string | null) => void; onCreateProject: () => void; }) { return (
    ); } /** Keep model policy adjacent to voice/send in the bottom action row. */ function SessionModelControl({ hasImageAttachments, modelCatalog, policyError, disabled, workspaceId, onPolicyChosen, }: { hasImageAttachments: boolean; modelCatalog: WorkspaceModelCatalogState; policyError: string | null; disabled: boolean; workspaceId: string; /** The person picked a model, reasoning level, or speed. */ onPolicyChosen: () => void; }) { const context = useAppContext(); return ( { if (open) void modelCatalog.refresh(); }} rows={modelCatalog.rows} model={context.model} effort={context.reasoningEffort} latencyMode={context.latencyMode} disabled={disabled} loading={modelCatalog.loading} error={modelCatalog.error ?? policyError} menuSide="bottom" connectModelsHref={`/workspaces/${encodeURIComponent(workspaceId)}/settings?section=models`} onModelChange={(model) => { onPolicyChosen(); context.setModel(model); }} onEffortChange={(effort) => { onPolicyChosen(); context.setReasoningEffort(effort); }} onLatencyModeChange={(latencyMode) => { onPolicyChosen(); context.setLatencyMode(latencyMode); }} /> ); } function SessionFolderPicker({ channels, selectedChannelId, disabled, onChange, onCreateProject, }: { channels: Channel[]; selectedChannelId: string | null; disabled: boolean; onChange: (channelId: string | null) => void; onCreateProject: () => void; }) { const selected = channels.find((channel) => channel.id === selectedChannelId) ?? null; const label = selected?.name ?? "Default"; return ( Save new session in onChange(null)}> Default {channels.map((channel) => ( onChange(channel.id)}> {channel.name} ))} New project ); } function workspaceRepositoryPickerProps( context: ReturnType, workspaceId: string, disabled: boolean, onConnectWorkspaceApp: () => void, page: PageHost, ): RepositoryContextPickerProps { return { setupMode: context.githubStatus?.setupMode ?? (context.clientConfig.productAccessMode === "managed" ? "platform" : "operator"), configured: context.githubStatus?.configured === true, status: context.githubStatus?.status ?? ("disabled" as const), installUrl: context.githubStatus?.installUrl ?? null, linkUrl: context.githubStatus?.linkUrl ?? null, installations: context.githubStatus?.installations ?? [], repositories: context.githubRepos, personalGitHubStatus: context.personalGitHubStatus, personalGitHubRepositories: context.personalGitHubRepositories, selectedPersonalGitHubRepoIds: context.selectedPersonalGitHubRepoIds, selectedPersonalGitHubRepoRefs: context.selectedPersonalGitHubRepoRefs, personalGitHubBusy: context.personalGitHubBusy, groups: context.repositoryGroups, selectedRepoIds: context.selectedRepoIds, selectedRepoRefs: context.selectedRepoRefs, selectedInstallationId: context.selectedInstallationId, manualRepos: context.manualRepos, manualOpen: context.manualReposOpen, githubAppOpen: context.githubAppOpen, org: context.githubOrg, pending: context.busy || disabled, repoBusy: context.repoBusy, githubAppBusy: context.githubAppBusy, onRefresh: async () => { await Promise.all([ context.refreshGitHub(workspaceId, undefined, { sync: true }), context.refreshPersonalGitHub(workspaceId), ]); }, onConnectPersonalGitHub: () => void context.connectPersonalGitHub(workspaceId), onTogglePersonalGitHubRepo: (repository) => void context.togglePersonalGitHubRepository(workspaceId, repository), onPersonalGitHubRefChange: (repositoryId: string, ref: string) => context.setSelectedPersonalGitHubRepoRefs((current) => ({ ...current, [repositoryId]: ref, })), onToggleRepo: context.toggleGitHubRepository, onRefChange: (repoId: number, ref: string) => context.setSelectedRepoRefs((current) => ({ ...current, [repoId]: ref })), onLoadGitHubBranches: async (repository) => { const acceptedTransition = context.captureWorkspaceInvocation(workspaceId); if (!acceptedTransition) throw new Error("The workspace changed; refresh and try again."); const { listGitHubRepositoryBranches } = await import("@opengeni/sdk/github-repositories"); const response = await listGitHubRepositoryBranches( context.client, workspaceId, repository.installationId, repository.id, { limit: 100 }, ); if (!context.ownsWorkspaceInvocation(workspaceId, acceptedTransition)) { throw new Error("The workspace changed; refresh and try again."); } return response.branches; }, onLoadPersonalGitHubBranches: async (repository) => { const acceptedTransition = context.captureWorkspaceInvocation(workspaceId); if (!acceptedTransition) throw new Error("The workspace changed; refresh and try again."); const connectionId = context.personalGitHubStatus?.connection?.id; if (!connectionId) throw new Error("Connect your GitHub identity to load branches."); const { listPersonalGitHubRepositoryBranches } = await import("@opengeni/sdk/github-repositories"); const response = await listPersonalGitHubRepositoryBranches( context.client, workspaceId, connectionId, repository.repositoryId, { limit: 100 }, ); if (!context.ownsWorkspaceInvocation(workspaceId, acceptedTransition)) { throw new Error("The workspace changed; refresh and try again."); } return response.branches; }, onManualOpenChange: context.setManualReposOpen, onManualAdd: context.addManualRepository, onManualUpdate: (id: number, patch: Partial) => context.setManualRepos((current) => current.map((repo) => (repo.id === id ? { ...repo, ...patch } : repo)), ), onManualRemove: (id: number) => context.setManualRepos((current) => current.filter((repo) => repo.id !== id)), onManualAttach: async (repository) => { const acceptedTransition = context.captureWorkspaceInvocation(workspaceId); if (!acceptedTransition) throw new Error("The workspace changed; try again."); const assertCurrent = () => { if (!context.ownsWorkspaceInvocation(workspaceId, acceptedTransition)) { throw new Error("The workspace changed; try again."); } }; assertCurrent(); return await attachManualRepository({ repository, workspaceRepositories: context.githubRepos, personalRepositories: context.personalGitHubRepositories, selectWorkspaceRepository: (matched, ref) => { assertCurrent(); context.setSelectedRepoIds((current) => { const next = context.selectedInstallationId !== null && context.selectedInstallationId !== matched.installationId ? new Set() : new Set(current); next.add(matched.id); return next; }); context.setSelectedRepoRefs((current) => ({ ...current, [matched.id]: ref, })); context.setSelectedPersonalGitHubRepoIds( (current) => new Set( [...current].filter( (id) => context.personalGitHubRepositories .find((candidate) => candidate.repositoryId === id) ?.fullName.toLowerCase() !== matched.fullName.toLowerCase(), ), ), ); }, selectPersonalRepository: async (matched, ref) => { if (!(await context.ensurePersonalGitHubAuthority(workspaceId))) { throw new Error("Your GitHub identity could not be authorized for this workspace."); } assertCurrent(); context.setSelectedRepoIds( (current) => new Set( [...current].filter( (id) => context.githubRepos .find((candidate) => candidate.id === id) ?.fullName.toLowerCase() !== matched.fullName.toLowerCase(), ), ), ); context.setSelectedPersonalGitHubRepoIds((current) => new Set(current).add(matched.repositoryId), ); context.setSelectedPersonalGitHubRepoRefs((current) => ({ ...current, [matched.repositoryId]: ref, })); }, verifyPublicGitHubRepository: async (request) => { const { verifyPublicGitHubRepositoryRef } = await import("@opengeni/sdk/github-repositories"); const verified = await verifyPublicGitHubRepositoryRef( context.client, workspaceId, request, ); assertCurrent(); return verified; }, attach: (attached) => { assertCurrent(); context.setManualRepos((current) => current.map((candidate) => (candidate.id === attached.id ? attached : candidate)), ); }, remove: (id) => { assertCurrent(); context.setManualRepos((current) => current.filter((candidate) => candidate.id !== id)); }, }); }, validationError: context.repositoryValidationError, onGitHubAppOpenChange: context.setGithubAppOpen, onOrgChange: context.setGithubOrg, onStartGitHubApp: () => void context.startGitHubAppManifestFlow(workspaceId), onConnectWorkspaceApp, onConfigureInstallation: (installationId: number) => openGitHubInstallationSettings(context.client, workspaceId, installationId, page.assign), onDisconnectInstallation: async (installationId: number) => { await context.disconnectGitHubInstallation(workspaceId, installationId); }, }; } function WorkspaceRepositoryMenuBody({ workspaceId, disabled, leading, catalogRefresh, onConnectWorkspaceApp, }: { workspaceId: string; disabled: boolean; leading?: ReactNode | undefined; catalogRefresh: ReturnType; onConnectWorkspaceApp: () => void; }) { const page = usePage(); const context = useAppContext(); return ( ); } // ── The promoted top-level compute target (the parent that gates the band) ──── type NewSessionPersonalResourceAccess = { names: string[]; visibility: "private" | "workspace"; }; type FixedResourceCatalogRecovery = { error: boolean; refreshing: boolean; onRetry: () => void; }; function ComputeTargetControl(props: { workspaceId: string; defaultSandboxBackend?: SandboxBackend | undefined; draft: SessionDraft; onChange: (draft: SessionDraft) => void; onComputeChange: (draft: SessionDraft) => void; disabled: boolean; personalResourceAccess: NewSessionPersonalResourceAccess; fleet: ReturnType; machines: MachineView[]; rigs: Rig[]; workspaceDefaultRigId: string | null; catalogRecovery: FixedResourceCatalogRecovery; selectedChannelId: string | null; selectionHistory: NewSessionSelectionHistory; }) { const { draft, onChange } = props; const { fleet, machines } = props; const selfhostedPrimary = props.defaultSandboxBackend === "selfhosted"; // Managed-primary deployments keep Connected Machine opt-in and hide the // chooser when the fleet is empty. Selfhosted-primary deployments always show // the required machine path, including its honest unavailable state. const machineAvailabilityKey = machines .map((machine) => `${machine.sandboxId}:${machine.state}`) .join("\u0000"); const selectedMachineSandboxId = draft.compute.kind === "machine" ? draft.compute.sandboxId : null; // A named machine stays selected when it disconnects or is removed. The // unavailable notice and submission gate require an explicit replacement. useEffect(() => { if ( selfhostedPrimary && draft.compute.kind === "machine" && selectedMachineSandboxId === null && !fleet.loading ) { const firstSelectableId = resolveSelectableMachineSandboxId(machines, null); if (firstSelectableId) onChange({ ...draft, compute: { kind: "machine", sandboxId: firstSelectableId, folder: rememberedMachineFolder( props.selectionHistory, props.selectedChannelId, firstSelectableId, ), }, }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selfhostedPrimary, fleet.loading, machineAvailabilityKey, selectedMachineSandboxId]); // A 404 means Connected Machines are off here, not a failure. const fleetLoadFailed = fleet.error != null && !(fleet.error instanceof OpenGeniApiError && fleet.error.status === 404); const attention = runsOnAttention({ draft, machines, fleetLoadFailed, fleetLoading: fleet.loading, }); // Where it runs is chosen under "+" > Runs on. Only what needs attention // shows under the composer: why Send waits on where it runs (with a retry // for a failed machine load), a catalog that couldn't be verified, and the // personal resources this chat will use. return ( <> {attention ? (
    void fleet.refresh()} connectAction={ } />
    ) : null} {props.catalogRecovery.error ? (
    Try again } > Try again to reload your available resources before starting this session.
    ) : null}
    ); } function PersonalResourceAccessInline(props: { access: NewSessionPersonalResourceAccess; embedded?: boolean | undefined; }) { if (props.access.names.length === 0) return null; const content = (

    {props.access.names.join(", ")} will be available for your work in this session. {props.access.visibility === "workspace" ? " Results are visible to people who can access this chat." : null}

    ); return props.embedded ? (
    {content}
    ) : (
    {content}
    ); } /** One quiet line on the new-chat page of a Personal workspace: its chats are private. */ function PrivateWorkspaceNote() { return (

    ); }