Scope thread activity pills.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Thomas Petersen
2026-05-01 17:49:15 -04:00
co-authored by Cursor
parent c7b698a0fe
commit 18dfbff379
4 changed files with 76 additions and 15 deletions
@@ -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 ? (
<BotActivityBar
agents={agentSessionAgents}
onOpenAgentSession={onOpenAgentSession}
openAgentSessionPubkey={openAgentSessionPubkey}
profiles={profiles}
typingBotPubkeys={threadBotTypingPubkeys}
/>
) : null
}
canResetWidth={canResetThreadPanelWidth}
onResetWidth={handleThreadPanelWidthReset}
onResizeStart={handleThreadPanelResizeStart}
@@ -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}
@@ -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 (
<div key={entry.message.id}>
@@ -246,13 +260,23 @@ export function MessageThreadPanel({
onToggleReaction={onToggleReaction}
profiles={profiles}
/>
{entry.summary && !isExpanded ? (
<MessageThreadSummaryRow
depth={entry.message.depth}
message={entry.message}
onOpenThread={onExpandReplies}
summary={entry.summary}
/>
{showSummary || showThreadActivity ? (
<div
className="mt-1 flex min-w-0 items-start gap-1.5"
style={{
marginLeft: `${getThreadControlOffsetPx(entry.message.depth)}px`,
}}
>
{entry.summary && showSummary ? (
<MessageThreadSummaryRow
alignWithText={false}
message={entry.message}
onOpenThread={onExpandReplies}
summary={entry.summary}
/>
) : null}
{showThreadActivity ? threadActivity : null}
</div>
) : null}
</div>
);
@@ -268,6 +292,15 @@ export function MessageThreadPanel({
</p>
</div>
)}
{threadActivity && threadReplies.length === 0 ? (
<div
className="mt-2 flex min-w-0 justify-start"
data-testid="message-thread-activity"
style={{ marginLeft: `${THREAD_TEXT_COLUMN_OFFSET_PX}px` }}
>
{threadActivity}
</div>
) : null}
<div aria-hidden className="h-px" ref={bottomAnchorRef} />
</div>
</div>
@@ -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,