Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ subdir = "agent-plugin"

Kit stores each conversation as an append-only JSONL transcript in `~/.kit/sessions`. Kit synchronizes each item to disk before it adds the item to memory.

You can resume a session from the TUI or with the `--resume` option of `kit prompt`. ACP clients can use `session/load` or `session/resume`.
You can list and persistently rename workspace sessions with `kit sessions`, `kit sessions rename <session-id> "Name"`, and `kit sessions rename <session-id> --clear`. In the TUI, `/sessions` opens the picker, `R` renames the selected session, and Enter resumes it. Custom names override generated titles in listings without changing session IDs or previews. You can also resume with the `--resume` option of `kit prompt`; ACP clients can use `session/load` or `session/resume`.

When the provider reports 80% context-window use, Kit converts older history into a structured note. Kit persists the replacement. Kit retains the bootstrap instructions and a tool-safe tail. Use `/compact` to compact the history on demand. See [TUI interaction and sessions](docs/user/tui-and-sessions.md#manage-sessions-and-compact-from-the-tui).

Expand Down
9 changes: 6 additions & 3 deletions docs/user/tui-and-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ List sessions for the workspace, then resume the ID shown in the header or catal

```sh
kit sessions --root /path/to/project
kit sessions rename <session-id> "OAuth token bug" --root /path/to/project
kit sessions rename <session-id> --clear --root /path/to/project
kit tui --root /path/to/project --resume <session-id>
```

The catalog requires an existing directory and is workspace-filtered and newest-first. It reports each durable top-level session ID and updated time; sessions created as subagents are omitted based on structured origin metadata in their initial transcript. Sessions created before Kit recorded that metadata remain visible. Filtering affects discovery only; a known omitted ID can still be resumed explicitly. The title comes from the earliest retained useful user text so compaction does not rename a session; the preview describes the current retained history. Display metadata removes terminal controls and Unicode default-ignorable formatting characters.
The catalog requires an existing directory and is workspace-filtered and newest-first. It reports each durable top-level session ID and updated time; sessions created as subagents are omitted based on structured origin metadata in their initial transcript. Sessions created before Kit recorded that metadata remain visible. Filtering affects discovery only; a known omitted ID can still be resumed explicitly. The generated title comes from the earliest retained useful user text so compaction does not rename a session; the preview describes the current retained history. A custom display name overrides that title in CLI, TUI, and ACP listings without changing the immutable session ID or preview. Names may contain Unicode, are trimmed, may be at most 100 characters, and may not contain line breaks or terminal control characters. Use `--clear` to restore the generated title.

A session ID must be 1–128 ASCII letters, digits, `-`, or `_`. `kit prompt` uses the same durable sessions: it prints `session_id: <id>` after its answer, and that ID can be continued by either `kit prompt --resume <session-id>` or `kit tui --resume <session-id>`.

Expand Down Expand Up @@ -101,7 +103,7 @@ The TUI handles `/new`, `/resume`, `/sessions`, `/close`, `/model`, `/effort`, a
/agents
```

These local commands are available only while the session is idle. `/agents` toggles the agent roster without starting a model turn. `/new` closes the current session and starts a fresh persisted session. It clears the visible transcript but does not delete or alter the previous session, which remains resumable by its ID. Text following `/new` becomes the new session's first prompt. `/resume <session-id>` closes the current session, resumes the requested durable session, and replays its transcript; selecting the already-active ID is a no-op. `/sessions` opens a visible newest-first selector for the same workspace; Up and Down move, Enter uses the existing resume flow, and Esc closes the dialog. `/close` closes the current session and exits the TUI.
These local commands are available only while the session is idle. `/agents` toggles the agent roster without starting a model turn. `/new` closes the current session and starts a fresh persisted session. It clears the visible transcript but does not delete or alter the previous session, which remains resumable by its ID. Text following `/new` becomes the new session's first prompt. `/resume <session-id>` closes the current session, resumes the requested durable session, and replays its transcript; selecting the already-active ID is a no-op. `/sessions` opens a visible newest-first selector for the same workspace. Up and Down move, Enter uses the existing resume flow, `R` opens an inline rename field, and Esc cancels renaming or closes the dialog. Submit an empty rename and confirm to clear the custom name. After a save, the picker remains open on the selected session and refreshes its displayed name. `/close` closes the current session and exits the TUI.

`/model` opens the model selector. `/effort` opens the advertised ACP reasoning-effort selector; `/effort default|low|medium|high` selects directly. In either dialog, Tab toggles saving the selection to `~/.kit/config.toml`, Enter selects, and Esc closes. Saving `default` removes top-level `reasoning_effort`; other values update it without replacing unrelated TOML. A new or resumed process starts from the resolved CLI/TOML default unless the selection was saved.

Expand All @@ -114,10 +116,11 @@ Kit stores durable JSONL transcripts, locks, and session-associated fatal error
```text
~/.kit/sessions/w-<workspace-hash>/<session-id>.jsonl
~/.kit/sessions/w-<workspace-hash>/<session-id>.lock
~/.kit/sessions/w-<workspace-hash>/<session-id>.metadata.json
~/.kit/errors/<session-id>/<event-id>.json
```

The workspace hash is the BLAKE3 digest of the canonical workspace-root path. It keeps identical session IDs in different workspaces in separate storage directories.
The workspace hash is the BLAKE3 digest of the canonical workspace-root path. It keeps identical session IDs in different workspaces in separate storage directories. The optional metadata sidecar stores only the custom display name, is replaced atomically, and does not modify or lock the append-only transcript. Missing or malformed metadata falls back to the generated title without hiding the session.

Fatal error records use their own versioned JSON schema and are not transcript content. Schema v2 adds optional structured transport diagnostics; schema v1 records remain readable. Transport diagnostics contain only bounded, allowlisted request/stream stage, retry, attempt, the provider's strictly validated `x-request-id` value, reqwest classification, and typed Hyper, HTTP/2, and I/O fields. Unknown or truncated source chains are identified without storing source text. Kit never stores raw error display/debug text, arbitrary headers, prompts, tool arguments, response bodies, credentials, URLs, or peer-controlled HTTP/2 debug text in these records. Files are written atomically with owner-only permissions on Unix, and Kit retains the newest 50 records per session. Cancellation is not a fatal error and does not create a record. When persistence succeeds, local prompt and ACP terminal errors include the log path; A2A records stay server-local.

Expand Down
106 changes: 97 additions & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,21 @@ enum AuthAction {
},
}

#[derive(Subcommand)]
enum SessionsAction {
/// Set or clear a session's custom display name.
Rename {
/// Durable session ID.
session_id: String,
/// New display name.
#[arg(required_unless_present = "clear", conflicts_with = "clear")]
name: Option<String>,
/// Clear the custom name and restore the generated title.
#[arg(long)]
clear: bool,
},
}

#[derive(Subcommand)]
enum Command {
/// Write the recommended configuration to ~/.kit/config.toml.
Expand All @@ -464,10 +479,12 @@ enum Command {
#[command(flatten)]
credentials: CredentialArgs,
},
/// List durable sessions for a workspace, newest first.
/// List or rename durable sessions for a workspace.
Sessions {
#[command(subcommand)]
action: Option<SessionsAction>,
/// Working directory and project context (defaults to config or `.`).
#[arg(long)]
#[arg(long, global = true)]
root: Option<PathBuf>,
},
/// Serve ACP on stdio with A2A, remote ACP, or both over HTTP.
Expand Down Expand Up @@ -834,9 +851,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
return Ok(());
}
let config = Config::load_default()?;
if let Command::Sessions { root } = &cli.command {
if let Command::Sessions { action, root } = &cli.command {
let root = config.root(root.clone());
print!("{}", format_sessions(&kit::session::catalog(&root)?));
match action {
None => print!("{}", format_sessions(&kit::session::catalog(&root)?)),
Some(SessionsAction::Rename {
session_id,
name,
clear,
}) => {
let display_name = if *clear { None } else { name.as_deref() };
kit::session::set_display_name(&root, session_id, display_name)?;
if *clear {
println!("Cleared name for session {session_id}");
} else if let Some(name) = name {
println!("Renamed session {session_id} to \"{}\"", name.trim());
}
}
}
return Ok(());
}
let openrouter_api_key =
Expand Down Expand Up @@ -1139,8 +1171,8 @@ mod tests {

use super::{
AuthAction, AuthProvider, Cli, Command, Config, CredentialArgs, CredentialStoreKind,
McpArgs, OTEL_CAPTURE_MESSAGE_CONTENT_ENV, ReasoningEffortArg, format_sessions,
init_config, resolve_openrouter_api_key, supervise_serve_with_trigger,
McpArgs, OTEL_CAPTURE_MESSAGE_CONTENT_ENV, ReasoningEffortArg, SessionsAction,
format_sessions, init_config, resolve_openrouter_api_key, supervise_serve_with_trigger,
validate_auth_storage,
};

Expand Down Expand Up @@ -1617,20 +1649,76 @@ future_option = true
let cli = Cli::try_parse_from(["kit", "sessions", "--root", "/tmp/project"]).unwrap();
assert!(matches!(
cli.command,
Command::Sessions { root: Some(root) }
Command::Sessions { action: None, root: Some(root) }
if root.as_path() == std::path::Path::new("/tmp/project")
));

let output = format_sessions(&[kit::session::CatalogEntry {
id: "session-1".into(),
title: Some("Fix tests".into()),
title: Some("OAuth token bug".into()),
preview: Some("Fix tests in the catalog".into()),
is_subagent: false,
updated_at: 0,
}]);
assert_eq!(
output,
"UPDATED\tID\tTITLE\tPREVIEW\n1970-01-01T00:00:00.000Z\tsession-1\tFix tests\tFix tests in the catalog\n"
"UPDATED\tID\tTITLE\tPREVIEW\n1970-01-01T00:00:00.000Z\tsession-1\tOAuth token bug\tFix tests in the catalog\n"
);
}

#[test]
fn sessions_rename_accepts_names_clear_and_root_in_either_position() {
let named = Cli::try_parse_from([
"kit",
"sessions",
"rename",
"s-abc123",
"OAuth token bug",
"--root",
"/tmp/project",
])
.unwrap();
assert!(matches!(
named.command,
Command::Sessions {
action: Some(SessionsAction::Rename {
session_id,
name: Some(name),
clear: false,
}),
root: Some(root),
} if session_id == "s-abc123"
&& name == "OAuth token bug"
&& root == std::path::Path::new("/tmp/project")
));

let cleared = Cli::try_parse_from([
"kit",
"sessions",
"--root",
"/tmp/project",
"rename",
"s-abc123",
"--clear",
])
.unwrap();
assert!(matches!(
cleared.command,
Command::Sessions {
action: Some(SessionsAction::Rename {
session_id,
name: None,
clear: true,
}),
root: Some(root),
} if session_id == "s-abc123"
&& root == std::path::Path::new("/tmp/project")
));

assert!(Cli::try_parse_from(["kit", "sessions", "rename", "s-abc123"]).is_err());
assert!(
Cli::try_parse_from(["kit", "sessions", "rename", "s-abc123", "name", "--clear",])
.is_err()
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/protocols/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,7 @@ impl Server {
.iter()
.map(|entry| {
SessionInfo::new(entry.id.clone(), cwd.clone())
.title(entry.title.clone())
.title(entry.title.as_deref().map(str::to_owned))
.updated_at(entry.updated_at_rfc3339())
})
.collect();
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/acp/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1573,7 +1573,7 @@ fn set_v2_config(
fn catalog_session_info(entry: &crate::session::CatalogEntry, cwd: &Path) -> wire::SessionInfo {
let mut info =
wire::SessionInfo::new(wire::SessionId::new(entry.id.clone()), cwd.to_path_buf())
.title(entry.title.clone())
.title(entry.title.as_deref().map(str::to_owned))
.updated_at(entry.updated_at_rfc3339());
if entry.is_subagent {
info = info.meta(serde_json::Map::from_iter([(
Expand Down
Loading
Loading