gui: reload coworkers after health, not only at mount

The mount-time persona fetch loses the race to the sidecar boot, leaving the
composer picker empty all session while Settings looked fine.
This commit is contained in:
Rohit C Prasad
2026-08-13 16:47:11 -07:00
committed by Rohit P
parent 5f3ffe385d
commit 49c16af076
2 changed files with 43 additions and 5 deletions
+31
View File
@@ -42,3 +42,34 @@ test("model picker recovers when settings fetches die during sidecar boot", asyn
});
await expect(page.getByTestId("models-loading")).toHaveCount(0);
});
test("coworker picker recovers when the persona fetch dies during sidecar boot", async ({
page,
}) => {
// Same cold-start shape as above, for /v1/personas (owner-hit 2026-08-13, packaged app):
// the mount-time fetch loses to the sidecar boot and its only other trigger is
// PERSONAS_CHANGED, so the composer's picker stayed empty for the WHOLE session — while
// Settings ▸ Coworkers (mounted later) listed everything and looked healthy.
let sidecarUp = false;
await page.route("**/v1/health", async (route) => {
await new Promise((r) => setTimeout(r, 700));
sidecarUp = true;
await route.fallback();
});
await page.route("**/v1/personas", async (route) => {
if (route.request().method() === "GET" && !sidecarUp) {
await route.abort();
return;
}
await route.fallback();
});
await page.goto("/");
await page.getByText("New session").first().click();
await page.getByTestId("coworker-chip").click();
// The menu must list real coworkers, not just its Import/Manage footer.
const menu = page.locator(".setup-menu");
await expect(menu.getByText("Code Coworker")).toBeVisible({ timeout: 10_000 });
await expect(menu.getByTestId("import-coworker")).toBeVisible();
});
+12 -5
View File
@@ -338,14 +338,16 @@ export function App() {
// Persona metadata drives workspace behavior by FAMILY, not by hardcoded id (so a DevOps/SecOps
// code-family persona gates a folder like Code, and a knowledge persona starts orphan like Cowork).
const [personas, setPersonas] = useState<Persona[] | null>(null);
const loadPersonas = useCallback(() => {
getPersonas().then(setPersonas).catch(() => {});
}, []);
useEffect(() => {
const load = () => getPersonas().then(setPersonas).catch(() => {});
load();
loadPersonas();
// The composer's coworker picker is always mounted on a fresh session — refetch on
// mutations (enable/install from Settings) instead of going stale.
window.addEventListener(PERSONAS_CHANGED, load);
return () => window.removeEventListener(PERSONAS_CHANGED, load);
}, []);
window.addEventListener(PERSONAS_CHANGED, loadPersonas);
return () => window.removeEventListener(PERSONAS_CHANGED, loadPersonas);
}, [loadPersonas]);
const personaOf = (a: string) => personas?.find((p) => p.id === a);
// Pending Inbox items for the ACTIVE session — surfaced inline above the composer so an
@@ -505,6 +507,11 @@ export function App() {
// on a cold start that left "Loading models…" stuck until the user visited
// Settings (owner-hit 2026-07-23). Health just answered, so this one lands.
loadSettings();
// Same race, same fix: the mount-time persona fetch loses to the sidecar boot in
// the packaged app, and its only other trigger is PERSONAS_CHANGED — so the
// composer's coworker picker stayed empty for the whole session while Settings
// (mounted later) looked fine (owner-hit 2026-08-13).
loadPersonas();
if (!cancelled) setBooting(false);
})
.catch(() => {