prototype: refine context shelf with remembered defaults and session tabs

This commit is contained in:
Codex
2026-09-12 12:29:02 +02:00
parent 36bf659ea9
commit 41b9fed4d5
11 changed files with 509 additions and 0 deletions
@@ -7,6 +7,10 @@ Question: where should the single workspace selector and independent global AI
model selector live when analytical work and all five administrative pages share
one application context inside Omics Portal?
Update: the owner selected direction C and requested three refinements in
[Context shelf review V4](../context-shelf/README.md), restoring right-rail session
tabs and resolving last choices before installation defaults. V3 is preserved.
## Run and compare
From `frontend/`, run `npm run prototype:application-context`.
+182
View File
@@ -0,0 +1,182 @@
import { useEffect, useRef, useState } from "react";
import { ArrowLeft, ArrowRight, ArrowUpRight, BookOpen, Check, ChevronDown, ChevronRight, Cpu, Database, FileText, FolderClock, Layers, LayoutDashboard, LockKeyhole, Menu, MessageSquare, Play, Search, Settings2, ShieldCheck, SlidersHorizontal, Terminal, Users, X } from "lucide-react";
import { Content, type ContentProps, type ConnectionValues } from "../application-context/Content";
import { Button } from "../application-context/ui";
import { inventory, type Item, type Scenario, type Surface, type Variant } from "../administration-review/model";
import { models, modules, readLocation, sessions, variants, workspaces, type Job, type Page, type Route, type Session } from "./model";
// Three complete-app proposals on the established namespaced routes. V3 is a
// disposable simulation, with no API, persistence, authorization or server writes.
const icons = { core: MessageSquare, sessions: FolderClock, database: Database, memory: BookOpen, evidence: FileText, workspace: Layers, pi: Terminal };
const titles: Record<Page, string> = { core: "Analytical workspace", sessions: "Sessions", database: "Database", memory: "Memory", evidence: "Evidence", workspace: "Workspace", pi: "Pi runtime" };
import { SessionRail } from "./SessionRail";
import { forget, remember, resolvePreferences } from "./preferences";
type Availability = "available" | "unavailable" | "no-workspaces" | "no-models" | "needs-preprocessing";
export function App() {
const [initialPreferences] = useState(resolvePreferences);
const [sources, setSources] = useState(initialPreferences.sources);
const [canReadAll, setCanReadAll] = useState(true);
const [route, setRoute] = useState<Route>(() => ({ ...readLocation(), workspace: initialPreferences.workspace }));
const { page, workspace, variant } = route;
const [modelId, setModelId] = useState(initialPreferences.model);
const [availability, setAvailability] = useState<Availability>("available");
const [width, setWidth] = useState("full");
const [portalOpen, setPortalOpen] = useState(false);
const [navOpen, setNavOpen] = useState(false);
const [shelfOpen, setShelfOpen] = useState(initialPreferences.invalid);
const [notice, setNotice] = useState(initialPreferences.invalid ? "A remembered choice is no longer available. Choose a valid workspace and model; no alternative has been selected silently." : "");
const [dirty, setDirty] = useState(false);
const [editing, setEditing] = useState(false);
const [section, setSection] = useState(0);
const [, setFocused] = useState(false);
const [selection, setSelection] = useState<Record<string, string>>({});
const [filters, setFilters] = useState<Record<string, string>>({});
const [cards, setCards] = useState<Record<string, Item[]>>({});
const [connections, setConnections] = useState<Record<string, ConnectionValues>>({});
const [operation, setOperation] = useState<Job | null>(null);
const busy = operation?.state === "running";
const busyRef = useRef(false); busyRef.current = busy;
const [active, setActive] = useState<Session | null>(null);
const [resumeTarget, setResumeTarget] = useState<Session | null>(null);
const [question, setQuestion] = useState("");
const [artifact, setArtifact] = useState("Question");
const [scenario, setScenario] = useState<Scenario>("ready");
const model = models.find(m => m.id === modelId);
const currentWorkspace = workspaces.find(w => w.id === workspace);
const ready = !!currentWorkspace && !!model && !["unavailable", "no-workspaces", "no-models"].includes(availability);
const admin = page !== "core" && page !== "sessions";
const scope = `${page}:${page === "pi" ? "installation" : workspace}`;
const dirtyRef = useRef(false); dirtyRef.current = dirty;
const lastUrl = useRef(location.href);
const mainRef = useRef<HTMLElement>(null);
const canLeave = () => !dirtyRef.current || window.confirm("Discard unsaved changes? Saved records and running operations will be kept.");
const resetDetail = () => { setDirty(false); setEditing(false); setSection(0); setFocused(false); setScenario("ready"); };
function writeRoute(next: Route, replace = false) {
const url = new URL(location.href);
url.searchParams.set("thoth_route", `${next.page === "core" || next.page === "sessions" ? "analysis" : "administration"}/${next.page}`);
url.searchParams.set("variant", next.variant);
if (next.workspace) url.searchParams.set("thoth_workspace", next.workspace); else url.searchParams.delete("thoth_workspace");
history[replace ? "replaceState" : "pushState"](null, "", url);
lastUrl.current = url.href; setRoute(next);
}
function navigate(next: Page) {
if (!ready || !canLeave()) return;
writeRoute({ ...route, page: next }); resetDetail(); setQuestion(""); setNavOpen(false); setResumeTarget(null); setNotice("");
requestAnimationFrame(() => mainRef.current?.focus());
}
function saveChoice(patch: { workspace?: string; model?: string }) {
const saved = remember(patch);
setSources(old => ({ ...old, ...(patch.workspace ? { workspace: saved ? "Last choice" : "This visit only" } : {}), ...(patch.model ? { model: saved ? "Last choice" : "This visit only" } : {}) }));
return saved;
}
function changeWorkspace(id: string) {
if (busy || id === workspace || !canLeave()) return;
setOperation(null); writeRoute({ ...route, workspace: id }); resetDetail(); setActive(null); setQuestion(""); setResumeTarget(null);
const saved = saveChoice({ workspace: id });
setNotice(saved ? "Workspace remembered for your next visit. Your model selection is unchanged." : "Browser storage is unavailable. This workspace selection will last only for this visit.");
}
function changeModel(id: string) {
if (busy) return;
setOperation(null); setModelId(id);
const saved = saveChoice({ model: id });
setNotice(saved ? "Model remembered for your next visit, independently of the workspace." : "Browser storage is unavailable. This model selection will last only for this visit.");
}
function cycle(delta: number) {
if (!canLeave()) return;
const keys: Variant[] = ["A", "B", "C"];
writeRoute({ ...route, variant: keys[(keys.indexOf(variant) + delta + 3) % 3] }, true);
resetDetail(); setQuestion(""); setShelfOpen(!ready);
}
useEffect(() => {
const pop = () => {
if (busyRef.current) { history.pushState(null, "", lastUrl.current); setNotice("Finish the current operation before following browser history."); return; }
if (!canLeave()) { history.pushState(null, "", lastUrl.current); return; }
setRoute(prev => ({ ...readLocation(), workspace: prev.workspace })); lastUrl.current = location.href; resetDetail(); setActive(null); setQuestion(""); setResumeTarget(null);
};
const unload = (e: BeforeUnloadEvent) => { if (dirtyRef.current) { e.preventDefault(); e.returnValue = ""; } };
window.addEventListener("popstate", pop); window.addEventListener("beforeunload", unload);
return () => { window.removeEventListener("popstate", pop); window.removeEventListener("beforeunload", unload); };
}, []);
useEffect(() => {
const url = new URL(location.href);
if (workspace) url.searchParams.set("thoth_workspace", workspace); else url.searchParams.delete("thoth_workspace");
history.replaceState(null, "", url); lastUrl.current = url.href;
}, [workspace, page, variant]);
useEffect(() => {
const key = (e: KeyboardEvent) => {
if ((e.target as HTMLElement).closest("input,textarea,select,button,[contenteditable],[role=tablist]")) return;
if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault(); cycle(e.key === "ArrowRight" ? 1 : -1); }
};
window.addEventListener("keydown", key); return () => window.removeEventListener("keydown", key);
});
function startJob(label: string, kind: "llm" | "embedding" | "system" = "system") {
if (busy || !ready || (kind === "llm" && !model?.metadata && admin && page !== "pi")) return;
setOperation({ id: Date.now(), label, workspace: page === "pi" || label.includes("repository") ? "installation" : workspace, model: kind === "llm" ? modelId : kind === "embedding" ? "Installation embedding model" : "No LLM required", state: "running" });
setResumeTarget(null);
setNotice("Operation started. Workspace, model and other operations are locked until it finishes.");
}
function resume(s: Session) {
if (busy || !ready || s.status === "archived" || availability === "needs-preprocessing" || !canLeave()) return;
saveChoice({ workspace: s.workspace }); setActive(s); setResumeTarget(null); resetDetail(); setQuestion(""); setArtifact("Question");
writeRoute({ ...route, workspace: s.workspace, page: "core" });
setOperation({ id: Date.now(), label: `Resume: ${s.title}`, workspace: s.workspace, model: modelId, state: "running" });
setNotice("Proposed resume: the original workspace and revision are retained; this run uses the globally selected model. Backend changes are required.");
}
const activeJob = busy && operation.workspace === workspace && (operation.label.startsWith("Resume:") || operation.label.startsWith("Analysis:")) ? operation : null;
const selectedContext = <div className="gc-selectors" aria-label="Global working context">
<label className="gc-selector"><span><Layers size={15}/>Workspace</span><select id="global-workspace" aria-label="Global workspace" value={workspace} onChange={e => changeWorkspace(e.target.value)} aria-describedby={busy ? "operation-lock" : undefined} disabled={busy || availability === "no-workspaces"}><option value="">Choose a workspace</option>{workspaces.map(w => <option key={w.id} value={w.id}>{w.name}</option>)}</select><small><span className="cs-source">{sources.workspace}</span> Core, sessions and administration</small></label>
<label className="gc-selector"><span><Cpu size={15}/>AI interaction model</span><select id="global-model" aria-label="Global AI model" value={modelId} onChange={e => changeModel(e.target.value)} aria-describedby={busy ? "operation-lock" : undefined} disabled={busy || availability === "no-models"}><option value="">Choose a model</option>{models.map(m => <option key={m.id} value={m.id}>{m.name}{m.metadata ? "" : " · Core only"}</option>)}</select><small><span className="cs-source">{sources.model}</span> {model ? `${model.provider} · ${model.metadata ? "Core + administration" : "Core only"}` : "Independent of the workspace"}</small></label>
</div>;
const contextShelf = <section className={`gc-shelf cs-shelf-${variant}`}>
<button className="gc-shelf-trigger" aria-expanded={shelfOpen || !ready} aria-controls="context-shelf" onClick={() => { if (ready) setShelfOpen(!shelfOpen); }}>
{variant === "A" && <><SlidersHorizontal size={18}/><span className="cs-summary-compact"><small>WORKING CONTEXT</small><strong>{currentWorkspace?.name ?? "Choose workspace"}<span className="gc-separator">/</span>{model?.name ?? "Choose model"}</strong></span></>}
{variant === "B" && <div className="cs-summary-ribbon"><span><Layers size={18}/><span><small>WORKSPACE</small><strong>{currentWorkspace?.name ?? "Choose workspace"}</strong></span></span><span><Cpu size={18}/><span><small>AI INTERACTION MODEL</small><strong>{model?.name ?? "Choose model"}</strong></span></span></div>}
{variant === "C" && <div className="cs-summary-heading"><span><small>THOTHII / WORKING CONTEXT</small><strong>{currentWorkspace?.name ?? "Choose workspace"}</strong></span><span className="cs-model-chip"><Cpu size={16}/>{model?.name ?? "Choose model"}</span></div>}
<span className="cs-context-action">{busy ? <LockKeyhole size={16}/> : <SlidersHorizontal size={16}/>}<span>{busy ? "Locked" : !ready ? "Required" : shelfOpen ? "Close" : "Change context"}</span><ChevronDown size={16}/></span>
</button>
{(shelfOpen || !ready) && <div id="context-shelf" className="gc-shelf-body"><div className="cs-context-intro"><p className="rv-eyebrow">Working preferences</p><h2>{variant === "C" ? "Where you work. Which AI you use." : "Choose once, return to the same context."}</h2><p>Your last choices take precedence over installation defaults. Workspace and model are remembered independently.</p></div>{selectedContext}<div className="cs-context-footer"><span>{busy ? "Both selectors are locked until the current operation ends." : "Saved on this browser for the prototype’s demo user."}</span>{ready && <Button onClick={() => setShelfOpen(false)}>Done</Button>}</div></div>}
</section>;
const nav = (p: Page) => {
const Icon = icons[p];
return <a key={p} href={`?thoth_route=${p === "core" || p === "sessions" ? "analysis" : "administration"}/${p}&variant=${variant}&thoth_workspace=${workspace}`} aria-current={page === p && ready ? "page" : undefined} aria-disabled={!ready} tabIndex={ready ? 0 : -1} onClick={e => { e.preventDefault(); navigate(p); }}><Icon size={17}/><span>{titles[p]}</span>{p === page && ready && <ChevronRight size={14}/>}</a>;
};
const gate = <section className="gc-gate"><span className="gc-gate-icon"><LockKeyhole size={28}/></span><p className="rv-eyebrow">ThothII · Start here</p><h1>Choose your working context</h1><p>Select a workspace and an AI model in the panel above. Both are required before opening analytical work or administration.</p><ol><li className={currentWorkspace && availability !== "no-workspaces" ? "done" : ""}><span>{currentWorkspace ? <Check size={16}/> : "1"}</span><div><strong>Where you work</strong><small>{currentWorkspace?.name ?? "Workspace, data, knowledge and sessions"}</small></div></li><li className={model && availability !== "no-models" ? "done" : ""}><span>{model ? <Check size={16}/> : "2"}</span><div><strong>Which AI you use</strong><small>{model?.name ?? "A single model preference for the application"}</small></div></li></ol>{["unavailable","no-models","no-workspaces"].includes(availability) && <div className="gc-warning" role="alert"><strong>{availability === "unavailable" ? "The selected provider is unavailable" : availability === "no-models" ? "No usable model is configured" : "No workspace is available"}</strong><p>No activity can start. Ask the installation operator to check the catalog, credentials or workspace registry. No automatic model fallback.</p></div>}<p className="gc-gate-foot">Your data stays inside the selected workspace. AI models belong to the installation.</p></section>;
const pageHeader = (title: string, subtitle: string, eyebrow = "Analysis") => <header className="gc-page-heading"><p className="rv-eyebrow">{eyebrow} <span>/ {page === "pi" ? "Installation" : currentWorkspace?.name}</span></p><h1>{title}</h1><p>{subtitle}</p></header>;
const resumeReview = resumeTarget && <section className="gc-resume" role="region" aria-label="Resume review"><p className="rv-eyebrow">Before resuming</p><h2 lang="it">{resumeTarget.title}</h2><dl><div><dt>Original workspace, retained</dt><dd>{workspaces.find(w => w.id === resumeTarget.workspace)?.name} · <code>{resumeTarget.revision}</code></dd></div><div><dt>Model for this resume</dt><dd>{model?.name} <small>From your global selection, not the session’s previous model</small></dd></div></dl>{resumeTarget.workspace !== workspace && <p className="gc-warning">Opening this session will switch the global workspace to its original workspace. Your model selection will not change.</p>}{availability === "needs-preprocessing" && <p className="gc-warning">Workspace preprocessing is required before resume.</p>}<div className="rv-actions"><Button primary disabled={busy || !ready || availability === "needs-preprocessing"} onClick={() => resume(resumeTarget)}>Resume with {model?.name ?? "selected model"}</Button><Button onClick={() => setResumeTarget(null)}>Cancel</Button></div></section>;
function submitQuestion() {
if (!ready || !question.trim() || availability === "needs-preprocessing" || busy) return;
const title = question.trim(); setActive({ id: `demo-${Date.now()}`, title, workspace, revision: "a71c9f2", status: "paused", lastModel: modelId, phase: 1 });
setQuestion(""); setDirty(false); startJob(`Analysis: ${title}`, "llm");
}
const core = <>{pageHeader(active ? active.title : "From a question to a trusted answer", active ? "Persisted artifacts and reviewer decisions guide this session." : "Ask about your data. Review the evidence, approve the interpretation, then build the query.")}
{availability === "needs-preprocessing" && <div className="gc-warning"><strong>Prepare this workspace before starting the core</strong><p>Workspace and model are selected. Administration remains available so you can complete preprocessing.</p><Button onClick={() => navigate("workspace")}>Open workspace readiness</Button></div>}
{active ? <div className="gc-analysis"><div className="gc-session-context"><LockKeyhole size={15}/><strong>{currentWorkspace?.name}</strong><span>Session revision {active.revision}</span><span>{active.status === "archived" ? "Read-only archive" : activeJob ? `Run model: ${models.find(m => m.id === activeJob.model)?.name}` : `Next AI operation: ${model?.name}`}</span></div><div className="gc-workflow"><ol>{["Question", "Schema linking", "SQL draft", "Validation"].map((t,i) => <li key={t} className={i === 1 ? "current" : ""}><span>{String(i+1).padStart(2,"0")}</span>{t}</li>)}</ol><small>Workflow excerpt · 8 phases in production</small></div><div className="gc-artifact-layout"><section className="gc-artifact"><nav aria-label="Session artifacts">{["Question","Schema","SQL"].map(t => <button key={t} aria-current={artifact === t ? "page" : undefined} onClick={() => setArtifact(t)}>{t}</button>)}</nav><div><p className="rv-eyebrow">Persisted artifact · example</p><h2>{artifact === "Question" ? "Domanda analitica" : artifact === "Schema" ? "Collegamento allo schema" : "Query proposta"}</h2>{artifact === "SQL" ? <pre><code>{`SELECT COUNT(DISTINCT patient_id)\nFROM ${currentWorkspace?.schema}.visits\nWHERE visited_at >= DATE '2025-01-01'\n AND visited_at < DATE '2026-01-01';`}</code></pre> : <><p lang="it">{artifact === "Question" ? active.title : "La relazione tra pazienti e visite definisce la popolazione osservata. Il conteggio deve mantenere distinti i pazienti."}</p><div className="gc-evidence-line"><FileText size={16}/><span>Evidence: popolazione di riferimento</span></div><p className="rv-small">Synthetic illustration, not generated from a database.</p></>}</div></section><section className="gc-review"><p className="rv-eyebrow">Human review</p><h2>{active.status === "archived" ? "Retained decisions" : activeJob ? "AI operation in progress" : "Confirm the interpretation"}</h2><p>{active.status === "archived" ? "This archive can be inspected, but its workflow cannot currently be resumed." : activeJob ? "Workspace and model are locked until this operation finishes. No other operation can start in the meantime." : "Check the proposed population and time range before proceeding."}</p>{active.status !== "archived" && <Button primary disabled={busy} onClick={() => startJob(`Analysis: ${active.title}`, "llm")}>{activeJob ? "Waiting for result" : `Approve & continue with ${model?.name}`}</Button>}<p className="rv-small">{active.status === "archived" ? "Archived and finalized sessions remain read-only." : "Model output proposes. The reviewer decides."}</p></section></div></div> : <div className="gc-core-start"><section className="gc-question"><div className="gc-question-heading"><span className="gc-symbol"><MessageSquare size={23}/></span><div><h2>Start an analytical question</h2><p>{currentWorkspace?.database} · Evidence and Memory in this workspace</p></div></div><form onSubmit={e => { e.preventDefault(); submitQuestion(); }}><label htmlFor="analysis-question" className="rv-eyebrow">Your question</label><textarea id="analysis-question" rows={4} value={question} placeholder="Quanti pazienti hanno almeno una visita di follow-up nel 2025?" onChange={e => { setQuestion(e.target.value); setDirty(!!e.target.value); }}/><div className="gc-composer-footer"><span><Cpu size={15}/>{model?.name}<small>Global model</small></span><button className="rv-button rv-primary" type="submit" disabled={!question.trim() || availability === "needs-preprocessing" || busy}>Start analysis<ArrowRight size={16}/></button></div></form></section><section className="gc-method"><p className="rv-eyebrow">A reviewable path</p><div><span><strong>01</strong>Ask a question</span><ChevronRight size={15}/><span><strong>02</strong>Review context</span><ChevronRight size={15}/><span><strong>03</strong>Validate the query</span></div><p>Every analytical result is backed by persisted artifacts and explicit review decisions.</p></section></div>}
</>;
const rows = page === "memory" ? cards[workspace] ?? inventory.memory : admin ? inventory[page] : [];
const selected = selection[scope] ?? rows[0]?.id;
const item = rows.find(r => r.id === selected);
const hasList = ["database","memory","evidence"].includes(page);
const list = <div className="rv-index"><div className="rv-index-title"><h2>{admin ? modules[page].noun : ""}</h2><span>{rows.length}</span></div><label className="rv-search"><Search size={16}/><input aria-label="Search records" placeholder="Search records" value={filters[scope] ?? ""} onChange={e => setFilters(f => ({ ...f, [scope]: e.target.value }))}/></label><div className="rv-records">{rows.filter(r => r.title.toLowerCase().includes((filters[scope] ?? "").toLowerCase())).map(r => <button key={r.id} className="rv-record" aria-current={selected === r.id ? "true" : undefined} onClick={() => { if (!canLeave()) return; setSelection(s => ({ ...s, [scope]: r.id })); setFocused(true); setEditing(false); setDirty(false); }}><small>{r.category}</small><strong>{r.title}</strong><span>{r.meta}</span><ChevronRight size={15}/></button>)}</div></div>;
function changeSection(n: number) { if (!canLeave()) return; setSection(n); setEditing(false); setDirty(false); setFocused(true); }
const sections = admin && <nav className="rv-step-nav" aria-label="Management sections">{modules[page].sections.map((s,i) => <button key={s} aria-current={section === i ? "page" : undefined} onClick={() => changeSection(i)}>{<span>{String(i+1).padStart(2,"0")}</span>}{s}</button>)}</nav>;
const contentProps: ContentProps | null = admin && ready && currentWorkspace && model ? {
surface: page, workspace: currentWorkspace, modelName: model.name, providerName: model.provider, scenario, item, section, editing,
onEdit: () => setEditing(true), onDirty: setDirty, onCancel: () => { if (canLeave()) { setEditing(false); setDirty(false); } },
onSave: updated => { if (updated) { setCards(c => ({ ...c, [workspace]: selected === "new" ? [updated, ...rows] : rows.map(r => r.id === updated.id ? updated : r) })); setSelection(s => ({ ...s, [scope]: updated.id })); } setDirty(false); setEditing(false); setNotice("Saved in this workspace’s demo data only."); },
onDelete: () => { const rest = rows.filter(r => r.id !== selected); setCards(c => ({ ...c, [workspace]: rest })); setSelection(s => ({ ...s, [scope]: rest[0]?.id ?? "" })); },
connectionValues: connections[workspace], onConnectionSave: values => { setConnections(c => ({ ...c, [workspace]: values })); setEditing(false); setDirty(false); setNotice("Connection saved in memory only. No real credentials were used."); },
onNavigate: navigate, onSection: changeSection, onScenario: setScenario, onNotice: setNotice,
onOperation: label => startJob(label, /generation|sensitivity|model test/i.test(label) ? "llm" : /preprocessing|index/i.test(label) ? "embedding" : "system"),
} : null;
const incompatible = page === "database" && section === 2 && !model?.metadata;
const detail = <>{incompatible && <div className="gc-warning" role="alert"><strong>{model?.name} is not enabled for metadata AI</strong><p>Select a model enabled for core and administration in the global selector. No other model will be used automatically.</p><Button onClick={() => { setShelfOpen(true); requestAnimationFrame(() => document.getElementById("global-model")?.focus()); }}>Change global model</Button></div>}<fieldset className="gc-content-fieldset" disabled={busy || incompatible}>{contentProps && <Content key={`${scope}:${selected}:${section}:${editing}`} {...contentProps}/>}</fieldset></>;
const administration = admin && <>{pageHeader(modules[page].title, modules[page].subtitle, "Administration")}<div className="gc-admin-context"><span>{page === "pi" ? "Installation scope: applies to all workspaces" : `Workspace: ${currentWorkspace?.name}`}</span><span>{page === "pi" ? "Global LLM selection is not configuration authoring" : `Global LLM: ${model?.name}`}</span>{page === "memory" && <Button primary disabled={busy} onClick={() => { if (busy || !canLeave()) return; setSelection(s => ({ ...s, [scope]: "new" })); setEditing(true); setFocused(true); setDirty(false); setSection(0); }}>New card</Button>}</div>
{page === "workspace" && <p className="gc-footnote">Preprocessing uses the installation embedding model. Selecting an interaction LLM does not change index compatibility.</p>}
{<div className="rv-operations"><div className="gc-operation-intro"><ShieldCheck size={19}/><span>{page === "pi" ? "Installation diagnostics" : `${currentWorkspace?.name} · Operational context`}</span><strong>{scenario === "ready" ? "Ready for review" : scenario}</strong></div><div className="rv-operation-body">{sections}<div className="rv-operation-detail">{hasList && section === 0 && <details className="rv-choose-record"><summary>Choose record<span>{item?.title ?? "New record"}</span></summary>{list}</details>}{detail}</div></div></div>}
</>;
return <><div className={`rv-host rv-host-${width} gc-host cs-host ${portalOpen ? "gc-portal-open" : ""}`}><header className="rv-topbar"><button aria-label="Toggle portal navigation" onClick={() => setPortalOpen(!portalOpen)}><Menu size={20}/></button><strong>Omics <span>Portal</span></strong><span className="rv-topbar-divider"/><span className="rv-topbar-location">Clinical data & research</span><span className="rv-topbar-user">Administrator <span className="rv-avatar">AD</span></span></header><div className="gc-host-body"><aside className="gc-portal" aria-label="Illustrative portal navigation"><p className="rv-eyebrow">Portal</p>{[[LayoutDashboard,"Overview"],[Users,"Patients"],[Database,"Clinical data"],[Settings2,"ThothII"]].map(([Icon,label]) => { const I = Icon as typeof Menu; return <div key={String(label)} className={label === "ThothII" ? "current" : ""}><I size={18}/><span>{String(label)}</span></div>; })}<small>Host shell preview</small></aside><div className="gc-mount"><div className="gc-brandline"><span className="gc-brand">Thoth<span>II</span></span><span>Clinical analytics workbench</span><small>Context shelf review · V4</small></div><div className={`gc-app gc-variant-${variant}`}><div className="gc-global-top">{contextShelf}</div>{operation && <section className="gc-operation-strip" aria-label="Current operation"><div><p className="rv-eyebrow">{busy ? "Operation running" : `Operation ${operation.state}`}</p><strong>{operation.label}</strong><small>{workspaces.find(w => w.id === operation.workspace)?.name ?? "Installation"} · {models.find(m => m.id === operation.model)?.name ?? operation.model}</small><p id="operation-lock">{busy ? "Workspace and model are locked. Finish this operation before starting another." : "Workspace and model are available again."}</p></div>{busy && <div className="rv-actions"><Button onClick={() => setOperation({ ...operation, state: "complete" })}>Demo: finish</Button><Button onClick={() => setOperation({ ...operation, state: "cancelled" })}>Demo: stop</Button><Button onClick={() => setOperation({ ...operation, state: "failed" })}>Demo: fail</Button></div>}</section>}<div className="gc-body"><aside className="gc-rail"><button className="gc-mobile-nav" onClick={() => setNavOpen(!navOpen)} aria-expanded={navOpen}><Menu size={17}/>Sessions & navigation<ChevronDown size={16}/></button><div className={`gc-navigation ${navOpen ? "open" : ""}`}><nav className="cs-core-nav" aria-label="Core navigation">{nav("core")}</nav><SessionRail workspace={workspace} ready={ready} busy={busy} activeId={active?.id} canReadAll={canReadAll} onOpen={s => { if (busy || !ready || !canLeave()) return; setNavOpen(false); if (s.status === "paused") setResumeTarget(s); else { resetDetail(); setQuestion(""); setActive(s); writeRoute({ ...route, page: "core" }); } requestAnimationFrame(() => mainRef.current?.focus()); }}/>{canReadAll && <details className="cs-admin-nav" open={variant === "A" ? true : undefined}><summary><Settings2 size={16}/>Administration<ChevronDown size={14}/></summary><nav aria-label="Administration navigation">{(["database","memory","evidence","workspace","pi"] as Page[]).map(nav)}</nav></details>}</div></aside><main ref={mainRef} tabIndex={-1} className={`gc-main rv-variant-${variant}`} aria-label={ready ? titles[page] : "Context required"}>{notice && <div className="rv-notice" role="status"><span>{notice}</span><button aria-label="Dismiss notice" onClick={() => setNotice("")}><X size={16}/></button></div>}{dirty && <div className="rv-dirty" role="status">Unsaved draft. Save or discard before changing workspace or page.</div>}{!ready ? gate : <>{resumeReview}{page === "core" && active && <div className="gc-new-question"><Button disabled={busy} onClick={() => { if (busy || !canLeave()) return; setActive(null); setQuestion(""); setDirty(false); setNotice("Ready for a new question in the selected workspace."); }}>New question<ArrowRight size={15}/></Button></div>}{page === "core" || page === "sessions" ? core : administration}</>}<footer className="gc-page-footer"><span>Prototype · Synthetic content only</span><span>{ready ? "Context applies across ThothII" : "Core and administration are locked"}</span></footer></main></div></div></div></div></div>
{import.meta.env.DEV && <aside className="rv-lab gc-lab" aria-label="Prototype review controls"><div className="rv-lab-main"><span className="rv-lab-tag">CONTEXT SHELF · V4</span><button aria-label="Previous variant" onClick={() => cycle(-1)}><ArrowLeft size={17}/></button><select aria-label="Layout variant" value={variant} onChange={e => { if (canLeave()) { writeRoute({ ...route, variant: e.target.value as Variant }, true); resetDetail(); setQuestion(""); setShelfOpen(!ready); } }}>{Object.entries(variants).map(([k,v]) => <option key={k} value={k}>{k} · {v}</option>)}</select><button aria-label="Next variant" onClick={() => cycle(1)}><ArrowRight size={17}/></button><span className="rv-lab-help">No server writes</span></div><details><summary>Review scenarios & state</summary><div className="rv-lab-options"><label>Host width<select aria-label="Host width" value={width} onChange={e => setWidth(e.target.value)}><option value="full">Available width</option><option value="compact">960 px host</option><option value="phone">390 px host</option></select></label><label>Availability<select aria-label="Availability scenario" disabled={busy} value={availability} onChange={e => { if (canLeave()) { setAvailability(e.target.value as Availability); resetDetail(); } }}><option value="available">Available</option><option value="unavailable">Provider unavailable</option><option value="no-workspaces">No workspaces</option><option value="no-models">No models</option><option value="needs-preprocessing">Needs preprocessing</option></select></label><Button disabled={busy} onClick={() => { if (busy || !canLeave()) return; setOperation(null); writeRoute({ ...route, workspace: "" }); setModelId(""); resetDetail(); setActive(null); setQuestion(""); setResumeTarget(null); setShelfOpen(true); }}>Clear current context</Button><Button disabled={busy || !ready} onClick={() => { if (!busy && canLeave()) { setResumeTarget(sessions[2]); setNavOpen(false); } }}>Demo: Genetics session link</Button><Button disabled={busy} onClick={() => { if (busy || !canLeave()) return; if (!forget()) { setNotice("Browser storage could not be cleared."); return; } location.reload(); }}>Forget saved choices</Button><Button disabled={busy} onClick={() => { if (busy || !canLeave()) return; setCanReadAll(!canReadAll); writeRoute({ ...route, page: "core" }); resetDetail(); }}>Demo access: {canReadAll ? "Administrator" : "Analyst"}</Button><p>Workspace source: {sources.workspace} · Model source: {sources.model}</p><p>Page: {page} · Workspace: {workspace || "none"} · Model: {model?.name ?? "none"} · Gate: {ready ? "open" : "closed"} · Draft: {dirty ? "unsaved" : "clean"} · Operation: {operation?.state ?? "idle"}</p><p>Only explicit workspace/model choices persist on this browser. Reload resets simulated operations and drafts. Defaults are illustrative. V1/V2/V3 are preserved on ports 5174/5175/5176.</p></div></details></aside>}
</>;
}
+151
View File
@@ -0,0 +1,151 @@
# Context shelf review V4
2026-09-12. **C is the chosen direction; C1/C2/C3 remain under review.**
Throwaway, development-only UI. No production API calls or server configuration
changes. Existing V1, V2 and V3 prototypes are retained.
## Run
From `frontend/`, run `npm run prototype:context-shelf`.
- C1: <http://127.0.0.1:5177/?thoth_route=analysis/core&variant=A>
- C2: <http://127.0.0.1:5177/?thoth_route=analysis/core&variant=B>
- C3: <http://127.0.0.1:5177/?thoth_route=analysis/core&variant=C>
The URL keys A/B/C identify the three descendants of C in this new review, not
the old context-bar/context-rail/context-shelf alternatives. Switch in the bottom
review bar without reloading, or with arrow keys outside interactive controls.
## What this iteration compares
All variants have one collapsible panel at the top of the entire ThothII app.
The panel is collapsed when valid selections have been resolved, but opens when
the required context is incomplete. A visible summary always identifies workspace
and interaction model. Opening the panel reveals both selectors and their origins.
| Proposal | Top panel | Session rail |
| --- | --- | --- |
| C1: Compact strip | A compact workspace/model summary; two side-by-side selectors when expanded | Administration precedes the session tabs, closest to the previous navigation order |
| C2: Context ribbon | Separate labelled workspace/model areas aligned with the expanded fields | Sessions first, with compact divided rows; administration below |
| C3: Workspace heading | Workspace is the main heading, model a secondary read-only label; expanded panel has explanation beside stacked fields | Sessions first, with individually bounded rows and administration below |
On narrow containers, fields stack and sessions/navigation expand inline above
the main content. The illustrative left navigation belongs to Omics Portal,
not to the ThothII application. Warm surfaces, Manrope controls, Fraunces headings
and the red portal frame continue the prior Impeccable direction.
## Session management restored to the right rail
- Remove the central “Continue where you left off” area and its separate session
navigation link. Access sessions from the right rail in every proposal.
- Restore My sessions / All sessions tabs, with an explicit selected tab and
keyboard Left/Right/Home/End navigation. Search and paused/archived groupings
are represented with synthetic records.
- All sessions is an authorized administrator view, not a cross-workspace selector.
Both tabs use the globally selected workspace. The administrator fixture adds
a colleague-owned archived record; the analyst fixture hides All sessions and
administration. Actual production permissions remain backend-owned.
- Resume stays behind a review of the session's original workspace/revision and
the globally selected model. Genuine archives remain read-only pending the
already-asked archive/restoration clarification.
- This is a layout study, not authorization to remove original session grouping,
rename, archive, bulk operations or other existing behavior from production.
Those must be retained and verified in the eventual implementation.
## Default and last-choice audit
The concepts already exist in current code, but in different places:
| Value | Existing source | Finding |
| --- | --- | --- |
| Installation workspace default | `Settings.workspace`, read from `SETTINGS_FILE`, normally `backend/data/settings.json` in local development | The inspected local file has `workspace: "psd"`. It is not a newly introduced YAML field. |
| Session model default | `modelCatalog.defaults.session` in the installation YAML | The PSD configuration has `zai/glm-5.3`. |
| Metadata model default | `modelCatalog.defaults.metadataGeneration` in the same YAML | The PSD configuration also has `zai/glm-5.3`; the schema allows the two defaults to differ. |
| Last personal choice | `frontend/src/workspaces/preferences.ts` | Currently module-level memory only, not durable browser storage. A page reload loses it. |
Evidence: `backend/src/settings/settings-store.ts`, `backend/src/routes/settings.ts`,
`backend/src/app.ts` (settings resolution), `backend/src/config.ts` (file location),
`frontend/src/shell/SteerInput.tsx`, `frontend/src/workspaces/preferences.ts`,
`tools/tht/internal/config/model_catalog.go`, and `deploy/psd/thothii-installation.yaml`.
The backend currently falls back to the first active registry workspace when the
settings file has no workspace. The frontend validates the returned workspace
against selectable revisions. This fallback is not an explicitly authored default
in the workspace repository. V4 does not invent a new YAML field or modify this
production behavior; its fixtures represent an explicit configured default.
The actual deployed settings and registry were not inspected. The local value
`psd` must not be confused with the synthetic prototype ID `psd-clinical`.
For the unified model preference, V4 represents the existing common PSD default.
If session and metadata defaults diverge in another installation, the production
spec must choose a single initial interaction preference explicitly; it must not
silently switch to a second model on entering administration. Eligibility gates
from V3 continue to prevent unsupported metadata AI operations.
## Requested resolution rule
Resolve workspace and model **independently**:
1. Use that field's last explicit user choice, if present.
2. Otherwise use its installation default.
3. Validate the resolved ID. If a remembered choice is unavailable, leave that
field unselected, explain the problem and open the panel. Do not silently
select a different workspace or model.
Defaults populate the selectors, so valid defaults satisfy the required-context
gate without an extra confirmation. Resolving a default does not mark it as an
explicit user choice. Changing just the model does not freeze the default workspace
as a remembered choice. Cancelling a dirty-workspace transition does not persist it.
An explicit confirmed resume adopts and remembers the session's workspace; the
global model remains independent. Page-history navigation does not restore an
obsolete workspace selection. Last-choice/default resolution takes precedence
over a stale workspace query parameter on a fresh visit.
## Prototype-only persistence
Only selected identifiers are persisted, using localStorage key
`thothii:prototype:context-shelf-v4:demo-user`. It is separate from production keys
and shared by these three variants on port 5177. Reload or reopening the same
browser restores the choices. There is no cross-device or authenticated-user
profile synchronization. No secrets, records or clinical data are stored.
The review bar exposes Forget saved choices, which removes only this prototype's
key and reloads with defaults. If browser storage is unavailable, selections work
for the current visit and the UI reports that they could not be remembered.
Drafts, record edits and the simulated operation remain ephemeral.
Production persistence must be scoped to installation and authenticated user;
the fixed demo-user key is not a production storage design. It must not update
shared server installation defaults with a person's choices.
## Retained operation rule
Exactly one core or administrative operation at a time. Both selectors and all
other operation starts are disabled until the running operation completes, stops
or fails. Browsing another page does not release this lock. This prototype does
not implement backend exclusion, reconnect recovery or persistent running jobs.
## Verification
- Isolated strict TypeScript check and whitespace check passed.
- C1/C2/C3: first visit resolves configured defaults without saving them as user
choices; model-only choice retains independent workspace default; reload restores
the last model and workspace; source labels reflect the correct precedence.
- C1/C2/C3: session tabs, owner filtering, keyboard tab navigation, removal of the
former central recent-sessions area, and operation locking/unlocking verified.
- Additional checks: unavailable saved workspace fails closed; rejected context
change preserves draft and storage; forgetting choices restores defaults;
analyst fixture hides All sessions and administration.
- 96 Chromium layout checks: 3 variants, 4 viewport widths (1600/1280/960/390),
open/closed panel plus core and all five administrative pages. No root overflow
or runtime errors; page and rail containment checked on the content pages.
- 390 px embedded host inside a 1600 px browser also checked, with a 368 px main
region and no host overflow. Desktop and mobile screenshots visually reviewed.
- No physical-device, Safari/WebKit, real Omics/Bootstrap or deployed-server
acceptance is claimed. Core and administrative actions use synthetic fixtures.
Next: choose the V4 arrangement, resolve any outstanding domain questions, then
continue the agreed to-spec / to-tickets process on Gitea. No new implementation
agents or tickets were started during this prototype review. Final integration
remains on the server, not the local Omics Portal checkout.
@@ -0,0 +1,17 @@
import { useRef, useState, type KeyboardEvent } from "react";
import { ChevronRight, FolderClock, Search } from "lucide-react";
import { sessions, type Session } from "./model";
export function SessionRail({ workspace, ready, busy, activeId, canReadAll, onOpen }: { workspace: string; ready: boolean; busy: boolean; activeId?: string; canReadAll: boolean; onOpen: (s: Session) => void }) {
const [scope, setScope] = useState<"mine" | "all">("mine");
const [query, setQuery] = useState("");
const tabs = useRef<Array<HTMLButtonElement | null>>([]);
const effectiveScope = canReadAll ? scope : "mine";
const rows = ready ? sessions.filter(s => s.workspace === workspace && (effectiveScope === "all" || s.owner === "You") && s.title.toLocaleLowerCase().includes(query.toLocaleLowerCase())) : [];
function onKey(e: KeyboardEvent<HTMLButtonElement>, index: number) {
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return;
e.preventDefault(); const target = e.key === "Home" ? 0 : e.key === "End" ? 1 : 1-index;
setScope(target === 0 ? "mine" : "all"); tabs.current[target]?.focus();
}
return <section className="cs-sessions" aria-label="Session navigation"><div className="cs-session-heading"><FolderClock size={17}/><h2>Sessions</h2><span>{rows.length}</span></div>{canReadAll ? <div className="cs-session-tabs" role="tablist" aria-label="Session scope">{(["mine","all"] as const).map((value,i) => <button key={value} ref={node => { tabs.current[i] = node; }} id={`scope-${value}`} type="button" role="tab" aria-controls="session-list" aria-selected={effectiveScope === value} tabIndex={effectiveScope === value ? 0 : -1} disabled={!ready} onClick={() => setScope(value)} onKeyDown={e => onKey(e,i)}>{value === "mine" ? "My sessions" : "All sessions"}</button>)}</div> : <p className="cs-mine-label">My sessions</p>}<div id="session-list" role={canReadAll ? "tabpanel" : undefined} aria-labelledby={canReadAll ? `scope-${effectiveScope}` : undefined}>{effectiveScope === "all" && <p className="cs-scope-note">Administrator view · Selected workspace</p>}<label className="rv-search"><Search size={15}/><input aria-label="Search sessions" placeholder="Search sessions" disabled={!ready} value={query} onChange={e => setQuery(e.target.value)}/></label>{(["paused","archived"] as const).map(status => <div className="cs-session-group" key={status}><p className="rv-eyebrow">{status === "paused" ? "In progress" : "Archived"}<span>{rows.filter(s => s.status === status).length}</span></p>{rows.filter(s => s.status === status).map(s => <button key={s.id} className="cs-session-row" disabled={busy || !ready} aria-current={s.id === activeId ? "true" : undefined} onClick={() => onOpen(s)}><span className="cs-session-mark">{status === "paused" ? String(s.phase).padStart(2,"0") : "✓"}</span><span><strong lang="it">{s.title}</strong><small>{effectiveScope === "all" ? `${s.owner} · ` : ""}{status === "paused" ? "Paused · Can resume" : "Read-only archive"}</small></span><ChevronRight size={13}/></button>)}</div>)}{!rows.length && <p className="cs-session-empty">{!ready ? "Choose a valid workspace and model to access sessions." : query ? "No sessions match this search." : "No sessions in this workspace yet."}</p>}</div></section>;
}
@@ -0,0 +1,2 @@
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/><title>ThothII · Context shelf variants</title><link rel="preconnect" href="https://fonts.googleapis.com"/><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/><link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Fraunces:opsz,wght@9..144,500;9..144,600&display=swap" rel="stylesheet"/></head><body><div id="root"></div><script type="module" src="/main.tsx"></script></body></html>
@@ -0,0 +1,7 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import "../administration-review/review.css";
import "../application-context/context.css";
import "./shelf.css";
createRoot(document.getElementById("root")!).render(<StrictMode><App /></StrictMode>);
@@ -0,0 +1,8 @@
import { sessions as originalSessions } from "../application-context/model";
export { models, modules, readLocation, workspaces } from "../application-context/model";
export type { Page, Route, Session, Job } from "../application-context/model";
export const variants = { A: "C1 · Compact strip", B: "C2 · Context ribbon", C: "C3 · Workspace heading" };
export const sessions = [
...originalSessions.map(s => ({ ...s, owner: "You" })),
{ id: "colleague-psd", title: "Popolazione osservata per anno", workspace: "psd-clinical", revision: "a71c9f2", status: "archived" as const, lastModel: "zai/glm-5.3", phase: 8, owner: "Elena Rossi" },
];
@@ -0,0 +1,33 @@
import { models, workspaces } from "./model";
// Prototype-only storage. Never read or overwrite production preference keys.
export const PREFERENCE_KEY = "thothii:prototype:context-shelf-v4:demo-user";
// Synthetic equivalents of settings.workspace and modelCatalog.defaults.session.
// No installation configuration is authored or changed by this prototype.
export const defaults = { workspace: "psd-clinical", model: "zai/glm-5.3" };
type Saved = { workspace?: string; model?: string };
export function readSaved(): Saved {
try {
const value = JSON.parse(localStorage.getItem(PREFERENCE_KEY) ?? "{}");
if (!value || typeof value !== "object") return {};
return {
...(typeof value.workspace === "string" && value.workspace ? { workspace: value.workspace } : {}),
...(typeof value.model === "string" && value.model ? { model: value.model } : {}),
};
} catch { return {}; }
}
export function resolvePreferences() {
const saved = readSaved();
const wantedWorkspace = saved.workspace ?? defaults.workspace;
const wantedModel = saved.model ?? defaults.model;
const workspace = workspaces.some(w => w.id === wantedWorkspace) ? wantedWorkspace : "";
const model = models.some(m => m.id === wantedModel) ? wantedModel : "";
return { workspace, model, sources: { workspace: saved.workspace ? "Last choice" : "Installation default", model: saved.model ? "Last choice" : "Installation default" }, invalid: !workspace || !model };
}
export function remember(patch: Saved): boolean {
try { localStorage.setItem(PREFERENCE_KEY, JSON.stringify({ ...readSaved(), ...patch })); return true; }
catch { return false; }
}
export function forget(): boolean {
try { localStorage.removeItem(PREFERENCE_KEY); return true; } catch { return false; }
}
@@ -0,0 +1,97 @@
/* V4: three descendants of C. All context controls expand inline above the full
application; none occupy the session rail. The existing product typography and
red Omics host are retained. Structure changes, not palette swaps. */
.cs-host .gc-brandline > small { font-size: .62rem; }
.cs-host .gc-shelf-trigger { min-height: 74px; gap: 20px; }
.cs-host .gc-shelf-trigger > span:nth-child(2) { flex: unset; }
.cs-host .cs-summary-compact { flex: 1 !important; min-width: 0; }
.cs-context-action { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 8px; margin-left: auto; color: var(--rv-red-dark); font-size: .75rem; font-weight: 700; }
.cs-context-action > span { white-space: nowrap; }
.cs-host .gc-shelf-body { padding: 26px; }
.cs-context-intro { margin-bottom: 22px; }
.cs-context-intro > h2 { margin: 8px 0 10px; font-size: 1.125rem; }
.cs-context-intro > p:last-child { color: var(--rv-muted); font-size: .8rem; line-height: 1.8; }
.cs-context-footer { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px; margin-top: 22px; border-top: 1px solid var(--rv-line); padding-top: 17px; }
.cs-context-footer > span { font-size: .72rem; color: var(--rv-muted); }
.cs-host .gc-selectors { max-width: none; }
.cs-host .gc-selector > small { display: flex; align-items: baseline; flex-wrap: wrap; gap: 7px; }
.cs-source { display: inline-block; color: var(--rv-ink); border: 1px solid var(--rv-line); background: var(--rv-paper); padding: 2px 6px; border-radius: 3px; font-size: .62rem; font-weight: 650; white-space: nowrap; }
.cs-summary-ribbon { flex: 1; min-width: 0; display: grid; grid-template-columns: minmax(0,1fr); gap: 14px; }
.cs-summary-ribbon > span { display: flex; align-items: center; gap: 12px; min-width: 0; }
.cs-summary-ribbon > span > span { min-width: 0; }
.cs-summary-ribbon svg { color: var(--rv-red-dark); }
.cs-summary-heading { display: flex; flex-wrap: wrap; align-items: center; gap: 18px; min-width: 0; flex: 1; }
.cs-summary-heading > span:first-child { min-width: 0; }
.cs-host .cs-summary-heading strong { font: 500 1.5rem/1.3 var(--rv-serif); letter-spacing: -.025em; }
.cs-model-chip { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 8px 12px; border: 1px solid var(--rv-line); background: var(--rv-paper); border-radius: 5px; font-size: .8rem; font-weight: 650; }
.cs-host .cs-shelf-C .gc-shelf-trigger { padding-block: 24px; }
.cs-host .gc-navigation { padding: 18px; }
.cs-host .gc-navigation.open { display: flex; flex-direction: column; }
.cs-core-nav { margin-bottom: 18px; }
.cs-host .gc-navigation .cs-core-nav { display: block; }
.cs-core-nav a { border: 1px solid var(--rv-line); background: var(--rv-paper); }
.cs-session-heading { display: flex; align-items: center; gap: 9px; margin: 4px 0 17px; }
.cs-host .cs-session-heading h2 { font-size: 1rem; letter-spacing: -.015em; }
.cs-session-heading > span { margin-left: auto; color: var(--rv-muted); font: .72rem var(--rv-mono); }
.cs-session-heading > svg { color: var(--rv-red-dark); }
.cs-sessions { min-width: 0; }
.cs-session-tabs { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); border-bottom: 1px solid var(--rv-line); margin-bottom: 17px; }
.cs-session-tabs button { border: 1px solid transparent; border-bottom: 2px solid transparent; background: transparent; padding: 12px 5px; font-size: .75rem !important; font-weight: 700 !important; }
.cs-session-tabs button[aria-selected=true] { background: var(--rv-red-soft); border-color: oklch(85% .025 23); border-bottom-color: var(--rv-red); color: var(--rv-red-dark); border-radius: 4px 4px 0 0; }
.cs-session-tabs button:hover { background: var(--rv-sunken); }
.cs-mine-label, .cs-scope-note { font-size: .7rem; margin: 9px 0 16px !important; color: var(--rv-muted); }
.cs-scope-note { color: var(--rv-amber); }
.cs-session-group { margin: 22px 0 12px; }
.cs-session-group > p { display: flex; justify-content: space-between; margin-bottom: 10px; }
.cs-session-group > p > span { color: var(--rv-muted); }
.cs-session-row { display: flex; width: 100%; gap: 10px; align-items: flex-start; text-align: left; background: transparent; padding: 14px 9px; border: 1px solid transparent; border-radius: 5px; }
.cs-session-row:hover { background: var(--rv-sunken); }
.cs-session-row[aria-current] { background: var(--rv-red-soft); border-color: oklch(83% .035 23); }
.cs-session-mark { display: grid; place-items: center; min-width: 23px; font: .65rem var(--rv-mono); color: var(--rv-red-dark); padding-top: 3px; }
.cs-session-row > span:nth-child(2) { min-width: 0; flex: 1; }
.cs-session-row strong { display: block; font-size: .8rem; line-height: 1.65; font-weight: 650; overflow-wrap: anywhere; }
.cs-session-row small { display: block; margin-top: 7px; font-size: .64rem; line-height: 1.6; color: var(--rv-muted); }
.cs-session-row > svg { margin-top: 5px; }
.cs-session-empty { font-size: .78rem; color: var(--rv-muted); line-height: 1.8; margin-block: 16px !important; }
.cs-admin-nav { margin-top: 26px; border-top: 1px solid var(--rv-line); padding-top: 17px; }
.cs-admin-nav > summary { display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: .75rem; font-weight: 750; min-height: 40px; list-style: none; }
.cs-admin-nav > summary > svg:last-child { margin-left: auto; }
.cs-admin-nav[open] > summary > svg:last-child { transform: rotate(180deg); }
.cs-admin-nav nav { margin-top: 10px; }
.cs-host .gc-navigation .cs-admin-nav nav { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); }
.cs-host .gc-variant-A .cs-admin-nav { order: 1; margin: 0 0 22px; padding: 10px 0; border-block: 1px solid var(--rv-line); }
.cs-host .gc-variant-A .cs-sessions { order: 2; }
.cs-host .gc-variant-A .cs-core-nav { order: 0; }
.cs-host .gc-variant-B .cs-sessions { padding: 18px 14px; border: 1px solid var(--rv-line); border-radius: 6px; background: var(--rv-paper); }
.cs-host .gc-variant-B .cs-session-row { border-radius: 0; border-bottom: 1px solid var(--rv-line); padding-inline: 0; }
.cs-host .gc-variant-B .cs-session-mark { display: none; }
.cs-host .gc-variant-C .cs-session-heading { padding: 15px 0; border-block: 1px solid var(--rv-line); }
.cs-host .gc-variant-C .cs-session-row { background: var(--rv-paper); border-color: var(--rv-line); margin-block: 8px; }
.cs-host .gc-variant-C .cs-session-row[aria-current] { background: var(--rv-red-soft); border-color: oklch(83% .035 23); }
.cs-host .gc-method { padding-top: 30px; }
@container application (min-width: 42rem) {
.cs-summary-ribbon { grid-template-columns: repeat(2,minmax(0,1fr)); }
.cs-summary-ribbon > span:last-child { padding-left: 25px; border-left: 1px solid var(--rv-line); }
.cs-host .cs-shelf-B .gc-shelf-trigger { min-height: 96px; }
.cs-host .cs-shelf-B .cs-context-intro { display: none; }
.cs-host .cs-shelf-C .gc-shelf-body { display: grid; grid-template-columns: minmax(0,.8fr) minmax(0,1.2fr); gap: 0 32px; }
.cs-host .cs-shelf-C .gc-selectors { grid-template-columns: minmax(0,1fr); }
.cs-host .cs-shelf-C .cs-context-footer { grid-column: 1 / -1; }
.cs-host .cs-shelf-C .cs-context-intro h2 { font: 500 1.5rem/1.4 var(--rv-serif); }
}
@container application (min-width: 55rem) {
.cs-host .gc-body, .cs-host .gc-variant-B .gc-body { grid-template-columns: minmax(0,1fr) 310px; }
.cs-host .gc-navigation { display: flex; flex-direction: column; }
.cs-host .gc-navigation .cs-admin-nav nav { grid-template-columns: minmax(0,1fr); }
}
@container application (max-width: 41.999rem) {
.cs-host .gc-shelf-trigger { gap: 13px; padding: 17px 16px; align-items: flex-start; }
.cs-host .cs-context-action > span { display: none; }
.cs-context-action { flex-wrap: nowrap; margin-top: 4px; }
.cs-host .gc-shelf-body { padding: 20px 16px; }
.cs-host .cs-summary-heading strong { font-size: 1.3rem; }
.cs-host .cs-summary-heading { gap: 12px; }
.cs-model-chip { padding: 6px 8px; font-size: .75rem; }
.cs-host .gc-brandline > small { display: none; }
}
@media (pointer: coarse) { .cs-session-tabs button, .cs-session-row, .cs-admin-nav > summary { min-height: 44px; } }