diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md
index 535a6dd712..e9ca2211c2 100644
--- a/docs/agent-lifecycle.md
+++ b/docs/agent-lifecycle.md
@@ -242,7 +242,7 @@ parentAgentId === thisAgent.id AND !archivedAt
- **Provider subagents** are child executions owned by Claude, Codex, or OpenCode. They are not inserted into `AgentManager` as managed agents. Providers emit a separate descriptor and timeline stream through `agent.provider_subagents.*`; the client keeps that state outside the normal agent store and merges only the presentation rows into the track. A descriptor's optional `parentSubagentId` identifies its direct provider-subagent parent; an absent value identifies a direct child of the managed agent.
-Clicking either kind opens a workspace tab. On compact layouts it opens in a [sheet](explorer-sidebar.md#sheets) over the subagents list instead; the sheet hides the composer and shows the agent's model and thinking as one line in its place (`PaneSubtitle`, the row provider subagents already use), and a Paseo subagent's sheet offers **Open as tab** (the full-screen glyph in the header) to get the composer back. A Paseo subagent tab is a normal interactive agent pane. A provider subagent tab is a read-only timeline pane with no composer, archive, detach, rewind, or fork actions. It shows its own direct children in a subagents track. Both panes use `AgentStreamView`, so message, reasoning, tool-call, and layout rendering stay identical.
+Clicking either kind opens a workspace tab. On compact layouts it opens in a [sheet](explorer-sidebar.md#sheets) over the subagents list instead; the sheet hides the composer and shows the agent's model and thinking as one line in its place (`PaneSubtitle`, the row provider subagents already use), and a Paseo subagent's sheet offers **Open as tab** (the full-screen glyph in the header) to get the composer back. A Paseo subagent tab is a normal interactive agent pane. A provider subagent tab is a read-only timeline pane with no composer, archive, detach, rewind, or fork actions. It shows its own direct children in a subagents track. Every subagent pane links back to its parent (`packages/app/src/subagents/parent/`): a pill above the composer when there is one, a parent box in the composer's place when there is not. Both panes use `AgentStreamView`, so message, reasoning, tool-call, and layout rendering stay identical.
Provider timelines use the same structural timeline item format but deliberately have a separate lifecycle and transport. A provider thread/session identifier is not a Paseo agent identifier, and closing its tab is always layout-only.
diff --git a/packages/app/e2e/browser/compact-workspace-sheet.spec.ts b/packages/app/e2e/browser/compact-workspace-sheet.spec.ts
index 990d490d9b..77d995eda7 100644
--- a/packages/app/e2e/browser/compact-workspace-sheet.spec.ts
+++ b/packages/app/e2e/browser/compact-workspace-sheet.spec.ts
@@ -99,6 +99,16 @@ test("compact opens files and subagents in a sheet, leaving the tabs for agents"
await expectSubagentRowVisible(page, child.id);
});
+ await test.step("the parent box in a subagent's sheet closes it onto the parent", async () => {
+ await page.getByTestId(`subagents-track-row-${child.id}`).click();
+ const childSheet = sheet(page, child.title);
+ await expectSheetAtSnap(page, child.title);
+ await expect(childSheet.getByTestId("composer-parent-agent-pill")).toHaveCount(0);
+ await childSheet.getByTestId("parent-agent-callout-open").click();
+ await expect(childSheet).toHaveCount(0);
+ await expectOpenTabCount(page, 1);
+ });
+
await test.step("Open as tab promotes the subagent to a full tab and ends the list", async () => {
await page.getByTestId(`subagents-track-row-${child.id}`).click();
await expectSheetAtSnap(page, child.title);
diff --git a/packages/app/e2e/browser/provider-subagents.real.spec.ts b/packages/app/e2e/browser/provider-subagents.real.spec.ts
index 08d8a1afe1..b24ca9ba4e 100644
--- a/packages/app/e2e/browser/provider-subagents.real.spec.ts
+++ b/packages/app/e2e/browser/provider-subagents.real.spec.ts
@@ -136,7 +136,8 @@ test.describe("real provider subagent timelines", () => {
panel.getByText("Start chatting with this agent...", { exact: true }),
).toHaveCount(0);
- await page.getByTestId(`workspace-tab-agent_${handle.agentId}`).first().click();
+ // A provider subagent has no composer; its callout is the way back to the parent.
+ await page.getByTestId("parent-agent-callout-open").click();
await expect(
page.getByTestId("assistant-message").filter({ hasText: "ROOT_DONE" }).last(),
).toBeVisible({ timeout: 60_000 });
diff --git a/packages/app/e2e/browser/subagent-origin-pane.spec.ts b/packages/app/e2e/browser/subagent-origin-pane.spec.ts
index 30b19e5099..545076ab06 100644
--- a/packages/app/e2e/browser/subagent-origin-pane.spec.ts
+++ b/packages/app/e2e/browser/subagent-origin-pane.spec.ts
@@ -125,6 +125,29 @@ test("opening an already-tabbed subagent reveals its left tab without a duplicat
}
});
+test("a subagent's parent pill returns to its parent", async ({ page }) => {
+ const workspace = await seedWorkspace({ repoPrefix: "subagent-parent-pill-" });
+ try {
+ const pair = await seedParentWithSubagent(workspace, {
+ parentTitle: "Refactor auth flow",
+ childTitle: "Write session tests",
+ });
+ await page.setViewportSize({ width: 1400, height: 900 });
+ await openAgentRoute(page, { workspaceId: pair.workspaceId, agentId: pair.child.id });
+
+ const pill = page.getByTestId("composer-parent-agent-pill");
+ await expect(pill).toHaveAccessibleName("Back to Refactor auth flow");
+ await pill.click();
+
+ await expect(page.getByTestId(`workspace-tab-agent_${pair.parent.id}`)).toHaveAttribute(
+ "aria-selected",
+ "true",
+ );
+ } finally {
+ await workspace.cleanup();
+ }
+});
+
test("a preferred chat file link still opens beside its right-pane agent", async ({ page }) => {
const target = "linked.ts:3";
const workspace = await seedMockAgentWorkspace({
diff --git a/packages/app/src/composer/tracks.tsx b/packages/app/src/composer/tracks.tsx
index 3a4541d285..82bee6e3fa 100644
--- a/packages/app/src/composer/tracks.tsx
+++ b/packages/app/src/composer/tracks.tsx
@@ -276,7 +276,11 @@ export function ComposerTrackRow({
* knockout for marks that sit on top of an icon, and nothing sits under this one, so it comes off
* too — leaving every mark's visible edge on the same rail whatever state it is in.
*/
-function ComposerTrackMark({ bucket }: { bucket: SidebarStateBucket | null }): ReactElement | null {
+export function ComposerTrackMark({
+ bucket,
+}: {
+ bucket: SidebarStateBucket | null;
+}): ReactElement | null {
if (!bucket) {
return null;
}
diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts
index c765f3b835..6149937f0c 100644
--- a/packages/app/src/i18n/resources/ar.ts
+++ b/packages/app/src/i18n/resources/ar.ts
@@ -1908,6 +1908,13 @@ export const ar: TranslationResources = {
backdrop: "خلفية القائمة",
},
subagents: {
+ parent: {
+ backToParent: "العودة إلى الوكيل الأصلي",
+ backTo: "العودة إلى {{label}}",
+ parent: "الأصل",
+ parentAgent: "الوكيل الأصلي",
+ openParent: "فتح الوكيل الأصلي",
+ },
title: "الوكلاء الفرعيون",
pillLabelOne: "وكيل فرعي واحد",
pillLabelMany: "{{count}} وكلاء فرعيين",
diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts
index 351fb33ac3..085a860182 100644
--- a/packages/app/src/i18n/resources/en.ts
+++ b/packages/app/src/i18n/resources/en.ts
@@ -1933,6 +1933,13 @@ export const en = {
backdrop: "Menu backdrop",
},
subagents: {
+ parent: {
+ backToParent: "Back to parent",
+ backTo: "Back to {{label}}",
+ parent: "Parent",
+ parentAgent: "Parent agent",
+ openParent: "Open parent",
+ },
title: "Subagents",
pillLabelOne: "1 subagent",
pillLabelMany: "{{count}} subagents",
diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts
index fd195ce599..b564ee83f3 100644
--- a/packages/app/src/i18n/resources/es.ts
+++ b/packages/app/src/i18n/resources/es.ts
@@ -1956,6 +1956,13 @@ export const es: TranslationResources = {
backdrop: "Fondo del menú",
},
subagents: {
+ parent: {
+ backToParent: "Volver al padre",
+ backTo: "Volver a {{label}}",
+ parent: "Padre",
+ parentAgent: "Agente padre",
+ openParent: "Abrir padre",
+ },
title: "Subagentes",
pillLabelOne: "1 subagente",
pillLabelMany: "{{count}} subagentes",
diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts
index 975d6a3736..ad7d5796f6 100644
--- a/packages/app/src/i18n/resources/fr.ts
+++ b/packages/app/src/i18n/resources/fr.ts
@@ -1954,6 +1954,13 @@ export const fr: TranslationResources = {
backdrop: "Arrière-plan du menu",
},
subagents: {
+ parent: {
+ backToParent: "Retour au parent",
+ backTo: "Retour à {{label}}",
+ parent: "Parent",
+ parentAgent: "Agent parent",
+ openParent: "Ouvrir le parent",
+ },
title: "Sous-agents",
pillLabelOne: "1 sous-agent",
pillLabelMany: "{{count}} sous-agents",
diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts
index 60a207e1e1..43a4952afa 100644
--- a/packages/app/src/i18n/resources/ja.ts
+++ b/packages/app/src/i18n/resources/ja.ts
@@ -1927,6 +1927,13 @@ export const ja: TranslationResources = {
backdrop: "メニューの背景",
},
subagents: {
+ parent: {
+ backToParent: "親に戻る",
+ backTo: "{{label}} に戻る",
+ parent: "親",
+ parentAgent: "親エージェント",
+ openParent: "親を開く",
+ },
title: "サブエージェント",
pillLabelOne: "サブエージェント 1 件",
pillLabelMany: "サブエージェント {{count}} 件",
diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts
index 4773625327..b3edee4ac6 100644
--- a/packages/app/src/i18n/resources/ko.ts
+++ b/packages/app/src/i18n/resources/ko.ts
@@ -1918,6 +1918,13 @@ export const ko: TranslationResources = {
backdrop: "메뉴 배경",
},
subagents: {
+ parent: {
+ backToParent: "상위 에이전트로 돌아가기",
+ backTo: "{{label}}(으)로 돌아가기",
+ parent: "상위",
+ parentAgent: "상위 에이전트",
+ openParent: "상위 에이전트 열기",
+ },
title: "하위 에이전트",
pillLabelOne: "하위 에이전트 1개",
pillLabelMany: "하위 에이전트 {{count}}개",
diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts
index 766c6d98ed..531f379172 100644
--- a/packages/app/src/i18n/resources/pt-BR.ts
+++ b/packages/app/src/i18n/resources/pt-BR.ts
@@ -1941,6 +1941,13 @@ export const ptBR: TranslationResources = {
backdrop: "Fundo do menu",
},
subagents: {
+ parent: {
+ backToParent: "Voltar ao pai",
+ backTo: "Voltar para {{label}}",
+ parent: "Pai",
+ parentAgent: "Agente pai",
+ openParent: "Abrir pai",
+ },
title: "Subagentes",
pillLabelOne: "1 subagente",
pillLabelMany: "{{count}} subagentes",
diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts
index 23f3d810ee..8923882017 100644
--- a/packages/app/src/i18n/resources/ru.ts
+++ b/packages/app/src/i18n/resources/ru.ts
@@ -1940,6 +1940,13 @@ export const ru: TranslationResources = {
backdrop: "Фон меню",
},
subagents: {
+ parent: {
+ backToParent: "Назад к родителю",
+ backTo: "Назад к {{label}}",
+ parent: "Родитель",
+ parentAgent: "Родительский агент",
+ openParent: "Открыть родителя",
+ },
title: "Субагенты",
pillLabelOne: "1 субагент",
pillLabelMany: "Субагентов: {{count}}",
diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts
index 43d8ceeef2..dedf6efc25 100644
--- a/packages/app/src/i18n/resources/zh-CN.ts
+++ b/packages/app/src/i18n/resources/zh-CN.ts
@@ -1886,6 +1886,13 @@ export const zhCN: TranslationResources = {
backdrop: "菜单背景",
},
subagents: {
+ parent: {
+ backToParent: "返回父代理",
+ backTo: "返回 {{label}}",
+ parent: "父代理",
+ parentAgent: "父代理",
+ openParent: "打开父代理",
+ },
title: "Subagent",
pillLabelOne: "1 个 subagent",
pillLabelMany: "{{count}} 个 subagent",
diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx
index af14b94229..ea7cc68b37 100644
--- a/packages/app/src/panels/agent-panel.tsx
+++ b/packages/app/src/panels/agent-panel.tsx
@@ -80,6 +80,7 @@ import {
} from "@/screens/agent/agent-ready-screen-bottom-anchor";
import { WorkspaceDraftAgentTab } from "@/composer/draft/workspace-tab";
import { AgentTracks, hasAgentTracks } from "@/panels/agent-tracks";
+import { ParentAgentCallout, useParentAgentId } from "@/subagents/parent";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import { buildDraftStoreKey, generateDraftId } from "@/stores/draft-keys";
import {
@@ -1109,7 +1110,11 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
});
const hasPluginComposerPills = useHasPluginComposerPills(serverId, workspaceId, agentId);
const hasActiveComposer = !agentState.archivedAt && !isArchivingCurrentAgent;
+ const parentAgentId = useParentAgentId(serverId, agentId);
+ // With a composer the parent is a pill above it; without one, the box takes the composer's place.
+ const parentPillAgentId = showsComposer ? parentAgentId : null;
const hasVisibleAgentTracks = hasAgentTracks({
+ parentAgentId: parentPillAgentId,
subagentRows,
tasks,
archiveFinishedStatus: archiveFinishedSubagents.status,
@@ -1161,6 +1166,9 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
composerState,
],
);
+ const parentCallout = parentAgentId ? (
+
+ ) : null;
const composerSection = showsComposer ? (
- ) : null;
+ ) : (
+ parentCallout
+ );
const streamContent = (
{showsComposer ? null : }
@@ -1203,6 +1213,7 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
serverId={serverId}
workspaceId={workspaceId}
agentId={agentId}
+ parentAgentId={parentPillAgentId}
subagentRows={subagentRows}
tasks={tasks}
archiveFinishedStatus={archiveFinishedSubagents.status}
diff --git a/packages/app/src/panels/agent-tracks.tsx b/packages/app/src/panels/agent-tracks.tsx
index 3fc42030a7..f7c724133a 100644
--- a/packages/app/src/panels/agent-tracks.tsx
+++ b/packages/app/src/panels/agent-tracks.tsx
@@ -16,6 +16,7 @@ import {
useSubagentOpensInSheet,
type SubagentRow,
} from "@/subagents";
+import { ParentAgentPill } from "@/subagents/parent";
import { SubagentsTrack } from "@/subagents/track";
import type { TodoEntry } from "@/types/stream";
import { navigateToAgent } from "@/utils/navigate-to-agent";
@@ -34,6 +35,7 @@ export const AgentTracks = memo(function AgentTracks({
serverId,
workspaceId,
agentId,
+ parentAgentId,
subagentRows,
tasks,
archiveFinishedStatus,
@@ -43,6 +45,7 @@ export const AgentTracks = memo(function AgentTracks({
serverId: string;
workspaceId: string;
agentId: string;
+ parentAgentId: string | null;
subagentRows: SubagentRow[];
tasks: TodoEntry[] | undefined;
archiveFinishedStatus: ArchiveFinishedStatus;
@@ -88,8 +91,12 @@ export const AgentTracks = memo(function AgentTracks({
[openSubagentTarget, serverId, workspaceId],
);
const handleOpenProviderSubagent = useCallback(
- (parentAgentId: string, subagentId: string) => {
- openSubagentTarget({ kind: "provider_subagent", parentAgentId, subagentId });
+ (ownerAgentId: string, subagentId: string) => {
+ openSubagentTarget({
+ kind: "provider_subagent",
+ parentAgentId: ownerAgentId,
+ subagentId,
+ });
},
[openSubagentTarget],
);
@@ -108,6 +115,7 @@ export const AgentTracks = memo(function AgentTracks({
if (
!hasWorkspaceDiffStat &&
!hasAgentTracks({
+ parentAgentId,
subagentRows,
tasks,
archiveFinishedStatus,
@@ -119,6 +127,7 @@ export const AgentTracks = memo(function AgentTracks({
return (
+ {parentAgentId ? : null}
0 ||
Boolean(tasks?.length) ||
archiveFinishedStatus.kind !== "idle" ||
diff --git a/packages/app/src/panels/provider-subagent-panel.tsx b/packages/app/src/panels/provider-subagent-panel.tsx
index ecd4a082bf..e36f7880ca 100644
--- a/packages/app/src/panels/provider-subagent-panel.tsx
+++ b/packages/app/src/panels/provider-subagent-panel.tsx
@@ -16,6 +16,7 @@ import { usePaneContext } from "@/panels/pane-context";
import { PaneSubtitle } from "@/panels/pane-subtitle";
import { definePanel, type PanelDescriptor } from "@/panels/panel-registry";
import { useSessionStore } from "@/stores/session-store";
+import { ParentAgentCallout } from "@/subagents/parent";
import { type SubagentRow, useSubagentsForParent } from "@/subagents/select";
import { useSubagentOpensInSheet } from "@/subagents/opens-in-sheet";
import { SubagentsTrack } from "@/subagents/track";
@@ -253,33 +254,37 @@ function ProviderSubagentPanel() {
return (
{subtitle ? : null}
-
-
+
+
+
+
+
);
}
const styles = StyleSheet.create((theme) => ({
container: { flex: 1, minHeight: 0 },
+ stream: { flex: 1, minHeight: 0 },
unsupported: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 },
unsupportedText: { color: theme.colors.foregroundMuted, textAlign: "center" },
}));
diff --git a/packages/app/src/subagents/parent/callout.tsx b/packages/app/src/subagents/parent/callout.tsx
new file mode 100644
index 0000000000..f76e1267e3
--- /dev/null
+++ b/packages/app/src/subagents/parent/callout.tsx
@@ -0,0 +1,116 @@
+import type { ReactElement } from "react";
+import { Text, View } from "react-native";
+import { useTranslation } from "react-i18next";
+import { CornerUpLeft } from "lucide-react-native";
+import { StyleSheet, withUnistyles } from "react-native-unistyles";
+import { Button } from "@/components/ui/button";
+import { ComposerTrackMark } from "@/composer/tracks";
+import { FOOTER_HEIGHT } from "@/constants/layout";
+import type { Theme } from "@/styles/theme";
+import { useParentAgent, type ParentAgent } from "./use-parent-agent";
+
+function ParentIcon({
+ icon: Icon,
+ size,
+ color = "",
+}: {
+ icon: ParentAgent["icon"];
+ size: number;
+ color?: string;
+}) {
+ return ;
+}
+
+const ThemedParentIcon = withUnistyles(ParentIcon);
+const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
+
+/** Stands in the composer's place on a subagent shown without one: who the parent is, and the way back. */
+export function ParentAgentCallout({
+ serverId,
+ parentAgentId,
+}: {
+ serverId: string;
+ parentAgentId: string;
+}): ReactElement {
+ const { t } = useTranslation();
+ const parent = useParentAgent(serverId, parentAgentId);
+
+ return (
+
+
+
+
+
+
+ {t("subagents.parent.parent")}
+
+
+ {parent.label ?? t("subagents.parent.parentAgent")}
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create((theme) => ({
+ rail: {
+ minHeight: FOOTER_HEIGHT,
+ alignItems: "center",
+ width: "100%",
+ padding: theme.spacing[4],
+ },
+ callout: {
+ width: "100%",
+ maxWidth: theme.contentMaxWidth,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: theme.spacing[3],
+ backgroundColor: theme.colors.surface1,
+ borderWidth: theme.borderWidth[1],
+ borderColor: theme.colors.borderAccent,
+ borderRadius: theme.borderRadius["2xl"],
+ padding: theme.spacing[3],
+ paddingRight: { xs: theme.spacing[3], md: theme.spacing[4] },
+ },
+ avatar: {
+ width: 36,
+ height: 36,
+ borderRadius: theme.borderRadius.lg,
+ backgroundColor: theme.colors.surface2,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ body: {
+ flex: 1,
+ minWidth: 0,
+ gap: theme.spacing[0.5],
+ },
+ eyebrow: {
+ fontSize: theme.fontSize.sm,
+ color: theme.colors.foregroundMuted,
+ },
+ titleRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: theme.spacing[2],
+ minWidth: 0,
+ },
+ title: {
+ flexShrink: 1,
+ fontSize: theme.fontSize.base,
+ fontWeight: theme.fontWeight.medium,
+ color: theme.colors.foreground,
+ },
+}));
diff --git a/packages/app/src/subagents/parent/index.ts b/packages/app/src/subagents/parent/index.ts
new file mode 100644
index 0000000000..daca7d3d16
--- /dev/null
+++ b/packages/app/src/subagents/parent/index.ts
@@ -0,0 +1,3 @@
+export { ParentAgentCallout } from "./callout";
+export { ParentAgentPill } from "./pill";
+export { useParentAgentId } from "./use-parent-agent";
diff --git a/packages/app/src/subagents/parent/pill.tsx b/packages/app/src/subagents/parent/pill.tsx
new file mode 100644
index 0000000000..63f9407f8b
--- /dev/null
+++ b/packages/app/src/subagents/parent/pill.tsx
@@ -0,0 +1,79 @@
+import { useCallback, useMemo, useState, type ReactElement } from "react";
+import { Text } from "react-native";
+import { useTranslation } from "react-i18next";
+import { ChevronLeft } from "lucide-react-native";
+import { StyleSheet, withUnistyles } from "react-native-unistyles";
+import { PressHighlight } from "@/components/ui/press-highlight";
+import { composerPillStyles } from "@/composer/pill-styles";
+import { ComposerTrackMark } from "@/composer/tracks";
+import type { Theme } from "@/styles/theme";
+import { useParentAgent } from "./use-parent-agent";
+
+const ThemedChevronLeft = withUnistyles(ChevronLeft);
+const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
+const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
+
+/**
+ * The way back from a subagent to the agent that started it. It leads the pill rail and names
+ * the parent rather than saying "parent": in a stack of subagents the title is what tells you
+ * where you will land.
+ */
+export function ParentAgentPill({
+ serverId,
+ parentAgentId,
+}: {
+ serverId: string;
+ parentAgentId: string;
+}): ReactElement {
+ const { t } = useTranslation();
+ const parent = useParentAgent(serverId, parentAgentId);
+ const [isHovered, setIsHovered] = useState(false);
+ const handleHoverIn = useCallback(() => setIsHovered(true), []);
+ const handleHoverOut = useCallback(() => setIsHovered(false), []);
+ const bodyStyle = useMemo(
+ () => [composerPillStyles.body, styles.body, isHovered && composerPillStyles.bodyActive],
+ [isHovered],
+ );
+ const labelStyle = useMemo(
+ () => [composerPillStyles.label, styles.label, isHovered && composerPillStyles.labelActive],
+ [isHovered],
+ );
+ const label = parent.label ?? t("subagents.parent.backToParent");
+
+ return (
+
+
+
+
+ {label}
+
+
+ );
+}
+
+const styles = StyleSheet.create((theme) => ({
+ // The chevron's box is wider than its stroke, so the pill pulls it toward the edge to keep the
+ // visible glyph on the same inset as other pills' leading marks.
+ body: {
+ flexShrink: 1,
+ minWidth: 0,
+ gap: theme.spacing[1.5],
+ paddingLeft: theme.spacing[2],
+ },
+ label: {
+ flexShrink: 1,
+ maxWidth: { xs: 140, md: 240 },
+ },
+}));
diff --git a/packages/app/src/subagents/parent/use-parent-agent.ts b/packages/app/src/subagents/parent/use-parent-agent.ts
new file mode 100644
index 0000000000..e3916e6bbe
--- /dev/null
+++ b/packages/app/src/subagents/parent/use-parent-agent.ts
@@ -0,0 +1,38 @@
+import { useCallback } from "react";
+import { useAgentDescriptor } from "@/agent-presentation";
+import type { PanelDescriptor } from "@/panels/panel-registry";
+import { useSessionStore } from "@/stores/session-store";
+import type { SidebarStateBucket } from "@/utils/sidebar-agent-state";
+import { navigateToAgent } from "@/utils/navigate-to-agent";
+
+export interface ParentAgent {
+ /** The parent's title, or null while it has none — callers fall back to a generic name. */
+ label: string | null;
+ icon: PanelDescriptor["icon"];
+ statusBucket: SidebarStateBucket | null;
+ open: () => void;
+}
+
+/** The parent of a managed agent, or null for a root or detached agent. */
+export function useParentAgentId(serverId: string, agentId: string): string | null {
+ return useSessionStore((state) => {
+ const session = state.sessions[serverId];
+ const agent = session?.agents.get(agentId) ?? session?.agentDetails.get(agentId);
+ return agent?.parentAgentId ?? null;
+ });
+}
+
+/** The parent as its own tab presents it, and the way back to it. */
+export function useParentAgent(serverId: string, parentAgentId: string): ParentAgent {
+ const descriptor = useAgentDescriptor({ kind: "agent", agentId: parentAgentId }, { serverId });
+ // The parent may live in another workspace; navigateToAgent opens it wherever it is.
+ const open = useCallback(() => {
+ navigateToAgent({ serverId, agentId: parentAgentId });
+ }, [parentAgentId, serverId]);
+ return {
+ label: descriptor.label || null,
+ icon: descriptor.icon,
+ statusBucket: descriptor.statusBucket,
+ open,
+ };
+}