Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
189 lines
7.7 KiB
TypeScript
189 lines
7.7 KiB
TypeScript
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<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 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<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 };
|
|
}
|
|
}
|