Skip to content

feat: Make UI resizable - #91

Merged
big-guy merged 17 commits into
ness-dev:mainfrom
big-guy:ui-resize
May 27, 2026
Merged

feat: Make UI resizable#91
big-guy merged 17 commits into
ness-dev:mainfrom
big-guy:ui-resize

Conversation

@big-guy

@big-guy big-guy commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Make the entire app scale to a user-chosen UI size. A new uiScale
setting drives the root html font-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

  • Settings → Appearance → UI size: slider with draft state + scoped
    em-based preview, Save/Cancel pattern (so dragging doesn't reflow the
    app on every tick).
  • View menu: Increase / Decrease / Reset UI Size.
  • Hotkeys: ⌘⇧+ to increase, ⌘− to decrease, ⌘= to reset.

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 the
slider mean something:

  1. Text → canonical ladder. Collapsed all text-[Npx] /
    text-xl/4xl/5xl to text-xs/sm/base/lg/2xl/3xl (~320 sites).
    Removed inline style={{ fontSize }} on form inputs that fell
    outside Tailwind.
  2. Icons → icon-* aliases. New rem-based utilities defined via
    Tailwind v4 @utility in styles.css: icon-2xs / xs / sm / base / lg / xl (10–32px). Migrated ~440 sites from lucide size={N} and
    raw w-N h-N pairs. Non-icon fixed-size boxes (status dots, color
    swatches, avatar circles, custom radios) intentionally keep their
    w-N h-N literals.
  3. Terminal font → tracked offset. xterm.js font-sizes are pixel
    literals (they have to be, monospace alignment), so XTerminal
    subscribes to settings/uiScaleChanged and applies a per-rung
    offset on top of the user's chosen terminal font size.
  4. Checkboxes → icon-base. Standardized all <input type=\"checkbox\"> to use icon-base so the hit target scales too.

State plumbing

Follows the standard slice pattern: settings.ts gains a UiScale
type, a SCALES table (single source of truth for label / rootPx /
terminalOffset), and a reducer case. IPC handler in main, exposed
through preload. Renderer applies document.documentElement.style.fontSize
via an effect on settings.uiScale. Five-file checklist obeyed; one
reducer 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

  • Drag the slider — only the scoped preview reflows, not the app.
  • Save — the whole app (sidebar, panels, dialogs, terminal) shifts together.
  • Cancel — draft drops; saved value is restored.
  • Close Settings without saving — draft auto-drops.
  • ⌘⇧+, ⌘−, ⌘= step / clamp / reset; View menu items mirror them.
  • Custom themes, theme mode pickers still appear (now below UI size).
  • Terminal fonts visibly grow with the rung.
  • At X-Large, no layout breaks (sidebar widths, modal scroll, etc.).

@big-guy big-guy changed the title text scale: consolidate to canonical text-xs/sm/base/lg/2xl/3xl feat: Make UI resizable May 25, 2026
@big-guy
big-guy marked this pull request as ready for review May 25, 2026 21:56
@big-guy
big-guy marked this pull request as draft May 25, 2026 22:06
@big-guy
big-guy force-pushed the ui-resize branch 2 times, most recently from 32a0e17 to 6a9b929 Compare May 26, 2026 00:30
@big-guy
big-guy marked this pull request as ready for review May 26, 2026 00:51
@big-guy

big-guy commented May 26, 2026

Copy link
Copy Markdown
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
big-guy force-pushed the ui-resize branch 3 times, most recently from 8e110e5 to 282ce1a Compare May 26, 2026 18:06
@big-guy
big-guy requested a review from frenchie4111 May 26, 2026 19:53
@big-guy big-guy self-assigned this May 26, 2026
big-guy added 14 commits May 26, 2026 17:43
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.
@big-guy

big-guy commented May 26, 2026

Copy link
Copy Markdown
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.

@frenchie4111

frenchie4111 commented May 27, 2026

Copy link
Copy Markdown
Collaborator

For me on small size the spotlight icons collide with the header

image

nit. Could we move the UI Size setting to be below the Mode/Theme selector in the settings?

other than that it LGTM

big-guy added 3 commits May 27, 2026 11:46
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).
@big-guy

big-guy commented May 27, 2026

Copy link
Copy Markdown
Collaborator Author

For me on small size the spotlight icons collide with the header

fixed

nit. Could we move the UI Size setting to be below the Mode/Theme selector in the settings?

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.

@big-guy
big-guy merged commit e049f40 into ness-dev:main May 27, 2026
1 check passed
@big-guy
big-guy deleted the ui-resize branch May 27, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants