feat: complete catalog-driven preprocessing
Publish documentation / publish (push) Successful in 2m12s

This commit is contained in:
Codex
2026-09-06 17:49:35 +02:00
parent 8707ae1d46
commit cffa60772e
141 changed files with 5898 additions and 3015 deletions
+50 -26
View File
@@ -17,6 +17,7 @@ import { StopConfirmDialog } from "./StopConfirmDialog";
import { SteerInput, ComposerFooter } from "./SteerInput";
import { WorkflowBar } from "./WorkflowBar";
import { DatabaseManagementPage } from "./DatabaseManagementPage";
import { WorkspacePreprocessingControl } from "./WorkspacePreprocessingControl";
import { Pencil, ArrowLeft, ArrowRight, ChevronDown, Trash2 } from "lucide-react";
import { Accordion } from "@base-ui/react/accordion";
import { Button, buttonVariants } from "../components/ui/button";
@@ -37,6 +38,9 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { CSSProperties, KeyboardEvent } from "react";
import { captureAuthOperation, isAuthOperationCurrent, StaleAuthOperationError, type AuthOperationGuard } from "../auth/authOperation";
import { getSettings } from "../api/settings";
import { workspacePreferences, type WorkspacePreference } from "../workspaces/preferences";
import { getWorkspacePreprocessingStatus } from "../api/workspace-preprocessing";
interface AppShellProps {
canLogout: boolean;
@@ -45,23 +49,6 @@ interface AppShellProps {
type ActiveSurface = "core" | "database-management";
type ActiveManagementPanel = "workspace" | "pi" | null;
const SESSION_SCOPES: readonly SessionScope[] = ["mine", "all"];
const APP_NAVIGATION_STORAGE_KEY = "thothii:app-navigation";
function readPersistedNavigation(): { surface: ActiveSurface; panel: ActiveManagementPanel } {
if (import.meta.env.MODE === "test") return { surface: "core", panel: null };
try {
const parsed = JSON.parse(window.localStorage.getItem(APP_NAVIGATION_STORAGE_KEY) ?? "null") as {
surface?: unknown;
panel?: unknown;
} | null;
return {
surface: parsed?.surface === "database-management" ? "database-management" : "core",
panel: parsed?.panel === "workspace" || parsed?.panel === "pi" ? parsed.panel : null,
};
} catch {
return { surface: "core", panel: null };
}
}
function sessionScopeTabClass(selected: boolean): string {
const base = "relative -mb-px flex h-8 w-full cursor-pointer items-center justify-center rounded-t-md border border-b-2 px-2 text-xs font-semibold tracking-[0.005em] outline-none focus-visible:z-10 focus-visible:ring-3 focus-visible:ring-[oklch(var(--nav-active-border)/0.28)]";
@@ -168,6 +155,33 @@ export function AppShell({ canLogout }: AppShellProps) {
const canManageDatabase = permissions.includes("database.manage");
const canManagePi = permissions.includes("pi.manage");
const authGeneration = useAuthGeneration();
const { data: installationSettings } = useQuery({
queryKey: ["settings"],
queryFn: getSettings,
});
const [workspacePreference, setWorkspacePreference] = useState<WorkspacePreference>(
() => workspacePreferences.load(),
);
useEffect(() => {
const unsubscribe = workspacePreferences.subscribe(setWorkspacePreference);
// Child effects may seed the singleton before this parent effect subscribes.
setWorkspacePreference(workspacePreferences.load());
return unsubscribe;
}, []);
// Only an ID selected from the active Workspace Registry may address workspace-scoped APIs.
// Installation settings are a legacy hint and can name a removed/non-catalog workspace.
const selectedWorkspaceId = workspacePreference.workspaceId;
const preprocessingQuery = useQuery({
queryKey: ["workspace-preprocessing", selectedWorkspaceId],
queryFn: () => getWorkspacePreprocessingStatus(selectedWorkspaceId!),
enabled: Boolean(authenticatedUser && selectedWorkspaceId),
refetchInterval: (query) => query.state.data?.state === "running" ? 1_000 : 5_000,
});
const preprocessingBlocksNewSession = Boolean(
selectedWorkspaceId
&& preprocessingQuery.data
&& preprocessingQuery.data.state !== "ready",
);
const { data: sessions = [] } = useQuery<SessionSummary[]>({
queryKey: ["sessions", sessionScope],
queryFn: async () => {
@@ -183,8 +197,7 @@ export function AppShell({ canLogout }: AppShellProps) {
const queryClient = useQueryClient();
const [showActivity, setShowActivity] = useState(false);
const persistedNavigation = readPersistedNavigation();
const [activeSurface, setActiveSurface] = useState<ActiveSurface>(persistedNavigation.surface);
const [activeSurface, setActiveSurface] = useState<ActiveSurface>("core");
const databaseManagementPresentation = resolveDatabaseManagementPresentation({
isDevelopment: import.meta.env.DEV,
mode: import.meta.env.MODE,
@@ -195,7 +208,7 @@ export function AppShell({ canLogout }: AppShellProps) {
const updateDatabaseNavigationState = useCallback((state: { dirty: boolean; busy: boolean }) => {
databaseNavigationRef.current = state;
}, []);
const [activeManagementPanel, setActiveManagementPanel] = useState<ActiveManagementPanel>(persistedNavigation.panel);
const [activeManagementPanel, setActiveManagementPanel] = useState<ActiveManagementPanel>(null);
const [adminNavigationValue, setAdminNavigationValue] = useState<string[]>([]);
const [activeOpen, setActiveOpen] = useState(true);
const [archiveOpen, setArchiveOpen] = useState(false);
@@ -206,10 +219,6 @@ export function AppShell({ canLogout }: AppShellProps) {
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
const [renameGroupTarget, setRenameGroupTarget] = useState<string | null>(null);
const operationEpochRef = useRef(0);
useEffect(() => {
if (import.meta.env.MODE === "test") return;
window.localStorage.setItem(APP_NAVIGATION_STORAGE_KEY, JSON.stringify({ surface: activeSurface, panel: activeManagementPanel }));
}, [activeManagementPanel, activeSurface]);
useEffect(() => () => { operationEpochRef.current += 1; }, []);
const groups = useMemo(
@@ -610,6 +619,10 @@ export function AppShell({ canLogout }: AppShellProps) {
}, [lastSystemEvent]);
function startNewSession() {
if (preprocessingBlocksNewSession) {
toast.warning(preprocessingQuery.data?.detail ?? "Workspace preprocessing is required.");
return;
}
if (!canLeaveDatabaseManagement()) return;
setActiveSurface("core");
setActiveManagementPanel(null);
@@ -841,7 +854,7 @@ export function AppShell({ canLogout }: AppShellProps) {
{/* Right session rail */}
{(activeSurface === "database-management" || !showActivity) && (
<aside aria-label="Session navigation" className="flex w-[15vw] shrink-0 flex-col bg-sidebar">
<aside aria-label="Session navigation" className="flex w-64 shrink-0 flex-col bg-sidebar">
<div className="relative px-4 pb-3 pt-5 text-center">
<h1 className="font-heading text-xl font-semibold leading-none tracking-tight text-foreground">
Thoth<span className="text-primary">II</span>
@@ -871,7 +884,11 @@ export function AppShell({ canLogout }: AppShellProps) {
size="sm"
className="w-full"
aria-current={currentNavigation === "core" ? "page" : undefined}
data-navigation-state={currentNavigation === "core" ? "current" : "available"}
data-navigation-state={preprocessingBlocksNewSession
? "unavailable"
: currentNavigation === "core" ? "current" : "available"}
disabled={preprocessingBlocksNewSession}
title={preprocessingBlocksNewSession ? preprocessingQuery.data?.detail : undefined}
onClick={startNewSession}
>
New session
@@ -949,6 +966,13 @@ export function AppShell({ canLogout }: AppShellProps) {
>
Pi management
</Button>
<WorkspacePreprocessingControl
workspaceId={selectedWorkspaceId}
status={preprocessingQuery.data}
loading={preprocessingQuery.isLoading}
unavailable={preprocessingQuery.isError}
canManage={canManageDatabase}
/>
</Accordion.Panel>
</Accordion.Item>
</Accordion.Root>