Skip to content

IOS Sim Refresh#239

Merged
arul28 merged 2 commits into
mainfrom
ade/ios-sim-refresh-12034e5e
May 3, 2026
Merged

IOS Sim Refresh#239
arul28 merged 2 commits into
mainfrom
ade/ios-sim-refresh-12034e5e

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented May 3, 2026

Summary

Refresh the ADE iOS simulator control path and tighten mobile sync/chat behavior for the iOS companion.

What Changed

  • Update the native Indigo simulator input helper for point-space touch payloads and the Xcode 26 mouse-event path.
  • Make Codex app-server chat starts feel immediate, move ADE guidance into Codex input context, and bridge native Codex plan items into ADE plan approvals.
  • Improve mobile sync resilience with mobile heartbeat grace, client heartbeats, pending changeset retries, legacy batch-id decode support, and queued chat.send delivery while reconnecting.
  • Fold iOS lane rebase attention into lane cards/detail UI and keep Work chat local echoes/tool grouping aligned with desktop behavior.
  • Update internal docs for the simulator, sync, and Codex plan-mode contracts.

Validation

  • npm --prefix apps/desktop run test -- src/main/services/ai/tools/systemPrompt.test.ts src/main/services/chat/agentChatService.test.ts src/main/services/ios/iosSimulatorService.test.ts src/main/services/sync/syncHostService.test.ts src/main/services/sync/syncRemoteCommandService.test.ts
  • npm --prefix apps/desktop run typecheck
  • npm --prefix apps/desktop run lint (passes with existing warnings)
  • npm --prefix apps/desktop run build
  • bash apps/desktop/native/ios-sim-helpers/build.sh --smoke --print-json
  • xcodebuild test -project apps/ios/ADE.xcodeproj -scheme ADE -destination id=2CD8BD1C-C5F5-4B9D-B446-803488E4F559 -only-testing:ADETests/ADETests/testSyncAutomaticReconnectWaitsForLiveLanDiscoveryWithoutTailnet -only-testing:ADETests/ADETests/testSyncForegroundReconnectStartRequiresAutomaticRoute -only-testing:ADETests/ADETests/testSyncMessageTooLongTransportFailureForcesErrorState -only-testing:ADETests/ADETests/testSyncClientHeartbeatUsesHalfServerIntervalWithBounds -only-testing:ADETests/ADETests/testRemoteCommandPolicyQueuesChatSendWhenOffline -only-testing:ADETests/ADETests/testWorkChatQueuedSendRequiresLiveSession -only-testing:ADETests/ADETests/testWorkTimelineCarriesQueuedLocalEchoDeliveryState -only-testing:ADETests/ADETests/testSyncChangesetBatchPayloadDecodesLegacyBatchWithoutBatchId
  • node scripts/validate-docs.mjs

Risks

  • Indigo uses private SimulatorKit APIs, so the helper keeps legacy fallback behavior and was smoke-built against the active Xcode 26.3 toolchain.
  • Mobile changeset retry behavior is intentionally conservative: failed apply results are acked with error state and retained for retry rather than forcing immediate disconnect.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Codex AI planning mode with plan approval workflow and explicit plan text rendering.
    • Implemented chat message delivery state tracking (queued vs. sent) based on connection status.
    • Added web search tool support to work chat.
    • Enhanced iOS simulator touch input with proper screen dimension handling.
  • Improvements

    • Redesigned rebase suggestion UI with PR badges and streamlined actions.
    • Optimized mobile peer heartbeat configuration for improved sync reliability.
    • Improved reconnection and transport failure handling for better stability.
    • Refined message composition/sending controls based on live connection state.

Greptile Summary

This PR refreshes the iOS simulator control path (Xcode 26 dual mouse-event signatures, point-space touch payloads), adds a native Codex plan-approval workflow with streaming plan text, and improves mobile sync resilience (client heartbeats, mobile heartbeat grace, changeset retry, queued chat.send delivery, legacy batchId decode). The changes are well-tested with dedicated unit tests covering the new sync, plan, and queue behaviours.

Confidence Score: 4/5

Safe to merge; findings are edge-case P2s that do not break normal flows.

The PR is thoroughly tested and the architecture is sound. The highest-confidence concern is the plan-approval persistence after a failed turn (when the failure arrives after an item/completed plan event), which is untested and could surface a stale approval prompt — but approving it is recoverable. The connectionState: .connecting during teardown is a UX inaccuracy rather than a data issue. No P0/P1 bugs were found.

apps/desktop/src/main/services/chat/agentChatService.ts (plan approval lifecycle on failed turns), apps/ios/ADE/Services/SyncService.swift (connectionState during relay-loop failure)

Important Files Changed

Filename Overview
apps/desktop/src/main/services/chat/agentChatService.ts Adds Codex plan-mode support (planTextByItemId Map, plan approval workflow, optimistic turn start); moves ADE developer instructions from collaborationMode settings to a separate input item; adds interactionMode reset on plan approval acceptance.
apps/ios/ADE/Services/SyncService.swift Adds client-side heartbeat task, mobile reconnect signaling, changeset apply-failure resilience (ack success + retain for retry), legacy batchId decode, and conditionally omits requestId from envelopes when nil/empty.
apps/desktop/native/ios-sim-helpers/sim-input.m Adds Xcode 26 9-argument IndigoMouse path alongside legacy 5-argument path, detected via xcodebuild version at first HID init; all touch events now accept absolute point + screen dimensions instead of pre-normalised ratios.
apps/desktop/src/main/services/sync/syncHostService.ts Raises heartbeat miss limit to 6 for iOS/phone peers (vs 2 for desktop) via new exported syncHeartbeatMissLimitForPeerMetadata helper.
apps/desktop/src/main/services/sync/syncRemoteCommandService.ts chat.send handler now passes awaitDispatch:true to sendMessage, ensuring the command is not acked until the turn is dispatched (required for correct queue behaviour).
apps/ios/ADE/Views/Work/WorkSessionDestinationView.swift Adds canSendChatMessages / sendWillQueueChatMessage computed properties; local echoes now carry deliveryState (sending / queued); sendMessage branches on delivery enum to update echo state and skip refresh for queued messages.
apps/ios/ADE/Views/Work/WorkTimelineHelpers.swift Moves transcriptIndicatesActiveTurn computation into buildWorkChatTimelineSnapshot (fixing previous per-frame O(n) scan); web search events now route to WorkToolCardModel to cluster with other tool cards.
apps/desktop/src/main/services/ios/iosSimulatorService.ts New iosurfaceInputPointPayload function returns absolute point + screen dimensions (used by tap and drag/swipe); normalizeIosSimulatorPointForIndigo is retained but no longer called by production code.

Sequence Diagram

sequenceDiagram
    participant iOS as iOS App
    participant Desktop as Desktop Host
    participant Codex as Codex App-Server

    Note over iOS, Desktop: Mobile Sync — client heartbeat
    iOS->>Desktop: hello (connect)
    Desktop-->>iOS: hello_ack (heartbeatIntervalMs)
    loop every serverMs/2 (5-25 s)
        iOS->>Desktop: heartbeat {kind:ping, dbVersion}
    end

    Note over iOS, Desktop: Queued chat.send while reconnecting
    iOS->>Desktop: chat.send (queueable=true)
    Desktop-->>iOS: {queued:true}
    iOS->>iOS: echo deliveryState = queued
    Desktop->>Desktop: queue until live session
    Desktop->>Codex: turn/start
    Codex-->>Desktop: turn/started

    Note over Desktop, Codex: Codex plan-mode approval
    Desktop->>Codex: turn/start (collaborationMode=plan)
    Codex-->>Desktop: item/plan/delta (streaming)
    Desktop->>Desktop: accumulate planTextByItemId
    Codex-->>Desktop: item/completed {type:plan}
    Desktop->>Desktop: emitCodexPlanTextApproval
    Desktop-->>iOS: approval_request {kind:plan_approval}
    iOS->>Desktop: respondToInput (accept)
    Desktop->>Desktop: permissionMode to edit, interactionMode to default
    Desktop->>Codex: turn/start (collaborationMode=default)
Loading

Comments Outside Diff (1)

  1. apps/desktop/src/main/services/ios/iosSimulatorService.ts, line 329-342 (link)

    P2 Dead exported function after coordinate API change

    normalizeIosSimulatorPointForIndigo now has no production call sites — both tap and drag were migrated to iosurfaceInputPointPayload in this PR. The function is still exported and tested, but shipping a dead public symbol can cause confusion about which API callers should use for Indigo input coordinates. If the function is kept only as a test utility, it should be unexported; if it's meant to be called externally in the future, a comment explaining that intent would help.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/ios/iosSimulatorService.ts
    Line: 329-342
    
    Comment:
    **Dead exported function after coordinate API change**
    
    `normalizeIosSimulatorPointForIndigo` now has no production call sites — both `tap` and `drag` were migrated to `iosurfaceInputPointPayload` in this PR. The function is still exported and tested, but shipping a dead public symbol can cause confusion about which API callers should use for Indigo input coordinates. If the function is kept only as a test utility, it should be unexported; if it's meant to be called externally in the future, a comment explaining that intent would help.
    
    How can I resolve this? If you propose a fix, please make it concise.

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

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/desktop/src/main/services/chat/agentChatService.ts:9765-9790
**Plan approval survives a failed turn when a `plan` item completes first**

`emitCodexPlanTextApproval` is called immediately when `item/completed` fires with `itemType === "plan"`, storing the approval in `runtime.approvals`. When the turn subsequently completes with `status === "failed"`, the code at turn-completion only clears approvals whose `kind !== "plan_approval"`, so the stale approval remains visible to the user.

The delta-only failure path is exercised by the existing `"does not request native Codex plan approval after a failed turn"` test, but that test accumulates text only via `item/plan/delta` — it never sends `item/completed` with `type: "plan"`. If Codex emits a completed plan item and then the turn fails (e.g., a subsequent tool call errors), the user will see an actionable plan-approval prompt from the failed turn. Approving it would transition `permissionMode` to `"edit"` and start a new turn, potentially causing unexpected implementation work.

Consider checking `status` in the `turn/completed` handler and explicitly clearing (or voiding) any pending `plan_approval` entries when the turn did not complete successfully.

### Issue 2 of 3
apps/desktop/native/ios-sim-helpers/sim-input.m:200-225
**`selectedXcodeMajor` uses the deprecated `[task launch]` API**

`NSTask`'s `-launch` method has been deprecated since macOS 10.13 in favour of `-launchAndReturnError:`. Because `[task launch]` raises an `NSInvalidArgumentException` when the executable is not found (rather than returning an error), the `@try/@catch` wrapper is still correct — but future SDK changes may alter `@catch`-able exceptions vs. hard errors. The version check is also called synchronously from `loadIndigoSymbolsOnly` during the first HID initialisation, so on the very first tap or swipe the call chain will block on `xcodebuild -version` completing (typically 0.5–1 s). Since the helper is long-lived this only happens once, but it is worth noting if startup latency is ever profiled.

Consider replacing `-launch` with `-launchAndReturnError:` and propagating the `NSError` to `elog`.

### Issue 3 of 3
apps/ios/ADE/Services/SyncService.swift:5408-5415
**`connectionState` set to `.connecting` during relay-loop failure before any reconnect begins**

In the reconnect-delay branch of the relay loop, `handleTransportFailure` is now called with `connectionState: .connecting`. `handleTransportFailure` maps this straight through `syncConnectionStateAfterTransportFailure`, so unless the error is `messageTooLong` the persisted `connectionState` becomes `.connecting` even though the socket is being torn down and no reconnect has started yet. A user watching the connection status during this window would see "Connecting" when the device is actually disconnected and waiting to retry. The original `.disconnected` value correctly reflected the socket state at that moment. The transition to `.connecting` should arguably wait until a reconnect attempt is actively in flight (e.g., inside `reconnectIfPossible`).

Reviews (2): Last reviewed commit: "fix: address PR review feedback" | Re-trigger Greptile

@vercel
Copy link
Copy Markdown

vercel Bot commented May 3, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview May 3, 2026 5:02am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

📝 Walkthrough

Walkthrough

This PR spans desktop and iOS platforms with five independent feature areas: Codex plan-mode system prompts and plan-text tracking; iOS Indigo touch input with screen-dimension payloads and Xcode version detection; sync service platform-aware heartbeat limits and chat delivery awareness; iOS lane rebase UI refactoring to remove defer actions; and iOS work-chat delivery-state tracking for queued vs. sent messages.

Changes

Codex Planning & Plan-Mode System Prompt

Layer / File(s) Summary
System Prompt Guidance
apps/desktop/src/main/services/ai/tools/systemPrompt.ts
When permissionMode === "plan" and runtime === "codex-cli", emit Codex-native plan-mode instructions (including proposed-plan mechanism and forbidden tools); otherwise fall back to generic read-only guidance.
Codex Runtime State
apps/desktop/src/main/services/chat/agentChatService.ts
Add planTextByItemId map to track streamed plan text per item, emit plan_text events on deltas, and create plan_approval pending-input requests when plans complete in plan mode.
Chat Integration & Dispatch
apps/desktop/src/main/services/chat/agentChatService.ts
Inject ADE system-context into Codex collaboration-mode input, emit optimistic user-message/status/activity for Codex, wrap onDispatched to fire at backend-acceptance time, and preserve plan approvals across planning→implementation transitions.
Tests & Verification
apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts, apps/desktop/src/main/services/chat/agentChatService.test.ts
Verify Codex plan-mode prompt content, immediate startup activity emission, plan item completion handling including plan-text and approval-request emission with summary-derived descriptions, and plan-approval persistence through turn completion.

iOS Indigo Touch Input & Payload Construction

Layer / File(s) Summary
Helper Function
apps/desktop/src/main/services/ios/iosSimulatorService.ts
Add iosurfaceInputPointPayload(point, screen) exporting normalized x/y plus numeric width/height with finite-value validation.
Tap & Drag Command Wiring
apps/desktop/src/main/services/ios/iosSimulatorService.ts
Update Indigo tap and drag/swipe paths to compute payloads and send point-space coordinates plus screen dimensions via sendIosurfaceInputCommand.
Native C Implementation
apps/desktop/native/ios-sim-helpers/sim-input.m
Add Xcode version detection; route sendTouch(x, y, width, height, ...) through modern 9-argument mouse path (Xcode ≥26) or legacy 5-argument path; update processEvent/swipe to accept width/height and emit normalized down/dragged/up events with per-step interpolation.
Documentation & Tests
apps/desktop/native/ios-sim-helpers/README.md, apps/desktop/src/main/services/ios/iosSimulatorService.test.ts
Document screen-dimension payload format and Xcode-version ABI differences; verify iosurfaceInputPointPayload construction.

Sync Service: Platform-Aware Heartbeat & Chat Delivery Awareness

Layer / File(s) Summary
Heartbeat Miss Limits
apps/desktop/src/main/services/sync/syncHostService.ts
Add DEFAULT_SYNC_HEARTBEAT_MISS_LIMIT = 2 and MOBILE_SYNC_HEARTBEAT_MISS_LIMIT = 6; export syncHeartbeatMissLimitForPeerMetadata(metadata) to return platform-specific thresholds; adjust heartbeat timeout condition to use platform limit instead of fixed 2.
Chat Delivery Result
apps/ios/ADE/Services/SyncService.swift
Add SyncChatMessageDelivery enum with .sent and .queued cases; change sendChatMessage(...) return type to SyncChatMessageDelivery and inspect host queued response flag to determine delivery state.
Chat Send Remote Command
apps/desktop/src/main/services/sync/syncRemoteCommandService.ts
Pass { awaitDispatch: true } options object to agentChatService.sendMessage from chat.send remote command handler.
iOS Sync Enhancements
apps/ios/ADE/Services/SyncService.swift
Add client-side heartbeat task (clientHeartbeatTask) initiated after hello using computed interval from syncClientHeartbeatIntervalNanoseconds(...); improve transport-failure handling with syncConnectionStateAfterTransportFailure(...) to mark message-too-long as .error; backfill missing changeset batchId from request envelope; omit requestId from JSON when empty; add isRemoteActionQueueable(_:) helper.
Tests
apps/desktop/src/main/services/sync/syncHostService.test.ts, apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts, apps/ios/ADETests/ADETests.swift
Verify platform-specific heartbeat miss limits; confirm chat.send with awaitDispatch option; verify automatic reconnect with LAN discovery; verify queued vs. sent delivery states and pending operation handling.

iOS Lanes Rebase UI Refactoring

Layer / File(s) Summary
Card-Level Warning Component
apps/ios/ADE/Views/Lanes/LaneComponents.swift
Add LaneCardRebaseWarningPresentation enum modeling suggestion/autoRebase states and computed UI properties (icon, tint, title, detail); add LaneCardRebaseWarning view; integrate warning into LaneStackCard via computed rebaseWarning property.
Banner API & UI
apps/ios/ADE/Views/Lanes/LaneDetailRebaseBanner.swift
Replace onRebase/onDefer with onViewRebase; add hasPr input; rebuild layout with "REBASE SUGGESTED" header, PR/behind badges, and single "View in Rebase/Merge tab" button (replacing prior three-button row); change displayed text source to bodyCopy.
Integration & Cleanup
apps/ios/ADE/Views/Lanes/LaneDetailGitSection.swift, apps/ios/ADE/Views/Lanes/LaneDetailScreen.swift, apps/ios/ADE/Views/Lanes/LaneListViewParts.swift, apps/ios/ADE/Views/LanesTabView.swift
Wire rebase banner to requestGitConfirmation(.rebaseLane) via onViewRebase; remove handleRebaseSuggestionDefer() method; remove lane-list attentionSection and related computed properties (visibleSuggestions, visibleAutoRebaseAttention); remove attention-section conditional from LanesTabView.

iOS Work Chat: Message Delivery State & Queueing Logic

Layer / File(s) Summary
Delivery State Model
apps/ios/ADE/Views/Work/WorkModels.swift
Add optional deliveryState: String? property to WorkLocalEchoMessage to track "queued" vs. "sending" state.
Send Queueing & Delivery Handling
apps/ios/ADE/Views/Work/WorkSessionDestinationView.swift
Add helper functions workChatCanSendMessages(...) and workChatSendWillQueueMessage(...) based on isLive, hostReachable, and chatSendQueueable policy; compute canComposeChatMessages, canSendChatMessages, sendWillQueueChatMessage and forward to WorkChatSessionView; update initial opening-prompt echo and staged echo to set deliveryState and branch send handling on .queued vs. .sent delivery results; add updateLocalEchoDeliveryState(...) method.
Composer Gating & Feedback
apps/ios/ADE/Views/Work/WorkChatSessionView.swift
Replace isLive-based gating with new canComposeMessages, canSendMessages, sendWillQueue inputs; gate canCompose on canComposeMessages and canSend on canSendMessages && !sending; update composerFeedback to reflect queued/sending/reconnect states; add transcriptIndicatesActiveTurn to broaden streaming-indicator logic beyond live+active sessions.
Message Assembly & Timeline
apps/ios/ADE/Views/Work/WorkNavigationAndTranscriptHelpers.swift, apps/ios/ADE/Views/Work/WorkTimelineHelpers.swift
Handle .webSearch envelope events as tool-card items (not standalone event cards); build WorkChatMessage with explicit deliveryState field to preserve queued/sending state in rendered timeline.
Tests & Previews
apps/ios/ADE/Views/Work/WorkPreviews.swift, apps/ios/ADETests/ADETests.swift
Add preview properties (canComposeMessages, canSendMessages, sendWillQueue); verify work-chat send eligibility logic; verify message delivery state preservation in timeline.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • chat-ux: collapse thoughts + scroll fixes #190: Modifies agent chat session archival types, API signatures, and related UI flows (e.g., archivedAt, archive/unarchive handlers), complementing this PR's agent chat service changes.
  • chat title summaries #110: Adds requestedCwd and manuallyNamed fields to agent chat session creation and updates downstream callers (e.g., prRebaseResolver), relating to agent chat initialization touched by this PR's system-prompt and chat-service work.
  • Preserve Claude chat resume state and surface orphan lanes #175: Modifies Work/agent chat UI components (WorkChatSessionView, WorkTimeline), session-list grouping, and chat-history handling, directly related to this PR's iOS work-chat delivery-state and timeline changes.

Suggested labels

desktop, ios

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'IOS Sim Refresh' is vague and generic, using non-descriptive terms that don't convey specific information about the actual changeset. Provide a more descriptive title that specifies key changes, such as 'Add Xcode-version-aware touch event handling for iOS simulator' or 'Support modern and legacy Indigo paths for iOS simulator input.'
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/ios-sim-refresh-12034e5e

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get your free trial and get 200 agent minutes per Slack user (a $50 value).


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 and usage tips.

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 3, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

Copilot AI commented May 3, 2026

@copilot review but do not make fixes

Here's the review — no changes made.


apps/desktop/native/ios-sim-helpers/sim-input.m

  • selectedXcodeMajor() launches a synchronous xcodebuild -version subprocess inside loadIndigoSymbolsOnly, which runs on the same call path as HID initialization. If xcodebuild is slow or absent (e.g., in a CI or non-Xcode env), the helper will stall or silently return 0 and fall back to the legacy path. A short timeout or a cached/env-var override would make this more robust.
  • In sendModernTouch, the CGPoint is pre-normalized (clampUnit(x / width)) but then width and height are also passed as the last two arguments. The intent appears to be passing normalized coordinates + logical dimensions to the 9-arg API, but without documentation of the private IndigoHIDMessageForMouseNSEvent 9-argument variant, it's hard to verify that the point and dimension arguments aren't double-normalizing. Worth a comment documenting the assumed coordinate-space contract.
  • warmIndigoService calls usleep(20000) (20 ms) unconditionally on the calling thread after each warmup send. This adds ~40 ms to every cold HID init. That's probably fine but should be noted since it blocks initialization.

apps/desktop/src/main/services/chat/agentChatService.ts

  • ADE context input only added when collaborationMode is truthy: buildCodexAdeContextInput is only pushed onto input inside if (collaborationMode). A Codex session that is not in plan/default collaboration mode (e.g., config-toml source) never receives the ADE guidance. The old path (developer_instructions on the settings payload) was always set when the session was Codex. Confirm this gap is intentional.
  • Optimistic turn start error recovery: With optimisticCodexTurnStart = true, the "started" status event and activity event are emitted before turn/start returns. If the network call throws after setSessionActive() but before markDispatched() fires, the session will appear stuck in an active/started state on the renderer. The exception handling inside sendCodexMessage sets runtime.awaitingTurnStart = false and rethrows, but there's no compensating "status: failed" event emitted from that catch block. A try/catch wrapper at the optimistic-start callsite that emits a cleanup status event on error would close this gap.
  • Plan approvals survive turn/completed indefinitely: At turn/completed the code now skips clearing plan_approval entries from runtime.approvals. If a plan approval is created but not resolved before the session's next send, the stale approval remains in the map and hasExistingApproval will match on description equality, silently suppressing a duplicate approval UI for a new plan with the same text. This could cause the approval widget to not appear for a legitimately new plan.
  • interactionMode mutation after plan approval: In the approval handler, managed.session.interactionMode = "default" is assigned directly. This looks like it's meant to exit plan mode in the session record, but permissionMode is also set to "edit" in the same block. If interactionMode is supposed to drive the Codex collaboration-mode payload on the next turn, this mutation won't take effect until sendMessage is called again — which is fine — but it's also not persisted unless persistChatState picks it up (it does). Just worth confirming this field isn't separately synchronized back to the Codex runtime process.
  • emitCodexPlanTextApproval returns true when not in plan mode: When managed.session.permissionMode !== "plan", the function returns true (indicating "handled") without emitting anything. Callers use the return value to gate further processing (no call site currently uses the return value in a branching way, so this is low risk), but the semantics of returning true for "skipped" vs. "emitted" are inconsistent — consider returning false or void for the no-op case to avoid confusion.

apps/desktop/src/main/services/sync/syncHostService.ts

  • syncHeartbeatMissLimitForPeerMetadata is exported but appears only used internally and in tests. Fine if it's meant to be testable, but the export is worth being intentional about.

apps/ios/ADE/Services/SyncService.swift

  • startClientHeartbeatTask allocates ISO8601DateFormatter() on every heartbeat loop iteration (ISO8601DateFormatter().string(from: Date())). The class already has private let syncDateFormatter = ISO8601DateFormatter(). Capturing self.syncDateFormatter (or a local copy) in the task closure would avoid repeated allocations in the hot loop.
  • Changeset apply errors are now acked as success: When database.applyChanges(batch.changes) throws, the catch block sets lastError and calls resolve(requestId:result:.success(payload)), effectively telling the server the changeset was successfully applied. If the server tracks which changesets each client has seen, it will never re-send this changeset. The PR notes this is "intentionally conservative" with a retry plan, but there's no retry path visible in this diff — the failed changeset is discarded. This risks a permanent data gap on the client if the error is persistent (e.g., a schema mismatch). Worth confirming there's a reconciliation path or surfacing the error more visibly.
  • sendEnvelope omits requestId when nil: Previously the envelope always included "requestId": requestId as Any (which serializes as null for a nil value). The new behavior omits the key entirely. If the server's message parser uses in: / key-presence to decide whether to send a reply, this is a behavior change. Confirm the server accepts envelopes without a requestId key (rather than "requestId": null) for fire-and-forget messages.
  • handleTransportFailure phase .disconnected.connecting: The reconnect loop now emits phase .connecting instead of .disconnected on initial retry. This flows into syncConnectionStateAfterTransportFailure, which means transient failures during reconnect attempts now show connectionState = .connecting rather than .disconnected. Verify the reconnect-state UI in the iOS app handles .connecting state gracefully when the user manually disconnects.

apps/ios/ADE/Views/Work/WorkChatSessionView.swift

  • transcriptIndicatesActiveTurn is an O(n) computed property called every render: It scans the entire transcript array on each SwiftUI render pass. For long transcripts this could cause jank, especially on a reconnect-intensive session. Consider deriving this value once in a .onChange(of: transcript) or task and storing it as @State.

apps/ios/ADE/Views/Lanes/LaneDetailRebaseBanner.swift

  • The Defer button has been removed with no replacement. If a user wants to acknowledge the suggestion without navigating to the Rebase/Merge tab (e.g., they plan to rebase from desktop), there's currently no way to dismiss the banner without acting on it. The old onDefer closure was the dismiss-without-acting path. The new onDismiss is still present in the struct signature but the Dismiss button body was removed — confirm the dismiss path is wired up elsewhere (e.g., via LaneCardRebaseWarning interaction).

Copilot finished work on behalf of arul28 May 3, 2026 04:42
Comment thread apps/ios/ADE/Views/Work/WorkChatSessionView.swift Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (4)
apps/ios/ADE/Services/SyncService.swift (1)

5512-5521: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't continue as healthy after applyChanges fails.

This catch path now NACKs the batch and sets lastError, but it no longer degrades connectionState/domain status or forces a recovery pass. If database.applyChanges(...) throws, the app can stay connected while serving stale local data. Please mark sync as failed and trigger reconnect/full hydration here instead of resolving this as a normal success path.

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

In `@apps/ios/ADE/Services/SyncService.swift` around lines 5512 - 5521, When
database.applyChanges(...) throws, do not resolve the request as a normal
success; instead keep the NACK and set lastError, mark the sync/connection state
as failed (e.g. set connectionState = .failed or call the existing method that
marks sync failure), and trigger the recovery/reconnect/full-hydration flow
before resolving. Replace the resolve(requestId: requestId, result:
.success(payload)) call in this catch block with logic that sets the failed
state and invokes the app's recovery/reconnect routine (the same code path used
elsewhere for full rehydration/reconnect), then resolve or requeue the request
only after recovery is scheduled.
apps/ios/ADE/Views/Work/WorkSessionDestinationView.swift (1)

405-467: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate the auto-sent opening prompt with the same send eligibility as the composer.

This path still stages a "sending" local echo and calls sendChatMessage(...) even when canSendChatMessages is false. In other words, the UI can correctly disable Send for an ended or non-queueable offline session, but the automatic opening prompt still tries to send anyway.

Proposed fix
 `@MainActor`
 func sendInitialOpeningPromptIfNeeded() async {
   let prompt = trimmedInitialOpeningPrompt
   guard !prompt.isEmpty else { return }
   guard !sending else { return }
+  guard canSendChatMessages else { return }
   let promptKey = "\(sessionId)|\(prompt)"
   guard handledOpeningPromptKey != promptKey else { return }
   if transcript.contains(where: { envelope in
     if case .userMessage(let text, _, _, _, _) = envelope.event {
       return text.trimmingCharacters(in: .whitespacesAndNewlines) == prompt
@@
 `@MainActor`
 func stageInitialOpeningPromptEchoIfNeeded() {
   let prompt = trimmedInitialOpeningPrompt
   guard !prompt.isEmpty else { return }
+  guard canSendChatMessages else { return }
   let promptKey = "\(sessionId)|\(prompt)"
   guard stagedOpeningPromptKey != promptKey else { return }
   stagedOpeningPromptKey = promptKey
   localEchoMessages.append(WorkLocalEchoMessage(
     text: prompt,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Views/Work/WorkSessionDestinationView.swift` around lines 405 -
467, The auto-send logic needs to respect the same send eligibility as the
composer: in sendInitialOpeningPromptIfNeeded and
stageInitialOpeningPromptEchoIfNeeded, gate staging and sending by the existing
canSendChatMessages (or the same predicate used to enable the Send button)
before creating a local echo or calling
syncService.sendChatMessage(sessionId:text:); if canSendChatMessages is false,
do not append a "sending" local echo or call sendChatMessage (you may still set
handledOpeningPromptKey to avoid retry loops), and use sendWillQueueChatMessage
only when queueing is allowed by that same predicate.
apps/desktop/src/main/services/ios/iosSimulatorService.ts (1)

4469-4470: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject negative swipe durations here.

durationMs is only checked for finiteness, so negative values reach the Indigo path and behave differently from the idb fallback, which clamps them to 0.01s. Tighten this to durationMs >= 0 so both backends stay equivalent.

Suggested fix
-      if (durationMs != null && !Number.isFinite(durationMs)) throw new Error("durationMs must be a number.");
+      if (durationMs != null && (!Number.isFinite(durationMs) || durationMs < 0)) {
+        throw new Error("durationMs must be a non-negative number.");
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/ios/iosSimulatorService.ts` around lines 4469
- 4470, The current validation only ensures durationMs is finite which allows
negative durations to pass through and diverge between Indigo and idb backends;
update the check that validates durationMs (the line referencing durationMs and
deltaValue) to reject negative values by requiring durationMs to be a finite
number >= 0 (e.g., change the condition to !Number.isFinite(durationMs) ||
durationMs < 0) and update the thrown error message to indicate a non-negative
duration, keeping the existing deltaValue validation as-is.
apps/desktop/native/ios-sim-helpers/sim-input.m (1)

418-462: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate touch/swipe numbers before coercing them.

This path now ACKs malformed payloads as success: missing x/y become 0, and missing width/height become 1, so bad point-space commands can turn into edge taps instead of a typed failure. JSON null values can also trip doubleValue unexpectedly here. Please reject non-finite required fields up front, and only treat omitted width/height as legacy normalized input when x/y are already in [0,1].

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

In `@apps/desktop/native/ios-sim-helpers/sim-input.m` around lines 418 - 462, The
touch/tap/swipe handlers accept nil/null or non-finite numbers and silently
coerce them (e.g. x/y -> 0, width/height -> 1); update validation so sendTouch
is only called with finite required coordinates and safe sizes: for all handlers
(the touch branch, tap branch using x/y/hold, and swipe branch using
startX/startY/endX/endY/durationMs) check that each required numeric field is
present, non-null and isfinite() (or equivalent) before coercion; if validation
fails return NO and populate errorOut; for width/height only allow the legacy
default (positiveOrDefault) when width/height are omitted AND the provided x/y
(or startX/startY/endX/endY) are already in [0,1]; otherwise treat
missing/invalid width/height as an error. Ensure JSON nulls are detected (not
just doubleValue) so malformed payloads are rejected rather than turned into
edge taps; reference sendTouch, positiveOrDefault, MouseEvent* and
MouseDirection* to locate where to add these checks.
🧹 Nitpick comments (5)
apps/ios/ADE/Views/Work/WorkModels.swift (1)

40-45: ⚡ Quick win

Use a typed delivery-state enum instead of raw strings.

This PR introduces a real state machine for local echoes, but deliveryState is still a free-form String?. A typo in any of the touched views/helpers will silently break badges/timeline rendering. I'd strongly prefer a shared enum here and in WorkChatMessage so the compiler enforces the allowed states.

♻️ Suggested direction
+enum WorkMessageDeliveryState: String, Equatable {
+  case sending
+  case queued
+}
+
 struct WorkLocalEchoMessage: Identifiable, Equatable {
   let id = UUID().uuidString
   let text: String
   let timestamp: String
-  var deliveryState: String? = nil
+  var deliveryState: WorkMessageDeliveryState? = nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Views/Work/WorkModels.swift` around lines 40 - 45, Replace the
free-form deliveryState String? on WorkLocalEchoMessage with a strongly-typed
enum (e.g., DeliveryState) and update any related model WorkChatMessage to use
the same enum so the compiler enforces valid states; define DeliveryState as a
Codable/Equatable RawRepresentable (String) enum listing the allowed cases
(pending, sent, failed, etc.), change WorkLocalEchoMessage.deliveryState to
DeliveryState? and update all usages/comparisons, initializers and JSON
(de)coding paths to convert to/from the enum instead of raw strings.
apps/ios/ADE/Views/Work/WorkChatSessionView.swift (1)

155-186: ⚡ Quick win

Cache active-turn detection instead of rescanning the transcript each render.

transcriptIndicatesActiveTurn is a computed property, so every unrelated WorkChatSessionView state update re-walks the entire transcript. This view already caches timeline-derived state to avoid that pattern; moving this flag into the existing rebuild path would keep long chats from paying an extra O(n) scan on every render.

As per coding guidelines, apps/ios/**/*.swift: iOS Swift app — check for memory management, Swift conventions, and proper SwiftUI patterns.

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

In `@apps/ios/ADE/Views/Work/WorkChatSessionView.swift` around lines 155 - 186,
transcriptIndicatesActiveTurn is expensive because it's a computed property that
rescans transcript on every render; convert it to a cached stored Bool that's
updated during the existing timeline rebuild path (instead of being recomputed
in the view). Add a stored property (e.g. a private var or `@State/`@Binding as
appropriate) like transcriptIndicatesActiveTurnCached, compute its value once
inside the timeline/session rebuild function (the routine that currently derives
timeline state) by reusing the same logic that inspects transcript and
envelope.event/.done, and then have the WorkChatSessionView read that cached
property rather than the computed transcriptIndicatesActiveTurn; ensure the
cached value is updated whenever transcript changes or the rebuild runs.
apps/desktop/src/main/services/ios/iosSimulatorService.test.ts (1)

503-513: ⚡ Quick win

Cover the validation branches too.

This only locks in the happy path. Please add one failure case for non-finite coordinates and one for invalid screen metrics, since those branches are what protect the Indigo fallback path from regressions.

Suggested additions
   it("builds point-space Indigo input payloads with screen dimensions", () => {
     expect(iosurfaceInputPointPayload(
       { x: 201, y: 830 },
       { width: 402, height: 874 },
     )).toEqual({
       x: 201,
       y: 830,
       width: 402,
       height: 874,
     });
   });
+
+  it("rejects non-finite point coordinates", () => {
+    expect(() => iosurfaceInputPointPayload(
+      { x: Number.NaN, y: 830 },
+      { width: 402, height: 874 },
+    )).toThrow(/point coordinates are required/i);
+  });
+
+  it("rejects invalid screen metrics", () => {
+    expect(() => iosurfaceInputPointPayload(
+      { x: 201, y: 830 },
+      { width: 0, height: 874 },
+    )).toThrow(/screen metrics are required/i);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/ios/iosSimulatorService.test.ts` around lines
503 - 513, Add two unit tests for iosurfaceInputPointPayload to cover validation
branches: one that calls iosurfaceInputPointPayload with non-finite coordinates
(e.g., x: Infinity or y: NaN) and asserts the function fails the validation
(expect a thrown error or a falsy/invalid result per the function's contract);
and one that calls iosurfaceInputPointPayload with invalid screen metrics (e.g.,
width/height <= 0 or NaN) and similarly asserts the validation branch is taken
(throw or falsy result). Ensure both tests reference iosurfaceInputPointPayload
so the non-happy-path branches are exercised.
apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts (1)

46-56: ⚡ Quick win

Add a non-interactive Codex plan-mode regression case.

Line 46 currently validates only positive Codex-plan wording. Please add a case for interactive: false to assert the prompt does not nudge request_user_input and does not include the non-Codex exitPlanMode approval path.

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

In `@apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts` around lines 46
- 56, Add a regression test that calls buildCodingAgentSystemPrompt with
permissionMode: "plan", runtime: "codex-cli", and interactive: false, and assert
the returned prompt does NOT contain the strings "request_user_input" and
"exitPlanMode"; this complements the existing positive case by verifying
non-interactive Codex plan prompts neither nudge for user input nor include the
non-Codex exitPlanMode approval path.
apps/desktop/src/main/services/chat/agentChatService.ts (1)

2907-2919: ⚡ Quick win

Type this helper against the Codex input contract.

Returning Record<string, unknown> drops compile-time validation for the payload we send to Codex, so field drift in type/text/text_elements becomes a runtime-only failure in the new ADE-context path. Please return the concrete Codex input-item type here and run the desktop typecheck/build/lint pass after tightening it. As per coding guidelines, apps/desktop/**/*.{ts,tsx}: Run typecheck with npm --prefix apps/desktop run typecheck, run build with npm --prefix apps/desktop run build, and run lint with npm --prefix apps/desktop run lint for desktop app validation.

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

In `@apps/desktop/src/main/services/chat/agentChatService.ts` around lines 2907 -
2919, The function buildCodexAdeContextInput currently returns Record<string,
unknown>; change its return signature to the concrete Codex input-item type used
by our Codex/ADE pipeline (replace Record<string, unknown> with the repository's
Codex input item interface—e.g., CodexInputItem or the exact name exported by
the Codex types), ensure the object shape matches that interface (type: "text",
text: string, text_elements: the correct array element type), add any necessary
imports, then run the desktop validations: npm --prefix apps/desktop run
typecheck, npm --prefix apps/desktop run build, and npm --prefix apps/desktop
run lint to confirm the fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/services/ai/tools/systemPrompt.ts`:
- Around line 149-153: The Codex-specific guidance currently unconditionally
tells the agent to use request_user_input when runtime === "codex-cli"; change
the prompt generation so the sentence that says "use request_user_input for
important clarifications when needed" is only included when the session is
interactive (i.e., interactive !== false), and when interactive === false
replace or augment that guidance with an explicit instruction to continue
without asking questions and proceed with the best-effort plan; update the
branch that builds the array for runtime === "codex-cli" (the two prompt strings
shown) to conditionally include the request_user_input text based on the
interactivity flag.

In `@apps/desktop/src/main/services/chat/agentChatService.test.ts`:
- Around line 4306-4374: Add Node typings and remove the deprecated compiler
option in the desktop tsconfig: install `@types/node` as a devDependency, add
"types": ["node"] under compilerOptions in the tsconfig (so the missing 'node'
definition TS2688 is resolved), and remove the deprecated "baseUrl" option from
the tsconfig.json (to avoid the TS5101 deprecation/error in future TS releases);
after making these edits run the validation commands: npm --prefix apps/desktop
run test, npm --prefix apps/desktop run typecheck, npm --prefix apps/desktop run
build, and npm --prefix apps/desktop run lint.

In `@apps/desktop/src/main/services/chat/agentChatService.ts`:
- Around line 10217-10225: The loop emitting plan approvals currently runs
unconditionally and can emit approvals even when a turn later fails; modify the
logic around runtime.planTextByItemId so you only call
emitCodexPlanTextApproval(managed, runtime, planText,
runtime.itemTurnIdByItemId.get(planItemId) ?? turnId) when the turn completed
successfully (check the turn completion/status branch) and otherwise skip
emitting and simply clear runtime.planTextByItemId; apply the same gating change
to the other similar block referenced (the one around lines 10231-10240) so
failures/cancels do not produce plan_approval events.
- Around line 10575-10588: Currently a new randomUUID is created for every plan
delta when params.itemId is missing, preventing fragments from grouping; change
the fallback key to a stable per-turn key instead of randomUUID: derive itemId
from params.itemId || turnIdFromParams || runtime.itemTurnIdByItemId.get(turnId)
|| runtime.activeTurnId (e.g. prefix with "__turn-{turnId}" so it's a distinct
stable key), use that stable key when reading/updating runtime.planTextByItemId
and when calling evictOldestEntries/emitChatEvent, and ensure when a real itemId
later arrives you migrate/merge buffered text from the per-turn key into the
real itemId and update runtime.itemTurnIdByItemId accordingly rather than using
randomUUID().

In `@apps/desktop/src/main/services/sync/syncHostService.test.ts`:
- Around line 293-300: The test asserting a wider heartbeat grace window for
mobile peers is a pure helper test and should not be skipped with the CRSQLite
gate; move the it("allows a wider heartbeat grace window for mobile peers", ...)
block out of the surrounding describe.skipIf(!isCrsqliteAvailable()) so it
always runs, keeping the assertion calls to
syncHeartbeatMissLimitForPeerMetadata({ platform: "iOS", deviceType: "phone" })
and syncHeartbeatMissLimitForPeerMetadata({ platform: "unknown", deviceType:
"phone" }) intact and unchanged; ensure the moved test remains in the same test
file alongside other non-CRSQLite-dependent tests.

In `@apps/ios/ADE/Views/Lanes/LaneComponents.swift`:
- Around line 534-536: The LaneCardRebaseWarning view is rendered visually but
not exposed to VoiceOver because LaneStackCard uses an explicit
accessibilityLabel (stackCardAccessibilityLabel) that omits the warning; update
the stackCardAccessibilityLabel computation (or the accessibilityLabel applied
to LaneStackCard) to append the rebase warning text when rebaseWarning is
non-nil (e.g., include rebaseWarning.summary or presentation.summary from the
rebaseWarning/presentation passed to LaneCardRebaseWarning) so the
accessibilityLabel includes both the existing stackCardAccessibilityLabel
content and the inline warning summary; alternatively, if you prefer
component-level accessibility, set an accessibilityLabel on
LaneCardRebaseWarning that returns the warning summary and ensure
LaneStackCard’s overall label concatenates it.
- Around line 556-562: The computed property rebaseWarning currently returns a
rebaseSuggestion before checking autoRebaseStatus, which can hide
higher-severity auto-rebase errors; change the order so
snapshot.autoRebaseStatus is evaluated first (and if status.state !=
"autoRebased" return .autoRebase(state:status.state,message:status.message)),
then fall back to snapshot.rebaseSuggestion (ensuring dismissedAt == nil) to
return .suggestion(behindCount:hasPr:), referencing the rebaseWarning var,
snapshot.autoRebaseStatus, snapshot.rebaseSuggestion and
LaneCardRebaseWarningPresentation.

In `@apps/ios/ADE/Views/Lanes/LaneDetailRebaseBanner.swift`:
- Line 72: The accessibilityLabel currently only uses bodyCopy and omits visible
dynamic badges (hasPr and behindCount); update the accessibilityLabel on
LaneDetailRebaseBanner to include those state values by composing a single
descriptive string (e.g., combine bodyCopy with hasPr presence and behindCount
number) so VoiceOver announces the PR status and how many commits behind; locate
the .accessibilityLabel("Rebase suggested. \(bodyCopy)") modifier and replace it
with a composed label that references bodyCopy, hasPr, and behindCount (ensuring
any optional values are safely formatted).

In `@apps/ios/ADE/Views/Work/WorkSessionDestinationView`+Actions.swift:
- Around line 12-30: sendMessage(_:) currently only filters duplicates/empty
input but still appends a WorkLocalEchoMessage and calls
syncService.sendChatMessage even when the live session is no longer sendable;
add the same live-session send-eligibility guard the composer uses at the start
of sendMessage(_:), and if the session is not sendable return early so you do
not append to localEchoMessages, set sending, or call
syncService.sendChatMessage(sessionId:text:). Ensure you reference the same
condition/flag used by the composer (the live-session send gate) so
updateLocalEchoDeliveryState, reconcileLocalEchoMessages, and other downstream
calls only run when the send gate allows sending.

---

Outside diff comments:
In `@apps/desktop/native/ios-sim-helpers/sim-input.m`:
- Around line 418-462: The touch/tap/swipe handlers accept nil/null or
non-finite numbers and silently coerce them (e.g. x/y -> 0, width/height -> 1);
update validation so sendTouch is only called with finite required coordinates
and safe sizes: for all handlers (the touch branch, tap branch using x/y/hold,
and swipe branch using startX/startY/endX/endY/durationMs) check that each
required numeric field is present, non-null and isfinite() (or equivalent)
before coercion; if validation fails return NO and populate errorOut; for
width/height only allow the legacy default (positiveOrDefault) when width/height
are omitted AND the provided x/y (or startX/startY/endX/endY) are already in
[0,1]; otherwise treat missing/invalid width/height as an error. Ensure JSON
nulls are detected (not just doubleValue) so malformed payloads are rejected
rather than turned into edge taps; reference sendTouch, positiveOrDefault,
MouseEvent* and MouseDirection* to locate where to add these checks.

In `@apps/desktop/src/main/services/ios/iosSimulatorService.ts`:
- Around line 4469-4470: The current validation only ensures durationMs is
finite which allows negative durations to pass through and diverge between
Indigo and idb backends; update the check that validates durationMs (the line
referencing durationMs and deltaValue) to reject negative values by requiring
durationMs to be a finite number >= 0 (e.g., change the condition to
!Number.isFinite(durationMs) || durationMs < 0) and update the thrown error
message to indicate a non-negative duration, keeping the existing deltaValue
validation as-is.

In `@apps/ios/ADE/Services/SyncService.swift`:
- Around line 5512-5521: When database.applyChanges(...) throws, do not resolve
the request as a normal success; instead keep the NACK and set lastError, mark
the sync/connection state as failed (e.g. set connectionState = .failed or call
the existing method that marks sync failure), and trigger the
recovery/reconnect/full-hydration flow before resolving. Replace the
resolve(requestId: requestId, result: .success(payload)) call in this catch
block with logic that sets the failed state and invokes the app's
recovery/reconnect routine (the same code path used elsewhere for full
rehydration/reconnect), then resolve or requeue the request only after recovery
is scheduled.

In `@apps/ios/ADE/Views/Work/WorkSessionDestinationView.swift`:
- Around line 405-467: The auto-send logic needs to respect the same send
eligibility as the composer: in sendInitialOpeningPromptIfNeeded and
stageInitialOpeningPromptEchoIfNeeded, gate staging and sending by the existing
canSendChatMessages (or the same predicate used to enable the Send button)
before creating a local echo or calling
syncService.sendChatMessage(sessionId:text:); if canSendChatMessages is false,
do not append a "sending" local echo or call sendChatMessage (you may still set
handledOpeningPromptKey to avoid retry loops), and use sendWillQueueChatMessage
only when queueing is allowed by that same predicate.

---

Nitpick comments:
In `@apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts`:
- Around line 46-56: Add a regression test that calls
buildCodingAgentSystemPrompt with permissionMode: "plan", runtime: "codex-cli",
and interactive: false, and assert the returned prompt does NOT contain the
strings "request_user_input" and "exitPlanMode"; this complements the existing
positive case by verifying non-interactive Codex plan prompts neither nudge for
user input nor include the non-Codex exitPlanMode approval path.

In `@apps/desktop/src/main/services/chat/agentChatService.ts`:
- Around line 2907-2919: The function buildCodexAdeContextInput currently
returns Record<string, unknown>; change its return signature to the concrete
Codex input-item type used by our Codex/ADE pipeline (replace Record<string,
unknown> with the repository's Codex input item interface—e.g., CodexInputItem
or the exact name exported by the Codex types), ensure the object shape matches
that interface (type: "text", text: string, text_elements: the correct array
element type), add any necessary imports, then run the desktop validations: npm
--prefix apps/desktop run typecheck, npm --prefix apps/desktop run build, and
npm --prefix apps/desktop run lint to confirm the fix.

In `@apps/desktop/src/main/services/ios/iosSimulatorService.test.ts`:
- Around line 503-513: Add two unit tests for iosurfaceInputPointPayload to
cover validation branches: one that calls iosurfaceInputPointPayload with
non-finite coordinates (e.g., x: Infinity or y: NaN) and asserts the function
fails the validation (expect a thrown error or a falsy/invalid result per the
function's contract); and one that calls iosurfaceInputPointPayload with invalid
screen metrics (e.g., width/height <= 0 or NaN) and similarly asserts the
validation branch is taken (throw or falsy result). Ensure both tests reference
iosurfaceInputPointPayload so the non-happy-path branches are exercised.

In `@apps/ios/ADE/Views/Work/WorkChatSessionView.swift`:
- Around line 155-186: transcriptIndicatesActiveTurn is expensive because it's a
computed property that rescans transcript on every render; convert it to a
cached stored Bool that's updated during the existing timeline rebuild path
(instead of being recomputed in the view). Add a stored property (e.g. a private
var or `@State/`@Binding as appropriate) like transcriptIndicatesActiveTurnCached,
compute its value once inside the timeline/session rebuild function (the routine
that currently derives timeline state) by reusing the same logic that inspects
transcript and envelope.event/.done, and then have the WorkChatSessionView read
that cached property rather than the computed transcriptIndicatesActiveTurn;
ensure the cached value is updated whenever transcript changes or the rebuild
runs.

In `@apps/ios/ADE/Views/Work/WorkModels.swift`:
- Around line 40-45: Replace the free-form deliveryState String? on
WorkLocalEchoMessage with a strongly-typed enum (e.g., DeliveryState) and update
any related model WorkChatMessage to use the same enum so the compiler enforces
valid states; define DeliveryState as a Codable/Equatable RawRepresentable
(String) enum listing the allowed cases (pending, sent, failed, etc.), change
WorkLocalEchoMessage.deliveryState to DeliveryState? and update all
usages/comparisons, initializers and JSON (de)coding paths to convert to/from
the enum instead of raw strings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bda03071-74fe-4822-b368-fc419656e684

📥 Commits

Reviewing files that changed from the base of the PR and between 87a6163 and c26a37a.

⛔ Files ignored due to path filters (5)
  • docs/features/chat/agent-routing.md is excluded by !docs/**
  • docs/features/chat/transcript-and-turns.md is excluded by !docs/**
  • docs/features/ios-simulator/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/remote-commands.md is excluded by !docs/**
📒 Files selected for processing (27)
  • apps/desktop/native/ios-sim-helpers/README.md
  • apps/desktop/native/ios-sim-helpers/sim-input.m
  • apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts
  • apps/desktop/src/main/services/ai/tools/systemPrompt.ts
  • apps/desktop/src/main/services/chat/agentChatService.test.ts
  • apps/desktop/src/main/services/chat/agentChatService.ts
  • apps/desktop/src/main/services/ios/iosSimulatorService.test.ts
  • apps/desktop/src/main/services/ios/iosSimulatorService.ts
  • apps/desktop/src/main/services/sync/syncHostService.test.ts
  • apps/desktop/src/main/services/sync/syncHostService.ts
  • apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts
  • apps/desktop/src/main/services/sync/syncRemoteCommandService.ts
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/Lanes/LaneComponents.swift
  • apps/ios/ADE/Views/Lanes/LaneDetailGitSection.swift
  • apps/ios/ADE/Views/Lanes/LaneDetailRebaseBanner.swift
  • apps/ios/ADE/Views/Lanes/LaneDetailScreen.swift
  • apps/ios/ADE/Views/Lanes/LaneListViewParts.swift
  • apps/ios/ADE/Views/LanesTabView.swift
  • apps/ios/ADE/Views/Work/WorkChatSessionView.swift
  • apps/ios/ADE/Views/Work/WorkModels.swift
  • apps/ios/ADE/Views/Work/WorkNavigationAndTranscriptHelpers.swift
  • apps/ios/ADE/Views/Work/WorkPreviews.swift
  • apps/ios/ADE/Views/Work/WorkSessionDestinationView+Actions.swift
  • apps/ios/ADE/Views/Work/WorkSessionDestinationView.swift
  • apps/ios/ADE/Views/Work/WorkTimelineHelpers.swift
  • apps/ios/ADETests/ADETests.swift
💤 Files with no reviewable changes (3)
  • apps/ios/ADE/Views/LanesTabView.swift
  • apps/ios/ADE/Views/Lanes/LaneDetailScreen.swift
  • apps/ios/ADE/Views/Lanes/LaneListViewParts.swift

Comment thread apps/desktop/src/main/services/ai/tools/systemPrompt.ts
Comment thread apps/desktop/src/main/services/chat/agentChatService.test.ts
Comment thread apps/desktop/src/main/services/chat/agentChatService.ts Outdated
Comment thread apps/desktop/src/main/services/chat/agentChatService.ts
Comment thread apps/desktop/src/main/services/sync/syncHostService.test.ts Outdated
Comment thread apps/ios/ADE/Views/Lanes/LaneComponents.swift
Comment thread apps/ios/ADE/Views/Lanes/LaneComponents.swift Outdated
Comment thread apps/ios/ADE/Views/Lanes/LaneDetailRebaseBanner.swift Outdated
Comment thread apps/ios/ADE/Views/Work/WorkSessionDestinationView+Actions.swift
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 3, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

Copilot AI commented May 3, 2026

@copilot review but do not make fixes

Updated review, incorporating Greptile's findings and a correction to my previous note — no changes made.


Correction from previous review

"transcriptIndicatesActiveTurn is an O(n) computed property called every render"

This was wrong. In the actual implementation, workTranscriptIndicatesActiveTurn is a standalone function called once inside buildWorkChatTimelineSnapshot (WorkTimelineHelpers.swift:20). The result is stored as a plain Bool on WorkChatTimelineSnapshot. The view just reads timelineSnapshot.transcriptIndicatesActiveTurn — a stored value lookup. No per-render scan. This is correct.


Greptile P2 finding: dead exported normalizeIosSimulatorPointForIndigo

Confirmed. normalizeIosSimulatorPointForIndigo (iosSimulatorService.ts:329) has no production call sites — both tap and drag were migrated to iosurfaceInputPointPayload in this PR. It is only referenced in test imports and test assertions. The function produces a different shape ({ x, y } normalized to [0,1]) than the new iosurfaceInputPointPayload ({ x, y, width, height } in point-space), so leaving both exported creates ambiguity about which API new callers should use for Indigo input. Either unexport it and move it to test-local scope, or add a deprecation comment pointing to iosurfaceInputPointPayload.


Remaining findings from previous review (still valid)

All of the following from my prior review stand:

  • sim-input.m: selectedXcodeMajor() is a blocking xcodebuild -version subprocess on the HID init path. No timeout or env-var override.
  • sim-input.m: sendModernTouch normalizes coordinates to [0,1] via clampUnit(x/width) then also passes width and height to the 9-arg API — the coordinate-space contract with the private Indigo variant is undocumented.
  • agentChatService.ts: ADE context input (buildCodexAdeContextInput) is skipped for config-toml Codex sessions; the old developer_instructions path was unconditional.
  • agentChatService.ts: Optimistic turn start emits status: started before turn/start returns; no compensating failure event is emitted if the network call throws — renderer can get stuck in started state.
  • agentChatService.ts: plan_approval entries survive turn/completed; hasExistingApproval matches on description equality, so a new plan with identical text silently suppresses the approval UI.
  • agentChatService.ts: emitCodexPlanTextApproval returns true for the "not in plan mode" no-op — inconsistent semantics vs. the "approval emitted" case.
  • SyncService.swift: startClientHeartbeatTask allocates ISO8601DateFormatter() on every loop iteration; self.syncDateFormatter already exists.
  • SyncService.swift: Failed applyChanges is acked as success to the server with no visible retry path — a persistent apply error silently drops the changeset permanently.
  • SyncService.swift: Envelope now omits requestId key entirely when nil (was "requestId": null); confirm server accepts key absence for fire-and-forget messages.
  • LaneDetailRebaseBanner.swift: onDismiss is still in the struct signature but no button body triggers it — no user-facing path to dismiss the rebase banner without navigating to the Rebase/Merge tab.

Copilot finished work on behalf of arul28 May 3, 2026 05:05
@arul28 arul28 merged commit 214dd23 into main May 3, 2026
25 checks passed
@arul28 arul28 deleted the ade/ios-sim-refresh-12034e5e branch May 3, 2026 14:57
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.

2 participants