From 0bb704418e007add959a7f1151744bf14827c53e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 13 Apr 2026 10:59:06 -0700 Subject: [PATCH 1/6] Use latest user message time for thread timestamps - Show thread timestamps from `latestUserMessageAt` when available - Compare nested session and turn objects by value to avoid stale sidebar state --- .../src/components/CommandPalette.logic.ts | 2 +- apps/web/src/components/Sidebar.tsx | 2 +- apps/web/src/store.ts | 68 ++++++++++++++----- 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/apps/web/src/components/CommandPalette.logic.ts b/apps/web/src/components/CommandPalette.logic.ts index c29f1271087..a8a0b5387d7 100644 --- a/apps/web/src/components/CommandPalette.logic.ts +++ b/apps/web/src/components/CommandPalette.logic.ts @@ -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); diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 2495429efd8..05e394b5db5 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -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)} )} diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts index 092593b06d2..7b515979528 100644 --- a/apps/web/src/store.ts +++ b/apps/web/src/store.ts @@ -3,6 +3,7 @@ import type { MessageId, OrchestrationCheckpointSummary, OrchestrationEvent, + OrchestrationLatestTurn, OrchestrationMessage, OrchestrationProposedPlan, OrchestrationReadModel, @@ -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, @@ -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 && @@ -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) ); } @@ -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; @@ -530,7 +572,7 @@ function writeThreadState( }; } - if ((previousThread?.session ?? null) !== nextThread.session) { + if (!threadSessionsEqual(previousThread?.session ?? null, nextThread.session)) { nextState = { ...nextState, threadSessionById: { @@ -610,16 +652,6 @@ function writeThreadState( }; } - if (!sidebarThreadSummariesEqual(previousSummary, nextSummary)) { - nextState = { - ...nextState, - sidebarThreadSummaryById: { - ...nextState.sidebarThreadSummaryById, - [nextThread.id]: nextSummary, - }, - }; - } - return nextState; } @@ -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: { From 0e8c1fe77b4294ff64423766fc39bcba00b4676d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 13 Apr 2026 12:15:59 -0700 Subject: [PATCH 2/6] Stabilize sidebar thread timestamp updates - Use refs to avoid invalidating sidebar row callbacks - Keep relative timestamps at "just now" under 60 seconds - Add React Scan script to the web shell --- apps/web/index.html | 1 + apps/web/src/components/Sidebar.tsx | 18 ++++++++---------- apps/web/src/hooks/useThreadActions.ts | 12 +++++++++--- apps/web/src/timestampFormat.ts | 3 +-- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/apps/web/index.html b/apps/web/index.html index 9f0329b6020..a0149eba6d8 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -8,6 +8,7 @@ +