Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions desktop/src/features/channels/ui/ChannelPane.helpers.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ test("focus idle drawers yield to every higher-priority auxiliary surface", () =
}
});

test("an explicit thread override keeps the idle panel in its own focus drawer", () => {
assert.equal(
shouldUseFocusIdleDrawer({
channelManagementOpen: false,
hasAgentSession: false,
hasIdleAuxiliaryPanel: true,
hasIdlePanelCloseHandler: true,
hasProfilePanel: false,
hasThreadSurface: true,
overrideThread: true,
useSplitAuxiliaryPane: false,
}),
true,
);
});

test("getChannelIntroKind names project homes ahead of regular streams", () => {
assert.equal(getChannelIntroKind(channel(), true), "project channel");
assert.equal(getChannelIntroKind(channel(), false), "regular channel");
Expand Down
6 changes: 4 additions & 2 deletions desktop/src/features/channels/ui/ChannelPane.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export function shouldUseFocusIdleDrawer({
hasIdlePanelCloseHandler,
hasProfilePanel,
hasThreadSurface,
overrideThread = false,
useSplitAuxiliaryPane,
}: {
channelManagementOpen: boolean;
Expand All @@ -18,14 +19,15 @@ export function shouldUseFocusIdleDrawer({
hasIdlePanelCloseHandler: boolean;
hasProfilePanel: boolean;
hasThreadSurface: boolean;
overrideThread?: boolean;
useSplitAuxiliaryPane: boolean;
}): boolean {
return (
useSplitAuxiliaryPane &&
(useSplitAuxiliaryPane || overrideThread) &&
!channelManagementOpen &&
!hasAgentSession &&
!hasProfilePanel &&
!hasThreadSurface &&
(!hasThreadSurface || overrideThread) &&
hasIdleAuxiliaryPanel &&
hasIdlePanelCloseHandler
);
Expand Down
29 changes: 20 additions & 9 deletions desktop/src/features/channels/ui/ChannelPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -419,10 +419,10 @@ export const ChannelPane = React.memo(function ChannelPane({
const isOverlay = useIsThreadPanelOverlay();
const useSplitAuxiliaryPane = !isSinglePanelView && !isOverlay;
const threadViewMode = useThreadViewMode();
const hasThreadSurface =
Boolean(threadHeadMessage) || shouldShowThreadSkeleton;
const useFocusThreadDrawer =
threadViewMode === "focus" &&
useSplitAuxiliaryPane &&
(Boolean(threadHeadMessage) || shouldShowThreadSkeleton);
threadViewMode === "focus" && useSplitAuxiliaryPane && hasThreadSurface;
const selectedAgent = React.useMemo(
() =>
agentSessionSelection.resolveSelectedAgentSession({
Expand All @@ -435,19 +435,26 @@ export const ChannelPane = React.memo(function ChannelPane({
);
const hasIdleAuxiliary =
Boolean(idleAuxiliaryPanel) && Boolean(onCloseIdleAuxiliaryPanel);
const priorityIdleAuxiliary = shouldPrioritizeIdleAuxiliary(
idleAuxiliaryOverridesThread,
hasIdleAuxiliary,
);
const overlayIdleAuxiliaryOverThread =
priorityIdleAuxiliary && hasThreadSurface && !isOverlay;
const replaceThreadWithIdleAuxiliary =
priorityIdleAuxiliary && hasThreadSurface && isOverlay;
const useFocusIdleDrawer = shouldUseFocusIdleDrawer({
channelManagementOpen,
hasAgentSession: Boolean(activeChannel && selectedAgent),
hasIdleAuxiliaryPanel: Boolean(idleAuxiliaryPanel),
hasIdlePanelCloseHandler: Boolean(onCloseIdleAuxiliaryPanel),
hasProfilePanel: Boolean(profilePanelPubkey),
hasThreadSurface: Boolean(threadHeadMessage) || shouldShowThreadSkeleton,
hasThreadSurface,
overrideThread: overlayIdleAuxiliaryOverThread,
useSplitAuxiliaryPane,
});
const priorityIdleAuxiliary = shouldPrioritizeIdleAuxiliary(
idleAuxiliaryOverridesThread,
hasIdleAuxiliary,
);
const showIdleAuxiliaryOverThread =
overlayIdleAuxiliaryOverThread && useFocusIdleDrawer;
const { channelIsCovered, markExitComplete } = useFocusDrawerPresence(
useFocusThreadDrawer || useFocusIdleDrawer,
priorityIdleAuxiliary
Expand Down Expand Up @@ -510,6 +517,7 @@ export const ChannelPane = React.memo(function ChannelPane({
useFocusThreadDrawer ? (
<FocusThreadDrawer
channelName={activeChannel?.name ?? "channel"}
escapeEnabled={!showIdleAuxiliaryOverThread}
hasActiveEdit={threadEditTarget !== null}
key={THREAD_SURFACE_KEY}
onClose={onCloseThread}
Expand Down Expand Up @@ -821,7 +829,7 @@ export const ChannelPane = React.memo(function ChannelPane({
useSplitAuxiliaryPane={useSplitAuxiliaryPane}
transparentChrome={hasSplitAuxiliaryPane}
/>
) : priorityIdleAuxiliary && idleAuxiliarySurface ? (
) : replaceThreadWithIdleAuxiliary && idleAuxiliarySurface ? (
idleAuxiliarySurface
) : threadHeadMessage ? (
(() => {
Expand Down Expand Up @@ -977,6 +985,9 @@ export const ChannelPane = React.memo(function ChannelPane({
idleAuxiliarySurface
)}
</AnimatePresence>
<AnimatePresence onExitComplete={markExitComplete}>
{showIdleAuxiliaryOverThread ? idleAuxiliarySurface : null}
</AnimatePresence>
</div>
);
});
7 changes: 6 additions & 1 deletion desktop/src/features/channels/ui/FocusThreadDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { cn } from "@/shared/lib/cn";
type FocusThreadDrawerProps = {
channelName: string;
children: React.ReactNode;
/** Prevent a covered drawer from handling Escape before its overlay. */
escapeEnabled?: boolean;
/** Accessible name for the drawer. Channel threads leave the default. */
label?: string;
hasActiveEdit?: boolean;
Expand Down Expand Up @@ -142,6 +144,7 @@ const REDUCED_MOTION_TRANSITION = { duration: 0.12, ease: "linear" } as const;
export function FocusThreadDrawer({
channelName,
children,
escapeEnabled = true,
label = "Thread",
hasActiveEdit = false,
onClose,
Expand All @@ -152,6 +155,8 @@ export function FocusThreadDrawer({
const previousFocusRef = React.useRef<HTMLElement | null>(null);

React.useEffect(() => {
if (!escapeEnabled) return;

function handleEscape(event: KeyboardEvent) {
if (event.key !== "Escape") return;
const target = event.target;
Expand All @@ -171,7 +176,7 @@ export function FocusThreadDrawer({
return () => {
window.removeEventListener("keydown", handleEscape, { capture: true });
};
}, [hasActiveEdit, onClose]);
}, [escapeEnabled, hasActiveEdit, onClose]);

React.useLayoutEffect(() => {
previousFocusRef.current =
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/channels/ui/RightAuxiliaryPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function RightAuxiliaryPane({
return (
<aside
className={cn(
"group/right-pane relative flex h-full shrink-0 flex-col overflow-hidden bg-background",
"group/right-pane relative isolate flex h-full shrink-0 flex-col overflow-hidden bg-background",
detached
? "bg-transparent"
: "before:pointer-events-none before:absolute before:bottom-0 before:left-0 before:top-0 before:z-50 before:w-px before:bg-border/80 before:content-['']",
Expand Down
72 changes: 71 additions & 1 deletion desktop/tests/e2e/project-commit-detail.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,7 @@ test("latest files commit opens its detail without a divider", async ({
await expect(page.getByTestId("project-commit-detail")).toBeVisible();
});

test("project workspace sheet enters at its settled width", async ({
test("project workspace sheet stays independent from an open thread", async ({
page,
}) => {
await enableProjectsFeature(page);
Expand All @@ -792,6 +792,7 @@ test("project workspace sheet enters at its settled width", async ({
await page.getByTestId("create-project-name").fill("sheet-motion-demo");
await page.getByTestId("create-project-submit").click();
await expect(page.getByTestId("project-channel-home")).toBeVisible();
await page.setViewportSize({ height: 720, width: 820 });

const summaryColumn = page.getByTestId("project-home-summary-column");
const resizeHandle = summaryColumn.getByTestId(
Expand Down Expand Up @@ -822,6 +823,75 @@ test("project workspace sheet enters at its settled width", async ({
expect(
Math.abs(settledDrawerWidth - enteringDrawerWidth),
).toBeLessThanOrEqual(1);

await focusDrawer.getByTestId("auxiliary-panel-close").click();
await expect(page.getByTestId("project-home-workspace-sheet")).toHaveCount(0);
await expect(page.getByTestId("project-home-summary-column")).toBeVisible();
await waitForMockLiveSubscription(page, "sheet-motion-demo");
const threadRootContent = "Workspace drawer thread root";
await page.evaluate((content) => {
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "sheet-motion-demo",
content,
});
}, threadRootContent);
const threadRoot = page
.getByTestId("message-timeline")
.getByTestId("message-row")
.filter({ hasText: threadRootContent });
await expect(threadRoot).toBeVisible();
await threadRoot.hover();
await threadRoot.getByRole("button", { name: "Reply" }).click();
await expect(page.getByTestId("message-thread-panel")).toBeVisible();

await page.getByTestId("project-home-context-tasks").click();
await expect(page.getByTestId("project-home-workspace-sheet")).toBeVisible();
const workspaceDrawer = page.getByTestId("focus-thread-drawer");
await expect(workspaceDrawer).toHaveCount(1);
await expect(
workspaceDrawer.getByTestId("project-home-workspace-sheet"),
).toBeVisible();
await expect(page.getByTestId("message-thread-panel")).toHaveCount(1);
await expect(workspaceDrawer.getByTestId("message-thread-panel")).toHaveCount(
0,
);

await workspaceDrawer.getByTestId("auxiliary-panel-close").click();
await expect(page.getByTestId("project-home-workspace-sheet")).toHaveCount(0);
await expect(page.getByTestId("message-thread-panel")).toBeVisible();

await page.setViewportSize({ height: 1080, width: 1920 });
const splitThreadPane = page
.locator(
'[data-testid="message-thread-panel"]:has([data-testid="right-auxiliary-pane-resize-handle"])',
)
.first();
await expect(splitThreadPane).toBeVisible();
const threadResizeHandle = splitThreadPane.getByTestId(
"right-auxiliary-pane-resize-handle",
);
const threadResizeHandleBox = await threadResizeHandle.boundingBox();
expect(threadResizeHandleBox).not.toBeNull();

await page.getByTestId("project-home-context-tasks").click();
await expect(page.getByTestId("project-home-workspace-sheet")).toBeVisible();
await waitForAnimations(page);
const workspaceCoversThreadDivider = await page.evaluate(
({ x, y }) => {
return Boolean(
document
.elementFromPoint(x, y)
?.closest('[data-testid="focus-thread-drawer"]'),
);
},
{
x:
(threadResizeHandleBox?.x ?? 0) +
(threadResizeHandleBox?.width ?? 0) / 2,
y: (threadResizeHandleBox?.y ?? 0) + 40,
},
);
expect(workspaceCoversThreadDivider).toBe(true);
});

test("commit detail opens from the commits feed with a diff", async ({
Expand Down
Loading