import { apiFetch } from "./client"; import { getLocale } from "../i18n"; import { captureAuthOperation, isAuthOperationCurrent, requireAuthOperationPrecondition, type AuthOperationPrecondition, } from "../auth/authOperation"; import { getSettings } from "./settings"; import { listWorkspaces } from "./workspaces"; import { WORKSPACE_POLICY_ERROR, WORKSPACE_SUMMARY_ERROR, WorkspaceSelectionError, workspacePolicyGate, workspacePreferences, type WorkspacePreference, } from "../workspaces/drafts"; import type { AuthenticatedUser, ResumeSessionResult, SessionScope, SessionSummary, SessionDocument, UiResponse, } from "./types"; type NewSessionInput = { question: string; name?: string } & Partial; async function selectedPreferences(precondition?: AuthOperationPrecondition): Promise { requireAuthOperationPrecondition(precondition); const saved = workspacePreferences.load(); if (saved.workspaceId && saved.provider && saved.model && saved.thinking) return saved; // A direct new-session entry point can run before the composer has mounted. Seed current // application memory from the installation defaults once, then keep choices ephemeral. const legacy = await getSettings(); requireAuthOperationPrecondition(precondition); const latest = workspacePreferences.load(); return workspacePreferences.save({ workspaceId: latest.workspaceId ?? legacy.workspace, provider: latest.provider ?? legacy.provider, model: latest.model ?? legacy.model, thinking: latest.thinking ?? legacy.thinking, }); } async function ensureWorkspaceSelectionPolicy(precondition?: AuthOperationPrecondition): Promise { while (true) { requireAuthOperationPrecondition(precondition); const preferences = workspacePreferences.load(); const workspaceId = preferences.workspaceId; if (!workspaceId) return preferences; workspacePolicyGate.beginSummary(workspaceId); let workspaces: Awaited>; try { workspaces = await listWorkspaces(); } catch { requireAuthOperationPrecondition(precondition); if (workspacePreferences.load().workspaceId !== workspaceId) continue; workspacePolicyGate.rejectSummary(workspaceId); throw new WorkspaceSelectionError(WORKSPACE_SUMMARY_ERROR); } requireAuthOperationPrecondition(precondition); if (workspacePreferences.load().workspaceId !== workspaceId) continue; const workspace = workspaces.find((candidate) => candidate.id === workspaceId); if (!workspace) { if (workspaces.length === 0) { workspacePolicyGate.allowLegacy(workspaceId); return workspacePreferences.load(); } let installationDefault: string | undefined; try { installationDefault = (await getSettings()).workspace; } catch { requireAuthOperationPrecondition(precondition); if (workspacePreferences.load().workspaceId !== workspaceId) continue; workspacePolicyGate.rejectSummary(workspaceId); throw new WorkspaceSelectionError(WORKSPACE_SUMMARY_ERROR); } requireAuthOperationPrecondition(precondition); if (workspacePreferences.load().workspaceId !== workspaceId) continue; const replacement = workspaces.find( (candidate) => candidate.id === installationDefault && candidate.revision, ) ?? workspaces.find((candidate) => candidate.revision); if (!replacement) { requireAuthOperationPrecondition(precondition); workspacePolicyGate.reject(workspaceId); throw new WorkspaceSelectionError(WORKSPACE_POLICY_ERROR); } requireAuthOperationPrecondition(precondition); workspacePreferences.save({ ...preferences, workspaceId: replacement.id }); continue; } if (!workspace.revision) { workspacePolicyGate.reject(workspaceId); throw new WorkspaceSelectionError(WORKSPACE_POLICY_ERROR); } workspacePolicyGate.select(workspaceId); workspacePolicyGate.resolve(workspaceId); return workspacePreferences.load(); } } export async function createSession(i: NewSessionInput, precondition?: AuthOperationPrecondition) { const interactionLanguage = getLocale(); const initiatingOperation = captureAuthOperation(); const effectivePrecondition = precondition ?? (initiatingOperation ? { operation: initiatingOperation, isCurrent: () => isAuthOperationCurrent(initiatingOperation, { sessionId: null, disposalEpoch: initiatingOperation.disposalEpoch, }), } : undefined); requireAuthOperationPrecondition(effectivePrecondition); await selectedPreferences(effectivePrecondition); requireAuthOperationPrecondition(effectivePrecondition); const selection = await ensureWorkspaceSelectionPolicy(effectivePrecondition); requireAuthOperationPrecondition(effectivePrecondition); return apiFetch<{ id: string }>("/sessions", { method: "POST", body: JSON.stringify({ ...i, ...selection, interactionLanguage }), }); } /** Best-effort warm-up; callers must not await it before showing the composer. */ export const prewarmRuntime = () => apiFetch("/runtime/prewarm", { method: "POST" }); /** Compatibility export for existing shell tests/callers; AuthGate uses the validated auth API. */ export const getMe = () => apiFetch("/me"); export const listSessions = (scope: SessionScope = "mine") => apiFetch(`/sessions?scope=${scope}`); // eslint-disable-next-line @typescript-eslint/no-explicit-any export const getSession = (id: string) => apiFetch(`/sessions/${id}`); export const postResponse = (id: string, uiResponse: UiResponse) => apiFetch(`/sessions/${id}/response`, { method: "POST", body: JSON.stringify({ ui_response: uiResponse }), }); export const postSteer = (id: string, text: string) => apiFetch(`/sessions/${id}/steer`, { method: "POST", body: JSON.stringify({ text }), }); export const closeSession = (id: string) => apiFetch(`/sessions/${id}/close`, { method: "POST" }); export async function resumeSession(id: string) { const operation = captureAuthOperation(); const precondition = operation ? { operation, isCurrent: () => isAuthOperationCurrent(operation, { sessionId: null, disposalEpoch: operation.disposalEpoch }), } : undefined; const selected = await selectedPreferences(precondition); requireAuthOperationPrecondition(precondition); return apiFetch(`/sessions/${id}/resume`, { method: "POST", // The workspace is owned by the persisted session, never by this request. body: JSON.stringify({ provider: selected.provider, model: selected.model }), }); } export const renameSession = (id: string, name: string) => apiFetch(`/sessions/${id}/rename`, { method: "POST", body: JSON.stringify({ name }) }); export const setSessionGroup = (id: string, group: string) => apiFetch(`/sessions/${id}/group`, { method: "POST", body: JSON.stringify({ group }) }); export const archiveSession = (id: string) => apiFetch(`/sessions/${id}/archive`, { method: "POST" }); export const unarchiveSession = (id: string) => apiFetch(`/sessions/${id}/unarchive`, { method: "POST" }); export const deleteSession = (id: string) => apiFetch(`/sessions/${id}`, { method: "DELETE" }); export const getSessionDocuments = (id: string) => apiFetch(`/sessions/${id}/documents`); export async function checkDwhHealth(): Promise<{ ok: boolean }> { const timeout = new Promise<{ ok: false }>((r) => setTimeout(() => r({ ok: false }), 5000)); try { return await Promise.race([ apiFetch<{ ok: boolean; detail: string }>("/health/dwh"), timeout, ]); } catch { return { ok: false }; } }