2 Commits
Author SHA1 Message Date
ZacharyZhang-NY 553ec1c620 chore(release): bump workspace version to 0.1.11
Warm build cache / warm (aarch64-apple-darwin) (push) Waiting to run
Warm build cache / warm (x86_64-apple-darwin) (push) Waiting to run
Warm build cache / warm (aarch64-unknown-linux-gnu) (push) Waiting to run
Warm build cache / warm (x86_64-pc-windows-msvc) (push) Waiting to run
Release / build (aarch64-apple-darwin) (push) Waiting to run
Release / build (x86_64-apple-darwin) (push) Waiting to run
Release / build (aarch64-unknown-linux-gnu) (push) Waiting to run
Release / build (x86_64-pc-windows-msvc) (push) Waiting to run
Release / publish GitHub Release (push) Blocked by required conditions
Warm build cache / warm (x86_64-unknown-linux-gnu) (push) Failing after 1m36s
Release / build (x86_64-unknown-linux-gnu) (push) Failing after 6s
All 62 crates inherit `workspace.package.version`, so the single edit
plus the refreshed lockfile carries the release. The `v0.1.11` tag is
what triggers the build; release.yml gates the tag against this value.

Ships the sessionless-model-switch fix (f656627): `/model` from a
non-project launch directory now starts the session its deferred switch
drains into instead of dangling silently.
2026-07-27 16:04:51 -04:00
ZacharyZhang-NY f656627c10 fix(tui): start the session a sessionless model switch defers into
Launching kigi in a non-project directory (~/Downloads, ~, /tmp — anywhere
`is_project_dir` rejects) leaves the agent view session-less behind the
project-picker question, which only a PLAIN prompt can open. Slash
commands still execute there, so `/model <name> <effort>` stashed its
switch in `deferred_model_switch` — a stash that assumes a create is in
flight — and with none pending it dangled forever with zero feedback:
the picker rendered, the user chose model and effort, and nothing
changed. Not a 0.1.10 regression: 0.1.9 pty-reproduces identically; the
report correlated with the upgrade only because every earlier launch
happened to be from a project directory.

Apply the QueueCommand precedent ("queued slash work bypasses the
picker, so create the deferred session or it never drains") to both
deferral sites — Action::SwitchModel in the router and
set_default_model's no-session branch — via
skip_picker_and_create_session, whose in-flight guard already makes it a
no-op while a create is pending, so the racing-create case is unchanged.
SessionCreated then applies the stash through the existing
apply_deferred_model_switch path; pty-verified end-to-end from
~/Downloads (session created, model changed, effort applied).
2026-07-27 15:55:57 -04:00
6 changed files with 171 additions and 73 deletions
Generated
+62 -62
View File
@@ -5442,7 +5442,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-acp-lib" name = "kigi-acp-lib"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"agent-client-protocol", "agent-client-protocol",
"async-trait", "async-trait",
@@ -5456,7 +5456,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-agent" name = "kigi-agent"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"chrono", "chrono",
"dirs 6.0.0", "dirs 6.0.0",
@@ -5486,7 +5486,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-agent-lifecycle" name = "kigi-agent-lifecycle"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"tokio", "tokio",
@@ -5495,7 +5495,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-auth" name = "kigi-auth"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"http 1.4.2", "http 1.4.2",
@@ -5508,7 +5508,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-bin" name = "kigi-bin"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -5543,7 +5543,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-chat-state" name = "kigi-chat-state"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"kigi-compaction", "kigi-compaction",
@@ -5560,7 +5560,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-codebase-graph" name = "kigi-codebase-graph"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"ahash", "ahash",
"clap", "clap",
@@ -5596,7 +5596,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-compaction" name = "kigi-compaction"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -5609,7 +5609,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-config" name = "kigi-config"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"base64", "base64",
"blake3", "blake3",
@@ -5632,7 +5632,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-config-types" name = "kigi-config-types"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"agent-client-protocol", "agent-client-protocol",
"indexmap", "indexmap",
@@ -5646,7 +5646,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-crash-handler" name = "kigi-crash-handler"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"libc", "libc",
@@ -5657,7 +5657,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-env" name = "kigi-env"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"tracing", "tracing",
"url", "url",
@@ -5665,7 +5665,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-fast-worktree" name = "kigi-fast-worktree"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -5697,7 +5697,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-file-utils" name = "kigi-file-utils"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"aws-config", "aws-config",
@@ -5721,7 +5721,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-fsnotify" name = "kigi-fsnotify"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"criterion", "criterion",
"dunce", "dunce",
@@ -5742,7 +5742,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-gix-status" name = "kigi-gix-status"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"gix", "gix",
"kigi-test-utils", "kigi-test-utils",
@@ -5752,7 +5752,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-hooks" name = "kigi-hooks"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"kigi-config", "kigi-config",
@@ -5771,7 +5771,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-hooks-plugins-types" name = "kigi-hooks-plugins-types"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@@ -5779,7 +5779,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-http" name = "kigi-http"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"kigi-auth", "kigi-auth",
"kigi-log", "kigi-log",
@@ -5794,7 +5794,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-hunk-tracker" name = "kigi-hunk-tracker"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"chrono", "chrono",
"dunce", "dunce",
@@ -5815,14 +5815,14 @@ dependencies = [
[[package]] [[package]]
name = "kigi-interjection-core" name = "kigi-interjection-core"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]] [[package]]
name = "kigi-log" name = "kigi-log"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -5840,7 +5840,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-markdown" name = "kigi-markdown"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"anstyle-lossy", "anstyle-lossy",
@@ -5864,14 +5864,14 @@ dependencies = [
[[package]] [[package]]
name = "kigi-markdown-core" name = "kigi-markdown-core"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"pulldown-cmark", "pulldown-cmark",
] ]
[[package]] [[package]]
name = "kigi-mcp" name = "kigi-mcp"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"agent-client-protocol", "agent-client-protocol",
"async-trait", "async-trait",
@@ -5908,7 +5908,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-memory" name = "kigi-memory"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
@@ -5942,7 +5942,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-mermaid" name = "kigi-mermaid"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"fontdb", "fontdb",
"image", "image",
@@ -5960,7 +5960,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-models" name = "kigi-models"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"kigi-env", "kigi-env",
"serde", "serde",
@@ -5970,7 +5970,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-pager-minimal" name = "kigi-pager-minimal"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"chrono", "chrono",
"crossterm", "crossterm",
@@ -5987,7 +5987,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-pager-pty-harness" name = "kigi-pager-pty-harness"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"alacritty_terminal", "alacritty_terminal",
"anyhow", "anyhow",
@@ -6012,7 +6012,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-pager-render" name = "kigi-pager-render"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"agent-client-protocol", "agent-client-protocol",
"anstyle", "anstyle",
@@ -6064,7 +6064,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-paths" name = "kigi-paths"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"camino", "camino",
"serde", "serde",
@@ -6074,7 +6074,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-prompt-queue" name = "kigi-prompt-queue"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@@ -6082,7 +6082,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-proto-build" name = "kigi-proto-build"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"pbjson-build", "pbjson-build",
@@ -6093,7 +6093,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-ratatui-inline" name = "kigi-ratatui-inline"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"ansi-width", "ansi-width",
"anstyle-parse 0.2.7", "anstyle-parse 0.2.7",
@@ -6110,7 +6110,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-ratatui-textarea" name = "kigi-ratatui-textarea"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"arboard", "arboard",
"chrono", "chrono",
@@ -6131,7 +6131,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-sampler" name = "kigi-sampler"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"async-openai", "async-openai",
"async-stream", "async-stream",
@@ -6154,7 +6154,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-sampling-types" name = "kigi-sampling-types"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"assert_matches", "assert_matches",
"async-openai", "async-openai",
@@ -6171,7 +6171,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-sandbox" name = "kigi-sandbox"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -6192,7 +6192,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-secrets" name = "kigi-secrets"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"regex", "regex",
"serde_json", "serde_json",
@@ -6230,7 +6230,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-shell" name = "kigi-shell"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"agent-client-protocol", "agent-client-protocol",
"anyhow", "anyhow",
@@ -6367,7 +6367,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-shell-base" name = "kigi-shell-base"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -6392,7 +6392,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-sqlite-journal" name = "kigi-sqlite-journal"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"libc", "libc",
"rusqlite", "rusqlite",
@@ -6403,7 +6403,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-subagent-resolution" name = "kigi-subagent-resolution"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"kigi-sampling-types", "kigi-sampling-types",
"kigi-tool-types", "kigi-tool-types",
@@ -6418,7 +6418,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-system-power" name = "kigi-system-power"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
"zbus", "zbus",
@@ -6426,7 +6426,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-test-support" name = "kigi-test-support"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"agent-client-protocol", "agent-client-protocol",
"anyhow", "anyhow",
@@ -6448,7 +6448,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-test-utils" name = "kigi-test-utils"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"runfiles", "runfiles",
"tracing", "tracing",
@@ -6457,11 +6457,11 @@ dependencies = [
[[package]] [[package]]
name = "kigi-token-estimation" name = "kigi-token-estimation"
version = "0.1.10" version = "0.1.11"
[[package]] [[package]]
name = "kigi-tool-protocol" name = "kigi-tool-protocol"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"kigi-tool-types", "kigi-tool-types",
"serde", "serde",
@@ -6472,7 +6472,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-tool-runtime" name = "kigi-tool-runtime"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -6490,7 +6490,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-tool-types" name = "kigi-tool-types"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"minijinja", "minijinja",
"schemars 1.2.1", "schemars 1.2.1",
@@ -6500,7 +6500,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-tools" name = "kigi-tools"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
@@ -6577,7 +6577,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-tools-api" name = "kigi-tools-api"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"kigi-proto-build", "kigi-proto-build",
"kigi-tool-protocol", "kigi-tool-protocol",
@@ -6590,11 +6590,11 @@ dependencies = [
[[package]] [[package]]
name = "kigi-tracing-macros" name = "kigi-tracing-macros"
version = "0.1.10" version = "0.1.11"
[[package]] [[package]]
name = "kigi-tty-utils" name = "kigi-tty-utils"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"libc", "libc",
"nix 0.30.1", "nix 0.30.1",
@@ -6604,7 +6604,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-tui" name = "kigi-tui"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"agent-client-protocol", "agent-client-protocol",
"ansi-to-tui", "ansi-to-tui",
@@ -6691,7 +6691,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-update" name = "kigi-update"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dunce", "dunce",
@@ -6720,14 +6720,14 @@ dependencies = [
[[package]] [[package]]
name = "kigi-version" name = "kigi-version"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"semver", "semver",
] ]
[[package]] [[package]]
name = "kigi-workspace" name = "kigi-workspace"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"agent-client-protocol", "agent-client-protocol",
"anyhow", "anyhow",
@@ -6806,7 +6806,7 @@ dependencies = [
[[package]] [[package]]
name = "kigi-workspace-types" name = "kigi-workspace-types"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"base64", "base64",
"chrono", "chrono",
@@ -8840,7 +8840,7 @@ dependencies = [
[[package]] [[package]]
name = "ptyctl" name = "ptyctl"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"alacritty_terminal", "alacritty_terminal",
"anyhow", "anyhow",
@@ -8858,7 +8858,7 @@ dependencies = [
[[package]] [[package]]
name = "ptyctl-cli" name = "ptyctl-cli"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
+1 -1
View File
@@ -76,7 +76,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.1.10" version = "0.1.11"
edition = "2024" edition = "2024"
license = "Apache-2.0" license = "Apache-2.0"
@@ -62,6 +62,7 @@ use super::session::lifecycle::{
clear_startup_actions, dispatch_agent_type_mismatch_answered, dispatch_exit_session, clear_startup_actions, dispatch_agent_type_mismatch_answered, dispatch_exit_session,
dispatch_new_session, dispatch_new_session_inner, dispatch_new_session_with_id, dispatch_new_session, dispatch_new_session_inner, dispatch_new_session_with_id,
dispatch_new_worktree_session, dispatch_trust_folder, open_new_session_question, dispatch_new_worktree_session, dispatch_trust_folder, open_new_session_question,
skip_picker_and_create_session,
}; };
use super::session::load::{ use super::session::load::{
dispatch_cycle_session_source_filter, dispatch_load_session, dispatch_pick_content_session, dispatch_cycle_session_source_filter, dispatch_load_session, dispatch_pick_content_session,
@@ -749,7 +750,12 @@ pub(crate) fn dispatch(action: Action, app: &mut AppView) -> Vec<Effect> {
}; };
let Some(session_id) = agent.session.session_id.clone() else { let Some(session_id) = agent.session.session_id.clone() else {
agent.session.deferred_model_switch = Some((model_id, effort)); agent.session.deferred_model_switch = Some((model_id, effort));
return vec![]; // No session bound: with a create in flight this is a no-op
// and `SessionCreated` applies the stash; with none in flight
// (project question pending — only a plain prompt opens it) a
// switch would dangle forever, so start the session like the
// QueueCommand arm does for queued slash work.
return skip_picker_and_create_session(app, id);
}; };
agent.session.model_switch_pending = true; agent.session.model_switch_pending = true;
vec![Effect::SwitchModel { vec![Effect::SwitchModel {
@@ -1519,12 +1519,20 @@ pub(in crate::app::dispatch) fn set_default_model(
effort: None, effort: None,
prev_model_id: prev_id.clone(), prev_model_id: prev_id.clone(),
}); });
} else if let Some(agent) = app.agents.get_mut(&aid) { } else {
// No session id yet — stash for if let Some(agent) = app.agents.get_mut(&aid) {
// `EventLoop::on_session_created` to apply once the session // No session id yet — stash for
// id materialises. Mirrors the deferred-switch handling in // `EventLoop::on_session_created` to apply once the session
// `Action::SwitchModel`. // id materialises. Mirrors the deferred-switch handling in
agent.session.deferred_model_switch = Some((new_id, None)); // `Action::SwitchModel`.
agent.session.deferred_model_switch = Some((new_id, None));
}
// With no create in flight (project question pending), the stash
// would dangle forever — start the session it drains into. No-op
// when a create is already pending.
effects.extend(
crate::app::dispatch::session::lifecycle::skip_picker_and_create_session(app, aid),
);
} }
effects effects
} }
@@ -481,7 +481,7 @@ fn session_failed_clears_flag_no_fetches() {
assert!(!app.agents[&id].pending_extensions_fetch); assert!(!app.agents[&id].pending_extensions_fetch);
} }
#[test] #[test]
fn switch_model_without_session_does_nothing() { fn switch_model_without_session_starts_the_session() {
let mut app = test_app_with_agent(); let mut app = test_app_with_agent();
let id = AgentId(0); let id = AgentId(0);
app.agents.get_mut(&id).unwrap().session.session_id = None; app.agents.get_mut(&id).unwrap().session.session_id = None;
@@ -493,7 +493,14 @@ fn switch_model_without_session_does_nothing() {
}, },
&mut app, &mut app,
); );
assert!(effects.is_empty()); // No create was in flight, so the switch starts the session its stash
// drains into (the stash alone dangled forever — the /model-in-Downloads
// silent no-op). `model_switch_pending` flips on SessionCreated.
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::CreateSession { .. }))
);
assert!(!app.agents[&id].session.model_switch_pending); assert!(!app.agents[&id].session.model_switch_pending);
} }
#[test] #[test]
@@ -668,11 +675,17 @@ fn switch_model_deferred_when_no_session_id() {
}, },
&mut app, &mut app,
); );
assert!(effects.is_empty()); // Stashed for SessionCreated — and the session it drains into is started
// (no create was in flight; a bare stash never drained).
assert_eq!( assert_eq!(
app.agents[&id].session.deferred_model_switch, app.agents[&id].session.deferred_model_switch,
Some((model_id, None)) Some((model_id, None))
); );
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::CreateSession { .. }))
);
assert!(!app.agents[&id].session.model_switch_pending); assert!(!app.agents[&id].session.model_switch_pending);
} }
#[test] #[test]
@@ -977,6 +977,77 @@ fn switch_model_pending_lifecycle() {
assert!(!app.agents[&id].session.model_switch_pending); assert!(!app.agents[&id].session.model_switch_pending);
} }
/// A model switch with no session AND no create in flight (the project-picker
/// question is pending; only a plain prompt can open it) must start the
/// deferred session itself, or the stashed switch dangles forever with zero
/// feedback — `/model X eff` in `~/Downloads` looked like "the model never
/// changes". Mirrors the `QueueCommand` arm: queued slash work bypasses the
/// picker and creates the session so the stash drains.
#[test]
fn switch_model_without_session_creates_the_deferred_session() {
let mut app = test_app_with_agent();
let id = AgentId(0);
app.agents.get_mut(&id).unwrap().session.session_id = None;
// Harness cwd is `/tmp` (a non-project dir); arm the picker gate the way
// startup leaves it (the harness pre-marks it shown for other tests).
app.project_picker_shown = false;
assert!(app.needs_project_picker());
let model_id = acp::ModelId::new(std::sync::Arc::from("kigi-4.5"));
let effects = dispatch(
Action::SwitchModel {
model_id: model_id.clone(),
effort: None,
},
&mut app,
);
assert_eq!(
app.agents[&id].session.deferred_model_switch,
Some((model_id, None)),
"switch must stay stashed for SessionCreated to apply"
);
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::CreateSession { .. })),
"sessionless switch must start the session the stash drains into"
);
}
/// Same stash path while a create IS in flight (`mcp_init_progress` set):
/// no duplicate `CreateSession` — the pending create applies the stash.
#[test]
fn switch_model_with_create_in_flight_does_not_duplicate_create() {
let mut app = test_app_with_agent();
let id = AgentId(0);
{
let agent = app.agents.get_mut(&id).unwrap();
agent.session.session_id = None;
agent.mcp_init_progress = Some(crate::app::agent_view::McpInitProgress {
total: 0,
connected: 0,
started_at: std::time::Instant::now(),
});
}
let effects = dispatch(
Action::SwitchModel {
model_id: acp::ModelId::new(std::sync::Arc::from("kigi-4.5")),
effort: None,
},
&mut app,
);
assert!(app.agents[&id].session.deferred_model_switch.is_some());
assert!(
!effects
.iter()
.any(|e| matches!(e, Effect::CreateSession { .. })),
"an in-flight create must not be duplicated"
);
}
#[test] #[test]
fn no_deferred_switch_means_no_extra_effect() { fn no_deferred_switch_means_no_extra_effect() {
// When there is no deferred model switch, SessionCreated should // When there is no deferred model switch, SessionCreated should