From 612b146fd6cfa522abb17be1de8373240abb9918 Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Tue, 25 Aug 2026 18:32:33 +0200 Subject: [PATCH] feat(status-ui): add compact session status strip --- ui/src/App.css | 154 +++++++++++++------- ui/src/App.test.tsx | 17 ++- ui/src/App.tsx | 71 ++++----- ui/src/components/ConversationWorkspace.tsx | 141 +++++++++++++++++- ui/src/components/components.test.tsx | 54 +++++++ 5 files changed, 332 insertions(+), 105 deletions(-) diff --git a/ui/src/App.css b/ui/src/App.css index c301fda..fafa814 100644 --- a/ui/src/App.css +++ b/ui/src/App.css @@ -66,16 +66,17 @@ select:focus-visible { } .workflow-header { display: flex; + min-height: 24px; align-items: center; justify-content: space-between; gap: 8px; cursor: grab; } -.workspace-heading, -.status-row { +.workspace-heading { display: flex; + min-width: 0; align-items: center; - gap: 8px; + gap: 7px; } .brand { color: #f0a347; @@ -84,39 +85,17 @@ select:focus-visible { letter-spacing: 0.16em; } .workflow-header h1 { + overflow: hidden; margin: 0; - font-size: 16px; + font-size: 14px; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; } -.status-row { - margin-top: 1px; - color: #aaa6a0; +.workflow-header > button { + padding: 4px 8px; font-size: 11px; } -.state-pill { - display: inline-flex; - align-items: center; - gap: 5px; - border: 1px solid #555; - border-radius: 999px; - padding: 2px 8px; - font-size: 11px; -} -.state-pill.working { - color: #ffd39a; - border-color: #f0a347; - background: #4b3420; - box-shadow: - 0 0 0 1px rgba(240, 163, 71, 0.22), - 0 0 10px rgba(240, 163, 71, 0.2); -} -.state-pill.recovering { - color: #e0c2a1; - border-color: #8d6745; -} -.state-pill.error { - color: #ffb4ba; - border-color: #a65b67; -} .working-pulse { width: 7px; height: 7px; @@ -126,17 +105,6 @@ select:focus-visible { box-shadow: 0 0 0 0 rgba(240, 163, 71, 0.7); animation: work-pulse 1.35s ease-out infinite; } -.working-banner { - display: inline-flex; - align-items: center; - gap: 5px; - padding: 2px 6px; - border: 1px solid #b76f26; - border-radius: 4px; - background: #41301f; - color: #ffd39a; - font-weight: 700; -} .workspace-warning { padding: 6px 8px; border: 1px solid #8c713c; @@ -883,31 +851,111 @@ select:focus-visible { margin: 5px 0; text-align: center; } -.composer-progress { +.session-status-strip { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 4px 6px; + border: 1px solid #454545; + border-left: 3px solid #687168; + border-radius: 5px; + background: #292a29; + color: #b7bcb7; +} +.session-status-primary, +.session-status-meta, +.status-chip { display: flex; align-items: center; +} +.session-status-primary { + min-width: 0; + flex: 1; gap: 6px; - color: #8f988e; - font-size: 11px; +} +.session-status-primary strong { + flex: 0 0 auto; + color: #d8ddd8; + font-size: 9px; + letter-spacing: 0.07em; + text-transform: uppercase; +} +.session-status-detail { + overflow: hidden; + color: #9da49d; + font-size: 10px; + text-overflow: ellipsis; + white-space: nowrap; +} +.session-status-meta { + max-width: 68%; + min-width: 0; + flex: 0 1 auto; + justify-content: flex-end; + gap: 4px; + overflow-x: auto; + scrollbar-width: none; +} +.session-status-meta::-webkit-scrollbar { + display: none; +} +.status-chip { + flex: 0 0 auto; + gap: 4px; + max-width: 180px; + overflow: hidden; + padding: 2px 5px; + border: 1px solid #494b49; + border-radius: 999px; + background: #323332; + color: #bcc1bc; + font-size: 9px; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} +.status-chip small { + color: #7f877f; + font-size: 8px; + font-weight: 750; + letter-spacing: 0.06em; + text-transform: uppercase; } .work-dot { width: 7px; height: 7px; + flex: 0 0 auto; border-radius: 50%; background: #687168; } -.composer-progress.working { - padding: 4px 6px; - border-left: 3px solid #f0a347; - background: #3b3025; - color: #ffd39a; - font-weight: 700; +.session-status-strip.working { + border-color: #60492f; + border-left-color: #f0a347; + background: #332d26; } -.composer-progress.working .work-dot { +.session-status-strip.working .session-status-primary strong, +.session-status-strip.working .session-status-detail { + color: #ffd39a; +} +.session-status-strip.working .work-dot { background: #f0a347; box-shadow: 0 0 8px #f0a347; animation: work-pulse 1.35s ease-out infinite; } +.session-status-strip.recovering { + border-left-color: #b9824b; +} +.session-status-strip.error { + border-color: #6f3f47; + border-left-color: #e47682; + background: #34272a; +} +.session-status-strip.error .session-status-primary strong, +.session-status-strip.error .session-status-detail { + color: #ffb4ba; +} .composer { display: flex; gap: 7px; diff --git a/ui/src/App.test.tsx b/ui/src/App.test.tsx index b9563a9..2b7e861 100644 --- a/ui/src/App.test.tsx +++ b/ui/src/App.test.tsx @@ -92,6 +92,13 @@ describe("App command progress and errors", () => { }); }); + it("shows explicit Pi readiness in the compact session status", () => { + render(); + const status = screen.getByRole("status", { name: "Session status" }); + expect(status).toHaveTextContent("Pi ready"); + expect(status).toHaveTextContent("Ready for your next prompt"); + }); + it("shows restart progress until the replacement snapshot is restored", async () => { const user = userEvent.setup(); const restart = deferred(); @@ -211,9 +218,13 @@ describe("App command progress and errors", () => { expect(screen.getByLabelText("Prompt")).toBeDisabled(); expect(screen.getByRole("button", { name: "Sending…" })).toBeDisabled(); expect(screen.getByText("Sending your prompt…")).toBeInTheDocument(); - expect(screen.getByText("Working", { selector: ".state-pill" })).toHaveClass( - "working", - ); + const sessionStatus = screen.getByRole("status", { name: "Session status" }); + expect(sessionStatus).toHaveTextContent("Working"); + expect(sessionStatus).toHaveTextContent("Sending prompt to Pi…"); + expect(sessionStatus).toHaveTextContent("Bridge"); + expect(sessionStatus).toHaveTextContent("ready"); + expect(sessionStatus).toHaveClass("working"); + expect(rendered.container.querySelector(".status-row")).toBeNull(); await act(async () => { submissionA.resolve({ delivery: "prompt" }); diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 40ee4b7..d1c40b4 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -58,6 +58,10 @@ function requestedCommand(command: string, args: Record) { : command; } +function sessionStateLabel(state?: string) { + return !state || state === "idle" ? "Pi ready" : state; +} + function commandProgress(command: string) { if (command === "restart") return "Restarting Pi and restoring the session…"; if (command === "renew_session") @@ -563,6 +567,19 @@ export default function App() { isSubmitting || selected?.progress.phase === "working" || selected?.progress.phase === "recovering"; + const sessionStatusLabel = + state.newSessionLaunch?.phase === "starting" + ? "Starting" + : isRenewing + ? "Renewing" + : isRestarting + ? "Restarting" + : isWorking + ? "Working" + : sessionStateLabel(selected?.summary.state); + const sessionStatusPhase = isWorking + ? ("working" as const) + : (selected?.progress.phase ?? "idle"); const isStartingSession = state.newSessionLaunch?.phase === "starting" || activeOperations.some((entry) => entry.kind === "creating"); @@ -600,50 +617,9 @@ export default function App() { void getCurrentWindow().startDragging(); }} > -
-
- PI -

{selected?.summary.label ?? "Pi workspace"}

- {selected && ( - - {isWorking && - )} -
-
- {isWorking && ( - - - )} - - {displayedStatus} - - {state.bridge.status} - {Object.values(selected?.extensionStatuses ?? {}).map((entry) => ( - - {entry} - - ))} -
+
+ PI +

{selected?.summary.label ?? "Pi workspace"}