Skip to content

Modernize and ship Feed entry points - #8174

Closed
lawrencecchen wants to merge 54 commits into
mainfrom
task-modernize-feed
Closed

lawrencecchen wants to merge 54 commits into
mainfrom
task-modernize-feed

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • gate Feed with feed-ui-enabled-release: Debug is enabled for dogfood, Release fails closed, and the PostHog rollout is active at 0%
  • remove All Activity and retain only actionable cards; resolved cards remain visible for the current process but are never restored
  • persist only pending decisions with atomic snapshots capped at 100 items and 2 MiB; cap legacy reads at 8 MiB and the in-memory event ring at 200 entries
  • keep telemetry memory-only, bound variable payloads and context caches, and rewrite the snapshot after resolution, expiry, or removal
  • keep OpenCode on one line and show the submitted question choice in the resolved card
  • make card clicks and Open Terminal use one stable-surface route, focus the current terminal owner, and flash the destination
  • add right-click Open Terminal and Remove actions

Verification

  • final tagged fleet build passed at commit 898c25133b with tag fd8174
  • live feed.jump preflight returned matched: true, selected surface 69232B6A-3767-478F-9E29-D818008FA2DC, and recorded flash count 1
  • resolved-question screenshot shows OpenCode on one line, Answering a question selected, and Answered: Answering a question
  • feed.list excluded injected telemetry and both legacy history files remained absent after resolution
  • CmuxControlSocket execution-policy suite: 20 tests passed; Feed coordinator jump suite: 1 test passed
  • Workstream focused suite: 34 tests passed; 100 large-payload stress snapshot stayed within 2 MiB
  • feature-flag lint, localization parsing, project wiring, and diff checks passed

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


Summary by cubic

Ships Feed by default behind the feed-ui-enabled-release flag with clear entry points: right sidebar, “Open Feed as Pane”, and a pinned .feed workspace. Adds feed.jump with main‑actor terminal focus, attention and notifications, bounded waits, and tighter retention/restore for pending decisions.

  • New Features

    • Gate Feed with feed-ui-enabled-release; show in right sidebar, command palette (“Open Feed as Pane”), and titlebar new‑workspace menu; create a native .feed pinned workspace via FeedOpeningCoordinator.
    • Add feed.jump on the control socket to run the same UI focus used by Feed rows; resolve sessions via a file‑backed lookup, prefer live surfaces, and follow stable surface identities; share a process‑wide projection via FeedPresentationStore and post inline‑action notifications using async UNUserNotificationCenter.
  • Bug Fixes

    • Retain only actionable pending items across restarts; cap memory ring at 200, persisted rows at 100, and snapshot bytes at 2 MB; trim oversized fields and drop telemetry rows; restore only the newest pending items.
    • Validate feed.jump targets and execute on the UI lane; add tests for jump dispatch, live surface ownership, and identity resolution.
    • Bound socket waits with FeedBlockingWaiterRegistry/FeedBlockingCallBridge; split waiter completion to close reply/timeout races; cancel timed‑out ingests and restore reply/notification fallbacks.
    • Preserve failed Feed replies; fix pane interaction and navigation; keep Feed panes out of right‑sidebar focus; align pane filters with content; expand tests for retention, routing, history boundaries, timeout bounds, editor scope transitions, and focus routing.

Written for commit 898c251. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a Feed experience with actionable requests, activity history, permissions, questions, exit plans, telemetry, and task summaries.
    • Feed can open as a pinned workspace or right-sidebar pane, with keyboard navigation and focus support.
    • Added notifications, attention indicators, and direct navigation to related terminal surfaces.
    • Added a remotely controlled Feed feature flag and new workspace creation option.
  • Updates

    • Removed the former Feed beta toggle from settings and the command palette.
    • Improved Feed localization, pluralization, formatting, and error messaging.
    • Improved retention and recovery of pending Feed items.
  • Bug Fixes

    • Prevented unavailable sidebar modes from being restored or opened.
    • Improved timeout handling and reply delivery reliability.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR promotes Feed from a beta setting to a remotely controlled feature, adds native Feed workspace and pane entry points, introduces Feed presentation and interaction components, refactors blocking coordination and notifications, changes workstream persistence to bounded snapshots, and updates localization, project wiring, and tests.

Changes

Native Feed rollout

Layer / File(s) Summary
Feature flag and workspace integration
Sources/FeatureFlags.swift, Sources/Workspace.swift, Sources/AppDelegate.swift, Sources/RightSidebar*, Sources/NewWorkspaceMenuModel.swift
Feed availability now uses feed-ui-enabled-release; Feed workspaces, panes, menus, command-palette entries, focus handling, and feature-disable reconciliation are wired through the application.
Feed UI and interaction
Sources/Feed/*, Sources/MainWindowFocusTypes.swift, Sources/MainWindowFocusController.swift
Native Feed presentation, actionable rows, telemetry, inline editors, keyboard focus, notifications, and row actions are added.
Blocking coordination and transport
Sources/Feed/FeedCoordinator.swift, Sources/Feed/FeedBlockingWaiterRegistry.swift, Sources/TerminalController.swift
Blocking ingestion uses actor-owned waiter state with timeout race handling; socket replies return actual delivery status and Feed payloads use shared encoding.
Workstream retention and persistence
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/*
Workstream kinds gain telemetry cases, Feed retention caps variable content, and persistence changes from append/page storage to bounded generation-aware pending snapshots.
Validation and resources
cmuxTests/*, cmuxUITests/*, Resources/Localizable.xcstrings, cmux.xcodeproj/project.pbxproj
Tests cover feature flags, Feed workspaces, focus routing, waiter ordering, retention, persistence, and localization/project inputs are updated.

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

Possibly related issues

  • manaflow-ai/cmux-dev-artifacts#4428 — Adds a Feed feature-flag test in the same PostHogAnalyticsPropertiesTests area.
  • manaflow-ai/cmux-dev-artifacts#4401 — Reports test execution for the same feature-flag test file modified here.

Possibly related PRs

  • manaflow-ai/cmux#3854 — Removes the former right-sidebar Feed beta toggle and related availability wiring.
  • manaflow-ai/cmux#3924 — Gates permission notification delivery and cancellation on awaiting-decision state.
  • manaflow-ai/cmux#8371 — Migrates overlapping Feed components from ObservableObject to @Observable.

Important

Pre-merge checks failed

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

❌ Failed checks (9 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error The PR adds production FeedBlockingCallBridge.wait(timeout:) using DispatchGroup.wait and uses it in feed.push/reply handlers, introducing blocking synchronization. Replace the bridge/waits with async actor- or continuation-based coordination so the socket path never blocks on a timeout or semaphore-like wait.
Cmux Cache Substitution Correctness ❌ Error WorkstreamPersistence.loadLegacyRemovedItemIDs() returns [] when the tombstone file exceeds the read cap, so snapshot restore can ignore prior deletions. Fail closed on oversized legacy tombstones, or implement a bounded migration that preserves deleted IDs before restoring pending items.
Cmux Algorithmic Complexity ❌ Error WorkstreamItem.retainedForFeed calls String.count/prefix on multiple unbounded event strings in the ingest hot path, causing full-string scans without a byte-bounded cap. Use a byte-bounded truncation helper (or pre-truncate upstream) and avoid String.count on untrusted payloads; keep identifiers exact.
Cmux Swift Concurrency ❌ Error FeedCoordinator.postNotificationIfStillAwaiting() spawns an unowned Task for ordinary async work; it isn’t stored, cancelled, or awaited. Make the notification path async and await it from ingestBlocking, or otherwise tie the task’s lifecycle to the request.
Cmux Swift Package Boundaries ❌ Error Pure reusable Feed logic (waiter registry, jump resolver, socket encoder, snapshots/store) was added under app-root Sources/Feed instead of a package target. Move the core Feed types into a small CmuxFeedCore SwiftPM target (start with FeedItemSnapshot/FeedPresentationSnapshot/FeedSocketEncoder), and leave only FeedCoordinator/UI/AppKit glue in the app target.
Cmux User-Facing Error Privacy ❌ Error PermissionInputPreview renders raw toolInputJSON for unknown tools, and FeedPermissionView shows it in approval UI, exposing unredacted payload dumps. Redact or summarize the fallback preview; never render raw toolInputJSON in user-visible copy, and scrub any secrets before display.
Cmux Full Internationalization ❌ Error Resources/Localizable.xcstrings adds new user-facing keys with only en/ja values, but the catalog already supports 20 locales. Add translated entries for every existing locale code in the touched string catalog, or remove unsupported locales before shipping.
Cmux Architecture Rethink ❌ Error WorkstreamStore now fire-and-forgets Task writes in schedulePendingSnapshot(), so no caller owns persistence and stale snapshot state can be dropped. Own/coalesce the snapshot writer inside WorkstreamStore or Persistence, await or store the task, and propagate failures instead of try?.
Cmux No Ambient Global State ❌ Error Fail: AppDelegate.swift:12 adds app-delegate state, FeedCoordinator.swift:21 adds a singleton, and FeedPanelView.swift:7 widens a file-scope helper. Inject a FeedCoordinator instance at the app seam, avoid AppDelegate-owned feedOpeningCoordinator, and keep feedDebugResponderSummary private/fileprivate.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description covers summary and verification, but it misses the template's Testing/Demo Video sections and the checklist/review-trigger structure. Add the missing Testing and Demo Video sections, include the required review-trigger block and checklist, and keep the verification details under the expected headings.
✅ Passed checks (14 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed New UI stores/coordinators are @MainActor or actors, and FeedCoordinator fences mutable UI state on MainActor while worker state lives in FeedBlockingWaiterRegistry.
Cmux Browser Automation Off-Main ✅ Passed PASS: The PR only adds feed routing; browser wait/callback/cookie verbs remain in socketWorkerMethods, and main-actor browser cases are limited to direct focus/show commands allowed by the rule.
Cmux Expensive Synchronous Load ✅ Passed No new main-actor history/session load was introduced; the new row actions use resolveOffMain, and the remaining feed.jump lookup was already an existing main-actor path.
Cmux No Hacky Sleeps ✅ Passed The diff contains no changed TypeScript/JavaScript/shell/build-runtime scripts, so the non-Swift no-hacky-sleeps rule is not violated.
Cmux Swift @Concurrent ✅ Passed PASS: The only nonisolated async helpers that leave the caller actor (evaluate, resolveOffMain) are annotated @concurrent, and file I/O is isolated on CmuxNotificationHookCache.
Cmux Swiftpm Lockfiles ✅ Passed The only dependency-related file changed is cmux.xcodeproj/project.pbxproj, and it adds source/build-file entries—not SwiftPM package refs; no Package.resolved or .gitignore changes appear.
Cmux Swift Logging ✅ Passed Only new logging is print(...) inside #if DEBUG Feed preview scaffolding; no production print/NSLog/Logger additions were found.
Cmux Swiftui State Layout ✅ Passed PR uses @Observable/@State for Feed state, passes snapshots/closures into LazyVStack rows, and adds no new GeometryReader or render-time mutation in changed SwiftUI views.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: the new Feed preview and button-style windows set stable cmux.* identifiers and are listed in cmuxAuxiliaryWindowIdentifiers, so Cmd+W uses the shared path.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/config/localization/test assets; no temp/build/cache/screenshot/log/artifact paths appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Only AppDelegate.swift changed, and its diff adds production feed routing/feature-flag logic without new #if DEBUG or *ForTesting seam exposure.
Title check ✅ Passed The title is concise and accurately summarizes the main change: shipping Feed entry points.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-modernize-feed

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.

@greptile-apps

greptile-apps Bot commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR ships the Feed UI behind a default-on feed-ui-enabled-release PostHog flag, splits FeedCoordinator into focused extension files, and replaces the old NSLock/DispatchSemaphore blocking-waiter with a Swift actor registry plus a bounded FeedBlockingCallBridge.

  • Concurrency hardening: ingestBlocking is now async, UI state is @MainActor-isolated, DispatchQueue.main.sync replaced with await MainActor.run, and FeedPanelViewModel migrated from ObservableObject to @Observable.
  • Entry points: FeedOpeningCoordinator centralises workspace and pane opening behind the flag; RightSidebarMode.isAvailable() is @MainActor and wired to CmuxFeatureFlags.shared; the old beta Settings toggle is removed.
  • Remaining issues: resolveAttentionTarget still calls jumpResolver.lookup (synchronous disk I/O) on the cooperative thread pool inside the feed.push socket-handler path, and concludeAttentionOnMain retains a Thread.isMainThread fast-path that is unreliable in Swift concurrency contexts and dead code in the new async callers.

Confidence Score: 3/5

Safe to merge after addressing the two concurrency issues; the core async-registry design is solid but leaves a cooperative-thread disk-read and a legacy Thread.isMainThread guard that can race with the main actor.

Two issues remain on the same hot path: resolveAttentionTarget performs a synchronous hook-session file read on the cooperative thread pool for every blocking feed.push event, and concludeAttentionOnMain uses a Thread.isMainThread check that is dead code in the new async callers and unsafe if the cooperative scheduler lands on the main OS thread, causing MainActor.assumeIsolated to be called without the actor lock held.

Sources/Feed/FeedCoordinator+Attention.swift (resolveAttentionTarget sync disk read) and Sources/Feed/FeedCoordinator.swift (concludeAttentionOnMain Thread.isMainThread guard)

Important Files Changed

Filename Overview
Sources/Feed/FeedCoordinator.swift Major refactor: sync blocking ingest replaced with async+registry; Thread.isMainThread guard in concludeAttentionOnMain is unsafe when called from cooperative-pool async tasks.
Sources/Feed/FeedCoordinator+Attention.swift New file: in-app attention surfacing; resolveAttentionTarget calls jumpResolver.lookup (synchronous disk I/O) from within ingestBlocking's cooperative-pool async context, blocking a cooperative thread on every blocking feed.push.
Sources/Feed/FeedBlockingWaiterRegistry.swift New actor-isolated registry replacing NSLock+dictionary; clean actor-based isolation with bounded AsyncStream.
Sources/Feed/FeedBlockingCallBridge.swift New synchronous bridge for socket-worker thread; wait(timeout:) correctly bounds the DispatchGroup wait.
Sources/Feed/FeedCoordinator+Notifications.swift Notification delivery split into focused file; correct async UNUserNotificationCenter usage with waiter-table guards against stale banners.
Sources/Feed/FeedJumpResolver.swift New resolver; resolveOffMain correctly wraps disk I/O in a detached task, but lookup called directly by resolveAttentionTarget still runs synchronously on callers' executors.
Sources/Feed/FeedOpeningCoordinator.swift Clean new entry-point coordinator; @MainActor, no ambient state, properly gates both workspace and pane paths behind the feature flag.
Sources/FeatureFlags.swift Feed flag added as named private static let feedDefinition; all accessors now use named constants instead of positional indices.
Sources/Feed/FeedPresentationStore.swift New @MainActor @Observable store; clean single-pass O(n) projection builder with reserved-capacity arrays and generation-guarded observation.
Sources/Feed/FeedSocketEncoding.swift New socket encoding helpers; dateFormatStyle is a stored instance property allocated once, not per-call.
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamPersistence.swift Widens redactedForPersistence() from private to public so FeedSocketEncoding can use it for safe diagnostic projections.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant SW as Socket Worker
    participant Bridge as FeedBlockingCallBridge
    participant IC as ingestBlocking
    participant Registry as FeedBlockingWaiterRegistry
    participant Main as MainActor

    SW->>Bridge: wait(timeout)
    Bridge->>IC: Task await ingestBlocking
    IC->>Registry: await register(requestID)
    Registry-->>IC: AsyncStream
    IC->>IC: resolveAttentionTarget sync disk IO
    IC->>Main: await MainActor.run ingest and surfaceAttention
    Main-->>IC: itemID and attentionTarget
    IC->>Registry: await recordIngest
    IC->>IC: postNotificationIfStillAwaiting
    IC->>IC: withTaskGroup decisionStream or timeout
    IC->>Registry: await remove requestID
    IC->>Main: concludeAttention or markResolved
    IC-->>Bridge: IngestBlockingResult
    Bridge-->>SW: result unblocks worker
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant SW as Socket Worker
    participant Bridge as FeedBlockingCallBridge
    participant IC as ingestBlocking
    participant Registry as FeedBlockingWaiterRegistry
    participant Main as MainActor

    SW->>Bridge: wait(timeout)
    Bridge->>IC: Task await ingestBlocking
    IC->>Registry: await register(requestID)
    Registry-->>IC: AsyncStream
    IC->>IC: resolveAttentionTarget sync disk IO
    IC->>Main: await MainActor.run ingest and surfaceAttention
    Main-->>IC: itemID and attentionTarget
    IC->>Registry: await recordIngest
    IC->>IC: postNotificationIfStillAwaiting
    IC->>IC: withTaskGroup decisionStream or timeout
    IC->>Registry: await remove requestID
    IC->>Main: concludeAttention or markResolved
    IC-->>Bridge: IngestBlockingResult
    Bridge-->>SW: result unblocks worker
Loading

Reviews (8): Last reviewed commit: "Share Feed projections and cancel timed-..." | Re-trigger Greptile

Comment thread Sources/FeatureFlags.swift Outdated

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/FeatureFlags.swift`:
- Around line 174-177: Replace the positional lookup in the isFeedUIEnabled
accessor and the other feature-flag accessors using Self.allFlags indices with
stable key-based lookups or named flag definitions. Ensure each accessor always
resolves its intended flag regardless of declaration order or future insertions.

In `@Sources/Feed/FeedBlockingWaiterRegistry.swift`:
- Around line 53-56: Update isAwaitingDecision(requestID:) to return true only
when a waiter exists and its decision is nil; preserve false for missing,
removed, resolved, or timed-out request IDs.
- Around line 22-28: Model each waiter as a single-terminal state machine: in
Sources/Feed/FeedBlockingWaiterRegistry.swift lines 22-28, make register reject
duplicate request IDs without replacing active waiters; in lines 36-45, make
delivery accept only the first decision and return an explicit accepted/rejected
result. In Sources/Feed/FeedCoordinator.swift lines 161-177, honor a decision
that won before removal instead of expiring solely from DispatchTimeoutResult,
and in lines 199-213, resolve persisted state only when registry delivery is
accepted.

In `@Sources/Feed/FeedCoordinator.swift`:
- Around line 118-120: Update the socket event handling around
FeedCoordinator.shared.store.ingest to remove DispatchQueue.main.sync and keep
ingestion asynchronous. Store the resulting item ID and attention target in the
waiter state without blocking on the main actor, preserving waitTimeout behavior
and avoiding synchronous main-thread calls.

In `@Sources/Feed/FeedCoordinator`+Attention.swift:
- Around line 26-37: Update lifecycleStatusKey(forSource:) and its callers to
resolve the source through the registered agent descriptor and use only its
authoritative panel ID. Remove the fallback that returns the raw source or
infers identity from the focused panel; when no registered descriptor or panel
ID exists, skip the panel lifecycle mutation entirely.

In `@Sources/Feed/FeedCoordinator`+Notifications.swift:
- Around line 20-36: Remove the DEBUG-only FeedCoordinatorTestHooks usage from
the notification flow, including the isAppActive override and
notificationPostObserver short-circuit. Use the production NSApp.isActive
behavior and normal notification delivery directly; move test control or
observation to coordinator dependency injection or the test target without
adding test-named members under Sources/.

In `@Sources/Feed/FeedCoordinator`+Socket.swift:
- Around line 43-46: Move FeedJumpResolver target resolution in
Sources/Feed/FeedCoordinator+Socket.swift lines 43-46 off MainActor for both
focus and send actions, using an off-main actor or repository, and return to
MainActor only for UI event dispatch. In
Sources/Feed/FeedNotificationPolicyContext.swift lines 56-60, snapshot the exact
window/workspace context on MainActor, then perform hook loading and parsing
off-main before applying the policy result.
- Around line 9-20: Update snapshot(pendingOnly:) to use the receiver’s
actor-isolated state rather than FeedCoordinator.shared, and remove the
synchronous DispatchQueue.main.sync path. Change off-main/socket callers to
cross the actor asynchronously while preserving the pendingOnly selection and
snapshot results.

In `@Sources/Feed/FeedCoordinatorState.swift`:
- Around line 15-27: Remove the entire FeedCoordinatorTestHooks registry,
including all static callback properties and the surrounding `#if` DEBUG block,
from the production FeedCoordinatorState source. Update any production
references to these hooks to use injected dependencies or test-target
observation instead, without introducing replacement test-only globals under
Sources.

In `@Sources/Feed/FeedExitPlanView.swift`:
- Around line 368-370: Update the heading-detection condition in
FeedExitPlanView so colon-terminated headings do not require a space; allow
single-word headings such as “Summary:” while preserving the existing length and
word-count limits.

In `@Sources/Feed/FeedItemRow.swift`:
- Around line 195-200: Update relativeTimeChip(_:) to return localized catalog
entries instead of hardcoded "<1m" and interpolated minute/hour/day
abbreviations. Add matching localization keys for the under-minute case and
explicit .one and .other plural forms for minutes, hours, and days in every
supported locale, preserving the current interval thresholds and displayed
counts.

In `@Sources/Feed/FeedItemSnapshot.swift`:
- Around line 37-98: Carry the payload’s authoritative request ID in
FeedItemSnapshot and include it in the approvePermission, replyQuestion, and
approveExitPlan action inputs; update FeedRowActions.bound to pass that ID
directly to FeedCoordinator.shared.deliverReply. Remove requestId(for:) and the
itemId.uuidString fallback, returning without submitting when the ID is absent.
Replace the unowned fire-and-forget reply tasks with explicitly actor- and
lifecycle-owned execution.

In `@Sources/Feed/FeedJumpResolver.swift`:
- Around line 6-80: Replace the static-only FeedJumpResolver and its
feedRequestFocus/feedRequestSendText notifications with an injectable routing
service/protocol that owns parsing, session lookup, focus, and send-text
operations. Make the service constructable and inject it into the feed
coordinator, then invoke the existing typed action owner for coordinator actions
instead of posting global notifications. Preserve the current session-file
lookup and target parsing behavior while removing the ambient notification side
channel.

In `@Sources/Feed/FeedKeyboardFocus.swift`:
- Around line 184-190: Remove the charactersIgnoringModifiers read and its chars
interpolation from FeedKeyboardFocus.keyDown(with:). Keep DEBUG diagnostics
limited to the key code, modifier metadata, and existing responder summary,
without logging any typed character content.

In `@Sources/Feed/FeedListView.swift`:
- Around line 19-23: Move Feed filtering and grouping out of FeedListView.body
into immutable FeedItemSnapshot collections prepared by FeedPanelViewModel, so
rendering reuses precomputed data. In Sources/Feed/FeedListView.swift lines
19-23, consume the view-model snapshots instead of rescanning raw items; in
Sources/Feed/FeedExitPlanView.swift line 327, parse plan blocks and Markdown
once when creating each snapshot; in Sources/Feed/FeedPermissionView.swift lines
150-153, parse permission JSON once before row construction; and in
Sources/Feed/FeedTelemetryView.swift lines 151-156, group todos by state in one
pass and reuse that grouped result.
- Around line 406-408: Scope Feed focus to the owning host window: update
FeedListView.activeFeedWindow() to obtain the window through the bridge rather
than NSApp.keyWindow/mainWindow, and revise FeedQuestionView.activeEditor
handling so blur targets the current host window/coordinator instead of a
process-wide editor singleton. Apply the corresponding changes in
Sources/Feed/FeedListView.swift lines 406-408 and
Sources/Feed/FeedQuestionView.swift lines 514-539.

In `@Sources/Feed/FeedNotificationPolicyContext.swift`:
- Around line 19-23: The FeedNotificationPolicyContext workspace lookup must
fail closed instead of falling back to an arbitrary window’s cmuxConfigStore. In
the context-building flow around workspaceID and context, resolve policy only
from the exact event.workspaceId context; when it is missing or unresolved, skip
workspace-scoped hooks or use an explicitly app-global policy source. Also
update the session/workspace identity handling around the line-41 path to use
the authoritative structured workspace source rather than treating a session ID
as a workspace ID.

In `@Sources/Feed/FeedPanelViewModel.swift`:
- Around line 24-34: Move the initial recheck into the storeInstallTask closure:
after confirming coordinator is available and before entering the
notificationCenter.notifications loop, call self.arm() while safely unwrapping
the weak self. Keep the existing notification-triggered arm() behavior
unchanged.

In `@Sources/Feed/FeedPermissionView.swift`:
- Around line 224-241: Remove the DEBUG-only styling laboratory from
FeedPermissionView.swift and keep FeedButton’s production implementation limited
to plainFeedButton behavior. Move debugStyleGeneration, systemGlassButton,
usesSystemGlassButtonStyle, and all related alternate visual implementations
into a dedicated debug-only file or wrapper outside the production FeedButton
type, preserving the existing debug behavior and availability handling there.
- Around line 244-317: Update plainFeedButton to use SwiftUI’s real
disabled-button semantics whenever dimmed is true, while preserving
performAction’s guard. Extend the hover/cursor handling around handleHover so
changes to dimmed while the pointer remains over the button immediately
reconcile the cursor stack, popping any stale not-allowed cursor when re-enabled
or no longer hovered.

In `@Sources/Feed/FeedQuestionView.swift`:
- Around line 446-470: Remove the text-based fallback from isPlanAskUserQuestion
and delete questionTextLooksLikePlanInterview. Gate the “Skip + plan
immediately” behavior solely on context?.permissionMode being case-insensitively
equal to “plan”; return false when the context or permission mode is absent.
- Around line 794-810: Update FeedQuestionView.blurField() to call
focusRightSidebarInActiveMainWindow(...) synchronously on the current focus
turn, removing the deferred Task { `@MainActor` in ... } wrapper. Preserve the
existing arguments and immediately fall back to window.makeFirstResponder(nil)
when the coordinator does not return true.

In `@Sources/Feed/FeedSocketEncoding.swift`:
- Around line 50-61: Update assignLimitedText to use the existing
primaryTextLimit constant as its default limit instead of the duplicated 8,000
literal, keeping the truncation and dictionary assignment behavior unchanged.
- Around line 85-86: Update FeedSocketEncoding.itemDict to reuse a cached or
shared ISO8601DateFormatter instead of instantiating one on each call. Keep the
existing date-encoding behavior unchanged while ensuring formatter creation
occurs outside the per-item mapping path.
- Around line 111-121: Update the feed encoding logic around the
permissionRequest case and its corresponding tool-result encoding to redact
sensitive payloads before assigning tool_input or tool_result. Replace raw JSON
truncation via assignLimitedText with the existing redaction mechanism, while
preserving the narrow tool_input_capabilities projection and existing field
names.

In `@Sources/Feed/FeedTelemetryView.swift`:
- Around line 177-180: Update the count-bearing localized strings in
FeedTelemetryView, including the feed.todos.moreCompleted text and the related
strings around the referenced section, to select explicit `.one` and `.other`
localization keys based on the count. Ensure singular values such as 1
completed, 1 done, 1 in progress, and 1 open use `.one`, while all other counts
use `.other`, preserving the existing displayed counts and fallback text.
🪄 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: 650da199-1292-4c68-99ec-d223b81aeacb

📥 Commits

Reviewing files that changed from the base of the PR and between d06bf35 and 8b5baf0.

📒 Files selected for processing (50)
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/BetaFeaturesCatalogSection.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BetaFeaturesSection.swift
  • Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Values/NewWorkspaceInitialSurface.swift
  • Resources/Localizable.xcstrings
  • Sources/App/WorkspaceRuntimeSettings.swift
  • Sources/AppDelegate+NewWorkspaceContextMenu.swift
  • Sources/AppDelegate+NewWorkspaceMenuRendering.swift
  • Sources/AppDelegate.swift
  • Sources/CommandPalette/CommandPaletteSettingsToggle.swift
  • Sources/ContentView+RightSidebarCommandPalette.swift
  • Sources/ContentView.swift
  • Sources/FeatureFlags.swift
  • Sources/Feed/FeedBlockingWaiterRegistry.swift
  • Sources/Feed/FeedCoordinator+Attention.swift
  • Sources/Feed/FeedCoordinator+Notifications.swift
  • Sources/Feed/FeedCoordinator+Socket.swift
  • Sources/Feed/FeedCoordinator.swift
  • Sources/Feed/FeedCoordinatorState.swift
  • Sources/Feed/FeedExitPlanView.swift
  • Sources/Feed/FeedItemRow.swift
  • Sources/Feed/FeedItemSnapshot.swift
  • Sources/Feed/FeedJumpResolver.swift
  • Sources/Feed/FeedKeyboardFocus.swift
  • Sources/Feed/FeedListView.swift
  • Sources/Feed/FeedNotificationPolicyContext.swift
  • Sources/Feed/FeedOpeningCoordinator.swift
  • Sources/Feed/FeedPanelView.swift
  • Sources/Feed/FeedPanelViewModel.swift
  • Sources/Feed/FeedPermissionView.swift
  • Sources/Feed/FeedQuestionView.swift
  • Sources/Feed/FeedSocketEncoding.swift
  • Sources/Feed/FeedTelemetryView.swift
  • Sources/FileExplorerState.swift
  • Sources/NewWorkspaceMenuModel.swift
  • Sources/RightSidebarMode+Availability.swift
  • Sources/RightSidebarModeShortcutMatcher.swift
  • Sources/RightSidebarPanelView.swift
  • Sources/RightSidebarToolPanel.swift
  • Sources/SettingsNavigation.swift
  • Sources/SettingsSearchAliases.swift
  • Sources/TabManager.swift
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/FeedCoordinatorTests.swift
  • cmuxTests/FileExplorerStateModePersistenceTests.swift
  • cmuxTests/NewWorkspaceMenuModelTests.swift
  • cmuxTests/PostHogAnalyticsPropertiesTests.swift
  • cmuxTests/RightSidebarCommandPaletteTests.swift
💤 Files with no reviewable changes (8)
  • Sources/SettingsSearchAliases.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/BetaFeaturesCatalogSection.swift
  • Sources/SettingsNavigation.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BetaFeaturesSection.swift
  • Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift
  • Sources/App/WorkspaceRuntimeSettings.swift
  • Sources/CommandPalette/CommandPaletteSettingsToggle.swift

Comment thread Sources/FeatureFlags.swift Outdated
Comment thread Sources/Feed/FeedBlockingWaiterRegistry.swift Outdated
Comment thread Sources/Feed/FeedBlockingWaiterRegistry.swift Outdated
Comment thread Sources/Feed/FeedCoordinator.swift Outdated
Comment thread Sources/Feed/FeedCoordinator+Attention.swift
Comment thread Sources/Feed/FeedQuestionView.swift Outdated
Comment thread Sources/Feed/FeedSocketEncoding.swift Outdated
Comment thread Sources/Feed/FeedSocketEncoding.swift Outdated
Comment thread Sources/Feed/FeedSocketEncoding.swift
Comment thread Sources/Feed/FeedTelemetryView.swift Outdated
Comment thread Sources/Feed/FeedCoordinator+Socket.swift Outdated
Comment thread Sources/Feed/FeedBlockingCallBridge.swift Outdated

@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 (2)
cmuxTests/FeedCoordinatorTests.swift (1)

509-520: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Await task results and an ingest completion signal.

These tests block on semaphores and use a fixed 500-iteration Task.yield() poll, while sharing the result through @unchecked Sendable. Keep the ingestion Task handle, await its value, and signal store ingestion deterministically; use the injected virtual clock for timeout behavior.

As per coding guidelines, tests must await real completion signals or virtual clocks rather than fixed waits and scheduler-dependent polling.

Also applies to: 554-583, 652-654

🤖 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/FeedCoordinatorTests.swift` around lines 509 - 520, Update the
ingestion tests around the Task-based ingestBlocking calls to retain and await
the Task handle’s result instead of using DispatchSemaphore, IngestResultBox, or
fixed Task.yield polling. Add and await the store-ingestion completion signal,
and drive timeout behavior through the injected virtual clock so completion and
timing remain deterministic; apply the same pattern to the referenced test
sections.

Source: Coding guidelines

Sources/Feed/FeedItemRow.swift (1)

267-276: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the payload’s authoritative request ID.

Each payload case already provides a non-optional request ID. Reading the duplicated optional snapshot.requestID makes inconsistent state representable and silently disables—or misroutes—the action. Bind the associated ID directly in each switch case.

Proposed fix
-        case .permissionRequest(_, let toolName, let toolInputJSON, _):
+        case .permissionRequest(let requestID, let toolName, let toolInputJSON, _):
...
                 onApprove: { mode in
-                    guard let requestID = snapshot.requestID else { return }
                     actions.approvePermission(requestID, mode)
                 }

-        case .exitPlan(_, let plan, _):
+        case .exitPlan(let requestID, let plan, _):
...
                 onApprove: { mode, feedback in
-                    guard let requestID = snapshot.requestID else { return }
                     actions.approveExitPlan(requestID, mode, feedback)
                 }

-        case .question(_, let questions):
+        case .question(let requestID, let questions):
...
                 onReply: { selections in
-                    guard let requestID = snapshot.requestID else { return }
                     actions.replyQuestion(requestID, selections)
                 }

As per path instructions, correctness-critical routing must use one reliable structured source and fail closed rather than relying on conflicting derived values.

Also applies to: 279-290, 293-307

🤖 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/Feed/FeedItemRow.swift` around lines 267 - 276, Use the authoritative
non-optional request ID associated with each permission payload case in the
FeedItemRow switch. Bind the ID directly in the .permissionRequest case and the
other affected cases at the referenced action closures, then pass that bound ID
to the approve, deny, or corresponding action methods instead of reading
snapshot.requestID; preserve the existing fail-closed behavior only where the
payload itself cannot provide an ID.

Sources: Coding guidelines, Path instructions

🤖 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 `@Resources/Localizable.xcstrings`:
- Around line 103499-103505: Update the Japanese values for the “Plan ready” and
“Permission needed” localization entries in Localizable.xcstrings to preserve
their English meanings: use wording equivalent to “プランの準備完了” for the former and
“権限が必要です” for the latter.

In `@Sources/Feed/FeedActivitySnapshotGroups.swift`:
- Around line 2-6: Store the combined ordered snapshots during
FeedActivitySnapshotGroups initialization instead of computing them through the
ordered property on every access. In
Sources/Feed/FeedActivitySnapshotGroups.swift lines 2-6, add stored
ordered-array state; in Sources/Feed/FeedListView.swift lines 25-29, consume it
once and reuse snapshots for focus navigation; in
Sources/Feed/FeedListView.swift lines 125-162, iterate identifiable snapshots
directly without Array(enumerated()) materialization.

In `@Sources/Feed/FeedPanelViewModel.swift`:
- Around line 25-34: Update the storeInstallTask notification loop in
FeedPanelViewModel so it does not strongly capture self for the lifetime of the
asynchronous sequence. Avoid the outer guard let self; capture the view model
weakly and unwrap it only for each arm() call, allowing deinit to cancel and
release the task when the panel is dismissed.

---

Outside diff comments:
In `@cmuxTests/FeedCoordinatorTests.swift`:
- Around line 509-520: Update the ingestion tests around the Task-based
ingestBlocking calls to retain and await the Task handle’s result instead of
using DispatchSemaphore, IngestResultBox, or fixed Task.yield polling. Add and
await the store-ingestion completion signal, and drive timeout behavior through
the injected virtual clock so completion and timing remain deterministic; apply
the same pattern to the referenced test sections.

In `@Sources/Feed/FeedItemRow.swift`:
- Around line 267-276: Use the authoritative non-optional request ID associated
with each permission payload case in the FeedItemRow switch. Bind the ID
directly in the .permissionRequest case and the other affected cases at the
referenced action closures, then pass that bound ID to the approve, deny, or
corresponding action methods instead of reading snapshot.requestID; preserve the
existing fail-closed behavior only where the payload itself cannot provide an
ID.
🪄 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: 0eaf68c5-69ca-426e-b525-afbeb311f684

📥 Commits

Reviewing files that changed from the base of the PR and between 6a0dd52 and e28efdd.

📒 Files selected for processing (46)
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamPersistence.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/BetaFeaturesCatalogSection.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BetaFeaturesSection.swift
  • Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift
  • Resources/Localizable.xcstrings
  • Sources/App/WorkspaceRuntimeSettings.swift
  • Sources/AppDelegate+NotificationDeliverySeams.swift
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/FeatureFlags.swift
  • Sources/Feed/FeedActivitySnapshotGroups.swift
  • Sources/Feed/FeedBlockingCallBridge.swift
  • Sources/Feed/FeedBlockingWaiterRegistry.swift
  • Sources/Feed/FeedCoordinator+Attention.swift
  • Sources/Feed/FeedCoordinator+Notifications.swift
  • Sources/Feed/FeedCoordinator+Socket.swift
  • Sources/Feed/FeedCoordinator.swift
  • Sources/Feed/FeedCoordinatorState.swift
  • Sources/Feed/FeedExitPlanView.swift
  • Sources/Feed/FeedItemRow.swift
  • Sources/Feed/FeedItemSnapshot.swift
  • Sources/Feed/FeedJumpResolver.swift
  • Sources/Feed/FeedKeyboardFocus.swift
  • Sources/Feed/FeedListView.swift
  • Sources/Feed/FeedNotificationPolicyContext.swift
  • Sources/Feed/FeedNotificationPolicySnapshot.swift
  • Sources/Feed/FeedPanelView.swift
  • Sources/Feed/FeedPanelViewModel.swift
  • Sources/Feed/FeedPermissionView.swift
  • Sources/Feed/FeedPresentationSnapshot.swift
  • Sources/Feed/FeedQuestionView.swift
  • Sources/Feed/FeedSocketEncoding.swift
  • Sources/Feed/FeedTelemetryView.swift
  • Sources/RightSidebarToolPanel.swift
  • Sources/SettingsNavigation.swift
  • Sources/SettingsSearchAliases.swift
  • Sources/TabManager.swift
  • Sources/TerminalController+ControlFeedContext.swift
  • Sources/TerminalController.swift
  • Sources/TerminalNotificationPolicy.swift
  • Sources/TerminalWindowPortal.swift
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/FeedCoordinatorTests.swift
  • cmuxTests/PostHogAnalyticsPropertiesTests.swift
💤 Files with no reviewable changes (2)
  • Sources/Feed/FeedCoordinatorState.swift
  • Sources/App/WorkspaceRuntimeSettings.swift

Comment thread Resources/Localizable.xcstrings Outdated
Comment thread Sources/Feed/FeedActivitySnapshotGroups.swift Outdated
Comment thread Sources/Feed/FeedPanelViewModel.swift Outdated

@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 (3)
Sources/Feed/FeedCoordinator+Notifications.swift (2)

191-253: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use await center.notificationSettings() instead of the callback pyramid.

Using the async version of notificationSettings flattens the closure pyramid and avoids the nested Task { @mainactor in } context, making the asynchronous control flow much cleaner.

As per path instructions, flag new or materially expanded use of legacy asynchronous patterns (like callback pyramids) when async functions are appropriate.

♻️ Proposed refactor
         let request = UNNotificationRequest(
             identifier: "feed.\(requestId)",
             content: content,
             trigger: nil
         )
 
         let center = UNUserNotificationCenter.current()
-        center.getNotificationSettings { settings in
-            Task { `@MainActor` [weak self] in
-                guard let self, await self.isAwaitingDecision(requestId: requestId) else { return }
-                switch settings.authorizationStatus {
-                case .authorized, .provisional:
-                    await self.addNotificationIfStillAwaiting(
-                        center: center,
-                        request: request,
-                        requestId: requestId,
-                        effects: effects
-                    )
-                case .notDetermined:
-                    var granted = false
-                    var requestFailed = false
-                    do {
-                        granted = try await center.requestAuthorization(options: [.alert, .sound])
-                    } catch {
-                        requestFailed = true
-                    }
-                    guard await self.isAwaitingDecision(requestId: requestId) else { return }
-                    if granted {
-                        await self.addNotificationIfStillAwaiting(
-                            center: center,
-                            request: request,
-                            requestId: requestId,
-                            effects: effects
-                        )
-                    } else {
-                        // A non-grant without an error is the user declining
-                        // the prompt just now: honor the fresh denial on this
-                        // very notification. A request error is not a user
-                        // decision, so the fallback stays audible (fail-open).
-                        await self.runFallbackEffectsIfStillAwaiting(
-                            requestId: requestId,
-                            title: title,
-                            subtitle: subtitle,
-                            body: body,
-                            effects: TerminalNotificationStore.fallbackEffects(
-                                effects,
-                                authorizationState: requestFailed ? .unknown : .denied
-                            ),
-                            runCommand: false
-                        )
-                    }
-                default:
-                    await self.runFallbackEffectsIfStillAwaiting(
-                        requestId: requestId,
-                        title: title,
-                        subtitle: subtitle,
-                        body: body,
-                        effects: TerminalNotificationStore.fallbackEffects(
-                            effects,
-                            authorizationState: TerminalNotificationStore.authorizationState(
-                                from: settings.authorizationStatus
-                            )
-                        ),
-                        runCommand: false
-                    )
-                }
-            }
-        }
+        let settings = await center.notificationSettings()
+        guard await isAwaitingDecision(requestId: requestId) else { return }
+        
+        switch settings.authorizationStatus {
+        case .authorized, .provisional:
+            await addNotificationIfStillAwaiting(
+                center: center,
+                request: request,
+                requestId: requestId,
+                effects: effects
+            )
+        case .notDetermined:
+            var granted = false
+            var requestFailed = false
+            do {
+                granted = try await center.requestAuthorization(options: [.alert, .sound])
+            } catch {
+                requestFailed = true
+            }
+            guard await isAwaitingDecision(requestId: requestId) else { return }
+            if granted {
+                await addNotificationIfStillAwaiting(
+                    center: center,
+                    request: request,
+                    requestId: requestId,
+                    effects: effects
+                )
+            } else {
+                await runFallbackEffectsIfStillAwaiting(
+                    requestId: requestId,
+                    title: title,
+                    subtitle: subtitle,
+                    body: body,
+                    effects: TerminalNotificationStore.fallbackEffects(
+                        effects,
+                        authorizationState: requestFailed ? .unknown : .denied
+                    ),
+                    runCommand: false
+                )
+            }
+        default:
+            await runFallbackEffectsIfStillAwaiting(
+                requestId: requestId,
+                title: title,
+                subtitle: subtitle,
+                body: body,
+                effects: TerminalNotificationStore.fallbackEffects(
+                    effects,
+                    authorizationState: TerminalNotificationStore.authorizationState(
+                        from: settings.authorizationStatus
+                    )
+                ),
+                runCommand: false
+            )
+        }
     }
🤖 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/Feed/FeedCoordinator`+Notifications.swift around lines 191 - 253,
Refactor the notification-settings flow around UNUserNotificationCenter so the
enclosing async method awaits center.notificationSettings() directly instead of
using getNotificationSettings with a nested Task. Preserve the existing
MainActor isolation, awaiting-decision checks, authorization handling, and
fallback behavior while flattening the callback structure.

Source: Path instructions


266-293: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use try await center.add(request) instead of the completion callback.

Using the async version of add(_:) flattens the closure pyramid and avoids creating a detached @MainActor Task.

As per path instructions, prefer modern Swift concurrency over legacy callback pyramids.

♻️ Proposed refactor
-        center.add(request) { error in
-            let didFail = error != nil
-            Task { `@MainActor` [weak self] in
-                guard let self else { return }
-                if !(await self.isAwaitingDecision(requestId: requestId)) {
-                    self.cancelNotification(requestId: requestId)
-                    return
-                }
-                if didFail {
-                    await self.runFallbackEffectsIfStillAwaiting(
-                        requestId: requestId,
-                        title: title,
-                        subtitle: subtitle,
-                        body: body,
-                        effects: effects,
-                        runCommand: false
-                    )
-                    return
-                }
-                if effects.command {
-                    NotificationSoundSettings.runCustomCommand(
-                        title: title,
-                        subtitle: subtitle,
-                        body: body
-                    )
-                }
-            }
-        }
+        do {
+            try await center.add(request)
+            guard await isAwaitingDecision(requestId: requestId) else {
+                cancelNotification(requestId: requestId)
+                return
+            }
+            if effects.command {
+                NotificationSoundSettings.runCustomCommand(
+                    title: title,
+                    subtitle: subtitle,
+                    body: body
+                )
+            }
+        } catch {
+            guard await isAwaitingDecision(requestId: requestId) else {
+                cancelNotification(requestId: requestId)
+                return
+            }
+            await runFallbackEffectsIfStillAwaiting(
+                requestId: requestId,
+                title: title,
+                subtitle: subtitle,
+                body: body,
+                effects: effects,
+                runCommand: false
+            )
+        }
🤖 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/Feed/FeedCoordinator`+Notifications.swift around lines 266 - 293,
Refactor the notification scheduling flow around center.add(request) to use its
async throwing form with try await instead of a completion callback and nested
`@MainActor` Task. Preserve the existing awaiting-decision check, failure fallback
via runFallbackEffectsIfStillAwaiting, and successful effects.command handling,
while propagating or handling the add error through the existing didFail
behavior.

Source: Path instructions

Sources/Feed/FeedCoordinator.swift (1)

202-216: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the Thread.isMainThread conditional dispatch.

Using Thread.isMainThread to conditionally dispatch work is an anti-pattern in modern Swift concurrency and fragments MainActor lifecycle ownership. Since both callers (ingestBlocking and deliverReply) are async, you can either use an unconditional Task { @mainactor in } for fire-and-forget, or better, fold the conclusion directly into the existing await MainActor.run blocks in the callers.

As per path instructions, do not patch symptoms with delayed dispatch or split UI lifecycle ownership, and use one explicit MainActor owner.

♻️ Proposed refactor (folding into existing `MainActor.run` blocks)
-  /// Concludes an attention overlay (if any) on the main actor, hopping if
-  /// called from the socket worker thread.
-  private func concludeAttentionOnMain(_ target: AttentionTarget?) {
-    guard let target else { return }
-    let conclude: `@Sendable` () -> Void = { [target] in
-      MainActor.assumeIsolated {
-        FeedCoordinator.shared.concludeBlockingDecisionAttention(target)
-      }
-    }
-    if Thread.isMainThread {
-      conclude()
-    } else {
-      Task { `@MainActor` in conclude() }
-    }
-  }
-
   `@MainActor`
   private func concludeBlockingDecisionAttentionIfPresent(_ target: AttentionTarget?) {

Then update the call sites. In ingestBlocking (around line 197):

    cancelNotification(requestId: requestId)
-   concludeAttentionOnMain(waiter?.attentionTarget)
-   expireTimedOutItem(waiter?.itemID)
+   Task { `@MainActor` [weak self] in
+     self?.concludeBlockingDecisionAttentionIfPresent(waiter?.attentionTarget)
+     if let itemId = waiter?.itemID {
+       self?.store?.markExpired(itemId)
+     }
+   }

In deliverReply (around line 239):

-   concludeAttentionOnMain(delivery.attentionTarget)
-
    await MainActor.run {
+     concludeBlockingDecisionAttentionIfPresent(delivery.attentionTarget)
      guard let store, let itemID = delivery.itemID else { 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/Feed/FeedCoordinator.swift` around lines 202 - 216, Remove the
Thread.isMainThread-based dispatch from concludeAttentionOnMain and use a single
explicit MainActor owner. Update ingestBlocking and deliverReply to conclude the
attention within their existing MainActor execution, reusing the appropriate
concludeBlockingDecisionAttentionIfPresent helper and preserving item expiration
in the ingestBlocking path.

Source: Path instructions

🤖 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/Feed/FeedCoordinator.swift`:
- Around line 202-216: Remove the Thread.isMainThread-based dispatch from
concludeAttentionOnMain and use a single explicit MainActor owner. Update
ingestBlocking and deliverReply to conclude the attention within their existing
MainActor execution, reusing the appropriate
concludeBlockingDecisionAttentionIfPresent helper and preserving item expiration
in the ingestBlocking path.

In `@Sources/Feed/FeedCoordinator`+Notifications.swift:
- Around line 191-253: Refactor the notification-settings flow around
UNUserNotificationCenter so the enclosing async method awaits
center.notificationSettings() directly instead of using getNotificationSettings
with a nested Task. Preserve the existing MainActor isolation, awaiting-decision
checks, authorization handling, and fallback behavior while flattening the
callback structure.
- Around line 266-293: Refactor the notification scheduling flow around
center.add(request) to use its async throwing form with try await instead of a
completion callback and nested `@MainActor` Task. Preserve the existing
awaiting-decision check, failure fallback via runFallbackEffectsIfStillAwaiting,
and successful effects.command handling, while propagating or handling the add
error through the existing didFail behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: abc7005e-98c4-4eae-961e-c7b53adac33b

📥 Commits

Reviewing files that changed from the base of the PR and between e28efdd and 2de82a6.

📒 Files selected for processing (6)
  • Sources/Feed/FeedCoordinator+Notifications.swift
  • Sources/Feed/FeedCoordinator.swift
  • Sources/Feed/FeedNotificationPolicyContext.swift
  • Sources/Feed/FeedNotificationPolicySnapshot.swift
  • Sources/Feed/FeedPanelViewModel.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSlotViews.swift

@greptile-apps

greptile-apps Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Too many files changed for review. (114 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

@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 (4)
Sources/Feed/FeedPlanBodyView.swift (4)

1-6: ⚠️ Potential issue | 🔴 Critical

LGTM!

Source: Coding guidelines


5628-5644: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Keep Feed coordination asynchronous instead of blocking the socket worker.

Both paths use FeedBlockingCallBridge.wait to synchronously block the socket worker thread for up to 10–130 seconds while waiting for async actor work. Since socket workers handle requests sequentially on a limited pool of worker threads, this stalls unrelated incoming socket commands for minutes at a time. This was raised in a previous review and remains unaddressed.

As per coding guidelines, do not introduce thread-blocking waits for async work.

  • Sources/TerminalController.swift#L5628-L5644: Update v2FeedPush (via socketWorkerV2Response) to use the existing v2AsyncResultCall pattern so the socket worker can return immediately and process other commands while waiting for ingestBlocking.
  • Sources/TerminalController.swift#L5764-L5779: Update v2DeliverFeedReply (and its callers like v2FeedPermissionReply) to use the v2AsyncResultCall pattern rather than blocking for up to 10 seconds.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/TerminalController.swift` around lines 5628 - 5644, The Feed response
paths must remain asynchronous instead of blocking socket workers. In
Sources/TerminalController.swift:5628-5644, update v2FeedPush via
socketWorkerV2Response to use the existing v2AsyncResultCall pattern around
ingestBlocking, preserving completion publication and response encoding. In
Sources/TerminalController.swift:5764-5779, update v2DeliverFeedReply and
callers such as v2FeedPermissionReply to use v2AsyncResultCall and remove
FeedBlockingCallBridge.wait-based blocking.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Feed/FeedRowActions.swift`:
- Around line 14-31: Update TaskStore’s task collection to use explicit NSLock
synchronization instead of relying on `@MainActor` isolation, including locking
all reads, insertions, removals, and cancellation during deinit. Ensure deinit
safely snapshots or clears the protected tasks dictionary while holding the
lock, then cancels the tasks without accessing isolated state.

---

Outside diff comments:
In `@Sources/Feed/FeedPlanBodyView.swift`:
- Around line 1-6: Keep Feed coordination asynchronous in the socket request
routing. Update the feed.push path around socketWorkerV2Response and v2FeedPush
to use the existing v2AsyncResultCall pattern, removing FeedBlockingCallBridge,
ingestTask, and bridge.wait. Apply the same asynchronous response flow to feed
permission, question, and exit-plan replies by replacing the synchronous
v2DeliverFeedReply wait while preserving their validation and response payloads.
- Around line 5628-5658: Update the feed.* route handlers in
socketWorkerV2Response, including the additional handlers near the
FeedBlockingCallBridge usage, to use the existing v2AsyncResultCall pattern like
mobile.attach_ticket.create. Remove synchronous FeedBlockingCallBridge.wait
calls while preserving the current FeedCoordinator task results and error
responses.
- Around line 5628-5644: Update the feed.* route handlers in FeedPlanBodyView to
use the existing v2AsyncResultCall pattern, matching
mobile.attach_ticket.create, instead of FeedBlockingCallBridge.wait. Return the
asynchronous result immediately while FeedCoordinator tasks complete, and remove
the synchronous wait path from the affected handlers, including the additional
handler range noted in the review.
- Around line 5628-5644: Update the feed.* route handlers in
socketWorkerV2Response, including v2FeedPush and v2FeedPermissionReply, to use
the existing v2AsyncResultCall pattern instead of FeedBlockingCallBridge.wait.
Return from the socket worker immediately while FeedCoordinator tasks complete
asynchronously, matching the mobile.attach_ticket.create flow and preserving
each handler’s existing success and error responses.

---

Duplicate comments:
In `@Sources/TerminalController.swift`:
- Around line 5628-5658: Replace the blocking Feed handlers with async variants:
in Sources/TerminalController.swift lines 5628-5658, update v2FeedPush and its
socketWorkerV2Response route to use v2AsyncResultCall, await
FeedCoordinator.shared.ingestBlocking, and remove FeedBlockingCallBridge and
synchronous waiting; in lines 5764-5779, similarly update v2DeliverFeedReply and
its corresponding reply routes to use v2AsyncResultCall and return the awaited
result without blocking the socket worker.
- Around line 5628-5644: The Feed response paths must remain asynchronous
instead of blocking socket workers. In
Sources/TerminalController.swift:5628-5644, update v2FeedPush via
socketWorkerV2Response to use the existing v2AsyncResultCall pattern around
ingestBlocking, preserving completion publication and response encoding. In
Sources/TerminalController.swift:5764-5779, update v2DeliverFeedReply and
callers such as v2FeedPermissionReply to use v2AsyncResultCall and remove
FeedBlockingCallBridge.wait-based blocking.
🪄 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: e229ff99-78ca-4926-918d-48caefbc3f21

📥 Commits

Reviewing files that changed from the base of the PR and between 028f4cf and 50a1c2b.

📒 Files selected for processing (18)
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandExecutionPolicyTests.swift
  • Sources/AppDelegate.swift
  • Sources/Feed/FeedBlockingWaiterRegistry.swift
  • Sources/Feed/FeedCoordinator+Notifications.swift
  • Sources/Feed/FeedCoordinator.swift
  • Sources/Feed/FeedInlineNativeTextView.swift
  • Sources/Feed/FeedJumpResolver.swift
  • Sources/Feed/FeedListView.swift
  • Sources/Feed/FeedNotificationPolicyContext.swift
  • Sources/Feed/FeedPanelView.swift
  • Sources/Feed/FeedPanelViewModel.swift
  • Sources/Feed/FeedPlanBodyView.swift
  • Sources/Feed/FeedPresentationStore.swift
  • Sources/Feed/FeedRowActions.swift
  • Sources/TerminalController.swift
  • cmuxTests/FeedCoordinatorTests.swift
  • cmuxTests/ShortcutAndCommandPaletteTests.swift

Comment thread Sources/Feed/FeedRowActions.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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
Sources/Feed/FeedRowActions.swift (1)

15-32: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Fix strict concurrency violation in TaskStore.deinit.

Because deinit is non-isolated and runs on whatever thread drops the final reference, it cannot safely read or modify the implicit @MainActor-isolated tasks dictionary. In Swift 6, this is a compiler error and can trap at runtime.

As per coding guidelines, "Avoid Swift 6 actor-isolation mistakes such as... shared mutable Sendable references without isolation". Protect the task collection manually with a lock so it can be safely cleared upon deallocation.

🔒️ Proposed fix using NSLock
-    `@MainActor`
-    final class TaskStore {
+    final class TaskStore: `@unchecked` Sendable {
         private var tasks: [UUID: Task<Void, Never>] = [:]
+        private let lock = NSLock()
 
         deinit {
-            for task in tasks.values {
+            lock.lock()
+            let currentTasks = tasks.values
+            lock.unlock()
+            for task in currentTasks {
                 task.cancel()
             }
         }
 
         func run(_ operation: `@escaping` `@MainActor` () async -> Void) {
             let id = UUID()
-            tasks[id] = Task { [weak self] in
+            let task = Task { [weak self] in
                 await operation()
-                self?.tasks.removeValue(forKey: id)
+                self?.remove(id)
             }
+            lock.lock()
+            tasks[id] = task
+            lock.unlock()
+        }
+        
+        private func remove(_ id: UUID) {
+            lock.lock()
+            tasks.removeValue(forKey: id)
+            lock.unlock()
         }
     }
🤖 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/Feed/FeedRowActions.swift` around lines 15 - 32, Update TaskStore’s
task-collection management to use an NSLock, protecting all reads, insertions,
removals, and deinit cancellation/clearing of tasks from nonisolated access.
Ensure deinit acquires the lock before extracting and clearing the collection,
then cancels the extracted tasks outside the lock, while preserving run’s task
lifecycle cleanup.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 9331-9338: Update the selection flow surrounding
invokeFeedSocketCommand so the workspaceId/surfaceId pair is validated through
the authoritative typed surface mapping before issuing any mutating command.
Fail closed when the mapping is missing or mismatched, then perform
workspace.select and surface.focus atomically or through a shared validated
operation so a rejected surface cannot leave the workspace changed.

---

Duplicate comments:
In `@Sources/Feed/FeedRowActions.swift`:
- Around line 15-32: Update TaskStore’s task-collection management to use an
NSLock, protecting all reads, insertions, removals, and deinit
cancellation/clearing of tasks from nonisolated access. Ensure deinit acquires
the lock before extracting and clearing the collection, then cancels the
extracted tasks outside the lock, while preserving run’s task lifecycle cleanup.
🪄 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: 9b5dfde3-9ec5-4d74-b292-a63fa70f2a5f

📥 Commits

Reviewing files that changed from the base of the PR and between 50a1c2b and 3b1869c.

📒 Files selected for processing (11)
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/Feed/FeedBlockingWaiterCompletion.swift
  • Sources/Feed/FeedBlockingWaiterRegistry.swift
  • Sources/Feed/FeedItemRow.swift
  • Sources/Feed/FeedPreviewWindowController.swift
  • Sources/Feed/FeedRowActions.swift
  • Sources/Feed/FeedStopDraft.swift
  • Sources/Feed/FeedTelemetryView.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/FeedFocusRoutingTests.swift

Comment thread Sources/AppDelegate.swift Outdated
@cursor

cursor Bot commented Jul 18, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

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

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

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamItem.swift`:
- Line 89: Remove the feedPrefix(512) truncation from WorkstreamItem identity
fields and preserve the original protocol identifiers consistently across
session, request, question, option, todo, and workstream references. Ensure
context-cache insertion, lookup, and removal use the same raw authoritative
identifiers, and reject over-limit events at the transport boundary rather than
rewriting IDs.
- Around line 204-208: Update String.feedPrefix(_:) to enforce the limit using
UTF-8 byte capacity rather than String.count, avoiding a full-input scan and
safely handling grapheme boundaries. Construct only the bounded UTF-8 prefix,
ensuring the returned string never exceeds maximumCharacters bytes while
preserving the original string when already within the limit.
- Around line 123-128: Redact permission JSON before truncating it, so
over-limit payloads remain valid for JSON-aware secret redaction; update the
permissionRequest handling in WorkstreamItem.swift at lines 123-128 accordingly.
Apply the same ordering to tool-use and tool-result JSON at WorkstreamItem.swift
lines 155-164. In WorkstreamPersistence.swift lines 95-98, redact the original
JSON first and then bound the safe result, and add a regression test covering an
over-limit payload containing a secret.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamPersistence.swift`:
- Around line 147-152: The loadLegacyRemovedItemIDs migration must fail closed
when the legacy tombstone file exceeds maximumLegacyReadBytes: do not return an
empty Set, which permits deleted rows to be restored. Propagate an
unavailable/failed migration outcome through the caller so restoration is
skipped while preserving deletion semantics; keep normal bounded-file loading
unchanged.
- Around line 111-125: Update the snapshot persistence flow around the
selectedLines handling so the new snapshot write, or empty-snapshot file
deletion, completes successfully before removing removedItemsFileURL. Do not
suppress errors from deleting the old snapshot in the empty branch, and preserve
tombstones whenever directory creation, writing, or deletion fails.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift`:
- Around line 69-76: Update start() so persistence.replacePendingItems is called
only after loadPendingItems succeeds; when restoration fails, preserve the
existing persisted snapshot by skipping compaction, or propagate the load error
instead. Keep the restored-item mapping and rebuildContextIndex behavior
unchanged for successful loads.
- Around line 181-191: Update schedulePendingSnapshot so the asynchronous
replacePendingItems operation is owned by a stored, cancellable writer task or
awaited through a caller-owned persistence path. Coalesce successive snapshots
while preserving the latest persistenceGeneration and pendingItems, and handle
replacement failures rather than discarding them, ensuring interrupted or failed
writes cannot leave resolved or expired cards persisted as pending.
- Around line 120-127: The removeItem(id:) implementation should not call
rebuildContextIndex() after deleting a card, because that discards
telemetry-only context absent from items. Preserve the existing context cache or
update only the removed item’s affected workstream, while leaving persistence
generation and replacement behavior unchanged.

In
`@Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/Workstream/WorkstreamStoreTests.swift`:
- Around line 96-98: Update the restart setup in WorkstreamStoreTests by
constructing a new WorkstreamPersistence with the same temporary file URL used
by the original persistence, then pass that fresh actor to WorkstreamStore. Keep
the existing ringCapacity, start call, and restored item assertion unchanged.
🪄 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: e284b80b-b150-4390-ab58-c0a6bf65539d

📥 Commits

Reviewing files that changed from the base of the PR and between e14b8ab and 898c251.

📒 Files selected for processing (6)
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamItem.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamKind.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamPersistence.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/Workstream/WorkstreamPersistenceTests.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/Workstream/WorkstreamStoreTests.swift

func retainedForFeed() -> WorkstreamItem {
WorkstreamItem(
id: id,
workstreamId: workstreamId.feedPrefix(512),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not truncate protocol identifiers.

This rewrites session, request, question, option, and todo IDs, creating collisions and breaking exact reply routing. It also mismatches the context cache: lookup/removal uses raw event.sessionId, while insertion uses the truncated workstreamId.

Preserve identifiers exactly, or reject over-limit events at the transport boundary.

As per path instructions, correctness-critical identity must use one authoritative structured source and fail closed rather than be rewritten.

Also applies to: 125-125, 132-132, 138-148, 175-175

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamItem.swift`
at line 89, Remove the feedPrefix(512) truncation from WorkstreamItem identity
fields and preserve the original protocol identifiers consistently across
session, request, question, option, todo, and workstream references. Ensure
context-cache insertion, lookup, and removal use the same raw authoritative
identifiers, and reject over-limit events at the transport boundary rather than
rewriting IDs.

Source: Path instructions

Comment on lines +123 to +128
case .permissionRequest(let requestId, let toolName, let toolInputJSON, let pattern):
return .permissionRequest(
requestId: requestId.feedPrefix(512),
toolName: toolName.feedPrefix(512),
toolInputJSON: toolInputJSON.feedPrefix(32_768),
pattern: pattern?.feedPrefix(4_096)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Redact JSON before any destructive truncation.

An over-limit JSON payload becomes invalid before persistence redaction. The redactor then falls back to environment-assignment matching, so JSON secrets such as "api_key":"sk-..." near the beginning can be persisted unredacted.

  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamItem.swift#L123-L128: avoid raw prefix truncation of permission JSON before redaction.
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamItem.swift#L155-L164: apply the same fix to tool-use and tool-result JSON.
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamPersistence.swift#L95-L98: redact the original JSON first, then bound the safe projection; add an over-limit secret regression test.
📍 Affects 2 files
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamItem.swift#L123-L128 (this comment)
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamItem.swift#L155-L164
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamPersistence.swift#L95-L98
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamItem.swift`
around lines 123 - 128, Redact permission JSON before truncating it, so
over-limit payloads remain valid for JSON-aware secret redaction; update the
permissionRequest handling in WorkstreamItem.swift at lines 123-128 accordingly.
Apply the same ordering to tool-use and tool-result JSON at WorkstreamItem.swift
lines 155-164. In WorkstreamPersistence.swift lines 95-98, redact the original
JSON first and then bound the safe result, and add a regression test covering an
over-limit payload containing a secret.

Comment on lines +204 to +208
private extension String {
func feedPrefix(_ maximumCharacters: Int) -> String {
guard count > maximumCharacters else { return self }
return String(prefix(maximumCharacters))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound UTF-8 bytes without scanning the full string.

String.count traverses the entire input, while one extended grapheme can contain unbounded bytes. Agent-controlled fields can therefore bypass the intended memory ceiling and stall MainActor ingestion. Implement a byte-bounded prefix instead.

As per coding guidelines, production hot paths must use bounded construction rather than full-input scans.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamItem.swift`
around lines 204 - 208, Update String.feedPrefix(_:) to enforce the limit using
UTF-8 byte capacity rather than String.count, avoiding a full-input scan and
safely handling grapheme boundaries. Construct only the bounded UTF-8 prefix,
ensuring the returned string never exceeds maximumCharacters bytes while
preserving the original string when already within the limit.

Source: Coding guidelines

Comment on lines +111 to +125
let fileManager = FileManager.default
try? fileManager.removeItem(at: removedItemsFileURL)
guard !selectedLines.isEmpty else {
try? fileManager.removeItem(at: fileURL)
return
}
try fileManager.createDirectory(
at: fileURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
if !fm.fileExists(atPath: fileURL.path) {
fm.createFile(atPath: fileURL.path, contents: nil)
var snapshot = Data(capacity: selectedByteCount)
for line in selectedLines.reversed() {
snapshot.append(line)
}
let fh = try FileHandle(forWritingTo: fileURL)
handle = fh
return fh
try snapshot.write(to: fileURL, options: .atomic)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Commit the snapshot before deleting legacy tombstones.

If directory creation or the atomic write fails after Line 112, the legacy log remains but its removal records are gone, allowing dismissed cards to reappear. The empty-snapshot branch has the same problem because file deletion errors are suppressed.

Delete tombstones only after the new snapshot—or deletion of the old snapshot—succeeds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamPersistence.swift`
around lines 111 - 125, Update the snapshot persistence flow around the
selectedLines handling so the new snapshot write, or empty-snapshot file
deletion, completes successfully before removing removedItemsFileURL. Do not
suppress errors from deleting the old snapshot in the empty branch, and preserve
tombstones whenever directory creation, writing, or deletion fails.

Comment on lines +147 to +152
private func loadLegacyRemovedItemIDs() throws -> Set<UUID> {
guard FileManager.default.fileExists(atPath: removedItemsFileURL.path) else { return [] }
let attributes = try FileManager.default.attributesOfItem(atPath: removedItemsFileURL.path)
let fileSize = (attributes[.size] as? NSNumber)?.intValue ?? 0
guard fileSize <= maximumLegacyReadBytes else { return [] }
let data = try Data(contentsOf: removedItemsFileURL)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Fail closed when the legacy tombstone file exceeds the read cap.

Returning an empty set treats every prior removal as absent, so deleted pending rows in the legacy tail can be restored. Report migration as unavailable and skip restoration, or implement a bounded migration that preserves deletion semantics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamPersistence.swift`
around lines 147 - 152, The loadLegacyRemovedItemIDs migration must fail closed
when the legacy tombstone file exceeds maximumLegacyReadBytes: do not return an
empty Set, which permits deleted rows to be restored. Propagate an
unavailable/failed migration outcome through the caller so restoration is
skipped while preserving deletion semantics; keep normal bounded-file loading
unchanged.

Comment on lines 69 to +76
public func start() async {
if let persistence {
if let page = try? await persistence.loadPage(limit: min(initialLoadLimit, ringCapacity)) {
items = page.items
hasMorePersistedItems = page.hasMoreBefore
oldestLoadedPersistenceOffset = page.startOffset
if let restored = try? await persistence.loadPendingItems(limit: ringCapacity) {
items = restored.suffix(ringCapacity).map { $0.retainedForFeed() }
rebuildContextIndex()
}
persistenceGeneration &+= 1
try? await persistence.replacePendingItems(items, generation: persistenceGeneration)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not compact after a failed restore.

When loadPendingItems throws, items remains empty and Line 76 immediately replaces or deletes the persisted snapshot as though the empty state were authoritative. Compact only after a successful load; otherwise preserve the file or propagate the failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift`
around lines 69 - 76, Update start() so persistence.replacePendingItems is
called only after loadPendingItems succeeds; when restoration fails, preserve
the existing persisted snapshot by skipping compaction, or propagate the load
error instead. Keep the restored-item mapping and rebuildContextIndex behavior
unchanged for successful loads.

Comment on lines +120 to +127
public func removeItem(id: UUID) async throws -> Bool {
guard items.contains(where: { $0.id == id }) else { return false }
items.removeAll { $0.id == id }
rebuildContextIndex()
if let persistence {
persistenceGeneration &+= 1
try await persistence.replacePendingItems(items, generation: persistenceGeneration)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not rebuild the context cache from retained cards after removal.

The cache also contains telemetry-only context that is absent from items. Removing one unrelated card therefore erases prompt/preamble context for sessions that have not produced an actionable card yet. Preserve the cache or update only the affected workstream.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift`
around lines 120 - 127, The removeItem(id:) implementation should not call
rebuildContextIndex() after deleting a card, because that discards
telemetry-only context absent from items. Preserve the existing context cache or
update only the removed item’s affected workstream, while leaving persistence
generation and replacement behavior unchanged.

Comment on lines +181 to +191
private func schedulePendingSnapshot() {
guard let persistence else { return }
persistenceGeneration &+= 1
let generation = persistenceGeneration
let pendingItems = items.filter { $0.status.isPending }
Task { [persistence, pendingItems] in
try? await persistence.replacePendingItems(
pendingItems,
generation: generation
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Own snapshot writes and handle failures.

This fire-and-forget task discards disk errors and has no flushable lifecycle. If it fails or is interrupted, resolved or expired cards remain pending on disk and reappear after restart. Use an owned/coalesced writer task or an awaited persistence path.

As per coding guidelines, meaningful asynchronous work must be stored, cancellable, or tied to a caller-owned operation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift`
around lines 181 - 191, Update schedulePendingSnapshot so the asynchronous
replacePendingItems operation is owned by a stored, cancellable writer task or
awaited through a caller-owned persistence path. Coalesce successive snapshots
while preserving the latest persistenceGeneration and pendingItems, and handle
replacement failures rather than discarding them, ensuring interrupted or failed
writes cannot leave resolved or expired cards persisted as pending.

Source: Coding guidelines

Comment on lines +96 to +98
let restored = WorkstreamStore(persistence: persistence, ringCapacity: 10)
await restored.start()
#expect(restored.items.map(\.id) == [kept.id])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a new persistence actor to model restart.

Reusing persistence carries its in-memory latestGeneration, so this can hide initialization or generation defects. Construct a new WorkstreamPersistence(fileURL: tmp) for restored.

Proposed fix
-        let restored = WorkstreamStore(persistence: persistence, ringCapacity: 10)
+        let restartedPersistence = WorkstreamPersistence(fileURL: tmp)
+        let restored = WorkstreamStore(
+            persistence: restartedPersistence,
+            ringCapacity: 10
+        )
📝 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
let restored = WorkstreamStore(persistence: persistence, ringCapacity: 10)
await restored.start()
#expect(restored.items.map(\.id) == [kept.id])
let restartedPersistence = WorkstreamPersistence(fileURL: tmp)
let restored = WorkstreamStore(
persistence: restartedPersistence,
ringCapacity: 10
)
await restored.start()
`#expect`(restored.items.map(\.id) == [kept.id])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/Workstream/WorkstreamStoreTests.swift`
around lines 96 - 98, Update the restart setup in WorkstreamStoreTests by
constructing a new WorkstreamPersistence with the same temporary file URL used
by the original persistence, then pass that fresh actor to WorkstreamStore. Keep
the existing ringCapacity, start call, and restored item assertion unchanged.

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants