Skip to content

Commit bbe2eb3

Browse files
sheehanmunimclaude
andcommitted
fix(web): show linked and retained PR status in sidebar rows
The sidebar rows still resolved a thread's PR from live git status alone, which required the thread's branch to match the current checkout. Three gaps followed: threads with a linked PR never showed a badge in the sidebar, switching the local checkout dropped every non-worktree thread's badge, and merged/closed states were lost as soon as the row remounted. The chat header and command palette already used resolveDisplayedThreadPr, so the header could say "merged" while the row showed nothing. Ports upstream's row wiring (pingdotgg#4755, pingdotgg#8160): rows read the linked PR through useLinkedThreadPullRequest, resolve the displayed PR and its provider from link → parent-held snapshot → git status, and publish snapshot updates into threadChangeRequestSnapshotsAtom, which the parent hands back to each row. Guard script pins both call sites. Web typecheck clean; sidebar/status-indicator suites 419 passing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent f6f4876 commit bbe2eb3

2 files changed

Lines changed: 68 additions & 3 deletions

File tree

‎apps/web/src/components/Sidebar.tsx‎

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,17 @@ import {
166166
import { resolveLocalCheckoutBranchMismatch } from "./BranchToolbar.logic";
167167
import {
168168
ThreadWorktreeIndicator,
169+
nextThreadChangeRequestSnapshot,
169170
prStatusIndicator,
170-
resolveThreadPr,
171+
resolveDisplayedThreadPr,
172+
resolveDisplayedThreadPrProvider,
173+
setThreadChangeRequestSnapshot,
171174
settledPrHoverColorClass,
172175
terminalStatusFromRunningIds,
176+
threadChangeRequestSnapshotsAtom,
177+
useLinkedThreadPullRequest,
173178
type TerminalStatusIndicator,
179+
type ThreadChangeRequestSnapshot,
174180
} from "./ThreadStatusIndicators";
175181
import {
176182
resolveSnoozePresets,
@@ -831,10 +837,19 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
831837
onUnsnooze: (threadRef: ScopedThreadRef) => void;
832838
onUnpin: (threadRef: ScopedThreadRef) => void;
833839
onAcknowledgeWoke: (threadRef: ScopedThreadRef, visitedAt: string) => void;
840+
// Parent-held PR snapshot: rows remount when settlement partitions move
841+
// them, so a merged/closed badge would vanish if the row kept it itself.
842+
changeRequestSnapshot: ThreadChangeRequestSnapshot | null;
843+
onChangeRequestSnapshot: (
844+
threadKey: string,
845+
snapshot: ThreadChangeRequestSnapshot | null,
846+
) => void;
834847
isOverlay?: boolean | undefined;
835848
}) {
836849
const {
837850
isRenaming,
851+
changeRequestSnapshot,
852+
onChangeRequestSnapshot,
838853
onCancelRename,
839854
onCommitRename,
840855
onContextMenu,
@@ -872,6 +887,12 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
872887
const terminalProcessCount = runningTerminalIds.length;
873888

874889
const gitCwd = thread.worktreePath ?? props.projectCwd;
890+
// A linked PR is read from the host directly, so the badge follows the
891+
// link even when the local checkout has moved to another branch.
892+
const linkedPullRequestStatus = useLinkedThreadPullRequest(
893+
leaseLiveStatus ? thread.environmentId : null,
894+
leaseLiveStatus ? thread.linkedPullRequest : null,
895+
);
875896
const gitStatus = useEnvironmentQuery(
876897
leaseLiveStatus && (thread.branch != null || thread.worktreePath !== null) && gitCwd !== null
877898
? vcsEnvironment.status({
@@ -884,9 +905,14 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
884905
JSON.stringify([thread.environmentId, gitCwd]),
885906
gitStatus.data,
886907
);
887-
const pr = resolveThreadPr({
908+
const retainTerminalOnBranchMismatch = thread.worktreePath === null;
909+
const pr = resolveDisplayedThreadPr({
888910
threadBranch: thread.branch,
889911
gitStatus: visibleGitStatus,
912+
snapshot: changeRequestSnapshot,
913+
retainTerminalOnBranchMismatch,
914+
linkedPullRequest: thread.linkedPullRequest,
915+
linkedPullRequestStatus,
890916
});
891917

892918
// Same semantics as the legacy sidebar (never-visited counts as read):
@@ -979,8 +1005,37 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
9791005
activeThreadBranch: thread.branch,
9801006
currentGitBranch: visibleGitStatus?.refName ?? null,
9811007
});
982-
const prStatus = prStatusIndicator(pr, visibleGitStatus?.sourceControlProvider);
1008+
const prProvider = resolveDisplayedThreadPrProvider({
1009+
threadBranch: thread.branch,
1010+
gitStatus: visibleGitStatus,
1011+
snapshot: changeRequestSnapshot,
1012+
retainTerminalOnBranchMismatch,
1013+
linkedPullRequest: thread.linkedPullRequest,
1014+
linkedPullRequestStatus,
1015+
});
1016+
const prStatus = prStatusIndicator(pr, prProvider);
9831017
const settledPrHoverClass = pr ? settledPrHoverColorClass(pr.state) : undefined;
1018+
useEffect(() => {
1019+
const nextSnapshot = nextThreadChangeRequestSnapshot({
1020+
threadBranch: thread.branch,
1021+
gitStatus: visibleGitStatus,
1022+
snapshot: changeRequestSnapshot,
1023+
retainTerminalOnBranchMismatch,
1024+
linkedPullRequest: thread.linkedPullRequest,
1025+
linkedPullRequestStatus,
1026+
});
1027+
if (nextSnapshot === undefined) return;
1028+
onChangeRequestSnapshot(threadKey, nextSnapshot);
1029+
}, [
1030+
changeRequestSnapshot,
1031+
visibleGitStatus,
1032+
linkedPullRequestStatus,
1033+
onChangeRequestSnapshot,
1034+
retainTerminalOnBranchMismatch,
1035+
thread.branch,
1036+
thread.linkedPullRequest,
1037+
threadKey,
1038+
]);
9841039

9851040
const modelInstanceId = thread.session?.providerInstanceId ?? thread.modelSelection.instanceId;
9861041
const providerEntry = props.providerEntryByInstanceId.get(modelInstanceId) ?? null;
@@ -2014,6 +2069,8 @@ export default function Sidebar() {
20142069
// fresh clock whenever it recomputes.
20152070
const [snoozeWakeTick, bumpSnoozeWakeTick] = useState(0);
20162071

2072+
const changeRequestSnapshotByKey = useAtomValue(threadChangeRequestSnapshotsAtom);
2073+
20172074
// Project scope: one menu above the list. Scoping filters the list without
20182075
// making the header width depend on the number or length of project names.
20192076
const [projectScopeKey, setProjectScopeKey] = useState<string | null>(null);
@@ -4045,6 +4102,8 @@ export default function Sidebar() {
40454102
onUnsnooze={attemptUnsnooze}
40464103
onUnpin={attemptUnpin}
40474104
onAcknowledgeWoke={acknowledgeWoke}
4105+
changeRequestSnapshot={changeRequestSnapshotByKey.get(threadKey) ?? null}
4106+
onChangeRequestSnapshot={setThreadChangeRequestSnapshot}
40484107
isOverlay={isOverlay}
40494108
/>
40504109
);

‎scripts/personal-verify-fork-features.sh‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ require apps/server/src/provider/Layers/GrokAdapter.ts "resolveDesktopMcp" "desk
8888
require apps/server/src/ws.ts "computerViewStream" "computer view RPCs registered"
8989
require apps/web/src/components/chat/ChatHeader.tsx "ComputerViewDialog" "computer view mounted in chat header"
9090

91+
# --- Sidebar linked-PR badge (upstream #4755/#8160 wiring, restored 2026-09-02) ---
92+
# Rows read the linked PR from the host and hold merged/closed state in the
93+
# parent atom; a merge dropped this once and left the sidebar blind to links.
94+
require apps/web/src/components/Sidebar.tsx "useLinkedThreadPullRequest(" "sidebar rows read linked PR status"
95+
require apps/web/src/components/Sidebar.tsx "onChangeRequestSnapshot={setThreadChangeRequestSnapshot}" "sidebar rows publish PR snapshots to the shared atom"
96+
9197
# --- Artwork + app icon pickers (44cf90dc0, 0efc0e293) ---
9298
require apps/web/src/components/settings/SettingsPanels.tsx "SidebarArtworkRow" "artwork picker in settings"
9399
require apps/web/src/components/settings/SettingsPanels.tsx "AppIconRow" "app icon picker in settings"

0 commit comments

Comments
 (0)