feat: Make UI resizable - #91
Merged
Merged
Conversation
big-guy
marked this pull request as ready for review
May 25, 2026 21:56
big-guy
marked this pull request as draft
May 25, 2026 22:06
big-guy
force-pushed
the
ui-resize
branch
2 times, most recently
from
May 26, 2026 00:30
32a0e17 to
6a9b929
Compare
big-guy
marked this pull request as ready for review
May 26, 2026 00:51
Collaborator
Author
|
I think this is good to go. The important thing to look at is CLAUDE.md because this is supposed to steer future changes to use the text-* and icon-* classes so things resize consistently going forward. |
big-guy
force-pushed
the
ui-resize
branch
3 times, most recently
from
May 26, 2026 18:06
8e110e5 to
282ce1a
Compare
Renderer was using ~19 distinct text sizes — most clustered around
text-[8-11px] with adjacent 1-2px variants doing little real work.
Collapsed them to a small canonical set:
text-[8px] / [9px] / [10px] / [11px] → text-xs (~313 sites)
text-[13px] / [15px] → text-sm (2 sites)
text-xl → text-lg (CommandCenter/Guide/JsonModeChat heads)
text-4xl → text-3xl (Guide hero)
text-5xl → text-3xl (NewProject/NewWorktree heros)
Also replaced inline style={{ fontSize: '13px' }} on form inputs
(NewProject, NewWorktree, ReportIssue) with text-sm via className —
prerequisite for the upcoming root-font-size UI scale slider, which
only affects rem-based sizes.
Left intentionally:
- Monaco / XTerminal configurable font sizes
- PerfMonitor HUD inline SVG numerics
- JsonModeChat's --chat-{body,chrome,meta}-text CSS variable system
- ReviewDiffPane inline styles inside Monaco view zones
Adds a convention rule (item 10) telling future Claudes which Tailwind text classes and icon sizing pattern to use so the upcoming uiScale slider can resize the whole UI in lockstep. Inline pixel sizes break that — they don't scale with root font-size — so the rule calls them out explicitly along with the legitimate exceptions (Monaco, HUD, chat-density vars, view zones, QR code).
The lucide size prop bakes a pixel literal into the SVG width/height
attributes, so icons stay fixed regardless of the root font-size the
upcoming uiScale slider will control. Replaced every
<Icon size={N} ...> with an equivalent w-N h-N className so icons
scale via root font-size.
Mapping:
size 9/10 → w-2.5 h-2.5
size 11/12 → w-3 h-3
size 13/14 → w-3.5 h-3.5
size 16 → w-4 h-4
size 18 → w-[1.125rem] h-[1.125rem]
size 20 → w-5 h-5
size 26 → w-[1.625rem] h-[1.625rem]
size 32 → w-8 h-8
QRCodeSVG size={128} kept — it's a non-lucide pixel-sized component.
AgentIcon: dropped size prop, takes only className (default w-4 h-4).
The inner ClaudeIcon/CodexIcon SVGs use width/height "1em" so they
fill the className-sized box.
RepoIcon: restructured to be font-size driven — width/height "1em"
on the outer span with the letter at "0.6em", so callers control
the size via text-* classes (and the letter scales proportionally).
Adds a new four-step UI density setting that the upcoming Appearance
slider and Cmd+= / Cmd+- hotkeys will drive. Default is 'small' (16px
root), so existing users see no behavior change until they pick a
larger rung.
Single-source-of-truth SCALES table in shared/state/settings.ts:
small 16px +0 terminal offset
medium 18px +2
large 20px +4
x-large 22px +6
Adding a fifth rung later is a one-line addition there — every
consumer (App.tsx font-size effect, Settings slider, XTerminal,
hotkey handlers) reads from this same array.
Wired end-to-end per the 5-file checklist in CLAUDE.md:
- shared/state/settings.ts: UiScale type, SCALES table, scaleSpec
helper, field, event variant, reducer case, initial value, test
- main/build-initial-state.ts: seed from config, with migration for
earlier internal names (compact→small, normal→medium, roomy→large)
- main/persistence.ts: Config.uiScale accepts old + new names
- main/index.ts: config:setUiScale IPC handler (dispatches event,
persists; deletes key when value is the default 'small')
- renderer/types.ts + build-backend.ts: setUiScale on the backend
- renderer/App.tsx: documentElement.style.fontSize effect
Slider UI, hotkeys, and the XTerminal offset follow in later commits.
Adds the user-facing UI-scale slider above "Terminal font" in Appearance. Native range input drives idx into the SCALES table (also clickable text labels under each notch), so the keyboard works for free. Critical UX: - Dragging the slider does NOT immediately resize the whole app. The drag updates a local draft; the live UI stays at the persisted scale until Save. - A small scoped preview box renders below the slider using inline em-based sizes anchored to the draft rung, so the user sees what the chosen size looks like without committing. - Save/Cancel appear only when dirty. Closing Settings without saving auto-reverts (draft is local state, dropped on unmount). Scroll re-anchor: when uiScale changes the root font-size shifts and every section reflows — the user's scroll position no longer points at the section they were reading. We capture activeSection in a ref and call scrollToSection on the next animation frame after App.tsx's font-size effect runs, so the user stays anchored to the same heading. Data-driven: the slider, labels, preview rootPx, and (later) hotkey ladder all read from SCALES in shared/state/settings.ts. Adding a fifth rung means appending one row there.
Adds two new hotkey actions (uiScaleUp / uiScaleDown) bound to
Cmd+= and Cmd+- by default. They step through the SCALES ladder
defined in shared/state/settings.ts and clamp at both ends — so
hammering Cmd+- on 'small' or Cmd+= on 'x-large' is a no-op rather
than wrapping.
Implementation reads from the same SCALES array the slider and
XTerminal use, so the four current rungs (and any future fifth) need
no separate ladder maintenance.
ACTION_LABELS updated for the hotkey cheatsheet ("Increase UI size" /
"Decrease UI size").
The terminal font size sat outside the rem-based UI scaling because xterm wants concrete pixel sizes. Without this commit, switching the new UI-size slider to a larger rung enlarged the whole UI except the xterm panes — which then looked dwarfed inside roomier chrome. Solution: keep `currentFontSize` as the user's configured terminal size (untouched by the slider) and add a separate `uiScaleOffset` derived from the active UiScale (see SCALES in shared/state/settings): small=+0, medium=+2, large=+4, x-large=+6. The pixel size shipped to xterm is `currentFontSize + uiScaleOffset`. Wired into the existing initFontCache subscription so the offset hydrates from the state snapshot at boot and updates live when `settings/uiScaleChanged` fires. The user's terminalFontSize preference is never overwritten by scale toggles — toggling back to 'small' restores exactly the original pixel size.
UI size is the control a user is most likely to be adjusting on each visit to Appearance — the moment they want to try a different scale, they don't want to scroll past the mode/theme/custom-themes blocks to find it. Promoted it to the top of the section, with a horizontal divider separating it from the theme cluster below. Intro tweaked from "Pick a color theme … takes effect immediately" to the more inclusive "Size, theme, and terminal font for the whole app", since UI size uses Save/Cancel rather than applying immediately.
Defines a small icon-size scale in styles.css via Tailwind v4's @Utility directive, parallel to the text-* ladder: icon-2xs 10px (0.625rem) icon-xs 12px (0.75rem) icon-sm 14px (0.875rem) icon-base 16px (1rem) — default icon-lg 20px (1.25rem) icon-xl 32px (2rem) Replaces 323 sites of `w-X h-X` on icon JSX (lucide + AgentIcon) with the matching alias. Non-icon fixed-size boxes — checkboxes, color swatches, decorative dots, avatar circles, step badges — keep their `w-N h-N` literals; those aren't icons and the alias name would mislead. CLAUDE.md item 10 updated to teach future contributors: - what classes to pick - what NOT to rename (the non-icon boxes above) - how to extend the scale (only when a one-off hits its third callsite, add an @Utility row in styles.css) The 18px and 26px one-offs remain as `w-[1.125rem] h-[1.125rem]` / `w-[1.625rem] h-[1.625rem]` arbitrary values — adding a rung for single-digit usage isn't worth the API surface.
The compact/normal/roomy names lived only in this branch's earlier experiment branch (terminal-agent-view-experiment) and never shipped. Migration handling for them in build-initial-state + the legacy union in persistence.ts were defensive code paths for a fork that no user ever ran, so they're dead weight. Removed.
Adds a fifth scale step at 14px / terminal offset -2 for users who want denser chrome than the default. The SCALES table is the single source of truth so the slider, hotkey ladder, App.tsx font-size effect, and XTerminal offset all pick it up automatically — the only other touches are widening the legacy type unions on the IPC handler, preload types, backend method, and persistence Config.uiScale. Default still 'small'; unknown disk values still fall back to 'small'.
Three new entries in the View submenu with accelerators: Increase UI Size ⌘+ (Shift+= on US layouts) Decrease UI Size ⌘- Reset UI Size ⌘= Each fires a signal that App.tsx subscribes to and steps through the SCALES table or jumps back to 'small'. The renderer-side keyboard hotkeys (uiScaleUp / uiScaleDown / uiScaleReset) are kept for non-Electron (web-client) contexts where there is no menu, and use the same bindings. Reset goes to 'small' (the default rung), not 'medium' — users who shrink to x-small + ⌘= expect the canonical baseline.
All 19 <input type="checkbox"> sites now carry icon-base so the hit target grows with the rest of the UI. Previously about half were unsized (relying on the browser's 13px default, which stays fixed when the user picks medium/large/x-large) and the rest used a mix of w-3.5 h-3.5 / w-4 h-4. Standardizing on icon-base makes them all consistent at 1rem. CLAUDE.md item 10 updated to remove the checkbox carve-out — treat checkboxes like icons. The non-icon exception remains for color swatches, decorative dots, and avatar circles, where the design intent is "this size, not bigger."
Recent upstream commits introduced six new lucide <Icon size={N}/> sites
(scratchpad, rewind menu, settings search). Snap them onto the icon-*
ladder so they scale with uiScale like the rest of the app.
Code2 size={12} → icon-xs (AllFilesPanel editor button)
RotateCcw size={12} → icon-xs (JsonModeChat rewind menu)
Search size={11} → icon-xs (Settings sidebar search input)
X size={10} → icon-2xs (Settings search clear button)
Chevron* size={12} → icon-xs (Settings hotkeys family expand)
QRCodeSVG size={128} is the existing exception — it's not a lucide icon
and the pixel size is legitimately fixed.
Collaborator
Author
|
@frenchie4111 if you're looking for something to review first, this is it. This one easily gets out of date because it changes so many style classes. |
Collaborator
frenchie4111
approved these changes
May 27, 2026
The "Harness" title in the sidebar's drag region was using rem-based classes (left-20, text-xs) that shrink with uiScale. The macOS traffic lights are native chrome positioned at a fixed pixel offset, so at X-Small (root 14px) the rem-based left-20 collapsed from 80px to 70px and the title slid underneath the traffic lights. Pinned the left offset and font-size to fixed pixels (left-[80px], text-[12px], top-[11px] was already pixel) so the title position is independent of the rem grid. Added a comment so a future contributor doesn't "fix" it back to the canonical scale.
Theme picker is the most common settings visit, so put it first. UI size now sits below Custom themes and above Terminal font, sandwiched between the two appearance clusters it relates to most (visual theme and font scaling). Removed the divider that separated the previous UI-size-then-theme layout — natural section spacing is enough. Also registers `appearance-ui-size` as a proper subsection so it appears in the Settings sidebar search and tree, matching the other appearance subsections.
The existing re-anchor effect only scrolled back to the active section after a uiScale save — if the user was reading Terminal font and saved a new size, the scroll snapped up to the top of Appearance. Track the active subsection too, and prefer it over the section when re-anchoring. Falls back to section scroll when no subsection is set (e.g. the user hasn't scrolled into a labelled subsection yet).
Collaborator
Author
fixed
that's where I put it before and was annoyed by the scrolling. I moved it and made it so we stay in the same subsection when the UI resizes now. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Make the entire app scale to a user-chosen UI size. A new
uiScalesetting drives the root
htmlfont-size, so every rem-based unit(text, icons, padding, gap) shifts in lockstep. Five rungs:
X-Small (14px), Small (16px), Medium (18px), Large (20px), X-Large (22px).
Small is the default — visually identical to today's app.
Entry points
em-based preview, Save/Cancel pattern (so dragging doesn't reflow the
app on every tick).
What had to change for "rem grid" to work
The app had drifted into ~19 ad-hoc text sizes and a mix of pixel-baked
icon sizes (lucide
size={N}prop) that would not scale. To make theslider mean something:
text-[Npx]/text-xl/4xl/5xltotext-xs/sm/base/lg/2xl/3xl(~320 sites).Removed inline
style={{ fontSize }}on form inputs that felloutside Tailwind.
icon-*aliases. New rem-based utilities defined viaTailwind v4
@utilityinstyles.css:icon-2xs / xs / sm / base / lg / xl(10–32px). Migrated ~440 sites from lucidesize={N}andraw
w-N h-Npairs. Non-icon fixed-size boxes (status dots, colorswatches, avatar circles, custom radios) intentionally keep their
w-N h-Nliterals.literals (they have to be, monospace alignment), so
XTerminalsubscribes to
settings/uiScaleChangedand applies a per-rungoffset on top of the user's chosen terminal font size.
icon-base. Standardized all<input type=\"checkbox\">to useicon-baseso the hit target scales too.State plumbing
Follows the standard slice pattern:
settings.tsgains aUiScaletype, a
SCALEStable (single source of truth for label / rootPx /terminalOffset), and a reducer case. IPC handler in main, exposed
through preload. Renderer applies
document.documentElement.style.fontSizevia an effect on
settings.uiScale. Five-file checklist obeyed; onereducer test added per new event variant.
CLAUDE.md
Added item §11 documenting the canonical text + icon sizes, the
icon-*table, and the exception list (Monaco, PerfMonitor HUD,JsonModeChat density tokens, ReviewDiffPane Monaco view zones) so
future contributors don't drift back to pixel literals.
Test plan