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
@@ -110,6 +110,70 @@
|
||||
);
|
||||
}
|
||||
|
||||
fn seed_windowed_models(agent: &mut AgentView) {
|
||||
for (slug, window) in [("wide", 1_000_000u64), ("narrow", 262_144)] {
|
||||
let mut info = make_model_info(slug);
|
||||
info.meta = serde_json::json!({ "totalContextTokens": window })
|
||||
.as_object()
|
||||
.cloned();
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.available
|
||||
.insert(acp::ModelId::new(std::sync::Arc::from(slug)), info);
|
||||
}
|
||||
agent
|
||||
.session
|
||||
.models
|
||||
.set_current(acp::ModelId::new(std::sync::Arc::from("wide")), None);
|
||||
agent.apply_context_used(15_000, 1_000_000);
|
||||
}
|
||||
|
||||
/// A follower mirrors a remote switch and installs the effective window
|
||||
/// the broadcast carries, like the invoking client does.
|
||||
#[test]
|
||||
fn model_changed_broadcast_installs_the_reported_context_window() {
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
seed_windowed_models(app.agents.get_mut(&AgentId(0)).unwrap());
|
||||
|
||||
let notif = model_changed_ext("sess-1", "narrow", None, Some(262_144));
|
||||
assert!(handle_ext_notification(¬if, &mut app));
|
||||
|
||||
let snap = app.agents[&AgentId(0)].context_state.as_ref().unwrap();
|
||||
assert_eq!((snap.used, snap.total, snap.usage_pct), (15_000, 262_144, 6));
|
||||
}
|
||||
|
||||
/// Same model, same effort, new effective window (the catalog was
|
||||
/// reloaded and re-applied): the follower must still request a redraw.
|
||||
#[test]
|
||||
fn model_changed_same_model_new_window_requests_redraw() {
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
seed_windowed_models(app.agents.get_mut(&AgentId(0)).unwrap());
|
||||
|
||||
let notif = model_changed_ext("sess-1", "wide", None, Some(2_000_000));
|
||||
assert!(handle_ext_notification(¬if, &mut app));
|
||||
|
||||
let snap = app.agents[&AgentId(0)].context_state.as_ref().unwrap();
|
||||
assert_eq!((snap.used, snap.total), (15_000, 2_000_000));
|
||||
|
||||
let again = model_changed_ext("sess-1", "wide", None, Some(2_000_000));
|
||||
assert!(!handle_ext_notification(&again, &mut app));
|
||||
}
|
||||
|
||||
/// A broadcast from a leader that predates the field carries no window;
|
||||
/// the snapshot is left for the next streaming update to refresh.
|
||||
#[test]
|
||||
fn model_changed_broadcast_without_window_keeps_the_snapshot() {
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
seed_windowed_models(app.agents.get_mut(&AgentId(0)).unwrap());
|
||||
|
||||
let notif = model_changed_ext("sess-1", "narrow", None, None);
|
||||
assert!(handle_ext_notification(¬if, &mut app));
|
||||
|
||||
let snap = app.agents[&AgentId(0)].context_state.as_ref().unwrap();
|
||||
assert_eq!((snap.used, snap.total), (15_000, 1_000_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn models_update_without_active_agent_uses_shell_default() {
|
||||
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
@@ -232,7 +296,7 @@
|
||||
let scrollback_before = agent.scrollback.len();
|
||||
assert!(!agent.session.model_switch_pending);
|
||||
|
||||
let notif = model_changed_ext("sess-1", "kigi-4", None);
|
||||
let notif = model_changed_ext("sess-1", "kigi-4", None, None);
|
||||
let changed = handle_ext_notification(¬if, &mut app);
|
||||
assert!(
|
||||
changed,
|
||||
@@ -278,7 +342,7 @@
|
||||
Some(acp::ModelId::new(std::sync::Arc::from("heavy")));
|
||||
assert!(!agent.session.model_switch_pending);
|
||||
|
||||
let notif = model_changed_ext("sess-1", "auto", None);
|
||||
let notif = model_changed_ext("sess-1", "auto", None, None);
|
||||
let changed = handle_ext_notification(¬if, &mut app);
|
||||
assert!(
|
||||
changed,
|
||||
@@ -328,7 +392,7 @@
|
||||
agent.session.model_switch_pending = true;
|
||||
let scrollback_before = agent.scrollback.len();
|
||||
|
||||
let notif = model_changed_ext("sess-1", "kigi-4", None);
|
||||
let notif = model_changed_ext("sess-1", "kigi-4", None, None);
|
||||
let changed = handle_ext_notification(¬if, &mut app);
|
||||
assert!(
|
||||
!changed,
|
||||
@@ -369,7 +433,7 @@
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
seed_models(agent, "kigi-3", &["kigi-3", "kigi-4"]);
|
||||
|
||||
let notif = model_changed_ext("sess-1", "kigi-99-unknown", None);
|
||||
let notif = model_changed_ext("sess-1", "kigi-99-unknown", None, None);
|
||||
let changed = handle_ext_notification(¬if, &mut app);
|
||||
assert!(
|
||||
!changed,
|
||||
@@ -400,7 +464,7 @@
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
seed_models(agent, "kigi-3", &["kigi-3", "kigi-4"]);
|
||||
|
||||
let notif = model_changed_ext("sess-1", "kigi-4", Some("high"));
|
||||
let notif = model_changed_ext("sess-1", "kigi-4", Some("high"), None);
|
||||
assert!(handle_ext_notification(¬if, &mut app));
|
||||
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
@@ -422,7 +486,7 @@
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
seed_models(agent, "kigi-3", &["kigi-3", "kigi-4"]);
|
||||
|
||||
let notif = model_changed_ext("sess-OTHER", "kigi-4", None);
|
||||
let notif = model_changed_ext("sess-OTHER", "kigi-4", None, None);
|
||||
let changed = handle_ext_notification(¬if, &mut app);
|
||||
assert!(!changed);
|
||||
|
||||
|
||||
Reference in new issue
Block a user