fix(desktop): stop Inbox-style session cards from clipping text (#93036)

* fix(desktop): stop Inbox-style session cards from clipping glyph ink

leading-none plus truncate (overflow:hidden) made the line box equal the
em-square, so Segoe UI on Windows shaved letter tops and bottoms. Give
truncated sidebar text 1.35 line-height and tighten card gaps so the
taller lines still fit.

* test(desktop): lock Inbox card lines to a line-height that fits glyph ink

Assert the workspace, title, and footer lines keep leading-[1.35] and
never fall back to leading-none, which is what clipped the screenshot.
This commit is contained in:
xxxigm
2026-08-24 07:34:45 +07:00
committed by GitHub
parent d962b21fa4
commit 0c1f1d2fe5
4 changed files with 81 additions and 11 deletions
+6 -1
View File
@@ -39,7 +39,12 @@ const rowPadTrail = 'pr-2'
const rowGap = 'gap-1.5'
const rowLead = 'grid size-3.5 shrink-0 place-items-center'
const rowInset = cn(rowPadX, rowGap, 'flex h-full min-w-0 items-center self-stretch py-0.5')
const rowLabel = 'min-w-0 truncate text-[0.8125rem] leading-none text-(--ui-text-secondary)'
// `truncate` is overflow:hidden. `leading-none` (line-height: 1) makes the
// line box equal the em-square, so glyph ink that sticks out — Segoe UI on
// Windows is ~1.33em — gets shaved. 1.35 leaves room; the shell still owns
// row height, so the extra leading just centers.
export const SIDEBAR_TRUNCATED_LEADING = 'leading-[1.35]' as const
const rowLabel = cn('min-w-0 truncate text-[0.8125rem] text-(--ui-text-secondary)', SIDEBAR_TRUNCATED_LEADING)
/** Inbox-style card (workspace + age, title + preview, model + size). */
export const SIDEBAR_ROW_CARD_MIN_H = 'min-h-[3.375rem]' as const
@@ -21,6 +21,11 @@ vi.mock('@/i18n', () => ({
useI18n: () => ({
t: {
sidebar: {
messageCount: (count: number) => `${count} messages`,
toolCallCount: (count: number) => `${count} tool calls`,
projects: {
home: 'Home'
},
row: {
ageMin: 'm',
ageNow: 'now',
@@ -31,6 +36,7 @@ vi.mock('@/i18n', () => ({
needsInput: 'Needs input',
sessionActions: 'Session actions',
sessionRunning: 'Running',
todoProgress: 'Tasks completed',
waitingForAnswer: 'Waiting for answer'
}
},
@@ -150,9 +156,10 @@ const handoffAvatar = (container: HTMLElement) =>
const noop = vi.fn()
const renderRow = (session: SessionInfo) =>
const renderRow = (session: SessionInfo, extra?: { card?: boolean }) =>
render(
<SidebarSessionRow
card={extra?.card}
isPinned={false}
isSelected={false}
onArchive={noop}
@@ -388,3 +395,32 @@ describe('SidebarSessionRow', () => {
expect(tipTrigger(avatar as HTMLElement)).toBeTruthy()
})
})
describe('Inbox-style session card', () => {
it('gives truncated card lines room for glyph ink instead of clipping them', () => {
renderRow(
makeSession({
cwd: '/Users/tomek/pursuit-support-agent',
message_count: 133,
model: 'gpt-4.1',
title: 'Ruff lint and pytest verification'
}),
{ card: true }
)
const workspace = screen.getByText('pursuit-support-agent')
const title = screen.getByText('Ruff lint and pytest verification').parentElement
const footer = screen.getByText('GPT-4.1').parentElement
expect(title).toBeTruthy()
expect(footer).toBeTruthy()
for (const el of [workspace, title!, footer!]) {
expect(el.className).not.toMatch(/\bleading-none\b/)
expect(el.className).toMatch(/leading-\[1\.35\]/)
}
expect(workspace.className).toMatch(/\btruncate\b/)
expect(screen.getByText('133 messages')).toBeTruthy()
})
})
@@ -38,6 +38,7 @@ import { SessionStatusDot } from '../session-status-dot'
import {
SIDEBAR_ROW_CARD_MIN_H,
SIDEBAR_TRUNCATED_LEADING,
SidebarRowBody,
SidebarRowGrab,
SidebarRowLabel,
@@ -416,7 +417,7 @@ function SidebarSessionRowImpl({
// The card is a grid with ONE spacing knob: --card-gap. Every row
// gap is gap-y-(--card-gap); the title/preview group opts out
// with its own tighter internal flex gap.
card && 'flex-col items-stretch justify-center py-1.5 [--card-gap:0.6rem] gap-(--card-gap)'
card && 'flex-col items-stretch justify-center py-1.5 [--card-gap:0.4rem] gap-(--card-gap)'
)}
// Middle-click = open in a new tab (browser muscle memory).
{...middleClickHandlers(() => {
@@ -505,12 +506,22 @@ function SidebarSessionRowImpl({
deterministic metadata line; detailed adds the initial
request preview. Compact keeps today's one-line row. */}
{density !== 'compact' && details.metadata && (
<span className="mt-0.5 block truncate text-[0.625rem] leading-none text-(--ui-text-tertiary)">
<span
className={cn(
'mt-0.5 block truncate text-[0.625rem] text-(--ui-text-tertiary)',
SIDEBAR_TRUNCATED_LEADING
)}
>
{details.metadata}
</span>
)}
{density === 'detailed' && details.preview && (
<span className="mt-1 block truncate text-[0.625rem] leading-none text-(--ui-text-quaternary)">
<span
className={cn(
'mt-1 block truncate text-[0.625rem] text-(--ui-text-quaternary)',
SIDEBAR_TRUNCATED_LEADING
)}
>
{details.preview}
</span>
)}
@@ -528,7 +539,12 @@ function SidebarSessionRowImpl({
entire width — nothing truncates against the kebab. */}
<div className="flex min-w-0 items-center gap-1.5">
{leadNode}
<span className="min-w-0 flex-1 truncate text-[0.6875rem] leading-none text-(--ui-text-tertiary)">
<span
className={cn(
'min-w-0 flex-1 truncate text-[0.6875rem] text-(--ui-text-tertiary)',
SIDEBAR_TRUNCATED_LEADING
)}
>
{context}
</span>
{handoffBadge}
@@ -536,10 +552,13 @@ function SidebarSessionRowImpl({
</div>
{/* Title + preview: ONE grouped cell with its own tight
internal gap — it does not inherit the card's rhythm. */}
<div className="-mt-[0.2em] flex min-w-0 flex-col gap-[0.3rem]">
<div className="flex min-w-0 flex-col gap-[0.15rem]">
<OverflowTip label={title}>
<SidebarRowLabel
className="hover-marquee text-[0.8125rem] leading-none font-medium text-(--ui-text-primary) group-data-[working=true]:text-foreground"
className={cn(
'hover-marquee text-[0.8125rem] font-medium text-(--ui-text-primary) group-data-[working=true]:text-foreground',
SIDEBAR_TRUNCATED_LEADING
)}
onPointerEnter={armMarquee}
onPointerLeave={disarmMarquee}
>
@@ -547,13 +566,23 @@ function SidebarSessionRowImpl({
</SidebarRowLabel>
</OverflowTip>
{session.preview && rowMeta.includes('preview') ? (
<span className="min-w-0 truncate text-[0.625rem] leading-none text-(--ui-text-quaternary)">
<span
className={cn(
'min-w-0 truncate text-[0.625rem] text-(--ui-text-quaternary)',
SIDEBAR_TRUNCATED_LEADING
)}
>
{session.preview}
</span>
) : null}
</div>
{model || size || todoProgress ? (
<span className="flex min-w-0 items-baseline gap-2 text-[0.625rem] leading-none text-(--ui-text-tertiary)">
<span
className={cn(
'flex min-w-0 items-baseline gap-2 text-[0.625rem] text-(--ui-text-tertiary)',
SIDEBAR_TRUNCATED_LEADING
)}
>
{model ? <span className="min-w-0 truncate">{model}</span> : null}
{size ? <span className="shrink-0 tabular-nums">{size}</span> : null}
{todoProgress ? (
@@ -55,7 +55,7 @@ export interface VirtualSessionListProps {
// Matches the card's typical rendered height (four lines when a preview
// exists) so long card lists don't jump under the scroll thumb before
// self-measurement catches up.
const CARD_ROW_ESTIMATE_PX = 66
const CARD_ROW_ESTIMATE_PX = 74
const DIVIDER_ESTIMATE_PX = 28
const OVERSCAN_ROWS = 12