From 5bf12ab7839d9f25ac5baf70f4611909fbe0770a Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:22:55 -0500 Subject: [PATCH] chore: cleanup subagent header --- .../session/session-child-navigation.spec.ts | 17 + .../src/pages/session/message-timeline.tsx | 360 ++++++++++-------- 2 files changed, 221 insertions(+), 156 deletions(-) diff --git a/packages/app/e2e/session/session-child-navigation.spec.ts b/packages/app/e2e/session/session-child-navigation.spec.ts index 4b21223846..c9fad1af85 100644 --- a/packages/app/e2e/session/session-child-navigation.spec.ts +++ b/packages/app/e2e/session/session-child-navigation.spec.ts @@ -29,6 +29,9 @@ test("task tool child-session link does not trigger stale show errors", async ({ await project.gotoSession(session.id) + const header = page.locator("[data-session-title]") + await expect(header.getByRole("button", { name: "More options" })).toBeVisible({ timeout: 30_000 }) + const card = page .locator('[data-component="task-tool-card"]') .filter({ hasText: /open child session/i }) @@ -37,6 +40,20 @@ test("task tool child-session link does not trigger stale show errors", async ({ await card.click() await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 }) + await expect(header.locator('[data-slot="session-title-parent"]')).toHaveText(session.title) + await expect(header.locator('[data-slot="session-title-child"]')).toHaveText(taskInput.description) + await expect(header.locator('[data-slot="session-title-separator"]')).toHaveText("/") + await expect + .poll( + () => + header.locator('[data-slot="session-title-separator"]').evaluate((el) => ({ + left: getComputedStyle(el).paddingLeft, + right: getComputedStyle(el).paddingRight, + })), + { timeout: 30_000 }, + ) + .toEqual({ left: "8px", right: "8px" }) + await expect(header.getByRole("button", { name: "More options" })).toHaveCount(0) await expect(page.getByText("Subagent sessions cannot be prompted.")).toBeVisible({ timeout: 30_000 }) await expect(page.getByRole("button", { name: "Back to main session." })).toBeVisible({ timeout: 30_000 }) await expect.poll(() => errs, { timeout: 5_000 }).toEqual([]) diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 14983094ab..349acd5720 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -68,6 +68,14 @@ const messageComments = (parts: Part[]): MessageComment[] => ] }) +const taskDescription = (part: Part, sessionID: string) => { + if (part.type !== "tool" || part.tool !== "task") return + const metadata = "metadata" in part.state ? part.state.metadata : undefined + if (metadata?.sessionId !== sessionID) return + const value = part.state.input?.description + if (typeof value === "string" && value) return value +} + const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => { const current = target instanceof Element ? target : undefined const nested = current?.closest("[data-scrollable]") @@ -300,8 +308,27 @@ export function MessageTimeline(props: { if (!id) return return sync.session.get(id) }) + const parentMessages = createMemo(() => { + const id = parentID() + if (!id) return emptyMessages + return sync.data.message[id] ?? emptyMessages + }) const parentTitle = createMemo(() => sessionTitle(parent()?.title) ?? language.t("command.session.new")) - const childTitle = createMemo(() => titleLabel() ?? (parentID() ? language.t("command.session.new") : "")) + const childTaskDescription = createMemo(() => { + const id = sessionID() + if (!id) return + return parentMessages() + .flatMap((message) => sync.data.part[message.id] ?? []) + .map((part) => taskDescription(part, id)) + .findLast((value): value is string => !!value) + }) + const childTitle = createMemo(() => { + if (!parentID()) return titleLabel() ?? "" + if (childTaskDescription()) return childTaskDescription() + const value = titleLabel()?.replace(/\s+\(@[^)]+ subagent\)$/, "") + if (value) return value + return language.t("command.session.new") + }) const showHeader = createMemo(() => !!(titleValue() || parentID())) const stageCfg = { init: 1, batch: 3 } const staging = createTimelineStaging({ @@ -405,8 +432,20 @@ export function MessageTimeline(props: { ), ) + createEffect( + on( + () => [parentID(), childTaskDescription()] as const, + ([id, description]) => { + if (!id || description) return + if (sync.data.message[id] !== undefined) return + void sync.session.sync(id) + }, + { defer: true }, + ), + ) + const openTitleEditor = () => { - if (!sessionID()) return + if (!sessionID() || parentID()) return setTitle({ editing: true, draft: titleLabel() ?? "" }) requestAnimationFrame(() => { titleRef?.focus() @@ -660,12 +699,17 @@ export function MessageTimeline(props: { - @@ -691,6 +735,7 @@ export function MessageTimeline(props: { when={title.editing} fallback={

@@ -702,6 +747,7 @@ export function MessageTimeline(props: { ref={(el) => { titleRef = el }} + data-slot="session-title-child" value={title.draft} disabled={titleMutation.isPending} class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px]" @@ -729,177 +775,179 @@ export function MessageTimeline(props: { {(id) => (
- { - setTitle("menuOpen", open) - if (open) return - }} - > - + { + setTitle("menuOpen", open) + if (open) return }} - aria-label={language.t("common.moreOptions")} - aria-expanded={title.menuOpen || share.open || title.pendingShare} - ref={(el: HTMLButtonElement) => { - more = el - }} - /> - - { - if (title.pendingRename) { - event.preventDefault() - setTitle("pendingRename", false) - openTitleEditor() - return - } - if (title.pendingShare) { - event.preventDefault() - requestAnimationFrame(() => { - setShare({ open: true, dismiss: null }) - setTitle("pendingShare", false) - }) - } + > + - { - setTitle("pendingRename", true) - setTitle("menuOpen", false) + aria-label={language.t("common.moreOptions")} + aria-expanded={title.menuOpen || share.open || title.pendingShare} + ref={(el: HTMLButtonElement) => { + more = el + }} + /> + + { + if (title.pendingRename) { + event.preventDefault() + setTitle("pendingRename", false) + openTitleEditor() + return + } + if (title.pendingShare) { + event.preventDefault() + requestAnimationFrame(() => { + setShare({ open: true, dismiss: null }) + setTitle("pendingShare", false) + }) + } }} > - {language.t("common.rename")} - - { - setTitle({ pendingShare: true, menuOpen: false }) + setTitle("pendingRename", true) + setTitle("menuOpen", false) }} > - - {language.t("session.share.action.share")} - + {language.t("common.rename")} - - void archiveSession(id())}> - {language.t("common.archive")} - - - dialog.show(() => )} - > - {language.t("common.delete")} - - - - - - more} - placement="bottom-end" - gutter={4} - modal={false} - onOpenChange={(open) => { - if (open) setShare("dismiss", null) - setShare("open", open) - }} - > - - { - setShare({ dismiss: "escape", open: false }) - event.preventDefault() - event.stopPropagation() - }} - onPointerDownOutside={() => { - setShare({ dismiss: "outside", open: false }) - }} - onFocusOutside={() => { - setShare({ dismiss: "outside", open: false }) - }} - onCloseAutoFocus={(event) => { - if (share.dismiss === "outside") event.preventDefault() - setShare("dismiss", null) - }} - > -
-
-
- {language.t("session.share.popover.title")} -
-
- {shareUrl() - ? language.t("session.share.popover.description.shared") - : language.t("session.share.popover.description.unshared")} -
-
-
- - {shareMutation.isPending - ? language.t("session.share.action.publishing") - : language.t("session.share.action.publish")} - - } + + { + setTitle({ pendingShare: true, menuOpen: false }) + }} > -
- -
- + + {language.t("session.share.action.share")} + + + + void archiveSession(id())}> + {language.t("common.archive")} + + + dialog.show(() => )} + > + {language.t("common.delete")} + + + + + + more} + placement="bottom-end" + gutter={4} + modal={false} + onOpenChange={(open) => { + if (open) setShare("dismiss", null) + setShare("open", open) + }} + > + + { + setShare({ dismiss: "escape", open: false }) + event.preventDefault() + event.stopPropagation() + }} + onPointerDownOutside={() => { + setShare({ dismiss: "outside", open: false }) + }} + onFocusOutside={() => { + setShare({ dismiss: "outside", open: false }) + }} + onCloseAutoFocus={(event) => { + if (share.dismiss === "outside") event.preventDefault() + setShare("dismiss", null) + }} + > +
+
+
+ {language.t("session.share.popover.title")} +
+
+ {shareUrl() + ? language.t("session.share.popover.description.shared") + : language.t("session.share.popover.description.unshared")} +
+
+
+ - {language.t("session.share.action.view")} + {shareMutation.isPending + ? language.t("session.share.action.publishing") + : language.t("session.share.action.publish")} + } + > +
+ +
+ + +
-
- + +
-
- - - + + + +
)}