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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user