+
+ Projects
+
+
+ No projects yet
+
)}
diff --git a/apps/web/src/components/Sidebar.drag.test.ts b/apps/web/src/components/Sidebar.drag.test.ts
index 1058dfcd88..c757eca87a 100644
--- a/apps/web/src/components/Sidebar.drag.test.ts
+++ b/apps/web/src/components/Sidebar.drag.test.ts
@@ -253,6 +253,37 @@ describe("sidebar collision detection", () => {
});
describe("sidebar drag projection", () => {
+ it.each([
+ ["a1", "a2"],
+ ["a1", sidebarMarkerId("settled-header")],
+ ["z", "a2"],
+ ])("keeps sparse shelves at the bottom when dragging %s over %s", (active, over) => {
+ const items = [
+ pinnedHeader,
+ divider,
+ thread("a1", "active"),
+ thread("a2", "active"),
+ marker("snoozed-header"),
+ thread("z", "snoozed"),
+ settledHeader,
+ thread("s", "settled"),
+ ];
+ const strategy = createSidebarSortingStrategy({
+ items,
+ settledOrder: over === sidebarMarkerId("settled-header") ? ["a1", "s"] : ["s"],
+ settledExpanded: true,
+ boundaryLabelHeight: 24,
+ snoozedThreadCount: 1,
+ });
+ const args = layout(items, active, over);
+ for (const rect of args.rects.slice(4)) {
+ rect.top += 400;
+ rect.bottom += 400;
+ }
+ const lastIndex = items.length - 1;
+ expect(strategy({ ...args, index: lastIndex })).toEqual(stationary);
+ });
+
const pinned = [
pinnedHeader,
thread("p1", "pinned"),
diff --git a/apps/web/src/components/Sidebar.drag.ts b/apps/web/src/components/Sidebar.drag.ts
index 0aef9dec14..bbba82b516 100644
--- a/apps/web/src/components/Sidebar.drag.ts
+++ b/apps/web/src/components/Sidebar.drag.ts
@@ -103,6 +103,7 @@ export function createSidebarSortingStrategy(input: {
snoozedThreadCount?: number;
cardHeight?: number;
slimHeight?: number;
+ compact?: boolean;
/** Space each pinned boundary opens for its label while dragging. The
* markers stay zero height at rest, so nothing is reserved until pickup. */
boundaryLabelHeight?: number;
@@ -140,11 +141,14 @@ 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.
- const scale =
- slimHeight !== undefined ? slimHeight / 36 : (headerScale ?? (cardHeight ?? 82) / 82);
- cardHeight ??= 82 * scale;
- slimHeight ??= 36 * scale;
+ // Compact icons use h-7; expanded cards include their vertical padding.
+ const scale = input.compact
+ ? (cardHeight ?? slimHeight ?? 28) / 28
+ : slimHeight !== undefined
+ ? slimHeight / 36
+ : (headerScale ?? (cardHeight ?? 82) / 82);
+ cardHeight ??= (input.compact ? 28 : 82) * scale;
+ slimHeight ??= (input.compact ? 28 : 36) * scale;
const labelHeight = (input.boundaryLabelHeight ?? 0) * scale;
const group = groups[target.section];
const order =
@@ -188,29 +192,62 @@ export function createSidebarSortingStrategy(input: {
marker("snoozed-header");
projected.push(...groups.snoozed);
}
- marker("settled-header");
- section("settled");
- const result = items.map(() => hidden);
- let top = rects[0].top;
- for (const item of projected) {
+ if (!input.compact) {
+ marker("settled-header");
+ section("settled");
+ }
+ const heights = projected.map((item) => {
const index = indices.get(sidebarListItemId(item));
const rect = index === undefined ? undefined : rects[index];
- if (index !== undefined && rect) result[index] = { ...stationary, y: top - rect.top };
const fallback =
item.kind === "thread" && (item.section === "pinned" || item.section === "active")
? cardHeight
: slimHeight;
const moved = item.kind === "thread" && item.key === active.key;
- const height =
- item.kind === "marker" &&
+ return item.kind === "marker" &&
(item.marker === "pinned-header" || item.marker === "pinned-divider")
- ? labelHeight
- : item.kind === "marker" && item.marker.endsWith("placeholder")
- ? slimHeight
- : moved
- ? fallback
- : (rect?.height ?? fallback);
- top += height + 1;
+ ? labelHeight
+ : item.kind === "marker" && item.marker.endsWith("placeholder")
+ ? slimHeight
+ : moved
+ ? fallback
+ : (rect?.height ?? fallback);
+ });
+ const firstShelf = items.findIndex(
+ (item) =>
+ item.kind === "marker" &&
+ (item.marker === "snoozed-header" || item.marker === "settled-header"),
+ );
+ const shelfRect = rects[firstShelf];
+ const beforeShelf = rects[firstShelf - 1];
+ const lastRect = rects.at(-1);
+ // Consume the shelf's auto margin as drag labels and resized rows need
+ // room, keeping the combined shelves at their measured bottom.
+ let shelfSpace =
+ !input.compact &&
+ shelfRect &&
+ beforeShelf &&
+ lastRect &&
+ shelfRect.top > beforeShelf.bottom + 1
+ ? Math.max(
+ 0,
+ lastRect.bottom - rects[0].top - heights.reduce((sum, height) => sum + height + 1, -1),
+ )
+ : 0;
+ const result = items.map(() => hidden);
+ let top = rects[0].top;
+ for (const [projectedIndex, item] of projected.entries()) {
+ if (
+ item.kind === "marker" &&
+ (item.marker === "snoozed-header" || item.marker === "settled-header")
+ ) {
+ top += shelfSpace;
+ shelfSpace = 0;
+ }
+ const index = indices.get(sidebarListItemId(item));
+ const rect = index === undefined ? undefined : rects[index];
+ if (index !== undefined && rect) result[index] = { ...stationary, y: top - rect.top };
+ top += heights[projectedIndex]! + 1;
}
result[activeIndex] = stationary;
return result;
diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx
index 917bb0b7fe..386f8bb88a 100644
--- a/apps/web/src/components/Sidebar.tsx
+++ b/apps/web/src/components/Sidebar.tsx
@@ -1,10 +1,12 @@
import { useSupportsMultiplePullRequests } from "~/hooks/useSupportsMultiplePullRequests";
+import { useCompactSidebarEnabled } from "../hooks/useSettings";
import { resolveThreadCurrentPullRequestLink } from "@t3tools/shared/threadPullRequests";
import { useAtomValue } from "@effect/atom-react";
import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences";
import * as Schema from "effect/Schema";
import {
DndContext,
+ DragOverlay,
useSensor,
useSensors,
type DragEndEvent,
@@ -76,6 +78,7 @@ import {
type MouseEvent as ReactMouseEvent,
type ReactNode,
} from "react";
+import { createPortal } from "react-dom";
import { useParams, useRouter } from "@tanstack/react-router";
import { useRightPanelStore } from "../rightPanelStore";
@@ -122,6 +125,7 @@ import { startNewThreadFromContext } from "../lib/chatThreadActions";
import { useClientSettings } from "../hooks/useSettings";
import { useCopyToClipboard } from "../hooks/useCopyToClipboard";
import { useLocalStorage } from "../hooks/useLocalStorage";
+import { SidebarCompletedTime } from "./sidebar/SidebarCompletedTime";
import { useNowMinute } from "../hooks/useNowMinute";
import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments";
import {
@@ -335,6 +339,7 @@ function SidebarThreadTooltip({
branchMismatch,
terminalStatus,
terminalProcessCount,
+ compactStatus,
}: {
thread: SidebarThreadSummary;
project: ProjectFaviconProject | null;
@@ -351,6 +356,7 @@ function SidebarThreadTooltip({
} | null;
terminalStatus: TerminalStatusIndicator | null;
terminalProcessCount: number;
+ compactStatus?: string | undefined;
}) {
const driverKind = providerEntry?.driverKind ?? null;
const supportsMultiplePullRequests = useSupportsMultiplePullRequests(thread.environmentId);
@@ -367,6 +373,7 @@ function SidebarThreadTooltip({
{thread.title}