187 lines
7.6 KiB
TypeScript
187 lines
7.6 KiB
TypeScript
import { apiFetch } from "./client";
|
|
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<WorkspacePreference>;
|
|
|
|
async function selectedPreferences(precondition?: AuthOperationPrecondition): Promise<WorkspacePreference> {
|
|
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<WorkspacePreference> {
|
|
while (true) {
|
|
requireAuthOperationPrecondition(precondition);
|
|
const preferences = workspacePreferences.load();
|
|
const workspaceId = preferences.workspaceId;
|
|
if (!workspaceId) return preferences;
|
|
workspacePolicyGate.beginSummary(workspaceId);
|
|
let workspaces: Awaited<ReturnType<typeof listWorkspaces>>;
|
|
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 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 }),
|
|
});
|
|
}
|
|
|
|
/** Best-effort warm-up; callers must not await it before showing the composer. */
|
|
export const prewarmRuntime = () =>
|
|
apiFetch<void>("/runtime/prewarm", { method: "POST" });
|
|
|
|
/** Compatibility export for existing shell tests/callers; AuthGate uses the validated auth API. */
|
|
export const getMe = () => apiFetch<AuthenticatedUser>("/me");
|
|
|
|
export const listSessions = (scope: SessionScope = "mine") =>
|
|
apiFetch<SessionSummary[]>(`/sessions?scope=${scope}`);
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export const getSession = (id: string) => apiFetch<any>(`/sessions/${id}`);
|
|
|
|
export const postResponse = (id: string, uiResponse: UiResponse) =>
|
|
apiFetch<void>(`/sessions/${id}/response`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ ui_response: uiResponse }),
|
|
});
|
|
|
|
export const postSteer = (id: string, text: string) =>
|
|
apiFetch<void>(`/sessions/${id}/steer`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ text }),
|
|
});
|
|
|
|
export const closeSession = (id: string) =>
|
|
apiFetch<void>(`/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<ResumeSessionResult>(`/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<void>(`/sessions/${id}/rename`, { method: "POST", body: JSON.stringify({ name }) });
|
|
|
|
export const setSessionGroup = (id: string, group: string) =>
|
|
apiFetch<void>(`/sessions/${id}/group`, { method: "POST", body: JSON.stringify({ group }) });
|
|
|
|
export const archiveSession = (id: string) =>
|
|
apiFetch<void>(`/sessions/${id}/archive`, { method: "POST" });
|
|
|
|
export const unarchiveSession = (id: string) =>
|
|
apiFetch<void>(`/sessions/${id}/unarchive`, { method: "POST" });
|
|
|
|
export const deleteSession = (id: string) =>
|
|
apiFetch<void>(`/sessions/${id}`, { method: "DELETE" });
|
|
|
|
export const getSessionDocuments = (id: string) =>
|
|
apiFetch<SessionDocument[]>(`/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 };
|
|
}
|
|
}
|