Skip to content

Implement mobile project switching and sync catalog#172

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

Implement mobile project switching and sync catalog#172
arul28 merged 11 commits into
mainfrom
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

Loading
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