Skip to content

Diff viewer: responsive toolbar that never overlaps at small widths - #6550

Merged
azooz2003-bit merged 4 commits into
mainfrom
feat-diff-toolbar-overflow
Jun 21, 2026
Merged

azooz2003-bit merged 4 commits into
mainfrom
feat-diff-toolbar-overflow

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jun 21, 2026 •

Copy link
Copy Markdown
Collaborator

What

At very small diff-panel widths, the toolbar controls overlapped/overlaid the accessory icon buttons and looked broken (reported with a screenshot). This makes the toolbar degrade gracefully via the priority+ overflow pattern: low-priority controls collapse into the existing ⋯ options menu as width shrinks, and nothing ever overlaps.

Root cause

#toolbar was a rigid grid minmax(0,1.1fr) minmax(124px,0.9fr) auto with a non-shrinking toolbar-actions. At small widths the middle 124px floor + the fixed actions column starved the left cell, whose contents overflowed (grid cells don't clip) and overlaid the accessory icons.

How

  • No-overlap guarantee: all toolbar grid tracks are minmax(0,…) and the cells use overflow-x: clip (with overflow-y: visible so the base picker / options popovers still drop below the bar); toolbar-actions can shrink. Overlap is now structurally impossible regardless of measurement.
  • Measured priority overflow: a ResizeObserver on #toolbar drives a pure resolver (toolbar-overflow.ts, unit-tested) that keeps the highest-priority controls and drops the rest as a clean priority suffix into the always-present ⋯ menu. Drop order (lowest first): external link → layout toggle → files toggle → repo select. The Base picker (primary, with its own internal shedding) and the ⋯ button are always visible. The options menu always lists the layout + external actions so anything dropped from the bar stays reachable.

At the reported extreme width the bar cleanly shows roughly [source] [Base… ▾] [⋯], accessories in the menu, no overlap.

Verification

  • New pure-resolver unit tests (8 cases: all-fit, single drop, priority-suffix, repo-dropped-last, extreme narrow, zero-width repo, non-finite budget, empty). 164 webviews tests pass; typecheck/lint/build green.
  • Visually verified at an extreme-narrow panel: controls truncate/collapse into ⋯, no overlap (vs the prior overlapping layout).

Note

This branch is stacked on #6484 (the branch picker), so the diff currently includes those commits. Once #6484 merges, I'll rebase this onto main so it shows only the toolbar change.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Make the diff viewer toolbar responsive with priority+ overflow so controls never overlap at small widths. Only accessory icons collapse into ⋯; the repo selector and Base picker stay usable at any width.

  • New Features

    • Priority+ toolbar overflow: only accessory icons overflow into ⋯ as width shrinks (drop order: external → layout → files). The repo select is always shown and truncates; Base and ⋯ are always visible. Measured with ResizeObserver and a pure resolveToolbarOverflow resolver; overflowed actions remain in the options menu.
    • Searchable Base picker: grouped refs with fuzzy filter, keyboard nav, and raw‑ref escape; shows head → base with reason and +ahead/−behind; regenerates the diff on pick. Works under HTTP and restored cmux-diff-viewer:// pages.
  • Bug Fixes

    • No overlap at any width: toolbar tracks use minmax(0, …) and cells overflow-x: clip (popovers still escape). Per‑file diff headers pin the diffstat and clip the header so counts never overlap the status icon.
    • Base picker popover no longer clips: now viewport‑anchored (position: fixed), portaled to document.body, clamped to the viewport, and flips above when space is tight. Outside‑click handling respects the portaled popover.

Written for commit 2c18e75. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features
    • Toolbar actions now adapt to available space by prioritizing what stays visible and moving lower-priority controls into the options menu; external and layout actions are shown contextually.
  • Bug Fixes
    • Improved narrow-layout rendering by tightening toolbar column shrink behavior and preventing horizontal spill/overlap in toolbar and diff headers.
    • Updated picker/popover to be viewport-anchored and portal-rendered for correct initial placement above diff content.
  • Tests
    • Added unit coverage for the toolbar overflow decision logic, including edge cases.

@vercel

vercel Bot commented Jun 21, 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 Jun 21, 2026 8:49pm
cmux-staging Building Building Preview, Comment Jun 21, 2026 8:49pm

@coderabbitai

coderabbitai Bot commented Jun 21, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e717275a-c4e6-4a88-b483-c52f71c2117c

📥 Commits

Reviewing files that changed from the base of the PR and between 671c114 and 2c18e75.

📒 Files selected for processing (3)
  • Resources/markdown-viewer/webviews-app/chunks/diffSurface.mjs
  • webviews/src/BranchBasePicker.tsx
  • webviews/src/styles.css

📝 Walkthrough

Walkthrough

Adds a toolbar overflow system to the diff viewer: a new toolbar-overflow.ts module defines a greedy priority-budget algorithm, useToolbarWidth.ts measures the toolbar's pixel width via ResizeObserver, App.tsx wires both together to conditionally hide toolbar controls (layout toggle, files toggle, external link) into the options menu, styles.css hardens the toolbar grid and diff header against overflow at narrow widths, and BranchBasePicker.tsx switches to viewport-anchored fixed positioning for the popover.

Changes

Toolbar Overflow and Popover Positioning

Layer / File(s) Summary
Toolbar overflow model, measurement hook, and tests
webviews/src/toolbar-overflow.ts, webviews/src/useToolbarWidth.ts, webviews/test/toolbar-overflow.test.ts
Exports ToolbarOverflowItem/Input/Result types and resolveToolbarOverflow (greedy priority-budget algorithm that greedily keeps fit items and drops a priority suffix when budget is exceeded). Exports useToolbarWidth hook that uses ResizeObserver to return a rounded pixel width or null. Tests cover all partitioning rules, edge cases for zero-width items, non-finite available widths, and empty input.
App.tsx toolbar overflow wiring and OptionsMenu fallback
webviews/src/App.tsx
Toolbar calls useToolbarWidth and resolveToolbarOverflow to derive per-control visibility booleans. Layout-toggle, files-toggle, and external-link icons are each conditionally rendered based on overflow results. OptionsMenu gains externalURL and onSetLayout props and adds secondary menu items for layout switching and external link so overflowed controls remain reachable.
Toolbar CSS grid hardening and overflow clipping
webviews/src/styles.css
Toolbar grid tracks switch to minmax(0,…) for shrink-to-zero behavior; overflow-x: clip with overflow-y: visible added to toolbar cell groups to prevent horizontal spill. Media query narrow layout gets matching minmax(0,auto) grid. Base picker popover switches from position: absolute to position: fixed for viewport-anchored rendering.
BranchBasePicker viewport-anchored popover
webviews/src/BranchBasePicker.tsx
Adds computePopoverStyle(rect) function to clamp horizontal placement to viewport bounds, prefer below-button placement, flip above when favorable, and set maxHeight based on available space. Popover state tracks computed position and only mounts dialog markup when both open and popoverStyle are ready. Effect measures button rect and attaches window resize/scroll (capture phase) listeners to keep fixed popover glued to button; cleans up listeners on close. Popover is portaled to document.body and dismiss logic expands to include portaled content.
Per-file diff header overflow prevention
webviews/src/styles.css
Diff header gains overflow-x: clip to prevent horizontal spill at narrow widths, and metadata section is set to flex-shrink: 0 so the file path side truncates instead of overlapping diffstat/status metadata.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Toolbar
  participant useToolbarWidth
  participant resolveToolbarOverflow
  participant OptionsMenu

  User->>Toolbar: window resize
  Toolbar->>useToolbarWidth: observe toolbar element
  useToolbarWidth-->>Toolbar: measured pixel width
  Toolbar->>resolveToolbarOverflow: {available, reserved, items}
  resolveToolbarOverflow-->>Toolbar: {visible, overflow} id lists
  Toolbar-->>Toolbar: render conditionally visible controls
  Toolbar->>OptionsMenu: pass externalURL, onSetLayout
  OptionsMenu-->>OptionsMenu: render secondary items for overflowed controls
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#6484: This PR's popover positioning changes for BranchBasePicker.tsx build directly on the searchable "smart-default" branch picker introduced in that PR.

Poem

🐰 When the toolbar gets too tight,
I shuffle icons out of sight—
Into the "..." menu they go,
Still reachable, just overflow!
A budget, a greedy loop, a clip—
No button shall be lost, hip hip! 🎉


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Full Internationalization ❌ Error PR introduces user-facing text "openSourceURL" label key for external link toolbar icon/menu without adding translations to any of the 20 supported locales in web/messages/. Add "openSourceURL" key with translations to all 20 locale files (en, ja, zh-CN, zh-TW, ko, de, es, fr, it, da, pl, ru, bs, ar, no, pt-BR, th, tr, km, uk) in web/messages/.
Docstring Coverage ⚠️ Warning Docstring coverage is 30.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (21 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: making the diff viewer toolbar responsive so controls overflow gracefully instead of overlapping at small widths.
Description check ✅ Passed The description covers What (overflow pattern for graceful degradation), Root cause (rigid grid design), and How (structural + measured overflow). Testing section describes verification with unit tests and visual confirmation. However, the checklist items are not marked as complete.
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 PR contains no Swift code changes—only TypeScript/React and CSS modifications. The swift-actor-isolation check applies exclusively to Swift production code and is not applicable here.
Cmux Swift Blocking Runtime ✅ Passed PR contains only TypeScript/React/CSS changes in webviews/ directory; no Swift files are modified, making the check inapplicable.
Cmux Expensive Synchronous Load ✅ Passed PR contains only webviews (TypeScript/React/CSS) changes; no Swift production code modifications present, so the Swift expensive sync load check does not apply.
Cmux Cache Substitution Correctness ✅ Passed PR introduces only transient UI measurement (useToolbarWidth hook using ResizeObserver) and pure overflow logic with no caching, persistence, or history/undo paths. Layout persistence logic appears...
Cmux No Hacky Sleeps ✅ Passed PR introduces no hacky sleeps. New toolbar-overflow files (toolbar-overflow.ts, useToolbarWidth.ts, test) contain zero sleeps/timers/polling. Pre-existing setTimeout in App.tsx is legitimate keyboa...
Cmux Algorithmic Complexity ✅ Passed All new code operates on fixed, tiny collections (≤3 toolbar items). resolveToolbarOverflow() is a single linear pass; useToolbarWidth() observes one element. Neither scales with user data or v...
Cmux Swift Concurrency ✅ Passed PR contains no Swift files; check applies only to cmux-owned Swift code. All changes are TypeScript/React webviews and CSS.
Cmux Swift @Concurrent ✅ Passed PR contains only TypeScript/React/CSS webviews changes; no Swift files are modified, so the Swift @concurrent check does not apply.
Cmux Swift File And Package Boundaries ✅ Passed This PR contains only web/TypeScript/React/CSS changes (webviews directory). No Swift production code changes exist, so the Swift file/package boundary rules do not apply.
Cmux Swiftpm Lockfiles ✅ Passed All SwiftPM lockfile requirements from swiftpm-package-resolved.md are satisfied: cmux-owned package .gitignore files don't ignore Package.resolved, all package-local and root Xcode Package.resolve...
Cmux Swift Logging ✅ Passed This PR contains no Swift code changes; it only modifies TypeScript/JavaScript/CSS webview files. The Swift logging check is inapplicable.
Cmux User-Facing Error Privacy ✅ Passed All user-facing error messages are properly sanitized. HTTP status codes logged only to console.warn (developer-only); users see localized label strings. No credentials, vendor names, or implementa...
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI or Swift code changes. All modifications are React/TypeScript web components and CSS. SwiftUI state layout check is not applicable.
Cmux Architecture Rethink ✅ Passed PR modifies only TypeScript/React/CSS webviews code; swift-architectural-rethink.md applies exclusively to Swift changes, and there are zero Swift files modified.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR contains no Swift code changes. Custom check applies only to Swift auxiliary window changes; this PR modifies only TypeScript/React webview files.
Cmux Source Artifacts ✅ Passed All 6 changed files (React components, stylesheets, TypeScript modules, and unit tests) are hand-written source code intentionally part of the product and test system. No generated artifacts, logs,...
Cmux No Test Or Debug Seam In Production Source ✅ Passed PR contains no Swift files under Sources/ paths; the check applies only to Swift production source and is not applicable.
✨ 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 feat-diff-toolbar-overflow

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.

@greptile-apps

greptile-apps Bot commented Jun 21, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes the diff-viewer toolbar responsive using a priority+ overflow pattern, so toolbar controls never overlap at narrow panel widths. A ResizeObserver-driven useToolbarWidth hook feeds a pure resolveToolbarOverflow resolver that keeps the highest-priority controls visible and collapses lower-priority ones into the always-present ⋯ menu; the BranchBasePicker popover is simultaneously moved to a document.body portal so it escapes the toolbar cell's overflow-x: clip and container-query containing block.

  • Toolbar layout fix: replaces the rigid minmax(124px, 0.9fr) grid track with minmax(0, 0.9fr) and adds overflow-x: clip / overflow-y: visible on every toolbar cell, making overlap structurally impossible regardless of content width or measurement timing.
  • Priority+ overflow: a new toolbar-overflow.ts pure resolver (6 unit tests) drops controls in priority order (external link → layout toggle → files toggle) and keeps dropped controls reachable in the ⋯ options menu.
  • Popover portal: BranchBasePicker is ported from position: absolute (clipped by the container cell) to a document.body portal with position: fixed, viewport-clamped and flip-above logic computed via computePopoverStyle.

Confidence Score: 5/5

Safe to merge — all toolbar layout changes are structural (CSS minmax + overflow-x:clip) with the JS overflow resolver as a graceful degradation layer on top, so neither component can cause overlap even if estimates are stale.

The overflow resolver is a pure function with six explicit unit tests covering every edge case, the CSS clip is the hard structural guarantee, the portal change in BranchBasePicker correctly handles both the outside-click contract and the viewport anchor lifecycle, and the ResizeObserver hook tears down cleanly. No timing-based synchronization, no hacky sleeps, no data-loss paths.

No files require special attention. All changed files are self-contained webview TypeScript/CSS — no Swift, no data persistence, no auth paths.

Important Files Changed

Filename Overview
webviews/src/toolbar-overflow.ts New pure resolver for the priority+ overflow model; clean, total function with correct priority-suffix invariant and proper NaN/non-finite handling.
webviews/src/useToolbarWidth.ts New ResizeObserver hook with correct lifecycle management (disconnect on cleanup), sub-pixel jitter deduplication via rounding, and graceful fallback when ResizeObserver is unavailable.
webviews/src/App.tsx Toolbar component wires the new overflow resolver; accessory icon buttons conditionally rendered; OptionsMenu gains layout-toggle and external-link entries as permanent fallbacks. Changes are clean.
webviews/src/BranchBasePicker.tsx Popover ported to document.body portal with position:fixed and JS-computed viewport-clamped/flip-above positioning; outside-click handler correctly updated to check both containerRef and popoverRef. One-frame delay before popover appears is intentional (avoids 0,0 flash).
webviews/src/styles.css Grid tracks changed to minmax(0,…) everywhere; overflow-x:clip / overflow-y:visible added to all toolbar cells; .base-picker-popover switched to position:fixed with inline-JS dimensions; per-file diff header diffstat pinned flex-shrink:0.
webviews/test/toolbar-overflow.test.ts Six unit tests covering all-fit, single-drop, priority-suffix invariant, extreme-narrow, NaN input, and empty-items edge cases. Test coverage is solid for the pure resolver.
Resources/markdown-viewer/webviews-app/chunks/diffSurface.mjs Minified bundle update reflecting the webview source changes; consistent with the existing repo pattern of committing compiled webview assets under Resources/.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ResizeObserver fires on #toolbar] --> B[useToolbarWidth returns width px]
    B --> C{width == null?}
    C -- yes --> D[overflow = empty Set\nrender all controls]
    C -- no --> E[resolveToolbarOverflow\navailable = width\nreserved = always-present + repo-select-min\nitems = files-toggle · layout-toggle · external-link]
    E --> F{budget fits item?}
    F -- yes, keep --> G[visible list]
    F -- no, or already dropping --> H[overflow list]
    G --> I[render icon button in toolbar]
    H --> J[hide icon button from toolbar\nkeep entry in ⋯ OptionsMenu]
    D --> I
    K[Base button clicked] --> L[open = true\npopoverStyle = null\nportal not painted]
    L --> M[useEffect fires\ncomputePopoverStyle from getBoundingClientRect]
    M --> N{more space below or above?}
    N -- below --> O[top = rect.bottom + gap\nbottom = auto\nmaxHeight = spaceBelow]
    N -- above --> P[top = auto\nbottom = viewportH - rect.top + gap\nmaxHeight = spaceAbove]
    O --> Q[createPortal to document.body\nposition: fixed]
    P --> Q
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[ResizeObserver fires on #toolbar] --> B[useToolbarWidth returns width px]
    B --> C{width == null?}
    C -- yes --> D[overflow = empty Set\nrender all controls]
    C -- no --> E[resolveToolbarOverflow\navailable = width\nreserved = always-present + repo-select-min\nitems = files-toggle · layout-toggle · external-link]
    E --> F{budget fits item?}
    F -- yes, keep --> G[visible list]
    F -- no, or already dropping --> H[overflow list]
    G --> I[render icon button in toolbar]
    H --> J[hide icon button from toolbar\nkeep entry in ⋯ OptionsMenu]
    D --> I
    K[Base button clicked] --> L[open = true\npopoverStyle = null\nportal not painted]
    L --> M[useEffect fires\ncomputePopoverStyle from getBoundingClientRect]
    M --> N{more space below or above?}
    N -- below --> O[top = rect.bottom + gap\nbottom = auto\nmaxHeight = spaceBelow]
    N -- above --> P[top = auto\nbottom = viewportH - rect.top + gap\nmaxHeight = spaceAbove]
    O --> Q[createPortal to document.body\nposition: fixed]
    P --> Q
Loading

Reviews (4): Last reviewed commit: "Diff toolbar: portal the base picker pop..." | Re-trigger Greptile

Comment thread CLI/cmux_open.swift
@@ -386,9 +412,95 @@ extension CMUXCLI {
var executablePath: String?

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 File size sprawl in an already-oversized file

CLI/cmux_open.swift grows from 6,196 → 8,017 lines (+1,821) in this diff, and the file-length budget ceiling is raised to match. The entire new branch-picker subsystem (ref resolution heuristics, PR-base cache, refs group builder, stale-while-revalidate disk cache, session descriptor, and the two new CLI commands) is self-contained and independently testable — exactly the shape the SwiftPM-package-boundary rule targets. Appending it to an already 6k-line file makes the file harder to navigate and grows what is already the second-largest file in the budget. Consider splitting the branch-picker logic into a dedicated file (e.g. cmux_open+branch_picker.swift or a separate extension file) to keep responsibilities separated and bring the parent file's line count back toward a manageable level.

Rule Used: Flag Swift changes that add too much unrelated res... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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: 5

🤖 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/BrowserPanel.swift`:
- Around line 2257-2265: The jsEscaped variable in BrowserPanel.swift currently
only escapes backslashes and double quotes before inserting the URL into
JavaScript code. Add escaping for newline characters (\n) and line separator
characters (\u2028 and \u2029) by adding additional replacingOccurrences calls
to the jsEscaped string preparation. These characters can break out of the
JavaScript string context if present in malformed CLI output, so escape newlines
to \\n, carriage returns to \\r, and the Unicode line/paragraph separators to
their escaped equivalents to prevent potential injection vulnerabilities.

In `@webviews/src/App.tsx`:
- Around line 576-579: The repo-select item in the overflowItems array can
become hidden when the toolbar narrows (when hasRepoSelect returns true and
width is set to 110 or when it's 0), but the overflow menu fallback rendering
does not provide an alternative way to access repo selection. Either remove
repo-select from the overflowItems array to keep it always visible, or add
proper handling in the overflow menu rendering logic (at the locations marked in
the comment: lines 668-677 and 932-940) to display and handle repo selection
when the selector is not visible in the main toolbar. Ensure users always have
at least one entrypoint to change repositories regardless of toolbar width.
- Around line 779-787: The `isValidBranchPickerPayload` type guard function is
incomplete and only validates some properties (refsURL, regenerateURLTemplate,
currentRef, headRef) while missing validation for required properties like
`repoRoot`, `currentReason`, `confidence`, and `aheadBehind`. This allows
partial or invalid payloads to pass validation and render a broken branch picker
instead of falling back to the legacy select. Extend the validation logic in the
`isValidBranchPickerPayload` function to include type checks and value
validation for all required properties of the BranchPickerPayload contract,
including `repoRoot`, `currentReason`, `confidence`, and `aheadBehind`, ensuring
that only complete and valid payloads are accepted.

In `@webviews/src/BranchBasePicker.tsx`:
- Around line 476-504: The filtering logic in the loop that iterates through all
groups and their rows (starting at line 478) rescans the entire dataset on every
query update, creating O(totalRows) complexity for cases with no or sparse
matches. Refactor this filtering path to use a precomputed index or single-pass
plan approach instead of calling fuzzyMatchSpan on every row during each
keystroke. Replace the current brute-force row-by-row scanning pattern with a
bounded-work algorithm that avoids repeatedly traversing the full groups and
rows collection for every query change.

In `@webviews/src/styles.css`:
- Around line 141-157: The `.toolbar-left` class clips horizontal overflow which
prevents the `.base-picker-popover` (a 320px absolute child of `#base-picker`)
from displaying properly when the left track is narrower than the popover width.
To fix this, either change `.base-picker-popover` to be portal or
fixed-positioned to remove it from the clipping context, or modify the
overflow-x clipping on `.toolbar-left` to allow it to escape (consider using CSS
`:has()` selector or a modifier class to relax clipping conditionally). Apply
the same fix to the other occurrence mentioned at lines 423-428.
🪄 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: 78737c36-2dd0-49c4-be70-ad0381763467

📥 Commits

Reviewing files that changed from the base of the PR and between eeb1fab and 3505e0a.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (17)
  • CLI/CMUXCLI+Process.swift
  • CLI/cmux.swift
  • CLI/cmux_open.swift
  • Resources/Localizable.xcstrings
  • Resources/markdown-viewer/webviews-app/chunks/diffSurface.mjs
  • Sources/Panels/BrowserPanel.swift
  • webviews/src/App.tsx
  • webviews/src/BranchBasePicker.tsx
  • webviews/src/actions.ts
  • webviews/src/icons.tsx
  • webviews/src/labels.ts
  • webviews/src/styles.css
  • webviews/src/toolbar-overflow.ts
  • webviews/src/useToolbarWidth.ts
  • webviews/test/actions.test.ts
  • webviews/test/branch-base-picker.test.tsx
  • webviews/test/toolbar-overflow.test.ts

Comment on lines +2257 to +2265
let metaEscaped = Self.htmlAttributeEscaped(viewerURLString)
let jsEscaped = viewerURLString
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
let html = """
<!doctype html><html><head><meta charset="utf-8">\
<meta http-equiv="refresh" content="0;url=\(metaEscaped)"></head>\
<body><script>window.location.replace("\(jsEscaped)");</script></body></html>
"""

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 | 🟡 Minor | ⚡ Quick win

Harden JS string escaping for defense in depth.

The current escaping only handles \ and ". While the URL is validated to have the custom scheme and comes from the trusted bundled CLI, adding newline/line-separator escaping provides defense in depth against malformed CLI output.

🛡️ Suggested fix
-            let jsEscaped = viewerURLString
-                .replacingOccurrences(of: "\\", with: "\\\\")
-                .replacingOccurrences(of: "\"", with: "\\\"")
+            let jsEscaped = viewerURLString
+                .replacingOccurrences(of: "\\", with: "\\\\")
+                .replacingOccurrences(of: "\"", with: "\\\"")
+                .replacingOccurrences(of: "\n", with: "\\n")
+                .replacingOccurrences(of: "\r", with: "\\r")
+                .replacingOccurrences(of: "\u{2028}", with: "\\u2028")
+                .replacingOccurrences(of: "\u{2029}", with: "\\u2029")
🤖 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/BrowserPanel.swift` around lines 2257 - 2265, The jsEscaped
variable in BrowserPanel.swift currently only escapes backslashes and double
quotes before inserting the URL into JavaScript code. Add escaping for newline
characters (\n) and line separator characters (\u2028 and \u2029) by adding
additional replacingOccurrences calls to the jsEscaped string preparation. These
characters can break out of the JavaScript string context if present in
malformed CLI output, so escape newlines to \\n, carriage returns to \\r, and
the Unicode line/paragraph separators to their escaped equivalents to prevent
potential injection vulnerabilities.

Comment thread webviews/src/App.tsx Outdated
Comment thread webviews/src/App.tsx
Comment on lines +779 to +787
function isValidBranchPickerPayload(value: any): value is BranchPickerPayload {
return Boolean(
value &&
typeof value === "object" &&
typeof value.refsURL === "string" && value.refsURL !== "" &&
typeof value.regenerateURLTemplate === "string" && value.regenerateURLTemplate !== "" &&
typeof value.currentRef === "string" &&
typeof value.headRef === "string",
);

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

Validate the full branch-picker contract before opting in.

This accepts partial branchPicker objects missing repoRoot, currentReason, confidence, or valid aheadBehind, and String.replace("{ref}", …) does not throw when the placeholder is absent—it silently keeps the original URL. Invalid payloads should fall back to the legacy select instead of rendering a broken picker.

Proposed validation tightening
+function isValidAheadBehind(value: unknown): value is BranchPickerPayload["aheadBehind"] {
+  if (value === null) {
+    return true;
+  }
+  if (!value || typeof value !== "object") {
+    return false;
+  }
+  const candidate = value as { ahead?: unknown; behind?: unknown };
+  return Number.isFinite(candidate.ahead) && Number.isFinite(candidate.behind);
+}
+
 function isValidBranchPickerPayload(value: any): value is BranchPickerPayload {
+  const aheadBehind = value?.aheadBehind;
   return Boolean(
     value &&
     typeof value === "object" &&
+    typeof value.repoRoot === "string" &&
     typeof value.refsURL === "string" && value.refsURL !== "" &&
-    typeof value.regenerateURLTemplate === "string" && value.regenerateURLTemplate !== "" &&
+    typeof value.regenerateURLTemplate === "string" &&
+    value.regenerateURLTemplate.includes("{ref}") &&
     typeof value.currentRef === "string" &&
-    typeof value.headRef === "string",
+    typeof value.headRef === "string" &&
+    typeof value.currentReason === "string" &&
+    (value.confidence === "high" || value.confidence === "low") &&
+    isValidAheadBehind(aheadBehind),
   );
 }
🤖 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 `@webviews/src/App.tsx` around lines 779 - 787, The
`isValidBranchPickerPayload` type guard function is incomplete and only
validates some properties (refsURL, regenerateURLTemplate, currentRef, headRef)
while missing validation for required properties like `repoRoot`,
`currentReason`, `confidence`, and `aheadBehind`. This allows partial or invalid
payloads to pass validation and render a broken branch picker instead of falling
back to the legacy select. Extend the validation logic in the
`isValidBranchPickerPayload` function to include type checks and value
validation for all required properties of the BranchPickerPayload contract,
including `repoRoot`, `currentReason`, `confidence`, and `aheadBehind`, ensuring
that only complete and valid payloads are accepted.

Comment on lines +476 to +504
// Filtering: match across ALL rows of every group, then cap the total
// rendered set so a query that matches thousands of rows stays cheap.
for (const group of groups) {
if (result.length >= FILTERED_TOTAL_CAP) {
break;
}
const groupLabel = resolveGroupLabel(group, label);
let firstInGroup = true;
for (const row of group.rows) {
if (result.length >= FILTERED_TOTAL_CAP) {
break;
}
const match = fuzzyMatchSpan(row.label, trimmed);
if (match == null) {
continue;
}
anyMatch = true;
result.push({
row,
groupId: group.id,
groupLabel,
firstInGroup,
match,
raw: false,
moreCount: 0,
});
firstInGroup = false;
}
}

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 | 🏗️ Heavy lift

Avoid rescanning the full refs corpus on every query update.

The filtered path performs row-by-row fuzzy matching across all groups per keystroke (Lines 478-504). For large ref sets this makes no-match/sparse-match input O(totalRows) each render on the interactive path.

Suggested direction
- const flat = buildFlatRows(groups, query, label);
+ const indexed = useMemo(() => buildPickerIndex(groups, label), [groups, label]);
+ const flat = buildFlatRowsFromIndex(indexed, query);

+ // build once when refs load:
+ // - pre-lowercase labels
+ // - optional prefix bucket map (first char -> candidate rows)
+ // - preserve group metadata for headers

As per coding guidelines, this picker path should avoid repeated full scans over scalable collections and prefer precomputed index/single-pass plans with bounded work per event.

🤖 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 `@webviews/src/BranchBasePicker.tsx` around lines 476 - 504, The filtering
logic in the loop that iterates through all groups and their rows (starting at
line 478) rescans the entire dataset on every query update, creating
O(totalRows) complexity for cases with no or sparse matches. Refactor this
filtering path to use a precomputed index or single-pass plan approach instead
of calling fuzzyMatchSpan on every row during each keystroke. Replace the
current brute-force row-by-row scanning pattern with a bounded-work algorithm
that avoids repeatedly traversing the full groups and rows collection for every
query change.

Source: Coding guidelines

Comment thread webviews/src/styles.css
azooz2003-bit and others added 2 commits June 21, 2026 13:27
…at small widths

The toolbar was a rigid grid (minmax(0,1.1fr) minmax(124px,0.9fr) auto) with a
non-shrinking toolbar-actions, so at small widths the left controls overflowed
their cell and overlaid the accessory icons.

- No-overlap guarantee: all toolbar grid tracks are minmax(0,...) and the cells
  use overflow-x:clip (overflow-y:visible so the base/options popovers still
  escape below the bar); toolbar-actions can shrink.
- Measured priority overflow: a ResizeObserver on #toolbar drives a pure
  resolver (toolbar-overflow.ts) that keeps the highest-priority controls and
  drops the rest as a clean priority suffix into the always-present options menu.
  Drop order (lowest first): external link -> layout -> files -> repo select.
  The base picker (primary) and the ⋯ button are always visible. The options
  menu always lists layout + external so dropped actions stay reachable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…at small widths

The @pierre/diffs per-file header is a flex space-between of the file-path side
([data-header-content], min-width:0 + truncating) and the diffstat side
([data-metadata]: status icon + +N/-N counts). The library leaves [data-metadata]
flex-shrinkable with white-space:nowrap and no overflow handling, so when the
panel is narrow it gets squished below its content width and its text spills left,
overlapping the change-status icon. Pin the diffstat (flex-shrink:0) so the path
side absorbs all shrinking and truncates, and clip the header as a no-overlap
safety net. Same priority+ idea as the toolbar fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…reachable

Two regressions from the responsive-toolbar change, caught by review:
- The base picker popover (320px absolute child of #base-picker in .toolbar-left)
  was clipped by the new overflow-x:clip safety net. Make it a viewport-anchored
  position:fixed floating element (JS-anchored to the button rect, clamped to the
  viewport, flips above when short on space below), so it escapes the toolbar
  clip while the clip stays as the controls' no-overlap guarantee.
- The repo <select> could overflow into the options menu, but a native select
  can't live there, so multi-repo users lost the switcher. Stop overflowing it:
  it's always rendered and truncates in place. Only the accessory icon controls
  overflow into the menu.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… container clip)

position:fixed alone did not free the popover: .toolbar-left has
container-type:inline-size (for the picker's @container queries), which makes it
the containing block for fixed descendants AND still clips them. Render the
popover via createPortal(document.body) so it leaves the container/clip subtree
entirely; the existing fixed + viewport-anchored positioning then resolves
against the viewport. Outside-click now checks both the container and a new
popoverRef so clicks inside the portaled popover don't dismiss it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@azooz2003-bit
azooz2003-bit merged commit 32568b1 into main Jun 21, 2026
45 of 48 checks passed
@azooz2003-bit
azooz2003-bit deleted the feat-diff-toolbar-overflow branch June 21, 2026 21:43

This branch was successfully deployed

1 active deployment
Preview – cmux — 2c18e750 Deployed Jun 21, 2026 by vercel[bot]
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.

1 participant