| author | |
| committer | |
| log | b9e529e0b56b995728ff10cd1b643df63b436258 |
| tree | 939724f709116797d44f4bf78c0d8d458cc88139 |
| parent | 0f702fcd6271859212f83bc7f7100a339afe95da |
- add `collectActiveTerminalThreadIds` to keep non-deleted server threads plus local draft threads
- use helper in root event router before orphan terminal state removal
- add unit tests for retained and deleted thread behavior3 files changed, 66 insertions(+), 3 deletions(-)
apps/web/src/lib/terminalStateCleanup.test.ts created+32| ... | ... | @@ -0,0 +1,32 @@ |
| 1 | import { ThreadId } from "@t3tools/contracts"; | |
| 2 | import { describe, expect, it } from "vitest"; | |
| 3 | ||
| 4 | import { collectActiveTerminalThreadIds } from "./terminalStateCleanup"; | |
| 5 | ||
| 6 | const threadId = (id: string): ThreadId => ThreadId.makeUnsafe(id); | |
| 7 | ||
| 8 | describe("collectActiveTerminalThreadIds", () => { | |
| 9 | it("retains non-deleted server threads", () => { | |
| 10 | const activeThreadIds = collectActiveTerminalThreadIds({ | |
| 11 | snapshotThreads: [ | |
| 12 | { id: threadId("server-1"), deletedAt: null }, | |
| 13 | { id: threadId("server-2"), deletedAt: null }, | |
| 14 | ], | |
| 15 | draftThreadIds: [], | |
| 16 | }); | |
| 17 | ||
| 18 | expect(activeThreadIds).toEqual(new Set([threadId("server-1"), threadId("server-2")])); | |
| 19 | }); | |
| 20 | ||
| 21 | it("ignores deleted server threads and keeps local draft threads", () => { | |
| 22 | const activeThreadIds = collectActiveTerminalThreadIds({ | |
| 23 | snapshotThreads: [ | |
| 24 | { id: threadId("server-active"), deletedAt: null }, | |
| 25 | { id: threadId("server-deleted"), deletedAt: "2026-03-05T08:00:00.000Z" }, | |
| 26 | ], | |
| 27 | draftThreadIds: [threadId("local-draft")], | |
| 28 | }); | |
| 29 | ||
| 30 | expect(activeThreadIds).toEqual(new Set([threadId("server-active"), threadId("local-draft")])); | |
| 31 | }); | |
| 32 | }); |
apps/web/src/lib/terminalStateCleanup.ts created+25| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | import type { ThreadId } from "@t3tools/contracts"; | |
| 2 | ||
| 3 | interface TerminalRetentionThread { | |
| 4 | id: ThreadId; | |
| 5 | deletedAt: string | null; | |
| 6 | } | |
| 7 | ||
| 8 | interface CollectActiveTerminalThreadIdsInput { | |
| 9 | snapshotThreads: readonly TerminalRetentionThread[]; | |
| 10 | draftThreadIds: Iterable<ThreadId>; | |
| 11 | } | |
| 12 | ||
| 13 | export function collectActiveTerminalThreadIds( | |
| 14 | input: CollectActiveTerminalThreadIdsInput, | |
| 15 | ): Set<ThreadId> { | |
| 16 | const activeThreadIds = new Set<ThreadId>(); | |
| 17 | for (const thread of input.snapshotThreads) { | |
| 18 | if (thread.deletedAt !== null) continue; | |
| 19 | activeThreadIds.add(thread.id); | |
| 20 | } | |
| 21 | for (const draftThreadId of input.draftThreadIds) { | |
| 22 | activeThreadIds.add(draftThreadId); | |
| 23 | } | |
| 24 | return activeThreadIds; | |
| 25 | } |
apps/web/src/routes/__root.tsx+9-3| ... | ... | @@ -14,12 +14,14 @@ import { Button } from "../components/ui/button"; |
| 14 | 14 | import { AnchoredToastProvider, ToastProvider, toastManager } from "../components/ui/toast"; |
| 15 | 15 | import { serverConfigQueryOptions, serverQueryKeys } from "../lib/serverReactQuery"; |
| 16 | 16 | import { readNativeApi } from "../nativeApi"; |
| 17 | import { useComposerDraftStore } from "../composerDraftStore"; | |
| 17 | 18 | import { useStore } from "../store"; |
| 18 | 19 | import { useTerminalStateStore } from "../terminalStateStore"; |
| 19 | 20 | import { preferredTerminalEditor } from "../terminal-links"; |
| 20 | 21 | import { terminalRunningSubprocessFromEvent } from "../terminalActivity"; |
| 21 | 22 | import { onServerConfigUpdated, onServerWelcome } from "../wsNativeApi"; |
| 22 | 23 | import { providerQueryKeys } from "../lib/providerReactQuery"; |
| 24 | import { collectActiveTerminalThreadIds } from "../lib/terminalStateCleanup"; | |
| 23 | 25 | |
| 24 | 26 | export const Route = createRootRouteWithContext<{ |
| 25 | 27 | queryClient: QueryClient; |
| ... | ... | @@ -154,9 +156,13 @@ function EventRouter() { |
| 154 | 156 | if (disposed) return; |
| 155 | 157 | latestSequence = Math.max(latestSequence, snapshot.snapshotSequence); |
| 156 | 158 | syncServerReadModel(snapshot); |
| 157 | const activeThreadIds = new Set( | |
| 158 | snapshot.threads.filter((t) => t.deletedAt === null).map((t) => t.id), | |
| 159 | ); | |
| 159 | const draftThreadIds = Object.keys( | |
| 160 | useComposerDraftStore.getState().draftThreadsByThreadId, | |
| 161 | ) as ThreadId[]; | |
| 162 | const activeThreadIds = collectActiveTerminalThreadIds({ | |
| 163 | snapshotThreads: snapshot.threads, | |
| 164 | draftThreadIds, | |
| 165 | }); | |
| 160 | 166 | removeOrphanedTerminalStates(activeThreadIds); |
| 161 | 167 | if (pending) { |
| 162 | 168 | pending = false; |