Skip to content

Extract CmuxFileWatch and consolidate file watchers (modular refactor, wave 2) - #5244

Merged
azooz2003-bit merged 4 commits into
mainfrom
feat-cmux-workspace-git
Jun 3, 2026
Merged

azooz2003-bit merged 4 commits into
mainfrom
feat-cmux-workspace-git

Conversation

@azooz2003-bit

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

Copy link
Copy Markdown
Collaborator

Wave-2 infrastructure leaf of the modular refactor. Pulls the FSEvents-backed path watcher out of TabManager into a standalone, git-agnostic CmuxFileWatch package, redesigned onto Swift 6 concurrency. No behavior change intended.

Scope: generic infra, not a git slice

The extracted code has no git knowledge — it just watches a set of paths and yields coalesced events. The git-specific path resolution (which paths matter, dirty-state computation) deliberately stays in TabManager, bound for the Wave-3 CmuxSidebarGit domain package that will depend on this leaf. This mirrors the CmuxProcess infra-leaf precedent (one outside-world capability per package), and CmuxSettings' JSONConfigFileWatcher is a candidate second consumer to migrate onto it later.

Redesign at the seam

  • actor RecursivePathWatcher exposes coalesced changes as AsyncStream<Void> instead of the old init?(descriptor:onChange:) completion-handler callback (forbidden in new package code). Takes paths: [String] directly and exposes watchedPaths for caller dedup; the git-flavored Descriptor is gone.
  • The leading-edge throttle moves off the shared-queue DispatchWorkItem onto a stored, cancel-on-stop Task driven by an injected FileWatchClock (the bounded/cancellable delay carve-out, justified inline). Behavior preserved: first event in a window arms one delay; events during the window coalesce; it reacts during a storm rather than waiting for changes to stop.
  • FileSystemEventStream keeps the hardened FSEventStream internals: one shared serial queue to bound threads, synchronous queue-confined teardown so the stream can't leak before deinit. The FSEvents context is now passed unretained (lifetime guaranteed by invalidate-before-deinit), which let me delete the callback box and both retain/release C trampolines, leaving only the one irreducible event callback.
  • TabManager imports the package, resolves watched paths, stores a per-key consumer Task that drains watcher.events on the MainActor, and tears it down alongside the watcher. Net -262 lines from TabManager.

Tests

The old DEBUG semaphore + Thread.sleep storm helper (and its testGitMetadataWatcherRefreshesDuringSustainedEventStorm XCTest) is replaced by a deterministic Swift Testing test that proves leading-edge coalescing with a gate clock and no real waiting (burstCoalescesAndThrottleRearms). The submodule-paths test stays (it exercises the git path-resolution builder, which stayed in TabManager).

Gates (all green from real logs)

  • swift build + 4 swift test (package)
  • cmux scheme: BUILD SUCCEEDED
  • cmux-unit scheme: BUILD SUCCEEDED (GlobalISel workaround)
  • check-pbxproj.sh passes; package linked into both cmux and cmux-unit

Author: @azooz2003-bit (via Aziz Albahar)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor

    • Switched several subsystems to a path-driven, per-repository filesystem watcher with improved lifecycle/teardown and more deterministic event handling.
  • New Features

    • Added a new file-watch library with recursive watching, a throttling clock seam, and a concurrency-based file watcher; integrated into builds.
    • Replaced multiple legacy dispatch-source watchers with the new watcher across stores, panels, and explorers.
  • Documentation

    • Added package README and updated architecture docs for the new watcher.
  • Tests

    • Added deterministic watcher tests and removed a timing-based flaky test.

Review feedback addressed (CodeRabbit / Greptile)

  • Synchronous, failable stream start (was deferred). The earlier revision started the FSEventStream in a Task inside init, which (a) could miss events in the gap before the task ran (kFSEventStreamEventIdSinceNow) and (b) swallowed creation failures (TabManager cached a silent watcher). Reworked so the stream is created synchronously in init? and returns nil on failure, matching the original watcher. The FSEvents sink forwards into a private raw-event AsyncStream (capturing a Sendable continuation, not the actor), which is what lets creation happen in-init; a single actor-isolated pump drains that raw stream and applies the leading-edge clock throttle.
  • Synchronous teardown (dropped fire-and-forget Task { await stop() }). TabManager now drops the watcher reference, which runs the watcher's deinit synchronously and invalidates the FSEventStream on its shared queue before returning — no window where the old and new streams co-watch. The consumer task captures the events stream (not the watcher), so removal is the last reference.
  • README test pattern. Added a "Testing" section showing the gate-clock instantiation per the package-docs guideline.
  • Re-verified: swift test (4), cmux + cmux-unit BUILD SUCCEEDED; behavior screenshot-verified end-to-end (sidebar git dirty indicator updates on file change) on the tagged build.

Update: consolidated all file-reload watchers onto CmuxFileWatch (per request)

Beyond the leaf extraction, this PR now also adds a single-file/dir FileWatcher
to CmuxFileWatch and migrates every hand-rolled file-reload watcher in the app
onto it, deleting ~350 net lines of bespoke DispatchSource code:

  • FileExplorerStore (FileExplorerDirectoryWatcher)
  • MarkdownPanel (file + nearest-ancestor directory watchers)
  • KeyboardShortcutSettingsFileStore (ShortcutSettingsFileWatcher primary + fallbacks)
  • CmuxConfigStore global-config + per-hook-file watchers (local-config watcher kept — it does search-directory path re-resolution, not reload-on-change)
  • CmuxSettings JSONConfigStore (JSONConfigFileWatcher replaced + deleted; CmuxSettings now depends on CmuxFileWatch)

Not migrated, by design (not file-reload watchers): TerminalController's socket-path liveness monitor (auth-path state machine, slated for CmuxControlSocket) and process-exit kqueue sources (FeedCoordinator, CMUXWorkstream).

FileWatcher adds nearest-existing-ancestor recovery (subsumes MarkdownPanel's ancestor walk) and an optional leading-edge throttle on the shared FileWatchClock.

Tests + verification

  • CmuxFileWatch: 11 tests (write→event, create-after-start, ancestor recovery, directory target, atomic inode replace, throttled yield, stop) — all green.
  • cmux + cmux-unit BUILD SUCCEEDED with the cross-package CmuxSettings → CmuxFileWatch dependency.
  • Live computer-use verification on the tagged build: sidebar git dirty indicator updates on file change (RecursivePathWatcher); MarkdownPanel auto-reloads on external edit (FileWatcher).

@vercel

vercel Bot commented Jun 2, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 3, 2026 3:53am
cmux-staging Building Building Preview, Comment Jun 3, 2026 3:53am

@coderabbitai

coderabbitai Bot commented Jun 2, 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 CmuxFileWatch (FSEvents wrapper, FileWatchClock, RecursivePathWatcher actor and tests), integrates it into TabManager with per-probe RecursivePathWatcher Tasks to schedule git-metadata refreshes, wires the package into Xcode, and removes a timing-based test.

Changes

File System Watcher Package and Integration

Layer / File(s) Summary
CmuxFileWatch package manifest and README
Packages/CmuxFileWatch/Package.swift, Packages/CmuxFileWatch/README.md
Defines the CmuxFileWatch Swift package and documents RecursivePathWatcher, its coalesced AsyncStream<Void> events, throttling via FileWatchClock, and usage/teardown.
FileSystemEventStream FSEvents wrapper
Packages/CmuxFileWatch/Sources/CmuxFileWatch/FileSystemEventStream.swift
Adds FileSystemEventStream: a wrapper around macOS FSEventStream with a shared serial DispatchQueue, C callback trampoline, idempotent stop(), and safe teardown in deinit.
FileWatchClock protocol and SystemFileWatchClock
Packages/CmuxFileWatch/Sources/CmuxFileWatch/FileWatchClock.swift
Introduces FileWatchClock protocol with cancellable async sleep(for:) and SystemFileWatchClock delegating to Task.sleep(for:).
RecursivePathWatcher actor with coalescing and throttle
Packages/CmuxFileWatch/Sources/CmuxFileWatch/RecursivePathWatcher.swift
Implements RecursivePathWatcher actor exposing events: AsyncStream<Void> with leading-edge throttling using injected FileWatchClock; includes failable init, idempotent stop(), test seam, and rearm behavior.
RecursivePathWatcher test suite with GateClock
Packages/CmuxFileWatch/Tests/CmuxFileWatchTests/RecursivePathWatcherTests.swift
Adds deterministic tests (GateClock) verifying empty-path init failure, directory watching, burst coalescing and rearming, and correct behavior after stop().
FileWatcher actor (single-path DispatchSource watcher)
Packages/CmuxFileWatch/Sources/CmuxFileWatch/FileWatcher.swift
Adds FileWatcher actor that monitors a single path (file or directory) via DispatchSource kqueue-backed sources, supports nearest-ancestor recovery and optional throttle, and provides events: AsyncStream<Void> and stop().
FileWatcher tests
Packages/CmuxFileWatch/Tests/CmuxFileWatchTests/FileWatcherTests.swift
Adds tests verifying write/create/recovery/atomic-replace behaviors, throttled yields, and stream completion after stop().
TabManager git-metadata watcher refactor: event streams and task scheduling
Sources/TabManager.swift
Imports CmuxFileWatch, computes watchedPaths, creates per-probe RecursivePathWatcher and a stored Task that iterates watcher.events to schedule scheduleWorkspaceGitMetadataRefreshIfPossible, and updates stop/stop-all lifecycle handling; removes DEBUG storm-delay measurement helper.
Xcode project build system integration
cmux.xcodeproj/project.pbxproj
Wires the new package into the Xcode project: adds PBXBuildFile entries, inserts into Frameworks build phases, registers a local package reference, and updates packageProductDependencies.
Test migration: remove timing-based test
cmuxTests/WorkspacePullRequestSidebarTests.swift
Removes timing-based sustained-event-storm test and points to deterministic package tests for coalescing verification.
CmuxSettings integration and JSONConfigStore switch
Packages/CmuxSettings/*, Packages/CmuxSettings/Sources/CmuxSettings/Stores/JSONConfigStore.swift
Adds CmuxFileWatch as a package dependency, updates README/doc, removes JSONConfigFileWatcher and replaces it with CmuxFileWatch.FileWatcher usage in JSONConfigStore.
CmuxConfigStore: replace DispatchSource watchers with FileWatcher
Sources/CmuxConfig.swift
Refactors CmuxConfigStore to use FileWatcher for hook files and global config, introducing per-watcher Tasks and updated start/stop and deinit logic.
FileExplorerStore watcher replacement
Sources/FileExplorerStore.swift
Replaces prior directory watcher with FileWatcher throttled to 300ms and a main-actor consumer Task that calls reload() and refreshGitStatus() on events; adds teardown cancellation.
KeyboardShortcutSettingsFileStore watcher refactor
Sources/KeyboardShortcutSettingsFileStore.swift
Replaces custom DispatchSource-based watcher with FileWatcher instances and per-watcher Tasks; updates deinit to cancel tasks and clear watchers.
MarkdownPanel file watcher conversion
Sources/Panels/MarkdownPanel.swift
Replaces DispatchSource-based file watcher with FileWatcher and async Task consumer; updates start/stop/deinit logic and stored watcher state.

Sequence Diagram

sequenceDiagram
  participant FSEvents
  participant FileSystemEventStream
  participant RecursivePathWatcher
  participant TabManager
  participant FileWatchClock

  FSEvents->>FileSystemEventStream: deliver low-level events
  FileSystemEventStream->>RecursivePathWatcher: invoke onEvent callback
  RecursivePathWatcher->>RecursivePathWatcher: handleFileSystemEvent() (leading-edge)
  alt no pending throttle
    RecursivePathWatcher->>FileWatchClock: sleep(for: throttleWindow)
  end
  FileWatchClock-->>RecursivePathWatcher: sleep completes
  RecursivePathWatcher->>TabManager: yield one event via events AsyncStream
  TabManager->>TabManager: scheduleWorkspaceGitMetadataRefreshIfPossible()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • manaflow-ai/cmux#2797: Both PRs modify the sidebar git metadata watcher logic—replacing/adjusting its update mechanism (polling/debouncing/watch lifecycle).
  • manaflow-ai/cmux#5131: Overlaps with prior git-metadata watcher changes and removal of the timing/debug helper.

Poem

🐰 I hop through quiet file-change trails,
Folding many knocks into one small bell,
A clock that waits, then wakes a task,
Refreshes march where soft events fell,
Tiny feet bring order — swift and well.


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error PR introduces Task.sleep in production code (FileWatchClock.swift) for filesystem throttling, violating the rule against Task.sleep outside allowed cases (tests/deterministic scaffolding only). Replace Task.sleep throttle with a real timer abstraction, scheduler, or defer to FSEventStream's existing coalescing; use FileWatchClock seam only for test determinism, not production delays.
Docstring Coverage ⚠️ Warning Docstring coverage is 39.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (16 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 Proper Swift 6 actor isolation: RecursivePathWatcher and FileWatcher are public actors; FileSystemEventStream is @unchecked Sendable with clear safety docs; all consumer tasks marked @MainActor.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift code; this check explicitly excludes Swift (covered by separate check) and applies only to TypeScript, JavaScript, shell, and non-Swift build scripts.
Cmux Algorithmic Complexity ✅ Passed New filter at line 1720 is only called during non-hot workspace removal. Git index parsing for watcher paths is in sidebar setup, not event loops.
Cmux Swift Concurrency ✅ Passed Tasks in watchers are tied to lifecycle via AsyncStream continuation finish. Consumer Tasks properly stored/cancelled. DispatchQueues limited to FSEventStream C API callbacks and existing paths.
Cmux Swift @Concurrent ✅ Passed FileWatchClock.sleep is implicitly nonisolated async on Sendable protocol. All watcher consumers use Task { @MainActor ... } to isolate I/O work from main actor correctly.
Cmux Swift File And Package Boundaries ✅ Passed All new files <400 lines, clear single responsibility; oversized files reduced >200 lines; file-watching properly extracted to CmuxFileWatch package; no mixed responsibilities.
Cmux Swift Logging ✅ Passed PR adds new CmuxFileWatch package and refactors file watchers throughout app; comprehensive search found zero violations of print/debugPrint/dump/NSLog in added/modified app/runtime code.
Cmux User-Facing Error Privacy ✅ Passed PR introduces no new user-facing error messages. New CmuxFileWatch package is infrastructure-only with no user-visible output; modified files only refactor file watching with no behavioral changes.
Cmux Full Internationalization ✅ Passed PR is a pure infrastructure/refactoring change with no new user-facing UI text, modified string catalogs, or web UI changes requiring internationalization.
Cmux Swiftui State Layout ✅ Passed PR is a file-watching infrastructure refactoring with no new SwiftUI state patterns; existing @Published properties only touched incidentally in their watcher backends.
Cmux Architecture Rethink ✅ Passed CmuxFileWatch uses proper Swift 6 concurrency patterns. No timing hacks, locks, semaphores, or split lifecycle ownership. Synchronous teardown and centralized state management.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not add or materially change user-visible NSWindow/NSPanel/NSWindowController/SwiftUI Window code; it refactors file-watching infrastructure only, with no auxiliary window identifier changes.
Title check ✅ Passed The title 'Extract CmuxFileWatch and consolidate file watchers (modular refactor, wave 2)' accurately summarizes the main change: extracting a new file-watching package and consolidating multiple file watchers across the codebase.
Description check ✅ Passed The description covers all required sections: Summary (what changed and why), Testing (how tested and verified), and a comprehensive checklist. It explains the architecture, scope, design decisions, and testing approach with appropriate detail.
✨ 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-cmux-workspace-git

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

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Jun 2, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extracts a new CmuxFileWatch SwiftPM package containing two actors — RecursivePathWatcher (FSEvents-backed, multi-path) and FileWatcher (kqueue-backed, single-path with ancestor-directory recovery) — and migrates all existing bespoke file-watching code in TabManager, FileExplorerStore, CmuxConfig, KeyboardShortcutSettingsFileStore, MarkdownPanel, and JSONConfigStore onto the new abstractions.

  • New package: FileSystemEventStream (shared-queue, synchronous teardown) + RecursivePathWatcher (leading-edge coalescing throttle via injectable FileWatchClock) + FileWatcher (per-descriptor kqueue source with inode-reattachment recovery and optional throttle), each with deterministic Swift Testing tests using a gate clock instead of timing-based semaphores.
  • TabManager: Drops the embedded WorkspaceGitMetadataWatcher class (-262 lines); lifecycle is now deinit-driven — dropping the dictionary reference is the last strong reference and synchronously invalidates the FSEventStream before the call returns.
  • Consumers: JSONConfigFileWatcher (which had a documented deferred-start race window) is deleted; all sites now use FileWatcher, which attaches its DispatchSources synchronously in init.

Confidence Score: 5/5

Safe to merge — the FSEvent stream lifecycle is correctly bounded with synchronous teardown, and the leading-edge coalescing behavior is deterministically verified with an injected gate clock.

The extraction correctly preserves the shared-queue thread-bounding strategy in FileSystemEventStream, fixes the old JSONConfigFileWatcher deferred-start race window, and resolves the fire-and-forget teardown concern from the previous review round. No behavioral regressions were found in the lifecycle paths.

FileWatcher.swift — per-instance DispatchQueue (vs. FileSystemEventStream's shared static queue) is worth tracking as more consumers migrate onto it.

Important Files Changed

Filename Overview
Packages/CmuxFileWatch/Sources/CmuxFileWatch/FileSystemEventStream.swift New FSEvents wrapper with shared static queue, synchronous teardown, and unretained context pointer correctly bounded by invalidate-before-deinit.
Packages/CmuxFileWatch/Sources/CmuxFileWatch/RecursivePathWatcher.swift New actor wrapping FileSystemEventStream with leading-edge throttle; synchronous failable init, weak-self pump task, and deinit-based teardown correctly bounded by the raw stream lifetime.
Packages/CmuxFileWatch/Sources/CmuxFileWatch/FileWatcher.swift New actor wrapping kqueue DispatchSource per path with recovery via nearest-ancestor directory; creates a per-instance DispatchQueue, unlike FileSystemEventStream's shared static queue.
Sources/TabManager.swift Removes ~262 lines of embedded WorkspaceGitMetadataWatcher; now delegates to RecursivePathWatcher with correct stop-then-start lifecycle and per-key consumer task cancellation.
Sources/FileExplorerStore.swift Replaces FileExplorerDirectoryWatcher with FileWatcher; stopDirectoryWatcher() and deinit both cancel the consumer task before dropping the watcher.
Sources/CmuxConfig.swift Hook and global config watchers migrated to FileWatcher with proper per-watcher Task lifecycle; deinit cancels consumer tasks and relies on FileWatcher.deinit to tear down sources.
Packages/CmuxSettings/Sources/CmuxSettings/Stores/JSONConfigStore.swift Drops JSONConfigFileWatcher (which had a known race window in its deferred-start init) for FileWatcher, which attaches sources synchronously and handles recovery via ancestor-directory watching.

Sequence Diagram

sequenceDiagram
    participant TM as TabManager (MainActor)
    participant RPW as RecursivePathWatcher (actor)
    participant FSES as FileSystemEventStream
    participant FQ as FSEvents shared queue
    participant CT as Consumer Task (@MainActor)

    TM->>RPW: init?(paths:) — synchronous
    RPW->>FSES: init?(paths:latency:onEvent:) — synchronous
    FSES->>FQ: FSEventStreamCreate + Start
    RPW->>RPW: "Task { [weak self] pump rawEvents }"
    TM->>CT: "Task { @MainActor for await _ in watcher.events }"

    FQ-->>FSES: FSEvent callback (onEvent)
    FSES->>RPW: rawContinuation.yield(())
    RPW->>RPW: handleRawEvent() → arm throttleTask
    Note over RPW: leading-edge: first event arms one delay
    RPW->>CT: continuation.yield(()) after clock.sleep
    CT->>TM: scheduleWorkspaceGitMetadataRefreshIfPossible

    TM->>CT: task.cancel()
    TM->>RPW: removeValue (last strong ref) → deinit
    RPW->>FSES: stop() — synchronous queue.sync
    FSES->>FQ: FSEventStreamInvalidate (synchronous)
    RPW->>RPW: rawContinuation.finish() → pump exits
    RPW->>RPW: continuation.finish() → events stream done
Loading

Reviews (5): Last reviewed commit: "Add FileWatcher tests for directory targ..." | Re-trigger Greptile

Comment thread Sources/TabManager.swift Outdated
Comment on lines +1712 to +1716
if let watcher = workspaceGitMetadataWatchersByKey.removeValue(forKey: key) {
// stop() finishes the events stream, which ends the consumer loop;
// the watcher's own deinit also tears down the FSEventStream.
Task { await watcher.stop() }
}

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 Unstructured fire-and-forget teardown with meaningful FSEventStream lifecycle

watcher.stop() used to run synchronously via DispatchQueue.sync inside the old class, guaranteeing the FSEventStream was fully invalidated before stopWorkspaceGitMetadataWatcher returned. The new fire-and-forget Task { await watcher.stop() } defers that invalidation until the actor hop fires: in the common stop-then-start path (line 1690 in the same file), the new watcher's FSEventStream is created and running before the old one's FSEventStreamInvalidate executes, so both streams briefly co-monitor the same paths. Under the cmux concurrency policy, fire-and-forget tasks with meaningful lifecycle should be avoided; consider making stopWorkspaceGitMetadataWatcher and its callers async so the await can be expressed directly, or keeping a cancellation token that forces the actor's isStopped flag synchronously while deferring only the C-level stream teardown.

self.events = stream
self.continuation = continuation
self.eventStream = nil
Task { await self.startEventStream() }

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 Unstructured Task in actor init

Task { await self.startEventStream() } is a fire-and-forget created inside init, making the FSEventStream attachment invisible to callers and the Swift structured-concurrency tree. The isStopped guard in startEventStream correctly handles the early-stop() race, but the approach also means the actor holds a strong self reference in an escaping task during the window between init returning and startEventStream running. The pattern is called out in the doc comment; consider an async static factory (WorkspaceGitMetadataWatcher.start(descriptor:)) as the idiomatic actor alternative, which would also remove the need to document the race window.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
Packages/CmuxWorkspaceGit/README.md (1)

12-31: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add an explicit test-pattern section for injected dependencies.

Please add a small “Testing” section that shows concrete instantiation with a test clock (and note/update corresponding DocC test pattern), not just a prose mention. This package has a non-trivial public surface and the README should include the test-friendly setup pattern directly.

As per coding guidelines: “Packages/*/README.md: Document the test pattern for any non-trivial public surface in the package README and DocC catalog, showing how to instantiate the type with test-friendly dependencies.”

🤖 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/CmuxWorkspaceGit/README.md` around lines 12 - 31, Add a "Testing"
section to the README that shows the concrete instantiation pattern for
injecting a test clock into the watcher: demonstrate creating a controllable
test implementation of WorkspaceGitWatchClock (used in place of
SystemWorkspaceGitWatchClock), passing it into WorkspaceGitMetadataWatcher,
starting the Task that iterates watcher.events, releasing the test clock to
simulate event coalescing, and tearing down via task.cancel() and await
watcher.stop(); also update the DocC test pattern to match this concrete example
and mention CmuxSidebarGit only as the consumer of resolved paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/WorkspacePullRequestSidebarTests.swift`:
- Around line 1635-1640: Add a single app-level smoke test in
WorkspacePullRequestSidebarTests.swift that verifies the TabManager bridge still
consumes repeated WorkspaceGitMetadataWatcher events and triggers the sidebar
refresh path: instantiate or inject a fake WorkspaceGitMetadataWatcher that can
emit a burst of events, start the TabManager consumer task (reference TabManager
and its consumer/bridge entrypoint), and assert that the sidebar refresh
scheduler or refresh side-effect (e.g., Sidebar.refresh invocation,
scheduleSidebarRefresh call, or the observable state the UI reads) was invoked
at least once; keep the test narrow (no timing-based waits) by using
deterministically emitted events and awaiting the expected refresh signal from
the TabManager.

In
`@Packages/CmuxWorkspaceGit/Sources/CmuxWorkspaceGit/WorkspaceGitMetadataWatcher.swift`:
- Around line 92-104: The init? currently defers starting the
FileSystemEventStream via Task { await self.startEventStream() }, which can miss
early filesystem events and hides start failures; instead call
startEventStream() synchronously inside the initializer, set self.eventStream
and self.continuation only after successful creation, and return nil if
startEventStream() (or FileSystemEventStream creation) fails; update the
initializer that takes descriptor: Descriptor and clock: any
WorkspaceGitWatchClock (SystemWorkspaceGitWatchClock default) to perform
synchronous startup and failure handling so events, continuation and eventStream
are valid when init returns.

In
`@Packages/CmuxWorkspaceGit/Tests/CmuxWorkspaceGitTests/WorkspaceGitMetadataWatcherTests.swift`:
- Around line 12-18: GateClock.sleep(for:) currently uses a non-throwing
continuation so it never resumes with cancellation, preventing the watcher’s
canceled throttle task from observing cancellation; change GateClock.sleep(for:)
to use withCheckedThrowingContinuation (CheckedContinuation<Void,
Error>/throwing continuation) and store that throwing continuation in sleepers,
and ensure the continuation is resumed(throwing: CancellationError()) when the
awaiting Task is cancelled (or when stop() triggers cancellation) so
WorkspaceGitWatchClock.sleep(for:) mirrors Task.sleep behavior; then add a test
that calls stop() while clock.waitForSleeper() has a pending sleeper and assert
the throttle flush path runs (i.e., stop-while-throttled is exercised). Include
references to GateClock.sleep(for:), WorkspaceGitWatchClock.sleep(for:), and the
test that calls stop().

---

Outside diff comments:
In `@Packages/CmuxWorkspaceGit/README.md`:
- Around line 12-31: Add a "Testing" section to the README that shows the
concrete instantiation pattern for injecting a test clock into the watcher:
demonstrate creating a controllable test implementation of
WorkspaceGitWatchClock (used in place of SystemWorkspaceGitWatchClock), passing
it into WorkspaceGitMetadataWatcher, starting the Task that iterates
watcher.events, releasing the test clock to simulate event coalescing, and
tearing down via task.cancel() and await watcher.stop(); also update the DocC
test pattern to match this concrete example and mention CmuxSidebarGit only as
the consumer of resolved paths.
🪄 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: e866ef07-dd74-438a-8cd5-bf6956162c6d

📥 Commits

Reviewing files that changed from the base of the PR and between 67014e3 and d03f5f7.

📒 Files selected for processing (9)
  • Packages/CmuxWorkspaceGit/Package.swift
  • Packages/CmuxWorkspaceGit/README.md
  • Packages/CmuxWorkspaceGit/Sources/CmuxWorkspaceGit/FileSystemEventStream.swift
  • Packages/CmuxWorkspaceGit/Sources/CmuxWorkspaceGit/WorkspaceGitMetadataWatcher.swift
  • Packages/CmuxWorkspaceGit/Sources/CmuxWorkspaceGit/WorkspaceGitWatchClock.swift
  • Packages/CmuxWorkspaceGit/Tests/CmuxWorkspaceGitTests/WorkspaceGitMetadataWatcherTests.swift
  • Sources/TabManager.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/WorkspacePullRequestSidebarTests.swift

Comment thread Packages/CmuxFileWatch/Sources/CmuxFileWatch/RecursivePathWatcher.swift Outdated
Comment on lines +12 to +18
func sleep(for duration: Duration) async throws {
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
sleepers.append(continuation)
let waiters = arrivalWaiters
arrivalWaiters.removeAll()
for waiter in waiters { waiter.resume() }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

clock_file=$(fd 'WorkspaceGitWatchClock.swift$' Packages | head -n1)
watcher_file=$(fd 'WorkspaceGitMetadataWatcher.swift$' Packages | head -n1)
test_file=$(fd 'WorkspaceGitMetadataWatcherTests.swift$' Packages | head -n1)

echo "== WorkspaceGitWatchClock / production clock =="
sed -n '1,200p' "$clock_file"

echo
echo "== Watcher stop/cancel path =="
rg -n -C3 'stop\(|cancel\(|sleep\(for:' "$watcher_file"

echo
echo "== GateClock and current tests =="
sed -n '1,140p' "$test_file"
rg -n -C2 'waitForSleeper|releaseOne|stop\(' "$test_file"

Repository: manaflow-ai/cmux

Length of output: 9375


🏁 Script executed:

#!/bin/bash
set -euo pipefail
watcher_file=$(fd 'WorkspaceGitMetadataWatcher.swift$' Packages | head -n1)
rg -n "throttleTask|flushThrottle|testThrottleClock|WorkspaceGitWatchClock" "$watcher_file" -S
echo "---- flushThrottle context ----"
rg -n -C3 "func flushThrottle|flushThrottle\(" "$watcher_file" -S
echo "---- throttleTask context ----"
rg -n -C5 "throttleTask = Task|try\?\s+await\s+clock\.sleep" "$watcher_file" -S

Repository: manaflow-ai/cmux

Length of output: 1846


Make GateClock.sleep(for:) cancellation-aware and cover stop-while-throttled.

WorkspaceGitMetadataWatcher cancels the throttle Task and relies on the clock contract (WorkspaceGitWatchClock.sleep(for:) async throws; production uses Task.sleep, which throws on cancellation) so the task can reach flushThrottle() (guarded by isStopped) instead of being left parked. GateClock.sleep(for:) never resumes or throws on cancellation (non-throwing continuation), so cancellation/stop behavior isn’t exercised and a flushThrottle()-after-stop() regression could slip through.

Update GateClock.sleep(for:) to resume on cancellation by throwing CancellationError, and add a test that calls stop() while clock.waitForSleeper() has a pending sleeper.

🤖 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/CmuxWorkspaceGit/Tests/CmuxWorkspaceGitTests/WorkspaceGitMetadataWatcherTests.swift`
around lines 12 - 18, GateClock.sleep(for:) currently uses a non-throwing
continuation so it never resumes with cancellation, preventing the watcher’s
canceled throttle task from observing cancellation; change GateClock.sleep(for:)
to use withCheckedThrowingContinuation (CheckedContinuation<Void,
Error>/throwing continuation) and store that throwing continuation in sleepers,
and ensure the continuation is resumed(throwing: CancellationError()) when the
awaiting Task is cancelled (or when stop() triggers cancellation) so
WorkspaceGitWatchClock.sleep(for:) mirrors Task.sleep behavior; then add a test
that calls stop() while clock.waitForSleeper() has a pending sleeper and assert
the throttle flush path runs (i.e., stop-while-throttled is exercised). Include
references to GateClock.sleep(for:), WorkspaceGitWatchClock.sleep(for:), and the
test that calls stop().

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 9 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Sources/TabManager.swift Outdated
@azooz2003-bit
azooz2003-bit force-pushed the feat-cmux-workspace-git branch from d03f5f7 to 986a861 Compare June 2, 2026 21:42
@azooz2003-bit azooz2003-bit changed the title Extract CmuxWorkspaceGit package (modular refactor, wave 2) Extract CmuxFileWatch package (modular refactor, wave 2) Jun 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
Packages/CmuxFileWatch/Tests/CmuxFileWatchTests/RecursivePathWatcherTests.swift (1)

12-19: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Make GateClock.sleep(for:) cancellation-aware and cover stop-while-throttled.

RecursivePathWatcher.stop() cancels the throttle task, but this fake clock never throws or resumes on cancellation, so the tests do not model SystemFileWatchClock/Task.sleep behavior. That leaves the “stop while a throttle delay is parked” path unexercised and can hide regressions in the post-cancel flush logic.

#!/bin/bash
set -euo pipefail

test_file='Packages/CmuxFileWatch/Tests/CmuxFileWatchTests/RecursivePathWatcherTests.swift'
watcher_file='Packages/CmuxFileWatch/Sources/CmuxFileWatch/RecursivePathWatcher.swift'
clock_file='Packages/CmuxFileWatch/Sources/CmuxFileWatch/FileWatchClock.swift'

echo "== GateClock implementation =="
sed -n '1,40p' "$test_file"

echo
echo "== Watcher cancellation path =="
sed -n '110,145p' "$watcher_file"

echo
echo "== Production clock contract =="
sed -n '18,36p' "$clock_file"

Also applies to: 94-105

🤖 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/CmuxFileWatch/Tests/CmuxFileWatchTests/RecursivePathWatcherTests.swift`
around lines 12 - 19, The fake GateClock's sleep(for:) must model Task.sleep
cancellation by making GateClock.sleep(for:) async throws and resume or throw on
Task cancellation; update the test implementation of sleep(for:) (the function
named sleep(for duration: Duration) async throws in the GateClock test helper)
to check for Task.isCancelled and resume the waiting CheckedContinuation by
throwing CancellationError (or directly throw when awaiting), so cancellations
resume/throw like SystemFileWatchClock/Task.sleep; then add or adjust the
RecursivePathWatcherTests to include a stop-while-throttled scenario that
triggers RecursivePathWatcher.stop() while a throttle delay is parked to ensure
the post-cancel flush logic is exercised.
Packages/CmuxFileWatch/Sources/CmuxFileWatch/RecursivePathWatcher.swift (1)

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

Start the FSEventStream before returning the watcher.

Deferring startEventStream() through Task { ... } reintroduces a real blind spot: any write that lands after init returns but before that task constructs the stream is outside the observed range because FileSystemEventStream subscribes with kFSEventStreamEventIdSinceNow. It also turns create/start failure into a silent no-op by still returning a watcher whose events stream never yields. This changes behavior for callers that assume a non-nil watcher is live.

#!/bin/bash
set -euo pipefail

watcher_file='Packages/CmuxFileWatch/Sources/CmuxFileWatch/RecursivePathWatcher.swift'
stream_file='Packages/CmuxFileWatch/Sources/CmuxFileWatch/FileSystemEventStream.swift'

echo "== Deferred startup in RecursivePathWatcher =="
sed -n '55,95p' "$watcher_file"

echo
echo "== FSEvents subscription point in FileSystemEventStream =="
sed -n '67,90p' "$stream_file"
🤖 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/CmuxFileWatch/Sources/CmuxFileWatch/RecursivePathWatcher.swift`
around lines 63 - 80, The initializer currently defers starting the
FSEventStream via Task { await startEventStream() } which creates a blind spot
and hides start failures; change init? to start the stream synchronously (call
startEventStream on the current executor before returning), make
startEventStream surface errors (throw or return Bool) and if stream
creation/start fails return nil from init?, and ensure self.eventStream and the
AsyncStream continuation are only exposed after successful start so a non-nil
watcher always has an active FileSystemEventStream (refer to init?,
startEventStream(), eventStream, events, continuation, and
FileSystemEventStream).
🤖 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 `@cmux.xcodeproj/project.pbxproj`:
- Line 1933: The project file is missing wiring for the new CmuxFileWatch
package in the unit-test target: add an XCLocalSwiftPackageReference and
corresponding XCSwiftPackageProductDependency for CmuxFileWatch, insert a
PBXBuildFile entry for CmuxFileWatch into the Frameworks build phase (the
F1000006 /* Frameworks */ list) of the cmuxTests target (F1000004 /* cmuxTests
*/), and ensure both the main cmux and the cmuxTests targets mirror the same
package/product entries (so cmux, cmux-cli and the unit-test target all include
CmuxFileWatch). Ensure identifiers match the existing CmuxFileWatch entries used
elsewhere in the pbxproj so the package is consistently referenced across
targets.

In `@Packages/CmuxFileWatch/README.md`:
- Around line 17-25: Add a short testing example to the README alongside the
runtime snippet that demonstrates injecting a fake FileWatchClock into
RecursivePathWatcher for deterministic tests: describe constructing a test
FakeFileWatchClock (or using an existing test helper), initializing
RecursivePathWatcher with that fake clock and test paths, producing/scheduling
synthetic events via the fake clock, awaiting the watcher's events in a Task to
assert reload/handling, and then tearing down by cancelling the Task and calling
await watcher.stop(); reference the RecursivePathWatcher type and the
FileWatchClock (or FakeFileWatchClock) so readers know which symbols to use.

In `@Sources/TabManager.swift`:
- Around line 1691-1703: The Task consumers created and stored in
workspaceGitMetadataWatcherRefreshTasksByKey (consuming
RecursivePathWatcher.events and calling
scheduleWorkspaceGitMetadataRefreshIfPossible) are not cancelled on teardown;
update TabManager to cancel those Tasks and stop/close the associated
RecursivePathWatcher entries (workspaceGitMetadataWatchersByKey) during deinit
(or the existing stop helper called from deinit) so the unstructured for-await
loops are terminated; ensure the same cleanup is applied for the other
watcher/task pairs referenced around the 1711-1716 and 1727-1734 ranges and that
you remove entries from both dictionaries after cancelling/stopping.

---

Duplicate comments:
In `@Packages/CmuxFileWatch/Sources/CmuxFileWatch/RecursivePathWatcher.swift`:
- Around line 63-80: The initializer currently defers starting the FSEventStream
via Task { await startEventStream() } which creates a blind spot and hides start
failures; change init? to start the stream synchronously (call startEventStream
on the current executor before returning), make startEventStream surface errors
(throw or return Bool) and if stream creation/start fails return nil from init?,
and ensure self.eventStream and the AsyncStream continuation are only exposed
after successful start so a non-nil watcher always has an active
FileSystemEventStream (refer to init?, startEventStream(), eventStream, events,
continuation, and FileSystemEventStream).

In
`@Packages/CmuxFileWatch/Tests/CmuxFileWatchTests/RecursivePathWatcherTests.swift`:
- Around line 12-19: The fake GateClock's sleep(for:) must model Task.sleep
cancellation by making GateClock.sleep(for:) async throws and resume or throw on
Task cancellation; update the test implementation of sleep(for:) (the function
named sleep(for duration: Duration) async throws in the GateClock test helper)
to check for Task.isCancelled and resume the waiting CheckedContinuation by
throwing CancellationError (or directly throw when awaiting), so cancellations
resume/throw like SystemFileWatchClock/Task.sleep; then add or adjust the
RecursivePathWatcherTests to include a stop-while-throttled scenario that
triggers RecursivePathWatcher.stop() while a throttle delay is parked to ensure
the post-cancel flush logic is exercised.
🪄 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: 90cc516c-a59a-4a5f-9d55-5fe8c458a3e1

📥 Commits

Reviewing files that changed from the base of the PR and between d03f5f7 and 986a861.

📒 Files selected for processing (9)
  • Packages/CmuxFileWatch/Package.swift
  • Packages/CmuxFileWatch/README.md
  • Packages/CmuxFileWatch/Sources/CmuxFileWatch/FileSystemEventStream.swift
  • Packages/CmuxFileWatch/Sources/CmuxFileWatch/FileWatchClock.swift
  • Packages/CmuxFileWatch/Sources/CmuxFileWatch/RecursivePathWatcher.swift
  • Packages/CmuxFileWatch/Tests/CmuxFileWatchTests/RecursivePathWatcherTests.swift
  • Sources/TabManager.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/WorkspacePullRequestSidebarTests.swift

Comment thread cmux.xcodeproj/project.pbxproj
Comment thread Packages/CmuxFileWatch/README.md
Comment thread Sources/TabManager.swift

@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.

Caution

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

⚠️ Outside diff range comments (1)
Sources/TabManager.swift (1)

1654-1665: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Own the descriptor-resolution task per probe.

Line 1654 starts a fire-and-forget Task for workspaceGitMetadataWatchedPaths(for:), but nothing cancels it when the directory changes or the probe is removed. The stale-request guard only drops the result after the git/index walk has already run. Mirror workspaceGitMetadataWatcherRefreshTasksByKey here so each key has a cancellable descriptor task, and cancel it from stopWorkspaceGitMetadataWatcher(for:) / stopAllWorkspaceGitMetadataWatchers().

Based on learnings: Applies to **/*.swift : Flag fire-and-forget Task { ... } work with meaningful lifecycle that is not stored, cancelled, or tied to a caller-owned operation in cmux-owned Swift code.

🤖 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/TabManager.swift` around lines 1654 - 1665, The code fires an
uncancelled Task around workspaceGitMetadataWatchedPaths(for:) and then applies
the result with applyWorkspaceGitMetadataWatcherDescriptor(...); instead, create
and store a cancellable Task per key in
workspaceGitMetadataWatcherRefreshTasksByKey (similar to existing watcher maps),
use that Task to perform the detached work and await its value, and ensure
stopWorkspaceGitMetadataWatcher(for:) and stopAllWorkspaceGitMetadataWatchers()
cancel and remove the stored Task so directory changes or probe removal cancel
in-flight descriptor-resolution work. Ensure weak-self/stale-request checks
remain but move the Task creation into the per-key storage so each probe owns
its lifecycle.
♻️ Duplicate comments (2)
Sources/TabManager.swift (1)

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

Call this from deinit.

This helper finally centralizes watcher/task shutdown, but Line 1434 still never invokes it. That leaves the stored for await consumers and their RecursivePathWatcher streams alive after TabManager teardown.

Suggested cleanup
 deinit {
+    stopAllWorkspaceGitMetadataWatchers()
     workspaceCycleCooldownTask?.cancel()
     agentPIDSweepTimer?.cancel()
     workspacePullRequestPollTimer?.cancel()
     workspaceGitMetadataFallbackTimer?.cancel()
     workspacePullRequestRefreshTask?.cancel()
 }
🤖 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/TabManager.swift` around lines 1726 - 1734, The TabManager
deinitialization never calls stopAllWorkspaceGitMetadataWatchers, leaving
for-await consumers and RecursivePathWatcher streams running; update
TabManager.deinit to call stopAllWorkspaceGitMetadataWatchers() (ensuring any
async stop calls are awaited or invoked via Task if deinit can't be async) so
that workspaceGitMetadataWatcherRefreshTasksByKey and
workspaceGitMetadataWatchersByKey are cancelled/stopped and then cleared by the
existing helper.
cmuxTests/WorkspacePullRequestSidebarTests.swift (1)

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

Keep one app-level smoke test for the TabManager watcher bridge.

Line 1635 removes the only sustained-event integration assertion and replaces it with package-level coverage notes. That leaves the TabManager consumer path (draining watcher events and scheduling sidebar refresh) unverified at the app layer. Please restore one deterministic, non-timing smoke test for that bridge contract.

🤖 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 `@cmuxTests/WorkspacePullRequestSidebarTests.swift` around lines 1635 - 1640,
Restore a single deterministic app-level smoke test in
WorkspacePullRequestSidebarTests that verifies the TabManager watcher bridge:
add a test that injects a fake watcher or test double (mirroring the approach in
WorkspaceGitMetadataWatcherTests.burstCoalescesAndThrottleRearms) and a
controllable clock into TabManager, simulate a sustained burst of file-change
events, advance the injected clock instead of sleeping, and assert that
TabManager drains the watcher events and schedules exactly one sidebar refresh
(verify via the Sidebar refresh hook/mock). Use the existing CmuxWorkspaceGit
watcher-logic test strategy (injected clock & no real waiting) to keep this test
deterministic and non-timing-based.
🤖 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 `@Sources/TabManager.swift`:
- Around line 1654-1665: The code fires an uncancelled Task around
workspaceGitMetadataWatchedPaths(for:) and then applies the result with
applyWorkspaceGitMetadataWatcherDescriptor(...); instead, create and store a
cancellable Task per key in workspaceGitMetadataWatcherRefreshTasksByKey
(similar to existing watcher maps), use that Task to perform the detached work
and await its value, and ensure stopWorkspaceGitMetadataWatcher(for:) and
stopAllWorkspaceGitMetadataWatchers() cancel and remove the stored Task so
directory changes or probe removal cancel in-flight descriptor-resolution work.
Ensure weak-self/stale-request checks remain but move the Task creation into the
per-key storage so each probe owns its lifecycle.

---

Duplicate comments:
In `@cmuxTests/WorkspacePullRequestSidebarTests.swift`:
- Around line 1635-1640: Restore a single deterministic app-level smoke test in
WorkspacePullRequestSidebarTests that verifies the TabManager watcher bridge:
add a test that injects a fake watcher or test double (mirroring the approach in
WorkspaceGitMetadataWatcherTests.burstCoalescesAndThrottleRearms) and a
controllable clock into TabManager, simulate a sustained burst of file-change
events, advance the injected clock instead of sleeping, and assert that
TabManager drains the watcher events and schedules exactly one sidebar refresh
(verify via the Sidebar refresh hook/mock). Use the existing CmuxWorkspaceGit
watcher-logic test strategy (injected clock & no real waiting) to keep this test
deterministic and non-timing-based.

In `@Sources/TabManager.swift`:
- Around line 1726-1734: The TabManager deinitialization never calls
stopAllWorkspaceGitMetadataWatchers, leaving for-await consumers and
RecursivePathWatcher streams running; update TabManager.deinit to call
stopAllWorkspaceGitMetadataWatchers() (ensuring any async stop calls are awaited
or invoked via Task if deinit can't be async) so that
workspaceGitMetadataWatcherRefreshTasksByKey and
workspaceGitMetadataWatchersByKey are cancelled/stopped and then cleared by the
existing helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 52b2ff55-92b8-4a5b-a4a6-d58e8c92697c

📥 Commits

Reviewing files that changed from the base of the PR and between 986a861 and 61136b4.

📒 Files selected for processing (9)
  • Packages/CmuxFileWatch/Package.swift
  • Packages/CmuxFileWatch/README.md
  • Packages/CmuxFileWatch/Sources/CmuxFileWatch/FileSystemEventStream.swift
  • Packages/CmuxFileWatch/Sources/CmuxFileWatch/FileWatchClock.swift
  • Packages/CmuxFileWatch/Sources/CmuxFileWatch/RecursivePathWatcher.swift
  • Packages/CmuxFileWatch/Tests/CmuxFileWatchTests/RecursivePathWatcherTests.swift
  • Sources/TabManager.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/WorkspacePullRequestSidebarTests.swift

Wave-2 infrastructure leaf of the modular refactor. Pulls the FSEvents-backed
path watcher out of TabManager into a standalone, git-agnostic CmuxFileWatch
package, redesigned onto Swift 6 concurrency. No behavior change intended.

The extracted code has no git knowledge: it just watches a set of paths and
yields coalesced events. The git-specific path resolution (which paths matter,
dirty-state computation) deliberately stays in TabManager, bound for the
Wave-3 CmuxSidebarGit domain package that will depend on this leaf. This mirrors
the CmuxProcess infra-leaf precedent (one outside-world capability per package).

- `actor RecursivePathWatcher` exposes coalesced changes as `AsyncStream<Void>`
  instead of the old `init?(descriptor:onChange:)` completion-handler callback.
  Takes `paths: [String]` directly and exposes `watchedPaths` for caller dedup;
  the git-flavored `Descriptor` is gone.
- The leading-edge throttle moves off the shared-queue `DispatchWorkItem` onto a
  stored, cancel-on-stop `Task` driven by an injected `FileWatchClock` (the
  bounded/cancellable delay carve-out, justified inline). Behavior preserved:
  first event in a window arms one delay; events during the window coalesce; it
  reacts during a storm rather than waiting for changes to stop.
- `FileSystemEventStream` keeps the hardened `FSEventStream` internals: one
  shared serial queue to bound threads, synchronous queue-confined teardown so
  the stream can't leak before deinit. The FSEvents context is passed unretained
  (lifetime guaranteed by invalidate-before-deinit), which lets the callback box
  and both retain/release C trampolines be deleted, leaving one event callback.
- TabManager imports the package, resolves watched paths, stores a per-key
  consumer Task that drains `watcher.events` on the MainActor, and tears it down
  alongside the watcher. Net -262 lines from TabManager.
- The old DEBUG semaphore + Thread.sleep storm helper (and its XCTest) is
  replaced by a deterministic Swift Testing test that proves leading-edge
  coalescing with a gate clock and no real waiting.

Gates green from real logs: swift build + 4 swift tests; cmux and cmux-unit both
BUILD SUCCEEDED (GlobalISel workaround). pbxproj wired into both targets;
check-pbxproj passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@azooz2003-bit
azooz2003-bit force-pushed the feat-cmux-workspace-git branch from 61136b4 to adef166 Compare June 3, 2026 01:41
@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Thanks — addressed the two Major findings in adef166:

  • Deferred FSEvents start → synchronous failable init?. The stream is now created in init (no missed-events gap) and returns nil on creation failure (no silently-cached watcher). The sink forwards into a private raw AsyncStream capturing a Sendable continuation (not the actor), so creation stays in-init; an actor pump drains it and applies the throttle.
  • Fire-and-forget Task { await stop() } → synchronous deinit teardown. TabManager drops the watcher reference; its deinit invalidates the FSEventStream synchronously on the shared queue before returning, so there's no stop-then-start window where both streams co-watch. The consumer task captures the events stream, not the watcher, so removal is the last reference.
  • Added the README "Testing" section (gate-clock pattern) per the package-docs guideline.

On the app-level smoke test for the TabManager bridge (nitpick): respectfully skipping. The consumer bridge lives inside the @MainActor TabManager god object, which can't be instantiated in a unit test without booting the app, so a faithful test would be an XCUITest — heavier than the contract warrants while the god object is still un-decomposed. The contract is covered two other ways: the package test proves the AsyncStream coalescing, and I verified the full path (file change → sidebar dirty indicator) end-to-end on the tagged build. Per the repo's test-quality policy (skip low-value tests when no practical behavioral test exists), I'd rather add a real seam when TabManager is decomposed (the bridge gets an injectable consumer then) than a synthetic unit test now.

@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 (1)
Sources/TabManager.swift (1)

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

Recompute watcher paths even when the directory is unchanged.

This fast-path makes the source directory the only invalidation key, but workspaceGitMetadataWatchedPaths(for:) also depends on the repo’s current git metadata layout. After a checkout that adds/removes submodules or changes included config paths, the watcher keeps the old path set and never subscribes to the new metadata locations, so later filesystem events can stop refreshing the sidebar state.

Suggested fix
-        if workspaceGitMetadataWatcherSourceDirectoryByKey[key] == directory,
-           workspaceGitMetadataWatchersByKey[key] != nil {
-            if workspaceGitMetadataWatcherDescriptorRequestsByKey[key]?.directory != directory {
-                workspaceGitMetadataWatcherDescriptorRequestsByKey.removeValue(forKey: key)
-            }
-            return
-        }
-
         if workspaceGitMetadataWatcherDescriptorRequestsByKey[key]?.directory == directory {
             return
         }
🤖 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/TabManager.swift` around lines 1635 - 1645, The current early-return
uses only workspaceGitMetadataWatcherSourceDirectoryByKey[key] to skip work, but
workspaceGitMetadataWatchedPaths(for:) can change without the directory changing
(e.g., submodule/config layout updates); modify the logic in the block that
references workspaceGitMetadataWatcherSourceDirectoryByKey,
workspaceGitMetadataWatchersByKey and
workspaceGitMetadataWatcherDescriptorRequestsByKey so that you still recompute
the watched paths and update/replace the descriptor request and watcher when
workspaceGitMetadataWatchedPaths(for: key) yields a different set (or when the
descriptor’s paths differ), instead of returning solely because the source
directory is equal; in short, remove or bypass the fast-path return and
compare/update based on the computed watched paths (using
workspaceGitMetadataWatchedPaths(for:)) to refresh subscriptions when the repo
metadata layout changes.
♻️ Duplicate comments (1)
Packages/CmuxFileWatch/Tests/CmuxFileWatchTests/RecursivePathWatcherTests.swift (1)

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

Make GateClock.sleep(for:) cancellation-aware and cover stop-while-throttled.

Line 12 parks sleep(for:) on a non-throwing continuation, so cancelling RecursivePathWatcher’s throttleTask never wakes the fake clock the way SystemFileWatchClock.sleep(for:) does. That leaves the test seam out of sync with production behavior and misses the important stop()-while-throttled path.

Please switch GateClock to a throwing, cancellation-aware continuation and add a test that stops the watcher after waitForSleeper() but before releaseOne(). That closes the gap and catches regressions where a cancelled throttle still flushes or leaves a parked task behind.

Also applies to: 62-105

🤖 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/CmuxFileWatch/Tests/CmuxFileWatchTests/RecursivePathWatcherTests.swift`
around lines 12 - 18, GateClock.sleep(for:) currently uses a non-throwing
continuation and must be changed to a throwing, cancellation-aware continuation
like SystemFileWatchClock.sleep(for:) so cancelling RecursivePathWatcher’s
throttleTask actually wakes the fake clock; update GateClock.sleep(for:) to use
withCheckedThrowingContinuation (CheckedContinuation<Void, Error>), store/resume
continuations with resume(throwing: CancellationError()) on task cancellation
and resume normally on releaseOne(), and ensure arrivalWaiters behavior is
preserved. Then add a test that starts a RecursivePathWatcher, calls
waitForSleeper(), calls stop() before releaseOne(), and asserts the throttleTask
was cancelled and no flush/parked task remains (no further callbacks after
stop()), to cover the stop-while-throttled path and mirror production behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/WorkspacePullRequestSidebarTests.swift`:
- Around line 1635-1640: The comment incorrectly names the package/tests; update
the comment text that currently references CmuxWorkspaceGit and
WorkspaceGitMetadataWatcherTests to instead reference CmuxFileWatch and
RecursivePathWatcherTests.burstCoalescesAndThrottleRearms so the note points to
the actual deterministic replacement test suite; locate the comment block
containing the symbols CmuxWorkspaceGit and WorkspaceGitMetadataWatcherTests and
replace them with CmuxFileWatch and
RecursivePathWatcherTests.burstCoalescesAndThrottleRearms respectively.

---

Outside diff comments:
In `@Sources/TabManager.swift`:
- Around line 1635-1645: The current early-return uses only
workspaceGitMetadataWatcherSourceDirectoryByKey[key] to skip work, but
workspaceGitMetadataWatchedPaths(for:) can change without the directory changing
(e.g., submodule/config layout updates); modify the logic in the block that
references workspaceGitMetadataWatcherSourceDirectoryByKey,
workspaceGitMetadataWatchersByKey and
workspaceGitMetadataWatcherDescriptorRequestsByKey so that you still recompute
the watched paths and update/replace the descriptor request and watcher when
workspaceGitMetadataWatchedPaths(for: key) yields a different set (or when the
descriptor’s paths differ), instead of returning solely because the source
directory is equal; in short, remove or bypass the fast-path return and
compare/update based on the computed watched paths (using
workspaceGitMetadataWatchedPaths(for:)) to refresh subscriptions when the repo
metadata layout changes.

---

Duplicate comments:
In
`@Packages/CmuxFileWatch/Tests/CmuxFileWatchTests/RecursivePathWatcherTests.swift`:
- Around line 12-18: GateClock.sleep(for:) currently uses a non-throwing
continuation and must be changed to a throwing, cancellation-aware continuation
like SystemFileWatchClock.sleep(for:) so cancelling RecursivePathWatcher’s
throttleTask actually wakes the fake clock; update GateClock.sleep(for:) to use
withCheckedThrowingContinuation (CheckedContinuation<Void, Error>), store/resume
continuations with resume(throwing: CancellationError()) on task cancellation
and resume normally on releaseOne(), and ensure arrivalWaiters behavior is
preserved. Then add a test that starts a RecursivePathWatcher, calls
waitForSleeper(), calls stop() before releaseOne(), and asserts the throttleTask
was cancelled and no flush/parked task remains (no further callbacks after
stop()), to cover the stop-while-throttled path and mirror production behavior.
🪄 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: 936f3b33-e337-4096-96f4-bf5d135c1875

📥 Commits

Reviewing files that changed from the base of the PR and between 61136b4 and adef166.

📒 Files selected for processing (9)
  • Packages/CmuxFileWatch/Package.swift
  • Packages/CmuxFileWatch/README.md
  • Packages/CmuxFileWatch/Sources/CmuxFileWatch/FileSystemEventStream.swift
  • Packages/CmuxFileWatch/Sources/CmuxFileWatch/FileWatchClock.swift
  • Packages/CmuxFileWatch/Sources/CmuxFileWatch/RecursivePathWatcher.swift
  • Packages/CmuxFileWatch/Tests/CmuxFileWatchTests/RecursivePathWatcherTests.swift
  • Sources/TabManager.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/WorkspacePullRequestSidebarTests.swift

Comment on lines +1635 to +1640
// The watcher's leading-edge coalescing (a sustained FSEvents burst yields
// one refresh per window rather than waiting for the repo to go quiet) is now
// verified deterministically in CmuxWorkspaceGit's package tests
// (WorkspaceGitMetadataWatcherTests.burstCoalescesAndThrottleRearms), with an
// injected clock and no real waiting, instead of the previous timing-based
// semaphore storm helper.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Correct stale package/test names in the replacement-note comment.

Line 1637–1639 references CmuxWorkspaceGit / WorkspaceGitMetadataWatcherTests, but the deterministic replacement in this PR is under CmuxFileWatch (RecursivePathWatcherTests.burstCoalescesAndThrottleRearms). Please align the comment so future maintenance points to the right suite.

Suggested edit
-    // verified deterministically in CmuxWorkspaceGit's package tests
-    // (WorkspaceGitMetadataWatcherTests.burstCoalescesAndThrottleRearms), with an
+    // verified deterministically in CmuxFileWatch package tests
+    // (RecursivePathWatcherTests.burstCoalescesAndThrottleRearms), with an
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// The watcher's leading-edge coalescing (a sustained FSEvents burst yields
// one refresh per window rather than waiting for the repo to go quiet) is now
// verified deterministically in CmuxWorkspaceGit's package tests
// (WorkspaceGitMetadataWatcherTests.burstCoalescesAndThrottleRearms), with an
// injected clock and no real waiting, instead of the previous timing-based
// semaphore storm helper.
// The watcher's leading-edge coalescing (a sustained FSEvents burst yields
// one refresh per window rather than waiting for the repo to go quiet) is now
// verified deterministically in CmuxFileWatch package tests
// (RecursivePathWatcherTests.burstCoalescesAndThrottleRearms), with an
// injected clock and no real waiting, instead of the previous timing-based
// semaphore storm helper.
🤖 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 `@cmuxTests/WorkspacePullRequestSidebarTests.swift` around lines 1635 - 1640,
The comment incorrectly names the package/tests; update the comment text that
currently references CmuxWorkspaceGit and WorkspaceGitMetadataWatcherTests to
instead reference CmuxFileWatch and
RecursivePathWatcherTests.burstCoalescesAndThrottleRearms so the note points to
the actual deterministic replacement test suite; locate the comment block
containing the symbols CmuxWorkspaceGit and WorkspaceGitMetadataWatcherTests and
replace them with CmuxFileWatch and
RecursivePathWatcherTests.burstCoalescesAndThrottleRearms respectively.

azooz2003-bit and others added 3 commits June 2, 2026 22:48
Adds the DispatchSource-backed single-path counterpart to RecursivePathWatcher:
`FileWatcher(path:throttle:clock:)` exposing coalesced changes as
`AsyncStream<Void>`, with nearest-existing-ancestor recovery (watches the
closest existing ancestor and migrates the source closer as directories appear,
reattaching the path source to the current inode) and an optional leading-edge
throttle on the shared `FileWatchClock` seam. Sources attach synchronously in
init (no missed-events window); a Sendable raw-event continuation keeps the
DispatchSource handlers off the actor so creation stays in-init.

Generalizes the bespoke single-file watchers scattered across the app so they
can converge here. 4 new tests (real-FS write, create-after-start, ancestor
recovery, stop) join the existing watcher suite; all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces five hand-rolled DispatchSource file watchers with CmuxFileWatch.
Each consumer now constructs a FileWatcher and drains its events stream on the
MainActor; the bespoke watcher classes and their fd/source/debounce bookkeeping
are deleted.

- FileExplorerStore: FileExplorerDirectoryWatcher -> FileWatcher (0.3s debounce
  becomes a leading-edge throttle).
- MarkdownPanel: file+directory DispatchSource watchers (incl. the
  nearest-ancestor walk) -> FileWatcher, whose recovery subsumes that logic.
- KeyboardShortcutSettingsFileStore: ShortcutSettingsFileWatcher (primary +
  fallbacks) -> array of FileWatchers + consumer tasks.
- CmuxConfigStore: global-config and per-hook-file watchers -> FileWatcher
  (reattach/fallback/createDirectory machinery removed). The local-config
  watcher stays bespoke: it does search-directory *path re-resolution*, not
  reload-on-change.
- CmuxSettings JSONConfigStore: its JSONConfigFileWatcher (already an
  actor+AsyncStream watcher, the model FileWatcher was based on) is replaced by
  FileWatcher and deleted; CmuxSettings now takes a path dependency on
  CmuxFileWatch.

Deliberately NOT migrated (not file-reload watchers): TerminalController's
socket-path liveness monitor (generation/identity state machine on the socket
auth path; belongs to the CmuxControlSocket extraction) and FeedCoordinator /
CMUXWorkstream process-exit sources.

Gates: package swift test (8); cmux + cmux-unit BUILD SUCCEEDED (GlobalISel
workaround); CmuxSettings standalone build + tests green (the pre-existing
SettingCatalog duplicate-key test failure also fails on main, unrelated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Solidifies the behaviors the migrated consumers depend on: watching a directory
target (FileExplorer), reattaching across an atomic inode replacement
(MarkdownPanel / JSONConfigStore saves), and delivering events under a throttle.
11 tests total, all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@azooz2003-bit azooz2003-bit changed the title Extract CmuxFileWatch package (modular refactor, wave 2) Extract CmuxFileWatch and consolidate file watchers (modular refactor, wave 2) Jun 3, 2026
coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
Packages/CmuxSettings/Sources/CmuxSettings/Stores/JSONConfigStore.swift (1)

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

Register the subscriber before reading the initial value.
JSONConfigStore.values(for:) reads/yields initial before await self.addSubscriber(...); a file change handled in that gap can be broadcast to existing subscribers while this one isn’t registered yet, so the stream may miss the first external edit after iteration starts (breaking “current value, then every later change”).

Suggested fix
-                let initial = await self.value(for: key)
-                continuation.yield(initial)
-
                 let id = UUID()
                 // bufferingNewest(1): the signal carries no payload, so under
                 // burst file changes we only care that *something* changed.
                 // Dropping intermediate signals is correct because the typed
                 // value is re-read on every consumed signal and deduped below.
                 // Bounded buffering prevents unbounded growth under load.
                 let (signal, signalContinuation) = AsyncStream<Void>.makeStream(
                     bufferingPolicy: .bufferingNewest(1)
                 )
                 await self.addSubscriber(id: id, continuation: signalContinuation)
+
+                let initial = await self.value(for: key)
+                continuation.yield(initial)
🤖 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/CmuxSettings/Sources/CmuxSettings/Stores/JSONConfigStore.swift`
around lines 112 - 124, In values(for:) register the subscriber before
reading/yielding the initial value so you don't miss an external change that
arrives between the read and registration: call await self.addSubscriber(id: id,
continuation: signalContinuation) (using the same UUID id and
AsyncStream/continuation created) before invoking let initial = await
self.value(for: key) and continuation.yield(initial), ensuring the new
subscriber is registered prior to delivering the current value.
🤖 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/CmuxSettings/README.md`:
- Around line 172-174: The README header still claims "Zero non-Foundation
dependencies" but Package.swift now lists a dependency on CmuxFileWatch; update
the README.md header/landing copy to reflect that CmuxSettings depends on
CmuxFileWatch (or otherwise remove the zero-dependencies claim). Locate the
top-of-file header text in README.md and replace the incorrect claim with an
accurate short description (e.g., mention the CmuxFileWatch dependency or simply
remove the zero-dependencies sentence) so it matches the Package.swift
dependency list.

---

Outside diff comments:
In `@Packages/CmuxSettings/Sources/CmuxSettings/Stores/JSONConfigStore.swift`:
- Around line 112-124: In values(for:) register the subscriber before
reading/yielding the initial value so you don't miss an external change that
arrives between the read and registration: call await self.addSubscriber(id: id,
continuation: signalContinuation) (using the same UUID id and
AsyncStream/continuation created) before invoking let initial = await
self.value(for: key) and continuation.yield(initial), ensuring the new
subscriber is registered prior to delivering the current value.
🪄 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: 75df848e-dcc3-41a7-b8e6-6f380d2d97d2

📥 Commits

Reviewing files that changed from the base of the PR and between adef166 and 91e9086.

📒 Files selected for processing (11)
  • Packages/CmuxFileWatch/Sources/CmuxFileWatch/FileWatcher.swift
  • Packages/CmuxFileWatch/Tests/CmuxFileWatchTests/FileWatcherTests.swift
  • Packages/CmuxSettings/Package.swift
  • Packages/CmuxSettings/README.md
  • Packages/CmuxSettings/Sources/CmuxSettings/CmuxSettings.docc/CmuxSettings.md
  • Packages/CmuxSettings/Sources/CmuxSettings/Stores/JSONConfigFileWatcher.swift
  • Packages/CmuxSettings/Sources/CmuxSettings/Stores/JSONConfigStore.swift
  • Sources/CmuxConfig.swift
  • Sources/FileExplorerStore.swift
  • Sources/KeyboardShortcutSettingsFileStore.swift
  • Sources/Panels/MarkdownPanel.swift
💤 Files with no reviewable changes (2)
  • Packages/CmuxSettings/Sources/CmuxSettings/CmuxSettings.docc/CmuxSettings.md
  • Packages/CmuxSettings/Sources/CmuxSettings/Stores/JSONConfigFileWatcher.swift

Comment on lines +172 to +174
`CmuxFileWatch.FileWatcher` and fans out file-change events to per-subscriber
bounded signal streams (no `N × parse` work under burst changes). File
watching itself lives in the `CmuxFileWatch` package.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the README’s dependency claim.

This section correctly says file watching now lives in CmuxFileWatch, but the README header still says CmuxSettings has “Zero non-Foundation dependencies.” That is no longer true after the new package dependency in Packages/CmuxSettings/Package.swift, so the landing description now misleads package consumers.

🤖 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/CmuxSettings/README.md` around lines 172 - 174, The README header
still claims "Zero non-Foundation dependencies" but Package.swift now lists a
dependency on CmuxFileWatch; update the README.md header/landing copy to reflect
that CmuxSettings depends on CmuxFileWatch (or otherwise remove the
zero-dependencies claim). Locate the top-of-file header text in README.md and
replace the incorrect claim with an accurate short description (e.g., mention
the CmuxFileWatch dependency or simply remove the zero-dependencies sentence) so
it matches the Package.swift dependency list.

@azooz2003-bit
azooz2003-bit dismissed coderabbitai[bot]’s stale review June 3, 2026 04:12

Dismissing per repo owner instruction to merge: gating CI is green and the remaining CodeRabbit findings are minor doc nits (stale comment names; CmuxSettings README dependency line), not behavior issues. Bot reviewers are advisory on this PR.

@azooz2003-bit
azooz2003-bit merged commit cde524c into main Jun 3, 2026
22 checks passed
azooz2003-bit added a commit that referenced this pull request Jun 3, 2026
Carryover doc nit from PR #5244: CmuxSettings has depended on CmuxFileWatch
since the file-watcher consolidation. Doc-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
azooz2003-bit added a commit that referenced this pull request Jun 4, 2026
…vice + PullRequestProbeService) (#5277)

* CmuxGit stage 1: extract local-git metadata service from TabManager

Lift the pure on-disk git-metadata domain out of TabManager into a new
Layer-2 package, CmuxGit, behind a stateless GitMetadataService actor:

- ResolvedGitRepository / GitWorkspaceMetadata value types.
- workspaceMetadata(for:) (branch, dirty, signatures), watchedPaths(for:)
  (incl. submodule gitlinks), repositorySlugs(forDirectory:).
- ~30 nonisolated static parsing helpers: repository resolution (.git file +
  commondir), HEAD/ref reads, git config include/includeIf + glob, index v2/v3/v4
  parse (prefix compression, assume-unchanged/skip-worktree), stat-based dirty
  detection, GitHub slug parsing.

TabManager now injects a GitMetadataService and calls it at the three entry
points (watched paths, repo slugs, initial metadata snapshot); the
GitMetadataService replaces the manual Task.detached offloading. ~1,120 lines
of helpers + value types removed from TabManager (11,506 -> 10,385).

37 package tests (repo resolution, root-escape stop, branch/detached HEAD,
dirty detection, index v2/v4, content-signature stability, watched paths +
submodule gitlinks, slug parsing, full include/includeIf rules) migrated from
the app test target where they tested the now-lifted statics. Remaining
WorkspacePullRequestSidebarTests integration tests exercise the cutover
end-to-end through TabManager. Fixes the carryover stale-comment doc nit
(CmuxWorkspaceGit/WorkspaceGitMetadataWatcherTests -> CmuxGit/CmuxFileWatch).

cmux + cmux-unit build green; swift build + swift test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CmuxGit: organize sources into Model/ and Parsing/ folders

Pure file moves (git mv), no code changes. SwiftPM scans the target
recursively, so subfolders are organizational only (one module, no
namespacing/access-control effect): GitMetadataService.swift stays at the
target root as the public entry point, value types move to Model/, the
nonisolated-static parsing extensions move to Parsing/, and test fixtures move
to Tests/CmuxGitTests/Fixtures/. swift build + 37 swift tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CmuxGit: make GitMetadataService a stateless struct, not an actor

The service holds no mutable shared state (every read is a pure function of the
directory argument), so an actor's serial executor protects nothing and only
hurts: it would funnel independent per-workspace reads through one executor and,
since the methods have no internal await, run them strictly sequentially. The
original TabManager code ran these in parallel via Task.detached/@Concurrent.

Make it a `Sendable` struct with `async` methods instead. A nonisolated async
method runs on the global concurrent executor (SE-0338), so callers still get
off-main offloading (replacing the hand-rolled Task.detached) AND concurrent
reads for independent repositories run in parallel again. All call sites already
await, so they're unchanged. Promote to an actor only if an in-memory cache is
added later (real shared state would then justify the serialization).

swift build + 37 swift tests, cmux, and cmux-unit all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CmuxGit: mark GitMetadataService reads @Concurrent (guarded for <6.2)

The reads do blocking filesystem work and must stay off the caller's thread.
A bare nonisolated async method runs off-actor today (SE-0338), but under Swift
6.2's NonisolatedNonsendingByDefault it would instead run on the caller's actor
(the main thread, when called from @mainactor TabManager). @Concurrent makes the
off-main / parallel execution an explicit property of the API, robust to that
default. Guarded with #if compiler(>=6.2) so the package still compiles on older
toolchains (where the bare async already runs off-main); mirrors the existing
githubRepositorySlugs(directory:) pattern in TabManager.

swift build + 37 swift tests, cmux, and cmux-unit all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CmuxGit: bump swift-tools-version to 6.2, drop the @Concurrent compiler guard

tools-version 6.2 requires a >=6.2 toolchain to build, so the #if compiler(>=6.2)
guard around @Concurrent is always true and is removed (the attribute is now
unconditional). CI builds the cmux project on Xcode 26 (the tests job already
compiled @Concurrent), so it resolves a 6.2 manifest fine.

swift build + 37 tests, cmux, and cmux-unit green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CmuxGit: drop @Concurrent + the 6.2 bump; use plain nonisolated async

CI's tests/tests-build-and-lag/ui-regressions jobs run on warp-macos-15
(Xcode 16.4 / Swift 6.1), which cannot resolve a swift-tools-version 6.2 manifest
("package 'cmuxgit' is using Swift tools version 6.2.0 but the installed version
is 6.1.0"). Only release-build (warp-macos-26 / Xcode 26) has 6.2.

@Concurrent (which needs the 6.2 compiler, hence the tools bump + #if guard) is
unnecessary here: a struct's plain `async` methods are nonisolated, and a
nonisolated async function already runs on the global concurrent executor off the
caller's actor (SE-0338) — identical to @Concurrent under the semantics we
compile with (we don't enable NonisolatedNonsendingByDefault anywhere). So drop
@Concurrent and revert the manifest to 6.0. Same off-main / parallel behavior,
resolves on Swift 6.1. A doc note flags that adopting NonisolatedNonsendingByDefault
later would require re-adding @Concurrent.

swift build + 37 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CmuxGit: spell nonisolated explicitly and pin the off-main execution contract

Make the three reads `public nonisolated func ... async` (the keyword was
implicit on a struct, but the whole design hinges on it — say it). Add a small
internal probe + a @mainactor pinning test proving the SE-0338 contract the
service relies on: a nonisolated async method awaited from the main actor runs
on the global concurrent executor, not the main thread. There is no
`nonisolated(sending)` spelling — the off-actor behavior IS the current default
for bare nonisolated async; `nonisolated(nonsending)` (caller's actor) only
becomes the default under NonisolatedNonsendingByDefault, and if that mode is
ever adopted this test fails and points at `@concurrent` as the fix
(pthread_main_np because Thread.isMainThread is noasync).

swift build + 38 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CmuxSettings: fix stale "zero non-Foundation dependencies" README claim

Carryover doc nit from PR #5244: CmuxSettings has depended on CmuxFileWatch
since the file-watcher consolidation. Doc-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CmuxGit stage 2: extract the GitHub PR probe pipeline from TabManager

Lift sub-domain B into PullRequestProbeService, a stateless Sendable struct
(same shape as GitMetadataService: nonisolated async, off the caller's actor,
parallel): candidate-seed slug resolution (via GitMetadataService), concurrent
REST repo fetch + per-branch lookups with the caller-owned repo cache,
GH_TOKEN/`gh auth token` auth via injected CommandRunning, and the pure
selection/policy logic (preferred PR, stale-merged filtering, cache/refresh
policy). Value types (WorkspacePullRequest* family, GitHubPullRequestProbeItem)
move to the package; new PullRequestStatus enum bridges the app's
SidebarPullRequestStatus by raw value, so the apply path is unchanged.

TabManager keeps the orchestration (probe state machines, per-key dictionaries,
poll timers, the apply path, and ownership of the repo cache) and calls the
service; commandRunner stays an init parameter and now feeds the service; a
debugLog closure seam carries cmuxDebugLog in DEBUG builds. Another -840 lines
(11,506 -> 9,557 across both stages). The DispatchSourceTimer poll loops are
deliberately NOT redesigned in this PR (existing app-target orchestration;
timing changes need their own dogfood).

Tests: the 7 pure TabManagerPullRequestProbeTests migrate to the package
(48 package tests total), plus new REST-decode (merged_at -> MERGED synthesis),
branch-endpoint encoding, and result-resolution coverage. The old "not a url"
malformed-URL fixture only failed URL(string:) on pre-macOS-14 SDK parsing;
replaced with an empty string, malformed on every SDK. ci.yml's package-test
list gains CmuxFileWatch, CmuxGit, and CmuxProcess - their suites were compiled
but never executed in CI (each verified passing standalone).

swift build + 48 swift tests, cmux, and cmux-unit all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CmuxGit: strengthen two package tests (CodeRabbit)

returnsNilOutsideAnyRepository asserts nil strictly (the temp dir is never
inside a repo), and the includeIf gitdir: assertion matches against the git
directory path, which is what git evaluates the condition against.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — 91e90869 Deployed Jun 3, 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