mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-07-09 17:28:45 +00:00
closes #5153
This commit is contained in:
parent
0c448e12e8
commit
ce554ad3de
11 changed files with 208 additions and 2 deletions
|
|
@ -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)).
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
|
|
|
|||
135
packages/coding-agent/test/startup-session-name.test.ts
Normal file
135
packages/coding-agent/test/startup-session-name.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue