Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,7 @@ benchmarks/baseline.json
.vibe/
.windsurf/
.zencoder/

# Python bytecode caches from scripts/
__pycache__/
*.pyc
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,30 @@ fx session resume last
fx session resume --id <id>
```

Inside the interactive shell, use `/resume` to open the session picker, `/resume last` for the latest workspace session, or `/resume <id>` for an exact session.

`fx session <id>` prints the saved conversation with a `[turn N]` label above every turn. Those labels are the boundaries the branch and undo commands take:

```bash
fx session fork <id> --at 7
fx session rewind <id> --by 2
```

`fork` copies the first 7 turns into a brand-new session and prints the new ID to resume. The source session is left exactly as it was. `rewind` drops the last 2 turns from the session in place, keeping its ID. Both accept `--id <id>` and `--json`.

Rewound turns are not erased. The rewind is recorded as a new revision in the session's event log, and the files those turns referenced stay on disk.

The interactive shell has the same two operations for the session it is already in:

```
/fork 7
/rewind 2
```

`/fork` branches the whole session at its current point; `/fork 7` branches at turn 7. Both forms name the source ID and the new one and leave you in the branch, while the source session keeps every turn it had. Bare `/rewind` opens a turn picker that restores to before the selected prompt and puts that prompt back in the composer. `/rewind 2` asks first: the message says how many turns it will drop and how many remain, and a second identical `/rewind 2` carries it out. Any other command in between cancels it.

Neither command reverts file edits, commands, commits, or API calls. They change the conversation only.

Each interactive session names its terminal tab. The title prefers the session name, falls back to the workspace name, and keeps the active model as secondary context. Renaming or resuming a session updates the tab, and exiting clears the fx-owned title. Noninteractive commands do not emit terminal-title controls.

Run `/feedback` to open the feedback form at `fx.sh/feedback`. It does not create a diagnostic or change the clipboard.
Expand Down
2 changes: 2 additions & 0 deletions scripts/pgso/corpus.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@
],
"verification_scenarios": [
{"name": "verify-auto-mode-reliability", "argv": ["bun", "test", "--max-concurrency", "1", "./auto-mode-reliability.test.ts"], "test_file": "auto-mode-reliability.test.ts"},
{"name": "verify-session-fork", "argv": ["bun", "test", "--max-concurrency", "1", "./session-fork.test.ts"], "test_file": "session-fork.test.ts", "requires_tmux": false},
{"name": "verify-tui-session-fork", "argv": ["bun", "test", "--max-concurrency", "1", "./tui-session-fork.test.ts"], "test_file": "tui-session-fork.test.ts", "requires_tmux": true},
{"name": "verify-oauth-keychain-migration", "argv": ["bun", "test", "--max-concurrency", "1", "./oauth-keychain-migration.test.ts"], "test_file": "oauth-keychain-migration.test.ts", "allow_keychain": true},
{"name": "verify-tui-auth-source-selection", "argv": ["bun", "test", "--max-concurrency", "1", "./tui-auth-source-selection.test.ts"], "test_file": "tui-auth-source-selection.test.ts"},
{"name": "verify-tui-composer-edit-contracts", "argv": ["bun", "test", "--max-concurrency", "1", "./tui-composer-edit-contracts.test.ts"], "test_file": "tui-composer-edit-contracts.test.ts"},
Expand Down
14 changes: 11 additions & 3 deletions src/builtins/commands.zig
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,16 @@ pub const top_level_specs = [_]TopLevelSpec{
.{
.kind = .session,
.token = "session",
.usage = "session <last|id>|--id <id> [--json] | session resume [last|<id>] | session resume --id <id> | session migrate <id>|--id <id> [--allow-large] [--json] | session recover <id>|--id <id> [--json]",
.summary = "Inspect, resume, migrate, or recover saved sessions",
.usage = "session <last|id>|--id <id> [--json] | session resume [last|<id>] | session resume --id <id> | session migrate <id>|--id <id> [--allow-large] [--json] | session recover <id>|--id <id> [--json] | session fork <id>|--id <id> --at <turn> [--json] | session rewind <id>|--id <id> --by <count> [--json]",
.summary = "Inspect, resume, migrate, recover, fork, or rewind saved sessions",
.options = &.{
.{ .flag = "last", .description = "Inspect the current workspace session" },
.{ .flag = "--id <id>", .description = "Inspect a saved session by exact id" },
.{ .flag = "resume [last|<id>]", .description = "Resume the latest workspace session or a session by id" },
.{ .flag = "migrate <id>", .description = "Migrate a saved session to the current format" },
.{ .flag = "recover <id>", .description = "Copy a recoverable corrupt session into a new session" },
.{ .flag = "fork <id> --at <turn>", .description = "Branch a session into a new session holding its first <turn> turns" },
.{ .flag = "rewind <id> --by <count>", .description = "Drop the last <count> turns from a session in place" },
.{ .flag = "--allow-large", .description = "Permit migrating an oversized session" },
json_option,
},
Expand Down Expand Up @@ -312,6 +314,8 @@ pub const top_level_help_groups = [_]TopLevelHelpGroup{
.{ .usage = "session resume [last|id]", .summary = "Resume the latest workspace session or a session by id" },
.{ .usage = "session migrate <id>", .summary = "Migrate a saved session to the current format" },
.{ .usage = "session recover <id>", .summary = "Copy a recoverable corrupt session" },
.{ .usage = "session fork <id> --at <turn>", .summary = "Branch a session at a turn into a new session" },
.{ .usage = "session rewind <id> --by <count>", .summary = "Drop the last turns from a session in place" },
} },
.{ .entries = &.{
.{ .kind = .login, .usage = "login [vercel|codex|grok]" },
Expand Down Expand Up @@ -435,9 +439,11 @@ pub const slash_specs = [_]SlashSpec{
.{ .kind = .clear_screen, .command = "/clear", .help_entry = "/clear", .completion_description = "start a fresh session and keep background processes", .presentation_category = .general, .show_in_welcome = true },
.{ .kind = .new_session, .command = "/new", .help_entry = "/new", .completion_description = "start a fresh session", .presentation_category = .session, .show_in_welcome = true },
.{ .kind = .reset_session, .command = "/reset", .help_entry = "/reset", .completion_description = "reset the current session context", .presentation_category = .session },
.{ .kind = .resume_session, .command = "/resume", .help_entry = "/resume", .completion_description = "resume a saved session", .presentation_category = .session },
.{ .kind = .resume_session, .command = "/resume", .help_entry = "/resume [last|<id>]", .completion_description = "resume a saved session", .presentation_category = .session, .has_args = true, .accepts_payload = true },
.{ .kind = .continue_recovery, .command = "/continue", .help_entry = "/continue", .completion_description = "continue a paused model response", .presentation_category = .session, .requires_prompt_credential = true },
.{ .kind = .rename_session, .command = "/rename", .help_entry = "/rename <title>", .completion_description = "rename the current session", .presentation_category = .session, .has_args = true, .accepts_payload = true },
.{ .kind = .fork_session, .command = "/fork", .help_entry = "/fork [turn]", .completion_description = "branch this session at a turn into a new one", .presentation_category = .session, .has_args = true, .accepts_payload = true },
.{ .kind = .rewind_session, .command = "/rewind", .help_entry = "/rewind [count]", .completion_description = "drop the last turns from this session", .presentation_category = .session, .has_args = true, .accepts_payload = true },
.{ .kind = .login, .command = "/login", .help_entry = "/login", .completion_description = "choose Vercel or Codex sign-in", .presentation_category = .account },
.{ .kind = .logout, .command = "/logout", .help_entry = "/logout [vercel|codex|grok]", .completion_description = "sign out of a provider session", .presentation_category = .account, .has_args = true, .accepts_payload = true },
.{ .kind = .setup, .command = "/setup", .help_entry = "/setup", .completion_description = "manage accounts and AI Gateway access", .presentation_category = .account },
Expand Down Expand Up @@ -538,6 +544,8 @@ test "built-in slash commands register exact active order" {
"/resume",
"/continue",
"/rename",
"/fork",
"/rewind",
"/login",
"/logout",
"/setup",
Expand Down
Loading