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
10 changes: 6 additions & 4 deletions docs/user/tui-and-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,15 +210,17 @@ The TUI handles `/new`, `/resume`, `/sessions`, `/close`, `/model`, `/effort`, `

The other commands listed above 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 on a provider list; Enter on a provider shows that provider's models. Once the session is open, providers that Kit can sign in to but that have no credentials yet are listed greyed out below the signed-in ones. Enter on one starts its terminal sign-in, the same flow as `/login <method>`, and is available only while the session is idle; after a successful sign-in, the selector reopens on that provider's models with the same Tab setting. Typing searches only the current page, and the search clears when you move between pages. Esc returns from the model list to the provider list, and closes the selector from the provider list. `/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. New sessions start from the resolved CLI/TOML default. Durable sessions remember their reasoning-effort selection, including an explicit provider `default`, and restore it on load or resume even if the process default has changed. Older transcripts without a recorded effort continue to use the process default. Forks inherit the source session’s current effort. Saving to TOML changes the default for future sessions; it is not required to retain the effort of the current session. Model selection still starts from the resolved CLI/TOML default on reload. Reasoning-effort records require a Kit version that understands transcript schema 6; older versions reject these records rather than silently discard the saved selection.
`/model` opens the model selector on a provider list; Enter on a provider shows that provider's models. Once the session is open, providers that Kit can sign in to but that have no credentials yet are listed greyed out below the signed-in ones. Enter on one starts its terminal sign-in, the same flow as `/login <method>`, and is available only while the session is idle; after a successful sign-in, the selector reopens on that provider's models with the same Tab setting. Typing searches only the current page, and the search clears when you move between pages. Esc returns from the model list to the provider list, and closes the selector from the provider list. `/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. New sessions start from the resolved CLI/TOML default. Durable sessions remember their reasoning-effort selection, including an explicit provider `default`, and restore it on load or resume even if the process default has changed. Older transcripts without a recorded effort continue to use the process default. Forks inherit the source session’s current effort. Saving to TOML changes the default for future sessions; it is not required to retain the effort of the current session. Reasoning-effort records require a Kit version that understands transcript schema 6; older versions reject these records rather than silently discard the saved selection.

Before changing models, Kit compares the latest provider-reported transcript occupancy with the target model's advertised context window. It adds a 20% tokenizer margin (`ceil(tokens × 1.20)`) and warns when that estimate is at least 80% of the target window. This is the latest request's occupancy, not accumulated session usage; cached input tokens are not counted twice.
Durable sessions also remember their model and restore it on load or resume, even if the configured default has changed. Forks inherit the source session's current model. An explicit `--model` or `--provider` on the command that resumes a session overrides the remembered model and replaces it. If Kit can show that the remembered model is unusable, because its provider has no credentials or a successful model listing omits it, the session uses the configured default for that attachment and Kit reports a warning; the remembered model stays on record and returns if it becomes available again. A failed listing proves nothing, so Kit keeps the remembered model. Older transcripts without a remembered model use the configured default. Model records require a Kit version that understands transcript schema 7; older versions reject them.

Before changing models, Kit compares the latest provider-reported transcript occupancy with the target model's advertised context window. It adds a 20% tokenizer margin (`ceil(tokens × 1.20)`) and warns when that estimate is at least 80% of the target window. It does not warn when the target window is at least as large as the window recorded with the latest usage, because that switch cannot leave the session with less room. This is the latest request's occupancy, not accumulated session usage; cached input tokens are not counted twice.

The TUI warning offers **Continue anyway**, **Compact**, and **Cancel** (the default). Continue switches without compacting. Compact runs the existing compactor with the **original model** and applies the new selection only after successful completion and durable transcript replacement. A failure or cancellation keeps the original model selected; compaction can already have changed the transcript if cancellation arrives after replacement. Cancel dismisses the warning without changing the model or transcript, and preserves the input draft. Esc cancels an in-progress switch; repeated Esc is harmless, and Ctrl+C while cancellation is pending exits if it is stuck. Stale confirmations are rejected when the session, selected model, target, or transcript changes.

If the latest token count or target context window is unavailable (including fallback/custom models without catalog metadata), Kit allows an **unchecked switch** rather than inventing a limit or forcing compaction. The estimate is a warning, not a guarantee that the next provider request fits.

External ACP clients receive a confirmation-required error instead of a Kit dialog. Its extension key in error data is `kit.model_switch`; clients can resubmit the same configuration request with `_meta: {"kit.model_switch": {"token": <returned token>, "action": "continue"}}`. Both ACP versions also accept `"action": "compact"` to compact transactionally with the original model. Confirmation tokens are session-local, in-memory, one-shot decisions, not persisted configuration.
External ACP clients receive a confirmation-required error (code `-31050`) instead of a Kit dialog. Its extension key in error data is `kit.model_switch`; clients can resubmit the same configuration request with `_meta: {"kit.model_switch": {"token": <returned token>, "action": "continue"}}`. Both ACP versions also accept `"action": "compact"` to compact transactionally with the original model. Confirmation tokens are session-local, in-memory, one-shot decisions, not persisted configuration.

The ACP server advertises `compact` for every new session. The TUI submits `/compact` unchanged like any other prompt; the runtime consumes exactly one text part beginning with the exact raw token before model dispatch and permits other client-provided context parts. Used alone, it ends after compaction. Whitespace-trimmed text following `/compact` and any other context parts are retained as the latest user message and start the next turn after compaction. Leading whitespace, near-misses such as `/compactness`, prompts containing multiple `/compact` command parts, and unknown slash commands remain ordinary prompts. Local commands win if an advertised command has the same name.

Expand Down Expand Up @@ -251,7 +253,7 @@ Session discovery and restoration are isolated to the server's canonical workspa

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.
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. After the restoration response, Kit sends a usage update with the latest recorded context occupancy when the context window is known: the window advertised for the selected model, or otherwise the window recorded with that usage if it belongs to that model. Changing models also refreshes this update with the new model's window when it is advertised.

### Session locks, `--resume`, and `--force`

Expand Down
2 changes: 1 addition & 1 deletion src/compaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1275,7 +1275,7 @@ fn compaction_reason(transcript: &[Item]) -> Option<CompactionReason> {
configured_reason(transcript, &CompactionConfig::default())
}

fn context_window(transcript: &[Item]) -> Option<u64> {
pub(crate) fn context_window(transcript: &[Item]) -> Option<u64> {
transcript
.iter()
.rev()
Expand Down
21 changes: 21 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,13 @@ impl Command {
.help("Do not serve ACP on stdio. Requires remote ACP over HTTP")
.requires("remote_acp"),
);
let command = command.arg(
clap::Arg::new("prefer_session_model")
.long("prefer-session-model")
.action(clap::ArgAction::SetTrue)
.help("Treat --model and --provider as defaults for resumed sessions")
.hide(true),
);
let command = command.arg(
clap::Arg::new("stdio_protocol_version")
.long("stdio-protocol-version")
Expand Down Expand Up @@ -1190,6 +1197,7 @@ impl Command {
no_a2a: required_arg(matches, "no_a2a")?,
no_stdio: required_arg(matches, "no_stdio")?,
stdio_protocol_version: required_arg(matches, "stdio_protocol_version")?,
prefer_session_model: required_arg(matches, "prefer_session_model")?,
server_credential_file: optional_arg(matches, "server_credential_file")?,
mcp: McpArgs::from_matches(matches)?,
terminal_auth_login: optional_arg(matches, "terminal_auth_login")?,
Expand Down Expand Up @@ -1770,6 +1778,8 @@ enum Command {
no_stdio: bool,
/// ACP wire version for stdio (defaults to v1 for compatibility).
stdio_protocol_version: AcpProtocolVersion,
/// Internal: `--model` and `--provider` are only defaults for resumed sessions.
prefer_session_model: bool,
/// Require this file's bearer token on every HTTP request.
server_credential_file: Option<PathBuf>,

Expand Down Expand Up @@ -2317,6 +2327,7 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
no_a2a,
no_stdio,
stdio_protocol_version,
prefer_session_model,
server_credential_file,
mcp,
terminal_auth_login: _,
Expand All @@ -2333,6 +2344,7 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
return Ok(());
}
let root = config.root(root);
let model_override = (model.is_some() || provider.is_some()) && !prefer_session_model;
let model = config.model(model);
let provider = config.provider(provider);
let reasoning_effort = config.reasoning_effort(reasoning_effort);
Expand Down Expand Up @@ -2365,6 +2377,7 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
runtime,
config.inference_providers.clone(),
)?;
let runtime = kit::Runtime::with_model_override(runtime, model_override)?;
let runtime = kit::Runtime::with_compaction(runtime, config.compaction)?;
let runtime = kit::Runtime::with_skill_poll_interval(
runtime,
Expand Down Expand Up @@ -2447,6 +2460,7 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
return Ok(());
}
let root = config.root(root);
let model_override = model.is_some() || provider.is_some();
let model = config.model(model);
let provider = config.provider(provider);
let reasoning_effort = config.reasoning_effort(reasoning_effort);
Expand Down Expand Up @@ -2478,6 +2492,7 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
runtime,
config.inference_providers.clone(),
)?;
let runtime = kit::Runtime::with_model_override(runtime, model_override)?;
let runtime = kit::Runtime::with_compaction(runtime, config.compaction)?;
let runtime = kit::Runtime::with_skill_poll_interval(
runtime,
Expand Down Expand Up @@ -2537,6 +2552,7 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
} => {
let (config, telemetry_settings, _telemetry, openrouter_api_key) = initialize.await?;
let root = config.root(root);
let model_override = model.is_some() || provider.is_some();
let model = config.model(model);
let provider = config.provider(provider);
let reasoning_effort = config.reasoning_effort(reasoning_effort);
Expand All @@ -2562,6 +2578,7 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
runtime,
config.inference_providers.clone(),
)?;
let runtime = kit::Runtime::with_model_override(runtime, model_override)?;
Comment thread
danielkov marked this conversation as resolved.
Resolved
let runtime = kit::Runtime::with_compaction(runtime, config.compaction)?;
let runtime = kit::Runtime::with_skill_poll_interval(
runtime,
Expand Down Expand Up @@ -2658,6 +2675,7 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
Some(Some(id)) => Some(id),
None => None,
};
let model_override = model.is_some() || provider.is_some();
let model = config.model(model);
let provider = config.provider(provider);
let reasoning_effort = config.reasoning_effort(reasoning_effort);
Expand Down Expand Up @@ -2696,6 +2714,7 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
resume.as_deref(),
force,
voice_enabled,
model_override,
&mut stop,
)
.await?
Expand Down Expand Up @@ -3324,6 +3343,7 @@ mod tests {
let Command::Serve {
a2a,
stdio_protocol_version,
prefer_session_model,
remote_acp,
no_a2a,
no_stdio,
Expand All @@ -3341,6 +3361,7 @@ mod tests {
super::AcpProtocolVersion::V1
));
assert!(!remote_acp && !no_a2a && !no_stdio && !resume && !force);
assert!(!prefer_session_model);
assert!(!mcp.no_configured_mcp_config && !mcp.legacy_mcp_config);
assert!(mcp.credentials.credential_store.is_none());
let cli = Cli::try_parse_from(["kit", "acp"]).unwrap();
Expand Down
Loading
Loading