diff --git a/studio/frontend/src/components/app-sidebar.tsx b/studio/frontend/src/components/app-sidebar.tsx index 25d88aeeea..b6e218793a 100644 --- a/studio/frontend/src/components/app-sidebar.tsx +++ b/studio/frontend/src/components/app-sidebar.tsx @@ -1551,7 +1551,7 @@ export function AppSidebar() {
-
+ {/* min-w-0 so long names truncate instead of overflowing; + pr on the button reserves room for the settings cog */} +
{displayTitle} Unsloth
- {/* settings cog (replaces the up/down chevron) */} - + {/* settings cog; sibling of the trigger (buttons cannot nest), + overlaid on the row's right edge, opens settings directly */} + diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx index 57213f07cb..62b8af6e3a 100644 --- a/studio/frontend/src/components/assistant-ui/thread.tsx +++ b/studio/frontend/src/components/assistant-ui/thread.tsx @@ -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" }> = ({ ), - canvas: ( + // Hidden by default; enabled from Settings > Chat > Canvas. + canvas: showCanvasMenuItem ? ( setArtifactsEnabled(!artifactsEnabled)} @@ -2968,7 +2972,7 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({ ) : null} - ), + ) : null, bypassPermissions: , projects: ( diff --git a/studio/frontend/src/features/chat/shared-composer.tsx b/studio/frontend/src/features/chat/shared-composer.tsx index da0285ef49..a0813fe27b 100644 --- a/studio/frontend/src/features/chat/shared-composer.tsx +++ b/studio/frontend/src/features/chat/shared-composer.tsx @@ -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({ ), - canvas: ( + // Hidden by default; enabled from Settings > Chat > Canvas. + canvas: showCanvasMenuItem ? ( setArtifactsEnabled(!artifactsEnabled)} @@ -1422,7 +1424,7 @@ export function SharedComposer({ ) : null} - ), + ) : null, bypassPermissions: , projects: ( diff --git a/studio/frontend/src/features/chat/stores/chat-runtime-store.ts b/studio/frontend/src/features/chat/stores/chat-runtime-store.ts index 2a81fe6ea8..abdaa871c0 100644 --- a/studio/frontend/src/features/chat/stores/chat-runtime-store.ts +++ b/studio/frontend/src/features/chat/stores/chat-runtime-store.ts @@ -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((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((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); diff --git a/studio/frontend/src/features/chat/stores/plus-menu-prefs-store.ts b/studio/frontend/src/features/chat/stores/plus-menu-prefs-store.ts index 822c66eeb5..960649a901 100644 --- a/studio/frontend/src/features/chat/stores/plus-menu-prefs-store.ts +++ b/studio/frontend/src/features/chat/stores/plus-menu-prefs-store.ts @@ -44,6 +44,8 @@ const DEFAULT_PINS: Record = { bypassPermissions: false, }; +export const PLUS_MENU_PINS_STORAGE_KEY = "unsloth_plus_menu_pins"; + export interface PlusMenuPrefsState { pins: Record; setPin: (id: PlusMenuItemId, value: boolean) => void; @@ -72,7 +74,7 @@ export const usePlusMenuPrefsStore = create()( })), }), { - 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) => { diff --git a/studio/frontend/src/features/settings/tabs/chat-tab.tsx b/studio/frontend/src/features/settings/tabs/chat-tab.tsx index 8ef68500cb..d5c9f10a4f 100644 --- a/studio/frontend/src/features/settings/tabs/chat-tab.tsx +++ b/studio/frontend/src/features/settings/tabs/chat-tab.tsx @@ -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) => ( + {/* Canvas toggles menu visibility; the rest toggle pin placement. */} togglePlusPin(item.id)} + checked={ + item.id === "canvas" ? showCanvasMenuItem : plusPins[item.id] + } + onCheckedChange={ + item.id === "canvas" + ? setShowCanvasMenuItem + : () => togglePlusPin(item.id) + } /> ))}