mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-10 22:20:27 +00:00
Keep Retry offered across model switches
Switch notices no longer consume the retry guard or hide the button. Error, switch model, Retry is the intended recovery path; retry runs on the new model.
This commit is contained in:
@@ -25,3 +25,25 @@ test("provider error shows a retriable notice; Retry re-runs without a new user
|
||||
// …and the button is gone now that the error notice is no longer the transcript tail.
|
||||
await expect(page.getByTestId("notice-retry")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Retry survives a model switch — the intended recovery path", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByText("Draft the launch note").first().click();
|
||||
const box = page.getByPlaceholder(/Ask the coworker/);
|
||||
await box.fill("please fail the turn");
|
||||
await box.press("Enter");
|
||||
await expect(page.getByTestId("notice-retry")).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Switch models: the info marker lands AFTER the error — Retry must stay offered
|
||||
// (owner-hit 2026-07-23: the switch notices consumed it).
|
||||
const picker = page.locator(".dd").filter({ hasText: "Claude Opus 4.8" });
|
||||
await picker.locator(".pill").click();
|
||||
await page.locator(".dd-item").filter({ hasText: "GPT-5.5" }).click();
|
||||
await expect(page.getByText(/Model switched to gpt-5.5/).first()).toBeVisible();
|
||||
const retry = page.getByTestId("notice-retry");
|
||||
await expect(retry).toBeVisible();
|
||||
|
||||
await retry.click();
|
||||
await expect(page.getByText("Recovered after retry.").first()).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByTestId("notice-retry")).toHaveCount(0);
|
||||
});
|
||||
|
||||
@@ -313,6 +313,19 @@ interface Props {
|
||||
onRetry?: () => void;
|
||||
}
|
||||
|
||||
// The transcript index whose notice gets the Retry button: the tail error notice, looking
|
||||
// through info notices after it (model switches must not consume the retry — switching
|
||||
// models and THEN retrying is the intended recovery path). -1 when the tail is anything else.
|
||||
export function retryAnchor(items: Item[]): number {
|
||||
for (let i = items.length - 1; i >= 0; i--) {
|
||||
const it = items[i];
|
||||
if (it.kind !== "notice") return -1;
|
||||
if (it.retriable) return i;
|
||||
if (it.tone !== "info") return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function Transcript({ items, running, streamingText, onRetry }: Props) {
|
||||
// §33 grouping: a turn = the maximal run of assistant/tool/resolved-approval items between
|
||||
// breakers (user, connector, notices, plan/dir requests…). Trailing assistant texts are the
|
||||
@@ -435,7 +448,7 @@ export function Transcript({ items, running, streamingText, onRetry }: Props) {
|
||||
return (
|
||||
<div className={"notice " + (item.tone === "warn" ? "warn" : "")} key={bi}>
|
||||
{item.text}
|
||||
{item.retriable && !running && onRetry && block.i === items.length - 1 && (
|
||||
{item.retriable && !running && onRetry && block.i === retryAnchor(items) && (
|
||||
<button className="btn ml-2" data-testid="notice-retry" onClick={onRetry}>
|
||||
Retry
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user