Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 18 additions & 20 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
getVisibleThreadsForProject,
groupSidebarThreadsByWorktree,
hasUnseenCompletion,
formatSidebarThreadDisplayTitle,
isContextMenuPointerDown,
isTrailingDoubleClick,
orderItemsByPreferredIds,
Expand Down Expand Up @@ -1000,35 +999,46 @@ describe("resolveProjectTitleClassName", () => {
});

describe("resolveSidebarWorktreeLabelMode", () => {
it("inlines populated non-main worktrees in the native sidebar", () => {
it("shows a compact header for populated non-main worktrees in the native sidebar", () => {
expect(
resolveSidebarWorktreeLabelMode({
enableNativeMacSidebar: true,
isMainCheckout: false,
threadGroupingMode: "worktree",
threadCount: 1,
worktreeGroupCount: 1,
}),
).toBe("inline");
).toBe("header");
});

it("hides the redundant main checkout label in the native sidebar", () => {
it("hides the redundant main checkout label when it is the only group", () => {
expect(
resolveSidebarWorktreeLabelMode({
enableNativeMacSidebar: true,
isMainCheckout: true,
threadGroupingMode: "worktree",
threadCount: 0,
worktreeGroupCount: 1,
}),
).toBe("hidden");
});

it("shows the main checkout header alongside other worktree groups", () => {
expect(
resolveSidebarWorktreeLabelMode({
enableNativeMacSidebar: true,
isMainCheckout: true,
threadGroupingMode: "worktree",
worktreeGroupCount: 2,
}),
).toBe("header");
});

it("preserves upstream grouping when the native sidebar is disabled", () => {
expect(
resolveSidebarWorktreeLabelMode({
enableNativeMacSidebar: false,
isMainCheckout: true,
threadGroupingMode: "worktree",
threadCount: 1,
worktreeGroupCount: 1,
}),
).toBe("header");
});
Expand All @@ -1039,7 +1049,7 @@ describe("resolveSidebarWorktreeLabelMode", () => {
enableNativeMacSidebar: true,
isMainCheckout: false,
threadGroupingMode: "worktree",
threadCount: 0,
worktreeGroupCount: 1,
}),
).toBe("header");
});
Expand All @@ -1065,18 +1075,6 @@ describe("shouldShowSidebarEmptyThreadState", () => {
});
});

describe("formatSidebarThreadDisplayTitle", () => {
it("combines the worktree and thread names", () => {
expect(formatSidebarThreadDisplayTitle("art-15915", "Use historic conversions")).toBe(
"art-15915: Use historic conversions",
);
});

it("keeps standalone thread titles unchanged", () => {
expect(formatSidebarThreadDisplayTitle(null, "General chat")).toBe("General chat");
});
});

describe("resolveProjectStatusIndicator", () => {
it("returns null when no threads have a notable status", () => {
expect(resolveProjectStatusIndicator([null, null])).toBeNull();
Expand Down
14 changes: 3 additions & 11 deletions apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,11 +513,10 @@ export function resolveSidebarWorktreeLabelMode(input: {
enableNativeMacSidebar: boolean;
isMainCheckout: boolean;
threadGroupingMode: "separate" | "worktree";
threadCount: number;
}): "header" | "hidden" | "inline" {
worktreeGroupCount: number;
}): "header" | "hidden" {
if (!input.enableNativeMacSidebar || input.threadGroupingMode !== "worktree") return "header";
if (input.isMainCheckout) return "hidden";
return input.threadCount > 0 ? "inline" : "header";
return input.isMainCheckout && input.worktreeGroupCount === 1 ? "hidden" : "header";
}

export function shouldShowSidebarEmptyThreadState(input: {
Expand All @@ -527,13 +526,6 @@ export function shouldShowSidebarEmptyThreadState(input: {
return input.showEmptyThreadState && !input.enableNativeMacSidebar;
}

export function formatSidebarThreadDisplayTitle(
worktreeLabel: string | null,
threadTitle: string,
): string {
return worktreeLabel ? `${worktreeLabel}: ${threadTitle}` : threadTitle;
}

export function resolveThreadStatusPill(input: {
thread: ThreadStatusInput;
}): ThreadStatusPill | null {
Expand Down
69 changes: 13 additions & 56 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ import { ProjectFavicon } from "./ProjectFavicon";
import { openDiscoveredPort } from "./preview/openDiscoveredPort";
import {
getSidebarThreadIdsToPrewarm,
formatSidebarThreadDisplayTitle,
isContextMenuPointerDown,
isTrailingDoubleClick,
orderItemsByPreferredIds,
Expand Down Expand Up @@ -347,8 +346,6 @@ function buildThreadJumpLabelMap(input: {
interface SidebarThreadRowProps {
thread: SidebarThreadSummary;
nestedUnderWorktree: boolean;
worktreeLabel: string | null;
onWorktreeContextMenu?: (event: React.MouseEvent<HTMLSpanElement>) => void;
projectCwd: string | null;
orderedProjectThreadKeys: readonly string[];
isActive: boolean;
Expand Down Expand Up @@ -411,8 +408,6 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
openPrLink,
thread,
nestedUnderWorktree,
worktreeLabel,
onWorktreeContextMenu,
} = props;
const threadRef = scopeThreadRef(thread.environmentId, thread.id);
const threadKey = scopedThreadKey(threadRef);
Expand Down Expand Up @@ -512,7 +507,6 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
: !isThreadRunning
? "pointer-events-none transition-opacity duration-150 max-sm:pr-6 group-hover/menu-sub-item:opacity-0 group-focus-within/menu-sub-item:opacity-0"
: "pointer-events-none";
const threadDisplayTitle = formatSidebarThreadDisplayTitle(worktreeLabel, thread.title);
const clearConfirmingArchive = useCallback(() => {
setConfirmingArchiveThreadKey((current) => (current === threadKey ? null : current));
}, [setConfirmingArchiveThreadKey, threadKey]);
Expand Down Expand Up @@ -712,6 +706,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
<SidebarMenuSubItem
className="w-full"
data-thread-item
data-nested-under-worktree={nestedUnderWorktree}
onMouseLeave={handleMouseLeave}
onBlurCapture={handleBlurCapture}
>
Expand All @@ -731,16 +726,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
onKeyDown={handleRowKeyDown}
onContextMenu={handleRowContextMenu}
>
<div
className={`native-sidebar-thread-content flex min-w-0 flex-1 items-center gap-1.5 text-left ${nestedUnderWorktree ? "pl-3" : ""}`}
data-nested-under-worktree={nestedUnderWorktree}
>
{worktreeLabel ? (
<GitBranchIcon
aria-hidden="true"
className="native-sidebar-inline-worktree-icon size-3.5 shrink-0"
/>
) : null}
<div className="native-sidebar-thread-content flex min-w-0 flex-1 items-center gap-1.5 text-left">
{prStatus && (
<Tooltip>
<TooltipTrigger
Expand Down Expand Up @@ -778,23 +764,12 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
className="native-sidebar-thread-title min-w-0 flex-1 truncate text-[13px]"
data-testid={`thread-title-${thread.id}`}
>
{worktreeLabel ? (
<>
<span
className="native-sidebar-inline-worktree"
onContextMenu={onWorktreeContextMenu}
>
{worktreeLabel}
</span>
<span className="native-sidebar-inline-worktree-separator">: </span>
</>
) : null}
<span className="native-sidebar-thread-title-segment">{thread.title}</span>
{thread.title}
</span>
}
/>
<TooltipPopup side="top" className="max-w-80 whitespace-normal leading-tight">
{threadDisplayTitle}
{thread.title}
</TooltipPopup>
</Tooltip>
)}
Expand Down Expand Up @@ -1275,20 +1250,13 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
updateThreadMetadata,
]);

const renderThread = (
thread: SidebarThreadSummary,
nestedUnderWorktree = false,
worktreeLabel: string | null = null,
onWorktreeContextMenu?: (event: React.MouseEvent<HTMLSpanElement>) => void,
) => {
const renderThread = (thread: SidebarThreadSummary, nestedUnderWorktree = false) => {
const threadKey = scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id));
return (
<SidebarThreadRow
key={threadKey}
thread={thread}
nestedUnderWorktree={nestedUnderWorktree}
worktreeLabel={worktreeLabel}
{...(onWorktreeContextMenu ? { onWorktreeContextMenu } : {})}
projectCwd={projectCwd}
orderedProjectThreadKeys={orderedProjectThreadKeys}
isActive={activeRouteThreadKey === threadKey}
Expand Down Expand Up @@ -1339,14 +1307,16 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
{shouldShowThreadPanel && threadGroupingMode === "worktree"
? renderedThreadGroups.flatMap((group) => {
const label =
enableSidebarWorktreeNavigation || group.label !== "Main"
? group.label
: "Main checkout";
enableNativeMacSidebar && group.isMainCheckout
? "Main checkout"
: enableSidebarWorktreeNavigation || group.label !== "Main"
? group.label
: "Main checkout";
const worktreeLabelMode = resolveSidebarWorktreeLabelMode({
enableNativeMacSidebar,
isMainCheckout: group.isMainCheckout,
threadGroupingMode,
threadCount: group.threads.length,
worktreeGroupCount: renderedThreadGroups.length,
});
const labelContent = (
<>
Expand Down Expand Up @@ -1387,25 +1357,13 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
)}
</SidebarMenuSubItem>
);
const handleInlineWorktreeContextMenu =
worktreeLabelMode === "inline"
? (event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
event.stopPropagation();
handleWorktreeGroupMenu(group, {
x: event.clientX,
y: event.clientY,
});
}
: undefined;
return [
...(worktreeLabelRow ? [worktreeLabelRow] : []),
...group.threads.map((thread) =>
renderThread(
thread,
enableSidebarWorktreeNavigation && worktreeLabelMode === "header",
worktreeLabelMode === "inline" ? label : null,
handleInlineWorktreeContextMenu,
worktreeLabelMode === "header" &&
(enableNativeMacSidebar || enableSidebarWorktreeNavigation),
),
),
];
Expand Down Expand Up @@ -1781,7 +1739,6 @@ const SidebarStandaloneChatList = memo(function SidebarStandaloneChatList(
key={threadKey}
thread={thread}
nestedUnderWorktree={false}
worktreeLabel={null}
projectCwd={null}
orderedProjectThreadKeys={orderedThreadKeys}
isActive={props.activeRouteThreadKey === threadKey}
Expand Down
63 changes: 4 additions & 59 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -494,13 +494,14 @@ body {
margin-block: 2px;
}

.native-macos-sidebar .native-sidebar-thread-content[data-nested-under-worktree="true"] {
padding-left: 0;
.native-macos-sidebar [data-thread-item][data-nested-under-worktree="true"] {
width: calc(100% - 12px);
margin-inline-start: 12px;
}

.native-macos-sidebar .native-sidebar-worktree-label {
height: 30px;
margin-top: 4px;
margin-top: 6px;
padding-inline: 10px;
color: rgb(29 29 31 / 64%);
font-size: 14px;
Expand Down Expand Up @@ -574,62 +575,6 @@ body {
line-height: 20px;
}

.native-macos-sidebar .native-sidebar-inline-worktree-icon,
.native-macos-sidebar .native-sidebar-inline-worktree {
color: rgb(29 29 31 / 56%);
}

.dark .native-macos-sidebar .native-sidebar-inline-worktree-icon,
.dark .native-macos-sidebar .native-sidebar-inline-worktree {
color: rgb(255 255 255 / 44%);
}

.native-macos-sidebar .native-sidebar-inline-worktree {
font-weight: 500;
}

.native-macos-sidebar .native-sidebar-inline-worktree-separator {
color: rgb(29 29 31 / 48%);
}

.dark .native-macos-sidebar .native-sidebar-inline-worktree-separator {
color: rgb(255 255 255 / 32%);
}

.native-macos-sidebar .native-sidebar-inline-worktree ~ .native-sidebar-thread-title-segment {
color: rgb(29 29 31 / 80%);
}

.dark .native-macos-sidebar .native-sidebar-inline-worktree ~ .native-sidebar-thread-title-segment {
color: rgb(255 255 255 / 60%);
}

.native-macos-sidebar
.native-sidebar-thread-row[data-active="true"]
.native-sidebar-thread-title-segment {
color: #000000;
}

.dark
.native-macos-sidebar
.native-sidebar-thread-row[data-active="true"]
.native-sidebar-thread-title-segment {
color: #ffffff;
}

.native-macos-sidebar
.native-sidebar-thread-row[data-finished="true"]
.native-sidebar-thread-title-segment {
color: #1d1d1f;
}

.dark
.native-macos-sidebar
.native-sidebar-thread-row[data-finished="true"]
.native-sidebar-thread-title-segment {
color: #ffffff;
}

.native-macos-sidebar [data-thread-status-label] {
width: 12px;
height: 12px;
Expand Down
17 changes: 9 additions & 8 deletions docs/personal-fork-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,15 @@ Turning a flag off preserves upstream behavior.
- Light mode uses dark, regular-weight conversation text with progressively softer project and
worktree context. Worktree labels use the same 14px size as thread titles instead of appearing
disabled or undersized.
- Populated worktrees use compact, branch-prefixed thread rows instead of separate group headers.
Conversation rows use a taller click target, and standalone worktree rows include a branch icon.
Worktree labels omit redundant thread counts. Empty worktrees remain standalone rows so they are
still available for new-chat and archive actions. Turning off the native sidebar flag restores
the upstream grouped layout and counts.
- The native layout omits the generic "No threads yet" row and the redundant main-checkout label;
main-checkout threads render as plain conversation titles. Actionable worktree rows use a pointer
cursor. Turning the flag off restores those upstream labels.
- Worktrees use compact branch headers with subtly inset conversation rows, keeping each checkout
visually distinct without repeating its branch on every thread. Worktree labels omit redundant
thread counts. Empty worktrees remain standalone rows so they are still available for new-chat
and archive actions. Turning off the native sidebar flag restores the upstream grouped layout
and counts.
- The native layout omits the generic "No threads yet" row. It hides the redundant main-checkout
label when no other checkout is shown, but restores a "Main checkout" header when multiple
checkout groups need a visible boundary. Actionable worktree rows use a pointer cursor. Turning
the flag off restores upstream labels.
- With sidebar worktree navigation enabled, worktree groups remain visible after their last thread
is archived. Archiving the last thread requires confirmation and leaves the checkout and its Git
registration intact. Worktree rows do not show inline archive buttons; every non-main worktree
Expand Down
Loading