From a86e9e2c7e468331258b32c0957f4a2aff8a5521 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Fri, 12 Jun 2026 15:40:50 +0800 Subject: [PATCH 1/2] fix(tabs): restore an active context after a draft-only restart An unsent draft tab is device-local and never persisted, so quitting with only a draft open reopened to zero tabs: the chat area was blank and the New chat button, Mod+T, and context menu were all dead because each was gated on the now-null active folder. Hydration now synthesizes an active draft whenever no tabs are restored, targeting the last-active folder or chat mode via a localStorage hint, without writing any conversation or folder row before the first send. The sidebar New chat button falls back to chat mode instead of disabling when there is no active folder. --- src/components/layout/sidebar.test.tsx | 10 +- src/components/layout/sidebar.tsx | 16 ++- src/contexts/tab-context.test.tsx | 168 +++++++++++++++++++++++++ src/contexts/tab-context.tsx | 72 +++++++++++ src/lib/last-active-context-storage.ts | 59 +++++++++ 5 files changed, 317 insertions(+), 8 deletions(-) create mode 100644 src/lib/last-active-context-storage.ts diff --git a/src/components/layout/sidebar.test.tsx b/src/components/layout/sidebar.test.tsx index 9c2c5f20..de3ddee4 100644 --- a/src/components/layout/sidebar.test.tsx +++ b/src/components/layout/sidebar.test.tsx @@ -9,6 +9,7 @@ import enMessages from "@/i18n/messages/en.json" // factories below (vi.mock is hoisted above imports). const spies = vi.hoisted(() => ({ openNewConversationTab: vi.fn(), + openChatModeTab: vi.fn(), setSearchOpen: vi.fn(), })) const mockState = vi.hoisted(() => ({ @@ -29,6 +30,7 @@ vi.mock("@/contexts/active-folder-context", () => ({ vi.mock("@/contexts/tab-context", () => ({ useTabContext: () => ({ openNewConversationTab: spies.openNewConversationTab, + openChatModeTab: spies.openChatModeTab, }), })) vi.mock("@/contexts/search-dialog-context", () => ({ @@ -53,6 +55,7 @@ function renderSidebar() { describe("Sidebar — fixed New chat / Search region", () => { beforeEach(() => { spies.openNewConversationTab.mockClear() + spies.openChatModeTab.mockClear() spies.setSearchOpen.mockClear() mockState.activeFolder = { id: 7, path: "/x" } }) @@ -77,12 +80,15 @@ describe("Sidebar — fixed New chat / Search region", () => { expect(getByText("Ctrl+K")).toBeTruthy() }) - it("disables New chat when no folder is active", () => { + it("falls back to chat mode (never disabled) when no folder is active", () => { mockState.activeFolder = null const { getByText } = renderSidebar() const btn = getByText("New chat").closest("button") as HTMLButtonElement - expect(btn.disabled).toBe(true) + // Defense-in-depth: the button stays clickable so a workspace that recovered + // to no active folder is never a dead end — it opens folderless chat mode. + expect(btn.disabled).toBe(false) fireEvent.click(btn) + expect(spies.openChatModeTab).toHaveBeenCalled() expect(spies.openNewConversationTab).not.toHaveBeenCalled() }) }) diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index 74165a15..3ee02a6b 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -61,7 +61,7 @@ export function Sidebar() { const t = useTranslations("Folder.sidebar") const { isOpen, toggle } = useSidebarContext() const { activeFolder } = useActiveFolder() - const { openNewConversationTab } = useTabContext() + const { openNewConversationTab, openChatModeTab } = useTabContext() const { setOpen: setSearchOpen } = useSearchDialog() const isMac = useIsMac() const { shortcuts } = useShortcutSettings() @@ -113,9 +113,15 @@ export function Sidebar() { }, [allExpanded]) const handleNewConversation = useCallback(() => { - if (!activeFolder) return + // Defense-in-depth: with no active folder (e.g. a cold start that recovered + // to nothing, or all folders closed) fall back to folderless chat mode + // rather than no-op, so this entry point is never a dead end. + if (!activeFolder) { + openChatModeTab() + return + } openNewConversationTab(activeFolder.id, activeFolder.path) - }, [activeFolder, openNewConversationTab]) + }, [activeFolder, openChatModeTab, openNewConversationTab]) if (!isOpen) return null @@ -201,14 +207,12 @@ export function Sidebar() {