Skip to content

Commit 821184b

Browse files
author
buihongduc132
committed
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.
1 parent 82636e1 commit 821184b

2 files changed

Lines changed: 97 additions & 1 deletion

File tree

packages/server/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ export interface ServerOptions {
8181
onReady?: (url: string, isRemote: boolean, port: number) => void;
8282
/** OpenCode client for querying available agents (OpenCode only) */
8383
opencodeClient?: OpencodeClient;
84+
/** Optional session ID for isolating storage and decisions per session */
85+
sessionId?: string;
86+
/** Working directory for the project (defaults to process.cwd()) */
87+
cwd?: string;
8488
/** When set to "archive", server runs in read-only archive browser mode */
8589
mode?: "archive";
8690
/** Custom plan save path — used by archive mode to find saved plans */
@@ -94,6 +98,10 @@ export interface ServerResult {
9498
url: string;
9599
/** Whether running in remote mode */
96100
isRemote: boolean;
101+
/** Session ID (provided or auto-generated) */
102+
sessionId: string;
103+
/** Working directory for the project */
104+
cwd: string;
97105
/** Wait for user decision (approve/deny) */
98106
waitForDecision: () => Promise<{
99107
approved: boolean;
@@ -125,7 +133,9 @@ const RETRY_DELAY_MS = 500;
125133
export async function startPlannotatorServer(
126134
options: ServerOptions
127135
): Promise<ServerResult> {
128-
const { plan, origin, htmlContent, permissionMode, sharingEnabled = true, shareBaseUrl, pasteApiUrl, onReady, mode, customPlanPath } = options;
136+
const { plan, origin, htmlContent, permissionMode, sharingEnabled = true, shareBaseUrl, pasteApiUrl, onReady, mode, customPlanPath, sessionId: optSessionId, cwd: optCwd } = options;
137+
const sessionId = optSessionId ?? crypto.randomUUID();
138+
const cwd = optCwd ?? process.cwd();
129139

130140
const isRemote = isRemoteSession();
131141
const configuredPort = getServerPort();
@@ -618,6 +628,8 @@ export async function startPlannotatorServer(
618628
port,
619629
url: serverUrl,
620630
isRemote,
631+
sessionId,
632+
cwd,
621633
waitForDecision: () => decisionPromise,
622634
...(donePromise && { waitForDone: () => donePromise }),
623635
stop: () => server.stop(),
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* PR F: Tests for optional sessionId and cwd on ServerOptions
3+
*
4+
* Adding optional sessionId/cwd fields enables future session isolation
5+
* without breaking any existing callers.
6+
*/
7+
import { describe, test, expect, afterAll, beforeAll } from "bun:test";
8+
9+
describe("optional sessionId/cwd on ServerOptions", () => {
10+
const controllers: AbortController[] = [];
11+
let savedPort: string | undefined;
12+
let savedRemote: string | undefined;
13+
14+
beforeAll(() => {
15+
savedPort = process.env.PLANNOTATOR_PORT;
16+
savedRemote = process.env.PLANNOTATOR_REMOTE;
17+
delete process.env.PLANNOTATOR_PORT;
18+
delete process.env.PLANNOTATOR_REMOTE;
19+
delete process.env.PLANNOTATOR_SERVER_URL;
20+
});
21+
22+
afterAll(() => {
23+
for (const c of controllers) c.abort();
24+
if (savedPort) process.env.PLANNOTATOR_PORT = savedPort;
25+
if (savedRemote) process.env.PLANNOTATOR_REMOTE = savedRemote;
26+
});
27+
28+
test("ServerResult includes sessionId when provided", async () => {
29+
const { startPlannotatorServer } = await import("./index");
30+
31+
const controller = new AbortController();
32+
controllers.push(controller);
33+
const server = await startPlannotatorServer({
34+
plan: "# Test",
35+
signal: controller.signal,
36+
sessionId: "my-custom-session-id",
37+
});
38+
39+
expect(server.sessionId).toBe("my-custom-session-id");
40+
});
41+
42+
test("ServerResult auto-generates sessionId when not provided", async () => {
43+
const { startPlannotatorServer } = await import("./index");
44+
45+
const controller = new AbortController();
46+
controllers.push(controller);
47+
const server = await startPlannotatorServer({
48+
plan: "# Test",
49+
signal: controller.signal,
50+
});
51+
52+
expect(server.sessionId).toBeDefined();
53+
expect(server.sessionId.length).toBeGreaterThan(0);
54+
// Should be a UUID format
55+
expect(server.sessionId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
56+
});
57+
58+
test("ServerResult includes cwd from options", async () => {
59+
const { startPlannotatorServer } = await import("./index");
60+
61+
const controller = new AbortController();
62+
controllers.push(controller);
63+
const server = await startPlannotatorServer({
64+
plan: "# Test",
65+
signal: controller.signal,
66+
cwd: "/tmp/custom-cwd",
67+
});
68+
69+
expect(server.cwd).toBe("/tmp/custom-cwd");
70+
});
71+
72+
test("ServerResult defaults cwd to process.cwd()", async () => {
73+
const { startPlannotatorServer } = await import("./index");
74+
75+
const controller = new AbortController();
76+
controllers.push(controller);
77+
const server = await startPlannotatorServer({
78+
plan: "# Test",
79+
signal: controller.signal,
80+
});
81+
82+
expect(server.cwd).toBe(process.cwd());
83+
});
84+
});

0 commit comments

Comments
 (0)