fix(tui): repaint the header context window on a /model switch
After `/model`, the header kept the previous model's window ("15K /
1.0M" on a 262K model) until the next turn or a restart. The pager
caches the window inside its context snapshot (streaming `totalTokens`
updates refresh it from the catalog) and nothing refreshed the cache
when the session's model changed. Comparing cached numbers against
catalog windows cannot tell a catalog-derived total from a shell
effective limit (KIGI_DEBUG_CONTEXT_WINDOW, header upgrades), so the
shell, which installs the effective window, now reports it:
- kigi-shell: `handle_set_session_model` returns the installed window;
`model_switch::apply` stamps it as `totalContextTokens` in the
`session/set_model` response meta and carries it on the
`ModelChanged` broadcast (`context_window`, serde-default: old pagers
ignore it, old leaders omit it). The response meta previously
serialized the actor's inner `Result` and reported success on an
actor failure; that error now propagates.
- kigi-tui: `Effect::SwitchModel` reads the meta; `SwitchModelComplete`
carries `Ok(Option<u64>)`; the completion handler and the follower
`ModelChanged` arm call `AgentView::apply_context_window`, which
re-derives the snapshot (pct, free tokens) and reports whether it
changed so a follower redraws on a window-only change.
- Tests: the bare `/model <name>` dispatch path asserts the snapshot and
the drawn header; follower with/without the field; same-model new
window redraw; actor returns the window; serde round trip.
Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
This commit is contained in:
1 parent
ff92a00fea
commit
b957e12370
16 files changed
+288
-27
No files matched your search
@@ -978,13 +978,13 @@ pub(in crate::app::dispatch) fn handle_switch_model_complete(
|
||||
agent_id: AgentId,
|
||||
model_id: acp::ModelId,
|
||||
effort: Option<ReasoningEffort>,
|
||||
result: Result<(), SwitchModelError>,
|
||||
result: Result<Option<u64>, SwitchModelError>,
|
||||
prev_model_id: Option<acp::ModelId>,
|
||||
) -> Vec<Effect> {
|
||||
if let Some(agent) = app.agents.get_mut(&agent_id) {
|
||||
agent.session.model_switch_pending = false;
|
||||
let mut effects = match result {
|
||||
Ok(()) => {
|
||||
Ok(context_window) => {
|
||||
agent.session.user_model_preference = Some(model_id.clone());
|
||||
let display_name = agent
|
||||
.session
|
||||
@@ -996,6 +996,9 @@ pub(in crate::app::dispatch) fn handle_switch_model_complete(
|
||||
let prev_model = agent.session.models.current.clone();
|
||||
let prev_effort = agent.session.models.reasoning_effort;
|
||||
agent.session.models.set_current(model_id.clone(), effort);
|
||||
if let Some(total) = context_window {
|
||||
agent.apply_context_window(total);
|
||||
}
|
||||
let resolved_effort = agent.session.models.reasoning_effort;
|
||||
let unchanged =
|
||||
prev_model.as_ref() == Some(&model_id) && prev_effort == resolved_effort;
|
||||
|
||||
@@ -1789,7 +1789,7 @@ fn switch_model_holds_prompt_until_complete() {
|
||||
agent_id: id,
|
||||
model_id,
|
||||
effort: None,
|
||||
result: Ok(()),
|
||||
result: Ok(None),
|
||||
prev_model_id: None,
|
||||
}),
|
||||
&mut app,
|
||||
|
||||
@@ -282,7 +282,7 @@ fn model_switch_pending_resets_correctly_across_success_and_failure() {
|
||||
agent_id: id,
|
||||
model_id: model_a,
|
||||
effort: None,
|
||||
result: Ok(()),
|
||||
result: Ok(None),
|
||||
prev_model_id: None,
|
||||
}),
|
||||
&mut app,
|
||||
|
||||
@@ -535,7 +535,7 @@ fn switch_model_complete_success_updates_model_and_pushes_message() {
|
||||
agent_id: id,
|
||||
model_id: model_id.clone(),
|
||||
effort: None,
|
||||
result: Ok(()),
|
||||
result: Ok(None),
|
||||
prev_model_id: None,
|
||||
}),
|
||||
&mut app,
|
||||
@@ -554,6 +554,96 @@ fn switch_model_complete_success_updates_model_and_pushes_message() {
|
||||
));
|
||||
}
|
||||
|
||||
/// The bare `/model <name>` path: `SetDefaultModel` moves `models.current`
|
||||
/// before the ACP round trip; the completion then installs the effective
|
||||
/// window the shell reported, so the header repaints at once.
|
||||
#[test]
|
||||
fn switch_model_complete_installs_the_reported_context_window() {
|
||||
let mut app = test_app_with_agent();
|
||||
let id = AgentId(0);
|
||||
let model = |slug: &str, window: u64| {
|
||||
let mid = acp::ModelId::new(std::sync::Arc::from(slug));
|
||||
let info = acp::ModelInfo::new(mid.clone(), slug.to_string()).meta(
|
||||
serde_json::json!({ "totalContextTokens": window })
|
||||
.as_object()
|
||||
.cloned(),
|
||||
);
|
||||
(mid, info)
|
||||
};
|
||||
let (wide, wide_info) = model("wide", 1_000_000);
|
||||
let (narrow, narrow_info) = model("narrow", 262_144);
|
||||
{
|
||||
let agent = app.agents.get_mut(&id).unwrap();
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.available
|
||||
.insert(wide.clone(), wide_info);
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.available
|
||||
.insert(narrow.clone(), narrow_info);
|
||||
agent.session.models.set_current(wide.clone(), None);
|
||||
agent.apply_context_used(15_000, 1_000_000);
|
||||
}
|
||||
|
||||
let effects = dispatch(Action::SetDefaultModel(narrow.clone()), &mut app);
|
||||
assert!(
|
||||
effects
|
||||
.iter()
|
||||
.any(|e| matches!(e, Effect::SwitchModel { .. }))
|
||||
);
|
||||
assert!(app.agents[&id].session.model_switch_pending);
|
||||
assert_eq!(
|
||||
app.agents[&id].context_state.as_ref().unwrap().total,
|
||||
1_000_000
|
||||
);
|
||||
|
||||
dispatch(
|
||||
Action::TaskComplete(TaskResult::SwitchModelComplete {
|
||||
agent_id: id,
|
||||
model_id: narrow,
|
||||
effort: None,
|
||||
result: Ok(Some(262_144)),
|
||||
prev_model_id: Some(wide),
|
||||
}),
|
||||
&mut app,
|
||||
);
|
||||
|
||||
let agent = app.agents.get_mut(&id).unwrap();
|
||||
let snap = agent.context_state.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
(snap.used, snap.total, snap.usage_pct),
|
||||
(15_000, 262_144, 6)
|
||||
);
|
||||
let area = ratatui::layout::Rect::new(0, 0, 100, 40);
|
||||
let mut buf = ratatui::buffer::Buffer::empty(area);
|
||||
agent.draw(
|
||||
area,
|
||||
&mut buf,
|
||||
&crate::actions::ActionRegistry::defaults(),
|
||||
&mut crate::scrollback::render::ScratchBuffer::new(),
|
||||
None,
|
||||
false,
|
||||
0,
|
||||
None,
|
||||
&crate::app::bundle::BundleState::default(),
|
||||
false,
|
||||
&mut Vec::new(),
|
||||
);
|
||||
let screen: String = (0..area.height)
|
||||
.map(|y| {
|
||||
(0..area.width)
|
||||
.filter_map(|x| buf.cell((x, y)).map(|c| c.symbol().to_string()))
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(screen.contains("15K / 262K"), "header: {screen:?}");
|
||||
assert!(!screen.contains("1.0M"), "stale window: {screen:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switch_model_complete_skips_message_and_persist_when_unchanged() {
|
||||
let mut app = test_app_with_agent();
|
||||
@@ -575,7 +665,7 @@ fn switch_model_complete_skips_message_and_persist_when_unchanged() {
|
||||
agent_id: id,
|
||||
model_id: model_id.clone(),
|
||||
effort: None,
|
||||
result: Ok(()),
|
||||
result: Ok(None),
|
||||
prev_model_id: None,
|
||||
}),
|
||||
&mut app,
|
||||
@@ -630,7 +720,7 @@ fn switch_model_complete_persists_resolved_effort_from_catalog_meta() {
|
||||
model_id: model_id.clone(),
|
||||
// user typed `/model Blackbox 4.7` with no effort
|
||||
effort: None,
|
||||
result: Ok(()),
|
||||
result: Ok(None),
|
||||
prev_model_id: None,
|
||||
}),
|
||||
&mut app,
|
||||
@@ -696,7 +786,7 @@ fn switch_to_non_reasoning_model_clears_persisted_effort() {
|
||||
agent_id: id,
|
||||
model_id: model_id.clone(),
|
||||
effort: None,
|
||||
result: Ok(()),
|
||||
result: Ok(None),
|
||||
prev_model_id: None,
|
||||
}),
|
||||
&mut app,
|
||||
@@ -930,7 +1020,7 @@ fn same_agent_type_switch_no_modal() {
|
||||
agent_id: id,
|
||||
model_id: model_b.clone(),
|
||||
effort: None,
|
||||
result: Ok(()),
|
||||
result: Ok(None),
|
||||
prev_model_id: None,
|
||||
}),
|
||||
&mut app,
|
||||
@@ -969,7 +1059,7 @@ fn switch_model_pending_lifecycle() {
|
||||
agent_id: id,
|
||||
model_id,
|
||||
effort: None,
|
||||
result: Ok(()),
|
||||
result: Ok(None),
|
||||
prev_model_id: None,
|
||||
}),
|
||||
&mut app,
|
||||
|
||||
Reference in new issue
Block a user