Skip to content

Commit c220483

Browse files
committed
fix(web): stop sticky-merging sidebar summary pending flags
The sidebar was stickily preserving the previous hasPendingApprovals / hasPendingUserInput / hasActionableProposedPlan flags across every thread-upserted shell event. This was defensive code from the era when the shell listing snapshot always reported these flags as false and relied on detail events to correct them. After upstream f7fa62a (shell snapshot queries), the server projection tracks pending_approval_count / pending_user_input_count / has_actionable_proposed_plan in SQL and updates them in the same transaction as the corresponding domain event. Every thread-aggregate event emits a fresh thread-upserted shell event derived from the updated projection — so the shell event's flags are the authoritative truth. Keeping the sticky merge caused a ghost "Pending Approval" indicator to linger in the sidebar forever on threads whose approval had long since been resolved. Update the regression tests to reflect the new authority: the shell event wins. Detail events can still flip flags to true; shell events can now flip them back to false.
1 parent 069b0e8 commit c220483

2 files changed

Lines changed: 19 additions & 18 deletions

File tree

apps/web/src/store.test.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,7 +1342,19 @@ describe("incremental orchestration updates", () => {
13421342
});
13431343
});
13441344

1345-
describe("shell events preserve detail-authoritative sidebar summary flags", () => {
1345+
describe("shell events are authoritative for sidebar summary flags", () => {
1346+
// Post-f7fa62aa (shell snapshot queries), the server projection tracks
1347+
// pendingApprovalCount / pendingUserInputCount / hasActionableProposedPlan
1348+
// in SQL and updates them in the same transaction that persists the
1349+
// corresponding domain events. Every thread-aggregate event emits a fresh
1350+
// thread-upserted shell event derived from the updated projection, so the
1351+
// shell event's flags are always consistent with the detail stream state.
1352+
//
1353+
// Earlier MarCode code stickily preserved prior detail-derived flags across
1354+
// shell events to guard a theoretical race. That made resolved approvals
1355+
// linger in the sidebar indefinitely (ghost "Pending Approval" badges on
1356+
// completed threads). The shell event must win.
1357+
13461358
function makeThreadUpsertedShellEvent(
13471359
thread: Thread,
13481360
overrides: Partial<OrchestrationThreadShell> = {},
@@ -1374,7 +1386,7 @@ describe("shell events preserve detail-authoritative sidebar summary flags", ()
13741386
};
13751387
}
13761388

1377-
it("keeps hasPendingUserInput after a thread-upserted shell event follows a detail activity", () => {
1389+
it("clears hasPendingUserInput when a later shell event reports the projection resolved it", () => {
13781390
const thread = makeThread();
13791391
const state = makeState(thread);
13801392

@@ -1424,11 +1436,10 @@ describe("shell events preserve detail-authoritative sidebar summary flags", ()
14241436
localEnvironmentId,
14251437
);
14261438
const afterShellSummary = selectSidebarThreadSummaryByRef(afterShell, ref);
1427-
expect(afterShellSummary?.hasPendingUserInput).toBe(true);
1428-
expect(resolveThreadStatusPill({ thread: afterShellSummary! })?.label).toBe("Awaiting Input");
1439+
expect(afterShellSummary?.hasPendingUserInput).toBe(false);
14291440
});
14301441

1431-
it("keeps hasActionableProposedPlan after a thread-upserted shell event follows a detail plan", () => {
1442+
it("clears hasActionableProposedPlan when a later shell event reports the projection resolved it", () => {
14321443
const thread = makeThread({
14331444
interactionMode: "plan",
14341445
latestTurn: {
@@ -1470,7 +1481,6 @@ describe("shell events preserve detail-authoritative sidebar summary flags", ()
14701481
localEnvironmentId,
14711482
);
14721483
const afterShellSummary = selectSidebarThreadSummaryByRef(afterShell, ref);
1473-
expect(afterShellSummary?.hasActionableProposedPlan).toBe(true);
1474-
expect(resolveThreadStatusPill({ thread: afterShellSummary! })?.label).toBe("Plan Ready");
1484+
expect(afterShellSummary?.hasActionableProposedPlan).toBe(false);
14751485
});
14761486
});

apps/web/src/store.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -799,21 +799,12 @@ function writeThreadShellState(
799799
}
800800

801801
const previousSummary = state.sidebarThreadSummaryById[nextThread.shell.id];
802-
const mergedSummary: SidebarThreadSummary = previousSummary
803-
? {
804-
...nextThread.summary,
805-
hasPendingApprovals: previousSummary.hasPendingApprovals,
806-
hasPendingUserInput: previousSummary.hasPendingUserInput,
807-
hasActionableProposedPlan: previousSummary.hasActionableProposedPlan,
808-
}
809-
: nextThread.summary;
810-
811-
if (!sidebarThreadSummariesEqual(previousSummary, mergedSummary)) {
802+
if (!sidebarThreadSummariesEqual(previousSummary, nextThread.summary)) {
812803
nextState = {
813804
...nextState,
814805
sidebarThreadSummaryById: {
815806
...nextState.sidebarThreadSummaryById,
816-
[nextThread.shell.id]: mergedSummary,
807+
[nextThread.shell.id]: nextThread.summary,
817808
},
818809
};
819810
}

0 commit comments

Comments
 (0)