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:
parent
37075c5422
commit
07ecdb34c0
9 changed files with 252 additions and 13 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export const en = {
|
|||
recipes: "Recipes",
|
||||
export: "Export",
|
||||
recents: "Recents",
|
||||
noChatsYet: "No chats yet",
|
||||
settings: "Settings",
|
||||
api: "API",
|
||||
lightMode: "Light Mode",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export const zhCN = {
|
|||
recipes: "配方",
|
||||
export: "导出",
|
||||
recents: "最近",
|
||||
noChatsYet: "暂无对话",
|
||||
settings: "设置",
|
||||
api: "API",
|
||||
lightMode: "浅色模式",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue