From a71b11caca88243a9e4399317bcc5234d432976c Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:03:32 -0600 Subject: [PATCH] fix(app): stale keyed show errors --- packages/app/e2e/actions.ts | 51 ++++ .../session/session-child-navigation.spec.ts | 37 +++ packages/ui/src/components/message-part.tsx | 202 ++++++------- packages/ui/src/components/session-turn.tsx | 274 +++++++++--------- 4 files changed, 315 insertions(+), 249 deletions(-) create mode 100644 packages/app/e2e/session/session-child-navigation.spec.ts diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts index fbb13008b..919a1add8 100644 --- a/packages/app/e2e/actions.ts +++ b/packages/app/e2e/actions.ts @@ -445,6 +445,57 @@ export async function seedSessionPermission( return { id: result.id } } +export async function seedSessionTask( + sdk: ReturnType, + input: { + sessionID: string + description: string + prompt: string + subagentType?: string + }, +) { + const text = [ + "Your only valid response is one task tool call.", + `Use this JSON input: ${JSON.stringify({ + description: input.description, + prompt: input.prompt, + subagent_type: input.subagentType ?? "general", + })}`, + "Do not output plain text.", + "Wait for the task to start and return the child session id.", + ].join("\n") + + const result = await seed({ + sdk, + sessionID: input.sessionID, + prompt: text, + timeout: 90_000, + probe: async () => { + const messages = await sdk.session.messages({ sessionID: input.sessionID, limit: 50 }).then((x) => x.data ?? []) + const part = messages + .flatMap((message) => message.parts) + .find((part) => { + if (part.type !== "tool" || part.tool !== "task") return false + if (part.state.input?.description !== input.description) return false + return typeof part.state.metadata?.sessionId === "string" && part.state.metadata.sessionId.length > 0 + }) + + if (!part) return + const id = part.state.metadata?.sessionId + if (typeof id !== "string" || !id) return + const child = await sdk.session + .get({ sessionID: id }) + .then((x) => x.data) + .catch(() => undefined) + if (!child?.id) return + return { sessionID: id } + }, + }) + + if (!result) throw new Error("Timed out seeding task tool") + return result +} + export async function seedSessionTodos( sdk: ReturnType, input: { diff --git a/packages/app/e2e/session/session-child-navigation.spec.ts b/packages/app/e2e/session/session-child-navigation.spec.ts new file mode 100644 index 000000000..ac2dca33c --- /dev/null +++ b/packages/app/e2e/session/session-child-navigation.spec.ts @@ -0,0 +1,37 @@ +import { seedSessionTask, withSession } from "../actions" +import { test, expect } from "../fixtures" + +test("task tool child-session link does not trigger stale show errors", async ({ page, sdk, gotoSession }) => { + test.setTimeout(120_000) + + const errs: string[] = [] + const onError = (err: Error) => { + errs.push(err.message) + } + page.on("pageerror", onError) + + await withSession(sdk, `e2e child nav ${Date.now()}`, async (session) => { + const child = await seedSessionTask(sdk, { + sessionID: session.id, + description: "Open child session", + prompt: "Search the repository for AssistantParts and then reply with exactly CHILD_OK.", + }) + + try { + await gotoSession(session.id) + + const link = page + .locator("a.subagent-link") + .filter({ hasText: /open child session/i }) + .first() + await expect(link).toBeVisible({ timeout: 30_000 }) + await link.click() + + await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 }) + await page.waitForTimeout(1000) + expect(errs).toEqual([]) + } finally { + page.off("pageerror", onError) + } + }) +}) diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index f59b17e9a..9286d2a92 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -527,19 +527,15 @@ export function AssistantParts(props: { return ( - {(msg) => ( - - {(p) => ( - - )} - - )} + + + ) })()} @@ -741,13 +737,11 @@ export function AssistantMessageDisplay(props: { return ( - {(p) => ( - - )} + ) })()} @@ -1410,11 +1404,9 @@ ToolRegistry.register({ trigger={{ title: i18n.t("ui.tool.list"), subtitle: getDirectory(props.input.path || "/") }} > - {(output) => ( -
- -
- )} +
+ +
) @@ -1436,11 +1428,9 @@ ToolRegistry.register({ }} > - {(output) => ( -
- -
- )} +
+ +
) @@ -1465,11 +1455,9 @@ ToolRegistry.register({ }} > - {(output) => ( -
- -
- )} +
+ +
) @@ -1613,16 +1601,14 @@ ToolRegistry.register({ - {(url) => ( - e.stopPropagation()} - > - {description()} - - )} + e.stopPropagation()} + > + {description()} + {description()} @@ -1747,7 +1733,9 @@ ToolRegistry.register({ {(diff) => } + + + } >
@@ -1974,74 +1962,72 @@ ToolRegistry.register({
} > - {(file) => ( -
- -
-
- - - - - {getFilename(file().relativePath)} - -
- -
- {getDirectory(file().relativePath)} -
-
-
-
+
+ +
+
+ + + - + {getFilename(single()!.relativePath)}
+ +
+ {getDirectory(single()!.relativePath)} +
+
+
+ + + +
+
+ } + > + + + + {i18n.t("ui.patch.action.created")} + + + + + {i18n.t("ui.patch.action.deleted")} + + + + + {i18n.t("ui.patch.action.moved")} + + + + + + } > - - - - {i18n.t("ui.patch.action.created")} - - - - - {i18n.t("ui.patch.action.deleted")} - - - - - {i18n.t("ui.patch.action.moved")} - - - - - - - } - > -
- -
-
- -
- )} +
+ +
+ +
+
) }, diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index a8a41b8ef..3323a9fc6 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -388,157 +388,149 @@ export function SessionTurn( >
- {(msg) => ( -
-
- +
+
+ +
+ +
+
- - {(part) => ( -
- -
- )} -
- 0}> -
- + 0}> +
+ +
+
+ +
+ + + -
-
- -
- - - - -
-
- - 0 && !working()}> -
- - -
-
- - {i18n.t("ui.sessionReview.change.modified")} - - - {edited()} {i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")} - -
- - -
+ +
+ + + 0 && !working()}> +
+ + +
+
+ {i18n.t("ui.sessionReview.change.modified")} + + {edited()} {i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")} + +
+ +
- - - -
- setExpanded(Array.isArray(value) ? value : value ? [value] : [])} - > - - {(diff) => { - const active = createMemo(() => expanded().includes(diff.file)) - const [visible, setVisible] = createSignal(false) +
+ + + +
+ setExpanded(Array.isArray(value) ? value : value ? [value] : [])} + > + + {(diff) => { + const active = createMemo(() => expanded().includes(diff.file)) + const [visible, setVisible] = createSignal(false) - createEffect( - on( - active, - (value) => { - if (!value) { - setVisible(false) - return - } + createEffect( + on( + active, + (value) => { + if (!value) { + setVisible(false) + return + } - requestAnimationFrame(() => { - if (!active()) return - setVisible(true) - }) - }, - { defer: true }, - ), - ) + requestAnimationFrame(() => { + if (!active()) return + setVisible(true) + }) + }, + { defer: true }, + ), + ) - return ( - - - -
- - - - {`\u202A${getDirectory(diff.file)}\u202C`} - - - - {getFilename(diff.file)} + return ( + + + +
+ + + + {`\u202A${getDirectory(diff.file)}\u202C`} + + {getFilename(diff.file)} + +
+ + + + + -
- - - - - - -
- - - - -
- -
-
-
- - ) - }} - - -
- - - -
- - - - {errorText()} - - -
- )} +
+ + + + +
+ +
+
+
+ + ) + }} + + +
+
+ + +
+ + + + {errorText()} + + +
{props.children}