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
45 changes: 45 additions & 0 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,51 @@ describe("isThreadSettledForDisplay", () => {
}),
).toBe(true);
});

it("auto-settles on merged/closed PR when the server supports settlement", () => {
const serverConfigs = {
get(_environmentId: string) {
return {
environment: {
capabilities: { threadSettlement: true },
},
};
},
};
// Activity well past the queued-turn grace window so PR auto-settle is
// not blocked by a just-sent message without a turn.
const activeThread = {
...baseThread,
settledOverride: null,
settledAt: null,
latestUserMessageAt: "2026-04-01T00:00:00.000Z",
};

expect(
isThreadSettledForDisplay(activeThread, {
serverConfigs,
now,
autoSettleAfterDays: null,
changeRequestState: "merged",
}),
).toBe(true);
expect(
isThreadSettledForDisplay(activeThread, {
serverConfigs,
now,
autoSettleAfterDays: null,
changeRequestState: "closed",
}),
).toBe(true);
expect(
isThreadSettledForDisplay(activeThread, {
serverConfigs,
now,
autoSettleAfterDays: null,
changeRequestState: null,
}),
).toBe(false);
});
});

describe("groupSettledThreadsByRecencyForSidebarV2", () => {
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,8 @@ export function hasUnseenCompletion(thread: ThreadStatusInput): boolean {
}

/**
* Shared settled classification for display surfaces (sidebar v2, board), so
* they always agree on what is settled. Threads on servers without the
* Shared settled classification for display surfaces (sidebar v1/v2, board),
* so they always agree on what is settled. Threads on servers without the
* settlement capability (old server, or descriptor not loaded yet) never
* classify as settled: the user could neither un-settle nor pin them, so
* auto-settling them would strand rows in a tail with no working affordances.
Expand Down
64 changes: 57 additions & 7 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { hasComposerDraftMessage, useComposerDraftStore } from "../composerDraft
import { ProjectFavicon, ProjectFaviconFallback } from "./ProjectFavicon";
import { useAtomValue } from "@effect/atom-react";
import { autoAnimate } from "@formkit/auto-animate";
import React, { useCallback, useEffect, memo, useMemo, useRef, useState } from "react";
import React, { useCallback, useContext, useEffect, memo, useMemo, useRef, useState } from "react";
import { useShallow } from "zustand/react/shallow";
import {
DndContext,
Expand Down Expand Up @@ -295,6 +295,20 @@ import {
type SidebarProjectSnapshot,
} from "../sidebarProjectGrouping";

/**
* Active sidebar rows report resolved PR state upward so hide-settled /
* settled-shelf classification can auto-settle merged/closed PRs the same way
* Sidebar V2 and the board do. Settled history rows skip reporting.
*/
type SidebarChangeRequestStateReporter = (
threadKey: string,
state: "open" | "closed" | "merged" | null,
) => void;
const noopSidebarChangeRequestStateReporter: SidebarChangeRequestStateReporter = () => {};
const SidebarChangeRequestStateContext = React.createContext<SidebarChangeRequestStateReporter>(
noopSidebarChangeRequestStateReporter,
);

/**
* Reveal provider details while Command/Control is held when the compact
* sidebar setting normally hides them.
Expand Down Expand Up @@ -576,6 +590,13 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
gitStatus: gitStatus.data ?? null,
});
const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider);
// Lift PR state so parent hide-settled / shelf classification can auto-settle
// merged/closed PRs (matches Sidebar V2 row reporting).
const onChangeRequestState = useContext(SidebarChangeRequestStateContext);
const prState = pr?.state ?? null;
useEffect(() => {
onChangeRequestState(threadKey, prState);
}, [onChangeRequestState, prState, threadKey]);
const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds);
const isConfirmingArchive = confirmingArchiveThreadKey === threadKey && !isThreadRunning;
const threadMetaClassName = isConfirmingArchive
Expand Down Expand Up @@ -3065,7 +3086,7 @@ const SidebarRecentThreadRow = memo(function SidebarRecentThreadRow(props: {
environment !== null && isDesktopLocalConnectionTarget(environment.entry.target);
const gitCwd = thread.worktreePath ?? project.workspaceRoot;
// Settled shelf rows match Sidebar V2 history: no list VCS subscription
// (PR auto-settle is out of scope here; badges aren't live on history).
// (PR auto-settle already applied or isn't needed; badges aren't live on history).
const gitStatus = useEnvironmentQuery(
!props.isSettled && (thread.branch != null || thread.worktreePath !== null) && gitCwd !== null
? vcsEnvironment.listStatus({
Expand All @@ -3079,6 +3100,13 @@ const SidebarRecentThreadRow = memo(function SidebarRecentThreadRow(props: {
gitStatus: gitStatus.data ?? null,
});
const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider);
// Report PR state for partition; settled history keeps last reported value.
const onChangeRequestState = useContext(SidebarChangeRequestStateContext);
const prState = pr?.state ?? null;
useEffect(() => {
if (props.isSettled) return;
onChangeRequestState(threadKey, prState);
}, [onChangeRequestState, prState, props.isSettled, threadKey]);
const threadModelPresentation = useMemo(
() =>
resolveThreadModelPresentation(
Expand Down Expand Up @@ -5261,23 +5289,45 @@ export default function Sidebar() {
visibleThreads,
]);
const isManualProjectSorting = sidebarProjectSortOrder === "manual";
// PR states stream in per-row (rows own the VCS subscriptions); a merged or
// closed PR auto-settles its thread on the next classification pass — same
// path Sidebar V2 and the board use so hide-settled matches across surfaces.
const [changeRequestStateByKey, setChangeRequestStateByKey] = useState<
ReadonlyMap<string, "open" | "closed" | "merged">
>(() => new Map());
const handleChangeRequestState = useCallback(
(threadKey: string, state: "open" | "closed" | "merged" | null) => {
setChangeRequestStateByKey((current) => {
if ((current.get(threadKey) ?? null) === state) return current;
const next = new Map(current);
if (state === null) {
next.delete(threadKey);
} else {
next.set(threadKey, state);
}
return next;
});
},
[],
);
const settledThreadKeys = useMemo(() => {
const now = `${nowMinute}:00.000Z`;
const keys = new Set<string>();
for (const thread of visibleThreads) {
const threadKey = scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id));
if (
isThreadSettledForDisplay(thread, {
serverConfigs,
now,
autoSettleAfterDays,
changeRequestState: null,
changeRequestState: changeRequestStateByKey.get(threadKey) ?? null,
})
) {
keys.add(scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)));
keys.add(threadKey);
}
}
return keys;
}, [autoSettleAfterDays, nowMinute, serverConfigs, visibleThreads]);
}, [autoSettleAfterDays, changeRequestStateByKey, nowMinute, serverConfigs, visibleThreads]);
const selectedProjectFilterKey =
storedProjectFilter !== null &&
sortedProjects.some((project) => project.projectKey === storedProjectFilter)
Expand Down Expand Up @@ -5672,7 +5722,7 @@ export default function Sidebar() {
}, []);

return (
<>
<SidebarChangeRequestStateContext.Provider value={handleChangeRequestState}>
{prewarmedSidebarThreadRefs.map((threadRef) => (
<SidebarThreadDetailPrewarmer key={scopedThreadKey(threadRef)} threadRef={threadRef} />
))}
Expand Down Expand Up @@ -5739,6 +5789,6 @@ export default function Sidebar() {
<SidebarChromeFooter />
</>
)}
</>
</SidebarChangeRequestStateContext.Provider>
);
}
5 changes: 5 additions & 0 deletions apps/web/src/forkSurfaceExistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ describe("fork surface existence (anti stack-drop)", () => {
expect(sidebar).toContain("recent-thread-settled-");
expect(sidebar).toContain("Un-settle thread");
expect(sidebar).toContain("!props.isSettled");
// Hide-settled must use row-lifted PR state (merged/closed auto-settle),
// same as Sidebar V2 — never hard-code changeRequestState: null here.
expect(sidebar).toContain("changeRequestStateByKey");
expect(sidebar).toContain("SidebarChangeRequestStateContext");
expect(sidebar).not.toContain("changeRequestState: null");
});

it("Sidebar V2 keeps Settled shelf labeling and new-thread affordance", () => {
Expand Down
Loading