From 821184b13205352535b2568b8d3e7a211fbf8ce9 Mon Sep 17 00:00:00 2001 From: buihongduc132 Date: Tue, 19 May 2026 02:54:06 +0700 Subject: [PATCH] feat(server): add optional sessionId and cwd to ServerOptions/ServerResult Adds two optional fields that enable future session isolation without breaking any existing callers: - sessionId: optional string, auto-generates UUID if omitted - cwd: optional string, defaults to process.cwd() Both fields appear on the returned ServerResult so callers can reference the session and working directory after server startup. This is a foundation for: - Concurrent sessions with isolated storage and decisions - Draft scoping per session - Remote client session management Zero behavior change for existing callers. --- packages/server/index.ts | 14 +++- packages/server/session-id-option.test.ts | 84 +++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 packages/server/session-id-option.test.ts diff --git a/packages/server/index.ts b/packages/server/index.ts index 8b1356a49..0f4216612 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -81,6 +81,10 @@ export interface ServerOptions { onReady?: (url: string, isRemote: boolean, port: number) => void; /** OpenCode client for querying available agents (OpenCode only) */ opencodeClient?: OpencodeClient; + /** Optional session ID for isolating storage and decisions per session */ + sessionId?: string; + /** Working directory for the project (defaults to process.cwd()) */ + cwd?: string; /** When set to "archive", server runs in read-only archive browser mode */ mode?: "archive"; /** Custom plan save path — used by archive mode to find saved plans */ @@ -94,6 +98,10 @@ export interface ServerResult { url: string; /** Whether running in remote mode */ isRemote: boolean; + /** Session ID (provided or auto-generated) */ + sessionId: string; + /** Working directory for the project */ + cwd: string; /** Wait for user decision (approve/deny) */ waitForDecision: () => Promise<{ approved: boolean; @@ -125,7 +133,9 @@ const RETRY_DELAY_MS = 500; export async function startPlannotatorServer( options: ServerOptions ): Promise { - const { plan, origin, htmlContent, permissionMode, sharingEnabled = true, shareBaseUrl, pasteApiUrl, onReady, mode, customPlanPath } = options; + const { plan, origin, htmlContent, permissionMode, sharingEnabled = true, shareBaseUrl, pasteApiUrl, onReady, mode, customPlanPath, sessionId: optSessionId, cwd: optCwd } = options; + const sessionId = optSessionId ?? crypto.randomUUID(); + const cwd = optCwd ?? process.cwd(); const isRemote = isRemoteSession(); const configuredPort = getServerPort(); @@ -618,6 +628,8 @@ export async function startPlannotatorServer( port, url: serverUrl, isRemote, + sessionId, + cwd, waitForDecision: () => decisionPromise, ...(donePromise && { waitForDone: () => donePromise }), stop: () => server.stop(), diff --git a/packages/server/session-id-option.test.ts b/packages/server/session-id-option.test.ts new file mode 100644 index 000000000..e57aca40a --- /dev/null +++ b/packages/server/session-id-option.test.ts @@ -0,0 +1,84 @@ +/** + * PR F: Tests for optional sessionId and cwd on ServerOptions + * + * Adding optional sessionId/cwd fields enables future session isolation + * without breaking any existing callers. + */ +import { describe, test, expect, afterAll, beforeAll } from "bun:test"; + +describe("optional sessionId/cwd on ServerOptions", () => { + const controllers: AbortController[] = []; + let savedPort: string | undefined; + let savedRemote: string | undefined; + + beforeAll(() => { + savedPort = process.env.PLANNOTATOR_PORT; + savedRemote = process.env.PLANNOTATOR_REMOTE; + delete process.env.PLANNOTATOR_PORT; + delete process.env.PLANNOTATOR_REMOTE; + delete process.env.PLANNOTATOR_SERVER_URL; + }); + + afterAll(() => { + for (const c of controllers) c.abort(); + if (savedPort) process.env.PLANNOTATOR_PORT = savedPort; + if (savedRemote) process.env.PLANNOTATOR_REMOTE = savedRemote; + }); + + test("ServerResult includes sessionId when provided", async () => { + const { startPlannotatorServer } = await import("./index"); + + const controller = new AbortController(); + controllers.push(controller); + const server = await startPlannotatorServer({ + plan: "# Test", + signal: controller.signal, + sessionId: "my-custom-session-id", + }); + + expect(server.sessionId).toBe("my-custom-session-id"); + }); + + test("ServerResult auto-generates sessionId when not provided", async () => { + const { startPlannotatorServer } = await import("./index"); + + const controller = new AbortController(); + controllers.push(controller); + const server = await startPlannotatorServer({ + plan: "# Test", + signal: controller.signal, + }); + + expect(server.sessionId).toBeDefined(); + expect(server.sessionId.length).toBeGreaterThan(0); + // Should be a UUID format + expect(server.sessionId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/); + }); + + test("ServerResult includes cwd from options", async () => { + const { startPlannotatorServer } = await import("./index"); + + const controller = new AbortController(); + controllers.push(controller); + const server = await startPlannotatorServer({ + plan: "# Test", + signal: controller.signal, + cwd: "/tmp/custom-cwd", + }); + + expect(server.cwd).toBe("/tmp/custom-cwd"); + }); + + test("ServerResult defaults cwd to process.cwd()", async () => { + const { startPlannotatorServer } = await import("./index"); + + const controller = new AbortController(); + controllers.push(controller); + const server = await startPlannotatorServer({ + plan: "# Test", + signal: controller.signal, + }); + + expect(server.cwd).toBe(process.cwd()); + }); +});