fix(desktop): show nested thread replies inline

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Thomas Petersen
2026-05-11 07:53:11 -04:00
parent 0230f2d438
commit 0fed686d43
5 changed files with 21 additions and 69 deletions

View File

@@ -81,7 +81,6 @@ type ChannelPaneProps = {
onDelete?: (message: TimelineMessage) => void;
onEdit?: (message: TimelineMessage) => void;
onEditSave?: (content: string) => Promise<void>;
onExpandThreadReplies: (message: TimelineMessage) => void;
onJoinChannel?: () => Promise<void>;
onOpenAgentSession: (pubkey: string) => void;
onOpenDm?: (pubkeys: string[]) => void;
@@ -143,7 +142,6 @@ export const ChannelPane = React.memo(function ChannelPane({
onDelete,
onEdit,
onEditSave,
onExpandThreadReplies,
onJoinChannel,
onOpenAgentSession,
onOpenDm,
@@ -418,7 +416,6 @@ export const ChannelPane = React.memo(function ChannelPane({
onDelete={onDelete}
onEdit={onEdit}
onEditSave={onEditSave}
onExpandReplies={onExpandThreadReplies}
onSelectReplyTarget={onSelectThreadReplyTarget}
onSend={onSendThreadReply}
onScrollTargetResolved={onThreadScrollTargetResolved}

View File

@@ -296,14 +296,8 @@ export function ChannelScreen({
timelineMessages,
openThreadHeadId,
threadReplyTargetId,
expandedThreadReplyIds,
),
[
expandedThreadReplyIds,
openThreadHeadId,
threadReplyTargetId,
timelineMessages,
],
[openThreadHeadId, threadReplyTargetId, timelineMessages],
);
const openThreadHeadMessage = threadPanelData.threadHead;
const threadMessages = threadPanelData.visibleReplies;
@@ -322,7 +316,6 @@ export function ChannelScreen({
handleDelete,
handleEdit,
handleEditSave,
handleExpandThreadReplies,
handleOpenThread,
handleSendMessage,
handleSendThreadReply,
@@ -496,7 +489,6 @@ export function ChannelScreen({
onDelete={handleDelete}
onEdit={handleEdit}
onEditSave={handleEditSave}
onExpandThreadReplies={handleExpandThreadReplies}
onOpenAgentSession={handleOpenAgentSession}
onOpenDm={handleOpenDm}
onCloseProfilePanel={handleCloseProfilePanel}

View File

@@ -150,67 +150,56 @@ function buildSummaryForDirectReplies(
};
}
function appendExpandedReplies(params: {
function appendThreadPanelReplies(params: {
entries: MainTimelineEntry[];
parentId: string;
depth: number;
directChildrenByParentId: Map<string, TimelineMessage[]>;
descendantStatsByMessageId: Map<string, ThreadDescendantStats>;
expandedReplyIds: ReadonlySet<string>;
visitedMessageIds: Set<string>;
}) {
const {
entries,
parentId,
depth,
directChildrenByParentId,
descendantStatsByMessageId,
expandedReplyIds,
visitedMessageIds,
} = params;
const directReplies = directChildrenByParentId.get(parentId) ?? [];
for (const reply of directReplies) {
if (visitedMessageIds.has(reply.id)) {
continue;
}
visitedMessageIds.add(reply.id);
entries.push({
message: normalizeInlineReplyMessage(reply, depth),
summary: buildSummaryForDirectReplies(
reply.id,
descendantStatsByMessageId,
),
summary: null,
});
if (expandedReplyIds.has(reply.id)) {
appendExpandedReplies({
entries,
parentId: reply.id,
depth: depth + 1,
directChildrenByParentId,
descendantStatsByMessageId,
expandedReplyIds,
});
}
appendThreadPanelReplies({
entries,
parentId: reply.id,
depth: depth + 1,
directChildrenByParentId,
visitedMessageIds,
});
}
}
function buildVisibleThreadReplies(params: {
openThreadHeadId: string;
directChildrenByParentId: Map<string, TimelineMessage[]>;
descendantStatsByMessageId: Map<string, ThreadDescendantStats>;
expandedReplyIds: ReadonlySet<string>;
}) {
const {
openThreadHeadId,
directChildrenByParentId,
descendantStatsByMessageId,
expandedReplyIds,
} = params;
const { openThreadHeadId, directChildrenByParentId } = params;
const entries: MainTimelineEntry[] = [];
appendExpandedReplies({
appendThreadPanelReplies({
entries,
parentId: openThreadHeadId,
depth: 0,
directChildrenByParentId,
descendantStatsByMessageId,
expandedReplyIds,
visitedMessageIds: new Set([openThreadHeadId]),
});
return entries;
@@ -238,7 +227,6 @@ export function buildThreadPanelData(
messages: TimelineMessage[],
openThreadHeadId: string | null,
threadReplyTargetId: string | null,
expandedReplyIds: ReadonlySet<string>,
): ThreadPanelData {
if (!openThreadHeadId) {
return {
@@ -267,8 +255,6 @@ export function buildThreadPanelData(
const visibleReplies = buildVisibleThreadReplies({
openThreadHeadId,
directChildrenByParentId,
descendantStatsByMessageId,
expandedReplyIds,
});
const replyTargetInBranch =

View File

@@ -16,7 +16,6 @@ import {
} from "@/shared/ui/OverlayPanelBackdrop";
import { MessageComposer } from "./MessageComposer";
import { MessageRow } from "./MessageRow";
import { MessageThreadSummaryRow } from "./MessageThreadSummaryRow";
import { TypingIndicatorRow } from "./TypingIndicatorRow";
import { useTimelineScrollManager } from "./useTimelineScrollManager";
@@ -35,7 +34,6 @@ type MessageThreadPanelProps = {
onDelete?: (message: TimelineMessage) => void;
onEdit?: (message: TimelineMessage) => void;
onEditSave?: (content: string) => Promise<void>;
onExpandReplies: (message: TimelineMessage) => void;
onResetWidth: () => void;
onResizeStart: (event: React.PointerEvent<HTMLButtonElement>) => void;
onScrollTargetResolved: () => void;
@@ -87,7 +85,6 @@ export function MessageThreadPanel({
onDelete,
onEdit,
onEditSave,
onExpandReplies,
onResetWidth,
onResizeStart,
onScrollTargetResolved,
@@ -244,15 +241,6 @@ export function MessageThreadPanel({
onToggleReaction={onToggleReaction}
profiles={profiles}
/>
{entry.summary ? (
<MessageThreadSummaryRow
depth={entry.message.depth}
layoutVariant="thread-reply"
message={entry.message}
onOpenThread={onExpandReplies}
summary={entry.summary}
/>
) : null}
</div>
);
})}

View File

@@ -424,8 +424,7 @@ test("opens a single-level thread panel with inline expansion", async ({
const firstReplySummaryRow = threadReplies.locator(
`[data-thread-head-id="${firstReplyId}"]`,
);
await expect(firstReplySummaryRow).toHaveCount(1);
await expect(firstReplySummaryRow).toContainText("2 replies");
await expect(firstReplySummaryRow).toHaveCount(0);
await expect(rootSummaryRow).toContainText("18 replies");
await expect(
@@ -448,16 +447,6 @@ test("opens a single-level thread panel with inline expansion", async ({
});
})
.toBeLessThanOrEqual(160);
await firstReplySummaryRow.click();
await expect(
threadReplies.getByTestId("message-row").filter({ hasText: nestedReply }),
).toHaveCount(0);
await expect(
threadReplies
.getByTestId("message-row")
.filter({ hasText: nestedReplyFromBob }),
).toHaveCount(0);
});
test("thread panel width uses session storage and reset handle", async ({