History

Restore lost useMemo calls in ChatView and fix threadActivities referential stability

Revert bare function calls / IIFEs back to useMemo for: modelOptions, workLogEntries, latestTurnHasToolActivity, pendingApprovals, timelineEntries, revertTurnCountByUserMessageId, completionSummary, and completionDividerBeforeEntryId. Add EMPTY_ACTIVITIES module-level constant so the ?? [] fallback doesn't create a new array reference every render, which was defeating memoization.

Julius Marminge committed 8 months ago

Commit fd4ff8c1 parent 1bcfbdf1 file · +38 −16Browse files

Unified · Split

apps/web/src/components/ChatView.tsx+38−16
@@ -19,6 +19,7 @@ import { · 18 unmodified lines
19 type TurnId,19 type TurnId,
20 normalizeModelSlug,20 normalizeModelSlug,
21 resolveModelSlug,21 resolveModelSlug,
22 OrchestrationThreadActivity,
22} from "@t3tools/contracts";23} from "@t3tools/contracts";
23import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";24import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
24import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";25import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -140,6 +141,7 @@ const ATTACHMENT_PREVIEW_HANDOFF_TTL_MS = 5000; · 115 unmodified lines
140const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`;141const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`;
141const IMAGE_ONLY_BOOTSTRAP_PROMPT =142const IMAGE_ONLY_BOOTSTRAP_PROMPT =
142 "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]";143 "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]";
144const EMPTY_ACTIVITIES: OrchestrationThreadActivity[] = [];
143const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = [];145const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = [];
144const EMPTY_PROJECT_ENTRIES: ProjectEntry[] = [];146const EMPTY_PROJECT_ENTRIES: ProjectEntry[] = [];
145const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120;147const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120;
@@ -611,22 +613,28 @@ export default function ChatView({ threadId }: ChatViewProps) { · 465 unmodified lines
611 );613 );
612 const selectedModel = resolveModelSlug(composerDraft.model ?? baseThreadModel);614 const selectedModel = resolveModelSlug(composerDraft.model ?? baseThreadModel);
613 const selectedEffort = composerDraft.effort ?? DEFAULT_REASONING;615 const selectedEffort = composerDraft.effort ?? DEFAULT_REASONING;
614 const modelOptions = getAppModelOptions(settings.customCodexModels, selectedModel);616 const modelOptions = useMemo(
617 () => getAppModelOptions(settings.customCodexModels, selectedModel),
618 [selectedModel, settings.customCodexModels],
619 );
615 const phase = derivePhase(activeThread?.session ?? null);620 const phase = derivePhase(activeThread?.session ?? null);
616 const isSendBusy = sendPhase !== "idle";621 const isSendBusy = sendPhase !== "idle";
617 const isPreparingWorktree = sendPhase === "preparing-worktree";622 const isPreparingWorktree = sendPhase === "preparing-worktree";
618 const isWorking = phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint;623 const isWorking = phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint;
619 const nowIso = new Date(nowTick).toISOString();624 const nowIso = new Date(nowTick).toISOString();
620 const threadActivities = activeThread?.activities ?? [];625 const threadActivities = activeThread?.activities ?? EMPTY_ACTIVITIES;
621 const workLogEntries = deriveWorkLogEntries(626 const workLogEntries = useMemo(
622 threadActivities,627 () => deriveWorkLogEntries(threadActivities, activeLatestTurn?.turnId ?? undefined),
623 activeLatestTurn?.turnId ?? undefined,628 [activeLatestTurn?.turnId, threadActivities],
629 );
630 const latestTurnHasToolActivity = useMemo(
631 () => hasToolActivityForTurn(threadActivities, activeLatestTurn?.turnId),
632 [activeLatestTurn?.turnId, threadActivities],
624 );633 );
625 const latestTurnHasToolActivity = hasToolActivityForTurn(634 const pendingApprovals = useMemo(
626 threadActivities,635 () => derivePendingApprovals(threadActivities),
627 activeLatestTurn?.turnId,636 [threadActivities],
628 );637 );
629 const pendingApprovals = derivePendingApprovals(threadActivities);
630 useEffect(() => {638 useEffect(() => {
631 attachmentPreviewHandoffByMessageIdRef.current = attachmentPreviewHandoffByMessageId;639 attachmentPreviewHandoffByMessageIdRef.current = attachmentPreviewHandoffByMessageId;
632 }, [attachmentPreviewHandoffByMessageId]);640 }, [attachmentPreviewHandoffByMessageId]);
@@ -744,7 +752,10 @@ export default function ChatView({ threadId }: ChatViewProps) { · 111 unmodified lines
744 }752 }
745 return [...serverMessagesWithPreviewHandoff, ...pendingMessages];753 return [...serverMessagesWithPreviewHandoff, ...pendingMessages];
746 }, [serverMessages, attachmentPreviewHandoffByMessageId, optimisticUserMessages]);754 }, [serverMessages, attachmentPreviewHandoffByMessageId, optimisticUserMessages]);
747 const timelineEntries = deriveTimelineEntries(timelineMessages, workLogEntries);755 const timelineEntries = useMemo(
756 () => deriveTimelineEntries(timelineMessages, workLogEntries),
757 [timelineMessages, workLogEntries],
758 );
748 const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } =759 const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } =
749 useTurnDiffSummaries(activeThread);760 useTurnDiffSummaries(activeThread);
750 const turnDiffSummaryByAssistantMessageId = useMemo(() => {761 const turnDiffSummaryByAssistantMessageId = useMemo(() => {
@@ -755,7 +766,7 @@ export default function ChatView({ threadId }: ChatViewProps) { · 4 unmodified lines
755 }766 }
756 return byMessageId;767 return byMessageId;
757 }, [turnDiffSummaries]);768 }, [turnDiffSummaries]);
758 const revertTurnCountByUserMessageId = (() => {769 const revertTurnCountByUserMessageId = useMemo(() => {
759 const byUserMessageId = new Map<MessageId, number>();770 const byUserMessageId = new Map<MessageId, number>();
760 for (let index = 0; index < timelineEntries.length; index += 1) {771 for (let index = 0; index < timelineEntries.length; index += 1) {
761 const entry = timelineEntries[index];772 const entry = timelineEntries[index];
@@ -786,9 +797,9 @@ export default function ChatView({ threadId }: ChatViewProps) { · 24 unmodified lines
786 }797 }
787 798
788 return byUserMessageId;799 return byUserMessageId;
789 })();800 }, [inferredCheckpointTurnCountByTurnId, timelineEntries, turnDiffSummaryByAssistantMessageId]);
790 801
791 const completionSummary = (() => {802 const completionSummary = useMemo(() => {
792 if (!latestTurnSettled) return null;803 if (!latestTurnSettled) return null;
793 if (!activeLatestTurn?.startedAt) return null;804 if (!activeLatestTurn?.startedAt) return null;
794 if (!activeLatestTurn.completedAt) return null;805 if (!activeLatestTurn.completedAt) return null;
@@ -796,8 +807,13 @@ export default function ChatView({ threadId }: ChatViewProps) { · 1 unmodified line
796 807
797 const elapsed = formatElapsed(activeLatestTurn.startedAt, activeLatestTurn.completedAt);808 const elapsed = formatElapsed(activeLatestTurn.startedAt, activeLatestTurn.completedAt);
798 return elapsed ? `Worked for ${elapsed}` : null;809 return elapsed ? `Worked for ${elapsed}` : null;
799 })();810 }, [
800 const completionDividerBeforeEntryId = (() => {811 activeLatestTurn?.completedAt,
812 activeLatestTurn?.startedAt,
813 latestTurnHasToolActivity,
814 latestTurnSettled,
815 ]);
816 const completionDividerBeforeEntryId = useMemo(() => {
801 if (!latestTurnSettled) return null;817 if (!latestTurnSettled) return null;
802 if (!activeLatestTurn?.startedAt) return null;818 if (!activeLatestTurn?.startedAt) return null;
803 if (!activeLatestTurn.completedAt) return null;819 if (!activeLatestTurn.completedAt) return null;
@@ -821,7 +837,13 @@ export default function ChatView({ threadId }: ChatViewProps) { · 17 unmodified lines
821 }837 }
822 }838 }
823 return inRangeMatch ?? fallbackMatch;839 return inRangeMatch ?? fallbackMatch;
824 })();840 }, [
841 activeLatestTurn?.completedAt,
842 activeLatestTurn?.startedAt,
843 completionSummary,
844 latestTurnSettled,
845 timelineEntries,
846 ]);
825 const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null;847 const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null;
826 const composerTrigger = useMemo(848 const composerTrigger = useMemo(
827 () => detectComposerTrigger(prompt, composerCursor),849 () => detectComposerTrigger(prompt, composerCursor),

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t