From b6d21b37b65dde9b5e58e37613014f06048e3682 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 20 Aug 2026 11:15:27 -0500 Subject: [PATCH 1/4] fix(desktop): sidebar row trailing inset, so the working arc stops clipping the age MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SidebarRowShell owned the row's height and, through the body, its leading inset — but nothing owned the trailing one. The actions slot rendered with no padding, so the age, chips and kebab sat on the row's border box. That is the same pixel a working row paints its arc on (`.arc-row` sets `--arc-standoff: 0rem`), so the animation ran straight through the text. Give the shell that inset. It is the only box containing both the one-line row's actions column and the card variant's in-body cluster, so one class covers every trailing thing a row can render. The card drops the body's label-to-actions gap in exchange: it has no such column to clear, and keeping the gap would pull its header in past every line below it. --- apps/desktop/src/app/chat/sidebar/chrome.tsx | 27 ++++++++++++++----- .../src/app/chat/sidebar/session-row.tsx | 8 ++++-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/app/chat/sidebar/chrome.tsx b/apps/desktop/src/app/chat/sidebar/chrome.tsx index a08bfa608c..ef83cc6d94 100644 --- a/apps/desktop/src/app/chat/sidebar/chrome.tsx +++ b/apps/desktop/src/app/chat/sidebar/chrome.tsx @@ -23,9 +23,19 @@ export function SidebarSectionMeta({ children }: { children: React.ReactNode }) // Height lives ONLY on SidebarRowShell (min-h-[1.625rem]). Inset children // stretch to fill the cell and center content internally — never items-center // on the shell grid, or short clusters (projects) float 1–2px off sessions. +// +// `rowPadX` is the BODY's padding: the lead's inset, plus the gap the label +// keeps from the actions column, both inside the row's click target. +// `rowPadTrail` is the row's own trailing inset and belongs to the SHELL — the +// only box containing both the actions column AND the card's in-body cluster, +// so one class insets every trailing thing a row can render. Owned anywhere +// else, the age / chips / kebab sit flush on the border box, which is exactly +// where a working row paints its arc (`.arc-row` has zero standoff) — the ring +// ran through the text. const rowMinH = 'min-h-[1.625rem]' -const rowPadX = 'pl-2 pr-1' +const rowPadX = 'pl-2 pr-2' +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') @@ -71,9 +81,11 @@ export function SidebarDateDivider({ ) } -/** Outer grid — sole owner of row height. The trailing `actions` slot is - * marked `data-row-actions` so a row-wide drag gesture can exclude it with - * one selector: it holds real controls, never grab surface. */ +/** Outer grid — sole owner of row height and of the trailing inset. The + * `actions` slot is marked `data-row-actions` so a row-wide drag gesture can + * exclude it with one selector: it holds real controls, never grab surface. + * It stretches so that exclusion covers the column, not just its tallest + * control. */ export function SidebarRowShell({ actions, actionsClassName, @@ -82,10 +94,13 @@ export function SidebarRowShell({ ...props }: React.ComponentProps<'div'> & { actions?: React.ReactNode; actionsClassName?: string }) { return ( -
+
{children} {actions ? ( -
+
{actions}
) : null} diff --git a/apps/desktop/src/app/chat/sidebar/session-row.tsx b/apps/desktop/src/app/chat/sidebar/session-row.tsx index 553061f882..aa88248daa 100644 --- a/apps/desktop/src/app/chat/sidebar/session-row.tsx +++ b/apps/desktop/src/app/chat/sidebar/session-row.tsx @@ -405,9 +405,13 @@ function SidebarSessionRowImpl({ Date: Thu, 20 Aug 2026 11:15:30 -0500 Subject: [PATCH 2/4] refactor(desktop): build the cron sidebar row from the shared row chrome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cron row had its own copy of the row grid — its own min-height, its own `grid-cols-[minmax(0,1fr)_auto]`, its own `pl-2 pr-1`, and a comment explaining that the numbers were chosen by hand to line up with the session rows above it. They had already drifted apart on the right edge. Compose SidebarRowShell / SidebarRowBody / SidebarRowLead / SidebarRowLabel instead, so a cron job and a session share one definition of what a row is and cannot drift again. --- .../app/chat/sidebar/cron-jobs-section.tsx | 98 ++++++++++--------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx b/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx index 9d4d54b9af..0ab0583447 100644 --- a/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +++ b/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx @@ -23,6 +23,7 @@ import type { CronJob } from '@/types/hermes' import { jobState, jobTitle, STATE_DOT } from '../../cron/job-state' import { SidebarPanelLabel } from '../../shell/sidebar-label' +import { SidebarRowBody, SidebarRowLabel, SidebarRowLead, SidebarRowShell } from './chrome' import { SidebarLoadMoreRow } from './load-more-row' const INACTIVE_STATES = new Set(['completed', 'disabled', 'error', 'paused']) @@ -304,21 +305,57 @@ function CronJobSidebarRow({ return (
+ {/* The shared row chrome, not a copy of it: a cron job and a session sit + in the same list, so they line up only if one place owns the geometry. */} -
- {/* Lead with the dot in the same w-3.5 cell + pl-2 the session rows use - so the cron dots line up with the sessions above; the caret sits next - to the label (matching the other sidebar disclosures) and the whole - label area toggles the run peek. */} + + + {meta} + +
+ + + + + + +
+
+ } + className="group/cron relative hover:bg-(--chrome-action-hover)" + > + {/* The caret sits next to the label (matching the other sidebar + disclosures) and the whole label area toggles the run peek. */} - + - {/* Trailing cluster: countdown by default, quick actions on hover. */} -
- - {meta} - -
- - - - - - -
-
-
+ {expanded && }
From 91670103fd74ecc6920156de22dc48106407f103 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 20 Aug 2026 11:15:34 -0500 Subject: [PATCH 3/4] fix(desktop): pagination ellipses land on the same sidebar edge as the rows "Load more" and a workspace's "show more" hang off the bottom of a list rather than sitting in a row, so they never saw the shell's trailing inset and stayed flush against the edge every row above them now stops short of. --- apps/desktop/src/app/chat/sidebar/load-more-row.tsx | 6 ++++-- .../src/app/chat/sidebar/projects/workspace-header.tsx | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/app/chat/sidebar/load-more-row.tsx b/apps/desktop/src/app/chat/sidebar/load-more-row.tsx index 617bad9172..a11e6d27c7 100644 --- a/apps/desktop/src/app/chat/sidebar/load-more-row.tsx +++ b/apps/desktop/src/app/chat/sidebar/load-more-row.tsx @@ -11,7 +11,9 @@ interface SidebarLoadMoreRowProps { // Compact "load more" affordance shared by recents, messaging, and cron. Kept // intentionally identical to workspace "show more" controls (ellipsis button) -// so pagination reads as one interaction everywhere. +// so pagination reads as one interaction everywhere. It hangs off the list +// instead of sitting in a row, so it repeats the row's trailing inset +// (SidebarRowShell's `pr-2`) to stay on the edge the rows stop at. export function SidebarLoadMoreRow({ step, onClick, loading = false }: SidebarLoadMoreRowProps) { const { t } = useI18n() const label = loading ? t.sidebar.loading : step > 0 ? t.sidebar.loadCount(step) : t.sidebar.loadMore @@ -20,7 +22,7 @@ export function SidebarLoadMoreRow({ step, onClick, loading = false }: SidebarLo