Studio: clickable sidebar settings cog, long name truncation, Canvas menu opt-in (#7171)

* Studio: make sidebar settings cog clickable, opens settings directly

* Studio: truncate long profile names so the settings cog stays visible

* Studio: tighten spacing between profile name and settings cog

* Studio: render settings cog as a sibling button instead of nesting it in the account trigger

* Studio: cap very long names in the welcome greeting

* Studio: hide the Canvas chat menu item by default behind a settings opt-in

* Studio: keep Canvas in the chat menu settings list as the visibility toggle

* Studio: drop the Canvas row description in chat menu settings

* Studio: keep Canvas visible for profiles that pinned it before the visibility flag
This commit is contained in:
Michael Han 2026-07-16 06:01:37 -07:00 committed by GitHub
commit aad11f4ef1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 77 additions and 21 deletions

View file

@ -1551,7 +1551,7 @@ export function AppSidebar() {
<SidebarMenuButton
size="lg"
aria-label={t("shell.accountMenu", { name: displayTitle })}
className="sidebar-nav-btn !h-[44px] -my-[3px] gap-[9px] px-2 py-[3px] rounded-[14px] group-data-[collapsible=icon]:!size-[34px] group-data-[collapsible=icon]:!rounded-full group-data-[collapsible=icon]:!p-0 group-data-[collapsible=icon]:mx-auto group-data-[collapsible=icon]:justify-center"
className="sidebar-nav-btn !h-[44px] -my-[3px] gap-[9px] pl-2 pr-[45px] py-[3px] rounded-[14px] group-data-[collapsible=icon]:!size-[34px] group-data-[collapsible=icon]:!rounded-full group-data-[collapsible=icon]:!p-0 group-data-[collapsible=icon]:mx-auto group-data-[collapsible=icon]:justify-center"
>
<div className="flex shrink-0 items-center">
<UserAvatar
@ -1561,21 +1561,12 @@ export function AppSidebar() {
className="!size-[32px] group-data-[collapsible=icon]:!rounded-full"
/>
</div>
<div className="flex flex-col gap-px leading-tight group-data-[collapsible=icon]:hidden">
{/* min-w-0 so long names truncate instead of overflowing;
pr on the button reserves room for the settings cog */}
<div className="flex min-w-0 flex-1 flex-col gap-px leading-tight group-data-[collapsible=icon]:hidden">
<span className="truncate font-heading text-[13.5px] tracking-[0.025em] dark:tracking-[0.04em] font-semibold text-nav-fg">{displayTitle}</span>
<span className="truncate text-[11.5px] tracking-nav text-muted-foreground">Unsloth</span>
</div>
{/* settings cog (replaces the up/down chevron) */}
<span
aria-hidden="true"
className="ml-auto flex size-[32px] shrink-0 items-center justify-center text-muted-foreground group-data-[collapsible=icon]:hidden"
>
<HugeiconsIcon
icon={Settings02Icon}
strokeWidth={1.5}
className="!size-[18px]"
/>
</span>
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
@ -1690,6 +1681,20 @@ export function AppSidebar() {
)}
</DropdownMenuContent>
</DropdownMenu>
{/* settings cog; sibling of the trigger (buttons cannot nest),
overlaid on the row's right edge, opens settings directly */}
<button
type="button"
aria-label={t("shell.navigation.settings")}
onClick={() => useSettingsDialogStore.getState().openDialog()}
className="absolute right-2 top-1/2 flex size-[32px] -translate-y-1/2 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-black/10 hover:text-foreground dark:hover:bg-white/10 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring group-data-[collapsible=icon]:hidden"
>
<HugeiconsIcon
icon={Settings02Icon}
strokeWidth={1.5}
className="!size-[18px]"
/>
</button>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>

View file

@ -1326,7 +1326,9 @@ const ThreadWelcome: FC<{
useEffect(() => {
// Prefer the nickname; otherwise first name only. Blank falls back to none.
const name = nickname.trim() || (displayName.trim().split(/\s+/)[0] ?? "");
const raw = nickname.trim() || (displayName.trim().split(/\s+/)[0] ?? "");
// Cap very long names so the greeting stays on one line.
const name = raw.length > 20 ? `${raw.slice(0, 20)}` : raw;
setWelcome(buildWelcome(new Date().getHours(), name));
}, [displayName, nickname]);
@ -2707,6 +2709,7 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({
const setCodeToolsEnabled = useChatRuntimeStore((s) => s.setCodeToolsEnabled);
const artifactsEnabled = useChatRuntimeStore((s) => s.artifactsEnabled);
const setArtifactsEnabled = useChatRuntimeStore((s) => s.setArtifactsEnabled);
const showCanvasMenuItem = useChatRuntimeStore((s) => s.showCanvasMenuItem);
const mcpEnabledForChat = useChatRuntimeStore((s) => s.mcpEnabledForChat);
const setMcpEnabledForChat = useChatRuntimeStore(
(s) => s.setMcpEnabledForChat,
@ -2957,7 +2960,8 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({
</DropdownMenuSubContent>
</DropdownMenuSub>
),
canvas: (
// Hidden by default; enabled from Settings > Chat > Canvas.
canvas: showCanvasMenuItem ? (
<DropdownMenuItem
className={artifactsEnabled ? "text-primary font-medium" : undefined}
onSelect={() => setArtifactsEnabled(!artifactsEnabled)}
@ -2968,7 +2972,7 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} className="ml-auto" />
) : null}
</DropdownMenuItem>
),
) : null,
bypassPermissions: <BypassPermissionsMenuItem />,
projects: (
<DropdownMenuSub>

View file

@ -613,6 +613,7 @@ export function SharedComposer({
);
const artifactsEnabled = useChatRuntimeStore((s) => s.artifactsEnabled);
const setArtifactsEnabled = useChatRuntimeStore((s) => s.setArtifactsEnabled);
const showCanvasMenuItem = useChatRuntimeStore((s) => s.showCanvasMenuItem);
const permissionMode = useChatRuntimeStore((s) => s.permissionMode);
const mcpEnabledForChat = useChatRuntimeStore((s) => s.mcpEnabledForChat);
const setMcpEnabledForChat = useChatRuntimeStore(
@ -1411,7 +1412,8 @@ export function SharedComposer({
</DropdownMenuSubContent>
</DropdownMenuSub>
),
canvas: (
// Hidden by default; enabled from Settings > Chat > Canvas.
canvas: showCanvasMenuItem ? (
<DropdownMenuItem
className={artifactsEnabled ? "text-primary font-medium" : undefined}
onSelect={() => setArtifactsEnabled(!artifactsEnabled)}
@ -1422,7 +1424,7 @@ export function SharedComposer({
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} className="ml-auto" />
) : null}
</DropdownMenuItem>
),
) : null,
bypassPermissions: <BypassPermissionsMenuItem />,
projects: (
<DropdownMenuSub>

View file

@ -23,6 +23,7 @@ import {
savePersistedChatSettingsPatch,
} from "../utils/chat-settings-storage";
import { useExternalProvidersStore } from "./external-providers-store";
import { PLUS_MENU_PINS_STORAGE_KEY } from "./plus-menu-prefs-store";
const HF_TOKEN_KEY = "unsloth_hf_token";
const HF_TOKEN_CHANGED_EVENT = "unsloth:hf-token-changed";
@ -31,6 +32,8 @@ export const CHAT_TOOLS_ENABLED_KEY = "unsloth_chat_tools_enabled";
export const CHAT_CODE_TOOLS_ENABLED_KEY = "unsloth_chat_code_tools_enabled";
export const CHAT_IMAGE_TOOLS_ENABLED_KEY = "unsloth_chat_image_tools_enabled";
export const CHAT_ARTIFACTS_ENABLED_KEY = "unsloth_chat_artifacts_enabled";
export const CHAT_SHOW_CANVAS_MENU_ITEM_KEY =
"unsloth_chat_show_canvas_menu_item";
export const CHAT_COLLAPSE_HTML_ARTIFACTS_KEY =
"unsloth_chat_collapse_html_artifacts";
export const CHAT_ALLOW_ARTIFACT_NETWORK_ACCESS_KEY =
@ -357,6 +360,24 @@ function saveBool(key: string, value: boolean): void {
}
}
// The visibility flag shipped after the menu pins, so when it is absent,
// profiles that had explicitly pinned Canvas keep it visible.
function loadShowCanvasMenuItem(): boolean {
const stored = loadOptionalBool(CHAT_SHOW_CANVAS_MENU_ITEM_KEY);
if (stored !== null) return stored;
if (!canUseStorage()) return false;
try {
const raw = localStorage.getItem(PLUS_MENU_PINS_STORAGE_KEY);
if (raw === null) return false;
const parsed = JSON.parse(raw) as {
state?: { pins?: { canvas?: boolean } };
};
return parsed.state?.pins?.canvas === true;
} catch {
return false;
}
}
/**
* "full" is intentionally not restorable: it disables the sandbox and every
* confirmation gate, so it must be re-enabled (through the warning dialog)
@ -642,6 +663,8 @@ type ChatRuntimeStore = {
codeToolsEnabled: boolean;
imageToolsEnabled: boolean;
artifactsEnabled: boolean;
// Whether the Canvas toggle is offered in the composer + menu (hidden by default).
showCanvasMenuItem: boolean;
collapseHtmlArtifacts: boolean;
allowArtifactNetworkAccess: boolean;
mcpEnabledForChat: boolean;
@ -818,6 +841,7 @@ type ChatRuntimeStore = {
enabled: boolean,
options?: { persist?: boolean },
) => void;
setShowCanvasMenuItem: (enabled: boolean) => void;
setCollapseHtmlArtifacts: (enabled: boolean) => void;
setAllowArtifactNetworkAccess: (enabled: boolean) => void;
setMcpEnabledForChat: (enabled: boolean) => void;
@ -1147,6 +1171,7 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set, get) => ({
codeToolsEnabled: loadBool(CHAT_CODE_TOOLS_ENABLED_KEY, false),
imageToolsEnabled: loadBool(CHAT_IMAGE_TOOLS_ENABLED_KEY, false),
artifactsEnabled: loadBool(CHAT_ARTIFACTS_ENABLED_KEY, false),
showCanvasMenuItem: loadShowCanvasMenuItem(),
collapseHtmlArtifacts: loadBool(CHAT_COLLAPSE_HTML_ARTIFACTS_KEY, false),
allowArtifactNetworkAccess: loadBool(
CHAT_ALLOW_ARTIFACT_NETWORK_ACCESS_KEY,
@ -1504,6 +1529,11 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set, get) => ({
}
return { artifactsEnabled };
}),
setShowCanvasMenuItem: (showCanvasMenuItem) =>
set(() => {
saveBool(CHAT_SHOW_CANVAS_MENU_ITEM_KEY, showCanvasMenuItem);
return { showCanvasMenuItem };
}),
setCollapseHtmlArtifacts: (collapseHtmlArtifacts) =>
set((state) => {
saveBool(CHAT_COLLAPSE_HTML_ARTIFACTS_KEY, collapseHtmlArtifacts);

View file

@ -44,6 +44,8 @@ const DEFAULT_PINS: Record<PlusMenuItemId, boolean> = {
bypassPermissions: false,
};
export const PLUS_MENU_PINS_STORAGE_KEY = "unsloth_plus_menu_pins";
export interface PlusMenuPrefsState {
pins: Record<PlusMenuItemId, boolean>;
setPin: (id: PlusMenuItemId, value: boolean) => void;
@ -72,7 +74,7 @@ export const usePlusMenuPrefsStore = create<PlusMenuPrefsState>()(
})),
}),
{
name: "unsloth_plus_menu_pins",
name: PLUS_MENU_PINS_STORAGE_KEY,
// Backfill any ids added in a later release so persisted state from an
// older version still resolves every menu item.
merge: (persisted, current) => {

View file

@ -175,6 +175,12 @@ export function ChatTab() {
const [clearing, setClearing] = useState(false);
const autoTitle = useChatRuntimeStore((state) => state.autoTitle);
const setAutoTitle = useChatRuntimeStore((state) => state.setAutoTitle);
const showCanvasMenuItem = useChatRuntimeStore(
(state) => state.showCanvasMenuItem,
);
const setShowCanvasMenuItem = useChatRuntimeStore(
(state) => state.setShowCanvasMenuItem,
);
const collapseHtmlArtifacts = useChatRuntimeStore(
(state) => state.collapseHtmlArtifacts,
);
@ -408,9 +414,16 @@ export function ChatTab() {
>
{PLUS_MENU_SETTINGS.map((item) => (
<SettingsRow key={item.id} label={item.label} icon={item.icon}>
{/* Canvas toggles menu visibility; the rest toggle pin placement. */}
<Switch
checked={plusPins[item.id]}
onCheckedChange={() => togglePlusPin(item.id)}
checked={
item.id === "canvas" ? showCanvasMenuItem : plusPins[item.id]
}
onCheckedChange={
item.id === "canvas"
? setShowCanvasMenuItem
: () => togglePlusPin(item.id)
}
/>
</SettingsRow>
))}