Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import test from "node:test";

import {
getChannelIntroKind,
shouldPrioritizeIdleAuxiliary,
shouldUseFocusIdleDrawer,
} from "./ChannelPane.helpers.ts";

Expand Down Expand Up @@ -56,3 +57,9 @@ test("getChannelIntroKind keeps private and ephemeral labels for other streams",
"ephemeral channel",
);
});

test("idle auxiliary priority does not depend on thread layout mode", () => {
assert.equal(shouldPrioritizeIdleAuxiliary(true, true), true);
assert.equal(shouldPrioritizeIdleAuxiliary(true, false), false);
assert.equal(shouldPrioritizeIdleAuxiliary(false, true), false);
});
8 changes: 8 additions & 0 deletions desktop/src/features/channels/ui/ChannelPane.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ export function getChannelIntroDescription(channel: Channel): string | null {
);
}

/** Whether a caller-owned auxiliary sheet should render ahead of a thread. */
export function shouldPrioritizeIdleAuxiliary(
overrideThread: boolean,
hasIdleAuxiliary: boolean,
) {
return overrideThread && hasIdleAuxiliary;
}

export function isWelcomeSetupSystemMessage(message: TimelineMessage) {
if (message.kind !== KIND_SYSTEM_MESSAGE) {
return false;
Expand Down
81 changes: 39 additions & 42 deletions desktop/src/features/channels/ui/ChannelPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
import { useWelcomeComposerBanner } from "@/features/channels/ui/useWelcomeComposerBanner";
import {
mentionsKnownAgent,
shouldPrioritizeIdleAuxiliary,
shouldUseFocusIdleDrawer,
} from "@/features/channels/ui/ChannelPane.helpers";
import { HuddleStartingView, HuddleTranscriptIntro } from "@/features/huddle";
Expand Down Expand Up @@ -84,6 +85,7 @@ export const ChannelPane = React.memo(function ChannelPane({
header,
idleAuxiliaryPanel = null,
idleAuxiliaryHeaderActions,
idleAuxiliaryOverridesThread = false,
idleAuxiliaryTitle = "",
hasOlderMessages,
historyExhausted,
Expand Down Expand Up @@ -263,7 +265,6 @@ export const ChannelPane = React.memo(function ChannelPane({
onEdit(target);
return true;
}, [findLastOwnEditable, messages, onEdit]);

const handleEditLastOwnThreadMessage = React.useCallback((): boolean => {
if (!onEdit) return false;
const scope: TimelineMessage[] = [];
Expand All @@ -284,7 +285,6 @@ export const ChannelPane = React.memo(function ChannelPane({
currentPubkey,
relaySelfQuery.data,
);

const isComposerDisabled =
!activeChannel?.isMember ||
activeChannel.archivedAt !== null ||
Expand All @@ -294,7 +294,6 @@ export const ChannelPane = React.memo(function ChannelPane({
isSending;
const knownAgentPubkeys = React.useMemo(() => {
const pubkeys = new Set<string>();

for (const pubkey of agentPubkeys ?? []) {
pubkeys.add(pubkey.toLowerCase());
}
Expand All @@ -304,7 +303,6 @@ export const ChannelPane = React.memo(function ChannelPane({
for (const agent of activityAgents) {
pubkeys.add(agent.pubkey.toLowerCase());
}

return pubkeys;
}, [activityAgents, agentPubkeys, agentSessionAgents]);
const handleSendMessage = React.useCallback(
Expand All @@ -323,7 +321,6 @@ export const ChannelPane = React.memo(function ChannelPane({
isActiveWelcomeChannel &&
(containsWelcomePersonaMention(content) ||
mentionsKnownAgent(mentionPubkeys, knownAgentPubkeys));

messageTimelineRef.current?.scrollToBottomOnNextUpdate();
await onSendMessage(
content,
Expand All @@ -333,7 +330,6 @@ export const ChannelPane = React.memo(function ChannelPane({
threadContext,
forceRest,
);

if (
channelId &&
channelId !== activeChannelId &&
Expand All @@ -342,7 +338,6 @@ export const ChannelPane = React.memo(function ChannelPane({
) {
await goChannel(channelId, { replace: true });
}

if (shouldCompleteWelcomeBanner) {
completeWelcomeComposerBanner();
}
Expand Down Expand Up @@ -396,7 +391,6 @@ export const ChannelPane = React.memo(function ChannelPane({
}),
[activeChannel, currentPubkey, profiles],
);

const handleWelcomeAddAgent = React.useCallback(() => {
onAddAgent?.({
beforeSend: () =>
Expand Down Expand Up @@ -439,7 +433,6 @@ export const ChannelPane = React.memo(function ChannelPane({
for (const message of threadAllMessages) {
messagesById.set(message.id, message);
}

return buildVideoReviewPresentationByMessageId({
channelId: activeChannel?.id ?? null,
channelName: activeChannel?.name,
Expand All @@ -460,7 +453,6 @@ export const ChannelPane = React.memo(function ChannelPane({
threadAllMessages,
threadHeadMessage,
]);

const isOverlay = useIsThreadPanelOverlay();
const useSplitAuxiliaryPane = !isSinglePanelView && !isOverlay;
const threadViewMode = useThreadViewMode();
Expand All @@ -478,6 +470,8 @@ export const ChannelPane = React.memo(function ChannelPane({
}),
[agentSessionAgents, openAgentSessionPubkey, profilePanelPubkey, profiles],
);
const hasIdleAuxiliary =
Boolean(idleAuxiliaryPanel) && Boolean(onCloseIdleAuxiliaryPanel);
const useFocusIdleDrawer = shouldUseFocusIdleDrawer({
channelManagementOpen,
hasAgentSession: Boolean(activeChannel && selectedAgent),
Expand All @@ -487,11 +481,17 @@ export const ChannelPane = React.memo(function ChannelPane({
hasThreadSurface: Boolean(threadHeadMessage) || shouldShowThreadSkeleton,
useSplitAuxiliaryPane,
});
const priorityIdleAuxiliary = shouldPrioritizeIdleAuxiliary(
idleAuxiliaryOverridesThread,
hasIdleAuxiliary,
);
const { channelIsCovered, markExitComplete } = useFocusDrawerPresence(
useFocusThreadDrawer || useFocusIdleDrawer,
useFocusThreadDrawer
? onCloseThread
: (onCloseIdleAuxiliaryPanel ?? onCloseThread),
priorityIdleAuxiliary
? (onCloseIdleAuxiliaryPanel ?? onCloseThread)
: useFocusThreadDrawer
? onCloseThread
: (onCloseIdleAuxiliaryPanel ?? onCloseThread),
);
const { changeThreadViewMode, layoutScrollTargetId, resolveScrollTarget } =
useThreadViewModeSwitch({
Expand Down Expand Up @@ -551,6 +551,25 @@ export const ChannelPane = React.memo(function ChannelPane({
) : (
wrapAux(panel, "idle-auxiliary-panel")
);
const idleAuxiliarySurface =
idleAuxiliaryPanel && onCloseIdleAuxiliaryPanel
? wrapIdlePanel(
<IdleAuxiliaryPanel
canResetWidth={canResetThreadPanelWidth}
headerControls={idleAuxiliaryHeaderActions}
isFocusDrawer={useFocusIdleDrawer}
isSinglePanelView={isSinglePanelView}
onClose={onCloseIdleAuxiliaryPanel}
onResetWidth={onResetThreadPanelWidth}
onResizeStart={onThreadPanelResizeStart}
title={idleAuxiliaryTitle}
useSplitAuxiliaryPane={useSplitAuxiliaryPane}
widthPx={threadPanelWidthPx}
>
{idleAuxiliaryPanel}
</IdleAuxiliaryPanel>,
)
: null;
const threadHeaderLeading = useSplitAuxiliaryPane ? (
<ThreadViewModeToggle onChange={changeThreadViewMode} />
) : undefined;
Expand All @@ -577,7 +596,6 @@ export const ChannelPane = React.memo(function ChannelPane({
data-testid="channel-shared-header-backdrop"
/>
) : null}

{!isSinglePanelView ? (
<section
aria-label="Channel messages and composer"
Expand Down Expand Up @@ -798,16 +816,8 @@ export const ChannelPane = React.memo(function ChannelPane({
</div>
</section>
) : 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.
*/}
<AnimatePresence onExitComplete={markExitComplete}>
{/* Serialize replacements so focus drawers keep one travel direction. */}
<AnimatePresence mode="wait" onExitComplete={markExitComplete}>
{channelManagementOpen && activeChannel ? (
<ChannelManagementAuxiliaryPanel
activeChannel={activeChannel}
Expand All @@ -824,6 +834,8 @@ export const ChannelPane = React.memo(function ChannelPane({
useSplitAuxiliaryPane={useSplitAuxiliaryPane}
transparentChrome={hasSplitAuxiliaryPane}
/>
) : priorityIdleAuxiliary && idleAuxiliarySurface ? (
idleAuxiliarySurface
) : threadHeadMessage ? (
(() => {
const panel = (
Expand Down Expand Up @@ -975,24 +987,9 @@ export const ChannelPane = React.memo(function ChannelPane({
);
return wrapAux(panel, "user-profile-panel");
})()
) : idleAuxiliaryPanel && onCloseIdleAuxiliaryPanel ? (
wrapIdlePanel(
<IdleAuxiliaryPanel
canResetWidth={canResetThreadPanelWidth}
headerControls={idleAuxiliaryHeaderActions}
isFocusDrawer={useFocusIdleDrawer}
isSinglePanelView={isSinglePanelView}
onClose={onCloseIdleAuxiliaryPanel}
onResetWidth={onResetThreadPanelWidth}
onResizeStart={onThreadPanelResizeStart}
title={idleAuxiliaryTitle}
useSplitAuxiliaryPane={useSplitAuxiliaryPane}
widthPx={threadPanelWidthPx}
>
{idleAuxiliaryPanel}
</IdleAuxiliaryPanel>,
)
) : null}
) : (
idleAuxiliarySurface
)}
</AnimatePresence>
</div>
);
Expand Down
6 changes: 4 additions & 2 deletions desktop/src/features/channels/ui/ChannelPane.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/features/channels/ui/ChannelScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function ChannelScreen({
headerEndActions,
idleAuxiliaryPanel,
idleAuxiliaryHeaderActions,
idleAuxiliaryOverridesThread,
idleAuxiliaryTitle,
onAddFiles,
onCloseIdleAuxiliaryPanel,
Expand Down Expand Up @@ -855,6 +856,7 @@ export function ChannelScreen({
header={channelHeader}
idleAuxiliaryPanel={idleAuxiliaryPanel}
idleAuxiliaryHeaderActions={idleAuxiliaryHeaderActions}
idleAuxiliaryOverridesThread={idleAuxiliaryOverridesThread}
idleAuxiliaryTitle={idleAuxiliaryTitle}
hasOlderMessages={hasOlderMessages}
historyExhausted={historyExhausted}
Expand Down
1 change: 1 addition & 0 deletions desktop/src/features/channels/ui/ChannelScreen.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type ChannelScreenProps = {
currentProfile?: Profile;
idleAuxiliaryPanel?: ReactNode;
idleAuxiliaryHeaderActions?: IdleAuxiliaryHeaderControls;
idleAuxiliaryOverridesThread?: boolean;
idleAuxiliaryTitle?: string;
headerEndActions?: ReactNode;
onAddFiles?: () => void;
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/channels/ui/FocusThreadDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export function FocusThreadDrawer({
// share a radius — a smaller one here would put two radii on one
// element. `shadow-panel-left` draws the left edge and its corners;
// see the token for why a `border-l` cannot.
"absolute inset-y-0 right-0 flex flex-col overflow-hidden rounded-l-2xl bg-background shadow-panel-left",
"absolute inset-y-0 right-0 flex flex-col overflow-hidden rounded-l-2xl bg-background shadow-panel-left outline-hidden",
)}
aria-label={label}
data-testid="focus-thread-drawer"
Expand Down
9 changes: 8 additions & 1 deletion desktop/src/features/channels/ui/RightAuxiliaryPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type RightAuxiliaryPaneProps = {
detached?: boolean;
onResetWidth: () => void;
onResizeStart: (event: React.PointerEvent<HTMLButtonElement>) => void;
showResizeIndicator?: boolean;
testId?: string;
widthPx: number;
};
Expand All @@ -23,6 +24,7 @@ export function RightAuxiliaryPane({
detached = false,
onResetWidth,
onResizeStart,
showResizeIndicator = true,
testId,
widthPx,
}: RightAuxiliaryPaneProps) {
Expand Down Expand Up @@ -56,7 +58,12 @@ export function RightAuxiliaryPane({
}
type="button"
>
<span className="absolute bottom-0 left-1/2 top-0 w-px -translate-x-1/2 bg-transparent group-hover/right-pane-resize:bg-border/80 group-focus-visible/right-pane-resize:bg-border/80" />
{showResizeIndicator ? (
<span
className="absolute bottom-0 left-1/2 top-0 w-px -translate-x-1/2 bg-transparent group-hover/right-pane-resize:bg-border/80 group-focus-visible/right-pane-resize:bg-border/80"
data-testid="right-auxiliary-pane-resize-indicator"
/>
) : null}
</button>
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col">
{children}
Expand Down
1 change: 1 addition & 0 deletions desktop/src/features/projects/createProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type CreateProjectInput = {
channelVisibility?: ChannelVisibility;
projectVisibility?: ProjectListingVisibility;
agents?: readonly CreateChannelManagedAgentInput[];
templateId?: string;
};

export type CreateProjectResult = {
Expand Down
8 changes: 4 additions & 4 deletions desktop/src/features/projects/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,9 @@ export type {
ProjectPullRequestCommentAnchor,
Repository,
};

export type ProjectPullRequestCommentDecision = "request-changes";

const HIDDEN_PROJECT_CARDS_KEY = "buzz.projects.hidden-cards.v1";

export type RepoState = {
branches: Array<{ name: string; commit: string }>;
tags: Array<{ name: string; commit: string }>;
Expand Down Expand Up @@ -213,8 +211,10 @@ function eventToRepoState(event: RelayEvent): RepoState {
updatedAt: event.created_at,
};
}

async function fetchRepoState(project: Repository): Promise<RepoState | null> {
/** Load the trusted relay state used to resolve a repository's live refs. */
export async function fetchRepoState(
project: Repository,
): Promise<RepoState | null> {
const relaySelf = await getRelaySelf();
const trustedAuthors = [
...new Set(
Expand Down
Loading
Loading