From eca040716359d13e3979b6e835e4bb2f4df97812 Mon Sep 17 00:00:00 2001 From: Nilay Yadav Date: Fri, 14 Aug 2026 03:12:49 +0530 Subject: [PATCH 1/2] Studio: add Manage chats bulk actions in Settings > Data --- .../chat/hooks/use-chat-sidebar-items.ts | 62 ++- studio/frontend/src/features/chat/index.ts | 5 + .../chat/stores/pinned-chats-store.ts | 12 + .../settings/components/manage-chats-view.tsx | 426 ++++++++++++++++++ .../src/features/settings/tabs/data-tab.tsx | 49 +- studio/frontend/src/i18n/locales/ar.ts | 3 + studio/frontend/src/i18n/locales/de.ts | 3 + studio/frontend/src/i18n/locales/en.ts | 3 + studio/frontend/src/i18n/locales/es.ts | 3 + studio/frontend/src/i18n/locales/fr.ts | 3 + studio/frontend/src/i18n/locales/hi.ts | 3 + studio/frontend/src/i18n/locales/it.ts | 3 + studio/frontend/src/i18n/locales/ja.ts | 3 + studio/frontend/src/i18n/locales/ko.ts | 3 + studio/frontend/src/i18n/locales/pt-br.ts | 3 + studio/frontend/src/i18n/locales/ru.ts | 3 + studio/frontend/src/i18n/locales/zh-CN.ts | 2 + 17 files changed, 567 insertions(+), 22 deletions(-) create mode 100644 studio/frontend/src/features/settings/components/manage-chats-view.tsx diff --git a/studio/frontend/src/features/chat/hooks/use-chat-sidebar-items.ts b/studio/frontend/src/features/chat/hooks/use-chat-sidebar-items.ts index 398b3b771..f59ff9be7 100644 --- a/studio/frontend/src/features/chat/hooks/use-chat-sidebar-items.ts +++ b/studio/frontend/src/features/chat/hooks/use-chat-sidebar-items.ts @@ -200,20 +200,30 @@ export async function renameChatItem( ); } -export async function archiveChatItem( - item: SidebarItem, +async function collectItemThreadIds( + items: SidebarItem[], + args: { includeArchived?: boolean } = {}, +): Promise { + const ids = new Set(); + for (const item of items) { + if (item.type === "single") { + ids.add(item.id); + continue; + } + const pair = await listStoredChatThreads({ pairId: item.id, ...args }); + for (const thread of pair) ids.add(thread.id); + } + return Array.from(ids); +} + +export async function archiveChatItems( + items: SidebarItem[], activeId: string | undefined, onSelect: (view: { mode: "single"; newThreadNonce: string }) => void, ): Promise { - const threadIds: string[] = - item.type === "single" - ? [item.id] - : ( - await listStoredChatThreads({ - pairId: item.id, - includeArchived: true, - }) - ).map((t) => t.id); + const threadIds = await collectItemThreadIds(items, { + includeArchived: true, + }); requestPromptQueueStop(threadIds); @@ -225,7 +235,7 @@ export async function archiveChatItem( threadIds.map((id) => updateStoredChatThread(id, { archived: true })), ); - if (activeId === item.id) { + if (activeId !== undefined && items.some((item) => item.id === activeId)) { useChatRuntimeStore.getState().setActiveThreadId(null); onSelect({ mode: "single", newThreadNonce: crypto.randomUUID() }); } @@ -233,6 +243,14 @@ export async function archiveChatItem( notifyChatHistoryUpdated(); } +export async function archiveChatItem( + item: SidebarItem, + activeId: string | undefined, + onSelect: (view: { mode: "single"; newThreadNonce: string }) => void, +): Promise { + return archiveChatItems([item], activeId, onSelect); +} + export async function archiveAllChatItems( activeId?: string, onSelect?: (view: { mode: "single"; newThreadNonce: string }) => void, @@ -286,16 +304,13 @@ export async function unarchiveChatItem(item: SidebarItem): Promise { notifyChatHistoryUpdated(); } -export async function deleteChatItem( - item: SidebarItem, +export async function deleteChatItems( + items: SidebarItem[], activeId: string | undefined, onSelect: (view: { mode: "single"; newThreadNonce: string }) => void, args: { deleteFiles?: boolean } = {}, ) { - const threadIds: string[] = - item.type === "single" - ? [item.id] - : (await listStoredChatThreads({ pairId: item.id })).map((t) => t.id); + const threadIds = await collectItemThreadIds(items); // Stop queued prompts and in-flight streams before deleting. requestPromptQueueStop(threadIds); @@ -315,7 +330,7 @@ export async function deleteChatItem( markChatThreadsDeleted(threadIds); notifyChatHistoryUpdated(); - if (activeId === item.id) { + if (activeId !== undefined && items.some((item) => item.id === activeId)) { useChatRuntimeStore.getState().setActiveThreadId(null); onSelect({ mode: "single", newThreadNonce: crypto.randomUUID() }); } @@ -332,3 +347,12 @@ export async function deleteChatItem( throw error; } } + +export async function deleteChatItem( + item: SidebarItem, + activeId: string | undefined, + onSelect: (view: { mode: "single"; newThreadNonce: string }) => void, + args: { deleteFiles?: boolean } = {}, +) { + return deleteChatItems([item], activeId, onSelect, args); +} diff --git a/studio/frontend/src/features/chat/index.ts b/studio/frontend/src/features/chat/index.ts index a46860c1c..f71b38eab 100644 --- a/studio/frontend/src/features/chat/index.ts +++ b/studio/frontend/src/features/chat/index.ts @@ -184,14 +184,19 @@ export { EXPORT_FORMATS_LIST, buildFineTuneJsonl, bulkExportConversationsByScope, + exportBulkConversationsMerged, + exportBulkConversationsSeparate, exportFineTuneJsonl, importConversationsFromFile, + type ConvExportFormat, type FineTuneFormat, } from "./prompt-storage/prompt-storage-dialog"; export { archiveAllChatItems, archiveChatItem, + archiveChatItems, deleteChatItem, + deleteChatItems, renameChatItem, unarchiveChatItem, useChatSidebarItems, diff --git a/studio/frontend/src/features/chat/stores/pinned-chats-store.ts b/studio/frontend/src/features/chat/stores/pinned-chats-store.ts index 0abb6cef4..294d30f45 100644 --- a/studio/frontend/src/features/chat/stores/pinned-chats-store.ts +++ b/studio/frontend/src/features/chat/stores/pinned-chats-store.ts @@ -11,6 +11,7 @@ export interface PinnedChatsState { pinnedIds: string[]; togglePin: (id: string) => void; unpin: (id: string) => void; + setManyPinned: (ids: string[], pinned: boolean) => void; } export const usePinnedChatsStore = create()( @@ -27,6 +28,17 @@ export const usePinnedChatsStore = create()( set((state) => ({ pinnedIds: state.pinnedIds.filter((x) => x !== id), })), + setManyPinned: (ids, pinned) => + set((state) => { + if (!pinned) { + const drop = new Set(ids); + return { + pinnedIds: state.pinnedIds.filter((x) => !drop.has(x)), + }; + } + const additions = ids.filter((id) => !state.pinnedIds.includes(id)); + return { pinnedIds: [...additions, ...state.pinnedIds] }; + }), }), { name: "unsloth_pinned_chats", diff --git a/studio/frontend/src/features/settings/components/manage-chats-view.tsx b/studio/frontend/src/features/settings/components/manage-chats-view.tsx new file mode 100644 index 000000000..555ad50dc --- /dev/null +++ b/studio/frontend/src/features/settings/components/manage-chats-view.tsx @@ -0,0 +1,426 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + type ConvExportFormat, + EXPORT_FORMATS_LIST, + type SidebarItem, + archiveChatItems, + deleteChatItems, + exportBulkConversationsMerged, + exportBulkConversationsSeparate, + moveChatItemToProject, + useChatProjects, + useChatRuntimeStore, + useChatSidebarItems, + usePinnedChatsStore, +} from "@/features/chat"; +import { isDownloadCancelled } from "@/lib/native-files"; +import { toast } from "@/lib/toast"; +import { + Archive02Icon, + Delete02Icon, + Download01Icon, + Folder01Icon, + PinIcon, +} from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { useNavigate, useRouterState } from "@tanstack/react-router"; +import { useRef, useState } from "react"; +import { useSettingsDialogStore } from "../stores/settings-dialog-store"; + +const MANAGE_PAGE_SIZE = 20; + +function formatCreatedAt(ms: number): string { + return new Date(ms).toLocaleDateString(undefined, { + year: "numeric", + month: "long", + day: "numeric", + }); +} + +function chatCount(n: number): string { + return n === 1 ? "1 chat" : `${n} chats`; +} + +export function ManageChatsView() { + const { items } = useChatSidebarItems({ requireMessages: false }); + const { projects } = useChatProjects(); + const navigate = useNavigate(); + const closeSettings = useSettingsDialogStore((s) => s.closeDialog); + const storeThreadId = useChatRuntimeStore((s) => s.activeThreadId); + // Open chat id from the route (single thread or compare pair), mirroring + // ArchivedChatsView: compare panes only live in the search params. + const openChatId = useRouterState({ + select: (s) => { + if (!s.location.pathname.startsWith("/chat")) return undefined; + const search = s.location.search as Record; + return search.thread ?? search.compare ?? storeThreadId ?? undefined; + }, + }); + const pinnedIds = usePinnedChatsStore((s) => s.pinnedIds); + const setManyPinned = usePinnedChatsStore((s) => s.setManyPinned); + + const [selectedIds, setSelectedIds] = useState>(new Set()); + const [visibleCount, setVisibleCount] = useState(MANAGE_PAGE_SIZE); + const [confirmingDelete, setConfirmingDelete] = useState(false); + const [busy, setBusy] = useState(false); + const lastToggledIndex = useRef(null); + + const visible = items.slice(0, visibleCount); + const selectedItems = items.filter((item) => selectedIds.has(item.id)); + const selectedCount = selectedItems.length; + const allVisibleSelected = + visible.length > 0 && visible.every((item) => selectedIds.has(item.id)); + const allSelectedPinned = + selectedCount > 0 && + selectedItems.every((item) => pinnedIds.includes(item.id)); + const projectNames = new Map(projects.map((p) => [p.id, p.name])); + + function toggleRow(index: number, shiftKey: boolean) { + const target = !selectedIds.has(visible[index].id); + const last = lastToggledIndex.current; + const anchor = + shiftKey && last !== null && last < visible.length ? last : index; + const [from, to] = [Math.min(anchor, index), Math.max(anchor, index)]; + setSelectedIds((prev) => { + const next = new Set(prev); + for (let i = from; i <= to; i++) { + if (target) next.add(visible[i].id); + else next.delete(visible[i].id); + } + return next; + }); + lastToggledIndex.current = index; + } + + function toggleAllVisible() { + setSelectedIds( + allVisibleSelected ? new Set() : new Set(visible.map((item) => item.id)), + ); + lastToggledIndex.current = null; + } + + function openChat(item: SidebarItem) { + navigate({ + to: "/chat", + search: + item.type === "single" ? { thread: item.id } : { compare: item.id }, + }); + closeSettings(); + } + + function resetView(view: { newThreadNonce: string }) { + navigate({ to: "/chat", search: { new: view.newThreadNonce } }); + } + + async function run( + action: () => Promise, + success: string, + failure: string, + ) { + setBusy(true); + try { + await action(); + setSelectedIds(new Set()); + toast.success(success); + } catch (err) { + toast.error(failure, { + description: err instanceof Error ? err.message : undefined, + }); + } finally { + setBusy(false); + } + } + + const handleArchive = () => + run( + () => archiveChatItems(selectedItems, openChatId, resetView), + `Archived ${chatCount(selectedCount)}`, + "Failed to archive chats", + ); + + const handleDelete = () => + run( + () => deleteChatItems(selectedItems, openChatId, resetView), + `Deleted ${chatCount(selectedCount)}`, + "Failed to delete chats", + ); + + const handleMove = (projectId: string | null) => + run( + async () => { + await Promise.all( + selectedItems.map((item) => moveChatItemToProject(item, projectId)), + ); + }, + projectId + ? `Moved ${chatCount(selectedCount)} to ${projectNames.get(projectId) ?? "project"}` + : `Moved ${chatCount(selectedCount)} to Recents`, + "Failed to move chats", + ); + + function handleTogglePin() { + const ids = selectedItems.map((item) => item.id); + setManyPinned(ids, !allSelectedPinned); + toast.success( + `${allSelectedPinned ? "Unpinned" : "Pinned"} ${chatCount(ids.length)}`, + ); + setSelectedIds(new Set()); + } + + async function handleExport(format: ConvExportFormat, merged: boolean) { + const threadIds = selectedItems.flatMap( + (item) => item.threadIds ?? [item.id], + ); + const basename = `selected-chats-${new Date().toISOString().slice(0, 10)}`; + try { + if (merged) { + await exportBulkConversationsMerged(threadIds, format, basename); + } else { + await exportBulkConversationsSeparate(threadIds, format, basename); + } + } catch (error) { + if (!isDownloadCancelled(error)) toast.error("Export failed."); + } + } + + if (items.length === 0) { + return ( +

+ No chats. +

+ ); + } + + const actionsDisabled = busy || selectedCount === 0; + + return ( +
+
+
+ 0 + ? "indeterminate" + : false + } + onCheckedChange={toggleAllVisible} + aria-label="Select all chats" + title="Select all" + /> + + {selectedCount > 0 + ? `${chatCount(selectedCount)} selected` + : chatCount(items.length)} + +
+ + + + + + !item.projectId)} + onSelect={() => void handleMove(null)} + > + Recents + + {projects.map((project) => ( + void handleMove(project.id)} + > + + {project.name} + + ))} + + + + + + + + + + {EXPORT_FORMATS_LIST.map(({ fmt, label }) => ( + void handleExport(fmt, true)} + > + {label} (combined) + + ))} + + {EXPORT_FORMATS_LIST.map(({ fmt, label }) => ( + void handleExport(fmt, false)} + > + {label} (per chat) + + ))} + + + +
+ +
+
+ + Name + Project + Date created +
+ {visible.map((item, index) => ( +
+ toggleRow(index, e.shiftKey)} + aria-label={`Select "${item.title}"`} + /> + + + {item.projectId ? (projectNames.get(item.projectId) ?? "") : ""} + + + {formatCreatedAt(item.createdAt)} + +
+ ))} + {items.length > visibleCount ? ( +
+ +
+ ) : null} +
+ + { + if (!o) setConfirmingDelete(false); + }} + > + + + + Delete {chatCount(selectedCount)} + + + Delete the{" "} + {selectedCount === 1 + ? "selected chat" + : `${selectedCount} selected chats`} + ? This cannot be undone. + + + + Cancel + { + setConfirmingDelete(false); + void handleDelete(); + }} + > + Delete + + + + +
+ ); +} diff --git a/studio/frontend/src/features/settings/tabs/data-tab.tsx b/studio/frontend/src/features/settings/tabs/data-tab.tsx index 2f8cb4146..d5302c33c 100644 --- a/studio/frontend/src/features/settings/tabs/data-tab.tsx +++ b/studio/frontend/src/features/settings/tabs/data-tab.tsx @@ -65,6 +65,7 @@ import { HugeiconsIcon } from "@hugeicons/react"; import { useNavigate, useRouterState } from "@tanstack/react-router"; import { useEffect, useRef, useState } from "react"; import { ArchivedChatsView } from "../components/archived-chats-dialog"; +import { ManageChatsView } from "../components/manage-chats-view"; import { createFineTuneRecipeFromChats, loadFineTuneDatasetInTrainTab, @@ -93,9 +94,9 @@ export function DataTab() { const [confirmOpen, setConfirmOpen] = useState(false); const [archiveConfirmOpen, setArchiveConfirmOpen] = useState(false); // Subpages swap the Data tab body instead of opening nested dialogs. - const [subpage, setSubpage] = useState<"main" | "archived" | "files">( - archivedChatsRequested ? "archived" : "main", - ); + const [subpage, setSubpage] = useState< + "main" | "manage" | "archived" | "files" + >(archivedChatsRequested ? "archived" : "main"); const [count, setCount] = useState(null); const [exporting, setExporting] = useState(false); const [archivedExporting, setArchivedExporting] = useState(false); @@ -407,6 +408,35 @@ export function DataTab() { } }; + if (subpage === "manage") { + return ( +
+
+ +

+ {t("settings.data.title")} +

+
+
+

+ {t("settings.data.manageChats")} +

+

+ {t("settings.data.manageChatsDescription")} +

+
+ +
+ ); + } + if (subpage === "archived") { return (
@@ -563,6 +593,19 @@ export function DataTab() {
+ + + + Date: Fri, 14 Aug 2026 19:26:45 +0530 Subject: [PATCH 2/2] Add staging CI workflows for unslothai/unsloth#8745 --- .github/workflows/staging-8745-macos-14.yml | 48 +++++++++++++++++++ .github/workflows/staging-8745-playwright.yml | 40 ++++++++++++++++ .../workflows/staging-8745-ubuntu-latest.yml | 48 +++++++++++++++++++ .../workflows/staging-8745-windows-latest.yml | 48 +++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 .github/workflows/staging-8745-macos-14.yml create mode 100644 .github/workflows/staging-8745-playwright.yml create mode 100644 .github/workflows/staging-8745-ubuntu-latest.yml create mode 100644 .github/workflows/staging-8745-windows-latest.yml diff --git a/.github/workflows/staging-8745-macos-14.yml b/.github/workflows/staging-8745-macos-14.yml new file mode 100644 index 000000000..19deeabf9 --- /dev/null +++ b/.github/workflows/staging-8745-macos-14.yml @@ -0,0 +1,48 @@ +name: "staging-8745 macos-14" +on: + push: + branches: ["pr-8745-xplat-ci"] + paths: + - 'studio/frontend/src/features/chat/hooks/use-chat-sidebar-items.ts' + - 'studio/frontend/src/features/chat/index.ts' + - 'studio/frontend/src/features/chat/stores/pinned-chats-store.ts' + - 'studio/frontend/src/features/settings/components/manage-chats-view.tsx' + - 'studio/frontend/src/features/settings/tabs/data-tab.tsx' + - 'studio/frontend/src/i18n/locales/ar.ts' + - 'studio/frontend/src/i18n/locales/de.ts' + - 'studio/frontend/src/i18n/locales/en.ts' + - 'studio/frontend/src/i18n/locales/es.ts' + - 'studio/frontend/src/i18n/locales/fr.ts' + - 'studio/frontend/src/i18n/locales/hi.ts' + - 'studio/frontend/src/i18n/locales/it.ts' + - 'studio/frontend/src/i18n/locales/ja.ts' + - 'studio/frontend/src/i18n/locales/ko.ts' + - 'studio/frontend/src/i18n/locales/pt-br.ts' + - 'studio/frontend/src/i18n/locales/ru.ts' + - 'studio/frontend/src/i18n/locales/zh-CN.ts' + - '.github/workflows/staging-8745-macos-14.yml' + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +permissions: + contents: read +defaults: + run: + shell: bash +jobs: + test: + runs-on: macos-14 + timeout-minutes: 30 + env: + UNSLOTH_COMPILE_DISABLE: '1' + UNSLOTH_IS_PRESENT: '1' + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: python -m pip install -r studio/backend/requirements/studio.txt pytest pytest-asyncio pytest-timeout python-multipart + - run: PYTHONPATH=studio/backend python -m pytest studio/backend/tests -q || true diff --git a/.github/workflows/staging-8745-playwright.yml b/.github/workflows/staging-8745-playwright.yml new file mode 100644 index 000000000..67bc702eb --- /dev/null +++ b/.github/workflows/staging-8745-playwright.yml @@ -0,0 +1,40 @@ +name: "staging-8745 studio-playwright" +on: + push: + branches: ["pr-8745-xplat-ci"] + paths: + - 'studio/**' + - '.github/workflows/staging-8745-playwright.yml' + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +permissions: + contents: read +jobs: + studio-live: + runs-on: ubuntu-latest + timeout-minutes: 40 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install playwright httpx pillow && python -m playwright install --with-deps chromium + - name: Install + launch Studio + run: | + ./install.sh --local --no-torch || pip install -e . + nohup unsloth studio -H 127.0.0.1 -p 8888 > studio.log 2>&1 & + for i in $(seq 1 60); do curl -sf http://127.0.0.1:8888/healthz && break; sleep 5; done + - name: Playwright smoke (studio_test_kit) + run: PYTHONPATH=.github/scripts python -m studio_test_kit._smoke_ui || true + - uses: actions/upload-artifact@v4 + if: always() + with: + name: studio-playwright-8745 + path: | + **/*.png + **/*.webm + studio.log diff --git a/.github/workflows/staging-8745-ubuntu-latest.yml b/.github/workflows/staging-8745-ubuntu-latest.yml new file mode 100644 index 000000000..0ad4727ba --- /dev/null +++ b/.github/workflows/staging-8745-ubuntu-latest.yml @@ -0,0 +1,48 @@ +name: "staging-8745 ubuntu-latest" +on: + push: + branches: ["pr-8745-xplat-ci"] + paths: + - 'studio/frontend/src/features/chat/hooks/use-chat-sidebar-items.ts' + - 'studio/frontend/src/features/chat/index.ts' + - 'studio/frontend/src/features/chat/stores/pinned-chats-store.ts' + - 'studio/frontend/src/features/settings/components/manage-chats-view.tsx' + - 'studio/frontend/src/features/settings/tabs/data-tab.tsx' + - 'studio/frontend/src/i18n/locales/ar.ts' + - 'studio/frontend/src/i18n/locales/de.ts' + - 'studio/frontend/src/i18n/locales/en.ts' + - 'studio/frontend/src/i18n/locales/es.ts' + - 'studio/frontend/src/i18n/locales/fr.ts' + - 'studio/frontend/src/i18n/locales/hi.ts' + - 'studio/frontend/src/i18n/locales/it.ts' + - 'studio/frontend/src/i18n/locales/ja.ts' + - 'studio/frontend/src/i18n/locales/ko.ts' + - 'studio/frontend/src/i18n/locales/pt-br.ts' + - 'studio/frontend/src/i18n/locales/ru.ts' + - 'studio/frontend/src/i18n/locales/zh-CN.ts' + - '.github/workflows/staging-8745-ubuntu-latest.yml' + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +permissions: + contents: read +defaults: + run: + shell: bash +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + UNSLOTH_COMPILE_DISABLE: '1' + UNSLOTH_IS_PRESENT: '1' + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: python -m pip install -r studio/backend/requirements/studio.txt pytest pytest-asyncio pytest-timeout python-multipart + - run: PYTHONPATH=studio/backend python -m pytest studio/backend/tests -q || true diff --git a/.github/workflows/staging-8745-windows-latest.yml b/.github/workflows/staging-8745-windows-latest.yml new file mode 100644 index 000000000..2e162eaf9 --- /dev/null +++ b/.github/workflows/staging-8745-windows-latest.yml @@ -0,0 +1,48 @@ +name: "staging-8745 windows-latest" +on: + push: + branches: ["pr-8745-xplat-ci"] + paths: + - 'studio/frontend/src/features/chat/hooks/use-chat-sidebar-items.ts' + - 'studio/frontend/src/features/chat/index.ts' + - 'studio/frontend/src/features/chat/stores/pinned-chats-store.ts' + - 'studio/frontend/src/features/settings/components/manage-chats-view.tsx' + - 'studio/frontend/src/features/settings/tabs/data-tab.tsx' + - 'studio/frontend/src/i18n/locales/ar.ts' + - 'studio/frontend/src/i18n/locales/de.ts' + - 'studio/frontend/src/i18n/locales/en.ts' + - 'studio/frontend/src/i18n/locales/es.ts' + - 'studio/frontend/src/i18n/locales/fr.ts' + - 'studio/frontend/src/i18n/locales/hi.ts' + - 'studio/frontend/src/i18n/locales/it.ts' + - 'studio/frontend/src/i18n/locales/ja.ts' + - 'studio/frontend/src/i18n/locales/ko.ts' + - 'studio/frontend/src/i18n/locales/pt-br.ts' + - 'studio/frontend/src/i18n/locales/ru.ts' + - 'studio/frontend/src/i18n/locales/zh-CN.ts' + - '.github/workflows/staging-8745-windows-latest.yml' + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +permissions: + contents: read +defaults: + run: + shell: bash +jobs: + test: + runs-on: windows-latest + timeout-minutes: 30 + env: + UNSLOTH_COMPILE_DISABLE: '1' + UNSLOTH_IS_PRESENT: '1' + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: python -m pip install -r studio/backend/requirements/studio.txt pytest pytest-asyncio pytest-timeout python-multipart + - run: PYTHONPATH=studio/backend python -m pytest studio/backend/tests -q || true