feat(coding-agent): add --fork session flag closes #2290
This commit is contained in:
@@ -205,6 +205,7 @@ pi -c # Continue most recent session
|
|||||||
pi -r # Browse and select from past sessions
|
pi -r # Browse and select from past sessions
|
||||||
pi --no-session # Ephemeral mode (don't save)
|
pi --no-session # Ephemeral mode (don't save)
|
||||||
pi --session <path> # Use specific session file or ID
|
pi --session <path> # Use specific session file or ID
|
||||||
|
pi --fork <path> # Fork specific session file or ID into a new session
|
||||||
```
|
```
|
||||||
|
|
||||||
### Branching
|
### Branching
|
||||||
@@ -219,6 +220,8 @@ pi --session <path> # Use specific session file or ID
|
|||||||
|
|
||||||
**`/fork`** - Create a new session file from the current branch. Opens a selector, copies history up to the selected point, and places that message in the editor for modification.
|
**`/fork`** - Create a new session file from the current branch. Opens a selector, copies history up to the selected point, and places that message in the editor for modification.
|
||||||
|
|
||||||
|
**`--fork <path|id>`** - Fork an existing session file or partial session UUID directly from the CLI. This copies the full source session into a new session file in the current project.
|
||||||
|
|
||||||
### Compaction
|
### Compaction
|
||||||
|
|
||||||
Long sessions can exhaust context windows. Compaction summarizes older messages while keeping recent ones.
|
Long sessions can exhaust context windows. Compaction summarizes older messages while keeping recent ones.
|
||||||
@@ -475,6 +478,7 @@ cat README.md | pi -p "Summarize this text"
|
|||||||
| `-c`, `--continue` | Continue most recent session |
|
| `-c`, `--continue` | Continue most recent session |
|
||||||
| `-r`, `--resume` | Browse and select session |
|
| `-r`, `--resume` | Browse and select session |
|
||||||
| `--session <path>` | Use specific session file or partial UUID |
|
| `--session <path>` | Use specific session file or partial UUID |
|
||||||
|
| `--fork <path>` | Fork specific session file or partial UUID into a new session |
|
||||||
| `--session-dir <dir>` | Custom session storage directory |
|
| `--session-dir <dir>` | Custom session storage directory |
|
||||||
| `--no-session` | Ephemeral mode (don't save) |
|
| `--no-session` | Ephemeral mode (don't save) |
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export interface Args {
|
|||||||
mode?: Mode;
|
mode?: Mode;
|
||||||
noSession?: boolean;
|
noSession?: boolean;
|
||||||
session?: string;
|
session?: string;
|
||||||
|
fork?: string;
|
||||||
sessionDir?: string;
|
sessionDir?: string;
|
||||||
models?: string[];
|
models?: string[];
|
||||||
tools?: ToolName[];
|
tools?: ToolName[];
|
||||||
@@ -89,6 +90,8 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
|
|||||||
result.noSession = true;
|
result.noSession = true;
|
||||||
} else if (arg === "--session" && i + 1 < args.length) {
|
} else if (arg === "--session" && i + 1 < args.length) {
|
||||||
result.session = args[++i];
|
result.session = args[++i];
|
||||||
|
} else if (arg === "--fork" && i + 1 < args.length) {
|
||||||
|
result.fork = args[++i];
|
||||||
} else if (arg === "--session-dir" && i + 1 < args.length) {
|
} else if (arg === "--session-dir" && i + 1 < args.length) {
|
||||||
result.sessionDir = args[++i];
|
result.sessionDir = args[++i];
|
||||||
} else if (arg === "--models" && i + 1 < args.length) {
|
} else if (arg === "--models" && i + 1 < args.length) {
|
||||||
@@ -202,6 +205,7 @@ ${chalk.bold("Options:")}
|
|||||||
--continue, -c Continue previous session
|
--continue, -c Continue previous session
|
||||||
--resume, -r Select a session to resume
|
--resume, -r Select a session to resume
|
||||||
--session <path> Use specific session file
|
--session <path> Use specific session file
|
||||||
|
--fork <path> Fork specific session file or partial UUID into a new session
|
||||||
--session-dir <dir> Directory for session storage and lookup
|
--session-dir <dir> Directory for session storage and lookup
|
||||||
--no-session Don't save session (ephemeral)
|
--no-session Don't save session (ephemeral)
|
||||||
--models <patterns> Comma-separated model patterns for Ctrl+P cycling
|
--models <patterns> Comma-separated model patterns for Ctrl+P cycling
|
||||||
|
|||||||
@@ -408,6 +408,32 @@ async function callSessionDirectoryHook(extensions: LoadExtensionsResult, cwd: s
|
|||||||
return customSessionDir;
|
return customSessionDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateForkFlags(parsed: Args): void {
|
||||||
|
if (!parsed.fork) return;
|
||||||
|
|
||||||
|
const conflictingFlags = [
|
||||||
|
parsed.session ? "--session" : undefined,
|
||||||
|
parsed.continue ? "--continue" : undefined,
|
||||||
|
parsed.resume ? "--resume" : undefined,
|
||||||
|
parsed.noSession ? "--no-session" : undefined,
|
||||||
|
].filter((flag): flag is string => flag !== undefined);
|
||||||
|
|
||||||
|
if (conflictingFlags.length > 0) {
|
||||||
|
console.error(chalk.red(`Error: --fork cannot be combined with ${conflictingFlags.join(", ")}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function forkSessionOrExit(sourcePath: string, cwd: string, sessionDir?: string): SessionManager {
|
||||||
|
try {
|
||||||
|
return SessionManager.forkFrom(sourcePath, cwd, sessionDir);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(chalk.red(`Error: ${message}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function createSessionManager(
|
async function createSessionManager(
|
||||||
parsed: Args,
|
parsed: Args,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
@@ -423,6 +449,21 @@ async function createSessionManager(
|
|||||||
effectiveSessionDir = await callSessionDirectoryHook(extensions, cwd);
|
effectiveSessionDir = await callSessionDirectoryHook(extensions, cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parsed.fork) {
|
||||||
|
const resolved = await resolveSessionPath(parsed.fork, cwd, effectiveSessionDir);
|
||||||
|
|
||||||
|
switch (resolved.type) {
|
||||||
|
case "path":
|
||||||
|
case "local":
|
||||||
|
case "global":
|
||||||
|
return forkSessionOrExit(resolved.path, cwd, effectiveSessionDir);
|
||||||
|
|
||||||
|
case "not_found":
|
||||||
|
console.error(chalk.red(`No session found matching '${resolved.arg}'`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (parsed.session) {
|
if (parsed.session) {
|
||||||
const resolved = await resolveSessionPath(parsed.session, cwd, effectiveSessionDir);
|
const resolved = await resolveSessionPath(parsed.session, cwd, effectiveSessionDir);
|
||||||
|
|
||||||
@@ -439,7 +480,7 @@ async function createSessionManager(
|
|||||||
console.log(chalk.dim("Aborted."));
|
console.log(chalk.dim("Aborted."));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
return SessionManager.forkFrom(resolved.path, cwd, effectiveSessionDir);
|
return forkSessionOrExit(resolved.path, cwd, effectiveSessionDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
case "not_found":
|
case "not_found":
|
||||||
@@ -698,6 +739,8 @@ export async function main(args: string[]) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateForkFlags(parsed);
|
||||||
|
|
||||||
const { initialMessage, initialImages } = await prepareInitialMessage(
|
const { initialMessage, initialImages } = await prepareInitialMessage(
|
||||||
parsed,
|
parsed,
|
||||||
settingsManager.getImageAutoResize(),
|
settingsManager.getImageAutoResize(),
|
||||||
|
|||||||
@@ -110,6 +110,12 @@ describe("parseArgs", () => {
|
|||||||
expect(result.session).toBe("/path/to/session.jsonl");
|
expect(result.session).toBe("/path/to/session.jsonl");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("parses --fork", () => {
|
||||||
|
const result = parseArgs(["--fork", "1234abcd"]);
|
||||||
|
expect(result.fork).toBe("1234abcd");
|
||||||
|
expect(result.messages).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test("parses --export", () => {
|
test("parses --export", () => {
|
||||||
const result = parseArgs(["--export", "session.jsonl"]);
|
const result = parseArgs(["--export", "session.jsonl"]);
|
||||||
expect(result.export).toBe("session.jsonl");
|
expect(result.export).toBe("session.jsonl");
|
||||||
|
|||||||
Reference in New Issue
Block a user