| author | |
| committer | |
| log | 32fe80d1647ea9fb466fe5fab67b74fea78f011b |
| tree | 28562e51f579392a7960571431efca89630d3f0b |
| parent | 6e93d42023a61f638d7a83c50083a18040f136c1 |
- 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
Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com>4 files changed, 31 insertions(+), 0 deletions(-)
apps/web/src/components/ChatView.tsx+6| ... | ... | @@ -479,6 +479,8 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 479 | 479 | const [isDragOverComposer, setIsDragOverComposer] = useState(false); |
| 480 | 480 | const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null); |
| 481 | 481 | const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]); |
| 482 | const optimisticUserMessagesRef = useRef(optimisticUserMessages); | |
| 483 | optimisticUserMessagesRef.current = optimisticUserMessages; | |
| 482 | 484 | const [localDraftErrorsByThreadId, setLocalDraftErrorsByThreadId] = useState< |
| 483 | 485 | Record<ThreadId, string | null> |
| 484 | 486 | >({}); |
| ... | ... | @@ -634,6 +636,9 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 634 | 636 | useEffect(() => { |
| 635 | 637 | return () => { |
| 636 | 638 | clearAttachmentPreviewHandoffs(); |
| 639 | for (const message of optimisticUserMessagesRef.current) { | |
| 640 | revokeUserMessagePreviewUrls(message); | |
| 641 | } | |
| 637 | 642 | }; |
| 638 | 643 | }, [clearAttachmentPreviewHandoffs]); |
| 639 | 644 | const handoffAttachmentPreviews = useCallback((messageId: MessageId, previewUrls: string[]) => { |
| ... | ... | @@ -1542,6 +1547,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1542 | 1547 | |
| 1543 | 1548 | if (!previous && current) { |
| 1544 | 1549 | terminalOpenByThreadRef.current[activeThreadId] = current; |
| 1550 | setTerminalFocusRequestId((value) => value + 1); | |
| 1545 | 1551 | return; |
| 1546 | 1552 | } else if (previous && !current) { |
| 1547 | 1553 | terminalOpenByThreadRef.current[activeThreadId] = current; |
apps/web/src/components/Sidebar.tsx+3| ... | ... | @@ -241,6 +241,7 @@ export default function Sidebar() { |
| 241 | 241 | ); |
| 242 | 242 | const getDraftThread = useComposerDraftStore((store) => store.getDraftThread); |
| 243 | 243 | const terminalStateByThreadId = useTerminalStateStore((state) => state.terminalStateByThreadId); |
| 244 | const clearTerminalState = useTerminalStateStore((state) => state.clearTerminalState); | |
| 244 | 245 | const setProjectDraftThreadId = useComposerDraftStore((store) => store.setProjectDraftThreadId); |
| 245 | 246 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); |
| 246 | 247 | const clearProjectDraftThreadId = useComposerDraftStore( |
| ... | ... | @@ -578,6 +579,7 @@ export default function Sidebar() { |
| 578 | 579 | }); |
| 579 | 580 | clearComposerDraftForThread(threadId); |
| 580 | 581 | clearProjectDraftThreadById(thread.projectId, thread.id); |
| 582 | clearTerminalState(threadId); | |
| 581 | 583 | if (shouldNavigateToFallback) { |
| 582 | 584 | if (fallbackThreadId) { |
| 583 | 585 | void navigate({ |
| ... | ... | @@ -619,6 +621,7 @@ export default function Sidebar() { |
| 619 | 621 | appSettings.confirmThreadDelete, |
| 620 | 622 | clearComposerDraftForThread, |
| 621 | 623 | clearProjectDraftThreadById, |
| 624 | clearTerminalState, | |
| 622 | 625 | markThreadUnread, |
| 623 | 626 | navigate, |
| 624 | 627 | projects, |
apps/web/src/routes/__root.tsx+9| ... | ... | @@ -143,11 +143,20 @@ function EventRouter() { |
| 143 | 143 | let syncing = false; |
| 144 | 144 | let pending = false; |
| 145 | 145 | |
| 146 | const removeOrphanedTerminalStates = | |
| 147 | useTerminalStateStore.getState().removeOrphanedTerminalStates; | |
| 148 | ||
| 146 | 149 | const flushSnapshotSync = async (): Promise<void> => { |
| 147 | 150 | const snapshot = await api.orchestration.getSnapshot(); |
| 148 | 151 | if (disposed) return; |
| 149 | 152 | latestSequence = Math.max(latestSequence, snapshot.snapshotSequence); |
| 150 | 153 | 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); | |
| 151 | 160 | if (pending) { |
| 152 | 161 | pending = false; |
| 153 | 162 | await flushSnapshotSync(); |
apps/web/src/terminalStateStore.ts+13| ... | ... | @@ -461,6 +461,7 @@ interface TerminalStateStoreState { |
| 461 | 461 | hasRunningSubprocess: boolean, |
| 462 | 462 | ) => void; |
| 463 | 463 | clearTerminalState: (threadId: ThreadId) => void; |
| 464 | removeOrphanedTerminalStates: (activeThreadIds: Set<ThreadId>) => void; | |
| 464 | 465 | } |
| 465 | 466 | |
| 466 | 467 | export const useTerminalStateStore = create<TerminalStateStoreState>()( |
| ... | ... | @@ -505,6 +506,18 @@ export const useTerminalStateStore = create<TerminalStateStoreState>()( |
| 505 | 506 | ), |
| 506 | 507 | clearTerminalState: (threadId) => |
| 507 | 508 | 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 | }), | |
| 508 | 521 | }; |
| 509 | 522 | }, |
| 510 | 523 | { |