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
11 changes: 9 additions & 2 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,10 @@ import {
shouldShowProviderStatusBanner,
} from "./chat/ProviderStatusBanner";
import { ThreadErrorBanner } from "./chat/ThreadErrorBanner";
import { resolveThreadPr } from "./ThreadStatusIndicators";
import {
resolveDisplayedThreadPr,
threadChangeRequestSnapshotsAtom,
} from "./ThreadStatusIndicators";
import { ComposerBannerStack, type ComposerBannerStackItem } from "./chat/ComposerBannerStack";
import {
DRAFT_HERO_TRANSITION_ANIMATION_ID,
Expand Down Expand Up @@ -1471,6 +1474,7 @@ function ChatViewContent(props: ChatViewProps) {
[activeThread],
);
const activeThreadKey = activeThreadRef ? scopedThreadKey(activeThreadRef) : null;
const changeRequestSnapshotByKey = useAtomValue(threadChangeRequestSnapshotsAtom);
const [timelineAnchor, setTimelineAnchor] = useState<{
readonly threadKey: string | null;
readonly messageId: MessageId | null;
Expand Down Expand Up @@ -3871,9 +3875,11 @@ function ChatViewContent(props: ChatViewProps) {
// so the banner and the sidebar row never disagree.
const activeThreadShell = useThreadShell(isServerThread ? activeThreadRef : null);
const autoSettleAfterDays = useClientSettings((settings) => settings.sidebarAutoSettleAfterDays);
const activeThreadPr = resolveThreadPr({
const activeThreadPr = resolveDisplayedThreadPr({
threadBranch: activeThread?.branch ?? null,
gitStatus: gitStatusQuery.data ?? null,
snapshot: activeThreadKey ? changeRequestSnapshotByKey.get(activeThreadKey) : undefined,
retainTerminalOnBranchMismatch: activeThread?.worktreePath === null,
});
const supportsSettlement = serverConfig?.environment.capabilities.threadSettlement === true;
const supportsSnooze = serverConfig?.environment.capabilities.threadSnooze === true;
Expand Down Expand Up @@ -3905,6 +3911,7 @@ function ChatViewContent(props: ChatViewProps) {
activeThreadPr?.state,
activeThreadShell,
autoSettleAfterDays,
changeRequestSnapshotByKey,
nowMinute,
supportsSettlement,
]);
Expand Down
90 changes: 57 additions & 33 deletions apps/web/src/components/SidebarV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,13 @@ import {
} from "./Sidebar.logic";
import { resolveLocalCheckoutBranchMismatch } from "./BranchToolbar.logic";
import {
nextThreadChangeRequestSnapshot,
prStatusIndicator,
resolveThreadPr,
resolveDisplayedThreadPr,
resolveDisplayedThreadPrProvider,
setThreadChangeRequestSnapshot,
threadChangeRequestSnapshotsAtom,
type ThreadChangeRequestSnapshot,
settledPrHoverColorClass,
} from "./ThreadStatusIndicators";
import {
Expand Down Expand Up @@ -391,11 +396,16 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
onUnsettle: (threadRef: ScopedThreadRef) => void;
onSnooze: (threadRef: ScopedThreadRef, preset: SnoozePreset) => void;
onUnsnooze: (threadRef: ScopedThreadRef) => void;
onChangeRequestState: (threadKey: string, state: "open" | "closed" | "merged" | null) => void;
changeRequestSnapshot: ThreadChangeRequestSnapshot | null;
onChangeRequestSnapshot: (
threadKey: string,
snapshot: ThreadChangeRequestSnapshot | null,
) => void;
}) {
const {
isRenaming,
onChangeRequestState,
changeRequestSnapshot,
onChangeRequestSnapshot,
onCancelRename,
onCommitRename,
onContextMenu,
Expand Down Expand Up @@ -502,18 +512,40 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
activeThreadBranch: thread.branch,
currentGitBranch: gitStatus.data?.refName ?? null,
});
const pr = resolveThreadPr({
const retainTerminalOnBranchMismatch = thread.worktreePath === null;
const pr = resolveDisplayedThreadPr({
threadBranch: thread.branch,
gitStatus: gitStatus.data,
snapshot: changeRequestSnapshot,
retainTerminalOnBranchMismatch,
});
const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider);
const prProvider = resolveDisplayedThreadPrProvider({
threadBranch: thread.branch,
gitStatus: gitStatus.data,
snapshot: changeRequestSnapshot,
retainTerminalOnBranchMismatch,
});
const prStatus = prStatusIndicator(pr, prProvider);
const settledPrHoverClass = pr ? settledPrHoverColorClass(pr.state) : undefined;
// Report the PR state up: the parent partitions rows with effectiveSettled,
// and a merged/closed PR auto-settles a thread — data only rows have.
const prState = pr?.state ?? null;
// Authoritative VCS updates only: a checkout on another branch must not
// clear a previously verified terminal PR for this thread.
useEffect(() => {
onChangeRequestState(threadKey, prState);
}, [onChangeRequestState, prState, threadKey]);
const nextSnapshot = nextThreadChangeRequestSnapshot({
threadBranch: thread.branch,
gitStatus: gitStatus.data,
snapshot: changeRequestSnapshot,
retainTerminalOnBranchMismatch,
});
if (nextSnapshot === undefined) return;
onChangeRequestSnapshot(threadKey, nextSnapshot);
}, [
gitStatus.data,
changeRequestSnapshot,
onChangeRequestSnapshot,
retainTerminalOnBranchMismatch,
thread.branch,
threadKey,
]);

const modelInstanceId = thread.session?.providerInstanceId ?? thread.modelSelection.instanceId;
const providerEntry = props.providerEntryByInstanceId.get(modelInstanceId) ?? null;
Expand Down Expand Up @@ -1157,26 +1189,10 @@ export default function SidebarV2() {
// fresh clock whenever it recomputes.
const [snoozeWakeTick, bumpSnoozeWakeTick] = useState(0);

// PR states stream in per-row (rows own the VCS subscriptions); a merged or
// closed PR auto-settles its thread on the next partition.
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;
});
},
[],
);
// Full PR snapshots stream in per-row (rows own the VCS subscriptions). A
// merged/closed PR auto-settles its thread; the snapshot retains badge
// metadata when the shared checkout later switches away from the thread branch.
const changeRequestSnapshotByKey = useAtomValue(threadChangeRequestSnapshotsAtom);

// Project scope: one menu above the list. Scoping filters the list without
// making the header width depend on the number or length of project names.
Expand Down Expand Up @@ -1394,7 +1410,14 @@ export default function SidebarV2() {
const supportsSnooze =
serverConfigs.get(thread.environmentId)?.environment.capabilities.threadSnooze === true;
const threadKey = scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id));
const changeRequestState = changeRequestStateByKey.get(threadKey) ?? null;
const snapshot = changeRequestSnapshotByKey.get(threadKey);
// Same resolved terminal PR the row displays: local thread branch
// metadata follows the shared checkout, while worktree snapshots remain
// scoped to the worktree's branch.
const changeRequestState =
snapshot != null && (thread.worktreePath === null || snapshot.branch === thread.branch)
? snapshot.pr.state
: null;
Comment thread
cursor[bot] marked this conversation as resolved.
// Snooze outranks settled classification: an explicitly snoozed thread
// belongs to the shelf even if it would also auto-settle (the shelf's
// wake time is a stronger statement about when it matters again).
Expand Down Expand Up @@ -1422,7 +1445,7 @@ export default function SidebarV2() {
};
}, [
autoSettleAfterDays,
changeRequestStateByKey,
changeRequestSnapshotByKey,
nowMinute,
scopedProjectKeys,
serverConfigs,
Expand Down Expand Up @@ -2460,7 +2483,8 @@ export default function SidebarV2() {
onUnsettle={attemptUnsettle}
onSnooze={attemptSnooze}
onUnsnooze={attemptUnsnooze}
onChangeRequestState={handleChangeRequestState}
changeRequestSnapshot={changeRequestSnapshotByKey.get(threadKey) ?? null}
onChangeRequestSnapshot={setThreadChangeRequestSnapshot}
/>
);
};
Expand Down
Loading
Loading