From 497006f10a850c7fa6dadb41e7b5bbcccecf8fca Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Tue, 14 Apr 2026 14:01:18 -0400 Subject: [PATCH 01/20] chore(desktop): polish reply design surfaces Align the header, timeline, thread panel, composer, and sidebar shells so the reply design branch reads as one consistent visual system. Made-with: Cursor --- .../src/features/channels/ui/ChannelPane.tsx | 61 ++++--- .../features/channels/ui/ChannelScreen.tsx | 172 +++++++++--------- desktop/src/features/chat/ui/ChatHeader.tsx | 11 +- .../features/messages/ui/MessageComposer.tsx | 8 +- .../messages/ui/MessageComposerToolbar.tsx | 18 +- .../src/features/messages/ui/MessageRow.tsx | 128 +++++++------ .../messages/ui/MessageThreadPanel.tsx | 79 ++++---- .../messages/ui/MessageThreadSummaryRow.tsx | 2 +- .../features/messages/ui/MessageTimeline.tsx | 20 +- .../messages/ui/TypingIndicatorRow.tsx | 10 +- .../src/features/sidebar/ui/AppSidebar.tsx | 5 - desktop/src/shared/ui/sidebar.tsx | 10 +- 12 files changed, 265 insertions(+), 259 deletions(-) diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index 3a43091de9f..51eb05da52b 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -107,8 +107,8 @@ export const ChannelPane = React.memo(function ChannelPane({ isSending; return ( -
-
+
+
- - +
+ + +
{threadHeadMessage ? ( diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index 52366919e7c..bef9cea6698 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -364,91 +364,97 @@ export function ChannelScreen({ return ( <> - setIsMembersSidebarOpen((prev) => !prev)} - /> - ) : null - } - channelType={activeChannel?.channelType} - visibility={activeChannel?.visibility} - description={channelDescription} - statusBadge={headerStatusBadge} - title={activeChannelTitle} - /> +
+
+ + setIsMembersSidebarOpen((prev) => !prev) + } + /> + ) : null + } + channelType={activeChannel?.channelType} + visibility={activeChannel?.visibility} + description={channelDescription} + statusBadge={headerStatusBadge} + title={activeChannelTitle} + /> +
-
- {activeChannel ? ( - activeChannel.channelType === "forum" ? ( - }> - - +
+ {activeChannel ? ( + activeChannel.channelType === "forum" ? ( + }> + + + ) : ( + }> + 1} + openThreadHeadId={openThreadHeadId} + personaLookup={personaLookup} + profiles={messageProfiles} + targetMessageId={targetMessageId} + threadHeadMessage={openThreadHeadMessage} + threadMessages={threadMessages} + threadTypingPubkeys={threadTypingPubkeys} + threadTotalReplyCount={threadTotalReplyCount} + threadReplyTargetId={threadReplyTargetId} + threadReplyTargetMessage={threadReplyTargetMessage} + typingPubkeys={mainTypingPubkeys} + /> + + ) ) : ( - }> - 1} - openThreadHeadId={openThreadHeadId} - personaLookup={personaLookup} - profiles={messageProfiles} - targetMessageId={targetMessageId} - threadHeadMessage={openThreadHeadMessage} - threadMessages={threadMessages} - threadTypingPubkeys={threadTypingPubkeys} - threadTotalReplyCount={threadTotalReplyCount} - threadReplyTargetId={threadReplyTargetId} - threadReplyTargetMessage={threadReplyTargetMessage} - typingPubkeys={mainTypingPubkeys} - /> - - ) - ) : ( -
-

- Select a channel to view messages. -

-
- )} +
+

+ Select a channel to view messages. +

+
+ )} +
@@ -87,6 +89,7 @@ export function ChatHeader({

{title}

@@ -96,12 +99,6 @@ export function ChatHeader({
) : null}
-

- {description} -

{actions ?
{actions}
: null} diff --git a/desktop/src/features/messages/ui/MessageComposer.tsx b/desktop/src/features/messages/ui/MessageComposer.tsx index 7747b3cec03..c0cb9daa234 100644 --- a/desktop/src/features/messages/ui/MessageComposer.tsx +++ b/desktop/src/features/messages/ui/MessageComposer.tsx @@ -508,13 +508,13 @@ export function MessageComposer({ return (
-
+
{ diff --git a/desktop/src/features/messages/ui/MessageComposerToolbar.tsx b/desktop/src/features/messages/ui/MessageComposerToolbar.tsx index ef7758dc9b3..5de17361720 100644 --- a/desktop/src/features/messages/ui/MessageComposerToolbar.tsx +++ b/desktop/src/features/messages/ui/MessageComposerToolbar.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { AtSign, Paperclip, SendHorizontal } from "lucide-react"; +import { ArrowUp, AtSign, Paperclip } from "lucide-react"; import { Button } from "@/shared/ui/button"; import { ComposerEmojiPicker } from "./ComposerEmojiPicker"; @@ -67,14 +67,22 @@ export const MessageComposerToolbar = React.memo(
); diff --git a/desktop/src/features/messages/ui/MessageRow.tsx b/desktop/src/features/messages/ui/MessageRow.tsx index 880dcb3e441..edd21877642 100644 --- a/desktop/src/features/messages/ui/MessageRow.tsx +++ b/desktop/src/features/messages/ui/MessageRow.tsx @@ -161,7 +161,7 @@ export const MessageRow = React.memo(
@@ -191,81 +191,79 @@ export const MessageRow = React.memo( )}
-
-
- {message.pubkey ? ( - - + + ) : ( +

{message.author} - - - ) : ( -

- {message.author} -

- )} - {message.personaDisplayName && - message.personaDisplayName !== message.author ? ( - - {message.personaDisplayName} - - ) : message.role ? ( -

- {message.role} -

- ) : null} -
-
-
- -
-
- {message.pending ? ( -

- Sending + + )} + {message.personaDisplayName && + message.personaDisplayName !== message.author ? ( + + {message.personaDisplayName} + + ) : message.role ? ( +

+ {message.role}

) : null} - {message.edited ? ( -

- (edited) -

- ) : null} -
- {renderBody()} +
+ + {message.pending ? ( +

+ Sending +

+ ) : null} + {message.edited ? ( +

+ (edited) +

+ ) : null} + +
+
{renderBody()}
-
- {canGoBack ? ( - - ) : null} -
-

Thread

+
+
+ {canGoBack ? ( + + ) : null} +
+

Thread

+
+ + {feedbackNoticeMessage ? ( +

+ {feedbackNoticeMessage} +

+ ) : null} + + {feedbackErrorMessage ? ( +

+ {feedbackErrorMessage} +

+ ) : null} - - - {feedbackNoticeMessage ? ( -

- {feedbackNoticeMessage} -

- ) : null} - {feedbackErrorMessage ? ( -

- {feedbackErrorMessage} -

- ) : null} - -
-
-

- Type -

-

Built-in persona

-

- Preferred model -

-

- {persona.model ?? "Use app default"} -

-
-
-

- Preferred provider -

-

- {persona.provider ?? "Use app default"} + System prompt

+
+                  {persona.systemPrompt}
+                
- -
-

- System prompt -

-
-                {persona.systemPrompt}
-              
-
-
+ ) : null} diff --git a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx index 8ec47403f28..8c294340c64 100644 --- a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx +++ b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx @@ -38,7 +38,6 @@ import { } from "@/shared/ui/alert-dialog"; import { Button } from "@/shared/ui/button"; import { Input } from "@/shared/ui/input"; -import { Separator } from "@/shared/ui/separator"; import { Sheet, SheetContent, @@ -214,7 +213,7 @@ export function ChannelManagementSheet({ data-testid="channel-management-sheet" side="right" > - +
{channel.name} @@ -314,8 +313,6 @@ export function ChannelManagementSheet({

) : null} - - ) : null} @@ -330,8 +327,6 @@ export function ChannelManagementSheet({ /> - -
- -
- -
- -
- + Members People and bots in {channel.name}. diff --git a/desktop/src/features/messages/ui/DayDivider.tsx b/desktop/src/features/messages/ui/DayDivider.tsx index 93a03cf21a8..8ca39cb9450 100644 --- a/desktop/src/features/messages/ui/DayDivider.tsx +++ b/desktop/src/features/messages/ui/DayDivider.tsx @@ -4,15 +4,15 @@ export function DayDivider({ label }: { label: string }) { return (
- -

+ +

{label}

- +
); } diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx index 19b2dbbc2cf..a5673d73b10 100644 --- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx +++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx @@ -1,3 +1,4 @@ +import * as React from "react"; import { ArrowLeft, X } from "lucide-react"; import type { MainTimelineEntry } from "@/features/messages/lib/threadPanel"; @@ -75,12 +76,11 @@ export function MessageThreadPanel({ threadReplies, threadTypingPubkeys, }: MessageThreadPanelProps) { - if (!threadHead) { - return null; - } + const threadBodyRef = React.useRef(null); + const threadContentRef = React.useRef(null); const composerReplyTarget = - replyTargetMessage && replyTargetMessage.id !== threadHead.id + threadHead && replyTargetMessage && replyTargetMessage.id !== threadHead.id ? { author: replyTargetMessage.author, body: replyTargetMessage.body, @@ -88,9 +88,52 @@ export function MessageThreadPanel({ } : null; + const scrollThreadToBottom = React.useCallback(() => { + const threadBody = threadBodyRef.current; + if (!threadBody) { + return; + } + + threadBody.scrollTo({ + top: threadBody.scrollHeight, + behavior: "auto", + }); + }, []); + + React.useLayoutEffect(() => { + if (!threadHead) { + return; + } + scrollThreadToBottom(); + }, [scrollThreadToBottom, threadHead?.id]); + + React.useEffect(() => { + if (!threadHead) { + return; + } + + const threadContent = threadContentRef.current; + if (!threadContent || typeof ResizeObserver === "undefined") { + return; + } + + const observer = new ResizeObserver(() => { + scrollThreadToBottom(); + }); + + observer.observe(threadContent); + return () => { + observer.disconnect(); + }; + }, [scrollThreadToBottom, threadHead?.id]); + + if (!threadHead) { + return null; + } + return (
); diff --git a/desktop/src/features/messages/ui/MessageThreadSummaryRow.tsx b/desktop/src/features/messages/ui/MessageThreadSummaryRow.tsx index 4d8d0a2f987..cfe8ff34fd9 100644 --- a/desktop/src/features/messages/ui/MessageThreadSummaryRow.tsx +++ b/desktop/src/features/messages/ui/MessageThreadSummaryRow.tsx @@ -40,14 +40,15 @@ export function MessageThreadSummaryRow({ summary: TimelineThreadSummary; }) { const visibleDepth = Math.min(Math.max(depth, 0), 6); - const marginLeftPx = visibleDepth * 28; + const messageTextOffsetPx = 50; + const marginLeftPx = visibleDepth * 28 + messageTextOffsetPx; const depthGuideOffsets = Array.from( { length: visibleDepth }, (_, index) => 14 + index * 28, ); return ( -
+
{depthGuideOffsets.length > 0 ? (
onOpenThread(message)} diff --git a/desktop/src/features/messages/ui/MessageTimeline.tsx b/desktop/src/features/messages/ui/MessageTimeline.tsx index 0283821fe41..09e6524ccb8 100644 --- a/desktop/src/features/messages/ui/MessageTimeline.tsx +++ b/desktop/src/features/messages/ui/MessageTimeline.tsx @@ -10,7 +10,6 @@ import { TooltipProvider } from "@/shared/ui/tooltip"; import { TimelineSkeleton } from "./TimelineSkeleton"; import { TimelineMessageList } from "./TimelineMessageList"; import { useLoadOlderOnScroll } from "./useLoadOlderOnScroll"; -import { useStickyDayHeader } from "./useStickyDayHeader"; import { useTimelineScrollManager } from "./useTimelineScrollManager"; type MessageTimelineProps = { @@ -121,21 +120,9 @@ export const MessageTimeline = React.memo(function MessageTimeline({ sentinelRef: topSentinelRef, }); - const stickyDayLabel = useStickyDayHeader(scrollContainerRef); - return (
- {stickyDayLabel && !isAtBottom ? ( -
-

- {stickyDayLabel} -

-
- ) : null}
, -) { - const [label, setLabel] = React.useState(null); - - const update = React.useCallback(() => { - const container = scrollContainerRef.current; - if (!container) { - return; - } - - const dividers = container.querySelectorAll( - "[data-testid='message-timeline-day-divider']", - ); - if (dividers.length === 0) { - setLabel(null); - return; - } - - const containerTop = container.getBoundingClientRect().top; - const stickyTriggerTop = containerTop + STICKY_DAY_TRIGGER_OFFSET_PX; - - // Walk dividers from the end — the last one whose top has reached the - // floating label's visual zone is the "current" day. - let current: string | null = null; - for (let i = dividers.length - 1; i >= 0; i--) { - const rect = dividers[i].getBoundingClientRect(); - if (rect.top <= stickyTriggerTop) { - current = dividers[i].getAttribute("data-day-label"); - break; - } - } - - setLabel(current); - }, [scrollContainerRef]); - - React.useEffect(() => { - const container = scrollContainerRef.current; - if (!container) { - return; - } - - container.addEventListener("scroll", update, { passive: true }); - // Run once on mount so the header appears if already scrolled. - update(); - return () => { - container.removeEventListener("scroll", update); - }; - }, [scrollContainerRef, update]); - - return label; -} From 607c77d6caf3c01a4f0845d9651c60741293a3c7 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Mon, 4 May 2026 09:16:30 -0400 Subject: [PATCH 04/20] Polish message date divider Co-authored-by: Cursor --- desktop/src/features/messages/ui/DayDivider.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/desktop/src/features/messages/ui/DayDivider.tsx b/desktop/src/features/messages/ui/DayDivider.tsx index 053aed6dbf9..0032b2be823 100644 --- a/desktop/src/features/messages/ui/DayDivider.tsx +++ b/desktop/src/features/messages/ui/DayDivider.tsx @@ -1,18 +1,14 @@ -import { Separator } from "@/shared/ui/separator"; - export function DayDivider({ label }: { label: string }) { return (
- -

+

{label}

-
); } From bb905767dbe466e390c34b641d4f366c6b485512 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Mon, 4 May 2026 11:28:21 -0400 Subject: [PATCH 05/20] Fix thread reply pill behavior Co-authored-by: Cursor --- .../features/channels/ui/ChannelScreen.tsx | 18 +++++++++++++++++ .../channels/useChannelPaneHandlers.ts | 20 ++++++++++++++++--- .../messages/ui/MessageThreadPanel.tsx | 7 ++----- .../messages/ui/MessageThreadSummaryRow.tsx | 4 +++- .../messages/ui/TimelineMessageList.tsx | 1 + 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index 45ed6d56d83..c868230df07 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -265,6 +265,23 @@ export function ChannelScreen({ (messageId: string) => directReplyIdsByParentId.get(messageId)?.[0] ?? null, [directReplyIdsByParentId], ); + const getReplyDescendantIdsForMessage = React.useCallback( + (messageId: string) => { + const descendantIds: string[] = []; + const pendingIds = [...(directReplyIdsByParentId.get(messageId) ?? [])]; + + while (pendingIds.length > 0) { + const currentId = pendingIds.pop(); + if (!currentId) continue; + + descendantIds.push(currentId); + pendingIds.push(...(directReplyIdsByParentId.get(currentId) ?? [])); + } + + return descendantIds; + }, + [directReplyIdsByParentId], + ); const threadPanelData = React.useMemo( () => buildThreadPanelData( @@ -309,6 +326,7 @@ export function ChannelScreen({ editTargetId, expandedThreadReplyIds, getFirstReplyIdForMessage, + getReplyDescendantIdsForMessage, openThreadHeadId, sendMessageMutation, setExpandedThreadReplyIds, diff --git a/desktop/src/features/channels/useChannelPaneHandlers.ts b/desktop/src/features/channels/useChannelPaneHandlers.ts index 98751c63624..f1adf9eef2e 100644 --- a/desktop/src/features/channels/useChannelPaneHandlers.ts +++ b/desktop/src/features/channels/useChannelPaneHandlers.ts @@ -21,6 +21,7 @@ export function useChannelPaneHandlers({ editTargetId, expandedThreadReplyIds, getFirstReplyIdForMessage, + getReplyDescendantIdsForMessage, openThreadHeadId, sendMessageMutation, setExpandedThreadReplyIds, @@ -36,6 +37,7 @@ export function useChannelPaneHandlers({ editTargetId: string | null; expandedThreadReplyIds: ReadonlySet; getFirstReplyIdForMessage: (messageId: string) => string | null; + getReplyDescendantIdsForMessage: (messageId: string) => string[]; openThreadHeadId: string | null; sendMessageMutation: ReturnType; setExpandedThreadReplyIds: React.Dispatch>>; @@ -158,21 +160,33 @@ export function useChannelPaneHandlers({ const handleExpandThreadReplies = React.useCallback( (message: { id: string }) => { - const firstReplyId = getFirstReplyIdForMessage(message.id); - if (!expandedThreadReplyIdsRef.current.has(message.id)) { + if (expandedThreadReplyIdsRef.current.has(message.id)) { + const descendantIds = getReplyDescendantIdsForMessage(message.id); setExpandedThreadReplyIds((current) => { const next = new Set(current); - next.add(message.id); + next.delete(message.id); + for (const descendantId of descendantIds) { + next.delete(descendantId); + } return next; }); + return; } + const firstReplyId = getFirstReplyIdForMessage(message.id); + setExpandedThreadReplyIds((current) => { + const next = new Set(current); + next.add(message.id); + return next; + }); + if (firstReplyId) { setThreadScrollTargetId(firstReplyId); } }, [ getFirstReplyIdForMessage, + getReplyDescendantIdsForMessage, setExpandedThreadReplyIds, setThreadScrollTargetId, ], diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx index e592bbfee7e..a4fb0eb0b11 100644 --- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx +++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx @@ -220,10 +220,6 @@ export function MessageThreadPanel({ {threadReplies.length > 0 ? (
{threadReplies.map((entry, index) => { - const nextDepth = - threadReplies[index + 1]?.message.depth ?? -1; - const isExpanded = nextDepth > entry.message.depth; - return (
- {entry.summary && !isExpanded ? ( + {entry.summary ? ( void; summary: TimelineThreadSummary; }) { const visibleDepth = Math.min(Math.max(depth, 0), 6); - const messageTextOffsetPx = 50; + const messageTextOffsetPx = layoutVariant === "thread-reply" ? 8 : 50; const marginLeftPx = visibleDepth * 28 + messageTextOffsetPx; const depthGuideOffsets = Array.from( { length: visibleDepth }, diff --git a/desktop/src/features/messages/ui/TimelineMessageList.tsx b/desktop/src/features/messages/ui/TimelineMessageList.tsx index 09ae20d7827..043c9501122 100644 --- a/desktop/src/features/messages/ui/TimelineMessageList.tsx +++ b/desktop/src/features/messages/ui/TimelineMessageList.tsx @@ -104,6 +104,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({ profiles={profiles} /> Date: Mon, 4 May 2026 13:56:00 -0400 Subject: [PATCH 06/20] Update thread smoke coverage Co-authored-by: Cursor --- .../src/features/messages/ui/MessageThreadPanel.tsx | 2 +- desktop/tests/e2e/messaging.spec.ts | 13 ++++++++++++- desktop/tests/e2e/smoke.spec.ts | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx index a4fb0eb0b11..672fd77b315 100644 --- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx +++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx @@ -219,7 +219,7 @@ export function MessageThreadPanel({ > {threadReplies.length > 0 ? (
- {threadReplies.map((entry, index) => { + {threadReplies.map((entry) => { return (
(element as HTMLElement).clientHeight, ); From 11998b11e091288ca0fc7fa59e74b5b0ccc6e457 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Mon, 4 May 2026 18:48:15 -0400 Subject: [PATCH 07/20] Fix sticky date divider handoff Group messages by day so sticky date labels push each other out during scroll instead of overlapping. Co-authored-by: Cursor --- .../src/features/messages/ui/DayDivider.tsx | 2 +- .../messages/ui/TimelineMessageList.tsx | 32 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/desktop/src/features/messages/ui/DayDivider.tsx b/desktop/src/features/messages/ui/DayDivider.tsx index 0032b2be823..f7926801d7d 100644 --- a/desktop/src/features/messages/ui/DayDivider.tsx +++ b/desktop/src/features/messages/ui/DayDivider.tsx @@ -6,7 +6,7 @@ export function DayDivider({ label }: { label: string }) { data-testid="message-timeline-day-divider" data-day-label={label} > -

+

{label}

diff --git a/desktop/src/features/messages/ui/TimelineMessageList.tsx b/desktop/src/features/messages/ui/TimelineMessageList.tsx index 043c9501122..dd5b4629ecb 100644 --- a/desktop/src/features/messages/ui/TimelineMessageList.tsx +++ b/desktop/src/features/messages/ui/TimelineMessageList.tsx @@ -52,27 +52,32 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({ searchMatchingMessageIds, searchQuery, }: TimelineMessageListProps) { - const elements: React.ReactNode[] = []; const entries = React.useMemo( () => buildMainTimelineEntries(messages), [messages], ); + const dayGroups: Array<{ + key: string; + label: string; + elements: React.ReactNode[]; + }> = []; + let currentDayGroup: (typeof dayGroups)[number] | null = null; for (let i = 0; i < entries.length; i++) { const { message, summary } = entries[i]; const prev = i > 0 ? entries[i - 1]?.message : null; if (!prev || !isSameDay(prev.createdAt, message.createdAt)) { - elements.push( - , - ); + currentDayGroup = { + key: `day-${message.createdAt}`, + label: formatDayHeading(message.createdAt), + elements: [], + }; + dayGroups.push(currentDayGroup); } if (message.kind === KIND_SYSTEM_MESSAGE) { - elements.push( + currentDayGroup?.elements.push( , ); } else if (summary && onReply) { - elements.push( + currentDayGroup?.elements.push(
( +
+ + {group.elements} +
+ )); }); From e40d4e71e389e0e5b2d75092d1e9c8ebbad14dd2 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Mon, 4 May 2026 19:02:52 -0400 Subject: [PATCH 08/20] Align active agent pill with timeline text Render active agent process pills inline with the message timeline so they sit under the latest message text instead of the composer edge. Co-authored-by: Cursor --- .../features/channels/ui/BotActivityBar.tsx | 2 +- .../src/features/channels/ui/ChannelPane.tsx | 40 +++++++++++-------- .../features/messages/ui/MessageTimeline.tsx | 35 +++++++++------- 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/desktop/src/features/channels/ui/BotActivityBar.tsx b/desktop/src/features/channels/ui/BotActivityBar.tsx index c0179d0c767..070526a54f0 100644 --- a/desktop/src/features/channels/ui/BotActivityBar.tsx +++ b/desktop/src/features/channels/ui/BotActivityBar.tsx @@ -56,7 +56,7 @@ export function BotActivityBar({ return (
{visibleAgents.map((agent) => { diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index 4550a2bfa19..bee6f16867d 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -229,6 +229,8 @@ export const ChannelPane = React.memo(function ChannelPane({ activeChannel.archivedAt !== null || activeChannel.channelType === "forum" || isSending; + const hasTypingActivity = typingPubkeys.length > 0; + const hasBotActivity = botTypingPubkeys.length > 0; const selectedAgent = React.useMemo( () => @@ -260,6 +262,18 @@ export const ChannelPane = React.memo(function ChannelPane({ currentPubkey={currentPubkey} fetchOlder={fetchOlder} hasOlderMessages={hasOlderMessages} + inlineFooter={ + hasBotActivity ? ( +
+ +
+ ) : null + } isFetchingOlder={isFetchingOlder} personaLookup={personaLookup} profiles={profiles} @@ -287,6 +301,16 @@ export const ChannelPane = React.memo(function ChannelPane({ searchQuery={channelFind.query} targetMessageId={targetMessageId} /> + {hasTypingActivity ? ( +
+ +
+ ) : null} {isNonMemberView ? (
)} -
- -
- -
-
{threadHeadMessage ? ( diff --git a/desktop/src/features/messages/ui/MessageTimeline.tsx b/desktop/src/features/messages/ui/MessageTimeline.tsx index 09e6524ccb8..a6e53ac86a8 100644 --- a/desktop/src/features/messages/ui/MessageTimeline.tsx +++ b/desktop/src/features/messages/ui/MessageTimeline.tsx @@ -15,6 +15,7 @@ import { useTimelineScrollManager } from "./useTimelineScrollManager"; type MessageTimelineProps = { channelId?: string | null; messages: TimelineMessage[]; + inlineFooter?: React.ReactNode; isLoading?: boolean; emptyTitle?: string; emptyDescription?: string; @@ -47,6 +48,7 @@ type MessageTimelineProps = { export const MessageTimeline = React.memo(function MessageTimeline({ channelId, messages, + inlineFooter, isLoading = false, emptyTitle = "No messages yet", emptyDescription = "Send the first message to start the thread.", @@ -172,21 +174,24 @@ export const MessageTimeline = React.memo(function MessageTimeline({ ) : null} {!isLoading && messages.length > 0 ? ( - + <> + + {inlineFooter} + ) : null}
From 5a675de57a4116825b161f96a1e00f584e6a2c66 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Mon, 4 May 2026 20:05:37 -0400 Subject: [PATCH 09/20] Fix desktop Biome cleanup after merge Remove redundant fragments and apply formatting so the full desktop Biome check passes before review. Co-authored-by: Cursor --- .../channels/ui/ChannelManagementSheet.tsx | 338 +++++++++--------- .../messages/ui/MessageComposerToolbar.tsx | 8 +- desktop/tests/e2e/smoke.spec.ts | 4 +- 3 files changed, 170 insertions(+), 180 deletions(-) diff --git a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx index 8c294340c64..6685fb580c4 100644 --- a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx +++ b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx @@ -259,61 +259,59 @@ export function ChannelManagementSheet({ ) : null} {showAccessSection ? ( - <> -
-
- {canJoin ? ( - - ) : null} - - {canLeave ? ( - - ) : null} -
- {joinChannelMutation.error instanceof Error ? ( -

- {joinChannelMutation.error.message} -

+
+
+ {canJoin ? ( + ) : null} - {leaveChannelMutation.error instanceof Error ? ( -

- {leaveChannelMutation.error.message} -

+ + {canLeave ? ( + ) : null} -
- +
+ {joinChannelMutation.error instanceof Error ? ( +

+ {joinChannelMutation.error.message} +

+ ) : null} + {leaveChannelMutation.error instanceof Error ? ( +

+ {leaveChannelMutation.error.message} +

+ ) : null} + ) : null}
{resolvedChannel.channelType !== "dm" ? ( - <> -
-
- {isArchived ? ( - - ) : ( - - )} -
- {archiveChannelMutation.error instanceof Error ? ( -

- {archiveChannelMutation.error.message} -

- ) : null} - {unarchiveChannelMutation.error instanceof Error ? ( -

- {unarchiveChannelMutation.error.message} -

- ) : null} -
- +
+
+ {isArchived ? ( + + ) : ( + + )} +
+ {archiveChannelMutation.error instanceof Error ? ( +

+ {archiveChannelMutation.error.message} +

+ ) : null} + {unarchiveChannelMutation.error instanceof Error ? ( +

+ {unarchiveChannelMutation.error.message} +

+ ) : null} +
) : null} {isOwner && resolvedChannel.channelType !== "dm" ? ( - <> -
+ - - - - - - - Delete channel? - - Delete {resolvedChannel.name} from the workspace list. - This action cannot be undone. - - - {deleteChannelMutation.error instanceof Error ? ( -

- {deleteChannelMutation.error.message} -

- ) : null} - - - - - - - - -
-
-
- + + + + + + Delete channel? + + Delete {resolvedChannel.name} from the workspace list. + This action cannot be undone. + + + {deleteChannelMutation.error instanceof Error ? ( +

+ {deleteChannelMutation.error.message} +

+ ) : null} + + + + + + + + +
+ +
) : null}
diff --git a/desktop/src/features/messages/ui/MessageComposerToolbar.tsx b/desktop/src/features/messages/ui/MessageComposerToolbar.tsx index c0578a655b5..13660d8d328 100644 --- a/desktop/src/features/messages/ui/MessageComposerToolbar.tsx +++ b/desktop/src/features/messages/ui/MessageComposerToolbar.tsx @@ -1,13 +1,7 @@ import * as React from "react"; import type { Editor } from "@tiptap/react"; import { AnimatePresence, motion } from "motion/react"; -import { - ALargeSmall, - ArrowUp, - AtSign, - Paperclip, - X, -} from "lucide-react"; +import { ALargeSmall, ArrowUp, AtSign, Paperclip, X } from "lucide-react"; import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; diff --git a/desktop/tests/e2e/smoke.spec.ts b/desktop/tests/e2e/smoke.spec.ts index 6876f8a2309..99251540fa3 100644 --- a/desktop/tests/e2e/smoke.spec.ts +++ b/desktop/tests/e2e/smoke.spec.ts @@ -361,7 +361,9 @@ test("supports multiline drafts with Ctrl+Enter and sends with Enter", async ({ await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); - await expect(page.getByRole("button", { name: "Send message" })).toBeVisible(); + await expect( + page.getByRole("button", { name: "Send message" }), + ).toBeVisible(); const initialInputHeight = await input.evaluate( (element) => (element as HTMLElement).clientHeight, ); From a7947767a11564e0de4db1d46440564ecc2963ea Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Mon, 4 May 2026 22:02:41 -0400 Subject: [PATCH 10/20] Align system message reactions Co-authored-by: Cursor --- .../features/messages/ui/MessageReactions.tsx | 4 +- .../features/messages/ui/SystemMessageRow.tsx | 37 +++++++++++-------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/desktop/src/features/messages/ui/MessageReactions.tsx b/desktop/src/features/messages/ui/MessageReactions.tsx index b5ea21e7e66..4e26398446c 100644 --- a/desktop/src/features/messages/ui/MessageReactions.tsx +++ b/desktop/src/features/messages/ui/MessageReactions.tsx @@ -49,19 +49,21 @@ export function MessageReactions({ canToggle, pending, onSelect, + className, }: { messageId: string; reactions: TimelineReaction[]; canToggle: boolean; pending: boolean; onSelect: (emoji: string) => void; + className?: string; }) { if (reactions.length === 0) { return null; } return ( -
+
{reactions.map((reaction) => ( -
+
-

{description}

+
+

{description}

+
+ { + void handleReactionSelect(emoji); + }} + /> + {reactionErrorMessage ? ( +

+ {reactionErrorMessage} +

+ ) : null} +
+
@@ -344,20 +363,6 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({
- { - void handleReactionSelect(emoji); - }} - /> - {reactionErrorMessage ? ( -

- {reactionErrorMessage} -

- ) : null}
); }); From 1e6b57916193e6e1a5e51de5137a51b089d7bdd9 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Mon, 4 May 2026 22:58:41 -0400 Subject: [PATCH 11/20] Refine message timeline spacing Co-authored-by: Cursor --- .../src/features/channels/ui/ChannelPane.tsx | 24 +- .../src/features/messages/ui/MessageRow.tsx | 23 +- .../messages/ui/MessageThreadSummaryRow.tsx | 4 +- .../features/messages/ui/MessageTimeline.tsx | 3 - .../features/messages/ui/SystemMessageRow.tsx | 229 +++++++----------- .../messages/ui/TimelineMessageList.tsx | 2 +- 6 files changed, 112 insertions(+), 173 deletions(-) diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index bee6f16867d..bacec7079ea 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -262,18 +262,6 @@ export const ChannelPane = React.memo(function ChannelPane({ currentPubkey={currentPubkey} fetchOlder={fetchOlder} hasOlderMessages={hasOlderMessages} - inlineFooter={ - hasBotActivity ? ( -
- -
- ) : null - } isFetchingOlder={isFetchingOlder} personaLookup={personaLookup} profiles={profiles} @@ -311,6 +299,18 @@ export const ChannelPane = React.memo(function ChannelPane({ />
) : null} + {hasBotActivity ? ( +
+
+ +
+
+ ) : null} {isNonMemberView ? (
+ {message.author} ) : ( -

+

{message.author}

); @@ -255,8 +254,8 @@ export const MessageRow = React.memo(
@@ -112,7 +122,11 @@ export function BotActivityBar({ key={agent.pubkey} onClick={() => onOpenAgentSession(agent.pubkey)} > - + {agent.name} diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index bacec7079ea..dc3231ed283 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -5,6 +5,7 @@ import { MessageComposer } from "@/features/messages/ui/MessageComposer"; import { MessageThreadPanel } from "@/features/messages/ui/MessageThreadPanel"; import { MessageTimeline } from "@/features/messages/ui/MessageTimeline"; import { TypingIndicatorRow } from "@/features/messages/ui/TypingIndicatorRow"; +import type { TypingIndicatorEntry } from "@/features/messages/useChannelTyping"; import { UserProfilePanel } from "@/features/profile/ui/UserProfilePanel"; import { ChannelFindBar } from "@/features/search/ui/ChannelFindBar"; import { AgentSessionThreadPanel } from "@/features/channels/ui/AgentSessionThreadPanel"; @@ -50,10 +51,49 @@ function getInitialThreadPanelWidth(): number { } } +function messageMentionsPubkey(message: TimelineMessage, pubkey: string) { + const normalizedPubkey = pubkey.toLowerCase(); + if (message.pubkey?.toLowerCase() === normalizedPubkey) { + return false; + } + + return ( + message.tags?.some( + (tag) => tag[0] === "p" && tag[1]?.toLowerCase() === normalizedPubkey, + ) ?? false + ); +} + +function findLatestMentionedMessageId( + messages: TimelineMessage[], + pubkey: string, +) { + for (let index = messages.length - 1; index >= 0; index -= 1) { + const message = messages[index]; + if (message && messageMentionsPubkey(message, pubkey)) { + return message.id; + } + } + + return null; +} + +function addPubkey( + map: Map, + messageId: string, + pubkey: string, +) { + const current = map.get(messageId) ?? []; + if (!current.some((value) => value.toLowerCase() === pubkey.toLowerCase())) { + current.push(pubkey); + } + map.set(messageId, current); +} + type ChannelPaneProps = { activeChannel: Channel | null; agentSessionAgents: ManagedAgent[]; - botTypingPubkeys: string[]; + botTypingEntries: TypingIndicatorEntry[]; channelFind: ReturnType; currentPubkey?: string; editTarget?: { @@ -118,7 +158,7 @@ type ChannelPaneProps = { export const ChannelPane = React.memo(function ChannelPane({ activeChannel, agentSessionAgents, - botTypingPubkeys, + botTypingEntries, channelFind, currentPubkey, editTarget = null, @@ -230,7 +270,53 @@ export const ChannelPane = React.memo(function ChannelPane({ activeChannel.channelType === "forum" || isSending; const hasTypingActivity = typingPubkeys.length > 0; - const hasBotActivity = botTypingPubkeys.length > 0; + const { messageActivityFooters, unanchoredBotTypingPubkeys } = + React.useMemo(() => { + const botPubkeysByMessageId = new Map(); + const unanchoredPubkeys: string[] = []; + for (const entry of botTypingEntries) { + const messageId = + entry.threadHeadId ?? + findLatestMentionedMessageId(messages, entry.pubkey); + if (messageId) { + addPubkey(botPubkeysByMessageId, messageId, entry.pubkey); + } else if ( + !unanchoredPubkeys.some( + (pubkey) => pubkey.toLowerCase() === entry.pubkey.toLowerCase(), + ) + ) { + unanchoredPubkeys.push(entry.pubkey); + } + } + + const footers: Record = {}; + for (const [messageId, pubkeys] of botPubkeysByMessageId) { + footers[messageId] = ( +
+ +
+ ); + } + + return { + messageActivityFooters: footers, + unanchoredBotTypingPubkeys: unanchoredPubkeys, + }; + }, [ + agentSessionAgents, + botTypingEntries, + messages, + onOpenAgentSession, + openAgentSessionPubkey, + profiles, + ]); + const hasBotActivity = unanchoredBotTypingPubkeys.length > 0; const selectedAgent = React.useMemo( () => @@ -263,6 +349,7 @@ export const ChannelPane = React.memo(function ChannelPane({ fetchOlder={fetchOlder} hasOlderMessages={hasOlderMessages} isFetchingOlder={isFetchingOlder} + messageFooters={messageActivityFooters} personaLookup={personaLookup} profiles={profiles} emptyDescription={ @@ -306,7 +393,8 @@ export const ChannelPane = React.memo(function ChannelPane({ agents={agentSessionAgents} onOpenAgentSession={onOpenAgentSession} openAgentSessionPubkey={openAgentSessionPubkey} - typingBotPubkeys={botTypingPubkeys} + profiles={profiles} + typingBotPubkeys={unanchoredBotTypingPubkeys} />
@@ -401,9 +489,9 @@ export const ChannelPane = React.memo(function ChannelPane({ agent={selectedAgent} canResetWidth={canResetThreadPanelWidth} channel={activeChannel} - isWorking={botTypingPubkeys.some( - (pubkey) => - pubkey.toLowerCase() === selectedAgent.pubkey.toLowerCase(), + isWorking={botTypingEntries.some( + (entry) => + entry.pubkey.toLowerCase() === selectedAgent.pubkey.toLowerCase(), )} onClose={onCloseAgentSession} onResetWidth={handleThreadPanelWidthReset} diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index 4b0f3d6e040..b85a826ce47 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -34,7 +34,10 @@ import { import { buildThreadPanelData } from "@/features/messages/lib/threadPanel"; import { useFetchOlderMessages } from "@/features/messages/useFetchOlderMessages"; import { useLoadMissingAncestors } from "@/features/messages/useLoadMissingAncestors"; -import { useChannelTyping } from "@/features/messages/useChannelTyping"; +import { + type TypingIndicatorEntry, + useChannelTyping, +} from "@/features/messages/useChannelTyping"; import { useUsersBatchQuery } from "@/features/profile/hooks"; import { mergeCurrentProfileIntoLookup } from "@/features/profile/lib/identity"; import type { @@ -142,13 +145,6 @@ export function ChannelScreen({ currentPubkey, latestMessageEvent, ); - const mainTypingPubkeys = React.useMemo( - () => - typingEntries - .filter((entry) => entry.threadHeadId === null) - .map((entry) => entry.pubkey), - [typingEntries], - ); const threadTypingPubkeys = React.useMemo( () => typingEntries @@ -170,21 +166,28 @@ export function ChannelScreen({ }); const managedAgentsQuery = useManagedAgentsQuery(); useManagedAgentObserverBridge(managedAgentsQuery.data ?? []); - const { humanTypingPubkeys, botTypingPubkeys } = React.useMemo(() => { + const { botTypingEntries, humanTypingPubkeys } = React.useMemo<{ + botTypingEntries: TypingIndicatorEntry[]; + humanTypingPubkeys: string[]; + }>(() => { const localAgentSet = new Set( (managedAgentsQuery.data ?? []) .filter((agent) => agent.backend.type === "local") .map((agent) => agent.pubkey.toLowerCase()), ); + const channelTypingEntries = typingEntries.filter( + (entry) => entry.threadHeadId === null, + ); + const agentTypingEntries = typingEntries.filter((entry) => + localAgentSet.has(entry.pubkey.toLowerCase()), + ); return { - humanTypingPubkeys: mainTypingPubkeys.filter( - (pk) => !localAgentSet.has(pk.toLowerCase()), - ), - botTypingPubkeys: mainTypingPubkeys.filter((pk) => - localAgentSet.has(pk.toLowerCase()), - ), + botTypingEntries: agentTypingEntries, + humanTypingPubkeys: channelTypingEntries + .filter((entry) => !localAgentSet.has(entry.pubkey.toLowerCase())) + .map((entry) => entry.pubkey), }; - }, [mainTypingPubkeys, managedAgentsQuery.data]); + }, [managedAgentsQuery.data, typingEntries]); const messageProfiles = React.useMemo(() => { const base = mergeCurrentProfileIntoLookup( @@ -468,7 +471,7 @@ export function ChannelScreen({ +
{reactions.map((reaction) => ( Promise; hasOlderMessages?: boolean; isFetchingOlder?: boolean; + messageFooters?: Record; /** Map from lowercase pubkey → persona display name for bot members. */ personaLookup?: Map; profiles?: UserProfileLookup; @@ -55,6 +56,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({ fetchOlder, hasOlderMessages = true, isFetchingOlder = false, + messageFooters, personaLookup, profiles, onDelete, @@ -172,23 +174,22 @@ export const MessageTimeline = React.memo(function MessageTimeline({ ) : null} {!isLoading && messages.length > 0 ? ( - <> - - + ) : null}
diff --git a/desktop/src/features/messages/ui/TimelineMessageList.tsx b/desktop/src/features/messages/ui/TimelineMessageList.tsx index 17430c44eaa..c322d3ae70d 100644 --- a/desktop/src/features/messages/ui/TimelineMessageList.tsx +++ b/desktop/src/features/messages/ui/TimelineMessageList.tsx @@ -17,6 +17,7 @@ type TimelineMessageListProps = { activeReplyTargetId?: string | null; currentPubkey?: string; highlightedMessageId?: string | null; + messageFooters?: Record; messages: TimelineMessage[]; onDelete?: (message: TimelineMessage) => void; onEdit?: (message: TimelineMessage) => void; @@ -41,6 +42,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({ activeReplyTargetId = null, currentPubkey, highlightedMessageId = null, + messageFooters, messages, onDelete, onEdit, @@ -77,17 +79,21 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({ } if (message.kind === KIND_SYSTEM_MESSAGE) { + const footer = messageFooters?.[message.id] ?? null; currentDayGroup?.elements.push( - , +
+ + {footer} +
, ); } else if (summary && onReply) { + const footer = messageFooters?.[message.id] ?? null; currentDayGroup?.elements.push(
+ {footer}
, ); } else { const isSearchMatch = searchMatchingMessageIds?.has(message.id) ?? false; const isSearchActive = message.id === searchActiveMessageId; + const footer = messageFooters?.[message.id] ?? null; currentDayGroup?.elements.push( - , +
+ + {footer} +
, ); } } From 96035eb4a14eebe4f77f763aa81a5cdab9457f97 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Wed, 6 May 2026 13:10:14 -0400 Subject: [PATCH 13/20] Move agent activity into composer Place working-agent selection beside the send button so activity stays close to message composition, and tighten chat paragraph spacing. Co-authored-by: Cursor --- .../features/channels/ui/BotActivityBar.tsx | 249 +++++++++--------- .../src/features/channels/ui/ChannelPane.tsx | 127 ++------- .../features/messages/ui/MessageComposer.tsx | 3 + desktop/src/shared/ui/markdown.tsx | 2 +- 4 files changed, 153 insertions(+), 228 deletions(-) diff --git a/desktop/src/features/channels/ui/BotActivityBar.tsx b/desktop/src/features/channels/ui/BotActivityBar.tsx index 6d49c0435ea..8d023dd9619 100644 --- a/desktop/src/features/channels/ui/BotActivityBar.tsx +++ b/desktop/src/features/channels/ui/BotActivityBar.tsx @@ -1,171 +1,166 @@ +import * as React from "react"; import { Loader2 } from "lucide-react"; import type { UserProfileLookup } from "@/features/profile/lib/identity"; import type { ManagedAgent } from "@/shared/api/types"; import { cn } from "@/shared/lib/cn"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, -} from "@/shared/ui/dropdown-menu"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; +import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; import { UserAvatar } from "@/shared/ui/UserAvatar"; +export type BotActivityAgent = Pick; + type BotActivityBarProps = { - agents: ManagedAgent[]; + agents: BotActivityAgent[]; onOpenAgentSession: (pubkey: string) => void; openAgentSessionPubkey: string | null; profiles?: UserProfileLookup; typingBotPubkeys: string[]; }; -const COMPACT_THRESHOLD = 4; -const OVERFLOW_THRESHOLD = 6; -const MAX_VISIBLE_WITH_OVERFLOW = 5; +const HOVER_OPEN_DELAY_MS = 150; +const HOVER_CLOSE_DELAY_MS = 180; -/** - * Compact right-aligned row of clickable bot pills. - * Only renders pills for bots that are currently typing (actively working). - */ -export function BotActivityBar({ +export function BotActivityComposerAction({ agents, onOpenAgentSession, openAgentSessionPubkey, profiles, typingBotPubkeys, }: BotActivityBarProps) { - if (typingBotPubkeys.length === 0) { - return null; - } - - const typingSet = new Set( - typingBotPubkeys.map((pubkey) => pubkey.toLowerCase()), + const [open, setOpen] = React.useState(false); + const hoverTimerRef = React.useRef | null>( + null, ); - const typingAgents = agents.filter((agent) => - typingSet.has(agent.pubkey.toLowerCase()), - ); + const typingAgents = React.useMemo(() => { + const typingSet = new Set( + typingBotPubkeys.map((pubkey) => pubkey.toLowerCase()), + ); + + return agents.filter((agent) => typingSet.has(agent.pubkey.toLowerCase())); + }, [agents, typingBotPubkeys]); + + const clearHoverTimer = React.useCallback(() => { + if (hoverTimerRef.current !== null) { + clearTimeout(hoverTimerRef.current); + hoverTimerRef.current = null; + } + }, []); + + const openWithDelay = React.useCallback(() => { + clearHoverTimer(); + hoverTimerRef.current = setTimeout(() => { + setOpen(true); + }, HOVER_OPEN_DELAY_MS); + }, [clearHoverTimer]); + + const closeWithDelay = React.useCallback(() => { + clearHoverTimer(); + hoverTimerRef.current = setTimeout(() => { + setOpen(false); + }, HOVER_CLOSE_DELAY_MS); + }, [clearHoverTimer]); + + const keepOpen = React.useCallback(() => { + clearHoverTimer(); + }, [clearHoverTimer]); + + React.useEffect(() => { + return () => clearHoverTimer(); + }, [clearHoverTimer]); if (typingAgents.length === 0) { return null; } - const { hiddenAgents, visibleAgents } = splitVisibleAgents( - typingAgents, - openAgentSessionPubkey, - ); - const isCompact = typingAgents.length >= COMPACT_THRESHOLD; - const agentAvatarUrl = (agent: ManagedAgent) => + const agentAvatarUrl = (agent: BotActivityAgent) => profiles?.[agent.pubkey.toLowerCase()]?.avatarUrl ?? null; + const selectedPubkey = openAgentSessionPubkey?.toLowerCase() ?? null; + const triggerLabel = + typingAgents.length === 1 + ? `${typingAgents[0]?.name ?? "Agent"} is working` + : `${typingAgents.length} agents working`; return ( -
- {visibleAgents.map((agent) => { - const isSelected = - openAgentSessionPubkey?.toLowerCase() === agent.pubkey.toLowerCase(); - return ( - - + + + + + event.preventDefault()} + side="top" + sideOffset={8} + > +
+ Agents working +
+
+ {typingAgents.map((agent) => { + const isSelected = selectedPubkey === agent.pubkey.toLowerCase(); + + return ( - - - {agent.name} is working — click to view activity - - - ); - })} - - {hiddenAgents.length > 0 ? ( - - - - - - - More agents working - - {hiddenAgents.map((agent) => ( - onOpenAgentSession(agent.pubkey)} + onClick={() => { + clearHoverTimer(); + setOpen(false); + onOpenAgentSession(agent.pubkey); + }} + type="button" > {agent.name} - - - ))} - - - ) : null} -
- ); -} - -function splitVisibleAgents( - typingAgents: ManagedAgent[], - openAgentSessionPubkey: string | null, -): { visibleAgents: ManagedAgent[]; hiddenAgents: ManagedAgent[] } { - if (typingAgents.length < OVERFLOW_THRESHOLD) { - return { visibleAgents: typingAgents, hiddenAgents: [] }; - } - - const selectedAgent = openAgentSessionPubkey - ? typingAgents.find( - (agent) => - agent.pubkey.toLowerCase() === openAgentSessionPubkey.toLowerCase(), - ) - : null; - - const visibleAgents = typingAgents.slice(0, MAX_VISIBLE_WITH_OVERFLOW); - - if ( - selectedAgent && - !visibleAgents.some((agent) => agent.pubkey === selectedAgent.pubkey) - ) { - visibleAgents[visibleAgents.length - 1] = selectedAgent; - } - - const visibleSet = new Set(visibleAgents.map((agent) => agent.pubkey)); - const hiddenAgents = typingAgents.filter( - (agent) => !visibleSet.has(agent.pubkey), + + + ); + })} +
+ + ); - - return { visibleAgents, hiddenAgents }; } diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index dc3231ed283..fb513c669a4 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -9,7 +9,10 @@ import type { TypingIndicatorEntry } from "@/features/messages/useChannelTyping" import { UserProfilePanel } from "@/features/profile/ui/UserProfilePanel"; import { ChannelFindBar } from "@/features/search/ui/ChannelFindBar"; import { AgentSessionThreadPanel } from "@/features/channels/ui/AgentSessionThreadPanel"; -import { BotActivityBar } from "@/features/channels/ui/BotActivityBar"; +import { + BotActivityComposerAction, + type BotActivityAgent, +} from "@/features/channels/ui/BotActivityBar"; import { Button } from "@/shared/ui/button"; import type { useChannelFind } from "@/features/search/useChannelFind"; import type { MainTimelineEntry } from "@/features/messages/lib/threadPanel"; @@ -51,47 +54,9 @@ function getInitialThreadPanelWidth(): number { } } -function messageMentionsPubkey(message: TimelineMessage, pubkey: string) { - const normalizedPubkey = pubkey.toLowerCase(); - if (message.pubkey?.toLowerCase() === normalizedPubkey) { - return false; - } - - return ( - message.tags?.some( - (tag) => tag[0] === "p" && tag[1]?.toLowerCase() === normalizedPubkey, - ) ?? false - ); -} - -function findLatestMentionedMessageId( - messages: TimelineMessage[], - pubkey: string, -) { - for (let index = messages.length - 1; index >= 0; index -= 1) { - const message = messages[index]; - if (message && messageMentionsPubkey(message, pubkey)) { - return message.id; - } - } - - return null; -} - -function addPubkey( - map: Map, - messageId: string, - pubkey: string, -) { - const current = map.get(messageId) ?? []; - if (!current.some((value) => value.toLowerCase() === pubkey.toLowerCase())) { - current.push(pubkey); - } - map.set(messageId, current); -} - type ChannelPaneProps = { activeChannel: Channel | null; + activityAgents: BotActivityAgent[]; agentSessionAgents: ManagedAgent[]; botTypingEntries: TypingIndicatorEntry[]; channelFind: ReturnType; @@ -157,6 +122,7 @@ type ChannelPaneProps = { export const ChannelPane = React.memo(function ChannelPane({ activeChannel, + activityAgents, agentSessionAgents, botTypingEntries, channelFind, @@ -270,53 +236,19 @@ export const ChannelPane = React.memo(function ChannelPane({ activeChannel.channelType === "forum" || isSending; const hasTypingActivity = typingPubkeys.length > 0; - const { messageActivityFooters, unanchoredBotTypingPubkeys } = - React.useMemo(() => { - const botPubkeysByMessageId = new Map(); - const unanchoredPubkeys: string[] = []; - for (const entry of botTypingEntries) { - const messageId = - entry.threadHeadId ?? - findLatestMentionedMessageId(messages, entry.pubkey); - if (messageId) { - addPubkey(botPubkeysByMessageId, messageId, entry.pubkey); - } else if ( - !unanchoredPubkeys.some( - (pubkey) => pubkey.toLowerCase() === entry.pubkey.toLowerCase(), - ) - ) { - unanchoredPubkeys.push(entry.pubkey); - } + const composerBotTypingPubkeys = React.useMemo(() => { + const pubkeys: string[] = []; + for (const entry of botTypingEntries) { + if ( + !pubkeys.some( + (pubkey) => pubkey.toLowerCase() === entry.pubkey.toLowerCase(), + ) + ) { + pubkeys.push(entry.pubkey); } - - const footers: Record = {}; - for (const [messageId, pubkeys] of botPubkeysByMessageId) { - footers[messageId] = ( -
- -
- ); - } - - return { - messageActivityFooters: footers, - unanchoredBotTypingPubkeys: unanchoredPubkeys, - }; - }, [ - agentSessionAgents, - botTypingEntries, - messages, - onOpenAgentSession, - openAgentSessionPubkey, - profiles, - ]); - const hasBotActivity = unanchoredBotTypingPubkeys.length > 0; + } + return pubkeys; + }, [botTypingEntries]); const selectedAgent = React.useMemo( () => @@ -349,7 +281,6 @@ export const ChannelPane = React.memo(function ChannelPane({ fetchOlder={fetchOlder} hasOlderMessages={hasOlderMessages} isFetchingOlder={isFetchingOlder} - messageFooters={messageActivityFooters} personaLookup={personaLookup} profiles={profiles} emptyDescription={ @@ -386,19 +317,6 @@ export const ChannelPane = React.memo(function ChannelPane({ />
) : null} - {hasBotActivity ? ( -
-
- -
-
- ) : null} {isNonMemberView ? (
+ } placeholder={ activeChannel?.archivedAt ? "Archived channels are read-only." diff --git a/desktop/src/features/messages/ui/MessageComposer.tsx b/desktop/src/features/messages/ui/MessageComposer.tsx index 1c7521b3c63..2f7e78421eb 100644 --- a/desktop/src/features/messages/ui/MessageComposer.tsx +++ b/desktop/src/features/messages/ui/MessageComposer.tsx @@ -58,6 +58,7 @@ type MessageComposerProps = { id: string; } | null; showTopBorder?: boolean; + toolbarExtraActions?: React.ReactNode; typingParentEventId?: string | null; typingRootEventId?: string | null; }; @@ -77,6 +78,7 @@ export function MessageComposer({ profiles, replyTarget = null, showTopBorder = false, + toolbarExtraActions, typingParentEventId = null, typingRootEventId = null, }: MessageComposerProps) { @@ -663,6 +665,7 @@ export function MessageComposer({ *:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*]:my-1" + ? "max-w-none break-words text-sm leading-5 text-foreground/90 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*]:my-0.5" : compact ? "max-w-none break-words text-[15px] leading-6 text-foreground/90 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*]:my-1.5" : "max-w-none break-words text-sm leading-7 text-foreground/90 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*]:my-3", From 4433e47038d4e3b5a66e8b5275f54d4bfe626ad3 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Wed, 6 May 2026 14:05:05 -0400 Subject: [PATCH 14/20] Fix composer activity type fallback Keep ChannelPane compatible with callers that only pass session agents while preserving the composer activity selector. Co-authored-by: Cursor --- desktop/src/features/channels/ui/ChannelPane.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index fb513c669a4..c72de494c42 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -56,7 +56,7 @@ function getInitialThreadPanelWidth(): number { type ChannelPaneProps = { activeChannel: Channel | null; - activityAgents: BotActivityAgent[]; + activityAgents?: BotActivityAgent[]; agentSessionAgents: ManagedAgent[]; botTypingEntries: TypingIndicatorEntry[]; channelFind: ReturnType; @@ -122,8 +122,8 @@ type ChannelPaneProps = { export const ChannelPane = React.memo(function ChannelPane({ activeChannel, - activityAgents, agentSessionAgents, + activityAgents = agentSessionAgents, botTypingEntries, channelFind, currentPubkey, From 4188f3132667e72785a022ef92cc32dc09c0ac8c Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Wed, 6 May 2026 14:19:44 -0400 Subject: [PATCH 15/20] Fix desktop header and popover smoke checks Restore channel descriptions in the header and make the profile popover smoke test target the profile popover explicitly. Co-authored-by: Cursor --- desktop/src/features/chat/ui/ChatHeader.tsx | 8 ++++++++ desktop/src/features/profile/ui/UserProfilePopover.tsx | 3 +++ desktop/tests/e2e/mentions.spec.ts | 9 +++------ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/desktop/src/features/chat/ui/ChatHeader.tsx b/desktop/src/features/chat/ui/ChatHeader.tsx index 102569732c6..fb343b77005 100644 --- a/desktop/src/features/chat/ui/ChatHeader.tsx +++ b/desktop/src/features/chat/ui/ChatHeader.tsx @@ -99,6 +99,14 @@ export function ChatHeader({
) : null}
+ {trimmedDescription ? ( +

+ {trimmedDescription} +

+ ) : null}
{actions ?
{actions}
: null} diff --git a/desktop/src/features/profile/ui/UserProfilePopover.tsx b/desktop/src/features/profile/ui/UserProfilePopover.tsx index a2ec0555b1c..632d94fbcd6 100644 --- a/desktop/src/features/profile/ui/UserProfilePopover.tsx +++ b/desktop/src/features/profile/ui/UserProfilePopover.tsx @@ -121,6 +121,7 @@ export function UserProfilePopover({ clearHoverTimer(); if (openProfilePanel) { event.preventDefault(); + event.stopPropagation(); setOpen(false); openProfilePanel(pubkey); } @@ -143,6 +144,7 @@ export function UserProfilePopover({ onKeyDown={(e) => { if ((e.key === "Enter" || e.key === " ") && openProfilePanel) { e.preventDefault(); + e.stopPropagation(); clearHoverTimer(); setOpen(false); openProfilePanel(pubkey); @@ -158,6 +160,7 @@ export function UserProfilePopover({ Date: Wed, 6 May 2026 15:38:51 -0400 Subject: [PATCH 16/20] Prevent profile popover from reopening on panel click Use the popover as a hover anchor so clicking an avatar opens the profile panel without toggling the hover card back open. Co-authored-by: Cursor --- desktop/src/features/profile/ui/UserProfilePopover.tsx | 6 +++--- desktop/tests/e2e/mentions.spec.ts | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/desktop/src/features/profile/ui/UserProfilePopover.tsx b/desktop/src/features/profile/ui/UserProfilePopover.tsx index 632d94fbcd6..30bcf36723d 100644 --- a/desktop/src/features/profile/ui/UserProfilePopover.tsx +++ b/desktop/src/features/profile/ui/UserProfilePopover.tsx @@ -13,7 +13,7 @@ import { rewriteRelayUrl } from "@/shared/lib/mediaUrl"; import { useAgentSession } from "@/shared/context/AgentSessionContext"; import { useProfilePanel } from "@/shared/context/ProfilePanelContext"; -import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; +import { Popover, PopoverAnchor, PopoverContent } from "@/shared/ui/popover"; import { BotIdenticon } from "@/features/messages/ui/BotIdenticon"; type UserProfilePopoverProps = { @@ -135,7 +135,7 @@ export function UserProfilePopover({ return ( - + {/* biome-ignore lint/a11y/useSemanticElements: wrapper div for hover/click behavior */}
{children}
-
+ Date: Wed, 6 May 2026 19:12:07 -0400 Subject: [PATCH 17/20] Update thread composer activity and header hover copy Show active agent controls in the thread composer and keep channel descriptions available as title hover text without rendering a second header line. Co-authored-by: Cursor --- .../src/features/channels/ui/ChannelPane.tsx | 30 +++++++++++++++++++ desktop/src/features/chat/ui/ChatHeader.tsx | 8 ----- .../messages/ui/MessageThreadPanel.tsx | 15 ++++++---- desktop/tests/e2e/integration.spec.ts | 8 +++-- 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index c72de494c42..7f8b50f2b42 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -249,6 +249,27 @@ export const ChannelPane = React.memo(function ChannelPane({ } return pubkeys; }, [botTypingEntries]); + const threadComposerBotTypingPubkeys = React.useMemo(() => { + if (!openThreadHeadId) { + return []; + } + + const pubkeys: string[] = []; + for (const entry of botTypingEntries) { + if (entry.threadHeadId !== openThreadHeadId) { + continue; + } + + if ( + !pubkeys.some( + (pubkey) => pubkey.toLowerCase() === entry.pubkey.toLowerCase(), + ) + ) { + pubkeys.push(entry.pubkey); + } + } + return pubkeys; + }, [botTypingEntries, openThreadHeadId]); const selectedAgent = React.useMemo( () => @@ -410,6 +431,15 @@ export const ChannelPane = React.memo(function ChannelPane({ widthPx={threadPanelWidthPx} threadReplies={threadMessages} threadTypingPubkeys={threadTypingPubkeys} + toolbarExtraActions={ + + } /> ) : activeChannel && selectedAgent ? ( ) : null}
- {trimmedDescription ? ( -

- {trimmedDescription} -

- ) : null}
{actions ?
{actions}
: null} diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx index 672fd77b315..ba26a22717b 100644 --- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx +++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx @@ -57,6 +57,7 @@ type MessageThreadPanelProps = { threadHead: TimelineMessage | null; threadReplies: MainTimelineEntry[]; threadTypingPubkeys: string[]; + toolbarExtraActions?: React.ReactNode; widthPx: number; }; @@ -100,6 +101,7 @@ export function MessageThreadPanel({ threadHead, threadReplies, threadTypingPubkeys, + toolbarExtraActions, widthPx, }: MessageThreadPanelProps) { const threadBodyRef = React.useRef(null); @@ -288,6 +290,12 @@ export function MessageThreadPanel({ ) : null}
+ -
diff --git a/desktop/tests/e2e/integration.spec.ts b/desktop/tests/e2e/integration.spec.ts index c65aee0f878..ff87f8e41ff 100644 --- a/desktop/tests/e2e/integration.spec.ts +++ b/desktop/tests/e2e/integration.spec.ts @@ -416,7 +416,10 @@ test("create channel with description", async ({ page }) => { await page.goto("/"); await createStream(page, channelName, description); - await expect(page.getByTestId("chat-description")).toContainText(description); + await expect(page.getByTestId("chat-title")).toHaveAttribute( + "title", + description, + ); }); test("multiple channels independent", async ({ page }) => { @@ -502,7 +505,8 @@ test("manage sheet updates channel details and context through the relay", async await page.getByTestId(`channel-${renamedChannel}`).click(); await expect(page.getByTestId("chat-title")).toHaveText(renamedChannel); // channelDescription deduplicates by showing only the first non-empty field - await expect(page.getByTestId("chat-description")).toContainText( + await expect(page.getByTestId("chat-title")).toHaveAttribute( + "title", updatedTopic, ); From 99beb2c0991f14b4ce0b39a4dd6c2c4a2a3586f5 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Wed, 6 May 2026 21:17:46 -0400 Subject: [PATCH 18/20] Show ephemeral header badge as icon only Co-authored-by: Cursor --- .../features/channels/ui/EphemeralChannelBadge.tsx | 12 ++---------- desktop/tests/e2e/channels.spec.ts | 11 +++++++---- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/desktop/src/features/channels/ui/EphemeralChannelBadge.tsx b/desktop/src/features/channels/ui/EphemeralChannelBadge.tsx index 64ba23c9608..c8af40f3149 100644 --- a/desktop/src/features/channels/ui/EphemeralChannelBadge.tsx +++ b/desktop/src/features/channels/ui/EphemeralChannelBadge.tsx @@ -1,9 +1,6 @@ import { Clock } from "lucide-react"; -import { - EPHEMERAL_CHANNEL_LABEL, - type EphemeralChannelDisplay, -} from "@/features/channels/lib/ephemeralChannel"; +import type { EphemeralChannelDisplay } from "@/features/channels/lib/ephemeralChannel"; import { cn } from "@/shared/lib/cn"; type EphemeralChannelBadgeProps = { @@ -24,10 +21,6 @@ export function EphemeralChannelBadge({ "aria-label": display.tooltipLabel, role: "img" as const, }; - const label = - isHeader && display.detailLabel - ? `${EPHEMERAL_CHANNEL_LABEL} · ${display.detailLabel}` - : EPHEMERAL_CHANNEL_LABEL; return ( - {isHeader ? {label} : null} ); } diff --git a/desktop/tests/e2e/channels.spec.ts b/desktop/tests/e2e/channels.spec.ts index cbac8f67db4..a6a1d18fe68 100644 --- a/desktop/tests/e2e/channels.spec.ts +++ b/desktop/tests/e2e/channels.spec.ts @@ -329,8 +329,10 @@ test("create ephemeral stream shows sidebar and header affordances", async ({ await expect( page.getByTestId(`channel-ephemeral-${channelName}`), ).toBeVisible(); - await expect(page.getByTestId("chat-ephemeral-badge")).toHaveText( - /Ephemeral.+left/, + await expect(page.getByTestId("chat-ephemeral-badge")).toBeVisible(); + await expect(page.getByTestId("chat-ephemeral-badge")).toHaveAttribute( + "title", + /Ephemeral channel\..+left/, ); await page.getByRole("button", { name: "Toggle Sidebar" }).click(); @@ -370,8 +372,9 @@ test("ephemeral countdown refreshes when switching channels after a clock jump", await page.getByTestId(`channel-${firstChannelName}`).click(); await expect(page.getByTestId("chat-title")).toHaveText(firstChannelName); - await expect(page.getByTestId("chat-ephemeral-badge")).toHaveText( - /Ephemeral.+22h left/, + await expect(page.getByTestId("chat-ephemeral-badge")).toHaveAttribute( + "title", + /Ephemeral channel\..+22 hours left/, ); }); From 8ee5db096ecc4e0f26a332f0d4f6f64114077ced Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Wed, 6 May 2026 21:43:49 -0400 Subject: [PATCH 19/20] Fix ephemeral badge smoke expectations Co-authored-by: Cursor --- desktop/tests/e2e/channels.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/tests/e2e/channels.spec.ts b/desktop/tests/e2e/channels.spec.ts index a6a1d18fe68..f8f028a37f5 100644 --- a/desktop/tests/e2e/channels.spec.ts +++ b/desktop/tests/e2e/channels.spec.ts @@ -332,7 +332,7 @@ test("create ephemeral stream shows sidebar and header affordances", async ({ await expect(page.getByTestId("chat-ephemeral-badge")).toBeVisible(); await expect(page.getByTestId("chat-ephemeral-badge")).toHaveAttribute( "title", - /Ephemeral channel\..+left/, + /Ephemeral channel\. Cleans up (tomorrow|in \d+ hours?)\./, ); await page.getByRole("button", { name: "Toggle Sidebar" }).click(); @@ -374,7 +374,7 @@ test("ephemeral countdown refreshes when switching channels after a clock jump", await expect(page.getByTestId("chat-title")).toHaveText(firstChannelName); await expect(page.getByTestId("chat-ephemeral-badge")).toHaveAttribute( "title", - /Ephemeral channel\..+22 hours left/, + /Ephemeral channel\. Cleans up in 22 hours\./, ); }); From 4877ee22565d3d5f86c430577bd8ed20d61ca576 Mon Sep 17 00:00:00 2001 From: thomaspblock Date: Wed, 6 May 2026 21:58:30 -0400 Subject: [PATCH 20/20] Fix duplicate video playback (#492) Co-authored-by: Cursor --- desktop/src/shared/ui/markdown.tsx | 62 +++--------------------------- 1 file changed, 5 insertions(+), 57 deletions(-) diff --git a/desktop/src/shared/ui/markdown.tsx b/desktop/src/shared/ui/markdown.tsx index 8490206d9aa..183332ba9fa 100644 --- a/desktop/src/shared/ui/markdown.tsx +++ b/desktop/src/shared/ui/markdown.tsx @@ -132,63 +132,11 @@ function createMarkdownComponents( ? rewriteRelayUrl(posterUrl) : undefined; return ( - - -
- -
-
- - - e.preventDefault()} - onInteractOutside={(e) => e.preventDefault()} - > - - Video preview - - - Full-size video preview. Press Escape or click outside the - video to close. - - - {/* biome-ignore lint/a11y/useMediaCaption: user-uploaded video, no captions available */} - - -
+ ); } return (