diff --git a/frontend/src/shell/AppShell.tsx b/frontend/src/shell/AppShell.tsx index 5fbe99ac..74e68cb3 100644 --- a/frontend/src/shell/AppShell.tsx +++ b/frontend/src/shell/AppShell.tsx @@ -1,13 +1,15 @@ import { useSessionStream } from "../stream/useSessionStream"; +import { useSessionStore } from "../store/sessionStore"; import { WidgetHost } from "./WidgetHost"; import { Transcript } from "./Transcript"; import { NavSessions } from "./NavSessions"; -import { NewSessionDialog } from "./NewSessionDialog"; import { SettingsDialog } from "./SettingsDialog"; -import { SteerInput } from "./SteerInput"; +import { SteerInput, ComposerFooter } from "./SteerInput"; import { WorkflowBar } from "./WorkflowBar"; +import { Button } from "../components/ui/button"; import { Toaster } from "../components/ui/sonner"; -import { useState } from "react"; +import { closeSession } from "../api/sessions"; +import { useRef, useState } from "react"; /** * The page is designed to live INSIDE the Omics Portal chrome (its left sidebar @@ -17,9 +19,32 @@ import { useState } from "react"; */ export function AppShell() { const [activeSessionId, setActiveSessionId] = useState(null); + const composerRef = useRef(null); + + // The harness "holds the ball" whenever a session is live and no widget is + // waiting on the human; once a widget appears, input is back in the user's court. + const pendingWidget = useSessionStore((s) => s.pendingWidget); + const resetSession = useSessionStore((s) => s.resetSession); + const working = Boolean(activeSessionId) && !pendingWidget; useSessionStream(activeSessionId); + function startNewSession() { + resetSession(); + setActiveSessionId(null); + requestAnimationFrame(() => composerRef.current?.focus()); + } + + async function stopSession() { + if (!activeSessionId) return; + try { + await closeSession(activeSessionId); + } finally { + resetSession(); + setActiveSessionId(null); + } + } + return (
{/* Conversation column */} @@ -45,28 +70,49 @@ export function AppShell() {
- {activeSessionId && ( -
-
- + {/* Sticky composer: the box stays pinned while the conversation scrolls above. */} +
+
+
+
+ +
+
- )} +
{/* Right session rail — symmetric to the portal's left sidebar */}