introduces workspace items as combined search+chat model

This commit is contained in:
2026-05-17 00:28:09 -07:00
parent a8e765e026
commit 411790ee04
13 changed files with 412 additions and 87 deletions

View File

@@ -20,8 +20,7 @@ import {
getChat,
listModels,
getSearch,
listChats,
listSearches,
listWorkspaceItems,
runCompletionStream,
runSearchStream,
suggestChatTitle,
@@ -37,6 +36,7 @@ import {
type SearchDetail,
type SearchSummary,
type ToolCallEvent,
type WorkspaceItem,
} from "@/lib/api";
import { useSessionAuth } from "@/hooks/use-session-auth";
import { cn } from "@/lib/utils";
@@ -588,20 +588,48 @@ function getSearchTitle(search: Pick<SearchSummary, "title" | "query">) {
return "New search";
}
function buildSidebarItems(chats: ChatSummary[], searches: SearchSummary[]): SidebarItem[] {
const items: SidebarItem[] = [
...chats.map((chat) => ({
kind: "chat" as const,
id: chat.id,
title: getChatTitle(chat),
updatedAt: chat.updatedAt,
createdAt: chat.createdAt,
initiatedProvider: chat.initiatedProvider,
initiatedModel: chat.initiatedModel,
lastUsedProvider: chat.lastUsedProvider,
lastUsedModel: chat.lastUsedModel,
})),
...searches.map((search) => ({
function chatWorkspaceItem(chat: ChatSummary): WorkspaceItem {
return { type: "chat", ...chat };
}
function searchWorkspaceItem(search: SearchSummary): WorkspaceItem {
return { type: "search", ...search };
}
function splitWorkspaceItems(items: WorkspaceItem[]) {
const chats: ChatSummary[] = [];
const searches: SearchSummary[] = [];
for (const item of items) {
if (item.type === "chat") {
const { type: _type, ...chat } = item;
chats.push(chat);
} else {
const { type: _type, ...search } = item;
searches.push(search);
}
}
return { chats, searches };
}
function buildSidebarItems(items: WorkspaceItem[]): SidebarItem[] {
return items.map((item) => {
if (item.type === "chat") {
const chat = item;
return {
kind: "chat" as const,
id: chat.id,
title: getChatTitle(chat),
updatedAt: chat.updatedAt,
createdAt: chat.createdAt,
initiatedProvider: chat.initiatedProvider,
initiatedModel: chat.initiatedModel,
lastUsedProvider: chat.lastUsedProvider,
lastUsedModel: chat.lastUsedModel,
};
}
const search = item;
return {
kind: "search" as const,
id: search.id,
title: getSearchTitle(search),
@@ -611,10 +639,21 @@ function buildSidebarItems(chats: ChatSummary[], searches: SearchSummary[]): Sid
initiatedModel: null,
lastUsedProvider: null,
lastUsedModel: null,
})),
];
};
});
}
return items.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
function upsertWorkspaceItem(items: WorkspaceItem[], item: WorkspaceItem, moveToFront = true) {
const withoutExisting = items.filter((existing) => existing.type !== item.type || existing.id !== item.id);
if (moveToFront) {
return [item, ...withoutExisting];
}
const existingIndex = items.findIndex((existing) => existing.type === item.type && existing.id === item.id);
if (existingIndex < 0) return [item, ...items];
const next = [...items];
next[existingIndex] = item;
return next;
}
function buildActiveRunsState(activeRuns: ActiveRunsResponse): ActiveRunsState {
@@ -675,6 +714,7 @@ export default function App() {
const [chats, setChats] = useState<ChatSummary[]>([]);
const [searches, setSearches] = useState<SearchSummary[]>([]);
const [workspaceItems, setWorkspaceItems] = useState<WorkspaceItem[]>([]);
const [selectedItem, setSelectedItem] = useState<SidebarSelection | null>(null);
const [selectedChat, setSelectedChat] = useState<ChatDetail | null>(null);
const [selectedSearch, setSelectedSearch] = useState<SearchDetail | null>(null);
@@ -801,7 +841,7 @@ export default function App() {
pendingAttachmentsRef.current = pendingAttachments;
}, [pendingAttachments]);
const sidebarItems = useMemo(() => buildSidebarItems(chats, searches), [chats, searches]);
const sidebarItems = useMemo(() => buildSidebarItems(workspaceItems), [workspaceItems]);
const filteredSidebarItems = useMemo(() => {
const query = sidebarQuery.trim().toLowerCase();
if (!query) return sidebarItems;
@@ -817,6 +857,7 @@ export default function App() {
const resetWorkspaceState = () => {
setChats([]);
setSearches([]);
setWorkspaceItems([]);
setSelectedItem(null);
setSelectedChat(null);
setSelectedSearch(null);
@@ -852,15 +893,16 @@ export default function App() {
const refreshCollections = async (preferredSelection?: SidebarSelection) => {
setIsLoadingCollections(true);
try {
const [nextChats, nextSearches] = await Promise.all([listChats(), listSearches()]);
const nextItems = buildSidebarItems(nextChats, nextSearches);
const nextWorkspaceItems = await listWorkspaceItems();
const { chats: nextChats, searches: nextSearches } = splitWorkspaceItems(nextWorkspaceItems);
setWorkspaceItems(nextWorkspaceItems);
setChats(nextChats);
setSearches(nextSearches);
setSelectedItem((current) => {
const hasItem = (candidate: SidebarSelection | null) => {
if (!candidate) return false;
return nextItems.some((item) => item.kind === candidate.kind && item.id === candidate.id);
return nextWorkspaceItems.some((item) => item.type === candidate.kind && item.id === candidate.id);
};
if (preferredSelection && hasItem(preferredSelection)) {
@@ -869,8 +911,8 @@ export default function App() {
if (hasItem(current)) {
return current;
}
const first = nextItems[0];
return first ? { kind: first.kind, id: first.id } : null;
const first = nextWorkspaceItems[0];
return first ? { kind: first.type, id: first.id } : null;
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
@@ -1551,6 +1593,7 @@ export default function App() {
const withoutExisting = current.filter((existing) => existing.id !== chat.id);
return [chat, ...withoutExisting];
});
setWorkspaceItems((current) => upsertWorkspaceItem(current, chatWorkspaceItem(chat)));
setSelectedItem({ kind: "chat", id: chatId });
setSelectedChat({
id: chat.id,
@@ -1616,6 +1659,7 @@ export default function App() {
return { ...chat, title: updatedChat.title, updatedAt: updatedChat.updatedAt };
})
);
setWorkspaceItems((current) => upsertWorkspaceItem(current, chatWorkspaceItem(updatedChat), false));
setSelectedChat((current) => {
if (!current || current.id !== updatedChat.id) return current;
return { ...current, title: updatedChat.title, updatedAt: updatedChat.updatedAt };
@@ -1748,6 +1792,11 @@ export default function App() {
searchId = search.id;
setDraftKind(null);
setSelectedItem({ kind: "search", id: searchId });
setSearches((current) => {
const withoutExisting = current.filter((existing) => existing.id !== search.id);
return [search, ...withoutExisting];
});
setWorkspaceItems((current) => upsertWorkspaceItem(current, searchWorkspaceItem(search)));
}
if (!searchId) {
@@ -2121,6 +2170,7 @@ export default function App() {
const withoutExisting = current.filter((existing) => existing.id !== chat.id);
return [chat, ...withoutExisting];
});
setWorkspaceItems((current) => upsertWorkspaceItem(current, chatWorkspaceItem(chat)));
setSelectedItem({ kind: "chat", id: chat.id });
setSelectedChat({
id: chat.id,
@@ -2296,6 +2346,7 @@ export default function App() {
const withoutExisting = current.filter((existing) => existing.id !== chat.id);
return [chat, ...withoutExisting];
});
setWorkspaceItems((current) => upsertWorkspaceItem(current, chatWorkspaceItem(chat)));
setSelectedItem({ kind: "chat", id: chat.id });
setSelectedChat({
id: chat.id,

View File

@@ -17,6 +17,16 @@ export type SearchSummary = {
updatedAt: string;
};
export type ChatWorkspaceItem = ChatSummary & {
type: "chat";
};
export type SearchWorkspaceItem = SearchSummary & {
type: "search";
};
export type WorkspaceItem = ChatWorkspaceItem | SearchWorkspaceItem;
export type Message = {
id: string;
createdAt: string;
@@ -214,6 +224,11 @@ export async function listChats() {
return data.chats;
}
export async function listWorkspaceItems() {
const data = await api<{ items: WorkspaceItem[] }>("/v1/workspace-items");
return data.items;
}
export async function verifySession() {
return api<{ authenticated: true; mode: "open" | "token" }>("/v1/auth/session");
}