From 7276b75dd6d9f2552acd853c68759d7945e79ff6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 18:43:08 -0800 Subject: [PATCH 1/6] Bootstrap default thread and route to it on server welcome - Create or reuse cwd project, create initial thread, and include bootstrap IDs in `server.welcome` - Update web event routing to expand the project and auto-navigate from `/` to the bootstrapped thread - Extend WebSocket welcome contract and add server/web tests for bootstrap payload handling --- apps/server/src/wsServer.test.ts | 43 +++++++++++++++++++++++-- apps/server/src/wsServer.ts | 54 +++++++++++++++++++++++++++----- apps/web/src/routes/__root.tsx | 40 ++++++++++++++++++++--- apps/web/src/store.ts | 9 ++++++ apps/web/src/wsNativeApi.test.ts | 33 +++++++++++++++++-- packages/contracts/src/ws.ts | 4 ++- 6 files changed, 166 insertions(+), 17 deletions(-) diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts index 78394af58557..3d8f53ec6957 100644 --- a/apps/server/src/wsServer.test.ts +++ b/apps/server/src/wsServer.test.ts @@ -448,23 +448,62 @@ describe("WebSocket Server", () => { const ws = await connectWs(port); connections.push(ws); - await waitForMessage(ws); // welcome + const welcome = (await waitForMessage(ws)) as WsPush; // welcome + expect(welcome.channel).toBe(WS_CHANNELS.serverWelcome); + expect(welcome.data).toEqual( + expect.objectContaining({ + cwd: "/test/bootstrap-workspace", + projectName: "bootstrap-workspace", + bootstrapProjectId: expect.any(String), + bootstrapThreadId: expect.any(String), + }), + ); const snapshotResponse = await sendRequest(ws, ORCHESTRATION_WS_METHODS.getSnapshot); expect(snapshotResponse.error).toBeUndefined(); const snapshot = snapshotResponse.result as { - projects: Array<{ workspaceRoot: string; title: string; defaultModel: string | null }>; + projects: Array<{ + id: string; + workspaceRoot: string; + title: string; + defaultModel: string | null; + }>; + threads: Array<{ + id: string; + projectId: string; + title: string; + model: string; + branch: string | null; + worktreePath: string | null; + }>; }; + const bootstrapProjectId = (welcome.data as { bootstrapProjectId?: string }).bootstrapProjectId; + const bootstrapThreadId = (welcome.data as { bootstrapThreadId?: string }).bootstrapThreadId; + expect(bootstrapProjectId).toBeDefined(); + expect(bootstrapThreadId).toBeDefined(); expect(snapshot.projects).toEqual( expect.arrayContaining([ expect.objectContaining({ + id: bootstrapProjectId, workspaceRoot: "/test/bootstrap-workspace", title: "bootstrap-workspace", defaultModel: "gpt-5-codex", }), ]), ); + expect(snapshot.threads).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: bootstrapThreadId, + projectId: bootstrapProjectId, + title: "New thread", + model: "gpt-5-codex", + branch: null, + worktreePath: null, + }), + ]), + ); }); it("logs outbound websocket push events in dev mode", async () => { diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index aa72c851f9d8..010014101740 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -16,6 +16,7 @@ import { ORCHESTRATION_WS_CHANNELS, ORCHESTRATION_WS_METHODS, ProjectId, + ThreadId, TerminalEvent, WS_CHANNELS, WS_METHODS, @@ -337,22 +338,56 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< yield* Scope.provide(orchestrationReactor.start, subscriptionsScope); + let welcomeBootstrapProjectId: ProjectId | undefined; + let welcomeBootstrapThreadId: ThreadId | undefined; + if (autoBootstrapProjectFromCwd) { yield* Effect.gen(function* () { const snapshot = yield* projectionReadModelQuery.getSnapshot(); - const existing = snapshot.projects.find((project) => project.workspaceRoot === cwd); - if (!existing) { + const existingProject = snapshot.projects.find( + (project) => project.workspaceRoot === cwd && project.deletedAt === null, + ); + let bootstrapProjectId: ProjectId; + let bootstrapProjectDefaultModel: string; + + if (!existingProject) { const createdAt = new Date().toISOString(); - const projectName = path.basename(cwd) || "project"; + bootstrapProjectId = ProjectId.makeUnsafe(crypto.randomUUID()); + const bootstrapProjectTitle = path.basename(cwd) || "project"; + bootstrapProjectDefaultModel = "gpt-5-codex"; yield* orchestrationEngine.dispatch({ type: "project.create", commandId: CommandId.makeUnsafe(crypto.randomUUID()), - projectId: ProjectId.makeUnsafe(crypto.randomUUID()), - title: projectName, + projectId: bootstrapProjectId, + title: bootstrapProjectTitle, workspaceRoot: cwd, - defaultModel: "gpt-5-codex", + defaultModel: bootstrapProjectDefaultModel, + createdAt, + }); + } else { + bootstrapProjectId = existingProject.id; + bootstrapProjectDefaultModel = existingProject.defaultModel ?? "gpt-5-codex"; + } + + const existingThread = snapshot.threads.find( + (thread) => thread.projectId === bootstrapProjectId && thread.deletedAt === null, + ); + if (!existingThread) { + const createdAt = new Date().toISOString(); + const threadId = ThreadId.makeUnsafe(crypto.randomUUID()); + yield* orchestrationEngine.dispatch({ + type: "thread.create", + commandId: CommandId.makeUnsafe(crypto.randomUUID()), + threadId, + projectId: bootstrapProjectId, + title: "New thread", + model: bootstrapProjectDefaultModel, + branch: null, + worktreePath: null, createdAt, }); + welcomeBootstrapProjectId = bootstrapProjectId; + welcomeBootstrapThreadId = threadId; } }).pipe( Effect.mapError( @@ -581,7 +616,12 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< const welcome: WsPush = { type: "push", channel: WS_CHANNELS.serverWelcome, - data: { cwd, projectName }, + data: { + cwd, + projectName, + ...(welcomeBootstrapProjectId ? { bootstrapProjectId: welcomeBootstrapProjectId } : {}), + ...(welcomeBootstrapThreadId ? { bootstrapThreadId: welcomeBootstrapThreadId } : {}), + }, }; logOutgoingPush(welcome, 1); ws.send(JSON.stringify(welcome)); diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 45bb836049e6..b21389e3d4dc 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -1,5 +1,11 @@ import { ThreadId } from "@t3tools/contracts"; -import { Outlet, createRootRouteWithContext, type ErrorComponentProps } from "@tanstack/react-router"; +import { + Outlet, + createRootRouteWithContext, + type ErrorComponentProps, + useNavigate, + useRouterState, +} from "@tanstack/react-router"; import { useEffect, useRef } from "react"; import { QueryClient, useQueryClient } from "@tanstack/react-query"; @@ -119,7 +125,10 @@ function errorDetails(error: unknown): string { function EventRouter() { const { dispatch } = useStore(); const queryClient = useQueryClient(); + const navigate = useNavigate(); + const pathname = useRouterState({ select: (state) => state.location.pathname }); const lastConfigIssuesSignatureRef = useRef(null); + const handledBootstrapThreadIdRef = useRef(null); useEffect(() => { const api = readNativeApi(); @@ -180,8 +189,31 @@ function EventRouter() { hasRunningSubprocess, }); }); - const unsubWelcome = onServerWelcome(() => { - void syncSnapshot(); + const unsubWelcome = onServerWelcome((payload) => { + void (async () => { + await syncSnapshot(); + + if (!payload.bootstrapProjectId || !payload.bootstrapThreadId) { + return; + } + dispatch({ + type: "SET_PROJECT_EXPANDED", + projectId: payload.bootstrapProjectId, + expanded: true, + }); + if (pathname !== "/") { + return; + } + if (handledBootstrapThreadIdRef.current === payload.bootstrapThreadId) { + return; + } + handledBootstrapThreadIdRef.current = payload.bootstrapThreadId; + await navigate({ + to: "/$threadId", + params: { threadId: payload.bootstrapThreadId }, + replace: true, + }); + })().catch(() => undefined); }); const unsubServerConfigUpdated = onServerConfigUpdated((payload) => { const signature = JSON.stringify(payload.issues); @@ -232,7 +264,7 @@ function EventRouter() { unsubWelcome(); unsubServerConfigUpdated(); }; - }, [dispatch, queryClient]); + }, [dispatch, navigate, pathname, queryClient]); return null; } diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts index 89b010603736..8a9af566a4f1 100644 --- a/apps/web/src/store.ts +++ b/apps/web/src/store.ts @@ -41,6 +41,7 @@ type Action = terminalId: string; hasRunningSubprocess: boolean; } + | { type: "SET_PROJECT_EXPANDED"; projectId: Project["id"]; expanded: boolean } | { type: "TOGGLE_THREAD_TERMINAL"; threadId: ThreadId } | { type: "SET_THREAD_TERMINAL_OPEN"; threadId: ThreadId; open: boolean } | { type: "SET_THREAD_TERMINAL_HEIGHT"; threadId: ThreadId; height: number } @@ -571,6 +572,14 @@ export function reducer(state: AppState, action: Action): AppState { ), }; + case "SET_PROJECT_EXPANDED": + return { + ...state, + projects: state.projects.map((p) => + p.id === action.projectId ? { ...p, expanded: action.expanded } : p, + ), + }; + case "TOGGLE_THREAD_TERMINAL": return { ...state, diff --git a/apps/web/src/wsNativeApi.test.ts b/apps/web/src/wsNativeApi.test.ts index 357c605996e8..aa266f8e0861 100644 --- a/apps/web/src/wsNativeApi.test.ts +++ b/apps/web/src/wsNativeApi.test.ts @@ -65,16 +65,41 @@ describe("wsNativeApi", () => { emitPush(WS_CHANNELS.serverWelcome, payload); expect(listener).toHaveBeenCalledTimes(1); - expect(listener).toHaveBeenCalledWith(payload); + expect(listener).toHaveBeenCalledWith(expect.objectContaining(payload)); const lateListener = vi.fn(); onServerWelcome(lateListener); expect(lateListener).toHaveBeenCalledTimes(1); - expect(lateListener).toHaveBeenCalledWith(payload); + expect(lateListener).toHaveBeenCalledWith(expect.objectContaining(payload)); expect(warnSpy).not.toHaveBeenCalled(); }); + it("preserves bootstrap ids from server.welcome payloads", async () => { + const { createWsNativeApi, onServerWelcome } = await import("./wsNativeApi"); + + createWsNativeApi(); + const listener = vi.fn(); + onServerWelcome(listener); + + emitPush(WS_CHANNELS.serverWelcome, { + cwd: "/tmp/workspace", + projectName: "t3-code", + bootstrapProjectId: "project-1", + bootstrapThreadId: "thread-1", + }); + + expect(listener).toHaveBeenCalledTimes(1); + expect(listener).toHaveBeenCalledWith( + expect.objectContaining({ + cwd: "/tmp/workspace", + projectName: "t3-code", + bootstrapProjectId: "project-1", + bootstrapThreadId: "thread-1", + }), + ); + }); + it("ignores invalid server.welcome payloads and keeps subscription active", async () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const { createWsNativeApi, onServerWelcome } = await import("./wsNativeApi"); @@ -87,7 +112,9 @@ describe("wsNativeApi", () => { emitPush(WS_CHANNELS.serverWelcome, { cwd: "/tmp/workspace", projectName: "t3-code" }); expect(listener).toHaveBeenCalledTimes(1); - expect(listener).toHaveBeenCalledWith({ cwd: "/tmp/workspace", projectName: "t3-code" }); + expect(listener).toHaveBeenCalledWith( + expect.objectContaining({ cwd: "/tmp/workspace", projectName: "t3-code" }), + ); expect(warnSpy).toHaveBeenCalledTimes(1); expect(warnSpy).toHaveBeenCalledWith("Dropped inbound WebSocket push payload", { reason: "decode-failed", diff --git a/packages/contracts/src/ws.ts b/packages/contracts/src/ws.ts index e8b0e073244e..7f94615bdd47 100644 --- a/packages/contracts/src/ws.ts +++ b/packages/contracts/src/ws.ts @@ -1,5 +1,5 @@ import { Schema, Struct } from "effect"; -import { TrimmedNonEmptyString } from "./baseSchemas"; +import { ProjectId, ThreadId, TrimmedNonEmptyString } from "./baseSchemas"; import { ClientOrchestrationCommand, @@ -163,5 +163,7 @@ export type WsResponse = typeof WsResponse.Type; export const WsWelcomePayload = Schema.Struct({ cwd: TrimmedNonEmptyString, projectName: TrimmedNonEmptyString, + bootstrapProjectId: Schema.optional(ProjectId), + bootstrapThreadId: Schema.optional(ThreadId), }); export type WsWelcomePayload = typeof WsWelcomePayload.Type; From a8d0e9b06b69df7e33ce6cdd94400b6f72400e33 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 18:58:59 -0800 Subject: [PATCH 2/6] Use pathname ref in EventRouter bootstrap navigation - Track latest `pathname` in a ref for bootstrap event handling - Prevent effect resubscriptions by removing `pathname` from deps - Keep auto-navigation gated to root route using current path --- apps/web/src/routes/__root.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index b21389e3d4dc..a7fbeb3395be 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -127,8 +127,10 @@ function EventRouter() { const queryClient = useQueryClient(); const navigate = useNavigate(); const pathname = useRouterState({ select: (state) => state.location.pathname }); + const pathnameRef = useRef(pathname); const lastConfigIssuesSignatureRef = useRef(null); const handledBootstrapThreadIdRef = useRef(null); + pathnameRef.current = pathname; useEffect(() => { const api = readNativeApi(); @@ -201,7 +203,7 @@ function EventRouter() { projectId: payload.bootstrapProjectId, expanded: true, }); - if (pathname !== "/") { + if (pathnameRef.current !== "/") { return; } if (handledBootstrapThreadIdRef.current === payload.bootstrapThreadId) { @@ -264,7 +266,7 @@ function EventRouter() { unsubWelcome(); unsubServerConfigUpdated(); }; - }, [dispatch, navigate, pathname, queryClient]); + }, [dispatch, navigate, queryClient]); return null; } From cd685c9a64478446ca3ccda6b73bcaea08b6c577 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 20:34:52 -0800 Subject: [PATCH 3/6] 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 3d8f53ec6957..e0c91312a021 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 010014101740..f00a99019c16 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 a7fbeb3395be..000e13204807 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) => { From 080752a3f76657416f95c4aaf83b5fd247885630 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 20:43:54 -0800 Subject: [PATCH 4/6] Recover bootstrap thread from snapshot when IDs are missing - Fetch orchestration snapshot when bootstrap project/thread IDs are absent - Resolve project by current workspace and choose latest non-deleted thread - Continue auto-navigation and expansion using resolved fallback IDs --- apps/web/src/routes/__root.tsx | 36 +++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 000e13204807..80bf213b40a8 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -198,26 +198,52 @@ function EventRouter() { return; } - if (!payload.bootstrapProjectId || !payload.bootstrapThreadId) { + const fallbackSnapshot = + payload.bootstrapProjectId && payload.bootstrapThreadId + ? null + : await api.orchestration.getSnapshot().catch(() => null); + if (disposed) { + return; + } + + const bootstrapProjectId = + payload.bootstrapProjectId ?? + fallbackSnapshot?.projects.find( + (project) => project.workspaceRoot === payload.cwd && project.deletedAt === null, + )?.id; + if (!bootstrapProjectId) { return; } dispatch({ type: "SET_PROJECT_EXPANDED", - projectId: payload.bootstrapProjectId, + projectId: bootstrapProjectId, expanded: true, }); + + const bootstrapThreadId = + payload.bootstrapThreadId ?? + fallbackSnapshot?.threads + .filter((thread) => thread.projectId === bootstrapProjectId && thread.deletedAt === null) + .toSorted((a, b) => { + const byDate = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + if (byDate !== 0) return byDate; + return b.id.localeCompare(a.id); + })[0]?.id; + if (!bootstrapThreadId) { + return; + } if (pathnameRef.current !== "/") { return; } - if (handledBootstrapThreadIdRef.current === payload.bootstrapThreadId) { + if (handledBootstrapThreadIdRef.current === bootstrapThreadId) { return; } await navigate({ to: "/$threadId", - params: { threadId: payload.bootstrapThreadId }, + params: { threadId: bootstrapThreadId }, replace: true, }); - handledBootstrapThreadIdRef.current = payload.bootstrapThreadId; + handledBootstrapThreadIdRef.current = bootstrapThreadId; })().catch(() => undefined); }); const unsubServerConfigUpdated = onServerConfigUpdated((payload) => { From 9e8363ceca733d63cdad0b5d8bbcda0ca4da9649 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 20:48:32 -0800 Subject: [PATCH 5/6] Require bootstrap IDs for root thread auto-navigation - Remove snapshot fallback lookup for missing bootstrap project/thread IDs - Only expand project and navigate when server payload includes both IDs --- apps/web/src/routes/__root.tsx | 35 +++++----------------------------- 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 80bf213b40a8..51e4e9b01393 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -198,52 +198,27 @@ function EventRouter() { return; } - const fallbackSnapshot = - payload.bootstrapProjectId && payload.bootstrapThreadId - ? null - : await api.orchestration.getSnapshot().catch(() => null); - if (disposed) { - return; - } - - const bootstrapProjectId = - payload.bootstrapProjectId ?? - fallbackSnapshot?.projects.find( - (project) => project.workspaceRoot === payload.cwd && project.deletedAt === null, - )?.id; - if (!bootstrapProjectId) { + if (!payload.bootstrapProjectId || !payload.bootstrapThreadId) { return; } dispatch({ type: "SET_PROJECT_EXPANDED", - projectId: bootstrapProjectId, + projectId: payload.bootstrapProjectId, expanded: true, }); - const bootstrapThreadId = - payload.bootstrapThreadId ?? - fallbackSnapshot?.threads - .filter((thread) => thread.projectId === bootstrapProjectId && thread.deletedAt === null) - .toSorted((a, b) => { - const byDate = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); - if (byDate !== 0) return byDate; - return b.id.localeCompare(a.id); - })[0]?.id; - if (!bootstrapThreadId) { - return; - } if (pathnameRef.current !== "/") { return; } - if (handledBootstrapThreadIdRef.current === bootstrapThreadId) { + if (handledBootstrapThreadIdRef.current === payload.bootstrapThreadId) { return; } await navigate({ to: "/$threadId", - params: { threadId: bootstrapThreadId }, + params: { threadId: payload.bootstrapThreadId }, replace: true, }); - handledBootstrapThreadIdRef.current = bootstrapThreadId; + handledBootstrapThreadIdRef.current = payload.bootstrapThreadId; })().catch(() => undefined); }); const unsubServerConfigUpdated = onServerConfigUpdated((payload) => { From 8effcf380afe007abf39444db659b777456aae8d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 21:01:54 -0800 Subject: [PATCH 6/6] Provide NodeServices layer in wsServer bootstrap test - inject `NodeServices.layer` into sqlite persistence setup in `wsServer.test.ts` - fixes test runtime dependencies for existing-state bootstrap path --- apps/server/src/wsServer.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts index e0c91312a021..d6ba9229b589 100644 --- a/apps/server/src/wsServer.test.ts +++ b/apps/server/src/wsServer.test.ts @@ -3,6 +3,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import * as NodeServices from "@effect/platform-node/NodeServices"; import { Effect, Exit, Layer, PubSub, Scope, Stream } from "effect"; import { describe, expect, it, afterEach, vi } from "vitest"; import { createServer } from "./wsServer"; @@ -510,7 +511,7 @@ describe("WebSocket Server", () => { const stateDir = makeTempDir("t3code-state-bootstrap-existing-"); const persistenceLayer = makeSqlitePersistenceLive( path.join(stateDir, "state.sqlite"), - ) as unknown as Layer.Layer; + ).pipe(Layer.provide(NodeServices.layer)) as unknown as Layer.Layer; const cwd = "/test/bootstrap-existing"; server = await createTestServer({