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 3a43091de..51eb05da5 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 52366919e..bef9cea66 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 7747b3cec..c0cb9daa2 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 ef7758dc9..5de173617 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 880dcb3e4..edd218776 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 8ec47403f..8c294340c 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 93a03cf21..8ca39cb94 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 19b2dbbc2..a5673d73b 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 4d8d0a2f9..cfe8ff34f 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 0283821fe..09e6524cc 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 053aed6db..0032b2be8 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 45ed6d56d..c868230df 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 98751c636..f1adf9eef 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 e592bbfee..a4fb0eb0b 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 09ae20d78..043c95011 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 a4fb0eb0b..672fd77b3 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 0032b2be8..f7926801d 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 043c95011..dd5b4629e 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 c0179d0c7..070526a54 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 4550a2bfa..bee6f1686 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 09e6524cc..a6e53ac86 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 8c294340c..6685fb580 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 c0578a655..13660d8d3 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 6876f8a23..99251540f 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 b5ea21e7e..4e2639844 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 bee6f1686..bacec7079 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 bacec7079..dc3231ed2 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 4b0f3d6e0..b85a826ce 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 17430c44e..c322d3ae7 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 6d49c0435..8d023dd96 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 dc3231ed2..fb513c669 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 1c7521b3c..2f7e78421 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 fb513c669..c72de494c 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 102569732..fb343b770 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 a2ec0555b..632d94fbc 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 632d94fbc..30bcf3672 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 c72de494c..7f8b50f2b 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 672fd77b3..ba26a2271 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 c65aee0f8..ff87f8e41 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 64ba23c96..c8af40f31 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 cbac8f67d..a6a1d18fe 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 a6a1d18fe..f8f028a37 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 8490206d9..183332ba9 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 (