From 8517721adb692746cf09d717465cfddd2fe853aa Mon Sep 17 00:00:00 2001 From: Michael Han <107991372+shimmyshimmer@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:25:56 -0700 Subject: [PATCH 001/194] Studio: move sidebar search into the header (#7304) * Studio: move sidebar search into the header Put the search action as an icon button next to the sidebar toggle in the header instead of a full-width nav row, so New Chat is the only fixed row above the scrolling list. The search row is kept for the collapsed icon rail only. Also add a small bottom gap under New Chat when it is pinned during scroll. * Studio: keep search row on custom-titlebar platforms The header search button only renders on mac/web where the brand row shows. On win/linux custom titlebars there's no header button, so keep the full-width search row visible instead of hiding it. * Studio: address review on sidebar search tooltip - Hide the search tooltip on mobile (hidden={isMobile}), matching the SidebarMenuButton tooltip convention. - Show Cmd K on Mac and Ctrl K elsewhere instead of a hardcoded glyph; the search dialog binds both meta and ctrl. Uses getClientPlatform so it is correct on web too, not just Tauri. --- .../frontend/src/components/app-sidebar.tsx | 57 ++++++++++++++++--- .../src/components/tauri/window-titlebar.tsx | 2 +- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/studio/frontend/src/components/app-sidebar.tsx b/studio/frontend/src/components/app-sidebar.tsx index a13828b06b..f4226760a2 100644 --- a/studio/frontend/src/components/app-sidebar.tsx +++ b/studio/frontend/src/components/app-sidebar.tsx @@ -45,6 +45,7 @@ import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; import { useAnimatedThemeToggle } from "@/components/ui/animated-theme-toggler"; import { + getClientPlatform, shouldUseCustomWindowTitlebar, shouldUseNativeMacWindowTitlebar, } from "@/components/tauri/window-titlebar"; @@ -343,6 +344,8 @@ export function AppSidebar() { ); const [usesCustomTitlebar] = useState(shouldUseCustomWindowTitlebar); const [usesNativeMacTitlebar] = useState(shouldUseNativeMacWindowTitlebar); + // Mac uses Cmd, others use Ctrl. Not Tauri-gated, so it's right on web too. + const [isMacPlatform] = useState(() => getClientPlatform().includes("mac")); const { pathname, search } = useRouterState({ select: (s) => ({ pathname: s.location.pathname, @@ -1194,27 +1197,55 @@ export function AppSidebar() { )} - {!isMobile && ( +
{helperText}
)} - {error &&{error}
} + {error && ( ++ {error} +
+ )}- {showPasswordMismatchWarning - ? "Please ensure passwords match." - : "Must be at least 8 characters."} + {showWhitespaceWarning + ? "New password cannot contain spaces." + : showPasswordMismatchWarning + ? "Please ensure passwords match." + : "Must be at least 8 characters."}
> )} diff --git a/studio/frontend/src/features/settings/components/change-password-dialog.tsx b/studio/frontend/src/features/settings/components/change-password-dialog.tsx index cd30d37d5d..c88fc48cac 100644 --- a/studio/frontend/src/features/settings/components/change-password-dialog.tsx +++ b/studio/frontend/src/features/settings/components/change-password-dialog.tsx @@ -78,6 +78,9 @@ function passwordValidationMessage( minLength: MIN_PASSWORD_LENGTH, }); } + if (/\s/.test(nextPassword)) { + return t("settings.general.passwordDialog.newHasSpaces"); + } if (nextPassword !== confirmPassword) { return t("settings.general.passwordDialog.mismatch"); } @@ -160,6 +163,7 @@ export function ChangePasswordDialog() { const currentTooShort = hasStartedTooShortPassword(current); const nextTooShort = hasStartedTooShortPassword(next); + const nextHasSpaces = /\s/.test(next); const mismatch = confirm.length > 0 && next !== confirm; const samePassword = hasReusablePassword(current, next); const validationMessage = passwordValidationMessage( @@ -279,13 +283,15 @@ export function ChangePasswordDialog() { minLength={MIN_PASSWORD_LENGTH} disabled={submitting} /> - {nextTooShort || samePassword ? ( + {nextTooShort || nextHasSpaces || samePassword ? ({nextTooShort ? t("settings.general.passwordDialog.newTooShort", { minLength: MIN_PASSWORD_LENGTH, }) - : t("settings.general.passwordDialog.samePassword")} + : nextHasSpaces + ? t("settings.general.passwordDialog.newHasSpaces") + : t("settings.general.passwordDialog.samePassword")}
) : null} diff --git a/studio/frontend/src/i18n/locales/en.ts b/studio/frontend/src/i18n/locales/en.ts index cf8a29b6d2..164833b41d 100644 --- a/studio/frontend/src/i18n/locales/en.ts +++ b/studio/frontend/src/i18n/locales/en.ts @@ -196,6 +196,7 @@ export const en = { currentTooShort: "Current password must be at least {minLength} characters.", newTooShort: "New password must be at least {minLength} characters.", + newHasSpaces: "New password cannot contain spaces.", mismatch: "Passwords do not match.", samePassword: "New password must be different from your current password.", diff --git a/unsloth_cli/commands/_password_prompt.py b/unsloth_cli/commands/_password_prompt.py index b6fd8ca34d..55f50acbf1 100644 --- a/unsloth_cli/commands/_password_prompt.py +++ b/unsloth_cli/commands/_password_prompt.py @@ -191,6 +191,10 @@ def prompt_new_password(verify_current: Callable[[str], bool], out: TextIO | Non out.write(f"Password must be at least {MIN_PASSWORD_LENGTH} characters. Try again.\n") out.flush() continue + if any(ch.isspace() for ch in password): + out.write("Password cannot contain spaces. Try again.\n") + out.flush() + continue if verify_current(password): out.write("New password must differ from the current password. Try again.\n") out.flush() @@ -233,6 +237,8 @@ def validate_new_password(candidate: str, verify_current: Callable[[str], bool]) current password), else None. Same policy as the interactive loop.""" if len(candidate) < MIN_PASSWORD_LENGTH: return f"Password must be at least {MIN_PASSWORD_LENGTH} characters." + if any(ch.isspace() for ch in candidate): + return "Password cannot contain spaces." if verify_current(candidate): return "New password must differ from the current password." return None From fa5498db0b6c089c1c9ddc8e82043d82be202bc6 Mon Sep 17 00:00:00 2001 From: Michael Han <107991372+shimmyshimmer@users.noreply.github.com> Date: Thu, 23 Jul 2026 00:44:42 -0700 Subject: [PATCH 019/194] Studio: UI font size scales all text consistently without moving layout (#7355) * Studio: make UI font size scale all text without moving layout The UI font size setting changes the root rem base, so only rem sized text reacted. Hundreds of px text classes, px font sizes in CSS, and chart labels stayed fixed, while rem based padding, widths and radii wrongly grew. Convert all text sizes to rem so every font follows the setting, and pin spacing, radius, container widths, sidebar and thread widths to px so layout no longer follows the rem base. Library styles (streamdown, react-flow) are re-based via overrides. All conversions are exact at the default 16px root, so the default rendering is unchanged. * Studio: keep logo at fixed size and fit tight controls at large UI fonts The logo lockups (sidebar wordmark with beta badge, onboarding wizard) are branding and now keep px sizes at any UI font size. Two controls clipped their text at the largest setting: the appearance color chips (fixed w-24) and the voice tab selects (fixed w-56). Both use min widths now, so they keep the default look at 16px and only grow when the text needs the room. * Studio: keep dropdown corners rounded when the menu scrolls A scrolling dropdown lost its rounded corners on the scrollbar side: WebKit paints the surface square when the rounded element itself hosts the scrollbar, which shows up in the desktop app whenever a menu overflows, for example at larger UI font sizes. Dropdown menu and select content now clip with overflow hidden and scroll an inner viewport instead. The surface padding insets the scrollbar clear of the curve, so corners stay rounded in every engine. Submenus are unaffected since sub content is portaled. * Studio: scale the logo lockups at half the UI font size rate Rather than pinning the logo, the sidebar lockup (sticker, wordmark, beta badge) and the onboarding lockup now follow the UI font size at half the rate of the change: size = base + (root - 16px) / 2, written as calc((base - 8)px + 0.5rem). A 4px font size change moves the logo by 2px, and the default 16px root renders the exact base sizes. * Studio: address review feedback on leading, grid tracks and select scrolling Numeric leading utilities (leading-3 through leading-10) derive from --spacing, so pinning spacing to px also froze their line-heights while the paired text sizes now scale. Define them as rem theme tokens so line-height follows the UI font size again; values are identical at the 16px default. Convert the grid tracks the rem-to-px codemod missed (rem followed by an underscore escaped the word boundary): the response details label column and the on-device folder rows. Make the Radix select viewport the bounded scroller instead of a wrapper div, so Radix's scroll handling and the browser scroll the same element. Restore the app's thin scrollbar with an inline style, which beats the scrollbar hiding stylesheet Radix injects at runtime. * Studio: cap voice select widths and update CI contracts --- studio/frontend/src/app/provider.tsx | 8 +- .../frontend/src/components/app-sidebar.tsx | 34 ++-- .../components/assistant-ui/audio-player.tsx | 2 +- .../message-response-details-sheet.tsx | 4 +- .../assistant-ui/message-timing.tsx | 2 +- .../src/components/assistant-ui/reasoning.tsx | 2 +- .../src/components/assistant-ui/sources.tsx | 2 +- .../src/components/assistant-ui/thread.tsx | 30 +-- .../assistant-ui/tool-ui-knowledge-base.tsx | 2 +- .../assistant-ui/tool-ui-render-html.tsx | 2 +- .../src/components/floating-monitor.tsx | 6 +- .../src/components/llama-update-banner.tsx | 8 +- .../frontend/src/components/section-card.tsx | 2 +- .../src/components/tauri/startup-screen.tsx | 2 +- .../src/components/tauri/update-banner.tsx | 16 +- .../src/components/tauri/update-screen.tsx | 4 +- .../src/components/tauri/window-titlebar.tsx | 8 +- studio/frontend/src/components/ui/chart.tsx | 2 +- .../src/components/ui/copyable-error-chip.tsx | 6 +- .../frontend/src/components/ui/data-table.tsx | 2 +- studio/frontend/src/components/ui/dialog.tsx | 2 +- .../src/components/ui/dropdown-menu.tsx | 17 +- .../src/components/ui/input-group.tsx | 4 +- studio/frontend/src/components/ui/select.tsx | 12 +- studio/frontend/src/components/ui/sidebar.tsx | 8 +- .../src/components/web/update-banner.tsx | 8 +- .../frontend/src/features/auth/login-page.tsx | 2 +- .../features/chat/artifacts/artifact-card.tsx | 4 +- .../frontend/src/features/chat/chat-page.tsx | 38 ++-- .../features/chat/chat-providers-dialog.tsx | 8 +- .../src/features/chat/chat-settings-sheet.tsx | 68 +++---- .../chat/components/chat-search-dialog.tsx | 6 +- .../chat/components/context-usage-bar.tsx | 4 +- .../chat/components/model-load-status.tsx | 12 +- .../components/openai-code-exec-section.tsx | 14 +- .../chat/components/project-switcher.tsx | 2 +- .../chat/hooks/use-chat-model-runtime.ts | 2 +- .../features/chat/permission-mode-select.tsx | 2 +- .../src/features/chat/projects-page.tsx | 16 +- .../prompt-storage/prompt-storage-dialog.tsx | 10 +- .../src/features/chat/thread-sidebar.tsx | 2 +- .../data-recipes/pages/data-recipes-page.tsx | 8 +- .../export/components/export-run-panel.tsx | 24 +-- .../export/components/method-picker.tsx | 2 +- .../export/components/quant-picker.tsx | 10 +- .../src/features/export/export-page.tsx | 36 ++-- .../features/hub/catalog/catalog-states.tsx | 32 ++-- .../hub/catalog/dataset-download-section.tsx | 2 +- .../src/features/hub/catalog/dot-tag.tsx | 2 +- .../features/hub/catalog/download-card.tsx | 2 +- .../catalog/external-link-confirm-dialog.tsx | 4 +- .../hub/catalog/gguf-download-card.tsx | 10 +- .../hub/catalog/gguf-status-cards.tsx | 4 +- .../features/hub/catalog/hub-detail-view.tsx | 2 +- .../features/hub/catalog/hub-option-menu.tsx | 4 +- .../features/hub/catalog/hub-section-row.tsx | 2 +- .../hub/catalog/local-dataset-card.tsx | 2 +- .../hub/catalog/local-on-device-card.tsx | 20 +- .../src/features/hub/catalog/model-card.tsx | 6 +- .../features/hub/catalog/model-inspector.tsx | 36 ++-- .../src/features/hub/catalog/model-readme.tsx | 20 +- .../hub/catalog/models-catalog-lists.tsx | 12 +- .../hub/catalog/models-catalog-rows.tsx | 30 +-- .../features/hub/catalog/models-header.tsx | 4 +- .../src/features/hub/catalog/models-table.tsx | 46 ++--- .../features/hub/catalog/models-toolbar.tsx | 10 +- .../hub/catalog/on-device-folders-dialog.tsx | 24 +-- .../src/features/hub/catalog/owner-avatar.tsx | 8 +- .../hub/catalog/owner-scope-toggle.tsx | 2 +- .../features/hub/catalog/recent-searches.tsx | 6 +- .../hub/catalog/safetensors-download-card.tsx | 2 +- .../hub/catalog/sampling-settings-dialog.tsx | 10 +- .../src/features/hub/catalog/shared.tsx | 4 +- .../hub/catalog/transport-conflict-dialog.tsx | 2 +- .../features/hub/catalog/transport-toggle.tsx | 2 +- .../hub/components/hf-token-indicator.tsx | 6 +- .../features/hub/components/page-heading.tsx | 4 +- .../download-manager-panel.tsx | 8 +- .../download-progress-bar.tsx | 2 +- studio/frontend/src/features/hub/hub-page.tsx | 2 +- studio/frontend/src/features/hub/hub.css | 78 ++++---- .../chat-template-editor-dialog.tsx | 4 +- .../components/model-config-page.tsx | 24 +-- .../components/model-selector.tsx | 14 +- .../model-selector/folder-browser.tsx | 10 +- .../components/model-selector/pickers.tsx | 62 +++---- .../components/model-selector/pill-tabs.tsx | 2 +- .../components/native-model-chip.tsx | 2 +- .../components/native-model-drop-overlay.tsx | 4 +- .../components/steps/model-selection-step.tsx | 4 +- .../components/steps/model-type-step.tsx | 2 +- .../onboarding/components/wizard-sidebar.tsx | 8 +- .../profile-personalization-panel.tsx | 4 +- .../rag/components/document-preview-sheet.tsx | 2 +- .../rag/components/document-status-chip.tsx | 2 +- .../rag/components/project-sources-panel.tsx | 4 +- .../components/retrieval-settings-section.tsx | 22 +-- .../recipe-studio/components/block-sheet.tsx | 4 +- .../executions/execution-sidebar.tsx | 2 +- .../components/executions/executions-view.tsx | 2 +- .../inline/inline-category-badges.tsx | 6 +- .../components/inline/inline-field.tsx | 2 +- .../components/inline/inline-llm.tsx | 2 +- .../components/inline/inline-seed.tsx | 6 +- .../components/recipe-graph-node.tsx | 4 +- .../components/recipe-studio-header.tsx | 10 +- .../runtime/execution-progress-island.tsx | 16 +- .../shared/available-references-inline.tsx | 14 +- .../models/local-recipe-model-selector.tsx | 20 +- .../recipe-studio/dialogs/preview-dialog.tsx | 2 +- .../dialogs/seed/seed-dialog.tsx | 2 +- .../tool-profile/tool-profile-dialog.tsx | 6 +- .../easy/github-crawler-easy-view.tsx | 2 +- .../recipe-studio/recipe-studio-page.tsx | 2 +- .../features/recipe-studio/utils/ui-tones.ts | 6 +- .../components/remote-code-consent-dialog.tsx | 8 +- .../settings/components/api-key-row.tsx | 4 +- .../components/api-monitor-console.tsx | 10 +- .../settings/components/color-picker.tsx | 2 +- .../settings/components/create-key-form.tsx | 2 +- .../components/embedding-model-combobox.tsx | 4 +- .../settings/components/key-reveal-card.tsx | 2 +- .../settings/components/language-select.tsx | 2 +- .../components/sidebar-menu-customizer.tsx | 4 +- .../components/update-studio-instructions.tsx | 4 +- .../components/uploaded-files-dialog.tsx | 6 +- .../settings/components/usage-examples.tsx | 34 ++-- .../src/features/settings/settings-dialog.tsx | 14 +- .../features/settings/tabs/resources-tab.tsx | 4 +- .../src/features/settings/tabs/voice-tab.tsx | 12 +- .../src/features/studio/history-card-grid.tsx | 12 +- .../studio/recent-trainings-section.tsx | 2 +- .../sections/charts/chart-settings-sheet.tsx | 2 +- .../sections/charts/eval-loss-chart-card.tsx | 8 +- .../sections/charts/grad-norm-chart-card.tsx | 4 +- .../charts/learning-rate-chart-card.tsx | 4 +- .../charts/training-loss-chart-card.tsx | 6 +- .../dataset-preview-dialog-mapping.tsx | 14 +- .../sections/dataset-preview-dialog.tsx | 20 +- .../studio/sections/dataset-section.tsx | 14 +- .../studio/sections/model-section.tsx | 16 +- .../studio/sections/params-section.tsx | 16 +- .../studio/sections/progress-section.tsx | 22 +-- .../studio/sections/s3-config-form.tsx | 2 +- .../studio/sections/training-section.tsx | 4 +- .../src/features/studio/studio-page.tsx | 2 +- .../studio/training-start-overlay.tsx | 8 +- .../features/tour/components/guided-tour.tsx | 8 +- studio/frontend/src/index.css | 171 ++++++++++++------ .../test_chat_thinking_compact_layout.py | 2 +- .../studio/test_compact_dropdown_submenus.py | 2 +- .../test_studio_text_descender_clipping.py | 2 +- .../test_voice_settings_select_width.py | 16 ++ 153 files changed, 860 insertions(+), 762 deletions(-) create mode 100644 tests/studio/test_voice_settings_select_width.py diff --git a/studio/frontend/src/app/provider.tsx b/studio/frontend/src/app/provider.tsx index e6c89b9cd7..275c3c6623 100644 --- a/studio/frontend/src/app/provider.tsx +++ b/studio/frontend/src/app/provider.tsx @@ -213,7 +213,7 @@ function TauriUpdateLayer({ } return ( -
-
+
unsloth
-
+
{t("shell.beta")}
@@ -1219,7 +1221,7 @@ export function AppSidebar() {
hidden={isMobile}
>
{t("shell.navigation.search")}
-
+
{isMacPlatform ? "⌘K" : "Ctrl+K"}
@@ -1536,7 +1538,7 @@ export function AppSidebar() {
className="sidebar-nav-btn h-[33px] rounded-full gap-[8.5px] pl-3 pr-2.5 font-medium group-hover/recent-item:pr-16 group-has-[.sidebar-row-action[data-state=open]]/recent-item:pr-8"
>
Generated image
{overlay.metadata ? ( -+
{overlay.metadata}
) : null} @@ -1390,7 +1390,7 @@ const ComposerAnimated: FC<{ disableQueue?: boolean; }> = ({ disabled, threadId, menuSide, disableQueue }) => { return ( -
+
{canvas.text}
+
{sizeLabel ? `${sizeLabel} download · ` : ""}No restart needed after update
@@ -209,7 +209,7 @@ export function LlamaUpdateBanner({LLMs can make mistakes. Double-check responses. @@ -651,7 +651,7 @@ const LoraCompareContent = memo(function LoraCompareContent({ handleName="base" header={
When off, all connections are disabled.
@@ -1616,7 +1616,7 @@ export function ChatProvidersSettings({ {provider.name} - + {provider.models.length}{" "} {provider.models.length === 1 ? "model" : "models"} @@ -1631,7 +1631,7 @@ export function ChatProvidersSettings({ ) : null}