Sort chat recents by last activity (#6844)

* show chat by by last activity

* Update chat thread updated_at logic and enhance sidebar chat item handling

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Nilay 2026-07-07 22:24:32 +05:30 committed by GitHub
commit 07ecdb34c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 252 additions and 13 deletions

View file

@ -27,16 +27,21 @@ export interface SidebarItem {
id: string;
title: string;
createdAt: number;
updatedAt: number;
isFork?: boolean;
projectId?: string | null;
}
function lastActivityAt(thread: ThreadRecord): number {
return thread.updatedAt ?? thread.createdAt;
}
export function groupThreads(
threads: ThreadRecord[],
archived = false,
): SidebarItem[] {
const items: SidebarItem[] = [];
const seenPairs = new Set<string>();
const pairItems = new Map<string, SidebarItem>();
for (const t of threads) {
// Coerce archived to a boolean before comparing. Legacy threads (from the
@ -48,30 +53,35 @@ export function groupThreads(
continue;
}
if (t.pairId) {
if (seenPairs.has(t.pairId)) {
const existing = pairItems.get(t.pairId);
if (existing) {
existing.updatedAt = Math.max(existing.updatedAt, lastActivityAt(t));
continue;
}
seenPairs.add(t.pairId);
items.push({
const item: SidebarItem = {
type: "compare",
id: t.pairId,
title: t.title,
createdAt: t.createdAt,
updatedAt: lastActivityAt(t),
projectId: t.projectId ?? null,
});
};
pairItems.set(t.pairId, item);
items.push(item);
} else if (!t.pairId) {
items.push({
type: "single",
id: t.id,
title: t.title,
createdAt: t.createdAt,
updatedAt: lastActivityAt(t),
isFork: Boolean(t.forkedFromThreadId),
projectId: t.projectId ?? null,
});
}
}
return items.sort((a, b) => b.createdAt - a.createdAt);
return items.sort((a, b) => b.updatedAt - a.updatedAt);
}
// Streaming fires CHAT_HISTORY_UPDATED_EVENT per chunk. Debounce so each quiet
@ -84,6 +94,7 @@ export function useChatSidebarItems(options?: {
requireMessages?: boolean;
}) {
const [allThreads, setAllThreads] = useState<ThreadRecord[]>([]);
const [loaded, setLoaded] = useState(false);
const enabled = options?.enabled ?? true;
const requireMessages = options?.requireMessages ?? true;
@ -111,6 +122,7 @@ export function useChatSidebarItems(options?: {
// were in flight, or if the effect was torn down.
if (cancelled || seq !== requestSeq) return;
setAllThreads(threads);
setLoaded(true);
} catch (error) {
if (isExpectedBackgroundChatStorageError(error)) {
return;
@ -144,7 +156,7 @@ export function useChatSidebarItems(options?: {
const archivedItems = groupThreads(allThreads ?? [], true);
const canCompare = useChatRuntimeStore((s) => Boolean(s.params.checkpoint));
return { items, archivedItems, canCompare };
return { items, archivedItems, canCompare, loaded };
}
function cancelIfRunning(threadId: string): void {

View file

@ -36,6 +36,7 @@ export interface ThreadRecord {
projectId?: string | null;
archived: boolean;
createdAt: number;
updatedAt?: number;
/**
* OpenAI shell tool container id from a prior response. When set, the
* next turn reuses it via `environment.type="container_reference"` so

View file

@ -590,7 +590,10 @@ export async function listStoredChatThreads(
}
return Array.from(byId.values())
.filter((thread) => matchesThreadListArgs(thread, args))
.sort((a, b) => b.createdAt - a.createdAt);
.sort(
(a, b) =>
(b.updatedAt ?? b.createdAt) - (a.updatedAt ?? a.createdAt),
);
}
export async function listStoredChatThreadsWithMessages(