diff --git a/internal/gui/assets/app.js b/internal/gui/assets/app.js index 8483e05f..277bae4c 100644 --- a/internal/gui/assets/app.js +++ b/internal/gui/assets/app.js @@ -12443,7 +12443,7 @@ async function loadUsage(asked) { if (asked) loadQuotas(true); if (usageTab === "sessions") return loadSessions(); if (usageTab === "requests") return loadLedger(); - if (usageTab === "context") return window.loadContext(); + if (usageTab === "context") return window.loadContext?.(); renderUsageLoading(); if (!asked) loadQuotas(); const p = period, read = ++usageRead; @@ -20205,7 +20205,7 @@ function refreshUsage(now = false) { // the newest page takes the requests as they come; an older one stays put if (ledger && (want || !ledOffset)) await loadLedger(true); } else if (usageTab === "context") { - await window.loadContext(); + await window.loadContext?.(); } else { // the reader asking reads the allowances afresh, a Claude account's by // running Claude Code's own /usage (the backend runs it at most once in 30s) diff --git a/internal/gui/assets/context.js b/internal/gui/assets/context.js index 5c5f9dc7..df38a1d4 100644 --- a/internal/gui/assets/context.js +++ b/internal/gui/assets/context.js @@ -616,4 +616,7 @@ } window.loadContext = loadContext; window.renderContext = renderContext; + // opened on the Context tab (?view=usage): app.js showed it, and may have + // read the state, before this file was here to load it + if (view === "usage" && usageTab === "context") loadContext().catch((e) => status(e.message, "err")); })(); diff --git a/internal/gui/tests/context-window.test.cjs b/internal/gui/tests/context-window.test.cjs index 60dac1d9..cf42a008 100644 --- a/internal/gui/tests/context-window.test.cjs +++ b/internal/gui/tests/context-window.test.cjs @@ -180,3 +180,31 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", } } } + +// context.js comes after app.js, which shows the page and reads the state +// before it is there: the state answered first, the tab still loads at once +// rather than at the refresh's first tick, five seconds on +for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) { + test(`${engine}: the Context tab loads when context.js arrives after the state`, async (t) => { + const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" })); + t.after(() => browser.close()); + const page = await (await browser.newContext({ viewport: { width: 1100, height: 900 } })).newPage(); + const errors = [], asked = []; + page.on("pageerror", (e) => errors.push(e.message)); + const base = serve("en", asked); + let answered; + const stateDone = new Promise((res) => { answered = res; }); + await page.route("**/*", async (r) => { + const p = new URL(r.request().url()).pathname; + if (p === "/context.js") await stateDone.then(() => new Promise((res) => setTimeout(res, 150))); + await base(r); + if (p === "/api/state") answered(); + }); + await page.addInitScript(() => { try { localStorage.clear(); localStorage.setItem("magpie.usageTab", "context"); } catch {} }); + await page.goto("http://magpie.test/?view=usage"); + await page.locator(".ctx-agent", { hasText: "Codex" }).waitFor({ timeout: 2500 }); + assert.equal(asked[0], "7"); + assert.doesNotMatch(await page.locator("#status").innerText(), /loadContext|not a function/); + assert.deepEqual(errors, []); + }); +}