Skip to content

Add markdown typography settings - #4106

Closed
lawrencecchen wants to merge 4 commits into
mainfrom
feature-markdown-typography-settings
Closed

lawrencecchen wants to merge 4 commits into
mainfrom
feature-markdown-typography-settings

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Adds cmux.json markdown typography settings for body font family, body size, per-heading sizes, code block font family, and code block size.
  • Routes those settings into the markdown WebView through theme CSS variables so open panels update through UserDefaults/AppStorage after config reload.
  • Documents the new markdown section in the configuration docs and JSON schema.

Acceptance criteria

  • Markdown preview uses existing typography by default.
  • Users can configure markdown.fontFamily, markdown.fontSize, markdown.headingSizes.h1 through h6, markdown.codeBlockFontFamily, and markdown.codeBlockFontSize.
  • Invalid markdown typography values are ignored per field while valid sibling settings still apply.
  • cmux open and cmux markdown open continue to use the markdown preview surface by default.

Verification

  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug -destination "platform=macOS,arch=arm64" -derivedDataPath /tmp/cmux-mdtypo-tests -only-testing:cmuxTests/MarkdownPanelTests -only-testing:cmuxTests/KeyboardShortcutSettingsFileStoreTests/testSettingsFileStoreAppliesMarkdownTypographySettings -only-testing:cmuxTests/KeyboardShortcutSettingsFileStoreTests/testSettingsFileStoreIgnoresInvalidMarkdownTypographySettings test
  • bun run lint (passes with existing warnings outside this change)
  • VERCEL_ENV=preview SKIP_ENV_VALIDATION=1 ... bun run build
  • ./scripts/reload.sh --tag mdtypo

Demo

  • Tagged local dogfood build: http://127.0.0.1:17320/mdtypo
  • Full cloud recording not captured yet. This is a config/rendering follow-up and the PR includes unit coverage plus tagged local build verification.

Note

Medium Risk
Moderate risk because it adds new settings parsing/validation and pushes additional CSS variables into the Markdown WebView theme, which could affect markdown rendering and live updates if misapplied.

Overview
Adds a new markdown section to cmux.json to configure Markdown preview typography (body/heading/code-block font families and sizes), with per-field validation so invalid values are ignored while valid siblings still apply.

Threads the resolved typography from UserDefaults/@AppStorage through MarkdownPanelView into MarkdownWebTheme, and updates the markdown viewer shell.html to consume the new CSS variables for font stacks and heading/code sizes.

Updates the config template, JSON schema, docs, and unit tests to cover CSS-variable output and settings-file application/invalid-value handling.

Reviewed by Cursor Bugbot for commit f34bd19. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Adds configurable Markdown typography in cmux.json and introduces unified View Zoom controls across Markdown, Browser, and text previews. Settings apply live via CSS variables and WebView zoom; zoom is accessible by shortcuts, menu, command palette, and a new header popover.

  • New Features

    • Markdown typography: fontFamily, fontSize, headingSizes.h1–h6, codeBlockFontFamily, codeBlockFontSize; unset headings derive from fontSize; frontmatter uses code font/size.
    • Live updates: @AppStorage → Markdown theme CSS variables and shell.html font/size variables; docs, cmux.schema.json, template updated; tests cover parsing and CSS output.
    • View Zoom: app-scoped zoom commands unify browserZoomIn/Out/Reset across Browser, Markdown, and text preview; per-panel zoom factor persists and updates WKWebView.pageZoom and text editor font size; command palette entries appear when supported; new header “View Zoom” popover with +/- buttons, reset, and slider; PDF/image viewers handle zoom key equivalents.
  • Bug Fixes

    • Markdown preview observes @AppStorage typography so open panels update without reload.
    • Zoom shortcuts are ignored when a focused terminal should handle them.

Written for commit 314a3c9. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Customizable markdown typography: body font, heading sizes (h1–h6), and code block font persisted and applied via CSS variables.
    • Unified view zoom: per-panel zoom controls, header zoom button, keyboard shortcuts routed to focused view, and zoom for browser/markdown/text/image/PDF previews.
  • Documentation

    • Added markdown settings to docs, example configuration, and JSON schema.
  • Tests

    • Added tests for typography parsing/validation, theme CSS variables, and view-zoom shortcut behavior.
  • Localization

    • Added localized strings for zoom controls.

Review Change Stack

@vercel

vercel Bot commented May 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 14, 2026 0:03am
cmux-staging Building Building Preview, Comment May 14, 2026 0:03am

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented May 13, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds user-configurable markdown preview typography and a unified view zoom system. It introduces a typed typography model, settings-file parsing and normalization, UI persistence and theme wiring (CSS variables), schema/docs updates, project build entries, zoom primitives/protocols, panel integrations, key routing, UI controls, and tests.

Changes

Markdown Typography Settings Feature

Layer / File(s) Summary
Typography Data Model and Validation
Sources/Panels/MarkdownTypographySettings.swift
MarkdownWebTypography and MarkdownTypographySettings provide typed fields, CSS variable formatting, heading-size derivation, normalization/sanitization, and range clamping.
Settings File Parsing Integration
Sources/KeyboardShortcutSettingsFileStore.swift, Sources/KeyboardShortcutSettingsFileStore+Template.swift
parseSettingsFile recognizes markdown; parseMarkdownSection and parseMarkdownSize validate and persist normalized typography values; default template includes markdown.
UI State Management and Theme Integration
Sources/Panels/MarkdownPanelView.swift, Sources/Panels/MarkdownWebRenderer.swift
Adds @AppStorage keys for typography, computes markdownTypography, passes typography into MarkdownWebTheme.resolve, and merges typography variables into theme.cssVariables used by the renderer.
Web Rendering CSS Parameterization
Resources/markdown-viewer/shell.html, Sources/Panels/MarkdownWebRenderer.swift
shell.html now reads --cmux-markdown-* variables for body and code block fonts/sizes; renderer wiring injects typography CSS variables into the web view and applies page zoom.
Schema Definition and Documentation
web/data/cmux.schema.json, web/app/[locale]/docs/configuration/page.tsx
Adds markdown schema with typography properties and bounds; documentation page includes markdown in section order and example configuration.
Project Configuration and Tests
GhosttyTabs.xcodeproj/project.pbxproj, cmuxTests/MarkdownPanelTests.swift, cmuxTests/WorkspaceUnitTests.swift
Adds new Swift sources to the Xcode target; tests verify theme CSS variables and settings-file parsing behavior (valid and invalid inputs).

View Zoom System

Layer / File(s) Summary
Zoom primitives and protocol
Sources/Panels/ViewZoomControls.swift
Adds ViewZoomCommand, ViewZoomControl helpers (normalization, applying commands, percent formatting, editor-size conversions) and ViewZoomControlling protocol with default impl.
Panel implementations and header UI
Sources/Panels/BrowserPanel.swift, Sources/Panels/FilePreviewPanel.swift, Sources/Panels/MarkdownPanel.swift, Sources/Panels/PanelContentView.swift
Panels conform to ViewZoomControlling, expose viewZoomFactor, implement setViewZoomFactor/performViewZoomCommand, and header UI gets PanelHeaderViewZoomButton bound to panel zoom.
File preview key handling and image/pdf wiring
Sources/Panels/FilePreview*.swift
Adds onViewZoomCommand callbacks, performKeyEquivalent overrides, and dispatchers mapping zoom commands to existing PDF/image zoom APIs.
Text editor zoom pipeline
Sources/Panels/FilePreviewTextEditor.swift
Extends text-edit protocol for zoom, applies preview font size from panel zoom, intercepts zoom keys to update panel zoom, and refactors smartMagnify/clamping.
Shortcut routing & app wiring
Sources/AppDelegate.swift, Sources/ContentView.swift, Sources/KeyboardShortcutContext.swift, Sources/TabManager.swift, Sources/cmuxApp.swift
Zoom shortcuts routed through AppDelegate → TabManager.performViewZoomCommand; command palette context key panelSupportsViewZoom; menu/command handlers invoke unified dispatcher; browser-specific zoom helpers removed.
Tests: keyboard & UI
cmuxTests/*
Adds tests for unbound shortcut handling, application-scoped zoom shortcuts, keyboard-driven zoom behavior, clamping, and MarkdownPanel zoom persistence across modes.

Sequence Diagram

sequenceDiagram
  participant User
  participant SettingsFile
  participant MarkdownPanelView
  participant MarkdownTypographySettings
  participant MarkdownWebTheme
  participant WebRenderer
  participant AppDelegate
  participant TabManager
  participant Panel as FocusedPanel

  User->>SettingsFile: edit `markdown` section
  SettingsFile->>MarkdownTypographySettings: parse & normalize
  MarkdownTypographySettings->>MarkdownPanelView: read via AppStorage/resolved
  MarkdownPanelView->>MarkdownWebTheme: resolve(backgroundColor, typography)
  MarkdownWebTheme->>WebRenderer: cssVariables
  WebRenderer->>WebRenderer: render with CSS vars

  User->>AppDelegate: zoom shortcut
  AppDelegate->>TabManager: performViewZoomCommand(command)
  TabManager->>Panel: performViewZoomCommand(command) (if conforms)
  Panel->>Panel: update viewZoomFactor / apply zoom
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I nibble fonts and size with care,
Variables bloom upon the air,
Headings stretch and code stays neat,
Zoom hops in with steady feet,
A tiny rabbit cheers in style and flair.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Architecture Rethink ❌ Error Duplicate decision paths: configuredViewZoomShortcutCommand checks custom shortcuts while shouldLetFocusedTerminalHandleViewZoomShortcut only checks literal patterns, causing routing inconsistency. Align shouldLetFocusedTerminalHandleViewZoomShortcut logic with configuredViewZoomShortcutCommand, or simplify to return true when Ghostty terminal has focus.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Add markdown typography settings' is concise, clear, and directly summarizes the main change: adding a new markdown typography configuration system.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed New types properly marked nonisolated+Sendable. ViewZoomControlling protocol @MainActor. Conforming classes preserve @MainActor. No implicit isolation issues detected.
Cmux Swift Blocking Runtime ✅ Passed No blocking synchronization patterns introduced. New files and components use pure computation and SwiftUI state without semaphores, manual locks, or blocking waits.
Cmux No Hacky Sleeps ✅ Passed No new hacky sleeps introduced. Pre-existing setTimeout calls in shell.html are presentation-only (allowed). All other changes are Swift (separate rule), config, schema, or documentation.
Cmux Swift Concurrency ✅ Passed No concurrency violations detected. New code uses modern async/await patterns within required AppKit/SwiftUI boundaries.
Cmux Swift @Concurrent ✅ Passed No async functions or @concurrent annotations present. MarkdownWebTypography/Settings use nonisolated correctly for synchronous helpers. ViewZoomControlling protocol correctly has @MainActor.
Cmux Swift File And Package Boundaries ✅ Passed New files are small (93-161 lines) with clear focus. Oversized files <250 additions. No mixed responsibilities. MarkdownTypographySettings is pure domain. No boundary violations.
Cmux Swift Logging ✅ Passed No logging violations found. New files contain zero logging. Modified production code uses only pre-existing logging. CLI print() statements are allowed for user-facing output per the rules.
Cmux User-Facing Error Privacy ✅ Passed No privacy rule violations. Logging is generic, no sensitive data in user-facing strings, CSS variables, or configuration. No credentials or internal details exposed.
Cmux Swiftui State Layout ✅ Passed No violations of SwiftUI state layout rules. Existing legacy classes with incidental @Published additions; no GeometryReader layout issues or render-time mutations in body.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds no new NSWindow/NSPanel/NSWindowController or SwiftUI Window/WindowGroup. Only new UI is PanelHeaderViewZoomButton (SwiftUI View with popover), which is exempt from the rule.
Description check ✅ Passed The PR description is comprehensive and addresses all template sections with sufficient detail on what changed, why, and how it was tested.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature-markdown-typography-settings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@socket-security

socket-security Bot commented May 13, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​ws@​8.20.19810010092100
Addednpm/​zod@​4.4.310010010095100

View full report

@greptile-apps

greptile-apps Bot commented May 13, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a markdown section to cmux.json for configuring typography (body font/size, per-heading sizes, code-block font/size), threads the settings through UserDefaults/@AppStorage into CSS variables applied to the markdown WebView, and simultaneously refactors zoom shortcuts to work across browser, markdown, and text-preview panels via a new ViewZoomControlling protocol.

  • Typography pipeline: MarkdownTypographySettings (new nonisolated enum/struct) validates and normalizes settings, parseMarkdownSection stores them per-field into managed UserDefaults, and ten @AppStorage bindings in MarkdownPanelView observe changes and recompute MarkdownWebTypography → CSS variables on every update. Heading sizes correctly derive relative to the configured body size rather than being fixed px.
  • ViewZoomControlling protocol: BrowserPanel, MarkdownPanel, and FilePreviewPanel (text mode) all conform; TabManager.performViewZoomCommand dispatches through the protocol; AppDelegate routing consolidated into configuredViewZoomShortcutCommand + shouldLetFocusedTerminalHandleViewZoomShortcut.
  • New PanelHeaderViewZoomButton: popover zoom UI with slider and step buttons; reused across markdown and file-preview panels.

Confidence Score: 5/5

Safe to merge — all new settings parsing is per-field validated with invalid values silently ignored, CSS variable injection is adequately guarded, and the ViewZoomControlling refactor is well-contained with unit test coverage for both happy and invalid-input paths.

The change is additive and conservative: typography defaults match the existing look, heading sizes now scale with body size rather than being fixed px, and the zoom protocol unification removes ad-hoc browser-only methods. The two observations raised are cosmetic (command-palette stale key for mode-switching FilePreviewPanel) and architectural (zoom interface duplicated across FilePreviewTextEditingPanel), neither affecting correctness in shipped behavior.

Sources/Panels/FilePreviewTextEditor.swift — the protocol now carries zoom methods that belong to ViewZoomControlling; worth splitting before more conformers are added. Sources/ContentView.swift — panelSupportsViewZoom is a point-in-time snapshot and will misreport for FilePreviewPanel after a mode switch.

Important Files Changed

Filename Overview
Sources/Panels/MarkdownTypographySettings.swift New file — clean nonisolated value types for typography defaults, validation, and CSS variable generation; heading sizes correctly derived relative to body font size
Sources/Panels/MarkdownPanelView.swift Adds 10 @AppStorage bindings to drive live typography updates and a zoom button binding; markdownTypography computed property correctly re-derives heading defaults from the configured body size
Sources/Panels/ViewZoomControls.swift New file — clean enum/protocol factoring of zoom commands; ViewZoomControlling protocol with a correct default performViewZoomCommand extension
Sources/Panels/PanelContentView.swift Adds PanelHeaderViewZoomButton popover component with slider, step buttons, and percent display; self-contained and correct
Sources/Panels/FilePreviewTextEditor.swift FilePreviewTextEditingPanel protocol now carries zoom methods duplicating ViewZoomControlling; SavingTextView zoom handler works correctly but font-size clamping range silently widened from [8,36] to [6.5,39]
Sources/Panels/FilePreviewPanel.swift ViewZoomControlling added; setViewZoomFactor/performViewZoomCommand correctly guard on previewMode == .text; PDF and image zoom still routed through local performKeyEquivalent overrides
Sources/KeyboardShortcutSettingsFileStore.swift parseMarkdownSection and parseMarkdownSize correctly validate and store typography settings; per-field validation allows valid siblings to apply when one field is invalid
Sources/AppDelegate.swift Zoom shortcut routing refactored to configuredViewZoomShortcutCommand + shouldLetFocusedTerminalHandleViewZoomShortcut; correctly passes through to terminal when Ghostty is first responder
Sources/ContentView.swift panelSupportsViewZoom context key added; stale for FilePreviewPanel when previewMode changes at runtime; zoom command palette entries broadened from browser-only to any supporting panel
Resources/markdown-viewer/shell.html CSS variables for font family/size and heading sizes wired in; heading defaults are correct relative em-based fallbacks that scale properly until applyTheme fires
Sources/TabManager.swift Replaced three browser-specific zoom methods with a unified performViewZoomCommand that dispatches through the ViewZoomControlling protocol on the focused panel

Sequence Diagram

sequenceDiagram
    participant CF as cmux.json
    participant SFS as CmuxSettingsFileStore
    participant UD as UserDefaults
    participant MPV as MarkdownPanelView (@AppStorage)
    participant MTS as MarkdownTypographySettings
    participant MWR as MarkdownWebRenderer (WKWebView)

    CF->>SFS: parseMarkdownSection(section)
    SFS->>SFS: normalizedFontFamily / normalizedSize
    SFS->>UD: "managedUserDefaults[key] = .string/.double"

    UD-->>MPV: "@AppStorage observes change (10 keys)"
    MPV->>MTS: normalizedFontFamily / normalizedSize (re-validate)
    MPV->>MTS: defaultHeadingSizes(forFontSize:) for unset headings
    MPV->>MWR: MarkdownWebTheme.resolve(typography:)
    MWR->>MWR: applyTheme → CSS variables on WKWebView

    note over MPV,MWR: Zoom path
    participant TM as TabManager
    participant AD as AppDelegate (shortcut monitor)
    AD->>TM: performViewZoomCommand(.zoomIn)
    TM->>MPV: MarkdownPanel.setViewZoomFactor
    MPV->>MWR: zoomFactor binding → WKWebView pageZoom
Loading

Reviews (4): Last reviewed commit: "Add View Zoom controls" | Re-trigger Greptile

Comment on lines +3 to +66
struct MarkdownWebTypography: Equatable {
struct HeadingSizes: Equatable {
let h1: Double
let h2: Double
let h3: Double
let h4: Double
let h5: Double
let h6: Double
}

let fontFamily: String
let fontSize: Double
let headingSizes: HeadingSizes
let codeBlockFontFamily: String
let codeBlockFontSize: Double

static let `default` = MarkdownWebTypography(
fontFamily: MarkdownTypographySettings.defaultFontFamily,
fontSize: MarkdownTypographySettings.defaultFontSize,
headingSizes: HeadingSizes(
h1: MarkdownTypographySettings.defaultHeadingSizes.h1,
h2: MarkdownTypographySettings.defaultHeadingSizes.h2,
h3: MarkdownTypographySettings.defaultHeadingSizes.h3,
h4: MarkdownTypographySettings.defaultHeadingSizes.h4,
h5: MarkdownTypographySettings.defaultHeadingSizes.h5,
h6: MarkdownTypographySettings.defaultHeadingSizes.h6
),
codeBlockFontFamily: MarkdownTypographySettings.defaultCodeBlockFontFamily,
codeBlockFontSize: MarkdownTypographySettings.defaultCodeBlockFontSize
)

var cssVariables: [String: String] {
[
"--cmux-markdown-font-family": fontFamily,
"--cmux-markdown-font-size": Self.cssPixels(fontSize),
"--cmux-markdown-h1-font-size": Self.cssPixels(headingSizes.h1),
"--cmux-markdown-h2-font-size": Self.cssPixels(headingSizes.h2),
"--cmux-markdown-h3-font-size": Self.cssPixels(headingSizes.h3),
"--cmux-markdown-h4-font-size": Self.cssPixels(headingSizes.h4),
"--cmux-markdown-h5-font-size": Self.cssPixels(headingSizes.h5),
"--cmux-markdown-h6-font-size": Self.cssPixels(headingSizes.h6),
"--cmux-markdown-code-block-font-family": codeBlockFontFamily,
"--cmux-markdown-code-block-font-size": Self.cssPixels(codeBlockFontSize),
]
}

private static func cssPixels(_ value: Double) -> String {
let rounded = (value * 100).rounded() / 100
if rounded.rounded() == rounded {
return "\(Int(rounded))px"
}
return "\(rounded)px"
}
}

enum MarkdownTypographySettings {
struct HeadingSizes: Equatable {
let h1: Double
let h2: Double
let h3: Double
let h4: Double
let h5: Double
let h6: Double
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing nonisolated on pure value types

MarkdownWebTypography and MarkdownTypographySettings are pure value/static-method types with no SwiftUI or AppKit lifecycle dependency. In Swift 6's MainActor-by-default isolation mode (active in this module), both types will be implicitly @MainActor, meaning callers on any other actor must await even the pure normalizedFontFamily, normalizedSize, and resolved(defaults:) helpers unnecessarily. Both types should be prefixed with nonisolated, matching the preferred shape nonisolated struct Model: Sendable { … } and nonisolated enum Namespace { … }. MarkdownTypographySettings.resolved(defaults:) uses only UserDefaults (thread-safe) and pure math — there is no reason to require main-actor access.

Rule Used: Flag new or materially worsened Swift 6 actor isol... (source)

Comment on lines +81 to +88
static let defaultHeadingSizes = HeadingSizes(
h1: 30,
h2: 22.5,
h3: 18.75,
h4: 15,
h5: 13.13,
h6: 12.75
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Absolute heading sizes don't scale when body font size is changed

Heading sizes are stored and applied as fixed px values. When a user sets markdown.fontSize: 20, the heading defaults stay at 30, 22.5, 18.75, 15, 13.13, and 12.75 px — meaning h4 through h6 are literally smaller than the body text. The CSS fallbacks (2em, 1.5em, etc.) in shell.html scale correctly, but once applyTheme fires those are replaced by the fixed-px CSS variables. Worth documenting this in the schema description or auto-deriving default heading sizes relative to the configured body size. Was the intent for heading sizes to be absolute px only, or should they have a relative/em option? Changing body font without touching headings currently produces headings smaller than body text for h4-h6.

Comment on lines +3 to +11
struct MarkdownWebTypography: Equatable {
struct HeadingSizes: Equatable {
let h1: Double
let h2: Double
let h3: Double
let h4: Double
let h5: Double
let h6: Double
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate HeadingSizes struct in the same file

MarkdownWebTypography.HeadingSizes and MarkdownTypographySettings.HeadingSizes have identical fields (h1–h6: Double). The settings type stores defaults as MarkdownTypographySettings.HeadingSizes then copies each field one-by-one into MarkdownWebTypography.HeadingSizes at every call site. Adding a new heading level would require updating both struct definitions and every construction site. MarkdownTypographySettings.HeadingSizes can be removed and the defaults changed to use MarkdownWebTypography.HeadingSizes directly.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 86bf9be. Configure here.

Comment thread Resources/markdown-viewer/shell.html Outdated
coderabbitai[bot]
coderabbitai Bot previously requested changes May 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/MarkdownPanelTests.swift`:
- Around line 67-70: The test currently asserts only h1 and h3 CSS variables but
the setup contains headingSizes.h1...h6, so add assertions for the remaining
heading keys to fully validate the mapping; specifically, in
MarkdownPanelTests.swift extend the XCTAssertEqual checks on theme.cssVariables
to include "--cmux-markdown-h2-font-size", "--cmux-markdown-h4-font-size",
"--cmux-markdown-h5-font-size", and "--cmux-markdown-h6-font-size" using the
expected values from headingSizes.h2/h4/h5/h6 so the test verifies all h1–h6
mappings alongside the existing code-block assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8e847dad-be88-4c6b-8b63-4dd05de3e786

📥 Commits

Reviewing files that changed from the base of the PR and between 8a5dcc1 and 86bf9be.

📒 Files selected for processing (11)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Resources/markdown-viewer/shell.html
  • Sources/KeyboardShortcutSettingsFileStore+Template.swift
  • Sources/KeyboardShortcutSettingsFileStore.swift
  • Sources/Panels/MarkdownPanelView.swift
  • Sources/Panels/MarkdownTypographySettings.swift
  • Sources/Panels/MarkdownWebRenderer.swift
  • cmuxTests/MarkdownPanelTests.swift
  • cmuxTests/WorkspaceUnitTests.swift
  • web/app/[locale]/docs/configuration/page.tsx
  • web/data/cmux.schema.json

Comment thread cmuxTests/MarkdownPanelTests.swift
Comment on lines +211 to +223
private var markdownTypography: MarkdownWebTypography {
_ = markdownFontFamily
_ = markdownFontSize
_ = markdownHeadingH1Size
_ = markdownHeadingH2Size
_ = markdownHeadingH3Size
_ = markdownHeadingH4Size
_ = markdownHeadingH5Size
_ = markdownHeadingH6Size
_ = markdownCodeBlockFontFamily
_ = markdownCodeBlockFontSize
return MarkdownTypographySettings.resolved()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Split source of truth between @AppStorage and resolved()

All 10 @AppStorage properties are read only to be discarded (_ = markdownFontFamily etc.); the theme is computed entirely from a separate, independent UserDefaults.standard read via MarkdownTypographySettings.resolved(). The @AppStorage bindings act as change-notification side-channels while the actual data flows through resolved().

Today this works because both use .standard as the backing store. But if a managed-settings UserDefaults suite is ever introduced (a natural next step given the existing managedUserDefaults path in CmuxSettingsFileStore), the @AppStorage bindings would observe the managed suite while resolved() still reads .standard — the view would stop updating. The fix is to compute MarkdownWebTypography directly from the @AppStorage values instead of discarding them and re-reading from UserDefaults.

coderabbitai[bot]
coderabbitai Bot previously requested changes May 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Panels/MarkdownPanelView.swift`:
- Around line 211-223: Add a brief comment above the computed property
markdownTypography explaining the `@AppStorage` dependency pattern: that the
property intentionally reads each `@AppStorage-backed` value (markdownFontFamily,
markdownFontSize, markdownHeadingH1Size, markdownHeadingH2Size,
markdownHeadingH3Size, markdownHeadingH4Size, markdownHeadingH5Size,
markdownHeadingH6Size, markdownCodeBlockFontFamily, markdownCodeBlockFontSize)
solely to establish SwiftUI change-tracking so the view updates, and that
MarkdownTypographySettings.resolved() is used to centralize
validation/normalization from UserDefaults; keep the comment short and focused
so future maintainers understand why the values are read but not directly used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 44f99610-1989-4d93-92dd-9209eb344faf

📥 Commits

Reviewing files that changed from the base of the PR and between 86bf9be and a5a6b3f.

📒 Files selected for processing (6)
  • Resources/markdown-viewer/shell.html
  • Sources/Panels/MarkdownPanelView.swift
  • Sources/Panels/MarkdownTypographySettings.swift
  • cmuxTests/MarkdownPanelTests.swift
  • cmuxTests/WorkspaceUnitTests.swift
  • web/data/cmux.schema.json

Comment on lines +211 to +223
private var markdownTypography: MarkdownWebTypography {
_ = markdownFontFamily
_ = markdownFontSize
_ = markdownHeadingH1Size
_ = markdownHeadingH2Size
_ = markdownHeadingH3Size
_ = markdownHeadingH4Size
_ = markdownHeadingH5Size
_ = markdownHeadingH6Size
_ = markdownCodeBlockFontFamily
_ = markdownCodeBlockFontSize
return MarkdownTypographySettings.resolved()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider a brief comment explaining the @AppStorage dependency pattern.

The computed property references all @AppStorage properties (lines 212–221) to establish SwiftUI dependencies, then calls resolved() to read and normalize the values from UserDefaults. This pattern keeps validation logic centralized in MarkdownTypographySettings and ensures the view updates when any typography setting changes.

A brief comment would help future maintainers understand why the properties are read but not directly used.

📝 Suggested comment
     private var markdownTypography: MarkdownWebTypography {
+        // Reference each `@AppStorage` property to establish SwiftUI dependencies;
+        // resolved() reads and normalizes the values from UserDefaults.
         _ = markdownFontFamily
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Panels/MarkdownPanelView.swift` around lines 211 - 223, Add a brief
comment above the computed property markdownTypography explaining the
`@AppStorage` dependency pattern: that the property intentionally reads each
`@AppStorage-backed` value (markdownFontFamily, markdownFontSize,
markdownHeadingH1Size, markdownHeadingH2Size, markdownHeadingH3Size,
markdownHeadingH4Size, markdownHeadingH5Size, markdownHeadingH6Size,
markdownCodeBlockFontFamily, markdownCodeBlockFontSize) solely to establish
SwiftUI change-tracking so the view updates, and that
MarkdownTypographySettings.resolved() is used to centralize
validation/normalization from UserDefaults; keep the comment short and focused
so future maintainers understand why the values are read but not directly used.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
Sources/Panels/MarkdownPanelView.swift (1)

211-261: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Eliminate duplicated validation logic by delegating to MarkdownTypographySettings.resolved().

The computed property reimplements all the validation logic (normalizing font families, validating sizes, deriving heading sizes) that already exists in MarkdownTypographySettings.resolved(). This violates DRY and creates maintenance risk—the two implementations can drift.

Refactor to reference each @AppStorage property (establishing SwiftUI dependencies), then delegate to the centralized resolution logic.

♻️ Proposed refactor
     private var markdownTypography: MarkdownWebTypography {
-        let bodyFontSize = MarkdownTypographySettings.normalizedSize(
-            markdownFontSize,
-            range: MarkdownTypographySettings.fontSizeRange
-        ) ?? MarkdownTypographySettings.defaultFontSize
-        let derivedHeadingSizes = MarkdownTypographySettings.defaultHeadingSizes(forFontSize: bodyFontSize)
-        return MarkdownWebTypography(
-            fontFamily: MarkdownTypographySettings.normalizedFontFamily(markdownFontFamily)
-                ?? MarkdownTypographySettings.defaultFontFamily,
-            fontSize: bodyFontSize,
-            headingSizes: MarkdownWebTypography.HeadingSizes(
-                h1: markdownHeadingSize(
-                    markdownHeadingH1Size,
-                    fallback: derivedHeadingSizes.h1
-                ),
-                h2: markdownHeadingSize(
-                    markdownHeadingH2Size,
-                    fallback: derivedHeadingSizes.h2
-                ),
-                h3: markdownHeadingSize(
-                    markdownHeadingH3Size,
-                    fallback: derivedHeadingSizes.h3
-                ),
-                h4: markdownHeadingSize(
-                    markdownHeadingH4Size,
-                    fallback: derivedHeadingSizes.h4
-                ),
-                h5: markdownHeadingSize(
-                    markdownHeadingH5Size,
-                    fallback: derivedHeadingSizes.h5
-                ),
-                h6: markdownHeadingSize(
-                    markdownHeadingH6Size,
-                    fallback: derivedHeadingSizes.h6
-                )
-            ),
-            codeBlockFontFamily: MarkdownTypographySettings.normalizedFontFamily(markdownCodeBlockFontFamily)
-                ?? MarkdownTypographySettings.defaultCodeBlockFontFamily,
-            codeBlockFontSize: MarkdownTypographySettings.normalizedSize(
-                markdownCodeBlockFontSize,
-                range: MarkdownTypographySettings.codeBlockFontSizeRange
-            ) ?? MarkdownTypographySettings.defaultCodeBlockFontSize
-        )
-    }
-
-    private func markdownHeadingSize(_ raw: Double, fallback: Double) -> Double {
-        return MarkdownTypographySettings.normalizedSize(
-            raw,
-            range: MarkdownTypographySettings.headingSizeRange
-        ) ?? fallback
+        // Reference each `@AppStorage` property to establish SwiftUI dependencies.
+        _ = markdownFontFamily
+        _ = markdownFontSize
+        _ = markdownHeadingH1Size
+        _ = markdownHeadingH2Size
+        _ = markdownHeadingH3Size
+        _ = markdownHeadingH4Size
+        _ = markdownHeadingH5Size
+        _ = markdownHeadingH6Size
+        _ = markdownCodeBlockFontFamily
+        _ = markdownCodeBlockFontSize
+        // Delegate to centralized validation/resolution logic.
+        return MarkdownTypographySettings.resolved()
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Panels/MarkdownPanelView.swift` around lines 211 - 261, The
markdownTypography computed property duplicates validation/normalization logic;
replace its body to simply construct and return the resolved typography by
delegating to MarkdownTypographySettings.resolved(...) (using the local
`@AppStorage-backed` properties such as markdownFontSize, markdownFontFamily,
markdownHeadingH1Size... and
markdownCodeBlockFontFamily/markdownCodeBlockFontSize) and remove/stop using the
local markdownHeadingSize helper for heading normalization so all validation is
centralized in MarkdownTypographySettings.resolved().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@Sources/Panels/MarkdownPanelView.swift`:
- Around line 211-261: The markdownTypography computed property duplicates
validation/normalization logic; replace its body to simply construct and return
the resolved typography by delegating to
MarkdownTypographySettings.resolved(...) (using the local `@AppStorage-backed`
properties such as markdownFontSize, markdownFontFamily,
markdownHeadingH1Size... and
markdownCodeBlockFontFamily/markdownCodeBlockFontSize) and remove/stop using the
local markdownHeadingSize helper for heading normalization so all validation is
centralized in MarkdownTypographySettings.resolved().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4aa2c849-b24f-4a8f-b86c-a150d4f66fc6

📥 Commits

Reviewing files that changed from the base of the PR and between a5a6b3f and f34bd19.

📒 Files selected for processing (2)
  • Sources/Panels/MarkdownPanelView.swift
  • Sources/Panels/MarkdownTypographySettings.swift

@lawrencecchen
lawrencecchen dismissed stale reviews from coderabbitai[bot] and coderabbitai[bot] May 13, 2026 14:52

Resolved in follow-up commits a5a6b3f and f34bd19; latest CodeRabbit review is comment-only and checks pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 12106-12118: The focused-terminal zoom handoff currently only
checks browserZoomShortcutAction(...) which misses user-remapped zoom shortcuts;
update shouldLetFocusedTerminalHandleViewZoomShortcut(_:) to also consult
configuredViewZoomShortcutCommand(for:) (using the same event data:
modifierFlags, charactersIgnoringModifiers, keyCode, literal chars) before
returning false, so if configuredViewZoomShortcutCommand(...) resolves to a zoom
command the method returns true and lets the terminal (cmuxOwningGhosttyView for
firstResponder / resolvedShortcutEventWindow) handle the shortcut.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1d07bf5f-0cc9-48a4-8529-ab5bd6f43a65

📥 Commits

Reviewing files that changed from the base of the PR and between f34bd19 and 314a3c9.

📒 Files selected for processing (19)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/KeyboardShortcutContext.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/FilePreviewMagnifyingPDFView.swift
  • Sources/Panels/FilePreviewPanel.swift
  • Sources/Panels/FilePreviewTextEditor.swift
  • Sources/Panels/MarkdownPanel.swift
  • Sources/Panels/MarkdownPanelView.swift
  • Sources/Panels/MarkdownWebRenderer.swift
  • Sources/Panels/PanelContentView.swift
  • Sources/Panels/ViewZoomControls.swift
  • Sources/TabManager.swift
  • Sources/cmuxApp.swift
  • cmuxTests/BrowserConfigTests.swift
  • cmuxTests/KeyboardShortcutContextTests.swift
  • cmuxTests/WindowAndDragTests.swift

Comment thread Sources/AppDelegate.swift
Comment on lines +12106 to +12118
private func shouldLetFocusedTerminalHandleViewZoomShortcut(_ event: NSEvent) -> Bool {
let targetWindow = resolvedShortcutEventWindow(event) ?? event.window ?? NSApp.keyWindow ?? NSApp.mainWindow
guard let firstResponder = targetWindow?.firstResponder,
cmuxOwningGhosttyView(for: firstResponder) != nil else {
return false
}
return browserZoomShortcutAction(
flags: event.modifierFlags,
chars: event.charactersIgnoringModifiers ?? "",
keyCode: event.keyCode,
literalChars: event.characters
) != nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Terminal handoff check misses remapped zoom shortcuts

shouldLetFocusedTerminalHandleViewZoomShortcut only returns true when browserZoomShortcutAction(...) matches the literal key pattern. That bypasses custom-configured zoom bindings (which do resolve in configuredViewZoomShortcutCommand(for:)), so terminal-focused remapped zoom shortcuts can be incorrectly consumed by AppDelegate.

🔧 Suggested fix
 private func shouldLetFocusedTerminalHandleViewZoomShortcut(_ event: NSEvent) -> Bool {
     let targetWindow = resolvedShortcutEventWindow(event) ?? event.window ?? NSApp.keyWindow ?? NSApp.mainWindow
     guard let firstResponder = targetWindow?.firstResponder,
           cmuxOwningGhosttyView(for: firstResponder) != nil else {
         return false
     }
-    return browserZoomShortcutAction(
-        flags: event.modifierFlags,
-        chars: event.charactersIgnoringModifiers ?? "",
-        keyCode: event.keyCode,
-        literalChars: event.characters
-    ) != nil
+    return true
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/AppDelegate.swift` around lines 12106 - 12118, The focused-terminal
zoom handoff currently only checks browserZoomShortcutAction(...) which misses
user-remapped zoom shortcuts; update
shouldLetFocusedTerminalHandleViewZoomShortcut(_:) to also consult
configuredViewZoomShortcutCommand(for:) (using the same event data:
modifierFlags, charactersIgnoringModifiers, keyCode, literal chars) before
returning false, so if configuredViewZoomShortcutCommand(...) resolves to a zoom
command the method returns true and lets the terminal (cmuxOwningGhosttyView for
firstResponder / resolvedShortcutEventWindow) handle the shortcut.

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 314a3c9b Deployed May 14, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants