Skip to content

[bug][mobile] Copy and Delete sit on top of the message timestamp — 42px of 110px covered in WebKit, and removing the year only recovers 30px #426

Description

@serge-ivo

Two defects in the chat message header — and the first does not fix the second

Reported together, but they are independent, and it matters: removing the year saves 30px; the buttons cover 42px. Shortening the stamp alone leaves the overlap.

Measured — Playwright, WebKit, production, signed in

Instance bd43f4de-… (/console/instances/:id/chat), 12 bubbles, deviceScaleFactor 2, isMobile, hasTouch. Bounding-box intersection of each action button against the timestamp <span>:

=== WebKit @ 320px ===          === WebKit @ 390px ===
sample stamp: "8 Aug 2026 at 2:12 pm"  (110px wide)
  Copy message      overlaps by 18×16px
  Delete this turn  overlaps by 24×16px

Identical at both widths (the buttons are absolutely positioned, so the overlap does not scale). 42 of 110px — 38% of the timestamp — sits underneath the two buttons, including the minutes on the right-hand end.

Stamp width by format, same font (700 10px system-ui), WebKit:

current (with year)   "8 Aug 2026 at 2:12 pm"   116px   —
no year               "8 Aug at 2:12 pm"         86px   saves 30px
time only             "2:12 pm"                  41px   saves 75px

Where it is

The overlap — three individually-correct decisions:

  1. InstanceDetail.tsx:58CopyButton: className="tap-target absolute top-1 right-1.5 …"
  2. DeleteTurnButton.tsx:75className="tap-target absolute top-1 right-8 …"
  3. InstanceDetail.tsx:1123-1124 — the header row is flex items-center justify-between gap-3, so {formatDateTime(m.createdAt)} is pinned to the same right edge, at the same vertical band.

The reason it is mobile-only is the visibility rule both buttons share:

opacity-100 sm:opacity-0 sm:group-hover:opacity-100

Above sm they are invisible until hover, so the collision is never seen. Below sm they are permanently visible — correct (there is no hover on touch, and CopyButton's own comment says so) — and permanently on top of the timestamp. Nothing is wrong with any of the three decisions; the bug is that no one reserved the space.

The yearpackages/sdk/src/ui.ts:243:

return d.toLocaleString(undefined, { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" });

What to do

Fix 1 — reserve the corner (this is the overlap fix, and it is one class)

The buttons occupy 42px from the right edge. Give the header row that much padding below sm, where they are always visible:

// InstanceDetail.tsx:1123 and :1124 — the user and assistant header rows
className="text-2xs … flex items-center justify-between gap-3 pr-12 sm:pr-0"

pr-12 = 48px ≥ 42px measured, and sm:pr-0 keeps the desktop layout byte-for-byte (the buttons are hover-only there, so no space needs reserving). Verify with the same script rather than by eye — the acceptance criterion below is the measurement.

Fix 2 — drop the year, but conditionally

Do not remove year outright. formatDateTime is shared — IndexingTab.tsx:322 ("Indexed …"), SystemMessage.tsx:40, and both chat headers — and an unqualified "8 Aug" on a row from last year is worse than the width it saves. Show the year only when it is not the current year:

const now = new Date();
const sameYear = d.getFullYear() === now.getFullYear();
return d.toLocaleString(undefined, {
  month: "short", day: "numeric",
  ...(sameYear ? {} : { year: "numeric" }),
  hour: "numeric", minute: "2-digit",
});

Every current-year message gets the 30px back; an older one stays unambiguous. This also makes the change safe for the two non-chat call sites, which was the reason not to just delete the option.

The full timestamp remains available on hover — stampTitle (messageStamp.ts) already puts the complete local time with the named zone on title. That is what makes shortening the visible stamp safe rather than lossy.

Alternatives considered and rejected

  • Time-only stamps ("2:12 pm", saves 75px). Tempting — it would also cover the 42px on its own. Rejected: there is no day divider. I grepped for one (isSameDay, DayDivider, dateSeparator) — nothing. The per-message date is currently the only date context in the thread, so time-only would make a scrollback undatable. It becomes the right answer only as part of adding a day divider, which is a separate, larger change and should be its own ticket.
  • Move the buttons into the header row as flex items (no absolute positioning). Cleaner in principle, but it re-opens [a11y][mobile] 40 interactive elements per screen are under 40px — 12px checkboxes on Behaviour, 16px Remove on Repo, 24px message actions #389, which deliberately gave these controls tap-target (24×24 visual, 44px vertical reach) because they are absolutely positioned and can expand outside the row without making every header taller. Making them flex children puts that height back into the row. Not worth it for a padding bug.
  • Hide the timestamp on mobile. Rejected: Loop and system messages carry createdAt but never render it — and elapsed-between-steps is the number that matters #336 added these stamps deliberately, and the gap/time is the scanning signal for loop and system rows.
  • Move the timestamp to the bubble's bottom edge. A real option, but it is a visual redesign of every message and needs a design pass; the padding fix is one class and reversible.

Acceptance criteria

  • Re-running the measurement in WebKit at 320px and 390px reports zero bounding-box intersection between Copy message / Delete this turn and the timestamp span.
  • A current-year message renders no year; a previous-year message still does.
  • Desktop (≥sm) layout is unchanged — the buttons stay hover-revealed and no padding is reserved.
  • Hovering a stamp still shows the full local time with the zone name.

Regression risk

  • pr-12 narrows the space for the left-hand group ("You"/"Assistant" + the playback button). At 320px that group is ~90px against a bubble of ~270px, so there is room — but the check belongs in the same measurement run, not in review.
  • Changing shared formatDateTime touches IndexingTab and SystemMessage. The conditional keeps both correct; a test pinning "current year ⇒ no year, other year ⇒ year" is the cheap guard.
  • The e2e mobile guard runs Chromium; this was found and measured in WebKit. Whatever test is added should run WebKit, or it will not see this class of defect.

Related: #389 (the tap-target work that made these buttons always-visible on mobile — this is its unforeseen interaction), #336 (why the stamps exist), #333 (the prior mobile-width defect, and the note that per-row width is scarce), #345 (stampTitle and the stored timezone).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions