Add chat thread forking
TestFlight / Build and upload (push) Successful in 1m52s

This commit is contained in:
2026-08-16 17:42:39 -07:00
parent 42022bf055
commit eb2b0d3ca0
17 changed files with 1410 additions and 244 deletions
+40 -21
View File
@@ -127,21 +127,27 @@ function upsertWorkspaceItem(items: WorkspaceItem[], item: WorkspaceItem) {
}
function buildSidebarItems(items: WorkspaceItem[]): SidebarItem[] {
const chatsById = new Map<string, ChatSummary>();
for (const item of items) {
if (item.type === "chat") chatsById.set(item.id, item);
}
return items.map((item) => {
if (item.type === "chat") {
const chat = item;
const starOwner = chatsById.get(chat.parentChatId ?? chat.id) ?? chat;
return {
kind: "chat" as const,
id: chat.id,
title: getChatTitle(chat),
updatedAt: chat.updatedAt,
createdAt: chat.createdAt,
starred: chat.starred,
starredAt: chat.starredAt,
initiatedProvider: chat.initiatedProvider,
initiatedModel: chat.initiatedModel,
lastUsedProvider: chat.lastUsedProvider,
lastUsedModel: chat.lastUsedModel,
kind: "chat" as const,
id: chat.id,
title: getChatTitle(chat),
updatedAt: chat.updatedAt,
createdAt: chat.createdAt,
starred: starOwner.starred,
starredAt: starOwner.starredAt,
initiatedProvider: chat.initiatedProvider,
initiatedModel: chat.initiatedModel,
lastUsedProvider: chat.lastUsedProvider,
lastUsedModel: chat.lastUsedModel,
};
}
@@ -979,11 +985,11 @@ async function main() {
focusComposer();
}
async function maybeSuggestTitle(chatId: string, content: string) {
const chatSummary = chats.find((chat) => chat.id === chatId);
const hasExistingTitle = Boolean(selectedChat?.id === chatId ? selectedChat.title?.trim() : chatSummary?.title?.trim());
if (hasExistingTitle || pendingTitleGeneration.has(chatId)) return;
async function maybeSuggestTitle(chat: ChatDetail, content: string) {
const needsGeneratedTitle = chat.titleGenerationPending || !chat.title?.trim();
if (!needsGeneratedTitle || pendingTitleGeneration.has(chat.id)) return;
const chatId = chat.id;
pendingTitleGeneration.add(chatId);
try {
const updated = await api.suggestChatTitle({ chatId, content });
@@ -993,6 +999,8 @@ async function main() {
selectedChat = {
...selectedChat,
title: updated.title,
parentChatId: updated.parentChatId,
titleGenerationPending: updated.titleGenerationPending,
updatedAt: updated.updatedAt,
starred: updated.starred,
starredAt: updated.starredAt,
@@ -1049,6 +1057,8 @@ async function main() {
selectedChat = {
id: chat.id,
title: chat.title,
parentChatId: chat.parentChatId,
titleGenerationPending: chat.titleGenerationPending,
createdAt: chat.createdAt,
updatedAt: chat.updatedAt,
starred: chat.starred,
@@ -1068,13 +1078,13 @@ async function main() {
throw new Error("Unable to initialize chat");
}
void maybeSuggestTitle(chatId, content);
let baseChat = selectedChat;
if (!baseChat || baseChat.id !== chatId) {
baseChat = await api.getChat(chatId);
}
void maybeSuggestTitle(baseChat, content);
const requestMessages: CompletionRequestMessage[] = [
...baseChat.messages
.filter((message) => !isToolCallLogMessage(message))
@@ -1392,6 +1402,8 @@ async function main() {
selectedChat = {
...selectedChat,
title: updated.title,
parentChatId: updated.parentChatId,
titleGenerationPending: updated.titleGenerationPending,
updatedAt: updated.updatedAt,
initiatedProvider: updated.initiatedProvider,
initiatedModel: updated.initiatedModel,
@@ -1405,12 +1417,16 @@ async function main() {
async function handleToggleStarSelection() {
if (!selectedItem) return;
const currentItem = getSidebarItems().find((item) => item.kind === selectedItem?.kind && item.id === selectedItem?.id);
const nextStarred = !currentItem?.starred;
setError(null);
if (selectedItem.kind === "chat") {
const updated = await api.updateChatStar(selectedItem.id, nextStarred);
const selectedSummary = chats.find((chat) => chat.id === selectedItem?.id);
const selectedParentChatId = selectedChat?.id === selectedItem.id
? selectedChat.parentChatId
: selectedSummary?.parentChatId;
const rootChatId = selectedParentChatId ?? selectedItem.id;
const rootSummary = chats.find((chat) => chat.id === rootChatId);
const updated = await api.updateChatStar(rootChatId, !rootSummary?.starred);
chats = chats.map((chat) => (chat.id === updated.id ? updated : chat));
if (!chats.some((chat) => chat.id === updated.id)) chats = [updated, ...chats];
workspaceItems = workspaceItems.map((item) => (item.type === "chat" && item.id === updated.id ? chatWorkspaceItem(updated) : item));
@@ -1421,6 +1437,8 @@ async function main() {
selectedChat = {
...selectedChat,
title: updated.title,
parentChatId: updated.parentChatId,
titleGenerationPending: updated.titleGenerationPending,
updatedAt: updated.updatedAt,
starred: updated.starred,
starredAt: updated.starredAt,
@@ -1431,7 +1449,8 @@ async function main() {
};
}
} else {
const updated = await api.updateSearchStar(selectedItem.id, nextStarred);
const currentItem = getSidebarItems().find((item) => item.kind === "search" && item.id === selectedItem?.id);
const updated = await api.updateSearchStar(selectedItem.id, !currentItem?.starred);
searches = searches.map((search) => (search.id === updated.id ? updated : search));
if (!searches.some((search) => search.id === updated.id)) searches = [updated, ...searches];
workspaceItems = workspaceItems.map((item) => (item.type === "search" && item.id === updated.id ? searchWorkspaceItem(updated) : item));
+4
View File
@@ -13,6 +13,8 @@ export type ModelCatalogResponse = {
export type ChatSummary = {
id: string;
title: string | null;
parentChatId: string | null;
titleGenerationPending: boolean;
createdAt: string;
updatedAt: string;
starred: boolean;
@@ -68,6 +70,8 @@ export type ToolCallEvent = {
export type ChatDetail = {
id: string;
title: string | null;
parentChatId: string | null;
titleGenerationPending: boolean;
createdAt: string;
updatedAt: string;
starred: boolean;