Skip to content

Add iOS agent event feed - #10064

Open
azooz2003-bit wants to merge 50 commits into
mainfrom
feat-ios-agent-feed-v2
Open

Add iOS agent event feed#10064
azooz2003-bit wants to merge 50 commits into
mainfrom
feat-ios-agent-feed-v2

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add a chronological iOS Feed tab alongside Workspaces and Notifications
  • render inline approvals, plan exits, questions, booleans, MCP forms, external elicitations, and completed-turn replies
  • bridge direct Codex app-server and OpenCode v2 interaction lifecycles, including stale-request invalidation and MCP session or persistent approvals
  • preserve resolved answers, redact secrets, and fail closed on unsupported schemas or routes

Testing

  • swift test --package-path Packages/macOS/CMUXAgentLaunch (316 tests)
  • swift test --package-path Packages/iOS/CmuxMobileShellModel (297 tests)
  • Swift parse checks for changed sources
  • localization JSON validation
  • OpenCode plugin syntax check

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Adds an iOS Agent Feed tab that aggregates coding‑agent activity across paired Macs with inline approvals, structured questions (booleans and MCP forms), and replyable turn completions. Previously iOS had no agent feed; now it shows revisioned, paged cross‑Mac events with an offline cache and live reconciliation, and fails closed on stale navigation or mismatched scopes.

  • Host capability workstream.feed.v1: Macs must implement workstream.feed.list (revisioned pages with next_cursor/has_more), publish workstream.feed.changed, and accept feed.invalidate; older hosts show “requires update.”
  • Authorization: workstream.feed.list uses account authorization; workstream.feed.action and workstream.feed.reply require a scoped attach ticket matching workspace_id/surface_id and reject mismatches.
  • Persistence and aggregation: per‑Mac bounded history with revision/cursor and a retention limit; on‑device cache isolated per account/team; one in‑flight refresh per Mac; cross‑Mac aggregation deduplicates newest‑first with a Needs Input filter; canonical device IDs fix stable row identity; anchored viewport during bursts.
  • Behavior: normalizes source aliases for consistent approval policy; supports persistent approval modes when advertised; renders boolean confirmations and MCP JSON‑schema elicitation (accept/decline/cancel); prefers v2 session APIs for permission replies with legacy fallback; preserves completed answers for offline browsing and redacts secret answers; unknown hook names remain visible.
  • UX/testing: adds a primary Feed tab with a “needs input” badge and five switchable designs; deep links tag the agentFeed origin; EN/JA localization; deterministic preview via CMUX_UITEST_AGENT_FEED_PREVIEW=1; performance probe; comprehensive UI/model and UITest coverage.

Written for commit 354df52. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added an iOS Agent Feed for cross-device activity, actionable requests, replies, permissions, questions, forms, and plan feedback.
    • Added filtering, pagination, refresh, unread counts, deep links, offline-cached content, and multiple display designs.
    • Added support for Codex and other registered agents, including structured questions, elicitation, and form responses.
    • Added persistent permission approvals and scoped feed authorization.
    • Added English and Japanese localization.
  • Bug Fixes

    • Improved handling of unknown, invalidated, or expired events, validation, redaction, navigation accuracy, and reconnect behavior.

@coderabbitai

coderabbitai Bot commented Aug 13, 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 PR adds an interactive, paginated Agent Feed across macOS and iOS. It adds structured question handling, Workstream history APIs, RPC authorization, offline caching, multi-Mac aggregation, deep linking, localized SwiftUI presentation, and automated validation.

Changes

Agent Feed and Workstream pipeline

Layer / File(s) Summary
Event classification and typed Workstream data
CLI/FeedEventClassifier.swift, Packages/macOS/CMUXAgentLaunch/.../Workstream/*
Structured question events map to actionable requests for registered sources. Workstream models preserve unknown values, support typed questions and forms, retain routing metadata, and parse schema-based inputs.
Feed storage, provider handling, and lifecycle
Sources/Feed/*, Sources/Panels/*, Resources/opencode-plugin.js
Feed storage supports revisioned snapshots, validated history cursors, serialized persistence, redaction, replies, invalidation, Codex input handling, and OpenCode v1/v2 request flows.
Feed RPC routing and authorization
Sources/TerminalController.swift, Sources/Mobile/*, Packages/iOS/CmuxMobileRPC/*, Sources/CmuxSocketEventMapper.swift
Feed listing, actions, replies, and invalidation use socket and mobile RPC paths with capability advertisement and workspace/surface authorization.
Mobile Feed models and aggregation
Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/*
Typed feed rows, questions, decisions, aggregation, filtering, pagination, refresh coalescing, mutation states, and status states are added.
iOS Feed state and persistence
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/*AgentFeed*, Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite*
The shell discovers Macs, restores scoped cache, refreshes and paginates feed pages, reconciles mutations, supports deep links, and resets state across identity changes.
iOS presentation and navigation
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/*, ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift
The Feed tab, response rows, five display designs, status banners, settings, navigation, accessibility metadata, localization, and preview routing are added.
Validation and fixtures
Packages/iOS/*/Tests/*AgentFeed*, cmuxTests/*Feed*, ios/cmuxUITests/AgentFeedUITests.swift, Packages/iOS/CmuxMobileShellUI/Sources/.../Debug/AgentFeed/*
Tests cover decoding, classification, persistence, pagination, authorization, mutations, navigation, localization, offline states, performance, and interaction flows.

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

Mergeability Score: 🟠 High · up to 9f8ab

The PR adds cross-device feed, approval, and structured-reply flows, but the current head still has a possible build failure plus concrete risks of exposing secrets, mis-handling approvals or form values, crashing on valid payloads, and blocking or starving feed/history work. The PR is not merge-ready without fixes or explicit owner acceptance of these high-impact issues.

Possibly related issues

Possibly related PRs

  • manaflow-ai/cmux#9586 — Directly modifies CLI/FeedEventClassifier.swift and agent-source classification registries.
  • manaflow-ai/cmux#9889 — Directly overlaps the iOS coding-agent Feed RPC, model, cache, shell, and UI implementation.
  • manaflow-ai/cmux#9776 — Directly overlaps Sources/Feed/FeedCoordinator.swift blocking-decision and Feed lifecycle handling.

Suggested reviewers: austinywang


Important

Pre-merge checks failed

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

❌ Failed checks (9 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error FeedCoordinator adds 10 waiterLock lock/unlock uses around new mobileRevision and invalidation state; lock calls rise 18→28 from main, extending manual synchronization in production. Own mobileRevision and waiter state in an actor or MainActor model, and wake blocking ingress through an explicit cancellation signal rather than extending NSLock-protected state.
Cmux Expensive Synchronous Load ❌ Error The new @MainActor workstream.feed.reply path calls sendTextToTarget→target(for:)→FeedJumpResolver.lookup, which synchronously reads and JSON-decodes an agent hook-session store. Resolve hook-session targets through a cached accessor or Task.detached/background repository, then return only the small target value to @MainActor.
Cmux Algorithmic Complexity ❌ Error MobileAgentFeedAggregation.swift:15 full-sorts all rows (O(N log N)); recomputeAgentFeedItems reruns it on refresh/status events, and the benchmark checks only subquadratic growth, not a UI budget. Use a bounded k-way heap or partial-selection merge over ordered Mac snapshots, or cache the aggregate; measure the 1000-workspace UI budget.
Cmux Swift Concurrency ❌ Error New @MainActor removeAgentFeedSnapshot launches an unretained Task for cache deletion; lifecycle callers cannot await or cancel it, matching prohibited fire-and-forget work. Store and cancel the cache-removal task, or make snapshot removal async and await it from hidden-Mac, promotion, and identity-reset paths.
Cmux Swift @Concurrent ❌ Error New @MainActor handleCodexUserInput performs JSON parsing, schema validation, payload construction, and JSON serialization before its detached wait, with no nonisolated/@Concurrent boundary. Move the parsing and payload construction into a nonisolated @concurrent helper or an explicit detached task; keep only MainActor state updates on the actor.
Cmux Swift Package Boundaries ❌ Error AgentSessionProcessStore adds +674 lines of pure Codex/MCP schema validation and answer conversion in an @MainActor app target; tests call these static helpers directly. Extract the pure MCP/app-server codec into a small CMUXAgentProtocol SwiftPM target, starting with public CodexMCPFormCodec; keep process and FeedCoordinator lifecycle wiring in the app.
Cmux User-Facing Error Privacy ❌ Error New Codex JSON-RPC error bodies expose the vendor name and interpolate raw upstream methods, such as mcpServer/elicitation/request, in user-visible error responses. Use a generic sanitized error and omit Codex app-server and the raw method; keep protocol details in private logs.
Cmux Full Internationalization ❌ Error The PR adds 13 production Feed/Codex keys to Resources/Localizable.xcstrings, which supports 20 locales, but each entry has values only for en and ja. Add translated, non-placeholder values for ar, bs, da, de, es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, and zh-Hant for every new key.
Cmux No Ambient Global State ❌ Error PR adds FeedJumpResolver.targets(for:) at Sources/Feed/FeedCoordinator.swift:1429 to a caseless static-only namespace; TerminalController.swift:14484 calls the new global runtime behavior. Make FeedJumpResolver constructable with an injected session-store/home-directory dependency. Create it at the app composition seam and inject it into TerminalController and FeedCoordinator.
Docstring Coverage ⚠️ Warning Docstring coverage is 14.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (15 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed Swift 6 targets have no default MainActor setting; new value models are nonisolated by context, while MobileShellComposite, WorkstreamStore, and coalescer use explicit @MainActor isolation and cach...
Cmux Browser Automation Off-Main ✅ Passed The PR diff adds Feed RPC routing only; browser.* worker routing and main-actor exclusions are unchanged from main, so this check's browser automation failure conditions are not introduced.
Cmux Cache Substitution Correctness ✅ Passed Fresh host reads still populate and persist Feed snapshots; disk cache is used only for documented offline/update-required restoration, and history waits for persistence drains before merging live...
Cmux No Hacky Sleeps ✅ Passed The only changed non-Swift runtime file is Resources/opencode-plugin.js; its diff adds no sleep, timer, polling, or fixed-delay construct. The existing setTimeout remains unchanged.
Cmux Swiftpm Lockfiles ✅ Passed The PR changes no Package.swift, Package.resolved, or package .gitignore files; cmux.xcodeproj only adds source-file build entries, not SwiftPM package references.
Cmux Swift Logging ✅ Passed The PR adds one unified Logger with nonisolated private let; its device ID and error use private redaction. The only print calls are in tests/UI tests, which the rule allows.
Cmux Swiftui State Layout ✅ Passed The PR uses @Observable, adds no legacy observation or GeometryReader, and passes value snapshots plus closures into LazyVStack rows; state writes occur only in callbacks.
Cmux Architecture Rethink ✅ Passed The diff adds no production sleeps, delayed dispatch, polling, or observers. Feed state has clear MainActor/store ownership, value-snapshot UI actions, and documented bridge and persistence invaria...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The diff adds SwiftUI views and mobile navigation only; it introduces no NSWindow, NSPanel, NSWindowController, Window, WindowGroup, or window-identifier assignment. The close-shortcut rule is not...
Cmux Source Artifacts ✅ Passed The full diff contains 87 Swift, localization, project, and JavaScript paths in source, test, and resource locations; no artifact directories, artifact extensions, or binary files were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The diff adds DEBUG-gated preview/probe code only under dedicated Sources/**/Debug folders; no new test/debug accessor or test hook appears in other production Swift sources.
Title check ✅ Passed The title clearly identifies the primary change: adding an iOS agent event feed.
Description check ✅ Passed The description clearly covers the feature scope and testing, but it omits the template's Demo Video, Review Trigger, and Checklist sections.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ios-agent-feed-v2

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.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Caution

CodeRabbit couldn't update its existing comment. The review summary may be out of date.

Error details
Validation Failed: {"resource":"IssueComment","code":"custom","field":"body","message":"body is too long (maximum is 65536 characters)"} - https://docs.github.com/rest/issues/comments#update-an-issue-comment

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

Caution

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

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

2196-2222: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Parse toolInputJSON once per item instead of seven times.

For each permission item, itemDict parses the same toolInputJSON string repeatedly: once in codexCapabilityToolInputJSON, once in safeToolInputSummary, and once inside each of the five FeedPermissionActionPolicy.supports* calls. itemDict runs for every item of a history page, so a full page multiplies these parses.

Compute the capability set once and reuse it for supported_modes.

Also applies to: 2341-2348

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

In `@Sources/Feed/FeedCoordinator.swift` around lines 2196 - 2222, Update the
item-dictionary construction around codexCapabilityToolInputJSON and
supported_modes to parse toolInputJSON once per permission item, compute the
shared capability set once, and reuse it for safeToolInputSummary and all
FeedPermissionActionPolicy.supports* checks. Apply the same reuse pattern to the
corresponding logic near the additional reported location.
🤖 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 `@CLI/FeedEventClassifier.swift`:
- Around line 147-148: Update the call to wireMapping in feedEventSemantic to
pass the normalized sourceKey instead of the raw source, ensuring
isSideEffectingTool receives canonical values such as "kiro" and preserves the
registered tool aliases for approval-card events.
- Around line 178-201: The question-event spelling table is duplicated and
divergent, causing inconsistent classification. In CLI/FeedEventClassifier.swift
lines 178-201, replace isQuestionEventName with the shared spelling-table call;
in
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift
lines 616-640, replace isQuestionEvent with that same shared call, preserving
the existing normalization behavior and using one canonical list.

In `@cmuxTests/MobileHostAuthorizationTests.swift`:
- Around line 771-800: Add a positive parameterized test alongside
testScopedAttachTicketRejectsFeedMutationOutsidePinnedRoute, covering both
workstream.feed.action and workstream.feed.reply with a scoped ticket whose
workspace_id and surface_id match the ticket’s pinned workspace and terminal;
assert ticketAuthorizationError returns nil.

In `@cmuxTests/MobileHostWorkspaceTicketAuthorizationTests.swift`:
- Around line 363-365: Update the authorization assertions for the
MobileHostRPCRequest entries in the scoped-ticket test: treat
workstream.feed.action and workstream.feed.reply as forbidden when params are
empty, while keeping agent-feed-list behavior unchanged. Alternatively, include
the required workspace_id in those requests to exercise the authorized
pinned-workspace route.

In `@ios/cmuxUITests/AgentFeedUITests.swift`:
- Around line 123-128: The correctness tests use runtime latency thresholds that
can fail under CI load. In ios/cmuxUITests/AgentFeedUITests.swift lines 123-128,
remove the frameP95, visibilityP95, frameStalls, and visibilityStalls assertions
while retaining the frames and visibility invariants and attaching latency
fields for inspection; in
Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileAgentFeedTests.swift
lines 303-332, remove the doubledTwice versus baseline comparison while
retaining the output-count assertion and printed benchmark line. Move threshold
evaluation to the dedicated performance job.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Line 10799: Update the foreground agent feed refresh flow around
scheduleForegroundAgentFeedRefresh(client:) to also run when
runtime?.supportsServerPushEvents is false. Trigger an initial refresh and
continue scheduling periodic refreshes for non-push runtimes, while preserving
the existing push-event listener behavior and avoiding duplicate refresh
scheduling.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+AgentFeed.swift:
- Around line 140-142: Update the catch blocks in sendAgentFeedAction and
sendAgentFeedReply to map errors to the existing curated failure reasons (host
unreachable, timed out, rejected, or unsupported) before assigning
agentFeedMutationStates[item.id] = .failed(message:). Remove the raw
String(describing: error) value and reuse the repository’s established
workspace-action failure mapping convention.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+SecondaryPromotion.swift:
- Around line 625-637: Update the warm-demotion flow around
resetForegroundAgentFeedIfInstanceChanged to remove the old bare Agent Feed
snapshot keyed by previousForegroundID before or alongside the tagged-feed
refresh. Preserve the existing foreground notification and agent-feed reset
behavior, and add regression coverage confirming the stale bare snapshot is
removed when a different Mac is promoted.

In
`@Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellAgentFeedPagingTests.swift`:
- Around line 63-66: Bound the paging loop in MobileShellAgentFeedPagingTests
around refreshAgentFeed and loadOlderAgentFeed with an explicit maximum
iteration count, increment the counter on each load, and assert the bound is not
exceeded while preserving the agentFeedCanLoadOlder termination condition.

In
`@Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileAgentFeedAggregation.swift`:
- Around line 7-15: Update items(from:) so merging retains only the best
Self.maxItemCount candidates instead of sorting all unique items before
truncation. Use Self.precedes to determine which candidates remain, then sort
the bounded set with the same comparator before returning it; preserve
newest-per-ID selection and existing tie-break behavior.

In
`@Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkstreamFeedListItem.swift`:
- Around line 197-212: Extend MobileWorkstreamDecision and decodeDecision to
support kind "boolean" by decoding its Boolean value into .boolean(value:), then
update the resolved-row rendering path to display that preserved value instead
of treating it as unknown.

In
`@Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkstreamFeedPayload.swift`:
- Around line 189-198: Update decodeScalarString to use non-throwing scalar
decoding attempts so numeric default_value inputs reach the numeric branches
instead of throwing. Check that integral Double values are within Int bounds
before conversion, preserving decimal formatting and avoiding traps for
out-of-range values. Add fixtures covering numeric and out-of-range
default_value cases.

In `@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedRow.swift`:
- Around line 1025-1034: Update displayAnswer to use the shared
normalizedInputType helper instead of comparing the raw input type, ensuring
every value normalized to secret—including password—is redacted. Extract or
reuse the helper across AgentFeedRow and AgentFeedRowCopy so both mappings
remain consistent.
- Around line 212-215: Update the switch in AgentFeedRow to use
MobileAgentFeedItem’s isTurnCompletion model property instead of duplicating the
lifecycle "sessionEnd" check. Preserve the existing requiresResponse gating for
stop completions, and ensure every completion recognized by the model renders
replyComposer when the upstream filter marks it replyable.
- Around line 465-469: Update the date_time validation branch in
fieldValueIsValid to reuse a single cached ISO8601DateFormatter instance instead
of constructing one for each validation call. Hoist the formatter to appropriate
shared or instance scope while preserving the existing date parsing and
string-length checks.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift`:
- Line 32: Document the intentional optional collection on
AgentFeedView.renderedItems: nil means no snapshot has been taken and should
fall back to source.visibleItems, while [] represents an empty snapshot. Add a
short explanatory comment or a targeted SwiftLint disable-next directive without
changing the optional behavior.

In
`@Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/UITestConfigTests.swift`:
- Around line 438-449: Update agentFeedPreviewAcceptsEnvironmentOrLaunchArgument
to wrap the two enabled expectations in a `#if` DEBUG branch, while retaining
assertions that the flag is disabled in the non-DEBUG branch, matching
agentChatPreviewFlagIsDebugOnly and the build-configuration contract.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamPayload.swift`:
- Around line 8-10: Update every permission-mode allow-set that maps agent
decisions, including emitKiroDecisionIfHandled and any Swift or JavaScript
consumers using WorkstreamPermissionMode, to include persistent alongside once,
always, all, and bypass. Preserve explicit allow-set matching and fail closed
for unrecognized modes, while leaving deny behavior unchanged.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamQuestionPrompt`+Parsing.swift:
- Around line 156-199: Map boolean default values in the defaultValue resolution
near rawDefaultValue and resolvedOptions to the generated option ids: true to
"yes" and false to "no", while preserving existing choice normalization and
other input types. Update
WorkstreamQuestionPromptParsingTests.parsesBooleanConfirmation to expect the
corresponding generated id instead of "1".
- Around line 225-230: Update scalarString(_:) to distinguish JSON booleans from
numeric NSNumber values before converting them. Inspect the underlying NSNumber
type and return "true"/"false" only for actual booleans; allow numeric 0 and 1
to reach NSNumber.stringValue so enum and const values retain their numeric
labels.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift`:
- Around line 174-176: In the history request flow containing the persistence
drain barrier, replace the while loop that repeatedly re-reads
persistenceDrainTask with a single snapshot of the current drain task and await
it once. This ensures historyPage waits only for work queued before the request,
while preserving normal behavior when no drain task is present.
- Around line 436-471: Update decisionForHistory to redact every unkeyed
selection whenever secretIDs is non-empty, while retaining keyed-field
validation and redaction across the full payload rather than only a
single-question fallback. Update QuestionActionArea.composedAnswers to submit
each answer keyed as question.id=answer so the agent receives the field
association correctly.
- Around line 61-64: Remove the test-only production seams: in
WorkstreamStore.swift (lines 61-64), delete activePersistenceDrainCount and make
persistenceDrainTask internal for `@testable` import access; in
WorkstreamPersistence.swift (lines 39-49), remove beforeAppend and its injecting
initializer; and at line 92 remove loadPageCallCount and its stored property.
Update tests to observe internal state directly.

Apply the same fix in
`@Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/Workstream/WorkstreamStoreTests.swift`
around lines 268 - 269.

In
`@Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/Workstream/WorkstreamStoreTests.swift`:
- Around line 230-231: Add a requirement immediately after splitting the decoded
cursor in the relevant test, asserting that parts contains at least three
components before accessing parts[2]. Preserve the existing tampered-data
construction and use the test framework’s established `#require` pattern so
malformed cursor formats produce a readable failure instead of trapping.

In `@Resources/Localizable.xcstrings`:
- Around line 207-241: Add translated localizations for all 20 locales supported
by Resources/Localizable.xcstrings to every newly added Feed, Codex, and
permission key group, including feed.question.boolean.no,
feed.question.boolean.yes, feed.form.accepted, feed.form.cancelled, and
feed.form.declined. Preserve the existing English and Japanese values and do not
leave any new key with only en/ja entries or rely on English fallback.

In `@Sources/Feed/FeedCoordinator.swift`:
- Around line 1389-1395: Update FeedJumpResolver.targets(for:) and its
lookup(agent:sessionId:) flow to create a per-call cache of parsed sessions
keyed by agent, so each agent’s *-hook-sessions.json file is read and parsed at
most once while resolving all workstreams. Perform the session-file loading off
the main actor, preserving registeredTargets as the fast path and using the
cached session map only on registry misses.
- Around line 541-558: Update the resolve closure to call the existing
publishMobileChange() method after marking the item resolved, removing its
duplicated waiterLock/mobileRevision increment and workstream.feed.changed
emission. Preserve the existing cancellation flow and rely on
publishMobileChange() as the single publication path.
- Around line 683-689: Update the validation flow around the question input-type
switch to create one shared ISO8601DateFormatter outside the values.allSatisfy
loop, then reuse it for each .dateTime value instead of allocating a formatter
per validation call. Preserve the existing date parsing behavior for other input
types.
- Around line 2233-2244: Update the question-kind routing in CLI/cmux.swift and
Resources/feed-tui/index.ts so rows with kind "boolean" or "form" are treated as
resolvable alongside "question", while preserving the existing handling for
question rows.

In `@Sources/Feed/FeedPanelView.swift`:
- Around line 20-21: Update the localized entries for the six feed permission
mode keys, including feed.permission.mode.persistent, in Localizable.xcstrings
by adding translations for all 18 currently missing supported locales while
preserving the existing en and ja values.

In `@Sources/Feed/FeedPermissionActionPolicy.swift`:
- Around line 145-157: Update codexMCPPersistScopes so it checks each
persistence source in order and selects the first container that actually
contains persist, rather than stopping when object["metadata"] exists without
that key. Preserve the existing precedence of the top-level mcp_persist value,
then metadata, then _meta/meta, and return nil only when none provides persist.

In `@Sources/Panels/AgentSessionProcessStore.swift`:
- Around line 321-338: Replace the detached call to
FeedCoordinator.ingestBlockingWithOutcome in the request-handling flow with an
async waiter that suspends rather than blocks a cooperative-pool thread. Update
the FeedCoordinator decision lifecycle so deliverReply and
invalidateBlockingRequest resume the waiter, while preserving timeout and
cancellation behavior for both blocking and non-blocking requests; do not use
DispatchSemaphore for this async path.

In `@Sources/Panels/CodexAppServerSession.swift`:
- Around line 556-583: Update the user-input task created in the Codex request
handling flow to be stored by rpcID, using the session’s existing task-tracking
collection. During provider teardown, cancel and remove the matching task and
invalidate its associated Feed request, while preserving the existing response
handling and cleanup behavior in the Task body.

In `@Sources/TerminalController.swift`:
- Around line 6105-6114: Update v2FeedInvalidate’s request_id validation to
reject empty or blank String values before calling
FeedCoordinator.shared.invalidateBlockingRequest. Return the existing
invalid_params error response for blank IDs, while preserving the current
invalidation and success behavior for non-blank IDs.

Apply the same fix in `@Sources/TerminalController.swift` around lines 6105 -
6111: Duplicate report of the same empty-request validation defect.

---

Outside diff comments:
In `@Sources/Feed/FeedCoordinator.swift`:
- Around line 2196-2222: Update the item-dictionary construction around
codexCapabilityToolInputJSON and supported_modes to parse toolInputJSON once per
permission item, compute the shared capability set once, and reuse it for
safeToolInputSummary and all FeedPermissionActionPolicy.supports* checks. Apply
the same reuse pattern to the corresponding logic near the additional reported
location.
🪄 Autofix

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 Plus

Run ID: f0b3e078-b5e7-4d6f-8e00-dc605a74410e

📥 Commits

Reviewing files that changed from the base of the PR and between 3166828 and 821cf79.

📒 Files selected for processing (85)
  • CLI/FeedEventClassifier.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileWorkstreamFeedExports.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCNotificationFeedAuthTests.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/TransportTestDoubles.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/AgentFeedCacheStore.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/AgentFeedCachedSnapshot.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/AgentFeedMacSnapshot.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentFeed.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+DeeplinkNavigation.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+HiddenMacs.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+SecondaryPromotion.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/SecondaryMacSubscription.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/AgentFeedCacheStoreTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ComposerSubmitRoutingTestSupport.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellAgentFeedPagingTests.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileAgentFeedAction.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileAgentFeedAggregation.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileAgentFeedFilter.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileAgentFeedItem.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileAgentFeedItemID.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileAgentFeedMutationState.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileAgentFeedPageAccumulator.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileAgentFeedRefreshTaskCoalescer.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileAgentFeedStatus.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkstreamFeedListItem.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkstreamFeedListResponse.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkstreamFeedPayload.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkstreamFeedStatus.swift
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileAgentFeedTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedL10n.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedRow.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedRowChrome.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedStoreView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Debug/AgentFeed/AgentFeedPerformanceProbe.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Debug/AgentFeed/AgentFeedPreviewScenario.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Debug/AgentFeed/AgentFeedPreviewView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileAgentFeedDesign.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimarySearchCoordinator.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTab.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePrimaryTabScaffold.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Resources/Localizable.xcstrings
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListLayoutPreviewView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift
  • Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/Debug/UITestConfig+AgentFeedPreview.swift
  • Packages/iOS/CmuxMobileSupport/Tests/CmuxMobileSupportTests/UITestConfigTests.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamEvent.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamItem.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamPayload.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamPersistence.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamQuestionPrompt+Parsing.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/Workstream/WorkstreamEventTests.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/Workstream/WorkstreamItemTests.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/Workstream/WorkstreamQuestionPromptParsingTests.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/Workstream/WorkstreamStoreTests.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Feed/ControlCommandCoordinator+Feed.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift
  • Resources/Localizable.xcstrings
  • Resources/opencode-plugin.js
  • Sources/CmuxSocketEventMapper.swift
  • Sources/Feed/FeedCoordinator.swift
  • Sources/Feed/FeedPanelView.swift
  • Sources/Feed/FeedPermissionActionPolicy.swift
  • Sources/Mobile/MobileHostService+Capabilities.swift
  • Sources/Mobile/MobileHostService+TicketAuthorization.swift
  • Sources/Panels/AgentSessionProcessStore.swift
  • Sources/Panels/AgentSessionRunningSession.swift
  • Sources/Panels/AgentSessionWebRendererCoordinator.swift
  • Sources/Panels/CodexAppServerSession.swift
  • Sources/TerminalController+ControlFeedContext.swift
  • Sources/TerminalController.swift
  • cmuxTests/CodexAppServerSessionTests.swift
  • cmuxTests/FeedCoordinatorTests.swift
  • cmuxTests/FeedEventClassificationTests.swift
  • cmuxTests/MobileHostAuthorizationTests.swift
  • cmuxTests/MobileHostWorkspaceTicketAuthorizationTests.swift
  • ios/cmux-ios.xcodeproj/project.pbxproj
  • ios/cmux/Resources/Localizable.xcstrings
  • ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift
  • ios/cmuxUITests/AgentFeedUITests.swift

Comment thread CLI/FeedEventClassifier.swift
Comment on lines +178 to +201
private static func isQuestionEventName(_ event: String) -> Bool {
let normalized = event.unicodeScalars.filter { CharacterSet.alphanumerics.contains($0) }
.map(String.init)
.joined()
.lowercased()
return [
"askuserquestion",
"askuserconfirmation",
"askuser",
"booleanquestion",
"confirmationrequest",
"questionasked",
"questionv2asked",
"questionrequest",
"elicitation",
"elicitationrequest",
"mcpelicitation",
"mcpserverelicitationrequest",
"requestuserinput",
"userinputrequest",
"inputrequest",
"toolrequestuserinput",
"itemtoolrequestuserinput",
].contains(normalized)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

One question-event spelling table, defined twice, already divergent. Both sites normalize an event name by stripping non-alphanumerics and lowercasing, then match a hardcoded list to decide whether the event is a structured user question. The lists disagree, so the same wire value classifies differently depending on which path an event takes.

  • CLI/FeedEventClassifier.swift#L178-L201: replace isQuestionEventName with a call into the shared spelling table; this copy uniquely contains confirmationrequest and questionrequest and omits question.
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift#L616-L640: replace isQuestionEvent with the same shared call; this copy uniquely contains question and omits confirmationrequest and questionrequest.
📍 Affects 2 files
  • CLI/FeedEventClassifier.swift#L178-L201 (this comment)
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift#L616-L640
🤖 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 `@CLI/FeedEventClassifier.swift` around lines 178 - 201, The question-event
spelling table is duplicated and divergent, causing inconsistent classification.
In CLI/FeedEventClassifier.swift lines 178-201, replace isQuestionEventName with
the shared spelling-table call; in
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift
lines 616-640, replace isQuestionEvent with that same shared call, preserving
the existing normalization behavior and using one canonical list.

Comment on lines +771 to +800
@Test(arguments: ["workstream.feed.action", "workstream.feed.reply"])
func testScopedAttachTicketRejectsFeedMutationOutsidePinnedRoute(method: String) throws {
let ticket = try scopedAttachTicket(workspaceID: "workspace", terminalID: "terminal")
let request = MobileHostRPCRequest(
id: "feed-mutation",
method: method,
params: [
"workspace_id": "other-workspace",
"surface_id": "other-terminal",
],
auth: MobileHostRPCAuth(attachToken: ticket.authToken, stackAccessToken: nil)
)
let error = MobileHostService.ticketAuthorizationError(ticket: ticket, request: request)
#expect(error?.code == "forbidden")
}
@Test(arguments: ["workstream.feed.action", "workstream.feed.reply"])
func testMacScopedAttachTicketAcceptsFeedMutationInAnyWorkspace(method: String) throws {
let ticket = try scopedAttachTicket(workspaceID: "", terminalID: nil)
let request = MobileHostRPCRequest(
id: "feed-mutation",
method: method,
params: [
"workspace_id": "other-workspace",
"surface_id": "other-terminal",
],
auth: MobileHostRPCAuth(attachToken: ticket.authToken, stackAccessToken: nil)
)
let error = MobileHostService.ticketAuthorizationError(ticket: ticket, request: request)
#expect(error == 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.

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add the positive scoped case for feed mutations.

The two new tests prove that a scoped ticket rejects a foreign route and that a Mac-wide ticket accepts any route. Neither asserts that a scoped ticket still authorizes workstream.feed.action and workstream.feed.reply on its own pinned workspace and terminal. A check that rejects every feed mutation would pass both tests.

💚 Proposed additional test
`@Test`(arguments: ["workstream.feed.action", "workstream.feed.reply"])
func testScopedAttachTicketAcceptsFeedMutationOnPinnedRoute(method: String) throws {
    let ticket = try scopedAttachTicket(workspaceID: "workspace", terminalID: "terminal")
    let request = MobileHostRPCRequest(
        id: "feed-mutation",
        method: method,
        params: [
            "workspace_id": "workspace",
            "surface_id": "terminal",
        ],
        auth: MobileHostRPCAuth(attachToken: ticket.authToken, stackAccessToken: nil)
    )
    `#expect`(MobileHostService.ticketAuthorizationError(ticket: ticket, request: request) == 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 `@cmuxTests/MobileHostAuthorizationTests.swift` around lines 771 - 800, Add a
positive parameterized test alongside
testScopedAttachTicketRejectsFeedMutationOutsidePinnedRoute, covering both
workstream.feed.action and workstream.feed.reply with a scoped ticket whose
workspace_id and surface_id match the ticket’s pinned workspace and terminal;
assert ticketAuthorizationError returns nil.

Comment on lines +363 to +365
MobileHostRPCRequest(id: "agent-feed-list", method: "workstream.feed.list", params: [:], auth: nil),
MobileHostRPCRequest(id: "agent-feed-action", method: "workstream.feed.action", params: [:], auth: nil),
MobileHostRPCRequest(id: "agent-feed-reply", method: "workstream.feed.reply", params: [:], auth: 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.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect scope checks and route resolution for feed mutation methods.
set -euo pipefail

rg -n -C 12 'workstream\.feed\.(action|reply)' --glob '*.swift' Sources Packages | head -200
rg -n -C 8 'ticketAuthorizationError' --glob '*.swift' Sources | head -120

Repository: manaflow-ai/cmux

Length of output: 23942


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ticket authorization implementation ---'
sed -n '1,180p' Sources/Mobile/MobileHostService+TicketAuthorization.swift

printf '%s\n' '--- feed handlers and parameter parsing ---'
rg -n -C 24 'func v2MobileWorkstreamFeed(Action|Reply)|v2MobileWorkstreamFeed(Action|Reply)|workstreamID|workspace_id|surface_id' \
  Sources/Mobile Sources/TerminalController.swift Packages/iOS/CmuxMobileShell/Sources --glob '*.swift' | head -500

printf '%s\n' '--- authorization tests around feed requests ---'
sed -n '320,390p' cmuxTests/MobileHostWorkspaceTicketAuthorizationTests.swift
rg -n -C 18 'feedMutations|workstream\.feed\.(action|reply)|ticketCoversTerminalRequest' \
  cmuxTests Packages --glob '*.swift' | head -400

Repository: manaflow-ai/cmux

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- feed handler definitions ---'
rg -n -l 'v2MobileWorkstreamFeed(Action|Reply)' Sources Packages --glob '*.swift' |
while IFS= read -r file; do
  printf '\nFILE: %s\n' "$file"
  rg -n -C 35 'v2MobileWorkstreamFeed(Action|Reply)' "$file"
done

printf '%s\n' '--- exact authorization tests ---'
sed -n '335,380p' cmuxTests/MobileHostWorkspaceTicketAuthorizationTests.swift
rg -n -C 30 'feedMutationsPreserveMatchingScopedTicket|feedMutationsReject|workstream\.feed\.(action|reply)' \
  Packages/iOS/CmuxMobileRPC/Tests cmuxTests --glob '*.swift'

printf '%s\n' '--- authorization helper remainder and parameter helpers ---'
sed -n '130,280p' Sources/Mobile/MobileHostService+TicketAuthorization.swift
rg -n -C 20 'func stringParamSelection|struct .*ParamSelection|stringParamSelection|containsIgnoredAliasParameters' \
  Sources Packages --glob '*.swift'

Repository: manaflow-ai/cmux

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- enclosing test setup ---'
sed -n '250,335p' cmuxTests/MobileHostWorkspaceTicketAuthorizationTests.swift

printf '%s\n' '--- all scopedTicket assignments and declarations ---'
rg -n -C 8 'scopedTicket|macWideTicket' cmuxTests/MobileHostWorkspaceTicketAuthorizationTests.swift

printf '%s\n' '--- deterministic authorization probe ---'
python3 - <<'PY'
def ticket_terminal_error(ticket_workspace, ticket_terminal, workspace=None, terminal=None):
    if not ticket_workspace.strip():
        return None
    if workspace is not None and workspace != ticket_workspace.strip():
        return "forbidden"
    if ticket_terminal and ticket_terminal.strip():
        return None if terminal == ticket_terminal.strip() else "forbidden"
    return None if workspace == ticket_workspace.strip() else "forbidden"

cases = [
    ("workspace", None, None, None),
    ("workspace", None, "workspace", "surface"),
    ("workspace", None, "other-workspace", "surface"),
    ("", None, None, None),
]
for case in cases:
    print(case, "=>", ticket_terminal_error(*case))
PY

Repository: manaflow-ai/cmux

Length of output: 5625


Make the scoped-ticket assertions match authorization behavior.

workstream.feed.action and workstream.feed.reply require workspace_id for a workspace-scoped ticket. With empty parameters, ticketAuthorizationError returns forbidden; only the Mac-wide ticket is authorized. Expect rejection for the scoped ticket, or provide the pinned workspace route.

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

In `@cmuxTests/MobileHostWorkspaceTicketAuthorizationTests.swift` around lines 363
- 365, Update the authorization assertions for the MobileHostRPCRequest entries
in the scoped-ticket test: treat workstream.feed.action and
workstream.feed.reply as forbidden when params are empty, while keeping
agent-feed-list behavior unchanged. Alternatively, include the required
workspace_id in those requests to exercise the authorized pinned-workspace
route.

Comment on lines +123 to +128
XCTAssertGreaterThanOrEqual(frames, 60, value)
XCTAssertEqual(visibility, 1, value)
XCTAssertLessThanOrEqual(frameP95, 33, value)
XCTAssertLessThanOrEqual(visibilityP95, 250, value)
XCTAssertEqual(frameStalls, 0, value)
XCTAssertEqual(visibilityStalls, 0, value)

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 | 🟠 Major | ⚡ Quick win

Correctness tests in this layer assert on measured elapsed time. Both sites gate a pass/fail result on runtime measured during the test run, so a correct implementation fails intermittently whenever CI is loaded. The repository test guidelines state: "Do not use fixed sleeps, measured wall-clock assertions, or hard absolute latency ceilings in correctness tests" and "must assert logical invariants rather than measured latency." Keep the measurements as reported instrumentation and move every threshold comparison to a dedicated performance job.

  • ios/cmuxUITests/AgentFeedUITests.swift#L123-L128: remove the frameP95 <= 33, visibilityP95 <= 250, frameStalls == 0, and visibilityStalls == 0 assertions on values that AgentFeedPerformanceProbe derives from CADisplayLink.timestamp and CACurrentMediaTime(); keep the frames >= 60 and visibility == 1 invariants and attach the latency fields for inspection.
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileAgentFeedTests.swift#L303-L332: remove the doubledTwice < baseline * 8 comparison over ContinuousClock samples; keep the output.count == min(size, MobileAgentFeedAggregation.maxItemCount) assertion and the printed benchmark line.
📍 Affects 2 files
  • ios/cmuxUITests/AgentFeedUITests.swift#L123-L128 (this comment)
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileAgentFeedTests.swift#L303-L332
🤖 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 `@ios/cmuxUITests/AgentFeedUITests.swift` around lines 123 - 128, The
correctness tests use runtime latency thresholds that can fail under CI load. In
ios/cmuxUITests/AgentFeedUITests.swift lines 123-128, remove the frameP95,
visibilityP95, frameStalls, and visibilityStalls assertions while retaining the
frames and visibility invariants and attaching latency fields for inspection; in
Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileAgentFeedTests.swift
lines 303-332, remove the doubledTwice versus baseline comparison while
retaining the output-count assertion and printed benchmark line. Move threshold
evaluation to the dedicated performance job.

Source: Coding guidelines

Comment on lines +20 to +21
case .persistent:
return String(localized: "feed.permission.mode.persistent", defaultValue: "remembered")

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm the new Feed localization keys exist with en and ja values.
set -euo pipefail

catalog=$(fd -g 'Localizable.xcstrings' Resources | head -1)
echo "catalog=$catalog"
for key in feed.permission.mode.persistent feed.form.accepted feed.form.declined \
           feed.form.cancelled feed.permission.session feed.permission.persistent; do
  echo "== $key"
  jq --arg k "$key" '.strings[$k].localizations | keys? // "MISSING"' "$catalog"
done

Repository: manaflow-ai/cmux

Length of output: 467


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== localization policy files =="
for f in CLAUDE.md AGENTS.md .github/review-bot-rules/full-internationalization.md; do
  if [ -f "$f" ]; then
    echo "--- $f"
    rg -n -i -C 3 'localiz|locale|supported language|English|Japanese|all supported' "$f" || true
  fi
done

echo "== catalog locale and key coverage =="
python3 - <<'PY'
import json
from pathlib import Path

p = Path("Resources/Localizable.xcstrings")
data = json.loads(p.read_text())
keys = [
    "feed.permission.mode.persistent",
    "feed.form.accepted",
    "feed.form.declined",
    "feed.form.cancelled",
    "feed.permission.session",
    "feed.permission.persistent",
]
for key in keys:
    entry = data.get("strings", {}).get(key)
    print(f"== {key}")
    if entry is None:
        print("MISSING")
        continue
    locs = entry.get("localizations", {})
    print("locales:", ", ".join(sorted(locs)))
    for locale, value in sorted(locs.items()):
        unit = value.get("stringUnit", {})
        print(f"{locale}: {unit.get('value')!r} ({unit.get('state')})")
PY

echo "== relevant source references =="
rg -n -C 2 'feed\.permission\.mode\.persistent|feed\.form\.(accepted|declined|cancelled)|feed\.permission\.(session|persistent)' Sources/Feed/FeedPanelView.swift

Repository: manaflow-ai/cmux

Length of output: 10385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json
from pathlib import Path

data = json.loads(Path("Resources/Localizable.xcstrings").read_text())
strings = data["strings"]
catalog_locales = sorted({
    locale
    for entry in strings.values()
    for locale in entry.get("localizations", {})
})
keys = [
    "feed.permission.mode.persistent",
    "feed.form.accepted",
    "feed.form.declined",
    "feed.form.cancelled",
    "feed.permission.session",
    "feed.permission.persistent",
]
print("catalog locales:", ", ".join(catalog_locales))
print("catalog locale count:", len(catalog_locales))
for key in keys:
    locs = set(strings.get(key, {}).get("localizations", {}))
    print(f"{key}: present={','.join(sorted(locs))}; missing={','.join(sorted(set(catalog_locales)-locs)) or '(none)'}")

print("== nearby feed key locale coverage ==")
for key in sorted(k for k in strings if k.startswith("feed.permission") or k.startswith("feed.form")):
    locs = sorted(strings[key].get("localizations", {}))
    if key in keys or len(locs) > 2:
        print(f"{key}: {','.join(locs)}")
PY

echo "== focused diff summary =="
git diff --stat -- Resources/Localizable.xcstrings Sources/Feed/FeedPanelView.swift
git diff --unified=1 -- Resources/Localizable.xcstrings | sed -n '1,220p'

Repository: manaflow-ai/cmux

Length of output: 1223


Add translations for all supported locales. These six keys have only en and ja, but Resources/Localizable.xcstrings contains 20 locales. Add the missing 18 translations.

🤖 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/FeedPanelView.swift` around lines 20 - 21, Update the localized
entries for the six feed permission mode keys, including
feed.permission.mode.persistent, in Localizable.xcstrings by adding translations
for all 18 currently missing supported locales while preserving the existing en
and ja values.

Source: Path instructions

Comment thread Sources/Feed/FeedPermissionActionPolicy.swift
Comment on lines +321 to +338
let timeout: TimeInterval
if request.isBlocking {
// Codex defines blocking input as waiting indefinitely. Keep a
// distant safety deadline while serverRequest/resolved and process
// exit provide the normal lifecycle-driven cancellation paths.
timeout = 7 * 24 * 60 * 60
} else {
timeout = min(
max(Double(request.autoResolutionMilliseconds ?? 120_000) / 1_000, 1),
120
)
}
let outcome = await Task.detached(priority: .userInitiated) {
FeedCoordinator.shared.ingestBlockingWithOutcome(
event: event,
waitTimeout: timeout
)
}.value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not block a cooperative-pool thread for up to seven days.

FeedCoordinator.ingestBlockingWithOutcome waits on a DispatchSemaphore until the deadline. Line 333 runs it inside Task.detached, so the wait occupies a thread of the Swift concurrency cooperative pool. With request.isBlocking, the deadline is 7 days (line 326); otherwise it is up to 120 seconds. Several concurrent Codex input requests therefore hold several pool threads for a long time, which can starve unrelated async work. Task cancellation also cannot interrupt the semaphore wait, so a cancelled Codex session leaves the thread blocked until the agent process exit path calls invalidateBlockingRequest.

Use an async waiter for the decision instead of a blocking semaphore on this path. One option is a continuation that FeedCoordinator resumes from deliverReply and invalidateBlockingRequest.

As per coding guidelines: "In non-test Swift application and runtime code, flag newly introduced or materially expanded blocking waits such as DispatchSemaphore, semaphore.wait(), DispatchGroup.wait(), and similar thread-blocking waits for async work."

🤖 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/AgentSessionProcessStore.swift` around lines 321 - 338,
Replace the detached call to FeedCoordinator.ingestBlockingWithOutcome in the
request-handling flow with an async waiter that suspends rather than blocks a
cooperative-pool thread. Update the FeedCoordinator decision lifecycle so
deliverReply and invalidateBlockingRequest resume the waiter, while preserving
timeout and cancellation behavior for both blocking and non-blocking requests;
do not use DispatchSemaphore for this async path.

Source: Coding guidelines

Comment thread Sources/Panels/CodexAppServerSession.swift Outdated
Comment thread Sources/TerminalController.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 (2)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+HiddenMacs.swift (1)

725-750: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear bare feed snapshots for an active tagged target.

foregroundPairingID uses connectedMacInstanceTag. If that tag is nil, isActiveMac can still be true through the stored isActive fallback, but the bare ID is not in targetPairingIDs. When a sibling remains, fullyHiddenPhysicalIDs is empty. The device-keyed notification and Agent Feed snapshots then survive after the active pairing is hidden and disconnected.

Use isActiveMac with foregroundMacDeviceID to remove both device-keyed snapshots.

Proposed fix
-        if let foregroundPairingID, targetPairingIDs.contains(foregroundPairingID) {
-            let identity = MobilePairedMac.pairingIdentity(from: foregroundPairingID)
-            removeNotificationFeedSnapshot(macDeviceID: identity.macDeviceID)
-            removeAgentFeedSnapshot(ownerKey: identity.macDeviceID)
+        if isActiveMac, let foregroundDeviceID = foregroundMacDeviceID {
+            removeNotificationFeedSnapshot(macDeviceID: foregroundDeviceID)
+            removeAgentFeedSnapshot(ownerKey: foregroundDeviceID)
         }

Based on learnings: notification-feed snapshots may be applied without a live secondary subscription, and tagged pairing macInstanceTag must be derivable from the owner key itself.

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

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+HiddenMacs.swift
around lines 725 - 750, Update the foreground snapshot cleanup near
foregroundPairingID to use isActiveMac together with foregroundMacDeviceID,
rather than relying on foregroundPairingID or targetPairingIDs. When the active
Mac is being hidden, remove both notification and Agent Feed snapshots keyed by
the bare foreground device ID, including cases where the active pairing has no
connectedMacInstanceTag and a sibling remains visible.

Source: Learnings

Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+SecondaryPromotion.swift (1)

86-90: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Await the retired client disconnect.

installControlConnection is already async, but the disabled-aggregation branch starts an unowned Task for connection.client.disconnect(). This lifecycle operation is not stored, cancellable, or tied to a caller-owned operation. Await it after retire() before returning.

Proposed fix
             removeFocusedConnection(ifMatching: connection)
             connection.client.retire()
-            Task { await connection.client.disconnect() }
+            await connection.client.disconnect()
             return

As per coding guidelines: “Do not create fire-and-forget Task { ... } work with meaningful lifecycle unless it is stored, cancellable, or tied to a caller-owned operation.”

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

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+SecondaryPromotion.swift
around lines 86 - 90, In the disabled-aggregation branch of
installControlConnection, replace the unowned Task wrapping
connection.client.disconnect() with an awaited disconnect after
connection.client.retire() and before returning. Keep the existing capability
and focused-connection cleanup unchanged.

Source: Coding guidelines

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

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+HiddenMacs.swift:
- Around line 725-750: Update the foreground snapshot cleanup near
foregroundPairingID to use isActiveMac together with foregroundMacDeviceID,
rather than relying on foregroundPairingID or targetPairingIDs. When the active
Mac is being hidden, remove both notification and Agent Feed snapshots keyed by
the bare foreground device ID, including cases where the active pairing has no
connectedMacInstanceTag and a sibling remains visible.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+SecondaryPromotion.swift:
- Around line 86-90: In the disabled-aggregation branch of
installControlConnection, replace the unowned Task wrapping
connection.client.disconnect() with an awaited disconnect after
connection.client.retire() and before returning. Keep the existing capability
and focused-connection cleanup unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 47c838db-b005-49cb-89c5-ef9800cc5321

📥 Commits

Reviewing files that changed from the base of the PR and between 821cf79 and 844297c.

📒 Files selected for processing (13)
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+HiddenMacs.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+SecondaryPromotion.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/SecondaryMacSubscription.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ComposerSubmitRoutingTestSupport.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/NotificationFeedPreviewView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Resources/Localizable.xcstrings
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift
  • ios/cmux-ios.xcodeproj/project.pbxproj
  • ios/cmux/Resources/Localizable.xcstrings
  • ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift
💤 Files with no reviewable changes (1)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
Sources/Panels/AgentSessionProcessStore.swift (2)

721-732: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject selections that violate the accepted MCP schema.

mcpElicitationIsSupported accepts enum and numeric-bound constraints. mcpValue falls through to typedMCPValue when a selection does not match an enum value. It also does not enforce minimum or maximum.

A stale or injected selection such as target=linux or count=99 can reach the MCP server despite the accepted schema. Validate property names, enum values, and numeric bounds. If validation fails, cancel the elicitation instead of serializing invalid content.

🤖 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/AgentSessionProcessStore.swift` around lines 721 - 732, The
MCP value handling around mcpValue must enforce the accepted schema before
serialization: validate property names, reject values outside allowed enum
selections, and enforce numeric minimum and maximum bounds. When validation
fails, cancel the elicitation rather than falling through to typedMCPValue;
preserve valid indexed, named, and schema-compatible values.

259-308: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Move Codex request normalization off MainActor.

CodexAppServerSession and AgentSessionProcessStore are @MainActor. Server-request input is not covered by the 64 KiB queued-text limit. Decode and normalize provider-controlled JSON in a non-main-actor helper. Pass Data or a Sendable DTO across the actor boundary. Keep only session and UI state changes on MainActor.

🤖 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/AgentSessionProcessStore.swift` around lines 259 - 308, Move
request JSON decoding and normalization currently performed in the
MainActor-isolated request handling flow around CodexAppServerSession and
AgentSessionProcessStore into a non-MainActor, Sendable helper. Pass the
resulting Data or Sendable DTO across the actor boundary, keeping only
session/UI state mutations and event coordination on MainActor; ensure
provider-controlled input remains outside the queued-text limit.

Source: Coding guidelines

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

Inline comments:
In `@cmuxTests/CodexAppServerSessionTests.swift`:
- Around line 1378-1382: Replace the fixed Task.yield loop after
session.consumeStdout in the approval test with an explicit completion signal
from userInputHandler. Await that signal before inspecting receivedRequest or
sentLines, using a continuation, AsyncStream, or deadline-bounded predicate so
assertions only run after the response task completes.

---

Outside diff comments:
In `@Sources/Panels/AgentSessionProcessStore.swift`:
- Around line 721-732: The MCP value handling around mcpValue must enforce the
accepted schema before serialization: validate property names, reject values
outside allowed enum selections, and enforce numeric minimum and maximum bounds.
When validation fails, cancel the elicitation rather than falling through to
typedMCPValue; preserve valid indexed, named, and schema-compatible values.
- Around line 259-308: Move request JSON decoding and normalization currently
performed in the MainActor-isolated request handling flow around
CodexAppServerSession and AgentSessionProcessStore into a non-MainActor,
Sendable helper. Pass the resulting Data or Sendable DTO across the actor
boundary, keeping only session/UI state mutations and event coordination on
MainActor; ensure provider-controlled input remains outside the queued-text
limit.
🪄 Autofix

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 Plus

Run ID: e7f94db2-8a82-41d5-9f89-61bc4ecfffa1

📥 Commits

Reviewing files that changed from the base of the PR and between 844297c and 21b3fe0.

📒 Files selected for processing (5)
  • CLI/CodexTeamsApprovalBridge.swift
  • Sources/Panels/AgentSessionProcessStore.swift
  • Sources/Panels/CodexAppServerSession.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/CodexAppServerSessionTests.swift

Comment on lines +1378 to +1382
session.consumeStdout(
#"{"id":"approval-1","method":"item/commandExecution/requestApproval","params":{"threadId":"thread-1","command":"swift test","availableDecisions":["accept","decline"]}}"#
+ "\n"
)
for _ in 0..<3 { await Task.yield() }

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

Replace the fixed yields with a completion signal.

Line 1382 does not guarantee that userInputHandler has run. The test can read receivedRequest or sentLines before the response task completes.

Signal from userInputHandler and await that signal before the assertions. Use a continuation, an AsyncStream, or a deadline-bounded predicate.

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

In `@cmuxTests/CodexAppServerSessionTests.swift` around lines 1378 - 1382, Replace
the fixed Task.yield loop after session.consumeStdout in the approval test with
an explicit completion signal from userInputHandler. Await that signal before
inspecting receivedRequest or sentLines, using a continuation, AsyncStream, or
deadline-bounded predicate so assertions only run after the response task
completes.

Source: Coding guidelines

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+SecondaryPromotion.swift:
- Line 650: In the reuse path following removeAgentFeedSnapshot(ownerKey:
previousForegroundID), also schedule scheduleSecondaryAgentFeedRefresh using
subscription.ownerKey.pairingID and the existing client. Keep the existing
scheduleSecondaryNotificationFeedRefresh call and ensure the demoted pairing’s
Agent Feed is refreshed immediately after its snapshot is removed.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamQuestionPrompt`+Parsing.swift:
- Around line 169-179: Update the rawDefault decoding near rawDefaultValue to
distinguish bridged JSON booleans from numbers before converting values. Apply
the existing CFBooleanGetTypeID-based check used for option values around the
NSNumber handling, preserving true/false conversion only for actual CFBoolean
values and using NSNumber.stringValue for numeric defaults.

In `@Sources/Panels/AgentSessionProcessStore.swift`:
- Around line 797-800: Remove the invalid optional chaining from the schema type
lookup in the surrounding conversion function: after the guard let schema
binding, access the type with schema["type"] instead of schema?["type"],
preserving the existing lowercasing and switch behavior.
🪄 Autofix

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 Plus

Run ID: 722899de-4465-497e-acd0-f39873b09d79

📥 Commits

Reviewing files that changed from the base of the PR and between 21b3fe0 and 1fa8ca4.

📒 Files selected for processing (17)
  • CLI/FeedEventClassifier.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/AgentFeedCacheStore.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentFeed.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+HiddenMacs.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+SecondaryPromotion.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/AgentFeedCacheStoreTests.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileAgentFeedMutationState.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedRow.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Resources/Localizable.xcstrings
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamQuestionPrompt+Parsing.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift
  • Sources/Feed/FeedCoordinator.swift
  • Sources/Feed/FeedPermissionActionPolicy.swift
  • Sources/Panels/AgentSessionProcessStore.swift
  • Sources/Panels/CodexAppServerSession.swift
  • Sources/TerminalController.swift
  • cmuxTests/CodexAppServerSessionTests.swift

removeNotificationFeedSnapshot(
macDeviceID: previousForegroundID
)
removeAgentFeedSnapshot(ownerKey: previousForegroundID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Refresh the demoted pairing’s Agent Feed after removing its bare snapshot.

Line 650 deletes the old foreground Agent Feed snapshot. The reuse path only schedules scheduleSecondaryNotificationFeedRefresh, so the demoted Mac has no pairing-keyed Agent Feed snapshot until an unrelated refresh occurs. Schedule scheduleSecondaryAgentFeedRefresh(ownerKey:client:) with subscription.ownerKey.pairingID in that path.

Proposed fix
 self.scheduleSecondaryNotificationFeedRefresh(
     macDeviceID: subscription.ownerKey.pairingID,
     client: subscription.client,
     displayName: subscription.displayName
 )
+self.scheduleSecondaryAgentFeedRefresh(
+    ownerKey: subscription.ownerKey.pairingID,
+    client: subscription.client
+)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+SecondaryPromotion.swift
at line 650, In the reuse path following removeAgentFeedSnapshot(ownerKey:
previousForegroundID), also schedule scheduleSecondaryAgentFeedRefresh using
subscription.ownerKey.pairingID and the existing client. Keep the existing
scheduleSecondaryNotificationFeedRefresh call and ensure the demoted pairing’s
Agent Feed is refreshed immediately after its snapshot is removed.

Comment on lines +169 to +179
let rawDefault: Any? = dictionary["default"] ?? dictionary["default_value"]
let rawDefaultValue: String?
if let value = rawDefault as? String {
rawDefaultValue = value
} else if let value = rawDefault as? Bool {
rawDefaultValue = value ? "true" : "false"
} else if let value = rawDefault as? NSNumber {
rawDefaultValue = value.stringValue
} else {
rawDefaultValue = 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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Check NSNumber before Bool so numeric defaults survive.

JSONSerialization bridges JSON integers to NSNumber. On the Objective-C bridge, an NSNumber holding 0 or 1 also satisfies as? Bool. Line 173 therefore runs before line 175, and {"type":"integer","default":1} produces rawDefaultValue == "true".

For inputType == .boolean the new mapping at lines 181-187 absorbs this, so boolean fields are correct. Numeric fields are not: AgentFeedRow.fieldValueIsValid parses the default with Double("true"), which returns nil and blocks form submission, and formSelectionsForSubmission would emit id=true.

Line 239 already solves this discrimination for option values. Apply the same rule to the default decoder.

🐛 Proposed fix for the boolean/number precedence
         let rawDefault: Any? = dictionary["default"] ?? dictionary["default_value"]
         let rawDefaultValue: String?
         if let value = rawDefault as? String {
             rawDefaultValue = value
-        } else if let value = rawDefault as? Bool {
-            rawDefaultValue = value ? "true" : "false"
         } else if let value = rawDefault as? NSNumber {
-            rawDefaultValue = value.stringValue
+            // JSONSerialization bridges booleans and numbers through
+            // NSNumber, and 0/1 satisfy `as? Bool`. Inspect the underlying
+            // type so numeric defaults keep their numeric text.
+            rawDefaultValue = CFGetTypeID(value as CFTypeRef) == CFBooleanGetTypeID()
+                ? (value.boolValue ? "true" : "false")
+                : value.stringValue
+        } else if let value = rawDefault as? Bool {
+            rawDefaultValue = value ? "true" : "false"
         } else {
             rawDefaultValue = nil
         }

Consider using the same CFBooleanGetTypeID() check at line 239. Int8-backed NSNumber values also report objCType == "c", so the CoreFoundation type id is the stricter test.

Based on learnings: "when guarding against boolean-typed JSON values bridged through [String: Any] … do NOT use value is Bool — JSON integers 0 and 1 round-trip as __NSCFNumber objects that satisfy as? Bool … The correct guard is a CoreFoundation type-id check: CFGetTypeID(value as CFTypeRef) == CFBooleanGetTypeID()."

📝 Committable suggestion

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

Suggested change
let rawDefault: Any? = dictionary["default"] ?? dictionary["default_value"]
let rawDefaultValue: String?
if let value = rawDefault as? String {
rawDefaultValue = value
} else if let value = rawDefault as? Bool {
rawDefaultValue = value ? "true" : "false"
} else if let value = rawDefault as? NSNumber {
rawDefaultValue = value.stringValue
} else {
rawDefaultValue = nil
}
let rawDefault: Any? = dictionary["default"] ?? dictionary["default_value"]
let rawDefaultValue: String?
if let value = rawDefault as? String {
rawDefaultValue = value
} else if let value = rawDefault as? NSNumber {
// JSONSerialization bridges booleans and numbers through
// NSNumber, and 0/1 satisfy `as? Bool`. Inspect the underlying
// type so numeric defaults keep their numeric text.
rawDefaultValue = CFGetTypeID(value as CFTypeRef) == CFBooleanGetTypeID()
? (value.boolValue ? "true" : "false")
: value.stringValue
} else if let value = rawDefault as? Bool {
rawDefaultValue = value ? "true" : "false"
} else {
rawDefaultValue = 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
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamQuestionPrompt`+Parsing.swift
around lines 169 - 179, Update the rawDefault decoding near rawDefaultValue to
distinguish bridged JSON booleans from numbers before converting values. Apply
the existing CFBooleanGetTypeID-based check used for option values around the
NSNumber handling, preserving true/false conversion only for actual CFBoolean
values and using NSNumber.stringValue for numeric defaults.

Source: Learnings

Comment thread Sources/Panels/AgentSessionProcessStore.swift Outdated
@azooz2003-bit
azooz2003-bit force-pushed the feat-ios-agent-feed-v2 branch from 713f9a5 to d135f50 Compare August 13, 2026 03:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Debug/AgentFeed/AgentFeedPreviewView.swift (1)

128-136: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Complete the reply acknowledgment state transition.

Acknowledge reply only clears drafts and mutationStates. It does not change the completed-turn item. The item remains in the .needsInput projection, so the reply composer appears again after acknowledgment. Update the item through the preview reconciliation path, or remove it from the needs-input projection, before clearing the mutation state.

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

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Debug/AgentFeed/AgentFeedPreviewView.swift`
around lines 128 - 136, Update the Acknowledge reply action in
AgentFeedPreviewView so it reconciles the completed-turn item out of the
needs-input projection before clearing drafts and mutationStates. Use the
existing preview reconciliation path or equivalent item update, ensuring the
reply composer does not reappear after acknowledgment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedRow.swift`:
- Around line 569-587: Update AgentFeedContext.body’s “No response. The agent
stopped waiting.” fallback so it is shown only when turnCompletionActionArea
will not render the fallback; suppress the header message for completed
needs-input turns with !interactionsEnabled and no lastAssistantMessage, while
preserving the existing behavior for other cases.

---

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Debug/AgentFeed/AgentFeedPreviewView.swift`:
- Around line 128-136: Update the Acknowledge reply action in
AgentFeedPreviewView so it reconciles the completed-turn item out of the
needs-input projection before clearing drafts and mutationStates. Use the
existing preview reconciliation path or equivalent item update, ensuring the
reply composer does not reappear after acknowledgment.
🪄 Autofix

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 Plus

Run ID: 9f8a65d1-2ca7-45d9-a024-3001296b2e1f

📥 Commits

Reviewing files that changed from the base of the PR and between 1fa8ca4 and 9f8abe7.

📒 Files selected for processing (11)
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkstreamFeedListItem.swift
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileAgentFeedTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedRow.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentFeedView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Debug/AgentFeed/AgentFeedPreviewScenario.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Debug/AgentFeed/AgentFeedPreviewView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Resources/Localizable.xcstrings
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/Workstream/WorkstreamStoreTests.swift
  • Sources/Feed/FeedCoordinator.swift
  • cmuxTests/FeedCoordinatorTests.swift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant