Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8799e04
Studio: UI polish for sidebar, menus, hub and toasts
shimmyshimmer Jun 13, 2026
c41ba23
Studio: shrink pinned unpin icon, match projects menu to composer menu
shimmyshimmer Jun 13, 2026
1918bc0
Studio: inset dropdown separators slightly from item width
shimmyshimmer Jun 13, 2026
09a2f2d
Studio: move archived chats from the sidebar into chat settings
shimmyshimmer Jun 13, 2026
ffcbfc0
Studio: make the archive toast open the archived chats list
shimmyshimmer Jun 13, 2026
70bd32b
Studio: keep chats archived when opened from the archived list
shimmyshimmer Jun 13, 2026
6e223ec
Chat settings: optional delete confirmation and menu cleanup
shimmyshimmer Jun 13, 2026
6cb93dc
Hub, dialog and page polish
shimmyshimmer Jun 13, 2026
a5b2b4b
Soften popup overlays and refine hub filters
shimmyshimmer Jun 13, 2026
4e42d71
Inline chat rename and review-feedback fixes
shimmyshimmer Jun 13, 2026
482a845
Merge branch 'main' into studio-ui-polish
shimmyshimmer Jun 13, 2026
b296bd0
Sidebar: borderless inline rename, no name flash, align titles
shimmyshimmer Jun 13, 2026
0c56d71
Profile: preferred name and avatar shape options
shimmyshimmer Jun 13, 2026
9b38cf2
Settings: show version block at the top of General
shimmyshimmer Jun 13, 2026
faa1640
Profile: pick a sloth sticker as your avatar
shimmyshimmer Jun 13, 2026
0057823
Profile: keep transparent avatars transparent
shimmyshimmer Jun 13, 2026
6e376e1
Profile: fall back to PNG for transparent avatars on Safari
shimmyshimmer Jun 13, 2026
104bbab
Archived chats: reset nav when deleting the open compare too
shimmyshimmer Jun 13, 2026
60b094c
Merge branch 'main' into studio-ui-polish
shimmyshimmer Jun 13, 2026
ef5a547
Profile: trim three sloth stickers from the picker
shimmyshimmer Jun 13, 2026
ab3a5b9
Chat: tighten model selector right padding
shimmyshimmer Jun 13, 2026
8ad34c4
Address review feedback on profile, reset and rename
shimmyshimmer Jun 13, 2026
6963385
Chat: rebalance model selector padding and chevron
shimmyshimmer Jun 13, 2026
1ff6d84
Hub: shrink the external link dialog icon
shimmyshimmer Jun 13, 2026
f81a8cb
Hub: shrink the external link dialog media circle
shimmyshimmer Jun 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
325 changes: 201 additions & 124 deletions studio/frontend/src/components/app-sidebar.tsx

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions studio/frontend/src/components/assistant-ui/model-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import { usePlatformStore } from "@/config/env";
import { isCustomProviderType } from "@/features/chat/external-providers";
import { cn } from "@/lib/utils";
import {
ArrowDown01Icon,
CloudIcon,
DashboardSquare01Icon,
FolderSearchIcon,
RemoveCircleIcon,
Search01Icon,
} from "@hugeicons/core-free-icons";
import { ChevronDownStandardIcon } from "@/lib/chevron-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { type KeyboardEvent, useMemo, useState } from "react";
import { Input } from "../ui/input";
Expand Down Expand Up @@ -151,9 +151,11 @@ function ModelSelectorTrigger({
"rounded-full border border-border/60 hover:bg-[#ececec] dark:hover:bg-[#2d2e32]",
variant === "ghost" && "rounded-full hover:bg-[#ececec] dark:hover:bg-[#2d2e32]",
variant === "muted" && "rounded-full bg-muted hover:bg-muted/80",
size === "sm" && "h-8 px-2.5 text-xs",
size === "default" && "h-9 px-3 text-sm",
size === "lg" && "h-10 px-3.5 text-sm",
// More left padding than right; the chevron is pulled close to the
// label (below) so the trigger reads balanced around the text.
size === "sm" && "h-8 pl-3 pr-1.5 text-xs",
size === "default" && "h-9 pl-4 pr-2 text-sm",
size === "lg" && "h-10 pl-4.5 pr-2.5 text-sm",
className,
)}
>
Expand Down Expand Up @@ -185,9 +187,9 @@ function ModelSelectorTrigger({
</span>
)}
</span>
<span className="flex size-4 shrink-0 items-center justify-center">
<span className="-ml-1 flex size-4 shrink-0 items-center justify-center">
<HugeiconsIcon
icon={ArrowDown01Icon}
icon={ChevronDownStandardIcon}
strokeWidth={1.75}
className="size-3.5 text-muted-foreground"
/>
Expand Down
7 changes: 4 additions & 3 deletions studio/frontend/src/components/assistant-ui/thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -783,13 +783,14 @@ const ThreadWelcome: FC<{
threadId?: string | null;
}> = ({ hideComposer, threadId }) => {
const displayName = useUserProfileStore((s) => s.displayName);
const nickname = useUserProfileStore((s) => s.nickname);
const [welcome, setWelcome] = useState<Welcome>(DEFAULT_WELCOME);

useEffect(() => {
// First name only, for a natural greeting; blank falls back to no name.
const name = displayName.trim().split(/\s+/)[0] ?? "";
// Prefer the nickname; otherwise first name only. Blank falls back to none.
const name = nickname.trim() || (displayName.trim().split(/\s+/)[0] ?? "");
setWelcome(buildWelcome(new Date().getHours(), name));
}, [displayName]);
}, [displayName, nickname]);

const currentEmojiSrc = `Sloth emojis/${welcome.sloth}`;

Expand Down
2 changes: 1 addition & 1 deletion studio/frontend/src/components/ui/alert-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function AlertDialogOverlay({
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50",
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/30 duration-100 supports-backdrop-filter:backdrop-blur-[2px] fixed inset-0 z-50",
className,
)}
{...props}
Expand Down
8 changes: 4 additions & 4 deletions studio/frontend/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function DialogOverlay({
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/80 duration-100 inset-0 isolate z-50",
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/30 supports-backdrop-filter:backdrop-blur-[2px] duration-100 inset-0 isolate z-50",
position === "fixed" ? "fixed" : "absolute",
className,
)}
Expand Down Expand Up @@ -89,7 +89,7 @@ function DialogContent({
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/5 grid max-w-[calc(100%-2rem)] gap-6 rounded-4xl p-6 text-sm ring-1 duration-100 sm:max-w-md top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2",
"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/5 grid max-w-[calc(100%-2rem)] gap-6 rounded-4xl px-7 pt-8 pb-7 text-sm ring-1 duration-100 sm:max-w-md top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2",
position === "fixed" ? "fixed" : "absolute",
className,
)}
Expand All @@ -100,7 +100,7 @@ function DialogContent({
<DialogPrimitive.Close data-slot="dialog-close" asChild>
<Button
variant="ghost"
className="absolute top-4 right-4"
className="absolute top-5 right-5"
size="icon-sm"
>
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
Expand Down Expand Up @@ -158,7 +158,7 @@ function DialogTitle({
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-base leading-none font-medium", className)}
className={cn("font-heading text-lg leading-none font-semibold", className)}
{...props}
/>
);
Expand Down
8 changes: 4 additions & 4 deletions studio/frontend/src/components/ui/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
import type * as React from "react";

import { Tick02Icon } from "@/lib/tick-icon";
import { ChevronRightStandardIcon } from "@/lib/chevron-icons";
import { cn } from "@/lib/utils";
import { ArrowRight01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";

function DropdownMenu({
Expand Down Expand Up @@ -184,7 +184,7 @@ function DropdownMenuSeparator({
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border/50 -mx-1 my-1 h-px", className)}
className={cn("bg-border/50 mx-1 my-1 h-px", className)}
{...props}
/>
);
Expand Down Expand Up @@ -232,8 +232,8 @@ function DropdownMenuSubTrigger({
>
{children}
<HugeiconsIcon
icon={ArrowRight01Icon}
strokeWidth={2}
icon={ChevronRightStandardIcon}
strokeWidth={1.5}
className="ml-auto size-[12px]"
/>
</DropdownMenuPrimitive.SubTrigger>
Expand Down
6 changes: 3 additions & 3 deletions studio/frontend/src/components/ui/menubar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type * as React from "react";

import { Tick02Icon } from "@/lib/tick-icon";
import { cn } from "@/lib/utils";
import { ArrowRight01Icon } from "@hugeicons/core-free-icons";
import { ChevronRightStandardIcon } from "@/lib/chevron-icons";
import { HugeiconsIcon } from "@hugeicons/react";

function Menubar({
Expand Down Expand Up @@ -241,8 +241,8 @@ function MenubarSubTrigger({
>
{children}
<HugeiconsIcon
icon={ArrowRight01Icon}
strokeWidth={2}
icon={ChevronRightStandardIcon}
strokeWidth={1.5}
className="ml-auto size-[12px]"
/>
</MenubarPrimitive.SubTrigger>
Expand Down
2 changes: 1 addition & 1 deletion studio/frontend/src/components/ui/sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function SheetOverlay({
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/80 duration-100 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs inset-0 z-50",
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/30 duration-100 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-[2px] inset-0 z-50",
position === "fixed" ? "fixed" : "absolute",
className,
)}
Expand Down
2 changes: 1 addition & 1 deletion studio/frontend/src/features/auth/components/auth-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null {

<Button
type="submit"
className="mx-auto flex w-fit px-6"
className="mx-auto flex w-fit px-4"
disabled={
loading ||
statusLoading ||
Expand Down
18 changes: 6 additions & 12 deletions studio/frontend/src/features/chat/chat-settings-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ import { useIsMobile } from "@/hooks/use-mobile";
import { useLlamaUpdateCheck } from "@/hooks/use-llama-update-check";
import { cn } from "@/lib/utils";
import {
ArrowDown01Icon,
ArrowTurnBackwardIcon,
Edit03Icon,
InformationCircleIcon,
LayoutAlignRightIcon,
} from "@hugeicons/core-free-icons";
import { ChevronDownStandardIcon } from "@/lib/chevron-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { ChevronDown, ExternalLink } from "lucide-react";
import { Tooltip as TooltipPrimitive } from "radix-ui";
Expand Down Expand Up @@ -911,7 +911,7 @@ export function ChatSettingsPanel({
>
<SelectTrigger
animateRadius={false}
icon={ArrowDown01Icon}
icon={ChevronDownStandardIcon}
iconClassName="size-3.5"
className="grid h-7 w-[64px] min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-1 rounded-full border-transparent bg-black/[0.04] dark:bg-white/[0.05] hover:bg-black/[0.06] dark:hover:bg-white/[0.1] pl-3 pr-2 py-0 text-[13px]! font-medium text-nav-fg focus-visible:ring-0 focus-visible:border-transparent [&_[data-slot=select-value]]:min-w-0 [&_[data-slot=select-value]]:truncate [&>svg]:shrink-0"
>
Expand Down Expand Up @@ -951,7 +951,7 @@ export function ChatSettingsPanel({
>
<SelectTrigger
animateRadius={false}
icon={ArrowDown01Icon}
icon={ChevronDownStandardIcon}
iconClassName="size-3.5"
className="grid h-7 w-[124px] min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-1 rounded-full border-transparent bg-black/[0.04] dark:bg-white/[0.05] hover:bg-black/[0.06] dark:hover:bg-white/[0.1] pl-3 pr-2 py-0 text-[13px]! font-medium text-nav-fg focus-visible:ring-0 focus-visible:border-transparent [&_[data-slot=select-value]]:min-w-0 [&_[data-slot=select-value]]:truncate [&>svg]:shrink-0"
data-test-id="speculative-type-select"
Expand Down Expand Up @@ -1172,7 +1172,7 @@ export function ChatSettingsPanel({
aria-hidden="true"
>
<HugeiconsIcon
icon={ArrowDown01Icon}
icon={ChevronDownStandardIcon}
className="size-3.5"
strokeWidth={2}
/>
Expand Down Expand Up @@ -1547,10 +1547,7 @@ export function ChatSettingsPanel({
setSystemPromptEditorOpen(nextOpen);
}}
>
<DialogContent
className="corner-squircle dialog-soft-surface sm:max-w-3xl"
overlayClassName="bg-background/35 supports-backdrop-filter:backdrop-blur-[1px]"
>
<DialogContent className="corner-squircle dialog-soft-surface sm:max-w-3xl">
<DialogHeader>
<DialogTitle>Edit System Prompt</DialogTitle>
<DialogDescription>
Expand Down Expand Up @@ -1827,10 +1824,7 @@ function ChatTemplateFields() {
</div>
</div>
<Dialog open={editorOpen} onOpenChange={setEditorOpen}>
<DialogContent
className="corner-squircle dialog-soft-surface sm:max-w-3xl"
overlayClassName="bg-background/35 supports-backdrop-filter:backdrop-blur-[1px]"
>
<DialogContent className="corner-squircle dialog-soft-surface sm:max-w-3xl">
<DialogHeader>
<DialogTitle>Edit Chat Template</DialogTitle>
<DialogDescription>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export function ModelLoadDescription({
<div className="flex h-full shrink-0 items-center self-center">
<Spinner className="size-3.5 text-muted-foreground" />
</div>
<div className="min-w-0 flex-1">
{title ? <p className="text-foreground leading-5 font-semibold">{title}</p> : null}
<div className="flex min-w-0 flex-1 flex-col justify-center">
{title ? <p className="text-foreground leading-tight font-semibold">{title}</p> : null}
{hasProgress ? (
<div className="w-full pt-1">
<div className="flex items-center justify-between gap-2 text-[10px] font-medium tracking-[0.08em] text-muted-foreground/80">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
ArrowDown01Icon,
Folder01Icon,
} from "@hugeicons/core-free-icons";
import { Folder01Icon } from "@hugeicons/core-free-icons";
import { Tick02Icon } from "@/lib/tick-icon";
import { ChevronDownStandardIcon } from "@/lib/chevron-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import type { ReactElement } from "react";
import type { ProjectRecord } from "../types";
Expand Down Expand Up @@ -60,7 +58,7 @@ export function ProjectSwitcher({
</span>
<span className="flex size-4 shrink-0 items-center justify-center">
<HugeiconsIcon
icon={ArrowDown01Icon}
icon={ChevronDownStandardIcon}
strokeWidth={1.75}
className="size-3.5 text-muted-foreground"
aria-hidden={true}
Expand All @@ -72,7 +70,7 @@ export function ProjectSwitcher({
side="bottom"
align="start"
sideOffset={0}
className="app-user-menu menu-soft-surface ring-0 min-w-56 max-w-72 max-h-72 py-2 font-heading rounded-[14px] border-0"
className="unsloth-plus-menu ring-0 min-w-56 max-w-72 max-h-72 font-heading"
>
{showLoadingRow ? (
<DropdownMenuItem disabled={true} className="text-muted-foreground">
Expand Down
2 changes: 2 additions & 0 deletions studio/frontend/src/features/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export {
} from "./chat-settings-sheet";
export { useChatRuntimeStore } from "./stores/chat-runtime-store";
export { useChatSearchStore } from "./stores/chat-search-store";
export { usePinnedChatsStore } from "./stores/pinned-chats-store";
export { useChatPreferencesStore } from "./stores/chat-preferences-store";
export {
PLUS_MENU_ORDER,
usePlusMenuPrefsStore,
Expand Down
9 changes: 7 additions & 2 deletions studio/frontend/src/features/chat/runtime-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,10 @@ function createStudioDbAdapter(
}
return {
remoteId: thread.id,
status: thread.archived ? "archived" : "regular",
// Always regular: archive state is owned by the app's own controls.
// Reporting archived here makes assistant-ui unarchive a chat the
// moment it is opened.
status: "regular",
title: thread.title,
};
},
Expand Down Expand Up @@ -658,8 +661,10 @@ function createStudioDbAdapter(
},

async unarchive(remoteId: string) {
// No-op on archive state: the app owns it via the sidebar menu and the
// archived chats settings dialog. assistant-ui calls this when an
// archived chat is opened, which must not unarchive it.
await ensureStoredChatThread(remoteId);
await updateStoredChatThread(remoteId, { archived: false });
},

async delete(remoteId: string) {
Expand Down
32 changes: 32 additions & 0 deletions studio/frontend/src/features/chat/stores/chat-preferences-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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 { create } from "zustand";
import { persist } from "zustand/middleware";

// Client-side chat UI prefs kept in localStorage, not the chat DB.
// confirmDeleteChats: when off, deleting a chat skips the confirm dialog.
export interface ChatPreferencesState {
confirmDeleteChats: boolean;
setConfirmDeleteChats: (value: boolean) => void;
}

export const useChatPreferencesStore = create<ChatPreferencesState>()(
persist(
(set) => ({
confirmDeleteChats: true,
setConfirmDeleteChats: (confirmDeleteChats) =>
set({ confirmDeleteChats }),
}),
{
name: "unsloth_chat_preferences",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset delete confirmations with local preferences

This new delete-confirmation preference persists under unsloth_chat_preferences, but General > Reset all local preferences only removes the hard-coded PREFS_KEYS list in general-tab.tsx, and this key is absent there. If a user turns confirmations off and then uses reset to restore local UI defaults, the reload keeps instant chat deletion enabled instead of returning to the safer default; add this storage key to the reset list.

Useful? React with 👍 / 👎.

merge: (persisted, current) => {
const saved = persisted as Partial<ChatPreferencesState> | undefined;
return {
...current,
confirmDeleteChats: saved?.confirmDeleteChats ?? true,
};
},
},
),
);
42 changes: 42 additions & 0 deletions studio/frontend/src/features/chat/stores/pinned-chats-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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 { create } from "zustand";
import { persist } from "zustand/middleware";

// Client-side pin state for chats, keyed by stable chat id. Kept in
// localStorage, not the chat DB. New pins are prepended so the most
// recently pinned chat sorts first in the Pinned section.
export interface PinnedChatsState {
pinnedIds: string[];
togglePin: (id: string) => void;
unpin: (id: string) => void;
}

export const usePinnedChatsStore = create<PinnedChatsState>()(
persist(
(set) => ({
pinnedIds: [],
togglePin: (id) =>
set((state) => ({
pinnedIds: state.pinnedIds.includes(id)
? state.pinnedIds.filter((x) => x !== id)
: [id, ...state.pinnedIds],
})),
unpin: (id) =>
set((state) => ({
pinnedIds: state.pinnedIds.filter((x) => x !== id),
})),
}),
{
name: "unsloth_pinned_chats",
merge: (persisted, current) => {
const saved = persisted as Partial<PinnedChatsState> | undefined;
return {
...current,
pinnedIds: Array.isArray(saved?.pinnedIds) ? saved.pinnedIds : [],
};
},
},
),
);
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ export function DataRecipesPage(): ReactElement {

return (
<div className="min-h-[calc(100dvh-var(--studio-titlebar-height,0px))] bg-background">
<main className="mx-auto w-full max-w-7xl px-6 py-8">
<main className="mx-auto w-full max-w-7xl px-5 py-8 sm:px-9">
<div className="flex items-center justify-between gap-4">
<div>
<h1 className="text-[30px] font-semibold leading-[1.04] tracking-[-0.028em] text-foreground sm:text-[34px]">
Expand Down
Loading
Loading