fix(app): stale keyed show errors

This commit is contained in:
Adam
2026-03-06 11:03:37 -06:00
parent e9568999c3
commit a71b11caca
4 changed files with 315 additions and 249 deletions
+51
View File
@@ -445,6 +445,57 @@ export async function seedSessionPermission(
return { id: result.id } return { id: result.id }
} }
export async function seedSessionTask(
sdk: ReturnType<typeof createSdk>,
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( export async function seedSessionTodos(
sdk: ReturnType<typeof createSdk>, sdk: ReturnType<typeof createSdk>,
input: { input: {
@@ -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)
}
})
})
+22 -36
View File
@@ -527,19 +527,15 @@ export function AssistantParts(props: {
return ( return (
<Show when={message()}> <Show when={message()}>
{(msg) => (
<Show when={part()}> <Show when={part()}>
{(p) => (
<Part <Part
part={p()} part={part()!}
message={msg()} message={message()!}
showAssistantCopyPartID={props.showAssistantCopyPartID} showAssistantCopyPartID={props.showAssistantCopyPartID}
turnDurationMs={props.turnDurationMs} turnDurationMs={props.turnDurationMs}
defaultOpen={partDefaultOpen(p(), props.shellToolDefaultOpen, props.editToolDefaultOpen)} defaultOpen={partDefaultOpen(part()!, props.shellToolDefaultOpen, props.editToolDefaultOpen)}
/> />
)}
</Show> </Show>
)}
</Show> </Show>
) )
})()} })()}
@@ -741,13 +737,11 @@ export function AssistantMessageDisplay(props: {
return ( return (
<Show when={part()}> <Show when={part()}>
{(p) => (
<Part <Part
part={p()} part={part()!}
message={props.message} message={props.message}
showAssistantCopyPartID={props.showAssistantCopyPartID} showAssistantCopyPartID={props.showAssistantCopyPartID}
/> />
)}
</Show> </Show>
) )
})()} })()}
@@ -1410,11 +1404,9 @@ ToolRegistry.register({
trigger={{ title: i18n.t("ui.tool.list"), subtitle: getDirectory(props.input.path || "/") }} trigger={{ title: i18n.t("ui.tool.list"), subtitle: getDirectory(props.input.path || "/") }}
> >
<Show when={props.output}> <Show when={props.output}>
{(output) => (
<div data-component="tool-output" data-scrollable> <div data-component="tool-output" data-scrollable>
<Markdown text={output()} /> <Markdown text={props.output!} />
</div> </div>
)}
</Show> </Show>
</BasicTool> </BasicTool>
) )
@@ -1436,11 +1428,9 @@ ToolRegistry.register({
}} }}
> >
<Show when={props.output}> <Show when={props.output}>
{(output) => (
<div data-component="tool-output" data-scrollable> <div data-component="tool-output" data-scrollable>
<Markdown text={output()} /> <Markdown text={props.output!} />
</div> </div>
)}
</Show> </Show>
</BasicTool> </BasicTool>
) )
@@ -1465,11 +1455,9 @@ ToolRegistry.register({
}} }}
> >
<Show when={props.output}> <Show when={props.output}>
{(output) => (
<div data-component="tool-output" data-scrollable> <div data-component="tool-output" data-scrollable>
<Markdown text={output()} /> <Markdown text={props.output!} />
</div> </div>
)}
</Show> </Show>
</BasicTool> </BasicTool>
) )
@@ -1613,16 +1601,14 @@ ToolRegistry.register({
<Show when={description()}> <Show when={description()}>
<Switch> <Switch>
<Match when={href()}> <Match when={href()}>
{(url) => (
<a <a
data-slot="basic-tool-tool-subtitle" data-slot="basic-tool-tool-subtitle"
class="clickable subagent-link" class="clickable subagent-link"
href={url()} href={href()!}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{description()} {description()}
</a> </a>
)}
</Match> </Match>
<Match when={true}> <Match when={true}>
<span data-slot="basic-tool-tool-subtitle">{description()}</span> <span data-slot="basic-tool-tool-subtitle">{description()}</span>
@@ -1747,7 +1733,9 @@ ToolRegistry.register({
<ToolFileAccordion <ToolFileAccordion
path={path()} path={path()}
actions={ actions={
<Show when={!pending() && props.metadata.filediff}>{(diff) => <DiffChanges changes={diff()} />}</Show> <Show when={!pending() && props.metadata.filediff}>
<DiffChanges changes={props.metadata.filediff!} />
</Show>
} }
> >
<div data-component="edit-content"> <div data-component="edit-content">
@@ -1974,7 +1962,6 @@ ToolRegistry.register({
</div> </div>
} }
> >
{(file) => (
<div data-component="apply-patch-tool"> <div data-component="apply-patch-tool">
<BasicTool <BasicTool
{...props} {...props}
@@ -1988,44 +1975,44 @@ ToolRegistry.register({
<TextShimmer text={i18n.t("ui.tool.patch")} active={pending()} /> <TextShimmer text={i18n.t("ui.tool.patch")} active={pending()} />
</span> </span>
<Show when={!pending()}> <Show when={!pending()}>
<span data-slot="message-part-title-filename">{getFilename(file().relativePath)}</span> <span data-slot="message-part-title-filename">{getFilename(single()!.relativePath)}</span>
</Show> </Show>
</div> </div>
<Show when={!pending() && file().relativePath.includes("/")}> <Show when={!pending() && single()!.relativePath.includes("/")}>
<div data-slot="message-part-path"> <div data-slot="message-part-path">
<span data-slot="message-part-directory">{getDirectory(file().relativePath)}</span> <span data-slot="message-part-directory">{getDirectory(single()!.relativePath)}</span>
</div> </div>
</Show> </Show>
</div> </div>
<div data-slot="message-part-actions"> <div data-slot="message-part-actions">
<Show when={!pending()}> <Show when={!pending()}>
<DiffChanges changes={{ additions: file().additions, deletions: file().deletions }} /> <DiffChanges changes={{ additions: single()!.additions, deletions: single()!.deletions }} />
</Show> </Show>
</div> </div>
</div> </div>
} }
> >
<ToolFileAccordion <ToolFileAccordion
path={file().relativePath} path={single()!.relativePath}
actions={ actions={
<Switch> <Switch>
<Match when={file().type === "add"}> <Match when={single()!.type === "add"}>
<span data-slot="apply-patch-change" data-type="added"> <span data-slot="apply-patch-change" data-type="added">
{i18n.t("ui.patch.action.created")} {i18n.t("ui.patch.action.created")}
</span> </span>
</Match> </Match>
<Match when={file().type === "delete"}> <Match when={single()!.type === "delete"}>
<span data-slot="apply-patch-change" data-type="removed"> <span data-slot="apply-patch-change" data-type="removed">
{i18n.t("ui.patch.action.deleted")} {i18n.t("ui.patch.action.deleted")}
</span> </span>
</Match> </Match>
<Match when={file().type === "move"}> <Match when={single()!.type === "move"}>
<span data-slot="apply-patch-change" data-type="modified"> <span data-slot="apply-patch-change" data-type="modified">
{i18n.t("ui.patch.action.moved")} {i18n.t("ui.patch.action.moved")}
</span> </span>
</Match> </Match>
<Match when={true}> <Match when={true}>
<DiffChanges changes={{ additions: file().additions, deletions: file().deletions }} /> <DiffChanges changes={{ additions: single()!.additions, deletions: single()!.deletions }} />
</Match> </Match>
</Switch> </Switch>
} }
@@ -2034,14 +2021,13 @@ ToolRegistry.register({
<Dynamic <Dynamic
component={fileComponent} component={fileComponent}
mode="diff" mode="diff"
before={{ name: file().filePath, contents: file().before }} before={{ name: single()!.filePath, contents: single()!.before }}
after={{ name: file().movePath ?? file().filePath, contents: file().after }} after={{ name: single()!.movePath ?? single()!.filePath, contents: single()!.after }}
/> />
</div> </div>
</ToolFileAccordion> </ToolFileAccordion>
</BasicTool> </BasicTool>
</div> </div>
)}
</Show> </Show>
) )
}, },
+5 -13
View File
@@ -388,22 +388,19 @@ export function SessionTurn(
> >
<div onClick={autoScroll.handleInteraction}> <div onClick={autoScroll.handleInteraction}>
<Show when={message()}> <Show when={message()}>
{(msg) => (
<div <div
ref={autoScroll.contentRef} ref={autoScroll.contentRef}
data-message={msg().id} data-message={message()!.id}
data-slot="session-turn-message-container" data-slot="session-turn-message-container"
class={props.classes?.container} class={props.classes?.container}
> >
<div data-slot="session-turn-message-content" aria-live="off"> <div data-slot="session-turn-message-content" aria-live="off">
<Message message={msg()} parts={parts()} interrupted={interrupted()} queued={queued()} /> <Message message={message()!} parts={parts()} interrupted={interrupted()} queued={queued()} />
</div> </div>
<Show when={compaction()}> <Show when={compaction()}>
{(part) => (
<div data-slot="session-turn-compaction"> <div data-slot="session-turn-compaction">
<Part part={part()} message={msg()} hideDetails /> <Part part={compaction()!} message={message()!} hideDetails />
</div> </div>
)}
</Show> </Show>
<Show when={assistantMessages().length > 0}> <Show when={assistantMessages().length > 0}>
<div data-slot="session-turn-assistant-content" aria-hidden={working()}> <div data-slot="session-turn-assistant-content" aria-hidden={working()}>
@@ -438,9 +435,7 @@ export function SessionTurn(
<Collapsible.Trigger> <Collapsible.Trigger>
<div data-component="session-turn-diffs-trigger"> <div data-component="session-turn-diffs-trigger">
<div data-slot="session-turn-diffs-title"> <div data-slot="session-turn-diffs-title">
<span data-slot="session-turn-diffs-label"> <span data-slot="session-turn-diffs-label">{i18n.t("ui.sessionReview.change.modified")}</span>
{i18n.t("ui.sessionReview.change.modified")}
</span>
<span data-slot="session-turn-diffs-count"> <span data-slot="session-turn-diffs-count">
{edited()} {i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")} {edited()} {i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")}
</span> </span>
@@ -494,9 +489,7 @@ export function SessionTurn(
{`\u202A${getDirectory(diff.file)}\u202C`} {`\u202A${getDirectory(diff.file)}\u202C`}
</span> </span>
</Show> </Show>
<span data-slot="session-turn-diff-filename"> <span data-slot="session-turn-diff-filename">{getFilename(diff.file)}</span>
{getFilename(diff.file)}
</span>
</span> </span>
<div data-slot="session-turn-diff-meta"> <div data-slot="session-turn-diff-meta">
<span data-slot="session-turn-diff-changes"> <span data-slot="session-turn-diff-changes">
@@ -538,7 +531,6 @@ export function SessionTurn(
</Card> </Card>
</Show> </Show>
</div> </div>
)}
</Show> </Show>
{props.children} {props.children}
</div> </div>