import { apiFetch } from "./client"; import { getSettings } from "./settings"; import { getWorkspace, listWorkspaces } from "./workspaces"; import { WORKSPACE_POLICY_ERROR, WORKSPACE_SUMMARY_ERROR, WorkspaceSelectionError, workspacePolicyGate, workspacePreferences, type WorkspacePreference, } from "../workspaces/drafts"; import type { Principal, ResumeSessionResult, SessionScope, SessionSummary, SessionDocument, UiResponse, } from "./types"; type NewSessionInput = { question: string; name?: string } & Partial; async function selectedPreferences(): Promise { 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 its // browser-local preferences from the legacy read-only defaults once, then keep them local. const legacy = await getSettings(); return workspacePreferences.save({ workspaceId: saved.workspaceId ?? legacy.workspace, provider: saved.provider ?? legacy.provider, model: saved.model ?? legacy.model, thinking: saved.thinking ?? legacy.thinking, }); } function reconcileWorkspacePolicy(preferences: WorkspacePreference, allowed: readonly string[], defaultModel?: string) { if (allowed.length === 0) throw new WorkspaceSelectionError(WORKSPACE_POLICY_ERROR); const selected = preferences.provider && preferences.model ? `${preferences.provider}/${preferences.model}` : undefined; if (selected && allowed.includes(selected)) return preferences; const replacement = defaultModel && allowed.includes(defaultModel) ? defaultModel : allowed[0]; const separator = replacement.indexOf("/"); if (separator <= 0 || separator === replacement.length - 1) { throw new WorkspaceSelectionError(WORKSPACE_POLICY_ERROR); } return workspacePreferences.save({ ...preferences, provider: replacement.slice(0, separator), model: replacement.slice(separator + 1), }); } async function ensureWorkspaceSelectionPolicy(): Promise { while (true) { const preferences = workspacePreferences.load(); const workspaceId = preferences.workspaceId; if (!workspaceId) return preferences; workspacePolicyGate.beginSummary(workspaceId); let workspace; try { workspace = (await listWorkspaces()).find((candidate) => candidate.id === workspaceId); } catch { if (workspacePreferences.load().workspaceId !== workspaceId) continue; workspacePolicyGate.rejectSummary(workspaceId); throw new WorkspaceSelectionError(WORKSPACE_SUMMARY_ERROR); } if (workspacePreferences.load().workspaceId !== workspaceId) continue; if (workspace && !workspace.revision) { workspacePolicyGate.reject(workspaceId); throw new WorkspaceSelectionError(WORKSPACE_POLICY_ERROR); } if (!workspace?.revision) { workspacePolicyGate.allowLegacy(workspaceId); return workspacePreferences.load(); } workspacePolicyGate.select(workspaceId); const outcome = await Promise.race([ getWorkspace(workspaceId).then( (record) => ({ kind: "record" as const, record }), () => ({ kind: "error" as const }), ), workspacePolicyGate.waitForCurrent(() => workspacePreferences.load()).then( (selection) => ({ kind: "selection" as const, selection }), ), ]); if (outcome.kind === "selection") { if (outcome.selection.workspaceId !== workspaceId) continue; return outcome.selection; } if (outcome.kind === "error") { if (workspacePreferences.load().workspaceId !== workspaceId) continue; workspacePolicyGate.reject(workspaceId); throw new WorkspaceSelectionError(WORKSPACE_POLICY_ERROR); } const { record } = outcome; if (workspacePreferences.load().workspaceId !== workspaceId) continue; let selection: WorkspacePreference; try { selection = reconcileWorkspacePolicy( preferences, record.workspace.llm_policy.allowed, record.workspace.llm_policy.default, ); } catch (error) { workspacePolicyGate.reject(workspaceId); throw error; } workspacePolicyGate.resolve(workspaceId); if (workspacePreferences.load().workspaceId === workspaceId) return selection; } } export async function createSession(i: NewSessionInput) { await selectedPreferences(); const selection = await ensureWorkspaceSelectionPolicy(); return apiFetch<{ id: string }>("/sessions", { method: "POST", body: JSON.stringify({ ...i, ...selection }), }); } /** Best-effort warm-up; callers must not await it before showing the composer. */ export const prewarmRuntime = () => apiFetch("/runtime/prewarm", { method: "POST" }); 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 const resumeSession = (id: string) => apiFetch(`/sessions/${id}/resume`, { method: "POST" }); 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 }; } }