feat: store workspace preferences and drafts locally

This commit is contained in:
2026-08-04 05:28:13 +02:00
parent 6c09adc05e
commit cee4cfa63d
8 changed files with 537 additions and 21 deletions
+67
View File
@@ -0,0 +1,67 @@
import { expect, test } from "vitest";
import type { CanonicalWorkspace } from "../api/workspaces";
import { workspaceDrafts, workspacePreferences } from "./drafts";
const workspace: CanonicalWorkspace = {
workspace: { schema_version: 2, id: "psd-clinical", name: "PSD Clinical", language: "en" },
dwh: {
engine: "postgres", database: "clinical", schema: "datawarehouse",
supported_transports: ["postgres_direct"],
},
semantic_index: {
vector_store: {
engine: "pgvector", database: "vectors", schema: "public", collection: "clinical",
dimensions: 768, distance: "cosine", supported_transports: ["pgvector_direct"],
},
embedding: { provider: "ollama_compatible", model: "nomic-embed-text", dimensions: 768 },
},
llm_policy: { allowed: ["zai/glm-5.2"] },
};
test("keeps an anonymous user's model selection in browser storage", () => {
workspacePreferences.save({
workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
});
expect(workspacePreferences.load()).toMatchObject({ model: "glm-5.2" });
});
test("reloads a canonical workspace draft and discards it by workspace ID", () => {
workspaceDrafts.save({
workspaceId: "psd-clinical",
baseCommit: "a".repeat(40),
baseBlob: "b".repeat(40),
workspace,
updatedAt: "2026-08-04T10:00:00.000Z",
});
expect(workspaceDrafts.load("psd-clinical")).toMatchObject({
baseCommit: "a".repeat(40), workspace,
});
workspaceDrafts.discard("psd-clinical");
expect(workspaceDrafts.load("psd-clinical")).toBeUndefined();
});
test("keeps canonical diagnostic configuration but never stores unknown draft fields", () => {
const configured = {
...workspace,
diagnostics: {
dwh_rest: {
method: "POST",
path: "/rpc/ping",
auth: "bearer",
response: { database: "database", schema: "schema" },
},
},
secret: "must-not-be-persisted",
} as CanonicalWorkspace & { secret: string };
workspaceDrafts.save({
workspaceId: "psd-clinical",
baseCommit: "a".repeat(40),
workspace: configured,
updatedAt: "2026-08-04T10:00:00.000Z",
});
expect(workspaceDrafts.load("psd-clinical")?.workspace.diagnostics).toEqual(configured.diagnostics);
expect(localStorage.getItem("thothii.workspace-registry.v1.draft.psd-clinical")).not.toContain("must-not-be-persisted");
});
+179
View File
@@ -0,0 +1,179 @@
import type { CanonicalDiagnostics, CanonicalWorkspace, RestDiagnosticRequest } from "../api/workspaces";
export { workspacePreferences, type WorkspacePreference } from "./preferences";
export interface WorkspaceDraft {
workspaceId: string;
baseCommit: string;
baseBlob?: string;
workspace: CanonicalWorkspace;
updatedAt: string;
}
const PREFIX = "thothii.workspace-registry.v1";
const DRAFT_PREFIX = `${PREFIX}.draft.`;
function storage(): Storage | undefined {
try { return window.localStorage; } catch { return undefined; }
}
function record(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: undefined;
}
function copyRequest(value: unknown): RestDiagnosticRequest | undefined {
const source = record(value);
if (
!source
|| (source.method !== "GET" && source.method !== "POST")
|| typeof source.path !== "string"
|| (source.auth !== "none" && source.auth !== "bearer" && source.auth !== "x-api-key")
) return undefined;
return { method: source.method, path: source.path, auth: source.auth };
}
function copyDiagnostics(value: unknown): CanonicalDiagnostics | undefined {
const source = record(value);
if (!source) return undefined;
const diagnostics: CanonicalDiagnostics = {};
if (source.dwh_rest !== undefined) {
const request = copyRequest(source.dwh_rest);
const response = record(record(source.dwh_rest)?.response);
if (!request || !response || typeof response.database !== "string" || typeof response.schema !== "string") return undefined;
diagnostics.dwh_rest = { ...request, response: { database: response.database, schema: response.schema } };
}
if (source.vector_rest !== undefined) {
const vector = record(source.vector_rest);
const request = copyRequest(vector?.metadata);
const response = record(record(vector?.metadata)?.response);
if (!vector || !request || !response || typeof response.collection !== "string" || typeof response.dimensions !== "string" || typeof response.distance !== "string") return undefined;
const metadata = { ...request, response: { collection: response.collection, dimensions: response.dimensions, distance: response.distance } };
let reversibleProbe: NonNullable<CanonicalDiagnostics["vector_rest"]>["reversible_probe"] | undefined;
if (vector.reversible_probe !== undefined) {
const probe = copyRequest(vector.reversible_probe);
const probeResponse = record(record(vector.reversible_probe)?.response);
if (!probe || probe.method !== "POST" || probe.auth === "none" || !probeResponse || typeof probeResponse.operation !== "string") return undefined;
reversibleProbe = {
method: "POST",
path: probe.path,
auth: probe.auth as "bearer" | "x-api-key",
response: { operation: probeResponse.operation },
};
}
diagnostics.vector_rest = { metadata, ...(reversibleProbe ? { reversible_probe: reversibleProbe } : {}) };
}
if (source.embedding !== undefined) {
const request = copyRequest(source.embedding);
const response = record(record(source.embedding)?.response);
if (!request || !response || typeof response.model !== "string" || typeof response.dimensions !== "string") return undefined;
diagnostics.embedding = { ...request, response: { model: response.model, dimensions: response.dimensions } };
}
return diagnostics;
}
function copyWorkspace(value: unknown): CanonicalWorkspace | undefined {
const source = record(value);
const metadata = record(source?.workspace);
const dwh = record(source?.dwh);
const semanticIndex = record(source?.semantic_index);
const vectorStore = record(semanticIndex?.vector_store);
const embedding = record(semanticIndex?.embedding);
const policy = record(source?.llm_policy);
const diagnostics = source?.diagnostics === undefined ? undefined : copyDiagnostics(source.diagnostics);
if (!metadata || !dwh || !semanticIndex || !vectorStore || !embedding || !policy) return undefined;
if (
typeof metadata.schema_version !== "number" || typeof metadata.id !== "string" || typeof metadata.name !== "string"
|| typeof metadata.language !== "string" || typeof dwh.database !== "string" || typeof dwh.schema !== "string"
|| typeof vectorStore.database !== "string" || typeof vectorStore.schema !== "string"
|| typeof vectorStore.collection !== "string" || typeof vectorStore.dimensions !== "number"
|| typeof embedding.model !== "string" || typeof embedding.dimensions !== "number"
|| !Array.isArray(dwh.supported_transports) || !Array.isArray(vectorStore.supported_transports) || !Array.isArray(policy.allowed)
) return undefined;
if (source?.diagnostics !== undefined && !diagnostics) return undefined;
return {
workspace: {
schema_version: metadata.schema_version as 2,
id: metadata.id,
name: metadata.name,
...(typeof metadata.description === "string" ? { description: metadata.description } : {}),
language: metadata.language as "en" | "it",
},
dwh: {
engine: dwh.engine as "postgres",
database: dwh.database,
schema: dwh.schema,
...(typeof dwh.port === "number" ? { port: dwh.port } : {}),
...(typeof dwh.timeout_ms === "number" ? { timeout_ms: dwh.timeout_ms } : {}),
supported_transports: dwh.supported_transports.filter((transport): transport is "postgres_direct" | "rest_api" | "ssh_tunnel" =>
transport === "postgres_direct" || transport === "rest_api" || transport === "ssh_tunnel"),
},
semantic_index: {
vector_store: {
engine: vectorStore.engine as "pgvector",
database: vectorStore.database,
schema: vectorStore.schema,
collection: vectorStore.collection,
dimensions: vectorStore.dimensions,
distance: vectorStore.distance as "cosine" | "l2" | "inner_product",
...(typeof vectorStore.port === "number" ? { port: vectorStore.port } : {}),
...(typeof vectorStore.timeout_ms === "number" ? { timeout_ms: vectorStore.timeout_ms } : {}),
supported_transports: vectorStore.supported_transports.filter((transport): transport is "pgvector_direct" | "rest_api" | "ssh_tunnel" =>
transport === "pgvector_direct" || transport === "rest_api" || transport === "ssh_tunnel"),
},
...(record(semanticIndex.vector_writer) ? { vector_writer: {} } : {}),
embedding: {
provider: embedding.provider as "ollama_compatible" | "openai_compatible",
model: embedding.model,
dimensions: embedding.dimensions,
...(typeof embedding.timeout_ms === "number" ? { timeout_ms: embedding.timeout_ms } : {}),
},
},
llm_policy: {
...(typeof policy.default === "string" ? { default: policy.default as `${string}/${string}` } : {}),
allowed: policy.allowed.filter((model): model is `${string}/${string}` => typeof model === "string"),
},
...(diagnostics ? { diagnostics } : {}),
};
}
function normalize(value: unknown): WorkspaceDraft | undefined {
const source = record(value);
const workspace = copyWorkspace(source?.workspace);
if (!source || !workspace || typeof source.workspaceId !== "string" || typeof source.baseCommit !== "string" || typeof source.updatedAt !== "string") {
return undefined;
}
return {
workspaceId: source.workspaceId,
baseCommit: source.baseCommit,
...(typeof source.baseBlob === "string" ? { baseBlob: source.baseBlob } : {}),
workspace,
updatedAt: source.updatedAt,
};
}
function key(id: string): string {
return `${DRAFT_PREFIX}${encodeURIComponent(id)}`;
}
/** Browser-only workspace drafts. Saving or editing one never calls the server. */
export const workspaceDrafts = {
load(id: string): WorkspaceDraft | undefined {
try {
const raw = storage()?.getItem(key(id));
return raw ? normalize(JSON.parse(raw)) : undefined;
} catch {
return undefined;
}
},
save(draft: WorkspaceDraft): void {
const safe = normalize(draft);
if (!safe) return;
try { storage()?.setItem(key(safe.workspaceId), JSON.stringify(safe)); } catch { /* storage is optional */ }
},
discard(id: string): void {
try { storage()?.removeItem(key(id)); } catch { /* storage is optional */ }
},
};