diff --git a/desktop/src/features/channels/ui/ChannelAgentSessionSurface.tsx b/desktop/src/features/channels/ui/ChannelAgentSessionSurface.tsx new file mode 100644 index 0000000000..64589bc316 --- /dev/null +++ b/desktop/src/features/channels/ui/ChannelAgentSessionSurface.tsx @@ -0,0 +1,105 @@ +import type * as React from "react"; + +import * as agentSessionSelection from "@/features/channels/ui/agentSessionSelection"; +import type { ChannelAgentSessionAgent } from "@/features/channels/ui/useChannelAgentSessions"; +import { getAgentSessionPanelPresentation } from "@/features/channels/lib/agentSessionPanelPresentation"; +import { AgentActivityDrawer } from "@/features/channels/ui/AgentActivityDrawer"; +import { AgentSessionThreadPanel } from "@/features/channels/ui/AgentSessionThreadPanel"; +import type { BotActivityAgent } from "@/features/channels/ui/BotActivityBar"; +import type { UserProfileLookup } from "@/features/profile/lib/identity"; +import type { Channel } from "@/shared/api/types"; + +type ChannelAgentSessionSurfaceProps = { + activeChannel: Channel; + activeChannelId: string | null; + activityAgents: BotActivityAgent[]; + agent: ChannelAgentSessionAgent; + /** Render the panel inside the cover drawer instead of the split pane. */ + isCoverDrawer: boolean; + isSinglePanelView: boolean; + onBack?: () => void; + onClose: () => void; + openAgentSessionChannelId: string | null; + profiles?: UserProfileLookup; + useSplitAuxiliaryPane: boolean; + widthPx: number; + /** + * Applies the split-pane presentation, including its resize affordances. + * Supplied by `ChannelPane` because that pane owns the resize state; the + * cover-drawer presentation is applied here. + */ + wrapSplitPane: (panel: React.ReactNode) => React.ReactNode; +}; + +/** + * The channel's agent activity surface: the session panel plus the channel + * re-scoping its content and actions depend on. + * + * Split out of `ChannelPane` so the re-scoping rule below has one home and is + * not another branch inside that component's auxiliary-surface chain. Which + * presentation this lands in is decided upstream and applied through `wrap`. + */ +export function ChannelAgentSessionSurface({ + activeChannel, + activeChannelId, + activityAgents, + agent, + isCoverDrawer, + isSinglePanelView, + onBack, + onClose, + openAgentSessionChannelId, + profiles, + useSplitAuxiliaryPane, + widthPx, + wrapSplitPane, +}: ChannelAgentSessionSurfaceProps) { + // When the panel was opened from a different channel than the currently + // active one, re-scope it to the active channel so that both the + // content/header AND channel-backed actions (e.g. Stop current turn) operate + // on the same channel object. + const effectiveAgentSessionChannelId = + openAgentSessionChannelId && activeChannel.id !== openAgentSessionChannelId + ? activeChannelId + : openAgentSessionChannelId; + const channel = effectiveAgentSessionChannelId + ? effectiveAgentSessionChannelId === activeChannel.id + ? activeChannel + : null + : agentSessionSelection.isAgentInActivityList({ + activityAgents, + selectedAgent: agent, + }) + ? activeChannel + : null; + + const layoutProps = getAgentSessionPanelPresentation({ + isCoverDrawer, + isSinglePanelView, + useSplitAuxiliaryPane, + }); + const panel = ( + + ); + + return isCoverDrawer ? ( + + {panel} + + ) : ( + wrapSplitPane(panel) + ); +} diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index 3312e37843..653af8bf8a 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -25,16 +25,12 @@ import { import { buildVideoReviewPresentationByMessageId } from "@/features/messages/lib/videoReviewContext"; import { useComposerHeightPadding } from "@/features/messages/ui/useComposerHeightPadding"; import { UserProfilePanel } from "@/features/profile/ui/UserProfilePanel"; -import { AgentSessionThreadPanel } from "@/features/channels/ui/AgentSessionThreadPanel"; import { ChannelManagementAuxiliaryPanel } from "@/features/channels/ui/ChannelManagementAuxiliaryPanel"; import { RightAuxiliaryPane } from "@/features/channels/ui/RightAuxiliaryPane"; import { ThreadViewModeToggle } from "@/features/channels/ui/ThreadViewModeToggle"; -import { AgentActivityDrawer } from "@/features/channels/ui/AgentActivityDrawer"; +import { ChannelAgentSessionSurface } from "@/features/channels/ui/ChannelAgentSessionSurface"; import { FocusThreadDrawer } from "@/features/channels/ui/FocusThreadDrawer"; -import { - AGENT_SESSION_SURFACE_KEY, - getAgentSessionPanelPresentation, -} from "@/features/channels/lib/agentSessionPanelPresentation"; +import { AGENT_SESSION_SURFACE_KEY } from "@/features/channels/lib/agentSessionPanelPresentation"; import { resolveChannelAuxiliarySurface, resolveChannelCoverDrawer, @@ -534,20 +530,10 @@ export const ChannelPane = React.memo(function ChannelPane({ ) : ( wrapAux(panel, "message-thread-panel", { key: THREAD_SURFACE_KEY }) ); - const wrapAgentSessionPanel = (panel: React.ReactNode) => - useAgentActivityDrawer ? ( - - {panel} - - ) : ( - wrapAux(panel, "agent-session-thread-panel", { - key: AGENT_SESSION_SURFACE_KEY, - }) - ); + const wrapAgentSessionSplitPane = (panel: React.ReactNode) => + wrapAux(panel, "agent-session-thread-panel", { + key: AGENT_SESSION_SURFACE_KEY, + }); const threadHeaderLeading = useSplitAuxiliaryPane ? ( ) : undefined; @@ -557,11 +543,6 @@ export const ChannelPane = React.memo(function ChannelPane({ isSinglePanelView, useSplitAuxiliaryPane, }); - const agentSessionLayoutProps = getAgentSessionPanelPresentation({ - isCoverDrawer: useAgentActivityDrawer, - isSinglePanelView, - useSplitAuxiliaryPane, - }); const timelineReplyHandler = activeChannel?.archivedAt || isHuddleTranscript ? undefined : onOpenThread; return ( @@ -910,42 +891,22 @@ export const ChannelPane = React.memo(function ChannelPane({ ) : auxiliarySurface === "agent-session" && activeChannel && selectedAgent ? ( - (() => { - // When the panel was opened from a different channel than the - // currently active one, re-scope it to the active channel so - // that both the content/header AND channel-backed actions (e.g. - // Stop current turn) operate on the same channel object. - const effectiveAgentSessionChannelId = - openAgentSessionChannelId && - activeChannel.id !== openAgentSessionChannelId - ? activeChannelId - : openAgentSessionChannelId; - const panel = ( - - ); - return wrapAgentSessionPanel(panel); - })() + ) : auxiliarySurface === "profile" && profilePanelPubkey ? ( (() => { const panel = (