authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-16 00:47:08-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-16 00:55:31-08:00
log73fa2d66c79eb1351fbae55a81c61e1f7b88c619
tree99d4be255d860e870410e7355264958145297d92
parent51dcb547edf34240a509ffb80f7486d382fd9445

Use stable IDs for approval actions in ChatView

- Derive `activeSessionId` and `activeThreadId` before approval callbacks - Avoid stale thread/session captures when submitting approval decisions - Pass `hasMessages` to `MessagesTimeline` instead of the full thread

1 files changed, 10 insertions(+), 9 deletions(-)

apps/web/src/components/ChatView.tsx+10-9
...@@ -51,7 +51,7 @@ import {...@@ -51,7 +51,7 @@ import {
51} from "../session-logic";51} from "../session-logic";
52import { isScrollContainerNearBottom } from "../chat-scroll";52import { isScrollContainerNearBottom } from "../chat-scroll";
53import { useStore } from "../store";53import { useStore } from "../store";
54import type { ChatImageAttachment, Thread } from "../types";54import type { ChatImageAttachment } from "../types";
55import BranchToolbar from "./BranchToolbar";55import BranchToolbar from "./BranchToolbar";
56import GitActionsControl from "./GitActionsControl";56import GitActionsControl from "./GitActionsControl";
57import {57import {
...@@ -168,6 +168,7 @@ export default function ChatView() {...@@ -168,6 +168,7 @@ export default function ChatView() {
168168
169 const activeThread = state.threads.find((t) => t.id === state.activeThreadId);169 const activeThread = state.threads.find((t) => t.id === state.activeThreadId);
170 const activeThreadId = activeThread?.id ?? null;170 const activeThreadId = activeThread?.id ?? null;
171 const activeSessionId = activeThread?.session?.sessionId;
171 const activeProject = state.projects.find((p) => p.id === activeThread?.projectId);172 const activeProject = state.projects.find((p) => p.id === activeThread?.projectId);
172 const selectedModel = resolveModelSlug(173 const selectedModel = resolveModelSlug(
173 activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL,174 activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL,
...@@ -895,28 +896,28 @@ export default function ChatView() {...@@ -895,28 +896,28 @@ export default function ChatView() {
895896
896 const onRespondToApproval = useCallback(897 const onRespondToApproval = useCallback(
897 async (requestId: string, decision: ProviderApprovalDecision) => {898 async (requestId: string, decision: ProviderApprovalDecision) => {
898 if (!api || !activeThread?.session) return;899 if (!api || !activeSessionId || !activeThreadId) return;
899900
900 setRespondingRequestIds((existing) =>901 setRespondingRequestIds((existing) =>
901 existing.includes(requestId) ? existing : [...existing, requestId],902 existing.includes(requestId) ? existing : [...existing, requestId],
902 );903 );
903 try {904 try {
904 await api.providers.respondToRequest({905 await api.providers.respondToRequest({
905 sessionId: activeThread.session.sessionId,906 sessionId: activeSessionId,
906 requestId,907 requestId,
907 decision,908 decision,
908 });909 });
909 } catch (err) {910 } catch (err) {
910 dispatch({911 dispatch({
911 type: "SET_ERROR",912 type: "SET_ERROR",
912 threadId: activeThread.id,913 threadId: activeThreadId,
913 error: err instanceof Error ? err.message : "Failed to submit approval decision.",914 error: err instanceof Error ? err.message : "Failed to submit approval decision.",
914 });915 });
915 } finally {916 } finally {
916 setRespondingRequestIds((existing) => existing.filter((id) => id !== requestId));917 setRespondingRequestIds((existing) => existing.filter((id) => id !== requestId));
917 }918 }
918 },919 },
919 [activeThread?.id, activeThread?.session, api, dispatch],920 [activeSessionId, activeThreadId, api, dispatch],
920 );921 );
921922
922 const onModelSelect = useCallback(923 const onModelSelect = useCallback(
...@@ -1015,7 +1016,7 @@ export default function ChatView() {...@@ -1015,7 +1016,7 @@ export default function ChatView() {
1015 onScroll={onMessagesScroll}1016 onScroll={onMessagesScroll}
1016 >1017 >
1017 <MessagesTimeline1018 <MessagesTimeline
1018 activeThread={activeThread}1019 hasMessages={activeThread.messages.length > 0}
1019 isWorking={isWorking}1020 isWorking={isWorking}
1020 timelineEntries={timelineEntries}1021 timelineEntries={timelineEntries}
1021 completionDividerBeforeEntryId={completionDividerBeforeEntryId}1022 completionDividerBeforeEntryId={completionDividerBeforeEntryId}
...@@ -1413,7 +1414,7 @@ const PendingApprovalsPanel = memo(function PendingApprovalsPanel({...@@ -1413,7 +1414,7 @@ const PendingApprovalsPanel = memo(function PendingApprovalsPanel({
1413});1414});
14141415
1415interface MessagesTimelineProps {1416interface MessagesTimelineProps {
1416 activeThread: Thread;1417 hasMessages: boolean;
1417 isWorking: boolean;1418 isWorking: boolean;
1418 timelineEntries: ReturnType<typeof deriveTimelineEntries>;1419 timelineEntries: ReturnType<typeof deriveTimelineEntries>;
1419 completionDividerBeforeEntryId: string | null;1420 completionDividerBeforeEntryId: string | null;
...@@ -1427,7 +1428,7 @@ interface MessagesTimelineProps {...@@ -1427,7 +1428,7 @@ interface MessagesTimelineProps {
1427}1428}
14281429
1429const MessagesTimeline = memo(function MessagesTimeline({1430const MessagesTimeline = memo(function MessagesTimeline({
1430 activeThread,1431 hasMessages,
1431 isWorking,1432 isWorking,
1432 timelineEntries,1433 timelineEntries,
1433 completionDividerBeforeEntryId,1434 completionDividerBeforeEntryId,
...@@ -1439,7 +1440,7 @@ const MessagesTimeline = memo(function MessagesTimeline({...@@ -1439,7 +1440,7 @@ const MessagesTimeline = memo(function MessagesTimeline({
1439 onImageExpand,1440 onImageExpand,
1440 messagesEndRef,1441 messagesEndRef,
1441}: MessagesTimelineProps) {1442}: MessagesTimelineProps) {
1442 if (activeThread.messages.length === 0 && !isWorking) {1443 if (!hasMessages && !isWorking) {
1443 return (1444 return (
1444 <div className="flex h-full items-center justify-center">1445 <div className="flex h-full items-center justify-center">
1445 <p className="text-sm text-muted-foreground/30">Send a message to start the conversation.</p>1446 <p className="text-sm text-muted-foreground/30">Send a message to start the conversation.</p>