Skip to content

Add React Feed to the workspace menu - #8171

Closed
lawrencecchen wants to merge 29 commits into
mainfrom
feat-feed-plus-dropdown
Closed

lawrencecchen wants to merge 29 commits into
mainfrom
feat-feed-plus-dropdown

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add Feed to the titlebar workspace dropdown and command palette through one configured action
  • keep the Swift sidebar Feed while adding a React workspace surface backed by typed native capabilities
  • publish immutable snapshots from the main-actor store and route replies through FeedCoordinator
  • serve packaged React assets through the trusted scheme handler with bounded concurrent zlib decoding

Verification

  • Blacksmith tagged build: https://github.com/manaflow-ai/cmux/actions/runs/29410302842
  • exercised the titlebar dropdown in fdrx and verified the React DOM and custom-scheme URL
  • bun run typecheck
  • bun test test/feed.test.tsx
  • bun run lint:ci
  • ./scripts/build-webviews-app.sh --check
  • ./scripts/check-pbxproj.sh
  • focused local Swift test build was blocked by stale local Ghostty generated symbols; the tagged cloud build compiled the affected tests and runtime sources

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


Summary by cubic

Adds a React Feed accessible from the workspace menu and command palette. It opens in a trusted, chrome-less tab, matches the terminal theme, and improves keyboard focus and session restore.

  • New Features

    • Adds cmux.feed to the workspace dropdown and command palette; opens Feed in a chrome-less trusted tab via cmux-diff-viewer.
    • Ships a React Feed webview at /feed with bundled feed.html; introduces FeedSurfaceBridge with typed native calls scoped to trusted Feed surfaces only.
    • UX: localized onboarding (“Load examples”), examples persist with history, paged activity (40/page) with “Load older”, empty/idle/error states, theme sync, stable scrollbars, and Tab moves focus straight to a card’s actions.
    • Focus and persistence: deferred webview focus is fulfilled on attachment and canceled on unfocus; Feed capability is preserved across session restore; legacy packaged Feed URLs auto-migrate to the trusted scheme with chrome hidden; omnibarVisible is captured and restored in layouts.
    • Hardening/tests: the scheme handler decodes/serves bundled assets with bounded concurrency; Feed native handler is hidden from normal pages. Tests cover URL trust, capability isolation, legacy URL migration, chrome/omnibar visibility, pagination, deferred focus, keyboard handoff, and coalesced AsyncPublisher demand.
  • Migration

    • Requires the browser to be enabled; the action is hidden otherwise.
    • No config changes. Use action id cmux.feed to wire custom menus or shortcuts.

Written for commit 8be94a2. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added a new Feed web surface with actionable approvals/permission prompts, exit plans, and question workflows, including loading, empty/error states, and “load older” paging.
    • Enabled Feed access from the command palette, new workspace menu, and built-in tab actions.
  • Bug Fixes
    • Improved Feed web-to-native messaging reliability and snapshot updates.
    • Hardened bundled Feed asset delivery so only trusted in-app content is served.
  • Tests
    • Added/expanded coverage for Feed URL trust, native capability restoration, and permission-reply UI/bridge behavior.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The pull request adds a native-backed Feed webview with React UI, WebKit bridge messaging, bundled compressed assets, feed actions in application menus, feed state observation, browser capability propagation, and omnibar visibility persistence.

Changes

Feed surface

Layer / File(s) Summary
Feed web UI and bridge contract
webviews/src/feed/*, webviews/test/feed.test.tsx
Adds typed snapshots, native calls, filtering, cards, feed actions, styling, and integration coverage.
Feed webview runtime and generated assets
webviews/src/main.tsx, webviews/src/router.tsx, webviews/src/surfaces/feedSurface.tsx, Resources/markdown-viewer/webviews-app/*, scripts/build-webviews-app.sh
Adds feed detection, routing, mounting, generated assets, and separated router/style modules.
Native bridge and bundled asset serving
Sources/Feed/FeedSurfaceBridge.swift, Sources/Panels/BrowserPanel.swift, cmux.xcodeproj/project.pbxproj, cmuxTests/*
Adds trusted asset registration, deflate decoding, WebKit bridge handling, snapshot publication, capability restoration, and tests.
Application feed actions and menu wiring
Sources/AppDelegate*.swift, Sources/Cmux*.swift, Sources/ContentView*.swift, Sources/NewWorkspaceMenuModel.swift, Sources/Workspace.swift, cmuxTests/NewWorkspaceMenuModelTests.swift
Adds feed workspace execution, command palette and context-menu entries, configuration metadata, and tests.
Feed panel observation lifecycle
Sources/Feed/FeedPanelView.swift, Sources/Feed/FeedPanelViewModel.swift
Migrates feed state observation and updates older-item task cancellation and loading behavior.
Browser surface omnibar persistence
Sources/CmuxConfig.swift, Sources/Workspace+CustomLayout.swift, Sources/WorkspaceConfigActionCapture.swift, Sources/Workspace.swift, cmuxTests/CmuxConfigTests.swift
Adds optional omnibar visibility to browser surface definitions, captures and restores it, and verifies encoding.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CommandPalette
  participant AppDelegate
  participant BrowserPanel
  participant FeedSurfaceBridge
  participant FeedApp
  User->>CommandPalette: Select Feed
  CommandPalette->>AppDelegate: Execute feed action
  AppDelegate->>BrowserPanel: Open feed workspace surface
  BrowserPanel->>FeedSurfaceBridge: Install bridge and serve feed assets
  FeedApp->>FeedSurfaceBridge: Subscribe to feed snapshots
  FeedSurfaceBridge-->>FeedApp: Publish feed.snapshot
  User->>FeedApp: Choose permission action
  FeedApp->>FeedSurfaceBridge: Submit permission reply
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning Summary and Verification are present, but the template's Demo Video, Review Trigger, and Checklist sections are missing. Add a demo video link, the review-trigger comment block, and a completed checklist section that matches the template.
✅ Passed checks (3 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.
Title check ✅ Passed The title is concise and accurately reflects the main change: adding a React Feed entry to the workspace menu.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-feed-plus-dropdown

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 adds a React Feed surface accessible from the workspace titlebar dropdown and command palette, backed by a new FeedSurfaceBridge that publishes typed, immutable snapshots from WorkstreamStore to a trusted cmux-diff-viewer scheme URL. It also ships bundled asset delivery with concurrent zlib decompression, deferred webview focus, and session restore for the Feed capability.

  • New FeedSurfaceBridge: Installs a WKScriptMessageHandlerWithReply on webviews with the .feed capability, trust-gates every message to the exact bundled /feed.html URL, and arms a recursive withObservationTracking subscription to push immutable snapshots on every WorkstreamStore mutation.
  • BrowserNativeCapability + session restore: Encodes the .feed capability at panel creation (from URL or explicit parameter), re-registers bundled assets on session restore, and migrates legacy file-URL sessions to the trusted scheme transparently.
  • React Feed app (webviews/src/feed/): Provides actionable/activity filtering, permission/exitPlan/question card rendering with full keyboard navigation, paged activity (40/page), demo items, theme sync, and a useSyncExternalStore-based snapshot store.

Confidence Score: 4/5

Safe to merge after addressing the overly broad UserDefaults observer in FeedSurfaceBridge.

The settings observer in FeedSurfaceBridge subscribes to all UserDefaults changes rather than only the four keys that actually affect integration readiness. Every unrelated settings write cancels the in-flight filesystem scan, resets all integrations to Checking, publishes that incorrect state to the subscribed Feed webview, and restarts probing across 9 config files. The rest of the PR is solid.

Sources/Feed/FeedSurfaceBridge.swift — the settingsObserver subscription scope.

Important Files Changed

Filename Overview
Sources/Feed/FeedSurfaceBridge.swift New WKScriptMessageHandlerWithReply bridge for the React Feed surface — trust-gates all incoming messages to the bundled feed URL, routes actions through FeedCoordinator, and arms a recursive withObservationTracking subscription. The UserDefaults observer is too broad and fires on every key change, causing spurious "Checking..." integration state.
Sources/Panels/BrowserPanel.swift Adds BrowserNativeCapability enum, bundled-asset serving with concurrent zlib decoding, deferred webview focus logic, and session restore for bundled Feed. The deferred focus flag (hasDeferredWebViewFocus) is correctly cleared in all paths.
Sources/Feed/FeedPanelViewModel.swift Migrates from ObservableObject/@published to @observable, properly cancels loadOlderTask in deinit, and moves the guard check inline — a clean, correct modernisation.
webviews/src/feed/App.tsx New React Feed surface with actionable/activity filters, permission/exitPlan/question card rendering, tab navigation, and demo items. Uses raw div/button elements for the tab list with hand-rolled ARIA and keyboard handling rather than a Base UI primitive.
webviews/src/feed/bridge.ts Minimal bridge: publishes snapshots via a useSyncExternalStore-compatible store, exposes a single cmuxFeed message handler, and lazily subscribes on first store subscriber.
Sources/Workspace+CustomLayout.swift Fixes omnibarVisible preservation during custom-layout session restore — now passes surface.omnibarVisible through to addBrowserPanel instead of ignoring it.
Sources/WorkspaceConfigActionCapture.swift Captures omnibarVisible into the serialized CmuxSurfaceDefinition during workspace layout save, completing the round-trip for custom layouts.
Resources/Localizable.xcstrings Adds en + ja translations for all new Feed UI strings (examples, integrations, filter, history, permission, question, exit-plan copy).
webviews/test/feed.test.tsx Comprehensive integration test covering permission reply flow, keyboard navigation (Tab, j/k, Home/End, Ctrl-N/P, Shift-Tab), paged activity, demo items, and native bridge mocking.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant AppDelegate
    participant BrowserPanel
    participant FeedSurfaceBridge
    participant FeedCoordinator
    participant ReactFeedApp

    User->>AppDelegate: Click Feed menu or palette
    AppDelegate->>FeedSurfaceBridge: feedURL() registers bundled assets
    FeedSurfaceBridge-->>AppDelegate: cmux-diff-viewer://bundled-feed-surface/feed.html
    AppDelegate->>BrowserPanel: init nativeCapabilities .feed
    BrowserPanel->>FeedSurfaceBridge: installIfNeeded on userContentController
    ReactFeedApp->>FeedSurfaceBridge: postMessage feed.subscribe
    FeedSurfaceBridge->>FeedCoordinator: read store items
    FeedSurfaceBridge-->>ReactFeedApp: initial snapshot
    FeedSurfaceBridge->>FeedSurfaceBridge: armSnapshotSubscription withObservationTracking
    FeedCoordinator->>FeedSurfaceBridge: onChange items mutated
    FeedSurfaceBridge->>ReactFeedApp: evaluateJavaScript cmuxFeedBridge.receive snapshot
    ReactFeedApp->>FeedSurfaceBridge: postMessage feed.permission.reply
    FeedSurfaceBridge->>FeedCoordinator: deliverReply decision permission mode
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 User
    participant AppDelegate
    participant BrowserPanel
    participant FeedSurfaceBridge
    participant FeedCoordinator
    participant ReactFeedApp

    User->>AppDelegate: Click Feed menu or palette
    AppDelegate->>FeedSurfaceBridge: feedURL() registers bundled assets
    FeedSurfaceBridge-->>AppDelegate: cmux-diff-viewer://bundled-feed-surface/feed.html
    AppDelegate->>BrowserPanel: init nativeCapabilities .feed
    BrowserPanel->>FeedSurfaceBridge: installIfNeeded on userContentController
    ReactFeedApp->>FeedSurfaceBridge: postMessage feed.subscribe
    FeedSurfaceBridge->>FeedCoordinator: read store items
    FeedSurfaceBridge-->>ReactFeedApp: initial snapshot
    FeedSurfaceBridge->>FeedSurfaceBridge: armSnapshotSubscription withObservationTracking
    FeedCoordinator->>FeedSurfaceBridge: onChange items mutated
    FeedSurfaceBridge->>ReactFeedApp: evaluateJavaScript cmuxFeedBridge.receive snapshot
    ReactFeedApp->>FeedSurfaceBridge: postMessage feed.permission.reply
    FeedSurfaceBridge->>FeedCoordinator: deliverReply decision permission mode
Loading

Reviews (15): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment on lines +70 to +77
static func feedURL() -> URL? {
do {
return try CmuxDiffViewerURLSchemeHandler.shared.registerBundledFeedAssets()
} catch {
NSLog("feed.surface.register.failed error=%@", String(describing: error))
return nil
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 NSLog bypasses the unified logging subsystem. The repo's logging rule requires os.Logger for all production diagnostics; NSLog output is not captured by the unified log store and cannot be filtered by subsystem or category in Console.app or log stream.

Suggested change
static func feedURL() -> URL? {
do {
return try CmuxDiffViewerURLSchemeHandler.shared.registerBundledFeedAssets()
} catch {
NSLog("feed.surface.register.failed error=%@", String(describing: error))
return nil
}
}
static func feedURL() -> URL? {
do {
return try CmuxDiffViewerURLSchemeHandler.shared.registerBundledFeedAssets()
} catch {
Logger.feed.error("feed.surface.register.failed error=\(error, privacy: .public)")
return nil
}
}

Rule Used: Flag production Swift diagnostics that bypass unif... (source)

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

Comment on lines +60 to +68
static func isTrustedFeedURL(_ url: URL?, resourceURL: URL? = Bundle.main.resourceURL) -> Bool {
_ = resourceURL
guard let url,
let expected = CmuxDiffViewerURLSchemeHandler.diffViewerURL(
token: CmuxDiffViewerURLSchemeHandler.bundledFeedToken,
requestPath: "/feed.html"
) else { return false }
return url == expected
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The resourceURL parameter is accepted with a meaningful default but immediately discarded via _ = resourceURL. The function derives its trust anchor solely from CmuxDiffViewerURLSchemeHandler.diffViewerURL, making the parameter a no-op. This is misleading to callers who may expect passing a different bundle root to affect the result.

Suggested change
static func isTrustedFeedURL(_ url: URL?, resourceURL: URL? = Bundle.main.resourceURL) -> Bool {
_ = resourceURL
guard let url,
let expected = CmuxDiffViewerURLSchemeHandler.diffViewerURL(
token: CmuxDiffViewerURLSchemeHandler.bundledFeedToken,
requestPath: "/feed.html"
) else { return false }
return url == expected
}
static func isTrustedFeedURL(_ url: URL?) -> Bool {
guard let url,
let expected = CmuxDiffViewerURLSchemeHandler.diffViewerURL(
token: CmuxDiffViewerURLSchemeHandler.bundledFeedToken,
requestPath: "/feed.html"
) else { return false }
return url == expected
}

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

🤖 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`+NewWorkspaceContextMenu.swift:
- Around line 120-133: Extract the shared configured-action gating from
resolvedBuiltInFeedAction and shouldAppendBuiltInNewAgentChatMenuItem into a
generic shouldAppendBuiltInMenuItem helper. Rename the existing helper and
update its call site, then reuse it for the feed action so both built-in menu
items consistently skip actions already present in newWorkspaceContextMenuItems.

In `@Sources/Feed/FeedSurfaceBridge.swift`:
- Around line 60-68: Remove the unused resourceURL parameter and its discard
from isTrustedFeedURL, then update all call sites to use the simplified
signature, verifying no callers depend on passing a resource root. Preserve the
existing URL comparison based on bundledFeedToken and /feed.html.
- Around line 70-77: Replace the NSLog call in feedURL()’s registration-failure
catch block with the repository’s Logger-based logging mechanism. Preserve the
existing failure message and error details while using the appropriate logger
category or instance already established for production Swift runtime code.

In `@Sources/NewWorkspaceMenuModel.swift`:
- Around line 92-105: Consolidate the duplicated CmuxResolvedConfigMenuAction
row construction for agentChatAction and feedAction in the surrounding
new-workspace menu setup. Map over the available built-in actions, preserving
each action’s id, title, icon, iconSourcePath, tooltip, deletable status, and
isDefault comparison with newWorkspaceActionID.

In `@Sources/Panels/BrowserPanel.swift`:
- Around line 1945-1984: The synchronous directory enumeration and file
registration in registerBundledFeedAssets is being reached from the MainActor
via FeedSurfaceBridge.feedURL(). Move the scan and registration behind an
explicit background Task/queue or `@concurrent` boundary, then hop back to
MainActor only when returning the resulting feed URL; update the feedURL call
path to await or otherwise consume that asynchronous result.

In `@webviews/src/feed/App.tsx`:
- Around line 145-172: The submit button in the normalizedQuestions flow
currently enables when any answer exists, allowing incomplete payloads. Update
the enablement condition on the button using answers and normalizedQuestions so
submission is enabled only when every question has a response, while preserving
the existing perform payload and answer generation.
- Around line 22-29: Update perform to catch the thrown native error from
callFeedNative and use its specific user-facing message when available, falling
back to snapshot.copy.requestFailed otherwise. Also add and expose an in-flight
state for perform, ensuring it is set before the request and cleared after
completion so FeedActions and QuestionActions can disable permission, exit-plan,
and question controls while requests are pending.

In `@webviews/src/feed/bridge.ts`:
- Around line 57-64: Handle the promise returned by ensureSubscribed() inside
feedSnapshotStore.subscribe by attaching rejection handling, so native bridge
failures do not become unhandled global rejections. Preserve the existing
listener registration and unsubscribe behavior, and route the failure through
the established error-handling mechanism.
🪄 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: 34fc0894-5060-4927-ac88-473d7dc6aaf3

📥 Commits

Reviewing files that changed from the base of the PR and between ace9cc8 and f313f7b.

📒 Files selected for processing (33)
  • Resources/markdown-viewer/webviews-app/assets/feedSurface.css
  • Resources/markdown-viewer/webviews-app/chunks/agentSessionSurface.mjs
  • Resources/markdown-viewer/webviews-app/chunks/diffSurface.mjs
  • Resources/markdown-viewer/webviews-app/chunks/feedSurface.mjs
  • Resources/markdown-viewer/webviews-app/chunks/installWebviewStyles.mjs
  • Resources/markdown-viewer/webviews-app/chunks/router.mjs
  • Resources/markdown-viewer/webviews-app/feed.html
  • Resources/markdown-viewer/webviews-app/main.mjs
  • Sources/AppDelegate+AgentChat.swift
  • Sources/AppDelegate+NewWorkspaceContextMenu.swift
  • Sources/AppDelegate.swift
  • Sources/CmuxConfig.swift
  • Sources/CmuxSurfaceTabBarBuiltInAction.swift
  • Sources/ContentView+AgentChatCommandPalette.swift
  • Sources/ContentView.swift
  • Sources/Feed/FeedPanelView.swift
  • Sources/Feed/FeedPanelViewModel.swift
  • Sources/Feed/FeedSurfaceBridge.swift
  • Sources/NewWorkspaceMenuModel.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/FeedCoordinatorTests.swift
  • cmuxTests/NewWorkspaceMenuModelTests.swift
  • scripts/build-webviews-app.sh
  • webviews/src/feed/App.tsx
  • webviews/src/feed/bridge.ts
  • webviews/src/feed/styles.css
  • webviews/src/feed/types.ts
  • webviews/src/main.tsx
  • webviews/src/router.tsx
  • webviews/src/surfaces/feedSurface.tsx
  • webviews/test/feed.test.tsx

Comment on lines +120 to +133
private func resolvedBuiltInFeedAction(
cmuxConfigStore: CmuxConfigStore
) -> CmuxResolvedConfigAction? {
guard BrowserAvailabilitySettings.isEnabled() else { return nil }
let actionID = CmuxSurfaceTabBarBuiltInAction.feed.configID
let action = cmuxConfigStore.resolvedAction(id: actionID) ?? .builtIn(.feed)
guard action.newWorkspaceMenu != false else { return nil }
let configuredActionIDs = Set(cmuxConfigStore.newWorkspaceContextMenuItems.compactMap { item -> String? in
guard case .action(let menuAction) = item else { return nil }
return menuAction.action.id
})
return configuredActionIDs.contains(actionID) ? nil : action
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract menu-item gating logic to avoid duplication.

The logic to scan cmuxConfigStore.newWorkspaceContextMenuItems and verify that the action isn't already overridden is identical to the logic in shouldAppendBuiltInNewAgentChatMenuItem. Consider renaming that helper to a generic name like shouldAppendBuiltInMenuItem and reusing it here to keep this menu-building logic DRY.

♻️ Proposed refactor
-    private func resolvedBuiltInFeedAction(
-        cmuxConfigStore: CmuxConfigStore
-    ) -> CmuxResolvedConfigAction? {
-        guard BrowserAvailabilitySettings.isEnabled() else { return nil }
-        let actionID = CmuxSurfaceTabBarBuiltInAction.feed.configID
-        let action = cmuxConfigStore.resolvedAction(id: actionID) ?? .builtIn(.feed)
-        guard action.newWorkspaceMenu != false else { return nil }
-        let configuredActionIDs = Set(cmuxConfigStore.newWorkspaceContextMenuItems.compactMap { item -> String? in
-            guard case .action(let menuAction) = item else { return nil }
-            return menuAction.action.id
-        })
-        return configuredActionIDs.contains(actionID) ? nil : action
-    }
+    private func resolvedBuiltInFeedAction(
+        cmuxConfigStore: CmuxConfigStore
+    ) -> CmuxResolvedConfigAction? {
+        guard BrowserAvailabilitySettings.isEnabled() else { return nil }
+        let actionID = CmuxSurfaceTabBarBuiltInAction.feed.configID
+        let action = cmuxConfigStore.resolvedAction(id: actionID) ?? .builtIn(.feed)
+        guard shouldAppendBuiltInMenuItem(action, actionID: actionID, cmuxConfigStore: cmuxConfigStore) else { return nil }
+        return action
+    }

(Note: Apply this alongside renaming the existing shouldAppendBuiltInNewAgentChatMenuItem helper on line 134 and its call site on line 110).

🤖 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/AppDelegate`+NewWorkspaceContextMenu.swift around lines 120 - 133,
Extract the shared configured-action gating from resolvedBuiltInFeedAction and
shouldAppendBuiltInNewAgentChatMenuItem into a generic
shouldAppendBuiltInMenuItem helper. Rename the existing helper and update its
call site, then reuse it for the feed action so both built-in menu items
consistently skip actions already present in newWorkspaceContextMenuItems.

Comment on lines +60 to +68
static func isTrustedFeedURL(_ url: URL?, resourceURL: URL? = Bundle.main.resourceURL) -> Bool {
_ = resourceURL
guard let url,
let expected = CmuxDiffViewerURLSchemeHandler.diffViewerURL(
token: CmuxDiffViewerURLSchemeHandler.bundledFeedToken,
requestPath: "/feed.html"
) else { return false }
return url == expected
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unused resourceURL parameter on isTrustedFeedURL.

resourceURL is accepted then immediately discarded (_ = resourceURL); the trust decision only ever depends on the fixed bundledFeedToken, never on the passed-in bundle root. This is dead/no-op and could mislead a future caller into thinking the check is sandboxable by resource root.

🧹 Proposed cleanup (verify no other call sites pass `resourceURL` first)
-    static func isTrustedFeedURL(_ url: URL?, resourceURL: URL? = Bundle.main.resourceURL) -> Bool {
-        _ = resourceURL
-        guard let url,
+    static func isTrustedFeedURL(_ url: URL?) -> Bool {
+        guard let url,
📝 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
static func isTrustedFeedURL(_ url: URL?, resourceURL: URL? = Bundle.main.resourceURL) -> Bool {
_ = resourceURL
guard let url,
let expected = CmuxDiffViewerURLSchemeHandler.diffViewerURL(
token: CmuxDiffViewerURLSchemeHandler.bundledFeedToken,
requestPath: "/feed.html"
) else { return false }
return url == expected
}
static func isTrustedFeedURL(_ url: URL?) -> Bool {
guard let url,
let expected = CmuxDiffViewerURLSchemeHandler.diffViewerURL(
token: CmuxDiffViewerURLSchemeHandler.bundledFeedToken,
requestPath: "/feed.html"
) else { return false }
return url == expected
}
🤖 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/FeedSurfaceBridge.swift` around lines 60 - 68, Remove the unused
resourceURL parameter and its discard from isTrustedFeedURL, then update all
call sites to use the simplified signature, verifying no callers depend on
passing a resource root. Preserve the existing URL comparison based on
bundledFeedToken and /feed.html.

Comment on lines +70 to +77
static func feedURL() -> URL? {
do {
return try CmuxDiffViewerURLSchemeHandler.shared.registerBundledFeedAssets()
} catch {
NSLog("feed.surface.register.failed error=%@", String(describing: error))
return nil
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace NSLog with Logger for the registration-failure diagnostic.

This is new production runtime code, not wrapped in #if DEBUG, so the repo's logging rule against introducing NSLog/print/etc. applies directly here.

As per coding guidelines: "Production Swift runtime code must not add print, debugPrint, dump, NSLog, ad hoc file logging, or stdout/stderr diagnostics; use Logger or the existing cmux debug log instead."

🪵 Proposed fix using Logger
+import OSLog
+
+nonisolated private let feedSurfaceBridgeLogger = Logger(subsystem: Logging.subsystem, category: "FeedSurfaceBridge")
+
 `@MainActor`
 final class FeedSurfaceBridge: NSObject, WKScriptMessageHandlerWithReply {
     ...
     static func feedURL() -> URL? {
         do {
             return try CmuxDiffViewerURLSchemeHandler.shared.registerBundledFeedAssets()
         } catch {
-            NSLog("feed.surface.register.failed error=%@", String(describing: error))
+            feedSurfaceBridgeLogger.error("feed.surface.register.failed error=\(error, privacy: .public)")
             return nil
         }
     }
📝 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
static func feedURL() -> URL? {
do {
return try CmuxDiffViewerURLSchemeHandler.shared.registerBundledFeedAssets()
} catch {
NSLog("feed.surface.register.failed error=%@", String(describing: error))
return nil
}
}
import OSLog
nonisolated private let feedSurfaceBridgeLogger = Logger(subsystem: Logging.subsystem, category: "FeedSurfaceBridge")
static func feedURL() -> URL? {
do {
return try CmuxDiffViewerURLSchemeHandler.shared.registerBundledFeedAssets()
} catch {
feedSurfaceBridgeLogger.error("feed.surface.register.failed error=\(error, privacy: .public)")
return nil
}
}
🤖 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/FeedSurfaceBridge.swift` around lines 70 - 77, Replace the NSLog
call in feedURL()’s registration-failure catch block with the repository’s
Logger-based logging mechanism. Preserve the existing failure message and error
details while using the appropriate logger category or instance already
established for production Swift runtime code.

Source: Coding guidelines

Comment on lines +92 to +105
if let feedAction {
createRows.append(.action(
CmuxResolvedConfigMenuAction(
id: feedAction.id,
title: feedAction.title,
icon: feedAction.icon,
iconSourcePath: feedAction.iconSourcePath,
tooltip: feedAction.tooltip,
action: feedAction
),
deletable: deletable(feedAction),
isDefault: feedAction.id == newWorkspaceActionID
))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consolidate built-in action row creation.

The CmuxResolvedConfigMenuAction creation block is identical to the one for agentChatAction directly above it. Consider mapping over the available built-in actions to keep the row setup DRY.

♻️ Proposed refactor

Consider replacing lines 78-105 with a compact map:

-        if let agentChatAction {
-            createRows.append(.action(
-                CmuxResolvedConfigMenuAction(
-                    id: agentChatAction.id,
-                    title: agentChatAction.title,
-                    icon: agentChatAction.icon,
-                    iconSourcePath: agentChatAction.iconSourcePath,
-                    tooltip: agentChatAction.tooltip,
-                    action: agentChatAction
-                ),
-                deletable: deletable(agentChatAction),
-                isDefault: agentChatAction.id == newWorkspaceActionID
-            ))
-        }
-        if let feedAction {
-            createRows.append(.action(
-                CmuxResolvedConfigMenuAction(
-                    id: feedAction.id,
-                    title: feedAction.title,
-                    icon: feedAction.icon,
-                    iconSourcePath: feedAction.iconSourcePath,
-                    tooltip: feedAction.tooltip,
-                    action: feedAction
-                ),
-                deletable: deletable(feedAction),
-                isDefault: feedAction.id == newWorkspaceActionID
-            ))
-        }
+        for action in [agentChatAction, feedAction].compactMap({ $0 }) {
+            createRows.append(.action(
+                CmuxResolvedConfigMenuAction(
+                    id: action.id,
+                    title: action.title,
+                    icon: action.icon,
+                    iconSourcePath: action.iconSourcePath,
+                    tooltip: action.tooltip,
+                    action: action
+                ),
+                deletable: deletable(action),
+                isDefault: action.id == newWorkspaceActionID
+            ))
+        }
📝 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
if let feedAction {
createRows.append(.action(
CmuxResolvedConfigMenuAction(
id: feedAction.id,
title: feedAction.title,
icon: feedAction.icon,
iconSourcePath: feedAction.iconSourcePath,
tooltip: feedAction.tooltip,
action: feedAction
),
deletable: deletable(feedAction),
isDefault: feedAction.id == newWorkspaceActionID
))
}
for action in [agentChatAction, feedAction].compactMap({ $0 }) {
createRows.append(.action(
CmuxResolvedConfigMenuAction(
id: action.id,
title: action.title,
icon: action.icon,
iconSourcePath: action.iconSourcePath,
tooltip: action.tooltip,
action: action
),
deletable: deletable(action),
isDefault: action.id == newWorkspaceActionID
))
}
🤖 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/NewWorkspaceMenuModel.swift` around lines 92 - 105, Consolidate the
duplicated CmuxResolvedConfigMenuAction row construction for agentChatAction and
feedAction in the surrounding new-workspace menu setup. Map over the available
built-in actions, preserving each action’s id, title, icon, iconSourcePath,
tooltip, deletable status, and isDefault comparison with newWorkspaceActionID.

Comment on lines +1945 to +1984
func registerBundledFeedAssets(resourceURL: URL? = Bundle.main.resourceURL) throws -> URL {
guard let root = resourceURL?
.appendingPathComponent("markdown-viewer/webviews-app", isDirectory: true)
.standardizedFileURL
.resolvingSymlinksInPath(),
let enumerator = FileManager.default.enumerator(
at: root,
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles]
) else {
throw NSError(domain: "CmuxDiffViewerURLSchemeHandler", code: 6)
}
let files = enumerator.compactMap { entry -> RegisteredFile? in
guard let fileURL = entry as? URL,
(try? fileURL.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true else {
return nil
}
var relativePath = String(fileURL.path.dropFirst(root.path.count))
if relativePath.hasSuffix(".deflate") {
relativePath.removeLast(".deflate".count)
}
let mimeType: String
if relativePath.hasSuffix(".html") {
mimeType = "text/html"
} else if relativePath.hasSuffix(".mjs") || relativePath.hasSuffix(".js") {
mimeType = "text/javascript"
} else if relativePath.hasSuffix(".css") {
mimeType = "text/css"
} else {
return nil
}
return RegisteredFile(requestPath: relativePath, fileURL: fileURL, mimeType: mimeType)
}
try register(token: Self.bundledFeedToken, files: files)
guard let url = Self.diffViewerURL(token: Self.bundledFeedToken, requestPath: "/feed.html") else {
throw NSError(domain: "CmuxDiffViewerURLSchemeHandler", code: 7)
}
return url
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

registerBundledFeedAssets does synchronous directory-scan/file-stat I/O on the caller's actor.

This enumerates the bundle's markdown-viewer/webviews-app tree and does per-file resourceValues/fileExists/isReadableFile checks (via register) synchronously. It's invoked with no await from FeedSurfaceBridge.feedURL(), which in turn is called synchronously from a MainActor context (AppDelegate+AgentChat.performConfiguredFeedAction). This matches the project rule against putting directory-scan/file-I/O loads on the main actor without an explicit hop — consider moving the enumeration+registration to a background Task/queue and hopping back to MainActor only to hand back the resulting URL.

As per coding guidelines: "In non-test Swift, do not add or move expensive synchronous agent-history disk, JSON, transcript, trajectory, syscall, directory-scan, or unbounded-file parsing loads onto the main actor or latency-sensitive interactive paths" and "flag changed CPU-heavy, file-I/O-heavy ... helpers called from UI isolation without an explicit actor hop or @concurrent boundary."

🤖 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/Panels/BrowserPanel.swift` around lines 1945 - 1984, The synchronous
directory enumeration and file registration in registerBundledFeedAssets is
being reached from the MainActor via FeedSurfaceBridge.feedURL(). Move the scan
and registration behind an explicit background Task/queue or `@concurrent`
boundary, then hop back to MainActor only when returning the resulting feed URL;
update the feedURL call path to await or otherwise consume that asynchronous
result.

Source: Coding guidelines

Comment thread webviews/src/feed/App.tsx
Comment on lines +22 to +29
const perform = async (method: string, params: Record<string, unknown>) => {
setError(null);
try {
await callFeedNative(method, params);
} catch {
setError(snapshot.copy.requestFailed);
}
};

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

perform() discards the specific native error and disables no in-flight state for actions.

callFeedNative throws with reply.error?.userMessage specifically so the UI can surface a targeted message, but the catch {} here ignores the caught error entirely and always falls back to the generic snapshot.copy.requestFailed. This defeats the purpose of the native userMessage field.

Separately, perform exposes no pending/in-flight flag, so callers (permission/exit-plan/question buttons in FeedActions/QuestionActions) can't disable themselves while a request is outstanding — unlike the "load more" button which uses snapshot.isLoadingOlder. Rapid double-clicks on e.g. "Allow Once" can fire duplicate feed.permission.reply calls before the snapshot updates the item's pending status.

🐛 Proposed fix to surface the specific error message
   const perform = async (method: string, params: Record<string, unknown>) => {
     setError(null);
     try {
       await callFeedNative(method, params);
-    } catch {
-      setError(snapshot.copy.requestFailed);
+    } catch (err) {
+      setError(err instanceof Error && err.message ? err.message : snapshot.copy.requestFailed);
     }
   };
📝 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
const perform = async (method: string, params: Record<string, unknown>) => {
setError(null);
try {
await callFeedNative(method, params);
} catch {
setError(snapshot.copy.requestFailed);
}
};
const perform = async (method: string, params: Record<string, unknown>) => {
setError(null);
try {
await callFeedNative(method, params);
} catch (err) {
setError(err instanceof Error && err.message ? err.message : snapshot.copy.requestFailed);
}
};
🤖 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 `@webviews/src/feed/App.tsx` around lines 22 - 29, Update perform to catch the
thrown native error from callFeedNative and use its specific user-facing message
when available, falling back to snapshot.copy.requestFailed otherwise. Also add
and expose an in-flight state for perform, ensuring it is set before the request
and cleared after completion so FeedActions and QuestionActions can disable
permission, exit-plan, and question controls while requests are pending.

Comment thread webviews/src/feed/App.tsx
Comment on lines +145 to +172
const answers = normalizedQuestions.flatMap((question) => {
const custom = freeText[question.id]?.trim();
if (custom) return [custom];
const values = selected[question.id] ?? [];
const labels = question.options.filter((option) => values.includes(option.id)).map((option) => option.label);
return labels.length > 0 ? [labels.join(", ")] : [];
});
return <div className="feed-question-actions">
{normalizedQuestions.map((question) => <div className="feed-question" key={question.id}>
{question.prompt && <div className="feed-question-prompt">{question.prompt}</div>}
<div className="feed-options">
{question.options.map((option) => (
<button aria-pressed={(selected[question.id] ?? []).includes(option.id)} key={option.id} onClick={() => toggle(question, option.id)}>
<strong>{option.label}</strong>
{option.description && <span>{option.description}</span>}
</button>
))}
</div>
<input
aria-label={copy.questionPlaceholder}
onChange={(event) => setFreeText((current) => ({ ...current, [question.id]: event.target.value }))}
placeholder={copy.questionPlaceholder}
value={freeText[question.id] ?? ""}
/>
</div>)}
<button className="primary" disabled={answers.length === 0} onClick={() => perform("feed.question.reply", { itemId: item.id, selections: answers })}>
{copy.questionSubmit}
</button>

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

Submit button label promises "All Answers" but enablement only requires one.

answers is computed via flatMap over normalizedQuestions, and the submit button is only disabled when answers.length === 0 — i.e. it enables once any single question has a selection or free-text answer, not once every question has one. Yet the button copy is copy.questionSubmit ("Submit All Answers"), and the payload (selections: answers) can be sent with fewer entries than questions exist for multi-question items, silently dropping unanswered questions.

Consider requiring answers.length === normalizedQuestions.length (or otherwise validating each question has a response) before enabling submission.

🤖 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 `@webviews/src/feed/App.tsx` around lines 145 - 172, The submit button in the
normalizedQuestions flow currently enables when any answer exists, allowing
incomplete payloads. Update the enablement condition on the button using answers
and normalizedQuestions so submission is enabled only when every question has a
response, while preserving the existing perform payload and answer generation.

Comment on lines +57 to +64
export const feedSnapshotStore = {
getSnapshot: () => currentSnapshot,
subscribe(listener: () => void) {
listeners.add(listener);
void ensureSubscribed();
return () => listeners.delete(listener);
},
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Catch the unhandled promise rejection from ensureSubscribed().

The ensureSubscribed function throws an error if the native bridge fails. Because subscribe calls it with void and no .catch(), a failure will result in an unhandled promise rejection in the global scope.

🛡️ Proposed fix to handle the rejection
 export const feedSnapshotStore = {
   getSnapshot: () => currentSnapshot,
   subscribe(listener: () => void) {
     listeners.add(listener);
-    void ensureSubscribed();
+    void ensureSubscribed().catch((error) => {
+      console.error("Failed to subscribe to feed:", error);
+    });
     return () => listeners.delete(listener);
   },
 };
📝 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
export const feedSnapshotStore = {
getSnapshot: () => currentSnapshot,
subscribe(listener: () => void) {
listeners.add(listener);
void ensureSubscribed();
return () => listeners.delete(listener);
},
};
export const feedSnapshotStore = {
getSnapshot: () => currentSnapshot,
subscribe(listener: () => void) {
listeners.add(listener);
void ensureSubscribed().catch((error) => {
console.error("Failed to subscribe to feed:", error);
});
return () => listeners.delete(listener);
},
};
🤖 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 `@webviews/src/feed/bridge.ts` around lines 57 - 64, Handle the promise
returned by ensureSubscribed() inside feedSnapshotStore.subscribe by attaching
rejection handling, so native bridge failures do not become unhandled global
rejections. Preserve the existing listener registration and unsubscribe
behavior, and route the failure through the established error-handling
mechanism.

@cursor

cursor Bot commented Jul 15, 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.

Caution

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

⚠️ Outside diff range comments (1)
Sources/AppDelegate+AgentChat.swift (1)

131-135: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix early return when switching to an already-open Feed tab.

If the Feed workspace is already open, executeWorkspaceCommand successfully focuses it without creating a new tab. In that case, tabManager.tabs will not contain any new IDs not in beforeIDs, causing this guard to fail and return false. This breaks the command palette flow by skipping onExecuted?() and incorrectly signaling a failure to the caller.

Since executeWorkspaceCommand returns true upon success (whether it created or reused a tab), you can remove the beforeIDs enforcement here.

🐛 Proposed fix to support reusing existing tabs
-        guard context.tabManager.tabs.contains(where: { !beforeIDs.contains($0.id) }) else {
-            return false
-        }
         onExecuted?()
         return true
🤖 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/AppDelegate`+AgentChat.swift around lines 131 - 135, Remove the
beforeIDs-based guard in executeWorkspaceCommand’s success path; rely on its
successful return value so reusing an existing Feed tab still invokes
onExecuted?() and returns true. Preserve the callback and success behavior for
newly created tabs.
🤖 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/AppDelegate`+AgentChat.swift:
- Around line 131-135: Remove the beforeIDs-based guard in
executeWorkspaceCommand’s success path; rely on its successful return value so
reusing an existing Feed tab still invokes onExecuted?() and returns true.
Preserve the callback and success behavior for newly created tabs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 178003b7-1f4e-4d87-9b3b-befe497cdfb3

📥 Commits

Reviewing files that changed from the base of the PR and between f313f7b and 639f82f.

📒 Files selected for processing (8)
  • Sources/AppDelegate+AgentChat.swift
  • Sources/CmuxConfig.swift
  • Sources/Feed/FeedSurfaceBridge.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Workspace+CustomLayout.swift
  • Sources/WorkspaceConfigActionCapture.swift
  • cmuxTests/CmuxConfigTests.swift
  • cmuxTests/FeedCoordinatorTests.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

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

77-84: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

registerBundledFeedAssets is called synchronously on the MainActor, blocking it with file I/O.

feedURL() calls registerBundledFeedAssets() synchronously. Since FeedSurfaceBridge is isolated to @MainActor, this blocks the main thread with a directory enumeration and per-file I/O checks.

As per coding guidelines, do not add expensive synchronous directory-scan or file I/O loads onto the main actor. Move the scan and registration behind an explicit background Task or actor, then hop back to the MainActor when returning the resulting feed URL.

🤖 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/FeedSurfaceBridge.swift` around lines 77 - 84, Update
FeedSurfaceBridge.feedURL so registerBundledFeedAssets does not execute
synchronously on the MainActor; move the directory scan and file I/O into an
explicit background Task or actor, then return or deliver the resulting URL by
hopping back to the MainActor while preserving the existing error logging and
nil failure behavior.

Source: Coding guidelines

♻️ Duplicate comments (1)
webviews/src/feed/App.tsx (1)

27-34: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

perform() discards the specific native error and disables no in-flight state for actions.

callFeedNative throws with reply.error?.userMessage specifically so the UI can surface a targeted message, but the catch {} here ignores the caught error entirely and always falls back to the generic snapshot.copy.requestFailed. This defeats the purpose of the native userMessage field.

Separately, perform exposes no pending/in-flight flag, so callers (permission/exit-plan/question buttons in FeedActions/QuestionActions) can't disable themselves while a request is outstanding — unlike the "load more" button which uses snapshot.isLoadingOlder. Rapid double-clicks on e.g. "Allow Once" can fire duplicate feed.permission.reply calls before the snapshot updates the item's pending status.

🐛 Proposed fix to surface the specific error message
-  const perform = async (method: string, params: Record<string, unknown>) => {
+  const perform = async (method: string, params: Record<string, unknown>) => {
     setError(null);
     try {
       await callFeedNative(method, params);
-    } catch {
-      setError(snapshot.copy.requestFailed);
+    } catch (err) {
+      setError(err instanceof Error && err.message ? err.message : snapshot.copy.requestFailed);
     }
   };
🤖 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 `@webviews/src/feed/App.tsx` around lines 27 - 34, Update perform in App.tsx to
preserve and display the specific error thrown by callFeedNative, using its
native userMessage when available and snapshot.copy.requestFailed as the
fallback. Also add and expose in-flight state for perform calls, then have the
permission, exit-plan, and question actions in FeedActions and QuestionActions
disable their controls while the corresponding request is pending, preventing
duplicate submissions.
🤖 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/FeedSurfaceBridge.swift`:
- Around line 77-84: Update FeedSurfaceBridge.feedURL so
registerBundledFeedAssets does not execute synchronously on the MainActor; move
the directory scan and file I/O into an explicit background Task or actor, then
return or deliver the resulting URL by hopping back to the MainActor while
preserving the existing error logging and nil failure behavior.

---

Duplicate comments:
In `@webviews/src/feed/App.tsx`:
- Around line 27-34: Update perform in App.tsx to preserve and display the
specific error thrown by callFeedNative, using its native userMessage when
available and snapshot.copy.requestFailed as the fallback. Also add and expose
in-flight state for perform calls, then have the permission, exit-plan, and
question actions in FeedActions and QuestionActions disable their controls while
the corresponding request is pending, preventing duplicate submissions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 83e62cfc-b7e6-4721-8fd4-233801e8a7ca

📥 Commits

Reviewing files that changed from the base of the PR and between 639f82f and faa1cd8.

📒 Files selected for processing (12)
  • Resources/markdown-viewer/webviews-app/assets/feedSurface.css
  • Resources/markdown-viewer/webviews-app/chunks/feedSurface.mjs
  • Sources/Feed/FeedSurfaceBridge.swift
  • Sources/Panels/BrowserPanel+AutomationRecovery.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Workspace.swift
  • cmuxTests/BrowserWebContentProcessTests.swift
  • cmuxTests/FeedCoordinatorTests.swift
  • webviews/src/feed/App.tsx
  • webviews/src/feed/styles.css
  • webviews/src/feed/types.ts
  • webviews/test/feed.test.tsx

@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

🤖 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/FeedSurfaceBridge.swift`:
- Line 199: Update FeedSurfaceBridge.snapshot() so the "items" serialization
does not map the unbounded store?.items collection on every observation update.
Apply the established pagination or hard-limit policy before itemDictionary, or
reuse a cache/diff mechanism for unchanged items, ensuring snapshot
serialization remains bounded and preserves the required feed ordering and
contents.
🪄 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: d72c35f2-bf5b-453c-9ed0-021d047a4bca

📥 Commits

Reviewing files that changed from the base of the PR and between faa1cd8 and 3bb629d.

📒 Files selected for processing (7)
  • Resources/markdown-viewer/webviews-app/assets/feedSurface.css
  • Resources/markdown-viewer/webviews-app/chunks/feedSurface.mjs
  • Sources/Feed/FeedSurfaceBridge.swift
  • webviews/src/feed/App.tsx
  • webviews/src/feed/styles.css
  • webviews/src/feed/types.ts
  • webviews/test/feed.test.tsx

let background = GhosttyApp.shared.defaultBackgroundColor
let foreground = GhosttyApp.shared.defaultForegroundColor
return [
"items": (store?.items ?? []).map(itemDictionary),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Unbounded full-collection serialization of feed items.

snapshot() serializes the entire store?.items array via map(itemDictionary) on every update triggered by withObservationTracking. As the feed history grows (e.g., hundreds or thousands of items from loading older activity), this full-collection scan and dictionary allocation on the main actor will become a performance bottleneck, introducing UI stutter when new items arrive or states change.

As per path instructions, avoid repeated full scans and unbenchmarked algorithm choices for scalable user data like feed items and snapshot serialization.

Please consider applying a pagination/hard limit bound to the serialized array to guarantee constant-time serialization, or implement a diffing/caching mechanism that only reserializes modified items.

🤖 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/FeedSurfaceBridge.swift` at line 199, Update
FeedSurfaceBridge.snapshot() so the "items" serialization does not map the
unbounded store?.items collection on every observation update. Apply the
established pagination or hard-limit policy before itemDictionary, or reuse a
cache/diff mechanism for unchanged items, ensuring snapshot serialization
remains bounded and preserves the required feed ordering and contents.

Source: Path instructions

@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

🤖 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/markdown-viewer/webviews-app/assets/feedSurface.css`:
- Line 1: Fix the source stylesheet and build lint boundary rather than editing
generated feedSurface.css directly: replace deprecated word-break: break-word
with overflow-wrap: anywhere, quote the SF Pro Text font family, and normalize
both currentColor values to the required casing. Configure Stylelint to exclude
minified generated assets or preserve lint-compatible formatting, then
regenerate the bundle.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2af4e430-598e-4387-8e88-87d486e8a762

📥 Commits

Reviewing files that changed from the base of the PR and between 3bb629d and 53aa3ab.

📒 Files selected for processing (2)
  • Resources/markdown-viewer/webviews-app/assets/feedSurface.css
  • webviews/src/feed/styles.css
🚧 Files skipped from review as they are similar to previous changes (1)
  • webviews/src/feed/styles.css

@@ -0,0 +1 @@
:root{color-scheme:light dark;font:12px/1.4 -apple-system,BlinkMacSystemFont,SF Pro Text,sans-serif;--feed-background: transparent;--feed-foreground: CanvasText;--feed-border: color-mix(in srgb, var(--feed-foreground) 14%, transparent);--feed-border-strong: color-mix(in srgb, var(--feed-foreground) 38%, transparent);--feed-fill: color-mix(in srgb, var(--feed-foreground) 5%, transparent);--feed-fill-hover: color-mix(in srgb, var(--feed-foreground) 9%, transparent);--feed-muted: color-mix(in srgb, var(--feed-foreground) 56%, transparent)}*{box-sizing:border-box}html{scrollbar-gutter:stable}html,body,#root{margin:0;min-height:100%;width:100%}body{background:transparent;color:var(--feed-foreground)}button,input{font:inherit}.feed-shell{background:var(--feed-background);color:var(--feed-foreground);min-height:100vh;padding:14px clamp(12px,3vw,28px) 28px}.feed-loading{opacity:.5}.feed-header{align-items:center;border-bottom:1px solid var(--feed-border);display:flex;justify-content:space-between;margin:0 auto 8px;max-width:760px;padding-bottom:10px}.feed-header h1,.feed-card-heading h2,.feed-question-prompt{font-size:inherit;font-weight:600;margin:0}.feed-filter{border:1px solid var(--feed-border);display:flex;gap:1px;padding:2px}.feed-filter button{background:transparent;border:0;color:var(--feed-muted);cursor:pointer;padding:3px 7px}.feed-filter button:hover{background:var(--feed-fill)}.feed-filter button[aria-selected=true]{background:var(--feed-foreground);color:var(--feed-background)}.feed-list{display:grid;gap:6px;margin:0 auto;max-width:760px}.feed-card{background:color-mix(in srgb,var(--feed-background) 96%,var(--feed-foreground) 4%);border:1px solid var(--feed-border);padding:10px}.feed-card-pending{border-color:var(--feed-border-strong)}.feed-card-heading{align-items:center;display:flex;gap:10px;justify-content:space-between}.feed-card-title{align-items:center;display:flex;gap:8px;min-width:0}.feed-card-heading h2{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.feed-source{align-items:center;color:var(--feed-muted);display:inline-flex;flex:none;gap:5px}.feed-source-logo{background:currentColor;display:inline-grid;height:13px;line-height:13px;-webkit-mask-image:var(--feed-source-icon);mask-image:var(--feed-source-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;place-items:center;width:13px}.feed-source-logo[data-fallback]{background:transparent;border:1px solid currentColor;-webkit-mask-image:none;mask-image:none}.feed-card-heading time,.feed-cwd{color:var(--feed-muted);flex:none}.feed-cwd{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;margin-top:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.feed-body{background:var(--feed-fill);border:1px solid var(--feed-border);font:inherit;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;margin:8px 0 0;max-height:240px;overflow:auto;padding:7px 8px;white-space:pre-wrap;word-break:break-word}.feed-actions{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.feed-actions button,.feed-question-actions>button,.feed-options button,.feed-load-more{background:transparent;border:1px solid var(--feed-border-strong);color:var(--feed-foreground);cursor:pointer;padding:4px 7px}.feed-actions button:hover,.feed-question-actions>button:hover,.feed-options button:hover,.feed-load-more:hover{background:var(--feed-fill-hover)}.feed-actions .primary,.feed-question-actions .primary{background:var(--feed-foreground);border-color:var(--feed-foreground);color:var(--feed-background)}.feed-actions .primary:hover,.feed-question-actions .primary:hover{opacity:.82}.feed-actions .danger{border-style:dashed}.feed-question-actions{display:grid;gap:7px;margin-top:8px}.feed-question{display:grid;gap:6px}.feed-question input{background:var(--feed-fill);border:1px solid var(--feed-border-strong);color:var(--feed-foreground);padding:5px 7px;width:100%}.feed-options{display:grid;gap:4px}.feed-options button{display:grid;gap:1px;text-align:left}.feed-options button[aria-pressed=true]{background:var(--feed-foreground);color:var(--feed-background)}.feed-options span{color:var(--feed-muted)}.feed-options button[aria-pressed=true] span{color:color-mix(in srgb,var(--feed-background) 70%,transparent)}.feed-empty{color:var(--feed-muted);padding:44px 12px;text-align:center}.feed-error{border:1px solid var(--feed-border-strong);margin:0 auto 6px;max-width:760px;padding:6px 8px}.feed-load-more{justify-self:center;margin-top:2px}button:disabled{cursor:default;opacity:.4}button:focus-visible,.feed-question input:focus-visible{outline:1px solid var(--feed-foreground);outline-offset:2px}@media(max-width:540px){.feed-header{align-items:stretch;flex-direction:column;gap:8px}.feed-filter{align-self:flex-start}.feed-card-title{align-items:flex-start;flex-direction:column;gap:3px}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Resolve Stylelint failures in the generated stylesheet.

Line 1 contains four reported violations: deprecated word-break: break-word, unquoted SF Pro Text, and two currentColor casing errors. Since linting is part of the PR verification, fix the source stylesheet/build lint boundary—not this generated bundle directly—and regenerate it. Replace word-break: break-word with overflow-wrap: anywhere, and exclude minified generated assets from Stylelint or ensure the bundler preserves the required formatting.

🧰 Tools
🪛 Stylelint (17.14.0)

[error] 1-1: Deprecated keyword "break-word" for property "word-break" (declaration-property-value-keyword-no-deprecated)

(declaration-property-value-keyword-no-deprecated)


[error] 1-1: Expected quotes around "SF Pro Text" (font-family-name-quotes)

(font-family-name-quotes)


[error] 1-1: Expected "currentColor" to be "currentcolor" (value-keyword-case)

(value-keyword-case)


[error] 1-1: Expected "currentColor" to be "currentcolor" (value-keyword-case)

(value-keyword-case)

🤖 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 `@Resources/markdown-viewer/webviews-app/assets/feedSurface.css` at line 1, Fix
the source stylesheet and build lint boundary rather than editing generated
feedSurface.css directly: replace deprecated word-break: break-word with
overflow-wrap: anywhere, quote the SF Pro Text font family, and normalize both
currentColor values to the required casing. Configure Stylelint to exclude
minified generated assets or preserve lint-compatible formatting, then
regenerate the bundle.

Source: Linters/SAST tools

@cursor

cursor Bot commented Jul 16, 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.

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog 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