History

Fix terminal focus on toggle, clean up orphaned terminal state, and revoke optimistic message blob URLs on unmount

- Add setTerminalFocusRequestId call when terminal opens via toggle so the terminal panel receives focus automatically - Call clearTerminalState when deleting threads in the Sidebar and add removeOrphanedTerminalStates to prune stale entries after read model sync - Track optimisticUserMessages via ref and revoke their blob URLs in the unmount cleanup effect to prevent memory leaks

Cursor Agent committed 8 months ago

Commit 32fe80d1 parent 6e93d424 files · +31 −0Browse files

Unified · Split

4 files
  1. apps/web/src/components/ChatView.tsx +6 −0
  2. apps/web/src/components/Sidebar.tsx +3 −0
  3. apps/web/src/routes/__root.tsx +9 −0
  4. apps/web/src/terminalStateStore.ts +13 −0
apps/web/src/components/ChatView.tsx+6−0
@@ -479,6 +479,8 @@ export default function ChatView({ threadId }: ChatViewProps) { · 478 unmodified lines
479479 const [isDragOverComposer, setIsDragOverComposer] = useState(false);
480480 const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null);
481481 const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]);
482+ const optimisticUserMessagesRef = useRef(optimisticUserMessages);
483+ optimisticUserMessagesRef.current = optimisticUserMessages;
482484 const [localDraftErrorsByThreadId, setLocalDraftErrorsByThreadId] = useState<
483485 Record<ThreadId, string | null>
484486 >({});
@@ -634,6 +636,9 @@ export default function ChatView({ threadId }: ChatViewProps) { · 149 unmodified lines
634636 useEffect(() => {
635637 return () => {
636638 clearAttachmentPreviewHandoffs();
639+ for (const message of optimisticUserMessagesRef.current) {
640+ revokeUserMessagePreviewUrls(message);
641+ }
637642 };
638643 }, [clearAttachmentPreviewHandoffs]);
639644 const handoffAttachmentPreviews = useCallback((messageId: MessageId, previewUrls: string[]) => {
@@ -1542,6 +1547,7 @@ export default function ChatView({ threadId }: ChatViewProps) { · 902 unmodified lines
15421547
15431548 if (!previous && current) {
15441549 terminalOpenByThreadRef.current[activeThreadId] = current;
1550+ setTerminalFocusRequestId((value) => value + 1);
15451551 return;
15461552 } else if (previous && !current) {
15471553 terminalOpenByThreadRef.current[activeThreadId] = current;
apps/web/src/components/Sidebar.tsx+3−0
@@ -241,6 +241,7 @@ export default function Sidebar() { · 240 unmodified lines
241241 );
242242 const getDraftThread = useComposerDraftStore((store) => store.getDraftThread);
243243 const terminalStateByThreadId = useTerminalStateStore((state) => state.terminalStateByThreadId);
244+ const clearTerminalState = useTerminalStateStore((state) => state.clearTerminalState);
244245 const setProjectDraftThreadId = useComposerDraftStore((store) => store.setProjectDraftThreadId);
245246 const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);
246247 const clearProjectDraftThreadId = useComposerDraftStore(
@@ -578,6 +579,7 @@ export default function Sidebar() { · 331 unmodified lines
578579 });
579580 clearComposerDraftForThread(threadId);
580581 clearProjectDraftThreadById(thread.projectId, thread.id);
582+ clearTerminalState(threadId);
581583 if (shouldNavigateToFallback) {
582584 if (fallbackThreadId) {
583585 void navigate({
@@ -619,6 +621,7 @@ export default function Sidebar() { · 35 unmodified lines
619621 appSettings.confirmThreadDelete,
620622 clearComposerDraftForThread,
621623 clearProjectDraftThreadById,
624+ clearTerminalState,
622625 markThreadUnread,
623626 navigate,
624627 projects,
apps/web/src/routes/__root.tsx+9−0
@@ -143,11 +143,20 @@ function EventRouter() { · 142 unmodified lines
143143 let syncing = false;
144144 let pending = false;
145145
146+ const removeOrphanedTerminalStates =
147+ useTerminalStateStore.getState().removeOrphanedTerminalStates;
148+
146149 const flushSnapshotSync = async (): Promise<void> => {
147150 const snapshot = await api.orchestration.getSnapshot();
148151 if (disposed) return;
149152 latestSequence = Math.max(latestSequence, snapshot.snapshotSequence);
150153 syncServerReadModel(snapshot);
154+ const activeThreadIds = new Set(
155+ snapshot.threads
156+ .filter((t) => t.deletedAt === null)
157+ .map((t) => t.id),
158+ );
159+ removeOrphanedTerminalStates(activeThreadIds);
151160 if (pending) {
152161 pending = false;
153162 await flushSnapshotSync();
apps/web/src/terminalStateStore.ts+13−0
@@ -461,6 +461,7 @@ interface TerminalStateStoreState { · 460 unmodified lines
461461 hasRunningSubprocess: boolean,
462462 ) => void;
463463 clearTerminalState: (threadId: ThreadId) => void;
464+ removeOrphanedTerminalStates: (activeThreadIds: Set<ThreadId>) => void;
464465 }
465466
466467 export const useTerminalStateStore = create<TerminalStateStoreState>()(
@@ -505,6 +506,18 @@ export const useTerminalStateStore = create<TerminalStateStoreState>()( · 38 unmodified lines
505506 ),
506507 clearTerminalState: (threadId) =>
507508 updateTerminal(threadId, () => createDefaultThreadTerminalState()),
509+ removeOrphanedTerminalStates: (activeThreadIds) =>
510+ set((state) => {
511+ const orphanedIds = Object.keys(state.terminalStateByThreadId).filter(
512+ (id) => !activeThreadIds.has(id as ThreadId),
513+ );
514+ if (orphanedIds.length === 0) return state;
515+ const next = { ...state.terminalStateByThreadId };
516+ for (const id of orphanedIds) {
517+ delete next[id as ThreadId];
518+ }
519+ return { terminalStateByThreadId: next };
520+ }),
508521 };
509522 },
510523 {

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