Skip to content

Commit 30e9b99

Browse files
authored
🤖 fix: sidebar layout — tighter indentation, always-visible actions, aligned status dots (#3124)
## Summary Overhauls the left sidebar layout to maximize horizontal real estate: actions always stay visible, group labels (A/B/C for best-of-n, variant names) never truncate, and status dots align directly with project folder icons — eliminating the vertical connector line. ## Background Three issues with the sidebar at narrow widths: 1. Action buttons (kebab menu, + button) got clipped by `overflow-hidden` and Radix's `display: table` inner wrapper 2. Variant/best-of-n indicators truncated away since they were part of the title text 3. Excessive left indentation wasted space, especially at deeper nesting levels ## Implementation **Radix ScrollArea fix** — The root cause of button clipping was Radix ScrollArea's internal `<div style="display: table; min-width: 100%">` wrapper. Table auto-layout sizes to max-content (full untruncated title width), expanding beyond the viewport. Combined with 4 layers of `overflow-hidden`, right-aligned buttons got pushed past the clip boundary. Fixed with a global CSS override: `[data-radix-scroll-area-viewport] > div { display: block !important; min-width: 0 !important; }`. **Status dot ↔ project icon alignment** — Removed the vertical connector line between project headers and workspace lists. Adjusted `getItemPaddingLeft` base from 4→10px so the status dot center (10px + 8px/2 = 18px) aligns with the project folder icon center (pl-2 8px + h-5/2 10px = 18px). Dropped workspace row margins from `ml-4`/`ml-5.5` to `ml-0`/`ml-2`. **Group labels as badges** — Split the variant/best-of label out of the truncated title string into a separate `shrink-0` span. Best-of-n candidates now get alphabetical labels (A, B, C…) derived from their index. ## Risks Low — all changes are CSS/layout. The Radix `display: table` override is the broadest change, but ScrollArea is only used in ProjectSidebar so impact is contained. Sub-agent connector lines (SVG elbows, trunk segments) are untouched and still use absolute positioning relative to `paddingLeft`. --- _Generated with `mux` • Model: `anthropic:claude-opus-4-6` • Thinking: `xhigh` • Cost: `$13.11`_ <!-- mux-attribution: model=anthropic:claude-opus-4-6 thinking=xhigh costs=13.11 -->
1 parent 3102607 commit 30e9b99

File tree

5 files changed

+44
-27
lines changed

5 files changed

+44
-27
lines changed

‎src/browser/components/AgentListItem/AgentListItem.tsx‎

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,12 @@ const HIDE_INLINE_ACTIONS_ON_MOBILE_TOUCH =
125125
const SHOW_INLINE_ACTIONS_ON_WIDE_TOUCH =
126126
"[@media(min-width:769px)_and_(hover:none)_and_(pointer:coarse)]:opacity-100";
127127

128-
/** Calculate left padding based on nesting depth */
128+
/** Calculate left padding based on nesting depth.
129+
* Base 10px places the status dot center at 18px — aligned with the project
130+
* folder icon center (pl-2 8px + half of h-5 w-5 button 10px = 18px). */
129131
function getItemPaddingLeft(depth?: number): number {
130132
const safeDepth = typeof depth === "number" && Number.isFinite(depth) ? Math.max(0, depth) : 0;
131-
return 8 + Math.min(32, safeDepth) * 12;
133+
return 10 + Math.min(32, safeDepth) * 8;
132134
}
133135

134136
function getSubAgentConnectorLeft(
@@ -314,7 +316,7 @@ function DraftAgentListItemInner(props: DraftAgentListItemProps) {
314316
<div
315317
className={cn(
316318
LIST_ITEM_BASE_CLASSES,
317-
sectionId != null ? "ml-8" : "ml-6.5",
319+
sectionId != null ? "ml-2" : "ml-0",
318320
"cursor-pointer pl-1 hover:bg-surface-secondary [&:hover_button]:opacity-100",
319321
isSelected && "bg-surface-secondary"
320322
)}
@@ -470,11 +472,15 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
470472

471473
// Display title (fallback to name for legacy workspaces without title)
472474
const workspaceTitle = metadata.title ?? metadata.name;
473-
const variantLabel =
474-
getTaskGroupKindFromMetadata(metadata.bestOf) === TASK_GROUP_KIND.VARIANTS
475-
? normalizeTaskGroupLabel(metadata.bestOf?.label)
475+
// Derive a short group label for grouped task children: explicit label for variants,
476+
// alphabetical letter (A, B, C…) for best-of-n candidates.
477+
const groupLabel =
478+
metadata.bestOf != null
479+
? getTaskGroupKindFromMetadata(metadata.bestOf) === TASK_GROUP_KIND.VARIANTS
480+
? normalizeTaskGroupLabel(metadata.bestOf.label)
481+
: String.fromCharCode(65 + (metadata.bestOf.index ?? 0))
476482
: undefined;
477-
const displayTitle = variantLabel ? `${variantLabel} · ${workspaceTitle}` : workspaceTitle;
483+
const displayTitle = groupLabel ? `${groupLabel} · ${workspaceTitle}` : workspaceTitle;
478484
const isEditing = editingWorkspaceId === workspaceId;
479485

480486
const linkSharingEnabled = useLinkSharingEnabled();
@@ -687,7 +693,9 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
687693
className={cn(
688694
LIST_ITEM_BASE_CLASSES,
689695
"group/row",
690-
sectionId != null ? "ml-7.5" : "ml-5",
696+
// No left margin — status dot aligns with the project folder icon.
697+
// Section members get a small indent for visual grouping.
698+
sectionId != null ? "ml-2" : "ml-0",
691699
isDragging && "opacity-50",
692700
isRemoving && "opacity-70",
693701
// Keep hover styles enabled for initializing workspaces so the row feels interactive.
@@ -1014,6 +1022,11 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
10141022
/>
10151023
) : (
10161024
<div className="flex min-w-0 items-center gap-1">
1025+
{/* Group label (variant name or A/B/C letter) rendered as a non-shrinkable
1026+
badge so it stays visible even when the sidebar is narrow. */}
1027+
{groupLabel && (
1028+
<span className="text-muted shrink-0 text-[12px] leading-6">{groupLabel}</span>
1029+
)}
10171030
<span
10181031
className={cn(
10191032
"min-w-0 flex-1 truncate text-left text-[14px] leading-6 transition-colors duration-200",
@@ -1022,7 +1035,7 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
10221035
titleColorClass
10231036
)}
10241037
>
1025-
{displayTitle}
1038+
{workspaceTitle}
10261039
</span>
10271040
</div>
10281041
)}

‎src/browser/components/ProjectSidebar/ProjectSidebar.tsx‎

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1941,8 +1941,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
19411941
data-project-path={projectPath}
19421942
className="text-secondary hover:bg-hover hover:border-border-light mr-1.5 flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded border border-transparent bg-transparent p-0 transition-all duration-200"
19431943
>
1944-
{/* Mobile: nudge folder icon left so it visually centers above connector line. */}
1945-
<span className="relative flex h-4 w-4 -translate-x-2 items-center justify-center md:translate-x-0">
1944+
<span className="relative flex h-4 w-4 items-center justify-center">
19461945
<ChevronRight
19471946
className="absolute inset-0 h-4 w-4 opacity-0 transition-[opacity,transform] duration-200 group-hover:opacity-100"
19481947
style={{
@@ -1967,7 +1966,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
19671966
</span>
19681967
</button>
19691968
<div
1970-
className="flex min-w-0 flex-1 items-center pr-2"
1969+
className="flex min-w-0 flex-1 items-center pr-1"
19711970
onContextMenu={(event) => handleOpenProjectMenu(event, projectPath)}
19721971
>
19731972
<Tooltip>
@@ -2081,15 +2080,9 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
20812080
aria-label={`Workspaces for ${projectName}`}
20822081
className="relative pt-1"
20832082
>
2084-
<div
2085-
aria-hidden="true"
2086-
className="bg-border pointer-events-none absolute top-1 bottom-0 left-4.5 w-px"
2087-
style={
2088-
projectFolderColor
2089-
? { backgroundColor: projectFolderColor }
2090-
: undefined
2091-
}
2092-
/>
2083+
{/* Vertical connector line removed — workspace status dots now
2084+
align directly with the project folder icon, so the tree
2085+
connector is no longer needed. */}
20932086
{(() => {
20942087
// Archived workspaces are excluded from workspaceMetadata so won't appear here
20952088

‎src/browser/components/ProjectSidebar/TaskGroupListItem.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ interface TaskGroupListItemProps {
2525
}
2626

2727
function getItemPaddingLeft(depth: number): number {
28-
return 12 + Math.min(32, Math.max(0, depth)) * 12;
28+
return 10 + Math.min(32, Math.max(0, depth)) * 8;
2929
}
3030

3131
export function TaskGroupListItem(props: TaskGroupListItemProps) {
@@ -56,7 +56,7 @@ export function TaskGroupListItem(props: TaskGroupListItemProps) {
5656
data-testid={`task-group-${props.groupId}`}
5757
className={cn(
5858
"bg-surface-primary relative flex items-start gap-1.5 rounded-l-sm py-2 pr-2 pl-1 select-none transition-all duration-150 hover:bg-surface-secondary",
59-
props.sectionId != null ? "ml-7.5" : "ml-5",
59+
props.sectionId != null ? "ml-2" : "ml-0",
6060
props.isSelected && "bg-surface-secondary"
6161
)}
6262
style={{ paddingLeft }}
@@ -72,7 +72,7 @@ export function TaskGroupListItem(props: TaskGroupListItemProps) {
7272
>
7373
<span
7474
aria-hidden="true"
75-
className="text-muted mt-0.5 -ml-4 inline-flex h-4 w-4 shrink-0 items-center justify-center"
75+
className="text-muted mt-0.5 -ml-2.5 inline-flex h-4 w-4 shrink-0 items-center justify-center"
7676
>
7777
<ChevronRight
7878
className="h-3 w-3 transition-transform duration-150"

‎src/browser/components/SectionHeader/SectionHeader.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export const SectionHeader: React.FC<SectionHeaderProps> = ({
107107

108108
return (
109109
<div
110-
className="group relative ml-4 flex items-center gap-1 py-1.5 pr-1 pl-3 select-none"
110+
className="group relative ml-0 flex items-center gap-1 py-1.5 pr-1 pl-2.5 select-none"
111111
data-section-id={section.id}
112112
>
113113
{/* Expand/Collapse Button */}
@@ -180,8 +180,9 @@ export const SectionHeader: React.FC<SectionHeaderProps> = ({
180180
</button>
181181
)}
182182

183-
{/* Right-side controls: add chat + section actions */}
184-
<div className="flex items-center opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)_and_(pointer:coarse)]:opacity-100">
183+
{/* Right-side controls: add chat + section actions. shrink-0 prevents flex
184+
compression from squeezing buttons out of the visible area at narrow widths. */}
185+
<div className="flex shrink-0 items-center opacity-0 transition-opacity group-hover:opacity-100 [@media(hover:none)_and_(pointer:coarse)]:opacity-100">
185186
{/* Add Chat — always visible on touch devices */}
186187
<Tooltip>
187188
<TooltipTrigger asChild>

‎src/browser/styles/globals.css‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2193,6 +2193,16 @@ input[type="checkbox"] {
21932193
}
21942194
}
21952195

2196+
/* Radix ScrollArea wraps viewport children in a `display: table; min-width: 100%`
2197+
div that lets content expand beyond the viewport width (table auto-layout sizes
2198+
to max-content). This pushes right-aligned elements (action buttons, badges) past
2199+
the overflow-hidden clip boundary. Force block layout so children respect the
2200+
viewport width constraint and flex rows truncate properly. */
2201+
[data-radix-scroll-area-viewport] > div {
2202+
display: block !important;
2203+
min-width: 0 !important;
2204+
}
2205+
21962206
.subagent-connector-active {
21972207
/* Remove solid background; dashes rendered by ::before pseudo-element */
21982208
background: none !important;

0 commit comments

Comments
 (0)