From 220ae6f7deba5cab4827adc299cafdaf8cdba74c Mon Sep 17 00:00:00 2001 From: Quanzheng Long Date: Fri, 25 Sep 2026 10:55:47 -0700 Subject: [PATCH] Focus the tool recovery region --- ARCHITECTURE.md | 3 +++ web/packages/superagent-ui/README.md | 4 ++++ .../superagent-ui/src/ToolRecoveryPanel.test.tsx | 4 ++-- web/packages/superagent-ui/src/ToolRecoveryPanel.tsx | 10 +++++----- web/packages/superagent-ui/styles.css | 4 ++++ web/src/App.test.tsx | 4 ++-- web/tests/full-stack.spec.ts | 2 +- 7 files changed, 21 insertions(+), 10 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4cd7d08..0d0af95 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -224,6 +224,9 @@ bubbles. Replays are idempotent and cannot remove messages that arrived later. A later authoritative Snapshot at a real waiting boundary reopens the gate. Stream loss is corrected by Snapshot. +Manual tool recovery moves programmatic focus to its labelled warning region. +The heading remains the accessible label without becoming a focused control. + Consumed user bubbles are anchored after the latest explicit message sequence seen before the consumption event. The event's own message sequence associates the hint with its durable turn and does not advance that temporary projection diff --git a/web/packages/superagent-ui/README.md b/web/packages/superagent-ui/README.md index 252bc24..8f265cc 100644 --- a/web/packages/superagent-ui/README.md +++ b/web/packages/superagent-ui/README.md @@ -90,6 +90,10 @@ consumer supplies `renderMessage`. User messages are compact, right-aligned bubbles; assistant messages remain left-aligned. Hovering or focusing either message reveals its local timestamp below the content. +`ToolRecoveryPanel` moves programmatic focus to the recovery region when it +appears. The existing warning card communicates that focus without drawing a +browser-default input outline around its heading. + Completed model and tool duration is `createdAt - startedAt`. Missing or negative legacy timing is omitted. Running durations update once per second but are excluded from live-region announcements. Stream timestamps remain ordering diff --git a/web/packages/superagent-ui/src/ToolRecoveryPanel.test.tsx b/web/packages/superagent-ui/src/ToolRecoveryPanel.test.tsx index 10db740..7153c9a 100644 --- a/web/packages/superagent-ui/src/ToolRecoveryPanel.test.tsx +++ b/web/packages/superagent-ui/src/ToolRecoveryPanel.test.tsx @@ -31,12 +31,12 @@ const recovery: PendingToolRecovery = { }; describe("ToolRecoveryPanel", () => { - it("focuses the warning and submits one ordered complete decision", () => { + it("focuses the recovery panel and submits one ordered complete decision", () => { const onResolve = vi.fn(); render(); expect( - screen.getByRole("heading", { name: "Execution outcome is unknown" }), + screen.getByRole("region", { name: "Execution outcome is unknown" }), ).toHaveFocus(); const continueOptions = screen.getAllByRole("radio", { name: "Continue with unknown result", diff --git a/web/packages/superagent-ui/src/ToolRecoveryPanel.tsx b/web/packages/superagent-ui/src/ToolRecoveryPanel.tsx index 58a95b4..e3b9048 100644 --- a/web/packages/superagent-ui/src/ToolRecoveryPanel.tsx +++ b/web/packages/superagent-ui/src/ToolRecoveryPanel.tsx @@ -50,7 +50,7 @@ export function ToolRecoveryPanel({ isSubmitting = false, onResolve, }: ToolRecoveryPanelProps) { - const headingRef = useRef(null); + const panelRef = useRef(null); const headingID = useId(); const [selections, setSelections] = useState< Record @@ -58,7 +58,7 @@ export function ToolRecoveryPanel({ const isDisabled = disabled || isSubmitting; useEffect(() => { - headingRef.current?.focus(); + panelRef.current?.focus(); }, []); const submitResume = () => { @@ -74,13 +74,13 @@ export function ToolRecoveryPanel({ return (

Tool recovery required

-

- Execution outcome is unknown -

+

Execution outcome is unknown

These operations may already have produced external effects. Retrying keeps the same call ID, but the MCP server may not deduplicate it. diff --git a/web/packages/superagent-ui/styles.css b/web/packages/superagent-ui/styles.css index 2b07dc0..e8f7aa1 100644 --- a/web/packages/superagent-ui/styles.css +++ b/web/packages/superagent-ui/styles.css @@ -443,6 +443,10 @@ font-size: 17px; } +.sa-tool-recovery:focus { + outline: none; +} + .sa-tool-recovery-eyebrow { margin: 0; color: var(--sa-subtle-color, #667085); diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index 9416dbb..51216c2 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -329,10 +329,10 @@ describe("App", () => { render(); - const heading = await screen.findByRole("heading", { + const recoveryPanel = await screen.findByRole("region", { name: "Execution outcome is unknown", }); - expect(heading).toHaveFocus(); + expect(recoveryPanel).toHaveFocus(); fireEvent.click( screen.getByRole("radio", { name: "Continue with unknown result" }), ); diff --git a/web/tests/full-stack.spec.ts b/web/tests/full-stack.spec.ts index 6af37e1..714d7fb 100644 --- a/web/tests/full-stack.spec.ts +++ b/web/tests/full-stack.spec.ts @@ -1634,7 +1634,7 @@ test("persists manual tool recovery and resumes only after a user decision", asy name: "Execution outcome is unknown", }); await expect(recoveryHeading).toBeVisible(); - await expect(recoveryHeading).toBeFocused(); + await expect(recovery).toBeFocused(); await expect( recovery.getByText("Error type:", { exact: false }), ).toBeVisible();