fix(desktop): align agent message metadata

Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
Matt Toohey
2026-08-26 16:35:32 +10:00
parent e8172b5bac
commit 5b6e3737af
3 changed files with 74 additions and 21 deletions

View File

@@ -209,7 +209,7 @@ export function InboxMessageRow({
<div className="min-w-0 flex-1">
{isContinuation ? null : (
<div
className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0"
className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0"
data-testid="message-header"
>
<UserProfilePopover
@@ -236,7 +236,7 @@ export function InboxMessageRow({
start of a line on its own. Gap matches the container's, so
spacing reads the same either side of the divider.
*/}
<span className="inline-flex min-w-0 items-center gap-x-2">
<span className="inline-flex min-w-0 items-baseline gap-x-2">
<MessageMetaSeparator />
{timestampNode}
</span>

View File

@@ -11,30 +11,13 @@ export function MessageAgentOwner({
}) {
return (
<span
className="inline-flex min-w-0 max-w-56 items-baseline gap-1 text-xs leading-4 text-muted-foreground/65"
className="inline-flex min-w-0 max-w-56 items-baseline gap-1 text-message-timestamp text-muted-foreground/65"
data-testid="message-agent-owner"
>
<span className="sr-only">
{ownerLabel ? "Agent managed by" : "Agent; owner unavailable"}
</span>
{/*
* Icon and label sit directly in this baseline row rather than in a nested
* flex wrapper, so the label's own baseline is what aligns with the author
* name beside it. Both branches share the icon for the same reason: two
* wrappers meant two alignment rules and the "owner unavailable" variant
* had drifted a pixel off the other one.
*
* `self-center` keeps the icon out of baseline alignment, so the label —
* not the icon's box — sets this chip's baseline. Centred on the line box
* the glyph's ink still rides ~1.6px above the text's cap band, reading as
* a couple of pixels too high; 0.125em drops its optical centre onto that
* band. In em so it holds under Cmd +/- zoom, and as a transform so it
* shifts nothing else in the row.
*/}
<Bot
aria-hidden="true"
className="h-3.5 w-3.5 shrink-0 translate-y-[0.125em] self-center"
/>
<Bot aria-hidden="true" className="h-3.5 w-3.5 shrink-0 self-center" />
{ownerPubkey && ownerLabel ? (
<>
<span aria-hidden="true" className="shrink-0">

View File

@@ -49,6 +49,18 @@ async function expectThreadReplyUnobscured(row: Locator) {
.toBe(true);
}
async function measureInlineBaseline(locator: Locator) {
return locator.evaluate((element) => {
const marker = document.createElement("span");
marker.style.cssText =
"display:inline-block;width:0;height:0;margin:0;padding:0;border:0";
element.append(marker);
const baseline = marker.getBoundingClientRect().top;
marker.remove();
return baseline;
});
}
async function measureThreadSummaryGeometry(summaryRow: Locator) {
return summaryRow.evaluate((summaryButton) => {
const summaryWrapper = summaryButton.parentElement;
@@ -396,6 +408,64 @@ test("agent owner label identifies the agent and owner", async ({ page }) => {
await expect(ownerTreatment.locator(".sr-only")).toHaveText(
"Agent managed by",
);
const author = aliceMessage.getByTestId("message-author");
const managedBy = ownerTreatment.getByText("managed by", { exact: true });
const owner = ownerTreatment.locator(".font-semibold");
const timestamp = aliceMessage.getByTestId("message-timestamp");
for (const fontSize of ["smaller", "default", "larger"]) {
await page.evaluate((value) => {
document.documentElement.dataset.fontSize = value;
}, fontSize);
const [
authorBaseline,
managedByBaseline,
ownerBaseline,
timestampBaseline,
] = await Promise.all([
measureInlineBaseline(author),
measureInlineBaseline(managedBy),
measureInlineBaseline(owner),
measureInlineBaseline(timestamp),
]);
const [ownerTypography, timestampTypography] = await Promise.all([
ownerTreatment.evaluate((element) => {
const style = getComputedStyle(element);
return { fontSize: style.fontSize, lineHeight: style.lineHeight };
}),
timestamp.evaluate((element) => {
const style = getComputedStyle(element);
return { fontSize: style.fontSize, lineHeight: style.lineHeight };
}),
]);
const iconCenterOffset = await ownerTreatment.evaluate((element) => {
const icon = element.querySelector("svg");
if (!icon) throw new Error("Expected the agent owner icon.");
const ownerRect = element.getBoundingClientRect();
const iconRect = icon.getBoundingClientRect();
return (
iconRect.top +
iconRect.height / 2 -
(ownerRect.top + ownerRect.height / 2)
);
});
expect(
{ managedByBaseline, ownerBaseline, timestampBaseline },
`message header baselines at ${fontSize} font size`,
).toEqual({
managedByBaseline: authorBaseline,
ownerBaseline: authorBaseline,
timestampBaseline: authorBaseline,
});
expect(ownerTypography).toEqual(timestampTypography);
expect(
Math.abs(iconCenterOffset),
`agent icon center at ${fontSize} font size`,
).toBeLessThan(0.01);
}
});
test("send a message and see it in timeline", async ({ page }) => {