Skip to content

Make the handoff say why it's blocked, and imports say what they are - #910

Merged
arul28 merged 6 commits into
mainfrom
ade/ok-couple-things-wanna-discuss-87c99204
Jul 27, 2026
Merged

Make the handoff say why it's blocked, and imports say what they are#910
arul28 merged 6 commits into
mainfrom
ade/ok-couple-things-wanna-discuss-87c99204

Conversation

@arul28

@arul28 arul28 commented Jul 27, 2026

Copy link
Copy Markdown
Owner

ADE   Open in ADE  ·  ade/ok-couple-things-wanna-discuss-87c99204 branch  ·  PR #910

Summary by CodeRabbit

  • New Features
    • Added sync/remote support for chat.fastForwardCrossMachineHandoffLane, enabling safe fast-forwarding of eligible destination lanes.
    • Cross-machine handoff modal now supports destination lane fast-forward prompts, unified native permission controls, and send progress tracking.
    • External session browsing/import now supports full-text search, started/latest anchors, and recent-message thread previews (including iOS rendering updates).
  • Bug Fixes
    • Improved handling of dirty, rebasing, diverged, and “behind origin” cases with clearer blockers and safer refusal behavior.
    • Strengthened transcript discovery/cleanup, message parsing/timestamps, and anchor/preview fallback logic.

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Jul 27, 2026 5:06pm

@greptile-apps greptile-apps 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.

Greptile has paused reviews on this repository — it used its 100 free open-source review credits for this billing period. Reviews resume automatically on August 7. To continue before then, an organization admin can keep reviews running past the free credits — those bill as normal usage.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR adds cross-machine destination lane fast-forwarding, structured handoff blockers and controls, richer external-session transcript discovery and rendering, iOS import support, and a shared permission picker.

Cross-machine handoff

Layer / File(s) Summary
Handoff contracts and preflight
apps/desktop/src/shared/..., apps/desktop/src/main/services/chat/...
Adds cross-machine fork capability checks, lane fast-forward metadata, ancestry validation, and guarded git merge --ff-only execution.
Remote command bridge
apps/ade-cli/src/services/sync/..., apps/desktop/src/main/services/adeActions/registry.ts
Registers, validates, advertises, and tests chat.fastForwardCrossMachineHandoffLane.
Handoff modal and blocker UI
apps/desktop/src/renderer/components/chat/..., apps/desktop/src/renderer/components/shared/BlockedAction.tsx
Adds destination controls, repository readiness, branch fixes, fast-forward actions, structured blockers, and send progress indicators.

External session transcripts

Layer / File(s) Summary
Transcript discovery and propagation
apps/desktop/src/main/services/externalSessions/*, apps/desktop/src/shared/types/externalSessions.ts
Discovers, cleans, clips, classifies, and propagates recent Claude and Codex messages through session summaries.
Desktop and CLI presentation
apps/desktop/src/renderer/components/terminals/importSessions/*, apps/ade-cli/src/tuiClient/*
Searches message content and renders conversation anchors and recent transcript messages.
iOS import experience
apps/ios/ADE/Models/RemoteModels.swift, apps/ios/ADE/Services/SyncService.swift, apps/ios/ADE/Views/Work/*
Decodes transcript messages, forwards optional session identifiers, and renders conversation previews and resume details.

Shared permission controls

Layer / File(s) Summary
Reusable permission picker
apps/desktop/src/renderer/components/shared/PermissionModePicker.tsx
Introduces shared permission option types, glyphs, styling, dropdown behavior, portal rendering, and keyboard dismissal.
Consumer integration
apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx, apps/desktop/src/renderer/components/shared/SessionLaunchModelControls.tsx
Replaces duplicated permission-picker logic with the shared component and styling constant.

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

Possibly related PRs

  • arul28/ADE#34: Extends the sync remote-command registration and parsing infrastructure used by the new lane fast-forward command.
  • arul28/ADE#683: Extends the same chat.* sync action surface and CLI command wiring.
  • arul28/ADE#712: Provides the external-session browser and pane wiring extended by the TUI changes.

Suggested labels: desktop, ios, docs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main changes: clearer blocked-handoff messaging and more descriptive import/session labels.
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.
✨ 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/ok-couple-things-wanna-discuss-87c99204

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 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: 6

🧹 Nitpick comments (3)
apps/desktop/src/main/services/externalSessions/discoveryUtils.test.ts (1)

105-118: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Misplaced doc block. The markup-density comment (Lines 105-110) explains the import-path test at Line 130, but now sits on the classifier-consistency test. Move it so each block documents the test below it.

♻️ Suggested move
-  /**
-   * The markup-density gate exists to keep junk out of row *previews*. It must
-   * not run on `cleanExternalSessionUserText`, which also builds the imported
-   * chat transcript — rejecting there silently deletes real messages from
-   * someone's history.
-   */
   /**
    * Regression: the count/preview reader and the messages reader each derived

and re-add it directly above it("keeps markup-heavy and very short user turns intact for the import path", ...).

🤖 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 `@apps/desktop/src/main/services/externalSessions/discoveryUtils.test.ts`
around lines 105 - 118, Move the markup-density JSDoc block currently above the
classifier-consistency test to directly above the import-path test named “keeps
markup-heavy and very short user turns intact for the import path.” Keep the
classifier-consistency regression comment immediately above “agrees across
count, preview, and messages about what a user turn is.”
apps/ade-cli/src/services/sync/syncRemoteCommandService.test.ts (1)

1925-1950: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a negative case for the new parser.

Only the happy path is covered. A missing laneId/expectedHead should surface the parser's specific error rather than reaching the service.

💚 Suggested additional assertions
+    await expect(service.execute(makePayload("chat.fastForwardCrossMachineHandoffLane", {
+      expectedHead: capsule.source.headSha,
+    }))).rejects.toThrow("chat.fastForwardCrossMachineHandoffLane requires laneId.");
+    await expect(service.execute(makePayload("chat.fastForwardCrossMachineHandoffLane", {
+      laneId: "lane-2",
+    }))).rejects.toThrow("chat.fastForwardCrossMachineHandoffLane requires expectedHead.");
🤖 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 `@apps/ade-cli/src/services/sync/syncRemoteCommandService.test.ts` around lines
1925 - 1950, Extend the tests around the cross-machine handoff command parser to
cover payloads missing laneId or expectedHead. Assert that each invalid payload
returns the parser’s specific validation error and that execute does not reach
the service handler, while preserving the existing happy-path assertions.
apps/desktop/src/main/services/chat/agentChatService.test.ts (1)

1604-1613: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for merge --ff-only failures.

installCrossMachineDestinationLaneGitFixture exposes branchRef and mergeExitCode, but the destination fast-forward tests never set them. Since mergeExitCode controls the merge --ff-only failure path, add a case with mergeExitCode: 1 or remove the unused fixture options.

🤖 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 `@apps/desktop/src/main/services/chat/agentChatService.test.ts` around lines
1604 - 1613, Add coverage for the merge --ff-only failure path by adding a
destination fast-forward test that configures
installCrossMachineDestinationLaneGitFixture with a valid branchRef and
mergeExitCode: 1, then asserts the expected failure behavior. If that scenario
is not needed, remove the unused branchRef and mergeExitCode fixture options
instead.
🤖 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 `@apps/desktop/src/renderer/components/chat/CrossMachineHandoffModal.tsx`:
- Around line 791-825: Remove the redundant standalone blocker UI near the
existing turn and publish controls, including the amber “Current turn is not
ready” block and standalone “Publish branch” button. Keep `continueBlockers` as
the single source for `turn-active`, `awaiting-input`, and `needs-push` actions,
rendered through `BlockedReasons` with its existing busy/disabled behavior.

In
`@apps/desktop/src/renderer/components/terminals/importSessions/affordances.test.ts`:
- Around line 288-319: Update sessionAnchors so it suppresses latest whenever
its text equals started, including titled single-message sessions. Add a test in
the sessionAnchors suite covering a titled session with one message and assert
latest is null while started remains the message text; preserve existing
behavior for distinct anchors and message-less sessions.

In
`@apps/desktop/src/renderer/components/terminals/importSessions/ImportSessionBrowser.tsx`:
- Line 647: Add keyboard access to the bounded transcript container in
ImportSessionBrowser by giving the max-height overflow wrapper a non-negative
tabindex and an accessible name. Preserve its existing scrolling behavior while
ensuring keyboard-only users can focus and scroll the region.

In
`@apps/desktop/src/renderer/components/terminals/importSessions/sessionPresentation.ts`:
- Around line 48-63: Update sessionAnchors so latest is also excluded when its
text matches the started value, reusing the existing started and heading
comparisons. Preserve the current null handling and return latest only when it
differs from both started and heading, matching the externalSessionAnchors
behavior.

In `@apps/desktop/src/shared/crossMachineHandoff.ts`:
- Around line 144-152: Update requirePositiveInteger to validate numeric values
with Number.isSafeInteger and require them to be at least 1, rejecting unsafe
behindBy distances at the decoder boundary while preserving the existing error
and return behavior.

In `@apps/ios/ADE/Views/Work/WorkImportSessionScreen.swift`:
- Around line 313-339: Scope chat-import configuration to session.provider
instead of screen-wide importProvider/importModelId state. Update the model
picker’s available provider/models or normalize the selected chat fields when
importSession handles a chat, ensuring model, permissionMode, reasoningEffort,
and fastMode all match the session provider while preserving existing behavior
for non-chat imports.

---

Nitpick comments:
In `@apps/ade-cli/src/services/sync/syncRemoteCommandService.test.ts`:
- Around line 1925-1950: Extend the tests around the cross-machine handoff
command parser to cover payloads missing laneId or expectedHead. Assert that
each invalid payload returns the parser’s specific validation error and that
execute does not reach the service handler, while preserving the existing
happy-path assertions.

In `@apps/desktop/src/main/services/chat/agentChatService.test.ts`:
- Around line 1604-1613: Add coverage for the merge --ff-only failure path by
adding a destination fast-forward test that configures
installCrossMachineDestinationLaneGitFixture with a valid branchRef and
mergeExitCode: 1, then asserts the expected failure behavior. If that scenario
is not needed, remove the unused branchRef and mergeExitCode fixture options
instead.

In `@apps/desktop/src/main/services/externalSessions/discoveryUtils.test.ts`:
- Around line 105-118: Move the markup-density JSDoc block currently above the
classifier-consistency test to directly above the import-path test named “keeps
markup-heavy and very short user turns intact for the import path.” Keep the
classifier-consistency regression comment immediately above “agrees across
count, preview, and messages about what a user turn is.”
🪄 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 Plus

Run ID: bf00616c-9089-4200-bdf6-d839c513b6c6

📥 Commits

Reviewing files that changed from the base of the PR and between 99956f9 and f49cb0c.

⛔ Files ignored due to path filters (7)
  • docs/features/chat/README.md is excluded by !docs/**
  • docs/features/chat/composer-and-ui.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/cross-machine-session-handoff.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/ios-companion.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/remote-commands.md is excluded by !docs/**
  • docs/features/terminals-and-sessions/external-session-import.md is excluded by !docs/**
📒 Files selected for processing (38)
  • apps/ade-cli/src/services/sync/syncRemoteCommandService.test.ts
  • apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
  • apps/ade-cli/src/tuiClient/__tests__/externalSessionBrowser.test.ts
  • apps/ade-cli/src/tuiClient/components/RightPane.tsx
  • apps/ade-cli/src/tuiClient/externalSessionBrowser.ts
  • apps/desktop/src/main/services/adeActions/registry.ts
  • apps/desktop/src/main/services/chat/agentChatService.test.ts
  • apps/desktop/src/main/services/chat/agentChatService.ts
  • apps/desktop/src/main/services/externalSessions/discoverClaude.ts
  • apps/desktop/src/main/services/externalSessions/discoverCodex.ts
  • apps/desktop/src/main/services/externalSessions/discoverCursor.ts
  • apps/desktop/src/main/services/externalSessions/discoverProviders.test.ts
  • apps/desktop/src/main/services/externalSessions/discoveryUtils.test.ts
  • apps/desktop/src/main/services/externalSessions/discoveryUtils.ts
  • apps/desktop/src/main/services/externalSessions/externalSessionsService.test.ts
  • apps/desktop/src/main/services/externalSessions/externalSessionsService.ts
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/chat/CrossMachineHandoffModal.test.tsx
  • apps/desktop/src/renderer/components/chat/CrossMachineHandoffModal.tsx
  • apps/desktop/src/renderer/components/chat/crossMachineHandoffPresentation.tsx
  • apps/desktop/src/renderer/components/shared/BlockedAction.tsx
  • apps/desktop/src/renderer/components/shared/PermissionModePicker.tsx
  • apps/desktop/src/renderer/components/shared/SessionLaunchModelControls.tsx
  • apps/desktop/src/renderer/components/terminals/importSessions/ImportSessionBrowser.tsx
  • apps/desktop/src/renderer/components/terminals/importSessions/affordances.test.ts
  • apps/desktop/src/renderer/components/terminals/importSessions/contract.ts
  • apps/desktop/src/renderer/components/terminals/importSessions/sessionPresentation.ts
  • apps/desktop/src/shared/crossMachineHandoff.ts
  • apps/desktop/src/shared/types/chat.test.ts
  • apps/desktop/src/shared/types/chat.ts
  • apps/desktop/src/shared/types/externalSessions.ts
  • apps/desktop/src/shared/types/sync.ts
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/Work/WorkExternalSessionAffordances.swift
  • apps/ios/ADE/Views/Work/WorkImportSessionScreen.swift
  • apps/ios/ADETests/ADETests.swift

Comment on lines +288 to +319
describe("sessionAnchors", () => {
it("returns the opening ask and the latest message", () => {
const anchors = sessionAnchors(session({
title: "Mobile chat truncation",
preview: "text is cut mid-word",
messages: [
{ role: "user", text: "text is cut mid-word", at: 1 },
{ role: "assistant", text: "Found it \u2014 byte-offset split.", at: 2 },
{ role: "user", text: "now shrink the logo", at: 3 },
],
}));
expect(anchors.started).toBe("text is cut mid-word");
expect(anchors.latest?.text).toBe("now shrink the logo");
});

it("suppresses an anchor that would repeat the heading", () => {
// Untitled single-message thread: heading === preview === that message, so
// printing it again below reads as a rendering bug.
const anchors = sessionAnchors(session({
title: null,
preview: "only one thing was ever said",
messages: [{ role: "user", text: "only one thing was ever said", at: 1 }],
}));
expect(anchors.started).toBeNull();
expect(anchors.latest).toBeNull();
});

it("has no latest anchor on an older host that sends no messages", () => {
const anchors = sessionAnchors(session({ title: "Something", preview: "a preview" }));
expect(anchors.started).toBe("a preview");
expect(anchors.latest).toBeNull();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Suppress latest when it duplicates started.

A titled single-message session currently renders the opening prompt twice: once as started and again as latest. Add this case and make sessionAnchors also reject a latest value equal to started, matching the CLI helper.

Proposed fix
-    latest: latest && asHeadingText(latest.text) !== heading ? latest : null,
+    latest: latest
+      && asHeadingText(latest.text) !== heading
+      && asHeadingText(latest.text) !== started
+      ? latest
+      : null,
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 289-297: Enforce overriding default config
Context: session({
title: "Mobile chat truncation",
preview: "text is cut mid-word",
messages: [
{ role: "user", text: "text is cut mid-word", at: 1 },
{ role: "assistant", text: "Found it \u2014 byte-offset split.", at: 2 },
{ role: "user", text: "now shrink the logo", at: 3 },
],
})
Note: [CWE-1188] Insecure Default Initialization of Resource (default session cookie name not overridden).

(default-session-config-typescript)


[warning] 305-309: Enforce overriding default config
Context: session({
title: null,
preview: "only one thing was ever said",
messages: [{ role: "user", text: "only one thing was ever said", at: 1 }],
})
Note: [CWE-1188] Insecure Default Initialization of Resource (default session cookie name not overridden).

(default-session-config-typescript)


[warning] 315-315: Enforce overriding default config
Context: session({ title: "Something", preview: "a preview" })
Note: [CWE-1188] Insecure Default Initialization of Resource (default session cookie name not overridden).

(default-session-config-typescript)

🤖 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
`@apps/desktop/src/renderer/components/terminals/importSessions/affordances.test.ts`
around lines 288 - 319, Update sessionAnchors so it suppresses latest whenever
its text equals started, including titled single-message sessions. Add a test in
the sessionAnchors suite covering a titled session with one message and assert
latest is null while started remains the message text; preserve existing
behavior for distinct anchors and message-less sessions.

Comment thread apps/desktop/src/shared/crossMachineHandoff.ts
Comment thread apps/ios/ADE/Views/Work/WorkImportSessionScreen.swift Outdated
@arul28

arul28 commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21ccd9f625

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +341 to +344
model: configuresChat ? importModelId : nil,
permissionMode: action.target == "chat" ? wire.permissionMode : nil,
reasoningEffort: configuresChat && !normalizedReasoning.isEmpty ? normalizedReasoning : nil,
fastMode: configuresChat && supportsFastMode ? importFastMode : 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.

P1 Badge Honor selected settings when importing an ADE chat

For every chat import, these newly sent model, permission, reasoning, and fast-mode values are discarded by externalSessionsService.ts: its target === "chat" branch calls importExternalChatSession with only provider/session/lane/cwd/fork/title, and the importer creates the chat with DEFAULT_CLAUDE_MODEL or DEFAULT_CODEX_MODEL. Consequently, the new iOS controls never affect the resulting chat—including a selected read-only access mode—so these fields need to be carried through ExternalChatImporter into chat creation.

AGENTS.md reference: AGENTS.md:L32-L35

Useful? React with 👍 / 👎.

Comment on lines +793 to +799
...(sourceCheck.needsPush
? [{
id: "needs-push",
title: `${sourceCheck.branch ?? "This branch"} hasn't been published`,
detail: "The other machine fetches your work from origin, so the branch has to exist there.",
fix: { label: "Publish branch", onFix: () => void publishBranch(), busy: Boolean(busyLabel) },
}]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Hide the publish action for diverged branches

When the source has diverged, sync.ahead is positive, so needsPush is also true and this block now shows “hasn't been published” with a Publish branch fix beside the divergence blocker. The upstream branch already exists, and a normal push is non-fast-forward and cannot resolve the divergence; the previous UI deliberately hid this action whenever hasSourceBlock was true. Only offer this blocker for a genuinely unpublished/non-diverged branch, or distinguish publishing from reconciliation.

AGENTS.md reference: AGENTS.md:L59-L60

Useful? React with 👍 / 👎.

@arul28

arul28 commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e529410f4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

role="listbox"
aria-label={ariaLabel}
data-permission-mode-picker-dropdown
className="fixed z-[100] overflow-hidden rounded-xl border border-white/[0.08] bg-[#13111A]/95 shadow-[0_18px_48px_rgba(0,0,0,0.55)] backdrop-blur-md"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Raise the permission menu above the handoff modal

When this shared picker is opened from CrossMachineHandoffModal, the portal is rendered at z-[100] while the modal overlay is at z-[190]. The option list therefore appears behind the modal/backdrop and cannot be clicked, making the newly added destination permission control unusable during cross-machine handoff. Use a layer above the modal or let the caller provide the portal layer.

Useful? React with 👍 / 👎.

arul28 and others added 3 commits July 27, 2026 12:12
Handing a chat to another machine dead-ended on a disabled Continue button
with no stated reason: a branch that was pushed but behind origin pushed a
blocker into a list that only ever fed `disabled`, under a green "main is
pushed" row that read only the push direction. Even past that, the
destination required its own lane to sit at the identical commit, which two
machines' `main` never do.

- Add a BlockedAction primitive. The button takes reasons, not a boolean, so
  a surface cannot disable its primary action without surfacing why.
- Report behind/diverged on the branch row; offer Update branch for behind,
  and keep diverged a hard block rather than choosing a reconcile strategy.
- Report laneFastForward from destination preflight and offer a repair that
  is re-validated there and only ever runs `git merge --ff-only`.
- Render the destination's model, reasoning, fast mode and permission pills.
  The capsule always carried these and the destination always applied them;
  only the UI was missing, so handoffs shipped whatever the local drawer held.
- Gate fork on cross-machine support. Droid's fork tab was live and threw.

Imports rendered a raw <task-notification> blob under an "ADE - 9m ago"
heading. Wrapper rejection is no longer a closed allow-list, previews may
only come from prefix records so a tail receipt can't pose as the opening
prompt, clipping stops at a word boundary, and rows carry started/latest
anchors over a scrollable thread.

Extract PermissionModePicker from AgentChatComposer into shared/ (removing a
three-way duplicated trigger class), and mirror it all on iOS with lossy
message decoding so one malformed element cannot drop a whole row.

Co-Authored-By: Claude <noreply@anthropic.com>
- Delete the standalone turn-state panel and Publish button. `BlockedReasons`
  already renders both from `continueBlockers`, so the same blocker and the
  same fix appeared twice with different disabled behavior on each copy.
- Suppress the `latest` anchor when it equals `started`, not just the heading.
  A titled single-message thread cleared the heading check and still printed
  the sentence twice; the TUI sibling already guarded this.
- Make the bounded transcript focusable so keyboard-only users can scroll it.
- Validate `behindBy` with isSafeInteger; isInteger admits 1e30.
- Scope iOS chat-import model/reasoning/fast mode to the session's own
  provider. They came from screen-level composer preferences while the request
  sent `provider: session.provider`, so importing a Claude session while the
  saved model was Codex shipped a mismatch the host cannot honor.

Co-Authored-By: Claude <noreply@anthropic.com>
…ntrols

- Only surface the Publish blocker when a plain push can resolve it. A
  diverged branch is also ahead, so it reported needsPush and offered Publish
  right beside the divergence blocker — but its upstream exists and the push
  is rejected as non-fast-forward. The pre-refactor UI hid it via
  `!hasSourceBlock`; the blocker list lost that guard.

- Remove the model, access mode, reasoning effort, and fast mode controls from
  the iOS import screen. The host discards all four for chat targets:
  externalSessionsService calls importExternalChatSession with only
  provider/session/lane/cwd/fork/title, and AgentChatImportExternalSessionArgs
  has no such fields. The controls changed nothing about the resulting chat,
  which is the exact bug class this branch exists to fix. Carrying them
  through chat creation is a separate change with its own provider-matching
  rules; the pre-existing model/permissionMode args on
  ExternalSessionImportArgs remain honoured for CLI targets.

Co-Authored-By: Claude <noreply@anthropic.com>
@arul28
arul28 force-pushed the ade/ok-couple-things-wanna-discuss-87c99204 branch from e529410 to 2c172fa Compare July 27, 2026 16:20

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

🧹 Nitpick comments (2)
apps/desktop/src/main/services/chat/agentChatService.ts (1)

29466-29590: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider verifying the lane worktree's checked-out branch before the ff-only merge.

The function re-validates fetch/reachability/dirty-state/ancestry from scratch (good — it doesn't trust the preflight snapshot), but never confirms that refreshedLane.worktreePath's current branch is actually branchRef before running git merge --ff-only expectedHead. If that invariant is ever violated (e.g. a user manually checked out a different ref in the lane worktree), this would silently fast-forward whatever branch is currently checked out instead of failing clearly.

💡 Proposed defensive check
     const status = await runGit(["status", "--porcelain=v1"], {
       cwd: refreshedLane.worktreePath,
       timeoutMs: 15_000,
     });
     if (status.exitCode !== 0) { ... }
     if (status.stdout.trim()) { ... }
+
+    const currentBranch = await runGit(["symbolic-ref", "--short", "HEAD"], {
+      cwd: refreshedLane.worktreePath,
+      timeoutMs: 8_000,
+    });
+    if (currentBranch.exitCode !== 0 || currentBranch.stdout.trim() !== branchRef) {
+      throw new Error(
+        `Cross-machine handoff lane '${refreshedLane.name}' is not currently on branch '${branchRef}'.`,
+      );
+    }
🤖 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 `@apps/desktop/src/main/services/chat/agentChatService.ts` around lines 29466 -
29590, Update fastForwardCrossMachineHandoffLane to verify the refreshed lane
worktree’s checked-out branch matches the validated branchRef before the merge.
Use the refreshed worktree and an appropriate Git branch query, fail clearly
when it differs or cannot be determined, and perform this check before git merge
--ff-only while preserving the existing validation flow.
apps/desktop/src/main/services/externalSessions/externalSessionsService.test.ts (1)

146-156: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Source-text regex assertion is brittle.

Counting messages: session.messages occurrences in the file passes for dead code, breaks on a rename or reformat, and silently under-asserts if a third summary path appears. Since findExternalSummary is reachable through importExternalSession, a behavioral assertion on the returned summary (or extracting the mapper and testing it directly) would pin the same contract without coupling to source text.

🤖 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
`@apps/desktop/src/main/services/externalSessions/externalSessionsService.test.ts`
around lines 146 - 156, The test for optional preview fields in
externalSessionsService.test.ts is coupled to source-text occurrence counts
instead of runtime behavior. Replace the fs.readFileSync and regex assertions
with a behavioral test that exercises importExternalSession and
findExternalSummary, then verifies the returned summaries preserve both messages
and preview fields across the supported summary paths.
🤖 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 `@apps/desktop/src/main/services/chat/agentChatService.ts`:
- Around line 29470-29590: Update fastForwardCrossMachineHandoffLane to require
and validate the established in-flight handoff/session context before performing
any fetch or merge, ensuring the requested laneId matches the previously
prepared handoff target. Reject generic or unrelated lane IDs, and preserve the
existing merge --ff-only validation only after this target check passes.

In `@apps/ios/ADE/Views/Work/WorkImportSessionScreen.swift`:
- Around line 898-901: Update latestAnchorMessage to normalize the latest
message text and compare it against normalized rowHeading plus the displayed
“started” anchor text. Return nil when either comparison matches, preserving the
existing latest-message behavior for non-colliding text.

---

Nitpick comments:
In `@apps/desktop/src/main/services/chat/agentChatService.ts`:
- Around line 29466-29590: Update fastForwardCrossMachineHandoffLane to verify
the refreshed lane worktree’s checked-out branch matches the validated branchRef
before the merge. Use the refreshed worktree and an appropriate Git branch
query, fail clearly when it differs or cannot be determined, and perform this
check before git merge --ff-only while preserving the existing validation flow.

In
`@apps/desktop/src/main/services/externalSessions/externalSessionsService.test.ts`:
- Around line 146-156: The test for optional preview fields in
externalSessionsService.test.ts is coupled to source-text occurrence counts
instead of runtime behavior. Replace the fs.readFileSync and regex assertions
with a behavioral test that exercises importExternalSession and
findExternalSummary, then verifies the returned summaries preserve both messages
and preview fields across the supported summary paths.
🪄 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 Plus

Run ID: 4b9bc045-ebf4-4d6b-803a-b1bee2d8f685

📥 Commits

Reviewing files that changed from the base of the PR and between f49cb0c and 2c172fa.

⛔ Files ignored due to path filters (2)
  • docs/features/chat/README.md is excluded by !docs/**
  • docs/features/chat/composer-and-ui.md is excluded by !docs/**
📒 Files selected for processing (38)
  • apps/ade-cli/src/services/sync/syncRemoteCommandService.test.ts
  • apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
  • apps/ade-cli/src/tuiClient/__tests__/externalSessionBrowser.test.ts
  • apps/ade-cli/src/tuiClient/components/RightPane.tsx
  • apps/ade-cli/src/tuiClient/externalSessionBrowser.ts
  • apps/desktop/src/main/services/adeActions/registry.ts
  • apps/desktop/src/main/services/chat/agentChatService.test.ts
  • apps/desktop/src/main/services/chat/agentChatService.ts
  • apps/desktop/src/main/services/externalSessions/discoverClaude.ts
  • apps/desktop/src/main/services/externalSessions/discoverCodex.ts
  • apps/desktop/src/main/services/externalSessions/discoverCursor.ts
  • apps/desktop/src/main/services/externalSessions/discoverProviders.test.ts
  • apps/desktop/src/main/services/externalSessions/discoveryUtils.test.ts
  • apps/desktop/src/main/services/externalSessions/discoveryUtils.ts
  • apps/desktop/src/main/services/externalSessions/externalSessionsService.test.ts
  • apps/desktop/src/main/services/externalSessions/externalSessionsService.ts
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/chat/CrossMachineHandoffModal.test.tsx
  • apps/desktop/src/renderer/components/chat/CrossMachineHandoffModal.tsx
  • apps/desktop/src/renderer/components/chat/crossMachineHandoffPresentation.tsx
  • apps/desktop/src/renderer/components/shared/BlockedAction.tsx
  • apps/desktop/src/renderer/components/shared/PermissionModePicker.tsx
  • apps/desktop/src/renderer/components/shared/SessionLaunchModelControls.tsx
  • apps/desktop/src/renderer/components/terminals/importSessions/ImportSessionBrowser.tsx
  • apps/desktop/src/renderer/components/terminals/importSessions/affordances.test.ts
  • apps/desktop/src/renderer/components/terminals/importSessions/contract.ts
  • apps/desktop/src/renderer/components/terminals/importSessions/sessionPresentation.ts
  • apps/desktop/src/shared/crossMachineHandoff.ts
  • apps/desktop/src/shared/types/chat.test.ts
  • apps/desktop/src/shared/types/chat.ts
  • apps/desktop/src/shared/types/externalSessions.ts
  • apps/desktop/src/shared/types/sync.ts
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/Work/WorkExternalSessionAffordances.swift
  • apps/ios/ADE/Views/Work/WorkImportSessionScreen.swift
  • apps/ios/ADETests/ADETests.swift
🚧 Files skipped from review as they are similar to previous changes (30)
  • apps/desktop/src/shared/types/sync.ts
  • apps/desktop/src/renderer/components/terminals/importSessions/contract.ts
  • apps/desktop/src/main/services/adeActions/registry.ts
  • apps/desktop/src/main/services/externalSessions/discoverCursor.ts
  • apps/desktop/src/shared/crossMachineHandoff.ts
  • apps/desktop/src/renderer/components/shared/BlockedAction.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
  • apps/ios/ADE/Services/SyncService.swift
  • apps/desktop/src/shared/types/chat.ts
  • apps/ade-cli/src/tuiClient/externalSessionBrowser.ts
  • apps/ade-cli/src/services/sync/syncRemoteCommandService.test.ts
  • apps/desktop/src/renderer/components/chat/crossMachineHandoffPresentation.tsx
  • apps/desktop/src/main/services/externalSessions/discoverCodex.ts
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/shared/types/chat.test.ts
  • apps/ios/ADETests/ADETests.swift
  • apps/desktop/src/renderer/components/shared/SessionLaunchModelControls.tsx
  • apps/ade-cli/src/tuiClient/components/RightPane.tsx
  • apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
  • apps/desktop/src/main/services/externalSessions/discoverClaude.ts
  • apps/desktop/src/main/services/externalSessions/discoverProviders.test.ts
  • apps/desktop/src/main/services/externalSessions/discoveryUtils.test.ts
  • apps/desktop/src/renderer/components/terminals/importSessions/sessionPresentation.ts
  • apps/desktop/src/renderer/components/shared/PermissionModePicker.tsx
  • apps/desktop/src/shared/types/externalSessions.ts
  • apps/desktop/src/renderer/components/terminals/importSessions/ImportSessionBrowser.tsx
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/ios/ADE/Views/Work/WorkExternalSessionAffordances.swift
  • apps/desktop/src/renderer/components/chat/CrossMachineHandoffModal.test.tsx
  • apps/desktop/src/renderer/components/chat/CrossMachineHandoffModal.tsx

Comment on lines +29470 to +29590
const fastForwardCrossMachineHandoffLane = async (
args: { laneId: string; expectedHead: string },
): Promise<{ ok: true; head: string }> => {
const laneId = typeof args.laneId === "string" ? args.laneId.trim() : "";
if (!laneId) {
throw new Error("Cross-machine handoff lane fast-forward requires a destination lane.");
}
const expectedHead = typeof args.expectedHead === "string" ? args.expectedHead.trim() : "";
if (!/^[0-9a-f]{40,64}$/i.test(expectedHead)) {
throw new Error("Cross-machine handoff lane fast-forward received an invalid source commit.");
}

const lane = await laneService.getSummary(laneId, { includeStatus: true });
if (!lane || lane.archivedAt) {
throw new Error(`Cross-machine handoff lane '${laneId}' is unavailable or archived.`);
}
const branchRef = await requireGitBranchForHandoff(lane.branchRef, lane.worktreePath);
const fetch = await runGit(
["fetch", "origin", `refs/heads/${branchRef}:refs/remotes/origin/${branchRef}`],
{
cwd: lane.worktreePath,
timeoutMs: 60_000,
env: await destinationGitEnv(),
},
);
if (fetch.exitCode !== 0) {
throw new Error(
`Cross-machine handoff lane '${lane.name}' could not fetch '${branchRef}': ${fetch.stderr.trim() || "unknown Git error"}`,
);
}
const fetchedHead = await requireGitOutputForHandoff(
["rev-parse", `refs/remotes/origin/${branchRef}`],
lane.worktreePath,
`Cross-machine handoff lane '${lane.name}' could not resolve origin/${branchRef}.`,
);
if (fetchedHead !== expectedHead) {
throw new Error(
`Cross-machine handoff lane '${lane.name}' cannot fast-forward because origin/${branchRef} no longer points at the expected source commit.`,
);
}
const reachable = await runGit(["cat-file", "-e", `${expectedHead}^{commit}`], {
cwd: lane.worktreePath,
timeoutMs: 8_000,
});
if (reachable.exitCode !== 0) {
throw new Error(
`Cross-machine handoff lane '${lane.name}' cannot reach the expected source commit after fetching origin.`,
);
}

const refreshedLane = await laneService.getSummary(laneId, { includeStatus: true });
if (!refreshedLane || refreshedLane.archivedAt) {
throw new Error(`Cross-machine handoff lane '${laneId}' became unavailable before it could be updated.`);
}
if (refreshedLane.status.rebaseInProgress) {
throw new Error(
`Cross-machine handoff lane '${refreshedLane.name}' cannot fast-forward while a rebase is in progress.`,
);
}
const status = await runGit(["status", "--porcelain=v1"], {
cwd: refreshedLane.worktreePath,
timeoutMs: 15_000,
});
if (status.exitCode !== 0) {
throw new Error(
`Cross-machine handoff lane '${refreshedLane.name}' could not be checked for local changes: ${status.stderr.trim() || "unknown Git error"}`,
);
}
if (status.stdout.trim()) {
throw new Error(
`Cross-machine handoff lane '${refreshedLane.name}' has uncommitted changes and cannot be fast-forwarded.`,
);
}

const laneHead = await requireGitOutputForHandoff(
["rev-parse", "HEAD"],
refreshedLane.worktreePath,
`Cross-machine handoff lane '${refreshedLane.name}' does not have a readable commit.`,
);
if (laneHead === expectedHead) {
throw new Error(
`Cross-machine handoff lane '${refreshedLane.name}' is already at the expected source commit.`,
);
}
const ancestor = await runGit(["merge-base", "--is-ancestor", laneHead, expectedHead], {
cwd: refreshedLane.worktreePath,
timeoutMs: 15_000,
});
if (ancestor.exitCode === 1) {
throw new Error(
`Cross-machine handoff lane '${refreshedLane.name}' has diverged; its current commit is not an ancestor of the expected source commit.`,
);
}
if (ancestor.exitCode !== 0) {
throw new Error(
`Cross-machine handoff lane '${refreshedLane.name}' ancestry could not be verified: ${ancestor.stderr.trim() || "unknown Git error"}`,
);
}

const merge = await runGit(["merge", "--ff-only", expectedHead], {
cwd: refreshedLane.worktreePath,
timeoutMs: 60_000,
});
if (merge.exitCode !== 0) {
throw new Error(
`Cross-machine handoff lane '${refreshedLane.name}' could not be fast-forwarded: ${merge.stderr.trim() || "git merge --ff-only failed"}`,
);
}
const head = await requireGitOutputForHandoff(
["rev-parse", "HEAD"],
refreshedLane.worktreePath,
`Cross-machine handoff lane '${refreshedLane.name}' could not verify the updated commit.`,
);
if (head !== expectedHead) {
throw new Error(
`Cross-machine handoff lane '${refreshedLane.name}' did not reach the expected source commit.`,
);
}
return { ok: true, head };
};

@coderabbitai coderabbitai Bot Jul 27, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how fastForwardCrossMachineHandoffLane is registered/allowlisted
fd -HI 'registry.ts' apps/desktop/src/main/services/adeActions
fd -HI 'syncRemoteCommandService.ts' apps/ade-cli/src/services/sync
rg -n -C5 -i 'fastForward|laneFastForward|fast_forward' apps/desktop/src/main/services/adeActions/registry.ts apps/ade-cli/src/services/sync/syncRemoteCommandService.ts 2>/dev/null

Repository: arul28/ADE

Length of output: 4846


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== remote command service outline relevant sections =="
wc -l apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
sed -n '1,180p' apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
echo
sed -n '4100,4145p' apps/ade-cli/src/services/sync/syncRemoteCommandService.ts

echo
echo "== inspect parser and registration flow =="
rg -n -C6 'register\(|viewerAllowed|remote command|remote-command|Remote.*Command|executeRemote|allowlist|allowed.*command|command.*allowed' apps/ade-cli/src/services/sync/syncRemoteCommandService.ts

echo
echo "== inspect desktop registry type/mapping =="
rg -n -C6 'fastForwardCrossMachineHandoffLane|type .*Register|register|allowlist|viewerAllowed|queueable' apps/desktop/src/main/services/adeActions/registry.ts

Repository: arul28/ADE

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== types for sync remote command policy/action/descriptor =="
rg -n -C4 'export (interface|type) (SyncRemoteCommand|SyncCommand|AgentChatRemoteCommand)|SyncRemoteCommandPolicy|SyncRemoteCommandAction|SyncRemoteCommandDescriptor|channelName|scope :' apps/shared apps/ade-cli/src apps/desktop/src/main/services/adeActions 2>/dev/null | head -n 220

echo
echo "== broad remote action definitions around chat actions type =="
sed -n '260,380p' apps/ade-cli/src/services/sync/syncRemoteCommandService.ts

echo
echo "== fast forward implementation excerpt =="
sed -n '29000,29600p' apps/desktop/src/main/services/chat/agentChatService.ts

Repository: arul28/ADE

Length of output: 19447


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== shared sync remote command types =="
rg -n -C6 'SyncRemoteCommandAction|SyncRemoteCommandDescriptor|SyncRemoteCommandPolicy|SyncCommandPayload|SyncCommandAckPayload' apps/desktop/src/shared apps/ade-cli/src/services/sync/syncPeerService.ts apps/ade-cli/src/services/sync/syncHostService.ts | head -n 220

echo
echo "== syncPeerService execute/deny path =="
sed -n '640,715p' apps/ade-cli/src/services/sync/syncPeerService.ts

echo
echo "== syncHostService command routing enforcement =="
rg -n -C8 'executeRemoteCommand|remoteCommandDescriptors|isRemoteCommandAllowed|viewerAllowed|commandRouting|allowed.*remote|disallow|deny|unavailable' apps/ade-cli/src/services/sync/syncHostService.ts | head -n 320

echo
echo "== registry action type around chat actions =="
sed -n '480,570p' apps/desktop/src/main/services/adeActions/registry.ts

Repository: arul28/ADE

Length of output: 37579


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== registry function registering actions =="
sed -n '450,500p' apps/desktop/src/main/services/adeActions/registry.ts

echo
echo "== registry type definitions =="
rg -n -C8 'export type AdeAction|export interface AdeAction|adeActionsRegistry|export const actions|remoteCommand|commandRouting|actions.*chat|chat.*actions' apps/desktop/src/main/services/adeActions/registry.ts apps/desktop/src/shared apps/ade-cli/src/services/sync/syncPeerService.ts | sed -n '1,260p'

echo
echo "== remote command service supported actions/descriptors implementation =="
rg -n -C8 'getSupportedActions|getDescriptors|supportedActions|descriptor|scope|policy|registerChatRemoteCommands|registerAllRemoteCommands|remoteCommandService' apps/ade-cli/src/services/sync/syncRemoteCommandService.ts apps/desktop/src/main/services/adeActions/registry.ts

echo
echo "== typescript definitions exact =="
sed -n '1,80p' apps/desktop/src/shared/types/sync* 2>/dev/null || true
rg -n -C8 'type SyncRemoteCommandAction|interface SyncRemoteCommandAction|type SyncRemoteCommandDescriptor|interface SyncRemoteCommandDescriptor|SyncRemoteCommandPolicy' apps/desktop/src/shared apps/ade-cli/src 2>/dev/null | sed -n '1,240p'

Repository: arul28/ADE

Length of output: 50366


Scope fastForwardCrossMachineHandoffLane to the established handoff handoff target.

The command is exposed to paired controllers with a generic { laneId, expectedHead } payload, so a caller can currently try laneId values outside the previously prepared/validated handoff target. Make the fast-forward path require an in-flight handoff/session context or otherwise scope the operation to the expected handoff target before the merge --ff-only.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 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 `@apps/desktop/src/main/services/chat/agentChatService.ts` around lines 29470 -
29590, Update fastForwardCrossMachineHandoffLane to require and validate the
established in-flight handoff/session context before performing any fetch or
merge, ensuring the requested laneId matches the previously prepared handoff
target. Reject generic or unrelated lane IDs, and preserve the existing merge
--ff-only validation only after this target check passes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Skipping this one deliberately, with reasoning.

fastForwardCrossMachineHandoffLane grants no capability a paired controller does not already have. The sync surface already exposes git.discardFile, git.stageAll, lanes.archive, and lanes.resolveBranchDrift as viewerAllowed: true — all strictly more destructive than this method, which can only merge --ff-only a lane that is clean, not rebasing, and a strict ancestor, onto a commit that origin/<branch> already points at. No commits are lost and no arbitrary commit can be checked out. A paired controller is a trusted controller for the machine, which is the documented threat model.

Binding the call to an in-flight handoff is not available cheaply either: the fast-forward runs during preflight, before any acceptance, and the destination's durable handoff record is only created at accept time. Adding that binding means introducing new durable preflight state — a design change rather than a quick win, and one that would need its own idempotency story.

Leaving as-is and noting it as a follow-up if we later want preflight to be stateful.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The Comment image shows the stylized “ADE” logo in large white block lettering with purple highlights and dark shadows, surrounded by layered wavy purple, pink, and blue contours on a black background.

@arul28, agreed. Given the trusted paired-controller threat model and the existing viewerAllowed mutation surface, this guarded merge --ff-only does not introduce a meaningful new capability. The clean-worktree, no-rebase, expected remote-head, reachability, and ancestor checks keep the operation non-destructive.

Requiring durable preflight handoff state would be a larger design change, so leaving this as a future consideration is appropriate. Withdrawing this finding.

🐇💭


✏️ Learnings added
Learnt from: arul28
Repo: arul28/ADE PR: 910
File: apps/desktop/src/main/services/chat/agentChatService.ts:29470-29590
Timestamp: 2026-07-27T16:39:36.856Z
Learning: In ADE cross-machine handoff flows, paired controllers are trusted controllers for the target machine. The sync remote-command surface intentionally permits viewer-allowed mutations including `git.discardFile`, `git.stageAll`, `lanes.archive`, and `lanes.resolveBranchDrift`. Therefore, `apps/desktop/src/main/services/chat/agentChatService.ts` `fastForwardCrossMachineHandoffLane` does not need to be bound to durable in-flight handoff state when it remains constrained to a clean, non-rebasing lane and a verified `git merge --ff-only` to the expected origin branch head. Preflight currently happens before accept-time durable handoff state exists; making it stateful requires a separate idempotent design.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread apps/ios/ADE/Views/Work/WorkImportSessionScreen.swift
… anchor

- The permission list is portalled to document.body at z-[100], so opened from
  the handoff modal (overlay z-[190]) it rendered behind the backdrop and could
  not be clicked — the destination permission control was unusable. Let the
  caller supply the layer; the composer keeps its existing stacking.
- Normalize both sides of the iOS latest-anchor comparison and check it against
  the started anchor too, matching the desktop and TUI rule.

Skipped, with reasoning on the thread: binding
fastForwardCrossMachineHandoffLane to an in-flight handoff. It grants no
capability a paired controller lacks — git.discardFile, lanes.archive and
lanes.resolveBranchDrift are already viewerAllowed and strictly more
destructive — and the fast-forward runs during preflight, before any durable
handoff record exists.

Co-Authored-By: Claude <noreply@anthropic.com>
@arul28

arul28 commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0ea90e4e45

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1082 to +1086
{destinationFastModeSupported && onFastModeChange ? (
<button
type="button"
aria-pressed={Boolean(fastMode)}
onClick={() => onFastModeChange(!fastMode)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pass Claude fast mode into the handoff capsule

When a fast-capable Claude model is selected, modelSupportsFastMode makes this new toggle visible, but crossMachineHandoffTarget in AgentChatPane.tsx still serializes fastMode only for Codex and OpenCode. Consequently, changing this toggle for Claude never reaches prepareCrossMachineHandoff; the service instead inherits the source session's fast setting, potentially starting the destination at the opposite tier from the UI selection. Pass the setting for every model for which this control is rendered.

AGENTS.md reference: AGENTS.md:L34-L34

Useful? React with 👍 / 👎.

Comment on lines +811 to +813
messages: recentExternalSessionMessagesFromRecords(
recentCodexRecords(record.sourcePath, lookupId != null),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Canonicalize Codex records before sampling messages

Normal Codex rollouts persist the same conversational turn as both a canonical event_msg and a mirrored response_item; the existing first-prompt and message-count paths explicitly prefer the canonical representation for this reason. Passing the raw suffix to the generic sampler classifies both representations as messages, so the new last-eight preview can duplicate user and assistant turns and evict genuine earlier exchanges, also producing incorrect latest anchors and search results. Filter or deduplicate the provider records in discovery before returning the shared summary rather than compensating in each client.

AGENTS.md reference: AGENTS.md:L33-L33

Useful? React with 👍 / 👎.

Comment thread apps/ios/ADE/Services/SyncService.swift Outdated
Comment on lines +8017 to +8018
reasoningEffort: String? = nil,
fastMode: Bool? = 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.

P2 Badge Wire the new iOS launch options into imports

These new parameters are never supplied by the iOS import flow: WorkImportSessionScreen.importSession remains the only caller and passes only provider/session/lane/target/mode, with no reasoning or fast-mode state or controls anywhere in the screen. Thus every iOS ADE-chat import still sends both values as nil and uses host defaults, despite this change documenting that users can select and persist them. Add the chat-only controls and pass their selections through this interface.

AGENTS.md reference: AGENTS.md:L34-L34

Useful? React with 👍 / 👎.

Same class as the rest of this branch — a surface that appears live but never
reaches the thing it claims to configure.

- Serialize destination fast mode for every model whose toggle the modal
  renders. It was spread only for Codex and OpenCode, so a fast-capable Claude
  model showed a working switch that never reached the capsule and the
  destination silently inherited the source's tier.
- Drop Codex mirror rows before sampling messages. Codex writes each turn as
  both a canonical event_msg and a mirrored response_item, so previews showed
  every turn twice and evicted genuinely older exchanges from the capped
  window, skewing the latest anchor and search along with it.
- Remove reasoningEffort and fastMode from the iOS importExternalSession
  signature. Nothing supplies them now that the inert controls are gone, and a
  parameter no caller can reach is the same lie in a different place.

Co-Authored-By: Claude <noreply@anthropic.com>
@arul28

arul28 commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c40d8b1202

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +241 to +244
const summarized = summarizeNativeControls(providerGroup, nativeControls).permissionMode;
const current = options.some((option) => option.value === summarized)
? summarized!
: options[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.

P1 Badge Do not label custom permissions as the default

When a source chat has a Codex native-control combination that summarizeNativeControls cannot map to a preset—for example, approvalPolicy: "never" with sandbox: "workspace-write"summarized is undefined, so this displays options[0] as “Default permissions.” The fallback is never written into nativeControls; AgentChatPane still serializes the original raw controls, and the destination runs them. A user can therefore send a handoff that appears to use the default/on-request policy while it actually skips approvals. Represent the state as Custom or normalize the underlying controls before allowing the handoff.

Useful? React with 👍 / 👎.

Comment on lines +29333 to +29334
const fetch = await runGit(
["fetch", "origin", `refs/heads/${branchRef}:refs/remotes/origin/${branchRef}`],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Force the remote-tracking ref update

When origin/<branch> on the destination still records a commit newer than or unrelated to the source commit after the remote branch was force-pushed or rewound, this unforced destination refspec is rejected as a non-fast-forward update. git fetch -h documents --force as “force overwrite of local reference”; without it, preflight adds a fetch blocker before checking whether the clean lane HEAD is actually an ancestor of the expected commit, so an otherwise safe fast-forward cannot be offered. Use a forced refspec (+refs/heads/...) or --force after the existing ls-remote equality check.

Useful? React with 👍 / 👎.

…king ref

- A Codex native combination the presets can't express (approval "never" with
  sandbox "workspace-write", say) fell back to the first option and rendered as
  "Default permissions". The raw controls are what actually travel in the
  capsule, so the pill claimed the destination would ask for approval while it
  would skip it. Show Custom instead, unselectable, since choosing a real preset
  is what rewrites the underlying controls.
- Fetch the destination's remote-tracking ref with a forced refspec. It is only
  a local mirror of origin, and when the destination still records a rewound or
  newer commit the unforced update is rejected as non-fast-forward, blocking a
  fast-forward that is otherwise safe. The existing origin/HEAD equality check
  is what gates the merge.

Co-Authored-By: Claude <noreply@anthropic.com>
@arul28

arul28 commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da2d17b682

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +5481 to +5482
const applyHandoffNativeControls = useCallback((next: NativeControlState) => {
setHandoffClaudePermissionMode(next.claudePermissionMode);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the destination interaction mode

When the source Claude chat is in plan mode, handoffNativeControlState carries interactionMode: "plan", but this callback drops the updated next.interactionMode. Selecting Default, Edit, or Full auto in the new destination picker therefore leaves summarizeNativeControls pinned to plan mode, so the picker does not change and the capsule still receives permissionMode: "plan". Store interaction mode in handoff-specific state alongside the other native controls rather than retaining the source chat's mode.

Useful? React with 👍 / 👎.

message.role === "user" ? "text-violet-300/85" : "text-emerald-300/70",
)}
>
{message.role === "user" ? "you" : "ADE"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Attribute imported replies to their provider

For every sampled Claude or Codex transcript, assistant turns are labeled ADE even though they were authored by that external provider. This makes the newly added conversation preview misattribute history on desktop (and the iOS row repeats the same label); use the already available summary.provider display name instead.

AGENTS.md reference: AGENTS.md:L59-L60

Useful? React with 👍 / 👎.

Comment on lines +327 to +330
`messages`. Chat imports also choose how the resulting ADE chat starts: model,
reasoning effort, fast mode (only where the model supports it), and permission
mode, seeded from and saved back to `WorkComposerPreferences` so the phone's
composer and its imports agree. Those fields are sent only for `target: "chat"`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove the unsupported iOS import-control claim

This newly documented iOS behavior does not exist in the reviewed implementation: WorkImportSessionScreen.importSession still sends only provider/session/lane/target/mode, and SyncService.importExternalSession accepts only optional model and permission mode—there are no model, reasoning, fast-mode, or permission controls in the screen and no reasoning/fast payload fields. The documentation currently tells users and maintainers that these settings are selectable and persisted when imports always use the existing defaults.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
apps/desktop/src/main/services/chat/agentChatService.ts (1)

29144-29188: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

cursorModeId inclusion condition checks the wrong variable — session fallback is silently dropped.

targetCursorModeId is computed with an args-override-or-session-fallback pattern identical to every other field here (targetClaudePermissionMode, targetCodexApprovalPolicy, etc.), so it's meant to carry the session's current Cursor mode into the capsule when the caller doesn't explicitly override it. But the capsule spread condition tests args.cursorModeId !== undefined instead of targetCursorModeId !== undefined:

...(args.cursorModeId !== undefined ? { cursorModeId: targetCursorModeId } : {}),

Since callers of prepareCrossMachineHandoff typically don't pass an explicit cursorModeId override, this means the capsule omits cursorModeId in the common case even when managed.session.cursorModeId holds real state (e.g. "plan" or "full-auto"), silently dropping it and leaving the destination chat on its default Cursor mode instead of inheriting the source's mode.

🐛 Proposed fix
-        ...(args.cursorModeId !== undefined ? { cursorModeId: targetCursorModeId } : {}),
+        ...(targetCursorModeId !== undefined ? { cursorModeId: targetCursorModeId } : {}),
🤖 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 `@apps/desktop/src/main/services/chat/agentChatService.ts` around lines 29144 -
29188, Update the target object in prepareCrossMachineHandoff so the
cursorModeId spread condition checks targetCursorModeId rather than
args.cursorModeId, preserving the computed explicit-override or
managed.session.cursorModeId fallback when present.
🤖 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 `@apps/desktop/src/main/services/chat/agentChatService.ts`:
- Around line 29144-29188: Update the target object in
prepareCrossMachineHandoff so the cursorModeId spread condition checks
targetCursorModeId rather than args.cursorModeId, preserving the computed
explicit-override or managed.session.cursorModeId fallback when present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 44803371-e02d-4253-be07-a30283a70cc2

📥 Commits

Reviewing files that changed from the base of the PR and between 2c172fa and da2d17b.

📒 Files selected for processing (11)
  • apps/desktop/src/main/services/chat/agentChatService.ts
  • apps/desktop/src/main/services/externalSessions/discoverCodex.ts
  • apps/desktop/src/main/services/externalSessions/discoveryUtils.test.ts
  • apps/desktop/src/main/services/externalSessions/discoveryUtils.ts
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/chat/CrossMachineHandoffModal.test.tsx
  • apps/desktop/src/renderer/components/chat/CrossMachineHandoffModal.tsx
  • apps/desktop/src/renderer/components/chat/crossMachineHandoffPresentation.tsx
  • apps/desktop/src/renderer/components/shared/PermissionModePicker.tsx
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/Work/WorkImportSessionScreen.swift
🚧 Files skipped from review as they are similar to previous changes (9)
  • apps/desktop/src/main/services/externalSessions/discoveryUtils.test.ts
  • apps/desktop/src/main/services/externalSessions/discoverCodex.ts
  • apps/desktop/src/renderer/components/chat/crossMachineHandoffPresentation.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/shared/PermissionModePicker.tsx
  • apps/desktop/src/main/services/externalSessions/discoveryUtils.ts
  • apps/ios/ADE/Views/Work/WorkImportSessionScreen.swift
  • apps/desktop/src/renderer/components/chat/CrossMachineHandoffModal.test.tsx
  • apps/desktop/src/renderer/components/chat/CrossMachineHandoffModal.tsx

@arul28
arul28 merged commit 0ff7cbc into main Jul 27, 2026
33 checks passed
@arul28
arul28 deleted the ade/ok-couple-things-wanna-discuss-87c99204 branch July 27, 2026 17:13
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