Skip to content

Move diff viewer into React app source#5074

Merged
lawrencecchen merged 27 commits into
mainfrom
task-diff-viewer-react-app
Jun 2, 2026
Merged

Move diff viewer into React app source#5074
lawrencecchen merged 27 commits into
mainfrom
task-diff-viewer-react-app

Conversation

@lawrencecchen
Copy link
Copy Markdown
Contributor

@lawrencecchen lawrencecchen commented Jun 1, 2026

Summary

  • Adds a source-owned React/Vite app for cmux open diff under diff-viewer/.
  • Keeps only generated runtime assets in Resources/markdown-viewer/diff-viewer-app/, because the macOS app serves bundled static resources.
  • Replaces the giant inline HTML/CSS/JS in CLI/cmux_open.swift with a small HTML shell plus JSON config.
  • Adds ./scripts/build-diff-viewer-app.sh and a CI job that verifies generated assets and runs React Doctor with --fail-on error.

Related follow-up: #5064

Verification

  • ./scripts/build-diff-viewer-app.sh
  • ./scripts/build-diff-viewer-app.sh --check
  • bun run react-doctor:ci from diff-viewer/ (0 errors, 8 warnings remain in the legacy bridge)
  • swiftc -parse CLI/cmux_open.swift
  • swiftc -parse Sources/Panels/BrowserPanel.swift
  • git diff --cached --check

Browser smoke note: the in-app Browser backend was unavailable in this session, so I could not complete the local browser fixture smoke before handoff.


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


Note

Medium Risk
Large behavioral surface (streaming diff, workers, session restore) moved out of Swift into a new bundle; mitigated by protocol versioning, asset check CI, and retained Pierre asset copy path.

Overview
The diff viewer UI moves from thousands of lines of inline HTML/CSS/JS in cmux_open.swift to a built React app (diff-viewer/) loaded via a minimal shell: JSON in #cmux-diff-viewer-config and main.mjs. Swift now copies bundled cmux-diff-viewer-app assets alongside the existing Pierre bundles and passes a structured assets + payload config.

CI adds react-apps-check: verify generated assets (build-diff-viewer-app.sh --check), typecheck, tests, oxlint, and React Doctor for diff-viewer/.

Host integration changes include transparent_background on browser open, background-opacity from Ghostty config in appearance JSON, a bumped local HTTP protocolVersion (react-app-v1) so stale servers restart, and commit in localized labels. Diff viewer surfaces can persist/restore with the custom scheme (per PR description; Swift wiring supports the slimmer page contract).

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


Summary by cubic

Moved the diff viewer into a source-owned React/Vite TypeScript app that builds a single main.mjs. Added transparent backdrops and reliable session restore; only restorable local diffs are kept (skips remote-only manifests and redirect stubs), restored diff switcher URLs use the custom scheme, streamed items inherit the collapse state, and a layout bug that clipped the files sidebar in narrow panes is fixed.

  • Refactors

    • Builds a single main.mjs into Resources/markdown-viewer/diff-viewer-app/; existing Pierre bundles remain.
    • Replaces inline HTML/CSS/JS with a small HTML shell and cmux-diff-viewer-config JSON; improves the loading shell.
    • Adds transparent_background to browser.open_split, threaded through WorkspaceBrowserPanel; the diff viewer opts in with a transparent page body so Ghostty themes show through.
    • Persists diff viewer surfaces across restarts: snapshot transparentBackground, token, and request path; on restore BrowserPanel re-registers via the app-owned scheme handler and navigates with cmux-diff-viewer://, independent of the local HTTP server. Only persists restorable viewers (local-only manifests, non-pending entry); restore skips remote-only manifests and redirect stubs. Also rewrites diff switcher option URLs to the custom scheme so switching targets works after a restart.
    • Extends the config schema (assets, payload like title, statusMessage, pendingReplacement, externalURL, labels, appearance); moves logic into src/viewer-controller.ts and App.tsx; converts to TypeScript, adds Tailwind via @tailwindcss/vite, and enables React Compiler (babel-plugin-react-compiler via @vitejs/plugin-react).
    • Bumps the local server protocol to react-app-v1 and persists protocolVersion; ships default labels with an optional dev assert; renames the CI job to react-apps-check (build assets via ./scripts/build-diff-viewer-app.sh --check, typecheck, tests, oxlint deny-warnings, React Doctor); updates tests for config parsing, file-tree refresh, backdrop behavior, Pierre tree bundle APIs, and session restore.
    • Reloads deferred diff replacements instead of document.write so the app reboots cleanly when the final HTML is ready.
    • New streamed diff items respect the global “collapse all diffs” state.
    • Fixes files sidebar clipping in narrow panes by constraining #app’s grid column to minmax(0, 1fr).
  • Migration

    • Edit the viewer in diff-viewer/, then run ./scripts/build-diff-viewer-app.sh and commit the updated assets.
    • Use ./scripts/build-diff-viewer-app.sh --check (or rely on CI) to confirm assets are current and types/tests pass.

Written for commit a8f5607. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Introduces a full client-side diff viewer with streaming large-diff rendering, toolbar, file sidebar, jump-to-file, copy-patch export, keyboard shortcuts, and theming/appearance controls; adds optional transparent background support for embedded web views.
  • Documentation

    • Adds a user-facing README with build/run notes and explains committed build output.
  • Style

    • Polished light/dark theming, responsive layout, improved focus states, and accessible UI behavior.
  • Tests / CI

    • Adds unit tests for file-tree, labels, and appearance; adds a build/check script and CI job to verify bundled assets and run typecheck/lint/tests.

@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 1, 2026

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 2, 2026 8:27am
cmux-staging Building Building Preview, Comment Jun 2, 2026 8:27am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

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
📝 Walkthrough

Walkthrough

Adds a standalone diff-viewer/ React+Bun package: build and CI integration, TypeScript types/config, a React entry and App shell, appearance/theming and styles, a streaming diff-controller that ingests and incrementally renders patch streams, file-tree refresh planning, tests, and a build-asset verification script.

Changes

Diff Viewer Feature

Layer / File(s) Summary
Build & Package Infrastructure
diff-viewer/package.json, diff-viewer/vite.config.mjs, diff-viewer/index.html, .github/workflows/ci.yml, scripts/build-diff-viewer-app.sh, diff-viewer/README.md
Package manifest and Vite library build, HTML mount shell, CI job diff-viewer-check that verifies built assets and runs typecheck/test/lint/react-doctor, and a Bash script to build or compare generated assets.
Types & Tooling Config
diff-viewer/src/types.ts, diff-viewer/src/global.d.ts, diff-viewer/tsconfig.json, diff-viewer/src/main.tsx
Adds DiffViewerPayload/DiffViewerConfig types, marks runtime Window.__cmuxDiffViewer, bundler-friendly tsconfig, and runtime entry helpers that read DOM-provided JSON and install inline styles and dataset flags.
React Initialization & App Shell
diff-viewer/src/main.tsx, diff-viewer/src/App.tsx
Entrypoint mounts the app, injects styles and document dataset flags; App renders toolbar, files sidebar/loading skeletons, status area, and initializes startDiffViewer(config) once.
Styling & Visual Presentation
diff-viewer/src/styles.css
Comprehensive stylesheet: theming tokens, toolbar/options menu, two-column grid, files sidebar, responsive collapse, sticky status panel with pending spinner, and reduced-motion support.
Appearance & Helpers
diff-viewer/src/appearance.ts
Appearance/theme types, default light/dark themes, resolution/merging, CSS variable application, and color/opacity helpers including appearanceBackgroundColor.
Diff Viewer Controller & Streaming
diff-viewer/src/viewer-controller.ts
Exports startDiffViewer(config): wiring, toolbar/shortcut handling, dynamic imports, optional worker pool, streaming payload.patchURL ingestion, parsing into per-file diffs, batch flushes into CodeView, file-tree sync, git-status aggregation, and git-apply copy generation.
File-tree Refresh Planning & Tests
diff-viewer/src/file-tree-refresh.ts, diff-viewer/test/file-tree-refresh.test.ts, diff-viewer/test/pierre-tree-bundle.test.ts
Planning logic to decide append vs reset for Pierre virtualized tree plus tests validating append/reset behavior and vendored Pierre bundle usage.
Labels & Tests
diff-viewer/src/labels.ts, diff-viewer/test/labels.test.ts
Default labels map and createDiffViewerLabelResolver with optional missing-label assertions; tests for payload precedence, default fallback, empty-value fallback, and deduplicated missing-key assertions.
Additional Tests & Swift Assertions
diff-viewer/test/appearance.test.ts, cmuxTests/CMUXOpenCommandTests.swift, diff-viewer/test/*
Bun tests for appearance helper; updated Swift tests to parse cmux-diff-viewer-config script, assert payload fields and asset mappings, and refactored HTML parsing helpers.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant startDiffViewer
  participant Network
  participant WorkerPool
  participant CodeView
  participant FileTree
  Browser->>startDiffViewer: call startDiffViewer(config)
  startDiffViewer->>Network: fetch(payload.patchURL) (stream)
  Network->>startDiffViewer: stream patch chunks (git/commit boundaries)
  startDiffViewer->>WorkerPool: submit highlight tasks (optional)
  WorkerPool->>CodeView: return highlighted fragments
  startDiffViewer->>CodeView: append parsed diff items (batched)
  startDiffViewer->>FileTree: update tree/navigation & git-status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

Possibly related PRs

"🐰 I stitched the streams into a view,
where lines arrive and workers chew,
pending to loaded, the tree takes flight,
assets bundled snug and tidy tight,
hop, point, and click — the diffs shine bright!"


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (4 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Actor Isolation ❌ Error Combine NotificationCenter sink in BrowserPanel accesses @MainActor method without explicit main dispatch, violating Swift 6 actor isolation rules. Wrap the sink closure with DispatchQueue.main.async { } or use .receive(on: DispatchQueue.main) to ensure applyWebViewBackground access is properly isolated.
Cmux Swift Logging ❌ Error BrowserPanel.swift adds 15 unguarded NSLog() calls and TerminalController.swift adds unguarded print() calls in app/runtime code, violating swift-logging.md rules. Replace NSLog/print with Logger from os.log, or guard with #if DEBUG if these are temporary debugging probes.
Cmux Full Internationalization ❌ Error Diff-viewer labels in xcstrings lack translations for three locales from routing.ts: no, zh-CN, zh-TW (only nb, zh-Hans, zh-Hant present). Add diffViewer.* translations for no, zh-CN, zh-TW locales to Localizable.xcstrings, or align routing.ts locales with xcstrings supported locales.
Cmux Architecture Rethink ❌ Error usesTransparentBackground is latched at init and never reset, causing external pages to render with transparent background violating the "internal cmux UI only" contract. Gate clear-background logic on current webView.url scheme to only apply transparency for internal/localhost URLs, or disable it when navigating to external content.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description covers the main changes (moving diff viewer to React app, asset management, CI job, scripts) and provides verification steps, but lacks demo video and is missing complete testing/checklist details. Add a demo video showing the diff viewer UI in action. Complete the testing checklist (mark tested items and detail manual verification steps beyond the verification commands listed).
✅ Passed checks (12 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: moving the diff viewer UI from inline code into a source-owned React application.
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 Blocking Runtime ✅ Passed Swift changes add parameter passing and conditional WebView styling with no blocking synchronization (no DispatchSemaphore, Thread.sleep, Task.sleep, asyncAfter, DispatchQueue.main.sync, or NSLock).
Cmux No Hacky Sleeps ✅ Passed All setTimeout usages are presentation timing, product debounce/throttle behavior, or frame-yielding—not race condition workarounds. No hacky sleeps detected.
Cmux Algorithmic Complexity ✅ Passed PR passes complexity rules: setCollapsed uses O(1) Map lookup avoiding nested scans; file-tree-refresh uses linear single-pass comparison; patch parsing has localized header searches.
Cmux Swift Concurrency ✅ Passed Modifications add only synchronous Bool parameter threading and conditional AppKit layer property setting; no new background DispatchQueues, Combine state, completion handlers, or Tasks introduced.
Cmux Swift @Concurrent ✅ Passed All Swift changes are synchronous and @MainActor-bound parameter threading with no async/concurrent annotation violations or CPU/file/network-heavy operations.
Cmux Swift File And Package Boundaries ✅ Passed Swift changes are small parameter additions to UI/AppKit glue files (BrowserPanel +59, TerminalController +4, Workspace +4), all under 250-line threshold with no mixed responsibilities.
Cmux User-Facing Error Privacy ✅ Passed All user-facing errors use generic language with no vendor names, credentials, tokens, env vars, or implementation details exposed.
Cmux Swiftui State Layout ✅ Passed No new @Published/@StateObject/@observable, no GeometryReader, no lazy-list store refs, no render-time mutations. Adds immutable property threaded via parameters; state updates in event handlers only.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No new NSWindow/NSPanel created. Lint script passes; all 26 cmux window identifiers are registered. PR only adds transparentBackground parameter to existing BrowserPanel.
✨ 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 task-diff-viewer-react-app

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

greptile-apps Bot commented Jun 1, 2026

Greptile Summary

This PR moves cmux open diff from thousands of lines of inline HTML/CSS/JS in CLI/cmux_open.swift into a source-owned React/Vite TypeScript app under diff-viewer/, shipping as a single bundled main.mjs served from Resources/. The Swift side emits a minimal HTML shell with a cmux-diff-viewer-config JSON script tag instead of the old inline payload literals.

  • Diff viewer app: New React app with TypeScript, Tailwind, React Compiler, and viewer-controller.ts; replaces document.write deferred-load with window.location.reload() so React bootstraps cleanly; fixes the files-sidebar grid-column clipping.
  • Session restore: Diff viewer surfaces now survive app restarts — manifest + files stay on disk, token is re-registered with CmuxDiffViewerURLSchemeHandler, navigation uses cmux-diff-viewer:// scheme; only local-only, non-pending surfaces are persisted.
  • Protocol & appearance: HTTP server protocol bumped to react-app-v1 (persisted for stale-server detection); transparent_background threaded through to BrowserPanel; background-opacity wired into appearance JSON; CI gains react-apps-check job.

Confidence Score: 5/5

The PR is safe to merge; the inline viewer extraction, session-restore plumbing, and transparent-background wiring are all self-consistent and the protocol-version bump correctly forces stale local servers to restart.

The Swift session-restore path correctly threads transparentBackground from the snapshot through panel creation before restoreSessionSnapshot is called. The long-poll deferred-load replacement from document.write to window.location.reload is the correct fix for ES-module re-evaluation. The protocol version is persisted so stale servers are always restarted. No logic errors found in the diff viewer restore flow, background compositing logic, or streaming renderer.

No files require special attention. The two notes are about CI step ordering and TypeScript index-signature coverage in diff-viewer/src/types.ts.

Important Files Changed

Filename Overview
CLI/cmux_open.swift Replaces multi-thousand-line inline HTML with a minimal shell + JSON config; adds appModuleURL, protocolVersion for stale-server detection, commit label, and backgroundOpacity parsing.
Sources/Panels/BrowserPanel.swift Adds usesTransparentBackground, diffViewerURL restore path in restoreSessionSnapshot, diffViewerRestorable/diffViewerComponents helpers, and applyWebViewBackground for transparent Ghostty theme passthrough.
diff-viewer/src/viewer-controller.ts New 2133-line controller replacing the inline HTML/JS; manages streaming diff rendering, worker pool, file tree, toolbar, keyboard shortcuts, and deferred-load via long-poll reload.
diff-viewer/src/App.tsx React shell that renders the HTML scaffold and kicks off startDiffViewer from a callback ref after the full tree is committed.
diff-viewer/src/types.ts Defines DiffViewerConfig/DiffViewerPayload; [key: string]: any index signature means all additional payload fields in viewer-controller.ts are typed as any.
.github/workflows/ci.yml Adds react-apps-check job; typecheck/test/lint/react-doctor steps rely on node_modules installed inside the preceding build script step.
Sources/SessionPersistence.swift Extends SessionBrowserPanelSnapshot with transparentBackground, diffViewerToken, and diffViewerRequestPath for cross-restart restore.
Sources/Workspace.swift Threads transparentBackground through addBrowserPanel and addSplitBrowserPanel, persists diffViewerComponents into the session snapshot.

Sequence Diagram

sequenceDiagram
    participant CLI as cmux open diff (CLI)
    participant FS as Disk (manifest + HTML)
    participant HTTP as Local HTTP Server
    participant App as macOS App
    participant BP as BrowserPanel
    participant WV as WKWebView
    participant React as React App (main.mjs)
    participant VC as viewer-controller.ts

    CLI->>FS: write manifest + HTML shell (config JSON)
    CLI->>HTTP: start local HTTP server (protocol: react-app-v1)
    CLI->>App: browser.open_split (transparent_background: true)
    App->>BP: "create BrowserPanel (usesTransparentBackground=true)"
    BP->>WV: bind webview → applyConfiguredWebViewBackground()
    BP->>WV: navigate to http://127.0.0.1 port/token/path
    WV->>HTTP: GET /token/path
    HTTP-->>WV: HTML shell (cmux-diff-viewer-config JSON + main.mjs)
    WV->>React: execute main.mjs
    React->>VC: startDiffViewer(config)
    VC->>HTTP: fetch patchURL → stream diff data
    HTTP-->>VC: streaming patch content

    Note over App,BP: On next app launch (session restore)
    App->>BP: create BrowserPanel (transparentBackground from snapshot)
    App->>BP: restoreSessionSnapshot (diffViewerToken + requestPath)
    BP->>FS: registerFromManifest(token) → read manifest JSON
    BP->>WV: navigate cmux-diff-viewer://token/path
    WV->>BP: WKURLSchemeHandler → serve file from FS
    WV->>React: execute main.mjs (fresh load, no HTTP server needed)
Loading

Reviews (19): Last reviewed commit: "Inherit collapse state for streamed diff..." | Re-trigger Greptile

Comment thread scripts/build-diff-viewer-app.sh Outdated
Comment on lines +16 to +20
if ! diff -qr "$OUT_DIR" "$tmp_dir" >/tmp/cmux-diff-viewer-app-diff.txt; then
cat /tmp/cmux-diff-viewer-app-diff.txt >&2
echo "diff viewer app assets are stale; run ./scripts/build-diff-viewer-app.sh" >&2
exit 1
fi
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 The diff output is written to a hardcoded /tmp/cmux-diff-viewer-app-diff.txt path. If two invocations of --check run concurrently on the same machine (e.g., a developer running it locally while CI also runs), they will race on the same file and one run's error output will be overwritten by the other. Using a temp file created by mktemp keeps each invocation isolated.

Suggested change
if ! diff -qr "$OUT_DIR" "$tmp_dir" >/tmp/cmux-diff-viewer-app-diff.txt; then
cat /tmp/cmux-diff-viewer-app-diff.txt >&2
echo "diff viewer app assets are stale; run ./scripts/build-diff-viewer-app.sh" >&2
exit 1
fi
diff_output="$(mktemp)"
if ! diff -qr "$OUT_DIR" "$tmp_dir" >"$diff_output"; then
cat "$diff_output" >&2
rm -f "$diff_output"
echo "diff viewer app assets are stale; run ./scripts/build-diff-viewer-app.sh" >&2
exit 1
fi
rm -f "$diff_output"

Comment thread diff-viewer/src/App.jsx Outdated
Comment on lines +112 to +118
const rootRef = useCallback((node) => {
if (!node || started.current) {
return;
}
started.current = true;
queueMicrotask(() => startDiffViewer(config));
}, [config]);
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 Unconventional timing for legacy-viewer init

startDiffViewer is dispatched via queueMicrotask inside a ref callback. React guarantees the full component tree is committed to the DOM before any ref callback fires, so all the document.getElementById calls in startDiffViewer (e.g. #viewer, #toolbar, #file-list) are already available at the point the ref fires — the microtask yield is redundant. The idiomatic React pattern for a post-commit, one-shot side-effect that touches the DOM is useEffect (or useLayoutEffect if it needs to run before paint), which makes the intent explicit and plays correctly with React Strict Mode's double-invocation in development. The started.current guard means the current approach is safe, but future readers will wonder why the extra yield exists.

Rule Used: Flag fixed sleeps, delayed dispatch, timers, polli... (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[bot]
coderabbitai Bot previously requested changes Jun 1, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 `@diff-viewer/index.html`:
- Line 2: The HTML root element (<html>) is missing a lang attribute which
impairs screen-reader pronunciation and triggers HTMLHint; update the document's
root tag in index.html (the <html> element) to include an appropriate lang value
(e.g., lang="en" or the project's primary language) so accessibility tools and
linters recognize the page language.
🪄 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: 83139185-c0d9-4402-b5dd-20994432978f

📥 Commits

Reviewing files that changed from the base of the PR and between 280e0b4 and a12ff06.

⛔ Files ignored due to path filters (1)
  • diff-viewer/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • .github/workflows/ci.yml
  • CLI/cmux_open.swift
  • Resources/markdown-viewer/diff-viewer-app/main.mjs
  • diff-viewer/README.md
  • diff-viewer/index.html
  • diff-viewer/package.json
  • diff-viewer/src/App.jsx
  • diff-viewer/src/legacy-viewer.js
  • diff-viewer/src/main.jsx
  • diff-viewer/src/styles.css
  • diff-viewer/vite.config.mjs
  • scripts/build-diff-viewer-app.sh

Comment thread diff-viewer/index.html Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 13 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="diff-viewer/src/styles.css">

<violation number="1" location="diff-viewer/src/styles.css:483">
P2: Unconditional `will-change: scroll-position` in the stylesheet forces the browser to maintain a compositor layer for `#viewer` at all times, consuming GPU memory with no measured performance problem to solve. MDN explicitly warns this is a premature optimization anti-pattern: it should only be used as a last resort for existing jank, never speculatively in a stylesheet.</violation>
</file>

<file name="scripts/build-diff-viewer-app.sh">

<violation number="1" location="scripts/build-diff-viewer-app.sh:16">
P2: Avoid writing diff output to a shared hardcoded `/tmp` filename; concurrent runs can clobber each other’s output and produce misleading check failures.</violation>
</file>

<file name="diff-viewer/src/App.jsx">

<violation number="1" location="diff-viewer/src/App.jsx:117">
P3: Initialize the viewer directly from a post-commit effect instead of queueing a microtask in the ref callback; the extra async hop is unnecessary and makes lifecycle behavior harder to reason about in Strict Mode.</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread diff-viewer/src/styles.css Outdated
Comment thread scripts/build-diff-viewer-app.sh Outdated
Comment thread diff-viewer/src/App.tsx Outdated
return;
}
started.current = true;
queueMicrotask(() => startDiffViewer(config));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Initialize the viewer directly from a post-commit effect instead of queueing a microtask in the ref callback; the extra async hop is unnecessary and makes lifecycle behavior harder to reason about in Strict Mode.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At diff-viewer/src/App.jsx, line 117:

<comment>Initialize the viewer directly from a post-commit effect instead of queueing a microtask in the ref callback; the extra async hop is unnecessary and makes lifecycle behavior harder to reason about in Strict Mode.</comment>

<file context>
@@ -0,0 +1,131 @@
+      return;
+    }
+    started.current = true;
+    queueMicrotask(() => startDiffViewer(config));
+  }, [config]);
+
</file context>

coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 1, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
diff-viewer/index.html (1)

2-2: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a lang attribute to <html>.

Missing lang hurts screen-reader pronunciation and accessibility.

♿ Proposed fix
-<html>
+<html lang="en">
🤖 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 `@diff-viewer/index.html` at line 2, Add a lang attribute to the root <html>
element to improve accessibility and screen-reader pronunciation: locate the
HTML root in index.html (the <html> tag) and set an appropriate language code
(e.g., lang="en" or the project's primary language) so the tag becomes <html
lang="...">.
🤖 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 `@diff-viewer/src/legacy-viewer.ts`:
- Line 822: The patch variable is currently declared with loose types (const
patch: { remove?: string[]; set?: any[] } = {}) and menu items at the other
location use any[]; replace these with proper typed interfaces/discriminated
unions to restore type safety: define a Patch type (e.g., { remove?: string[];
set?: Array<YourPatchEntry> }) and a MenuItem discriminated union
(separator/action/segment) like the example in the comment, then update the
declarations (patch and the menu items array) and any functions that consume
them to use those new types (search for the identifier patch and the menu items
array around line 1202 to locate usages) so callers and reducers operate with
concrete fields instead of any.

---

Duplicate comments:
In `@diff-viewer/index.html`:
- Line 2: Add a lang attribute to the root <html> element to improve
accessibility and screen-reader pronunciation: locate the HTML root in
index.html (the <html> tag) and set an appropriate language code (e.g.,
lang="en" or the project's primary language) so the tag becomes <html
lang="...">.
🪄 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: 9bb54b92-57d1-400a-a013-3ddee8df6020

📥 Commits

Reviewing files that changed from the base of the PR and between a12ff06 and 5ecc876.

⛔ Files ignored due to path filters (1)
  • diff-viewer/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • .github/workflows/ci.yml
  • CLI/cmux_open.swift
  • Resources/markdown-viewer/diff-viewer-app/main.mjs
  • diff-viewer/index.html
  • diff-viewer/package.json
  • diff-viewer/src/App.tsx
  • diff-viewer/src/global.d.ts
  • diff-viewer/src/legacy-viewer.ts
  • diff-viewer/src/main.tsx
  • diff-viewer/src/types.ts
  • diff-viewer/tsconfig.json
  • diff-viewer/vite.config.mjs

Comment thread diff-viewer/src/viewer-controller.ts Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 12 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="diff-viewer/src/App.jsx">

<violation number="1" location="diff-viewer/src/App.jsx:117">
P3: Initialize the viewer directly from a post-commit effect instead of queueing a microtask in the ref callback; the extra async hop is unnecessary and makes lifecycle behavior harder to reason about in Strict Mode.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread diff-viewer/tsconfig.json Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
diff-viewer/src/legacy-viewer.ts (2)

74-75: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against missing payload.title and payload.appearance.

Line 74: If payload.title is undefined, the document title becomes the literal string "undefined".

Line 75: If payload.appearance is undefined, applyViewerAppearance will throw when accessing appearance.themes.light.background.

🛡️ Suggested defensive guards
-document.title = payload.title;
-applyViewerAppearance(payload.appearance);
+if (typeof payload.title === "string") {
+  document.title = payload.title;
+}
+if (payload.appearance) {
+  applyViewerAppearance(payload.appearance);
+}

Alternatively, if appearance is a required contract field, consider adding validation early and throwing a clear error rather than a cryptic property access failure.

🤖 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 `@diff-viewer/src/legacy-viewer.ts` around lines 74 - 75, Guard against missing
payload.title and payload.appearance: before setting document.title check if
payload.title is a non-empty string and only assign it (or fall back to a
sensible default like an empty string or "Untitled"); before calling
applyViewerAppearance validate payload.appearance (or its nested keys used by
applyViewerAppearance, e.g., appearance.themes.light.background) and either
supply a default appearance object or throw a clear validation error; update the
call sites around document.title and applyViewerAppearance to perform these
checks and fail-fast with a descriptive error if appearance is required.

12-16: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Add type annotations to resolveAssetURL parameters.

The parameters have implicit any types. Consider typing them explicitly:

🔧 Suggested fix
-  const resolveAssetURL = (value, name) => {
+  const resolveAssetURL = (value: unknown, name: string): string => {
     if (typeof value !== "string" || value.length === 0) {
       throw new Error(`Missing cmux diff viewer asset: ${name}`);
     }
     return new URL(value, window.location.href).href;
   };
🤖 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 `@diff-viewer/src/legacy-viewer.ts` around lines 12 - 16, The function
resolveAssetURL currently has implicit any parameters (value, name); update its
signature to explicitly type the parameters (e.g., value: string, name: string)
and its return type (e.g., : string) so TypeScript can validate inputs and
outputs; locate the resolveAssetURL declaration and add the appropriate
parameter and return type annotations while keeping the existing runtime checks
intact.
🤖 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.

Outside diff comments:
In `@diff-viewer/src/legacy-viewer.ts`:
- Around line 74-75: Guard against missing payload.title and payload.appearance:
before setting document.title check if payload.title is a non-empty string and
only assign it (or fall back to a sensible default like an empty string or
"Untitled"); before calling applyViewerAppearance validate payload.appearance
(or its nested keys used by applyViewerAppearance, e.g.,
appearance.themes.light.background) and either supply a default appearance
object or throw a clear validation error; update the call sites around
document.title and applyViewerAppearance to perform these checks and fail-fast
with a descriptive error if appearance is required.
- Around line 12-16: The function resolveAssetURL currently has implicit any
parameters (value, name); update its signature to explicitly type the parameters
(e.g., value: string, name: string) and its return type (e.g., : string) so
TypeScript can validate inputs and outputs; locate the resolveAssetURL
declaration and add the appropriate parameter and return type annotations while
keeping the existing runtime checks intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ff838dc1-cef9-4002-9251-42fce821b6a0

📥 Commits

Reviewing files that changed from the base of the PR and between 5ecc876 and 9cc13f9.

📒 Files selected for processing (2)
  • Resources/markdown-viewer/diff-viewer-app/main.mjs
  • diff-viewer/src/legacy-viewer.ts

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f5827e15ed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread CLI/cmux_open.swift
<body>
<script id="cmux-diff-viewer-config" type="application/json">\(configLiteral)</script>
<div id="root"></div>
<script type="module" src="\(appModuleURL)"></script>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Update stale diff viewer HTML assertions

The macOS unit step in .github/workflows/ci.yml:311-328 runs the cmux-unit suite, and cmuxTests/CMUXOpenCommandTests.swift:387-403 still asserts that the generated HTML string contains the old inline viewer DOM and script snippets such as id="files-sidebar" and WORKER_POOL_MODULE_URL. With this change the page only emits the config, root node, and external module script here, so those assertions no longer match and the unit job fails whenever this test runs. Please update the test to inspect the generated app asset or the new minimal HTML shape.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
diff-viewer/src/viewer-controller.ts (3)

74-74: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid unconditionally resetting document.title inside the controller.

This overrides the fallback title already set in main.tsx when payload.title is absent. Guard the assignment so it only runs for non-empty titles.

Suggested fix
-document.title = payload.title;
+if (typeof payload.title === "string" && payload.title.length > 0) {
+  document.title = payload.title;
+}
🤖 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 `@diff-viewer/src/viewer-controller.ts` at line 74, The controller currently
unconditionally assigns document.title = payload.title which overwrites the
fallback set in main.tsx; update the assignment to only run when payload.title
is a non-empty string (e.g. check payload.title exists and payload.title.trim()
!== '') so document.title is left intact when no title is provided. Locate the
statement in viewer-controller.ts (the document.title / payload.title
assignment) and replace it with a guarded conditional that only sets
document.title for valid, non-empty titles.

263-263: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Localize the hardcoded Commit label.

Commit ${index + 1} is user-facing English text in production TS code and should come from localized labels.

As per coding guidelines: “For production user-facing text, fail partial localization … web UI … changes must read from next-intl or another locale-specific source and update every locale …”.

🤖 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 `@diff-viewer/src/viewer-controller.ts` at line 263, The hardcoded user-facing
string `Commit ${index + 1}` should be replaced with a localized label: fetch
the locale message (e.g., a message key like "commitLabel" with a placeholder)
from next-intl (or your app's i18n helper) and format it with the index+1 value
instead of returning the template literal directly; update the corresponding
locale files to include the "commitLabel" entry (e.g., "Commit {number}") so all
locales are covered.

1450-1454: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Flat file-tree refresh does repeated full-list rebuilds during streaming.

In fallback (non-Pierre) mode, each refresh clears and rebuilds the entire file list, which is repeated full-collection work on a hot path and will degrade with large diffs (~1000 files).

As per coding guidelines: apply .github/review-bot-rules/algorithmic-complexity.md and flag repeated full-collection work in hot paths for scalable user data.

🤖 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 `@diff-viewer/src/viewer-controller.ts` around lines 1450 - 1454, The fallback
refresh currently always clears fileList (fileList.textContent = "") and calls
setupFlatFileExplorer(entries), causing repeated full-list rebuilds; change this
hot-path to perform incremental updates: keep a cached previousEntries map (by
path/id) in the viewer controller, compute a delta between sourceEntries(source)
and previousEntries, call syncFileTreeSelectionMaps as needed, and update the
DOM via targeted add/remove/update operations (or provide a new
setupFlatFileExplorerIncremental(entriesDelta) helper) instead of clearing and
rebuilding the entire list; ensure functions referenced (sourceEntries,
syncFileTreeSelectionMaps, setupFlatFileExplorer, fileList) are updated to
support incremental diff application and update previousEntries after applying
deltas.
🤖 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.

Outside diff comments:
In `@diff-viewer/src/viewer-controller.ts`:
- Line 74: The controller currently unconditionally assigns document.title =
payload.title which overwrites the fallback set in main.tsx; update the
assignment to only run when payload.title is a non-empty string (e.g. check
payload.title exists and payload.title.trim() !== '') so document.title is left
intact when no title is provided. Locate the statement in viewer-controller.ts
(the document.title / payload.title assignment) and replace it with a guarded
conditional that only sets document.title for valid, non-empty titles.
- Line 263: The hardcoded user-facing string `Commit ${index + 1}` should be
replaced with a localized label: fetch the locale message (e.g., a message key
like "commitLabel" with a placeholder) from next-intl (or your app's i18n
helper) and format it with the index+1 value instead of returning the template
literal directly; update the corresponding locale files to include the
"commitLabel" entry (e.g., "Commit {number}") so all locales are covered.
- Around line 1450-1454: The fallback refresh currently always clears fileList
(fileList.textContent = "") and calls setupFlatFileExplorer(entries), causing
repeated full-list rebuilds; change this hot-path to perform incremental
updates: keep a cached previousEntries map (by path/id) in the viewer
controller, compute a delta between sourceEntries(source) and previousEntries,
call syncFileTreeSelectionMaps as needed, and update the DOM via targeted
add/remove/update operations (or provide a new
setupFlatFileExplorerIncremental(entriesDelta) helper) instead of clearing and
rebuilding the entire list; ensure functions referenced (sourceEntries,
syncFileTreeSelectionMaps, setupFlatFileExplorer, fileList) are updated to
support incremental diff application and update previousEntries after applying
deltas.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0a766da6-39bc-44d1-a71c-e0e31cf700ac

📥 Commits

Reviewing files that changed from the base of the PR and between 9cc13f9 and f5827e1.

⛔ Files ignored due to path filters (1)
  • diff-viewer/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • .github/workflows/ci.yml
  • Resources/markdown-viewer/diff-viewer-app/main.mjs
  • diff-viewer/package.json
  • diff-viewer/src/App.tsx
  • diff-viewer/src/main.tsx
  • diff-viewer/src/styles.css
  • diff-viewer/src/viewer-controller.ts
  • diff-viewer/vite.config.mjs

Comment thread diff-viewer/src/main.tsx
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 327cbfa8ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread diff-viewer/src/viewer-controller.ts Outdated
viewerElement.replaceChildren(status);
}
document.body.dataset.loading = options.loading === true || options.pending === true ? "true" : "false";
document.body.dataset.statusOnly = "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.

P2 Badge Restore status-only layout for status pages

When the payload has a statusMessage (for example an error/no-diff page, or a pending replacement page), showStatusMessage now forces data-status-only to false. The previous generated HTML set data-status-only="true" for those pages and had CSS to hide #files-sidebar; with this React version, status pages keep the empty file sidebar and 0-count footer visible beside the message. Preserve the status-only state for status/pending pages or explicitly hide the sidebar in that branch.

Useful? React with 👍 / 👎.

coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 1, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 `@scripts/build-diff-viewer-app.sh`:
- Around line 17-21: The failure handling for the diff command treats any
non-zero exit from diff as "stale assets"; change the logic around the diff
invocation (the diff command comparing "$OUT_DIR" and "$tmp_dir" writing to
"$diff_output") to inspect diff's exit status ($?) and differentiate status 1
(files differ) from status 2 (runtime/error): if status is 1, print the diff
content and exit 1 with the stale-assets message as currently done; if status is
2, print the diff output and/or a clear error message indicating diff failed
(include the error/errno if available) and exit with a distinct non-zero code
(e.g., 2) so callers can distinguish runtime errors from mismatches.
🪄 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: 46dc91af-5fab-4a79-bf53-e922ae75e681

📥 Commits

Reviewing files that changed from the base of the PR and between f5827e1 and 327cbfa.

📒 Files selected for processing (8)
  • Resources/markdown-viewer/diff-viewer-app/main.mjs
  • diff-viewer/index.html
  • diff-viewer/src/App.tsx
  • diff-viewer/src/global.d.ts
  • diff-viewer/src/styles.css
  • diff-viewer/src/viewer-controller.ts
  • diff-viewer/tsconfig.json
  • scripts/build-diff-viewer-app.sh
💤 Files with no reviewable changes (1)
  • diff-viewer/src/styles.css

Comment thread scripts/build-diff-viewer-app.sh Outdated
Match the markdown panel: the page paints no fill (body transparent) and lets
the cmux window backdrop provide the background, so transparent Ghostty themes
show through identically. `--cmux-diff-bg` stays opaque since it feeds text and
menu color-mix blends. Removes the now-unused page-owned opacity baking.

Enable React Compiler via babel-plugin-react-compiler (React 19 target), wired
through @vitejs/plugin-react's babel option. Vite 7 + plugin-react 5 + React
19.2 (which ships react/compiler-runtime) support it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread diff-viewer/package.json
"react": "19.2.3",
"react-dom": "19.2.3",
"vite": "^7.2.7"
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Build tools incorrectly listed as production dependencies

Medium Severity

@vitejs/plugin-react, vite, and other build-only packages are listed under dependencies instead of devDependencies. In Vite's lib mode, all packages in dependencies are externalized by default, meaning react and react-dom would produce bare-specifier imports (e.g., import ... from 'react') in the built main.mjs that cannot resolve in a browser context. Moving vite and @vitejs/plugin-react to devDependencies is also correct for package hygiene, but the main risk is that react and react-dom being in dependencies triggers externalization, producing a broken runtime bundle.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 11c4097. Configure here.

coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 2, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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/BrowserPanel.swift`:
- Around line 3444-3448: The usesTransparentBackground Bool is currently latched
for the BrowserPanel lifetime which keeps the WKWebView clear after navigations;
change this to be scoped to the current content/URL instead and update the code
paths that apply/clear the webview background (e.g. usesTransparentBackground,
applyWebViewBackground(color:), and the navigation/delegate handlers in
BrowserPanel such as the methods that handle newContent/navigation decisions) so
the clear background is only enabled when the active content is an internal cmux
surface (or URL scheme) and is explicitly disabled when the panel navigates away
or is repurposed; gate the clear-background logic on content kind or URL scheme
and ensure any navigation delegate callbacks clear the transparent state.
🪄 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: b077caa7-664f-4cf4-80a1-9c2005c1183e

📥 Commits

Reviewing files that changed from the base of the PR and between 7e1e55e and 11c4097.

⛔ Files ignored due to path filters (1)
  • diff-viewer/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • CLI/cmux_open.swift
  • Resources/markdown-viewer/diff-viewer-app/main.mjs
  • Sources/Panels/BrowserPanel.swift
  • Sources/TerminalController.swift
  • Sources/Workspace.swift
  • cmuxTests/CMUXOpenCommandTests.swift
  • diff-viewer/package.json
  • diff-viewer/src/appearance.ts
  • diff-viewer/src/styles.css
  • diff-viewer/test/appearance.test.ts
  • diff-viewer/vite.config.mjs

Comment thread Sources/Panels/BrowserPanel.swift
lawrencecchen and others added 4 commits June 1, 2026 23:18
Save the diff viewer token, request path, and transparent-background flag in
the browser surface session snapshot instead of discarding diff URLs as
temporary. On restore, BrowserPanel re-registers the token from its on-disk
manifest with the app-owned CmuxDiffViewerURLSchemeHandler and navigates via
the cmux-diff-viewer:// custom scheme, so the diff comes back even though the
local HTTP server that originally served it is gone. Restored surfaces also
come back transparent. (Diff content currently lives in /tmp, which survives
kill/reopen; a durable Application Support store is a follow-up.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
react-doctor flagged the diff viewer root callback ref as redundant manual
memoization. React Compiler memoizes it, so use the bare function.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The job will cover more in-app React UIs than the diff viewer over time.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8aa252e40

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/Panels/BrowserPanel.swift Outdated
let fileObjects = object["files"] as? [[String: Any]] else {
return false
}
let files = fileObjects.compactMap { Self.registeredFile(from: $0) }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve remote patch entries when restoring manifests

When a streamed GitHub diff is opened with CMUX_DIFF_VIEWER_STREAM_REMOTE=1, the manifest entry for the patch sidecar is written with remote_url and an empty file_path. This restoration path converts every manifest entry into a local RegisteredFile, so register(...) later rejects the remote patch entry as an unreadable local file and registerFromManifest returns false; the persisted remote diff viewer cannot be restored after an app restart. Preserve remote_url through the scheme handler or avoid trying to restore these manifests as local-only files.

Useful? React with 👍 / 👎.

if let token = snapshot.diffViewerToken,
let requestPath = snapshot.diffViewerRequestPath,
CmuxDiffViewerURLSchemeHandler.shared.registerFromManifest(token: token),
let diffURL = CmuxDiffViewerURLSchemeHandler.diffViewerURL(token: token, requestPath: requestPath) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Rewrite restored selector URLs off the dead HTTP server

For restored diff viewers that have source/repo/base selectors, the HTML payload still contains the absolute http://127.0.0.1:<old-port>/... option URLs generated when the page was first written. This branch only changes the currently restored page to cmux-diff-viewer://..., so after an app restart (when the original local HTTP server is gone) changing a selector navigates back to the stale port even though the alternate pages are in the manifest. Re-map the payload option URLs to the custom scheme during restore or store them as restorable URLs when the pages are generated.

Useful? React with 👍 / 👎.

Address Codex review:
- shouldRenderWebViewForSessionSnapshot() returned false for diff viewers
  (their URL is "temporary", so the persisted URL is nil), so a restored diff
  surface registered its token but never navigated. Honor the render intent
  for diff viewers via diffViewerSessionComponents().
- registerFromManifest() now skips manifests with remote_url / empty file_path
  entries (streamed remote diffs the local scheme handler cannot serve) instead
  of throwing inside register().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address Codex review: persisting any diff viewer left a blank panel on restart
when the diff could not be restored via the custom scheme. Gate persistence on
a new diffViewerRestorable() check that requires a local-only manifest (no
remote_url streamed PR diffs) and an entry page that is not a pending
placeholder (whose deferred-load wait endpoint only the local HTTP server
serves). Factored manifest loading into localManifestFiles().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +2650 to +2694
private func localManifestFiles(token: String) -> [RegisteredFile]? {
guard Self.isValidToken(token) else { return nil }
let manifestURL = trustedRootURL.appendingPathComponent(".manifest-\(token).json", isDirectory: false)
guard let data = try? Data(contentsOf: manifestURL),
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let fileObjects = object["files"] as? [[String: Any]],
!fileObjects.isEmpty else {
return nil
}
var files: [RegisteredFile] = []
for fileObject in fileObjects {
let filePath = fileObject["file_path"] as? String ?? ""
if fileObject["remote_url"] is String || filePath.isEmpty {
return nil
}
guard let file = Self.registeredFile(from: fileObject) else { return nil }
files.append(file)
}
return files
}

/// Whether a diff viewer surface can be restored through the custom scheme.
/// Requires a local-only manifest and an entry page that is not a pending
/// placeholder — pending pages poll a deferred-load wait endpoint that only
/// the local HTTP server implements, so they would render-fail under the
/// custom scheme.
func diffViewerRestorable(token: String, requestPath: String) -> Bool {
guard let files = localManifestFiles(token: token),
let entry = files.first(where: { $0.requestPath == requestPath }),
let handle = try? FileHandle(forReadingFrom: entry.fileURL) else {
return false
}
defer { try? handle.close() }
let head = (try? handle.read(upToCount: 1024)) ?? Data()
if let text = String(data: head, encoding: .utf8),
text.contains("data-cmux-diff-pending=\"true\"") {
return false
}
return true
}

/// Extracts the diff viewer `(token, requestPath)` from a live diff viewer
/// URL, accepting both the custom scheme (`cmux-diff-viewer://<token>/<path>`)
/// and the local HTTP server form (`http://127.0.0.1:<port>/<token>/<path>#cmux-diff-viewer`).
static func diffViewerComponents(from url: URL?) -> (token: String, requestPath: 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.

P1 Synchronous disk I/O on the main actor

localManifestFiles performs blocking disk reads (Data(contentsOf:), FileHandle(forReadingFrom:), handle.read(upToCount:)) and is called from two @MainActor-isolated BrowserPanel paths:

  1. restoreSessionSnapshotregisterFromManifestlocalManifestFiles (which also feeds into register, itself doing FileManager.default.fileExists and isReadableFile for every registered file)
  2. shouldPersistSessionSnapshotdiffViewerRestorablelocalManifestFiles + FileHandle read

Both paths run synchronously on the main actor — the session-restore path runs at app launch, and shouldPersistSessionSnapshot may be called on every save cycle. The full chain (manifest JSON read → file-exists checks per registered file → 1 KB header read) blocks the main thread for each diff viewer surface being restored or persisted. These helpers should be async, dispatching their I/O off the main actor (e.g. via nonisolated func ... async throws using structured concurrency), with the callers await-ing the result.

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!

Address Codex review: a restored diff viewer runs under cmux-diff-viewer://,
but the source/repo/base option URLs were generated against the now-dead local
HTTP server, so switching diff targets after restart hit a dead port. Rewrite
those option URLs to the custom scheme (reusing the current token, dropping the
query/fragment the scheme handler rejects) when the page runs under the custom
scheme; same-session http pages are unchanged.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e3dd54907

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +4854 to +4858
navigateWithoutInsecureHTTPPrompt(
to: diffURL,
recordTypedNavigation: false,
preserveRestoredSessionHistory: 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.

P2 Badge Allow workers for restored diff viewers

When a saved diff viewer is restored through cmux-diff-viewer://, the HTML is served by CmuxDiffViewerURLSchemeHandler, whose CSP sets default-src 'none' and never adds worker-src 'self'. In this restored-only path the React viewer still calls createCodeViewWorkerPool() and attempts to spawn the bundled diff worker, so WebKit blocks the worker and every restored large diff silently falls back to main-thread highlighting; same-session HTTP viewers do not hit that CSP. Please add a worker source directive for the custom-scheme HTML response before navigating restored viewers this way.

Useful? React with 👍 / 👎.

Address Codex review: deferred diff pages already evaluated main.mjs, and
module scripts run once per realm, so document.write-ing the final replacement
HTML left an empty #root (the module bootstrap never re-ran). Reload instead so
a fresh document load re-bootstraps React against the now-final on-disk HTML.

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

@cursor cursor Bot left a comment

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.

There are 2 total unresolved issues (including 1 from previous review).

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 35c7679. Configure here.

Comment thread diff-viewer/src/viewer-controller.ts
#app is a grid with only grid-template-rows set, so its implicit single column
defaulted to `auto` (max-content) and let #content size to its content width
instead of the viewport. With a narrow pane that pushed the files sidebar past
the window's right edge (measured #content 887px vs viewport 753px). Constrain
#app's column to minmax(0, 1fr) so #content fits the viewport and the grid
distributes viewer + files within it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lawrencecchen and others added 2 commits June 2, 2026 01:18
Address Codex review: deferred git diffs overwrite the opening page with a
data-cmux-diff-redirect stub that bounces to the original http://127.0.0.1:<port>
URL. diffViewerRestorable only rejected pending placeholders, so a snapshot
captured before the WebView followed the redirect would restore the stub and
navigate to a dead local HTTP server. Reject redirect stubs too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address Codex review: "Collapse all diffs" only rewrote already-flushed items,
so items parsed afterward (while a large diff is still streaming) rendered
expanded despite the toolbar showing collapsed. Initialize new diff items with
collapsed: appState.collapsed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lawrencecchen lawrencecchen dismissed coderabbitai[bot]’s stale review June 2, 2026 10:46

Dismissing: this review is on commit 11c4097, now 5+ commits stale. All findings have been addressed in later commits; Codex review is clean on HEAD a8f5607, all CI is green, and no actionable inline comments remain.

@lawrencecchen lawrencecchen merged commit 1a7ebde into main Jun 2, 2026
20 checks passed
@lawrencecchen lawrencecchen deleted the task-diff-viewer-react-app branch June 2, 2026 10:46
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