From 49472da6699256f6aafc88820e482e403d0ab693 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 20:34:52 -0800 Subject: [PATCH] Return existing bootstrap thread IDs in welcome and guard root navigatio - Server welcome now includes bootstrap project/thread IDs when cwd project/thread already exist - Added regression test for reconnecting with persisted bootstrap state - Root route ignores post-dispose welcome handling and marks thread as handled after navigate --- apps/server/src/wsServer.test.ts | 57 +++++++++++++++++++++++++++++++- apps/server/src/wsServer.ts | 3 ++ apps/web/src/routes/__root.tsx | 5 ++- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts index 3d8f53ec6957b7198a5992ed5257821e66685467..e0c91312a02109c1b7e96a473bfe6c95dc59af02 100644 --- a/apps/server/src/wsServer.test.ts +++ b/apps/server/src/wsServer.test.ts @@ -38,7 +38,7 @@ import type { TerminalWriteInput, } from "@t3tools/contracts"; import { TerminalManager, type TerminalManagerShape } from "./terminal/Services/Manager"; -import { SqlitePersistenceMemory } from "./persistence/Layers/Sqlite"; +import { makeSqlitePersistenceLive, SqlitePersistenceMemory } from "./persistence/Layers/Sqlite"; import { SqlClient } from "effect/unstable/sql"; import { ProviderService, type ProviderServiceShape } from "./provider/Services/ProviderService"; import { Open, type OpenShape } from "./open"; @@ -506,6 +506,61 @@ describe("WebSocket Server", () => { ); }); + it("includes bootstrap ids in welcome when cwd project and thread already exist", async () => { + const stateDir = makeTempDir("t3code-state-bootstrap-existing-"); + const persistenceLayer = makeSqlitePersistenceLive( + path.join(stateDir, "state.sqlite"), + ) as unknown as Layer.Layer; + const cwd = "/test/bootstrap-existing"; + + server = await createTestServer({ + cwd, + stateDir, + persistenceLayer, + autoBootstrapProjectFromCwd: true, + }); + let addr = server.address(); + let port = typeof addr === "object" && addr !== null ? addr.port : 0; + expect(port).toBeGreaterThan(0); + + const firstWs = await connectWs(port); + connections.push(firstWs); + const firstWelcome = (await waitForMessage(firstWs)) as WsPush; + const firstBootstrapProjectId = (firstWelcome.data as { bootstrapProjectId?: string }) + .bootstrapProjectId; + const firstBootstrapThreadId = (firstWelcome.data as { bootstrapThreadId?: string }) + .bootstrapThreadId; + expect(firstBootstrapProjectId).toBeDefined(); + expect(firstBootstrapThreadId).toBeDefined(); + + firstWs.close(); + await closeTestServer(); + server = null; + + server = await createTestServer({ + cwd, + stateDir, + persistenceLayer, + autoBootstrapProjectFromCwd: true, + }); + addr = server.address(); + port = typeof addr === "object" && addr !== null ? addr.port : 0; + expect(port).toBeGreaterThan(0); + + const secondWs = await connectWs(port); + connections.push(secondWs); + const secondWelcome = (await waitForMessage(secondWs)) as WsPush; + expect(secondWelcome.channel).toBe(WS_CHANNELS.serverWelcome); + expect(secondWelcome.data).toEqual( + expect.objectContaining({ + cwd, + projectName: "bootstrap-existing", + bootstrapProjectId: firstBootstrapProjectId, + bootstrapThreadId: firstBootstrapThreadId, + }), + ); + }); + it("logs outbound websocket push events in dev mode", async () => { const logSpy = vi.spyOn(console, "log").mockImplementation(() => { // Keep test output clean while verifying websocket logs. diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index 0100141017407fe598e3deccbfeff7c5cdb83308..f00a99019c16ae0173c88d3478e4fa7379776310 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -388,6 +388,9 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< }); welcomeBootstrapProjectId = bootstrapProjectId; welcomeBootstrapThreadId = threadId; + } else { + welcomeBootstrapProjectId = bootstrapProjectId; + welcomeBootstrapThreadId = existingThread.id; } }).pipe( Effect.mapError( diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index a7fbeb3395bedfac5ae1060cb03780e47c5c55d8..000e132048071c15a26c393ea514572d9f328a4e 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -194,6 +194,9 @@ function EventRouter() { const unsubWelcome = onServerWelcome((payload) => { void (async () => { await syncSnapshot(); + if (disposed) { + return; + } if (!payload.bootstrapProjectId || !payload.bootstrapThreadId) { return; @@ -209,12 +212,12 @@ function EventRouter() { if (handledBootstrapThreadIdRef.current === payload.bootstrapThreadId) { return; } - handledBootstrapThreadIdRef.current = payload.bootstrapThreadId; await navigate({ to: "/$threadId", params: { threadId: payload.bootstrapThreadId }, replace: true, }); + handledBootstrapThreadIdRef.current = payload.bootstrapThreadId; })().catch(() => undefined); }); const unsubServerConfigUpdated = onServerConfigUpdated((payload) => { -- 2.54.0