Skip to content

Commit 36c235e

Browse files
committed
🤖 Fix workspace recency after compaction
When chat history is compacted, user messages are replaced with a summary (assistant message with compacted=true). The recency calculation only checked for user messages, causing compacted workspaces to sink to the bottom. Changes: - Rename WorkspaceState.lastUserMessageAt → recencyTimestamp (clearer semantics) - Add fallback: if no user messages, use most recent compacted message timestamp - Keeps Date.now() for new compaction summaries (workspace jumps to top, correct UX) This ensures workspaces with only compacted history maintain their position in the sidebar, while still preferring user messages during active streams. Generated with `cmux`
1 parent 3a22a64 commit 36c235e

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)