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
27 changes: 27 additions & 0 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
formatWorkingDurationLabel,
shouldNavigateAfterProjectRemoval,
shouldClearThreadSelectionOnMouseDown,
sidebarRowNeedsAttention,
sortLogicalProjectsForSidebar,
sortSettledThreadsForSidebar,
pinOrderKeyBetween,
Expand Down Expand Up @@ -318,6 +319,32 @@ describe("hasUnseenCompletion", () => {
});
});

describe("sidebarRowNeedsAttention", () => {
const quietRow = {
isUnread: false,
isWoke: false,
isActive: false,
isSelected: false,
};

it("lets a read row be dimmed", () => {
expect(sidebarRowNeedsAttention(quietRow)).toBe(false);
});

it("keeps an unread row at full contrast", () => {
expect(sidebarRowNeedsAttention({ ...quietRow, isUnread: true })).toBe(true);
});

it("keeps a woken row at full contrast", () => {
expect(sidebarRowNeedsAttention({ ...quietRow, isWoke: true })).toBe(true);
});

it("exempts the active and selected rows", () => {
expect(sidebarRowNeedsAttention({ ...quietRow, isActive: true })).toBe(true);
expect(sidebarRowNeedsAttention({ ...quietRow, isSelected: true })).toBe(true);
});
});

describe("createThreadJumpHintVisibilityController", () => {
beforeEach(() => {
vi.useFakeTimers();
Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,21 @@ export function hasUnseenCompletion(thread: ThreadStatusInput): boolean {
return completedAt > lastVisitedAt;
}

/**
* Whether a sidebar row is exempt from every dimming treatment: it needs a
* human (unread completion, fresh wake) or the user is already on it. Both the
* receded surface and the in-flight fade read this, so the two can never
* disagree about which rows stay at full contrast.
*/
export function sidebarRowNeedsAttention(input: {
isUnread: boolean;
isWoke: boolean;
isActive: boolean;
isSelected: boolean;
}): boolean {
return input.isUnread || input.isWoke || input.isActive || input.isSelected;
}

export function shouldClearThreadSelectionOnMouseDown(target: HTMLElement | null): boolean {
if (target === null) return true;
return !target.closest(THREAD_SELECTION_SAFE_SELECTOR);
Expand Down
15 changes: 9 additions & 6 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ import {
resolveSidebarThreadStatus,
searchSidebarThreadsByTitle,
shouldCreateNewThreadInCurrentProject,
sidebarRowNeedsAttention,
resolveWorkingStartedAt,
sortLogicalProjectsForSidebar,
sortPinnedThreadsForSidebar,
Expand Down Expand Up @@ -833,8 +834,13 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
// stands out.
const isInFlight =
status === "working" || status === "monitoring" || status === "approval" || status === "input";
const shouldRecede =
(status === "ready" || isInFlight) && !isUnread && !isWoke && !props.isActive && !isSelected;
const needsAttention = sidebarRowNeedsAttention({
isUnread,
isWoke,
isActive: props.isActive,
isSelected,
});
const shouldRecede = (status === "ready" || isInFlight) && !needsAttention;
// Status hues follow the system-wide convention set by sidebar v1 and the
// mobile Live Activity/widgets (amber approval, indigo input, sky working)
// so a thread reads the same color everywhere it surfaces.
Expand Down Expand Up @@ -1105,10 +1111,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
: shouldRecede
? "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
: "bg-transparent text-sidebar-foreground hover:bg-sidebar-row-hover",
isInFlight &&
!props.isActive &&
!isSelected &&
"opacity-70 transition-opacity hover:opacity-100",
isInFlight && !needsAttention && "opacity-70 transition-opacity hover:opacity-100",
);

const title = isRenaming ? (
Expand Down
Loading