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