From e20583aac8331d9e4052e2d1147251205d37dffe Mon Sep 17 00:00:00 2001 From: Daniel Nouri Date: Fri, 30 Jan 2026 01:41:58 +0100 Subject: [PATCH] feat(coding-agent): add set_session_name RPC command (#1075) - Add set_session_name command with empty name validation - Expose sessionName in get_state response - Add setSessionName() to AgentSession and RpcClient - Document in docs/rpc.md --- packages/coding-agent/docs/rpc.md | 22 ++++++++++++- .../coding-agent/src/core/agent-session.ts | 12 +++++++ .../coding-agent/src/modes/rpc/rpc-client.ts | 7 ++++ .../coding-agent/src/modes/rpc/rpc-mode.ts | 10 ++++++ .../coding-agent/src/modes/rpc/rpc-types.ts | 3 ++ packages/coding-agent/test/rpc.test.ts | 33 +++++++++++++++++++ 6 files changed, 86 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/docs/rpc.md b/packages/coding-agent/docs/rpc.md index 5a0f9123..ee4b9f61 100644 --- a/packages/coding-agent/docs/rpc.md +++ b/packages/coding-agent/docs/rpc.md @@ -154,6 +154,7 @@ Response: "followUpMode": "one-at-a-time", "sessionFile": "/path/to/session.jsonl", "sessionId": "abc123", + "sessionName": "my-feature-work", "autoCompactionEnabled": true, "messageCount": 5, "pendingMessageCount": 0 @@ -161,7 +162,7 @@ Response: } ``` -The `model` field is a full [Model](#model) object or `null`. +The `model` field is a full [Model](#model) object or `null`. The `sessionName` field is the display name set via `set_session_name`, or omitted if not set. #### get_messages @@ -612,6 +613,25 @@ Response: Returns `{"text": null}` if no assistant messages exist. +#### set_session_name + +Set a display name for the current session. The name appears in session listings and helps identify sessions. + +```json +{"type": "set_session_name", "name": "my-feature-work"} +``` + +Response: +```json +{ + "type": "response", + "command": "set_session_name", + "success": true +} +``` + +The current session name is available via `get_state` in the `sessionName` field. + ### Commands #### get_commands diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 8a7a2e52..3d8468af 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -590,6 +590,11 @@ export class AgentSession { return this.sessionManager.getSessionId(); } + /** Current session display name, if set */ + get sessionName(): string | undefined { + return this.sessionManager.getSessionName(); + } + /** Scoped models for cycling (from --models flag) */ get scopedModels(): ReadonlyArray<{ model: Model; thinkingLevel: ThinkingLevel }> { return this._scopedModels; @@ -2181,6 +2186,13 @@ export class AgentSession { return true; } + /** + * Set a display name for the current session. + */ + setSessionName(name: string): void { + this.sessionManager.appendSessionInfo(name); + } + /** * Create a fork from a specific entry. * Emits before_fork/fork session events to extensions. diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index 6735d031..91d2b0f8 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -362,6 +362,13 @@ export class RpcClient { return this.getData<{ text: string | null }>(response).text; } + /** + * Set the session display name. + */ + async setSessionName(name: string): Promise { + await this.send({ type: "set_session_name", name }); + } + /** * Get all messages in the session. */ diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 00c36ee8..994d50e0 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -349,6 +349,7 @@ export async function runRpcMode(session: AgentSession): Promise { followUpMode: session.followUpMode, sessionFile: session.sessionFile, sessionId: session.sessionId, + sessionName: session.sessionName, autoCompactionEnabled: session.autoCompactionEnabled, messageCount: session.messages.length, pendingMessageCount: session.pendingMessageCount, @@ -490,6 +491,15 @@ export async function runRpcMode(session: AgentSession): Promise { return success(id, "get_last_assistant_text", { text }); } + case "set_session_name": { + const name = command.name.trim(); + if (!name) { + return error(id, "set_session_name", "Session name cannot be empty"); + } + session.setSessionName(name); + return success(id, "set_session_name"); + } + // ================================================================= // Messages // ================================================================= diff --git a/packages/coding-agent/src/modes/rpc/rpc-types.ts b/packages/coding-agent/src/modes/rpc/rpc-types.ts index 7e8ccb1f..eace0811 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-types.ts @@ -58,6 +58,7 @@ export type RpcCommand = | { id?: string; type: "fork"; entryId: string } | { id?: string; type: "get_fork_messages" } | { id?: string; type: "get_last_assistant_text" } + | { id?: string; type: "set_session_name"; name: string } // Messages | { id?: string; type: "get_messages" } @@ -96,6 +97,7 @@ export interface RpcSessionState { followUpMode: "all" | "one-at-a-time"; sessionFile?: string; sessionId: string; + sessionName?: string; autoCompactionEnabled: boolean; messageCount: number; pendingMessageCount: number; @@ -185,6 +187,7 @@ export type RpcResponse = success: true; data: { text: string | null }; } + | { id?: string; type: "response"; command: "set_session_name"; success: true } // Messages | { id?: string; type: "response"; command: "get_messages"; success: true; data: { messages: AgentMessage[] } } diff --git a/packages/coding-agent/test/rpc.test.ts b/packages/coding-agent/test/rpc.test.ts index 240400b2..41a4f1dc 100644 --- a/packages/coding-agent/test/rpc.test.ts +++ b/packages/coding-agent/test/rpc.test.ts @@ -282,4 +282,37 @@ describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_T text = await client.getLastAssistantText(); expect(text).toContain("test123"); }, 90000); + + test("should set and get session name", async () => { + await client.start(); + + // Initially undefined + let state = await client.getState(); + expect(state.sessionName).toBeUndefined(); + + // Set name + await client.setSessionName("my-test-session"); + + // Verify via state + state = await client.getState(); + expect(state.sessionName).toBe("my-test-session"); + + // Wait for file writes + await new Promise((resolve) => setTimeout(resolve, 200)); + + // Verify session_info entry in session file + const sessionsPath = join(sessionDir, "sessions"); + const sessionDirs = readdirSync(sessionsPath); + const cwdSessionDir = join(sessionsPath, sessionDirs[0]); + const sessionFiles = readdirSync(cwdSessionDir).filter((f) => f.endsWith(".jsonl")); + const sessionContent = readFileSync(join(cwdSessionDir, sessionFiles[0]), "utf8"); + const entries = sessionContent + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + + const sessionInfoEntries = entries.filter((e: { type: string }) => e.type === "session_info"); + expect(sessionInfoEntries.length).toBe(1); + expect(sessionInfoEntries[0].name).toBe("my-test-session"); + }, 30000); });