fix(studio): the pin button hides until its row is hovered, a pinned row keeps a marker, and the model menu's image badge no longer wraps

.ds-row .sesspin-action set display: grid in a later rule of equal specificity than the one hiding it, so every row showed the button at rest. A pinned row shows .ds-pin at rest when quick actions are on, as it already did when they are off. The badge in the model menu could shrink to one character per line beside a long name; it is now flex: none. Both are held by perf scripts and a rows.css rule test.
This commit is contained in:
esengine committed 2026-11-07 09:19:30 -08:00
1 parent 3db2fc12a6
commit 03943d8a84
7 files changed
+106 -10

No files matched your search

@@ -0,0 +1,28 @@
import assert from "node:assert/strict";
import { chromium } from "playwright";
const PAGE = process.env.PERF_URL ?? "http://localhost:4399/perf.html?pref=zh";
const browser = await chromium.launch();
try {
const page = await browser.newPage({ viewport: { width: 1300, height: 900 } });
const errors = [];
page.on("pageerror", (error) => errors.push(error.message));
await page.goto(PAGE, { waitUntil: "networkidle" });
await page.waitForSelector(".model-picker");
await page.locator(".model-picker").first().click();
await page.waitForSelector(".studio-model-menu .studio-item-line");
const sizes = await page.evaluate(() => {
const lines = [...document.querySelectorAll(".studio-model-menu .studio-item-line")].filter((line) => line.querySelector(".studio-item-badge"));
for (const line of lines) line.querySelector(".lb").textContent = "workbuddy/deepseek-v4.1-flash-preview-extended";
return lines.map((line) => {
const badge = line.querySelector(".studio-item-badge").getBoundingClientRect();
return { width: Math.round(badge.width), height: Math.round(badge.height) };
});
});
assert.ok(sizes.length > 0, "the menu has a model that reads images");
for (const size of sizes) assert.ok(size.height <= 20, `a long model name leaves the badge on one line (${JSON.stringify(size)})`);
assert.deepEqual(errors, []);
console.log(`PASS model menu badge stays on one line beside a long name (${sizes.length} rows)`);
} finally {
await browser.close();
}
@@ -0,0 +1,43 @@
import assert from "node:assert/strict";
import { chromium } from "playwright";
const PAGE = process.env.PERF_URL ?? "http://localhost:4399/perf.html?pref=zh";
const browser = await chromium.launch();
try {
const page = await browser.newPage({ viewport: { width: 1300, height: 900 } });
const errors = [];
page.on("pageerror", (error) => errors.push(error.message));
await page.addInitScript(() => localStorage.setItem("rx-session-quick-actions", "on"));
await page.goto(PAGE, { waitUntil: "networkidle" });
await page.waitForSelector(".ds-row.sessrow");
await page.waitForTimeout(500);
const shown = (row, selector) =>
row.evaluate((el, sel) => {
const node = el.querySelector(sel);
return !!node && getComputedStyle(node).display !== "none" && node.getBoundingClientRect().width > 0;
}, selector);
const rows = page.locator(".ds-row.sessrow");
await page.mouse.move(700, 450);
for (let i = 0; i < Math.min(await rows.count(), 5); i++) {
assert.equal(await shown(rows.nth(i), ".sesspin-action"), false, `row ${i}: a resting row shows no pin button`);
assert.equal(await shown(rows.nth(i), ".sesspin-marker"), false, `row ${i}: an unpinned row shows no pin marker`);
}
const first = rows.first();
await first.hover();
assert.equal(await shown(first, ".sesspin-action"), true, "hovering a row offers the pin button");
await first.locator(".sesspin-action").click();
await page.waitForTimeout(400);
await page.evaluate(() => document.activeElement?.blur());
await page.mouse.move(700, 450);
await page.waitForTimeout(200);
const pinned = page.locator('.ds-row.sessrow:has(.sesspin-marker)').first();
assert.equal(await shown(pinned, ".sesspin-marker"), true, "a pinned row keeps its marker while the pointer is away");
assert.equal(await shown(pinned, ".sesspin-action"), false, "and still hides the button");
await pinned.hover();
assert.equal(await shown(pinned, ".sesspin-marker"), false, "the marker gives way to the button under the pointer");
assert.equal(await shown(pinned, ".sesspin-action"), true);
assert.deepEqual(errors, []);
console.log("PASS session row pin button and marker");
} finally {
await browser.close();
}
@@ -132,6 +132,7 @@
:root .composer-menu.studio-model-menu .studio-item-line { flex-wrap: nowrap; align-items: center; gap: 8px; }
:root .composer-menu.studio-model-menu .studio-item-line > .lb { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: var(--w-med) var(--fs-ui)/1.3 var(--ui); }
:root .composer-menu.studio-model-menu .studio-item-meta { flex: none; color: var(--faint); font: var(--w-reg) var(--fs-micro)/1.3 var(--ui); white-space: nowrap; }
:root .composer-menu.studio-model-menu .studio-item-badge { flex: none; white-space: nowrap; }
:root .composer-menu.studio-model-menu .mi[data-on]::after { color: var(--net-ink); }
:root .composer-menu.studio-effort-menu .mi.head { padding: 12px 14px 8px; }
:root .composer-menu.studio-effort-menu .mi.head:first-child .lb { color: var(--text); font: var(--w-semi) var(--fs-ui)/1.3 var(--ui); }
+4 -2
View File
@@ -48,8 +48,10 @@
@media (hover: none) { .ds-row .ds-more { display: grid; } }
.ds-row .ds-pin { width: 12px; height: 12px; flex: none; }
.ds-row .sessquick, .ds-row .sesspin-action { flex: none; display: none; }
.ds-row:is(:hover, :focus-within) .sessquick, .ds-row:is(:hover, :focus-within) .sesspin-action { display: flex; }
.ds-row .sessquick > button, .ds-row .sesspin-action { width: 24px; height: 28px; padding: 0; display: grid; place-items: center; border: 0; border-radius: var(--r-xs); background: transparent; color: var(--faint); cursor: pointer; }
.ds-row:is(:hover, :focus-within) .sessquick { display: flex; }
.ds-row:is(:hover, :focus-within) .sesspin-action { display: grid; }
.ds-row .sessquick > button, .ds-row .sesspin-action { width: 24px; height: 28px; padding: 0; place-items: center; border: 0; border-radius: var(--r-xs); background: transparent; color: var(--faint); cursor: pointer; }
.ds-row .sessquick > button { display: grid; }
.ds-row .sessquick > button:hover, .ds-row .sesspin-action:hover { background: var(--raised); color: var(--text); }
.ds-row .sessquick > button:disabled, .ds-row .sesspin-action:disabled { opacity: .35; cursor: default; }
.ds-row .sessquick .studio-icon, .ds-row .sesspin-action .studio-icon { width: 14px; height: 14px; }
@@ -32,4 +32,10 @@ describe("session and project rows follow the board", () => {
expect(rule(".ds-row:is(:hover, :focus-within) > :is(.ds-meta, .ds-pin)")).toMatch(/display:\s*none/);
expect(rule(".ds-row:is(:hover, :focus-within) .ds-more")).toMatch(/display:\s*grid/);
});
it("keeps the pin button hidden until the row is hovered or focused", () => {
expect(rule(".ds-row .sessquick, .ds-row .sesspin-action")).toMatch(/display:\s*none/);
expect(rule(".ds-row .sessquick > button, .ds-row .sesspin-action")).not.toMatch(/display:/);
expect(rule(".ds-row:is(:hover, :focus-within) .sesspin-action")).toMatch(/display:\s*grid/);
});
});
@@ -12,14 +12,18 @@ export function SessionPinAction({ target, title, pinned, enabled, disabled, onP
enabled: boolean;
onPin: () => void;
}) {
if (!enabled) return pinned ? <StudioIcon name="pin" className="sesspin-marker ds-pin" aria-label={t("session.pinned")} /> : null;
return <button className="sesspin-action" data-action="session.pin" data-target={target}
aria-pressed={pinned} disabled={disabled}
title={t(pinned ? "common.unpin" : "common.pin_session")}
aria-label={t(pinned ? "session.unpin_session_title" : "session.pin_session_title", { title })}
onClick={(event) => { event.stopPropagation(); onPin(); }}>
<StudioIcon name="pin" aria-label={pinned ? t("session.pinned") : undefined} />
</button>;
const marker = pinned ? <StudioIcon name="pin" className="sesspin-marker ds-pin" aria-label={enabled ? undefined : t("session.pinned")} aria-hidden={enabled ? true : undefined} /> : null;
if (!enabled) return marker;
return <>
{marker}
<button className="sesspin-action" data-action="session.pin" data-target={target}
aria-pressed={pinned} disabled={disabled}
title={t(pinned ? "common.unpin" : "common.pin_session")}
aria-label={t(pinned ? "session.unpin_session_title" : "session.pin_session_title", { title })}
onClick={(event) => { event.stopPropagation(); onPin(); }}>
<StudioIcon name="pin" aria-label={pinned ? t("session.pinned") : undefined} />
</button>
</>;
}
export function SessionArchiveAction({ target, title, archived, disabled, onArchive }: Target & {
@@ -0,0 +1,12 @@
## 修复
- 修复 2.58.0 里,开着「会话行快捷按钮」时,侧栏每一行会话都一直显示一个置顶图标(不管有没有置顶):现在这个按钮只在鼠标移上去或键盘聚焦时才出现;已置顶的会话在没悬停时会一直显示一个小图钉,方便一眼看出(#15012)。
- 修复输入框右下角的模型选单里,模型名字较长时,右边的「读图」标签被挤成两行竖着排的问题;现在标签始终在一行里完整显示(#15012)。
---
<!-- lang:en -->
## Fixed
- With "Quick buttons on session rows" turned on, every session row in the sidebar showed a pin icon all the time, pinned or not (2.58.0). The button now appears only when the pointer is over the row or it has keyboard focus, and a pinned session keeps a small pin on the row while nothing is hovered (#15012).
- In the model menu next to the message box, the "reads images" tag was squeezed into two stacked characters beside a long model name; it now stays whole on one line (#15012).