import { useEffect, useId, useMemo, useState, type ChangeEvent, type ReactNode } from "react"; import type { CanonicalWorkspace, PublishWorkspaceRequest, WorkspaceRecord, WorkspaceSummary } from "../api/workspaces"; import type { WorkspaceBootstrapDraft } from "../workspaces/drafts"; import { Button } from "../components/ui/button"; type FieldErrors = Record; export type WorkspaceEditorMode = | { kind: "bootstrap"; catalog: WorkspaceSummary; draft: WorkspaceBootstrapDraft } | { kind: "read_only"; catalog: WorkspaceSummary; record: WorkspaceRecord }; type BootstrapEditorProps = { mode: Extract; onSaveDraft?: (draft: WorkspaceBootstrapDraft) => void; onRequestCreate?: (request: PublishWorkspaceRequest, draft: WorkspaceBootstrapDraft) => void; }; type ReadOnlyEditorProps = { mode: Extract; /** Absent in read-only mode; declared so the union is uniformly addressable. */ onSaveDraft?: never; onRequestCreate?: never; }; export type WorkspaceEditorProps = BootstrapEditorProps | ReadOnlyEditorProps; const fieldClass = "h-9 w-full rounded-md border border-input bg-background px-2.5 text-sm shadow-xs outline-none focus-visible:ring-3 focus-visible:ring-ring/25 aria-invalid:border-destructive"; function positiveInteger(value: number | undefined, label: string, max = Number.MAX_SAFE_INTEGER): string | undefined { if (value === undefined) return undefined; if (!Number.isInteger(value) || value < 1 || value > max) { return max === 65_535 ? "Port must be between 1 and 65535" : `${label} must be a positive whole number`; } return undefined; } function validate(workspace: CanonicalWorkspace): FieldErrors { const errors: FieldErrors = {}; if (!/^[a-z][a-z0-9-]{2,62}$/.test(workspace.workspace.id)) { errors["workspace.id"] = "Use 3–63 lowercase letters, numbers, or hyphens; start with a letter"; } if (!workspace.workspace.name.trim()) errors["workspace.name"] = "Workspace name is required"; if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(workspace.dwh.database)) errors["dwh.database"] = "Use a database identifier"; if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(workspace.dwh.schema)) errors["dwh.schema"] = "Use a schema identifier"; if (!workspace.dwh.supported_transports.length) errors["dwh.transport"] = "Choose at least one DWH transport"; const dwhPort = positiveInteger(workspace.dwh.port, "DWH port", 65_535); if (dwhPort) errors["dwh.port"] = dwhPort; const dwhTimeout = positiveInteger(workspace.dwh.timeout_ms, "DWH timeout"); if (dwhTimeout) errors["dwh.timeout"] = dwhTimeout; if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(workspace.semantic_index.vector_store.collection)) errors["vector.collection"] = "Use a collection identifier"; if (!workspace.llm_policy.allowed.length || workspace.llm_policy.allowed.some((model) => !/^[^/\s]+\/[^/\s]+$/.test(model))) { errors["llm.allowed"] = "Use provider/model entries separated by commas"; } if (workspace.llm_policy.default && !workspace.llm_policy.allowed.includes(workspace.llm_policy.default)) { errors["llm.default"] = "Default model must be in the allowlist"; } return errors; } function selectedValues(event: ChangeEvent): string[] { return Array.from(event.currentTarget.selectedOptions, (option) => option.value); } function numberOrUndefined(value: string): number | undefined { return value.trim() === "" ? undefined : Number(value); } function coerceCatalogMetadata(workspace: CanonicalWorkspace, catalog: WorkspaceSummary): CanonicalWorkspace { return { ...workspace, workspace: { ...workspace.workspace, id: catalog.id, name: catalog.displayName, description: catalog.description, }, }; } function Field({ label, error, hint, children, }: { label: string; error?: string; hint?: string; children: (props: { id: string; describedBy?: string; invalid: boolean }) => ReactNode; }) { const id = useId(); const errorId = `${id}-error`; const hintId = `${id}-hint`; const describedBy = [hint ? hintId : undefined, error ? errorId : undefined].filter(Boolean).join(" ") || undefined; return (
{children({ id, describedBy, invalid: Boolean(error) })} {hint &&

{hint}

} {error && }
); } function Section({ title, children }: { title: string; children: ReactNode }) { return (

{title}

{children}
); } function EvidenceSummary({ evidence }: { evidence: NonNullable }) { const sourceIdentity = evidence.source.type === "http" ? `${evidence.source.uris.length} canonical URI${evidence.source.uris.length === 1 ? "" : "s"}` : evidence.source.uri; return (
Source type
{evidence.source.type}
Source
{sourceIdentity}
Chunk size
{evidence.policy.max_chunk_chars.toLocaleString("en-US")}
Retention
{evidence.policy.retain_published_generations.toLocaleString("en-US")}

Evidence summary is read-only. Curate source files or URIs in Git.

); } export function WorkspaceEditor(props: WorkspaceEditorProps) { const bootstrapMode = props.mode.kind === "bootstrap"; const initialWorkspace = props.mode.kind === "bootstrap" ? coerceCatalogMetadata(props.mode.draft.workspace, props.mode.catalog) : props.mode.record.workspace; const [workspace, setWorkspace] = useState(initialWorkspace); const [errors, setErrors] = useState({}); const readOnly = !bootstrapMode; const allowedModels = useMemo(() => workspace.llm_policy.allowed.join(", "), [workspace.llm_policy.allowed]); useEffect(() => { setWorkspace(props.mode.kind === "bootstrap" ? coerceCatalogMetadata(props.mode.draft.workspace, props.mode.catalog) : props.mode.record.workspace); setErrors({}); }, [bootstrapMode, props.mode]); function update(change: (previous: CanonicalWorkspace) => CanonicalWorkspace) { if (readOnly) return; setWorkspace((previous) => { const next = coerceCatalogMetadata(change(previous), props.mode.catalog); setErrors(validate(next)); return next; }); } function currentDraft(): WorkspaceBootstrapDraft { if (props.mode.kind !== "bootstrap") throw new Error("Read-only workspaces cannot create drafts"); return { workspaceId: props.mode.catalog.id, baseCommit: props.mode.draft.baseCommit, workspace: coerceCatalogMetadata(workspace, props.mode.catalog), updatedAt: new Date().toISOString(), }; } function saveDraft() { if (props.mode.kind !== "bootstrap") return; const nextErrors = validate(workspace); setErrors(nextErrors); if (Object.keys(nextErrors).length > 0) return; props.onSaveDraft?.(currentDraft()); } function requestCreate() { if (props.mode.kind !== "bootstrap") return; const nextErrors = validate(workspace); setErrors(nextErrors); if (Object.keys(nextErrors).length > 0) return; const draft = currentDraft(); props.onRequestCreate?.({ action: "create", workspace: draft.workspace, baseCommit: draft.baseCommit }, draft); } return (
{ event.preventDefault(); saveDraft(); }} noValidate>
{({ id, describedBy, invalid }) => update((value) => ({ ...value, workspace: { ...value.workspace, id: event.target.value } }))} />} {({ id, describedBy, invalid }) => update((value) => ({ ...value, workspace: { ...value.workspace, name: event.target.value } }))} />} {({ id, describedBy, invalid }) => update((value) => ({ ...value, workspace: { ...value.workspace, description: event.target.value || undefined } }))} />} {({ id, describedBy, invalid }) => }
{({ id, describedBy, invalid }) => } {({ id, describedBy, invalid }) => } {({ id, describedBy, invalid }) => update((value) => ({ ...value, dwh: { ...value.dwh, database: event.target.value } }))} />} {({ id, describedBy, invalid }) => update((value) => ({ ...value, dwh: { ...value.dwh, schema: event.target.value } }))} />} {({ id, describedBy, invalid }) => update((value) => ({ ...value, dwh: { ...value.dwh, port: numberOrUndefined(event.target.value) } }))} />} {({ id, describedBy, invalid }) => update((value) => ({ ...value, dwh: { ...value.dwh, timeout_ms: numberOrUndefined(event.target.value) } }))} />}
{({ id, describedBy, invalid }) => } {({ id, describedBy, invalid }) => update((value) => ({ ...value, semantic_index: { ...value.semantic_index, vector_store: { ...value.semantic_index.vector_store, collection: event.target.value } } }))} />} {({ id, describedBy, invalid }) => } {({ id, describedBy, invalid }) => } {({ id, describedBy, invalid }) => } {({ id, describedBy, invalid }) => }
{({ id, describedBy, invalid }) => update((value) => { const allowed = event.target.value.split(",").map((model) => model.trim()).filter(Boolean) as `${string}/${string}`[]; return { ...value, llm_policy: { allowed, ...(value.llm_policy.default && allowed.includes(value.llm_policy.default) ? { default: value.llm_policy.default } : {}) } }; })} />} {({ id, describedBy, invalid }) => }
{workspace.evidence && }

Hosts, users, secret-file paths, and credentials are installation bindings. They are intentionally not editable or stored in browser drafts.

{readOnly ? (

To change this workspace, edit {workspace.workspace.id}/workspace.yaml, commit/push, then Pull.

) : (
{"onRequestCreate" in props && props.onRequestCreate && }
)} ); }