Skip to content

iOS diff viewer: native tree + Pierre web rendering over new mobile git RPC - #8014

Closed
azooz2003-bit wants to merge 16 commits into
mainfrom
feat-ios-diff-viewer-dfv1
Closed

azooz2003-bit wants to merge 16 commits into
mainfrom
feat-ios-diff-viewer-dfv1

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 13, 2026 •

Copy link
Copy Markdown
Collaborator

Adds a GitHub-quality read-only diff viewer to the iOS app, phase 1 of the diff viewer program.

Mac host gains mobile.workspace.git.status and mobile.workspace.git.diff RPCs (workspace-scoped ticket auth, worktree-vs-HEAD baseline, rename detection, untracked-as-added, binary flags, per-path diff batching with a 4MB soft cap and truncated/too_large reporting) backed by a new WorkspaceGitService in Packages/macOS/CmuxGit. The diff web bundle (webviews/) gains a payload.mobileHost mode: desktop chrome hidden, window.cmuxMobileDiff control API (scrollToFile, nextFile/prevFile, setLayout unified/split via Pierre diffStyle, setThemeMode), and throttled ready/stats/currentFile/error messages to a cmuxMobileDiff WebKit handler; macOS behavior is unchanged without the flag.

On iOS, a DEBUG-gated Changes toolbar button on the workspace screen opens a full-screen viewer: native collapsible file tree (M/A/D/R badges, +/- counts, rename old→new) from the status RPC, then a continuous Pierre-rendered diff in a WKWebView served through a cmux-mobile-diff:// scheme handler that streams RPC patch batches. Native sticky header shows the current file with prev/next chevrons and reopens the tree as a sheet; portrait renders unified, landscape true side-by-side; light/dark themes registered from static cmux Ghostty palettes; loading, empty, error, and too-large states; 37 localized strings (en+ja). Debug flag: cmux.mobile.debug.diffViewerChangesEnabled.

Supersedes the earlier prototype in #5629.

Dogfood seed: a repo with modified/added/deleted/renamed/binary/untracked files plus a 100-file, ~10k-line-diff repo; recipe in the first PR comment.

🤖 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

Adds a read-only diff viewer to iOS with a native changed-files tree and Pierre-rendered diffs, powered by new mobile Git RPCs. Phase 1 and DEBUG-gated; macOS behavior is unchanged.

  • New Features

    • Mac host: workspace-scoped Git status and size-capped diffs (4MB) with rename pairing (old_path), binary flags, untracked-as-added, 20-path batching, and truncated/too_large reporting.
    • CmuxGit: WorkspaceGitService via system Git; joins porcelain+numstat; computes untracked line counts/binary in-process (2,000 cap, truncated_untracked); tests added.
    • Web bundle (mobile host): hides desktop UI; exposes window.cmuxMobileDiff controls (next/prev/scrollToFile/setLayout/setThemeMode) and ready/stats/currentFile/error messages; tags binary patches; split/unified and light/dark via Pierre options.
    • iOS UI: full-screen viewer with native collapsible tree and a WKWebView served by a cmux-mobile-diff:// handler streaming batched patches with truncated retries; sticky header, portrait unified/landscape split, light/dark themes, too‑large and error-with-retry states; localized (en, ja).
    • Auth and session: Git RPCs require workspace tickets and are advertised by the host; the viewer routes RPCs over the shell’s live mobileDiffRPCClient (shared session) to avoid short‑lived attach-ticket expiry.
  • Bug Fixes

    • Stream resilience: keep rendered content on mid-stream failure with retry banner; scroll-to-file retries as items stream; tree built once per snapshot; scheme responses now return HTTP 200 with proper headers so fetch() accepts them in WKWebView; UTType-based MIME mapping with a true-unknown extension fallback; single feature-flag seam.
    • Git robustness and auth: core.quotepath=off, GIT_OPTIONAL_LOCKS=0, consume C-entry second record, diff against the empty tree for unborn HEAD, and only read regular files for untracked stats (non-regular files report 0/0); client and host enforce mobile.workspace.git.* auth; re-applied host ticket allowlist after merge.
    • Observability: structured os_log at the Mac RPC handler, iOS scheme-handler patch stream, and web-posted renderer errors to localize failures across seams.
    • Build/infra: updated dependency-graph originHash; ignored the workspace-level SwiftPM lockfile path and removed the stray file; adopted main’s submodule pointers.

Written for commit 3f8b807. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added a native “Changes” diff viewer for mobile with unified/split layouts, light/dark/system theme support, file navigation, and a “changed files” tree.
    • Added streamed workspace Git status/diff with rename-aware output, path batching, and progressive handling for size limits (too large/binary indicators).
  • Bug Fixes
    • Fixed authorization for workspace Git status/diff RPC requests, including method aliases.
  • Tests
    • Added/expanded decoding, batching, and UI/web integration tests for the diff viewer and Git models.
  • Documentation
    • Updated CmuxGit documentation for the new workspace Git status/diff service behavior and examples.

cmux reload-cloud and others added 5 commits July 13, 2026 13:21
Mac host: workspace-scoped git status (porcelain+numstat, renames,
binary, untracked-as-added) and size-capped per-path unified diff
(4MB soft cap, truncated/too_large reporting) via WorkspaceGitService
in CmuxGit. iOS: response decoders + auth registration in CmuxMobileRPC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
payload.mobileHost hides desktop chrome and exposes window.cmuxMobileDiff
(scrollToFile/nextFile/prevFile/setLayout/setThemeMode) plus throttled
ready/stats/currentFile/error messages to webkit cmuxMobileDiff handler.
Split uses Pierre diffStyle. macOS behavior unchanged without the flag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DEBUG-gated Changes toolbar button on workspace detail opens a
fullScreenCover: native collapsible file tree (M/A/D/R badges, +/-
counts, renames) from mobile.workspace.git.status, and a continuous
Pierre-rendered diff in a WKWebView served via cmux-mobile-diff://
scheme handler that streams batched mobile.workspace.git.diff patches.
Native sticky header (current file, prev/next, tree sheet), unified
portrait / split landscape, light/dark themes, loading/empty/error and
too-large states. Debug flag cmux.mobile.debug.diffViewerChangesEnabled.
Localized en+ja (37 keys).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 14, 2026 10:43pm
cmux-staging Building Building Preview, Comment Jul 14, 2026 10:43pm

@coderabbitai

coderabbitai Bot commented Jul 13, 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
📝 Walkthrough

Walkthrough

Adds end-to-end mobile workspace Git status/diff support, including macOS Git parsing and RPC endpoints, iOS RPC models and native SwiftUI browsing, a bundled mobile WebKit viewer, authorization updates, and tests.

Changes

Workspace Git backend

Layer / File(s) Summary
Git service, parsing, and response models
Packages/macOS/CmuxGit/Sources/CmuxGit/..., Packages/macOS/CmuxGit/README.md, Packages/macOS/CmuxGit/Tests/...
Adds Git status/diff models, parsing, byte-capped accumulation, command execution, validation, integration tests, and documentation.

Mobile RPC transport

Layer / File(s) Summary
Workspace Git RPC endpoints
Sources/TerminalController+MobileGit.swift, Sources/TerminalController.swift, Sources/Mobile/MobileHostService.swift, Packages/iOS/CmuxMobileRPC/..., cmux.xcodeproj/project.pbxproj
Adds status and diff RPC aliases, workspace authorization, parameter validation, typed response decoding, error conversion, and project wiring.

Mobile web diff bridge

Layer / File(s) Summary
Mobile-host rendering and messaging
webviews/src/..., Resources/markdown-viewer/..., webviews/test/...
Adds mobile-host layout and theme handling, native bridge messages, throttled navigation updates, binary detection, render-error reporting, bundled CSS changes, and web tests.

Native iOS viewer

Layer / File(s) Summary
Diff browsing UI and streaming
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/..., Packages/iOS/CmuxMobileShellUI/Tests/...
Adds RPC batching and streaming, WebKit hosting, status projections, changed-file tree navigation, detail navigation, loading/error states, and related tests.

Application integration

Layer / File(s) Summary
Scene, workspace, settings, and resources
ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift, ios/cmux/Resources/Localizable.xcstrings, ios/cmux-ios.xcodeproj/project.pbxproj
Injects per-route RPC clients, adds the debug Changes toggle and toolbar presentation, adds localized strings, and bundles the markdown viewer resources.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WorkspaceDetailView
  participant MobileDiffViewerModel
  participant MobileDiffRPCService
  participant TerminalController
  participant WorkspaceGitService
  participant MobileDiffWebView
  WorkspaceDetailView->>MobileDiffViewerModel: Present Changes viewer
  MobileDiffViewerModel->>MobileDiffRPCService: loadStatus()
  MobileDiffRPCService->>TerminalController: mobile.workspace.git.status
  TerminalController->>WorkspaceGitService: status(forDirectory:)
  WorkspaceGitService-->>TerminalController: WorkspaceGitStatus
  TerminalController-->>MobileDiffRPCService: status response
  MobileDiffWebView->>MobileDiffRPCService: patchStream(paths:)
  MobileDiffRPCService->>TerminalController: mobile.workspace.git.diff
  TerminalController->>WorkspaceGitService: diff(forDirectory:paths:)
  WorkspaceGitService-->>TerminalController: WorkspaceGitDiff
  TerminalController-->>MobileDiffWebView: streamed patch chunks
Loading

Possibly related PRs

  • manaflow-ai/cmux#7384 — Both PRs modify MobileCoreRPCClient authorization and fallback method-switch logic.

Suggested reviewers: lawrencecchen


Important

Pre-merge checks failed

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

❌ Failed checks (4 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error MobileDiffTreeBuilder linearly rescans sibling arrays per path component, and MobileDiffTreeDirectory.fileCount recursively rescans subtrees during render; both can go quadratic on large trees. Use a child-directory dictionary in the builder and cache subtree file counts on the frozen tree so visibleRows/row rendering stays linear per snapshot.
Cmux Swift @Concurrent ❌ Error Nonisolated async Git/RPC helpers are called from @MainActor code with no @concurrent or detached hop. Add @concurrent to the heavy nonisolated async helpers or move the work behind an explicit Task.detached/actor hop before calling them from UI isolation.
Cmux User-Facing Error Privacy ❌ Error mobileDiffErrorMessage returns raw error.message, and App.tsx posts it to the mobile bridge, so user-visible diff errors can leak upstream/internal details. Return a generic render-failure string in user-visible UI; keep raw error text only in internal logs or non-user-visible diagnostics.
Cmux Swiftui State Layout ❌ Error MobileDiffScreen adds GeometryReader at line 38 to switch .split/.unified, a geometry-driven layout measurement the rule forbids. Replace the GeometryReader with a localized measurement approach (onGeometryChange or a background probe) or static size-class logic that doesn’t alter parent layout.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description is detailed, but it does not follow the required template: explicit Testing, Demo Video, Review Trigger, and Checklist sections are missing. Add the missing template sections with concrete local/manual test notes, a demo video link, the review-trigger comment block, and a completed checklist.
✅ Passed checks (19 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed New MainActor types are intentional SwiftUI/WebKit UI code, and the added service/value models stay nonisolated Sendable; no actor-isolation regression found.
Cmux Swift Blocking Runtime ✅ Passed No new semaphores, waits, sleeps, asyncAfter, main-queue sync, or manual locks were introduced in the touched production Swift files; the new code is async/actor-based.
Cmux Browser Automation Off-Main ✅ Passed Only WorkspaceGitService.swift changed; no browser.* socket automation code, worker routing, or WebKit-wait paths were touched.
Cmux Expensive Synchronous Load ✅ Passed No new agent-history/session/transcript loads were added; the new Git work is behind nonisolated async services and awaited RPCs, not sync main-actor UI code.
Cmux Cache Substitution Correctness ✅ Passed Fresh Git reads stay authoritative; reused state is transient UI snapshot/layout with refresh or explicit-payload precedence, not a stale persistence/undo cache.
Cmux No Hacky Sleeps ✅ Passed Timers are limited to a tested throttled emitter and an rAF post-render refresh; no fixed sleeps or polling loops were added in production code.
Cmux Swift Concurrency ✅ Passed The new Swift async work uses SwiftUI/WebKit boundaries and a cancellable AsyncThrowingStream task; no new Combine or non-main DispatchQueue patterns were introduced.
Cmux Swift File And Package Boundaries ✅ Passed Core Git logic lives in small CmuxGit/CmuxMobileShellUI package files; only pre-existing oversized app files were touched incidentally.
Cmux Swiftpm Lockfiles ✅ Passed PASS: CmuxMobileShellUI adds only a local path dependency, cmux.xcodeproj only adds a source file, and no repo-owned .gitignore changes Package.resolved; no lockfile diff required.
Cmux Swift Logging ✅ Passed No PR-era patch adds print/debugPrint/dump/NSLog or unsafe Logger usage in production Swift; the only NSLog found is DEBUG-gated.
Cmux Full Internationalization ✅ Passed New Swift UI strings use L10n.string/String(localized:), the added xcstrings entries have en/ja translations, and web-view labels are supplied from those localized native strings.
Cmux Architecture Rethink ✅ Passed The new viewer uses explicit model/controller/coordinator ownership at bridge boundaries; no sleeps, polling, locks, or workaround timing paths were introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR adds only views/sheets/fullScreenCover flows; no new NSWindow/WindowGroup/NSPanel/NSWindowController or cmuxAuxiliaryWindowIdentifiers changes were introduced.
Cmux Source Artifacts ✅ Passed Only changed path is a Swift source file in Packages/macOS/CmuxGit; the diff adds a regular-file lstat guard, with no artifact/cache/build paths.
Cmux No Test Or Debug Seam In Production Source ✅ Passed New #if DEBUG blocks gate real iOS diff-viewer UI; no test-only/debugger-only APIs or widened test seams were added in production Sources.
Cmux No Ambient Global State ✅ Passed The new Swift code is owned by constructable types/services and only uses private file-scope helpers plus allowed constants; no new top-level API, mutable globals, or singletons were introduced.
Title check ✅ Passed The title clearly summarizes the main change: a native iOS diff viewer built on new mobile Git RPCs and Pierre web rendering.
✨ 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-ios-diff-viewer-dfv1

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.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Dogfood seed recipe used for evidence (creates /tmp/dfv1-seed/{basic,perf,clean}):

#!/bin/bash
# Seed repos for iOS diff viewer phase 1 acceptance criteria.
# Creates /tmp/dfv1-seed/basic (M/A/D/R + binary + large + untracked) and /tmp/dfv1-seed/perf (100 files / ~10k changed lines).
set -euo pipefail

ROOT=/tmp/dfv1-seed
rm -rf "$ROOT"
mkdir -p "$ROOT"

# ---------- basic repo ----------
B="$ROOT/basic"
mkdir -p "$B" && cd "$B"
git init -q -b main
git config user.email dfv1@example.com && git config user.name "DFV1 Seed"

mkdir -p Sources/App Sources/Util Docs
# file to modify
cat > Sources/App/Login.swift <<'EOF'
import Foundation

struct LoginValidator {
    func validate(email: String, password: String) -> Bool {
        guard email.contains("@") else { return false }
        guard password.count >= 8 else { return false }
        return true
    }

    func normalize(_ email: String) -> String {
        return email.lowercased()
    }
}
EOF
# file to delete
cat > Sources/Util/Legacy.swift <<'EOF'
// Legacy helpers kept for compatibility.
func legacyThing() -> Int { return 42 }
func legacyOther() -> Int { return 7 }
EOF
# file to rename
cat > Sources/Util/StringHelpers.swift <<'EOF'
extension String {
    var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) }
    var isBlank: Bool { trimmed.isEmpty }
}
EOF
# binary file (small PNG)
printf '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n\x2d\xb4\x00\x00\x00\x00IEND\xaeB\x60\x82' > Docs/logo.png
# large text file
seq 1 3000 | awk '{print "line " $1 ": stable content alpha beta gamma"}' > Docs/big.txt
# typescript file for highlighting variety
cat > Sources/App/api.ts <<'EOF'
export interface User { id: string; name: string }
export async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) throw new Error(`failed: ${res.status}`)
  return res.json() as Promise<User>
}
EOF
git add -A && git commit -qm "seed base"

# modified (word-level changes on some lines)
sed -i '' -e 's/password.count >= 8/password.count >= 12/' -e 's/email.lowercased()/email.trimmingCharacters(in: .whitespaces).lowercased()/' Sources/App/Login.swift
cat >> Sources/App/Login.swift <<'EOF'

extension LoginValidator {
    func strength(of password: String) -> Int {
        var score = password.count
        if password.rangeOfCharacter(from: .decimalDigits) != nil { score += 5 }
        if password.rangeOfCharacter(from: .uppercaseLetters) != nil { score += 5 }
        return score
    }
}
EOF
# modified typescript (intra-line)
sed -i '' -e 's|/api/users/|/api/v2/users/|' -e 's/failed:/fetchUser failed:/' Sources/App/api.ts
# added (staged)
cat > Sources/App/Signup.swift <<'EOF'
import Foundation

struct SignupFlow {
    let validator = LoginValidator()
    func canSubmit(email: String, password: String) -> Bool {
        validator.validate(email: email, password: password)
    }
}
EOF
git add Sources/App/Signup.swift
# deleted
git rm -q Sources/Util/Legacy.swift
# renamed (with tiny modification to keep >50% similarity)
git mv Sources/Util/StringHelpers.swift Sources/Util/TextHelpers.swift
printf '\n// moved 2026-07\n' >> Sources/Util/TextHelpers.swift
git add Sources/Util/TextHelpers.swift
# binary modified
printf '\x00\x01\x02extra' >> Docs/logo.png
# large diff in big file: change a band of lines + insert block
sed -i '' -e 's/stable content alpha/CHANGED content omega/' Docs/big.txt
# untracked
cat > Docs/notes.md <<'EOF'
# Release notes draft
- diff viewer phase 1
- untracked file must appear as Added
EOF

# ---------- perf repo ----------
P="$ROOT/perf"
mkdir -p "$P" && cd "$P"
git init -q -b main
git config user.email dfv1@example.com && git config user.name "DFV1 Seed"
mkdir -p src
for i in $(seq 1 100); do
  f="src/module_$(printf '%03d' "$i").swift"
  { echo "import Foundation"; echo "struct Module$i {";
    for j in $(seq 1 80); do echo "    func f${j}_v1() -> Int { return $((i*100+j)) }"; done;
    echo "}"; } > "$f"
done
git add -A && git commit -qm "seed base"
# change ~50 lines in every file (~5000 removed + ~5000 added = ~10k changed lines)
for i in $(seq 1 100); do
  f="src/module_$(printf '%03d' "$i").swift"
  sed -i '' -e 's/_v1() -> Int { return \([0-9]*[02468]\) }/_v2() -> Int { return \1 + 1 }/' "$f"
done

echo "Seeded:"
cd "$ROOT/basic" && echo "-- basic:" && git status --porcelain=v1 | head -12
cd "$ROOT/perf" && echo "-- perf: $(git diff --numstat | wc -l | tr -d ' ') files changed, $(git diff --shortstat)"

@greptile-apps

greptile-apps Bot commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a read-only diff viewer to the iOS app. The main changes are:

  • Workspace-scoped Git status and diff RPCs on the Mac host.
  • A native iOS changed-files tree with streamed web diff rendering.
  • Mobile diff navigation, layout, theme, loading, error, and size-limit states.
  • Ticket authorization, localization, tests, and package/project wiring.

Confidence Score: 5/5

This looks safe to merge.

  • The latest changes address the previously reported repository and file-handling failures.
  • The updated scheme handler tracks cancellation across WebKit task delivery.
  • No separate blocking issue remains in the reviewed fixes.

Important Files Changed

Filename Overview
Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift Implements bounded Git status and diff operations, including unborn repositories, untracked files, renames, binary detection, and response limits.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffURLSchemeHandler.swift Serves bundled resources and streams patch data into the diff web view with cancellation tracking.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffRPCService.swift Loads status data and streams capped diff batches with truncation handling.
Packages/iOS/CmuxMobileShellUI/Package.swift Adds the direct mobile RPC package dependency used by the diff viewer.

Reviews (10): Last reviewed commit: "iOS diff viewer: serve scheme responses ..." | Re-trigger Greptile

Comment on lines +42 to +47
arguments: ["diff", "HEAD", "--numstat", "-z", "-M", "--"],
acceptedExitStatuses: [0],
operation: "status.numstat"
)

let parser = WorkspaceGitStatusParser()

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 Unborn HEAD Rejects New Repositories

When the workspace is a newly initialized repository with no commits, git diff HEAD fails because HEAD does not resolve. The status RPC then reports an error instead of returning the untracked files as additions, so the diff viewer cannot open for that repository.

Comment on lines +55 to +62
var untrackedNumstatByPath: [String: String] = [:]
for entry in porcelainEntries where entry.untracked {
untrackedNumstatByPath[entry.path] = try await runGit(
in: repoRoot,
arguments: ["diff", "--no-index", "--numstat", "--no-color", "--", "/dev/null", entry.path],
acceptedExitStatuses: [0, 1],
operation: "status.untracked_numstat"
)

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 Untracked Files Spawn Unbounded Processes

This runs a separate git diff --no-index process serially for every untracked file. A workspace with a generated or dependency directory containing thousands of files can launch thousands of subprocesses before status returns, leaving the Changes screen loading for an excessive time and consuming substantial host resources.

Rule Used: Flag production code that adds nested full-collect... (source)

Comment on lines +104 to +109
guard isLive(identifier) else { throw CancellationError() }
if !chunk.tooLargePaths.isEmpty {
onTooLargePaths(chunk.tooLargePaths)
}
if !chunk.data.isEmpty {
task.didReceive(chunk.data)

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 Stopped Scheme Task Can Receive Data

The liveness check and didReceive call are separate operations. If WebKit stops the scheme task after the check while a patch chunk arrives during reload or dismissal, this path can call didReceive on an invalidated WKURLSchemeTask, which can raise a WebKit exception and terminate the app.

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

🤖 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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffHeaderView.swift`:
- Around line 17-20: Update the Image frame sizing in the back, previous, and
next Button declarations to provide at least a 44×44pt tap target. Preserve the
existing chevron imagery and button actions while applying the same minimum
dimensions consistently across all three navigation controls.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffHostPage.swift`:
- Around line 34-43: Add focused unit coverage for htmlData() using a title
containing </script>, &, <, >, and ". Assert the generated HTML neutralizes the
script-closing sequence and applies the expected HTML escaping in the title
element, preserving the existing escaping behavior.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffRPCService.swift`:
- Around line 6-73: Move the response decoding performed by
MobileDiffRPCService.loadStatus(), loadDiff(), and the patchStream() batch flow
off the main actor by adding an explicit non-main execution hop or applying
`@concurrent` where supported. Preserve the existing request, retry, cancellation,
yielding, and error-propagation behavior while ensuring MobileDiffStatusSnapshot
and MobileSyncGitDiffResponse parsing cannot block UI rendering.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffScreen.swift`:
- Around line 40-43: Replace the root GeometryReader in MobileDiffScreen with
onGeometryChange, storing the measured size or width needed to derive
MobileDiffHostPage.Layout. Preserve the existing landscape-to-split and
portrait-to-unified behavior while keeping the rest of the screen outside a
geometry container.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeDirectory.swift`:
- Around line 10-11: Update MobileDiffTreeDirectory so fileCount is computed
once during tree construction and stored as an immutable value, rather than
recursively recalculating descendants on every access. Adjust the directory
initializer/building logic and MobileDiffTree.append usage as needed while
preserving the existing count for each directory row.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeView.swift`:
- Around line 14-16: Cache the flattened rows derived from snapshot.files
instead of constructing MobileDiffTree and calling visibleRows inside body.
Update that cached result only when snapshot changes, such as through the view’s
snapshot-change lifecycle, while continuing to apply collapsedDirectories and
selectedPath interactions without rebuilding the tree for each render.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffWebViewCoordinator.swift`:
- Around line 50-53: Update the "error" handling in MobileDiffWebViewCoordinator
so it never forwards the bridge’s raw message to controller.showError. Always
display a curated user-safe error message such as Self.renderError, while
retaining any raw detail only for internal diagnostics if needed.

In
`@Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/MobileDiffTreeTests.swift`:
- Around line 32-35: Update the assertions in MobileDiffTreeTests around
visibleRows so they do not index visible after non-fatal `#expect` checks. Compare
visible.map(\.id) directly with the expected IDs, or use try `#require` to
establish the row count before indexing, while preserving the existing expected
order.

In
`@Packages/macOS/CmuxGit/Sources/CmuxGit/Parsing/WorkspaceGitStatusParser.swift`:
- Around line 5-13: Update parse in WorkspaceGitStatusParser so constructing
numstatByPath cannot trap when trackedEntries contains duplicate paths. Replace
Dictionary(uniqueKeysWithValues:) with a uniquing approach and define the
duplicate behavior explicitly, such as retaining the first or last entry, while
preserving the existing parsing flow.

In `@Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift`:
- Around line 55-63: Update the untracked-file numstat logic around
untrackedNumstatByPath to execute git calls concurrently with a bounded task
group or equivalent concurrency limit, rather than awaiting each runGit
sequentially. Preserve each entry’s path-to-numstat association, accepted exit
statuses, operation name, and existing error behavior while preventing unbounded
subprocess concurrency.
🪄 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: 09654c73-b5d4-4abd-957c-2d76462953be

📥 Commits

Reviewing files that changed from the base of the PR and between a023bef and 1b389a0.

📒 Files selected for processing (70)
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncGitDiffResponse.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncGitDiffTooLargeFile.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncGitStatusFile.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncGitStatusResponse.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileSyncGitResponseTests.swift
  • Packages/iOS/CmuxMobileShellUI/Package.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffAppearance.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffBatchPlanner.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffFileChange.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffHeaderView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffHostPage.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffMIMEType.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffMutableDirectory.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffPatchChunk.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffRPCClientFactory.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffRPCService.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffScreen.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffStatusSnapshot.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTree.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeBuilder.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeDirectory.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeRow.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeRowView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeScreen.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffURLSchemeHandler.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffViewerCover.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffViewerModel.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffWebView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffWebViewController.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffWebViewCoordinator.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/MobileDiffBatchPlannerTests.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/MobileDiffMIMETypeTests.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/MobileDiffTreeTests.swift
  • Packages/macOS/CmuxGit/README.md
  • Packages/macOS/CmuxGit/Sources/CmuxGit/Model/WorkspaceGitDiff.swift
  • Packages/macOS/CmuxGit/Sources/CmuxGit/Model/WorkspaceGitStatus.swift
  • Packages/macOS/CmuxGit/Sources/CmuxGit/Model/WorkspaceGitStatusFile.swift
  • Packages/macOS/CmuxGit/Sources/CmuxGit/Model/WorkspaceGitTooLargePath.swift
  • Packages/macOS/CmuxGit/Sources/CmuxGit/Parsing/WorkspaceGitNumstatEntry.swift
  • Packages/macOS/CmuxGit/Sources/CmuxGit/Parsing/WorkspaceGitParseError.swift
  • Packages/macOS/CmuxGit/Sources/CmuxGit/Parsing/WorkspaceGitPorcelainEntry.swift
  • Packages/macOS/CmuxGit/Sources/CmuxGit/Parsing/WorkspaceGitStatusParser.swift
  • Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitDiffAccumulator.swift
  • Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift
  • Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitServiceError.swift
  • Packages/macOS/CmuxGit/Tests/CmuxGitTests/WorkspaceGitDiffAccumulatorTests.swift
  • Packages/macOS/CmuxGit/Tests/CmuxGitTests/WorkspaceGitStatusParserTests.swift
  • Resources/markdown-viewer/webviews-app/chunks/diffSurface.mjs
  • Sources/Mobile/MobileHostService.swift
  • Sources/TerminalController+MobileGit.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
  • ios/cmux-ios.xcodeproj/project.pbxproj
  • ios/cmux/Resources/Localizable.xcstrings
  • ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift
  • webviews/src/App.tsx
  • webviews/src/diff-stream.ts
  • webviews/src/global.d.ts
  • webviews/src/mobile-diff.ts
  • webviews/src/pierre-options.ts
  • webviews/src/styles.css
  • webviews/src/surfaces/diffSurface.tsx
  • webviews/src/types.ts
  • webviews/test/app.test.tsx
  • webviews/test/mobile-diff.test.ts
  • webviews/test/pierre-options.test.ts

Comment on lines +17 to +20
Button(action: back) {
Image(systemName: "chevron.left")
.frame(width: 32, height: 36)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Chevron buttons are smaller than Apple's minimum tap-target guidance.

back, previous, and next use .frame(width: 32, height: 36), below Apple's recommended 44×44pt minimum hit target. These are frequently-tapped primary navigation controls in the sticky header, so undersized targets increase mis-taps, especially for users with motor impairments.

♿️ Proposed fix to widen tap targets
             Button(action: back) {
                 Image(systemName: "chevron.left")
-                    .frame(width: 32, height: 36)
+                    .frame(width: 44, height: 44)
             }

Apply similarly to the previous and next buttons.

Also applies to: 51-54, 60-63

🤖 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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffHeaderView.swift`
around lines 17 - 20, Update the Image frame sizing in the back, previous, and
next Button declarations to provide at least a 44×44pt tap target. Preserve the
existing chevron imagery and button actions while applying the same minimum
dimensions consistently across all three navigation controls.

Comment on lines +34 to +43
let jsonData = try JSONSerialization.data(withJSONObject: config, options: [.sortedKeys])
guard var json = String(data: jsonData, encoding: .utf8) else {
throw CocoaError(.fileWriteInapplicableStringEncoding)
}
json = json.replacingOccurrences(of: "</", with: "<\\/")
let escapedTitle = title
.replacingOccurrences(of: "&", with: "&amp;")
.replacingOccurrences(of: "<", with: "&lt;")
.replacingOccurrences(of: ">", with: "&gt;")
.replacingOccurrences(of: "\"", with: "&quot;")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider unit tests for the HTML/JSON escaping in htmlData().

The </script> neutralization and <title> HTML-escaping are the only defenses against injection from a user-editable title (workspace name). A small test asserting a title containing </script>, &, <, >, " renders safely would guard against silent regressions here.

🤖 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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffHostPage.swift`
around lines 34 - 43, Add focused unit coverage for htmlData() using a title
containing </script>, &, <, >, and ". Assert the generated HTML neutralizes the
script-closing sequence and applies the expected HTML escaping in the title
element, preserving the existing escaping behavior.

Comment on lines +40 to +43
GeometryReader { geometry in
let layout: MobileDiffHostPage.Layout = geometry.size.width > geometry.size.height
? .split
: .unified

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd Package.swift Packages/iOS/CmuxMobileShellUI --exec cat {}

Repository: manaflow-ai/cmux

Length of output: 2879


🏁 Script executed:

#!/bin/bash
sed -n '1,220p' Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffScreen.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 6193


Replace the root GeometryReader with onGeometryChange Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffScreen.swift:40-43

CmuxMobileShellUI targets iOS 18, so this split/unified choice can use onGeometryChange instead of wrapping the whole screen in GeometryReader. That keeps the measurement localized and avoids making the entire view hierarchy depend on a geometry container.

🤖 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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffScreen.swift`
around lines 40 - 43, Replace the root GeometryReader in MobileDiffScreen with
onGeometryChange, storing the measured size or width needed to derive
MobileDiffHostPage.Layout. Preserve the existing landscape-to-split and
portrait-to-unified behavior while keeping the rest of the screen outside a
geometry container.

Source: Coding guidelines

Comment on lines +10 to +11
var fileCount: Int {
files.count + directories.reduce(0) { $0 + $1.fileCount }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Compute directory file counts once.

fileCount recursively walks every descendant each time it is read, while MobileDiffTree.append consumes it once per directory row. Deep or large trees therefore repeat subtree scans and can degrade toward O(n²). Compute the count during tree construction and store it as an immutable value.

As per path instructions, tree building/rendering should avoid repeated full-collection scans.

🤖 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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeDirectory.swift`
around lines 10 - 11, Update MobileDiffTreeDirectory so fileCount is computed
once during tree construction and stored as an immutable value, rather than
recursively recalculating descendants on every access. Adjust the directory
initializer/building logic and MobileDiffTree.append usage as needed while
preserving the existing count for each directory row.

Source: Path instructions

Comment thread Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeView.swift Outdated
Comment on lines +5 to +13
func parse(
porcelain: String,
trackedNumstat: String,
untrackedNumstatByPath: [String: String]
) throws -> [WorkspaceGitStatusFile] {
let porcelainEntries = try parsePorcelain(porcelain)
let trackedEntries = try parseNumstat(trackedNumstat)
var numstatByPath = Dictionary(uniqueKeysWithValues: trackedEntries.map { ($0.path, $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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='Packages/macOS/CmuxGit/Sources/CmuxGit/Parsing/WorkspaceGitStatusParser.swift'
wc -l "$FILE"
echo '---'
cat -n "$FILE" | sed -n '1,220p'
echo '--- related references ---'
rg -n "parseNumstat|uniqueKeysWithValues|numstatByPath|WorkspaceGitStatusParser" Packages/macOS/CmuxGit/Sources/CmuxGit -n

Repository: manaflow-ai/cmux

Length of output: 9003


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift | cat -n
echo '--- tests/search ---'
rg -n "WorkspaceGitStatusParser|parseNumstat|trackedNumstat|untrackedNumstatByPath|numstat" Packages/macOS/CmuxGit -g '*Test*' -g '*Tests*' -g '*.swift'

Repository: manaflow-ai/cmux

Length of output: 13300


🌐 Web query:

git diff --numstat -z duplicate path entries same path duplicate numstat records rename copy documentation

💡 Result:

When using git diff --numstat, you may observe multiple records for the same path or confusing output in specific scenarios, primarily due to how Git handles unmerged paths (merge conflicts) or rename/copy detection [1][2][3]. Unmerged Paths: When a repository has unresolved merge conflicts, git diff --numstat may show multiple entries for the same path [1][2]. This occurs because Git displays the unmerged status (e.g., stage #1, #2, #3 entries) in addition to or instead of the standard diff between a specific stage and the working tree [2]. These entries represent the distinct states of the file in the index during a conflict [2]. While --numstat is intended for machine consumption, these duplicate path entries in the output can be misleading if a script assumes a one-to-one mapping between paths and output lines [1]. Rename and Copy Detection: Git's rename and copy detection (controlled by -M and -C options) transforms the list of file changes [4]. When a file is renamed or copied, the output of git diff --numstat reflects the change using a path-pairing format (e.g., {old_path => new_path}) [5]. If rename/copy detection is active, the paths are reported as the transformation pair rather than as two separate, simple file entries [4]. Key Considerations: 1. Machine Consumption: Because --numstat can output multiple records for a single path in complex states (like unmerged entries), script writers should be aware that the output is not guaranteed to be a unique list of paths [1]. 2. Rename/Copy Formatting: The output format for renames and copies is specifically designed to indicate the relationship between the source and destination paths using brace expansion notation [5]. 3. Reliability: For parsing needs, some Git developers have suggested using --raw output in conjunction with or instead of --numstat to more clearly distinguish the status of each path (e.g., modified, added, renamed) [2]. For further details on how Git processes these changes, consult the gitdiffcore(7) manual page, which details the transformation process for renames and copies [4].

Citations:


Dictionary(uniqueKeysWithValues:) can crash on duplicate numstat paths
Packages/macOS/CmuxGit/Sources/CmuxGit/Parsing/WorkspaceGitStatusParser.swift:12 builds numstatByPath from raw git diff --numstat -z -M output. If Git emits the same path twice in an edge-case repo state, this traps the host process instead of failing the parse. Use a uniquing initializer or handle duplicates explicitly.

🤖 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
`@Packages/macOS/CmuxGit/Sources/CmuxGit/Parsing/WorkspaceGitStatusParser.swift`
around lines 5 - 13, Update parse in WorkspaceGitStatusParser so constructing
numstatByPath cannot trap when trackedEntries contains duplicate paths. Replace
Dictionary(uniqueKeysWithValues:) with a uniquing approach and define the
duplicate behavior explicitly, such as retaining the first or last entry, while
preserving the existing parsing flow.

Comment thread Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift Outdated
…, quotepath, stream resilience

Status: compute untracked add counts/binary in-process (no per-file git
spawn, 2000-entry cap + truncated_untracked), diff against empty tree on
unborn HEAD, consume C-entry second record, GIT_OPTIONAL_LOCKS=0,
core.quotepath=off. Diff RPC paths carry old_path so -M pairs renames.
Capabilities advertise the git methods. iOS: batched truncated retries,
mid-stream failure keeps rendered content with localized retry banner,
scroll-to-file retries as items stream, tree built once per snapshot,
shared path splitter, UTType MIME, single feature-flag seam. Web: raw
useEffect removed, currentFile reporting on owned event points.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 14, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

Comment on lines +234 to +236
let handle = try FileHandle(forReadingFrom: url)
defer { try? handle.close() }
let data = try handle.read(upToCount: untrackedReadByteCap) ?? Data()

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 Guard non-regular file reads

The new in-process stats path opens every retained untracked path and reads it without a timeout or file-type check. If the repository contains an untracked FIFO, opening it can succeed while the read waits indefinitely for a writer. The status RPC and Changes screen then remain stuck. Check that the path is a regular file before reading it, or use a cancellation-aware bounded operation.

Rule Used: Flag new blocking or timing-based synchronization ... (source)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (3)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeView.swift (1)

43-53: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use modern String(localized:defaultValue:) with interpolation.

Using String(format:) with manually passed arguments prevents automatic ICU pluralization in the string catalog (resulting in "1 files changed"). As per the coding guidelines, use String(localized:defaultValue:) with string interpolation, which natively handles locale-aware number formatting and automatic pluralization rules.

♻️ Proposed refactor
     private var summary: String {
         String(
-            format: L10n.string(
-                "mobile.diff.summaryFormat",
-                defaultValue: "%d files changed  +%d −%d"
-            ),
-            snapshot.files.count,
-            snapshot.totalAdditions,
-            snapshot.totalDeletions
+            localized: "mobile.diff.summaryFormat",
+            defaultValue: "\(snapshot.files.count) files changed  +\(snapshot.totalAdditions) −\(snapshot.totalDeletions)"
         )
     }
🤖 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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeView.swift`
around lines 43 - 53, Update the summary computed property to use
String(localized:defaultValue:) with interpolation instead of String(format:)
and manually passed arguments. Preserve the existing mobile.diff.summaryFormat
localization key and default message while allowing locale-aware number
formatting and pluralization.

Sources: Coding guidelines, Learnings

Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift (1)

123-138: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Avoid repeated full-repository rescans during batched diff generation.

The diff RPC processes paths in batches of up to 20, but WorkspaceGitService.diff executes a full git status over the entire repository for every batch just to discover which requested paths are untracked. As per path instructions, production code must avoid repeated batch rescans for scalable workloads.

  • Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift#L123-L138: Remove the redundant git status execution and untrackedPaths extraction; instead, accept the untracked boolean as a property on WorkspaceGitDiffPath.
  • Sources/TerminalController+MobileGit.swift#L63-L81: Read the untracked boolean for each path from the JSON payload (the client already has this from the initial status response) and pass it into the WorkspaceGitDiffPath initializer.
🤖 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 `@Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift` around
lines 123 - 138, Remove the full-repository git status call and untrackedPaths
extraction from WorkspaceGitService.diff; use the untracked property supplied by
each WorkspaceGitDiffPath instead. In Sources/TerminalController+MobileGit.swift
lines 63-81, read each path’s untracked boolean from the existing JSON payload
and pass it to the WorkspaceGitDiffPath initializer.

Source: Path instructions

Sources/TerminalController+MobileGit.swift (1)

114-126: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Resolve the workspace globally to avoid active-window bias.

Because the mobile RPC payload only provides workspace_id without a window_id, v2ResolveTabManager(params:) will default to the host's currently active window. If the target workspace lives in a background window, tabManager.tabs.first will fail and incorrectly return "Workspace not found".

As per learnings, apply the established pattern to locate the correct workspace across all windows if it is not found in the active manager (e.g., using AppDelegate.shared?.locateSurface(surfaceId:) or iterating AppDelegate.shared?.mainWindowContexts to locate the target workspace_id).

🤖 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/TerminalController`+MobileGit.swift around lines 114 - 126, Update
the workspace lookup in the method containing v2ResolveTabManager(params:) so it
searches all window contexts when the active tabManager does not contain
workspaceID. Reuse the established global lookup pattern, such as
AppDelegate.shared?.locateSurface(surfaceId:) or mainWindowContexts, and return
"Workspace not found" only after the workspace is absent globally.

Source: Learnings

♻️ Duplicate comments (5)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffRPCService.swift (2)

10-17: ⚠️ Potential issue | 🟠 Major

Move diff parsing off the main actor.

loadStatus() decodes the Git status payload. Under Swift 6 NonisolatedNonsendingByDefault, this nonisolated async function will inherit the caller's actor isolation (e.g., @MainActor). As per path instructions, mark heavy async parsing helpers with @concurrent to ensure they execute off the main actor and don't stall rendering.

⚡ Proposed fix
-    func loadStatus() async throws -> MobileDiffStatusSnapshot {
+    `@concurrent`
+    func loadStatus() async throws -> MobileDiffStatusSnapshot {
🤖 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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffRPCService.swift`
around lines 10 - 17, Mark the async loadStatus() parsing helper with
`@concurrent` so MobileSyncGitStatusResponse.decode(data) and
MobileDiffStatusSnapshot construction execute off the caller’s actor, while
preserving the existing request and return behavior.

Source: Path instructions


56-64: ⚠️ Potential issue | 🟡 Minor

Move diff parsing off the main actor.

Similarly, loadDiff() decodes the diff payload. While it currently runs on the global pool when called from patchStream, marking it with @concurrent ensures it remains safely off the main actor regardless of future caller isolation changes.

⚡ Proposed fix
-    private func loadDiff(paths: [MobileDiffRequestPath]) async throws -> MobileSyncGitDiffResponse {
+    `@concurrent`
+    private func loadDiff(paths: [MobileDiffRequestPath]) async throws -> MobileSyncGitDiffResponse {
🤖 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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffRPCService.swift`
around lines 56 - 64, Mark the loadDiff(paths:) method with `@concurrent` so its
RPC response decoding via MobileSyncGitDiffResponse.decode remains off the main
actor regardless of caller isolation.

Source: Path instructions

Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffScreen.swift (1)

37-83: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Replace the root GeometryReader with onGeometryChange.

As per coding guidelines, prefer onGeometryChange over GeometryReader for measuring SwiftUI layout to avoid making the entire view hierarchy depend on a geometry container.

♻️ Proposed refactor
 struct MobileDiffScreen: View {
     let model: MobileDiffViewerModel
     // ...
     `@State` private var controller: MobileDiffWebViewController
     `@State` private var isTreePresented = false
+    `@State` private var layout: MobileDiffHostPage.Layout = .unified
 
     // ...
 
     var body: some View {
-        GeometryReader { geometry in
-            let layout: MobileDiffHostPage.Layout = geometry.size.width > geometry.size.height
-                ? .split
-                : .unified
-            VStack(spacing: 0) {
+        VStack(spacing: 0) {
                 MobileDiffHeaderView(
                     path: controller.currentPath,
                     index: controller.currentIndex,
                     total: controller.total,
                     back: back,
                     openTree: openTree,
                     previous: controller.previousFile,
                     next: controller.nextFile
                 )
                 Divider()
                 ZStack {
                     Color(uiColor: .systemBackground)
                     MobileDiffWebView(
                         controller: controller,
                         service: model.service,
                         files: snapshot.files,
                         layout: layout,
                         theme: colorScheme,
                         title: workspaceTitle,
                         onTooLargePaths: model.markTooLarge,
                         onPartialFailure: model.markPartialDiffFailure
                     )
                     if let errorMessage = controller.errorMessage {
                         diffError(errorMessage)
                     }
                     if let message = model.partialDiffErrorMessage {
                         partialDiffError(message)
                     }
                 }
                 .overlay(alignment: .top) {
                     if !controller.isReady, controller.errorMessage == nil {
                         ProgressView()
                             .progressViewStyle(.linear)
                             .accessibilityLabel(
                                 L10n.string("mobile.diff.loadingDiff", defaultValue: "Loading diff…")
                             )
                     }
                 }
-            }
-            .background(Color(uiColor: .systemBackground))
         }
+        .background(Color(uiColor: .systemBackground))
+        .onGeometryChange(for: CGSize.self) { proxy in
+            proxy.size
+        } action: { size in
+            layout = size.width > size.height ? .split : .unified
+        }
         .toolbar(.hidden, for: .navigationBar)
         .sheet(isPresented: $isTreePresented) {
             treeSheet
         }
     }
🤖 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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffScreen.swift`
around lines 37 - 83, Replace the root GeometryReader in MobileDiffScreen.body
with onGeometryChange, storing the container size in local state and deriving
the split/unified Layout from that size. Preserve the existing
MobileDiffHeaderView, MobileDiffWebView, error overlays, and loading overlay
behavior while removing the geometry container dependency.

Source: Coding guidelines

Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffHeaderView.swift (1)

17-20: 📐 Maintainability & Code Quality | 🟡 Minor | 💤 Low value

Chevron buttons are smaller than Apple's minimum tap-target guidance.

As noted in a previous review, the navigation chevrons use .frame(width: 32, height: 36), which is below Apple's recommended 44×44pt minimum hit target. These are frequently-tapped primary navigation controls in the sticky header, so undersized targets increase mis-taps, especially for users with motor impairments.

  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffHeaderView.swift#L17-L20: Widen the tap target for the back button to at least 44×44.
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffHeaderView.swift#L51-L54: Widen the tap target for the previous button to at least 44×44.
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffHeaderView.swift#L60-L63: Widen the tap target for the next button to at least 44×44.
🤖 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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffHeaderView.swift`
around lines 17 - 20, Increase the frame of the back button in
MobileDiffHeaderView.swift at lines 17-20 to at least 44×44 points. Apply the
same minimum tap-target size to the previous button at lines 51-54 and the next
button at lines 60-63; update all three button frames while preserving their
existing actions and layout behavior.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffWebViewCoordinator.swift (1)

52-55: 🔒 Security & Privacy | 🟡 Minor | 💤 Low value

Avoid forwarding raw exception text across the mobile diff bridge.

The previous review comment noted that forwarding error.message verbatim from the renderer could surface implementation details to the user, and requested that a curated user-safe message be used instead.

The current implementation (message.flatMap { $0.isEmpty ? nil : $0 } ?? Self.renderError) only uses Self.renderError if the raw message is empty or missing. If a raw message is provided, it is still forwarded directly to the UI.

Always display a curated user-safe error message, while retaining the raw detail only for internal diagnostics if needed.

🔒️ Proposed fix to always use a safe user-facing error
         case "error":
-            let message = (body["message"] as? String)?
-                .trimmingCharacters(in: .whitespacesAndNewlines)
-            controller.showError(message.flatMap { $0.isEmpty ? nil : $0 } ?? Self.renderError)
+            if let rawMessage = body["message"] as? String, !rawMessage.isEmpty {
+                // Retain rawMessage for internal diagnostics if necessary, e.g., logging
+            }
+            controller.showError(Self.renderError)
🤖 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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffWebViewCoordinator.swift`
around lines 52 - 55, Update the "error" case in MobileDiffWebViewCoordinator so
controller.showError always receives the curated Self.renderError message,
regardless of any renderer-provided body["message"] value. Do not forward the
raw message to the UI; retain it only for internal diagnostics if existing
diagnostic handling requires it.
🤖 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 `@Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift`:
- Around line 231-236: Update untrackedStats(for:in:) to inspect the target with
FileManager.default.attributesOfItem and continue only when its type is a
regular file, returning the existing empty/error result for FIFOs, devices, and
other non-regular paths. Also adjust the status loop that processes untracked
paths to avoid monopolizing the cooperative pool, using the surrounding async
flow to offload the batch or yield between reads.

---

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeView.swift`:
- Around line 43-53: Update the summary computed property to use
String(localized:defaultValue:) with interpolation instead of String(format:)
and manually passed arguments. Preserve the existing mobile.diff.summaryFormat
localization key and default message while allowing locale-aware number
formatting and pluralization.

In `@Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift`:
- Around line 123-138: Remove the full-repository git status call and
untrackedPaths extraction from WorkspaceGitService.diff; use the untracked
property supplied by each WorkspaceGitDiffPath instead. In
Sources/TerminalController+MobileGit.swift lines 63-81, read each path’s
untracked boolean from the existing JSON payload and pass it to the
WorkspaceGitDiffPath initializer.

In `@Sources/TerminalController`+MobileGit.swift:
- Around line 114-126: Update the workspace lookup in the method containing
v2ResolveTabManager(params:) so it searches all window contexts when the active
tabManager does not contain workspaceID. Reuse the established global lookup
pattern, such as AppDelegate.shared?.locateSurface(surfaceId:) or
mainWindowContexts, and return "Workspace not found" only after the workspace is
absent globally.

---

Duplicate comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffHeaderView.swift`:
- Around line 17-20: Increase the frame of the back button in
MobileDiffHeaderView.swift at lines 17-20 to at least 44×44 points. Apply the
same minimum tap-target size to the previous button at lines 51-54 and the next
button at lines 60-63; update all three button frames while preserving their
existing actions and layout behavior.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffRPCService.swift`:
- Around line 10-17: Mark the async loadStatus() parsing helper with `@concurrent`
so MobileSyncGitStatusResponse.decode(data) and MobileDiffStatusSnapshot
construction execute off the caller’s actor, while preserving the existing
request and return behavior.
- Around line 56-64: Mark the loadDiff(paths:) method with `@concurrent` so its
RPC response decoding via MobileSyncGitDiffResponse.decode remains off the main
actor regardless of caller isolation.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffScreen.swift`:
- Around line 37-83: Replace the root GeometryReader in MobileDiffScreen.body
with onGeometryChange, storing the container size in local state and deriving
the split/unified Layout from that size. Preserve the existing
MobileDiffHeaderView, MobileDiffWebView, error overlays, and loading overlay
behavior while removing the geometry container dependency.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffWebViewCoordinator.swift`:
- Around line 52-55: Update the "error" case in MobileDiffWebViewCoordinator so
controller.showError always receives the curated Self.renderError message,
regardless of any renderer-provided body["message"] value. Do not forward the
raw message to the UI; retain it only for internal diagnostics if existing
diagnostic handling requires it.
🪄 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: 4326cad1-053c-42c1-96b5-e1fc54406b0d

📥 Commits

Reviewing files that changed from the base of the PR and between 1b389a0 and cbdb24d.

⛔ Files ignored due to path filters (1)
  • ios/cmux.xcworkspace/xcshareddata/swiftpm/Package.resolved is excluded by !**/Package.resolved
📒 Files selected for processing (40)
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncGitStatusResponse.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileSyncGitResponseTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffBatchPlanner.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffHeaderView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffMIMEType.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffPath.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffRPCService.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffRPCServiceError.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffRequestPath.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffScreen.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffStatusSnapshot.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeBuilder.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeRowView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffTreeView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffURLSchemeHandler.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffViewerFeature.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffViewerModel.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffWebView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffWebViewController.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileDiffWebViewCoordinator.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/MobileDiffBatchPlannerTests.swift
  • Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/MobileDiffTreeTests.swift
  • Packages/macOS/CmuxGit/README.md
  • Packages/macOS/CmuxGit/Sources/CmuxGit/Model/WorkspaceGitDiffPath.swift
  • Packages/macOS/CmuxGit/Sources/CmuxGit/Model/WorkspaceGitStatus.swift
  • Packages/macOS/CmuxGit/Sources/CmuxGit/Model/WorkspaceGitStatusFile.swift
  • Packages/macOS/CmuxGit/Sources/CmuxGit/Parsing/WorkspaceGitStatusParser.swift
  • Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift
  • Packages/macOS/CmuxGit/Tests/CmuxGitTests/Fixtures/RealGitRepository.swift
  • Packages/macOS/CmuxGit/Tests/CmuxGitTests/WorkspaceGitServiceTests.swift
  • Packages/macOS/CmuxGit/Tests/CmuxGitTests/WorkspaceGitStatusParserTests.swift
  • Resources/markdown-viewer/webviews-app/chunks/diffSurface.mjs
  • Sources/TerminalController+MobileGit.swift
  • Sources/TerminalController.swift
  • ios/cmux/Resources/Localizable.xcstrings
  • webviews/src/App.tsx
  • webviews/src/mobile-diff.ts
  • webviews/test/mobile-diff.test.ts

Comment thread Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift
cmux reload-cloud and others added 2 commits July 13, 2026 19:25
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

Greptile P1: opening an untracked FIFO/socket blocks the status RPC
indefinitely; symlink content in git is the target path, not target
bytes. lstat-gate before reading; non-regular files report 0/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +236 to +246
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
guard let type = attributes[.type] as? FileAttributeType, type == .typeRegular else {
return WorkspaceGitNumstatEntry(
path: path,
oldPath: nil,
additions: 0,
deletions: 0,
binary: false
)
}
let handle = try FileHandle(forReadingFrom: url)

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 Close the check-open race. The file type is checked by path, but FileHandle(forReadingFrom:) opens that path in a separate operation. If a build process replaces an untracked regular file with a FIFO between these calls, the open can block indefinitely waiting for a writer. Git command timeouts and Swift task cancellation do not interrupt this synchronous open, so the status request and Changes screen can still hang. Open the descriptor non-blockingly and validate that descriptor with fstat before reading it.

Rule Used: Flag new blocking or timing-based synchronization ... (source)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift (1)

234-245: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Thread starvation risk remains unaddressed.

While this lstat gate successfully prevents deadlocks on FIFOs and sockets, the status loop (lines 66-78) still executes this synchronous file-I/O up to 2,000 times back-to-back without yielding. As noted in the previous review, this monopolizes a thread on the cooperative pool and can starve the Swift runtime.

To fully resolve the issue, please add await Task.yield() inside the loop processing untracked paths.

⚡ Proposed fix for the `status` loop (around line 75)
        for entry in porcelainEntries {
            guard entry.untracked else {
                retainedEntries.append(entry)
                continue
            }
            guard untrackedCount < Self.maximumUntrackedEntries else {
                truncatedUntracked = true
                continue
            }
            untrackedCount += 1
            
            await Task.yield() // Yield to prevent cooperative pool starvation
            
            retainedEntries.append(entry)
            untrackedStatsByPath[entry.path] = Self.untrackedStats(for: entry.path, in: repoRoot)
        }
🤖 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 `@Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift` around
lines 234 - 245, In the status loop that processes untracked entries, add await
Task.yield() before each synchronous untracked-stat calculation, after the entry
limit and count checks. Keep retained entries, truncation handling, and
untrackedStatsByPath updates unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift`:
- Around line 234-245: In the status loop that processes untracked entries, add
await Task.yield() before each synchronous untracked-stat calculation, after the
entry limit and count checks. Keep retained entries, truncation handling, and
untrackedStatsByPath updates unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c1d2d763-3071-4980-8b69-bf291f0d1a20

📥 Commits

Reviewing files that changed from the base of the PR and between 13bc954 and 7eed6ed.

📒 Files selected for processing (1)
  • Packages/macOS/CmuxGit/Sources/CmuxGit/WorkspaceGitService.swift

cmux reload-cloud and others added 3 commits July 13, 2026 20:19
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Structured os_log at the three diff seams (Mac git RPC handler, iOS
scheme-handler patch stream, web-posted renderer errors) so failures
localize from unified logs. UTType maps .bin to application/macbinary
on CI, so the fallback test case needs an unregistered extension.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Changes feature minted its own client from activeRoute+activeTicket;
attach tickets are short-TTL, so diffs failed auth minutes after attach
and the button was silently disabled on restored sessions. Expose the
composite's shared remoteClient (connected-only) and drop the factory,
env key, and viewer-owned disconnects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cmux reload-cloud and others added 2 commits July 14, 2026 10:58
Re-apply the mobile git RPC ticket allowlist in the relocated
MobileHostService+TicketAuthorization.swift; keep both sides of the
pbxproj wiring (MobileGit + MobileChatArtifacts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
git add -A during the merge swept the Xcode-generated
ios/cmux.xcworkspace/xcshareddata lockfile back in; the resolved-policy
gate rejects that location. Ignore it so resolution artifacts stay out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 14, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

…s them

WKWebView's fetch() surfaces a non-HTTP URLResponse from a
WKURLSchemeHandler as status 0, so the shared diff-stream client threw
"Loading diff… (0)" the moment /patch headers arrived and the viewer
showed "Changes unavailable" on every open while the stream completed
underneath. Route all MobileDiffURLSchemeHandler responses through
MobileDiffHTTPResponseFactory, which emits HTTPURLResponse 200 with
Content-Type (charset only for text/*), Cache-Control: no-store, and
Content-Length when the size is known.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Final fix record for the last open defect on this branch: WKWebView's fetch() reports a plain URLResponse from a WKURLSchemeHandler as status 0, so the diff-stream client threw Loading diff… (0) the instant /patch headers arrived and every open showed "Changes unavailable" while the stream completed underneath (Mac RPCs all succeeded — see the probe logs in the previous comment thread). 3f8b807 routes all scheme responses through MobileDiffHTTPResponseFactory (HTTPURLResponse 200, Content-Type with charset for text/*, Cache-Control: no-store, Content-Length when known), with unit tests asserting the response shape. iOS-sim-SDK package builds pass.

On-sim end-to-end verification was attempted and abandoned: the shared dev Mac's tagged host app livelocked at ~78% CPU (the known sidebar LazyStack bug, fixes pending in #8067 / #8068), starving mobile RPCs, and the simulator's WKWebView WebContent process stalled under host load. Both blockers are unrelated to this change.

This hybrid (Pierre-in-WKWebView) approach is superseded by the fully native SwiftUI viewer being built on feat-ios-native-diff; this PR will be closed when the native PR opens.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Superseded by #8107 — the diff viewer was rebuilt from scratch as a fully native SwiftUI implementation (no WKWebView/Pierre bundle) over a new mobile.workspace.changes.* RPC, per owner direction. The WKWebView status-0 fetch fix investigated here is recorded at #8014 (comment) for future webview work.

This branch was successfully deployed

1 active deployment
Preview – cmux — 3f8b807d Deployed Jul 14, 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