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 {...@@ -38,7 +38,7 @@ import type {
38 TerminalWriteInput,38 TerminalWriteInput,
39} from "@t3tools/contracts";39} from "@t3tools/contracts";
40import { TerminalManager, type TerminalManagerShape } from "./terminal/Services/Manager";40import { TerminalManager, type TerminalManagerShape } from "./terminal/Services/Manager";
41import { SqlitePersistenceMemory } from "./persistence/Layers/Sqlite";41import { makeSqlitePersistenceLive, SqlitePersistenceMemory } from "./persistence/Layers/Sqlite";
42import { SqlClient } from "effect/unstable/sql";42import { SqlClient } from "effect/unstable/sql";
43import { ProviderService, type ProviderServiceShape } from "./provider/Services/ProviderService";43import { ProviderService, type ProviderServiceShape } from "./provider/Services/ProviderService";
44import { Open, type OpenShape } from "./open";44import { Open, type OpenShape } from "./open";
...@@ -506,6 +506,61 @@ describe("WebSocket Server", () => {...@@ -506,6 +506,61 @@ describe("WebSocket Server", () => {
506 );506 );
507 });507 });
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
509 it("logs outbound websocket push events in dev mode", async () => {564 it("logs outbound websocket push events in dev mode", async () => {
510 const logSpy = vi.spyOn(console, "log").mockImplementation(() => {565 const logSpy = vi.spyOn(console, "log").mockImplementation(() => {
511 // Keep test output clean while verifying websocket logs.566 // 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<...@@ -388,6 +388,9 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
388 });388 });
389 welcomeBootstrapProjectId = bootstrapProjectId;389 welcomeBootstrapProjectId = bootstrapProjectId;
390 welcomeBootstrapThreadId = threadId;390 welcomeBootstrapThreadId = threadId;
391 } else {
392 welcomeBootstrapProjectId = bootstrapProjectId;
393 welcomeBootstrapThreadId = existingThread.id;
391 }394 }
392 }).pipe(395 }).pipe(
393 Effect.mapError(396 Effect.mapError(
apps/web/src/routes/__root.tsx+4-1
...@@ -194,6 +194,9 @@ function EventRouter() {...@@ -194,6 +194,9 @@ function EventRouter() {
194 const unsubWelcome = onServerWelcome((payload) => {194 const unsubWelcome = onServerWelcome((payload) => {
195 void (async () => {195 void (async () => {
196 await syncSnapshot();196 await syncSnapshot();
197 if (disposed) {
198 return;
199 }
197200
198 if (!payload.bootstrapProjectId || !payload.bootstrapThreadId) {201 if (!payload.bootstrapProjectId || !payload.bootstrapThreadId) {
199 return;202 return;
...@@ -209,12 +212,12 @@ function EventRouter() {...@@ -209,12 +212,12 @@ function EventRouter() {
209 if (handledBootstrapThreadIdRef.current === payload.bootstrapThreadId) {212 if (handledBootstrapThreadIdRef.current === payload.bootstrapThreadId) {
210 return;213 return;
211 }214 }
212 handledBootstrapThreadIdRef.current = payload.bootstrapThreadId;
213 await navigate({215 await navigate({
214 to: "/$threadId",216 to: "/$threadId",
215 params: { threadId: payload.bootstrapThreadId },217 params: { threadId: payload.bootstrapThreadId },
216 replace: true,218 replace: true,
217 });219 });
220 handledBootstrapThreadIdRef.current = payload.bootstrapThreadId;
218 })().catch(() => undefined);221 })().catch(() => undefined);
219 });222 });
220 const unsubServerConfigUpdated = onServerConfigUpdated((payload) => {223 const unsubServerConfigUpdated = onServerConfigUpdated((payload) => {