fix(ui): reset session view on stop and exit

This commit is contained in:
Codex
2026-09-26 16:40:37 +02:00
parent bd416f7327
commit 0d2e573e0d
2 changed files with 49 additions and 15 deletions
@@ -1213,6 +1213,38 @@ test("closing and reopening Model activity preserves the complete activity log",
expect(useSessionStore.getState().activityLog).toEqual(beforeClose); expect(useSessionStore.getState().activityLog).toEqual(beforeClose);
}); });
test.each(["stop", "session_exit"])("%s restores the empty landing with model activity closed", async (exit) => {
const close = vi.fn(() => new HttpResponse(null, { status: 204 }));
server.use(
http.post("/api/sessions/:id/resume", () => resumeResult("s1")),
http.post("/api/sessions/:id/close", close),
);
wrap(adminUser);
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: "Resume" }));
await userEvent.click(screen.getByRole("button", { name: "Show model activity" }));
act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "Previous question" }));
expect(screen.getByLabelText("Model activity timeline")).toHaveTextContent("Previous question");
const source = FakeEventSource.instances.at(-1)!;
if (exit === "stop") {
await userEvent.click(screen.getByRole("button", { name: /stop and save session/i }));
await userEvent.click(screen.getAllByRole("button", { name: "Stop & save" })[0]);
} else {
act(() => source.emit({ type: "system_event", event: "session_exit" }));
}
expect(await screen.findByText(/type your question/i)).toBeInTheDocument();
expect(screen.queryByRole("heading", { name: "Model activity" })).not.toBeInTheDocument();
expect(screen.getByTestId("app-shell")).toHaveAttribute("data-activity-layout", "closed");
expect(screen.queryByRole("complementary", { name: "Session summary" })).not.toBeInTheDocument();
expect(screen.getByRole("complementary", { name: "Session navigation" })).toBeVisible();
expect(screen.getByRole("textbox", { name: /new question/i })).toHaveValue("");
expect(useSessionStore.getState().activityLog).toEqual([]);
expect(source.closed).toBe(true);
expect(close).toHaveBeenCalledTimes(exit === "stop" ? 1 : 0);
});
test("a new question after finalization restores the landing and administration navigation", async () => { test("a new question after finalization restores the landing and administration navigation", async () => {
useSessionStore.getState().resetSession(); useSessionStore.getState().resetSession();
let finalized = false; let finalized = false;
+17 -15
View File
@@ -694,6 +694,20 @@ export function AppShell({ canLogout }: AppShellProps) {
authGeneration, authGeneration,
); );
function resetSessionView() {
invalidateResumeIntent();
newSessionOperationRef.current = null;
resetSession();
selectActiveSession(null);
// Panel visibility belongs to the shell, outside the session store.
// Clear it too so leaving a session restores the initial landing layout.
setPanelSession(null);
setShowActivity(false);
setCreatingSession(false);
setAwaitingQuestion(false);
setStopConfirm(false);
}
// A backend "session_exit" system event (e.g. the replay server emitting it // A backend "session_exit" system event (e.g. the replay server emitting it
// when the reviewer picks "Esci") asks us to leave the live session view and // when the reviewer picks "Esci") asks us to leave the live session view and
// return to the landing state. We deliberately do NOT also POST /close here — // return to the landing state. We deliberately do NOT also POST /close here —
@@ -705,10 +719,7 @@ export function AppShell({ canLogout }: AppShellProps) {
if (ev === "session_exit") { if (ev === "session_exit") {
// Never let a streamed event terminate the managed Pi child. Only the // Never let a streamed event terminate the managed Pi child. Only the
// explicit “Stop & save” action is allowed to call /close. // explicit “Stop & save” action is allowed to call /close.
invalidateResumeIntent(); resetSessionView();
resetSession();
selectActiveSession(null);
setAwaitingQuestion(false);
} }
// The final workflow turn ends with the session already finalized on disk: // The final workflow turn ends with the session already finalized on disk:
// refetch now instead of waiting for the 10s poll, so the completed state // refetch now instead of waiting for the 10s poll, so the completed state
@@ -725,15 +736,8 @@ export function AppShell({ canLogout }: AppShellProps) {
} }
if (!canLeaveDatabaseManagement()) return; if (!canLeaveDatabaseManagement()) return;
navigate({ surface: "core" }, true); navigate({ surface: "core" }, true);
invalidateResumeIntent(); resetSessionView();
newSessionOperationRef.current = null;
resetSession();
// Restore the landing layout, including the right-hand administration rail.
setPanelSession(null);
setShowActivity(false);
setAwaitingQuestion(true); setAwaitingQuestion(true);
setCreatingSession(false);
selectActiveSession(null);
// Best effort only: session creation keeps the authoritative readiness gate. // Best effort only: session creation keeps the authoritative readiness gate.
// Composer focus is deliberately independent of this network request. // Composer focus is deliberately independent of this network request.
void prewarmRuntime().catch(() => undefined); void prewarmRuntime().catch(() => undefined);
@@ -781,9 +785,7 @@ export function AppShell({ canLogout }: AppShellProps) {
} finally { } finally {
if (!isAuthOperationCurrent(guard, { sessionId: id, disposalEpoch: operationEpochRef.current }) if (!isAuthOperationCurrent(guard, { sessionId: id, disposalEpoch: operationEpochRef.current })
|| activeSessionIdRef.current !== id) return; || activeSessionIdRef.current !== id) return;
resetSession(); resetSessionView();
selectActiveSession(null);
setAwaitingQuestion(false);
} }
} }