From fc12843cef20c1b414ebd62ffa686a4b6bfddba1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 18 Oct 2026 14:16:28 +0700 Subject: [PATCH] feat(app): show each subagent's model and effort under its title (#7290) Paseo subagent rows in the subagents list had an empty info strip: the row only rendered the daemon-provided subtitle, which only provider subagents carry. The row now uses the same facts line an agent's sheet shows when it opens without its composer, so both surfaces resolve model, thinking, mode, and context the way the composer's controls do. The facts sit under the title instead of beside it, so a long task name keeps the row's width instead of fighting the strip for it. --- .../browser/compact-workspace-sheet.spec.ts | 8 +++ packages/app/src/panels/agent-facts-line.ts | 29 ++++++++++ packages/app/src/panels/agent-panel.tsx | 26 +-------- packages/app/src/subagents/track.tsx | 54 +++++++++++++++---- 4 files changed, 83 insertions(+), 34 deletions(-) diff --git a/packages/app/e2e/browser/compact-workspace-sheet.spec.ts b/packages/app/e2e/browser/compact-workspace-sheet.spec.ts index 77a2a0882b..990d490d9b 100644 --- a/packages/app/e2e/browser/compact-workspace-sheet.spec.ts +++ b/packages/app/e2e/browser/compact-workspace-sheet.spec.ts @@ -71,6 +71,14 @@ test("compact opens files and subagents in a sheet, leaving the tabs for agents" await closeSheet(page, "linked.ts"); }); + await test.step("a subagent's row shows the facts its sheet shows", async () => { + await page.getByTestId("subagents-track-header").click(); + await expect(page.getByTestId(`subagents-track-row-facts-${child.id}`)).toHaveText( + "Ten second stream · Low · Load test", + ); + await tapSheetBackdrop(page); + }); + await test.step("a subagent opens in a sheet over the subagents list", async () => { await openSubagentFromCompactTrack(page, child.id); diff --git a/packages/app/src/panels/agent-facts-line.ts b/packages/app/src/panels/agent-facts-line.ts index 7d3cd80332..7c01e5ce42 100644 --- a/packages/app/src/panels/agent-facts-line.ts +++ b/packages/app/src/panels/agent-facts-line.ts @@ -1,7 +1,10 @@ import type { AgentMode, AgentModelDefinition } from "@getpaseo/protocol/agent-types"; +import { useShallow } from "zustand/shallow"; import { formatAgentModeLabel } from "@/agent-controls/labels"; import { formatTokenCount } from "@/components/context-window-meter.utils"; import { resolveAgentModelSelection } from "@/composer/agent-controls/utils"; +import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; +import { useSessionStore } from "@/stores/session-store"; export interface AgentFactsInput { models: AgentModelDefinition[] | null; @@ -33,6 +36,32 @@ export function buildAgentFactsLine(input: AgentFactsInput): string | null { return parts.join(" · "); } +/** The facts line for a Paseo agent in the session store, kept current as the agent reports. */ +export function useAgentFactsLine(serverId: string, agentId: string): string | null { + const agent = useSessionStore( + useShallow((state) => { + const current = state.sessions[serverId]?.agents.get(agentId); + return current + ? { + provider: current.provider, + cwd: current.cwd, + runtimeModelId: current.runtimeInfo?.model ?? null, + configuredModelId: current.model ?? null, + runtimeThinkingOptionId: current.runtimeInfo?.thinkingOptionId ?? null, + explicitThinkingOptionId: current.thinkingOptionId ?? null, + availableModes: current.availableModes, + currentModeId: current.currentModeId, + contextWindowUsedTokens: current.lastUsage?.contextWindowUsedTokens ?? null, + contextWindowMaxTokens: current.lastUsage?.contextWindowMaxTokens ?? null, + } + : null; + }), + ); + const { entries } = useProvidersSnapshot(serverId, { cwd: agent?.cwd }); + const models = entries?.find((entry) => entry.provider === agent?.provider)?.models ?? null; + return agent ? buildAgentFactsLine({ ...agent, models }) : null; +} + /** The mode control shows the current mode, or the provider's first one until the agent reports. */ function resolveModeLabel(modes: AgentMode[], currentModeId: string | null | undefined) { const mode = modes.find((candidate) => candidate.id === currentModeId) ?? modes[0]; diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx index a9e11df570..7ae7d0c470 100644 --- a/packages/app/src/panels/agent-panel.tsx +++ b/packages/app/src/panels/agent-panel.tsx @@ -60,9 +60,8 @@ import { useContainerWidthBelow } from "@/hooks/use-container-width"; import { reconcileMissingAgentStateWithPresentAgent } from "@/panels/agent-panel-load-state"; import { TimelineSyncStatus } from "@/timeline/sync-status"; import { usePaneContext, usePaneFocus } from "@/panels/pane-context"; -import { buildAgentFactsLine } from "@/panels/agent-facts-line"; +import { useAgentFactsLine } from "@/panels/agent-facts-line"; import { PaneSubtitle } from "@/panels/pane-subtitle"; -import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; import { definePanel, type PanelDescriptor } from "@/panels/panel-registry"; import { RenderProfile } from "@/utils/render-profiler"; import { useHasPluginComposerPills } from "@/plugins"; @@ -1384,28 +1383,7 @@ function TimelineSyncErrorCallout({ * without its composer. */ function SheetAgentFacts({ serverId, agentId }: { serverId: string; agentId: string }) { - const agent = useSessionStore( - useShallow((state) => { - const current = state.sessions[serverId]?.agents.get(agentId); - return current - ? { - provider: current.provider, - cwd: current.cwd, - runtimeModelId: current.runtimeInfo?.model ?? null, - configuredModelId: current.model ?? null, - runtimeThinkingOptionId: current.runtimeInfo?.thinkingOptionId ?? null, - explicitThinkingOptionId: current.thinkingOptionId ?? null, - availableModes: current.availableModes, - currentModeId: current.currentModeId, - contextWindowUsedTokens: current.lastUsage?.contextWindowUsedTokens ?? null, - contextWindowMaxTokens: current.lastUsage?.contextWindowMaxTokens ?? null, - } - : null; - }), - ); - const { entries } = useProvidersSnapshot(serverId, { cwd: agent?.cwd }); - const models = entries?.find((entry) => entry.provider === agent?.provider)?.models ?? null; - const text = agent ? buildAgentFactsLine({ ...agent, models }) : null; + const text = useAgentFactsLine(serverId, agentId); return text ? : null; } diff --git a/packages/app/src/subagents/track.tsx b/packages/app/src/subagents/track.tsx index c1834587d2..1914993700 100644 --- a/packages/app/src/subagents/track.tsx +++ b/packages/app/src/subagents/track.tsx @@ -13,6 +13,7 @@ import { type WorkspaceTabPresentation, } from "@/screens/workspace/workspace-tab-presentation"; import type { Theme } from "@/styles/theme"; +import { useAgentFactsLine } from "@/panels/agent-facts-line"; import { getPanelManifest } from "@/panels/panel-manifest"; import type { SubagentRow } from "./select"; import type { ArchiveFinishedStatus } from "./use-archive-finished"; @@ -219,14 +220,16 @@ function SubagentsTrackRow({ ({ active }: { active: boolean }) => ( <> - - {displayLabel} - - {presentation.subtitle ? ( - - {presentation.subtitle} + + + {displayLabel} - ) : null} + {row.kind === "paseo" ? ( + + ) : ( + + )} + {row.kind === "paseo" ? ( ; +} + +function SubagentFacts({ rowId, text }: { rowId: string; text: string | null }) { + if (!text) return null; + return ( + + {text} + + ); +} + function SubagentRowActions({ rowId, displayLabel, @@ -360,9 +379,24 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.base, color: theme.colors.foreground, }, - // Trailing metadata — provider context on a subagent row, progress on the archive row. No width - // cap: the panel's own ceiling bounds it. It shrinks twice as fast as the label, so a wordy - // provider subtitle gives way first instead of squeezing the thing that names the row. + // Title over facts. The facts sit under the title rather than beside it, so a long task name + // keeps the row's full width and neither line truncates the other. + rowText: { + flexGrow: 1, + flexShrink: 1, + flexBasis: "auto", + minWidth: 0, + }, + rowTitle: { + fontSize: theme.fontSize.base, + color: theme.colors.foreground, + }, + rowFacts: { + fontSize: theme.fontSize.sm, + color: theme.colors.foregroundMuted, + }, + // Progress on the archive row. It shrinks twice as fast as the label, so a long retry count + // gives way first instead of squeezing the thing that names the row. rowTrailing: { flexShrink: 2, minWidth: 0,