diff --git a/apps/desktop/src/app/chat/sidebar/session-row.tsx b/apps/desktop/src/app/chat/sidebar/session-row.tsx index 9f355b9d5f0e1..f8c03fbb0c09c 100644 --- a/apps/desktop/src/app/chat/sidebar/session-row.tsx +++ b/apps/desktop/src/app/chat/sidebar/session-row.tsx @@ -112,10 +112,9 @@ function SidebarSessionRowImpl({ const totalTokens = session.input_tokens + session.output_tokens const cost = sessionCostUsd(session) - // Tokens, cost and age share the one trailing slot rather than each claiming - // their own column: several switched on read as one figure, not as a - // widening gutter. - const pinnedFacts = [ + // Tokens, cost and age share one figure rather than each claiming a column: + // several switched on read as one number, not as a widening gutter. + const figures = [ rowMeta.includes('tokens') && totalTokens > 0 ? compactNumber(totalTokens) : null, // Sub-cent spend rounds to "$0.00", which reads as a bug rather than as a // cheap session — below a cent the row says nothing at all. @@ -123,18 +122,43 @@ function SidebarSessionRowImpl({ pinnedAge ? age : null ].filter(Boolean) as string[] - // The kebab covers the END of the slot, so only the last fact steps aside for - // it. With tokens and age both on, hovering costs you the age and keeps the - // number you switched on to read. - const pinnedTail = pinnedFacts.at(-1) ?? '' - const pinnedHead = pinnedFacts.slice(0, -1).join(' · ') - const pinnedLabel = pinnedFacts.join(' · ') - // Chips that ride in the BODY, beside the title. The kebab lifts out of the - // actions slot, so it only ever covers what's in there — a body chip has no - // reason to step aside, and the hover-age (which does overlay the body) is - // dropped rather than made to fight them. - const bodyChip = Boolean(pr) || pinnedProfile || (showProfile && hasProfileTag) - const pinnedMeta = Boolean(pinnedLabel) || bodyChip + // Everything the Show menu puts after the title shares ONE right-aligned + // slot, in reading order: identity chips, then the figures. The kebab covers + // the END of that slot on hover, so only the last thing in it steps aside — + // with tokens and age both on you lose the age and keep the number you + // switched on, and a PR keeps its place (and its click) unless it IS the last + // thing. Chips used to render in the body instead, which left them stranded + // to the left of the kebab's own column: never flush right, never swapping. + const trailing: { key: string; node: React.ReactNode }[] = [] + + if ((showProfile || pinnedProfile) && hasProfileTag) { + trailing.push({ key: 'profile', node: }) + } + + if (pr) { + trailing.push({ key: 'pr', node: }) + } + + if (figures.length) { + const head = figures.slice(0, -1).join(' · ') + + trailing.push({ + key: 'figures', + node: ( + + {head} + {/* The figures own their tail: the separator goes with it. */} + + {head && ' · '} + {figures.at(-1)} + + + ) + }) + } + + // A chip that ends the slot hides whole; the figures handle their own tail. + const chipEndsSlot = trailing.length > 0 && !figures.length // A handed-off session's live source is local, but it originated on a // messaging platform — surface that origin as a small badge so e.g. a // Telegram thread continued here still reads as Telegram. @@ -167,38 +191,26 @@ function SidebarSessionRowImpl({ > - {/* Pinned metadata stays put through a turn — it was switched on to - be read. Only the tail hands its slot to the kebab; anything - ahead of it stays legible while you hover. The hover-only age is - an overlay instead, so it needs the row to open up 48px of right - padding — which beside a body chip reads as a hole. A row that - already shows a chip skips it. */} - {(pinnedLabel || (!liveTurn && !bodyChip)) && ( + // whatever is switched on, with no width to hand-maintain. Nothing + // switched on leaves the slot to the kebab alone; hover changes what + // you can see in it, never how wide it is. +
+ {trailing.map(({ key, node }, index) => ( - {pinnedLabel ? ( - <> - {pinnedHead} - {/* Never narrower than the kebab that has to cover it. */} - - {pinnedHead && ' · '} - {pinnedTail} - - - ) : ( - age - )} + {node} - )} + ))} 0 && 'absolute right-0' )} size="icon" variant="ghost" @@ -266,10 +278,10 @@ function SidebarSessionRowImpl({ > {showsRunningArc(dotState) &&