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

@ -359,7 +359,11 @@ export function AppSidebar() {
const activeProjectId = isChatRoute
? ((search.project as string | undefined) ?? null)
: null;
const { items: allChatItems } = useChatSidebarItems({
const {
items: allChatItems,
archivedItems: archivedChatItems,
loaded: chatItemsLoaded,
} = useChatSidebarItems({
enabled: !isStudioRoute,
requireMessages: false,
});
@ -1306,6 +1310,16 @@ export function AppSidebar() {
renderChatSidebarItem(item, "recent"),
)}
</SidebarMenu>
{/* "No chats yet" only when there is truly no history:
project-scoped and archived threads leave Recents empty
but still count as existing chats. */}
{chatItemsLoaded &&
allChatItems.length === 0 &&
archivedChatItems.length === 0 && (
<p className="px-3 py-2 text-xs text-muted-foreground">
{t("shell.navigation.noChatsYet")}
</p>
)}
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>

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(

View file

@ -41,6 +41,7 @@ export const en = {
recipes: "Recipes",
export: "Export",
recents: "Recents",
noChatsYet: "No chats yet",
settings: "Settings",
api: "API",
lightMode: "Light Mode",

View file

@ -41,6 +41,7 @@ export const zhCN = {
recipes: "配方",
export: "导出",
recents: "最近",
noChatsYet: "暂无对话",
settings: "设置",
api: "API",
lightMode: "浅色模式",