perf(desktop): paint thread feedback immediately

Commit the existing thread skeleton before the deferred thread-panel mount, even
when the root is already cached. Keep the expensive panel work transitional and
cover the one-frame loading state with a mutation-observer regression test.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
This commit is contained in:
Wes
2026-08-15 16:20:08 -06:00
parent a5d5eef255
commit 52f87b0515
4 changed files with 73 additions and 15 deletions

View File

@@ -782,6 +782,20 @@ export const ChannelPane = React.memo(function ChannelPane({
useSplitAuxiliaryPane={useSplitAuxiliaryPane}
transparentChrome={hasSplitAuxiliaryPane}
/>
) : shouldShowThreadSkeleton ? (
(() => {
if (isHuddleTranscript) {
return wrapThreadPanel(<HuddleStartingView />);
}
const panel = (
<MessageThreadPanelSkeleton
{...threadLayoutProps}
onClose={onCloseThread}
widthPx={threadPanelWidthPx}
/>
);
return wrapThreadPanel(panel);
})()
) : threadHeadMessage ? (
(() => {
const panel = (
@@ -854,20 +868,6 @@ export const ChannelPane = React.memo(function ChannelPane({
);
return wrapThreadPanel(panel);
})()
) : shouldShowThreadSkeleton ? (
(() => {
if (isHuddleTranscript) {
return wrapThreadPanel(<HuddleStartingView />);
}
const panel = (
<MessageThreadPanelSkeleton
{...threadLayoutProps}
onClose={onCloseThread}
widthPx={threadPanelWidthPx}
/>
);
return wrapThreadPanel(panel);
})()
) : activeChannel && selectedAgent ? (
(() => {
// When the panel was opened from a different channel than the

View File

@@ -157,6 +157,9 @@ export function ChannelScreen({
const [editTargetId, setEditTargetId] = React.useState<string | null>(null);
const [optimisticOpenThreadHeadId, setOptimisticOpenThreadHeadId] =
React.useState<string | null | undefined>(undefined);
const [pendingOpenThreadHeadId, setPendingOpenThreadHeadId] = React.useState<
string | null
>(null);
const clearOptimisticThreadOverride = React.useCallback(() => {
setOptimisticOpenThreadHeadId(undefined);
}, []);
@@ -171,6 +174,11 @@ export function ChannelScreen({
openThreadHeadId,
optimisticOpenThreadHeadId,
});
React.useEffect(() => {
if (pendingOpenThreadHeadId === effectiveOpenThreadHeadId) {
setPendingOpenThreadHeadId(null);
}
}, [effectiveOpenThreadHeadId, pendingOpenThreadHeadId]);
const isNotifiedForEffectiveThread =
effectiveOpenThreadHeadId != null
? isNotifiedForThread(effectiveOpenThreadHeadId)
@@ -489,6 +497,7 @@ export function ChannelScreen({
recordThreadInteraction,
openThreadHeadId: effectiveOpenThreadHeadId,
onOptimisticOpenThreadHeadIdChange: setOptimisticOpenThreadHeadId,
onPendingOpenThreadHeadIdChange: setPendingOpenThreadHeadId,
onRequestEmptyEditDelete: setEmptyDeleteId,
sendMessageMutation,
setExpandedThreadReplyIds,
@@ -683,7 +692,10 @@ export function ChannelScreen({
? threadFirstUnreadReplyId
: null;
const shouldShowThreadSkeleton = Boolean(
effectiveOpenThreadHeadId && activeChannel && !displayedThreadHeadMessage,
pendingOpenThreadHeadId ||
(effectiveOpenThreadHeadId &&
activeChannel &&
!displayedThreadHeadMessage),
);
const isNarrowPanelViewport =
channelContentWidthPx > 0 &&

View File

@@ -31,6 +31,7 @@ export function useChannelPaneHandlers({
profiles,
recordThreadInteraction,
onOptimisticOpenThreadHeadIdChange,
onPendingOpenThreadHeadIdChange,
onRequestEmptyEditDelete,
openThreadHeadId,
sendMessageMutation,
@@ -54,6 +55,7 @@ export function useChannelPaneHandlers({
onOptimisticOpenThreadHeadIdChange: React.Dispatch<
React.SetStateAction<string | null | undefined>
>;
onPendingOpenThreadHeadIdChange: (value: string | null) => void;
onRequestEmptyEditDelete: (eventId: string) => void;
openThreadHeadId: string | null;
sendMessageMutation: ReturnType<typeof useSendMessageMutation>;
@@ -199,6 +201,7 @@ export function useChannelPaneHandlers({
const handleOpenThread = React.useCallback(
(message: { id: string }) => {
if (openThreadHeadIdRef.current === message.id) {
onPendingOpenThreadHeadIdChange(null);
deferPanelState(() => {
onOptimisticOpenThreadHeadIdChange(null);
setOpenThreadHeadId(null);
@@ -210,6 +213,7 @@ export function useChannelPaneHandlers({
return;
}
onPendingOpenThreadHeadIdChange(message.id);
deferPanelState(() => {
onOptimisticOpenThreadHeadIdChange(message.id);
setOpenThreadHeadId(message.id);
@@ -222,6 +226,7 @@ export function useChannelPaneHandlers({
[
deferPanelState,
onOptimisticOpenThreadHeadIdChange,
onPendingOpenThreadHeadIdChange,
setEditTargetId,
setExpandedThreadReplyIds,
setOpenThreadHeadId,

View File

@@ -2781,6 +2781,47 @@ test("composer is focused after switching to a different channel", async ({
await expect(input).toBeFocused();
});
test("thread open paints immediate feedback before deferred panel work", async ({
page,
}) => {
await installMockBridge(page, { threadRepliesDelayMs: 800 });
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const rootMessage = page
.getByTestId("message-timeline")
.getByTestId("message-row")
.first();
await rootMessage.hover();
await page.evaluate(() => {
const observed = { loading: false };
Object.assign(window, { __BUZZ_THREAD_OPEN_OBSERVED__: observed });
const observer = new MutationObserver(() => {
if (document.querySelector('[data-testid="message-thread-loading"]')) {
observed.loading = true;
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
await rootMessage.getByRole("button", { name: "Reply" }).click();
await expect
.poll(() =>
page.evaluate(
() =>
(
window as Window & {
__BUZZ_THREAD_OPEN_OBSERVED__?: { loading: boolean };
}
).__BUZZ_THREAD_OPEN_OBSERVED__?.loading ?? false,
),
)
.toBe(true);
await expect(page.getByTestId("message-thread-panel")).toBeVisible();
});
test("thread composer is focused after clicking the reply icon", async ({
page,
}) => {