Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions web/packages/superagent-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions web/packages/superagent-ui/src/ToolRecoveryPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<ToolRecoveryPanel onResolve={onResolve} recovery={recovery} />);

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",
Expand Down
10 changes: 5 additions & 5 deletions web/packages/superagent-ui/src/ToolRecoveryPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ export function ToolRecoveryPanel({
isSubmitting = false,
onResolve,
}: ToolRecoveryPanelProps) {
const headingRef = useRef<HTMLHeadingElement>(null);
const panelRef = useRef<HTMLElement>(null);
const headingID = useId();
const [selections, setSelections] = useState<
Record<string, ToolRecoveryAction>
>(() => initialSelections(recovery.calls));
const isDisabled = disabled || isSubmitting;

useEffect(() => {
headingRef.current?.focus();
panelRef.current?.focus();
}, []);

const submitResume = () => {
Expand All @@ -74,13 +74,13 @@ export function ToolRecoveryPanel({

return (
<section
ref={panelRef}
className="sa-tool-recovery recovery-card"
aria-labelledby={headingID}
tabIndex={-1}
>
<p className="sa-tool-recovery-eyebrow">Tool recovery required</p>
<h2 id={headingID} ref={headingRef} tabIndex={-1}>
Execution outcome is unknown
</h2>
<h2 id={headingID}>Execution outcome is unknown</h2>
<p className="sa-tool-recovery-warning">
These operations may already have produced external effects. Retrying
keeps the same call ID, but the MCP server may not deduplicate it.
Expand Down
4 changes: 4 additions & 0 deletions web/packages/superagent-ui/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions web/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,10 +329,10 @@ describe("App", () => {

render(<App />);

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" }),
);
Expand Down
2 changes: 1 addition & 1 deletion web/tests/full-stack.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down