Skip to content

Commit 72ed2cc

Browse files
committed
🤖 fix: restore pre-redesign sidebar hierarchy
Restore the flatter sidebar hierarchy so project rows, section headers, and "Older than …" buckets read as distinct layers again. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `n/a`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=n/a -->
1 parent 3102607 commit 72ed2cc

File tree

6 files changed

+211
-195
lines changed

6 files changed

+211
-195
lines changed

‎src/browser/components/AddSectionButton/AddSectionButton.tsx‎

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ import { Plus } from "lucide-react";
33
// import { Tooltip, TooltipTrigger, TooltipContent } from "../Tooltip/Tooltip";
44

55
interface AddSectionButtonProps {
6-
onCreateSection: (name: string) => void;
6+
onCreateSection: (name: string) => Promise<boolean>;
77
}
88

9+
const alignWithSectionCaretStyle: React.CSSProperties = {
10+
borderLeftWidth: 3,
11+
borderLeftColor: "transparent",
12+
};
13+
914
export const AddSectionButton: React.FC<AddSectionButtonProps> = ({ onCreateSection }) => {
1015
const [isCreating, setIsCreating] = useState(false);
16+
const [isSubmitting, setIsSubmitting] = useState(false);
1117
const [name, setName] = useState("");
1218
const inputRef = useRef<HTMLInputElement>(null);
1319

@@ -17,34 +23,70 @@ export const AddSectionButton: React.FC<AddSectionButtonProps> = ({ onCreateSect
1723
}
1824
}, [isCreating]);
1925

20-
const handleSubmit = () => {
26+
const handleSubmit = async () => {
27+
if (isSubmitting) {
28+
return;
29+
}
30+
2131
const trimmed = name.trim();
22-
if (trimmed) {
23-
onCreateSection(trimmed);
32+
if (!trimmed) {
33+
setName("");
34+
setIsCreating(false);
35+
return;
36+
}
37+
38+
setIsSubmitting(true);
39+
try {
40+
// Keep the input open until creation succeeds so backend/IPC failures do not
41+
// look like they created a section successfully.
42+
const didCreateSection = await onCreateSection(trimmed);
43+
if (!didCreateSection) {
44+
return;
45+
}
46+
setName("");
47+
setIsCreating(false);
48+
} catch {
49+
// The caller owns error presentation; keep the current draft visible so the
50+
// user can retry instead of losing their typed section name.
51+
} finally {
52+
setIsSubmitting(false);
2453
}
25-
setName("");
26-
setIsCreating(false);
54+
};
55+
56+
const submitWithoutThrowing = () => {
57+
handleSubmit().catch(() => undefined);
2758
};
2859

2960
if (isCreating) {
3061
return (
31-
<div className="flex items-center px-2 py-0.5">
62+
<div
63+
// Match the section header's reserved 3px color rail so the add affordance's
64+
// plus icon stays horizontally aligned with the section caret.
65+
className="flex items-center gap-1 px-2 py-0.5"
66+
style={alignWithSectionCaretStyle}
67+
>
68+
<div className="flex h-5 w-5 shrink-0 items-center justify-center">
69+
<Plus size={12} className="text-muted/60" />
70+
</div>
3271
<input
3372
ref={inputRef}
3473
type="text"
3574
value={name}
3675
onChange={(e) => setName(e.target.value)}
37-
onBlur={handleSubmit}
76+
disabled={isSubmitting}
77+
onBlur={submitWithoutThrowing}
3878
onKeyDown={(e) => {
39-
if (e.key === "Enter") handleSubmit();
79+
if (e.key === "Enter") {
80+
submitWithoutThrowing();
81+
}
4082
if (e.key === "Escape") {
4183
setName("");
4284
setIsCreating(false);
4385
}
4486
}}
4587
placeholder="Section name..."
4688
data-testid="add-section-input"
47-
className="bg-background/50 text-foreground ml-6 min-w-0 flex-1 rounded border border-white/20 px-1.5 py-0.5 text-[11px] outline-none select-text"
89+
className="bg-background/50 text-foreground min-w-0 flex-1 rounded border border-white/20 px-1.5 py-0.5 text-[11px] outline-none select-text"
4890
/>
4991
</div>
5092
);
@@ -54,9 +96,14 @@ export const AddSectionButton: React.FC<AddSectionButtonProps> = ({ onCreateSect
5496
<button
5597
onClick={() => setIsCreating(true)}
5698
data-testid="add-section-button"
57-
className="text-muted/60 hover:text-muted flex w-full cursor-pointer items-center justify-center gap-1 border-none bg-transparent px-2 py-0.5 text-[11px] transition-colors"
99+
// Keep the affordance in the same icon/text columns as section rows so the
100+
// add-sub-folder action reads as part of the project hierarchy.
101+
className="text-muted/60 hover:text-muted flex w-full cursor-pointer items-center gap-1 border-none bg-transparent px-2 py-0.5 text-left text-[11px] transition-colors"
102+
style={alignWithSectionCaretStyle}
58103
>
59-
<Plus size={12} />
104+
<div className="flex h-5 w-5 shrink-0 items-center justify-center">
105+
<Plus size={12} />
106+
</div>
60107
<span>Add section</span>
61108
</button>
62109
);

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

Lines changed: 23 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ export interface AgentListItemProps extends AgentListItemBaseProps {
8686
variant?: "workspace";
8787
metadata: FrontendWorkspaceMetadata;
8888
projectName: string;
89-
subAgentConnectorLayout?: "default" | "task-group-member";
9089
isArchiving?: boolean;
9190
/** True when deletion is in-flight (optimistic UI while backend removes). */
9291
isRemoving?: boolean;
@@ -121,26 +120,18 @@ const LIST_ITEM_BASE_CLASSES =
121120
"bg-surface-primary relative flex items-start gap-1.5 rounded-l-sm py-2 pr-1.5 select-none transition-all duration-150";
122121

123122
const HIDE_INLINE_ACTIONS_ON_MOBILE_TOUCH =
124-
"[@media(max-width:768px)_and_(hover:none)_and_(pointer:coarse)]:invisible [@media(max-width:768px)_and_(hover:none)_and_(pointer:coarse)]:pointer-events-none";
123+
"[@media(max-width:768px)_and_(hover:none)_and_(pointer:coarse)]:hidden";
125124
const SHOW_INLINE_ACTIONS_ON_WIDE_TOUCH =
126125
"[@media(min-width:769px)_and_(hover:none)_and_(pointer:coarse)]:opacity-100";
126+
// Dense sidebar icon buttons should not inherit the global 44px coarse-pointer
127+
// minimum, otherwise hidden/inline actions still reserve row width and push the
128+
// visible controls off-screen.
129+
const COMPACT_SIDEBAR_ICON_BUTTON_CLASSES = "!min-h-0 !min-w-0";
127130

128131
/** Calculate left padding based on nesting depth */
129132
function getItemPaddingLeft(depth?: number): number {
130133
const safeDepth = typeof depth === "number" && Number.isFinite(depth) ? Math.max(0, depth) : 0;
131-
return 8 + Math.min(32, safeDepth) * 12;
132-
}
133-
134-
function getSubAgentConnectorLeft(
135-
indentLeft: number,
136-
layout: "default" | "task-group-member"
137-
): number {
138-
return layout === "task-group-member" ? indentLeft - 2 : indentLeft + 9;
139-
}
140-
141-
function getAncestorTrunkLeft(depth: number, layout: "default" | "task-group-member"): number {
142-
const indentLeft = getItemPaddingLeft(depth);
143-
return layout === "task-group-member" ? indentLeft + 6 : indentLeft + 8;
134+
return 12 + Math.min(32, safeDepth) * 12;
144135
}
145136

146137
type VisualState = "active" | "idle" | "seen" | "hidden" | "error" | "question";
@@ -257,7 +248,12 @@ function QuickArchiveButton(props: {
257248
<TooltipTrigger asChild>
258249
<button
259250
type="button"
260-
className="text-muted hover:text-foreground focus-visible:text-foreground pointer-events-none inline-flex h-4 w-4 cursor-pointer items-center justify-center border-none bg-transparent p-0 opacity-0 transition-[color,opacity] duration-200 group-focus-within/row:pointer-events-auto group-focus-within/row:opacity-100 group-hover/row:pointer-events-auto group-hover/row:opacity-100"
251+
className={cn(
252+
"text-muted hover:text-foreground focus-visible:text-foreground pointer-events-none inline-flex h-4 w-4 cursor-pointer items-center justify-center border-none bg-transparent p-0 opacity-0 transition-[color,opacity] duration-200 group-focus-within/row:pointer-events-auto group-focus-within/row:opacity-100 group-hover/row:pointer-events-auto group-hover/row:opacity-100",
253+
COMPACT_SIDEBAR_ICON_BUTTON_CLASSES,
254+
HIDE_INLINE_ACTIONS_ON_MOBILE_TOUCH,
255+
SHOW_INLINE_ACTIONS_ON_WIDE_TOUCH
256+
)}
261257
onKeyDown={stopKeyboardPropagation}
262258
onClick={(event) => {
263259
event.stopPropagation();
@@ -294,31 +290,20 @@ function ActionButtonWrapper(props: { children: React.ReactNode; className?: str
294290
// ─────────────────────────────────────────────────────────────────────────────
295291

296292
function DraftAgentListItemInner(props: DraftAgentListItemProps) {
297-
const { projectPath, isSelected, depth, sectionId, draft } = props;
293+
const { projectPath, isSelected, depth, draft } = props;
298294
const paddingLeft = getItemPaddingLeft(depth);
299295
const hasPromptPreview = draft.promptPreview.length > 0;
300-
const draftBorderStyle: React.CSSProperties = {
301-
backgroundImage: [
302-
"repeating-linear-gradient(to right, var(--color-border) 0 5px, transparent 5px 10px)",
303-
"repeating-linear-gradient(to right, var(--color-border) 0 5px, transparent 5px 10px)",
304-
"repeating-linear-gradient(to bottom, var(--color-border) 0 5px, transparent 5px 10px)",
305-
].join(", "),
306-
backgroundSize: "100% 1.5px, 100% 1.5px, 1.5px 100%",
307-
backgroundPosition: "left top, left bottom, left top",
308-
backgroundRepeat: "no-repeat",
309-
};
310296

311297
const ctxMenu = useContextMenuPosition({ longPress: true });
312298

313299
return (
314300
<div
315301
className={cn(
316302
LIST_ITEM_BASE_CLASSES,
317-
sectionId != null ? "ml-8" : "ml-6.5",
318-
"cursor-pointer pl-1 hover:bg-surface-secondary [&:hover_button]:opacity-100",
303+
"border-border cursor-pointer border-t border-b border-l border-dashed pl-1 hover:bg-surface-secondary [&:hover_button]:opacity-100",
319304
isSelected && "bg-surface-secondary"
320305
)}
321-
style={{ paddingLeft, ...draftBorderStyle }}
306+
style={{ paddingLeft }}
322307
onClick={() => {
323308
if (ctxMenu.suppressClickIfLongPress()) return;
324309
draft.onOpen();
@@ -374,6 +359,7 @@ function DraftAgentListItemInner(props: DraftAgentListItemProps) {
374359
type="button"
375360
className={cn(
376361
"text-muted hover:text-content-destructive inline-flex h-4 w-4 cursor-pointer items-center justify-center border-none bg-transparent p-0 opacity-0 transition-colors duration-200",
362+
COMPACT_SIDEBAR_ICON_BUTTON_CLASSES,
377363
// Keep long-press as the compact mobile affordance on narrow
378364
// touch layouts, but show the button on wider touch screens so
379365
// it never becomes an invisible tappable hotspot.
@@ -687,7 +673,6 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
687673
className={cn(
688674
LIST_ITEM_BASE_CLASSES,
689675
"group/row",
690-
sectionId != null ? "ml-7.5" : "ml-5",
691676
isDragging && "opacity-50",
692677
isRemoving && "opacity-70",
693678
// Keep hover styles enabled for initializing workspaces so the row feels interactive.
@@ -813,6 +798,7 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
813798
type="button"
814799
className={cn(
815800
"text-muted inline-flex h-4 w-4 items-center justify-center border-none bg-transparent p-0 transition-colors duration-200",
801+
COMPACT_SIDEBAR_ICON_BUTTON_CLASSES,
816802
// Keep cancel affordance hidden until row-hover while initializing,
817803
// but force it visible as a spinner once deletion starts.
818804
isRemoving
@@ -873,6 +859,7 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
873859
ref={overflowMenuButtonRef}
874860
className={cn(
875861
"text-muted hover:text-foreground inline-flex h-4 w-4 cursor-pointer items-center justify-center border-none bg-transparent p-0 transition-colors duration-200",
862+
COMPACT_SIDEBAR_ICON_BUTTON_CLASSES,
876863
ctxMenu.isOpen ? "opacity-100" : "opacity-0",
877864
HIDE_INLINE_ACTIONS_ON_MOBILE_TOUCH,
878865
SHOW_INLINE_ACTIONS_ON_WIDE_TOUCH
@@ -992,7 +979,7 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
992979
)}
993980

994981
{/* Keep title row anchored so status dot/title align across single+double-line states. */}
995-
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
982+
<div className="flex min-w-0 flex-1 flex-col gap-0.5 overflow-hidden">
996983
<div
997984
className={cn(
998985
// Keep the title column shrinkable on narrow/mobile viewports so the
@@ -1013,7 +1000,7 @@ function RegularAgentListItemInner(props: AgentListItemProps) {
10131000
data-workspace-id={workspaceId}
10141001
/>
10151002
) : (
1016-
<div className="flex min-w-0 items-center gap-1">
1003+
<div className="flex min-w-0 flex-1 items-center gap-1 overflow-hidden">
10171004
<span
10181005
className={cn(
10191006
"min-w-0 flex-1 truncate text-left text-[14px] leading-6 transition-colors duration-200",
@@ -1104,15 +1091,9 @@ function AgentListItemInner(props: UnifiedAgentListItemProps) {
11041091
// Connector geometry is driven by render metadata so visible siblings keep
11051092
// consistent single/middle/last shapes as parents expand/collapse children.
11061093
const isElbowActive = props.metadata.taskStatus === "running";
1107-
// Task-group members use a slightly different left rail so their connector
1108-
// trunk aligns with the group's leading chevron column.
1109-
const connectorLayout = props.subAgentConnectorLayout ?? "default";
1110-
const connectorLeft = getSubAgentConnectorLeft(
1111-
getItemPaddingLeft(props.depth),
1112-
connectorLayout
1113-
);
1094+
const indentLeft = getItemPaddingLeft(props.depth);
11141095
const ancestorTrunks = rowMeta.ancestorTrunks.map((trunk) => ({
1115-
left: getAncestorTrunkLeft(trunk.depth, connectorLayout),
1096+
left: getItemPaddingLeft(trunk.depth) - 4,
11161097
active: trunk.active,
11171098
}));
11181099

@@ -1123,7 +1104,7 @@ function AgentListItemInner(props: UnifiedAgentListItemProps) {
11231104
sharedTrunkActiveThroughRow={rowMeta.sharedTrunkActiveThroughRow}
11241105
sharedTrunkActiveBelowRow={rowMeta.sharedTrunkActiveBelowRow}
11251106
ancestorTrunks={ancestorTrunks}
1126-
connectorLeft={connectorLeft}
1107+
indentLeft={indentLeft}
11271108
isSelected={props.isSelected}
11281109
isElbowActive={isElbowActive}
11291110
>

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@ interface SubAgentListItemProps {
77
sharedTrunkActiveThroughRow: boolean;
88
sharedTrunkActiveBelowRow: boolean;
99
ancestorTrunks: ReadonlyArray<{ left: number; active: boolean }>;
10-
connectorLeft: number;
10+
indentLeft: number;
1111
isSelected: boolean;
1212
isElbowActive: boolean;
1313
children: React.ReactNode;
1414
}
1515

1616
export function SubAgentListItem(props: SubAgentListItemProps) {
17-
// The parent passes the absolute row-space x position for this connector trunk.
18-
const connectorLeft = props.connectorLeft;
17+
const connectorLeft = props.indentLeft - 10;
1918
const connectorFillClass = props.isSelected ? "bg-border" : "bg-border-light";
2019
const connectorBorderClass = props.isSelected ? "border-border" : "border-border-light";
2120
const connectorColor = props.isSelected ? "var(--color-border)" : "var(--color-border-light)";

0 commit comments

Comments
 (0)