diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index 35033acf2..df1733462 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -102,6 +102,7 @@ type ChannelPaneProps = { openThreadHeadId: string | null; openAgentSessionPubkey: string | null; threadHeadMessage: TimelineMessage | null; + threadBotTypingPubkeys: string[]; threadMessages: MainTimelineEntry[]; threadTypingPubkeys: string[]; threadReplyTargetId: string | null; @@ -148,6 +149,7 @@ export const ChannelPane = React.memo(function ChannelPane({ openAgentSessionPubkey, targetMessageId, threadHeadMessage, + threadBotTypingPubkeys, threadMessages, threadScrollTargetId, threadTypingPubkeys, @@ -373,6 +375,17 @@ export const ChannelPane = React.memo(function ChannelPane({ replyTargetId={threadReplyTargetId} replyTargetMessage={threadReplyTargetMessage} scrollTargetId={threadScrollTargetId} + threadActivity={ + threadBotTypingPubkeys.length > 0 ? ( + + ) : null + } canResetWidth={canResetThreadPanelWidth} onResetWidth={handleThreadPanelWidthReset} onResizeStart={handleThreadPanelResizeStart} diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index a9945a3e6..48c695e1a 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -140,7 +140,7 @@ export function ChannelScreen({ .map((entry) => entry.pubkey), [typingEntries], ); - const threadTypingPubkeys = React.useMemo( + const allThreadTypingPubkeys = React.useMemo( () => typingEntries .filter((entry) => entry.threadHeadId === openThreadHeadId) @@ -161,7 +161,12 @@ export function ChannelScreen({ }); const managedAgentsQuery = useManagedAgentsQuery(); useManagedAgentObserverBridge(managedAgentsQuery.data ?? []); - const { humanTypingPubkeys, botTypingPubkeys } = React.useMemo(() => { + const { + humanTypingPubkeys, + botTypingPubkeys, + threadTypingPubkeys, + threadBotTypingPubkeys, + } = React.useMemo(() => { const localAgentSet = new Set( (managedAgentsQuery.data ?? []) .filter((agent) => agent.backend.type === "local") @@ -173,13 +178,21 @@ export function ChannelScreen({ ), botTypingPubkeys: [ ...new Set( - typingEntries - .map((entry) => entry.pubkey) - .filter((pk) => localAgentSet.has(pk.toLowerCase())), + mainTypingPubkeys.filter((pk) => localAgentSet.has(pk.toLowerCase())), + ), + ], + threadTypingPubkeys: allThreadTypingPubkeys.filter( + (pk) => !localAgentSet.has(pk.toLowerCase()), + ), + threadBotTypingPubkeys: [ + ...new Set( + allThreadTypingPubkeys.filter((pk) => + localAgentSet.has(pk.toLowerCase()), + ), ), ], }; - }, [mainTypingPubkeys, managedAgentsQuery.data, typingEntries]); + }, [allThreadTypingPubkeys, mainTypingPubkeys, managedAgentsQuery.data]); const messageProfiles = React.useMemo(() => { const base = mergeCurrentProfileIntoLookup( @@ -471,6 +484,7 @@ export function ChannelScreen({ profiles={messageProfiles} targetMessageId={targetMessageId} threadHeadMessage={openThreadHeadMessage} + threadBotTypingPubkeys={threadBotTypingPubkeys} threadMessages={threadMessages} threadTypingPubkeys={threadTypingPubkeys} threadReplyTargetId={threadReplyTargetId} diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx index 629eb79f1..af94ed2aa 100644 --- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx +++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx @@ -54,12 +54,20 @@ type MessageThreadPanelProps = { replyTargetId: string | null; replyTargetMessage: TimelineMessage | null; scrollTargetId: string | null; + threadActivity?: React.ReactNode; threadHead: TimelineMessage | null; threadReplies: MainTimelineEntry[]; threadTypingPubkeys: string[]; widthPx: number; }; +const THREAD_TEXT_COLUMN_OFFSET_PX = 8; + +function getThreadControlOffsetPx(depth = 0): number { + const visibleDepth = Math.min(Math.max(depth, 0), 6); + return visibleDepth * 28 + THREAD_TEXT_COLUMN_OFFSET_PX; +} + function canManageMessage( message: TimelineMessage, currentPubkey: string | undefined, @@ -97,6 +105,7 @@ export function MessageThreadPanel({ replyTargetId, replyTargetMessage, scrollTargetId, + threadActivity, threadHead, threadReplies, threadTypingPubkeys, @@ -223,6 +232,11 @@ export function MessageThreadPanel({ const nextDepth = threadReplies[index + 1]?.message.depth ?? -1; const isExpanded = nextDepth > entry.message.depth; + const isLastEntry = index === threadReplies.length - 1; + const showSummary = Boolean(entry.summary && !isExpanded); + const showThreadActivity = Boolean( + threadActivity && isLastEntry, + ); return (
@@ -246,13 +260,23 @@ export function MessageThreadPanel({ onToggleReaction={onToggleReaction} profiles={profiles} /> - {entry.summary && !isExpanded ? ( - + {showSummary || showThreadActivity ? ( +
+ {entry.summary && showSummary ? ( + + ) : null} + {showThreadActivity ? threadActivity : null} +
) : null}
); @@ -268,6 +292,15 @@ export function MessageThreadPanel({

)} + {threadActivity && threadReplies.length === 0 ? ( +
+ {threadActivity} +
+ ) : null}
diff --git a/desktop/src/features/messages/ui/MessageThreadSummaryRow.tsx b/desktop/src/features/messages/ui/MessageThreadSummaryRow.tsx index 3ed50e92b..8c354de45 100644 --- a/desktop/src/features/messages/ui/MessageThreadSummaryRow.tsx +++ b/desktop/src/features/messages/ui/MessageThreadSummaryRow.tsx @@ -34,16 +34,17 @@ export function MessageThreadSummaryRow({ message, onOpenThread, summary, + textColumnOffsetPx = 60, }: { alignWithText?: boolean; depth?: number; message: TimelineMessage; onOpenThread: (message: TimelineMessage) => void; summary: TimelineThreadSummary; + textColumnOffsetPx?: number; }) { const visibleDepth = Math.min(Math.max(depth, 0), 6); - const messageTextOffsetPx = 60; - const marginLeftPx = visibleDepth * 28 + messageTextOffsetPx; + const marginLeftPx = visibleDepth * 28 + textColumnOffsetPx; const depthGuideOffsets = Array.from( { length: visibleDepth }, (_, index) => 14 + index * 28,