Studio: new-chat shortcut, composer draft autosave, archive threads (#5771)

* new-chat shortcut, composer draft autosave, archive threads

* fix

* Studio: harden chat UX additions for legacy threads and unavailable storage

Two robustness fixes on top of the new chat UX features:

- groupThreads: coerce archived to a boolean before comparing
  (Boolean(t.archived) !== archived). Threads from the older browser-only
  Studio, or any record predating the archived field, can carry
  archived === undefined/null. The raw `!== archived` comparison dropped
  those from BOTH the Recents and Archived sidebar groups, hiding existing
  chats. Treat missing as not-archived so legacy chats still appear in
  Recents.

- composer draft autosave: wrap the localStorage read and write in
  try/catch. When storage is unavailable (private mode, disabled cookies,
  blocked storage) or full (quota exceeded), getItem/setItem throw; the
  throw in the restore effect would surface to React and break the chat
  page. Draft persistence is best-effort, so degrade quietly.

Verified with bun unit tests on the real groupThreads (legacy undefined no
longer vanishes) and Playwright across chromium, firefox and webkit (draft
save/restore/isolation/clear, new-chat shortcut + crypto.randomUUID, and
localStorage blocked/quota throw handling). tsc clean; no new eslint findings.

* Fix composer draft bleed and orphan cleanup for PR #5771

Centralize composer draft storage in a small util and tighten two edge cases:

- New chat draft bleed: every new chat shared the chat-draft:__new__ slot, so
  starting a fresh chat could restore the previous one's half-typed text. Clear
  that slot at every new chat entry point (sidebar buttons and Cmd/Ctrl+Shift+O).

- Orphan drafts: deleting a thread left its chat-draft:<id> key behind. Clear
  the draft for every deleted thread id.

New util utils/composer-draft.ts owns the key format and wraps localStorage in
try/catch (private mode, blocked storage, quota), replacing the inline copy in
thread.tsx so reads and writes stay best effort everywhere.

* address review

* fix: remove unused chat sidebar binding

---------

Co-authored-by: Daniel Han <michaelhan2050@gmail.com>
Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Co-authored-by: imagineer99 <samleejackson0@gmail.com>
This commit is contained in:
Nilay 2026-06-12 18:06:31 +05:30 committed by GitHub
commit 911ceba7fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 289 additions and 13 deletions

View file

@ -6,7 +6,7 @@ import { Navbar } from "@/components/navbar";
import { fetchDeviceType, usePlatformStore } from "@/config/env";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { SettingsDialog, useSettingsDialogStore } from "@/features/settings";
import { useChatRuntimeStore } from "@/features/chat";
import { clearNewChatDraft, useChatRuntimeStore } from "@/features/chat";
import { useTrainingUnloadGuard } from "@/features/training";
import { useSidebarPin } from "@/hooks/use-sidebar-pin";
import { useT, type TranslationKey } from "@/i18n";
@ -15,6 +15,7 @@ import {
createRootRoute,
redirect,
useMatches,
useNavigate,
useRouterState,
} from "@tanstack/react-router";
import { AnimatePresence, motion } from "motion/react";
@ -78,6 +79,7 @@ function RootLayout() {
const hideNavbar = HIDDEN_NAVBAR_ROUTES.includes(pathname);
const isChatRoute = pathname.startsWith("/chat");
const { pinned, setPinned, togglePinned } = useSidebarPin();
const navigate = useNavigate();
useTrainingUnloadGuard();
@ -107,11 +109,24 @@ function RootLayout() {
if ((e.metaKey || e.ctrlKey) && e.key === ",") {
e.preventDefault();
useSettingsDialogStore.getState().openDialog();
return;
}
// Cmd/Ctrl+Shift+O opens a new chat.
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.code === "KeyO") {
e.preventDefault();
clearNewChatDraft(); // fresh chat starts empty, no bleed from the last one
const chatRuntime = useChatRuntimeStore.getState();
chatRuntime.setActiveThreadId(null);
chatRuntime.setActiveProjectId(null);
void navigate({
to: "/chat",
search: { new: crypto.randomUUID() },
});
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []);
}, [navigate]);
useEffect(() => {
if (isChatRoute) return;