Skip to content

Implement mobile project switching and sync catalog#172

Merged
arul28 merged 11 commits intomainfrom
multi-project-view-mobile
Apr 22, 2026
Merged

Implement mobile project switching and sync catalog#172
arul28 merged 11 commits intomainfrom
multi-project-view-mobile

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented Apr 22, 2026

Summary

Implements the mobile multi-project home and desktop sync catalog flow. Desktop now advertises a project catalog to paired phones, handles project switch requests, and keeps sync host status fresh when switching projects. The iOS app now launches through an ADE-style project home, can open connection settings from that surface, lists desktop projects, switches between them, and can return to the active app surface. The mobile runtime scopes cached lanes, sessions, files, PRs, and snapshots to the active project so stale data from another desktop project does not bleed into the current mobile view.

This also includes the branch’s desktop onboarding/computer-use and web editorial updates that were already part of this work branch, plus the CodeRabbit follow-up fixes for project switching rollback/cancellation, onboarding tour cleanup, mission preflight fallback messaging, and web accessibility/reduced-motion details.

Validation

  • Live Computer Use verification with Electron desktop and iOS Simulator: paired the phone, confirmed desktop connected peer status, opened two desktop projects, switched mobile between ADE and mobile-lanes-tab-2d82c012, verified project-scoped Work data, and checked the simulator SQLite database for project/lane/session counts and foreign-key health.
  • git diff --check
  • xcrun swiftc -parse apps/ios/ADE/App/ContentView.swift apps/ios/ADE/Models/RemoteModels.swift apps/ios/ADE/Services/Database.swift apps/ios/ADE/Services/SyncService.swift apps/ios/ADE/Views/Components/ADEDesignSystem.swift apps/ios/ADE/Views/AttentionDrawer/AttentionDrawerButton.swift apps/ios/ADE/Views/Work/WorkRootScreen.swift apps/ios/ADETests/ADETests.swift
  • plutil -lint apps/ios/ADE/Info.plist
  • xcodebuild test -quiet -project apps/ios/ADE.xcodeproj -scheme ADE -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.2' -only-testing:ADETests/ADETests/testDatabaseListsMobileProjectsAndScopesCachedRuntimeByActiveProject -only-testing:ADETests/ADETests/testSyncServiceProjectHomeUsesCachedProjectsAndLocalSelection -only-testing:ADETests/ADETests/testDatabaseFetchSessionsHidesSessionsWhenLaneRowIsMissing
  • asdf exec npm --prefix apps/desktop run typecheck
  • asdf exec npm --prefix apps/desktop run build
  • asdf exec npm --prefix apps/desktop run lint exits with 0 errors and the existing warning backlog
  • Desktop targeted and sharded Vitest runs, including TopBar, sync host/protocol, computer-use control plane, onboarding, queue landing, and rerun failed shards
  • asdf exec npm --prefix apps/ade-cli run typecheck
  • asdf exec npm --prefix apps/ade-cli run test
  • asdf exec npm --prefix apps/ade-cli run build
  • asdf exec npm --prefix apps/web run typecheck
  • asdf exec npm --prefix apps/web run build
  • node scripts/validate-docs.mjs
  • coderabbit review --agent --type uncommitted; valid findings were fixed. A final rerun hit the service rate limit after the fix pass.

Notes

The full coderabbit --agent branch review cannot run on this PR shape because the service rejects diffs over 150 files; the uncommitted scoped reviews were used before committing and valid findings from those passes were addressed.

Summary by CodeRabbit

  • New Features

    • Mobile project catalog & switching (desktop + iOS Project Home); phone‑sync panel in desktop TopBar; onboarding tours support contextual step templates; new editorial web components and annotated figure.
  • Bug Fixes

    • Local computer‑use gated to authorized roles; mission preflight enforces computer‑use proof gating; DB reads scoped to active project; improved sync polling/focus refresh; unsupported tool calls return structured errors.
  • Documentation

    • Docs links migrated to the /docs/ structure.
  • Refactor

    • Site redesigned to editorial layout; legacy showcase components removed; typography updated.

Greptile Summary

This PR implements end-to-end mobile multi-project switching: the desktop now builds and advertises a project catalog to paired phones, handles project_switch_request messages, and manages per-project sync host lifecycle; the iOS app gains a Project Home screen, catalog merging with dedup, and project-scoped DB reads for lanes, sessions, files, PRs, snapshots, and integration proposals. Previous review concerns (sequential Promise.all, fetchIntegrationProposals scoping, rollback/cancellation guard) are all resolved.

  • P1 — handleDrop stale-closure in TopBar.tsx: if fetchRecent fires on window focus mid-drag, the drop handler splices from a stale recentProjects snapshot and persists a wrong order via reorderRecent.

Confidence Score: 4/5

Safe to merge after addressing the stale-closure drag-reorder bug in TopBar; iOS offline fallback race is low-probability and non-data-corrupting.

One P1 bug (drag reorder overwrites fresh server state with stale closure data on mid-drag focus event) keeps the score at 4. All prior P1 concerns from previous review rounds are resolved. The P2 iOS offline fallback race is a narrow edge case that only produces a user-visible error message, not data loss.

apps/desktop/src/renderer/components/app/TopBar.tsx — handleDrop stale recentProjects closure.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/components/app/TopBar.tsx New phone-sync panel and drag-reorder for project tabs; handleDrop closes over potentially-stale recentProjects if fetchRecent fires mid-drag.
apps/ios/ADE/Services/SyncService.swift Adds mobile project switching, catalog refresh, dedup logic, and rollback-on-error; cancellation guard is correct; minor race in offline fallback branch.
apps/ios/ADE/Services/Database.swift fetchIntegrationProposals now correctly scoped with WHERE project_id = ?; all other data queries already project-scoped.
apps/desktop/src/main/main.ts listMobileSyncProjects now parallelises context fetches with Promise.all; prepareMobileSyncProjectConnection adds lease management and dedup catalog lookup.
apps/desktop/src/main/services/sync/syncHostService.ts Adds project catalog and project switch request handling to desktop sync host; validation and auth checks look correct.
apps/ade-cli/src/adeRpcServer.ts Unsupported tool calls and role-gated computer-use tools now return structured JsonRpcError responses caught at the ade/actions/call boundary.
apps/desktop/src/main/services/missions/missionPreflightService.ts Computer-use proof gating added to preflight checklist with blocking vs warning classification and fallback coverage detection.
apps/ios/ADE/App/ContentView.swift Adds ProjectHomeView gate before rootTabs; navigation request handlers correctly call closeProjectHome() before tab switching.

Sequence Diagram

sequenceDiagram
    participant Phone as iOS App
    participant Desktop as Desktop (main.ts)
    participant SyncHost as SyncHostService

    Phone->>Desktop: project_catalog_request
    Desktop->>Desktop: listMobileSyncProjects() Promise.all over contexts
    Desktop-->>Phone: project_catalog_response { projects[] }

    Phone->>Phone: refreshProjectCatalog() merge remote + cached, dedup by rootPath

    Phone->>Desktop: project_switch_request { projectId, rootPath }
    Desktop->>Desktop: prepareMobileSyncProjectConnection() ensureProjectContextForMobileSync()
    Desktop->>SyncHost: initialize() + getStatus()
    Desktop-->>Phone: project_switch_result { ok, connection, project }

    Phone->>Phone: switchToDesktopProject() save rollback state, setActiveProjectId
    Phone->>SyncHost: connectUsingProfile(new token/port)
    alt success
        Phone->>Phone: projectHomePresented = false
    else error and still current selection
        Phone->>Phone: rollback activeProjectId, token, profile then reconnectIfPossible
    else error and superseded by newer selection
        Phone->>Phone: rethrow no rollback
    end
Loading

Comments Outside Diff (3)

  1. apps/ios/ADE/Services/Database.swift, line 1817-1845 (link)

    P1 fetchIntegrationProposals not scoped to active project

    Unlike fetchLanes, fetchTerminalSessions, and fetchPullRequests, this function omits any project-id filter and returns all rows across every project in the local database. After a project switch, proposals whose source_lane_ids_json contains lanes from the previous project will still appear in the integration-proposals tab for the newly selected project. The integration_proposals table has no project_id column, so a join through lanes is needed:

    select ip.*
      from integration_proposals ip
     where exists (
       select 1
         from lanes l
        where l.project_id = ?
          and json_each.value = l.id
        cross join json_each(ip.source_lane_ids_json)
     )
     order by ip.created_at desc

    Alternatively, add a project_id column at the schema layer if per-project isolation is desired here.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/ios/ADE/Services/Database.swift
    Line: 1817-1845
    
    Comment:
    **`fetchIntegrationProposals` not scoped to active project**
    
    Unlike `fetchLanes`, `fetchTerminalSessions`, and `fetchPullRequests`, this function omits any project-id filter and returns all rows across every project in the local database. After a project switch, proposals whose `source_lane_ids_json` contains lanes from the previous project will still appear in the integration-proposals tab for the newly selected project. The `integration_proposals` table has no `project_id` column, so a join through lanes is needed:
    
    ```sql
    select ip.*
      from integration_proposals ip
     where exists (
       select 1
         from lanes l
        where l.project_id = ?
          and json_each.value = l.id
        cross join json_each(ip.source_lane_ids_json)
     )
     order by ip.created_at desc
    ```
    
    Alternatively, add a `project_id` column at the schema layer if per-project isolation is desired here.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  2. apps/ios/ADE/Services/Database.swift, line 1830-1857 (link)

    P1 fetchIntegrationProposals not scoped to active project

    The query returns every row in integration_proposals across all projects. The table has a project_id NOT NULL column with an index (idx_integration_proposals_project), so a scoped filter is straightforward. After a project switch, proposals whose source_lane_ids_json references lanes from the previous project will still appear in the integration-proposals tab for the newly selected project.

    Add a WHERE project_id = ? filter directly — the join through lanes is not needed since the column exists:

    select id, source_lane_ids_json, ...
      from integration_proposals
     where project_id = ?
     order by created_at desc
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/ios/ADE/Services/Database.swift
    Line: 1830-1857
    
    Comment:
    **`fetchIntegrationProposals` not scoped to active project**
    
    The query returns every row in `integration_proposals` across all projects. The table has a `project_id NOT NULL` column with an index (`idx_integration_proposals_project`), so a scoped filter is straightforward. After a project switch, proposals whose `source_lane_ids_json` references lanes from the previous project will still appear in the integration-proposals tab for the newly selected project.
    
    Add a `WHERE project_id = ?` filter directly — the join through lanes is not needed since the column exists:
    
    ```sql
    select id, source_lane_ids_json, ...
      from integration_proposals
     where project_id = ?
     order by created_at desc
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  3. apps/desktop/src/renderer/components/app/TopBar.tsx, line 269-281 (link)

    P1 handleDrop closes over stale recentProjects

    handleDrop is wrapped in useCallback with [dragIdx, recentProjects] as deps, which means on every render where recentProjects changes a new callback is created. However, if a focus-triggered fetchRecent fires between dragstart and drop (e.g. the window re-focuses mid-drag), recentProjects in state is replaced with a fresh server response, but the handleDrop closure still holds the old snapshot. The splice then produces a reordered list from the stale array, overwriting the just-refreshed data. The reorderRecent IPC call will persist the stale order too.

    Consider using a ref to always read the latest recentProjects inside handleDrop, or suppress fetchRecent while a drag is in progress (dragIdx !== null).

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/renderer/components/app/TopBar.tsx
    Line: 269-281
    
    Comment:
    **`handleDrop` closes over stale `recentProjects`**
    
    `handleDrop` is wrapped in `useCallback` with `[dragIdx, recentProjects]` as deps, which means on every render where `recentProjects` changes a new callback is created. However, if a focus-triggered `fetchRecent` fires between `dragstart` and `drop` (e.g. the window re-focuses mid-drag), `recentProjects` in state is replaced with a fresh server response, but the `handleDrop` closure still holds the old snapshot. The `splice` then produces a reordered list from the stale array, overwriting the just-refreshed data. The `reorderRecent` IPC call will persist the stale order too.
    
    Consider using a `ref` to always read the latest `recentProjects` inside `handleDrop`, or suppress `fetchRecent` while a drag is in progress (`dragIdx !== null`).
    
    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
This is a comment left during a code review.
Path: apps/desktop/src/renderer/components/app/TopBar.tsx
Line: 269-281

Comment:
**`handleDrop` closes over stale `recentProjects`**

`handleDrop` is wrapped in `useCallback` with `[dragIdx, recentProjects]` as deps, which means on every render where `recentProjects` changes a new callback is created. However, if a focus-triggered `fetchRecent` fires between `dragstart` and `drop` (e.g. the window re-focuses mid-drag), `recentProjects` in state is replaced with a fresh server response, but the `handleDrop` closure still holds the old snapshot. The `splice` then produces a reordered list from the stale array, overwriting the just-refreshed data. The `reorderRecent` IPC call will persist the stale order too.

Consider using a `ref` to always read the latest `recentProjects` inside `handleDrop`, or suppress `fetchRecent` while a drag is in progress (`dragIdx !== null`).

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/ios/ADE/Services/SyncService.swift
Line: 633-648

Comment:
**Offline fallback branch allows project switch while connected**

The guard at line 634 (`guard connectionState != .connected && connectionState != .syncing`) is the right safety gate for the offline/local-only path, but if a connection drops between the catalog check and this branch (race between reconnect and a user tap), `connectionState` could be `.connected` here even though `canSendLiveRequests()` returned false a moment ago — the guard then *rejects* a switch that should be allowed under offline rules, leaving the user with an error message instead of falling through to the local cache path. The race window is narrow but results in a user-facing error on every reconnect that coincides with a project tap.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (10): Last reviewed commit: "Allow cached mobile project ids during s..." | Re-trigger Greptile

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

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

Project Deployment Actions Updated (UTC)
ade Ready Ready Preview, Comment Apr 22, 2026 1:34pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

Caller-aware local computer-use gating added; mobile project catalog and switch protocol implemented across desktop and iOS; mission preflight now enforces phase-level proof gating and capability fallbacks; onboarding/tour lifecycle and context threading added; editorial homepage redesign with many new components; numerous tests and iOS DB project-scoping changes.

Changes

Cohort / File(s) Summary
CLI RPC tests & server
apps/ade-cli/src/adeRpcServer.test.ts, apps/ade-cli/src/adeRpcServer.ts
Expanded tests for hiding local-computer tools; made local-computer-use checks caller-role–aware and enforced policy at discovery and execution time.
Computer-use backends & preflight
apps/desktop/src/main/services/computerUse/computerUseArtifactBrokerService.ts, apps/desktop/src/main/services/computerUse/controlPlane.ts, apps/desktop/src/main/services/computerUse/controlPlane.test.ts, apps/desktop/src/main/services/missions/missionPreflightService.ts, apps/desktop/src/main/services/missions/missionPreflightService.test.ts
Detect external backends (ghost, agent-browser) in status, add helper to collect required proof kinds from phases, and reintroduce phase-aware preflight checks that compute missing/blocking kinds and capability diagnostics.
Desktop main & sync host/service
apps/desktop/src/main/main.ts, apps/desktop/src/main/services/sync/syncService.ts, apps/desktop/src/main/services/sync/syncHostService.ts, apps/desktop/src/main/services/sync/syncHostService.test.ts, apps/desktop/src/main/services/sync/syncProtocol.test.ts
Wired optional projectCatalogProvider into sync service/host, added project-catalog/switch message handlers and hello payload projects, implemented handoff leases and lazy project-context init, plus tests for catalog/switch flow.
Shared sync types
apps/desktop/src/shared/types/sync.ts
Added mobile project/catalog/switch payload types and new sync envelope variants; added projectCatalog feature flag and optional projects in hello payload.
Onboarding / Tour system
apps/desktop/src/renderer/state/onboardingStore.ts, apps/desktop/src/renderer/state/onboardingStore.test.ts, apps/desktop/src/renderer/components/onboarding/tour/TourHost.tsx, apps/desktop/src/renderer/components/onboarding/tour/TourOverlay.tsx, apps/desktop/src/renderer/components/onboarding/tour/TourStep.tsx, apps/desktop/src/renderer/components/onboarding/HelpMenu.tsx, apps/desktop/src/renderer/components/onboarding/tour/TourOverlay.test.tsx
Added persisted tour context (activeTourCtx), lifecycle hooks (beforeEnter/afterLeave/actions/branching), ActIntro handling, selector robustness, and tests for actIntro Escape behavior.
TopBar / Phone sync UI
apps/desktop/src/renderer/components/app/TopBar.tsx, apps/desktop/src/renderer/components/app/TopBar.test.tsx, apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx
Refactored sync status/labels to phone-centric wording, added polling+focus refresh, removed navigation-on-click, added phone-sync overlay panel and tests for interactions and label refresh.
Docs links & onboarding tests
apps/desktop/src/renderer/onboarding/docsLinks.ts, apps/desktop/src/renderer/onboarding/docsLinks.test.ts, apps/desktop/src/renderer/onboarding/stepBuilders/stepBuilders.test.ts, apps/desktop/src/renderer/onboarding/tours/*.test.ts, apps/web/src/app/layout/SiteLayout.tsx
Switched docs base to /docs, updated many doc link targets, relaxed tests that forbade /docs/, and made header/footer conditional on home route.
iOS: sync, DB scoping & UI
apps/ios/ADE/Models/RemoteModels.swift, apps/ios/ADE/Services/Database.swift, apps/ios/ADE/Services/SyncService.swift, apps/ios/ADE/ContentView.swift, apps/ios/ADE/Views/Components/ADEDesignSystem.swift, apps/ios/ADE/Views/AttentionDrawer/AttentionDrawerButton.swift, apps/ios/ADE/Views/Work/WorkRootScreen.swift, apps/ios/ADETests/ADETests.swift, apps/ios/ADE/Assets.xcassets/.../Contents.json
Added mobile project wire types; introduced active-project override, scoped DB reads/writes to project context; implemented SyncService project catalog/switch APIs + UI (project home), toolbar project button, and many iOS tests.
Website editorial redesign & styles
apps/web/src/app/pages/HomePage.tsx, apps/web/src/styles/globals.css, apps/web/index.html, apps/web/public/mockup.html
Replaced homepage with chapter-based editorial composition, added editorial palette and new fonts, added static mockup HTML, and updated Google Fonts.
New editorial components
apps/web/src/components/editorial/...
Added ~16 new presentational components (Chapter, Lede, Masthead, CompetitorEquation, AnnotatedFigure, BackCover, FeatureGrid, DeviceComposition, etc.) with Framer Motion and reduced-motion support.
Removed web showcase components
apps/web/src/components/FeatureGallery.tsx, apps/web/src/components/MultiDeviceShowcase.tsx, apps/web/src/components/ProductShowcase.tsx, apps/web/src/components/ProviderOrbit.tsx, apps/web/src/components/ui/ImageAutoSlider.tsx
Deleted several large prior marketing/showcase components as part of the homepage redesign.
Missions & phase validation
apps/desktop/src/main/services/missions/missionService.ts, apps/desktop/src/main/services/missions/missionService.test.ts
Added validationGate.evidenceRequirements and validationGate.capabilityFallback fields with coercion helpers; test updated to save and assert modified validationGate values.
Various tests & small UI tweaks
multiple apps/desktop/*, apps/ios/*, apps/web/*
Numerous test additions/adjustments, accessibility and behavior tweaks across platforms (sync, onboarding, missions, iOS DB and SyncService tests).

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

desktop, ios, web

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.45% 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
Title check ✅ Passed The title clearly and concisely summarizes the main feature additions: mobile project switching and sync catalog implementation, which directly aligns with the core objectives of this comprehensive PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch multi-project-view-mobile

Copy link
Copy Markdown

@capy-ai capy-ai Bot left a comment

Choose a reason for hiding this comment

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

Added 5 comments

Comment thread apps/desktop/src/main/services/missions/missionPreflightService.ts
Comment thread apps/desktop/src/main/services/missions/missionPreflightService.ts
Comment thread apps/desktop/src/main/main.ts Outdated
.map(toRecentProjectSummary)
.find((entry) => normalizeProjectRoot(entry.rootPath) === targetRoot) ?? null;
const project = await mobileProjectSummaryForContext(ctx, recent, { useProjectRowId: true });
mobileSyncHandoffLeases.set(targetRoot, Date.now() + MOBILE_SYNC_HANDOFF_LEASE_MS);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

This handoff lease is only consulted inside hasActiveProjectWorkloads() during a rebalance pass, but the new switch path only queues a rebalance immediately after setting the lease. If the phone cancels the switch or never reconnects, nothing schedules another rebalance when the 60s lease expires, so the inactive project context can stay resident indefinitely with its sync host, DB, and watchers still alive until some unrelated project switch happens. ts // apps/desktop/src/main/main.ts const project = await mobileProjectSummaryForContext(ctx, recent, { useProjectRowId: true }); mobileSyncHandoffLeases.set(targetRoot, Date.now() + MOBILE_SYNC_HANDOFF_LEASE_MS); projectLastActivatedAt.set(targetRoot, Date.now()); scheduleProjectContextRebalance(); Schedule a follow-up rebalance for leaseExpiresAt (or explicitly clear/rebalance when the handoff finishes/fails) so abandoned mobile switches do not pin warm contexts forever.

Comment thread apps/ade-cli/src/adeRpcServer.ts
Comment thread apps/ios/ADE/Services/SyncService.swift
Comment thread apps/desktop/src/main/main.ts
Comment thread apps/ios/ADE/Services/SyncService.swift
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: 18

Caution

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

⚠️ Outside diff range comments (5)
apps/desktop/src/renderer/components/onboarding/tour/TourOverlay.tsx (1)

157-196: ⚠️ Potential issue | 🟠 Major

Disable the overlay Escape handler during act intro steps.

ActIntro already handles Escape as skip via onSkip, but the overlay-level keydown handler still treats the same Escape as tour dismissal. Pressing Escape on an intro step can therefore advance and dismiss the tour in the same key event.

Proposed fix
   useEffect(() => {
+    if (step.actIntro) return;
     const onKeyDown = (e: KeyboardEvent) => {
       if (e.key === "Escape") {
         e.preventDefault();
         handleDismiss();
         return;
@@
     };
     window.addEventListener("keydown", onKeyDown);
     return () => window.removeEventListener("keydown", onKeyDown);
-  }, [handleNext, handlePrev, handleDismiss]);
+  }, [step.actIntro, handleNext, handlePrev, handleDismiss]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/onboarding/tour/TourOverlay.tsx` around
lines 157 - 196, The overlay-level keydown handler (useEffect -> onKeyDown) is
intercepting Escape during ActIntro and causing both skip and dismiss; update
onKeyDown to no-op when step.actIntro is truthy so ActIntro can handle Escape
itself (i.e., return early if step.actIntro), and add step.actIntro (or step) to
the effect dependency array so the handler updates when the step changes;
reference the useEffect/onKeyDown block, the step.actIntro check, and the
handleDismiss/ActIntro onSkip handlers when making the change.
apps/ade-cli/src/adeRpcServer.ts (1)

4136-4145: ⚠️ Potential issue | 🟠 Major

Add policy guard to close direct-call bypass for get_environment_info.

get_environment_info is hidden from the tool list via LOCAL_COMPUTER_USE_TOOL_NAMES, but a direct RPC invocation bypasses this because the handler never calls ensureLocalComputerUse. This allows callers to access sensitive environment details (frontmost app, display configuration) without authorization.

Add the policy guard before reading system state:

Fix
  if (name === "get_environment_info") {
+   ensureLocalComputerUse(name, "environmentInfo");
    const includeDisplays = asBoolean(toolArgs.includeDisplays, false);
    const capabilities = getLocalComputerUseCapabilities();

Also add a test case asserting that get_environment_info is denied for external callers (similar to the existing screenshot_environment denial test). Currently only the success case with orchestrator role is tested.

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

In `@apps/ade-cli/src/adeRpcServer.ts` around lines 4136 - 4145, The
get_environment_info RPC handler currently reads sensitive system state without
calling the policy guard; update the handler to call
ensureLocalComputerUse(toolName, "environmentInfo") (or simply
ensureLocalComputerUse with appropriate toolName) before any system access so
external callers are denied when isLocalComputerUseAllowed(callerCtx) is false;
reference the existing ensureLocalComputerUse function and
LOCAL_COMPUTER_USE_TOOL_NAMES to pick the same capability key
("environmentInfo") and mirror the placement used in other handlers (e.g.,
screenshot handler). Also add a unit/integration test that asserts
get_environment_info is denied for non-orchestrator/external callers (pattern it
after the existing screenshot_environment denial test) to prevent future
regressions.
apps/ios/ADE/Services/Database.swift (3)

720-739: ⚠️ Potential issue | 🟠 Major

Filter lane list snapshots by currentProjectId().

fetchLaneListSnapshots joins lanes but only filters archive state, so cached snapshots from inactive projects can still appear in the mobile lane list after switching projects.

🐛 Proposed fix
   func fetchLaneListSnapshots(includeArchived: Bool) -> [LaneListSnapshot] {
+    guard let projectId = currentProjectId() else { return [] }
     let sql = """
       select s.lane_id, s.snapshot_json, s.updated_at
         from lane_list_snapshots s
         join lanes l on l.id = s.lane_id
-       where (? = 1 or l.archived_at is null)
+       where l.project_id = ?
+         and (? = 1 or l.archived_at is null)
        order by l.created_at desc
     """
-    return query(sql, bind: { statement in
-      sqlite3_bind_int(statement, 1, includeArchived ? 1 : 0)
+    return query(sql, bind: { [self] statement in
+      try self.bindText(projectId, to: statement, index: 1)
+      sqlite3_bind_int(statement, 2, includeArchived ? 1 : 0)
     }) { statement in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Services/Database.swift` around lines 720 - 739,
fetchLaneListSnapshots currently only filters archived lanes and can return
snapshots from other projects; update the SQL and binding in
fetchLaneListSnapshots to also restrict to the active project by
joining/filtering on l.project_id = ? (or equivalent) using currentProjectId().
Modify the SQL (in the function fetchLaneListSnapshots) to add "and l.project_id
= ?" to the WHERE clause, add a second sqlite3_bind_* call in the bind closure
to bind currentProjectId(), and ensure the rest of the function (the query call
and decoding) remains unchanged.

1559-1688: ⚠️ Potential issue | 🟠 Major

Scope PR list reads to the active project.

fetchPullRequests() and fetchPullRequestListItems(forLane:) still read all projects. This can leak PRs from the previously active desktop project into the current mobile project view.

🐛 Proposed direction
   func fetchPullRequests() -> [PrSummary] {
+    guard let projectId = currentProjectId() else { return [] }
     let sql = """
       select id, lane_id, project_id, repo_owner, repo_name, github_pr_number, github_url, github_node_id,
              title, state, base_branch, head_branch, checks_status, review_status, additions, deletions,
              last_synced_at, created_at, updated_at
         from pull_requests
+       where project_id = ?
        order by updated_at desc
     """
-    return query(sql) { statement in
+    return query(sql, bind: { [self] statement in
+      try self.bindText(projectId, to: statement, index: 1)
+    }) { statement in

For fetchPullRequestListItems(forLane:), add pr.project_id = ? to the generated where clause and bind laneId as the second parameter when present.

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

In `@apps/ios/ADE/Services/Database.swift` around lines 1559 - 1688, Both
fetchPullRequests() and fetchPullRequestListItems(forLane:) are missing a filter
by the active project, causing PRs from other projects to leak; update both SQL
queries to include "pr.project_id = ?" in their WHERE clauses and bind the
active project id when preparing the statement (for
fetchPullRequestListItems(forLane:) ensure that when laneId is present you use
"where pr.project_id = ? and pr.lane_id = ?" or when laneId is nil use "where
pr.project_id = ?" and bind the project id as the first parameter and laneId as
the second parameter when applicable), and adjust the parameter binding order in
the query execution code for the functions fetchPullRequests() and
fetchPullRequestListItems(forLane:) accordingly.

696-708: ⚠️ Potential issue | 🟠 Major

Scope lane-detail snapshot deletion to the active project.

Line 698 deletes every lane_detail_snapshots row when the active project hydrates with no lanes, and Lines 700-708 delete details for any lane not in the current hydration set, including lanes from other projects. That can wipe cached lane details for inactive projects when switching.

🐛 Proposed fix
       let laneIds = orderedLanes.map(\.id)
       if laneIds.isEmpty {
-        try exec("delete from lane_detail_snapshots")
+        _ = try execute("""
+          delete from lane_detail_snapshots
+           where lane_id in (
+             select id
+               from lanes
+              where project_id = ?
+           )
+        """) { statement in
+          try bindText(projectId, to: statement, index: 1)
+        }
       } else {
-        try exec("""
+        _ = try execute("""
           delete from lane_detail_snapshots
-           where not exists (
+           where lane_id in (
+             select id
+               from lanes
+              where project_id = ?
+           )
+             and not exists (
              select 1
                from temp_hydrated_lane_ids hydrated
               where hydrated.id = lane_detail_snapshots.lane_id
            )
-        """)
+        """) { statement in
+          try bindText(projectId, to: statement, index: 1)
+        }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Services/Database.swift` around lines 696 - 708, The deletion
currently removes snapshots across all projects; scope it to the active project
by including the project's id in the WHERE clause. When laneIds.isEmpty, change
the exec call to delete only rows with project_id = <activeProjectId> (use the
variable that holds the current project id), and in the non-empty branch add
"and lane_detail_snapshots.project_id = <activeProjectId>" to the delete ...
where not exists (...) query; ensure temp_hydrated_lane_ids only contains IDs
for that same project or join/filter by project_id to avoid removing snapshots
from other projects.
🧹 Nitpick comments (11)
apps/desktop/src/main/services/computerUse/computerUseArtifactBrokerService.ts (1)

485-537: Cache external-backend detection to avoid spawning sh -lc subprocesses on every call.

commandExists("ghost") and commandExists("agent-browser") run spawnSync("sh", ["-lc", ...]) on POSIX platforms (login shell sources rc files, i.e., tens–hundreds of ms each). getBackendStatus is invoked on hot paths — buildComputerUseOwnerSnapshot for every owner snapshot and runPreflight — so every preflight/snapshot read does 2 synchronous subprocess launches that block the main process. The installed state of these CLIs changes rarely; memoize (ideally in service closure, optionally with a short TTL or an invalidate hook when capabilities are re-checked).

♻️ Suggested memoization inside the service closure
   const allowedImportRoots = Array.from(new Set([
     layout.artifactsDir,
     layout.tmpDir,
     os.tmpdir(),
     path.join(os.homedir(), ".agent-browser"),
   ]));
+
+  type BackendProbe = { installed: boolean; at: number };
+  const BACKEND_PROBE_TTL_MS = 60_000;
+  const backendProbeCache = new Map<string, BackendProbe>();
+  const probeBackend = (command: string): boolean => {
+    const cached = backendProbeCache.get(command);
+    const now = Date.now();
+    if (cached && now - cached.at < BACKEND_PROBE_TTL_MS) return cached.installed;
+    const installed = commandExists(command);
+    backendProbeCache.set(command, { installed, at: now });
+    return installed;
+  };
@@
-    const ghostInstalled = commandExists("ghost");
+    const ghostInstalled = probeBackend("ghost");
@@
-    const agentBrowserInstalled = commandExists("agent-browser");
+    const agentBrowserInstalled = probeBackend("agent-browser");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/main/services/computerUse/computerUseArtifactBrokerService.ts`
around lines 485 - 537, getBackendStatus currently calls commandExists("ghost")
and commandExists("agent-browser") every invocation, spawning costly sh -lc
subprocesses; memoize those checks in the service closure and use the cached
values in getBackendStatus (or add a short TTL/invalidate hook) so you don't
spawn on every snapshot/preflight. Concretely: add closure-scoped variables
(e.g., ghostInstalledCached, agentBrowserInstalledCached and
ghostCheckedAt/agentCheckedAt if TTL desired) initialized once (or on first
use), replace direct commandExists calls in getBackendStatus with the cached
booleans, and add an invalidate or refresh call in the existing capability
re-check flow (the function that updates local capabilities, e.g.,
getLocalComputerUseCapabilities or its re-check handler) to reset or refresh the
cached values when a capability scan runs. Ensure getBackendStatus still returns
correct supportedKinds/state/detail based on the cached booleans.
apps/web/src/components/editorial/FadeBand.tsx (1)

3-6: Update the stale height comment.

The component documents a 40vh band, but the rendered class uses h-[18vh] min-h-[110px].

📝 Proposed comment fix
- * 40vh gradient band for dark↔cream transitions.
+ * Responsive gradient band for dark↔cream transitions.

Also applies to: 22-22

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

In `@apps/web/src/components/editorial/FadeBand.tsx` around lines 3 - 6, The top
comment in the FadeBand component is stale: it says "40vh gradient band" but the
JSX uses class names h-[18vh] and min-h-[110px]; update the comment in FadeBand
(component FadeBand) to reflect the actual rendered height (18vh with a 110px
min-height) or make it generic (e.g., "18vh / min 110px gradient band") so the
doc matches the class names h-[18vh] and min-h-[110px].
apps/desktop/src/renderer/onboarding/stepBuilders/stepBuilders.test.ts (1)

65-69: Use the shared docs catalog as the allowed URL set.

Prefix validation still permits stale hardcoded paths. Checking against docsLinks keeps builder steps aligned with the centralized docs contract.

🧪 Proposed assertion update
 import type { TourCtx } from "../registry";
+import { docs } from "../docsLinks";
@@
       it("every docUrl points at ade-app.dev", () => {
+        const allowed = new Set<string>(Object.values(docs));
         for (const step of steps) {
           if (!step.docUrl) continue;
-          expect(step.docUrl.startsWith(VALID_DOCS_PREFIX)).toBe(true);
+          expect(allowed.has(step.docUrl), `unknown docUrl: ${step.docUrl}`).toBe(true);
         }
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/onboarding/stepBuilders/stepBuilders.test.ts`
around lines 65 - 69, The test currently asserts docUrl prefix by checking
step.docUrl.startsWith(VALID_DOCS_PREFIX), which allows stale hardcoded paths;
change the assertion to validate each step.docUrl against the shared docs
catalog instead—import or reference the docsLinks collection and assert
docsLinks.has(step.docUrl) (or docsLinks.includes(step.docUrl) depending on its
type) for every step that has a docUrl, replacing the VALID_DOCS_PREFIX check in
the test that iterates over steps and uses the step.docUrl symbol.
apps/desktop/src/renderer/onboarding/tours/highlights.test.ts (1)

62-67: Assert highlight doc URLs come from docsLinks.

This domain-only check won’t catch a hardcoded or stale highlight URL. Mirror the lanesTour coverage by validating against Object.values(docs).

🧪 Proposed tighter assertion
 import { describe, it, expect, beforeAll } from "vitest";
 import "./index";
 import { getTour, listTours } from "../registry";
+import { docs } from "../docsLinks";
@@
   it("every docUrl on highlight steps points to ade-app.dev", () => {
+    const allowed = new Set<string>(Object.values(docs));
     for (const tour of listTours("highlights")) {
       for (const step of tour.steps) {
         if (!step.docUrl) continue;
-        expect(step.docUrl).toMatch(/^https:\/\/www\.ade-app\.dev/);
+        expect(allowed.has(step.docUrl), `unknown docUrl: ${step.docUrl}`).toBe(true);
       }
     }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/onboarding/tours/highlights.test.ts` around lines
62 - 67, The test currently only asserts docUrl starts with the ade-app.dev
domain; instead, change the assertion to validate that every step.docUrl (from
listTours("highlights") / variable step) is one of the canonical links in the
docs object by checking inclusion in Object.values(docs). Locate the test in
highlights.test.ts where listTours("highlights") is iterated and replace the
expect(step.docUrl).toMatch(...) with an assertion that step.docUrl is in
Object.values(docs) (mirroring the lanesTour coverage) and skip steps without
docUrl as before.
apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx (1)

171-184: Consider gating the 5s poll on document visibility.

refreshWhenVisible fires every 5 seconds for as long as the settings pane is mounted, even when the Electron window is hidden or the OS has backgrounded it. Each tick runs two IPC round-trips (getStatus + listDevices). Since onEvent("sync-status") already pushes updates, you could skip polling when document.visibilityState !== "visible" and rely on the existing focus listener to catch up on re-show.

Also note refresh() itself does not check cancelled before calling setStatus/setDevices/setError, so a tick that resolves after unmount will still update state. The current cancelled guard only fires before the call.

♻️ Proposed fix
     const refreshWhenVisible = () => {
-      if (!cancelled) {
-        void refresh().catch(() => {});
-      }
+      if (cancelled) return;
+      if (typeof document !== "undefined" && document.visibilityState !== "visible") return;
+      void refresh().catch(() => {});
     };
     const interval = window.setInterval(refreshWhenVisible, 5_000);
     window.addEventListener("focus", refreshWhenVisible);
+    document.addEventListener("visibilitychange", refreshWhenVisible);
     return () => {
       cancelled = true;
       window.clearInterval(interval);
       window.removeEventListener("focus", refreshWhenVisible);
+      document.removeEventListener("visibilitychange", refreshWhenVisible);
       dispose();
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx` around
lines 171 - 184, The polling callback refreshWhenVisible should skip calling
refresh() when the document is not visible and the refresh function should stop
applying state updates if the component has been unmounted: update
refreshWhenVisible to first check document.visibilityState === "visible" (in
addition to the existing cancelled check) before calling refresh(), and modify
refresh (or wrap its internal promises) to consult the cancelled flag before
calling setStatus, setDevices, or setError so resolved/errored ticks after
unmount do not update state; reference the existing refreshWhenVisible, refresh,
and cancelled symbols and ensure the useEffect cleanup still sets cancelled and
clears the interval/listener as before.
apps/desktop/src/renderer/components/app/TopBar.test.tsx (1)

37-79: Type the sync fixture against SyncRoleSnapshot.

The test fixture mirrors an IPC/shared sync payload, but Record<string, unknown> lets contract drift slip through. Typing the helper keeps these mocks checked when sync fields change.

Proposed type tightening
 import React from "react";
 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
 import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
 import { TopBar } from "./TopBar";
 import { useAppStore } from "../../state/appStore";
+import type { SyncRoleSnapshot } from "../../../shared/types";
@@
-function makeSyncSnapshot(overrides: Record<string, unknown> = {}) {
+function makeSyncSnapshot(overrides: Partial<SyncRoleSnapshot> = {}): SyncRoleSnapshot {
   return {
     mode: "standalone",
     role: "brain",

As per coding guidelines, **/*.{ts,tsx}: Keep IPC contracts, preload types, shared types, and renderer usage in sync whenever an interface changes.

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

In `@apps/desktop/src/renderer/components/app/TopBar.test.tsx` around lines 37 -
79, The helper makeSyncSnapshot should be typed to the shared IPC contract to
prevent drift: change its signature to accept overrides:
Partial<SyncRoleSnapshot> and return SyncRoleSnapshot, and import the
SyncRoleSnapshot type (from the shared IPC/preload types used by the renderer).
Update any usage that passes overrides to satisfy Partial<SyncRoleSnapshot> and
ensure the returned object matches SyncRoleSnapshot (keep the current object
shape but let TypeScript validate it).
apps/ios/ADE/App/ContentView.swift (1)

23-30: Keep the tab subtree mounted when showing project home.

Swapping rootTabs out destroys the tab view hierarchy while the project picker is open, which can reset nested navigation/scroll/input state when returning to the active app surface. Consider overlaying ProjectHomeView when an active project exists, and only replacing tabs when there is no active project.

♻️ Suggested SwiftUI structure
-    Group {
-      if syncService.shouldShowProjectHome {
-        ProjectHomeView()
-      } else {
-        rootTabs
-      }
-    }
+    ZStack {
+      if syncService.activeProjectId != nil {
+        rootTabs
+      }
+
+      if syncService.shouldShowProjectHome {
+        ProjectHomeView()
+          .transition(.opacity)
+      }
+    }

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

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

In `@apps/ios/ADE/App/ContentView.swift` around lines 23 - 30, The current if/else
replaces the tab subtree (rootTabs) with ProjectHomeView causing the tab view
hierarchy to unmount; change the layout to keep rootTabs mounted and overlay
ProjectHomeView when syncService.shouldShowProjectHome is true and there is an
active project, e.g. use a ZStack or .overlay to always render rootTabs and
conditionally present ProjectHomeView above it, and only fully replace rootTabs
when there is no active project; update logic around
syncService.shouldShowProjectHome / ProjectHomeView so the tab hierarchy
(rootTabs) is never destroyed while the picker is shown.
apps/ios/ADETests/ADETests.swift (1)

578-610: Cover lane-less project workspaces to catch cross-project leakage.

This only verifies lane-backed workspaces. If root/project workspaces use laneId == nil, a listWorkspaces() implementation that scopes via lanes can still leak stale workspaces across projects without this test failing.

🧪 Suggested test coverage expansion
     try database.replaceFilesWorkspaces([
+      FilesWorkspace(
+        id: "workspace-root-one",
+        kind: "project",
+        laneId: nil,
+        name: "Project One",
+        rootPath: "/tmp/project-one",
+        isReadOnlyByDefault: true,
+        mobileReadOnly: true
+      ),
       FilesWorkspace(
         id: "workspace-one",
         kind: "worktree",
@@
     try database.replaceFilesWorkspaces([
+      FilesWorkspace(
+        id: "workspace-root-two",
+        kind: "project",
+        laneId: nil,
+        name: "Project Two",
+        rootPath: "/tmp/project-two",
+        isReadOnlyByDefault: true,
+        mobileReadOnly: true
+      ),
       FilesWorkspace(
         id: "workspace-two",
         kind: "worktree",
@@
-    XCTAssertEqual(database.listWorkspaces().map(\.id), ["workspace-two"])
+    XCTAssertEqual(Set(database.listWorkspaces().map(\.id)), Set(["workspace-root-two", "workspace-two"]))
@@
-    XCTAssertEqual(database.listWorkspaces().map(\.id), ["workspace-one"])
+    XCTAssertEqual(Set(database.listWorkspaces().map(\.id)), Set(["workspace-root-one", "workspace-one"]))

Also applies to: 612-619

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

In `@apps/ios/ADETests/ADETests.swift` around lines 578 - 610, Add test cases that
include lane-less (laneId: nil) project workspaces to the replaceFilesWorkspaces
calls so listWorkspaces() can't leak across projects; specifically, in the
existing setup that calls database.replaceFilesWorkspaces(...) (the two
occurrences around the current blocks) add an additional FilesWorkspace entry
with laneId: nil (e.g., a root/project workspace for "project-one" and another
for "project-two") and then assert via the same listWorkspaces() checks that
only workspaces for the active project are returned; update the two blocks where
replaceFilesWorkspaces(...) is invoked to include these laneId: nil workspaces
to cover lane-less workspace behavior.
apps/desktop/src/shared/types/sync.ts (1)

249-254: Make successful switch results require project and connection.

The current type permits { ok: true }, which would compile but leave iOS without the connection payload needed to complete a switch.

♻️ Proposed type tightening
-export type SyncProjectSwitchResultPayload = {
-  ok: boolean;
-  message?: string | null;
-  project?: SyncMobileProjectSummary | null;
-  connection?: SyncProjectConnectionPayload | null;
-};
+export type SyncProjectSwitchResultPayload =
+  | {
+      ok: true;
+      message?: string | null;
+      project: SyncMobileProjectSummary;
+      connection: SyncProjectConnectionPayload;
+    }
+  | {
+      ok: false;
+      message?: string | null;
+      project?: null;
+      connection?: null;
+    };

As per coding guidelines, **/*.{ts,tsx}: Keep IPC contracts, preload types, shared types, and renderer usage in sync whenever an interface changes.

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

In `@apps/desktop/src/shared/types/sync.ts` around lines 249 - 254, The current
SyncProjectSwitchResultPayload type allows ok: true without required
project/connection; change it to a discriminated union so that when ok is true
the payload requires project: SyncMobileProjectSummary and connection:
SyncProjectConnectionPayload, and when ok is false keep message?: string | null
(and optional project/connection if needed). Update the type alias
SyncProjectSwitchResultPayload to a union of { ok: true; project:
SyncMobileProjectSummary; connection: SyncProjectConnectionPayload; message?:
string | null } | { ok: false; message?: string | null; project?:
SyncMobileProjectSummary | null; connection?: SyncProjectConnectionPayload |
null } so callers can rely on project/connection when ok === true.
apps/ios/ADE/Services/SyncService.swift (2)

3600-3602: Don't block hello on the remote catalog refresh.

refreshRemoteProjectCatalog() goes through awaitResponse with the default 30s timeout and intentionally uses disconnectOnTimeout: false, so a slow or silent desktop response will stall this continuation for up to 30s. Since hello is awaited from connectUsingProfile, the reconnect path keeps reconnectConnectInFlight = true and cannot service network-path / foreground-triggered retries for the whole window. The catalog is already delivered inline via hello_ok.projects (applied at L3663-3666) and/or pushed as an unsolicited project_catalog (L3824-3827), and showProjectHome() fires the same refresh as a detached Task — mirroring that here keeps behavior consistent without holding hello open.

♻️ Proposed fix
     await restoreTrackedOpenLanesAfterReconnect()
-    await refreshRemoteProjectCatalog()
+    Task { `@MainActor` [weak self] in
+      await self?.refreshRemoteProjectCatalog()
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Services/SyncService.swift` around lines 3600 - 3602, Currently
the code awaits refreshRemoteProjectCatalog() which blocks the hello
continuation (called from connectUsingProfile) and leaves
reconnectConnectInFlight true for the awaitResponse timeout; change the call so
the refresh runs asynchronously without awaiting it (e.g., spawn a Task or
Task.detached that calls await refreshRemoteProjectCatalog()), but keep await
restoreTrackedOpenLanesAfterReconnect() as-is so tracked lanes are restored
before continuing; mirror the same detached-refresh approach used by
showProjectHome to avoid blocking hello and allow retries to proceed.

641-645: Normalize rootPaths before matching to avoid duplicate catalog entries.

isActiveProject (L568-572) and the active-project reconcile branch (L663-665) both match root paths via normalizedProjectRoot(...), but this fallback compares remote.rootPath and cachedProject.rootPath verbatim. Cached rows come out of SQLite with trailing slashes/whitespace already trimmed, while remoteProjectCatalog rows come straight from the desktop JSON and can carry a trailing / or padding. When they diverge only in that way the cached and remote descriptors of the same project won't merge, so projects ends up with two entries for the same path and the active-project sort/selection becomes ambiguous.

♻️ Proposed fix
       } else if let match = mergedById.first(where: { entry in
         let remote = entry.value
-        guard let left = remote.rootPath, let right = cachedProject.rootPath else { return false }
-        return left == right
+        guard let left = normalizedProjectRoot(remote.rootPath),
+              let right = normalizedProjectRoot(cachedProject.rootPath)
+        else { return false }
+        return left == right
       }) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Services/SyncService.swift` around lines 641 - 645, The fallback
match against mergedById compares remote.rootPath and cachedProject.rootPath
verbatim which allows mismatches when remote paths have trailing
slashes/whitespace; update this branch to normalize both sides using the
existing normalizedProjectRoot(...) helper (the same normalization used by
isActiveProject and the active-project reconcile branch) before comparing—i.e.,
compute normalizedProjectRoot(remote.rootPath) and
normalizedProjectRoot(cachedProject.rootPath) (ensuring you handle nils) and
compare those so cached and remote descriptors for the same project merge
correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/main.ts`:
- Around line 3486-3501: mobileProjectSummaryForContext currently can return
either ctx.projectId or a stable root id
(`root:${normalizeProjectRoot(ctx.project.rootPath)}`), which causes mismatch
between catalog entries (which use the normalized root id) and switch-success
payloads that return ctx.projectId; fix by ensuring switch-success responses use
the stable root id: update the code that builds the switch success payload to
call mobileProjectSummaryForContext with options.useProjectRowId set to false
(or remove the useProjectRowId branch so the function always returns
`root:${normalizeProjectRoot(...)}`), and ensure any places that previously
returned ctx.projectId instead use the mobileProjectSummaryForContext result so
IDs remain stable across catalog and switch results (referenced symbols:
mobileProjectSummaryForContext, ctx.projectId, normalizeProjectRoot).

In `@apps/desktop/src/main/services/sync/syncHostService.ts`:
- Around line 1637-1653: The hello_ok payload always advertises
projectCatalog.enabled: true even when no projectCatalogProvider exists; update
the code that builds/sends the hello_ok response (the send call that includes
buildProjectCatalogPayload and projectCatalog.projects) to set
features.projectCatalog.enabled based on whether projectCatalogProvider is
available (e.g., whether buildProjectCatalogPayload returns available or a
provider exists) instead of hardcoding true, and update the
SyncFeatureFlags.projectCatalog.enabled type in shared types (SyncFeatureFlags /
projectCatalog) to be a plain boolean so the flag can reflect actual
availability.

In `@apps/desktop/src/renderer/components/app/TopBar.tsx`:
- Around line 571-607: The backdrop currently uses onMouseDown to close the
phone sync panel which allows click-through; change the outer backdrop handler
to onClick (keep the inner onMouseDown stopPropagation), and add proper
modal/dialog semantics to the panel: add role="dialog" and aria-modal="true" on
the panel container, give the header title element (the "Phone sync" text in the
inner header) an id and set aria-labelledby to that id, and make the panel
focusable (tabIndex={-1}) and move focus to it or the close button when
phoneSyncOpen becomes true; also add an onKeyDown on the panel to close on
Escape by calling setPhoneSyncOpen(false) so keyboard users can dismiss it.
Ensure references to phoneSyncOpen, setPhoneSyncOpen, and SyncDevicesSection are
preserved.

In `@apps/desktop/src/renderer/components/onboarding/HelpMenu.tsx`:
- Around line 234-236: The code treats a whitespace-only
firstStep.waitForSelector as valid because it isn't trimmed; update the selector
computation so waitForSelector is trimmed and treated as falsy when empty before
falling back to target (i.e., use firstStep?.waitForSelector?.trim() or
equivalent), referencing the symbols firstStep, waitForSelector, target, and
selector so the trimmed/empty waitForSelector won't block tour startup.

In `@apps/desktop/src/renderer/onboarding/docsLinks.test.ts`:
- Around line 5-8: The test "every URL is under ade-app.dev" only asserts
host-level coverage and misses path-level mismatches; update the test in
docsLinks.test.ts to assert canonical routes for the centralized docs mapping
(variable docs) by defining a small dictionary of expected paths for known keys
(e.g., keys used by tours/glossary) and then iterating over Object.entries(docs)
to check both the host (https://www.ade-app.dev) and that the pathname contains
the expected canonical route for that key; keep the original host regex but add
an assertion that url.includes(expectedPath) for keys present in the
expected-path map and fall back to the host-only check for others.

In `@apps/desktop/src/renderer/onboarding/docsLinks.ts`:
- Around line 17-27: Update the doc URL mappings so each key points to its
specific Mintlify page instead of generic overviews: change lanesCreating →
`${DOCS_BASE}/lanes/creating`, lanesStacks → `${DOCS_BASE}/lanes/stacks`,
lanesPacks → `${DOCS_BASE}/lanes/packs`, lanesEnvironment →
`${DOCS_BASE}/lanes/environment`; change chatOverview →
`${DOCS_BASE}/missions/agent-chat`, chatContext →
`${DOCS_BASE}/missions/context`, chatCapabilities →
`${DOCS_BASE}/missions/capabilities`; and change terminals →
`${DOCS_BASE}/tools/terminals`, filesEditor → `${DOCS_BASE}/tools/files-editor`
so the keys (lanesCreating, lanesStacks, lanesPacks, lanesEnvironment,
chatOverview, chatContext, chatCapabilities, terminals, filesEditor) point to
their matching Mintlify routes.

In `@apps/desktop/src/renderer/state/onboardingStore.ts`:
- Around line 194-207: prevStep currently changes activeStepIndex without
invoking the current step's afterLeave hook, so call runAfterLeave for the
current step before mutating state: inside prevStep (use activeStepIndex from
get() and tour from getTour(activeTourId)), construct ctx as you already do
(createTourCtx if needed), then await
runAfterLeave(tour?.steps[activeStepIndex], ctx) before calling set({
activeStepIndex: nextIndex, activeTourCtx: ctx }), then continue with the
existing onboarding update and finally await
runBeforeEnter(tour?.steps[nextIndex], ctx); ensure you handle missing
tour/step/null ctx the same way other paths do.

In `@apps/ios/ADE/App/ContentView.swift`:
- Around line 248-255: The UI allows selecting other projects while a switch is
pending because only the matching row is blocked; wrap ProjectHomeRow's action
and state with a global guard using syncService.projectSwitchInFlightRootPath
(or a helper like syncService.isProjectSwitchInFlight) so that when
projectSwitchInFlightRootPath != nil all rows are gated: change the trailing
closure passed to ProjectHomeRow from always calling
syncService.selectProject(project) to conditionally no-op when
projectSwitchInFlightRootPath != nil, and pass a derived isSwitching/isDisabled
flag (e.g., isSwitching: syncService.projectSwitchInFlightRootPath != nil) to
the row so it visually disables all rows while a switch is in flight.

In `@apps/web/public/mockup.html`:
- Around line 728-731: Replace the placeholder href="#" links in the public
mockup HTML so they aren’t exposed as dead links: locate the <nav class="nav">
anchors (including the <a class="download"> element) and the similar anchors
around lines 811-814 and update each href to the real target URLs (Docs, GitHub,
Download) or remove/disable the href and use a non-navigable element or data
attribute if the mockup shouldn’t be live; ensure the changes only affect the
public mockup file and keep link text and classes intact.

In `@apps/web/src/app/layout/SiteLayout.tsx`:
- Around line 14-18: Currently SiteFooter is conditionally rendered only when
!isHome, which hides legal/privacy/terms links on the homepage; update the JSX
in SiteLayout so SiteFooter is always rendered (replace the conditional {!isHome
&& <SiteFooter />} with an unconditional <SiteFooter />) to ensure footer/legal
links remain reachable on "/". Keep the existing conditional for SiteHeader
as-is.

In `@apps/web/src/app/pages/HomePage.tsx`:
- Around line 64-81: The animated arrow currently uses an inline style
("animation: 'nudge 2.4s...'") on the span and a local <style> keyframes block;
this ignores users who prefer reduced motion. Move the animation into a named
CSS rule (e.g., .turn-arrow) applied to the span (the span with className
"font-serif italic ..." and inline style) and add a media query `@media`
(prefers-reduced-motion: reduce) { .turn-arrow { animation: none !important; } }
(keep the `@keyframes` nudge in the same stylesheet). This ensures the arrow
animation is disabled for reduced-motion users while keeping SSR/react patterns
intact.

In `@apps/web/src/components/editorial/AnnotatedFigure.tsx`:
- Around line 93-123: The callout label elements rendered in the callouts.map
(the motion.div / inner span that displays c.label) must be hidden from
assistive tech; add aria-hidden="true" (and ensure no tabindex) to the label
element (preferably the span containing {c.label} or the surrounding motion.div)
so these visual-only annotations aren’t exposed; if any label content is
essential, move that text into the figcaption or image alt instead.

In `@apps/web/src/components/editorial/BackCover.tsx`:
- Around line 75-89: The CTA in BackCover.tsx uses a hover translate
(`hover:-translate-y-[1px]`) which ignores users' reduced motion settings;
update the primary CTA anchor in the BackCover component to only move when
motion is allowed by replacing the class with a motion-safe variant (e.g.,
`motion-safe:hover:-translate-y-[1px]`) or remove the translation entirely and
rely on color-only hover feedback (keep other classes like Download,
LINKS.releases, and the anchor props unchanged). Ensure the same change is
applied to any other CTA anchor in this file (e.g., the GitHub link) so hover
movement is gated by motion-safe for accessibility.

In `@apps/web/src/components/editorial/CompetitorEquation.tsx`:
- Around line 60-77: The plus separators and abbreviated labels in
CompetitorEquation are decorative and causing redundant screen-reader output;
mark the motion.span that renders "+" and the short-label span (the element
rendering {app.short}) as presentation-only for ATs (e.g., add
aria-hidden="true" or role="presentation") so screen readers only read the img
alt with the full competitor name; keep the img alt as-is and do not remove
visible text styling — only change accessibility attributes on the decorative
motion.span and the short label span.

In `@apps/web/src/components/editorial/Cutout.tsx`:
- Around line 25-38: The reduceMotion variable in Cutout.tsx should default to
the safe "reduced" state when the hook returns null on SSR; change the
declaration to use the nullish fallback (useReducedMotion() ?? true) and update
any checks that depend on reduceMotion (the motion.div initial and whileInView
props) to continue using that variable so animations are disabled by default
during the indeterminate SSR render.

In `@apps/web/src/components/editorial/FeatureGrid.tsx`:
- Around line 135-141: The hover translate/scale must respect reduced-motion:
use the same reduced-motion signal used elsewhere (e.g., useReducedMotion()) or
Tailwind's motion-safe/motion-reduce utilities to disable transforms; modify the
container div class and the img class in FeatureGrid (the element with class
"group-hover:-translate-y-[3px]" and the img with "group-hover:scale-[1.04]") so
that the transform classes are only applied when motion is allowed (either by
prepending motion-safe: to the hover classes or by conditionally applying those
class names when useReducedMotion() is false), ensuring hover still works
without motion for reduced-motion users.

In `@apps/web/src/components/editorial/Masthead.tsx`:
- Around line 10-51: The Masthead component's header (the outer div, the center
volume span, and the nav anchors in nav) currently forces a single-row layout
and can overflow on narrow screens; update the container and nav to be
responsive by allowing wrapping (e.g., add flex-wrap to the outer flex container
and/or nav), reduce spacing and font size at small breakpoints (use responsive
utility classes or media queries for the span and nav items), and change
alignment so items stack vertically or center when space is limited; ensure
links keep their aria-labels and retain SSR/React patterns used in Masthead so
accessibility and server rendering are preserved.

In `@apps/web/src/styles/globals.css`:
- Around line 31-32: Update the CSS variable declarations for --font-sans and
--font-serif to quote the font family names that Stylelint flags (e.g.,
"BlinkMacSystemFont" and "Georgia") so they pass the value-keyword-case rule;
specifically edit the lines that define --font-sans and --font-serif to wrap the
flagged family names in quotes while preserving the existing fallbacks and
order.

---

Outside diff comments:
In `@apps/ade-cli/src/adeRpcServer.ts`:
- Around line 4136-4145: The get_environment_info RPC handler currently reads
sensitive system state without calling the policy guard; update the handler to
call ensureLocalComputerUse(toolName, "environmentInfo") (or simply
ensureLocalComputerUse with appropriate toolName) before any system access so
external callers are denied when isLocalComputerUseAllowed(callerCtx) is false;
reference the existing ensureLocalComputerUse function and
LOCAL_COMPUTER_USE_TOOL_NAMES to pick the same capability key
("environmentInfo") and mirror the placement used in other handlers (e.g.,
screenshot handler). Also add a unit/integration test that asserts
get_environment_info is denied for non-orchestrator/external callers (pattern it
after the existing screenshot_environment denial test) to prevent future
regressions.

In `@apps/desktop/src/renderer/components/onboarding/tour/TourOverlay.tsx`:
- Around line 157-196: The overlay-level keydown handler (useEffect ->
onKeyDown) is intercepting Escape during ActIntro and causing both skip and
dismiss; update onKeyDown to no-op when step.actIntro is truthy so ActIntro can
handle Escape itself (i.e., return early if step.actIntro), and add
step.actIntro (or step) to the effect dependency array so the handler updates
when the step changes; reference the useEffect/onKeyDown block, the
step.actIntro check, and the handleDismiss/ActIntro onSkip handlers when making
the change.

In `@apps/ios/ADE/Services/Database.swift`:
- Around line 720-739: fetchLaneListSnapshots currently only filters archived
lanes and can return snapshots from other projects; update the SQL and binding
in fetchLaneListSnapshots to also restrict to the active project by
joining/filtering on l.project_id = ? (or equivalent) using currentProjectId().
Modify the SQL (in the function fetchLaneListSnapshots) to add "and l.project_id
= ?" to the WHERE clause, add a second sqlite3_bind_* call in the bind closure
to bind currentProjectId(), and ensure the rest of the function (the query call
and decoding) remains unchanged.
- Around line 1559-1688: Both fetchPullRequests() and
fetchPullRequestListItems(forLane:) are missing a filter by the active project,
causing PRs from other projects to leak; update both SQL queries to include
"pr.project_id = ?" in their WHERE clauses and bind the active project id when
preparing the statement (for fetchPullRequestListItems(forLane:) ensure that
when laneId is present you use "where pr.project_id = ? and pr.lane_id = ?" or
when laneId is nil use "where pr.project_id = ?" and bind the project id as the
first parameter and laneId as the second parameter when applicable), and adjust
the parameter binding order in the query execution code for the functions
fetchPullRequests() and fetchPullRequestListItems(forLane:) accordingly.
- Around line 696-708: The deletion currently removes snapshots across all
projects; scope it to the active project by including the project's id in the
WHERE clause. When laneIds.isEmpty, change the exec call to delete only rows
with project_id = <activeProjectId> (use the variable that holds the current
project id), and in the non-empty branch add "and
lane_detail_snapshots.project_id = <activeProjectId>" to the delete ... where
not exists (...) query; ensure temp_hydrated_lane_ids only contains IDs for that
same project or join/filter by project_id to avoid removing snapshots from other
projects.

---

Nitpick comments:
In
`@apps/desktop/src/main/services/computerUse/computerUseArtifactBrokerService.ts`:
- Around line 485-537: getBackendStatus currently calls commandExists("ghost")
and commandExists("agent-browser") every invocation, spawning costly sh -lc
subprocesses; memoize those checks in the service closure and use the cached
values in getBackendStatus (or add a short TTL/invalidate hook) so you don't
spawn on every snapshot/preflight. Concretely: add closure-scoped variables
(e.g., ghostInstalledCached, agentBrowserInstalledCached and
ghostCheckedAt/agentCheckedAt if TTL desired) initialized once (or on first
use), replace direct commandExists calls in getBackendStatus with the cached
booleans, and add an invalidate or refresh call in the existing capability
re-check flow (the function that updates local capabilities, e.g.,
getLocalComputerUseCapabilities or its re-check handler) to reset or refresh the
cached values when a capability scan runs. Ensure getBackendStatus still returns
correct supportedKinds/state/detail based on the cached booleans.

In `@apps/desktop/src/renderer/components/app/TopBar.test.tsx`:
- Around line 37-79: The helper makeSyncSnapshot should be typed to the shared
IPC contract to prevent drift: change its signature to accept overrides:
Partial<SyncRoleSnapshot> and return SyncRoleSnapshot, and import the
SyncRoleSnapshot type (from the shared IPC/preload types used by the renderer).
Update any usage that passes overrides to satisfy Partial<SyncRoleSnapshot> and
ensure the returned object matches SyncRoleSnapshot (keep the current object
shape but let TypeScript validate it).

In `@apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx`:
- Around line 171-184: The polling callback refreshWhenVisible should skip
calling refresh() when the document is not visible and the refresh function
should stop applying state updates if the component has been unmounted: update
refreshWhenVisible to first check document.visibilityState === "visible" (in
addition to the existing cancelled check) before calling refresh(), and modify
refresh (or wrap its internal promises) to consult the cancelled flag before
calling setStatus, setDevices, or setError so resolved/errored ticks after
unmount do not update state; reference the existing refreshWhenVisible, refresh,
and cancelled symbols and ensure the useEffect cleanup still sets cancelled and
clears the interval/listener as before.

In `@apps/desktop/src/renderer/onboarding/stepBuilders/stepBuilders.test.ts`:
- Around line 65-69: The test currently asserts docUrl prefix by checking
step.docUrl.startsWith(VALID_DOCS_PREFIX), which allows stale hardcoded paths;
change the assertion to validate each step.docUrl against the shared docs
catalog instead—import or reference the docsLinks collection and assert
docsLinks.has(step.docUrl) (or docsLinks.includes(step.docUrl) depending on its
type) for every step that has a docUrl, replacing the VALID_DOCS_PREFIX check in
the test that iterates over steps and uses the step.docUrl symbol.

In `@apps/desktop/src/renderer/onboarding/tours/highlights.test.ts`:
- Around line 62-67: The test currently only asserts docUrl starts with the
ade-app.dev domain; instead, change the assertion to validate that every
step.docUrl (from listTours("highlights") / variable step) is one of the
canonical links in the docs object by checking inclusion in Object.values(docs).
Locate the test in highlights.test.ts where listTours("highlights") is iterated
and replace the expect(step.docUrl).toMatch(...) with an assertion that
step.docUrl is in Object.values(docs) (mirroring the lanesTour coverage) and
skip steps without docUrl as before.

In `@apps/desktop/src/shared/types/sync.ts`:
- Around line 249-254: The current SyncProjectSwitchResultPayload type allows
ok: true without required project/connection; change it to a discriminated union
so that when ok is true the payload requires project: SyncMobileProjectSummary
and connection: SyncProjectConnectionPayload, and when ok is false keep
message?: string | null (and optional project/connection if needed). Update the
type alias SyncProjectSwitchResultPayload to a union of { ok: true; project:
SyncMobileProjectSummary; connection: SyncProjectConnectionPayload; message?:
string | null } | { ok: false; message?: string | null; project?:
SyncMobileProjectSummary | null; connection?: SyncProjectConnectionPayload |
null } so callers can rely on project/connection when ok === true.

In `@apps/ios/ADE/App/ContentView.swift`:
- Around line 23-30: The current if/else replaces the tab subtree (rootTabs)
with ProjectHomeView causing the tab view hierarchy to unmount; change the
layout to keep rootTabs mounted and overlay ProjectHomeView when
syncService.shouldShowProjectHome is true and there is an active project, e.g.
use a ZStack or .overlay to always render rootTabs and conditionally present
ProjectHomeView above it, and only fully replace rootTabs when there is no
active project; update logic around syncService.shouldShowProjectHome /
ProjectHomeView so the tab hierarchy (rootTabs) is never destroyed while the
picker is shown.

In `@apps/ios/ADE/Services/SyncService.swift`:
- Around line 3600-3602: Currently the code awaits refreshRemoteProjectCatalog()
which blocks the hello continuation (called from connectUsingProfile) and leaves
reconnectConnectInFlight true for the awaitResponse timeout; change the call so
the refresh runs asynchronously without awaiting it (e.g., spawn a Task or
Task.detached that calls await refreshRemoteProjectCatalog()), but keep await
restoreTrackedOpenLanesAfterReconnect() as-is so tracked lanes are restored
before continuing; mirror the same detached-refresh approach used by
showProjectHome to avoid blocking hello and allow retries to proceed.
- Around line 641-645: The fallback match against mergedById compares
remote.rootPath and cachedProject.rootPath verbatim which allows mismatches when
remote paths have trailing slashes/whitespace; update this branch to normalize
both sides using the existing normalizedProjectRoot(...) helper (the same
normalization used by isActiveProject and the active-project reconcile branch)
before comparing—i.e., compute normalizedProjectRoot(remote.rootPath) and
normalizedProjectRoot(cachedProject.rootPath) (ensuring you handle nils) and
compare those so cached and remote descriptors for the same project merge
correctly.

In `@apps/ios/ADETests/ADETests.swift`:
- Around line 578-610: Add test cases that include lane-less (laneId: nil)
project workspaces to the replaceFilesWorkspaces calls so listWorkspaces() can't
leak across projects; specifically, in the existing setup that calls
database.replaceFilesWorkspaces(...) (the two occurrences around the current
blocks) add an additional FilesWorkspace entry with laneId: nil (e.g., a
root/project workspace for "project-one" and another for "project-two") and then
assert via the same listWorkspaces() checks that only workspaces for the active
project are returned; update the two blocks where replaceFilesWorkspaces(...) is
invoked to include these laneId: nil workspaces to cover lane-less workspace
behavior.

In `@apps/web/src/components/editorial/FadeBand.tsx`:
- Around line 3-6: The top comment in the FadeBand component is stale: it says
"40vh gradient band" but the JSX uses class names h-[18vh] and min-h-[110px];
update the comment in FadeBand (component FadeBand) to reflect the actual
rendered height (18vh with a 110px min-height) or make it generic (e.g., "18vh /
min 110px gradient band") so the doc matches the class names h-[18vh] and
min-h-[110px].
🪄 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: f86a1b42-6b75-4627-b2cf-be55cb0cc020

📥 Commits

Reviewing files that changed from the base of the PR and between 6035d16 and 8aceeb6.

⛔ Files ignored due to path filters (4)
  • apps/web/public/images/features/git-history.png is excluded by !**/*.png
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/ios-companion.md is excluded by !docs/**
📒 Files selected for processing (61)
  • apps/ade-cli/src/adeRpcServer.test.ts
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/computerUse/computerUseArtifactBrokerService.ts
  • apps/desktop/src/main/services/computerUse/controlPlane.test.ts
  • apps/desktop/src/main/services/computerUse/controlPlane.ts
  • apps/desktop/src/main/services/missions/missionPreflightService.test.ts
  • apps/desktop/src/main/services/missions/missionPreflightService.ts
  • apps/desktop/src/main/services/sync/syncHostService.test.ts
  • apps/desktop/src/main/services/sync/syncHostService.ts
  • apps/desktop/src/main/services/sync/syncProtocol.test.ts
  • apps/desktop/src/main/services/sync/syncService.ts
  • apps/desktop/src/renderer/components/app/TopBar.test.tsx
  • apps/desktop/src/renderer/components/app/TopBar.tsx
  • apps/desktop/src/renderer/components/onboarding/HelpMenu.tsx
  • apps/desktop/src/renderer/components/onboarding/tour/TourHost.tsx
  • apps/desktop/src/renderer/components/onboarding/tour/TourOverlay.tsx
  • apps/desktop/src/renderer/components/onboarding/tour/TourStep.tsx
  • apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx
  • apps/desktop/src/renderer/onboarding/docsLinks.test.ts
  • apps/desktop/src/renderer/onboarding/docsLinks.ts
  • apps/desktop/src/renderer/onboarding/stepBuilders/stepBuilders.test.ts
  • apps/desktop/src/renderer/onboarding/tours/firstJourneyTour.test.ts
  • apps/desktop/src/renderer/onboarding/tours/highlights.test.ts
  • apps/desktop/src/renderer/onboarding/tours/lanesTour.test.ts
  • apps/desktop/src/renderer/state/onboardingStore.test.ts
  • apps/desktop/src/renderer/state/onboardingStore.ts
  • apps/desktop/src/shared/types/sync.ts
  • apps/ios/ADE/App/ContentView.swift
  • apps/ios/ADE/Assets.xcassets/BrandMark.imageset/Contents.json
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/ios/ADE/Services/Database.swift
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/AttentionDrawer/AttentionDrawerButton.swift
  • apps/ios/ADE/Views/Components/ADEDesignSystem.swift
  • apps/ios/ADE/Views/Work/WorkRootScreen.swift
  • apps/ios/ADETests/ADETests.swift
  • apps/web/index.html
  • apps/web/public/mockup.html
  • apps/web/src/app/layout/SiteLayout.tsx
  • apps/web/src/app/pages/HomePage.tsx
  • apps/web/src/components/FeatureGallery.tsx
  • apps/web/src/components/MultiDeviceShowcase.tsx
  • apps/web/src/components/ProductShowcase.tsx
  • apps/web/src/components/ProviderOrbit.tsx
  • apps/web/src/components/editorial/AnnotatedFigure.tsx
  • apps/web/src/components/editorial/BackCover.tsx
  • apps/web/src/components/editorial/Chapter.tsx
  • apps/web/src/components/editorial/ChapterHeadline.tsx
  • apps/web/src/components/editorial/CompetitorEquation.tsx
  • apps/web/src/components/editorial/Cutout.tsx
  • apps/web/src/components/editorial/DeviceComposition.tsx
  • apps/web/src/components/editorial/FadeBand.tsx
  • apps/web/src/components/editorial/FeatureGrid.tsx
  • apps/web/src/components/editorial/IPhoneFrame.tsx
  • apps/web/src/components/editorial/IndexPage.tsx
  • apps/web/src/components/editorial/Lede.tsx
  • apps/web/src/components/editorial/Masthead.tsx
  • apps/web/src/components/editorial/PullQuote.tsx
  • apps/web/src/components/ui/ImageAutoSlider.tsx
  • apps/web/src/styles/globals.css
💤 Files with no reviewable changes (6)
  • apps/web/src/components/ui/ImageAutoSlider.tsx
  • apps/web/src/components/ProductShowcase.tsx
  • apps/web/src/components/ProviderOrbit.tsx
  • apps/web/src/components/MultiDeviceShowcase.tsx
  • apps/desktop/src/renderer/onboarding/tours/firstJourneyTour.test.ts
  • apps/web/src/components/FeatureGallery.tsx

Comment thread apps/desktop/src/main/main.ts
Comment thread apps/desktop/src/main/services/sync/syncHostService.ts
Comment thread apps/desktop/src/renderer/components/app/TopBar.tsx
Comment thread apps/desktop/src/renderer/components/onboarding/HelpMenu.tsx
Comment thread apps/desktop/src/renderer/onboarding/docsLinks.test.ts
Comment thread apps/web/src/components/editorial/CompetitorEquation.tsx
Comment thread apps/web/src/components/editorial/Cutout.tsx Outdated
Comment thread apps/web/src/components/editorial/FeatureGrid.tsx Outdated
Comment on lines +10 to +51
<div className="mx-auto flex max-w-[1240px] items-center gap-6 px-[clamp(20px,3vw,40px)] py-[11px]">
<a href="/" className="flex items-center gap-2" aria-label="ADE home">
<img
src="/images/ade-wordmark.png"
alt="ADE"
className="h-[22px] w-auto"
style={{ filter: "brightness(1.05)" }}
/>
</a>

<div className="flex-1 text-center">
<span className="text-[11px] uppercase tracking-[0.22em] text-[color:var(--color-cream-muted)]">
Vol. 1 &middot; v1.1.0 &middot; Apr 2026
</span>
</div>

<nav className="flex items-center gap-5 text-[12px] uppercase tracking-[0.14em]">
<a
href={LINKS.docs}
target="_blank"
rel="noreferrer"
className="text-[color:var(--color-cream-muted)] transition-colors hover:text-[color:var(--color-cream)]"
>
Docs
</a>
<a
href={LINKS.github}
target="_blank"
rel="noreferrer"
className="text-[color:var(--color-cream-muted)] transition-colors hover:text-[color:var(--color-cream)]"
>
GitHub
</a>
<a
href={LINKS.releases}
target="_blank"
rel="noreferrer"
className="text-[color:var(--color-violet-bright)] transition-colors hover:text-[color:var(--color-cream)]"
>
Download <span aria-hidden>&darr;</span>
</a>
</nav>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make the replacement masthead responsive.

Because the global mobile header is hidden on /, this masthead becomes the only home navigation. The issue label plus three nav links stay in one row and can overflow on narrow screens.

📱 Proposed responsive adjustment
-      <div className="mx-auto flex max-w-[1240px] items-center gap-6 px-[clamp(20px,3vw,40px)] py-[11px]">
+      <div className="mx-auto flex max-w-[1240px] items-center gap-4 px-[clamp(20px,3vw,40px)] py-[11px] sm:gap-6">
         <a href="/" className="flex items-center gap-2" aria-label="ADE home">
@@
-        <div className="flex-1 text-center">
+        <div className="hidden flex-1 text-center md:block">
@@
-        <nav className="flex items-center gap-5 text-[12px] uppercase tracking-[0.14em]">
+        <nav className="ml-auto flex shrink-0 items-center gap-3 text-[11px] uppercase tracking-[0.12em] sm:gap-5 sm:text-[12px] sm:tracking-[0.14em]">

As per coding guidelines, apps/web/**: Web app — check for accessibility, SSR compatibility, and proper React patterns.

📝 Committable suggestion

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

Suggested change
<div className="mx-auto flex max-w-[1240px] items-center gap-6 px-[clamp(20px,3vw,40px)] py-[11px]">
<a href="/" className="flex items-center gap-2" aria-label="ADE home">
<img
src="/images/ade-wordmark.png"
alt="ADE"
className="h-[22px] w-auto"
style={{ filter: "brightness(1.05)" }}
/>
</a>
<div className="flex-1 text-center">
<span className="text-[11px] uppercase tracking-[0.22em] text-[color:var(--color-cream-muted)]">
Vol. 1 &middot; v1.1.0 &middot; Apr 2026
</span>
</div>
<nav className="flex items-center gap-5 text-[12px] uppercase tracking-[0.14em]">
<a
href={LINKS.docs}
target="_blank"
rel="noreferrer"
className="text-[color:var(--color-cream-muted)] transition-colors hover:text-[color:var(--color-cream)]"
>
Docs
</a>
<a
href={LINKS.github}
target="_blank"
rel="noreferrer"
className="text-[color:var(--color-cream-muted)] transition-colors hover:text-[color:var(--color-cream)]"
>
GitHub
</a>
<a
href={LINKS.releases}
target="_blank"
rel="noreferrer"
className="text-[color:var(--color-violet-bright)] transition-colors hover:text-[color:var(--color-cream)]"
>
Download <span aria-hidden>&darr;</span>
</a>
</nav>
<div className="mx-auto flex max-w-[1240px] items-center gap-4 px-[clamp(20px,3vw,40px)] py-[11px] sm:gap-6">
<a href="/" className="flex items-center gap-2" aria-label="ADE home">
<img
src="/images/ade-wordmark.png"
alt="ADE"
className="h-[22px] w-auto"
style={{ filter: "brightness(1.05)" }}
/>
</a>
<div className="hidden flex-1 text-center md:block">
<span className="text-[11px] uppercase tracking-[0.22em] text-[color:var(--color-cream-muted)]">
Vol. 1 &middot; v1.1.0 &middot; Apr 2026
</span>
</div>
<nav className="ml-auto flex shrink-0 items-center gap-3 text-[11px] uppercase tracking-[0.12em] sm:gap-5 sm:text-[12px] sm:tracking-[0.14em]">
<a
href={LINKS.docs}
target="_blank"
rel="noreferrer"
className="text-[color:var(--color-cream-muted)] transition-colors hover:text-[color:var(--color-cream)]"
>
Docs
</a>
<a
href={LINKS.github}
target="_blank"
rel="noreferrer"
className="text-[color:var(--color-cream-muted)] transition-colors hover:text-[color:var(--color-cream)]"
>
GitHub
</a>
<a
href={LINKS.releases}
target="_blank"
rel="noreferrer"
className="text-[color:var(--color-violet-bright)] transition-colors hover:text-[color:var(--color-cream)]"
>
Download <span aria-hidden>&darr;</span>
</a>
</nav>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/editorial/Masthead.tsx` around lines 10 - 51, The
Masthead component's header (the outer div, the center volume span, and the nav
anchors in nav) currently forces a single-row layout and can overflow on narrow
screens; update the container and nav to be responsive by allowing wrapping
(e.g., add flex-wrap to the outer flex container and/or nav), reduce spacing and
font size at small breakpoints (use responsive utility classes or media queries
for the span and nav items), and change alignment so items stack vertically or
center when space is limited; ensure links keep their aria-labels and retain
SSR/React patterns used in Masthead so accessibility and server rendering are
preserved.

Comment thread apps/web/src/styles/globals.css Outdated
export const docs = {
// Root / welcome
home: DOCS_BASE,
home: SITE_BASE,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

This change redefines docs.home to https://www.ade-app.dev, but docs.home is the target used by the desktop Help menu and onboarding settings when the user clicks “ADE docs”. The repo’s own web config still treats https://www.ade-app.dev/docs as the docs entrypoint (apps/web/src/lib/links.ts, apps/web/vercel.json), so this regresses those entrypoints from the docs index to the marketing homepage. Point docs.home back to the Mintlify base (DOCS_BASE) and update the companion test that now asserts the site root.

// apps/desktop/src/renderer/onboarding/docsLinks.ts
const SITE_BASE = "https://www.ade-app.dev";
const DOCS_BASE = `${SITE_BASE}/docs`;

export const docs = {
  home: SITE_BASE,

|| requirement === "video_recording"
);
if (requiresBrowserEvidence && descriptor) {
const likelyBrowserCapable = descriptor.capabilities.tools && descriptor.capabilities.vision;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

This new capability gate hard-codes every browser/screenshot proof requirement to tools && vision, which makes preflight fail otherwise valid mission configs that rely on external proof capture instead of model vision. For example, the registry includes selectable tool-capable/non-vision models like openai/gpt-5.3-codex-spark, and dynamic OpenCode descriptors default to vision: false; those models can still drive agent-browser/other external backends, and the runtime prompt already tells workers to prefer external backends for screenshot, browser_trace, and related artifacts. Turning that into a hard preflight failure blocks launches that the rest of ADE would allow. Fix by basing the check on actual proof backend/tool availability, or at minimum avoid requiring vision for proof kinds that can be satisfied by external automation.

// apps/desktop/src/main/services/missions/missionPreflightService.ts
if (requiresBrowserEvidence && descriptor) {
  const likelyBrowserCapable = descriptor.capabilities.tools && descriptor.capabilities.vision;
  if (!likelyBrowserCapable) {
    const message = `${phase.name}: requires browser/screenshot evidence, but ${descriptor.displayName} does not advertise both tools and vision support.`;

Comment thread apps/ios/ADE/Services/SyncService.swift Outdated
} else if let match = mergedById.first(where: { entry in
let remote = entry.value
guard let left = remote.rootPath, let right = cachedProject.rootPath else { return false }
return left == right
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

refreshProjectCatalog() re-keys a cached project onto a remote catalog entry only when the raw rootPath strings are identical, even though this same service already treats project roots as normalized elsewhere (activeProjectRootPath, normalizedProjectRoot). That means a cached row like /tmp/project-two/ and a live desktop catalog entry /tmp/project-two survive as two separate projects entries after connect, so the project home can show duplicates for the same desktop project and the active/switching state can attach to the wrong row. Normalize both sides before comparing (and keep the other raw root-path checks in this flow consistent with that rule).

// apps/ios/ADE/Services/SyncService.swift
      } else if let match = mergedById.first(where: { entry in
        let remote = entry.value
        guard let left = remote.rootPath, let right = cachedProject.rootPath else { return false }
        return left == right
      }) {

Comment thread apps/ios/ADE/Services/Database.swift
Comment thread apps/ios/ADE/Assets.xcassets/BrandMark.imageset/Contents.json
label: "Workspace graph",
blurb: "See the shape of your repo — files, imports, and open lanes.",
src: "/images/features/workspacegraph.png",
docs: "/tools/workspace",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

The new editorial homepage hard-codes several docs slugs that do not exist in the current docs site. For example, https://www.ade-app.dev/docs/tools/workspace now returns the live docs 404 page, while both the repo content and production nav expose tools/workspace-graph.mdx at /docs/tools/workspace-graph; the same mismatch pattern is repeated in @apps/web/src/components/editorial/IndexPage.tsx for routes like /tools/files, /tools/git, /tools/prs, /configuration/models, and /cto/linear-sync, while the repo still contains /tools/files-editor, /tools/history, /tools/pull-requests, /configuration/ai-providers, and /cto/linear. That means users who click the redesigned landing page's docs CTAs are sent to dead pages instead of the referenced feature docs. Update these entries to the existing slugs or add matching redirects before shipping.

// apps/web/src/components/editorial/FeatureGrid.tsx
{
  label: "Workspace graph",
  blurb: "See the shape of your repo — files, imports, and open lanes.",
  docs: "/tools/workspace",
}

{ name: "CLI · ade", page: "12", href: "/tools/cli" },
{ name: "Dispatch", page: "17", href: "/missions/dispatch" },
{ name: "Figma integration", page: "32", href: "/tools/figma" },
{ name: "File viewer", page: "05", href: "/tools/files" },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

Several of the new homepage docs links were copied to slugs that do not exist in the repo’s docs map, so the landing page now sends users to missing docs pages instead of the feature docs it advertises. For example, docs.json and the checked-in docs files contain tools/files-editor, tools/history, tools/pull-requests, and tools/workspace-graph, but the new editorial components link to /tools/files, /tools/git, /tools/prs, and /tools/workspace instead. The same stale slugs also appear in the feature grid. Update these hardcoded href values to the current docs routes before shipping.

// apps/web/src/components/editorial/IndexPage.tsx
{ name: "File viewer", page: "05", href: "/tools/files" },
{ name: "Git history", page: "07", href: "/tools/git" },
...
{ name: "PR review", page: "20", href: "/tools/prs" },
{ name: "Workspace graph", page: "06", href: "/tools/workspace" },
// apps/web/src/components/editorial/FeatureGrid.tsx
docs: "/tools/files",
...
docs: "/tools/git",
...
docs: "/tools/workspace",

Copy link
Copy Markdown

@capy-ai capy-ai Bot left a comment

Choose a reason for hiding this comment

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

Added 4 comments

else capabilityWarnings.push(message);
}
}
if (requiresBrowserEvidence && !descriptor) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

runPreflight() already accepts models that are present in getAvailabilityAsync() even when they are not resolvable from modelRegistry (availableDirect at lines 267-277), but this new branch turns the same case back into a hard capability failure as soon as a phase requires screenshot/browser evidence. That means a mission using a runtime-discovered model ID (for example a local/OpenCode model that is available at runtime but has no registry descriptor) will still be blocked even when agent-browser or another proof backend can satisfy the evidence contract. This reintroduces the exact false-negative launch block the preflight change is meant to avoid. Skip the unresolved-model failure when backend/local proof coverage already exists, or derive capability checks from the runtime availability record instead.

// apps/desktop/src/main/services/missions/missionPreflightService.ts
      if (requiresBrowserEvidence && !descriptor) {
        const message = `${phase.name}: requires browser/screenshot evidence, but model ${phase.model.modelId} could not be resolved for capability checks.`;
        if ((phase.validationGate.capabilityFallback ?? "block") === "block") capabilityIssues.push(message);
        else capabilityWarnings.push(message);
      }
Suggested change
if (requiresBrowserEvidence && !descriptor) {
if (requiresBrowserEvidence && !descriptor && !browserEvidenceCoveredByBackend) {

Comment thread apps/ios/ADE/Services/Database.swift
Comment thread apps/web/src/app/pages/HomePage.tsx
Comment thread apps/web/src/app/pages/HomePage.tsx
Comment thread apps/ade-cli/src/adeRpcServer.ts Outdated
if (name === "get_environment_info") {
const includeDisplays = asBoolean(toolArgs.includeDisplays, false);
const capabilities = getLocalComputerUseCapabilities();
const capabilities = ensureLocalComputerUse(name, "environmentInfo");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

get_environment_info now goes through ensureLocalComputerUse, which throws whenever environmentInfo.available is false: ts // apps/ade-cli/src/adeRpcServer.ts if (name === "get_environment_info") { const includeDisplays = asBoolean(toolArgs.includeDisplays, false); const capabilities = ensureLocalComputerUse(name, "environmentInfo"); const frontmostApp = capabilities.environmentInfo.available That changes this read-only diagnostic tool from returning structured blocked_by_capability / missing state to hard-failing on Linux or on macOS hosts where osascript is unavailable, even though @AGENTS.md says computer-use features should gracefully degrade off macOS. The regression is user-facing because ade proof environment can no longer inspect why local computer use is unavailable for an authorized session. Keep the new policyDenied check for unauthorized callers, but do not treat missing local capability as fatal for this inspection endpoint.

Suggested change
const capabilities = ensureLocalComputerUse(name, "environmentInfo");
const capabilities = isLocalComputerUseAllowed(callerCtx) ? getLocalComputerUseCapabilities() : ensureLocalComputerUse(name, "environmentInfo");

Comment thread apps/ios/ADE/Services/Database.swift Outdated
if let activeProjectIdOverride {
return activeProjectIdOverride
}
return queryString("select id from projects order by created_at asc limit 1")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

When activeProjectIdOverride is nil, this new fallback silently selects the oldest project row instead of the most recently used one, which makes the app derive cached state from the wrong project before the user finishes project selection. In this PR, SyncService.init() now restores any saved selection and then unconditionally calls refreshActiveSessionsAndSnapshot(), and that path feeds database.fetchSessions() plus writeWorkspaceSnapshotNow()/database.fetchPullRequestListItems() into the shared widget/live-activity snapshot. On a fresh install or cleared selection state with multiple cached projects, the phone can therefore show sessions/PRs from an unrelated older desktop project immediately on launch. swift // apps/ios/ADE/Services/Database.swift func currentProjectId() -> String? { if let activeProjectIdOverride { return activeProjectIdOverride } return queryString("select id from projects order by created_at asc limit 1") } Order the fallback by recency (last_opened_at, then created_at) so the implicit selection matches the project-home MRU behavior.

Suggested change
return queryString("select id from projects order by created_at asc limit 1")
return queryString("select id from projects order by last_opened_at desc, created_at desc limit 1")

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

Caution

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

⚠️ Outside diff range comments (6)
apps/ios/ADE/Services/Database.swift (6)

1830-1858: ⚠️ Potential issue | 🟠 Major

Filter integration proposals by currentProjectId().

integration_proposals has project_id, but this reader has no active-project guard or where project_id = ?. After switching projects, the mobile UI can surface proposals from another cached project.

Proposed project filter
   func fetchIntegrationProposals() -> [IntegrationProposal] {
+    guard let projectId = currentProjectId() else { return [] }
     let sql = """
@@
         from integration_proposals
+       where project_id = ?
        order by created_at desc
     """
 
-    return query(sql) { statement in
+    return query(sql, bind: { [self] statement in
+      try self.bindText(projectId, to: statement, index: 1)
+    }) { statement in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Services/Database.swift` around lines 1830 - 1858, The query in
fetchIntegrationProposals reads from integration_proposals without scoping to
the active project, causing proposals from other projects to appear; update
fetchIntegrationProposals to guard on currentProjectId() and add a WHERE
project_id = ? clause (or equivalent parameter binding) to the SQL and pass
currentProjectId() as the bound parameter before executing the query so only
proposals for the active project are returned.

696-708: ⚠️ Potential issue | 🟠 Major

Keep lane detail snapshot cleanup scoped to the active project.

Line 698 and Line 701 delete lane_detail_snapshots globally. Hydrating project B can drop cached lane details for project A, which breaks the multi-project cache isolation this PR adds.

Proposed scoped cleanup
-      let laneIds = orderedLanes.map(\.id)
-      if laneIds.isEmpty {
-        try exec("delete from lane_detail_snapshots")
+      if orderedLaneIds.isEmpty {
+        _ = try execute("""
+          delete from lane_detail_snapshots
+           where lane_id in (select id from lanes where project_id = ?)
+        """) { statement in
+          try bindText(projectId, to: statement, index: 1)
+        }
       } else {
-        try exec("""
+        _ = try execute("""
           delete from lane_detail_snapshots
-           where not exists (
-             select 1
-               from temp_hydrated_lane_ids hydrated
-              where hydrated.id = lane_detail_snapshots.lane_id
-           )
-        """)
+           where lane_id in (select id from lanes where project_id = ?)
+             and not exists (
+               select 1
+                 from temp_hydrated_lane_ids hydrated
+                where hydrated.id = lane_detail_snapshots.lane_id
+             )
+        """) { statement in
+          try bindText(projectId, to: statement, index: 1)
+        }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Services/Database.swift` around lines 696 - 708, The delete
currently removes lane_detail_snapshots globally; restrict it to the active
project by scoping the DELETE with the project identifier used for hydration.
Update the exec SQL (the branches that delete from lane_detail_snapshots and the
branch using temp_hydrated_lane_ids) to include a condition that matches the
current project (e.g., lane_detail_snapshots.project_id = :currentProjectId or
join/filter by hydrated.project_id), and pass the project id from the same
context that produced orderedLanes so deletions only affect snapshots for that
project.

1917-1936: ⚠️ Potential issue | 🟠 Major

Filter queue landing state by the active project.

queue_landing_state includes project_id and even has a project index, but this fetch reads every queue. Project switching can therefore show queue state from another desktop project.

Proposed project-scoped queue query
   func fetchQueueStates() -> [QueueLandingState] {
+    guard let projectId = currentProjectId() else { return [] }
     let sql = """
@@
         from queue_landing_state q
-        left join pr_groups g on g.id = q.group_id
+        left join pr_groups g on g.id = q.group_id and g.project_id = q.project_id
+       where q.project_id = ?
        order by q.updated_at desc, q.started_at desc
     """
 
-    return query(sql) { statement in
+    return query(sql, bind: { [self] statement in
+      try self.bindText(projectId, to: statement, index: 1)
+    }) { statement in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Services/Database.swift` around lines 1917 - 1936, The
fetchQueueStates function currently selects every row from queue_landing_state
causing cross-project leakage; modify the SQL in fetchQueueStates to include a
WHERE q.project_id = ? (or named parameter) and pass the current project's id
into the query so results are scoped to the active project; update the
fetchQueueStates call sites if needed to provide the project id or read the
active project id from the same context used elsewhere (refer to function
fetchQueueStates and the queue_landing_state.project_id column and its index).

1648-1662: ⚠️ Potential issue | 🟡 Minor

Scope PR auxiliary joins to the active PR’s project.

The base PR rows are filtered by pr.project_id, but Lines 1650-1661 can still attach group or integration metadata from projection rows whose project_id differs. Keep those joins project-constrained so stale projection rows cannot leak workflow state across projects.

Proposed join constraints
-        left join pr_group_members gm on gm.pr_id = pr.id
-        left join pr_groups g on g.id = gm.group_id
+        left join pr_group_members gm on gm.pr_id = pr.id
+        left join pr_groups g on g.id = gm.group_id and g.project_id = pr.project_id
@@
-        ) group_counts on group_counts.group_id = gm.group_id
+        ) group_counts on group_counts.group_id = g.id
@@
-      ? "left join integration_proposals ip on ip.linked_pr_id = pr.id"
+      ? "left join integration_proposals ip on ip.linked_pr_id = pr.id and ip.project_id = pr.project_id"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Services/Database.swift` around lines 1648 - 1662, The
prGroupJoins and integrationJoin blocks can attach rows from other projects;
modify the join predicates in the prGroupJoins string (references:
pr_group_members gm, pr_groups g, group_counts) to include a project constraint
matching pr.project_id (e.g. join on gm.pr_id = pr.id AND gm.project_id =
pr.project_id and likewise ensure g and group_counts are scoped to gm.project_id
or pr.project_id), and update integrationJoin (integration_proposals ip) to
include ip.project_id = pr.project_id (or ip.linked_pr_id = pr.id AND
ip.project_id = pr.project_id) so both hasPrGroupContext and
hasIntegrationWorkflowContext paths only pull rows from the same project as pr.

1008-1092: ⚠️ Potential issue | 🟠 Major

Reject PR hydration payloads for a different project before deleting stale rows.

Line 1036 can insert PRs under pr.projectId, but Line 1092 deletes stale rows using the active projectId. If a delayed project-A hydration lands after switching to project B, it can insert A’s PRs while deleting B’s PRs.

Proposed mismatch guard
+      let scopedPrs = payload.prs.filter { pr in
+        pr.projectId.isEmpty || pr.projectId == projectId
+      }
+      guard scopedPrs.count == payload.prs.count else {
+        throw sqliteError("Pull request hydration payload does not match the active project.")
+      }
+      let scopedPrIds = Set(scopedPrs.map(\.id))
+
-      for pr in payload.prs {
+      for pr in scopedPrs {
         _ = try execute("""
           insert into pull_requests(
@@
-          try bindText(pr.projectId.isEmpty ? projectId : pr.projectId, to: statement, index: 2)
+          try bindText(projectId, to: statement, index: 2)
@@
-      for snapshot in payload.snapshots {
+      for snapshot in payload.snapshots where scopedPrIds.contains(snapshot.prId) {
@@
-      try deleteStalePullRequestRows(projectId: projectId, keeping: payload.prs.map(\.id))
+      try deleteStalePullRequestRows(projectId: projectId, keeping: scopedPrs.map(\.id))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Services/Database.swift` around lines 1008 - 1092, This code can
insert PRs using pr.projectId while later calling
deleteStalePullRequestRows(projectId: projectId), risking cross-project
deletion; add a guard at the start of the hydration handler to reject the
payload when payload.projectId (or any non-empty pr.projectId values) does not
match the active projectId: check payload.projectId != "" && payload.projectId
!= projectId and/or ensure all payload.prs[].projectId (if set) equal projectId,
and if any mismatch throw or return an error before performing inserts into
pull_requests, pull_request_snapshots, or calling deleteStalePullRequestRows;
update the handler surrounding payload.prs, payload.snapshots, and the
deleteStalePullRequestRows call to enforce this guard.

1161-1245: ⚠️ Potential issue | 🟠 Major

Require an active project and scope incoming workspaces before writing.

Line 1163 allows projectId to be nil, and Lines 1216-1245 upsert every incoming workspace even when its lane/root does not belong to the active project. The stale-delete path is scoped, but the write path is not.

Proposed active-project guard and scoped writes
   func replaceFilesWorkspaces(_ workspaces: [FilesWorkspace]) throws {
     guard tableExists("files_workspaces") else { return }
-    let projectId = currentProjectId()
-    let projectRoot = projectId.flatMap { id in
+    guard let projectId = currentProjectId() else {
+      throw sqliteError(SyncHydrationMessaging.waitingForProjectData)
+    }
+    let projectRoot = queryString("select root_path from projects where id = ? limit 1", bind: { [self] statement in
+      try self.bindText(projectId, to: statement, index: 1)
+    })
-      queryString("select root_path from projects where id = ? limit 1", bind: { [self] statement in
-        try self.bindText(id, to: statement, index: 1)
-      })
-    }
     let activeLaneIds = Set(query("select id from lanes where project_id = ?", bind: { [self] statement in
-      if let projectId {
-        try self.bindText(projectId, to: statement, index: 1)
-      } else {
-        sqlite3_bind_null(statement, 1)
-      }
+      try self.bindText(projectId, to: statement, index: 1)
     }) { statement in
       stringValue(statement, index: 0) ?? ""
     })
+    let scopedWorkspaces = workspaces.filter { workspace in
+      if let laneId = workspace.laneId {
+        return activeLaneIds.contains(laneId)
+      }
+      guard workspace.kind == "primary", let projectRoot else { return false }
+      return workspace.rootPath == projectRoot
+    }
     try exec("begin immediate")
     do {
-      let incomingIds = Set(workspaces.map(\.id))
+      let incomingIds = Set(scopedWorkspaces.map(\.id))
@@
-      for workspace in workspaces {
+      for workspace in scopedWorkspaces {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Services/Database.swift` around lines 1161 - 1245, The
replaceFilesWorkspaces function currently allows a nil projectId and
unconditionally upserts all incoming workspaces; change it to require an active
project (guard let projectId / projectRoot) and before the upsert loop filter
the incoming workspaces to a scopedWorkspaces set that only includes workspaces
whose laneId is in activeLaneIds or whose kind == "primary" and rootPath ==
projectRoot, then iterate over scopedWorkspaces for the insert/update and only
delete staleIds computed against scopedExistingIds as already done; reference
replaceFilesWorkspaces, projectId, projectRoot, activeLaneIds,
scopedExistingIds, staleIds and the workspaces insertion loop.
🧹 Nitpick comments (4)
apps/desktop/src/main/services/missions/missionPreflightService.ts (2)

754-768: Optional: flatten the summary nested ternary for readability.

The indentation on lines 762-767 drops a level between the warningMissingComputerUseKinds and fallbackOnlyKinds branches, which reads like a parse-level fall-through even though the semantics are correct. Extracting this into a small helper (or a sequence of if/early-return statements building the string) would make the precedence of blocked > warning-only > fallback-only > external-backend obvious at a glance and keep it in sync with the matching branching at line 331-339 in the checklist item.

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

In `@apps/desktop/src/main/services/missions/missionPreflightService.ts` around
lines 754 - 768, The nested ternary in the computerUse.summary field is hard to
read; replace it with a small helper or sequential if/return logic that enforces
the precedence blocked > warning-only > fallback-only > external-backend. Locate
the computerUse object (the summary property) and implement a function like
computeComputerUseSummary(requiredComputerUseKinds, computerUseBlocked,
blockingMissingComputerUseKinds, warningMissingComputerUseKinds,
fallbackOnlyKinds, availableExternalBackends) or inline if/else that returns the
same strings: first return the no-requirement message when
requiredComputerUseKinds.length === 0, then if computerUseBlocked return the
blocked message using blockingMissingComputerUseKinds (or fall back to
requiredComputerUseKinds), then if warningMissingComputerUseKinds.length > 0
return the warning message, then if fallbackOnlyKinds.length > 0 return the
fallback-only message, otherwise return the external-backends message using
availableExternalBackends.join(", ").

367-400: Capability gate now correctly honors backend coverage and fallback policy — LGTM.

browserEvidenceCoveredByBackend gates both the tools/vision descriptor check (line 373-374) and the unresolved-descriptor check (line 381), resolving the two earlier false-negatives for external-backend-satisfied proof and runtime-discovered models. Per-requirement messages at line 386-399 route via capabilityFallback correctly.

Minor: at line 391, localCapability = backendStatus ? null : getCapabilityForRequirement(requirementKind). When backendStatus is present but its localFallback doesn't cover the kind, localCapability is forced to null, so localStateReason (lines 393-395) always resolves to "missing required tooling" — the "blocked by platform support" reason is effectively unreachable whenever computerUseArtifactBrokerService is wired. If the finer-grained signal matters in the broker-present path, consider also consulting getCapabilityForRequirement(requirementKind) as a fallback for the reason string only.

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

In `@apps/desktop/src/main/services/missions/missionPreflightService.ts` around
lines 367 - 400, The localCapability assignment currently sets localCapability =
backendStatus ? null : getCapabilityForRequirement(requirementKind) which makes
the "blocked_by_capability" reason unreachable when backendStatus exists; change
this so when backendStatus exists but its localFallback does not cover the
requirementKind you still consult getCapabilityForRequirement(requirementKind)
(or at least use its .state for determining localStateReason) — i.e., compute
localCapability = backendStatus ?
(backendStatus.localFallback?.coversRequirement ? null :
getCapabilityForRequirement(requirementKind)) :
getCapabilityForRequirement(requirementKind), then keep the existing
localDetail/localStateReason logic that checks localCapability?.state ===
"blocked_by_capability" to produce the correct reason string.
apps/desktop/src/main/services/sync/syncHostService.ts (1)

1056-1079: Protocol-level validation for project switch requests is optional defensive programming, not required.

SyncProjectSwitchRequestPayload fields are optional, and prepareMobileSyncProjectConnection safely handles empty or missing payloads. When both projectId and rootPath are null/undefined, the provider correctly returns an error response ("That project is not available from this desktop.") rather than causing undefined behavior. Adding explicit validation at the handler boundary would be a defensive improvement, but it is not required to prevent a security issue.

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

In `@apps/desktop/src/main/services/sync/syncHostService.ts` around lines 1056 -
1079, Remove any protocol-level validation in handleProjectSwitchRequest that
rejects requests when SyncProjectSwitchRequestPayload fields (e.g.,
projectId/rootPath) are missing; instead, forward the raw payload to
args.projectCatalogProvider.prepareProjectConnection (as currently done) and let
that provider return the appropriate error response. Ensure no added checks on
payload properties remain in handleProjectSwitchRequest so
prepareProjectConnection handles invalid or empty payloads.
apps/ios/ADE/Services/SyncService.swift (1)

3644-3655: Redundant project_catalog_request right after hello.

applyHelloPayload already decodes projects from the hello_ok envelope (lines 3716‑3719) and feeds them through applyRemoteProjectCatalog. Unconditionally calling refreshRemoteProjectCatalog() here then fires a second project_catalog_request round‑trip for the same data on every successful connect/reconnect. Consider gating this on the hello payload not having supplied projects, or dropping it and relying on the hello‑provided catalog plus the on‑demand refresh in showProjectHome().

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

In `@apps/ios/ADE/Services/SyncService.swift` around lines 3644 - 3655,
applyHelloPayload already decodes projects from the hello_ok envelope and calls
applyRemoteProjectCatalog, so refreshRemoteProjectCatalog() right after connect
causes a redundant project_catalog_request; update the connect flow to skip
calling refreshRemoteProjectCatalog() when applyHelloPayload provided projects
(e.g. detect presence of projects in the hello payload or have applyHelloPayload
return a flag/enum indicating whether it applied a catalog) and only call
refreshRemoteProjectCatalog() when no projects were included, or remove the
unconditional refresh and rely on the hello-provided catalog plus on-demand
refresh in showProjectHome(); refer to applyHelloPayload,
applyRemoteProjectCatalog, refreshRemoteProjectCatalog, showProjectHome and the
hello_ok/projects handling to implement the gate or removal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/main.ts`:
- Around line 3628-3655: If ensureProjectContextForMobileSync(targetRoot) may
create a new (cold) context that should be cleaned up on failure, wrap the
initialization and status checks so failures clean up newly-created contexts:
record whether the context was newly created (e.g., a boolean newContext =
ctx.isNew || set immediately after calling ensureProjectContextForMobileSync),
then if ctx.syncService.initialize(), ctx.syncService.getStatus(), or readiness
checks throw, if newContext then either call the context teardown method (e.g.,
ctx.close() or ctx.dispose() if present) or remove any references (delete
mobileSyncHandoffLeases/mobileSyncHandoffLeaseTimers and call
scheduleProjectContextRebalance()) before rethrowing the error; ensure you
reference ensureProjectContextForMobileSync, ctx.syncService.initialize,
getStatus, mobileSyncHandoffLeases, mobileSyncHandoffLeaseTimers,
scheduleProjectContextRebalance, and projectLastActivatedAt when implementing
this cleanup.

In `@apps/desktop/src/renderer/components/app/TopBar.tsx`:
- Around line 116-138: The polling currently swallows errors from
window.ade.sync.getStatus() so stale sync UI can remain; update the
refreshSyncStatus handler used in the useEffect (the function that calls
window.ade.sync.getStatus()) to setSyncSnapshot(null) when the promise rejects
(or otherwise on catch), ensuring the UI is cleared on failure; keep the
existing interval, focus listener and dispose logic (onEvent "sync-status"
handler) intact and only add the snapshot-clear in the catch path of
getStatus().

In `@apps/desktop/src/renderer/state/onboardingStore.ts`:
- Around line 154-165: startTour currently proceeds even if getTour(id) returns
undefined, causing activeTourId/activeStepIndex/activeTourCtx to be set and
updateTourStep called for a non-existent tour; change startTour to check the
result of getTour(id) immediately after trimming and return early (or handle
error) if tour is falsy before calling createTourCtx, set(...),
onboarding.updateTourStep, or runBeforeEnter, and only then proceed to
createTourCtx, set active state, call onboarding.updateTourStep, and
runBeforeEnter with tour.steps[0]; reference the startTour function, getTour,
createTourCtx, runBeforeEnter, and onboarding.updateTourStep when making this
guard.
- Around line 194-207: The prevStep handler (prevStep) runs runAfterLeave and
runBeforeEnter even when activeStepIndex is 0, causing no-op navigations to
re-run step teardown/setup; modify prevStep to early-return when activeStepIndex
<= 0 (or when nextIndex === activeStepIndex) before calling
runAfterLeave/runBeforeEnter and before updating API progress, so no lifecycle
hooks run if there is no actual step change (refer to prevStep, runAfterLeave,
runBeforeEnter, activeStepIndex, nextIndex).

In `@apps/ios/ADE/Services/SyncService.swift`:
- Around line 837-859: When rolling back in the catch block after restoring
activeProjectId/profile/token/catalog, also perform the same local-selection
cleanup that selectProject does: increment the localStateRevision (or call the
helper that bumps it), call refreshActiveSessionsAndSnapshot() to update the
Published activeSessions, and call scheduleWorkspaceSnapshotWrite() to refresh
the App Group/widget snapshot so the UI (Work tab, widgets, attention drawer)
immediately reflects the previous project; place these calls after
refreshProjectCatalog() and before changing connectionState/currentAddress so
the restored state is fully applied before reconnect logic runs.

In `@apps/ios/ADETests/ADETests.swift`:
- Around line 530-606: The fetchIntegrationProposals method in Database.swift
does not filter by currentProjectId() and will return proposals from all
projects; to fix add regression coverage in
testDatabaseScopesPullRequestReadsByActiveProject by inserting rows into the
integration_proposals table for both 'project-1' and 'project-2' (matching the
seeded PR ids), then after database.setActiveProjectId("project-1") assert that
database.fetchIntegrationProposals() returns only proposals for project-1, and
after setActiveProjectId("project-2") assert it returns only project-2
proposals; alternatively ensure fetchIntegrationProposals itself is updated to
include "where project_id = ?" using currentProjectId() to enforce project
scoping.

In `@apps/web/src/components/editorial/BackCover.tsx`:
- Around line 75-91: The two CTA anchor elements that use target="_blank" (the
anchor linking to LINKS.releases with the Download icon and the anchor linking
to LINKS.github with the Github icon) must expose the new-tab behavior to
assistive tech; update each anchor to include either an explicit aria-label that
appends "opens in a new tab" (e.g., aria-label="Download DMG, opens in a new
tab") or add an inline visually-hidden span (screen-reader-only text) such as
<span className="sr-only"> (or your project's existing hidden utility)
containing "opens in a new tab" after the visible text; keep the existing
target/rel attributes and ensure the change is plain JSX (SSR-safe) and applied
to both the LINKS.releases and LINKS.github anchors so screen readers receive
the cue.

In `@apps/web/src/components/editorial/CompetitorEquation.tsx`:
- Around line 26-28: The rowEnd calculation only multiplies stagger by
COMPETITORS.length but ignores separators/chips between competitors; update
rowEnd to use the total child count (competitors plus separators) i.e.
(COMPETITORS.length * 2 - 1) so it reads something like rowEnd = 0.1 +
((COMPETITORS.length * 2 - 1) * stagger) to ensure Row 2 starts after all Row 1
children animate.

---

Outside diff comments:
In `@apps/ios/ADE/Services/Database.swift`:
- Around line 1830-1858: The query in fetchIntegrationProposals reads from
integration_proposals without scoping to the active project, causing proposals
from other projects to appear; update fetchIntegrationProposals to guard on
currentProjectId() and add a WHERE project_id = ? clause (or equivalent
parameter binding) to the SQL and pass currentProjectId() as the bound parameter
before executing the query so only proposals for the active project are
returned.
- Around line 696-708: The delete currently removes lane_detail_snapshots
globally; restrict it to the active project by scoping the DELETE with the
project identifier used for hydration. Update the exec SQL (the branches that
delete from lane_detail_snapshots and the branch using temp_hydrated_lane_ids)
to include a condition that matches the current project (e.g.,
lane_detail_snapshots.project_id = :currentProjectId or join/filter by
hydrated.project_id), and pass the project id from the same context that
produced orderedLanes so deletions only affect snapshots for that project.
- Around line 1917-1936: The fetchQueueStates function currently selects every
row from queue_landing_state causing cross-project leakage; modify the SQL in
fetchQueueStates to include a WHERE q.project_id = ? (or named parameter) and
pass the current project's id into the query so results are scoped to the active
project; update the fetchQueueStates call sites if needed to provide the project
id or read the active project id from the same context used elsewhere (refer to
function fetchQueueStates and the queue_landing_state.project_id column and its
index).
- Around line 1648-1662: The prGroupJoins and integrationJoin blocks can attach
rows from other projects; modify the join predicates in the prGroupJoins string
(references: pr_group_members gm, pr_groups g, group_counts) to include a
project constraint matching pr.project_id (e.g. join on gm.pr_id = pr.id AND
gm.project_id = pr.project_id and likewise ensure g and group_counts are scoped
to gm.project_id or pr.project_id), and update integrationJoin
(integration_proposals ip) to include ip.project_id = pr.project_id (or
ip.linked_pr_id = pr.id AND ip.project_id = pr.project_id) so both
hasPrGroupContext and hasIntegrationWorkflowContext paths only pull rows from
the same project as pr.
- Around line 1008-1092: This code can insert PRs using pr.projectId while later
calling deleteStalePullRequestRows(projectId: projectId), risking cross-project
deletion; add a guard at the start of the hydration handler to reject the
payload when payload.projectId (or any non-empty pr.projectId values) does not
match the active projectId: check payload.projectId != "" && payload.projectId
!= projectId and/or ensure all payload.prs[].projectId (if set) equal projectId,
and if any mismatch throw or return an error before performing inserts into
pull_requests, pull_request_snapshots, or calling deleteStalePullRequestRows;
update the handler surrounding payload.prs, payload.snapshots, and the
deleteStalePullRequestRows call to enforce this guard.
- Around line 1161-1245: The replaceFilesWorkspaces function currently allows a
nil projectId and unconditionally upserts all incoming workspaces; change it to
require an active project (guard let projectId / projectRoot) and before the
upsert loop filter the incoming workspaces to a scopedWorkspaces set that only
includes workspaces whose laneId is in activeLaneIds or whose kind == "primary"
and rootPath == projectRoot, then iterate over scopedWorkspaces for the
insert/update and only delete staleIds computed against scopedExistingIds as
already done; reference replaceFilesWorkspaces, projectId, projectRoot,
activeLaneIds, scopedExistingIds, staleIds and the workspaces insertion loop.

---

Nitpick comments:
In `@apps/desktop/src/main/services/missions/missionPreflightService.ts`:
- Around line 754-768: The nested ternary in the computerUse.summary field is
hard to read; replace it with a small helper or sequential if/return logic that
enforces the precedence blocked > warning-only > fallback-only >
external-backend. Locate the computerUse object (the summary property) and
implement a function like computeComputerUseSummary(requiredComputerUseKinds,
computerUseBlocked, blockingMissingComputerUseKinds,
warningMissingComputerUseKinds, fallbackOnlyKinds, availableExternalBackends) or
inline if/else that returns the same strings: first return the no-requirement
message when requiredComputerUseKinds.length === 0, then if computerUseBlocked
return the blocked message using blockingMissingComputerUseKinds (or fall back
to requiredComputerUseKinds), then if warningMissingComputerUseKinds.length > 0
return the warning message, then if fallbackOnlyKinds.length > 0 return the
fallback-only message, otherwise return the external-backends message using
availableExternalBackends.join(", ").
- Around line 367-400: The localCapability assignment currently sets
localCapability = backendStatus ? null :
getCapabilityForRequirement(requirementKind) which makes the
"blocked_by_capability" reason unreachable when backendStatus exists; change
this so when backendStatus exists but its localFallback does not cover the
requirementKind you still consult getCapabilityForRequirement(requirementKind)
(or at least use its .state for determining localStateReason) — i.e., compute
localCapability = backendStatus ?
(backendStatus.localFallback?.coversRequirement ? null :
getCapabilityForRequirement(requirementKind)) :
getCapabilityForRequirement(requirementKind), then keep the existing
localDetail/localStateReason logic that checks localCapability?.state ===
"blocked_by_capability" to produce the correct reason string.

In `@apps/desktop/src/main/services/sync/syncHostService.ts`:
- Around line 1056-1079: Remove any protocol-level validation in
handleProjectSwitchRequest that rejects requests when
SyncProjectSwitchRequestPayload fields (e.g., projectId/rootPath) are missing;
instead, forward the raw payload to
args.projectCatalogProvider.prepareProjectConnection (as currently done) and let
that provider return the appropriate error response. Ensure no added checks on
payload properties remain in handleProjectSwitchRequest so
prepareProjectConnection handles invalid or empty payloads.

In `@apps/ios/ADE/Services/SyncService.swift`:
- Around line 3644-3655: applyHelloPayload already decodes projects from the
hello_ok envelope and calls applyRemoteProjectCatalog, so
refreshRemoteProjectCatalog() right after connect causes a redundant
project_catalog_request; update the connect flow to skip calling
refreshRemoteProjectCatalog() when applyHelloPayload provided projects (e.g.
detect presence of projects in the hello payload or have applyHelloPayload
return a flag/enum indicating whether it applied a catalog) and only call
refreshRemoteProjectCatalog() when no projects were included, or remove the
unconditional refresh and rely on the hello-provided catalog plus on-demand
refresh in showProjectHome(); refer to applyHelloPayload,
applyRemoteProjectCatalog, refreshRemoteProjectCatalog, showProjectHome and the
hello_ok/projects handling to implement the gate or removal.
🪄 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: 56ebdaee-ddcc-4722-9816-82afe91dd593

📥 Commits

Reviewing files that changed from the base of the PR and between 8aceeb6 and 61f7464.

📒 Files selected for processing (32)
  • apps/ade-cli/src/adeRpcServer.test.ts
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/computerUse/computerUseArtifactBrokerService.ts
  • apps/desktop/src/main/services/missions/missionPreflightService.test.ts
  • apps/desktop/src/main/services/missions/missionPreflightService.ts
  • apps/desktop/src/main/services/missions/missionService.test.ts
  • apps/desktop/src/main/services/missions/missionService.ts
  • apps/desktop/src/main/services/orchestrator/aiOrchestratorService.test.ts
  • apps/desktop/src/main/services/sync/syncHostService.ts
  • apps/desktop/src/renderer/components/app/TopBar.tsx
  • apps/desktop/src/renderer/components/onboarding/HelpMenu.tsx
  • apps/desktop/src/renderer/components/onboarding/tour/TourOverlay.tsx
  • apps/desktop/src/renderer/onboarding/docsLinks.test.ts
  • apps/desktop/src/renderer/onboarding/docsLinks.ts
  • apps/desktop/src/renderer/state/onboardingStore.ts
  • apps/desktop/src/shared/types/sync.ts
  • apps/ios/ADE/App/ContentView.swift
  • apps/ios/ADE/Services/Database.swift
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADETests/ADETests.swift
  • apps/web/public/mockup.html
  • apps/web/src/app/layout/SiteLayout.tsx
  • apps/web/src/app/pages/HomePage.tsx
  • apps/web/src/components/editorial/AnnotatedFigure.tsx
  • apps/web/src/components/editorial/BackCover.tsx
  • apps/web/src/components/editorial/CompetitorEquation.tsx
  • apps/web/src/components/editorial/Cutout.tsx
  • apps/web/src/components/editorial/FeatureGrid.tsx
  • apps/web/src/components/editorial/IndexPage.tsx
  • apps/web/src/components/editorial/Masthead.tsx
  • apps/web/src/styles/globals.css
✅ Files skipped from review due to trivial changes (6)
  • apps/desktop/src/main/services/missions/missionService.ts
  • apps/web/public/mockup.html
  • apps/web/src/styles/globals.css
  • apps/web/src/components/editorial/Cutout.tsx
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/desktop/src/renderer/onboarding/docsLinks.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/web/src/app/layout/SiteLayout.tsx
  • apps/desktop/src/renderer/onboarding/docsLinks.test.ts
  • apps/ade-cli/src/adeRpcServer.test.ts
  • apps/desktop/src/renderer/components/onboarding/HelpMenu.tsx
  • apps/web/src/components/editorial/Masthead.tsx
  • apps/desktop/src/main/services/computerUse/computerUseArtifactBrokerService.ts
  • apps/web/src/components/editorial/IndexPage.tsx
  • apps/web/src/components/editorial/AnnotatedFigure.tsx

Comment thread apps/desktop/src/main/main.ts Outdated
Comment thread apps/desktop/src/renderer/components/app/TopBar.tsx
Comment thread apps/desktop/src/renderer/state/onboardingStore.ts Outdated
Comment thread apps/desktop/src/renderer/state/onboardingStore.ts
Comment thread apps/ios/ADE/Services/SyncService.swift
Comment thread apps/ios/ADETests/ADETests.swift
Comment thread apps/web/src/components/editorial/BackCover.tsx
Comment thread apps/web/src/components/editorial/CompetitorEquation.tsx
Comment thread apps/ios/ADE/Services/SyncService.swift
Comment on lines +162 to +163
if (step.actIntro) handleNext();
else handleDismiss();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

For actIntro steps this new branch makes TourOverlay call handleNext() on Escape, but the rendered ActIntro already installs its own Escape listener and also calls onSkip={handleNext}. One keypress therefore advances twice, which skips the first real step after each intro (for example the many act*.intro beats in @apps/desktop/src/renderer/onboarding/tours/firstJourneyTour.ts) or can finish the tour early when the intro is last. Let only one layer own Escape for act-intro mode.

// apps/desktop/src/renderer/components/onboarding/tour/TourOverlay.tsx
if (e.key === "Escape") {
  e.preventDefault();
  if (step.actIntro) handleNext();
  else handleDismiss();
}
Suggested change
if (step.actIntro) handleNext();
else handleDismiss();
if (!step.actIntro) handleDismiss();

{ name: "CTO (chief technical officer)", page: "15", href: "/cto/overview" },
{ name: "CLI · ade", page: "12", href: "/tools/project-home" },
{ name: "Dispatch", page: "17", href: "/missions/creating" },
{ name: "Figma integration", page: "32", href: "/ai-tools/cursor" },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

This new index entry sends users to https://www.ade-app.dev/docs/ai-tools/cursor, and that URL currently resolves to Mintlify's 404 Page Not Found page rather than feature documentation. The rest of this redesign uses the index as a navigation surface, so shipping a dead entry creates a broken outbound link directly from the homepage.

// apps/web/src/components/editorial/IndexPage.tsx
const ENTRIES: Entry[] = [
  { name: "Figma integration", page: "32", href: "/ai-tools/cursor" },
  // ...
];

Comment thread apps/ios/ADE/Services/SyncService.swift Outdated
}
return (try? decode(actions, as: [SyncRemoteCommandDescriptor].self)) ?? []
}()
if let projects = payload["projects"],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

remoteProjectCatalog is new persistent state, but hello_ok only replaces it when the payload happens to include projects. If the phone first connects to a desktop that advertises a catalog and then reconnects to a desktop that does not (or an older host on the same pairing path), the old remote-only entries stay merged into projects, so Project Home shows projects from the wrong computer and lets the user tap stale targets. The changed code here is the gap:

// apps/ios/ADE/Services/SyncService.swift
if let projects = payload["projects"],
   let catalog = try? decode(["projects": projects], as: MobileProjectCatalogPayload.self) {
  applyRemoteProjectCatalog(catalog)
}

Clear the remote catalog when hello_ok has no projects field so host changes cannot leak the previous desktop's catalog into the current session.

Suggested change
if let projects = payload["projects"],
if let projects = payload["projects"],
let catalog = try? decode(["projects": projects], as: MobileProjectCatalogPayload.self) {
applyRemoteProjectCatalog(catalog)
} else {
applyRemoteProjectCatalog(MobileProjectCatalogPayload(projects: []))
}

Comment thread apps/ios/ADE/Services/SyncService.swift
Comment thread apps/ios/ADE/Services/SyncService.swift
if (hasLocalComputerUseCoverage(requirementKind)) continue;
const localCapability = backendStatus ? null : getCapabilityForRequirement(requirementKind);
const localDetail = backendStatus?.localFallback?.detail ?? localCapability?.detail;
const localStateReason = localCapability?.state === "blocked_by_capability"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

When the desktop runs through the normal production path, createMissionPreflightService is instantiated with a real computerUseArtifactBrokerService, so this branch always forces localCapability to null. That means the new capability error can never classify a failure as blocked_by_capability, even though @apps/desktop/src/main/services/computerUse/localComputerUse.ts reports that exact state on non-macOS platforms. ts // apps/desktop/src/main/services/missions/missionPreflightService.ts const localCapability = backendStatus ? null : getCapabilityForRequirement(requirementKind); const localDetail = backendStatus?.localFallback?.detail ?? localCapability?.detail; const localStateReason = localCapability?.state === "blocked_by_capability" ? "blocked by platform support" : "missing required tooling"; On Linux, required proof failures will therefore tell operators that the machine is missing tooling instead of explaining that ADE local capture is unsupported on the platform, which sends them toward the wrong remediation. Preserve the local fallback state in ComputerUseBackendStatus (or re-read getLocalComputerUseCapabilities() here) and derive the message from that state before building the checklist entry.

Comment on lines +391 to +395
const localCapability = backendStatus ? null : getCapabilityForRequirement(requirementKind);
const localDetail = backendStatus?.localFallback?.detail ?? localCapability?.detail;
const localStateReason = localCapability?.state === "blocked_by_capability"
? "blocked by platform support"
: "missing required tooling";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

When computerUseArtifactBrokerService is wired in production, this branch forces localCapability to null, so the newly added capability diagnostics can never emit the blocked by platform support path even on platforms where getLocalComputerUseCapabilities() returns blocked_by_capability (for example Linux, per @apps/desktop/src/main/services/computerUse/localComputerUse.ts). That means a blocked mission is reported as if the user is merely missing tools, which sends operators toward the wrong remediation for a hard failure.

// apps/desktop/src/main/services/missions/missionPreflightService.ts
const localCapability = backendStatus ? null : getCapabilityForRequirement(requirementKind);
const localDetail = backendStatus?.localFallback?.detail ?? localCapability?.detail;
const localStateReason = localCapability?.state === "blocked_by_capability"
  ? "blocked by platform support"
  : "missing required tooling";

Always consulting getCapabilityForRequirement() for the message path preserves the platform-blocked vs tooling-missing distinction without changing the broker coverage logic.

Suggested change
const localCapability = backendStatus ? null : getCapabilityForRequirement(requirementKind);
const localDetail = backendStatus?.localFallback?.detail ?? localCapability?.detail;
const localStateReason = localCapability?.state === "blocked_by_capability"
? "blocked by platform support"
: "missing required tooling";
const localCapability = getCapabilityForRequirement(requirementKind);
const localDetail = localCapability?.detail ?? backendStatus?.localFallback?.detail;
const localStateReason = localCapability?.state === "blocked_by_capability"
? "blocked by platform support"
: "missing required tooling";

Comment thread apps/ade-cli/src/adeRpcServer.ts
Comment thread apps/ios/ADE/Services/SyncService.swift Outdated
connectionState == .connected || connectionState == .syncing
else { return }

guard activeProjectId != nil else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟠 High] [🔵 Bug]

On a fresh phone or after clearing the saved selection, connecting to an older desktop that does not advertise projectCatalog now strands the app on Project Home. applyHelloPayload() falls back to refreshProjectCatalog(), which normalizes with allowSingleProjectFallback: false, so no project is auto-selected; then performInitialHydration() exits before requesting any data, and selectProject() refuses manual selection while the legacy host is already connected. The result is a successful socket connection that can never hydrate lanes/work/PRs until the user reconnects to a newer desktop.

// apps/ios/ADE/Services/SyncService.swift
guard activeProjectId != nil else {
  refreshProjectCatalog()
  return
}

Comment thread apps/web/src/app/layout/SiteLayout.tsx
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: 13

♻️ Duplicate comments (1)
apps/ios/ADE/Services/SyncService.swift (1)

4507-4518: ⚠️ Potential issue | 🟠 Major

Don’t hydrate no-catalog hosts under a stale persisted project ID.

When activeProjectId is restored from UserDefaults, this closure accepts it if the project exists locally, even if the connected host did not advertise a catalog to validate it. The subsequent live refreshes can then write the current host’s data into an old project scope. Make persisted selections host-scoped or clear/rederive the active project before initial hydration when supportsProjectCatalog == false.

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

In `@apps/ios/ADE/Services/SyncService.swift` around lines 4507 - 4518, The
current closure passed to InitialHydrationGate.waitForProjectRow accepts a
persisted activeProjectId from UserDefaults even when the connected host did not
advertise a catalog, allowing stale project scopes to be used; change the logic
so that when supportsProjectCatalog == false you do not accept a persisted
activeProjectId — either clear/derive self.activeProjectId (e.g., set to nil or
rederive from database.currentProjectId) before calling
InitialHydrationGate.waitForProjectRow or update the closure to also check
supportsProjectCatalog and refuse the persisted id if the host has no catalog;
ensure you reference activeProjectId, supportsProjectCatalog,
InitialHydrationGate.waitForProjectRow, and
database.hasProject/database.currentProjectId in the update.
🧹 Nitpick comments (2)
apps/web/src/components/editorial/BackCover.tsx (1)

4-4: Unused import: IPhoneFrame.

IPhoneFrame is imported but never referenced in this file (the iPhone mockup at lines 151–173 is rendered inline with raw divs). Drop the import to avoid dead code / potential lint failures.

-import { IPhoneFrame } from "./IPhoneFrame";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/editorial/BackCover.tsx` at line 4, Remove the unused
import IPhoneFrame from BackCover.tsx: delete the line importing IPhoneFrame
since the component is not referenced (the iPhone mockup uses inline divs at the
current render) and ensure no other references to the IPhoneFrame symbol remain
in this file so lint errors are resolved.
apps/ios/ADETests/ADETests.swift (1)

666-702: Cover project-level workspaces without laneId.

This scoping regression only seeds lane-backed workspaces, so it won’t catch stale root/project workspaces leaking across active projects. Add one laneId: nil workspace per project and assert only the active project’s root workspace is returned after switching.

🧪 Proposed coverage expansion
     try database.replaceFilesWorkspaces([
+      FilesWorkspace(
+        id: "workspace-project-one",
+        kind: "project",
+        laneId: nil,
+        name: "Project One",
+        rootPath: "/tmp/project-one",
+        isReadOnlyByDefault: true,
+        mobileReadOnly: true
+      ),
       FilesWorkspace(
         id: "workspace-one",
         kind: "worktree",
         laneId: "lane-one",
@@
     try database.replaceFilesWorkspaces([
+      FilesWorkspace(
+        id: "workspace-project-two",
+        kind: "project",
+        laneId: nil,
+        name: "Project Two",
+        rootPath: "/tmp/project-two",
+        isReadOnlyByDefault: true,
+        mobileReadOnly: true
+      ),
       FilesWorkspace(
         id: "workspace-two",
         kind: "worktree",
         laneId: "lane-two",
@@
     XCTAssertEqual(database.fetchLanes(includeArchived: true).map(\.id), ["lane-two"])
     XCTAssertEqual(database.fetchSessions().map(\.id), ["session-two"])
-    XCTAssertEqual(database.listWorkspaces().map(\.id), ["workspace-two"])
+    XCTAssertEqual(Set(database.listWorkspaces().map(\.id)), Set(["workspace-project-two", "workspace-two"]))
 
     database.setActiveProjectId("project-1")
     XCTAssertEqual(database.fetchLanes(includeArchived: true).map(\.id), ["lane-one"])
     XCTAssertEqual(database.fetchSessions().map(\.id), ["session-one"])
-    XCTAssertEqual(database.listWorkspaces().map(\.id), ["workspace-one"])
+    XCTAssertEqual(Set(database.listWorkspaces().map(\.id)), Set(["workspace-project-one", "workspace-one"]))

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

Also applies to: 688-707

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

In `@apps/ios/ADETests/ADETests.swift` around lines 666 - 702, Add coverage for
project-level (root) workspaces missing a laneId by seeding one FilesWorkspace
with laneId: nil for each project before switching active project; update the
calls around replaceFilesWorkspaces and setActiveProjectId (referencing
FilesWorkspace and database.replaceFilesWorkspaces /
database.setActiveProjectId) so that after switching you assert
database.listWorkspaces() only includes the root workspace for the active
project (in addition to existing lane-backed workspace assertions using
fetchLanes and fetchSessions); ensure one nil laneId workspace per project is
inserted and the expected IDs are asserted after the active project change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/main.ts`:
- Around line 3599-3613: The current find uses an OR between requestedRoot and
requestedProjectId so a stale root or id can incorrectly win; change the
matching logic in the catalogEntry lookup (the catalog.projects.find that uses
normalizeProjectRoot, requestedRoot, and requestedProjectId) to require both
identifiers when both are provided (i.e., if requestedRoot != null &&
requestedProjectId != null then require entryRoot === requestedRoot AND entry.id
=== requestedProjectId), otherwise fall back to single-constraint matching as
before; also update the projectContexts fallback loop that sets catalogEntry
(the loop using projectContexts, ctx.projectId and normalizeProjectRoot and
mobileProjectSummaryForContext) to respect requestedRoot when provided so the
chosen context/root agrees with requestedProjectId and requestedRoot.
- Around line 3628-3679: The failure path can clear a lease or close a context
created by a concurrent successful request because hadExistingContext is
captured before awaiting ensureProjectContextForMobileSync; to fix, generate a
unique owner token (e.g., const leaseOwner = Symbol() or random id) before
calling ensureProjectContextForMobileSync, store the owner alongside the lease
in mobileSyncHandoffLeases (e.g., {owner, expiresAt}), set the corresponding
timer entry in mobileSyncHandoffLeaseTimers with that owner, and in the catch
cleanup only remove timers/leases or call closeProjectContext(targetRoot) if the
stored owner matches leaseOwner; alternatively (or additionally) serialize
prepareMobileSyncProjectConnection per targetRoot (a per-target mutex around
ensureProjectContextForMobileSync / ctx.syncService.initialize / getStatus) to
avoid concurrent races.
- Around line 3517-3528: The function mobileProjectSummaryForRecent currently
returns persisted summaries with isCached: false; change it to isCached: true so
persisted "recent" rows are marked as cached and remain distinguishable from
live AppContext summaries; update the return in mobileProjectSummaryForRecent
(which builds a SyncMobileProjectSummary) to set isCached: true (leave other
fields as-is so live context can still overwrite).

In `@apps/desktop/src/main/services/missions/missionPreflightService.ts`:
- Around line 367-385: The predicate computing browserEvidenceCoveredByBackend
is currently checking all evidenceRequirements; narrow it to only the
browser/screenshot-flavored requirements before calling
hasExternalComputerUseCoverage/hasLocalComputerUseCoverage (e.g. filter
evidenceRequirements where kind === "screenshot" or maps to a
browser/computer-use kind), so browserEvidenceCoveredByBackend reflects only
browser-related coverage; update the logic that sets
browserEvidenceCoveredByBackend (and any helpers it uses:
hasExternalComputerUseCoverage, hasLocalComputerUseCoverage) and keep the rest
of the checks that reference requiresBrowserEvidence, descriptor,
likelyBrowserCapable, phase, capabilityWarnings/capabilityIssues and
validationGate.capabilityFallback unchanged.
- Around line 391-400: The substring check using
backendStatus?.localFallback?.detail.includes("blocked_by_capability") is
fragile; remove localFallbackBlocked and stop probing localFallback.detail.
Instead determine platform-blocked solely from the structured
localCapability?.state === "blocked_by_capability" (i.e. use localCapability for
the canonical state) and adjust the localStateReason computation and resulting
message accordingly; if broker support for a structured state is later added to
backendStatus.localFallback (e.g. localFallback.state), switch to that field
rather than any detail string.
- Around line 755-769: Update the MissionPreflightResult type definition to
document the "blocking-only" semantics for computerUse.missingKinds: add a JSDoc
comment on the missingKinds field stating it contains only
blockingMissingComputerUseKinds (not warning-only kinds), and mention that the
UI/renderer consumes checklist.details for separate blocking vs warning strings
(see blockingMissingComputerUseKinds and checklist.details for context) so
consumers and external callers/telemetry don't assume missingKinds includes
non-blocking items.

In `@apps/desktop/src/renderer/components/app/TopBar.tsx`:
- Around line 28-47: deriveSyncLabel can return a “ready” message while
syncDotClass shows an error; fix by giving the error state priority: in
deriveSyncLabel (the function handling SyncRoleSnapshot), after the null check
add an early check like if (snapshot.client?.state === "error") return "Phone
sync error" so any snapshot whose client is in error returns the error label
before evaluating brain/standalone ready labels; this aligns deriveSyncLabel
with syncDotClass.
- Around line 582-625: The phone sync panel is declared aria-modal but does not
trap focus; when phoneSyncOpen is true Tab can reach background controls. Fix by
implementing a focus trap around the dialog (or use a shared dialog/focus-trap
primitive) tied to phoneSyncPanelRef and phoneSyncOpen: on open move focus into
the panel (e.g., to the first focusable inside SyncDevicesSection or a focusable
sentinel), on Tab/Shift+Tab detect when focus would leave and wrap it to the
last/first focusable, and on close restore focus. Update the dialog container
code that uses phoneSyncPanelRef, setPhoneSyncOpen, and SyncDevicesSection to
include the trap logic or replace the markup with the shared dialog component
that enforces modal focus-trapping.

In `@apps/ios/ADE/Services/SyncService.swift`:
- Around line 606-611: projectSwitchInFlightRootPath is being set to only a
whitespace-trimmed rootPath, but isSwitchingProject compares against a
normalized root (so trailing slashes cause mismatches); normalize the path
before storing by trimming whitespace and removing any trailing path
separator(s) (e.g., strip trailing "/" or platform-specific separator) and store
that value in projectSwitchInFlightRootPath whenever assigning from
project.rootPath (including the assignment inside projectSelectionTask and the
other occurrence that sets projectSwitchInFlightRootPath), so isSwitchingProject
and stored in-flight root use the same normalized representation.
- Around line 848-856: After switching the active remote project (the block that
mutates remoteProjectCatalog, calls setActiveProjectId(...) and
refreshProjectCatalog() and sets latestRemoteDbVersion = 0), also advance the
local state revision and rebuild derived local state so the UI reflects the new
project's sessions/PR snapshot immediately; e.g. increment localStateRevision
(or call the existing helper that bumps it) and invoke the code-path that
rebuilds activeSessions and the widget snapshot (use your project's helpers such
as rebuildActiveSessions()/refreshWidgetSnapshot() or
refreshDerivedLocalState()) right after setting latestRemoteDbVersion so the app
doesn't keep showing the previous project's data.

In `@apps/web/src/components/editorial/IndexPage.tsx`:
- Around line 7-40: The index contains multiple entries that share the same page
numbers likely due to copy/paste drift (e.g., "Automations", "iOS app", "Mobile
sync" all page "28"; "Byok", "Model configuration", "Providers", "Settings" all
page "22"; "Missions", "Orchestrator", "Planning phase", "Testing phase" all
page "14"; "Context packs" & "Merge conflicts" page "26"; "Lanes" & "Worktrees
(lanes)" page "04"). Verify the intended page numbers for each entry and either
(a) consolidate entries intentionally grouped (e.g., keep "Lanes" and "Worktrees
(lanes)" on the same page) or (b) correct the mistaken page values so each
distinct doc has the correct page; update the array entries for "Automations",
"iOS app", "Mobile sync", "Byok", "Model configuration", "Providers",
"Settings", "Missions", "Orchestrator", "Planning phase", "Testing phase",
"Context packs", and "Merge conflicts" accordingly.
- Around line 6-41: Update the ENTRIES constant's entry for "Team (CTO org)"
inside the Entry[] array: change its href from "/cto/team" to the correct slug
"/cto/workers" (look for the object with name "Team (CTO org)" in the ENTRIES
array). Ensure no other fields are altered; optionally re-evaluate the "CLI ·
ade" href if you want to update that separately.
- Line 48: The reduce-motion default currently forces reduced motion during
SSR/first paint by using useReducedMotion() ?? true; decide whether you want the
conservative SSR-safe behavior or the typical framer-motion idiom and update
both occurrences in IndexPage.tsx accordingly: if you want SSR/hydration safety
keep or explicitly document useReducedMotion() ?? true; if you want animations
enabled by default on client-first renders change both to useReducedMotion() ??
false so the variable reduceMotion (and its second occurrence) will treat null
as “no preference” rather than “reduce motion.” Ensure you update the two lines
where reduceMotion is assigned.

---

Duplicate comments:
In `@apps/ios/ADE/Services/SyncService.swift`:
- Around line 4507-4518: The current closure passed to
InitialHydrationGate.waitForProjectRow accepts a persisted activeProjectId from
UserDefaults even when the connected host did not advertise a catalog, allowing
stale project scopes to be used; change the logic so that when
supportsProjectCatalog == false you do not accept a persisted activeProjectId —
either clear/derive self.activeProjectId (e.g., set to nil or rederive from
database.currentProjectId) before calling InitialHydrationGate.waitForProjectRow
or update the closure to also check supportsProjectCatalog and refuse the
persisted id if the host has no catalog; ensure you reference activeProjectId,
supportsProjectCatalog, InitialHydrationGate.waitForProjectRow, and
database.hasProject/database.currentProjectId in the update.

---

Nitpick comments:
In `@apps/ios/ADETests/ADETests.swift`:
- Around line 666-702: Add coverage for project-level (root) workspaces missing
a laneId by seeding one FilesWorkspace with laneId: nil for each project before
switching active project; update the calls around replaceFilesWorkspaces and
setActiveProjectId (referencing FilesWorkspace and
database.replaceFilesWorkspaces / database.setActiveProjectId) so that after
switching you assert database.listWorkspaces() only includes the root workspace
for the active project (in addition to existing lane-backed workspace assertions
using fetchLanes and fetchSessions); ensure one nil laneId workspace per project
is inserted and the expected IDs are asserted after the active project change.

In `@apps/web/src/components/editorial/BackCover.tsx`:
- Line 4: Remove the unused import IPhoneFrame from BackCover.tsx: delete the
line importing IPhoneFrame since the component is not referenced (the iPhone
mockup uses inline divs at the current render) and ensure no other references to
the IPhoneFrame symbol remain in this file so lint errors are resolved.
🪄 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: d6549794-5c1f-4322-9a46-5626f75fac8b

📥 Commits

Reviewing files that changed from the base of the PR and between 61f7464 and 1ac5656.

📒 Files selected for processing (18)
  • apps/ade-cli/src/adeRpcServer.test.ts
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/missions/missionPreflightService.test.ts
  • apps/desktop/src/main/services/missions/missionPreflightService.ts
  • apps/desktop/src/main/services/orchestrator/aiOrchestratorService.test.ts
  • apps/desktop/src/renderer/components/app/TopBar.tsx
  • apps/desktop/src/renderer/components/onboarding/tour/TourOverlay.test.tsx
  • apps/desktop/src/renderer/components/onboarding/tour/TourOverlay.tsx
  • apps/desktop/src/renderer/state/onboardingStore.test.ts
  • apps/desktop/src/renderer/state/onboardingStore.ts
  • apps/ios/ADE/Services/Database.swift
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADETests/ADETests.swift
  • apps/web/src/app/layout/SiteLayout.tsx
  • apps/web/src/components/editorial/BackCover.tsx
  • apps/web/src/components/editorial/CompetitorEquation.tsx
  • apps/web/src/components/editorial/IndexPage.tsx
🚧 Files skipped from review as they are similar to previous changes (9)
  • apps/web/src/app/layout/SiteLayout.tsx
  • apps/desktop/src/renderer/state/onboardingStore.test.ts
  • apps/ade-cli/src/adeRpcServer.test.ts
  • apps/web/src/components/editorial/CompetitorEquation.tsx
  • apps/desktop/src/renderer/components/onboarding/tour/TourOverlay.tsx
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/desktop/src/renderer/state/onboardingStore.ts
  • apps/ios/ADE/Services/Database.swift
  • apps/desktop/src/main/services/missions/missionPreflightService.test.ts

Comment thread apps/desktop/src/main/main.ts
Comment thread apps/desktop/src/main/main.ts
Comment thread apps/desktop/src/main/main.ts Outdated
Comment thread apps/desktop/src/main/services/missions/missionPreflightService.ts Outdated
Comment thread apps/desktop/src/main/services/missions/missionPreflightService.ts
Comment thread apps/ios/ADE/Services/SyncService.swift
Comment thread apps/ios/ADE/Services/SyncService.swift
Comment thread apps/web/src/components/editorial/IndexPage.tsx
Comment thread apps/web/src/components/editorial/IndexPage.tsx
Comment thread apps/web/src/components/editorial/IndexPage.tsx
Copy link
Copy Markdown

@capy-ai capy-ai Bot left a comment

Choose a reason for hiding this comment

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

Added 5 comments

Comment thread apps/ios/ADE/Services/SyncService.swift
Comment thread apps/ios/ADE/Services/SyncService.swift Outdated
try await InitialHydrationGate.waitForProjectRow(
currentProjectId: { self.database.currentProjectId() },
currentProjectId: {
guard let activeProjectId = self.activeProjectId else { return self.database.currentProjectId() }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

When a paired desktop does not advertise projectCatalog, activeProjectId can still be nil or point at an older cached project when the socket reconnects. This new gate unblocks on database.currentProjectId(), which only returns whichever cached project is currently selected / most recently opened, not the project row for the connected host. I verified that the first hydration pass immediately calls refreshLaneSnapshots(), refreshWorkSessions(), and refreshPullRequestSnapshots(), and the new DatabaseService.replace* implementations now scope those destructive refreshes by currentProjectId(). On legacy hosts with multiple cached projects, the phone can therefore archive/write the connected host's lanes, sessions, and PR snapshots into the wrong local project before the real project identity is known, breaking backward compatibility and cross-project isolation. The fix is to wait for the connected host's project row (or skip scoped snapshot refreshes until host/project identity is resolved) instead of falling back to an arbitrary cached project.

// apps/ios/ADE/Services/SyncService.swift
try await InitialHydrationGate.waitForProjectRow(
  currentProjectId: {
    guard let activeProjectId = self.activeProjectId else { return self.database.currentProjectId() }
    return self.database.hasProject(id: activeProjectId) ? activeProjectId : nil
  },
)

await runActions(result);
}
if (step.waitForSelector) {
await waitForSelector(step.waitForSelector, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

runBeforeEnter() now wires every waitForSelector step through the shared helper, but it never passes the helper's optional signal, so dismissing the tour or superseding the step leaves that helper's MutationObserver, interval, and timeout running until the fallback timeout expires (many of these steps use 30s timeouts in the registered tours). That creates stacked background polling on document during repeated skip/replay flows and is exactly the lifecycle leak this change was meant to avoid. Abort the in-flight wait when nextStep, prevStep, dismissCurrentTour, or completeCurrentTour invalidates the pending step.

// apps/desktop/src/renderer/state/onboardingStore.ts
if (step.waitForSelector) {
  await waitForSelector(step.waitForSelector, {
    timeoutMs: step.fallbackAfterMs,
  });
}

Comment thread apps/web/src/app/layout/SiteLayout.tsx
Copy link
Copy Markdown

@capy-ai capy-ai Bot left a comment

Choose a reason for hiding this comment

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

Added 1 comment

Comment thread apps/desktop/src/main/main.ts
@arul28 arul28 merged commit 9753b80 into main Apr 22, 2026
23 checks passed
@arul28 arul28 deleted the multi-project-view-mobile branch April 22, 2026 13:46
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