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
This commit is contained in:
Daniel Nouri 2026-01-30 01:41:58 +01:00 committed by GitHub
parent cb08758696
commit e20583aac8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 86 additions and 1 deletions

View file

@ -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

View file

@ -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<any>; 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.

View file

@ -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<void> {
await this.send({ type: "set_session_name", name });
}
/**
* Get all messages in the session.
*/

View file

@ -349,6 +349,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
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<never> {
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
// =================================================================

View file

@ -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[] } }

View file

@ -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);
});