Skip to content
Merged
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
9 changes: 7 additions & 2 deletions docs/user/getting-started-and-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,18 @@ Start the ACP-backed terminal client at a project root:
kit tui --root /path/to/project
```

Resume a persisted conversation by its displayed session ID:
Choose a persisted conversation in the startup picker, or resume directly by ID:

```sh
kit tui --root /path/to/project --resume
kit tui --root /path/to/project --resume <session-id>
```

If a dead process left a stale session lock, `--force` can accompany `--resume`. It is not a general overwrite option and Clap rejects it without a resume argument.
Without an ID, `--resume` opens the same workspace-scoped, newest-first picker as `/sessions`, including session names and inline rename. Workspace selection honors `--root` and configured root defaults as in normal startup. No new persisted session is created just to show the picker. Plain `kit tui` is unchanged.

`Esc` at the top level or `Ctrl+C` cancels startup successfully without creating or resuming a session; `Esc` while renaming only cancels the rename. An empty catalog reports no resumable sessions and exits successfully. Catalog read failures exit unsuccessfully with an actionable error; a selected session that cannot be resumed reports its resume error instead of starting a new session.

If a dead process left a stale session lock, `--force` can accompany either form of `--resume`. With the picker, it applies only to the selected session and never takes over a lock held by a live process. It is not a general overwrite option and Clap rejects it without `--resume`. See [session locks and recovery](tui-and-sessions.md#session-locks---resume-and---force).

### One-shot automation with `kit prompt`

Expand Down
17 changes: 14 additions & 3 deletions docs/user/tui-and-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,18 @@ Start an interactive session at a project root with the installed binary:
kit tui --root /path/to/project
```

List sessions for the workspace, then resume the ID shown in the header or catalog:
Open the session picker at startup, or resume a known ID directly:

```sh
kit tui --root /path/to/project --resume
kit tui --root /path/to/project --resume <session-id>
```

Without an ID, `--resume` opens the same workspace-scoped, newest-first picker as `/sessions`, with the same session names, selection, and inline rename interactions. It resolves `--root` and configured root defaults just like normal startup. No new persisted session is created to display the picker; a session is resumed only after selection. Plain `kit tui` still starts normally.

At the top-level picker, `Esc` or `Ctrl+C` cancels startup and exits successfully without creating or resuming a session; `Esc` during inline rename only cancels the rename. An empty catalog reports that the workspace has no resumable sessions and exits successfully. Catalog read failures report an actionable error and exit unsuccessfully. If the selected session disappears, becomes invalid, or is locked before resume, Kit reports the resume error rather than starting a new session.

You can also list and rename sessions from the command line, then resume the ID shown in the header or catalog:

```sh
kit sessions --root /path/to/project
Expand Down Expand Up @@ -188,7 +199,7 @@ ACP v1 clients restore a closed durable session with `session/load` and discover

Session discovery and restoration are isolated to the server's canonical workspace root. The primary `cwd` must match that root. ACP `session/new`, `session/load`, `session/resume`, and supported `session/fork` requests can also supply `additionalDirectories`: absolute paths to existing project directories. Kit canonicalizes and deduplicates these paths, loads their ancestor `AGENTS.md` instructions as session context, and reports the roots in `SessionInfo`. Additional roots are not a filesystem allowlist and do not change the default tool cwd, configuration, or durable session namespace. Each attachment supplies its complete additional-root set; an empty list clears prior extra roots. These roots are persisted as extensible transcript metadata, so old sessions without that metadata report an empty list. Legacy transcripts under a project-local `.kit/sessions` directory follow the same migration and root checks as CLI resume; they do not make a same-named session visible from another workspace. Old global transcripts without workspace metadata are excluded from discovery in every workspace, but an explicit resume by ID remains supported and binds the transcript to that workspace. An individually malformed or concurrently incomplete transcript is omitted from catalog results without preventing valid sessions from being listed; explicit resume remains strict and reports its error.

An arbitrary ACP load or resume never applies the server process's configured `--force` setting. The one exception is the initial resume requested by `kit tui --resume <id> --force`: only that matching configured session may use the explicit stale-lock override. If another live Kit instance owns the session lock, restoration fails instead of taking over the session. A missing or invalid ID also fails normally. After the session closes and releases its lock, an ACP client can restore it again.
An arbitrary ACP load or resume never applies the server process's configured `--force` setting. The one exception is the initial resume requested by `kit tui --resume [<id>] --force`: only the explicitly named or picker-selected session may use the stale-lock override. If another live Kit instance owns the session lock, restoration fails instead of taking over the session. A missing or invalid ID also fails normally. After the session closes and releases its lock, an ACP client can restore it again.

Before the restoration response, Kit replays the canonical transcript as ordered ACP updates for representable user text and attachments, assistant text and thoughts, and tool calls and results. Internal instructions, ambient context, notifications, and provider-specific content are not replayed to the client, but remain in the model transcript. Because compaction replaces the canonical transcript, restoring a compacted session replays its canonical summary history rather than the superseded pre-compaction items.

Expand All @@ -206,7 +217,7 @@ first confirm that no Kit process is still using the session. Then retry the res
kit tui --root /path/to/project --resume <session-id> --force
```

`--force` is only for a stale lock left by an exited or crashed process, and the CLI accepts it only with `--resume`. It does not steal a lock held by a live process: the OS-level lock check instead reports `session is actively locked by another Kit instance (...)`. Do not manually remove a lock belonging to a running Kit process.
`--force` is only for a stale lock left by an exited or crashed process, and the CLI accepts it only with `--resume`. With `kit tui --resume --force`, the override applies only to the session selected in the startup picker. It does not steal a lock held by a live process: the OS-level lock check instead reports `session is actively locked by another Kit instance (...)`. Do not manually remove a lock belonging to a running Kit process.

A new session ID that already exists reports `session ... already exists; use --resume`; a missing resume target reports `session ... does not exist`. Use the correct ID and mode rather than `--force` for either error.

Expand Down
84 changes: 71 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1092,7 +1092,8 @@ impl Command {
.value_name("RESUME")
.value_parser(clap::value_parser!(String))
.action(clap::ArgAction::Set)
.help("Resume this persisted session id"),
.num_args(0..=1)
.help("Resume a persisted session by ID, or open the session picker without an ID"),
);
command.arg(
clap::Arg::new("force")
Expand Down Expand Up @@ -1175,7 +1176,10 @@ impl Command {
reasoning_effort: optional_arg(matches, "reasoning_effort")?,
a2a: optional_arg(matches, "a2a")?,
mcp: McpArgs::from_matches(matches)?,
resume: optional_arg(matches, "resume")?,
resume: matches
.contains_id("resume")
.then(|| optional_arg(matches, "resume"))
.transpose()?,
force: required_arg(matches, "force")?,
}),
_ => Err(clap::Error::raw(
Expand Down Expand Up @@ -1709,8 +1713,8 @@ enum Command {
a2a: Option<String>,

mcp: McpArgs,
/// Resume this persisted session id.
resume: Option<String>,
/// Resume by ID, or open the workspace session picker without an ID.
resume: Option<Option<String>>,
/// Override the resumed session's stale lock.
force: bool,
},
Expand Down Expand Up @@ -2377,22 +2381,40 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
// the selected reference before starting that subprocess.
let _ = config.harnesses()?;
let root = config.root(root);
let mut stop = kit::tui::Stop::new()?;
let resume = match resume {
Some(None) => match kit::tui::pick_session(&root, &mut stop).await? {
Some(id) => Some(id),
None => return Ok(()),
},
Some(Some(id)) => Some(id),
None => None,
};
let model = config.model(model);
let provider = config.provider(provider);
let reasoning_effort = config.reasoning_effort(reasoning_effort);
let a2a = config.a2a(a2a);
let credential_storage = mcp.credentials.storage(&config)?;
let (_, explicit_mcp) = mcp.config_paths(&config)?;
let _ = config.plugin_runtime(&root).await?;
let voice_enabled = config.experimental.voice;
let config_path = config.config_path.clone();
tokio::task::spawn_blocking(move || {
if let Some(path) = config_path {
fs::global().require_disk(path)?;
}
Ok::<_, io::Error>(())
})
.await??;
let prepared = stop
.until(async {
let _ = config.plugin_runtime(&root).await?;
let config_path = config.config_path.clone();
tokio::task::spawn_blocking(move || {
if let Some(path) = config_path {
fs::global().require_disk(path)?;
}
Ok::<_, io::Error>(())
})
.await??;
Ok::<_, Box<dyn std::error::Error>>(())
})
.await;
let Some(prepared) = prepared else {
return Ok(());
};
prepared?;
kit::tui::run_with_reasoning_effort_and_openrouter_key(
&root,
&model,
Expand All @@ -2406,6 +2428,7 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
resume.as_deref(),
force,
voice_enabled,
&mut stop,
)
.await?
}
Expand Down Expand Up @@ -2439,6 +2462,41 @@ mod tests {
supervise_serve_with_trigger, validate_auth_storage,
};

#[cfg(feature = "tui")]
#[test]
fn tui_resume_forms_preserve_direct_and_normal_startup() {
for (args, expected, expected_force) in [
(vec!["kit", "tui"], None, false),
(vec!["kit", "tui", "--resume"], Some(None), false),
(vec!["kit", "tui", "--resume", "--force"], Some(None), true),
(
vec!["kit", "tui", "--resume", "s-example"],
Some(Some("s-example".to_string())),
false,
),
(
vec!["kit", "tui", "--resume", "s-example", "--force"],
Some(Some("s-example".to_string())),
true,
),
] {
let cli = Cli::try_parse_from(args).unwrap();
let Command::Tui { resume, force, .. } = cli.command else {
panic!("expected TUI")
};
assert_eq!(resume, expected);
assert_eq!(force, expected_force);
}
assert!(Cli::try_parse_from(["kit", "tui", "--force"]).is_err());
assert!(Cli::try_parse_from(["kit", "prompt", "hello", "--resume"]).is_err());
let help = Cli::try_parse_from(["kit", "tui", "--help"])
.err()
.unwrap()
.to_string();
assert!(help.contains("--resume [<RESUME>]"));
assert!(help.contains("session picker without an ID"));
}

#[cfg(feature = "tui")]
#[test]
fn config_experimental_voice_defaults_and_strict_boolean() {
Expand Down
Loading
Loading