Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const clientSettings: ClientSettings = {
proactivePanelsEnabled: true,
showSkillsInSlashMenu: false,
providerModelPreferences: {},
compactSidebarThreadRows: true,
sidebarProjectGroupingMode: "repository_path",
sidebarProjectGroupingOverrides: {
"environment-1:/tmp/project-a": "separate",
Expand Down
37 changes: 36 additions & 1 deletion apps/web/src/components/Sidebar.drag.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function preview(
scale = 1,
) {
const strategy = createSidebarSortingStrategy(input);
const args = layout(input.items, active, over, scale);
const args = layout(input.items, active, over, scale, input.compactThreadRows ? 64 : 82);
return new Map(
input.items.map((item, index) => [sidebarListItemId(item), strategy({ ...args, index })]),
);
Expand Down Expand Up @@ -640,6 +640,41 @@ describe("sidebar drag projection", () => {
expect(result.get(sidebarMarkerId("active-placeholder"))?.y).toBe(62.5);
});

it.each([1, 0.75])("reserves compact cards in empty sections at scale %s", (scale) => {
const items = [
pinnedHeader,
divider,
marker("active-placeholder"),
settledHeader,
thread("s", "settled"),
];
const input = { items, settledOrder: [], settledExpanded: true, compactThreadRows: true };
const pinned = preview(input, "s", sidebarMarkerId("pinned-header"), scale);
expect(pinned.get(sidebarMarkerId("pinned-header"))).toEqual(stationary);
expect(pinned.get(sidebarMarkerId("pinned-divider"))?.y).toBe(64 * scale + 1);
const active = preview(input, "s", sidebarMarkerId("active-placeholder"), scale);
expect(active.get(sidebarMarkerId("active-placeholder"))?.scaleY).toBe(0);
expect(active.get(sidebarMarkerId("settled-header"))?.y).toBe(64 * scale);
});

it.each([1, 0.75])("shrinks a compact card when moved to Settled at scale %s", (scale) => {
const items = [
pinnedHeader,
divider,
thread("a", "active"),
settledHeader,
thread("s", "settled"),
];
const result = preview(
{ items, settledOrder: ["s", "a"], settledExpanded: true, compactThreadRows: true },
"a",
"s",
scale,
);
expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(-28 * scale);
expect(result.get("s")?.y).toBe(-28 * scale);
});

it("updates the projection when the target or measured geometry changes", () => {
const strategy = createSidebarSortingStrategy({
items: pinned,
Expand Down
10 changes: 7 additions & 3 deletions apps/web/src/components/Sidebar.drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export function createSidebarSortingStrategy(input: {
routeThreadKey?: string | null;
snoozedThreadCount?: number;
cardHeight?: number;
compactThreadRows?: boolean;
slimHeight?: number;
/** Space each pinned boundary opens for its label while dragging. The
* markers stay zero height at rest, so nothing is reserved until pickup. */
Expand Down Expand Up @@ -140,10 +141,13 @@ export function createSidebarSortingStrategy(input: {
else slimHeight ??= rects[index]?.height;
if (item.key !== active.key) groups[item.section].push(item);
}
// Cards are 4.875rem + 0.25rem padding; slim rows/placeholders are h-9.
// Include the card's 0.25rem row padding in either density.
const baseCardHeight = input.compactThreadRows ? 64 : 82;
const scale =
slimHeight !== undefined ? slimHeight / 36 : (headerScale ?? (cardHeight ?? 82) / 82);
cardHeight ??= 82 * scale;
slimHeight !== undefined
? slimHeight / 36
: (headerScale ?? (cardHeight ?? baseCardHeight) / baseCardHeight);
cardHeight ??= baseCardHeight * scale;
slimHeight ??= 36 * scale;
const labelHeight = (input.boundaryLabelHeight ?? 0) * scale;
const group = groups[target.section];
Expand Down
130 changes: 84 additions & 46 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@ function SidebarThreadTooltip({
}) {
const driverKind = providerEntry?.driverKind ?? null;
const supportsMultiplePullRequests = useSupportsMultiplePullRequests(thread.environmentId);
const isWorking = resolveSidebarThreadStatus(thread) === "working";
const workingStartedAt = isWorking ? resolveWorkingStartedAt(thread) : null;
return (
<TooltipPopup
side="right"
Expand Down Expand Up @@ -382,6 +384,14 @@ function SidebarThreadTooltip({
</div>
</div>
) : null}
{workingStartedAt !== null ? (
<div className="flex min-w-0 items-center gap-2">
<ClockIcon className="size-3 shrink-0 stroke-muted-foreground" />
<div className="min-w-0 truncate text-foreground/75">
Working for <WorkingDuration startedAt={workingStartedAt} />
</div>
</div>
) : null}
{driverKind ? (
<div className="flex min-w-0 items-center gap-2">
<ProviderInstanceIcon
Expand Down Expand Up @@ -705,6 +715,7 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: {
onDiscard: (draftId: DraftId) => void;
}) {
const { composer, draftId, onDiscard, onNavigate, session } = props;
const compact = useClientSettings((settings) => settings.compactSidebarThreadRows);
const promptPreview =
replaceComposerContextReferences(composer.prompt, (occurrence) => occurrence.label)
.trim()
Expand Down Expand Up @@ -756,7 +767,12 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: {
onClick={handleActivate}
onKeyDown={handleKeyDown}
>
<div className="relative z-10 h-[4.875rem] px-[var(--sidebar-row-content-inset)] py-[var(--sidebar-content-inset)]">
<div
className={cn(
"relative z-10 px-[var(--sidebar-row-content-inset)] py-[var(--sidebar-content-inset)]",
compact ? "flex h-[3.75rem] flex-col justify-center" : "h-[4.875rem]",
)}
>
<div className="flex h-5 min-w-0 items-center gap-1.5">
<SquarePenIcon aria-hidden className={draftPenClassName} />
{props.project ? (
Expand Down Expand Up @@ -952,6 +968,7 @@ const dropVerbBadge: Record<SidebarDropVerb, ReactNode> = {
const SidebarThreadRow = memo(function SidebarThreadRow(props: {
thread: SidebarThreadSummary;
variant: "card" | "slim";
compact: boolean;
// Slim rows are either settled (action: un-settle) or merely quiet
// (seen Ready threads — action: settle).
variantAction: "settle" | "unsettle" | "unsnooze";
Expand Down Expand Up @@ -1178,7 +1195,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {

const modelInstanceId = thread.session?.providerInstanceId ?? thread.modelSelection.instanceId;
const providerEntry = props.providerEntryByInstanceId.get(modelInstanceId) ?? null;
const driverKind = providerEntry?.driverKind ?? null;
const showInstanceBadge =
providerEntry !== null &&
shouldShowInstanceBadge(providerEntry, props.providerEntryByInstanceId.values());
Expand Down Expand Up @@ -1718,8 +1734,8 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
{...sortableRootProps}
{...(fileDropHandlers ?? {})}
className={cn(
// Matches the h-[4.875rem] content box; the py-0.5 padding is added on top.
"list-none py-0.5 [content-visibility:auto] [contain-intrinsic-size:auto_78px]",
"list-none py-0.5 [content-visibility:auto]",
props.compact ? "[contain-intrinsic-size:auto_60px]" : "[contain-intrinsic-size:auto_78px]",
sortable?.isDragging && "relative z-20",
)}
>
Expand All @@ -1740,7 +1756,12 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
/>
}
>
<div className="relative z-10 h-[4.875rem] px-[var(--sidebar-row-content-inset)] py-[var(--sidebar-content-inset)]">
<div
className={cn(
"relative z-10 px-[var(--sidebar-row-content-inset)] py-[var(--sidebar-content-inset)]",
props.compact ? "flex h-[3.75rem] flex-col justify-center" : "h-[4.875rem]",
)}
>
<div className="flex h-5 min-w-0 items-center gap-1.5">
{draftIndicator}
{props.project ? (
Expand All @@ -1749,15 +1770,17 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
{props.projectDisplayName ? (
<span
className={cn(
"min-w-0 flex-1 truncate text-secondary-label text-xs",
"min-w-0 truncate text-secondary-label text-xs",
shouldRecede ? "font-normal" : "font-medium",
)}
>
{props.projectDisplayName}
</span>
) : (
<span className="flex-1" />
)}
) : null}
{props.compact && thread.worktreePath !== null ? (
<ThreadWorktreeIndicator thread={thread} />
) : null}
<span className="flex-1" />
{pinIndicator}
{/* The visible state owns this slot's width: status at rest,
actions on hover/keyboard focus or while the popover is open. Keeping
Expand Down Expand Up @@ -1794,7 +1817,12 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
)}
>
<AlarmClockIcon aria-hidden className="size-4 shrink-0" />
<span role="status">{topStatus.label}</span>
<span
role="status"
className={props.compact ? "sr-only" : undefined}
>
{topStatus.label}
</span>
</button>
}
/>
Expand Down Expand Up @@ -1823,8 +1851,10 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
{/* The label alone is the live region: a role="status"
wrapper around the ticking duration would make
screen readers announce every second. */}
<span role="status">{topStatus.label}</span>
{status === "working" ? (
<span role="status" className={props.compact ? "sr-only" : undefined}>
{topStatus.label}
</span>
{!props.compact && status === "working" ? (
<span aria-hidden>
<WorkingDuration startedAt={resolveWorkingStartedAt(thread)} />
</span>
Expand Down Expand Up @@ -1895,40 +1925,44 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
</span>
)}
</div>
<div className="mt-1 flex min-w-0">
<div
className={cn(
"mt-1 flex min-w-0",
props.compact ? "items-center gap-1.5" : "flex-col",
)}
>
{title}
{isRegeneratingTitle ? (
<span role="status" className="sr-only">
Regenerating title
</span>
) : null}
</div>
<div className="mt-0.5 flex min-w-0 items-center gap-1.5 text-secondary-label text-xs">
{/* Always the branch. The plan step used to take this slot while
working, but it truncated to a half-sentence and dropped the
branch, so the row lost its most stable identifier. */}
{thread.branch ? (
<>
<ThreadWorktreeIndicator thread={thread} />
<span className="min-w-0 flex-1 truncate whitespace-nowrap text-muted-foreground/40">
{thread.branch}
</span>
</>
) : (
<span className="flex-1" />
)}
{terminalStatusIcon}
{prBadge}
{diff ? (
<span className="shrink-0 font-mono">
<span className="text-diff-addition-foreground">+{diff.insertions}</span>{" "}
<span className="text-diff-deletion-foreground">−{diff.deletions}</span>
</span>
) : null}
<span
aria-hidden
className="pointer-events-none ml-auto inline-flex shrink-0 items-center gap-1"
className={cn(
"inline-flex items-center text-secondary-label text-xs",
props.compact ? "ml-auto shrink-0 gap-1" : "mt-0.5 w-full min-w-0 gap-1.5",
)}
>
{!props.compact ? (
thread.branch ? (
<>
<ThreadWorktreeIndicator thread={thread} />
<span className="min-w-0 flex-1 truncate whitespace-nowrap text-muted-foreground/40">
{thread.branch}
</span>
</>
) : (
<span className="flex-1" />
)
) : null}
{terminalStatusIcon}
{prBadge}
{diff ? (
<span className="shrink-0 font-mono">
<span className="text-diff-addition-foreground">+{diff.insertions}</span>{" "}
<span className="text-diff-deletion-foreground">−{diff.deletions}</span>
</span>
) : null}
{isRemote ? (
<span className="inline-flex shrink-0 items-center text-sidebar-muted-foreground/70">
<EnvironmentMachineIcon
Expand All @@ -1938,18 +1972,18 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
/>
</span>
) : null}
{driverKind ? (
<span className="inline-flex shrink-0 items-center">
{providerEntry?.driverKind ? (
<span
aria-hidden
className="pointer-events-none ml-auto inline-flex shrink-0 items-center"
>
<ProviderInstanceIcon
driverKind={driverKind}
driverKind={providerEntry.driverKind}
displayName={
providerEntry?.displayName ??
thread.session?.providerName ??
modelInstanceId
providerEntry.displayName ?? thread.session?.providerName ?? modelInstanceId
}
accentColor={providerEntry?.accentColor}
accentColor={providerEntry.accentColor}
showBadge={showInstanceBadge}
// Glyph dims, badge stays saturated; offset matches the composer trigger.
iconClassName="size-3.5 opacity-60"
badgeClassName="right-[-0.1875rem] bottom-[-0.1875rem] h-3 min-w-3 px-0.5 text-[7px]"
/>
Expand Down Expand Up @@ -2118,6 +2152,7 @@ export default function Sidebar() {
const router = useRouter();
const { isMobile, setOpenMobile } = useSidebar();
const keybindings = useAtomValue(primaryServerKeybindingsAtom);
const compactThreadRows = useClientSettings((s) => s.compactSidebarThreadRows);
const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete);
const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive);
const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder);
Expand Down Expand Up @@ -3403,6 +3438,7 @@ export default function Sidebar() {
() =>
createSidebarSortingStrategy({
items: sidebarListItems,
compactThreadRows,
boundaryLabelHeight: SIDEBAR_DRAG_LABEL_HEIGHT,
settledOrder: draggedSettledOrder,
settledExpanded: settledShelfExpanded,
Expand All @@ -3412,6 +3448,7 @@ export default function Sidebar() {
}),
[
draggedSettledOrder,
compactThreadRows,
routeThreadKey,
settledShelfExpanded,
settledVisibleCount,
Expand Down Expand Up @@ -4595,6 +4632,7 @@ export default function Sidebar() {
key={`${threadKey}:${rowVariant}`}
thread={thread}
variant={rowVariant}
compact={compactThreadRows}
// Snoozed rows wake, settled rows un-settle, and cards settle.
variantAction={
section === "snoozed"
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,9 @@ export function useSettingsRestore(onRestored?: () => void) {
...(settings.diffColorScheme !== DEFAULT_UNIFIED_SETTINGS.diffColorScheme
? ["Diff colors"]
: []),
...(settings.compactSidebarThreadRows !== DEFAULT_UNIFIED_SETTINGS.compactSidebarThreadRows
? ["Compact thread rows"]
: []),
...(settings.panelAnimationDurationMs !== DEFAULT_UNIFIED_SETTINGS.panelAnimationDurationMs
? ["Panel animations"]
: []),
Expand Down Expand Up @@ -636,6 +639,7 @@ export function useSettingsRestore(onRestored?: () => void) {
settings.confirmThreadDelete,
settings.confirmThreadUnpin,
settings.composerCollapseOnScroll,
settings.compactSidebarThreadRows,
settings.addProjectBaseDirectory,
settings.defaultThreadEnvMode,
settings.newWorktreesStartFromOrigin,
Expand Down Expand Up @@ -752,6 +756,7 @@ export function useSettingsRestore(onRestored?: () => void) {
environmentIdentificationMode: DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode,
glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity,
panelAnimationDurationMs: DEFAULT_UNIFIED_SETTINGS.panelAnimationDurationMs,
compactSidebarThreadRows: DEFAULT_UNIFIED_SETTINGS.compactSidebarThreadRows,
sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,
sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode,
sidebarAutoSettleAfterDays: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays,
Expand Down Expand Up @@ -1318,6 +1323,19 @@ export function AppearanceSettingsPanel() {
}
/>
) : null}
<SettingsRow
{...searchableSetting("compact-thread-rows")}
description="Use shorter two-line cards for active and pinned threads."
control={
<Switch
checked={settings.compactSidebarThreadRows}
onCheckedChange={(checked) =>
updateSettings({ compactSidebarThreadRows: Boolean(checked) })
}
aria-label="Compact thread rows"
/>
}
/>
<SettingsRow
{...searchableSetting("diff-color-scheme")}
description="Choose colors for additions and deletions, including change counts."
Expand Down
Loading
Loading