feat(shell): 30/55/15 layout, formatted activity log, instant resume, stop confirmation

Left panels 30vw / central 55% / right rail 15vw, minimal padding. Model-activity
log and documents render via .thot-prose; decisions become hairline rows with
semantic type chips. Resume switches the view optimistically (instant feedback).
The Stop button now asks for confirmation before interrupting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 18:15:48 +02:00
co-authored by Claude Opus 4.8
parent a719ad5990
commit f1b071d165
5 changed files with 102 additions and 33 deletions
+22 -6
View File
@@ -8,6 +8,7 @@ import { SessionMenu } from "./SessionMenu";
import { SessionDocumentsPanel } from "./SessionDocumentsPanel";
import { RenameDialog } from "./RenameDialog";
import { DeleteConfirmDialog } from "./DeleteConfirmDialog";
import { StopConfirmDialog } from "./StopConfirmDialog";
import { SteerInput, ComposerFooter } from "./SteerInput";
import { WorkflowBar } from "./WorkflowBar";
import { Pencil } from "lucide-react";
@@ -42,6 +43,7 @@ export function AppShell() {
const [archiveOpen, setArchiveOpen] = useState(false);
const [renameTarget, setRenameTarget] = useState<SessionSummary | null>(null);
const [deleteTarget, setDeleteTarget] = useState<SessionSummary | null>(null);
const [stopConfirm, setStopConfirm] = useState(false);
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
const [renameGroupTarget, setRenameGroupTarget] = useState<string | null>(null);
@@ -68,12 +70,16 @@ export function AppShell() {
});
}
async function doResume(id: string) {
const s = sessions.find((x) => x.id === id) ?? null;
// Optimistic switch: change to the session view IMMEDIATELY so the click feels
// instant (the resume POST spawns a Pi process and can take seconds). The
// working spinner shows straight away; the backend calls run after.
setPanelSession(null);
resetSession();
setActiveSessionId(id);
try {
await resumeSession(id);
setPanelSession(null);
setActiveSessionId(id);
// Optimistic phase paint on resume: colour the re-entry phase immediately,
// before the first gate arrives (mirrors the F1 paint for new questions).
// Optimistic phase paint: colour the re-entry phase before the first gate.
// The manifest's `phase` is the 1-based current phase (1..8).
try {
const m = (await getSession(id)) as { phase?: number };
@@ -84,6 +90,10 @@ export function AppShell() {
/* non-fatal: the first gate will set the phase */
}
} catch {
// Revert the optimistic switch and restore the panel.
resetSession();
setActiveSessionId(null);
if (s) setPanelSession(s);
toast.error("Failed to resume session.");
}
}
@@ -216,7 +226,7 @@ export function AppShell() {
<SteerInput
sessionId={activeSessionId}
onSessionCreated={setActiveSessionId}
onStop={stopSession}
onStop={() => setStopConfirm(true)}
inputRef={composerRef}
/>
</div>
@@ -227,7 +237,7 @@ export function AppShell() {
</div>
{/* Right session rail — symmetric to the portal's left sidebar */}
<aside className="flex w-[280px] shrink-0 flex-col border-l border-border bg-sidebar">
<aside className="flex w-[15vw] shrink-0 flex-col border-l border-border 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>
@@ -333,6 +343,12 @@ export function AppShell() {
<Toaster />
<StopConfirmDialog
open={stopConfirm}
onOpenChange={setStopConfirm}
onConfirm={stopSession}
/>
{renameTarget && (
<RenameDialog
open