Repository navigation
Add Feed sidebar + cmux feed-hook + OpenCode plugin (workstream MVP) - #3057
Conversation
New Swift package Packages/CMUXWorkstream declares .iOS(.v18) + .macOS(.v14). Holds the workstream (Feed) data model, an append-only JSONL persistence actor, and a @mainactor @observable store with request-id correlation for routing blocking-hook replies. WorkstreamEvent mirrors Vibe Island's wire field names (session_id, hook_event_name, _source, tool_input, _opencode_request_id) so existing agent hook payloads flow through unchanged. Actionable kinds (permissionRequest, exitPlan, question) default to .pending; telemetry kinds default to .telemetry and are hidden from the Feed by default. 15 tests.
Wires Packages/CMUXWorkstream as an XCLocalSwiftPackageReference on the GhosttyTabs target, parallel to CMUXAuthCore. Enables importing `CMUXWorkstream` from Sources/. `xcodebuild -scheme cmux build` succeeds.
Adds six V2 verbs on the cmux socket:
- feed.push hook/plugin ingest; blocks for user decision when
wait_timeout_seconds > 0
- feed.permission.reply, feed.question.reply, feed.exit_plan.reply
resolve pending items from the UI
- feed.jump focus-intent verb (stub resolution lands in the UI PR)
- feed.list pending/all snapshot for iOS + tests
FeedCoordinator owns the shared @mainactor WorkstreamStore singleton and
mediates between the socket worker thread and the main-actor store. It
also parks blocking hooks on a semaphore keyed by request_id so the
socket reply arrives after the user approves/denies in Feed.
Store is instantiated in AppDelegate.applicationDidFinishLaunching and
immediately starts replay-from-disk.
- `RightSidebarMode.feed` with SF Symbol `dot.radiowaves.left.and.right` and localized label "Feed". Persists through the existing FileExplorerState.mode → UserDefaults path. - `FeedPanelView` renders a segmented Actionable / All filter and a chronological list where pending items float above resolved. Rows receive immutable `FeedItemSnapshot` + closure action bundles, so the store is never referenced below the lazy layout boundary. - Inline rows: PermissionRow (Once/Always/All/Bypass/Deny), ExitPlanRow (Bypass/Auto-accept/Manual/Deny), QuestionRow (radio or checkbox + Submit), TelemetryRow (compact summary for the "All" filter).
New `cmux feed-hook --source <agent>` reads an agent hook JSON payload
from stdin and forwards it to the running cmux app via the `feed.push`
V2 socket verb. For Feed-actionable events (ExitPlanMode,
AskUserQuestion, PermissionRequest) it blocks up to 120s waiting for
the user's decision in the Feed sidebar, then emits the decision JSON
on stdout in each agent's expected format (Claude's
`{"decision": "approve", "systemMessage": "..."}`, Codex's
`{"decision": "approve", "remember": "session"}`, etc.).
Non-actionable events (PreToolUse for ordinary tools, PostToolUse,
Stop, etc.) are pushed as telemetry without blocking — they show up in
the Feed's "All" filter but the agent proceeds immediately. Outside a
cmux terminal (no CMUX_SURFACE_ID) the command silently no-ops so it's
safe to leave installed everywhere.
Installer wiring lands separately; for now users opt in by adding
`cmux feed-hook --source <agent>` as a PreToolUse entry in their
agent's hook config.
Replaces the native macOS Picker + .buttonStyle(.borderless) rows with the same pill-style buttons and row aesthetic used by SessionIndexView: - Control bar at 29pt height, horizontal 8 / vertical 3 padding, with GroupingButton-equivalent pill selection (icon + 11pt label, rounded corner 4, opacity-based hover/selection fills). - Rows use HStack(spacing: 6) + 12pt title + leading 32 / trailing 12 / vertical 4 padding, with the same 6px-inset rounded hover background that SessionRow uses. Icons are SF Symbols at 11pt weight medium, clamped to 12×12 like the agent asset icons. - Status tag chips (Pending / Resolved · mode / Expired) render as small rounded badges next to the relative timestamp, all in the same opacity/tint palette as the sidebar. - Action chips (Once / Always / All / Bypass / Deny for permissions, Bypass / Auto-accept / Manual / Deny for plan mode, Submit for questions) reuse the same FeedPillButton primitive, with Deny tinted red and hover fills matching the rest of the sidebar. - Telemetry rows collapse to a single monospaced summary line instead of a verbose multi-line block so "All" view stays scannable. No functional change to the coordinator, socket verbs, or feed-hook — purely UI polish to stop feeling like a generic system Picker.
- `AgentHookDef` grows a `feedHookEvents` list. For each entry we emit a second hook on that agent event invoking `cmux feed-hook --source <agent>` with a 120s timeout so user decisions don't trip the agent's default (5s–10s) hook timeout. Claude's wrapper, Codex, Gemini, Copilot, CodeBuddy, Factory, Qoder all get `PreToolUse` wired; Cursor gets `beforeShellExecution`. - Install/uninstall now scrub both the per-agent hook marker and the new `feedHookMarker` so reinstall is idempotent. - `runSetupHooks` gains a binary-detection gate (`command -v <agent>`) and skips agents missing on PATH, printing a summary line. - New `cmux opencode install-hooks [--project]` / `uninstall-hooks` verbs. `setup-hooks` invokes them conditionally when `opencode` is on PATH. - Adds `Resources/opencode-plugin.js` (bundled into cmux.app). The plugin connects to `$CMUX_SOCKET_PATH` (fallback `~/.config/cmux/cmux.sock`), maps OpenCode's event bus to `feed.push` frames, blocks for permission decisions via a 120s Promise.race, then acknowledges via `client.session.permissions`.
- Tab badge: red pill showing pending-item count on the Feed ModeBarButton in RightSidebarPanelView; feeds off `FeedCoordinator.shared.store?.pending.count`. - Jump: `FeedJumpResolver` parses `<agent>-<sessionId>` workstream ids, looks up `~/.cmuxterm/<agent>-hook-sessions.json` to recover `(workspaceId, surfaceId)`, posts `.feedRequestFocus`, and AppDelegate routes it through the existing workspace.select + surface.focus V2 verbs via new `TerminalController.handleSocketLine`. Double-tap a Feed row to jump. - Native notifications: when a blocking actionable item arrives and the app isn't key, post a `UNUserNotificationCenter` banner with inline action buttons on three new categories (CMUXFeedPermission, CMUXFeedExitPlan, CMUXFeedQuestion). Action callbacks route back to `FeedCoordinator.deliverReply`. First authorization request is lazy. - CLI: `cmux feed clear [--yes]` truncates `~/.cmuxterm/workstream.jsonl` for users who want to reset the persistent Feed history.
- Adds 23 Feed-related keys to Resources/Localizable.xcstrings with English + Japanese values (rightSidebar.mode.feed, feed.filter.*, feed.empty.*, feed.status.*, feed.badge.*, feed.permission.*, feed.exitplan.*, feed.question.*). - docs/feed.md: architecture diagram, install matrix per CLI, decision semantics per mode, timeout behavior, storage paths, jump gesture, troubleshooting. Linked from docs/notifications.md. - cmuxUITests/FeedSidebarUITests.swift: boots a tagged cmux with a dedicated socket, injects a synthetic PermissionRequest via feed.push, toggles the right sidebar to Feed, clicks Allow Once, and asserts the socket response carries status=resolved + mode=once. - Skipping #20 (affordance polish) as a ship-now call; filter chips can wait for a concrete user complaint.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a Feed subsystem: CLI feed/opencode commands and hook bridge, a new SPM package Changes
Sequence Diagram(s)sequenceDiagram
participant Agent as Agent (hook)
participant HookProc as feed-hook
participant Socket as cmux Socket
participant TC as TerminalController
participant FC as FeedCoordinator
participant Store as WorkstreamStore
participant UI as FeedPanelView
participant User as User
Agent->>HookProc: writes hook JSON on stdin
HookProc->>Socket: send V2 `feed.push` (event + request_id, wait_timeout)
Socket->>TC: receive frame -> dispatch handler
TC->>FC: ingestBlocking(event, waitTimeout)
FC->>Store: ingest(event)
Store-->>FC: item created (id, request_id)
FC->>UI: publish update (pending item)
User->>UI: select decision
UI->>FC: deliverReply(requestId, decision)
FC->>Store: markResolved(itemId, decision)
FC-->>TC: return resolved payload
TC-->>HookProc: handler returns decision JSON
HookProc->>Agent: writes decision JSON to stdout
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Before: if a user hand-edited hooks.json to add a custom command inside the same nested group as a cmux hook, install/uninstall kept the entire group untouched (preserving the user's command but also leaving the stale cmux hook in place), then appended a fresh cmux group — leading to duplicate cmux entries on reinstall. After: we walk each group's `hooks` array and strip only the cmux-owned entries per-command. Groups whose only content was cmux-owned drop entirely; groups with any user command survive with the cmux entries pruned. Uninstall applies the same rule. User content is never touched. Also preserves: - every top-level key in hooks.json / settings.json besides `hooks` - non-cmux entries under the same agentEvent - groups with unknown shape (passed through verbatim) - `codex_hooks` line behavior in ~/.codex/config.toml (only that line is touched, rest of the TOML preserved) - any pre-existing OpenCode plugin file whose header lacks the cmux marker (install refuses to overwrite; uninstall skips it)
Greptile SummaryAdds the Feed workstream MVP: a new right-sidebar panel surfacing AI agent permission requests,
Confidence Score: 3/5Not safe to merge — the deadlock in Two P1 bugs present: deadlock on all UI action paths and socket threading policy violation in the telemetry hot path. The rest of the PR (store, CLI, plugin, socket verbs, view snapshot boundary) is well-implemented. Sources/Feed/FeedCoordinator.swift (both P1 bugs), Sources/AppDelegate.swift (notification handler deadlock path) Important Files Changed
Sequence DiagramsequenceDiagram
participant Agent as AI Agent Hook
participant CLI as cmux feed-hook
participant Socket as Socket Worker Thread
participant Coord as FeedCoordinator
participant Store as WorkstreamStore (MainActor)
participant UI as FeedPanelView (MainActor)
participant Notif as UNUserNotificationCenter
Agent->>CLI: stdin hook JSON
CLI->>Socket: feed.push (wait_timeout=120s)
Socket->>Coord: ingestBlocking(event, waitTimeout=120)
Coord->>Store: DispatchQueue.main.sync → store.ingest(event)
Store-->>UI: @Observable triggers FeedListView re-render
Coord->>Socket: blocks on DispatchSemaphore
alt App is not key window
Coord->>Notif: postFeedNotification(event)
Notif-->>UI: banner with Allow/Deny actions
end
alt User clicks in Feed UI
UI->>Coord: Task @MainActor → deliverReply(requestId, decision)
Note over Coord: ⚠️ DispatchQueue.main.sync from main → DEADLOCK
else User taps notification action
Notif->>Coord: handleFeedNotificationResponse → deliverReply(...)
Note over Coord: ⚠️ Called on main thread → DEADLOCK
else feed.*.reply via socket (safe path)
Socket->>Coord: deliverReply(requestId, decision)
Coord->>Store: DispatchQueue.main.sync → markResolved
Coord->>Socket: semaphore.signal()
Socket-->>CLI: {status: resolved, decision: {...}}
CLI-->>Agent: stdout decision JSON
end
Reviews (1): Last reviewed commit: "Per-hook filter so user's hooks survive ..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 11
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (11)
Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamEvent.swift-9-10 (1)
9-10:⚠️ Potential issue | 🟡 MinorEither implement
extraFieldsJSONor remove this contract.The docs promise unknown-field preservation, but
WorkstreamEventhas noextraFieldsJSONproperty andCodingKeysdrops unknown keys during decode/encode. This makes the forward-compatibility guarantee false.🛠️ Minimal doc fix if preservation is not implemented yet
-/// `_opencode_request_id`. Unknown fields are preserved verbatim in -/// `extraFieldsJSON` for forward compatibility. +/// `_opencode_request_id`.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamEvent.swift` around lines 9 - 10, The docs promise preservation of unknown fields but WorkstreamEvent lacks an extraFieldsJSON property and its CodingKeys drop unknown keys; either add an extraFieldsJSON storage and implement custom init(from:) and encode(to:) on WorkstreamEvent to capture and re-emit unknown keys (read the keyed container, iterate container.allKeys, decode known keys via CodingKeys and decode unknown keys into a JSON-compatible representation stored in extraFieldsJSON) or remove the forward-compatibility statement and any references to extraFieldsJSON from the documentation and comments; locate the WorkstreamEvent type and its CodingKeys enum to apply the change.Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamEvent.swift-153-164 (1)
153-164:⚠️ Potential issue | 🟡 MinorUse 64-bit integer extraction for JSON numbers.
NSNumber.intValuetruncates when the value exceeds the platform'sNSIntegerrange. While target platforms (iOS 18+, macOS 14+) are 64-bit, whereNSIntegerand SwiftIntare bothInt64, explicitly usingint64Valueis more defensive and clarifies intent. Additionally, add a regression test with a value aboveInt32.maxto catch any future platform changes.Proposed fix
- return .int(n.intValue) + return .int(Int(n.int64Value))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamEvent.swift` around lines 153 - 164, In fromAny(_:) replace the current Int extraction that uses NSNumber.intValue with a 64-bit extraction to avoid truncation: when handling NSNumber (the branch that currently returns .int(n.intValue)) use n.int64Value (or convert to Swift Int from n.int64Value) so JSON integers are preserved on 64-bit platforms; update the AnyJSON case or initializer if needed to accept Int64-backed values and adjust callers accordingly, and add a regression test that decodes/encodes a numeric value > Int32.max to ensure no truncation/regression.Sources/RightSidebarPanelView.swift-135-140 (1)
135-140:⚠️ Potential issue | 🟡 MinorLocalize the tooltip and use ICU plural forms for the pending count.
helpTextreturns the literal English"\(count) pending"with noString(localized:)wrapping and no pluralization. This violates the Swift localization guideline and the established.one/.otherconvention used elsewhere in the app (e.g.,statusMenu.unreadCount.one/other). JA/EN callers will all see the English word, and 1 vs n is not handled.🌐 Proposed fix using a pluralized xcstrings key
private var helpText: String { - if badgeCount > 0 { - return "\(mode.label) · \(badgeCount) pending" - } - return mode.label + guard badgeCount > 0 else { return mode.label } + let pending = String( + localized: "rightSidebar.mode.feed.pendingCount", + defaultValue: "\(badgeCount) pending" + ) + return "\(mode.label) · \(pending)" }Add
rightSidebar.mode.feed.pendingCounttoResources/Localizable.xcstringswith.one/.othervariations for EN and JA (e.g., EN.one: "1 pending",.other: "%lld pending").As per coding guidelines: "All user-facing strings must be localized using
String(localized: "key.name", defaultValue: "English text")and added toResources/Localizable.xcstringswith translations for all supported languages". Based on learnings, pluralized strings in this repo should use ICU.one/.otherkeys rather than a single format string.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/RightSidebarPanelView.swift` around lines 135 - 140, The helpText computed property currently returns an English literal for the pending count (using badgeCount and mode.label) and must be localized with ICU plural forms; change the branch that returns "\(mode.label) · \(badgeCount) pending" to build the localized pending-count string via String(localized: "rightSidebar.mode.feed.pendingCount", defaultValue: "%lld pending") using badgeCount as the argument (respecting .one/.other variants), then combine it with mode.label (which remains localized). Also add the pluralized key rightSidebar.mode.feed.pendingCount to Resources/Localizable.xcstrings with .one and .other entries for each supported language (follow the statusMenu.unreadCount.one/other pattern).Sources/AppDelegate.swift-12672-12712 (1)
12672-12712:⚠️ Potential issue | 🟡 MinorLocalize the feed notification action titles.
These inline notification button titles are user-facing. Please wrap them with
String(localized:defaultValue:)and add EN/JA entries toResources/Localizable.xcstrings.Proposed localization update
- UNNotificationAction(identifier: "feed.permission.once", title: "Allow Once"), - UNNotificationAction(identifier: "feed.permission.always", title: "Always"), + UNNotificationAction( + identifier: "feed.permission.once", + title: String(localized: "feed.notification.permission.allowOnce", defaultValue: "Allow Once") + ), + UNNotificationAction( + identifier: "feed.permission.always", + title: String(localized: "feed.notification.permission.always", defaultValue: "Always") + ), UNNotificationAction( - identifier: "feed.permission.deny", title: "Deny", + identifier: "feed.permission.deny", + title: String(localized: "feed.notification.permission.deny", defaultValue: "Deny"), options: [.destructive] ), @@ - UNNotificationAction(identifier: "feed.exit_plan.bypassPermissions", title: "Bypass"), - UNNotificationAction(identifier: "feed.exit_plan.autoAccept", title: "Auto-accept"), - UNNotificationAction(identifier: "feed.exit_plan.manual", title: "Manual"), + UNNotificationAction( + identifier: "feed.exit_plan.bypassPermissions", + title: String(localized: "feed.notification.exitPlan.bypass", defaultValue: "Bypass") + ), + UNNotificationAction( + identifier: "feed.exit_plan.autoAccept", + title: String(localized: "feed.notification.exitPlan.autoAccept", defaultValue: "Auto-accept") + ), + UNNotificationAction( + identifier: "feed.exit_plan.manual", + title: String(localized: "feed.notification.exitPlan.manual", defaultValue: "Manual") + ), @@ UNNotificationAction( - identifier: "feed.question.open", title: "Reply", + identifier: "feed.question.open", + title: String(localized: "feed.notification.question.reply", defaultValue: "Reply"), options: [.foreground] ),As per coding guidelines, “All user-facing strings must be localized using
String(localized: "key.name", defaultValue: "English text")and added toResources/Localizable.xcstringswith translations for all supported languages”.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/AppDelegate.swift` around lines 12672 - 12712, Wrap all user-facing notification action titles in the UNNotificationCategory declarations with String(localized:defaultValue:) (e.g., replace the literal "Allow Once", "Always", "Deny", "Bypass", "Auto-accept", "Manual", "Reply" used in the UNNotificationAction initializers inside the CMUXFeedPermission, CMUXFeedExitPlan, and CMUXFeedQuestion category setups) and add corresponding key entries to Resources/Localizable.xcstrings for EN and JA (use keys matching the action identifiers like "feed.permission.once", "feed.permission.always", "feed.permission.deny", "feed.exit_plan.bypassPermissions", "feed.exit_plan.autoAccept", "feed.exit_plan.manual", "feed.question.open" with English and Japanese translations).CLI/cmux.swift-1898-1900 (1)
1898-1900:⚠️ Potential issue | 🟡 MinorRegister the new commands with pre-socket help.
cmux feed --help,cmux feed clear --help, andcmux opencode --helpare intercepted bydispatchSubcommandHelpbefore these command blocks run, butsubcommandUsage(_:)has nofeed/opencodecases, so they print “Unknown command”. Add help cases and top-level usage entries instead of relying on the in-block help branch.🛠️ Proposed direction
private func subcommandUsage(_ command: String) -> String? { switch command { + case "feed": + return """ + Usage: cmux feed clear [--yes] + + Manage Feed history. + """ + case "opencode": + return """ + Usage: cmux opencode <install-hooks|uninstall-hooks> [--project] + + Manage the cmux OpenCode Feed plugin. + """ case "ping": return """Also applies to: 1906-1916
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLI/cmux.swift` around lines 1898 - 1900, dispatchSubcommandHelp is intercepting help for "feed" and "opencode" but subcommandUsage(_:) lacks cases for them, causing "Unknown command" to print; add switch cases for "feed" and "opencode" (and the nested "clear" help for feed) in subcommandUsage(_:) with the correct usage strings, and also add top-level usage entries for "feed" and "opencode" in the function that prints global usage so cmux feed --help, cmux feed clear --help, and cmux opencode --help show proper pre-socket help instead of falling through to the in-block help branch.CLI/cmux.swift-14516-14532 (1)
14516-14532:⚠️ Potential issue | 🟡 MinorCount OpenCode plugin operations in the setup summary.
When
cmux setup-hooks --agent opencodeinstalls successfully,countremains0, so the final “Done” line reports0 installed. Incrementcountafter successful OpenCode install/uninstall.🛠️ Proposed fix
if agentFilter == nil || agentFilter?.lowercased() == "opencode" { if isUninstall { do { try uninstallOpenCodePlugin() } catch { print(" opencode: \(error.localizedDescription)") + skipped += 1 + return } + count += 1 } else if Self.isBinaryOnPath("opencode") { do { try installOpenCodePlugin(projectLocal: false) } catch { print(" opencode: \(error.localizedDescription)") + skipped += 1 + return } + count += 1 } else { print(" opencode: skipped (binary not found on PATH)") skippedNoBinary.append("opencode")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLI/cmux.swift` around lines 14516 - 14532, The summary count isn't incremented when OpenCode is successfully installed/uninstalled, so update the block that handles agentFilter == nil || agentFilter?.lowercased() == "opencode" to increment the overall count variable after a successful operation: after try uninstallOpenCodePlugin() completes (no catch) and after try installOpenCodePlugin(projectLocal: false) completes (no catch). Do not increment on the skipped branch (where Self.isBinaryOnPath("opencode") is false) or inside the catch paths; only increment count when the try returns without throwing, keeping existing updates to skipped and skippedNoBinary intact.docs/feed.md-13-13 (1)
13-13:⚠️ Potential issue | 🟡 MinorAdd a language to the fenced diagram block.
markdownlintflags this as MD040. Usetextfor the ASCII architecture diagram.Proposed fix
-``` +```text🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/feed.md` at line 13, The fenced code block in docs/feed.md is missing a language specifier and triggers markdownlint MD040; update the opening fence for the ASCII architecture diagram by adding the language token "text" (i.e., change the opening "```" to "```text") so the diagram block is explicitly marked as text.docs/feed.md-138-138 (1)
138-138:⚠️ Potential issue | 🟡 MinorDon’t say notification banners post without authorization.
postFeedNotificationonly adds the request when authorization is already granted/provisional or the permission prompt is granted. If notifications are denied, no banner is posted.Proposed fix
-**Notifications aren't showing inline buttons.** The three Feed categories (`CMUXFeedPermission`, `CMUXFeedExitPlan`, `CMUXFeedQuestion`) are registered at app launch. On first Feed use, macOS may prompt for notification authorization; without it the banner still posts but without action buttons. +**Notifications aren't showing inline buttons.** The three Feed categories (`CMUXFeedPermission`, `CMUXFeedExitPlan`, `CMUXFeedQuestion`) are registered at app launch. On first Feed use, macOS may prompt for notification authorization; if authorization is denied, Feed rows still appear in the sidebar but no native banner is delivered.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/feed.md` at line 138, Docs incorrectly claim banners post without authorization; update the text that follows the Feed category registration (mentions CMUXFeedPermission, CMUXFeedExitPlan, CMUXFeedQuestion) to state that postFeedNotification only enqueues a UNNotificationRequest when authorization is already granted or provisional, or when the permission prompt is accepted, and that if the user denies notifications no banner will be posted; remove or reword the sentence asserting banners still post without authorization and add a brief note describing the denied behavior.docs/feed.md-48-48 (1)
48-48:⚠️ Potential issue | 🟡 MinorCorrect the scrollback claim for evicted Feed items.
The store keeps only the most recent ring-buffer items in memory; older JSONL entries are disk-only unless a paging/load path exists. “Older items only surface if you scroll back” overpromises current behavior.
Proposed fix
-All events (actionable and telemetry) are appended to `~/.cmuxterm/workstream.jsonl` for audit. Memory holds the most recent 2000 items in a ring; older items only surface if you scroll back. +All events (actionable and telemetry) are appended to `~/.cmuxterm/workstream.jsonl` for audit. Memory holds the most recent 2000 items in a ring; older items remain available in the JSONL audit log.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/feed.md` at line 48, The doc line overstates scrollback behavior; update the sentence in docs/feed.md that mentions "~/.cmuxterm/workstream.jsonl" and the "ring" of 2000 items to clearly state that only the most recent 2000 items are kept in-memory and older entries reside on disk (workstream.jsonl) and will not be available unless an explicit paging/load path is implemented; replace "Older items only surface if you scroll back" with wording that older items are disk-only unless a paging/load mechanism is provided.Sources/Feed/FeedPanelView.swift-323-354 (1)
323-354:⚠️ Potential issue | 🟡 MinorLocalize the remaining Feed row labels and telemetry summaries.
Visible labels like
Question,session start,session end,stop,error,ok, and the todos count sentence bypass localization; raw decision mode values also expose wire names likeautoAccept.Suggested direction
- return "\(snapshot.source.rawValue.capitalized) · Question" + return "\(snapshot.source.rawValue.capitalized) · \(String(localized: "feed.kind.question", defaultValue: "Question"))" ... - case .sessionStart: return "session start" - case .sessionEnd: return "session end" + case .sessionStart: + return String(localized: "feed.telemetry.sessionStart", defaultValue: "session start") + case .sessionEnd: + return String(localized: "feed.telemetry.sessionEnd", defaultValue: "session end")Also use localized display labels for
WorkstreamPermissionMode/WorkstreamExitPlanMode, and plural-aware keys for the todos summary. As per coding guidelines, "All user-facing strings must be localized usingString(localized: "key.name", defaultValue: "English text")and added toResources/Localizable.xcstringswith translations for all supported languages".Also applies to: 552-568
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Feed/FeedPanelView.swift` around lines 323 - 354, The Feed row UI currently uses hard-coded English strings (e.g., "Question", "ExitPlanMode", timestamp labels in helpText, telemetry summaries like "session start"/"session end"/"stop"/"error"/"ok", todo counts and raw WorkstreamDecision mode values such as "autoAccept") — update primaryTitle, helpText, and resolvedBadgeLabel to use String(localized: "key.name", defaultValue: "English text") for each visible label; map WorkstreamPermissionMode and WorkstreamExitPlanMode raw values to localized display labels (use a helper or computed property on those enums), replace the todos summary with a plural-aware localized key, and ensure any telemetry summary labels around lines referenced (the other feed row code) are similarly localized using the same pattern so no user-facing string remains hard-coded.cmuxUITests/FeedSidebarUITests.swift-172-175 (1)
172-175:⚠️ Potential issue | 🟡 MinorHandle partial socket writes in the UI test helper.
send()is allowed to write fewer bytes than requested. Loop until the whole JSONL frame is sent, otherwise this test can flake by sending a truncatedfeed.push.Proposed fix
let data = line.data(using: .utf8)! - _ = data.withUnsafeBytes { bytes in - send(sockFd, bytes.baseAddress, data.count, 0) + try data.withUnsafeBytes { bytes in + guard let base = bytes.baseAddress else { return } + var sent = 0 + while sent < data.count { + let n = send(sockFd, base.advanced(by: sent), data.count - sent, 0) + if n <= 0 { + throw NSError( + domain: "FeedPush", + code: 3, + userInfo: [NSLocalizedDescriptionKey: "send() failed errno=\(errno)"] + ) + } + sent += n + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmuxUITests/FeedSidebarUITests.swift` around lines 172 - 175, The test helper currently calls send(sockFd, bytes.baseAddress, data.count, 0) once and assumes all bytes are written; change this to loop until the entire buffer for `data` is sent by repeatedly calling `send` (from the block using `data.withUnsafeBytes`) and advancing the pointer/remaining byte count by the returned `ssize_t`; handle short writes by retrying, retry on EINTR, and treat negative return values as errors (failing the test or throwing) so `feed.push` JSONL frames are never truncated when using `sockFd`, `line`, `data.withUnsafeBytes` and `send`.
🧹 Nitpick comments (2)
Packages/CMUXWorkstream/Tests/CMUXWorkstreamTests/WorkstreamStoreTests.swift (1)
42-54: Optional: also assertpendingis drained after expiration.The test verifies the item's status transitions to
.expired, but it would be stronger to also assertstore.pending.isEmptyso a future regression in thependingfilter (e.g., treating.expiredas still pending) is caught here. Low priority — current coverage is sufficient for the core behavior.♻️ Proposed strengthening
clock.advance(200) store.expirePending(olderThan: 60) + `#expect`(store.pending.isEmpty) if case .expired = store.items[0].status { // ok } else { Issue.record("expected .expired status after timeout") }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Packages/CMUXWorkstream/Tests/CMUXWorkstreamTests/WorkstreamStoreTests.swift` around lines 42 - 54, Add an assertion after calling store.expirePending(olderThan:) to ensure the pending collection is drained: verify store.pending.isEmpty (or equivalent) in the WorkstreamStoreTests.expirePending test so that when the item's status becomes .expired the pending queue no longer contains it; this complements the existing check of store.items[0].status and guards against regressions in WorkstreamStore.expirePending handling.CLI/cmux.swift (1)
13863-13868: Remove cmux hooks inside mixed nested groups.For nested configs,
allSatisfyonly removes groups where every hook is cmux-owned. If a group contains both a user hook and an older cmux hook, reinstall/uninstall leaves the cmux command behind and can duplicate entries on the next install. Filter the group’shookslist, then drop the group only if it becomes empty.♻️ Proposed direction
- groups.removeAll { group in - guard let hookList = group["hooks"] as? [[String: Any]] else { return false } - return hookList.allSatisfy { isCmuxOwnedCommand($0["command"] as? String ?? "") } - } + groups = groups.compactMap { group in + guard let hookList = group["hooks"] as? [[String: Any]] else { return group } + var nextGroup = group + let filteredHooks = hookList.filter { + !isCmuxOwnedCommand($0["command"] as? String ?? "") + } + guard !filteredHooks.isEmpty else { return nil } + nextGroup["hooks"] = filteredHooks + return nextGroup + }Also applies to: 13962-13969
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLI/cmux.swift` around lines 13863 - 13868, The current logic uses hookList.allSatisfy to drop a group only when every hook is cmux-owned, leaving mixed groups with cmux hooks intact; instead, iterate each group in groups and replace its "hooks" array with a filtered list that removes entries where isCmuxOwnedCommand((hook["command"] as? String) ?? "") is true, then remove the group entirely if its filtered "hooks" becomes empty and finally set hooks[event] = groups.isEmpty ? nil : groups; apply the same change to the other identical block handling groups/hooks (the code using variables groups, group, hookList, isCmuxOwnedCommand, and hooks[event]).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CLI/cmux.swift`:
- Around line 14291-14307: The code sets waitTimeout to 120s for actionable
feed.push but still calls client.send(command:) which uses the default 15s
receive timeout; modify SocketClient.send to accept an optional responseTimeout
parameter and if provided use it for the first read timeout, then update the
call site that builds the payload (where waitTimeout and line are defined) to
call client.send(command: line, responseTimeout: waitTimeout) so actionable
hooks use the 120s socket read window to match the server-side wait.
In `@Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamEvent.swift`:
- Around line 84-89: WorkstreamEvent currently decodes and stores tool_input
verbatim into toolInputJSON which then gets persisted by WorkstreamPersistence;
add redaction or allowlist filtering before persistence to avoid storing
sensitive shell commands/paths/credentials. Modify the persistence path (e.g.,
WorkstreamPersistence.persist... or the place that consumes
WorkstreamEvent.toolInputJSON) to run a sanitizer that mirrors the allowlist
behavior used by SessionRestoreCommandSettings.isCommandAllowed(_:), and ensure
WorkstreamEvent.toolInputJSON is replaced with the sanitized/filtered string (or
omitted) prior to writing JSONL; alternatively document the retention policy
clearly where WorkstreamPersistence is defined if you choose not to persist raw
tool_input.
In `@Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamPersistence.swift`:
- Around line 71-76: The clear() method is currently swallowing errors by using
try? when removing the file and closing the file handle; change it to propagate
failures instead: replace the try? calls with throwing tries (or capture and
rethrow a combined error) so that removeItem(at: fileURL) and handle.close()
return errors to the caller, ensure handle is still set to nil after
successful/failed close as appropriate, and update the function signature/flow
in WorkstreamPersistence.clear() so consumers (e.g., cmux feed clear) receive a
failure when file removal or handle closing fails.
- Around line 48-66: loadRecent currently reads the entire file into memory
(Data(contentsOf: fileURL)) then applies suffix(limit), which can OOM for an
unbounded audit log and also doesn't guard limit <= 0; change loadRecent to
return early if limit <= 0 and implement tail-reading: open the file
(FileHandle), seek from the end and read fixed-size chunks backwards
accumulating bytes until you have at least limit newline-separated lines (or
reach BOF), then split the accumulated Data by 0x0A, take the last `limit` rows,
decode each with decoder.decode(WorkstreamItem.self, from:), and append to out;
keep the existing behavior of silently dropping malformed lines and use fileURL
and decoder as in the original function.
In `@Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamStore.swift`:
- Around line 236-245: The switch in WorkstreamStore.swift is discarding
payloads for .userPromptSubmit, .stop/.subagentStop, and .todoWrite by returning
empty values; update those branches to forward the associated values into the
feed entries so telemetry is preserved: for .userPromptSubmit return
(.userPrompt, .userPrompt(text: <use the associated prompt text>)), for .stop
and .subagentStop return (.stop, .stop(reason: <use the associated stop
reason>)), and for .todoWrite return (.todos, .todos(<use the associated todo
array>)); keep .sessionStart and .sessionEnd as-is. Ensure you reference the
enum cases exactly (.userPromptSubmit, .userPrompt, .stop(reason:),
.subagentStop, .todoWrite, .todos) when locating and changing the switch.
In `@Resources/opencode-plugin.js`:
- Around line 34-38: The resolver currently correlates responses using
msg?.result?.request_id or msg?.request_id, but FeedCoordinator waits on
event.requestId and the permission frame may use _opencode_request_id or the
top-level id; update the correlation to derive requestId from (in order)
msg?.id, msg?._opencode_request_id, or msg?.result?.request_id so
pending.get(requestId) / pending.delete(requestId) / resolver(...) match
FeedCoordinator's event.requestId; apply the same change at both correlation
sites (the block handling pending resolution around the variables requestId,
pending, and resolver).
- Around line 45-58: The write() path currently swallows failures and leaves
pushBlocking() waiters stuck; modify write() and the connection error/close
handling so failures immediately fail any pending blocking waits (e.g., the
pushBlocking pendingPromises/pending map used by pushBlocking) instead of
letting them time out. Specifically: in write(frame) after if (!client) client =
connect(); if client is still null, immediately iterate pending blocking
requests and reject them with a clear error; likewise, catch write errors and in
the catch block reject/fail all pending pushBlocking waiters and clear their
timers; also factor the duplicate logic into a helper (e.g.,
failAllPendingWithError(error)) and call it from conn.on("close"),
conn.on("error"), and write() error paths so pending pushBlocking calls fail
fast when the cmux socket is unavailable.
In `@Sources/Feed/FeedCoordinator.swift`:
- Around line 284-307: The notification construction uses raw, non-localized
strings and includes sensitive data (event.cwd and event.toolInputJSON) that can
leak in system banners; update the switch handling of event.hookEventName to
replace hard-coded titles and bodies with localized values via String(localized:
"key", defaultValue: "...") for each case (e.g., keys for
CMUXFeedPermission.title/body, CMUXFeedExitPlan.title/body,
CMUXFeedQuestion.title/body) and redact or omit sensitive fields (never directly
interpolate event.cwd or event.toolInputJSON into the notification body—replace
with a generic localized placeholder like "Decision needed" or "Review required"
or move sensitive details into the app-only payload). Ensure the code paths that
set content.title, content.body, and content.categoryIdentifier use these
localized and redacted values and add the corresponding keys to
Resources/Localizable.xcstrings.
- Around line 94-108: The synchronous DispatchQueue.main.sync causes deadlock
when deliverReply is called from the main actor; change deliverReply to be async
and replace the DispatchQueue.main.sync { MainActor.assumeIsolated { ... } }
block with await MainActor.run { ... } so the work runs on the main actor
without blocking. Specifically, update the signature of deliverReply to async,
and inside use await MainActor.run { let store = FeedCoordinator.shared.store;
guard let store else { return }; if let itemId = Self.findItemId(for: requestId,
in: store.items) { store.markResolved(itemId, decision: decision) } } ensuring
deliverReply, Self.findItemId, and store.markResolved are called from the main
actor context asynchronously.
In `@Sources/TerminalController.swift`:
- Around line 7123-7124: In v2FeedPush validate the caller-controlled
wait_timeout_seconds instead of defaulting nonnumeric to 0: extract the raw
value from params, ensure it is a numeric Double, isFinite, and within an
allowed window (e.g. >= 0 and <= MAX_WAIT_TIMEOUT), and if not return
V2CallResult.invalid_params with a clear message; if it is valid, use the
clamped/capped timeout value (replace the current waitTimeout assignment).
Ensure the check is done inside the v2FeedPush function and reference the
waitTimeout variable and V2CallResult.invalid_params for the failure path.
---
Minor comments:
In `@CLI/cmux.swift`:
- Around line 1898-1900: dispatchSubcommandHelp is intercepting help for "feed"
and "opencode" but subcommandUsage(_:) lacks cases for them, causing "Unknown
command" to print; add switch cases for "feed" and "opencode" (and the nested
"clear" help for feed) in subcommandUsage(_:) with the correct usage strings,
and also add top-level usage entries for "feed" and "opencode" in the function
that prints global usage so cmux feed --help, cmux feed clear --help, and cmux
opencode --help show proper pre-socket help instead of falling through to the
in-block help branch.
- Around line 14516-14532: The summary count isn't incremented when OpenCode is
successfully installed/uninstalled, so update the block that handles agentFilter
== nil || agentFilter?.lowercased() == "opencode" to increment the overall count
variable after a successful operation: after try uninstallOpenCodePlugin()
completes (no catch) and after try installOpenCodePlugin(projectLocal: false)
completes (no catch). Do not increment on the skipped branch (where
Self.isBinaryOnPath("opencode") is false) or inside the catch paths; only
increment count when the try returns without throwing, keeping existing updates
to skipped and skippedNoBinary intact.
In `@cmuxUITests/FeedSidebarUITests.swift`:
- Around line 172-175: The test helper currently calls send(sockFd,
bytes.baseAddress, data.count, 0) once and assumes all bytes are written; change
this to loop until the entire buffer for `data` is sent by repeatedly calling
`send` (from the block using `data.withUnsafeBytes`) and advancing the
pointer/remaining byte count by the returned `ssize_t`; handle short writes by
retrying, retry on EINTR, and treat negative return values as errors (failing
the test or throwing) so `feed.push` JSONL frames are never truncated when using
`sockFd`, `line`, `data.withUnsafeBytes` and `send`.
In `@docs/feed.md`:
- Line 13: The fenced code block in docs/feed.md is missing a language specifier
and triggers markdownlint MD040; update the opening fence for the ASCII
architecture diagram by adding the language token "text" (i.e., change the
opening "```" to "```text") so the diagram block is explicitly marked as text.
- Line 138: Docs incorrectly claim banners post without authorization; update
the text that follows the Feed category registration (mentions
CMUXFeedPermission, CMUXFeedExitPlan, CMUXFeedQuestion) to state that
postFeedNotification only enqueues a UNNotificationRequest when authorization is
already granted or provisional, or when the permission prompt is accepted, and
that if the user denies notifications no banner will be posted; remove or reword
the sentence asserting banners still post without authorization and add a brief
note describing the denied behavior.
- Line 48: The doc line overstates scrollback behavior; update the sentence in
docs/feed.md that mentions "~/.cmuxterm/workstream.jsonl" and the "ring" of 2000
items to clearly state that only the most recent 2000 items are kept in-memory
and older entries reside on disk (workstream.jsonl) and will not be available
unless an explicit paging/load path is implemented; replace "Older items only
surface if you scroll back" with wording that older items are disk-only unless a
paging/load mechanism is provided.
In `@Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamEvent.swift`:
- Around line 9-10: The docs promise preservation of unknown fields but
WorkstreamEvent lacks an extraFieldsJSON property and its CodingKeys drop
unknown keys; either add an extraFieldsJSON storage and implement custom
init(from:) and encode(to:) on WorkstreamEvent to capture and re-emit unknown
keys (read the keyed container, iterate container.allKeys, decode known keys via
CodingKeys and decode unknown keys into a JSON-compatible representation stored
in extraFieldsJSON) or remove the forward-compatibility statement and any
references to extraFieldsJSON from the documentation and comments; locate the
WorkstreamEvent type and its CodingKeys enum to apply the change.
- Around line 153-164: In fromAny(_:) replace the current Int extraction that
uses NSNumber.intValue with a 64-bit extraction to avoid truncation: when
handling NSNumber (the branch that currently returns .int(n.intValue)) use
n.int64Value (or convert to Swift Int from n.int64Value) so JSON integers are
preserved on 64-bit platforms; update the AnyJSON case or initializer if needed
to accept Int64-backed values and adjust callers accordingly, and add a
regression test that decodes/encodes a numeric value > Int32.max to ensure no
truncation/regression.
In `@Sources/AppDelegate.swift`:
- Around line 12672-12712: Wrap all user-facing notification action titles in
the UNNotificationCategory declarations with String(localized:defaultValue:)
(e.g., replace the literal "Allow Once", "Always", "Deny", "Bypass",
"Auto-accept", "Manual", "Reply" used in the UNNotificationAction initializers
inside the CMUXFeedPermission, CMUXFeedExitPlan, and CMUXFeedQuestion category
setups) and add corresponding key entries to Resources/Localizable.xcstrings for
EN and JA (use keys matching the action identifiers like "feed.permission.once",
"feed.permission.always", "feed.permission.deny",
"feed.exit_plan.bypassPermissions", "feed.exit_plan.autoAccept",
"feed.exit_plan.manual", "feed.question.open" with English and Japanese
translations).
In `@Sources/Feed/FeedPanelView.swift`:
- Around line 323-354: The Feed row UI currently uses hard-coded English strings
(e.g., "Question", "ExitPlanMode", timestamp labels in helpText, telemetry
summaries like "session start"/"session end"/"stop"/"error"/"ok", todo counts
and raw WorkstreamDecision mode values such as "autoAccept") — update
primaryTitle, helpText, and resolvedBadgeLabel to use String(localized:
"key.name", defaultValue: "English text") for each visible label; map
WorkstreamPermissionMode and WorkstreamExitPlanMode raw values to localized
display labels (use a helper or computed property on those enums), replace the
todos summary with a plural-aware localized key, and ensure any telemetry
summary labels around lines referenced (the other feed row code) are similarly
localized using the same pattern so no user-facing string remains hard-coded.
In `@Sources/RightSidebarPanelView.swift`:
- Around line 135-140: The helpText computed property currently returns an
English literal for the pending count (using badgeCount and mode.label) and must
be localized with ICU plural forms; change the branch that returns
"\(mode.label) · \(badgeCount) pending" to build the localized pending-count
string via String(localized: "rightSidebar.mode.feed.pendingCount",
defaultValue: "%lld pending") using badgeCount as the argument (respecting
.one/.other variants), then combine it with mode.label (which remains
localized). Also add the pluralized key rightSidebar.mode.feed.pendingCount to
Resources/Localizable.xcstrings with .one and .other entries for each supported
language (follow the statusMenu.unreadCount.one/other pattern).
---
Nitpick comments:
In `@CLI/cmux.swift`:
- Around line 13863-13868: The current logic uses hookList.allSatisfy to drop a
group only when every hook is cmux-owned, leaving mixed groups with cmux hooks
intact; instead, iterate each group in groups and replace its "hooks" array with
a filtered list that removes entries where isCmuxOwnedCommand((hook["command"]
as? String) ?? "") is true, then remove the group entirely if its filtered
"hooks" becomes empty and finally set hooks[event] = groups.isEmpty ? nil :
groups; apply the same change to the other identical block handling groups/hooks
(the code using variables groups, group, hookList, isCmuxOwnedCommand, and
hooks[event]).
In
`@Packages/CMUXWorkstream/Tests/CMUXWorkstreamTests/WorkstreamStoreTests.swift`:
- Around line 42-54: Add an assertion after calling
store.expirePending(olderThan:) to ensure the pending collection is drained:
verify store.pending.isEmpty (or equivalent) in the
WorkstreamStoreTests.expirePending test so that when the item's status becomes
.expired the pending queue no longer contains it; this complements the existing
check of store.items[0].status and guards against regressions in
WorkstreamStore.expirePending handling.
🪄 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: cb1a25f5-aa62-4fd6-a303-c5b87dba2117
📒 Files selected for processing (26)
CLI/cmux.swiftGhosttyTabs.xcodeproj/project.pbxprojPackages/CMUXWorkstream/Package.swiftPackages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamAction.swiftPackages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamEvent.swiftPackages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamItem.swiftPackages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamKind.swiftPackages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamPayload.swiftPackages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamPersistence.swiftPackages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamSource.swiftPackages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamStore.swiftPackages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamTransport.swiftPackages/CMUXWorkstream/Tests/CMUXWorkstreamTests/WorkstreamEventTests.swiftPackages/CMUXWorkstream/Tests/CMUXWorkstreamTests/WorkstreamItemTests.swiftPackages/CMUXWorkstream/Tests/CMUXWorkstreamTests/WorkstreamPersistenceTests.swiftPackages/CMUXWorkstream/Tests/CMUXWorkstreamTests/WorkstreamStoreTests.swiftResources/Localizable.xcstringsResources/opencode-plugin.jsSources/AppDelegate.swiftSources/Feed/FeedCoordinator.swiftSources/Feed/FeedPanelView.swiftSources/RightSidebarPanelView.swiftSources/TerminalController.swiftcmuxUITests/FeedSidebarUITests.swiftdocs/feed.mddocs/notifications.md
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
CLI/cmux.swift (1)
14313-14328:⚠️ Potential issue | 🟠 MajorMatch the socket read timeout to the Feed decision wait.
Actionable Feed hooks ask the server to wait up to 120s, but
client.send(command:)still uses the default 15s receive timeout, so the hook can fall back to{}while the server is still waiting.🐛 Proposed direction
- response = try client.send(command: line) + response = try client.send( + command: line, + responseTimeout: waitTimeout > 0 ? waitTimeout + 5 : nil + )Add the optional
responseTimeoutparameter toSocketClient.sendand use it for the first response read timeout.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLI/cmux.swift` around lines 14313 - 14328, The feed.push call sets wait_timeout_seconds to 120 for actionable hooks but the socket read still uses the SocketClient.send default 15s timeout, causing premature {} responses; update the SocketClient.send API to accept an optional responseTimeout parameter and wire it through so the first read uses that timeout, then call client.send(command: line, responseTimeout: waitTimeout) (or convert waitTimeout to seconds Double) where feed.push is sent; ensure the new parameter is used only for the initial response-read and preserve existing default behavior when nil.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CLI/cmux.swift`:
- Around line 14430-14438: The Codex branch currently converts mode "always" to
remember "session", losing persistent approval; update the mapping in the block
handling source == "codex" so that when mode is "always" or "bypass" the
out["remember"] value is set to "always", otherwise set it to "session"
(preserve the existing removal of "systemMessage" and assignment to
out["remember"]). Locate the conditional that checks mode and source (variables
mode, source and dictionary out) and replace the existing ternary logic (which
only maps "bypass" to "always") with logic that treats "always" and "bypass" as
persistent approvals.
- Around line 14298-14303: The "_ppid" value in the eventDict currently uses
ProcessInfo.processInfo.processIdentifier (the current process PID); change it
to send the parent PID instead by calling getppid() and converting to an Int
(i.e., set "_ppid" to Int(getppid())). Update the eventDict definition (the
dictionary built where session_id, hook_event_name, _source, and _ppid are set)
so that the "_ppid" entry uses getppid() rather than
ProcessInfo.processInfo.processIdentifier.
- Around line 14538-14557: The OpenCode branch fails to update the setup totals
because it never increments the shared counter; inside the block that handles
OpenCode (the if checking agentFilter and Self.isBinaryOnPath("opencode")),
increment the same `count` variable when installOpenCodePlugin(projectLocal:)
succeeds and when uninstallOpenCodePlugin() succeeds (i.e., after the try
completes), and ensure skipped/skippedNoBinary logic remains unchanged so the
final print("Done: \(count) ...") reflects OpenCode operations correctly.
- Around line 13781-13783: feedHookCommand currently builds a cmux feed-hook
invocation without passing the hook's configured event, so the feed
defaults/misclassifies to PreToolUse; update the command string in
feedHookCommand to include the configured event by adding the --event argument
with the hook def's agentEvent (e.g. append `--event \(def.agentEvent)` properly
quoted) so the installed hook forwards the correct event name; apply the same
change to the other similar invocations referenced (the blocks at the other
locations) that build cmux feed-hook commands.
- Around line 2818-2827: runFeedHook currently checks the CMUX_SURFACE_ID guard
only after calling client.connect()/authentication which causes external hooks
to fail with socket errors; modify runFeedHook so the CMUX_SURFACE_ID presence
check happens immediately at the start (before any client.connect() or auth
calls) and short-circuit by returning the same empty hook response (i.e. `{}`
behavior used by other hook bridges) and recording the appropriate telemetry
breadcrumb (e.g., "feed-hook.no-surface" or reuse existing breadcrumb flow)
instead of opening the socket; reference the runFeedHook function,
CMUX_SURFACE_ID guard, and client.connect()/authentication call sites to locate
and update the logic.
---
Duplicate comments:
In `@CLI/cmux.swift`:
- Around line 14313-14328: The feed.push call sets wait_timeout_seconds to 120
for actionable hooks but the socket read still uses the SocketClient.send
default 15s timeout, causing premature {} responses; update the
SocketClient.send API to accept an optional responseTimeout parameter and wire
it through so the first read uses that timeout, then call client.send(command:
line, responseTimeout: waitTimeout) (or convert waitTimeout to seconds Double)
where feed.push is sent; ensure the new parameter is used only for the initial
response-read and preserve existing default behavior when nil.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
`cmux setup-hooks` (and all per-agent install-hooks paths + OpenCode
plugin + codex config.toml) now prints a colored unified diff instead
of dumping the full new content. Output is framed with the target
path both above and below the diff so the user always knows which
file is about to be written regardless of scrollback position:
─── Will write to /Users/…/.codex/hooks.json ───
--- old
+++ new
@@ -42,0 +43,7 @@
+ {
+ "command": "[ -n \"$CMUX_SURFACE_ID\" ] && … cmux feed-hook --source codex …",
+ "timeout": 120000,
+ "type": "command"
+ }
─── The above will be written to /Users/…/.codex/hooks.json ───
Colors (only when stdout is a tty, via isatty check):
- +/- lines green/red
- @@ hunk headers cyan
- file headers dim
- JSON strings cyan, numbers yellow, booleans/null magenta for
fallback when diff isn't available (new file, diff missing)
Shells out to /usr/bin/diff -u against temp files; gracefully falls
back to the full pretty-printed content if diff isn't on the system.
Applies to agent hook files, codex config.toml, and the OpenCode
plugin JS.
- Bold the +/- lines in addition to coloring them, so additions and deletions stand out even when the diff scrolls past quickly. - Prepend a summary line "+N additions, -M deletions" so users see the shape of the change before scanning for specific entries. - Swap the "Will write to" header to "Will create" when the target file doesn't exist, so the banner matches reality. - Handle the no-op case explicitly with "(no changes — file already matches target)" instead of dumping full content. - When diff isn't available (binary missing, temp write failure), prefix every fallback line with a bold green `+` so the user still sees which content is landing.
There was a problem hiding this comment.
13 issues found across 26 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamPersistence.swift">
<violation number="1" location="Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamPersistence.swift:52">
P2: `loadRecent(limit:)` loads the full JSONL file into memory, which does not scale with the unbounded log design.</violation>
<violation number="2" location="Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamPersistence.swift:72">
P2: `clear()` swallows filesystem errors with `try?`, so callers can get a false success when the file was not actually cleared.</violation>
</file>
<file name="Sources/RightSidebarPanelView.swift">
<violation number="1" location="Sources/RightSidebarPanelView.swift:137">
P3: Localize the `helpText` pending tooltip instead of hard-coding the English word "pending".</violation>
</file>
<file name="Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamItem.swift">
<violation number="1" location="Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamItem.swift:62">
P2: Validate or normalize explicit `status` in the initializer; as written, non-actionable items can be initialized with actionable statuses.</violation>
</file>
<file name="Resources/opencode-plugin.js">
<violation number="1" location="Resources/opencode-plugin.js:34">
P1: Response correlation ignores V2 response `id`, so resolved `feed.push` replies are not matched to pending requests.</violation>
<violation number="2" location="Resources/opencode-plugin.js:45">
P2: Socket close/error handlers do not clear buffered state or pending waiters, causing unnecessary 120s stalls after disconnects.</violation>
</file>
<file name="cmuxUITests/FeedSidebarUITests.swift">
<violation number="1" location="cmuxUITests/FeedSidebarUITests.swift:154">
P2: Replace assertion-based error handling with a thrown error in this throwing socket helper so failures abort immediately.
(Based on your team's feedback about throwing timeout/errors in async test helpers.) [FEEDBACK_USED]</violation>
</file>
<file name="Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamEvent.swift">
<violation number="1" location="Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamEvent.swift:62">
P2: Unknown event fields are dropped instead of preserved, breaking the forward-compatibility contract described for this type.</violation>
<violation number="2" location="Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamEvent.swift:105">
P2: `tool_input` is silently dropped when `toolInputJSON` is non-JSON text; encode should fall back to encoding the raw string.</violation>
</file>
<file name="Sources/Feed/FeedCoordinator.swift">
<violation number="1" location="Sources/Feed/FeedCoordinator.swift:56">
P1: `deliverReply` can deadlock because it always calls `DispatchQueue.main.sync`, even when already running on the main actor.</violation>
</file>
<file name="Sources/AppDelegate.swift">
<violation number="1" location="Sources/AppDelegate.swift:7595">
P1: Feed focus routing uses wrong V2 parameter names (`workspace`/`surface`), so `workspace.select` and `surface.focus` reject the request and focus does not occur.</violation>
</file>
<file name="Sources/TerminalController.swift">
<violation number="1" location="Sources/TerminalController.swift:2160">
P2: Add the new `feed.*` RPC methods to `system.capabilities`; otherwise capability-driven clients won’t discover supported feed commands.</violation>
</file>
<file name="Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamStore.swift">
<violation number="1" location="Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamStore.swift:38">
P2: Remove the unused request-id mapping dictionaries (`requestIdToItemId` and `itemIdToRequestId`) and their associated lifecycle code.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
CLI/cmux.swift (3)
14495-14510:⚠️ Potential issue | 🟠 MajorMatch the socket timeout to the 120-second Feed wait.
feed.pushis asked to wait up to 120 seconds, butclient.send(command:)still uses the CLI default receive timeout of 15 seconds. Any user decision after ~15 seconds is treated as a socket failure and degrades to{}.🐛 Proposed direction
- response = try client.send(command: line) + response = try client.send( + command: line, + responseTimeout: isActionable ? waitTimeout + 5 : nil + )This also needs a
SocketClient.send(command:responseTimeout:)overload or equivalent per-call receive timeout path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLI/cmux.swift` around lines 14495 - 14510, The socket receive timeout for feed.push must match the wait timeout (isActionable ? 120 : 0); add a per-call receive-timeout path to the SocketClient API (e.g., add or expose SocketClient.send(command:responseTimeout:) or similar) and change the call site that currently invokes client.send(command: line) inside the do block to use the new overload and pass the same waitTimeout (or a nonzero mapped value when waitTimeout>0) so the socket will wait up to 120 seconds for the feed.push response; update any related method signatures (SocketClient.send) and callers to support this per-call responseTimeout without altering global defaults.
2818-2827:⚠️ Potential issue | 🟠 MajorNo-op
feed-hookbefore opening the socket.The graceful outside-cmux guard inside
runFeedHookis reached only afterclient.connect()and authentication. If an installed hook fires outside cmux with no socket, this path errors instead of printing{}like the other hook handlers.🐛 Proposed fix
if ["copilot-hook", "codebuddy-hook", "factory-hook", "qoder-hook"].contains(command) { guard ProcessInfo.processInfo.environment["CMUX_SURFACE_ID"] != nil else { print("{}") return } } + + if command == "feed-hook" { + guard ProcessInfo.processInfo.environment["CMUX_SURFACE_ID"]?.isEmpty == false else { + print("{}") + return + } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLI/cmux.swift` around lines 2818 - 2827, The feed-hook path calls runFeedHook only after attempting client.connect()/auth, so when a hook fires outside cmux (no socket) it errors instead of returning the empty JSON; modify the "feed-hook" dispatch to detect the missing socket before calling runFeedHook (or move the graceful outside-cmux guard from inside runFeedHook to run before client.connect()), and when no socket is present print "{}" and return successfully; reference the existing case "feed-hook" branch and the runFeedHook function and the client.connect() call so the check is applied prior to connection/auth.
14720-14739:⚠️ Potential issue | 🟡 MinorInclude OpenCode in the setup-hooks summary count.
OpenCode install/uninstall runs outside the
agentDefsloop, butcountis never incremented.cmux setup-hooks --agent opencodecan successfully install the plugin while reportingDone: 0 installed.🐛 Proposed fix
if isUninstall { do { try uninstallOpenCodePlugin() } catch { print(" opencode: \(error.localizedDescription)") } } else if Self.isBinaryOnPath("opencode") { do { try installOpenCodePlugin(projectLocal: false) } catch { print(" opencode: \(error.localizedDescription)") } + count += 1 } else {Also increment after a successful uninstall if you want uninstall summaries to include OpenCode.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLI/cmux.swift` around lines 14720 - 14739, The OpenCode branch does not update the overall summary `count`, so successful installs/uninstalls report as zero; inside the block guarded by `if agentFilter == nil || agentFilter?.lowercased() == "opencode"` increment `count` when `installOpenCodePlugin(projectLocal: false)` or `uninstallOpenCodePlugin()` completes successfully (and also increment for successful uninstall when `isUninstall` is true), leaving the existing error handling and the `skipped`/`skippedNoBinary` updates intact; reference `agentFilter`, `isUninstall`, `uninstallOpenCodePlugin()`, `installOpenCodePlugin(projectLocal:)`, `Self.isBinaryOnPath("opencode")`, `count`, `skipped`, and `skippedNoBinary` when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CLI/cmux.swift`:
- Around line 13698-13700: classifyFeedEvent currently lacks a branch for the
installed feed hook "beforeShellExecution", so those events fall through to
("PreToolUse", false) and cannot return a user decision; update the classifier
(function/classifyFeedEvent) to explicitly handle the "beforeShellExecution"
event and return the correct mapping (e.g., the intended event name and whether
it requires user decision) consistent with other feedHookEvents entries, or
alternatively remove "beforeShellExecution" from the feedHookEvents list if it
should be telemetry-only; reference the feedHookEvents array and the
classifyFeedEvent switch/lookup to add or remove the mapping accordingly.
- Around line 1891-1916: The generic help gate is catching "cmux feed --help"
and "cmux opencode --help" before the feed/opencode branches run because
subcommandUsage(_:) has no entries for those commands; update
subcommandUsage(_:) to include usage text for "feed" (e.g., "feed clear
[--yes]") and "opencode" (e.g., "opencode install-hooks|uninstall-hooks") or
modify the pre-socket help gate to bypass help handling when command == "feed"
or command == "opencode" so the existing handlers (runFeedClear,
runOpenCodeInstallHooks, runOpenCodeUninstallHooks) can execute and present the
intended help text.
---
Duplicate comments:
In `@CLI/cmux.swift`:
- Around line 14495-14510: The socket receive timeout for feed.push must match
the wait timeout (isActionable ? 120 : 0); add a per-call receive-timeout path
to the SocketClient API (e.g., add or expose
SocketClient.send(command:responseTimeout:) or similar) and change the call site
that currently invokes client.send(command: line) inside the do block to use the
new overload and pass the same waitTimeout (or a nonzero mapped value when
waitTimeout>0) so the socket will wait up to 120 seconds for the feed.push
response; update any related method signatures (SocketClient.send) and callers
to support this per-call responseTimeout without altering global defaults.
- Around line 2818-2827: The feed-hook path calls runFeedHook only after
attempting client.connect()/auth, so when a hook fires outside cmux (no socket)
it errors instead of returning the empty JSON; modify the "feed-hook" dispatch
to detect the missing socket before calling runFeedHook (or move the graceful
outside-cmux guard from inside runFeedHook to run before client.connect()), and
when no socket is present print "{}" and return successfully; reference the
existing case "feed-hook" branch and the runFeedHook function and the
client.connect() call so the check is applied prior to connection/auth.
- Around line 14720-14739: The OpenCode branch does not update the overall
summary `count`, so successful installs/uninstalls report as zero; inside the
block guarded by `if agentFilter == nil || agentFilter?.lowercased() ==
"opencode"` increment `count` when `installOpenCodePlugin(projectLocal: false)`
or `uninstallOpenCodePlugin()` completes successfully (and also increment for
successful uninstall when `isUninstall` is true), leaving the existing error
handling and the `skipped`/`skippedNoBinary` updates intact; reference
`agentFilter`, `isUninstall`, `uninstallOpenCodePlugin()`,
`installOpenCodePlugin(projectLocal:)`, `Self.isBinaryOnPath("opencode")`,
`count`, `skipped`, and `skippedNoBinary` when making the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
- installAgentHooks short-circuits when pretty-printed old content already matches new. Prints "<Agent> hooks already up to date at <path>" and moves on without prompting or writing. - FeedPillButton backgroundFill now matches SessionIndexView's GroupingButton: clear when unselected-and-unhovered, 0.05 hover, 0.10 selected. Fixes the always-visible bg on unselected segments of the Actionable / All filter.
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
Sources/Feed/FeedPanelView.swift (1)
161-163: Drop the redundant jump indirection.
FeedItemSnapshotalready carriesworkstreamId, and both switch branches return the same value. Removing the helper also avoids the misleading “snapshot doesn't carry it” comment.♻️ Proposed cleanup
-private func snapshotWorkstreamId(_ s: FeedItemSnapshot) -> String { - s.workstreamId -} - @@ .help(helpText) .onTapGesture(count: 2) { - actions.jump(workstreamIdForJump) + actions.jump(snapshot.workstreamId) } } - private var workstreamIdForJump: String { - // Store mirror of the workstream id; snapshot doesn't carry it - // directly because payloads do. Fall back to source-only when - // the item kind doesn't embed a session linkage. - switch snapshot.payload { - case .permissionRequest, .exitPlan, .question: - return snapshotWorkstreamId(snapshot) - default: - return snapshotWorkstreamId(snapshot) - } - } -Also applies to: 245-260
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Feed/FeedPanelView.swift` around lines 161 - 163, Remove the redundant helper snapshotWorkstreamId and inline FeedItemSnapshot.workstreamId wherever it’s used: delete the snapshotWorkstreamId(_ s: FeedItemSnapshot) function, replace its call sites with s.workstreamId, and remove any comments suggesting the snapshot lacks workstreamId; do the same cleanup for the duplicated helper variant used in the other location (the second helper around lines 245–260) so all callers use FeedItemSnapshot.workstreamId directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CLI/cmux.swift`:
- Around line 13965-13973: The preview is shown but never prompts the user to
confirm before writing; update the flow around Self.printInstallPreview so that
after printing the preview (when skipConfirm is false) you explicitly solicit a
yes/no confirmation (e.g., via an existing confirm helper or a readLine-based
prompt) and only proceed to call try newContent.write(toFile: configPath, ...)
if the user answered yes; if the user declines, skip the write and keep
existingContent. Apply the same change to the analogous block referenced around
lines 14420-14430 so both Codex `config.toml` post-install and OpenCode plugin
install honor the confirmation contract (use symbols skipConfirm,
Self.printInstallPreview, configPath, existingContent, newContent, and the write
call to locate the sites to change).
- Around line 14763-14769: The setup output currently prints
error.localizedDescription which hides CLIError.description; update the error
handling in the uninstallOpenCodePlugin() and
installOpenCodePlugin(projectLocal:) call sites so that when catching an error
you check if it is a CLIError (e.g., if let cliErr = error as? CLIError) and
print cliErr.description, otherwise print error.localizedDescription — keep the
same " opencode: ..." prefix to preserve format.
- Around line 14536-14551: The feed push is asking the server to wait up to
waitTimeout (isActionable ? 120 : 0) seconds, but client.send(command:) still
uses the default ~15s socket read timeout; update the socket read timeout to
match waitTimeout before calling client.send(command:) (or call the send
overload that accepts a timeout) so actionable Feed items aren't cut off—locate
the block building params/line and adjust the SocketClient timeout/configuration
around the client.send(command:) invocation.
- Around line 1891-1916: subcommandUsage currently lacks entries for the new
pre-socket commands so "cmux feed --help" and "cmux opencode --help" fall
through to the generic unknown-command message; add cases for "feed" and
"opencode" inside subcommandUsage to return appropriate usage strings (e.g.
"cmux feed clear [--yes]" referencing runFeedClear and "cmux opencode
install-hooks|uninstall-hooks" referencing runOpenCodeInstallHooks and
runOpenCodeUninstallHooks) and ensure the handler recognizes --help/ -h aliases
for those subcommands.
- Around line 14250-14262: The code spawns a Process called process and only
reads pipe.fileHandleForReading.readDataToEndOfFile() after
process.waitUntilExit(), which can deadlock if /usr/bin/diff fills the stdout
pipe; change the logic in this diff-spawning block so stdout is drained
concurrently (e.g., attach a readabilityHandler on pipe.fileHandleForReading or
write diff output to a temporary file) before or while calling
process.waitUntilExit(); ensure you still call try process.run(), handle the
thrown error as before, and return the collected String output as currently
done.
In `@Sources/Feed/FeedPanelView.swift`:
- Around line 323-354: Replace all hardcoded user-facing strings in
FeedPanelView with localized lookups: update primaryTitle cases (the
"ExitPlanMode" and "Question" branches) and any title fallback to use
String(localized:..., defaultValue:...), change helpText components if they
include user-facing labels, and modify resolvedBadgeLabel to localize the
decision labels instead of using rawValue (e.g., localize "feed.badge.plan",
"feed.badge.allowed", "feed.badge.answered" and create separate keys for each
decision variant currently returned via m.rawValue). Do the same for
telemetry/summary strings referenced elsewhere (e.g., "session start", "error",
"todos: ...") so they use String(localized:..., defaultValue:...) and add
corresponding entries into Resources/Localizable.xcstrings with translations;
ensure keys are stable and descriptive and update any code paths using
snapshot.payload, primaryTitle, helpText, or resolvedBadgeLabel to use those
localized keys.
- Around line 491-532: The submit path blocks empty-option questions because
FeedPillButton is disabled when selectedIds.isEmpty; change its enabled logic to
allow submission when options.isEmpty and the row is pending (i.e., set disabled
to !status.isPending || (!options.isEmpty && selectedIds.isEmpty)), adjust
tint/opacity to use the same condition, and ensure the submit action still calls
onReply(Array(selectedIds)) (which will be [] for no-options). Also prevent
UI-only changes after resolution by disabling the option toggle Buttons when
!status.isPending (e.g., apply .disabled(!status.isPending) to the Buttons or
check status.isPending inside their action) so selections cannot change once the
item is no longer pending.
---
Nitpick comments:
In `@Sources/Feed/FeedPanelView.swift`:
- Around line 161-163: Remove the redundant helper snapshotWorkstreamId and
inline FeedItemSnapshot.workstreamId wherever it’s used: delete the
snapshotWorkstreamId(_ s: FeedItemSnapshot) function, replace its call sites
with s.workstreamId, and remove any comments suggesting the snapshot lacks
workstreamId; do the same cleanup for the duplicated helper variant used in the
other location (the second helper around lines 245–260) so all callers use
FeedItemSnapshot.workstreamId directly.
🪄 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: d017c548-eb83-4e01-9ef7-76b193a391ef
📒 Files selected for processing (2)
CLI/cmux.swiftSources/Feed/FeedPanelView.swift
Both installers were printing the preview banner but never calling readLine — they'd write the file immediately regardless of the user's intent, so `cmux setup-hooks` appeared to skip the y/N prompt for brand-new installs (the OpenCode plugin and the codex_hooks toggle). Now both ask "Proceed? [y/N]" after the preview and abort on anything other than a y-prefix response. Matches installAgentHooks' behavior. Also short-circuits with "already up to date" when the content matches, consistent with the agent-hook path.
|
No dependency changes detected. Learn more about Socket for GitHub. 👍 No dependency changes detected in pull request |
Summary
Adds a Feed mode to the right sidebar. Three actionable kinds surface inline: permission requests,
ExitPlanMode, andAskUserQuestion. Telemetry (tool use, user prompts, assistant messages,TodoWrite) is stored but hidden behind an Actionable / All toggle inside the Feed view.Works across all 9 supported CLIs via
cmux feed-hook --source <agent>(hook-file agents: Claude, Codex, Cursor, Gemini, Copilot, CodeBuddy, Factory, Qoder) plus an OpenCode plugin (Resources/opencode-plugin.js).cmux setup-hooksauto-installs everything, gated on each binary being onPATH.Internal name is workstream; UI label is Feed.
What's in this PR
Packages/CMUXWorkstream/— new dual-platform Swift package (.iOS(.v18),.macOS(.v14)). Schema +@MainActor @ObservableWorkstreamStore+ append-only JSONL persistence actor + recursiveAnyJSONfor opaquetool_input. 15 unit tests pass.Six V2 socket verbs on
TerminalController:feed.push(blocking whenwait_timeout_seconds > 0),feed.permission.reply,feed.question.reply,feed.exit_plan.reply,feed.jump,feed.list. Request-id correlation routes replies back throughFeedCoordinator's semaphore map.feed.jumpadded to the focus-intent allowlist.FeedCoordinatorsingleton wired inAppDelegate.applicationDidFinishLaunching. Parks blocking hooks on aDispatchSemaphore, postsUNUserNotificationCenterbanners with inline Allow/Deny/Plan-mode buttons when the app isn't key, and routes action responses back via three new categories (CMUXFeedPermission,CMUXFeedExitPlan,CMUXFeedQuestion).FeedPanelViewright-sidebar mode matching the Sessions page aesthetic — 12×12 icons, 13pt titles, rounded hover backgrounds with 6px inset, pill action buttons with destructive-red tinting for Deny, Pending / Resolved / Expired status chips. Pending items float above resolved; Actionable / All segmented filter at top. Double-tap a row →feed.jumpfocuses the cmux workspace + surface viaworkspace.select+surface.focus.Tab badge: red pill with the pending count on the Feed
ModeBarButton(red dot + "9+" cap).cmux feed-hook --source <agent>CLI bridge: reads agent stdin, callsfeed.pushwith 120s blocking wait for actionable events, emits each agent's expected decision JSON on stdout (ClaudesystemMessage, Codexremember, generic approve/block).cmux opencode install-hooks [--project]anduninstall-hooksdrop~/.config/opencode/plugins/cmux-feed.jswith a// cmux-feed-plugin-marker v1header for safe upgrades. The plugin connects to$CMUX_SOCKET_PATH, maps OpenCode's event bus, and acknowledges permission decisions viaclient.session.permissions.Installer auto-wiring:
AgentHookDefgrows afeedHookEventslist; each agent gets a PreToolUse (orbeforeShellExecutionfor Cursor) hook invokingcmux feed-hook --source <name>with a 120 000 ms per-event timeout layered alongside the existing notification hooks. Removal is idempotent via a second marker.runSetupHooksrunscommand -v <agent>and skips agents missing onPATH, printing a summary line.Persistence: unbounded JSONL at
~/.cmuxterm/workstream.jsonl; memory ring cap is 2000 (decided against per-workstream cap for now).cmux feed clear [--yes]truncates.Localization: 23 new string keys added to
Resources/Localizable.xcstringswith English and Japanese translations.Docs: new
docs/feed.mdwith architecture diagram, install matrix, decision semantics, storage paths, troubleshooting.docs/notifications.mdcross-links Feed.E2E: new
cmuxUITests/FeedSidebarUITests.swift— boots tagged cmux, injects a synthetic permission request via socket, toggles to Feed, clicks Allow Once, asserts thefeed.pushresponse returnsstatus: resolved, mode: once.Deferred (iOS-blocked)
CMUXWorkstreamfrom the iOS target — waits for the iOS app to land in the repo.WorkstreamTransportover relay WebSocket — waits for terminal-sync.The packages already declare
.iOS(.v18)so the iOS app can link them unchanged once it arrives.Test plan (dogfood)
cd Packages/CMUXWorkstream && swift test— 15/15 pass../scripts/reload.sh --tag cmux-feedsucceeds.cmux setup-hooksreports each agent as installed or "skipped (binary not found on PATH)". Verify~/.codex/hooks.jsoncontains bothcodex-hookandfeed-hook --source codexentries. Check~/.config/opencode/plugins/cmux-feed.jsexists with marker comment.nccall returns{"ok":true,"result":{"status":"resolved","decision":{"kind":"permission","mode":"once"},"item_id":"…"}}. Row stays in Feed with green "Allowed · once" badge.nccall and updates the Feed row.cmux-hook session-startfires (sidebar Sessions list will populate). Trigger a permission, double-tap the Feed row. cmux focuses the Claude terminal.PreToolUseframe withtool_name=Read(no blocking), switch Feed to "All", verify thetoolUserow appears only in "All" view.cmux feed clear --yestruncates the JSONL. Restart the app — Feed is empty.gh workflow run test-e2e.yml --ref task-cmux-feed -f test_filter=FeedSidebarUITests. Watch the run; pass means the full click-through is green in CI.cmux setup-hooksnotification flow still fires (no double-send, no duplicate notifications).Files changed
Packages/CMUXWorkstream/**(new)Sources/Feed/FeedCoordinator.swift,Sources/Feed/FeedPanelView.swift(new)Sources/TerminalController.swift,Sources/RightSidebarPanelView.swift,Sources/AppDelegate.swift(extended)CLI/cmux.swift(feed-hook bridge, feed-clear, opencode install, feedHookEvents)Resources/opencode-plugin.js,Resources/Localizable.xcstrings(new keys)GhosttyTabs.xcodeproj/project.pbxproj(package + files)docs/feed.md(new),docs/notifications.md(cross-link)cmuxUITests/FeedSidebarUITests.swift(new)Summary by cubic
Adds a right‑sidebar Feed to review and approve agent decisions, with tap‑to‑jump and an Activity view. Sync
cmux feed-hookblocks up to 120s for actionable tools and returnshookSpecificOutputviafeed.*, powered by the newCMUXWorkstreamstore and a bundledOpenCodeplugin.New Features
cmux feed-hook --source <agent>; side‑effecting PreToolUse (Bash/Edit/Write/…) blocks up to 120s and returns decisions on stdout (hookSpecificOutput). V2 verbs:feed.push,feed.permission.reply,feed.question.reply,feed.exit_plan.reply,feed.jump,feed.list. Native notifications fire when the app isn’t key. Outside cmux, the hook no‑ops.cmux opencode install-hooks/uninstall-hooksmanage the plugin with a colored diff preview, y/N prompt, no‑op detection, and idempotent installs; telemetry is pushed for all hook subcommands.cmux feed cleartruncates persistent history.CMUXWorkstreamSwift package (iOS/macOS) with an@Observablestore, append‑only JSONL persistence, request‑id correlation for blocking hooks, and a 2k in‑memory ring.Bug Fixes
Written for commit 451cb58. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes / Behavior
Tests
Documentation