With a dozen agents in range the agent strip on the Usage page's Sessions tab grew wider than the window. Nothing bounded it, so its intrinsic width widened .sess-tools, the pane and the page, and #view-usage — which scrolls sideways — scrolled the whole page: the last agent's filter was only reachable by dragging the page itself. It joins the rule the app already had for a strip too wide for the window (the regions, and Connect's five APIs on a narrow one), which bounds it to 100% and lets it scroll sideways itself, and its options keep their own width as those strips' do. Chromium and WebKit, English and Chinese, API faked.
This commit is contained in:
1 parent
e63fbb0e69
commit
29e6d14857
3 files changed
+224
-6
No files matched your search
@@ -2190,15 +2190,17 @@ body.window .ag-sk-icon { width: 22px; height: 22px; border-radius: 6px; }
|
||||
pointer-events: none;
|
||||
}
|
||||
.segs .thumb { border-radius: 5px; }
|
||||
/* a strip too wide for the window scrolls in itself: the regions, and
|
||||
Connect's five APIs on a narrow one */
|
||||
.segs.regions, .connect .segs { max-width: 100%; overflow-x: auto; }
|
||||
/* a strip too wide for the window scrolls in itself: the regions,
|
||||
Connect's five APIs on a narrow one, and Usage → Sessions' agents,
|
||||
which #929 left widening the whole page until its last agent's
|
||||
filter was only reachable by dragging the page sideways */
|
||||
.segs.regions, .connect .segs, #sessAgent { max-width: 100%; overflow-x: auto; }
|
||||
/* no scrollbar-width here: Chromium (WebView2) then drops every
|
||||
::-webkit-scrollbar rule and draws Windows' classic bar, arrows and all,
|
||||
under the strip; the app's thin thumb instead, slimmer still */
|
||||
:where(body:not(.mac)) :is(.segs.regions, .connect .segs)::-webkit-scrollbar { height: 6px; }
|
||||
:where(body:not(.mac)) :is(.segs.regions, .connect .segs)::-webkit-scrollbar-thumb { border-width: 1px; }
|
||||
.segs.regions .opt, .connect .segs .opt { flex: none; }
|
||||
:where(body:not(.mac)) :is(.segs.regions, .connect .segs, #sessAgent)::-webkit-scrollbar { height: 6px; }
|
||||
:where(body:not(.mac)) :is(.segs.regions, .connect .segs, #sessAgent)::-webkit-scrollbar-thumb { border-width: 1px; }
|
||||
.segs.regions .opt, .connect .segs .opt, #sessAgent .opt { flex: none; }
|
||||
/* an installed list's order (#481): small, beside the list's heading */
|
||||
.segs.sortby { align-self: center; flex: none; }
|
||||
.segs.sortby .opt { height: 20px; padding: 0 8px; font-size: 11px; }
|
||||
|
||||
@@ -1078,6 +1078,17 @@ scroll refused), another agent's switches the page to it, and All sessions
|
||||
opens the page. At 420px a row still fits with its title readable. English
|
||||
and Chinese, WebKit (Chromium too where it launches), API faked.
|
||||
|
||||
`sess-agent-strip.test.cjs` keeps Usage → Sessions' agent filter inside the
|
||||
window (#929): with eleven agents in range the strip is wider than an 866px
|
||||
window, and it scrolls in itself instead of the page scrolling sideways, the
|
||||
way the regions strip and Connect's five APIs do; its first and last option
|
||||
are both reachable and both choose their agent, the model and folder picks
|
||||
beside it stay visible and open, and in a window wider than the strip nothing
|
||||
scrolls and the options keep their own widths. One agent in range still
|
||||
leaves the filter hidden, and the other strips on the page and elsewhere keep
|
||||
their behaviour. No left-border accent. English and Chinese, Chromium and
|
||||
WebKit, API faked.
|
||||
|
||||
`sessions-purge.test.cjs` erases sessions in magpie's trash for good (#487):
|
||||
a trashed row's Delete forever and the Trash's Empty trash each ask in
|
||||
magpie's own dialog (a browser `confirm()` fails the test), Cancel sends
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
// Run with Node's test runner and Playwright on the module path; see README.md.
|
||||
// #929 (yetone/magpie): with a dozen agents in range, Usage → Sessions' agent
|
||||
// filter grew wider than the window, so the whole page scrolled sideways and
|
||||
// the last agent's filter was only reachable by dragging the page itself. A
|
||||
// strip too wide for the window scrolls in itself — the regions strip and
|
||||
// Connect's five APIs already do — so this one does too, and its options keep
|
||||
// their own width. English and Chinese, Chromium and WebKit.
|
||||
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 at = (min) => new Date(Date.now() - min * 60e3).toISOString();
|
||||
// names long enough that eleven agents make a strip wider than an 866px window
|
||||
const NAMES = ["Trae CN Agent Long Name Here", "Kilo Code Agent Long Name", "Pi", "WorkBuddy",
|
||||
"DeepSeek Harness Agent", "Claude Code", "Codex", "omp", "OpenCode Agent", "Qoder Code", "ZCode Agent"];
|
||||
const NARROW = { width: 866, height: 800 };
|
||||
const WIDE = { width: 1600, height: 800 };
|
||||
|
||||
const sess = (agent, name, k) => ({
|
||||
agent, id: `${agent}-${k}`, cwd: `/work/${agent}/part${k}`, title: `task ${k} on ${name}`, start: at(20), last: at(10),
|
||||
models: [{ model: k ? "m-two" : "m-one", input: 1000, output: 100, cache_read: 500, cache_write: 0, cost: 0.5, priced: true }],
|
||||
input: 1000, output: 100, cache_read: 500, cache_write: 0, cost: 0.5, unpriced: 0,
|
||||
resume: "x", path: `~/.${agent}/x/${k}.jsonl`, name, icon: "generic",
|
||||
});
|
||||
|
||||
function serve(lang, names) {
|
||||
const sessions = names.flatMap((name, i) => [0, 1].map((k) => sess(`ag${i}`, name, k)));
|
||||
const agents = Object.fromEntries(names.map((name, i) => [`ag${i}`, name]));
|
||||
const state = { agents: [], profiles: [], settings: { lang, theme: "light" } };
|
||||
return async (r) => {
|
||||
const url = new URL(r.request().url());
|
||||
const json = (data) => r.fulfill({ json: data });
|
||||
if (url.pathname === "/boot.js") return r.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:false};` });
|
||||
if (url.pathname === "/wails/runtime.js") return r.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
|
||||
if (url.pathname === "/api/state") return json(state);
|
||||
if (url.pathname === "/api/sessions") return json({ sessions, terminal: true, dirs: [] });
|
||||
if (url.pathname === "/api/sessions/stats") {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const usage = sessions.map((s) => ({ agent: s.agent, cwd: s.cwd, model: s.models[0].model, input: s.input, output: s.output, cache_read: s.cache_read, cache_write: 0, cost: s.cost, priced: true }));
|
||||
return json({ from: today, to: today, days: [{ date: today, usage, active: [] }], agents });
|
||||
}
|
||||
if (url.pathname === "/api/sessions/overview") return json({ count: sessions.length, median: 5000, p90: 15000, days: [1], top: { tokens: [], cost: [], active: [] } });
|
||||
if (url.pathname === "/api/sessions/manage") {
|
||||
const ids = Object.keys(agents);
|
||||
const want = url.searchParams.get("agent");
|
||||
const agent = ids.includes(want) ? want : ids[0];
|
||||
return json({
|
||||
agents: ids.map((id) => ({ agent: id, count: 2, deletable: true, name: agents[id], icon: "generic" })),
|
||||
agent, sessions: sessions.filter((s) => s.agent === agent), terminal: true, trash: [], trashDir: "~/magpie/trash/sessions",
|
||||
});
|
||||
}
|
||||
if (url.pathname === "/api/groups") return json({ groups: [] });
|
||||
if (url.pathname === "/api/usage/quotas") return json([]);
|
||||
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)];
|
||||
const body = await fs.readFile(file).catch(() => null);
|
||||
await (body ? r.fulfill({ body, contentType }) : r.fulfill({ status: 404, body: "" }));
|
||||
};
|
||||
}
|
||||
|
||||
// a box's own sideways overflow, and whether it scrolls sideways at all
|
||||
const box = (page, sel) => page.locator(sel).evaluate((el) => ({
|
||||
scroll: el.scrollWidth, client: el.clientWidth, overflow: getComputedStyle(el).overflowX,
|
||||
}));
|
||||
const inside = (page, sel, outer = "#view-usage") => page.locator(sel).evaluate((el, o) => {
|
||||
const o1 = document.querySelector(o).getBoundingClientRect();
|
||||
const r = el.getBoundingClientRect();
|
||||
return r.left >= o1.left - 1 && r.right <= o1.right + 1;
|
||||
}, outer);
|
||||
|
||||
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
|
||||
for (const lang of ["en", "zh"]) {
|
||||
test(`${engine}: ${lang}: Usage → Sessions' agent strip scrolls in itself, not the page`, async (t) => {
|
||||
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
|
||||
const page = await (await browser.newContext({ viewport: NARROW, reducedMotion: "reduce" })).newPage();
|
||||
t.after(() => browser.close());
|
||||
page.setDefaultTimeout(8000);
|
||||
const errors = [];
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
await page.route("**/*", serve(lang, NAMES));
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem("magpie.usageTab", "sessions");
|
||||
localStorage.setItem("magpie.sessRange", "all");
|
||||
// the Usage page's own 5 s refresh would rebuild the strip between a
|
||||
// measurement and the click it measures; one read is enough here
|
||||
localStorage.setItem("magpie.usageEvery", "0");
|
||||
});
|
||||
await page.goto("http://magpie.test/?view=usage");
|
||||
await page.waitForFunction(() => document.querySelectorAll("#sessAgent .opt").length === 12);
|
||||
|
||||
// #929: eleven agents in range made the strip wider than the window, and
|
||||
// the page scrolled sideways instead of the strip
|
||||
const view = await box(page, "#view-usage");
|
||||
assert.ok(view.scroll <= view.client + 1, `the Usage page scrolls sideways by ${view.scroll - view.client}px with ${NAMES.length} agents in range`);
|
||||
const seg = await box(page, "#sessAgent");
|
||||
assert.equal(seg.overflow, "auto", "the agent strip doesn't scroll sideways in itself");
|
||||
assert.ok(seg.scroll > seg.client + 1, `the strip fits the window at ${seg.client}px, so nothing is being fixed`);
|
||||
|
||||
// every option is reachable: the last one, then the first, by scrolling
|
||||
// the strip — none of them is cut off past the strip's own edge
|
||||
const opts = page.locator("#sessAgent .opt");
|
||||
// a real click where a reader's would land: scroll the option into the
|
||||
// strip, then press the middle of it rather than trust a stale handle
|
||||
const press = async (which) => {
|
||||
for (let tries = 3; tries; tries--) {
|
||||
const p = await page.locator("#sessAgent").evaluate((strip, which) => {
|
||||
const all = strip.querySelectorAll(".opt");
|
||||
const el = which === "first" ? all[0] : all[all.length - 1];
|
||||
el.scrollIntoView({ block: "nearest", inline: "nearest" });
|
||||
const r = el.getBoundingClientRect();
|
||||
const s = strip.getBoundingClientRect();
|
||||
return { x: r.left + r.width / 2, y: r.top + r.height / 2, text: el.textContent, cut: r.left < s.left - 1 || r.right > s.right + 1 };
|
||||
}, which);
|
||||
assert.equal(p.cut, false, `the ${which} agent's filter sits outside the strip and is cut off`);
|
||||
await page.mouse.click(p.x, p.y);
|
||||
try {
|
||||
await page.waitForFunction((text) => document.querySelector("#sessAgent .opt.on")?.textContent === text, p.text, { polling: 100, timeout: 2000 });
|
||||
return p.text;
|
||||
} catch (e) {
|
||||
if (!tries) throw e; // WebKit drops the odd press; a reader would press again
|
||||
}
|
||||
}
|
||||
};
|
||||
const first = (await opts.first().innerText()).trim();
|
||||
assert.equal(await press("last"), NAMES[NAMES.length - 1], "clicking the last agent's filter didn't choose it");
|
||||
assert.equal(await press("first"), first, "clicking the first agent's filter didn't choose it again");
|
||||
|
||||
// the picks beside the strip stay visible and inside the window
|
||||
for (const id of ["#sessModel", "#sessFolder"]) {
|
||||
assert(await page.locator(id).isVisible(), `${id} is not visible beside the strip`);
|
||||
assert(await inside(page, id), `${id} sits outside the window`);
|
||||
}
|
||||
await page.locator("#sessModel").click();
|
||||
await page.locator(".pop.proto-menu.sess-menu").waitFor();
|
||||
await page.keyboard.press("Escape");
|
||||
await page.locator("#sessFolder").click();
|
||||
await page.locator(".pop.proto-menu.sess-menu").waitFor();
|
||||
await page.keyboard.press("Escape");
|
||||
// no click on the strip left an accent behind; the picks beside it keep
|
||||
// their own box border on all four sides
|
||||
const borders = await page.evaluate(() => [...document.querySelectorAll("#sessAgent .opt")]
|
||||
.map((e) => getComputedStyle(e).borderLeftWidth).filter((w) => w && parseFloat(w) > 0));
|
||||
assert.deepEqual(borders, [], "no left-border accent");
|
||||
|
||||
// a window wider than the strip: nothing scrolls sideways, and the
|
||||
// options keep their own widths instead of being stretched to fill it
|
||||
await page.setViewportSize(WIDE);
|
||||
await page.waitForTimeout(120);
|
||||
const wideView = await box(page, "#view-usage");
|
||||
assert.ok(wideView.scroll <= wideView.client + 1, `a window wider than the strip still scrolls the page sideways by ${wideView.scroll - wideView.client}px`);
|
||||
const fit = await box(page, "#sessAgent");
|
||||
assert.ok(fit.scroll <= fit.client + 1, "the strip scrolls sideways in a window that fits it");
|
||||
const widths = await opts.evaluateAll((els) => els.map((el) => ({ w: el.getBoundingClientRect().width, need: el.scrollWidth })));
|
||||
assert.ok(widths.every((o) => o.need <= o.w + 1), "an option was squeezed narrower than its own text");
|
||||
assert.ok(new Set(widths.map((o) => Math.round(o.w))).size > 1, "every option is the same width: they were stretched, not laid out by their text");
|
||||
assert.deepEqual(errors, []);
|
||||
|
||||
// one agent: the strip stays hidden, as it was before (#929 changed
|
||||
// nothing about when the filter shows)
|
||||
await page.unroute("**/*");
|
||||
await page.route("**/*", serve(lang, NAMES.slice(0, 1)));
|
||||
await page.reload();
|
||||
await page.waitForFunction(() => document.querySelectorAll("#sessAgent .opt").length > 0);
|
||||
assert.equal(await page.locator("#sessAgent").isVisible(), false, "one agent in range should leave the agent filter hidden");
|
||||
const one = await box(page, "#view-usage");
|
||||
assert.ok(one.scroll <= one.client + 1, "one agent in range still scrolls the page sideways");
|
||||
|
||||
// the other strips are untouched: the Usage page's own tab strip and
|
||||
// range strip still don't scroll in themselves, and the regions strip
|
||||
// and Connect's still do
|
||||
await page.unroute("**/*");
|
||||
await page.route("**/*", serve(lang, NAMES));
|
||||
await page.goto("http://magpie.test/?view=usage");
|
||||
await page.waitForFunction(() => document.querySelectorAll("#sessAgent .opt").length === 12);
|
||||
assert.equal((await box(page, "#period")).overflow, "visible", "the Usage page's range strip started scrolling in itself");
|
||||
assert.equal((await box(page, "#usageTab")).overflow, "visible", "the Usage page's tab strip started scrolling in itself");
|
||||
const others = await page.evaluate(() => {
|
||||
const out = {};
|
||||
const probe = (classes, host) => {
|
||||
const wrap = document.createElement("div");
|
||||
if (host) wrap.className = host;
|
||||
const el = document.createElement("div");
|
||||
el.className = classes;
|
||||
el.append(Object.assign(document.createElement("button"), { className: "opt", textContent: "x" }));
|
||||
wrap.append(el);
|
||||
document.body.append(wrap);
|
||||
out[host ? `${classes} in ${host}` : classes] = { overflow: getComputedStyle(el).overflowX, flex: getComputedStyle(el.firstChild).flex };
|
||||
wrap.remove();
|
||||
};
|
||||
probe("segs regions");
|
||||
probe("segs", "connect");
|
||||
return out;
|
||||
});
|
||||
assert.equal(others["segs regions"].overflow, "auto", "the regions strip stopped scrolling in itself");
|
||||
assert.equal(others["segs regions"].flex, "0 0 auto", "the regions strip's options lost their own width");
|
||||
assert.equal(others["segs in connect"].overflow, "auto", "Connect's API strip stopped scrolling in itself");
|
||||
assert.equal(others["segs in connect"].flex, "0 0 auto", "Connect's API options lost their own width");
|
||||
assert.deepEqual(errors, []);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user