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, + }; +}