Preserve draft views during collection refreshes

This commit is contained in:
2026-07-25 17:14:39 -07:00
parent 211fad972a
commit 048456b8a5

View File

@@ -93,6 +93,11 @@ type ActiveRunsState = {
chats: Record<string, true>; chats: Record<string, true>;
searches: Record<string, true>; searches: Record<string, true>;
}; };
type RefreshCollectionsOptions = {
preferredSelection?: SidebarSelection;
reportTransientError?: boolean;
selectFallback?: boolean;
};
function readSidebarSelectionFromUrl(): SidebarSelection | null { function readSidebarSelectionFromUrl(): SidebarSelection | null {
if (typeof window === "undefined") return null; if (typeof window === "undefined") return null;
@@ -1125,7 +1130,11 @@ export default function App() {
resetWorkspaceState(); resetWorkspaceState();
}; };
const refreshCollections = async (preferredSelection?: SidebarSelection, reportTransientError = true) => { const refreshCollections = async ({
preferredSelection,
reportTransientError = true,
selectFallback = false,
}: RefreshCollectionsOptions = {}) => {
setIsLoadingCollections(true); setIsLoadingCollections(true);
try { try {
const nextWorkspaceItems = await listWorkspaceItems(); const nextWorkspaceItems = await listWorkspaceItems();
@@ -1146,6 +1155,9 @@ export default function App() {
if (hasItem(current)) { if (hasItem(current)) {
return current; return current;
} }
if (!selectFallback) {
return null;
}
const first = nextWorkspaceItems[0]; const first = nextWorkspaceItems[0];
return first ? { kind: first.type, id: first.id } : null; return first ? { kind: first.type, id: first.id } : null;
}); });
@@ -1242,7 +1254,12 @@ export default function App() {
if (!isAuthenticated) return; if (!isAuthenticated) return;
const preferredSelection = initialRouteSelectionRef.current; const preferredSelection = initialRouteSelectionRef.current;
initialRouteSelectionRef.current = null; initialRouteSelectionRef.current = null;
void Promise.all([refreshCollections(preferredSelection ?? undefined), refreshModels(), refreshChatTools(), refreshActiveRuns()]); void Promise.all([
refreshCollections({ preferredSelection: preferredSelection ?? undefined, selectFallback: true }),
refreshModels(),
refreshChatTools(),
refreshActiveRuns(),
]);
}, [isAuthenticated]); }, [isAuthenticated]);
useEffect(() => { useEffect(() => {
@@ -1269,8 +1286,8 @@ export default function App() {
} }
setError((current) => (current && isRecoverableStreamDisconnect(new Error(current)) ? null : current)); setError((current) => (current && isRecoverableStreamDisconnect(new Error(current)) ? null : current));
void refreshActiveRuns(); void refreshActiveRuns();
void refreshCollections(selectedItemRef.current ?? undefined, false);
const currentSelection = selectedItemRef.current; const currentSelection = selectedItemRef.current;
void refreshCollections({ reportTransientError: false });
if (currentSelection?.kind === "chat") { if (currentSelection?.kind === "chat") {
void refreshChat(currentSelection.id); void refreshChat(currentSelection.id);
} else if (currentSelection?.kind === "search") { } else if (currentSelection?.kind === "search") {
@@ -1385,10 +1402,18 @@ export default function App() {
const selectedKey = selectedItem ? `${selectedItem.kind}:${selectedItem.id}` : null; const selectedKey = selectedItem ? `${selectedItem.kind}:${selectedItem.id}` : null;
const transcriptViewKey = draftKind ? `draft:${draftKind}` : selectedKey ?? "empty"; const transcriptViewKey = draftKind ? `draft:${draftKind}` : selectedKey ?? "empty";
const selectedChatPendingState = selectedItem?.kind === "chat" ? pendingChatStates[selectedItem.id] ?? null : null; const selectedChatPendingState =
const selectedSearchRunState = selectedItem?.kind === "search" ? runningSearchStates[selectedItem.id] ?? null : null; draftKind === null && selectedItem?.kind === "chat" ? pendingChatStates[selectedItem.id] ?? null : null;
const selectedChatIsActive = selectedItem?.kind === "chat" && (!!selectedChatPendingState || !!activeRuns.chats[selectedItem.id]); const selectedSearchRunState =
const selectedSearchIsActive = selectedItem?.kind === "search" && (!!selectedSearchRunState || !!activeRuns.searches[selectedItem.id]); draftKind === null && selectedItem?.kind === "search" ? runningSearchStates[selectedItem.id] ?? null : null;
const selectedChatIsActive =
draftKind === null &&
selectedItem?.kind === "chat" &&
(!!selectedChatPendingState || !!activeRuns.chats[selectedItem.id]);
const selectedSearchIsActive =
draftKind === null &&
selectedItem?.kind === "search" &&
(!!selectedSearchRunState || !!activeRuns.searches[selectedItem.id]);
const isSearchMode = draftKind ? draftKind === "search" : selectedItem?.kind === "search"; const isSearchMode = draftKind ? draftKind === "search" : selectedItem?.kind === "search";
const isSearchRunning = !!selectedSearchIsActive; const isSearchRunning = !!selectedSearchIsActive;
const isSendingActiveChat = draftKind !== "search" && !!selectedChatIsActive; const isSendingActiveChat = draftKind !== "search" && !!selectedChatIsActive;
@@ -1495,7 +1520,10 @@ export default function App() {
}; };
}, []); }, []);
const messages = selectedChat?.messages ?? []; const messages =
draftKind === null && selectedItem?.kind === "chat" && selectedChat?.id === selectedItem.id
? selectedChat.messages
: [];
useEffect(() => { useEffect(() => {
if (isSearchMode && pendingAttachments.length) { if (isSearchMode && pendingAttachments.length) {
@@ -1602,6 +1630,7 @@ export default function App() {
setError(null); setError(null);
setContextMenu(null); setContextMenu(null);
setDraftKind("chat"); setDraftKind("chat");
selectedItemRef.current = null;
setSelectedItem(null); setSelectedItem(null);
setSelectedChat(null); setSelectedChat(null);
setSelectedSearch(null); setSelectedSearch(null);
@@ -1738,6 +1767,7 @@ export default function App() {
setError(null); setError(null);
setContextMenu(null); setContextMenu(null);
setDraftKind("search"); setDraftKind("search");
selectedItemRef.current = null;
setSelectedItem(null); setSelectedItem(null);
setSelectedChat(null); setSelectedChat(null);
setSelectedSearch(null); setSelectedSearch(null);
@@ -2065,7 +2095,7 @@ export default function App() {
} else { } else {
await deleteSearch(target.id); await deleteSearch(target.id);
} }
await refreshCollections(); await refreshCollections({ selectFallback: true });
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);
if (message.includes("bearer token")) { if (message.includes("bearer token")) {
@@ -2450,7 +2480,7 @@ export default function App() {
backgroundSuspendedChatStreamsRef.current.delete(chatId); backgroundSuspendedChatStreamsRef.current.delete(chatId);
const persistedChat = await retryAfterAppResume(() => getChat(chatId)); const persistedChat = await retryAfterAppResume(() => getChat(chatId));
await refreshCollections(target, false); await refreshCollections({ preferredSelection: target, reportTransientError: false });
if (isCurrentSelection(target)) { if (isCurrentSelection(target)) {
setSelectedChat(persistedChat); setSelectedChat(persistedChat);
setSelectedSearch(null); setSelectedSearch(null);
@@ -2664,7 +2694,7 @@ export default function App() {
} }
} }
await refreshCollections(target); await refreshCollections({ preferredSelection: target });
return target; return target;
}; };
@@ -2783,7 +2813,7 @@ export default function App() {
setSelectedChat(persistedChat); setSelectedChat(persistedChat);
setSelectedSearch(null); setSelectedSearch(null);
} }
void refreshCollections(target); void refreshCollections({ preferredSelection: target });
return; return;
} catch (resumeError) { } catch (resumeError) {
err = resumeError; err = resumeError;
@@ -2889,7 +2919,7 @@ export default function App() {
{ signal: abortController.signal } { signal: abortController.signal }
); );
await refreshCollections(target); await refreshCollections({ preferredSelection: target });
if (isCurrentSelection(target)) { if (isCurrentSelection(target)) {
await refreshSearch(searchId); await refreshSearch(searchId);
} }
@@ -2957,7 +2987,7 @@ export default function App() {
messages: [], messages: [],
}); });
setSelectedSearch(null); setSelectedSearch(null);
await refreshCollections({ kind: "chat", id: chat.id }); await refreshCollections({ preferredSelection: { kind: "chat", id: chat.id } });
await refreshChat(chat.id); await refreshChat(chat.id);
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);
@@ -3119,7 +3149,7 @@ export default function App() {
messages: [], messages: [],
}); });
setSelectedSearch(null); setSelectedSearch(null);
await refreshCollections({ kind: "chat", id: chat.id }); await refreshCollections({ preferredSelection: { kind: "chat", id: chat.id } });
await refreshChat(chat.id); await refreshChat(chat.id);
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);