Skip to content

Deeplinks#326

Merged
arul28 merged 7 commits into
mainfrom
ade/deeplinks-d52aa89e
May 21, 2026
Merged

Deeplinks#326
arul28 merged 7 commits into
mainfrom
ade/deeplinks-d52aa89e

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented May 21, 2026

Summary

Describe the change.

What Changed

Key files and behaviors.

Validation

How you tested.

Risks

Anything to watch.

ADE   Open in ADE  ·  ade/deeplinks-d52aa89e branch  ·  PR #326

Summary by CodeRabbit

  • New Features

    • ADE deeplinks: share/open lane, branch, PR, and Linear-issue links across devices; CLI: ade open, ade link, ade linear install
    • Multi-chat tiled view with split-chat shortcuts (Ctrl+G, Tab, Ctrl+W) and tile minimap
    • Desktop banners/modals for inbound deeplinks and clipboard-detected deeplinks; iOS "Send to Mac" flow
    • Better terminal resume/continue UX and terminal session listing/clipboard support
  • Documentation

    • New ADE deeplinks guide with examples and workflow notes

Review Change Stack

Greptile Summary

This PR ships the complete ADE deeplinks system: an ade:// protocol handler in the Electron main process, CLI subcommands (ade open, ade link, ade linear install), a clipboard-sniffing banner, an inbound branch-deeplink modal, a multi-chat tiled TUI view, and an iOS "Send to Mac" confirmation card backed by a Vercel /open serverless function that rewrites OpenGraph tags for rich unfurls.

  • Desktop: protocolHandler.ts registers the ade:// scheme; adeDeeplinkFooter.ts idempotently appends a branded footer to all newly created PR bodies; laneService.ts wraps all git worktree mutations in an AsyncLocalStorage-backed serial queue to prevent concurrent worktree races.
  • CLI: deeplinks.ts adds ade open/ade link/ade linear install; runLinearInstall merges with existing ~/.linear/coding-tools.json but skips the promised backup when the file exists with malformed JSON.
  • iOS / Web: DeepLinkRouter.swift routes new cross-machine deeplink shapes to a SendToMacCard sheet; apps/web/api/open.ts rewrites OG meta tags from validated, HTML-escaped query parameters.

Confidence Score: 3/5

Two defects need fixing before merge: the BRANCH_BAD_RE regex breaks every branch-based deeplink, and runLinearInstall silently destroys a user config file when JSON parse fails.

The BRANCH_BAD_RE bug means isValidBranch rejects any branch containing a hyphen, breaking all branch and repo deeplinks end-to-end and causing the new tests to fail. Separately, runLinearInstall backs up coding-tools.json only when JSON.parse succeeds; if the file is malformed the backup is skipped and the file is silently overwritten. Both are regressions on new code introduced in this PR.

apps/desktop/src/shared/deeplinks.ts (BRANCH_BAD_RE regex) and apps/ade-cli/src/commands/deeplinks.ts (runLinearInstall backup logic)

Important Files Changed

Filename Overview
apps/desktop/src/shared/deeplinks.ts Core URL builder/parser; BRANCH_BAD_RE treats [ -] as a character range (space-hyphen, 0x20-0x2D) so isValidBranch rejects any branch containing a hyphen, breaking every branch deeplink.
apps/ade-cli/src/commands/deeplinks.ts New CLI commands. runLinearInstall backup logic only runs when JSON parse succeeds; a malformed existing config is silently overwritten without backup.
apps/desktop/src/main/services/deeplinks/protocolHandler.ts Registers ade:// scheme, single-instance lock, and buffers cold-start URLs. Non-claiming channel logic is intentional and well-commented.
apps/desktop/src/renderer/components/app/InboundDeeplinkModal.tsx Confirmation dialog for inbound branch deeplinks. Unstable onClose/onLaneOpened refs cause the existingLane effect to re-fire on each lanes-poll update.
apps/desktop/src/shared/adeDeeplinkFooter.ts Idempotent PR-body footer block; HTML-escapes all user values, regex-based marker detection is correct.
apps/desktop/src/renderer/components/app/ClipboardDeeplinkBanner.tsx Polls clipboard on window focus; correctly converts HTTPS deeplinks to ade:// form before calling openExternal.
apps/desktop/src/main/services/lanes/laneService.ts Introduces runGitWorktreeMutation serial queue using AsyncLocalStorage for re-entrancy; wraps all git worktree mutations.
apps/desktop/src/main/services/prs/prService.ts Appends branded Open in ADE footer to all created PRs, with a follow-up PATCH once the PR number is known.
apps/ios/ADE/App/DeepLinkRouter.swift Extended to dispatch new desktop deeplink shapes via adeSendToMacRequested notification instead of local navigation.
apps/web/api/open.ts New Vercel serverless function rewriting OpenGraph tags; all user values are properly HTML-escaped.

Sequence Diagram

sequenceDiagram
    participant User
    participant OS
    participant Desktop as Electron Main
    participant Renderer
    participant CLI as ADE CLI
    participant iOS

    Note over User,iOS: Outbound sharing
    User->>CLI: ade link branch owner/repo feat-x
    CLI->>CLI: buildDeeplink to https URL
    CLI-->>User: URL copied to clipboard

    Note over User,iOS: Inbound desktop cold-start
    User->>OS: open ade://repo/owner/repo/branch/feat-x
    OS->>Desktop: open-url event
    Desktop->>Desktop: parseDeeplink to AppNavigationTarget
    Desktop->>Renderer: IPC appNavigate branch target
    Renderer->>Renderer: InboundDeeplinkModal shown
    User->>Renderer: Confirm
    Renderer->>Desktop: IPC createLaneFromPrBranch
    Desktop-->>Renderer: lane created navigate /lanes

    Note over User,iOS: iOS Send to Mac
    User->>iOS: tap ade://lane/uuid link
    iOS->>iOS: DeepLinkRouter posts adeSendToMacRequested
    iOS->>User: SendToMacCard sheet
    User->>iOS: Confirm send
    iOS->>Desktop: SyncService relay

    Note over User,iOS: Clipboard detection
    User->>Renderer: paste ade:// URL then focus window
    Renderer->>Renderer: ClipboardDeeplinkBanner appears
    User->>Renderer: click Open
    Renderer->>OS: openExternal ade:// URL
    OS->>Desktop: open-url event navigate
Loading

Comments Outside Diff (2)

  1. apps/desktop/src/shared/deeplinks.ts, line 63 (link)

    P1 BRANCH_BAD_RE rejects hyphenated branch names

    The character class [ -] contains a space and a literal hyphen (ECMAScript non-unicode mode treats - at the end of a character class as a literal). This means isValidBranch("feat-deeplinks") returns false, causing parseDeeplink to reject the overwhelming majority of real-world branch names (e.g. feat-deeplinks, fix-bug-123). The new tests asserting that feat-deeplinks parses successfully will also fail. Based on the comment ("traversal + control chars"), the intended fix is to remove the hyphen: change [ -] to [ ] (space only).

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/shared/deeplinks.ts
    Line: 63
    
    Comment:
    **`BRANCH_BAD_RE` rejects hyphenated branch names**
    
    The character class `[ -]` contains a space and a literal hyphen (ECMAScript non-unicode mode treats `-` at the end of a character class as a literal). This means `isValidBranch("feat-deeplinks")` returns `false`, causing `parseDeeplink` to reject the overwhelming majority of real-world branch names (e.g. `feat-deeplinks`, `fix-bug-123`). The new tests asserting that `feat-deeplinks` parses successfully will also fail. Based on the comment ("traversal + control chars"), the intended fix is to remove the hyphen: change `[ -]` to `[ ]` (space only).
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  2. apps/desktop/src/shared/deeplinks.ts, line 63 (link)

    P1 BRANCH_BAD_RE rejects all hyphenated branch names

    The character class [ -] contains a space and a literal hyphen (in ECMAScript non-unicode mode, - at the end of a class is treated as a literal, not a range). This means isValidBranch rejects any branch containing a -, so names like feat-deeplinks, fix-bug-123, and users/arul/feat-x all return false. The existing test asserting that feat-deeplinks parses successfully will fail. The fix is to remove the hyphen from the character class: const BRANCH_BAD_RE = /(^|\/)\.\.($|\/)| /;

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/shared/deeplinks.ts
    Line: 63
    
    Comment:
    **`BRANCH_BAD_RE` rejects all hyphenated branch names**
    
    The character class `[ -]` contains a space and a literal hyphen (in ECMAScript non-unicode mode, `-` at the end of a class is treated as a literal, not a range). This means `isValidBranch` rejects any branch containing a `-`, so names like `feat-deeplinks`, `fix-bug-123`, and `users/arul/feat-x` all return `false`. The existing test asserting that `feat-deeplinks` parses successfully will fail. The fix is to remove the hyphen from the character class: `const BRANCH_BAD_RE = /(^|\/)\.\.($|\/)| /;`
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

Fix All in Claude Code

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

---

### Issue 1 of 2
apps/ade-cli/src/commands/deeplinks.ts:289-310
**Backup skipped when existing config has malformed JSON**

`existing` is only truthy when `JSON.parse` succeeds. If `~/.linear/coding-tools.json` is present but contains invalid JSON (e.g., a partial write from a previous install), the backup branch is skipped and the file is silently overwritten without any copy. The user's config is destroyed with no recovery path, even though the comment promises "Backs up any existing file alongside." The fix is to gate backup creation on whether the file *exists* rather than on whether the JSON was parseable.

```suggestion
  const fileExists = fs.existsSync(cfgPath);
  let existing: Record<string, unknown> | null = null;
  if (fileExists) {
    try {
      existing = JSON.parse(fs.readFileSync(cfgPath, "utf8")) as Record<string, unknown>;
    } catch {
      existing = null;
    }
  }

  const merged = existing ? { ...existing, ...desiredEntry } : desiredEntry;
  const serialized = JSON.stringify(merged, null, 2) + "\n";

  if (dryRun) {
    return {
      output: `Would write ${cfgPath}\n\n${serialized}`,
      exitCode: 0,
    };
  }

  fs.mkdirSync(cfgDir, { recursive: true });

  if (fileExists) {
    const backupPath = `${cfgPath}.bak-${new Date()
      .toISOString()
      .replace(/[:.]/g, "")
      .slice(0, 15)}`;
    fs.copyFileSync(cfgPath, backupPath);
    fs.writeFileSync(cfgPath, serialized);
    return {
      output: `Wrote ${cfgPath} (backup at ${backupPath})\n`,
      exitCode: 0,
    };
  }
  fs.writeFileSync(cfgPath, serialized);
  return { output: `Wrote ${cfgPath}\n`, exitCode: 0 };
```

### Issue 2 of 2
apps/desktop/src/renderer/components/app/InboundDeeplinkModal.tsx:63-69
Both `onClose` and `onLaneOpened` are new arrow-function references on every render of `AppNavigationBridge`. Including them in the effect dependency array causes the effect to re-fire on every parent re-render (including frequent lanes-poll updates). When `existingLane` is truthy, this calls `navigate` and `setInboundBranch(null)` repeatedly until the modal unmounts. Memoising the callbacks with `useCallback` is the clean fix; alternatively, read the latest values through refs inside a stable effect.

```suggestion
  const onLaneOpenedRef = React.useRef(onLaneOpened);
  const onCloseRef = React.useRef(onClose);
  React.useEffect(() => {
    onLaneOpenedRef.current = onLaneOpened;
    onCloseRef.current = onClose;
  });

  React.useEffect(() => {
    // If a lane already exists for this branch, jump straight to it — no modal.
    if (existingLane) {
      onLaneOpenedRef.current(existingLane.id);
      onCloseRef.current();
    }
  }, [existingLane]);
```

Reviews (4): Last reviewed commit: "ship: address follow-up review feedback" | Re-trigger Greptile

arul28 and others added 4 commits May 20, 2026 21:48
Snapshot of in-progress deeplinks work before pulling Tui Perf and
Mac VM updates from main. Safe to soft-reset after the merge if you
want these changes back in the working tree.

- ade:// protocol handler + parser (apps/desktop/src/main/services/deeplinks)
- iOS DeepLinkRouter extended for lane / repo / pr / linear-issue forms
  plus the cross-machine "Send to your Mac" path
- syncRemoteCommandService.deeplinks.open for iOS -> desktop bounce
- buildDeeplink + ensureAdeDeeplinkFooter shared utilities
- LinearLaneCardService now publishes the cross-machine ADE deeplink
- New renderer surfaces: ClipboardDeeplinkBanner, CrossRepoPrBanner,
  InboundDeeplinkModal
- ade-deeplinks agent skill, OpenPage web handler, docs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	apps/ade-cli/src/tuiClient/__tests__/adeApi.test.ts
#	apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts
#	apps/ade-cli/src/tuiClient/app.tsx
#	apps/desktop/src/main/services/ipc/registerIpc.ts
#	apps/desktop/src/shared/adeCliGuidance.ts
- Code simplification across ade-cli, desktop main, and web (extract helpers, collapse nested ternaries, dead-branch removal)
- Add multi-chat grid layout, hit-test registry, and add-chat mode to TUI client

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ReviewPage: relax formatCompareTargetDescription param to the Pick subset it actually reads, fixing 4 TS2345s
- SessionListPane test: align with refactored card (no inline tool label, lane name only in group header)
- ChatAppearancePreview test: accept 22% or 28% accent mix (light-accent providers now use 22%)
- ChatGitToolbar test: match new /lanes?laneId=... route and drop removed Run-menu test
- iOS bootstrap SQL: regenerate to include lane_worktree_locks table

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 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 21, 2026 9:20am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

📝 Walkthrough
📝 Walkthrough
✨ 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/deeplinks-d52aa89e

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 21, 2026

@copilot review but do not make fixes

@capy-ai
Copy link
Copy Markdown

capy-ai Bot commented May 21, 2026

Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

Copy link
Copy Markdown
Contributor

Copilot AI commented May 21, 2026

@copilot review but do not make fixes

Here's the review:


🐛 Bug: BRANCH_BAD_RE incorrectly rejects branches with hyphens
apps/desktop/src/shared/deeplinks.ts line 63

const BRANCH_BAD_RE = /(^|\/)\.\.($|\/)|[ -]/;

[ -] matches a space and a literal hyphen (a - at the end of a character class is always literal). Every hyphenated branch (feat-deeplinks, feat-x, ade/deeplinks-d52aa89e, etc.) causes isValidBranch to return false, making parseDeeplink return ok: false for valid links. The unit tests in deeplinks.test.ts that assert ok: true for branches like "feat-deeplinks" would fail with this regex. The comment ("reject traversal + control chars") suggests the intent was /(^|\/)\.\.($|\/)|\s/ or /(^|\/)\.\.($|\/)| / (space only).


🔧 OpenPage.tsx doesn't handle linear-issue
apps/web/src/app/pages/OpenPage.tsx

parseQuery and buildAdeUrl only handle lane, branch, and pr. A link like https://ade.app/open?type=linear-issue&issue=ADE-123 renders the "missing or invalid link parameters" fallback on the client — the auto-launch never fires. The server-side open.ts generates the correct OG tags so unfurls look fine, but clicking through is broken for this type.


🔍 iOS SendToMacCard only reads the first branch segment
apps/ios/ADE/Views/Deeplinks/SendToMacCard.swift line 43

self.kind = .repoBranch(owner: parts[0], repo: parts[1], branch: parts[3])

For ade://repo/o/r/branch/users/arul/feat-x, parts[3] is "users" only. The label shows "Branch users in o/r". The raw URL is still forwarded to the Mac correctly, so it's display-only, but the branch name should be parts[3...].joined(separator: "/") to match the desktop's multi-segment handling.


ℹ️ deeplinks.open sync failure is silent on iOS

When the desktop isn't running, deeplinks.open returns { ok: false, ... } rather than throwing. SendToMacCard.sendToMac() doesn't inspect the response, so the UI always shows "Sent ✓" even when the link was never delivered.


ℹ️ Dynamic dispatch in desktopBridgeServer.ts

The (service as unknown as Record<string, unknown>)[name] pattern on line 128 bypasses TypeScript. The ALLOWED_METHODS set is the only safety net — a method rename on BuiltInBrowserService would silently break at runtime instead of at compile time. A typed dispatch map would be more robust.

Copilot finished work on behalf of arul28 May 21, 2026 07:47
Comment thread apps/desktop/src/main/services/deeplinks/protocolHandler.ts 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: 16

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (3)
apps/ade-cli/src/tuiClient/components/Drawer.tsx (1)

173-193: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep add-mode UX consistent in MiniDrawer.

addMode updates full drawer labels/hints, but mini density still shows normal LANES/chats copy, which drops the “pick chat/add/cancel” guidance in the same flow.

💡 Suggested patch
@@
       <MiniDrawer
         width={width}
         borderColor={borderColor}
+        addMode={addMode}
+        emphasisColor={emphasisColor}
         lanes={laneRows}
@@
 function MiniDrawer({
   width,
   borderColor,
+  addMode,
+  emphasisColor,
@@
 }: {
   width: number;
   borderColor: string;
+  addMode: boolean;
+  emphasisColor: string;
@@
       <Box paddingX={1}>
-        <Text bold color={theme.color.violet}>
-          LANES · {loading && lanes.length === 0 ? "…" : lanes.length}
+        <Text bold color={addMode ? emphasisColor : theme.color.violet}>
+          {addMode ? "PICK CHAT" : `LANES · ${loading && lanes.length === 0 ? "…" : lanes.length}`}
         </Text>
       </Box>
@@
       <Box paddingX={1} flexShrink={0}>
         <Text color={theme.color.t4} wrap="truncate">
-          {!focused ? "\n" : mode === "chats" ? "↑↓ chats · Esc lanes" : "↑↓ lanes · ↵ open"}
+          {!focused
+            ? "\n"
+            : addMode
+              ? "↑↓ pick chat · ↵ add · esc cancel"
+              : mode === "chats"
+                ? "↑↓ chats · Esc lanes"
+                : "↑↓ lanes · ↵ open"}
         </Text>
       </Box>

Also applies to: 640-643, 723-723

🤖 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/tuiClient/components/Drawer.tsx` around lines 173 - 193, The
MiniDrawer isn't receiving the add-mode flag so it continues to render normal
LANES/chats copy; update the Drawer render to pass the addMode prop into
MiniDrawer (same way as FullDrawer/other densities) and ensure MiniDrawer's
props/type (MiniDrawer component signature) accept and use addMode to switch
labels/hints; also apply the same fix for the other indicated render sites (the
other MiniDrawer usages around the file) so mini density shows the "pick chat /
add / cancel" guidance when addMode is true.
apps/desktop/src/main/services/ipc/runtimeBridge.ts (1)

685-700: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Category changes won’t take effect for live subscriptions.

ensureRuntimeEventSubscription only keys reuse on bindingKey (Line 269). After this change, when request.category changes for the same binding, the old subscription is kept, so live pushed events can remain on the previous category.

💡 Proposed fix
 type RuntimeEventWindowSubscription = {
   bindingKey: string;
+  requestKey: string;
   cleanup: (() => void) | null;
 };

 const ensureRuntimeEventSubscription = (
   sender: WebContents,
   bindingKey: string,
+  requestKey: string,
   subscribe: RuntimeEventSubscribe,
 ): void => {
   const existing = runtimeEventSubscriptions.get(sender.id);
-  if (existing?.bindingKey === bindingKey) return;
+  if (existing?.requestKey === requestKey) return;
   cleanupRuntimeEventSubscription(sender.id);
   watchRuntimeEventSender(sender);
-  runtimeEventSubscriptions.set(sender.id, { bindingKey, cleanup: null });
+  runtimeEventSubscriptions.set(sender.id, { bindingKey, requestKey, cleanup: null });

   const onEnded = () => {
     const current = runtimeEventSubscriptions.get(sender.id);
-    if (current?.bindingKey === bindingKey) {
+    if (current?.requestKey === requestKey) {
       runtimeEventSubscriptions.delete(sender.id);
     }
   };

@@
         if (
           !current ||
-          current.bindingKey !== bindingKey ||
+          current.requestKey !== requestKey ||
           sender.isDestroyed()
         ) {
           cleanup();
           return;
         }

@@
         const current = runtimeEventSubscriptions.get(sender.id);
-        if (current?.bindingKey === bindingKey && !current.cleanup) {
+        if (current?.requestKey === requestKey && !current.cleanup) {
           runtimeEventSubscriptions.delete(sender.id);
         }
         ensureRuntimeEventSubscription(
           event.sender,
           binding.key,
+          `${binding.key}:${arg?.request?.category ?? "*"}`,
           (onEvent, onEnded) =>
       ensureRuntimeEventSubscription(
         event.sender,
         `remote:${target.id}:${projectId}`,
+        `remote:${target.id}:${projectId}:${arg?.request?.category ?? "*"}`,
         (onEvent, onEnded) =>

Also applies to: 732-747

🤖 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/ipc/runtimeBridge.ts` around lines 685 - 700,
ensureRuntimeEventSubscription currently deduplicates subscriptions only by
binding.key, so changing request.category for the same binding reuses the old
subscription and new category won't take effect; to fix, make the subscription
key include the request's category (or entire request object) when calling
ensureRuntimeEventSubscription from the local path branch — i.e., change the key
argument from binding.key to a composite key (for example
`${binding.key}:${arg?.request?.category}` or serialize the request) when
invoking ensureRuntimeEventSubscription so
localRuntimeConnectionPool.subscribeEventsForRoot is registered per-category and
live events follow the updated category filter.
apps/desktop/src/main/services/lanes/laneService.ts (1)

3829-4019: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope the worktree queue to the actual git mutation.

Wrapping the entire delete flow in runGitWorktreeMutation means stopAll, watcher teardown, teardownEnv, the 250ms delay, and DB cleanup all hold the global worktree-mutation queue. If any of those steps is slow or hangs, unrelated lane create/import/adopt calls are blocked even though no git worktree command is running. Keep the queue around the git worktree remove/prune section only.

🤖 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/lanes/laneService.ts` around lines 3829 -
4019, The outer await runGitWorktreeMutation(...) currently wraps the entire
delete flow and must be removed so the global worktree-mutation queue only
protects the actual git worktree removal; un-wrap the whole try/catch block from
the outer call and instead keep the existing inner
runStep("git_worktree_remove", ...) which already calls
runGitWorktreeMutation(async () => { ... }); in its body (leave that inner
runGitWorktreeMutation and the removeWorktreeDirectoryWithRecovery/ runGit calls
intact). Concretely: delete the outer runGitWorktreeMutation invocation that
begins before the try (the call using runGitWorktreeMutation and its closing
paren after the catch), leaving all runStep(...) steps, the try/catch/finalize
logic, and the inner runGitWorktreeMutation around the git worktree remove
section unchanged.
🟡 Minor comments (15)
apps/ade-cli/src/commands/deeplinks.ts-95-104 (1)

95-104: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Require --linear-issue when using Linear flag mode.

Line 95 currently allows --branch without --linear-issue, which emits a type=linear-issue URL missing issue. That creates an ambiguous/invalid deeplink payload.

Proposed fix
-  if (linearIssue || branch) {
+  if (linearIssue || branch) {
+    if (!linearIssue) {
+      throw new CliDeeplinkUsageError("--linear-issue is required when using --branch");
+    }
     // Build an https://ade.app/open URL with the hints Linear gave us. The
🤖 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/commands/deeplinks.ts` around lines 95 - 104, The code
builds a "type=linear-issue" deeplink even when branch is provided without
linearIssue, producing an invalid payload; update the validation in the deeplink
handling so that when branch is set but linearIssue is missing (i.e., branch &&
!linearIssue) you surface a clear error/usage message and abort instead of
calling openAndReport; modify the logic around linearIssue, branch,
URLSearchParams and the call to openAndReport to check this case and fail fast
(or require --linear-issue) so you never emit a type=linear-issue URL without an
issue parameter.
apps/ade-cli/src/commands/deeplinks.ts-235-240 (1)

235-240: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject repo slugs with more than one slash.

Line 236 accepts values like a/b/c and silently truncates to owner a, repo b. That can generate incorrect links.

Proposed fix
 function parseRepoSlug(repo: string): { repoOwner: string; repoName: string } {
-  const [repoOwner, repoName] = repo.split("/");
-  if (!repoOwner || !repoName) {
+  const parts = repo.split("/");
+  if (parts.length !== 2) {
+    throw new CliDeeplinkUsageError("Repo must be in 'owner/repo' form");
+  }
+  const [repoOwner, repoName] = parts;
+  if (!repoOwner || !repoName) {
     throw new CliDeeplinkUsageError("Repo must be in 'owner/repo' form");
   }
   return { repoOwner, repoName };
 }
🤖 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/commands/deeplinks.ts` around lines 235 - 240, The
parseRepoSlug function currently splits on "/" and silently accepts strings like
"a/b/c"; update parseRepoSlug to validate that repo.split("/") yields exactly
two parts (length === 2) and throw CliDeeplinkUsageError("Repo must be in
'owner/repo' form") for any other case (zero, one, or more than two segments),
ensuring it rejects slugs with more than one slash rather than truncating them.
apps/ade-cli/src/tuiClient/app.tsx-7674-7677 (1)

7674-7677: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Resolve the Ctrl+G shortcut collision.

This branch consumes Ctrl+G before the existing external-editor path at Line 7969, so the prompt-editor shortcut becomes unreachable from chat.

🤖 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/tuiClient/app.tsx` around lines 7674 - 7677, The Ctrl+G
shortcut is being handled by the chat branch (isCtrlInput + startAddMode) before
the external-editor handling, making the prompt-editor shortcut unreachable;
change the logic so the external-editor path gets first dibs — either move the
if (pane === "chat" && isCtrlInput(input, key, "g")) { startAddMode(); return; }
block to after the external-editor handling, or add a guard so you only call
startAddMode when the external-editor handler is not applicable (e.g., check the
same condition/flag used by the external-editor path or call that handler and
only call startAddMode if it returns false). Ensure you keep references to
isCtrlInput and startAddMode while deferring to the external-editor handling.
apps/ade-cli/src/tuiClient/components/GridMiniMap.tsx-13-16 (1)

13-16: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clamp focus to the rendered cell count, not raw count.

When count > 6, the map renders 6 cells, but focus is clamped against count - 1. A high focused index can leave every rendered cell unfocused.

💡 Suggested patch
 export function gridMiniMapText(count: number, focusedIndex: number): string {
-  if (count <= 1) return "▣";
-  return mapFor(count, Math.max(0, Math.min(focusedIndex, count - 1)));
+  const visibleCount = Math.max(1, Math.min(6, count));
+  if (visibleCount <= 1) return "▣";
+  return mapFor(
+    visibleCount,
+    Math.max(0, Math.min(focusedIndex, visibleCount - 1)),
+  );
 }
🤖 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/tuiClient/components/GridMiniMap.tsx` around lines 13 - 16,
gridMiniMapText clamps focus against the raw count which is wrong when the
minimap renders a limited number of cells (6); compute the number of rendered
cells first (e.g., const rendered = Math.min(count, 6) or use the same limit
mapFor uses) and clamp focusedIndex against rendered - 1 instead of count - 1,
then call mapFor(count, Math.max(0, Math.min(focusedIndex, rendered - 1))).
Refer to gridMiniMapText and mapFor when making the change.
apps/ade-cli/src/tuiClient/deeplinkRow.ts-65-76 (1)

65-76: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate PR number as a positive integer before building links.

On Line 66 and Line 75, prNumber is not fully validated. Values like -1, 1.5, or NaN can still flow into buildDeeplink(...) and produce invalid deeplinks.

Proposed fix
 export function buildDeeplinkForRow(row: DeeplinkRow): string | null {
+  const isValidPrNumber = (value: number): boolean => Number.isInteger(value) && value > 0;
+
   if (row.kind === "lane") {
     if (!row.lane.id) return null;
     const target: DeeplinkTarget = { kind: "lane", laneId: row.lane.id };
     return buildDeeplink(target, { form: "ade" });
   }
   const pr = row.pr;
   if ("repoOwner" in pr) {
-    if (!pr.repoOwner || !pr.repoName || !pr.prNumber) return null;
+    if (!pr.repoOwner || !pr.repoName || !isValidPrNumber(pr.prNumber)) return null;
     return buildDeeplink(
       { kind: "pr", repoOwner: pr.repoOwner, repoName: pr.repoName, prNumber: pr.prNumber },
       { form: "ade" },
     );
   }
   const parsed = parseGitHubPrUrl(pr.url);
   if (!parsed) return null;
+  const prNumber = pr.prNumber ?? parsed.prNumber;
+  if (!isValidPrNumber(prNumber)) return null;
   return buildDeeplink(
-    { kind: "pr", repoOwner: parsed.repoOwner, repoName: parsed.repoName, prNumber: pr.prNumber ?? parsed.prNumber },
+    { kind: "pr", repoOwner: parsed.repoOwner, repoName: parsed.repoName, prNumber },
     { form: "ade" },
   );
 }
🤖 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/tuiClient/deeplinkRow.ts` around lines 65 - 76, The PR
number isn't validated as a positive integer before calling buildDeeplink, so
values like -1, 1.5 or NaN can pass through; update the checks in the branch
that handles "repoOwner" (where pr.prNumber is used) and in the parsed branch
(where parsed.prNumber is used) to ensure the PR number is a positive integer
(e.g., coerce to a number then require Number.isInteger(value) && value > 0) and
return null if it fails, keeping the rest of the buildDeeplink calls unchanged;
reference the symbols pr, parsed, pr.prNumber, parsed.prNumber, buildDeeplink
and parseGitHubPrUrl to locate and modify the validation logic.
apps/desktop/src/main/services/chat/agentChatService.ts-9676-9685 (1)

9676-9685: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

hook_progress is checked in the outer condition but not handled.

The outer condition includes hook_progress, but the inner if/else if chain only handles hook_started and hook_response. If hook_progress messages enter this block, they'll fall through without the intended suppression.

Proposed fix to suppress hook_progress alongside hook_started
           if (hookMsg.subtype === "hook_started") {
             // Claude SDK hook start messages are high-frequency lifecycle noise.
             // Keep failures below, but do not persist successful hook bookkeeping
             // into the user-visible transcript.
             continue;
+          } else if (hookMsg.subtype === "hook_progress") {
+            // Also suppress hook progress messages.
+            continue;
           } else if (hookMsg.subtype === "hook_response") {
🤖 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 9676 -
9685, The code checks for msg.type === "system" including subtype
"hook_progress" but the inner branch only handles "hook_started" and
"hook_response", so add handling for "hook_progress" in the same place as
"hook_started" to suppress it; inside the block where hookMsg.subtype is
inspected (the branch using hookMsg.subtype === "hook_started" /
"hook_response"), add an else if (hookMsg.subtype === "hook_progress") {
continue; } so progress messages are treated as lifecycle noise and not
persisted to the transcript.
apps/desktop/src/main/services/deeplinks/protocolHandler.ts-22-25 (1)

22-25: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle scheme matching case-insensitively in deeplink arg detection.

Line 24 only matches lowercase ade://. Uppercase/mixed-case scheme inputs can be missed and never dispatched.

Suggested fix
 const ADE_OPEN_HTTPS_RE = /^https?:\/\/ade\.app\/open\b/i;
+const ADE_SCHEME_RE = new RegExp(`^${ADE_DEEPLINK_SCHEME}://`, "i");

 function isAdeDeeplinkArg(arg: unknown): arg is string {
   if (typeof arg !== "string") return false;
-  return arg.startsWith(`${ADE_DEEPLINK_SCHEME}://`) || ADE_OPEN_HTTPS_RE.test(arg);
+  return ADE_SCHEME_RE.test(arg) || ADE_OPEN_HTTPS_RE.test(arg);
 }
🤖 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/deeplinks/protocolHandler.ts` around lines 22
- 25, The scheme check in isAdeDeeplinkArg currently only matches lowercase
ADE_DEEPLINK_SCHEME and can miss uppercase/mixed-case inputs; update the
function to perform a case-insensitive scheme check by normalizing the arg
(e.g., const s = String(arg).toLowerCase()) before testing
startsWith(`${ADE_DEEPLINK_SCHEME}://`) or modify ADE_OPEN_HTTPS_RE to be
case-insensitive (add the /i flag) so both the startsWith and regex checks
accept uppercase/mixed-case schemes; keep the type guard name isAdeDeeplinkArg
and use ADE_DEEPLINK_SCHEME and ADE_OPEN_HTTPS_RE to locate the change.
apps/desktop/src/main/services/lanes/laneService.test.ts-3055-3057 (1)

3055-3057: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid fixed sleeps in these concurrency tests.

Line 3056 and Line 3109 rely on timing delays, which can make CI flaky. Prefer explicit latches/promises for “delete started” / “create reached worktree add” checkpoints.

Also applies to: 3108-3111

🤖 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/lanes/laneService.test.ts` around lines 3055 -
3057, Replace the fixed setTimeout-based waits with explicit synchronization
promises: instrument the test to create a Promise (or latch) that is resolved
from inside the code-path where the delete actually begins (e.g., in the
stubbed/mocked implementation invoked by service.delete) and await that Promise
before asserting service.hasRunningDelete(); do the same for the "create reached
worktree add" checkpoint by resolving a Promise from the stub/mocked
worktree.add (or the helper invoked by the create flow) instead of using
setTimeout. Specifically, modify the test around service.delete and
service.hasRunningDelete to await a "deleteStarted" promise resolved by the
delete-start stub, and update the create/worktree add test to await a
"createAtWorktreeAdd" promise resolved by the worktree add mock, removing both
setTimeout usages.
apps/desktop/src/main/services/prs/prService.ts-3563-3572 (1)

3563-3572: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve Linear reference in the final body-enrichment retry path.

If the earlier adopt-existing body PATCH fails, this pass only reapplies the ADE footer, so closeLinearIssueOnMerge intent can be dropped even when this PATCH succeeds.

💡 Proposed fix
-    const currentBody = typeof pr?.body === "string" ? pr.body : prBody;
-    const enrichedBody = ensureAdeDeeplinkFooter(currentBody, {
+    const currentBody = typeof pr?.body === "string" ? pr.body : prBody;
+    const closeOnMerge = args.closeLinearIssueOnMerge === true;
+    const linearAdjustedCurrentBody = lane.linearIssue
+      ? ensureLinearPrReference(
+          currentBody,
+          lane.linearIssue,
+          closeOnMerge,
+          closeOnMerge ? { preserveExisting: false } : undefined,
+        )
+      : currentBody;
+    const enrichedBody = ensureAdeDeeplinkFooter(linearAdjustedCurrentBody, {
       repoOwner: repo.owner,
       repoName: repo.name,
       branch: headBranch,
       prNumber,
     });
🤖 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/prs/prService.ts` around lines 3563 - 3572,
The final retry that recomputes enrichedBody must preserve the Linear-close
intent/reference from the previously adopted PR body so it isn't lost when the
intermediate adopt-existing PATCH fails; update the currentBody selection logic
used with ensureAdeDeeplinkFooter to prefer or merge in the original prBody's
Linear metadata/intent (e.g., keep the closeLinearIssueOnMerge flag or Linear
issue reference embedded in prBody) rather than using only pr?.body or a rebuilt
prBody, so ensureAdeDeeplinkFooter receives a body that still contains the
Linear reference.
apps/desktop/src/main/services/runtime/processRegistryService.ts-92-104 (1)

92-104: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard heartbeat writes when the service is not started.

Line 92 and Line 107 can write runtime_processes even after stop()/before start(), which can leave stale self rows and false-positive liveness.

💡 Suggested fix
       heartbeatTimer = setInterval(() => {
+        if (!started) return;
         try {
           writeOwnRow(null);
         } catch (error) {
           logger.warn("process_registry.heartbeat_failed", {
             pid,
             role,
             error: error instanceof Error ? error.message : String(error),
           });
         }
       }, heartbeatIntervalMs);
       heartbeatTimer.unref?.();
     },

     /** Force a heartbeat write now — useful right before a long blocking op. */
     heartbeat(): void {
+      if (!started) return;
       try {
         writeOwnRow(null);
       } catch (error) {
         logger.warn("process_registry.heartbeat_failed", {
           pid,
           role,
           error: error instanceof Error ? error.message : String(error),
         });
       }
     },

Also applies to: 107-117

🤖 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/runtime/processRegistryService.ts` around
lines 92 - 104, The heartbeat and other periodic writes call writeOwnRow even
after stop() or before start(), causing stale rows; add and check a
service-running flag (e.g., this.started or isRunning) inside the process
registry instance and only call writeOwnRow from the heartbeatTimer callback and
the other periodic/initial write paths if that flag is true, set the flag to
true in start() and false in stop(), and ensure heartbeatTimer is
cleared/unref'd in stop() so no writes occur after stop; update references to
heartbeatTimer, writeOwnRow, start(), and stop() to use this guard.
apps/desktop/src/renderer/components/app/CrossRepoPrBanner.tsx-150-169 (1)

150-169: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Offset this banner to avoid collision with the clipboard deeplink banner.

This banner uses the same fixed top-center anchor as ClipboardDeeplinkBanner, so it can be partially/fully obscured when both are active.

💡 Proposed fix
-        top: 14,
+        top: 56,
🤖 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/app/CrossRepoPrBanner.tsx` around lines
150 - 169, The CrossRepoPrBanner's fixed top-center positioning collides with
ClipboardDeeplinkBanner; modify CrossRepoPrBanner (the JSX div with style
object) to account for the other banner by applying a dynamic top offset instead
of a hardcoded top:14. Implement a small utility/prop or read a shared layout
flag (e.g., isClipboardDeeplinkVisible) and set top to a larger value or use
calc() (e.g., top: isClipboardDeeplinkVisible ? 56 : 14 or top: "calc(14px +
var(--clipboard-banner-height))") so the banners stack without overlap; update
the component to accept/derive that flag from parent state or a shared context
used by ClipboardDeeplinkBanner.
apps/desktop/src/renderer/components/app/CrossRepoPrBanner.tsx-130-144 (1)

130-144: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Do not dismiss on failed project switch.

switchProject always calls dismiss(), even when openRepo fails, which removes the quick retry affordance.

💡 Proposed fix
   const switchProject = async () => {
     if (!matchingProject) {
       dismiss();
       return;
     }
     const openRepo = window.ade?.project?.openRepo;
-    if (typeof openRepo === "function") {
-      try {
-        await openRepo({ rootPath: matchingProject.rootPath });
-      } catch {
-        // ignore; user can retry from the recent-projects UI
-      }
-    }
-    dismiss();
+    if (typeof openRepo !== "function") {
+      dismiss();
+      return;
+    }
+    try {
+      await openRepo({ rootPath: matchingProject.rootPath });
+      dismiss();
+    } catch {
+      // keep banner visible so the user can retry
+    }
   };
🤖 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/app/CrossRepoPrBanner.tsx` around lines
130 - 144, switchProject currently always calls dismiss(), which removes the
retry affordance even when openRepo fails; change the control flow so dismiss()
is only invoked on the no-matchingProject path or after a successful openRepo
call. Concretely, keep the early return when !matchingProject, then if typeof
window.ade?.project?.openRepo === "function" await openRepo(...) inside try and
call dismiss() in the try (not after the try/catch); in the catch do not call
dismiss so the UI can retry; if openRepo is not a function keep the original
dismiss() path. Ensure references to switchProject, matchingProject, openRepo,
and dismiss are updated accordingly.
apps/desktop/src/renderer/components/app/InboundDeeplinkModal.tsx-73-81 (1)

73-81: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear stale preflight state in the branch-only path.

At Line 73, the branch-only early return sets error but does not clear a previously loaded preflight, so reused modal instances can render stale PR metadata from an older target.

💡 Proposed fix
     if (!target.prNumber) {
+      setPreflight(null);
       // Branch-only deeplinks without a PR number aren't supported by the
       // existing preflight (which is PR-centric). Surface a clear message
       // so the receiver knows what to do.
       setError(
         "This deeplink references a branch without an associated PR. Open the PR in GitHub first, or share the PR deeplink instead.",
       );
       setLoading(false);
       return;
     }
🤖 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/app/InboundDeeplinkModal.tsx` around
lines 73 - 81, The branch-only early-return in InboundDeeplinkModal sets error
and loading but does not clear any previously loaded preflight state, causing
stale PR metadata to persist; update that branch-only path to also clear the
preflight state (e.g. call the preflight setter such as setPreflight(null) or
setPreflight(undefined)) before setLoading(false) and return so reused modal
instances don't render old preflight data.
apps/ios/ADE/Services/SyncService.swift-7537-7543 (1)

7537-7543: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard openDeeplink when URL is missing to avoid silent command drops.

sendRemoteCommand still sends deeplinks.open with empty args if payload["url"] is absent/blank, and failures are swallowed later. Early-return here to avoid dispatching an invalid command.

💡 Suggested fix
     case .openDeeplink:
       // Desktop `deeplinks.open` expects a `url` arg — the same `ade://...`
       // string the user tapped on iOS. We pass it through verbatim so the
       // host can reuse its own router.
-      if let url = payload["url"] as? String, !url.isEmpty {
-        args["url"] = url
-      }
+      guard let url = (payload["url"] as? String)?
+        .trimmingCharacters(in: .whitespacesAndNewlines),
+        !url.isEmpty else {
+        return
+      }
+      args["url"] = url
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Services/SyncService.swift` around lines 7537 - 7543, In the
.openDeeplink branch inside SyncService (where sendRemoteCommand is prepared),
guard that payload["url"] is a non-empty String and early-return (or skip
dispatch) if it's missing/blank so you do not call sendRemoteCommand with empty
args; update the code around the case .openDeeplink handling to validate url
up-front (the existing if let url = payload["url"] as? String, !url.isEmpty) and
otherwise return/continue before constructing args and calling sendRemoteCommand
so invalid commands are not dispatched or silently dropped.
apps/ios/ADE/Views/Deeplinks/SendToMacCard.swift-38-43 (1)

38-43: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve full branch paths in repo deeplink parsing.

Line 43 uses only parts[3] for branch, so branch names like feature/foo are truncated to feature in the UI summary.

Suggested fix
-    case "repo":
-      if parts.count >= 4,
-         parts[2].lowercased() == "branch",
-         !parts[0].isEmpty,
-         !parts[1].isEmpty,
-         !parts[3].isEmpty {
-        self.kind = .repoBranch(owner: parts[0], repo: parts[1], branch: parts[3])
+    case "repo":
+      if parts.count >= 4,
+         parts[2].lowercased() == "branch",
+         !parts[0].isEmpty,
+         !parts[1].isEmpty {
+        let branch = parts.dropFirst(3).joined(separator: "/")
+        if !branch.isEmpty {
+          self.kind = .repoBranch(owner: parts[0], repo: parts[1], branch: branch)
+        } else {
+          self.kind = .other
+        }
       } else {
         self.kind = .other
       }
🤖 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/ios/ADE/Views/Deeplinks/SendToMacCard.swift` around lines 38 - 43, The
deeplink parsing currently sets branch to only parts[3], which drops any slashes
in branch names (e.g., feature/foo); in SendToMacCard.swift where you assign
self.kind = .repoBranch(owner: parts[0], repo: parts[1], branch: parts[3]),
change the assignment to join all path segments from index 3 through the end
(e.g., join parts[3...] with "/" ) so the full branch path is preserved in the
UI summary while keeping the existing parts.count >= 4 validation.
🧹 Nitpick comments (4)
apps/ade-cli/src/tuiClient/__tests__/deeplinkKeybind.test.ts (1)

14-14: ⚡ Quick win

Use camelCase for the local UUID constant.

Line 14 uses LANE_UUID; rename it to laneUuid to match TS naming conventions in this repo.

Suggested rename
-const LANE_UUID = "550e8400-e29b-41d4-a716-446655440000";
+const laneUuid = "550e8400-e29b-41d4-a716-446655440000";
...
-    const row: DeeplinkRow = { kind: "lane", lane: { id: LANE_UUID } };
-    expect(buildDeeplinkForRow(row)).toBe(`ade://lane/${LANE_UUID}`);
+    const row: DeeplinkRow = { kind: "lane", lane: { id: laneUuid } };
+    expect(buildDeeplinkForRow(row)).toBe(`ade://lane/${laneUuid}`);
...
-      { kind: "lane", lane: { id: LANE_UUID } },
+      { kind: "lane", lane: { id: laneUuid } },
...
-    expect(recorded.url).toBe(`ade://lane/${LANE_UUID}`);
+    expect(recorded.url).toBe(`ade://lane/${laneUuid}`);

As per coding guidelines **/*.{ts,tsx,js,jsx}: Use camelCase for variable names in TypeScript/JavaScript files.

🤖 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/tuiClient/__tests__/deeplinkKeybind.test.ts` at line 14, The
constant LANE_UUID in deeplinkKeybind.test.ts should be renamed to camelCase
(laneUuid); update the declaration and all usages of LANE_UUID within the file
(e.g., any tests referencing LANE_UUID) to laneUuid so it follows the repo's
TypeScript naming conventions and avoids lint failures.
apps/ade-cli/src/tuiClient/aggregate.ts (1)

338-347: ⚡ Quick win

Rename runtime suppression constant to camelCase.

Use camelCase for this variable name (e.g., suppressedRuntimeActivities) to match project TS naming rules.

Suggested rename
-const SUPPRESSED_RUNTIME_ACTIVITIES = new Set([
+const suppressedRuntimeActivities = new Set([
...
-    if (SUPPRESSED_RUNTIME_ACTIVITIES.has(activity)) return null;
+    if (suppressedRuntimeActivities.has(activity)) return null;

As per coding guidelines **/*.{ts,tsx,js,jsx}: Use camelCase for variable names in TypeScript/JavaScript files.

Also applies to: 352-352

🤖 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/tuiClient/aggregate.ts` around lines 338 - 347, Rename the
constant SUPPRESSED_RUNTIME_ACTIVITIES to camelCase suppressedRuntimeActivities
and update every usage in this module (and any re-exports/imports) to the new
identifier; adjust any references such as where it's read/iterated (e.g., in
aggregate.ts functions that check or iterate this set) to use
suppressedRuntimeActivities and ensure TypeScript imports/exports remain
consistent so the symbol compiles under the new name.
apps/desktop/src/main/services/sessions/sessionService.ts (1)

492-543: 💤 Low value

Clarify behavior when liveOwnerPids is provided as an empty Set vs. undefined.

The current logic treats both undefined and an empty Set identically—both result in ownerGuardSql = " and owner_pid is null", reconciling only sessions without an owner.

This is likely intentional (conservative default), but consider whether an explicitly empty liveOwnerPids Set (meaning "no processes are alive") should instead reconcile all sessions regardless of owner_pid. If current behavior is correct, a brief doc comment clarifying the distinction would help future maintainers.

🤖 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/sessions/sessionService.ts` around lines 492 -
543, The function reconcileStaleRunningSessions conflates undefined
liveOwnerPids (meaning "only ownerless sessions") and an explicitly empty Set
(which should mean "no live owners, so reconcile all sessions"); change the
owner guard logic in reconcileStaleRunningSessions so: if liveOwnerPids ===
undefined keep ownerGuardSql = " and owner_pid is null"; else build ownerParams
= normalized list from liveOwnerPids and if ownerParams.length === 0 set
ownerGuardSql = "" (no owner constraint) so all running sessions are reconciled;
otherwise keep ownerGuardSql = ` and (owner_pid is null or owner_pid not in
(...))`; ensure params = [...normalizedExcludedToolTypes, ...ownerParams] and
both the select and update use the same whereSql and params order.
apps/desktop/src/renderer/components/chat/AgentChatPane.tsx (1)

157-160: ⚡ Quick win

Use camelCase for new toolbar class constants.

Please rename CHAT_TOOLBAR_ACTION_BASE and CHAT_TOOLBAR_ACTION_IDLE to camelCase to match repo naming rules for TS/JS variables.

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Use camelCase for variable names in TypeScript/JavaScript files.

🤖 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/chat/AgentChatPane.tsx` around lines 157
- 160, Rename the toolbar class constants CHAT_TOOLBAR_ACTION_BASE and
CHAT_TOOLBAR_ACTION_IDLE to camelCase (e.g. chatToolbarActionBase and
chatToolbarActionIdle) throughout AgentChatPane and any other files that import
or reference them; update their declarations in AgentChatPane.tsx and replace
all usages (including JSX className concatenations, exports, and any tests) to
the new camelCase identifiers to satisfy the TS/JS naming guidelines while
preserving the string values unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a8bfad10-a340-48cf-be15-c3873e98f2f2

📥 Commits

Reviewing files that changed from the base of the PR and between a17e0d0 and cdb07c4.

⛔ Files ignored due to path filters (7)
  • apps/ios/ADE.xcodeproj/project.pbxproj is excluded by !**/*.xcodeproj/project.pbxproj
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/features/ade-code/README.md is excluded by !docs/**
  • docs/features/deeplinks/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/ios-companion.md is excluded by !docs/**
  • docs/features/terminals-and-sessions/README.md is excluded by !docs/**
  • goal.md is excluded by !*.md
📒 Files selected for processing (142)
  • apps/ade-cli/README.md
  • apps/ade-cli/src/adeRpcServer.test.ts
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/ade-cli/src/bootstrap.test.ts
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/commands/deeplinks.test.ts
  • apps/ade-cli/src/commands/deeplinks.ts
  • apps/ade-cli/src/eventBuffer.ts
  • apps/ade-cli/src/lib/clipboard.ts
  • apps/ade-cli/src/multiProjectRpcServer.ts
  • apps/ade-cli/src/services/sync/syncHostService.ts
  • apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
  • apps/ade-cli/src/services/sync/syncService.ts
  • apps/ade-cli/src/tuiClient/__tests__/ChatView.test.tsx
  • apps/ade-cli/src/tuiClient/__tests__/Drawer.test.tsx
  • apps/ade-cli/src/tuiClient/__tests__/GridMiniMap.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/adeApi.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/aggregate.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/deeplinkKeybind.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/format.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/hitTestRegistry.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/multiChatLayout.test.ts
  • apps/ade-cli/src/tuiClient/adeApi.ts
  • apps/ade-cli/src/tuiClient/aggregate.ts
  • apps/ade-cli/src/tuiClient/app.tsx
  • apps/ade-cli/src/tuiClient/cli.tsx
  • apps/ade-cli/src/tuiClient/components/AddChatMode.tsx
  • apps/ade-cli/src/tuiClient/components/ChatView.tsx
  • apps/ade-cli/src/tuiClient/components/Drawer.tsx
  • apps/ade-cli/src/tuiClient/components/FooterControls.tsx
  • apps/ade-cli/src/tuiClient/components/GridMiniMap.tsx
  • apps/ade-cli/src/tuiClient/components/MultiChatGrid.tsx
  • apps/ade-cli/src/tuiClient/components/RightPane.tsx
  • apps/ade-cli/src/tuiClient/deeplinkRow.ts
  • apps/ade-cli/src/tuiClient/format.ts
  • apps/ade-cli/src/tuiClient/hitTestRegistry.ts
  • apps/ade-cli/src/tuiClient/keybindings/index.ts
  • apps/ade-cli/src/tuiClient/multiChatLayout.ts
  • apps/desktop/resources/agent-skills/ade-deeplinks/SKILL.md
  • apps/desktop/src/main/main.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/cto/linearClient.ts
  • apps/desktop/src/main/services/cto/linearLaneCardService.test.ts
  • apps/desktop/src/main/services/cto/linearLaneCardService.ts
  • apps/desktop/src/main/services/deeplinks/protocolHandler.test.ts
  • apps/desktop/src/main/services/deeplinks/protocolHandler.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/ipc/runtimeBridge.ts
  • apps/desktop/src/main/services/lanes/laneService.test.ts
  • apps/desktop/src/main/services/lanes/laneService.ts
  • apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.test.ts
  • apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.ts
  • apps/desktop/src/main/services/prs/prService.test.ts
  • apps/desktop/src/main/services/prs/prService.ts
  • apps/desktop/src/main/services/pty/ptyService.test.ts
  • apps/desktop/src/main/services/pty/ptyService.ts
  • apps/desktop/src/main/services/remoteRuntime/remoteConnectionPool.test.ts
  • apps/desktop/src/main/services/remoteRuntime/remoteConnectionPool.ts
  • apps/desktop/src/main/services/review/reviewContextBuilder.test.ts
  • apps/desktop/src/main/services/review/reviewService.test.ts
  • apps/desktop/src/main/services/review/reviewService.ts
  • apps/desktop/src/main/services/review/reviewTargetMaterializer.test.ts
  • apps/desktop/src/main/services/runtime/processRegistryService.test.ts
  • apps/desktop/src/main/services/runtime/processRegistryService.ts
  • apps/desktop/src/main/services/sessions/sessionService.test.ts
  • apps/desktop/src/main/services/sessions/sessionService.ts
  • apps/desktop/src/main/services/state/kvDb.ts
  • apps/desktop/src/preload/global.d.ts
  • apps/desktop/src/preload/preload.test.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/renderer/components/app/App.tsx
  • apps/desktop/src/renderer/components/app/AppShell.tsx
  • apps/desktop/src/renderer/components/app/ClipboardDeeplinkBanner.tsx
  • apps/desktop/src/renderer/components/app/CrossRepoPrBanner.tsx
  • apps/desktop/src/renderer/components/app/InboundDeeplinkModal.tsx
  • apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx
  • apps/desktop/src/renderer/components/app/TopBar.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.test.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.submit.test.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/chat/ChatComposerShell.tsx
  • apps/desktop/src/renderer/components/chat/ChatGitToolbar.test.tsx
  • apps/desktop/src/renderer/components/chat/ChatGitToolbar.tsx
  • apps/desktop/src/renderer/components/chat/ChatSubagentsPanel.tsx
  • apps/desktop/src/renderer/components/chat/ChatTerminalDrawer.tsx
  • apps/desktop/src/renderer/components/chat/ChatWorkLogBlock.tsx
  • apps/desktop/src/renderer/components/chat/chatSurfaceTheme.ts
  • apps/desktop/src/renderer/components/chat/chatTranscriptRows.test.ts
  • apps/desktop/src/renderer/components/chat/chatTranscriptRows.ts
  • apps/desktop/src/renderer/components/chat/codex/CodexOpenInCliButton.tsx
  • apps/desktop/src/renderer/components/files/FilesPage.tsx
  • apps/desktop/src/renderer/components/lanes/LaneContextMenu.tsx
  • apps/desktop/src/renderer/components/lanes/LaneWorkPane.tsx
  • apps/desktop/src/renderer/components/lanes/LanesPage.test.ts
  • apps/desktop/src/renderer/components/lanes/LanesPage.tsx
  • apps/desktop/src/renderer/components/lanes/laneDesignTokens.ts
  • apps/desktop/src/renderer/components/lanes/laneUtils.ts
  • apps/desktop/src/renderer/components/lanes/useLaneWorkSessions.test.ts
  • apps/desktop/src/renderer/components/lanes/useLaneWorkSessions.ts
  • apps/desktop/src/renderer/components/onboarding/HelpMenu.tsx
  • apps/desktop/src/renderer/components/review/ReviewPage.test.tsx
  • apps/desktop/src/renderer/components/review/ReviewPage.tsx
  • apps/desktop/src/renderer/components/review/reviewTypes.ts
  • apps/desktop/src/renderer/components/settings/ChatAppearancePreview.test.tsx
  • apps/desktop/src/renderer/components/terminals/SessionCard.tsx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.test.tsx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.tsx
  • apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx
  • apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx
  • apps/desktop/src/renderer/components/terminals/useWorkSessions.ts
  • apps/desktop/src/renderer/components/ui/TabBackground.tsx
  • apps/desktop/src/renderer/components/usage/HeaderUsageControl.tsx
  • apps/desktop/src/renderer/index.css
  • apps/desktop/src/renderer/onboarding/tours/laneWorkPaneHighlightsTour.ts
  • apps/desktop/src/renderer/onboarding/tours/laneWorkPaneTour.ts
  • apps/desktop/src/shared/adeCliGuidance.ts
  • apps/desktop/src/shared/adeDeeplinkFooter.test.ts
  • apps/desktop/src/shared/adeDeeplinkFooter.ts
  • apps/desktop/src/shared/deeplinks.test.ts
  • apps/desktop/src/shared/deeplinks.ts
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/shared/types/core.ts
  • apps/desktop/src/shared/types/remoteRuntime.ts
  • apps/desktop/src/shared/types/review.ts
  • apps/desktop/src/shared/types/sessions.ts
  • apps/desktop/src/shared/types/sync.ts
  • apps/ios/ADE/App/ADEApp.swift
  • apps/ios/ADE/App/DeepLinkRouter.swift
  • apps/ios/ADE/Resources/DatabaseBootstrap.sql
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/Deeplinks/SendToMacCard.swift
  • apps/web/api/open.ts
  • apps/web/api/tsconfig.json
  • apps/web/src/app/SiteRoutes.tsx
  • apps/web/src/app/pages/OpenPage.tsx
  • apps/web/vercel.json
💤 Files with no reviewable changes (7)
  • apps/desktop/src/shared/types/review.ts
  • apps/desktop/src/renderer/components/lanes/laneUtils.ts
  • apps/desktop/src/renderer/components/review/reviewTypes.ts
  • apps/desktop/src/renderer/components/files/FilesPage.tsx
  • apps/desktop/src/main/services/review/reviewTargetMaterializer.test.ts
  • apps/desktop/src/main/services/review/reviewContextBuilder.test.ts
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx

Comment thread apps/ade-cli/src/adeRpcServer.ts
Comment thread apps/ade-cli/src/bootstrap.ts
Comment thread apps/ade-cli/src/commands/deeplinks.ts Outdated
Comment thread apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
Comment thread apps/ade-cli/src/tuiClient/aggregate.ts Outdated
Comment thread apps/desktop/src/main/services/pty/ptyService.ts
Comment thread apps/desktop/src/shared/adeDeeplinkFooter.ts
Comment thread apps/ios/ADE/App/DeepLinkRouter.swift
Comment thread apps/ios/ADE/Views/Deeplinks/SendToMacCard.swift
Comment thread apps/web/src/app/pages/OpenPage.tsx
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 21, 2026

@copilot review but do not make fixes

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 21, 2026

@copilot review but do not make fixes

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

Caution

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

⚠️ Outside diff range comments (2)
apps/desktop/src/main/services/ipc/runtimeBridge.ts (1)

264-283: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate forwarded runtime events by requestKey, not only bindingKey.

After this change, subscriptions are keyed by requestKey, but forwarded events are still validated only against bindingKey. A late callback from a prior category subscription can still pass the guard and emit stale events.

Suggested fix
-function sendRuntimeEvent(
-  sender: WebContents,
-  bindingKey: string,
-  event: RemoteRuntimeBufferedEvent,
-): void => {
+function sendRuntimeEvent(
+  sender: WebContents,
+  bindingKey: string,
+  requestKey: string,
+  event: RemoteRuntimeBufferedEvent,
+): void => {
   const existing = runtimeEventSubscriptions.get(sender.id);
-  if (!existing || existing.bindingKey !== bindingKey || sender.isDestroyed())
+  if (
+    !existing ||
+    existing.bindingKey !== bindingKey ||
+    existing.requestKey !== requestKey ||
+    sender.isDestroyed()
+  )
     return;
   const payload: RemoteRuntimeEventNotificationPayload = {
     bindingKey,
     event,
   };
@@
-    void subscribe(
-      (event) => sendRuntimeEvent(sender, bindingKey, event),
+    void subscribe(
+      (event) => sendRuntimeEvent(sender, bindingKey, requestKey, event),
       onEnded,
     )

Also applies to: 286-300

🤖 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/ipc/runtimeBridge.ts` around lines 264 - 283,
ensureRuntimeEventSubscription registers subscriptions keyed by requestKey but
the forwarded events are only validated against bindingKey, allowing stale
callbacks to emit; update the subscribe callback and the onEnded/cleanup logic
to verify both requestKey and bindingKey before calling sendRuntimeEvent and
before deleting entries. Specifically, in ensureRuntimeEventSubscription change
the subscribe success callback (the first arg to RuntimeEventSubscribe) to
capture the local requestKey and bindingKey and, on each event, fetch
runtimeEventSubscriptions.get(sender.id) and only call sendRuntimeEvent(sender,
bindingKey, event) if current?.requestKey === requestKey && current?.bindingKey
=== bindingKey; apply the same dual-key guard in the corresponding subscription
code path around lines 286-300 and ensure
cleanupRuntimeEventSubscription/onEnded use the requestKey match as well.
apps/ade-cli/src/tuiClient/app.tsx (1)

2539-2545: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard the transcript mirror until the new session is actually loaded.

When activeSessionId flips, events still holds the previous session until refreshState() hydrates the new one. This effect immediately copies that stale array into eventsBySessionId[activeSessionId], so a newly focused tile can render the wrong transcript and keep it if the reload fails.

Suggested fix
 useEffect(() => {
-  if (!activeSessionId) return;
+  if (!activeSessionId || loadedSessionIdRef.current !== activeSessionId) return;
   setEventsBySessionId((prev) => {
     if (prev[activeSessionId] === events) return prev;
     return { ...prev, [activeSessionId]: events };
   });
 }, [activeSessionId, events]);
🤖 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/tuiClient/app.tsx` around lines 2539 - 2545, The effect is
copying stale `events` into `eventsBySessionId` when `activeSessionId` flips;
add a guard so we only mirror `events` into `eventsBySessionId` if those
`events` were actually hydrated for the current session. Implement this by
introducing a ref (e.g., lastHydratedSessionRef) that you set when the
refresh/hydration finishes (or when the source that provides `events` confirms
it's for a given session), then in the existing useEffect (the one referencing
activeSessionId, events, setEventsBySessionId) return early unless
lastHydratedSessionRef.current === activeSessionId; keep the existing identity
check (prev[activeSessionId] === events) otherwise so you only update when the
events belong to the currently loaded session.
🧹 Nitpick comments (2)
apps/ios/ADE/Services/SyncService.swift (1)

7541-7546: ⚡ Quick win

Allowlist deeplink formats before forwarding to desktop.

This currently forwards any non-empty string. Restrict to supported deeplink forms (ade://... and https://ade.app/open...) to harden the remote-command path.

♻️ Suggested patch
       guard let url = (payload["url"] as? String)?
         .trimmingCharacters(in: .whitespacesAndNewlines),
-        !url.isEmpty else {
+        !url.isEmpty,
+        let components = URLComponents(string: url),
+        (
+          components.scheme?.lowercased() == "ade"
+            || (
+              components.scheme?.lowercased() == "https"
+                && components.host?.lowercased() == "ade.app"
+                && components.path == "/open"
+            )
+        )
+      else {
         return
       }
       args["url"] = url
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Services/SyncService.swift` around lines 7541 - 7546, Currently
any non-empty payload["url"] is forwarded; restrict to approved deeplink formats
by parsing the trimmed url (the local variable url derived from payload["url"])
using URLComponents and validating either scheme == "ade" OR (scheme == "https"
&& host == "ade.app" && path.starts(with: "/open")). If validation fails, return
early instead of setting args["url"]. Update the guard/validation around the url
extraction (the block that assigns args["url"] = url) to perform this allowlist
check before forwarding.
apps/ade-cli/src/adeRpcServer.test.ts (1)

1288-1300: ⚡ Quick win

Expand external PTY gating coverage to match the test intent.

On Line 1288, the test says “methods” but only asserts pty.create is blocked. Add assertions for pty.sendToSession, pty.write, pty.resize, pty.dispose, and pty.list so permission regressions on any direct PTY RPC are caught.

Suggested test shape
 it("hides direct PTY RPC methods from external sessions", async () => {
   const { runtime } = createRuntime();
   const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
   await initialize(handler, { role: "external" });

-  await expect(handler({
-    jsonrpc: "2.0",
-    id: 2,
-    method: "pty.create",
-    params: { args: { laneId: "lane-1", title: "Claude", cols: 120, rows: 40 } },
-  })).rejects.toMatchObject({ code: JsonRpcErrorCode.methodNotFound });
-  expect(runtime.ptyService.create).not.toHaveBeenCalled();
+  const blocked = [
+    {
+      method: "pty.create",
+      params: { args: { laneId: "lane-1", title: "Claude", cols: 120, rows: 40 } },
+      spy: runtime.ptyService.create,
+    },
+    {
+      method: "pty.sendToSession",
+      params: { args: { sessionId: "session-1", text: "continue" } },
+      spy: runtime.ptyService.sendToSession,
+    },
+    { method: "pty.write", params: { args: { ptyId: "pty-1", data: "x" } }, spy: runtime.ptyService.write },
+    { method: "pty.resize", params: { args: { ptyId: "pty-1", cols: 100, rows: 30 } }, spy: runtime.ptyService.resize },
+    { method: "pty.dispose", params: { args: { ptyId: "pty-1", sessionId: "session-1" } }, spy: runtime.ptyService.dispose },
+    { method: "pty.list", params: { args: { laneId: "lane-1", limit: 20 } }, spy: runtime.ptyService.list },
+  ] as const;
+
+  for (const [index, rpc] of blocked.entries()) {
+    await expect(handler({
+      jsonrpc: "2.0",
+      id: 2 + index,
+      method: rpc.method,
+      params: rpc.params,
+    })).rejects.toMatchObject({ code: JsonRpcErrorCode.methodNotFound });
+    expect(rpc.spy).not.toHaveBeenCalled();
+  }
 });
🤖 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/adeRpcServer.test.ts` around lines 1288 - 1300, The test
currently only checks that "pty.create" is blocked for external sessions; extend
it to assert that "pty.sendToSession", "pty.write", "pty.resize", "pty.dispose",
and "pty.list" are also rejected with JsonRpcErrorCode.methodNotFound by
invoking handler with each method name (using the same jsonrpc/id/params shape)
and expecting rejects.toMatchObject({ code: JsonRpcErrorCode.methodNotFound });
also add ensures that runtime.ptyService.sendToSession, .write, .resize,
.dispose, and .list have not been called (similar to the existing expect for
runtime.ptyService.create) to fully cover direct PTY gating in
createAdeRpcRequestHandler/handler.
🤖 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/ade-cli/src/tuiClient/app.tsx`:
- Around line 7976-7979: The Ctrl+G handler for starting add-mode is being
shadowed by an earlier Ctrl+G branch that calls editPromptInExternalEditor when
chat input is focused, so startAddMode() never runs; to fix, either choose a
non-colliding chord for the add-mode handler (replace isCtrlInput(input, key,
"g") with another key/chord) or make precedence explicit by moving the pane ===
"chat" && isCtrlInput(input, key, "g") check above the
editPromptInExternalEditor branch or add a disambiguating condition (e.g.,
ensure the editPromptInExternalEditor path checks the specific focus state
before handling Ctrl+G); update references to startAddMode, isCtrlInput,
editPromptInExternalEditor, pane, input, and key accordingly.

In `@apps/desktop/src/main/main.ts`:
- Around line 6005-6028: The current draining of pendingAppNavigationRequests
calls openAdeWindow() during bootstrap and can race with later creation of
initialWindow, causing two windows; update the logic so
dispatchAppNavigationRequest no longer directly calls openAdeWindow during early
bootstrap but instead awaits or reuses the shared initial-window promise used by
the bootstrap (or delay draining until after initialWindow is created).
Concretely, modify dispatchAppNavigationRequest to obtain the app's
single-initial-window via the existing shared promise or getter (referencing
dispatchAppNavigationRequest, openAdeWindow, pendingAppNavigationRequests, and
initialWindow) and use that window result instead of invoking openAdeWindow
directly, or move the loop that splices pendingAppNavigationRequests to run
after initialWindow has been established during bootstrap.

---

Outside diff comments:
In `@apps/ade-cli/src/tuiClient/app.tsx`:
- Around line 2539-2545: The effect is copying stale `events` into
`eventsBySessionId` when `activeSessionId` flips; add a guard so we only mirror
`events` into `eventsBySessionId` if those `events` were actually hydrated for
the current session. Implement this by introducing a ref (e.g.,
lastHydratedSessionRef) that you set when the refresh/hydration finishes (or
when the source that provides `events` confirms it's for a given session), then
in the existing useEffect (the one referencing activeSessionId, events,
setEventsBySessionId) return early unless lastHydratedSessionRef.current ===
activeSessionId; keep the existing identity check (prev[activeSessionId] ===
events) otherwise so you only update when the events belong to the currently
loaded session.

In `@apps/desktop/src/main/services/ipc/runtimeBridge.ts`:
- Around line 264-283: ensureRuntimeEventSubscription registers subscriptions
keyed by requestKey but the forwarded events are only validated against
bindingKey, allowing stale callbacks to emit; update the subscribe callback and
the onEnded/cleanup logic to verify both requestKey and bindingKey before
calling sendRuntimeEvent and before deleting entries. Specifically, in
ensureRuntimeEventSubscription change the subscribe success callback (the first
arg to RuntimeEventSubscribe) to capture the local requestKey and bindingKey
and, on each event, fetch runtimeEventSubscriptions.get(sender.id) and only call
sendRuntimeEvent(sender, bindingKey, event) if current?.requestKey ===
requestKey && current?.bindingKey === bindingKey; apply the same dual-key guard
in the corresponding subscription code path around lines 286-300 and ensure
cleanupRuntimeEventSubscription/onEnded use the requestKey match as well.

---

Nitpick comments:
In `@apps/ade-cli/src/adeRpcServer.test.ts`:
- Around line 1288-1300: The test currently only checks that "pty.create" is
blocked for external sessions; extend it to assert that "pty.sendToSession",
"pty.write", "pty.resize", "pty.dispose", and "pty.list" are also rejected with
JsonRpcErrorCode.methodNotFound by invoking handler with each method name (using
the same jsonrpc/id/params shape) and expecting rejects.toMatchObject({ code:
JsonRpcErrorCode.methodNotFound }); also add ensures that
runtime.ptyService.sendToSession, .write, .resize, .dispose, and .list have not
been called (similar to the existing expect for runtime.ptyService.create) to
fully cover direct PTY gating in createAdeRpcRequestHandler/handler.

In `@apps/ios/ADE/Services/SyncService.swift`:
- Around line 7541-7546: Currently any non-empty payload["url"] is forwarded;
restrict to approved deeplink formats by parsing the trimmed url (the local
variable url derived from payload["url"]) using URLComponents and validating
either scheme == "ade" OR (scheme == "https" && host == "ade.app" &&
path.starts(with: "/open")). If validation fails, return early instead of
setting args["url"]. Update the guard/validation around the url extraction (the
block that assigns args["url"] = url) to perform this allowlist check before
forwarding.
🪄 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: 563acdab-8e57-4d3b-9ee3-da40b21048be

📥 Commits

Reviewing files that changed from the base of the PR and between cdb07c4 and 7588392.

📒 Files selected for processing (33)
  • apps/ade-cli/src/adeRpcServer.test.ts
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/commands/deeplinks.ts
  • apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
  • apps/ade-cli/src/tuiClient/__tests__/deeplinkKeybind.test.ts
  • apps/ade-cli/src/tuiClient/aggregate.ts
  • apps/ade-cli/src/tuiClient/app.tsx
  • apps/ade-cli/src/tuiClient/components/Drawer.tsx
  • apps/ade-cli/src/tuiClient/components/GridMiniMap.tsx
  • apps/ade-cli/src/tuiClient/deeplinkRow.ts
  • apps/ade-cli/src/tuiClient/multiChatLayout.ts
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/chat/agentChatService.ts
  • apps/desktop/src/main/services/deeplinks/protocolHandler.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/ipc/runtimeBridge.ts
  • apps/desktop/src/main/services/pty/ptyService.test.ts
  • apps/desktop/src/main/services/pty/ptyService.ts
  • apps/desktop/src/main/services/runtime/processRegistryService.test.ts
  • apps/desktop/src/main/services/runtime/processRegistryService.ts
  • apps/desktop/src/main/services/sessions/sessionService.test.ts
  • apps/desktop/src/main/services/sessions/sessionService.ts
  • apps/desktop/src/main/services/state/kvDb.ts
  • apps/desktop/src/renderer/components/app/ClipboardDeeplinkBanner.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/shared/adeDeeplinkFooter.ts
  • apps/desktop/src/shared/types/sessions.ts
  • apps/ios/ADE/App/DeepLinkRouter.swift
  • apps/ios/ADE/Resources/DatabaseBootstrap.sql
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/Deeplinks/SendToMacCard.swift
  • apps/web/src/app/pages/OpenPage.tsx

Comment thread apps/ade-cli/src/adeRpcServer.ts
Comment thread apps/ade-cli/src/tuiClient/app.tsx
Comment thread apps/desktop/src/main/main.ts Outdated
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 21, 2026

@copilot review but do not make fixes

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