Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion apps/web/src/components/CommandPalette.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export function buildThreadActionItems(input: {
searchTerms: [thread.title, projectTitle ?? "", thread.branch ?? ""],
title: thread.title,
description: descriptionParts.join(" · "),
timestamp: formatRelativeTimeLabel(thread.updatedAt ?? thread.createdAt),
timestamp: formatRelativeTimeLabel(thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt),
icon: input.icon,
run: async () => {
await input.runThread(thread);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
: "text-muted-foreground/40"
}`}
>
{formatRelativeTimeLabel(thread.updatedAt ?? thread.createdAt)}
{formatRelativeTimeLabel(thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt)}
</span>
)}
</span>
Expand Down
68 changes: 50 additions & 18 deletions apps/web/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
MessageId,
OrchestrationCheckpointSummary,
OrchestrationEvent,
OrchestrationLatestTurn,
OrchestrationMessage,
OrchestrationProposedPlan,
OrchestrationReadModel,
Expand Down Expand Up @@ -344,6 +345,49 @@ function buildSidebarThreadSummary(thread: Thread): SidebarThreadSummary {
};
}

function sourceProposedPlansEqual(
left: OrchestrationLatestTurn["sourceProposedPlan"] | undefined,
right: OrchestrationLatestTurn["sourceProposedPlan"] | undefined,
): boolean {
if (left === right) return true;
if (left === undefined || right === undefined) return false;
return left.threadId === right.threadId && left.planId === right.planId;
}

function latestTurnsEqual(
left: OrchestrationLatestTurn | null | undefined,
right: OrchestrationLatestTurn | null | undefined,
): boolean {
if (left === right) return true;
if (left == null || right == null) return false;
return (
left.turnId === right.turnId &&
left.state === right.state &&
left.requestedAt === right.requestedAt &&
left.startedAt === right.startedAt &&
left.completedAt === right.completedAt &&
left.assistantMessageId === right.assistantMessageId &&
sourceProposedPlansEqual(left.sourceProposedPlan, right.sourceProposedPlan)
);
}

function threadSessionsEqual(
left: ThreadSession | null | undefined,
right: ThreadSession | null | undefined,
): boolean {
if (left === right) return true;
if (left == null || right == null) return false;
return (
left.provider === right.provider &&
left.status === right.status &&
left.orchestrationStatus === right.orchestrationStatus &&
left.activeTurnId === right.activeTurnId &&
left.createdAt === right.createdAt &&
left.updatedAt === right.updatedAt &&
left.lastError === right.lastError
);
}

function sidebarThreadSummariesEqual(
left: SidebarThreadSummary | undefined,
right: SidebarThreadSummary,
Expand All @@ -354,11 +398,11 @@ function sidebarThreadSummariesEqual(
left.projectId === right.projectId &&
left.title === right.title &&
left.interactionMode === right.interactionMode &&
left.session === right.session &&
threadSessionsEqual(left.session, right.session) &&
left.createdAt === right.createdAt &&
left.archivedAt === right.archivedAt &&
left.updatedAt === right.updatedAt &&
left.latestTurn === right.latestTurn &&
latestTurnsEqual(left.latestTurn, right.latestTurn) &&
left.branch === right.branch &&
left.worktreePath === right.worktreePath &&
left.latestUserMessageAt === right.latestUserMessageAt &&
Expand Down Expand Up @@ -391,8 +435,8 @@ function threadShellsEqual(left: ThreadShell | undefined, right: ThreadShell): b
function threadTurnStatesEqual(left: ThreadTurnState | undefined, right: ThreadTurnState): boolean {
return (
left !== undefined &&
left.latestTurn === right.latestTurn &&
left.pendingSourceProposedPlan === right.pendingSourceProposedPlan
latestTurnsEqual(left.latestTurn, right.latestTurn) &&
sourceProposedPlansEqual(left.pendingSourceProposedPlan, right.pendingSourceProposedPlan)
);
}

Expand Down Expand Up @@ -475,8 +519,6 @@ function writeThreadState(
const nextTurnState = toThreadTurnState(nextThread);
const previousShell = state.threadShellById[nextThread.id];
const previousTurnState = state.threadTurnStateById[nextThread.id];
const previousSummary = state.sidebarThreadSummaryById[nextThread.id];
const nextSummary = buildSidebarThreadSummary(nextThread);

let nextState = state;

Expand Down Expand Up @@ -530,7 +572,7 @@ function writeThreadState(
};
}

if ((previousThread?.session ?? null) !== nextThread.session) {
if (!threadSessionsEqual(previousThread?.session ?? null, nextThread.session)) {
nextState = {
...nextState,
threadSessionById: {
Expand Down Expand Up @@ -610,16 +652,6 @@ function writeThreadState(
};
}

if (!sidebarThreadSummariesEqual(previousSummary, nextSummary)) {
nextState = {
...nextState,
sidebarThreadSummaryById: {
...nextState.sidebarThreadSummaryById,
[nextThread.id]: nextSummary,
},
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sidebar summary never written for full thread updates

High Severity

The writeThreadState function no longer writes to sidebarThreadSummaryById, but it's the sole write path for many thread events. When a thread.created event fires, the thread ID is added to threadIds and other structures, but no sidebarThreadSummaryById entry is created. Since selectSidebarThreadsForProjectRef filters out threads without a sidebar summary entry, newly created threads won't appear in the sidebar until a separate thread-upserted shell event arrives. Similarly, all event-driven updates (thread.message-sent, thread.archived, session-set, etc.) via updateThreadState → writeThreadState won't update the sidebar's latestUserMessageAt, hasPendingApprovals, or other derived fields — the very fields this PR aims to leverage.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0bb7044. Configure here.

return nextState;
}

Expand Down Expand Up @@ -686,7 +718,7 @@ function writeThreadShellState(
};
}

if ((state.threadSessionById[nextThread.shell.id] ?? null) !== nextThread.session) {
if (!threadSessionsEqual(state.threadSessionById[nextThread.shell.id] ?? null, nextThread.session)) {
nextState = {
...nextState,
threadSessionById: {
Expand Down
Loading