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,