routing, gui: the context window's card folds to its head, with how full it is beside the title ("114K / 272K · 42%"), and stays folded for the next request and after the page comes back, so the request list under it has the room (0xBCD18E on X: 这个"上下文窗口"的板块非常占"上下"高度,还不能折叠,下面只能展示三行)

ctxCard takes opts.foldable (Routing passes it; the Usage page's cards
don't): its title becomes a button with a chevron, aria-expanded, and a
click toggles .shut on the card, which hides all but the head. The choice
is kept in localStorage (magpie.ctxShut). The head is morphed in place, so
the click reads the button from the event, and the card that is there
keeps .shut across live patches; a new card takes it from the saved choice.

Verified: routing-ctx-fold.test.cjs (new), at 420 and 1100 wide in
Chromium and WebKit, 3/3 each: open, then folded to one line (<60px) with
the figure, the list under it up by >200px, nothing runs over at 420px;
another request picked is still folded; after a reload still folded;
opened again, 400 cells and the same height as before. It fails on the old
code (no .ctx-fold). routing-*, context-*, usage*, group-context,
click-scroll, panel-routing, old-webkit, gui-ja/de/zh-tw: WebKit 371/371;
Chromium 370/371 once at --test-concurrency=4, routing-blocked's "picking
the request moved the page", which passes 3/3 alone and in two more
routing-* runs at the same concurrency (196/196 each). Not filed as caused
by this change; seen once under load.
This commit is contained in:
yetone committed 2026-10-09 01:40:02 +08:00
1 parent 0d20aaba45
commit af798bd1bf
4 files changed
+193 -4

No files matched your search

+11
View File
@@ -109,6 +109,17 @@ i.dot.free { background: var(--k-free); box-shadow: inset 0 0 0 1px color-mix(in
/* Routing: the card under the story of the request */
.rt-log .ctx-card { margin-top: 12px; }
/* folded: its head alone, how full it is beside the title */
.ctx-fold { display: inline-flex; align-items: center; gap: 6px; min-width: 0; height: 22px; margin: -2px 0 -2px -4px; padding: 0 6px 0 4px; border: 0; border-radius: 6px; background: none; color: inherit; font: inherit; cursor: pointer; }
.ctx-fold:hover { background: var(--pill); }
.ctx-fold .tw { display: inline-flex; width: 10px; flex: none; color: var(--faint); }
.ctx-fold .tw svg { transition: transform .2s var(--ease, ease); }
.ctx-card.shut .ctx-fold .tw svg { transform: rotate(-90deg); }
.ctx-short { display: none; flex: none; font-size: 11.5px; color: var(--muted); white-space: nowrap; }
.ctx-card.shut .ctx-short { display: inline; }
.ctx-card.shut { padding-block: 10px; }
.ctx-card.shut > :not(.ctx-head) { display: none; }
:where(.ctx-card.shut.max420) .ctx-model { display: none; }
/* the Usage page's Context tab */
.ctx-agents { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr)); gap: 12px; margin: 4px 0 18px; }
+28 -3
View File
@@ -227,8 +227,14 @@
// ctxUpdate(r, opts) draws another route, or the same one further on, in
// it: what is the same stays the same nodes, its grid's cells among them,
// so nothing flashes or comes in again and the grid keeps its height
// a card that folds (Routing's, opts.foldable) keeps the reader's
// choice: folded, it is its head alone, with how full the window is
// beside the title, so the request list under it has the room
// (0xBCD18E on X: it took most of the height, three rows were left)
let ctxShut = false;
try { ctxShut = localStorage.getItem("magpie.ctxShut") === "1"; } catch {}
function ctxCard(r, opts = {}) {
const card = el("div", "ctx-card");
const card = el("div", "ctx-card" + (opts.foldable && ctxShut ? " shut" : ""));
card.dataset.place = opts.place || "";
if (cardWidth[card.dataset.place] !== undefined) sizeCard(card, cardWidth[card.dataset.place]);
cardSizes.observe(card);
@@ -343,13 +349,32 @@
// the head: what it is and how it was counted
const head = el("div", "ctx-head");
const title = el("span", "ctx-title", t("Context window"));
let title = el("span", "ctx-title", t("Context window"));
if (opts.foldable) {
const fold = el("button", "ctx-fold");
fold.type = "button";
fold.setAttribute("aria-expanded", String(!card.classList.contains("shut")));
const tw = el("span", "tw");
tw.append(svg(CHEV, 10, 1.7));
fold.append(tw, title);
// the head is morphed, so the button that is there is the target
fold.onclick = (e) => {
ctxShut = !card.classList.contains("shut");
card.classList.toggle("shut", ctxShut);
tip.hidden = true;
e.currentTarget.setAttribute("aria-expanded", String(!ctxShut));
try { localStorage.setItem("magpie.ctxShut", ctxShut ? "1" : "0"); } catch {}
};
title = fold;
}
const st = el("span", "ctx-state " + (live ? "live" : p.counted ? "counted" : "est"));
st.append(el("i"), t(live ? "Live" : p.counted ? "Counted" : "Estimated"));
st.title = t(live ? "The request is on its way: the prompt is estimated from what the agent sent"
: p.counted ? "As many tokens as the vendor counted; the parts are measured from the request and scaled to it"
: "Estimated from what the agent sent: the vendor didn't say how many tokens it read");
head.append(title, el("span", "grow"));
head.append(title);
if (opts.foldable) head.append(el("span", "ctx-short", fmtK(p.tokens) + (window ? " / " + fmtK(window) + " · " + pct(full) : "")));
head.append(el("span", "grow"));
if (r.model) head.append(el("code", "ctx-model", r.model));
head.append(st);
+1 -1
View File
@@ -1452,7 +1452,7 @@
const card = ctxBox.firstElementChild;
const draw = card?.ctxUpdate && (still || !pinned) ? card.ctxUpdate : (r, o) => ctxBox.replaceChildren(window.ctxCard(r, o));
draw(r, {
still, series, tab: ctxTab, place: "routing",
still, series, tab: ctxTab, place: "routing", foldable: true,
onTab: (id) => { ctxTab = id; },
crumbs: [agentName(r.agent), sess && (sess.length > 14 ? sess.slice(0, 12) + "…" : sess), "#" + r.id],
onPoint: (pt) => {
@@ -0,0 +1,153 @@
// Run with Node's test runner and Playwright on the module path; see README.md.
// Routing: the context window's card folds to its head (0xBCD18E on X: it
// took most of the height, so the request list under it showed three
// rows, even on a portrait screen). Folded, it is one line with how full
// the window is beside the title, the list under it moves up, and it stays
// folded for the next request picked and after the page comes back;
// opened, it is all there again. In Chromium and WebKit, at a phone's
// width and a desk's.
const assert = require("node:assert/strict");
const fs = require("node:fs/promises");
const path = require("node:path");
const { test } = require("node:test");
const { chromium, webkit } = require("playwright");
const assets = path.resolve(__dirname, "../assets");
const now = Date.now();
const iso = (ms) => new Date(ms).toISOString();
const seat = { id: "codex@me", provider: "codex", name: "ChatGPT", who: "me@example.com", kind: "account", model: "gpt-6.1-sol", known: true, plan: "Pro" };
const prompt = (n) => ({
window: 272000, tokens: 90000 + n * 8000, counted: true, turns: n,
parts: [
{ kind: "system", tokens: 9000, items: [{ name: "prompt", tokens: 9000 }] },
{ kind: "tools", tokens: 12000, items: [{ name: "exec_command", tokens: 7000 }, { name: "apply_patch", tokens: 5000 }] },
{ kind: "files", tokens: 30000 + n * 4000, items: [{ name: "/work/app/routing.js", tag: "read", tokens: 20000 + n * 3000 }, { name: "/work/app/context.js", tag: "read", tokens: 10000 + n * 1000 }] },
{ kind: "results", tokens: 20000 + n * 2000, items: [{ name: "exec_command", tag: "result", n: n + 2, tokens: 20000 + n * 2000 }] },
{ kind: "chat", tokens: 19000 + n * 2000, items: [{ name: "turn", tag: "turns", n, tokens: 19000 + n * 2000 }] },
],
});
function req(id) {
const t0 = now - (200 - id) * 1000;
return {
id, seq: id * 10, time: iso(t0), agent: "codex", session: "019a-session", model: "gpt-6.1-sol", provider: "codex",
order: [seat], tries: [{ id: seat.id, model: seat.model, start: iso(t0), done: true, status: 200, ms: 4000 }],
done: true, status: 200, ms: 4000, tokens: 2000, cost: 0.05, priced: true, usage: [{ in: 3000, cache_read: 87000 + id * 100, out: 400 }],
prompt: prompt(id - 100),
};
}
const routes = [req(101), req(102), req(103)];
function serve() {
const state = { agents: [{ id: "codex", name: "Codex", path: "/test/codex", fields: [] }], profiles: [], settings: { lang: "en", theme: "light" } };
return async (route) => {
const url = new URL(route.request().url());
const json = (data) => route.fulfill({ json: data });
if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: 'window.bootPrefs = {lang:"en",theme:"light",web:true};' });
if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
if (url.pathname === "/api/state") return json(state);
if (url.pathname === "/api/gateway/trace") {
if (url.searchParams.get("wait")) return; // nothing more comes
return json({ mine: true, now: new Date().toISOString(), seq: routes[routes.length - 1].seq, totals: { requests: 0, rerouted: 0, errors: 0 }, routes });
}
if (url.pathname === "/api/gateway/history") return json({ cut: false, days: [], routes: [] });
if (url.pathname === "/api/groups") return json({ groups: [], models: [], pools: [] });
if (url.pathname === "/api/providers") return json({ providers: [], excluded: [], gateway: { running: true, window: true, url: "http://127.0.0.1:3999" } });
if (url.pathname.startsWith("/api/")) return json({});
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
await route.fulfill({ body: await fs.readFile(file).catch(() => ""), contentType });
};
}
const look = (page) => page.evaluate(() => {
const card = document.querySelector(".rt-ctx .ctx-card"), fold = card.querySelector(".ctx-fold"), short = card.querySelector(".ctx-short");
const shown = (e) => !!e && e.getClientRects().length > 0;
return {
shut: card.classList.contains("shut"),
expanded: fold.getAttribute("aria-expanded"),
height: card.offsetHeight,
short: shown(short) ? short.textContent : null,
grid: shown(card.querySelector(".ctx-waffle")),
contents: shown(card.querySelector(".ctx-contents")),
cells: card.querySelectorAll(".ctx-waffle > i").length,
crumbs: card.querySelector(".ctx-crumbs").textContent,
overflow: card.scrollWidth - card.clientWidth + (document.documentElement.scrollWidth - document.documentElement.clientWidth),
after: card.getBoundingClientRect().bottom,
};
});
// the reader scrolls to what they click with the wheel, as at a phone's
// width the card is under the request list
async function click(page, b) {
const h = page.viewportSize().height;
for (let i = 0; i < 20; i++) {
const r = await b.boundingBox();
if (r && r.y >= 0 && r.y + r.height <= h) break;
await page.mouse.move(page.viewportSize().width / 2, h / 2);
await page.mouse.wheel(0, r && r.y < 0 ? -200 : 200);
await page.waitForTimeout(100);
}
await b.click();
await page.waitForTimeout(300);
}
const fold = (page) => click(page, page.locator(".rt-ctx .ctx-fold"));
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
test(`${engine}: Routing's context window folds to its head and stays folded`, async (t) => {
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
t.after(() => browser.close());
for (const [width, height] of [[420, 760], [1100, 760]]) {
await t.test(`${width}x${height}`, async () => {
const context = await browser.newContext({ viewport: { width, height } });
const page = await context.newPage();
page.setDefaultTimeout(5000);
const errors = [];
page.on("pageerror", (e) => errors.push(e.message));
await page.route("**/*", serve());
await page.goto("http://magpie.test/?view=routing");
await page.locator(".rt-ctx .ctx-waffle").waitFor();
await page.waitForTimeout(800);
const open = await look(page);
assert.equal(open.shut, false, "it starts open");
assert.equal(open.expanded, "true");
assert(open.grid && open.contents, "open, the grid and contents are there");
assert.equal(open.short, null, "open, the short figure isn't shown");
await fold(page);
const shut = await look(page);
assert.equal(shut.shut, true, "a click folds it");
assert.equal(shut.expanded, "false");
assert(!shut.grid && !shut.contents, "folded, the grid and contents are gone");
assert.equal(shut.short, "114K / 272K · 42%", "folded, how full it is is beside the title");
assert(shut.height < 60, `folded, it is one line: ${shut.height}px`);
assert(shut.after < open.after - 200, `what is under it moves up: ${open.after} -> ${shut.after}`);
assert(shut.overflow <= 0, `nothing runs over at ${width}px: ${shut.overflow}`);
// another request picked: still folded, telling of that one
await click(page, page.locator(".rt-req").last());
const other = await look(page);
assert.equal(other.shut, true, "the next request's card is folded too");
assert.notEqual(other.crumbs, shut.crumbs, "it tells of the request picked");
// the page comes back: still folded
await page.reload();
await page.locator(".rt-ctx .ctx-card").waitFor();
await page.waitForTimeout(500);
assert.equal((await look(page)).shut, true, "it stays folded after the page comes back");
// opened again, it is all there
await fold(page);
const again = await look(page);
assert.equal(again.shut, false);
assert.equal(again.expanded, "true");
assert(again.grid && again.contents, "opened, the grid and contents are back");
assert.equal(again.cells, 400);
assert.equal(again.height, open.height, "and it is as tall as it was");
assert.deepEqual(errors, []);
await context.close();
});
}
});
}