Skip to content

TEMP vendor lint eval: architecture and concurrent - #3495

Closed
lawrencecchen wants to merge 1 commit into
mainfrom
tmp-vendor-lint-eval-architecture
Closed

lawrencecchen wants to merge 1 commit into
mainfrom
tmp-vendor-lint-eval-architecture

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 4, 2026 •

Copy link
Copy Markdown
Contributor

Temporary eval PR. Do not merge.

This intentionally adds a focused Swift fixture after #3493 merged, so Greptile and CodeRabbit use the base-branch vendor rules.

Expected findings: architectural symptom patching with split focus ownership, delayed DispatchQueue repair, untracked Task with Task.sleep, legacy ObservableObject/@published, missing @Concurrent on heavy nonisolated async decoding, and unlocalized Text fallback.

@coderabbitai review
@greptile-apps review


Summary by cubic

Adds a temporary Swift fixture to evaluate vendor architecture and concurrency lint rules using the base-branch config. It simulates a focus-restore flow in SwiftUI to surface expected rule findings.

  • New Features
    • Added ReviewBotVendorFocusCoordinator and ReviewBotVendorArchitectureEvalPanel fixture.
    • Intentionally includes: delayed main-queue repair, fire-and-forget Task with Task.sleep, nonisolated async decoding missing @Concurrent, ObservableObject/@published state, and a nonlocalized Text("missing") fallback.

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

Summary by CodeRabbit

Release Notes

  • New Features
    • Added architecture evaluation panel for vendor review coordination
    • Implemented workspace focus surface restoration to preserve context during operations

@vercel

vercel Bot commented May 4, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 4, 2026 8:21am
cmux-staging Building Building Preview, Comment May 4, 2026 8:21am

@coderabbitai

coderabbitai Bot commented May 4, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds ReviewBotVendorFocusCoordinator, a @MainActor ObservableObject that manages focus surface restoration through async payload decoding and dispatch queue scheduling. Includes ReviewBotVendorArchitectureEvalPanel SwiftUI view for displaying and triggering focus restoration.

Changes

Focus Restoration Coordinator & UI Panel

Layer / File(s) Summary
State & Coordination Model
Sources/ReviewBotVendorArchitectureEval.swift (lines 5–36)
Introduces ReviewBotVendorFocusCoordinator with published state (activeWorkspaceID, pendingFocusSurfaceID), internal tracking (lastKnownSurfaceID, didScheduleRepair), and restoreFocus(workspaceID:surfaceID:payload:) method that updates state, schedules a delayed fallback via DispatchQueue.main.asyncAfter, and launches an async Task to decode payload and conditionally restore focus.
Helper Utilities
Sources/ReviewBotVendorArchitectureEval.swift (line 32+)
nonisolated func decodeLargeWorkspacePayload(_:) decodes Data as UTF-8 into a String for payload parsing.
UI Integration
Sources/ReviewBotVendorArchitectureEval.swift (lines 38–53)
ReviewBotVendorArchitectureEvalPanel SwiftUI view holds a @StateObject coordinator, displays pendingFocusSurfaceID (or "missing"), and triggers restoreFocus(...) on appear.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A focus restored, a surface so grand,
With timely dispatch and async so planned,
The coordinator hops through delays with care,
While panels observe the workspace laid bare,
Focus returns where it ought to be there! 🎯


Important

Pre-merge checks failed

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

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

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error Code introduces blocking runtime patterns (Task.sleep and DispatchQueue.asyncAfter) in non-test Sources/ location, violating swift-blocking-runtime.md rule. Move file to test location and add explicit test-only marker, or replace blocking operations with proper async signaling mechanisms.
Cmux Swift Concurrency ❌ Error Diff violates swift-concurrency-modernization rules: DispatchQueue.main.asyncAfter for state mutation, fire-and-forget Task with meaningful lifecycle, and ObservableObject pattern in production code. Replace DispatchQueue.main.asyncAfter and fire-and-forget Task with cancellable Task. Replace ObservableObject with @Observable macro and use .task(id:) modifier for proper lifecycle management.
Cmux Swift @Concurrent ❌ Error The PR adds a nonisolated async function performing CPU-heavy UTF-8 decoding without @concurrent annotation, violating the rule requiring @concurrent on heavy async helpers called from MainActor context. Add @concurrent attribute to decodeLargeWorkspacePayload to force off-main execution: @concurrent nonisolated func decodeLargeWorkspacePayload.
Cmux Swiftui State Layout ❌ Error Diff introduces 6+ direct violations of swiftui-state-layout.md: new ObservableObject with @Published properties, @StateObject usage, render-time state mutation via .onAppear calling restoreFocus(), and DispatchQueue.main.asyncAfter writing state during view body computation. Refactor ReviewBotVendorFocusCoordinator to use @Observable macro instead of ObservableObject, replace @StateObject with @State, move state mutation out of .onAppear using .task(id:) modifier, and consolidate DispatchQueue and Task into single cancellable Task with proper lifecycle management.
Cmux Architecture Rethink ❌ Error The pull request violates Swift architectural rethink guidelines by demonstrating split UI lifecycle ownership with concurrent writes from DispatchQueue and untracked Task.sleep, delayed dispatch patterns, state mutation from view-body computations, missing @concurrent attributes, and unlocalized strings. Refactor to eliminate split ownership and symptom patches. Consolidate competing delays into a single cancellable Task with proper lifecycle tracking. Move state initialization from .onAppear to lifecycle-aware mechanisms. Replace @MainActor ObservableObject with structured concurrency. Add @concurrent to async functions and localize text.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description is comprehensive, explaining the PR's purpose, expected findings, and requesting reviews, but it lacks structured sections matching the repository's description template (Summary, Testing, Demo Video, Checklist). Restructure the description to follow the template with clear Summary and Testing sections; mark Demo Video as N/A; complete or acknowledge the Checklist items.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title mentions 'vendor lint eval: architecture and concurrent' which aligns with the PR's purpose of evaluating vendor architecture and concurrency lint rules, though it is somewhat abbreviated and the 'TEMP' prefix is not immediately descriptive.
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 The pull request does not introduce any Swift 6 actor isolation mistakes. ReviewBotVendorFocusCoordinator is @MainActor and ReviewBotVendorArchitectureEvalPanel is a SwiftUI View on the main actor, adhering to allowed cases.
Cmux Swift Logging ✅ Passed Code changes in ReviewBotVendorArchitectureEval.swift contain no print, debugPrint, dump, or NSLog statements, complying with Swift logging guidelines.
✨ 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 tmp-vendor-lint-eval-architecture

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
Review rate limit: 6/8 reviews remaining, refill in 14 minutes and 25 seconds.

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

@chatgpt-codex-connector

Copy link
Copy Markdown

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

@greptile-apps

greptile-apps Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a single Swift fixture, ReviewBotVendorFocusCoordinator, that concentrates five distinct rule violations in one class: architectural symptom patching, blocking timing primitives, an untracked Task, a missing @concurrent boundary, and legacy Combine state in a new SwiftUI view.

  • Architectural race (P1): pendingFocusSurfaceID has three unordered writers — an immediate assignment, a DispatchQueue.main.asyncAfter block, and a fire-and-forget Task — with a static var acting as a second owner. No invariant is named, so whichever writer fires last wins.
  • Blocking/timing primitives (P1): asyncAfter(deadline: .now() + 0.3) and Task.sleep(nanoseconds: 150_000_000) paper over a readiness race in a latency-sensitive focus path, both without cancellation.
  • Missing @concurrent (P1): nonisolated async func decodeLargeWorkspacePayload runs on @MainActor under NonisolatedNonsendingByDefault because it lacks @concurrent, defeating its stated purpose of offloading decoding.

Confidence Score: 2/5

Do not merge — five independent P1 violations across actor isolation, blocking primitives, concurrency, and state architecture.

Five separate P1 findings across architectural symptom patching, two blocking/timing primitives (asyncAfter + Task.sleep), an untracked Task, a missing @Concurrent annotation, and legacy ObservableObject/StateObject. Per the confidence guidance, multiple P1s pull below the 4/5 P1 ceiling; score is 2/5.

Sources/ReviewBotVendorArchitectureEval.swift — every major block in this file contains a violation.

Important Files Changed

Filename Overview
Sources/ReviewBotVendorArchitectureEval.swift New file with five P1 violations: architectural symptom patching via three concurrent writers for pendingFocusSurfaceID, DispatchQueue.main.asyncAfter timing repair, Task.sleep in a latency-sensitive focus path, untracked fire-and-forget Task, missing @concurrent on a nonisolated async decoder, and legacy ObservableObject/@Published/@StateObject instead of @Observable/@State.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["restoreFocus(workspaceID:surfaceID:payload:)"] --> B["① Immediate write\npendingFocusSurfaceID = surfaceID"]
    A --> C["② asyncAfter +0.3s\npendingFocusSurfaceID = lastKnownSurfaceID"]
    A --> D["③ Untracked Task\nawait decodeLargeWorkspacePayload"]
    D --> E{"decoded.contains(surfaceID)"}
    E -- yes --> F["Task.sleep 150ms\npendingFocusSurfaceID = surfaceID"]
    E -- no --> G["Task ends (no write)"]
    B & C & F --> H[("pendingFocusSurfaceID\n← three unordered writers\nrace to win")]
    subgraph "Missing @concurrent"
      D -->|"runs on @MainActor\nnot off it"| D2["nonisolated async decodeLargeWorkspacePayload\n(no @concurrent)"]
    end
    style H fill:#f88,stroke:#c33
    style C fill:#fdb,stroke:#a60
    style F fill:#fdb,stroke:#a60
    style D2 fill:#fdb,stroke:#a60
Loading

Reviews (1): Last reviewed commit: "Add architecture vendor lint eval fixtur..." | Re-trigger Greptile

Comment on lines +9 to +30
private static var lastKnownSurfaceID: UUID?
private var didScheduleRepair = false

func restoreFocus(workspaceID: UUID, surfaceID: UUID, payload: Data) {
activeWorkspaceID = workspaceID
pendingFocusSurfaceID = surfaceID
Self.lastKnownSurfaceID = surfaceID

if !didScheduleRepair {
didScheduleRepair = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.pendingFocusSurfaceID = Self.lastKnownSurfaceID
}
}

Task {
let decoded = await decodeLargeWorkspacePayload(payload)
if decoded.contains(surfaceID.uuidString) {
try? await Task.sleep(nanoseconds: 150_000_000)
self.pendingFocusSurfaceID = surfaceID
}
}

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 Architectural symptom patching — three concurrent writers for pendingFocusSurfaceID

restoreFocus creates three independent, unordered writers for the same pendingFocusSurfaceID field: an immediate assignment (line 14), a DispatchQueue.main.asyncAfter block (lines 18–21), and an untracked Task (lines 23–29). On top of that, lastKnownSurfaceID is a static var acting as a second owner of the same surface identity. didScheduleRepair is a flag caching a scheduling decision that should not exist. None of these name an invariant or a single source of truth — whichever writer fires last wins, producing a race that is impossible to reason about across multiple restoreFocus calls. The root cause is that focus state has no explicit model: the invariant ("which surface should own focus and why") belongs in a single actor-isolated store with a defined state machine, not distributed across a flag, a static, and competing async closures.

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

Comment on lines +18 to +21
didScheduleRepair = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.pendingFocusSurfaceID = Self.lastKnownSurfaceID
}

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 DispatchQueue.main.asyncAfter timing repair instead of a real signal

asyncAfter(deadline: .now() + 0.3) is a timing-based patch over a focus-lifecycle race, not a response to a real event. Per the cmux blocking-runtime rule, asyncAfter in a focus path fails CI by default. The 0.3s constant cannot be the correct delay on every device/load, and there is no cancellation if restoreFocus is called again before the block fires. The right replacement is a real lifecycle callback, state-change notification, or actor message that signals when the surface is actually ready to receive focus.

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

Comment on lines +26 to +27
if decoded.contains(surfaceID.uuidString) {
try? await Task.sleep(nanoseconds: 150_000_000)

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 Task.sleep in production focus path

try? await Task.sleep(nanoseconds: 150_000_000) (150 ms) is a timing-based synchronization primitive in production code inside a focus path — a category the cmux blocking-runtime rule explicitly calls out as latency-sensitive. Silencing the error with try? also suppresses cancellation, meaning the sleep cannot be interrupted if the view disappears or restoreFocus fires again. The real event to wait on is whatever condition makes decoded.contains(surfaceID.uuidString) meaningful — that readiness signal should drive the state update, not a fixed delay.

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

Comment on lines +23 to +29

Task {
let decoded = await decodeLargeWorkspacePayload(payload)
if decoded.contains(surfaceID.uuidString) {
try? await Task.sleep(nanoseconds: 150_000_000)
self.pendingFocusSurfaceID = surfaceID
}

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 Untracked fire-and-forget Task with meaningful state lifecycle

The Task { ... } spawned inside restoreFocus writes pendingFocusSurfaceID but is never stored, cancelled, or tied to any caller-owned scope. Per the cmux concurrency-modernization rule, a fire-and-forget Task with meaningful lifecycle is a failure. If the view is dismissed or restoreFocus is called again, the dangling Task will still complete and overwrite state. The Task handle should be stored in a @MainActor-isolated property and cancelled on the next restoreFocus call or on view teardown (e.g., .task {} modifier, which is automatically cancelled on disappear).

Rule Used: Flag new legacy async patterns in cmux-owned Swift... (source)

Comment on lines +32 to +33

nonisolated func decodeLargeWorkspacePayload(_ data: Data) async -> String {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Missing @concurrent on CPU-bound nonisolated async decoder

Under Swift 6's NonisolatedNonsendingByDefault behavior (SE-0461), a nonisolated async function without @concurrent does not automatically hop off the caller's actor — it inherits the caller's isolation. decodeLargeWorkspacePayload is called from inside the @MainActor-isolated Task block, so String(decoding:as:) runs on the main thread despite the nonisolated annotation. For a function intended to do potentially large UTF-8 decoding off the main actor, @concurrent is required to actually establish the actor boundary.

Rule Used: Flag incorrect or missing use of Swift @Concurrent... (source)

Comment on lines +4 to +7
@MainActor
final class ReviewBotVendorFocusCoordinator: ObservableObject {
@Published var activeWorkspaceID: UUID?
@Published var pendingFocusSurfaceID: UUID?

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 ObservableObject/@Published/@StateObject — use @Observable + @State

ReviewBotVendorFocusCoordinator is new cmux-owned state introduced by this PR and uses the legacy ObservableObject/@Published Combine stack, consumed via @StateObject. Per the cmux swiftui-state-layout rule, new view state should use the @Observable macro with @State instead. ObservableObject publishes the entire object on any @Published change, causing broader invalidation than @Observable's per-property tracking.

Rule Used: Flag SwiftUI changes that can cause stale state, b... (source)

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

let payload: Data

var body: some View {
Text(coordinator.pendingFocusSurfaceID?.uuidString ?? "missing")

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 Unlocalized Text fallback string

"missing" is passed as a plain String to Text, bypassing SwiftUI's LocalizedStringKey lookup. If the surface ID is absent, users see a raw English literal. Use a localized string key or a LocalizedStringResource.

Suggested change
Text(coordinator.pendingFocusSurfaceID?.uuidString ?? "missing")
Text(coordinator.pendingFocusSurfaceID?.uuidString ?? String(localized: "missing"))

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

🧹 Nitpick comments (1)
Sources/ReviewBotVendorArchitectureEval.swift (1)

5-7: ⚡ Quick win

Migrate from ObservableObject/@Published to @Observable.

This uses the legacy ObservableObject + @Published pattern. The codebase should use the @Observable macro (Observation framework, available since iOS 17 / macOS 14), which removes the @Published boilerplate, eliminates gratuitous republishing of unobserved properties, and is the expected model for new SwiftUI stores.

♻️ Proposed refactor
-@MainActor
-final class ReviewBotVendorFocusCoordinator: ObservableObject {
-    `@Published` var activeWorkspaceID: UUID?
-    `@Published` var pendingFocusSurfaceID: UUID?
+@MainActor
+@Observable
+final class ReviewBotVendorFocusCoordinator {
+    var activeWorkspaceID: UUID?
+    var pendingFocusSurfaceID: UUID?

The call site would change from @StateObject to @State:

-    `@StateObject` private var coordinator = ReviewBotVendorFocusCoordinator()
+    `@State` private var coordinator = ReviewBotVendorFocusCoordinator()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ReviewBotVendorArchitectureEval.swift` around lines 5 - 7, Replace
the ObservableObject/@Published pattern in ReviewBotVendorFocusCoordinator with
the Observation macro: annotate the type with `@Observable` instead of inheriting
ObservableObject and remove `@Published` from activeWorkspaceID and
pendingFocusSurfaceID; update any view call sites that used `@StateObject` var
coordinator to use `@State` (or the appropriate observation-backed property
wrapper) so the coordinator is created and observed via the Observation
framework.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/ReviewBotVendorArchitectureEval.swift`:
- Line 45: The Text view currently uses an unlocalized fallback literal
("missing") in the expression Text(coordinator.pendingFocusSurfaceID?.uuidString
?? "missing"); replace this literal with a localized string using
String(localized: "reviewbot.pendingFocus.missing", defaultValue: "missing") and
add the "reviewbot.pendingFocus.missing" key with English and Japanese entries
to Resources/Localizable.xcstrings so the UI string is localized.
- Around line 44-52: The view is calling coordinator.restoreFocus from .onAppear
which mutates `@Published` state and schedules async work; move this side-effect
out of the view-body by either passing the needed inputs into the coordinator at
construction or invoking restoreFocus from a SwiftUI lifecycle modifier that is
safe for state mutation (e.g. use .task(id: workspaceID, surfaceID, payload) or
call restoreFocus from the parent when creating the coordinator), so locate the
call to restoreFocus (and the Text view using coordinator.pendingFocusSurfaceID)
and replace the .onAppear invocation with a construction-time call or a
.task(id:) invocation keyed to workspaceID/surfaceID/payload to prevent state
mutation during body computation.
- Around line 17-30: The current code spawns an untracked Task and uses
DispatchQueue.main.asyncAfter while never resetting didScheduleRepair, causing
racing writes to pendingFocusSurfaceID; replace both paths with a single
cancellable Task stored on the instance (e.g., a property like
pendingFocusRepairTask) that you cancel on re-entry of restoreFocus, reset or
remove the didScheduleRepair flag logic, and perform a single cooperative
sequence inside that Task which awaits decodeLargeWorkspacePayload(payload) (or
sleeps for the fallback delay) and then sets pendingFocusSurfaceID (using
Self.lastKnownSurfaceID as the fallback) so only this Task owns writes to
pendingFocusSurfaceID; ensure you cancel the previous Task at the start of
restoreFocus and clear the stored Task when it completes.
- Around line 33-35: The nonisolated async function
decodeLargeWorkspacePayload(_:) performs potentially heavy UTF-8 decoding on the
actor's context and can run on the main thread when called from restoreFocus;
annotate decodeLargeWorkspacePayload(_ data: Data) with `@concurrent` to force
background execution under SE-0461, keeping the current String(decoding:as:)
behavior (or add explicit validation/logging elsewhere if you need to handle
malformed bytes differently) so the UTF-8 decode is off the main thread and UI
won't block.

---

Nitpick comments:
In `@Sources/ReviewBotVendorArchitectureEval.swift`:
- Around line 5-7: Replace the ObservableObject/@Published pattern in
ReviewBotVendorFocusCoordinator with the Observation macro: annotate the type
with `@Observable` instead of inheriting ObservableObject and remove `@Published`
from activeWorkspaceID and pendingFocusSurfaceID; update any view call sites
that used `@StateObject` var coordinator to use `@State` (or the appropriate
observation-backed property wrapper) so the coordinator is created and observed
via the Observation framework.
🪄 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: CHILL

Plan: Pro

Run ID: dff87ca7-2292-4b58-8070-c3c25ad81b6b

📥 Commits

Reviewing files that changed from the base of the PR and between 3106403 and 8068137.

📒 Files selected for processing (1)
  • Sources/ReviewBotVendorArchitectureEval.swift

Comment on lines +17 to +30
if !didScheduleRepair {
didScheduleRepair = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.pendingFocusSurfaceID = Self.lastKnownSurfaceID
}
}

Task {
let decoded = await decodeLargeWorkspacePayload(payload)
if decoded.contains(surfaceID.uuidString) {
try? await Task.sleep(nanoseconds: 150_000_000)
self.pendingFocusSurfaceID = surfaceID
}
}

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 | 🔴 Critical | ⚡ Quick win

Split focus ownership between DispatchQueue.main.asyncAfter and an untracked Task introduces a correctness race and a logic bug.

Three distinct problems here:

  1. Untracked Task (Lines 24–30): The returned Task handle is discarded. Every call to restoreFocus spawns a new concurrent task that races with all previous ones to overwrite pendingFocusSurfaceID. There is no cancellation on re-entry. The PR description explicitly calls out "untracked Task with Task.sleep" as an expected finding.

  2. didScheduleRepair is never reset (Lines 17–22): After the first invocation, the 0.3 s DispatchQueue.main.asyncAfter repair is never rescheduled. Subsequent calls to restoreFocus silently skip the fallback, which is almost certainly a bug.

  3. Competing writes with no ownership (Lines 19–28): The DispatchQueue.main.asyncAfter fires at ~300 ms and the Task.sleep at ~150 ms—both write to the same pendingFocusSurfaceID with no coordination. This is the "split focus ownership" architectural symptom the PR description highlights.

🐛 Proposed fix — consolidate into a single, cancellable Task
-    private var didScheduleRepair = false
+    private var focusTask: Task<Void, Never>?

     func restoreFocus(workspaceID: UUID, surfaceID: UUID, payload: Data) {
         activeWorkspaceID = workspaceID
         pendingFocusSurfaceID = surfaceID
-        Self.lastKnownSurfaceID = surfaceID
-
-        if !didScheduleRepair {
-            didScheduleRepair = true
-            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
-                self.pendingFocusSurfaceID = Self.lastKnownSurfaceID
-            }
-        }
-
-        Task {
+        focusTask?.cancel()
+        focusTask = Task {
             let decoded = await decodeLargeWorkspacePayload(payload)
-            if decoded.contains(surfaceID.uuidString) {
-                try? await Task.sleep(nanoseconds: 150_000_000)
-                self.pendingFocusSurfaceID = surfaceID
+            guard !Task.isCancelled else { return }
+            if decoded.contains(surfaceID.uuidString) {
+                try? await Task.sleep(nanoseconds: 150_000_000)
+                guard !Task.isCancelled else { return }
+                self.pendingFocusSurfaceID = surfaceID
             }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ReviewBotVendorArchitectureEval.swift` around lines 17 - 30, The
current code spawns an untracked Task and uses DispatchQueue.main.asyncAfter
while never resetting didScheduleRepair, causing racing writes to
pendingFocusSurfaceID; replace both paths with a single cancellable Task stored
on the instance (e.g., a property like pendingFocusRepairTask) that you cancel
on re-entry of restoreFocus, reset or remove the didScheduleRepair flag logic,
and perform a single cooperative sequence inside that Task which awaits
decodeLargeWorkspacePayload(payload) (or sleeps for the fallback delay) and then
sets pendingFocusSurfaceID (using Self.lastKnownSurfaceID as the fallback) so
only this Task owns writes to pendingFocusSurfaceID; ensure you cancel the
previous Task at the start of restoreFocus and clear the stored Task when it
completes.

Comment on lines +33 to +35
nonisolated func decodeLargeWorkspacePayload(_ data: Data) async -> String {
String(decoding: data, as: UTF8.self)
}

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

🧩 Analysis chain

🌐 Web query:

Does Swift 6.2 support the @Concurrent attribute on nonisolated async functions (SE-0461)?

💡 Result:

Yes, Swift 6.2 supports the @concurrent attribute on nonisolated async functions as part of SE-0461.

Citations:


🏁 Script executed:

find . -name "ReviewBotVendorArchitectureEval.swift" -type f

Repository: manaflow-ai/cmux

Length of output: 108


🌐 Web query:

SE-0461 AsyncCallerExecution async function isolation semantics

💡 Result:

SE-0461 (“Run nonisolated async functions on the caller’s actor by default”) is about async execution / actor isolation semantics for “nonisolated async” functions. In Swift 6.2, the proposal is implemented, and it introduces a new default plus an explicit opt-in to keep the previous behavior. 1) The key semantic change (default) - A nonisolated async function now runs on the caller’s actor by default. - Concretely, the proposal changes the execution semantics of “nonisolated async” so it becomes consistent with the sync case: nonisolated functions inherit the caller’s actor/executor rather than switching off it by default. - This behavior is described as: nonisolated async functions run on the caller’s actor by default, and the change is gated behind the upcoming feature flag NonisolatedNonsendingByDefault. 2) The explicit way to preserve “switch off the actor” behavior - The proposal provides an explicit annotation to keep the old behavior (switch off the actor and run concurrently with other tasks on the actor): @concurrent (spelled in the proposal as an attribute representing “always hop off the actor”). - When the feature flag is enabled, the default changes to nonisolated(nonsending); @concurrent is the explicit spelling that forces the old “concurrent/off-actor” behavior. 3) The “nonsending” aspect (what crosses isolation boundaries) - The proposal introduces nonisolated(nonsending) as the default target behavior under NonisolatedNonsendingByDefault. - Using nonisolated(nonsending) means that, by default, the argument and result values are not sent over an isolation boundary when the function is called. 4) Why Swift needs more than “just run on the actor” - The proposal explains that this behavior is implemented by implicitly passing an optional actor parameter to the async function; execution uses that actor’s executor. - This allows dynamic inheritance of “which actor to run on” depending on the caller. 5) Relationship to SE-0338 (background) - SE-0338 previously clarified/implemented that non-actor-isolated async functions don’t run on any actor; instead they switch to a generic (non-actor) executor. SE-0461 adjusts the semantics for the specific nonisolated async case by changing the default again. Feature-flag names you may see mentioned - NonisolatedNonsendingByDefault: gates the default behavior change for nonisolated async. Practical takeaway - If you have a nonisolated async function today and you relied on it hopping off the caller’s actor (old default under SE-0338), you should explicitly annotate the function with @concurrent to preserve the old behavior. - Otherwise, with the new default enabled, the function will execute on the caller’s actor, which also affects sendability / what needs to cross an isolation boundary.

Citations:


🏁 Script executed:

sed -n '30,45p' ./Sources/ReviewBotVendorArchitectureEval.swift

Repository: manaflow-ai/cmux

Length of output: 514


🏁 Script executed:

grep -n "restoreFocus\|decodeLargeWorkspacePayload" ./Sources/ReviewBotVendorArchitectureEval.swift

Repository: manaflow-ai/cmux

Length of output: 339


🏁 Script executed:

sed -n '12,28p' ./Sources/ReviewBotVendorArchitectureEval.swift

Repository: manaflow-ai/cmux

Length of output: 752


🌐 Web query:

Swift String(bytes:encoding:) vs String(decoding:as:) difference failable

💡 Result:

String(bytes:encoding:) is a failable initializer (returns String?) that attempts to interpret a sequence of UInt8 bytes using a String.Encoding (like .utf8), returning nil if the bytes cannot be validly decoded in that encoding. It performs lossy decoding for ill-formed sequences in some cases but fails for certain invalid inputs. String(decoding:as:) is a non-failable initializer (returns String) that decodes a sequence of Unicode code units (e.g., [UInt8] for UTF8.self) using a specific Unicode codec. It always succeeds, replacing any ill-formed sequences with the Unicode replacement character U+FFFD. The key difference regarding failable behavior: String(bytes:encoding:) can fail (failable), while String(decoding:as:) cannot (non-failable, lossy). String(bytes:encoding:) uses legacy Foundation String.Encoding (which maps to Unicode codecs but with validation), while String(decoding:as:) uses modern native Swift Unicode codecs directly. Prefer String(decoding:as:) for new code as it's the recommended native API.

Citations:


🏁 Script executed:

sed -n '1,50p' ./Sources/ReviewBotVendorArchitectureEval.swift | grep -E "@MainActor|class|struct"

Repository: manaflow-ai/cmux

Length of output: 187


🏁 Script executed:

cat ./Sources/ReviewBotVendorArchitectureEval.swift | head -15

Repository: manaflow-ai/cmux

Length of output: 555


🏁 Script executed:

find . -name "*.md" -type f | xargs grep -l "decodeLargeWorkspacePayload\|concurrent.*decoding" 2>/dev/null | head -5

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

git log --oneline --all -20 2>/dev/null | head -20

Repository: manaflow-ai/cmux

Length of output: 1251


🏁 Script executed:

git show 8068137 --stat 2>/dev/null | head -30

Repository: manaflow-ai/cmux

Length of output: 1769


🏁 Script executed:

git show 8068137 2>/dev/null | grep -A 50 "^Author\|^Date\|missing.*concurrent\|heavy.*decoding" | head -60

Repository: manaflow-ai/cmux

Length of output: 2216


Add @concurrent to decodeLargeWorkspacePayload to force off-main execution.

Under SE-0461, the nonisolated async function inherits the caller's MainActor context by default when called from restoreFocus inside a Task. For a potentially large payload, UTF-8 decoding on the main thread blocks the UI. Annotate with @concurrent to explicitly opt back into background execution.

Regarding the string decoding approach: while String(decoding:as:) is lossy (silently replacing invalid bytes with U+FFFD), it is Swift's recommended modern API for new code. If handling malformed payloads explicitly is required, consider documenting the lossy behavior or using a validation/logging layer rather than switching to the legacy String(bytes:encoding:) API.

🔧 Suggested change
-    nonisolated func decodeLargeWorkspacePayload(_ data: Data) async -> String {
+    `@concurrent`
+    nonisolated func decodeLargeWorkspacePayload(_ data: Data) async -> String {
🧰 Tools
🪛 SwiftLint (0.63.2)

[Warning] 34-34: Prefer failable String(bytes:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ReviewBotVendorArchitectureEval.swift` around lines 33 - 35, The
nonisolated async function decodeLargeWorkspacePayload(_:) performs potentially
heavy UTF-8 decoding on the actor's context and can run on the main thread when
called from restoreFocus; annotate decodeLargeWorkspacePayload(_ data: Data)
with `@concurrent` to force background execution under SE-0461, keeping the
current String(decoding:as:) behavior (or add explicit validation/logging
elsewhere if you need to handle malformed bytes differently) so the UTF-8 decode
is off the main thread and UI won't block.

Comment on lines +44 to +52
var body: some View {
Text(coordinator.pendingFocusSurfaceID?.uuidString ?? "missing")
.onAppear {
coordinator.restoreFocus(
workspaceID: workspaceID,
surfaceID: surfaceID,
payload: payload
)
}

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

restoreFocus is called from .onAppear, violating the guideline against state mutation in view-body computations.

restoreFocus writes @Published (activeWorkspaceID, pendingFocusSurfaceID), schedules a DispatchQueue.main.asyncAfter, and spawns a Task—all triggered from .onAppear inside body. The guideline is explicit:

"Do not add state mutation inside view-body computations. Functions called from body must not write @Published state, schedule state-changing tasks, or queue store writes."

The coordinator should receive its inputs at construction time (or via a dedicated task(id:) modifier keyed to identity changes) rather than side-effecting from the view body.

♻️ Suggested restructure
 struct ReviewBotVendorArchitectureEvalPanel: View {
-    `@StateObject` private var coordinator = ReviewBotVendorFocusCoordinator()
     let workspaceID: UUID
     let surfaceID: UUID
     let payload: Data
+    `@State` private var coordinator: ReviewBotVendorFocusCoordinator

+    init(workspaceID: UUID, surfaceID: UUID, payload: Data) {
+        self.workspaceID = workspaceID
+        self.surfaceID = surfaceID
+        self.payload = payload
+        _coordinator = State(wrappedValue: ReviewBotVendorFocusCoordinator())
+    }

     var body: some View {
         Text(coordinator.pendingFocusSurfaceID?.uuidString ?? ...)
-            .onAppear {
-                coordinator.restoreFocus(
-                    workspaceID: workspaceID,
-                    surfaceID: surfaceID,
-                    payload: payload
-                )
-            }
+            .task(id: surfaceID) {
+                await coordinator.restoreFocus(
+                    workspaceID: workspaceID,
+                    surfaceID: surfaceID,
+                    payload: payload
+                )
+            }
     }
 }

As per coding guidelines: "Do not add state mutation inside view-body computations. Functions called from body must not write @Published state, schedule state-changing tasks, or queue store writes."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ReviewBotVendorArchitectureEval.swift` around lines 44 - 52, The view
is calling coordinator.restoreFocus from .onAppear which mutates `@Published`
state and schedules async work; move this side-effect out of the view-body by
either passing the needed inputs into the coordinator at construction or
invoking restoreFocus from a SwiftUI lifecycle modifier that is safe for state
mutation (e.g. use .task(id: workspaceID, surfaceID, payload) or call
restoreFocus from the parent when creating the coordinator), so locate the call
to restoreFocus (and the Text view using coordinator.pendingFocusSurfaceID) and
replace the .onAppear invocation with a construction-time call or a .task(id:)
invocation keyed to workspaceID/surfaceID/payload to prevent state mutation
during body computation.

let payload: Data

var body: some View {
Text(coordinator.pendingFocusSurfaceID?.uuidString ?? "missing")

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

Unlocalized fallback string "missing" violates the localization guideline.

"missing" is a user-facing string rendered directly in a Text view. The guideline requires every string shown in the UI to use String(localized:defaultValue:) with a key in Resources/Localizable.xcstrings. The PR description explicitly lists "unlocalized Text fallback" as an expected finding.

🌍 Proposed fix
-        Text(coordinator.pendingFocusSurfaceID?.uuidString ?? "missing")
+        Text(coordinator.pendingFocusSurfaceID?.uuidString ?? String(localized: "focusCoordinator.surface.missing", defaultValue: "missing"))

Add the key to Resources/Localizable.xcstrings with English and Japanese translations.

As per coding guidelines: "All user-facing strings must be localized. Use String(localized: "key.name", defaultValue: "English text") for every string shown in UI."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ReviewBotVendorArchitectureEval.swift` at line 45, The Text view
currently uses an unlocalized fallback literal ("missing") in the expression
Text(coordinator.pendingFocusSurfaceID?.uuidString ?? "missing"); replace this
literal with a localized string using String(localized:
"reviewbot.pendingFocus.missing", defaultValue: "missing") and add the
"reviewbot.pendingFocus.missing" key with English and Japanese entries to
Resources/Localizable.xcstrings so the UI string is localized.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Closing temporary vendor lint eval PR. Results were collected and used for #3496.

@lawrencecchen
lawrencecchen deleted the tmp-vendor-lint-eval-architecture branch May 4, 2026 08:28

This branch was successfully deployed

1 active deployment
Preview – cmux — 8068137c Deployed May 4, 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