diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index 531a13724b..784d4d7fbc 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -84,6 +84,7 @@ export const ChannelPane = React.memo(function ChannelPane({ header, idleAuxiliaryPanel = null, idleAuxiliaryHeaderActions, + idleAuxiliaryOverridesThread = false, idleAuxiliaryTitle = "", hasOlderMessages, historyExhausted, @@ -487,11 +488,15 @@ export const ChannelPane = React.memo(function ChannelPane({ hasThreadSurface: Boolean(threadHeadMessage) || shouldShowThreadSkeleton, useSplitAuxiliaryPane, }); + const priorityIdleAuxiliary = + idleAuxiliaryOverridesThread && useFocusIdleDrawer; const { channelIsCovered, markExitComplete } = useFocusDrawerPresence( useFocusThreadDrawer || useFocusIdleDrawer, - useFocusThreadDrawer - ? onCloseThread - : (onCloseIdleAuxiliaryPanel ?? onCloseThread), + priorityIdleAuxiliary + ? (onCloseIdleAuxiliaryPanel ?? onCloseThread) + : useFocusThreadDrawer + ? onCloseThread + : (onCloseIdleAuxiliaryPanel ?? onCloseThread), ); const { changeThreadViewMode, layoutScrollTargetId, resolveScrollTarget } = useThreadViewModeSwitch({ @@ -551,6 +556,25 @@ export const ChannelPane = React.memo(function ChannelPane({ ) : ( wrapAux(panel, "idle-auxiliary-panel") ); + const idleAuxiliarySurface = + idleAuxiliaryPanel && onCloseIdleAuxiliaryPanel + ? wrapIdlePanel( + + {idleAuxiliaryPanel} + , + ) + : null; const threadHeaderLeading = useSplitAuxiliaryPane ? ( ) : undefined; @@ -799,14 +823,7 @@ export const ChannelPane = React.memo(function ChannelPane({ ) : null} - {/* - * `AnimatePresence` keeps the focus thread drawer mounted through its exit - * animation — without it the drawer's own existence condition - * (`useFocusThreadDrawer`, which is derived from `threadHeadMessage`) goes - * false on the same frame as the close, and there is nothing left to - * animate. It can hold the real thread through the exit rather than a - * frozen snapshot because the panel is fully prop-driven. - */} + {/* Keep focus drawers mounted until their exit animation finishes. */} {channelManagementOpen && activeChannel ? ( + ) : priorityIdleAuxiliary && idleAuxiliarySurface ? ( + idleAuxiliarySurface ) : threadHeadMessage ? ( (() => { const panel = ( @@ -975,24 +994,9 @@ export const ChannelPane = React.memo(function ChannelPane({ ); return wrapAux(panel, "user-profile-panel"); })() - ) : idleAuxiliaryPanel && onCloseIdleAuxiliaryPanel ? ( - wrapIdlePanel( - - {idleAuxiliaryPanel} - , - ) - ) : null} + ) : ( + idleAuxiliarySurface + )} ); diff --git a/desktop/src/features/channels/ui/ChannelPane.types.ts b/desktop/src/features/channels/ui/ChannelPane.types.ts index 3295113306..83a9794e5a 100644 --- a/desktop/src/features/channels/ui/ChannelPane.types.ts +++ b/desktop/src/features/channels/ui/ChannelPane.types.ts @@ -49,11 +49,13 @@ export type ChannelPaneProps = { header?: React.ReactNode; /** * Idle-state body for the right auxiliary pane (project extras, etc.). - * Shown only when no thread, profile, agent session, or channel-management - * panel is open — the same slot as those panels. + * Uses the same slot as thread, profile, agent-session, and management panels. + * By default it yields to those surfaces; callers may opt into thread override. */ idleAuxiliaryPanel?: React.ReactNode; idleAuxiliaryHeaderActions?: IdleAuxiliaryHeaderControls; + /** Show the idle auxiliary surface ahead of an already-open thread. */ + idleAuxiliaryOverridesThread?: boolean; idleAuxiliaryTitle?: string; hasOlderMessages?: boolean; /** True when the loaded window provably starts at the channel's beginning. */ diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index 795647ec2f..1a92135e9c 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -91,6 +91,7 @@ export function ChannelScreen({ headerEndActions, idleAuxiliaryPanel, idleAuxiliaryHeaderActions, + idleAuxiliaryOverridesThread, idleAuxiliaryTitle, onAddFiles, onCloseIdleAuxiliaryPanel, @@ -855,6 +856,7 @@ export function ChannelScreen({ header={channelHeader} idleAuxiliaryPanel={idleAuxiliaryPanel} idleAuxiliaryHeaderActions={idleAuxiliaryHeaderActions} + idleAuxiliaryOverridesThread={idleAuxiliaryOverridesThread} idleAuxiliaryTitle={idleAuxiliaryTitle} hasOlderMessages={hasOlderMessages} historyExhausted={historyExhausted} diff --git a/desktop/src/features/channels/ui/ChannelScreen.types.ts b/desktop/src/features/channels/ui/ChannelScreen.types.ts index 33ac2375ce..e555066704 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.types.ts +++ b/desktop/src/features/channels/ui/ChannelScreen.types.ts @@ -21,6 +21,7 @@ export type ChannelScreenProps = { currentProfile?: Profile; idleAuxiliaryPanel?: ReactNode; idleAuxiliaryHeaderActions?: IdleAuxiliaryHeaderControls; + idleAuxiliaryOverridesThread?: boolean; idleAuxiliaryTitle?: string; headerEndActions?: ReactNode; onAddFiles?: () => void; diff --git a/desktop/src/features/projects/ui/ProjectChannelHome.tsx b/desktop/src/features/projects/ui/ProjectChannelHome.tsx index e259ca2487..7d4619096e 100644 --- a/desktop/src/features/projects/ui/ProjectChannelHome.tsx +++ b/desktop/src/features/projects/ui/ProjectChannelHome.tsx @@ -352,6 +352,7 @@ export function ProjectChannelHome({ backLabel: workspaceDetail?.backLabel, onBack: workspaceDetail?.onBack, }} + idleAuxiliaryOverridesThread={workspaceSheetOpen} idleAuxiliaryTitle={ workspaceSheetTab ? projectHomeWorkspaceSheetTitle(workspaceSheetTab) diff --git a/desktop/tests/e2e/project-commit-detail.spec.ts b/desktop/tests/e2e/project-commit-detail.spec.ts index 5064527d4a..dd5501dbf2 100644 --- a/desktop/tests/e2e/project-commit-detail.spec.ts +++ b/desktop/tests/e2e/project-commit-detail.spec.ts @@ -791,6 +791,14 @@ test("commit detail opens from the commits feed with a diff", async ({ .first(); await expect(projectEntry).toBeVisible({ timeout: 10_000 }); await projectEntry.click(); + const rootMessage = page + .getByTestId("message-timeline") + .getByTestId("message-row") + .first(); + await expect(rootMessage).toBeVisible(); + await rootMessage.hover(); + await rootMessage.getByRole("button", { name: "Reply" }).click(); + await expect(page.getByTestId("focus-thread-drawer")).toBeVisible(); await page.getByTestId("project-home-context-tasks").click(); await expect(page.getByTestId("project-home-workspace-sheet")).toBeVisible(); const workspaceSheet = page.getByTestId("project-home-workspace-sheet");