diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index 73e40e8cd89..db10c12dc95 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -939,6 +939,7 @@ export function AppShell() { onSelectChannel={(channelId) => { void goChannel(channelId); }} + relayUrl={communitiesHook.activeCommunity?.relayUrl} /> { return { default: module.ChannelManagementSheet }; }); +const MembersSidebar = React.lazy(async () => { + const module = await import("@/features/channels/ui/MembersSidebar"); + return { default: module.MembersSidebar }; +}); + export type BrowseDialogType = "stream" | "forum" | null; type AppShellOverlaysProps = { @@ -30,6 +35,7 @@ type AppShellOverlaysProps = { onChannelManagementOpenChange: (open: boolean) => void; onDeleteActiveChannel: () => void; onSelectChannel: (channelId: string) => void; + relayUrl?: string; }; export function AppShellOverlays({ @@ -45,7 +51,11 @@ export function AppShellOverlays({ onChannelManagementOpenChange, onDeleteActiveChannel, onSelectChannel, + relayUrl, }: AppShellOverlaysProps) { + const [membersChannel, setMembersChannel] = React.useState( + null, + ); const [visibleBrowseDialogType, setVisibleBrowseDialogType] = React.useState(null); const { cancelDeferredModalOpen, openNextFrame: openModalNextFrame } = @@ -89,11 +99,28 @@ export function AppShellOverlays({ channel={activeChannel} currentPubkey={currentPubkey} onDeleted={onDeleteActiveChannel} + onOpenMembers={() => setMembersChannel(activeChannel)} onOpenChange={onChannelManagementOpenChange} open={true} /> ) : null} + + {membersChannel ? ( + + { + if (!nextOpen) { + setMembersChannel(null); + } + }} + open={true} + relayUrl={relayUrl} + /> + + ) : null} ); } diff --git a/desktop/src/features/agent-memory/ui/MemorySection.tsx b/desktop/src/features/agent-memory/ui/MemorySection.tsx index cfc5ed7bd92..d63ceeb3cd9 100644 --- a/desktop/src/features/agent-memory/ui/MemorySection.tsx +++ b/desktop/src/features/agent-memory/ui/MemorySection.tsx @@ -10,6 +10,7 @@ import { Skeleton } from "@/shared/ui/skeleton"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; const MEMORY_LIST_PREVIEW_LIMIT = 3; +type MemorySectionVariant = "cards" | "grouped"; const MEMORY_TRUNCATED_TOOLTIP = "This list may be incomplete — the relay returned the maximum number of memories."; @@ -41,15 +42,17 @@ const MEMORY_DANGLING_REF_TOOLTIP = */ export function MemorySection({ agentPubkey, + variant = "cards", viewerIsOwner, }: { agentPubkey: string; + variant?: MemorySectionVariant; viewerIsOwner: boolean; }): React.ReactElement | null { // Hide entirely for non-owners. if (!viewerIsOwner) return null; - return ; + return ; } export function MemoryRefreshButton({ @@ -92,7 +95,13 @@ export function MemoryRefreshButton({ ); } -function MemorySectionForOwner({ agentPubkey }: { agentPubkey: string }) { +function MemorySectionForOwner({ + agentPubkey, + variant, +}: { + agentPubkey: string; + variant: MemorySectionVariant; +}) { const { query, graph } = useAgentMemoryGraph(agentPubkey); // Order matters here. We want: @@ -107,13 +116,14 @@ function MemorySectionForOwner({ agentPubkey }: { agentPubkey: string }) { return (
- {showInitialSkeleton ? : null} + {showInitialSkeleton ? : null} {showInitialError ? ( query.refetch()} retrying={query.isFetching} + variant={variant} /> ) : null} @@ -123,10 +133,17 @@ function MemorySectionForOwner({ agentPubkey }: { agentPubkey: string }) { still have prior data on screen. Distinct from the initial error state above. */} {query.isError && !query.isFetching ? ( - query.refetch()} /> + query.refetch()} + variant={variant} + /> ) : null} - + ) : null}
@@ -135,11 +152,11 @@ function MemorySectionForOwner({ agentPubkey }: { agentPubkey: string }) { // ── Subviews ──────────────────────────────────────────────────────────────── -function MemorySkeleton() { +function MemorySkeleton({ variant }: { variant: MemorySectionVariant }) { return (
@@ -154,16 +171,21 @@ function MemoryErrorState({ error, onRetry, retrying, + variant, }: { error: unknown; onRetry: () => void; retrying: boolean; + variant: MemorySectionVariant; }) { const message = error instanceof Error ? error.message : String(error ?? "unknown error"); return (
@@ -189,10 +211,19 @@ function MemoryErrorState({ ); } -function MemoryStaleErrorBanner({ onRetry }: { onRetry: () => void }) { +function MemoryStaleErrorBanner({ + onRetry, + variant, +}: { + onRetry: () => void; + variant: MemorySectionVariant; +}) { return (
@@ -211,9 +242,11 @@ function MemoryStaleErrorBanner({ onRetry }: { onRetry: () => void }) { function MemoryGraphView({ graph, truncated, + variant, }: { graph: NonNullable["graph"]>; truncated: boolean; + variant: MemorySectionVariant; }) { const { rootedTree, orphans, dangling } = graph; const [showAllEntries, setShowAllEntries] = React.useState(false); @@ -250,10 +283,13 @@ function MemoryGraphView({ : entries.slice(0, MEMORY_LIST_PREVIEW_LIMIT); return ( -
+
{!core && memories.length > 0 ? (

No core memory yet — agent @@ -261,12 +297,18 @@ function MemoryGraphView({

) : null} -
+
{visibleEntries.map((entry) => ( ))}
@@ -276,14 +318,22 @@ function MemoryGraphView({ count={entries.length} onClick={() => setShowAllEntries(true)} truncated={truncated} + variant={variant} /> ) : null} - {truncated && !hasMoreEntries ? : null} + {truncated && !hasMoreEntries ? ( + + ) : null} {hasMoreEntries && showAllEntries ? ( + ) : channel.channelType !== "dm" ? ( +
+

+ {channel.name} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+ ) : null}
); } -export function ChannelQuickAction({ - active, - disabled, - icon: Icon, - label, - onClick, +export function FieldGroup({ + children, + description, testId, + title, }: { - active?: boolean; - disabled?: boolean; - icon: LucideIcon; - label: string; - onClick: () => void; + children: React.ReactNode; + description?: React.ReactNode; testId?: string; + title?: React.ReactNode; }) { return ( - - ); -} - -export function FieldGroup({ children }: { children: React.ReactNode }) { - return ( -
{children}
+ + {children} + ); } @@ -114,26 +137,51 @@ export function getMarkdownPreviewText(content: string) { .join(" "); } +function truncateIdentifier(value: string) { + if (value.length <= 12) return value; + return `${value.slice(0, 8)}…${value.slice(-4)}`; +} + export function CopyFieldRow({ icon: Icon, label, value, testId, }: { - icon: LucideIcon; + icon?: LucideIcon; label: string; value: string; testId?: string; }) { + const [copied, setCopied] = React.useState(false); + const resetTimerRef = React.useRef(null); + + React.useEffect( + () => () => { + if (resetTimerRef.current !== null) { + window.clearTimeout(resetTimerRef.current); + } + }, + [], + ); + async function handleCopy() { await writeTextToClipboard(value); + setCopied(true); + if (resetTimerRef.current !== null) { + window.clearTimeout(resetTimerRef.current); + } + resetTimerRef.current = window.setTimeout(() => { + setCopied(false); + resetTimerRef.current = null; + }, 1_500); toast.success(`Copied ${label.toLowerCase()}`); } return ( ); } @@ -160,73 +235,203 @@ export function CopyFieldRow({ export function InfoFieldRow({ icon: Icon, label, + multiline = false, + onClick, + trailing, value, testId, }: { - icon: LucideIcon; + icon?: LucideIcon; label: string; + multiline?: boolean; + onClick?: () => void; + trailing?: React.ReactNode; value: string; testId?: string; }) { - return ( -
- - - + const content = ( + <> + {Icon ? ( + + ) : null} - + {label} - + {value} -
+ {trailing} + ); -} -export function NarrativeGroup({ children }: { children: React.ReactNode }) { + if (onClick) { + return ( + + ); + } + return ( -
{children}
+
+ {content} +
); } -export function NarrativeField({ +export function EditableInfoFieldRow({ + editTestId, icon: Icon, label, + multiline = false, + onEdit, value, testId, }: { - icon: LucideIcon; + editTestId?: string; + icon?: LucideIcon; label: string; + multiline?: boolean; + onEdit?: () => void; value: string; testId: string; }) { - return ( -
- - - - - + const content = ( + <> + {Icon ? ( + + ) : null} + + {label} - + {value} + {onEdit ? ( + + ) : null} + + ); + + if (onEdit) { + return ( + + ); + } + + return ( +
+ {content}
); } +type ActionFieldRowProps = { + destructive?: boolean; + description?: string; + disabled?: boolean; + icon: LucideIcon; + label: string; + onClick?: () => void; + testId: string; +}; + +export const ActionFieldRow = React.forwardRef< + HTMLButtonElement, + ActionFieldRowProps +>(function ActionFieldRow( + { + destructive = false, + description, + disabled, + icon: Icon, + label, + onClick, + testId, + ...triggerProps + }, + ref, +) { + return ( + + ); +}); + export function IngressRow({ description, + helpText, icon: Icon, label, onClick, @@ -234,6 +439,7 @@ export function IngressRow({ trailing, }: { description?: string; + helpText?: string; icon: LucideIcon; label: string; onClick: () => void; @@ -241,29 +447,51 @@ export function IngressRow({ trailing?: string; }) { return ( - + + + {helpText} + + + ) : null} +
+ {description ? ( + + {description} + + ) : null}
- {description ? ( - - {description} + {trailing ? ( + + {trailing} ) : null} - - {trailing ? ( - {trailing} - ) : null} - - + +
+ ); } diff --git a/desktop/src/features/channels/ui/ChannelMemberAvatarStack.tsx b/desktop/src/features/channels/ui/ChannelMemberAvatarStack.tsx new file mode 100644 index 00000000000..a9662bbf047 --- /dev/null +++ b/desktop/src/features/channels/ui/ChannelMemberAvatarStack.tsx @@ -0,0 +1,75 @@ +import * as React from "react"; + +import { useUsersBatchQuery } from "@/features/profile/hooks"; +import { resolveUserLabel } from "@/features/profile/lib/identity"; +import type { ChannelMember } from "@/shared/api/types"; +import { normalizePubkey } from "@/shared/lib/pubkey"; +import { UserAvatar } from "@/shared/ui/UserAvatar"; + +const MAX_VISIBLE_AVATARS = 3; + +export function ChannelMemberAvatarStack({ + currentPubkey, + members, +}: { + currentPubkey?: string; + members: ChannelMember[]; +}) { + const visibleMembers = members.slice(0, MAX_VISIBLE_AVATARS); + const visiblePubkeys = React.useMemo( + () => members.slice(0, MAX_VISIBLE_AVATARS).map((member) => member.pubkey), + [members], + ); + const profilesQuery = useUsersBatchQuery(visiblePubkeys); + const profiles = profilesQuery.data?.profiles; + const overflowCount = members.length - visibleMembers.length; + const stackItemCount = visibleMembers.length + (overflowCount > 0 ? 1 : 0); + + if (members.length === 0) { + return null; + } + + return ( +
+ {visibleMembers.map((member, index) => { + const normalizedPubkey = normalizePubkey(member.pubkey); + const profile = profiles?.[normalizedPubkey]; + const label = resolveUserLabel({ + currentPubkey, + fallbackName: member.displayName, + profiles, + pubkey: member.pubkey, + }); + + return ( + 0 ? "-ml-2" : ""} + data-testid="channel-management-member-avatar" + key={normalizedPubkey} + style={{ zIndex: index + 1 }} + > + + + ); + })} + {overflowCount > 0 ? ( + + +{overflowCount} + + ) : null} +
+ ); +} diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index 28f3f8aae7a..8fc7cfaf51a 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -762,6 +762,7 @@ export const ChannelPane = React.memo(function ChannelPane({ key="channel-management-panel" onChannelManagementDeleted={onChannelManagementDeleted} onCloseChannelManagement={onCloseChannelManagement} + onOpenMembers={onOpenMembers} onResetThreadPanelWidth={onResetThreadPanelWidth} onThreadPanelResizeStart={onThreadPanelResizeStart} threadPanelWidthPx={threadPanelWidthPx} diff --git a/desktop/src/features/channels/ui/channelFormStyles.ts b/desktop/src/features/channels/ui/channelFormStyles.ts index 60e4053901a..df07001293c 100644 --- a/desktop/src/features/channels/ui/channelFormStyles.ts +++ b/desktop/src/features/channels/ui/channelFormStyles.ts @@ -2,4 +2,4 @@ export const CHANNEL_FORM_FIELD_SHELL_CLASS = "rounded-xl border border-input bg-muted/40 transition-colors duration-150 ease-out hover:border-muted-foreground/40 focus-within:border-muted-foreground/50"; export const CHANNEL_FORM_FIELD_CONTROL_CLASS = - "border-0 bg-transparent text-muted-foreground/55 shadow-none outline-none ring-0 transition-colors duration-150 ease-out placeholder:text-muted-foreground/55 focus:bg-transparent focus:text-foreground focus:outline-hidden focus-visible:ring-0"; + "border-0 bg-transparent text-foreground shadow-none outline-none ring-0 transition-colors duration-150 ease-out placeholder:text-muted-foreground/55 focus:bg-transparent focus:text-foreground focus:outline-hidden focus-visible:ring-0"; diff --git a/desktop/src/features/home/ui/HomeMembersSidebarOverlay.tsx b/desktop/src/features/home/ui/HomeMembersSidebarOverlay.tsx new file mode 100644 index 00000000000..88df346af43 --- /dev/null +++ b/desktop/src/features/home/ui/HomeMembersSidebarOverlay.tsx @@ -0,0 +1,37 @@ +import * as React from "react"; + +import { useCommunities } from "@/features/communities/useCommunities"; +import type { Channel } from "@/shared/api/types"; + +const MembersSidebar = React.lazy(async () => { + const module = await import("@/features/channels/ui/MembersSidebar"); + return { default: module.MembersSidebar }; +}); + +export function HomeMembersSidebarOverlay({ + channel, + currentPubkey, + onClose, +}: { + channel: Channel | null; + currentPubkey?: string; + onClose: () => void; +}) { + const { activeCommunity } = useCommunities(); + + if (!channel) return null; + + return ( + + { + if (!nextOpen) onClose(); + }} + open={true} + relayUrl={activeCommunity?.relayUrl} + /> + + ); +} diff --git a/desktop/src/features/home/ui/HomeView.tsx b/desktop/src/features/home/ui/HomeView.tsx index 4ffbbaddc53..3b7bff44ea0 100644 --- a/desktop/src/features/home/ui/HomeView.tsx +++ b/desktop/src/features/home/ui/HomeView.tsx @@ -61,7 +61,7 @@ import { useRelaySelfQuery } from "@/features/moderation/hooks"; import { resolveUserLabel } from "@/features/profile/lib/identity"; import { useRemindLater } from "@/features/reminders/ui/RemindMeLaterProvider"; import { deleteMessage, sendChannelMessage } from "@/shared/api/tauri"; -import type { HomeFeedResponse } from "@/shared/api/types"; +import type { Channel, HomeFeedResponse } from "@/shared/api/types"; import { KIND_REACTION } from "@/shared/constants/kinds"; import { topChromeInset } from "@/shared/layout/chromeLayout"; import { cn } from "@/shared/lib/cn"; @@ -72,6 +72,7 @@ import { AUXILIARY_PANEL_SINGLE_COLUMN_BREAKPOINT_PX } from "@/shared/layout/Aux import { useHistorySearchState } from "@/shared/hooks/useHistorySearchState"; import { ProfilePanelProvider } from "@/shared/context/ProfilePanelContext"; import { Button } from "@/shared/ui/button"; +import { HomeMembersSidebarOverlay } from "./HomeMembersSidebarOverlay"; const INBOX_SEARCH_KEYS = [ "item", @@ -167,6 +168,9 @@ export function HomeView({ const [managedChannelId, setManagedChannelId] = React.useState( null, ); + const [membersChannel, setMembersChannel] = React.useState( + null, + ); const { goChannel } = useAppNavigation(); const openDmMutation = useOpenDmMutation(); const openDm = openDmMutation.mutateAsync; @@ -972,6 +976,7 @@ export function HomeView({ channel={managedChannel} currentPubkey={currentPubkey} layout="split" + onOpenMembers={() => setMembersChannel(managedChannel)} onOpenChange={(nextOpen) => { if (!nextOpen) { setManagedChannelId(null); @@ -983,6 +988,11 @@ export function HomeView({ ) : null}
+ setMembersChannel(null)} + /> ); } diff --git a/desktop/src/features/profile/ui/ProfileTabContentTransition.tsx b/desktop/src/features/profile/ui/ProfileTabContentTransition.tsx new file mode 100644 index 00000000000..b5174251dc2 --- /dev/null +++ b/desktop/src/features/profile/ui/ProfileTabContentTransition.tsx @@ -0,0 +1,116 @@ +import * as React from "react"; +import { + AnimatePresence, + motion, + type Variants, + useReducedMotion, +} from "motion/react"; + +import type { ProfilePanelTab } from "@/features/profile/ui/UserProfilePanelUtils"; +import { cn } from "@/shared/lib/cn"; + +type TabTransitionDirection = -1 | 0 | 1; + +type TabTransitionContext = { + direction: TabTransitionDirection; + reduceMotion: boolean; +}; + +const TAB_CONTENT_OFFSET_PX = 28; + +const tabContentVariants: Variants = { + enter: ({ direction, reduceMotion }: TabTransitionContext) => ({ + opacity: reduceMotion ? 1 : 0.72, + pointerEvents: "auto", + transform: reduceMotion + ? "translateX(0px)" + : `translateX(${direction * TAB_CONTENT_OFFSET_PX}px)`, + }), + center: { + opacity: 1, + pointerEvents: "auto", + transform: "translateX(0px)", + }, + exit: ({ direction, reduceMotion }: TabTransitionContext) => ({ + opacity: reduceMotion ? 1 : 0, + pointerEvents: "none", + transform: reduceMotion + ? "translateX(0px)" + : `translateX(${-direction * TAB_CONTENT_OFFSET_PX}px)`, + }), +}; + +export function ProfileTabContentTransition({ + activeTab, + children, + className, + tabs, +}: { + activeTab: ProfilePanelTab; + children: React.ReactNode; + className?: string; + tabs: ProfilePanelTab[]; +}) { + const reduceMotion = useReducedMotion() ?? false; + const [lastTransition, setLastTransition] = React.useState<{ + direction: TabTransitionDirection; + tab: ProfilePanelTab; + }>({ direction: 0, tab: activeTab }); + const previousIndex = tabs.indexOf(lastTransition.tab); + const activeIndex = tabs.indexOf(activeTab); + const direction: TabTransitionDirection = + lastTransition.tab === activeTab + ? lastTransition.direction + : previousIndex < 0 || activeIndex < 0 || previousIndex === activeIndex + ? 0 + : activeIndex > previousIndex + ? 1 + : -1; + const transitionContext: TabTransitionContext = { + direction, + reduceMotion, + }; + + React.useLayoutEffect(() => { + if (lastTransition.tab !== activeTab) { + setLastTransition({ direction, tab: activeTab }); + } + }, [activeTab, direction, lastTransition.tab]); + + return ( +
0 ? "forward" : "backward" + } + > + + + {children} + + +
+ ); +} diff --git a/desktop/src/features/profile/ui/UserProfileAgentActions.tsx b/desktop/src/features/profile/ui/UserProfileAgentActions.tsx index 81db3405b21..5919a16e29b 100644 --- a/desktop/src/features/profile/ui/UserProfileAgentActions.tsx +++ b/desktop/src/features/profile/ui/UserProfileAgentActions.tsx @@ -6,23 +6,12 @@ import { Download, Power, Settings, - Trash2, } from "lucide-react"; import type { IdentityArchiveActions } from "@/features/identity-archive/hooks"; import { ArchiveConfirmDialog } from "@/features/profile/ui/ArchiveConfirmDialog"; import type { ManagedAgent } from "@/shared/api/types"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/shared/ui/alert-dialog"; -import { Button, buttonVariants } from "@/shared/ui/button"; +import { Button } from "@/shared/ui/button"; import { DropdownMenu, DropdownMenuContent, @@ -37,7 +26,6 @@ export function UserProfileAgentSettingsMenu({ isPending, isBot = false, managedAgent, - onDelete, onDuplicatePersona, onExportPersona, onToggleAutoStart, @@ -47,14 +35,12 @@ export function UserProfileAgentSettingsMenu({ isPending: boolean; isBot?: boolean; managedAgent?: ManagedAgent; - onDelete?: () => void; onDuplicatePersona?: () => void; onExportPersona?: () => void; onToggleAutoStart?: () => void; personaActionKey?: string; }) { const [archiveConfirmOpen, setArchiveConfirmOpen] = React.useState(false); - const [deleteConfirmOpen, setDeleteConfirmOpen] = React.useState(false); const actionKey = managedAgent?.pubkey ?? "persona-draft"; const personaKey = personaActionKey ?? actionKey; const canToggleAutoStart = @@ -66,11 +52,8 @@ export function UserProfileAgentSettingsMenu({ const hasArchiveAction = archiveActions?.canArchive === true && archiveActions.isArchived !== undefined; - const shouldConfirmAgentDelete = - managedAgent !== undefined && onDelete !== undefined; - const hasManageActions = hasArchiveAction || Boolean(onDelete); const hasActions = - canToggleAutoStart || hasPrimaryActions || hasManageActions; + canToggleAutoStart || hasPrimaryActions || hasArchiveAction; if (!hasActions) { return null; @@ -142,7 +125,7 @@ export function UserProfileAgentSettingsMenu({ Export ) : null} - {hasManageActions && (canToggleAutoStart || hasPrimaryActions) ? ( + {hasArchiveAction && (canToggleAutoStart || hasPrimaryActions) ? ( ) : null} {hasArchiveAction && archiveActions ? ( @@ -166,24 +149,6 @@ export function UserProfileAgentSettingsMenu({ ) ) : null} - {onDelete && hasArchiveAction ? : null} - {onDelete ? ( - { - if (shouldConfirmAgentDelete) { - setDeleteConfirmOpen(true); - return; - } - onDelete(); - }} - > - - Delete agent - - ) : null} {hasArchiveAction && archiveActions ? ( @@ -198,32 +163,17 @@ export function UserProfileAgentSettingsMenu({ open={archiveConfirmOpen} /> ) : null} - {shouldConfirmAgentDelete ? ( - { - setDeleteConfirmOpen(false); - onDelete(); - }} - onOpenChange={setDeleteConfirmOpen} - open={deleteConfirmOpen} - /> - ) : null} ); } export function UserProfileAgentSettingsMenuSlot({ archiveActions, - canDeletePersona, canInstantiateAgent, canManagePersona, isAgentActionPending, isBot, managedAgent, - onDeleteAgent, - onDeletePersona, onDuplicatePersona, onExportPersona, onToggleAutoStart, @@ -231,14 +181,11 @@ export function UserProfileAgentSettingsMenuSlot({ viewerIsOwner, }: { archiveActions: IdentityArchiveActions; - canDeletePersona: boolean; canInstantiateAgent: boolean; canManagePersona: boolean; isAgentActionPending: boolean; isBot: boolean; managedAgent?: ManagedAgent; - onDeleteAgent: () => void; - onDeletePersona: () => void; onDuplicatePersona: () => void; onExportPersona: () => void; onToggleAutoStart: () => void; @@ -250,11 +197,12 @@ export function UserProfileAgentSettingsMenuSlot({ const settingsActionPending = isAgentActionPending || archiveActions.isPending; const sharedProps = { - archiveActions: canShowArchiveAction ? archiveActions : undefined, + archiveActions: !isBot && canShowArchiveAction ? archiveActions : undefined, isBot, isPending: settingsActionPending, - onDuplicatePersona: canManagePersona ? onDuplicatePersona : undefined, - onExportPersona: canManagePersona ? onExportPersona : undefined, + onDuplicatePersona: + !isBot && canManagePersona ? onDuplicatePersona : undefined, + onExportPersona: !isBot && canManagePersona ? onExportPersona : undefined, personaActionKey, }; @@ -263,22 +211,16 @@ export function UserProfileAgentSettingsMenuSlot({ ); } if (canInstantiateAgent) { - return ( - - ); + return ; } - if (canShowArchiveAction) { + if (canShowArchiveAction && !isBot) { return ( void; - onOpenChange: (open: boolean) => void; - open: boolean; -}) { - const isProviderAgent = agent.backend.type === "provider"; - - return ( - - - - Delete this agent? - - Deleting this agent stops and removes the agent from this community. - - -
    -
  • Removes the local management record and saved agent key
  • -
  • Removes the agent from every channel it belongs to
  • -
  • - Archives the agent's identity on the relay so it no longer - appears in member lists or mention suggestions -
  • -
  • - {isProviderAgent - ? "Requests remote deletion; if it is online, Buzz first sends a shutdown command when possible. If the deployment cannot be reached through a channel, the remote process may keep running without local management." - : "Stops any local agent process before deleting the record"} -
  • -
-

- You can also archive this agent from the profile settings menu if you - want to hide the agent instead of removing it. -

- - - - - - {isPending ? "Deleting..." : "Delete agent"} - - -
-
- ); -} diff --git a/desktop/src/features/profile/ui/UserProfileAgentManagementRows.tsx b/desktop/src/features/profile/ui/UserProfileAgentManagementRows.tsx new file mode 100644 index 00000000000..8c6b4138cd7 --- /dev/null +++ b/desktop/src/features/profile/ui/UserProfileAgentManagementRows.tsx @@ -0,0 +1,283 @@ +import * as React from "react"; +import { + Archive, + ArchiveRestore, + CopyPlus, + Download, + Trash2, + type LucideIcon, +} from "lucide-react"; + +import type { IdentityArchiveActions } from "@/features/identity-archive/hooks"; +import { ArchiveConfirmDialog } from "@/features/profile/ui/ArchiveConfirmDialog"; +import type { ManagedAgent } from "@/shared/api/types"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/shared/ui/alert-dialog"; +import { Button, buttonVariants } from "@/shared/ui/button"; +import { PanelSectionGroup } from "@/shared/ui/PanelSectionGroup"; + +export function UserProfileAgentManagementRows({ + archiveActions, + canArchiveAgent, + canDeleteAgent, + isDeletePending, + managedAgent, + onDeleteAgent, + onDuplicateAgent, + onExportAgent, +}: { + archiveActions: IdentityArchiveActions; + canArchiveAgent: boolean; + canDeleteAgent: boolean; + isDeletePending: boolean; + managedAgent?: ManagedAgent; + onDeleteAgent: () => void; + onDuplicateAgent?: () => void; + onExportAgent?: () => void; +}) { + if ( + !onDuplicateAgent && + !onExportAgent && + !canArchiveAgent && + !canDeleteAgent + ) { + return null; + } + + return ( + + {onDuplicateAgent ? ( + + ) : null} + {onExportAgent ? ( + + ) : null} + {canArchiveAgent ? ( + + ) : null} + {canDeleteAgent ? ( + + ) : null} + + ); +} + +function ProfileAgentActionRow({ + destructive = false, + disabled = false, + icon: Icon, + label, + onClick, + testId, +}: { + destructive?: boolean; + disabled?: boolean; + icon: LucideIcon; + label: string; + onClick: () => void; + testId: string; +}) { + return ( + + ); +} + +function ProfileArchiveAgentRow({ + archiveActions, +}: { + archiveActions: IdentityArchiveActions; +}) { + const [confirmOpen, setConfirmOpen] = React.useState(false); + const isArchived = archiveActions.isArchived === true; + const Icon = isArchived ? ArchiveRestore : Archive; + const label = archiveActions.isPending + ? isArchived + ? "Unarchiving…" + : "Archiving…" + : isArchived + ? "Unarchive agent" + : "Archive agent"; + + return ( + <> + { + if (isArchived) { + archiveActions.unarchive(); + return; + } + setConfirmOpen(true); + }} + testId={ + isArchived + ? "user-profile-unarchive-agent-row" + : "user-profile-archive-agent-row" + } + /> + { + archiveActions.archive(); + setConfirmOpen(false); + }} + onOpenChange={setConfirmOpen} + open={confirmOpen} + /> + + ); +} + +function ProfileDeleteAgentRow({ + isPending, + managedAgent, + onDelete, +}: { + isPending: boolean; + managedAgent?: ManagedAgent; + onDelete: () => void; +}) { + const [confirmOpen, setConfirmOpen] = React.useState(false); + + return ( + <> + { + if (managedAgent) { + setConfirmOpen(true); + return; + } + onDelete(); + }} + testId="user-profile-delete-agent-row" + /> + {managedAgent ? ( + { + setConfirmOpen(false); + onDelete(); + }} + onOpenChange={setConfirmOpen} + open={confirmOpen} + /> + ) : null} + + ); +} + +function AgentDeleteConfirmDialog({ + agent, + isPending, + onConfirm, + onOpenChange, + open, +}: { + agent: ManagedAgent; + isPending: boolean; + onConfirm: () => void; + onOpenChange: (open: boolean) => void; + open: boolean; +}) { + const isProviderAgent = agent.backend.type === "provider"; + + return ( + + + + Delete this agent? + + Deleting this agent stops and removes the agent from this community. + + +
    +
  • Removes the local management record and saved agent key
  • +
  • Removes the agent from every channel it belongs to
  • +
  • + Archives the agent's identity on the relay so it no longer + appears in member lists or mention suggestions +
  • +
  • + {isProviderAgent + ? "Requests remote deletion; if it is online, Buzz first sends a shutdown command when possible. If the deployment cannot be reached through a channel, the remote process may keep running without local management." + : "Stops any local agent process before deleting the record"} +
  • +
+

+ Archive this agent if you want to hide it instead of removing it. +

+ + + + + + {isPending ? "Deleting…" : "Delete agent"} + + +
+
+ ); +} diff --git a/desktop/src/features/profile/ui/UserProfileEditAgentDialog.tsx b/desktop/src/features/profile/ui/UserProfileEditAgentDialog.tsx new file mode 100644 index 00000000000..b1f4f54eedf --- /dev/null +++ b/desktop/src/features/profile/ui/UserProfileEditAgentDialog.tsx @@ -0,0 +1,34 @@ +import { AgentDialog } from "@/features/agents/ui/AgentDialog"; +import type { EditAgentFocusTarget } from "@/features/agents/openEditAgentEvent"; +import type { ManagedAgent } from "@/shared/api/types"; + +export function UserProfileEditAgentDialog({ + agent, + canEdit, + initialFocus, + onEditLinkedPersona, + onOpenChange, + open, +}: { + agent: ManagedAgent | undefined; + canEdit: boolean; + initialFocus: EditAgentFocusTarget | undefined; + onEditLinkedPersona: (() => void) | undefined; + onOpenChange: (open: boolean) => void; + open: boolean; +}) { + if (!canEdit || !agent) { + return null; + } + + return ( + + ); +} diff --git a/desktop/src/features/profile/ui/UserProfilePanel.tsx b/desktop/src/features/profile/ui/UserProfilePanel.tsx index cb188dd0089..91040a28e24 100644 --- a/desktop/src/features/profile/ui/UserProfilePanel.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanel.tsx @@ -2,10 +2,7 @@ import * as React from "react"; import { toast } from "sonner"; import { useAppNavigation } from "@/app/navigation/useAppNavigation"; -import { - useAgentMemoryQuery, - useIsManagedAgent, -} from "@/features/agent-memory/hooks"; +import { useIsManagedAgent } from "@/features/agent-memory/hooks"; import { type AttachManagedAgentToChannelResult, useAcpRuntimesQuery, @@ -33,13 +30,7 @@ import { resolveStartRuntimeForDefinition, } from "@/features/agents/lib/instanceInputForDefinition"; import { describeLogFile } from "@/features/agents/ui/agentUi"; -import { AgentDialog } from "@/features/agents/ui/AgentDialog"; import { useAgentLifecycleActions } from "@/features/profile/ui/useAgentLifecycleActions"; -import { - consumePendingOpenEditAgent, - type EditAgentFocusTarget, - subscribeOpenEditAgent, -} from "@/features/agents/openEditAgentEvent"; import { duplicatePersonaDialogState, editPersonaDialogState, @@ -59,13 +50,15 @@ import { import { ownsAuthorAgent } from "@/features/profile/lib/identity"; import { resolveProfileActivityAgent } from "@/features/profile/lib/profileActivityAgent"; import { - AgentInfoFocusedView, AgentInstructionsFocusedView, + ProfileSummaryView, +} from "@/features/profile/ui/UserProfilePanelSections"; +import { + AgentInfoFocusedView, ChannelsFocusedView, DiagnosticsFocusedView, MemoryFocusedView, - ProfileSummaryView, -} from "@/features/profile/ui/UserProfilePanelSections"; +} from "@/features/profile/ui/UserProfilePanelFocusedViews"; import { AgentConfigurationFocusedView } from "@/features/profile/ui/UserProfilePanelAgentDetails"; import { UserProfileAgentSettingsMenuSlot } from "@/features/profile/ui/UserProfileAgentActions"; import { useProfileAgentDeletion } from "@/features/profile/ui/UserProfilePanelDeletion"; @@ -86,7 +79,7 @@ import { type UserProfilePanelProps, useRetainedPersona, } from "@/features/profile/ui/UserProfilePanelUtils"; -import { useProfileDmAction } from "@/features/profile/ui/useProfileDmAction"; +import { useProfileInteractionActions } from "@/features/profile/ui/useProfileInteractionActions"; import { useUserStatusQuery } from "@/features/user-status/hooks"; import { useOpenAgentActivity } from "@/features/agents/useOpenAgentActivity"; import { useEscapeKey } from "@/shared/hooks/useEscapeKey"; @@ -101,6 +94,8 @@ import type { } from "@/shared/api/types"; import { UserProfilePanelFrame } from "@/features/profile/ui/UserProfilePanelFrame"; import { getUserProfilePanelHeaderContent } from "@/features/profile/ui/UserProfilePanelHeaderContent"; +import { UserProfileEditAgentDialog } from "@/features/profile/ui/UserProfileEditAgentDialog"; +import { useProfileEditAgentRequest } from "@/features/profile/ui/useProfileEditAgentRequest"; export type { ProfilePanelTab, ProfilePanelView }; export function UserProfilePanel({ @@ -154,28 +149,27 @@ export function UserProfilePanel({ }, [onTabChange], ); - const [editAgentOpen, setEditAgentOpen] = React.useState(false); - const [editAgentFocus, setEditAgentFocus] = React.useState< - EditAgentFocusTarget | undefined - >(undefined); - - // Open the Edit Agent dialog when `requestOpenEditAgent(pubkey)` fires from - // a card or other non-panel surface (e.g. `ConfigNudgeCard`). Mirrors the - // `subscribeOpenCreateAgent` pattern in AgentsView. - React.useEffect(() => { - if (!pubkey) return; - // Consume any pending request that arrived before this panel mounted. - const pending = consumePendingOpenEditAgent(pubkey); - if (pending !== false) { - setEditAgentFocus(pending === true ? undefined : pending); - setEditAgentOpen(true); - } - // Subscribe for events that arrive while the panel is mounted. - return subscribeOpenEditAgent(pubkey, (focus) => { - setEditAgentFocus(focus); - setEditAgentOpen(true); - }); - }, [pubkey]); + const [stickyChrome, setStickyChrome] = React.useState({ + active: false, + height: 0, + }); + const handleStickyChromeChange = React.useCallback( + (nextState: { active: boolean; height: number }) => { + setStickyChrome((currentState) => + currentState.active === nextState.active && + currentState.height === nextState.height + ? currentState + : nextState, + ); + }, + [], + ); + const { + focus: editAgentFocus, + open: editAgentOpen, + setFocus: setEditAgentFocus, + setOpen: setEditAgentOpen, + } = useProfileEditAgentRequest(pubkey); const [addToChannelOpen, setAddToChannelOpen] = React.useState(false); const [personaDialogState, setPersonaDialogState] = React.useState(null); @@ -325,15 +319,8 @@ export function UserProfilePanel({ }), [effectivePubkey, isBot, managedAgent, profile, relayAgent, viewerIsOwner], ); - // Observer ingestion (frame decryption + derived active-turn liveness) is - // owner-global — mounted once in AppShell via useAgentObserverIngestion — - // covering both locally managed agents and declared-owned relay agents. - const canEditAgent = - isOwner === true && - (managedAgent !== undefined || resolvedPersona !== undefined); - const memoryQuery = useAgentMemoryQuery(effectivePubkey, { - enabled: viewerIsOwner && Boolean(effectivePubkey), - }); + // Observer ingestion is owner-global across local and declared-owned agents. + const canEditAgent = Boolean(isOwner && (managedAgent ?? resolvedPersona)); const isSelf = currentPubkey !== undefined && pubkeyLower.length > 0 && @@ -395,10 +382,22 @@ export function UserProfilePanel({ setView("summary", { replace: true }); setTab("info", { replace: true }); }, [setTab, setView, targetKey]); - const { handleMessage, isOpeningDm } = useProfileDmAction({ + const { + canHuddle, + canMessage, + canWave, + handleHuddle, + handleMessage, + handleWave, + isStartingHuddle, + pendingAction, + } = useProfileInteractionActions({ effectivePubkey, + enabled: onOpenDm !== undefined, + isBot, + isSelf, onClose, - onOpenDm, + viewerIsOwner, }); const handleEditAgent = React.useCallback(() => { @@ -407,7 +406,7 @@ export function UserProfilePanel({ return; } setEditAgentOpen(true); - }, [resolvedPersona]); + }, [resolvedPersona, setEditAgentOpen]); const { deleteManagedAgentRecord, deleteManagedAgentsForPersona } = useProfileAgentDeletion({ @@ -707,31 +706,27 @@ export function UserProfilePanel({ : null; const ownerProfilePubkey = ownerPubkey ?? (isOwner === true ? (currentPubkey ?? null) : null); - const ownerAvatarProfile = ownerPubkey - ? ownerProfileQuery.data - : currentProfileQuery.data; - const memoryCount = - memoryQuery.data && - (memoryQuery.data.core ? 1 : 0) + memoryQuery.data.memories.length; const agentInstruction = resolveAgentInstruction( managedAgent, resolvedPersona, ); const canManagePersona = isOwner === true && resolvedPersona !== undefined; - const canEditPersona = canManagePersona; const canDeletePersona = canManagePersona && !resolvedPersona?.sourceTeam; + const canDeleteProfileAgent = + isBot && + ((viewerIsOwner && managedAgent !== undefined) || + (canInstantiateAgent && canDeletePersona)); + const handleDeleteProfileAgent = + viewerIsOwner && managedAgent ? handleDeleteAgent : handleDeletePersona; const archiveActions = useIdentityArchive(effectivePubkey); - const agentSettingsMenu = ( + const agentSettingsMenu = isBot ? null : ( setView("summary"), + onEditAgent: canEditAgent ? handleEditAgent : undefined, view, viewerIsOwner, }, @@ -783,10 +778,12 @@ export function UserProfilePanel({ ? "flex flex-col overflow-hidden" : "overflow-y-auto", )} + data-testid="user-profile-scroll-body" > {view === "summary" ? ( setAddToChannelOpen(true)} + onDeleteAgent={handleDeleteProfileAgent} + onDuplicateAgent={ + isBot && canManagePersona ? handleDuplicatePersona : undefined + } + onExportAgent={ + isBot && canManagePersona ? handleExportPersona : undefined + } onOpenInstance={(instancePubkey) => onOpenProfile?.(instancePubkey)} onOpenActivity={handleOpenActivity} onOpenChannel={handleOpenChannel} onOpenDiagnostics={() => setView("diagnostics")} - onOpenInstructions={() => setView("instructions")} + onStickyChromeChange={handleStickyChromeChange} onTabChange={setTab} - onOpenDm={onOpenDm} - onCreateCard={ - canManagePersona && resolvedPersona - ? () => - setCardMintTarget({ - // Prefer the live instance pubkey; fall back to the - // persona/definition id (same resolution as export). - id: managedAgent?.pubkey ?? resolvedPersona.id, - name: resolvedPersona.displayName, - // Locking needs an instance keypair to encrypt to. - canLock: Boolean(managedAgent?.pubkey), - }) - : undefined - } presenceStatus={presenceStatus} profile={profile} pubkey={effectivePubkey} @@ -905,28 +899,27 @@ export function UserProfilePanel({ ) : null} ); - const editAgentDialog = - canEditAgent && managedAgent ? ( - { - setEditAgentOpen(false); - setEditAgentFocus(undefined); - setPersonaDialogState(editPersonaDialogState(resolvedPersona)); - } - : undefined - } - onOpenChange={(next) => { - setEditAgentOpen(next); - if (!next) setEditAgentFocus(undefined); - }} - open={editAgentOpen} - /> - ) : null; + const editAgentDialog = ( + { + setEditAgentOpen(false); + setEditAgentFocus(undefined); + setPersonaDialogState(editPersonaDialogState(resolvedPersona)); + } + : undefined + } + onOpenChange={(next) => { + setEditAgentOpen(next); + if (!next) setEditAgentFocus(undefined); + }} + open={editAgentOpen} + /> + ); const addAgentToChannelDialog = managedAgent ? ( diff --git a/desktop/src/features/profile/ui/UserProfilePanelAgentDetails.tsx b/desktop/src/features/profile/ui/UserProfilePanelAgentDetails.tsx index 844de528e23..1da2e597dd8 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelAgentDetails.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelAgentDetails.tsx @@ -116,11 +116,11 @@ export function AgentInstructionRow({ trimmedInstruction.length > 0 && onOpenInstructions !== undefined; const rowContent = ( <> - - - +
-
Instructions
+
+ Agent instructions +
{trimmedInstruction ? ( canOpenInstructions ? ( void; testId?: string; @@ -82,7 +83,6 @@ export function useProfileFieldBuckets({ isOwner, managedAgent, onOpenProfile, - ownerAvatarUrl, ownerDisplayName, ownerHandle, ownerProfilePubkey, @@ -98,7 +98,6 @@ export function useProfileFieldBuckets({ isOwner: boolean | undefined; managedAgent: ManagedAgent | undefined; onOpenProfile?: (pubkey: string) => void; - ownerAvatarUrl: string | null; ownerDisplayName: string | null; ownerHandle: string | null; ownerProfilePubkey: string | null; @@ -118,7 +117,6 @@ export function useProfileFieldBuckets({ includeOperationalFields: isOwner === true, managedAgent, onOpenProfile, - ownerAvatarUrl, ownerDisplayName, ownerHandle, ownerProfilePubkey, @@ -136,7 +134,6 @@ export function useProfileFieldBuckets({ isOwner, managedAgent, onOpenProfile, - ownerAvatarUrl, ownerDisplayName, ownerHandle, ownerProfilePubkey, @@ -167,10 +164,17 @@ export function buildPublicFields({ if (pubkey) { fields.push({ + copyValue: pubkey, displayValue: truncatePubkey(pubkey), - displayNode: , - icon: Fingerprint, + displayNode: ( + + ), label: "Public key", + testId: "user-profile-public-key", }); } @@ -220,7 +224,6 @@ export function buildOwnerFields({ includeOperationalFields, managedAgent, onOpenProfile, - ownerAvatarUrl, ownerDisplayName, ownerHandle, ownerProfilePubkey, @@ -233,7 +236,6 @@ export function buildOwnerFields({ includeOperationalFields: boolean; managedAgent: ManagedAgent | undefined; onOpenProfile?: (pubkey: string) => void; - ownerAvatarUrl: string | null; ownerDisplayName: string | null; ownerHandle: string | null; ownerProfilePubkey: string | null; @@ -256,18 +258,6 @@ export function buildOwnerFields({ : null; const ownerClickable = Boolean(onOpenProfile && ownerProfilePubkey); - const ownerContent = ( - <> - - {ownerDisplayName} - - ); if (ownerDisplayName) { fields.push({ @@ -275,12 +265,7 @@ export function buildOwnerFields({ ? undefined : (ownerProfilePubkey ?? ownerPubkey ?? ownerHandle ?? undefined), displayValue: ownerDisplayName, - displayNode: ( - - {ownerContent} - - ), - icon: UserRound, + displayNode: {ownerDisplayName}, label: "Managed by", onClick: ownerClickable && ownerProfilePubkey @@ -335,8 +320,10 @@ export function buildOwnerFields({ .replace(/\b\w/g, (char: string) => char.toUpperCase()), displayNode: ( ), @@ -439,52 +426,114 @@ function orderProfileFields(fields: ProfileField[]) { ]; } -export function ProfileFieldRows({ fields }: { fields: ProfileField[] }) { +export function ProfileFieldRows({ + fields, + variant = "default", +}: { + fields: ProfileField[]; + variant?: "default" | "runtime"; +}) { return ( <> {orderProfileFields(fields).map((field) => ( - + ))} ); } -export function ProfileFieldGroup({ fields }: { fields: ProfileField[] }) { +export function ProfileSectionGroup({ + children, + headerAction, + testId, + title, +}: { + children: React.ReactNode; + headerAction?: React.ReactNode; + testId?: string; + title?: string; +}) { + return ( + +
{children}
+
+ ); +} + +export function ProfileFieldGroup({ + fields, + title, +}: { + fields: ProfileField[]; + title?: string; +}) { return ( -
-
- -
-
+ + + ); } -function ProfileFieldRow({ field }: { field: ProfileField }) { +function ProfileFieldRow({ + field, + variant, +}: { + field: ProfileField; + variant: "default" | "runtime"; +}) { const Icon = field.icon; const isCopyable = Boolean(field.copyValue); const isActionable = Boolean(field.onClick); + const isTrailingDisplay = + variant === "runtime" && field.label === "Status" && field.displayNode; + const { copied, copy } = useCopyFeedback({ + label: field.label, + value: field.copyValue ?? "", + }); const content = ( <> - - - + {variant === "default" && Icon ? ( + + ) : null} - + {field.label} - - {field.displayNode ?? field.displayValue} - + {!isTrailingDisplay ? ( + + {field.displayNode ?? field.displayValue} + + ) : null} + {isTrailingDisplay ? field.displayNode : null} {field.trailingNode} {isActionable ? ( - + ) : isCopyable ? ( - + ) : null} ); @@ -493,7 +542,7 @@ function ProfileFieldRow({ field }: { field: ProfileField }) { return ( + + ))} + + )} + +
+ ); +} + +export function AgentInfoFocusedView({ + metadataFields, +}: { + metadataFields: ProfileField[]; +}) { + if (metadataFields.length === 0) { + return null; + } + + return ( +
+ +
+ ); +} + +export function DiagnosticsFocusedView({ + canOpenAgentLogs, + fields, + logContent, + logError, + logLoading, + managedAgent, +}: { + canOpenAgentLogs: boolean; + fields: ProfileField[]; + logContent: string | null; + logError: Error | null; + logLoading: boolean; + managedAgent: ManagedAgent | undefined; +}) { + const hasLog = canOpenAgentLogs && managedAgent !== undefined; + const lastErrorField = fields.find((field) => field.label === "Last error"); + const detailFields = fields.filter( + (field) => field.label !== "Last error" && field.label !== "Status", + ); + + if (!lastErrorField && detailFields.length === 0 && !hasLog) { + return null; + } + + return ( +
+ {lastErrorField ? ( + + +
+ Last error + + {lastErrorField.displayValue} + +
+
+ ) : null} + {detailFields.length > 0 ? ( + + ) : null} + {hasLog ? ( +
+ +
+ ) : null} +
+ ); +} diff --git a/desktop/src/features/profile/ui/UserProfilePanelFrame.tsx b/desktop/src/features/profile/ui/UserProfilePanelFrame.tsx index 81d1860448c..11a744ee8a4 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelFrame.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelFrame.tsx @@ -1,7 +1,11 @@ import type * as React from "react"; -import { AuxiliaryPanel } from "@/shared/layout/AuxiliaryPanel"; -import { AuxiliaryPanelHeader } from "@/shared/layout/AuxiliaryPanel"; +import { + AUXILIARY_PANEL_DEFAULT_SURFACE_CLASS, + AuxiliaryPanel, + AuxiliaryPanelHeader, +} from "@/shared/layout/AuxiliaryPanel"; +import { cn } from "@/shared/lib/cn"; type UserProfilePanelFrameProps = { addAgentToChannelDialog: React.ReactNode; @@ -18,6 +22,9 @@ type UserProfilePanelFrameProps = { personaDialogs: React.ReactNode; profileBody: React.ReactNode; splitPaneClamp: boolean; + stickyChromeActive: boolean; + stickyChromeEnabled: boolean; + stickyChromeHeight: number; widthPx: number; transparentChrome?: boolean; }; @@ -37,12 +44,16 @@ export function UserProfilePanelFrame({ personaDialogs, profileBody, splitPaneClamp, + stickyChromeActive, + stickyChromeEnabled, + stickyChromeHeight, widthPx, transparentChrome = false, }: UserProfilePanelFrameProps) { return ( - {headerLeftContent} - {headerActions} - + <> +