From d67409ff16c3b5c44d5b6373142c2759f9719b3d Mon Sep 17 00:00:00 2001 From: mptyl Date: Sun, 28 Jun 2026 20:36:08 +0200 Subject: [PATCH] feat(frontend): rework chat composer (sticky input, inline new session, status footer) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Center ThothII logo + tagline; break "Human In The Loop" onto a second line - Rename "New question" → "New session"; the button now clears the screen and focuses the sticky bottom composer instead of opening a dialog - Composer is always rendered and pinned at the bottom; typing the first message there creates the session (no modal) - Add inline stop control (square + inner circle) that interrupts and saves the session via /close - Always-present send affordance (CornerDownLeft ↵) on the right of the box - Status footer under the box: mic (inert), model name, thinking level, and a context-usage gauge (placeholder) - Brand-tinted spinner beside the logo while the harness/backend holds the ball; hidden once a widget needs human input Co-Authored-By: Claude Opus 4.8 --- frontend/src/shell/AppShell.tsx | 88 +++++++++++++--- frontend/src/shell/NavSessions.tsx | 2 +- frontend/src/shell/NewSessionDialog.tsx | 2 +- frontend/src/shell/SteerInput.tsx | 128 ++++++++++++++++++++---- frontend/src/shell/f1-loop.test.tsx | 11 +- 5 files changed, 192 insertions(+), 39 deletions(-) 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 */}