Add startup session name flag
Some checks are pending
CI / build-check-test (push) Waiting to run

closes #5153
This commit is contained in:
Mario Zechner 2026-05-29 11:40:44 +02:00
parent 0c448e12e8
commit ce554ad3de
11 changed files with 208 additions and 2 deletions

View file

@ -10,6 +10,7 @@
### New Features
- **Startup session naming** - `--name` / `-n` sets the session display name at startup for interactive, print, JSON, and RPC modes.
- **Claude Opus 4.8 support** - Adds Anthropic Claude Opus 4.8 metadata and updates Opus adaptive-thinking coverage.
- **Selective tool disablement** - `--exclude-tools` / `-xt` disables specific built-in, extension, or custom tools while leaving the rest available. See [Tool Options](docs/usage.md#tool-options).
- **Headless Codex subscription login** - `/login` can use device-code auth for ChatGPT Plus/Pro Codex subscriptions. See [Subscriptions](docs/providers.md#subscriptions) and [OpenAI Codex](docs/providers.md#openai-codex).
@ -17,6 +18,7 @@
### Added
- Added `--name` / `-n` to set the session display name at startup ([#5153](https://github.com/earendil-works/pi/issues/5153)).
- Added `--exclude-tools` / `-xt` to disable specific built-in, extension, or custom tools while leaving the rest available ([#5109](https://github.com/earendil-works/pi/issues/5109)).
- Added OpenAI Codex subscription device-code login as a selectable headless alternative while keeping browser login as the default ([#4911](https://github.com/earendil-works/pi/pull/4911) by [@vegarsti](https://github.com/vegarsti)).
- Added `streamingBehavior` to extension input events so extensions can distinguish idle prompts from mid-stream steers and queued follow-ups ([#5107](https://github.com/earendil-works/pi/pull/5107) by [@DanielThomas](https://github.com/DanielThomas)).

View file

@ -239,6 +239,7 @@ Sessions auto-save to `~/.pi/agent/sessions/` organized by working directory.
pi -c # Continue most recent session
pi -r # Browse and select from past sessions
pi --no-session # Ephemeral mode (don't save)
pi --name "my task" # Set session display name at startup
pi --session <path|id> # Use specific session file or ID
pi --fork <path|id> # Fork specific session file or ID into a new session
```
@ -545,6 +546,7 @@ cat README.md | pi -p "Summarize this text"
| `--fork <path\|id>` | Fork specific session file or partial UUID into a new session |
| `--session-dir <dir>` | Custom session storage directory |
| `--no-session` | Ephemeral mode (don't save) |
| `--name <name>`, `-n <name>` | Set session display name at startup |
### Tool Options
@ -605,6 +607,9 @@ pi -p "Summarize this codebase"
# Non-interactive with piped stdin
cat README.md | pi -p "Summarize this text"
# Named one-shot session
pi --name "release audit" -p "Audit this repository"
# Different model
pi --provider openai --model gpt-4o "Help me refactor"

View file

@ -136,6 +136,7 @@ Sessions are saved automatically:
```bash
pi -c # Continue most recent session
pi -r # Browse previous sessions
pi --name "my task" # Set session display name at startup
pi --session <path|id> # Open a specific session
```

View file

@ -13,6 +13,7 @@ pi --mode rpc [options]
Common options:
- `--provider <name>`: Set the LLM provider (anthropic, openai, google, etc.)
- `--model <pattern>`: Model pattern or ID (supports `provider/id` and optional `:<thinking>`)
- `--name <name>` / `-n <name>`: Set the session display name at startup
- `--no-session`: Disable session persistence
- `--session-dir <path>`: Custom session storage directory
@ -694,7 +695,7 @@ Response:
}
```
The current session name is available via `get_state` in the `sessionName` field.
The current session name is available via `get_state` in the `sessionName` field. To set the initial name when starting RPC mode, pass `--name <name>` or `-n <name>` to the `pi --mode rpc` process.
### Commands

View file

@ -282,7 +282,7 @@ Set `label` to `undefined` to clear a label.
### SessionInfoEntry
Session metadata (e.g., user-defined display name). Set via `/name` command or `pi.setSessionName()` in extensions.
Session metadata (e.g., user-defined display name). Set via `/name`, `--name` / `-n`, or `pi.setSessionName()` in extensions.
```json
{"type":"session_info","id":"k1l2m3n4","parentId":"j0k1l2m3","timestamp":"2024-12-03T14:35:00.000Z","name":"Refactor auth module"}

View file

@ -10,6 +10,7 @@ Sessions auto-save to `~/.pi/agent/sessions/`, organized by working directory. E
pi -c # Continue most recent session
pi -r # Browse and select from past sessions
pi --no-session # Ephemeral mode; do not save
pi --name "my task" # Set session display name at startup
pi --session <path|id> # Use a specific session file or partial session ID
pi --fork <path|id> # Fork a session file or partial session ID into a new session
```
@ -56,6 +57,13 @@ Use `/name <name>` to set a human-readable session name:
/name Refactor auth module
```
Set the name at startup with `--name` or `-n`:
```bash
pi --name "Refactor auth module"
pi --name "CI audit" -p "Review this build failure"
```
Named sessions are easier to find in `/resume` and `pi -r`.
## Branching with `/tree`

View file

@ -76,6 +76,7 @@ Sessions are saved automatically to `~/.pi/agent/sessions/`, organized by workin
pi -c # Continue most recent session
pi -r # Browse and select a session
pi --no-session # Ephemeral mode; do not save
pi --name "my task" # Set session display name at startup
pi --session <path|id> # Use a specific session file or session ID
pi --fork <path|id> # Fork a session into a new session file
```
@ -178,6 +179,7 @@ cat README.md | pi -p "Summarize this text"
| `--fork <path\|id>` | Fork a session file or partial UUID into a new session |
| `--session-dir <dir>` | Custom session storage directory |
| `--no-session` | Ephemeral mode; do not save |
| `--name <name>`, `-n <name>` | Set session display name at startup |
### Tool Options
@ -242,6 +244,9 @@ pi -p "Summarize this codebase"
# Non-interactive with piped stdin
cat README.md | pi -p "Summarize this text"
# Named one-shot session
pi --name "release audit" -p "Audit this repository"
# Different model
pi --provider openai --model gpt-4o "Help me refactor"

View file

@ -21,6 +21,7 @@ export interface Args {
help?: boolean;
version?: boolean;
mode?: Mode;
name?: string;
noSession?: boolean;
session?: string;
sessionId?: string;
@ -93,6 +94,12 @@ export function parseArgs(args: string[]): Args {
} else if (arg === "--append-system-prompt" && i + 1 < args.length) {
result.appendSystemPrompt = result.appendSystemPrompt ?? [];
result.appendSystemPrompt.push(args[++i]);
} else if (arg === "--name" || arg === "-n") {
if (i + 1 < args.length) {
result.name = args[++i];
} else {
result.diagnostics.push({ type: "error", message: "--name requires a value" });
}
} else if (arg === "--no-session") {
result.noSession = true;
} else if (arg === "--session" && i + 1 < args.length) {
@ -237,6 +244,7 @@ ${chalk.bold("Options:")}
--fork <path|id> Fork specific session file or partial UUID into a new session
--session-dir <dir> Directory for session storage and lookup
--no-session Don't save session (ephemeral)
--name, -n <name> Set session display name
--models <patterns> Comma-separated model patterns for Ctrl+P cycling
Supports globs (anthropic/*, *sonnet*) and fuzzy matching
--no-tools, -nt Disable all tools by default (built-in and extension)
@ -283,6 +291,9 @@ ${chalk.bold("Examples:")}
# Continue previous session
${APP_NAME} --continue "What did we discuss?"
# Start a named session
${APP_NAME} --name "Refactor auth module"
# Use different model
${APP_NAME} --provider openai --model gpt-4o-mini "Help me refactor this code"

View file

@ -571,6 +571,14 @@ export async function main(args: string[], options?: MainOptions) {
process.exit(1);
}
}
if (parsed.name !== undefined) {
const name = parsed.name.trim();
if (!name) {
console.error(chalk.red("Error: --name requires a non-empty value"));
process.exit(1);
}
sessionManager.appendSessionInfo(name);
}
time("createSessionManager");
const resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);

View file

@ -157,6 +157,36 @@ describe("parseArgs", () => {
});
});
describe("--name flag", () => {
test("parses --name flag with value", () => {
const result = parseArgs(["--name", "my-session"]);
expect(result.name).toBe("my-session");
});
test("parses -n shorthand", () => {
const result = parseArgs(["-n", "quick-session"]);
expect(result.name).toBe("quick-session");
});
test("preserves empty values for main validation", () => {
const result = parseArgs(["--name", ""]);
expect(result.name).toBe("");
});
test("reports missing value", () => {
const result = parseArgs(["--name"]);
expect(result.diagnostics).toEqual([{ type: "error", message: "--name requires a value" }]);
});
test("works alongside other flags", () => {
const result = parseArgs(["--name", "named-run", "--print", "--model", "gpt-4o", "hello"]);
expect(result.name).toBe("named-run");
expect(result.print).toBe(true);
expect(result.model).toBe("gpt-4o");
expect(result.messages).toEqual(["hello"]);
});
});
describe("--no-session flag", () => {
test("parses --no-session flag", () => {
const result = parseArgs(["--no-session"]);

View file

@ -0,0 +1,135 @@
import { spawn } from "node:child_process";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { ENV_AGENT_DIR } from "../src/config.ts";
const cliPath = resolve(__dirname, "../src/cli.ts");
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});
function createTempDir(): string {
const dir = mkdtempSync(join(tmpdir(), "pi-startup-session-name-"));
tempDirs.push(dir);
return dir;
}
interface CliDirs {
agentDir: string;
projectDir: string;
sessionFile: string;
}
interface CliResult {
code: number | null;
signal: NodeJS.Signals | null;
stderr: string;
}
function createSessionFile(projectDir: string, sessionFile: string): void {
const timestamp = new Date().toISOString();
writeFileSync(
sessionFile,
`${JSON.stringify({ type: "session", version: 3, id: "existing-session", timestamp, cwd: projectDir })}\n${JSON.stringify(
{
type: "message",
id: "assistant-1",
parentId: null,
timestamp,
message: {
role: "assistant",
content: [{ type: "text", text: "hello" }],
provider: "anthropic",
model: "claude-sonnet-4-5",
timestamp: Date.now(),
},
},
)}\n`,
);
}
function readSessionInfoNames(sessionFile: string): string[] {
return readFileSync(sessionFile, "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line) as { type?: string; name?: string })
.filter((entry) => entry.type === "session_info")
.map((entry) => entry.name ?? "");
}
async function runCli(args: string[], dirs: CliDirs): Promise<CliResult> {
let stderr = "";
const child = spawn(process.execPath, [cliPath, ...args], {
cwd: dirs.projectDir,
env: {
...process.env,
[ENV_AGENT_DIR]: dirs.agentDir,
PI_OFFLINE: "1",
TSX_TSCONFIG_PATH: resolve(__dirname, "../../../tsconfig.json"),
},
stdio: ["ignore", "ignore", "pipe"],
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
return new Promise((resolvePromise, reject) => {
const timeout = setTimeout(() => {
child.kill("SIGKILL");
}, 10_000);
child.on("error", (error) => {
clearTimeout(timeout);
reject(error);
});
child.on("close", (code, signal) => {
clearTimeout(timeout);
resolvePromise({ code, signal, stderr });
});
});
}
function setup(): CliDirs {
const tempRoot = createTempDir();
const dirs = {
agentDir: join(tempRoot, "agent"),
projectDir: join(tempRoot, "project"),
sessionFile: join(tempRoot, "session.jsonl"),
};
mkdirSync(dirs.agentDir, { recursive: true });
mkdirSync(dirs.projectDir, { recursive: true });
createSessionFile(dirs.projectDir, dirs.sessionFile);
return dirs;
}
describe("startup session name", () => {
it("sets --name on the selected session before runtime model validation", async () => {
const dirs = setup();
const result = await runCli(
["--session", dirs.sessionFile, "--name", " CLI Named Session ", "--model", "missing-model", "-p", "hi"],
dirs,
);
expect(result.code).toBe(1);
expect(result.signal).toBeNull();
expect(readSessionInfoNames(dirs.sessionFile)).toEqual(["CLI Named Session"]);
});
it("rejects empty --name values without appending session metadata", async () => {
const dirs = setup();
const result = await runCli(
["--session", dirs.sessionFile, "--name", " ", "--model", "missing-model", "-p", "hi"],
dirs,
);
expect(result.code).toBe(1);
expect(result.signal).toBeNull();
expect(result.stderr).toContain("--name requires a non-empty value");
expect(readSessionInfoNames(dirs.sessionFile)).toEqual([]);
});
});