feat(frontend): minimal central area; WIP icon above composer toggles model-activity panel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 21:47:50 +02:00
co-authored by Claude Sonnet 4.6
parent b391101a5d
commit feda972a08
2 changed files with 46 additions and 8 deletions
@@ -78,6 +78,23 @@ test("a failed resume keeps the panel open and does not activate the session", a
await waitFor(() => expect(screen.getByText("Domanda originale")).toBeInTheDocument());
});
import { act } from "@testing-library/react";
import { useSessionStore } from "../store/sessionStore";
test("WIP icon toggles the Model activity panel and shows the streamed text", async () => {
wrap();
// Activate a session by opening + resuming is heavy; instead drive the store directly.
// The WIP icon only renders with an active session, so simulate one via Resume.
server.use(http.post("http://localhost:8787/sessions/:id/resume", () => new HttpResponse(null, { status: 204 })));
await userEvent.click(await screen.findByText("Attiva uno")); // opens docs panel
await userEvent.click(await screen.findByRole("button", { name: /resume/i })); // active session
act(() => { useSessionStore.getState().applyEvent({ type: "text_delta", text: "Looking at dim_patient." }); });
// Model activity hidden until the WIP icon is clicked
expect(screen.queryByText(/Looking at dim_patient/)).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: /model activity/i }));
expect(await screen.findByText(/Looking at dim_patient/)).toBeInTheDocument();
});
test("renaming a group reassigns its members via setSessionGroup", async () => {
const groupSets: Array<{ id: string; group: string }> = [];
server.use(
+29 -8
View File
@@ -1,7 +1,8 @@
import { useSessionStream } from "../stream/useSessionStream";
import { useSessionStore } from "../store/sessionStore";
import { WidgetHost } from "./WidgetHost";
import { Transcript } from "./Transcript";
import { CentralStatus } from "./CentralStatus";
import { ModelActivityPanel } from "./ModelActivityPanel";
import { NavSessions } from "./NavSessions";
import { SessionMenu } from "./SessionMenu";
import { SessionDocumentsPanel } from "./SessionDocumentsPanel";
@@ -36,6 +37,7 @@ export function AppShell() {
const queryClient = useQueryClient();
const [panelSession, setPanelSession] = useState<SessionSummary | null>(null);
const [showActivity, setShowActivity] = useState(false);
const [activeOpen, setActiveOpen] = useState(true);
const [archiveOpen, setArchiveOpen] = useState(false);
const [renameTarget, setRenameTarget] = useState<SessionSummary | null>(null);
@@ -53,7 +55,14 @@ export function AppShell() {
function openPanel(id: string) {
const s = sessions.find((x) => x.id === id);
if (s) setPanelSession(s);
if (s) { setPanelSession(s); setShowActivity(false); }
}
function toggleActivity() {
setShowActivity((v) => {
const next = !v;
if (next) setPanelSession(null);
return next;
});
}
async function doResume(id: string) {
try {
@@ -151,6 +160,7 @@ export function AppShell() {
onResume={doResume}
/>
)}
{showActivity && <ModelActivityPanel onClose={() => setShowActivity(false)} />}
{/* Conversation column */}
<div className="flex min-w-0 flex-1 flex-col">
@@ -166,7 +176,7 @@ export function AppShell() {
<div className="mx-auto flex max-w-3xl flex-col gap-6">
{activeSessionId ? (
<>
<Transcript />
<CentralStatus />
<WidgetHost sessionId={activeSessionId} />
</>
) : (
@@ -178,6 +188,20 @@ export function AppShell() {
{/* Sticky composer: the box stays pinned while the conversation scrolls above. */}
<div className="shrink-0 px-6 pb-6 pt-2">
<div className="mx-auto max-w-3xl">
{activeSessionId && (
<div className="mb-1.5 flex justify-start">
<button
type="button"
onClick={toggleActivity}
aria-label="Model activity"
aria-pressed={showActivity}
title="Model activity"
className="grid size-7 place-items-center rounded-md border border-border text-primary transition-colors hover:bg-muted"
>
<WorkingSpinner spinning={working} />
</button>
</div>
)}
<div className="rounded-2xl border border-input bg-card shadow-sm transition-colors focus-within:border-primary/40 focus-within:ring-3 focus-within:ring-ring/15">
<div className="px-2.5 py-2">
<SteerInput
@@ -196,9 +220,6 @@ export function AppShell() {
{/* 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">
<div className="relative px-4 pb-3 pt-5 text-center">
{working && (
<WorkingSpinner className="absolute left-4 top-[1.35rem]" />
)}
<h1 className="font-heading text-xl font-semibold leading-none tracking-tight text-foreground">
Thoth<span className="text-primary">II</span>
</h1>
@@ -347,14 +368,14 @@ export function AppShell() {
}
/** Brand-tinted spinner shown while the assistant/harness is working. */
function WorkingSpinner({ className }: { className?: string }) {
function WorkingSpinner({ className, spinning = true }: { className?: string; spinning?: boolean }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
role="status"
aria-label="Assistant is working"
className={["size-4 animate-spin text-primary", className].filter(Boolean).join(" ")}
className={["size-4 text-primary", spinning ? "animate-spin" : "", className].filter(Boolean).join(" ")}
>
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="3" opacity="0.2" />
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />