From 1416b2cb756d1f0f5d3b2d9463864bafc6885a42 Mon Sep 17 00:00:00 2001 From: Jesus Armando Anaya <1445792+JArmandoAnaya@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:58:15 -0700 Subject: [PATCH 1/8] feat(ui): make suggest panel collapsible --- .../ui-core/src/annotator/EditorNotice.tsx | 40 +++++++++++++++++-- .../ui-core/src/annotator/SuggestPanel.tsx | 13 +++--- .../src/annotator/suggestPanel.test.tsx | 19 +++++++++ 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/frontend/ui-core/src/annotator/EditorNotice.tsx b/frontend/ui-core/src/annotator/EditorNotice.tsx index e9df0a09..3a174d9e 100644 --- a/frontend/ui-core/src/annotator/EditorNotice.tsx +++ b/frontend/ui-core/src/annotator/EditorNotice.tsx @@ -48,7 +48,9 @@ * desktop; see `MAX_WIDTH` below. */ -import type { JSX, ReactNode } from "react"; +import { ChevronDown, ChevronRight } from "lucide-react"; +import { Button } from "@robomous/ui-core"; +import { useId, useState, type JSX, type ReactNode } from "react"; /** * The surface's width, stated here because the number is a measurement rather @@ -96,6 +98,8 @@ export interface EditorNoticeProps { readonly tone: "calm" | "warn"; readonly icon: ReactNode; readonly children: ReactNode; + /** Adds a compact, open-by-default toggle for notices that can obstruct the canvas. */ + readonly collapsible?: boolean; /** The kernel's identifier, where a bug report can quote it. Never the message. */ readonly title?: string; } @@ -106,14 +110,19 @@ export function EditorNotice({ icon, children, title, + collapsible = false, }: EditorNoticeProps): JSX.Element { + const [collapsed, setCollapsed] = useState(false); + const contentId = useId(); return (
@@ -126,7 +135,32 @@ export function EditorNotice({ {/* `min-w-0` lets the column shrink below its content's intrinsic width, which is the half of the wrap rule flexbox owns — without it a long token widens the flex item instead of breaking. */} -
{children}
+ + {collapsible && ( + + )}
); } diff --git a/frontend/ui-core/src/annotator/SuggestPanel.tsx b/frontend/ui-core/src/annotator/SuggestPanel.tsx index 77c1c4ee..45696d89 100644 --- a/frontend/ui-core/src/annotator/SuggestPanel.tsx +++ b/frontend/ui-core/src/annotator/SuggestPanel.tsx @@ -257,7 +257,7 @@ export function SuggestPanel({ */ if (isParked(session)) { return ( - }> + }>

{heldClass === null ? "Nothing selected to suggest for" @@ -301,6 +301,7 @@ export function SuggestPanel({ const copy = BLOCKER_COPY[blocker]; return ( }> + }>

That suggestion could not be made

{/* The server's sentence, verbatim. It is the one that carries the install command when the cause is a missing extra. */} @@ -339,7 +340,7 @@ export function SuggestPanel({ // lives in `usePendingIndicator` rather than here. if (session.status === "asking") { return ( - }> + }>

Looking at that…

@@ -358,7 +359,7 @@ export function SuggestPanel({ if (session.status === "none") { return ( - }> + }>

Nothing to suggest there

@@ -399,7 +400,7 @@ export function SuggestPanel({ */ if (isAcceptable(session)) { return ( - }> + }>

A shape for “{session.labelClass}”

@@ -453,7 +454,7 @@ export function SuggestPanel({ activeBrowserModel?.state === "activating"; return ( - }> + }> {!serverTabBlocked && (browserTabUnacquired ? ( <> diff --git a/frontend/ui-core/src/annotator/suggestPanel.test.tsx b/frontend/ui-core/src/annotator/suggestPanel.test.tsx index 251dc485..3582febb 100644 --- a/frontend/ui-core/src/annotator/suggestPanel.test.tsx +++ b/frontend/ui-core/src/annotator/suggestPanel.test.tsx @@ -233,6 +233,25 @@ describe("what the panel says while the tool is working", () => { expect(screen.getByTestId("suggest-panel").textContent).toContain("vehicle"); }); + it("can collapse the notice to clear the canvas and reopen it in place", async () => { + render(mount()); + + const toggle = screen.getByTestId("suggest-panel-collapse"); + const content = screen.getByTestId("suggest-idle").parentElement; + expect(toggle.getAttribute("aria-expanded")).toBe("true"); + expect(content?.hasAttribute("hidden")).toBe(false); + + await userEvent.click(toggle); + expect(toggle.getAttribute("aria-expanded")).toBe("false"); + expect(toggle.getAttribute("aria-label")).toBe("Show suggest panel"); + expect(content?.hasAttribute("hidden")).toBe(true); + + await userEvent.click(toggle); + expect(toggle.getAttribute("aria-expanded")).toBe("true"); + expect(toggle.getAttribute("aria-label")).toBe("Hide suggest panel"); + expect(content?.hasAttribute("hidden")).toBe(false); + }); + it("says a request is in flight, in the async vocabulary and not a new spinner", () => { render(mount({ session: asked() })); expect(screen.getByTestId("suggest-asking")).toBeTruthy(); From 26e791752f64711b149b698fe19bbcdb7bc8c1f7 Mon Sep 17 00:00:00 2001 From: Jesus Armando Anaya <1445792+JArmandoAnaya@users.noreply.github.com> Date: Thu, 17 Sep 2026 18:14:47 -0700 Subject: [PATCH 2/8] fix(ui): retain suggestion actions when reduced --- .../ui-core/src/annotator/EditorNotice.tsx | 13 ++++- .../ui-core/src/annotator/SuggestPanel.tsx | 52 ++++++++++++++----- .../src/annotator/suggestPanel.test.tsx | 14 +++++ 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/frontend/ui-core/src/annotator/EditorNotice.tsx b/frontend/ui-core/src/annotator/EditorNotice.tsx index 3a174d9e..35489cf9 100644 --- a/frontend/ui-core/src/annotator/EditorNotice.tsx +++ b/frontend/ui-core/src/annotator/EditorNotice.tsx @@ -100,6 +100,13 @@ export interface EditorNoticeProps { readonly children: ReactNode; /** Adds a compact, open-by-default toggle for notices that can obstruct the canvas. */ readonly collapsible?: boolean; + /** + * The essential controls that remain when a collapsible notice is reduced. + * + * A reduced notice clears most of the canvas without making an active decision + * unreachable. + */ + readonly reducedContent?: ReactNode; /** The kernel's identifier, where a bug report can quote it. Never the message. */ readonly title?: string; } @@ -111,9 +118,11 @@ export function EditorNotice({ children, title, collapsible = false, + reducedContent, }: EditorNoticeProps): JSX.Element { const [collapsed, setCollapsed] = useState(false); const contentId = useId(); + const reduced = collapsed && reducedContent !== undefined; return ( {collapsible && ( - - + void; + readonly onDiscard: () => void; + readonly reduced?: boolean; +}): JSX.Element { + return ( +
+ + +
+ ); +} + /** * Which connection a click goes through: a line, or a picker where there is a choice. * diff --git a/frontend/ui-core/src/annotator/suggestPanel.test.tsx b/frontend/ui-core/src/annotator/suggestPanel.test.tsx index 3582febb..9f2ec051 100644 --- a/frontend/ui-core/src/annotator/suggestPanel.test.tsx +++ b/frontend/ui-core/src/annotator/suggestPanel.test.tsx @@ -252,6 +252,20 @@ describe("what the panel says while the tool is working", () => { expect(content?.hasAttribute("hidden")).toBe(false); }); + it("keeps accept and discard available while a shown suggestion is reduced", async () => { + const onAccept = vi.fn(); + const onDiscard = vi.fn(); + render(mount({ session: shown(), onAccept, onDiscard })); + + await userEvent.click(screen.getByTestId("suggest-panel-collapse")); + expect(screen.getByTestId("suggest-shown-reduced")).toBeTruthy(); + + await userEvent.click(screen.getByTestId("suggest-accept")); + await userEvent.click(screen.getByTestId("suggest-discard")); + expect(onAccept).toHaveBeenCalledTimes(1); + expect(onDiscard).toHaveBeenCalledTimes(1); + }); + it("says a request is in flight, in the async vocabulary and not a new spinner", () => { render(mount({ session: asked() })); expect(screen.getByTestId("suggest-asking")).toBeTruthy(); From 8109c6261f608609aabe9e755dc838750dc48169 Mon Sep 17 00:00:00 2001 From: Jesus Armando Anaya <1445792+JArmandoAnaya@users.noreply.github.com> Date: Thu, 17 Sep 2026 18:16:02 -0700 Subject: [PATCH 3/8] fix(ui): top-align reduced notice controls --- frontend/ui-core/src/annotator/EditorNotice.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/ui-core/src/annotator/EditorNotice.tsx b/frontend/ui-core/src/annotator/EditorNotice.tsx index 35489cf9..f6330f91 100644 --- a/frontend/ui-core/src/annotator/EditorNotice.tsx +++ b/frontend/ui-core/src/annotator/EditorNotice.tsx @@ -130,7 +130,7 @@ export function EditorNotice({ role="status" {...(title === undefined ? {} : { title })} className={`pointer-events-auto flex gap-2 rounded-lg border p-3 text-xs shadow-lg ${ - collapsed ? "w-auto items-center" : "w-full" + collapsed ? "w-auto items-start" : "w-full" } ${ tone === "warn" ? "border-destructive/40 bg-destructive/5" : "border-border bg-card" }`} From c21ed54ba7688521e5bdd9a06e6f8d856bfbe12e Mon Sep 17 00:00:00 2001 From: Jesus Armando Anaya <1445792+JArmandoAnaya@users.noreply.github.com> Date: Thu, 17 Sep 2026 18:18:56 -0700 Subject: [PATCH 4/8] fix(ui): anchor notice controls independently --- frontend/ui-core/src/annotator/EditorNotice.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/ui-core/src/annotator/EditorNotice.tsx b/frontend/ui-core/src/annotator/EditorNotice.tsx index f6330f91..d31d1c02 100644 --- a/frontend/ui-core/src/annotator/EditorNotice.tsx +++ b/frontend/ui-core/src/annotator/EditorNotice.tsx @@ -130,13 +130,13 @@ export function EditorNotice({ role="status" {...(title === undefined ? {} : { title })} className={`pointer-events-auto flex gap-2 rounded-lg border p-3 text-xs shadow-lg ${ - collapsed ? "w-auto items-start" : "w-full" + collapsed ? "w-auto" : "w-full" } ${ tone === "warn" ? "border-destructive/40 bg-destructive/5" : "border-border bg-card" }`} >