Refresh context usage during generation

Update the context-window indicator when each new Agent 0 generation starts while deduplicating streamed updates. Keep the completion refresh for final provider usage and cover the event-driven behavior in the plugin contract and regression test.

Alessandro committed Aug 27, 2026 at 14:28 UTC 6a6cecff8527b164668c7a6ab2f76b6b1ed7cfa1
3 files changed +28 -1
plugins/_context_window/AGENTS.md
+2
@@ -41,6 +41,8 @@
41 restores the accepted response after the accounting tail is drained.
42 - Responses API turns keep their native result and callback behavior unchanged.
43 - Older chats without a stored breakdown show the explanatory empty state.
44 +- The indicator refreshes once per new Agent 0 generation and again when the
45 + run completes; streamed updates to the same generation do not refetch it.
46 - `_model_config` supplies the effective model limit and the
47 `model-context-strip-end` WebUI slot; it does not own this feature's state.
48 - The `contextWindowUsage` Interface setting defaults to visible on mobile and
plugins/_context_window/extensions/webui/apply_snapshot_before/refresh-context-window.js
+19 -1
@@ -3,6 +3,17 @@ import { store as contextWindowStore } from "/plugins/_context_window/webui/cont
3 const OVERRIDE_REVISION_KEY = "_model_config_override_revision";
4 let lastContextId = "";
5 let lastRevision = null;
6 +let lastGenerationKey = "";
7 +
8 +function latestGenerationKey(logs) {
9 + if (!Array.isArray(logs)) return "";
10 + for (let index = logs.length - 1; index >= 0; index--) {
11 + const item = logs[index];
12 + if (item?.type !== "agent" || Number(item.agentno || 0) !== 0) continue;
13 + return `${item.no ?? ""}:${item.id ?? ""}`;
14 + }
15 + return "";
16 +}
17
18 export default async function refreshContextWindow(ctx) {
19 const snapshot = ctx?.snapshot;
@@ -10,15 +21,22 @@ export default async function refreshContextWindow(ctx) {
21 if (!contextId) {
22 lastContextId = "";
23 lastRevision = null;
24 + lastGenerationKey = "";
25 return;
26 }
27
28 const contexts = Array.isArray(snapshot?.contexts) ? snapshot.contexts : [];
29 const active = contexts.find(item => item?.id === contextId) || null;
30 const revision = active?.[OVERRIDE_REVISION_KEY] || null;
19 - if (contextId === lastContextId && revision === lastRevision) return;
31 + const generationKey = latestGenerationKey(snapshot?.logs);
32 + if (
33 + contextId === lastContextId
34 + && revision === lastRevision
35 + && (!generationKey || generationKey === lastGenerationKey)
36 + ) return;
37
38 lastContextId = contextId;
39 lastRevision = revision;
40 + if (generationKey) lastGenerationKey = generationKey;
41 await contextWindowStore.refresh(contextId);
42 }
plugins/_context_window/tests/test_context_window.py
+7
@@ -145,6 +145,10 @@ def test_webui_and_accounting_are_plugin_owned():
145 helper = (ROOT / "plugins/_context_window/helpers/usage.py").read_text(
146 encoding="utf-8"
147 )
148 + refresh_hook = (
149 + ROOT
150 + / "plugins/_context_window/extensions/webui/apply_snapshot_before/refresh-context-window.js"
151 + ).read_text(encoding="utf-8")
152
153 assert 'id="model-context-strip-end"' in model_switcher
154 assert "contextWindowUsage" not in model_switcher
@@ -186,6 +190,9 @@ def test_webui_and_accounting_are_plugin_owned():
190 assert "Breakdown available after the next message." in component
191 assert "startswith(" not in helper
192 assert "rpartition(" not in helper
193 + assert 'item?.type !== "agent"' in refresh_hook
194 + assert "Number(item.agentno || 0) !== 0" in refresh_hook
195 + assert "generationKey === lastGenerationKey" in refresh_hook
196
197
198 def test_source_prompt_extensions_are_registered():