| ... | @@ -19,6 +19,7 @@ import { | ... | @@ -19,6 +19,7 @@ import { |
| 19 | type TurnId, | 19 | type TurnId, |
| 20 | normalizeModelSlug, | 20 | normalizeModelSlug, |
| 21 | resolveModelSlug, | 21 | resolveModelSlug, |
| | 22 | OrchestrationThreadActivity, |
| 22 | } from "@t3tools/contracts"; | 23 | } from "@t3tools/contracts"; |
| 23 | import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; | 24 | import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; |
| 24 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | 25 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; |
| ... | @@ -140,6 +141,7 @@ const ATTACHMENT_PREVIEW_HANDOFF_TTL_MS = 5000; | ... | @@ -140,6 +141,7 @@ const ATTACHMENT_PREVIEW_HANDOFF_TTL_MS = 5000; |
| 140 | const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; | 141 | const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; |
| 141 | const IMAGE_ONLY_BOOTSTRAP_PROMPT = | 142 | const 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).]"; |
| | 144 | const EMPTY_ACTIVITIES: OrchestrationThreadActivity[] = []; |
| 143 | const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; | 145 | const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; |
| 144 | const EMPTY_PROJECT_ENTRIES: ProjectEntry[] = []; | 146 | const EMPTY_PROJECT_ENTRIES: ProjectEntry[] = []; |
| 145 | const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120; | 147 | const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120; |
| ... | @@ -611,22 +613,28 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -611,22 +613,28 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 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) { | ... | @@ -744,7 +752,10 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 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) { | ... | @@ -755,7 +766,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 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) { | ... | @@ -786,9 +797,9 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 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) { | ... | @@ -796,8 +807,13 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 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) { | ... | @@ -821,7 +837,13 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 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), |