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:
雷电芽衣 committed 2026-09-13 00:15:14 -04:00
1 parent ff92a00fea
commit b957e12370
16 files changed
+288 -27

No files matched your search

@@ -733,6 +733,20 @@ impl AgentView {
}
}
}
/// The shell reported the session's effective context window (model
/// switch). The snapshot caches the previous model's window, so the
/// header would show it until the next streaming update. Returns
/// whether the snapshot changed, so the caller can request a redraw.
pub fn apply_context_window(&mut self, total: u64) -> bool {
let Some((used, cached)) = self.context_state.as_ref().map(|c| (c.used, c.total)) else {
return false;
};
if cached == total {
return false;
}
self.apply_context_used(used, total);
true
}
/// Record a key event to the input flight recorder.
///
/// Zero heap allocations — stores raw `Copy` types in the ring buffer.
@@ -1262,6 +1276,36 @@ mod resolve_turn_activity_tests {
}
}
#[cfg(test)]
mod apply_context_window_tests {
use super::super::test_agent_view;
fn view() -> super::AgentView {
test_agent_view(Some("s1"), std::path::PathBuf::from("/tmp"))
}
#[test]
fn reported_window_replaces_the_cached_total() {
let mut agent = view();
agent.apply_context_used(15_000, 1_000_000);
assert!(agent.apply_context_window(262_144));
let snap = agent.context_state.as_ref().unwrap();
assert_eq!(
(snap.used, snap.total, snap.usage_pct, snap.free_tokens),
(15_000, 262_144, 6, 262_144 - 15_000)
);
}
#[test]
fn same_window_reports_no_change() {
let mut agent = view();
agent.apply_context_used(15_000, 1_000_000);
assert!(!agent.apply_context_window(1_000_000));
}
#[test]
fn no_snapshot_stays_none() {
let mut agent = view();
assert!(!agent.apply_context_window(262_144));
assert!(agent.context_state.is_none());
}
}
#[cfg(test)]
mod status_window_tests {
use super::super::test_agent_view;
#[test]