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:
parent
26faceecf7
commit
aad11f4ef1
6 changed files with 77 additions and 21 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue