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

@@ -10,6 +10,7 @@ import type {
SearchStreamHandlers,
SearchSummary,
SessionStatus,
WorkspaceItem,
} from "./types.js";
type RequestOptions = {
@@ -41,6 +42,11 @@ export class SybilApiClient {
return data.chats;
}
async listWorkspaceItems() {
const data = await this.request<{ items: WorkspaceItem[] }>("/v1/workspace-items");
return data.items;
}
async createChat(title?: string) {
const data = await this.request<{ chat: ChatSummary }>("/v1/chats", {
method: "POST",

View File

@@ -11,6 +11,7 @@ import type {
SearchDetail,
SearchSummary,
ToolCallEvent,
WorkspaceItem,
} from "./types.js";
type SidebarSelection = { kind: "chat" | "search"; id: string };
@@ -93,9 +94,38 @@ function getSearchTitle(search: Pick<SearchSummary, "title" | "query">) {
return "New search";
}
function buildSidebarItems(chats: ChatSummary[], searches: SearchSummary[]): SidebarItem[] {
const items: SidebarItem[] = [
...chats.map((chat) => ({
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 upsertWorkspaceItem(items: WorkspaceItem[], item: WorkspaceItem) {
return [item, ...items.filter((existing) => existing.type !== item.type || existing.id !== item.id)];
}
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),
@@ -105,8 +135,11 @@ function buildSidebarItems(chats: ChatSummary[], searches: SearchSummary[]): Sid
initiatedModel: chat.initiatedModel,
lastUsedProvider: chat.lastUsedProvider,
lastUsedModel: chat.lastUsedModel,
})),
...searches.map((search) => ({
};
}
const search = item;
return {
kind: "search" as const,
id: search.id,
title: getSearchTitle(search),
@@ -116,10 +149,8 @@ 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 asToolLogMetadata(value: unknown): ToolLogMetadata | null {
@@ -195,6 +226,7 @@ async function main() {
let authMode: "open" | "token" | null = null;
let chats: ChatSummary[] = [];
let searches: SearchSummary[] = [];
let workspaceItems: WorkspaceItem[] = [];
let selectedItem: SidebarSelection | null = null;
let selectedChat: ChatDetail | null = null;
let selectedSearch: SearchDetail | null = null;
@@ -377,7 +409,7 @@ async function main() {
}
function getSidebarItems() {
return buildSidebarItems(chats, searches);
return buildSidebarItems(workspaceItems);
}
function getSelectedChatSummary() {
@@ -701,6 +733,7 @@ async function main() {
function resetWorkspaceState() {
chats = [];
searches = [];
workspaceItems = [];
selectedItem = null;
selectedChat = null;
selectedSearch = null;
@@ -767,11 +800,13 @@ async function main() {
updateUI();
try {
const [nextChats, nextSearches] = await Promise.all([api.listChats(), api.listSearches()]);
const nextWorkspaceItems = await api.listWorkspaceItems();
const { chats: nextChats, searches: nextSearches } = splitWorkspaceItems(nextWorkspaceItems);
workspaceItems = nextWorkspaceItems;
chats = nextChats;
searches = nextSearches;
const nextItems = buildSidebarItems(nextChats, nextSearches);
const nextItems = buildSidebarItems(nextWorkspaceItems);
if (options?.preferredSelection && hasItem(nextItems, options.preferredSelection)) {
selectedItem = options.preferredSelection;
draftKind = null;
@@ -876,6 +911,7 @@ async function main() {
try {
const updated = await api.suggestChatTitle({ chatId, content });
chats = chats.map((chat) => (chat.id === updated.id ? { ...chat, title: updated.title, updatedAt: updated.updatedAt } : chat));
workspaceItems = workspaceItems.map((item) => (item.type === "chat" && item.id === updated.id ? chatWorkspaceItem(updated) : item));
if (selectedChat?.id === updated.id) {
selectedChat = { ...selectedChat, title: updated.title, updatedAt: updated.updatedAt };
}
@@ -920,6 +956,7 @@ async function main() {
chatId = chat.id;
draftKind = null;
chats = [chat, ...chats.filter((existing) => existing.id !== chat.id)];
workspaceItems = upsertWorkspaceItem(workspaceItems, chatWorkspaceItem(chat));
selectedItem = { kind: "chat", id: chat.id };
pendingChatState = pendingChatState ? { ...pendingChatState, chatId } : pendingChatState;
selectedChat = {
@@ -1085,6 +1122,7 @@ async function main() {
draftKind = null;
selectedItem = { kind: "search", id: searchId };
searches = [search, ...searches.filter((existing) => existing.id !== search.id)];
workspaceItems = upsertWorkspaceItem(workspaceItems, searchWorkspaceItem(search));
selectedChat = null;
forceScrollToBottom = true;
updateUI();

View File

@@ -29,6 +29,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;