authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-26 20:34:52-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-26 21:05:53-08:00
log49472da6699256f6aafc88820e482e403d0ab693
tree345e17fb85907e044c1d9bea92d4392b203bdb78
parent69a2eac2ff1ba0af1afd0334514789340051cab2

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

3 files changed, 63 insertions(+), 2 deletions(-)

apps/server/src/wsServer.test.ts+56-1
......@@ -38,7 +38,7 @@ import type {
3838 TerminalWriteInput,
3939} from "@t3tools/contracts";
4040import { TerminalManager, type TerminalManagerShape } from "./terminal/Services/Manager";
41import { SqlitePersistenceMemory } from "./persistence/Layers/Sqlite";
41import { makeSqlitePersistenceLive, SqlitePersistenceMemory } from "./persistence/Layers/Sqlite";
4242import { SqlClient } from "effect/unstable/sql";
4343import { ProviderService, type ProviderServiceShape } from "./provider/Services/ProviderService";
4444import { Open, type OpenShape } from "./open";
......@@ -506,6 +506,61 @@ describe("WebSocket Server", () => {
506506 );
507507 });
508508
509 it("includes bootstrap ids in welcome when cwd project and thread already exist", async () => {
510 const stateDir = makeTempDir("t3code-state-bootstrap-existing-");
511 const persistenceLayer = makeSqlitePersistenceLive(
512 path.join(stateDir, "state.sqlite"),
513 ) as unknown as Layer.Layer<SqlClient.SqlClient, never>;
514 const cwd = "/test/bootstrap-existing";
515
516 server = await createTestServer({
517 cwd,
518 stateDir,
519 persistenceLayer,
520 autoBootstrapProjectFromCwd: true,
521 });
522 let addr = server.address();
523 let port = typeof addr === "object" && addr !== null ? addr.port : 0;
524 expect(port).toBeGreaterThan(0);
525
526 const firstWs = await connectWs(port);
527 connections.push(firstWs);
528 const firstWelcome = (await waitForMessage(firstWs)) as WsPush;
529 const firstBootstrapProjectId = (firstWelcome.data as { bootstrapProjectId?: string })
530 .bootstrapProjectId;
531 const firstBootstrapThreadId = (firstWelcome.data as { bootstrapThreadId?: string })
532 .bootstrapThreadId;
533 expect(firstBootstrapProjectId).toBeDefined();
534 expect(firstBootstrapThreadId).toBeDefined();
535
536 firstWs.close();
537 await closeTestServer();
538 server = null;
539
540 server = await createTestServer({
541 cwd,
542 stateDir,
543 persistenceLayer,
544 autoBootstrapProjectFromCwd: true,
545 });
546 addr = server.address();
547 port = typeof addr === "object" && addr !== null ? addr.port : 0;
548 expect(port).toBeGreaterThan(0);
549
550 const secondWs = await connectWs(port);
551 connections.push(secondWs);
552 const secondWelcome = (await waitForMessage(secondWs)) as WsPush;
553 expect(secondWelcome.channel).toBe(WS_CHANNELS.serverWelcome);
554 expect(secondWelcome.data).toEqual(
555 expect.objectContaining({
556 cwd,
557 projectName: "bootstrap-existing",
558 bootstrapProjectId: firstBootstrapProjectId,
559 bootstrapThreadId: firstBootstrapThreadId,
560 }),
561 );
562 });
563
509564 it("logs outbound websocket push events in dev mode", async () => {
510565 const logSpy = vi.spyOn(console, "log").mockImplementation(() => {
511566 // Keep test output clean while verifying websocket logs.
apps/server/src/wsServer.ts+3
......@@ -388,6 +388,9 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
388388 });
389389 welcomeBootstrapProjectId = bootstrapProjectId;
390390 welcomeBootstrapThreadId = threadId;
391 } else {
392 welcomeBootstrapProjectId = bootstrapProjectId;
393 welcomeBootstrapThreadId = existingThread.id;
391394 }
392395 }).pipe(
393396 Effect.mapError(
apps/web/src/routes/__root.tsx+4-1
......@@ -194,6 +194,9 @@ function EventRouter() {
194194 const unsubWelcome = onServerWelcome((payload) => {
195195 void (async () => {
196196 await syncSnapshot();
197 if (disposed) {
198 return;
199 }
197200
198201 if (!payload.bootstrapProjectId || !payload.bootstrapThreadId) {
199202 return;
......@@ -209,12 +212,12 @@ function EventRouter() {
209212 if (handledBootstrapThreadIdRef.current === payload.bootstrapThreadId) {
210213 return;
211214 }
212 handledBootstrapThreadIdRef.current = payload.bootstrapThreadId;
213215 await navigate({
214216 to: "/$threadId",
215217 params: { threadId: payload.bootstrapThreadId },
216218 replace: true,
217219 });
220 handledBootstrapThreadIdRef.current = payload.bootstrapThreadId;
218221 })().catch(() => undefined);
219222 });
220223 const unsubServerConfigUpdated = onServerConfigUpdated((payload) => {