Skip to content

Commit 2652c0b

Browse files
t3dotggclaude
andcommitted
Address PR #3960 review findings and CI failures
CI: add settledOverride/settledAt + new client settings to desktop and mobile test fixtures and the mobile threadDetailToShell mapping. Review fixes (cursor bugbot + macroscope): - failed status now requires session.status === "error" instead of any lingering lastError on a stopped/ready session - effectiveSettled: pending approvals or user input always win — blocked work can never collapse into the settled tail, even manually settled - decider unsettle-on-activity guards check settledOverride === "settled" so activity no longer erases a user's explicit keep-active override - repeat thread.settle re-emits with updatedAt = original settledAt, so double-settles no longer bump recency ordering - sidebarAutoSettleAfterDays bounded 1..90 in schema and patch - approval wait sort/label use shell.updatedAt (bumped by the approval activity in the projection pipeline) instead of latestUserMessageAt / turn start, with the approximation documented - v2 rows: Enter/Space keyboard activation; double-click inline rename ported from v1 plus a context-menu Rename entry - new-thread picker: cross-project creation no longer inherits the active thread's branch/worktree; preselection consults the active draft via resolveThreadActionProjectRef; stale project scope resets to All - settle worktree prompt: vcs status checked before offering (and again at click time) — dirty or ahead worktrees are never offered Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 4441a7a commit 2652c0b

21 files changed

Lines changed: 386 additions & 68 deletions

apps/desktop/src/settings/DesktopClientSettings.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ const clientSettings: ClientSettings = {
2020
diffIgnoreWhitespace: true,
2121
favorites: [],
2222
providerModelPreferences: {},
23+
sidebarAutoSettleAfterDays: 3,
2324
sidebarProjectGroupingMode: "repository_path",
2425
sidebarProjectGroupingOverrides: {
2526
"environment-1:/tmp/project-a": "separate",
2627
},
2728
sidebarProjectSortOrder: "manual",
2829
sidebarThreadSortOrder: "created_at",
2930
sidebarThreadPreviewCount: 6,
31+
sidebarV2Enabled: false,
3032
timestampFormat: "24-hour",
3133
wordWrap: true,
3234
};

apps/mobile/src/features/archive/archivedThreadList.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ function makeThread(
4141
hasPendingUserInput: false,
4242
hasActionableProposedPlan: false,
4343
...input,
44+
settledOverride: input.settledOverride ?? null,
45+
settledAt: input.settledAt ?? null,
4446
};
4547
}
4648

apps/mobile/src/features/home/homeListItems.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ function makeThread(id: string, projectId: ProjectId): EnvironmentThreadShell {
4747
createdAt: "2026-06-01T00:00:00.000Z",
4848
updatedAt: "2026-06-01T00:00:00.000Z",
4949
archivedAt: null,
50+
settledOverride: null,
51+
settledAt: null,
5052
session: null,
5153
latestUserMessageAt: null,
5254
hasPendingApprovals: false,

apps/mobile/src/features/home/homeThreadList.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ function makeThread(
4141
hasPendingUserInput: false,
4242
hasActionableProposedPlan: false,
4343
...input,
44+
settledOverride: input.settledOverride ?? null,
45+
settledAt: input.settledAt ?? null,
4446
};
4547
}
4648

apps/mobile/src/lib/repositoryGroups.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ function makeThread(
3838
hasPendingUserInput: false,
3939
hasActionableProposedPlan: false,
4040
...input,
41+
settledOverride: input.settledOverride ?? null,
42+
settledAt: input.settledAt ?? null,
4143
};
4244
}
4345

apps/mobile/src/lib/threadActivity.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ function makeThread(
5050
checkpoints: [],
5151
session: null,
5252
...input,
53+
settledOverride: input.settledOverride ?? null,
54+
settledAt: input.settledAt ?? null,
5355
};
5456
}
5557

apps/mobile/src/state/use-thread-selection.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ function threadDetailToShell(
5858
createdAt: thread.createdAt,
5959
updatedAt: thread.updatedAt,
6060
archivedAt: thread.archivedAt,
61+
settledOverride: thread.settledOverride,
62+
settledAt: thread.settledAt,
6163
session: thread.session,
6264
latestUserMessageAt: latestUserMessageAt(thread),
6365
hasPendingApprovals: false,

apps/server/src/orchestration/decider.settled.test.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ it.layer(NodeServices.layer)("settled thread decider", (it) => {
9898
expect(reEmitEvents[0]?.type).toBe("thread.settled");
9999
if (reEmitEvents[0]?.type === "thread.settled") {
100100
expect(reEmitEvents[0].payload.settledAt).toBe(SETTLED_AT);
101+
expect(reEmitEvents[0].payload.updatedAt).toBe(SETTLED_AT);
101102
}
102103
}),
103104
);
@@ -199,10 +200,55 @@ it.layer(NodeServices.layer)("settled thread decider", (it) => {
199200
readModel: makeReadModel("active"),
200201
});
201202
const sessionEvents = Array.isArray(sessionResult) ? sessionResult : [sessionResult];
202-
expect(sessionEvents.map((event) => event.type)).toEqual([
203-
"thread.unsettled",
204-
"thread.session-set",
203+
expect(sessionEvents.map((event) => event.type)).toEqual(["thread.session-set"]);
204+
}),
205+
);
206+
207+
it.effect("preserves an explicit active override during activity", () =>
208+
Effect.gen(function* () {
209+
const turnResult = yield* decideOrchestrationCommand({
210+
command: {
211+
type: "thread.turn.start",
212+
commandId: CommandId.make("cmd-active-turn-start"),
213+
threadId: ThreadId.make("thread-1"),
214+
message: {
215+
messageId: MessageId.make("message-active"),
216+
role: "user",
217+
text: "Continue",
218+
attachments: [],
219+
},
220+
runtimeMode: "full-access",
221+
interactionMode: "default",
222+
createdAt: NOW,
223+
},
224+
readModel: makeReadModel("active"),
225+
});
226+
const turnEvents = Array.isArray(turnResult) ? turnResult : [turnResult];
227+
expect(turnEvents.map((event) => event.type)).toEqual([
228+
"thread.message-sent",
229+
"thread.turn-start-requested",
205230
]);
231+
232+
const activityResult = yield* decideOrchestrationCommand({
233+
command: {
234+
type: "thread.activity.append",
235+
commandId: CommandId.make("cmd-active-approval"),
236+
threadId: ThreadId.make("thread-1"),
237+
activity: {
238+
id: EventId.make("activity-active"),
239+
tone: "approval",
240+
kind: "approval.requested",
241+
summary: "Command approval requested",
242+
payload: null,
243+
turnId: null,
244+
createdAt: NOW,
245+
},
246+
createdAt: NOW,
247+
},
248+
readModel: makeReadModel("active"),
249+
});
250+
const activityEvents = Array.isArray(activityResult) ? activityResult : [activityResult];
251+
expect(activityEvents.map((event) => event.type)).toEqual(["thread.activity-appended"]);
206252
}),
207253
);
208254

apps/server/src/orchestration/decider.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
337337
payload: {
338338
threadId: command.threadId,
339339
settledAt,
340-
updatedAt: occurredAt,
340+
updatedAt: settledAt,
341341
},
342342
};
343343
}
@@ -519,7 +519,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
519519
createdAt: command.createdAt,
520520
},
521521
};
522-
if (targetThread.settledOverride === null) {
522+
if (targetThread.settledOverride !== "settled") {
523523
return [userMessageEvent, turnStartRequestedEvent];
524524
}
525525
const unsettledEvent: Omit<OrchestrationEvent, "sequence"> = {
@@ -681,7 +681,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
681681
// must not fight a user's explicit settle.
682682
const isSessionActivity =
683683
command.session.status === "starting" || command.session.status === "running";
684-
if (thread.settledOverride === null || !isSessionActivity) {
684+
if (thread.settledOverride !== "settled" || !isSessionActivity) {
685685
return sessionSetEvent;
686686
}
687687
const unsettledEvent: Omit<OrchestrationEvent, "sequence"> = {
@@ -857,7 +857,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
857857
const wakesSettledThread =
858858
command.activity.kind === "approval.requested" ||
859859
command.activity.kind === "user-input.requested";
860-
if (thread.settledOverride === null || !wakesSettledThread) {
860+
if (thread.settledOverride !== "settled" || !wakesSettledThread) {
861861
return activityAppendedEvent;
862862
}
863863
const unsettledEvent: Omit<OrchestrationEvent, "sequence"> = {

apps/web/src/components/Sidebar.logic.test.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -592,17 +592,23 @@ describe("resolveSidebarV2Status", () => {
592592
).toBe("working");
593593
});
594594

595-
it("reports failed only for stopped sessions with an error", () => {
595+
it("reports failed only while the session status is error", () => {
596596
expect(
597597
resolveSidebarV2Status({
598598
hasPendingApprovals: false,
599-
session: { ...session, status: "stopped" as const, lastError: "boom" },
599+
session: { ...session, status: "error" as const, lastError: "boom" },
600600
}),
601601
).toBe("failed");
602602
expect(
603603
resolveSidebarV2Status({
604604
hasPendingApprovals: false,
605-
session: { ...session, status: "stopped" as const },
605+
session: { ...session, status: "stopped" as const, lastError: "persisted" },
606+
}),
607+
).toBe("ready");
608+
expect(
609+
resolveSidebarV2Status({
610+
hasPendingApprovals: false,
611+
session: { ...session, status: "ready" as const, lastError: "persisted" },
606612
}),
607613
).toBe("ready");
608614
});
@@ -617,12 +623,13 @@ describe("sortThreadsForSidebarV2", () => {
617623
id: string;
618624
hasPendingApprovals?: boolean;
619625
latestUserMessageAt: string;
626+
updatedAt?: string;
620627
}) => ({
621628
id: input.id,
622629
hasPendingApprovals: input.hasPendingApprovals ?? false,
623630
session: null,
624631
createdAt: "2026-03-09T10:00:00.000Z",
625-
updatedAt: input.latestUserMessageAt,
632+
updatedAt: input.updatedAt ?? input.latestUserMessageAt,
626633
latestUserMessageAt: input.latestUserMessageAt,
627634
});
628635

@@ -632,12 +639,14 @@ describe("sortThreadsForSidebarV2", () => {
632639
sortable({
633640
id: "approval-new",
634641
hasPendingApprovals: true,
635-
latestUserMessageAt: "2026-03-09T11:00:00.000Z",
642+
latestUserMessageAt: "2026-03-09T08:00:00.000Z",
643+
updatedAt: "2026-03-09T11:00:00.000Z",
636644
}),
637645
sortable({
638646
id: "approval-old",
639647
hasPendingApprovals: true,
640-
latestUserMessageAt: "2026-03-09T09:00:00.000Z",
648+
latestUserMessageAt: "2026-03-09T11:30:00.000Z",
649+
updatedAt: "2026-03-09T09:00:00.000Z",
641650
}),
642651
sortable({ id: "stale", latestUserMessageAt: "2026-03-09T08:00:00.000Z" }),
643652
]);

0 commit comments

Comments
 (0)