Skip to content

Commit 60a71e2

Browse files
authored
🤖 Fix workspace recency after compaction (#229)
## Problem After compaction, workspaces would drop to the bottom of the sidebar list. This happened because recency calculation only checked for user messages, and compaction replaces chat history with a summary (assistant message with `compacted: true`). ## Solution Updated recency calculation to use a fallback: 1. **Prefer user messages** - avoids constant reordering during concurrent streams 2. **Fall back to compacted messages** - prevents sinking to bottom after compaction 3. **null if neither exists** - workspace has no activity The compaction summary continues to use `Date.now()` as its timestamp, so workspaces jump to the top after compaction (correct UX, since it's active usage). ## Changes - Renamed `WorkspaceState.lastUserMessageAt` → `recencyTimestamp` for clearer semantics - Added fallback logic to check compacted message timestamp when no user messages exist ## Testing - ✅ `make typecheck` passes - ✅ `make test` passes (409 pass, 1 skip, 0 fail) _Generated with `cmux`_
1 parent 7758a94 commit 60a71e2

File tree

1 file changed

+21
-8
lines changed

1 file changed

+21
-8
lines changed

src/hooks/useWorkspaceAggregators.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export interface WorkspaceState {
3131
loading: boolean;
3232
cmuxMessages: CmuxMessage[];
3333
currentModel: string;
34-
lastUserMessageAt: number | null; // Timestamp of most recent user message (null if no user messages)
34+
recencyTimestamp: number | null; // Timestamp for sorting: most recent user message, or compacted message if no user messages
3535
}
3636

3737
/**
@@ -69,14 +69,27 @@ export function useWorkspaceAggregators(workspaceMetadata: Map<string, Workspace
6969
const isCaughtUp = caughtUpRef.current.get(workspaceId) ?? false;
7070
const activeStreams = aggregator.getActiveStreams();
7171

72-
// Get most recent user message timestamp (persisted, survives restarts)
73-
// Using user messages instead of assistant messages avoids constant reordering
74-
// when multiple concurrent streams are running
72+
// Get recency timestamp for sorting:
73+
// 1. Prefer most recent user message (avoids constant reordering during concurrent streams)
74+
// 2. Fallback to most recent compacted message (prevents workspace from dropping to bottom after compaction)
75+
// 3. null if no messages with timestamps
7576
const messages = aggregator.getAllMessages();
7677
const lastUserMsg = [...messages]
7778
.reverse()
7879
.find((m) => m.role === "user" && m.metadata?.timestamp);
79-
const lastUserMessageAt = lastUserMsg?.metadata?.timestamp ?? null;
80+
81+
let recencyTimestamp: number | null = null;
82+
if (lastUserMsg?.metadata?.timestamp) {
83+
recencyTimestamp = lastUserMsg.metadata.timestamp;
84+
} else {
85+
// No user messages - check for compacted messages
86+
const lastCompactedMsg = [...messages]
87+
.reverse()
88+
.find((m) => m.metadata?.compacted === true && m.metadata?.timestamp);
89+
if (lastCompactedMsg?.metadata?.timestamp) {
90+
recencyTimestamp = lastCompactedMsg.metadata.timestamp;
91+
}
92+
}
8093

8194
return {
8295
messages: aggregator.getDisplayedMessages(),
@@ -85,7 +98,7 @@ export function useWorkspaceAggregators(workspaceMetadata: Map<string, Workspace
8598
loading: !hasMessages && !isCaughtUp,
8699
cmuxMessages: aggregator.getAllMessages(),
87100
currentModel: aggregator.getCurrentModel() ?? "claude-sonnet-4-5",
88-
lastUserMessageAt,
101+
recencyTimestamp,
89102
};
90103
},
91104
[getAggregator]
@@ -343,8 +356,8 @@ export function useWorkspaceAggregators(workspaceMetadata: Map<string, Workspace
343356
() => {
344357
const timestamps: Record<string, number> = {};
345358
for (const [id, state] of workspaceStates) {
346-
if (state.lastUserMessageAt !== null) {
347-
timestamps[id] = state.lastUserMessageAt;
359+
if (state.recencyTimestamp !== null) {
360+
timestamps[id] = state.recencyTimestamp;
348361
}
349362
}
350363
return timestamps;

0 commit comments

Comments
 (0)