// The subagent-lineage surface: the shared pieces that render a session's // spawned workers. It is deliberately DECOUPLED from goals — a session's agent // tree is orthogonal to whether it carries a goal. One compact tree component // ({@link SubagentTree}) backs its single home: // - SessionChrome agents segment (production) — host passes SubagentTree as // `agentsPanel`; the dock chip opens that panel in-place. // SpawnedByBreadcrumb is the inverse link a child session shows back to the // manager that spawned it. // // Design language: one dense line per agent — a single status-tone dot + a // truncated title + a quiet relative-time hint — the whole row a hover-lit // deep-link. Grandchildren thread off a hairline rail (one level, expandable), // never boxes. Calm at rest; the chevron affordance lifts on hover. // // Copy doctrine: human language only. Internal status slugs (requires_action, // active, …) never leak into a rendered string. import { t } from "../../lib/console-text"; import { formatRelativeTime } from "../../../lib/format"; import { sessionDisplayStatus, type SessionDisplayStatus } from "../../../timeline-model"; import { sessionAgentAttention } from "../../../session-agents-model"; import type { LineageNode, SessionStatus, SessionSummary } from "@opengeni/sdk"; import { Link } from "../../lib/navigation"; import { BotIcon, ChevronRightIcon, EllipsisIcon } from "lucide-react"; import { useState, type ReactNode } from "react"; import { STATUS_META, StatusDot, type StatusTone } from "../../../ui/status-dot"; import { sessionDisplayTitle } from "../../lib/session-rename"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "../../../ui/dropdown-menu"; import { cn } from "../../lib/utils"; import { sessionControlPaused } from "../../lib/session-rail"; /** Children (depth 0) plus one level of grandchildren (depth 1) — the tree goes exactly one level deeper, so a depth-1 row never draws its own expander. */ const MAX_DEPTH = 1; /** * The loud trailing word for a row a manager must act on. A child waiting on a * human also says for HOW LONG ("Needs you · 10h", from the server's * `requiresActionSince`), so a parked child is visible at a glance and the row * links straight to where the human answers. Null for calm rows. */ export function subagentAttentionHint( session: Pick, paused: boolean, now: Date = new Date(), ): { word: string; waitingFor: string; title?: string | undefined } | null { const attention = sessionAgentAttention(t, session, paused, now); if (!attention) return null; const { tone: _tone, ...hint } = attention; return hint; } /** Map a session lifecycle status onto the six-tone status language. */ export function sessionStatusTone(status: SessionDisplayStatus): StatusTone { switch (status) { case "requires_action": return "waiting"; case "blocked": return "failed"; case "running": return "running"; case "queued": return "queued"; case "failed": return "failed"; case "cancelled": return "cancelled"; default: return "idle"; } } function isLiveStatus(status: SessionStatus): boolean { return status === "running"; } /* --- the shared compact tree ------------------------------------------------ */ /** The lineage tree, shared verbatim by the chip popover and the dock tab. */ export function SubagentTree({ workspaceId, nodes, onNavigate, }: { workspaceId: string; nodes: LineageNode[]; onNavigate?: (() => void) | undefined; }) { // Flat fleets (no grandchildren) skip the expander gutter so chrome stays dense. const showLead = nodes.some((node) => node.children.length > 0); return ( ); } function SubagentRow({ node, workspaceId, depth, showLead, onNavigate, }: { node: LineageNode; workspaceId: string; depth: number; showLead: boolean; onNavigate?: (() => void) | undefined; }) { const [open, setOpen] = useState(false); const title = sessionDisplayTitle(node.session); const paused = sessionControlPaused(node.session); const tone = paused ? "waiting" : sessionStatusTone(sessionDisplayStatus(node.session)); const live = !paused && isLiveStatus(node.session.status); const canExpand = depth < MAX_DEPTH && node.children.length > 0; // The trailing hint stays calm and compact for the common case (relative // time), and turns loud ONLY for the two rows a manager must act on: a failed // agent and one waiting on you spell the word out in their own status tone, so // they don't hide behind a color dot in a long list. const attention = subagentAttentionHint(node.session, paused); const attentionWord = attention?.word ?? null; const hint = attentionWord ?? formatRelativeTime(t, node.session.updatedAt); const hintClass = attentionWord ? cn(STATUS_META[tone].text, "font-medium") : "text-fg-subtle"; const hintTitle = attention?.title; const waitingFor = attention?.waitingFor ?? ""; return (
  • {/* The container owns the hover wash + focus ring so the WHOLE row lights as one target; the Link inside covers dot→title→hint (the nav hit area), the chevron toggles without navigating. */}
    {/* Lead cluster: expand chevron + child-count. Omitted entirely for flat fleets so the chrome agents panel stays dense. */} {showLead ? ( {canExpand ? ( <> {node.children.length} ) : null} ) : null} onNavigate?.()} title={title} className="flex h-full min-w-0 flex-1 items-center gap-2 text-xs text-fg-muted outline-hidden group-hover/row:text-fg" > {title} {hint ? ( {hint} ) : null}
    {canExpand && open ? ( // Grandchildren thread off a hairline rail aligned under the parent's // chevron column — a descending line, not a box. ) : null}
  • ); } /** The quiet section label both homes wear above the tree. */ export function SubagentsLabel({ count }: { count: number }) { return (
    Agents {count > 0 ? · {count} : null}
    ); } /* --- "spawned by" breadcrumb (child sessions link back to their parent) ----- */ export function SessionAncestryBreadcrumb({ workspaceId, parentSessionId, ancestors, loading, error, }: { workspaceId: string; /** Known from the current session even before the lineage request settles. */ parentSessionId: string | null; /** Root-to-direct-parent order. */ ancestors: SessionSummary[]; loading?: boolean | undefined; error?: Error | null | undefined; }): ReactNode { if (parentSessionId && (loading || error)) { const state = error ? "unavailable" : "loading"; return ( ); } if (error) { return Session ancestry unavailable; } if (ancestors.length === 0) { return null; } const parent = ancestors.at(-1)!; const parentLabel = lineageLabel(parent); const middle = ancestors.slice(1, -1); return ( ); } function BreadcrumbLink({ workspaceId, session, }: { workspaceId: string; session: SessionSummary; }) { const label = lineageLabel(session); return ( {label} ); } function lineageLabel(session: SessionSummary): string { return sessionDisplayTitle(session); }