Studio: bulk export and import in Settings Chat Data, MCP pill off switch (#6141)

* Studio: bulk export and import in Settings Chat Data, MCP pill off switch

- Settings -> Chat -> Data gains Export Recents and Projects (Recents or
  Recents + Projects, Raw JSONL / CSV / ShareGPT, combined or per chat)
  and Import chats, reusing the sidebar Recents menu actions
- Extract bulkExportConversationsByScope so the sidebar and Settings share
  one implementation; expose the export and import helpers via the chat
  feature index
- MCP composer pill icon now swaps to an X on hover like Search, Code and
  RAG; clicking it turns MCP off without opening the server menu
- en and zh-CN locale strings added (parity check passes)

* Reveal the pill X on hover for off-switch icons regardless of active look

The X was gated on data-active, so an MCP pill with no servers enabled
(or a RAG pill without a model) closed on icon click but never showed
the affordance. Off-switch pills only render while their feature is on,
so hover now always reveals the X.

* Replace the Recents hamburger menu with an Export all chats link to Settings

Bulk export and import now live in Settings -> Chat -> Data, so the
sidebar Recents header menu is gone. Each chat's Export submenu gains
Export all chats, which opens Settings on the Chat tab.
This commit is contained in:
Daniel Han 2026-06-10 05:11:15 -07:00 committed by GitHub
commit dfb49fd2cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 233 additions and 115 deletions

View file

@ -51,7 +51,6 @@ import {
Delete02Icon,
Download01Icon,
DownloadSquare01Icon,
Upload01Icon,
Edit03Icon,
FolderAddIcon,
FolderExportIcon,
@ -72,11 +71,6 @@ import {
exportConversationRawJsonl,
exportConversationCsv,
exportConversationShareGPT,
exportBulkConversationsMerged,
exportBulkConversationsSeparate,
importConversationsFromFile,
EXPORT_FORMATS_LIST,
type ConvExportFormat,
} from "@/features/chat/prompt-storage/prompt-storage-dialog";
import { listStoredChatThreads } from "@/features/chat/utils/chat-history-storage";
import {
@ -259,40 +253,6 @@ export function AppSidebar() {
const isStudioRoute = pathname === "/studio" || pathname.startsWith("/studio/");
const [chatOpen, setChatOpen] = useState(true);
const recentsImportInputRef = useRef<HTMLInputElement>(null);
async function handleImportToRecents(file: File) {
try {
const count = await importConversationsFromFile(file, null);
if (count === 0) {
toast.info("No conversations found in file.");
} else {
toast.success(`Imported ${count} conversation${count === 1 ? "" : "s"} to Recents.`);
}
} catch {
toast.error("Import failed.");
}
}
async function handleBulkExport(scope: "recents" | "all", fmt: ConvExportFormat, merged: boolean) {
try {
const threads = await listStoredChatThreads({
includeArchived: false,
...(scope === "recents" ? { projectId: null } : {}),
});
const ids = [...new Set(threads.map((t) => t.id))];
if (ids.length === 0) { toast.info("No conversations to export."); return; }
const ts = new Date().toISOString().slice(0, 10);
const basename = scope === "all" ? `all-chats-${ts}` : `recents-${ts}`;
if (merged) {
await exportBulkConversationsMerged(ids, fmt, basename);
} else {
await exportBulkConversationsSeparate(ids, fmt, basename);
}
} catch {
toast.error("Export failed.");
}
}
const [trainOpen, setTrainOpen] = useState(true);
const [runsOpen, setRunsOpen] = useState(true);
@ -719,6 +679,15 @@ export function AppSidebar() {
{label}
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
{/* Bulk export and import live in Settings -> Chat -> Data. */}
<DropdownMenuItem
onSelect={() =>
useSettingsDialogStore.getState().openDialog("chat")
}
>
Export all chats
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem
@ -736,18 +705,6 @@ export function AppSidebar() {
return (
<>
{/* Hidden file inputs for chat import */}
<input
ref={recentsImportInputRef}
type="file"
accept=".jsonl,.ndjson,.csv"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) void handleImportToRecents(file);
e.target.value = "";
}}
/>
<Sidebar
collapsible="icon"
variant="sidebar"
@ -966,62 +923,6 @@ export function AppSidebar() {
{t("shell.navigation.recents")}
<ChevronDown className="size-3.5 opacity-0 transition-[transform,opacity] duration-200 group-hover/sb-collap:opacity-100 group-focus-visible/sb-collap:opacity-100 data-[state=open]:rotate-0 [[data-state=closed]_&]:rotate-[-90deg] [[data-state=closed]_&]:opacity-100" />
</CollapsibleTrigger>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="ml-auto flex items-center justify-center rounded-sm p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-0"
title="Export recents"
>
<MoreHorizontalIcon className="size-3.5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="start" className="w-56">
<DropdownMenuItem onSelect={() => recentsImportInputRef.current?.click()}>
<HugeiconsIcon icon={Upload01Icon} strokeWidth={1.75} className="size-icon mr-1" />
Import chats
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<HugeiconsIcon icon={Download01Icon} strokeWidth={1.75} className="size-icon mr-1" />
Export Recents
</DropdownMenuSubTrigger>
<DropdownMenuSubContent avoidCollisions={false} className="unsloth-plus-menu w-52">
{EXPORT_FORMATS_LIST.map(({ fmt, label }) => (
<DropdownMenuItem key={`r-m-${fmt}`} onSelect={() => void handleBulkExport("recents", fmt, true)}>
{label} combined
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
{EXPORT_FORMATS_LIST.map(({ fmt, label }) => (
<DropdownMenuItem key={`r-s-${fmt}`} onSelect={() => void handleBulkExport("recents", fmt, false)}>
{label} per chat
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<HugeiconsIcon icon={Download01Icon} strokeWidth={1.75} className="size-icon mr-1" />
Export Recents + Projects
</DropdownMenuSubTrigger>
<DropdownMenuSubContent avoidCollisions={false} className="unsloth-plus-menu w-52">
{EXPORT_FORMATS_LIST.map(({ fmt, label }) => (
<DropdownMenuItem key={`a-m-${fmt}`} onSelect={() => void handleBulkExport("all", fmt, true)}>
{label} combined
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
{EXPORT_FORMATS_LIST.map(({ fmt, label }) => (
<DropdownMenuItem key={`a-s-${fmt}`} onSelect={() => void handleBulkExport("all", fmt, false)}>
{label} per chat
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
</div>
</SidebarGroupLabel>
<CollapsibleContent>