Skip to content

EPIC: Project Switching Navigation #193

@kirich1409

Description

@kirich1409

EPIC: Project Switching Navigation

Multi-window project navigation for the Relay macOS client: close project returns to Welcome, File/Window menus allow quick project switching, opening another project shows a prompt (same window / new window / cancel with "remember"), multiple windows with the same project are prevented via SwiftUI WindowGroup(for: ProjectID.self) and a unified open-handler for external triggers.

Goal

Make project management fluid: users can close a project without quitting the app, switch between recent/open projects from menus, and never accidentally open the same project in two windows.

Background & Decisions

Based on the research report at swarm-report/project-switching-research.md (not in repo — swarm-report/ is gitignored; see local dev workspace or request a reviewer share it). The following design decisions are locked:

  • Approach A: SwiftUI multi-window. Window(id: "welcome") for singleton Welcome; WindowGroup(for: ProjectID.self) for project windows. Rejected: Approach D (single-window multi-tab, 3× cheaper but doesn't match literal requirement "in this window / in a new window").
  • Hybrid ProjectID migration. Add canonicalPath: String to Project without replacing the existing UUID — lazy backfill during first load. Low-regression, easily rollbackable.
  • Identity. Two clones of the same repo = two separate projects (path-based identity, no git remote inference).
  • Soft-limit 8 open windows (warning, no hard limit).
  • Worktree = separate window (different canonical paths).
  • Default prompt every time on conflicting context, with "Remember choice" checkbox; Preferences pane to reset.
  • Switch "in this window" reuses the existing closeProjectAlert when active sessions exist.
  • URL scheme deferred out of MVP (basic infra in T-14).
  • macOS 26 / WWDC25: verified — no breaking changes in multi-window SwiftUI API; WindowGroup(for:) dedup-by-value and scene restoration are stable.

Requirements (from user request)

  1. Closing a project returns to Welcome (not closing the whole app).
  2. Menu navigation between projects — File → Open Recent + Window → Open Projects.
  3. When opening another project, prompt — "In this window / In a new window / Cancel", with a checkbox to remember.
  4. Never allow two windows with the same project — activate the existing window instead.

MVP Scope

MVP = T-1 through T-15 (14 tasks, ~8–10 eng-days). T-14 (external open triggers) and T-15 (session isolation diagnostic) are NOT originally part of M1+M2+M3 but are required for requirement 4 to hold in practice — without T-14, dragging a folder onto the Dock icon bypasses the dedup guard; without T-15, terminal sessions may leak across windows.

Deferred (post-MVP): T-16 (multi-monitor polish), T-17 (accessibility deep pass), T-18 (ghost project folder), T-19 (extend reopenLastProject to reopenAllWindows).

Milestones (logical grouping)

Milestone Scope Closes requirements
M1. Foundation Canonical ProjectID + SwiftData migration + memory dedup via ProjectID + red-dot close alert — (infra)
M2. Multi-window core WindowFeature decomposition + WindowGroup(for: ProjectID.self) + per-window Welcome + single-instance guard + allowsAutomaticWindowTabbing Req 1, Req 4
M3. Menu + switcher File → Open Recent + Window → Open Projects + @FocusedValue<WindowID> + confirmation sheet + Preferences pane Req 2, Req 3
M4. External triggers Unified application(_:open:) handler + URL scheme placeholder Req 4 edge cases
M5. Polish Terminal session isolation tests + multi-monitor + accessibility + ghost projects — (QA)
M6. Restoration Extend reopenLastProjectreopenAllWindows — (QoL)

Task List (19 tasks)

# Issue Task Wave Complexity Depends on
T-1 #194 Introduce ProjectID canonical identity value type 1 S
T-2 #196 Extend Project model with canonicalPath + SwiftData migration 2 M #194
T-3 #197 Replace .path equality with ProjectID across reducers 2 S #194
T-4 #195 Handle red-dot window close → closeProjectAlert 1 M
T-5 #198 Introduce WindowFeature reducer per window 3 L #194, #197
T-6 #200 Move serverList & cloudNavigation sheets into WindowFeature 3 M #198
T-7 #201 Switch RelayApp to WindowGroup(for:) + Window(id: "welcome") 4 L #198, #200
T-8 #202 Enable NSWindow.allowsAutomaticWindowTabbing 5 S #201
T-9 #203 Introduce @FocusedValue<WindowID> for menu targeting 5 M #201
T-10 #209 File → Open Recent dynamic submenu 5 M #203
T-11 #204 Window → Open Projects dynamic list 5 M #201
T-12 #210 Confirmation sheet "In this / new / Cancel" + "Remember" 5 L #203, #209, #204
T-13 #211 Preferences pane — "When opening a project" picker 5 S #210
T-14 #205 Unified application(_:open:) open-handler 5 M #194, #201
T-15 #206 Terminal session isolation between windows (diagnostic + fix) 5 M (up to L) #201
T-16 #207 Multi-monitor positioning defaults 5 S #201
T-17 #212 Accessibility pass — VoiceOver across multi-window UX 5 M #210
T-18 #199 Ghost project handling (folder deleted externally) 3 M #196
T-19 #208 Extend reopenLastProject → reopenAllWindows 5 M #201

Dependency Graph

T-1 (#194) ──→ T-2 (#196) ──→ T-18 (#199)
   │              │
   │              └──→ T-3 (#197) ──┐
   │                                │
   └────────────────────────────────┴──→ T-5 (#198) ──→ T-6 (#200) ──→ T-7 (#201) ──┐
                                                                                      ├──→ T-8 (#202)
                                                                                      ├──→ T-9 (#203) ──→ T-10 (#209) ──→ T-12 (#210) ──→ T-13 (#211)
                                                                                      │                                         └──→ T-17 (#212)
                                                                                      ├──→ T-11 (#204) ───────────────────────┘
                                                                                      ├──→ T-14 (#205)
                                                                                      ├──→ T-15 (#206)
                                                                                      ├──→ T-16 (#207)
                                                                                      └──→ T-19 (#208)

T-4 (#195) (independent — red-dot close alert)

Risks

Technical

  • SwiftData migration regressions — mitigated by hybrid approach (add canonicalPath without replacing UUID).
  • Terminal session leakage across windows via sharedTerminalRegistry — T-15 diagnostic; if the test fails, scope TerminalSession by windowID.
  • WindowFeature refactor touches every reducer — recommend feature flag on RelayApp.body as rollback safety net.
  • .commands menu targeting in multi-window requires @FocusedValue — standard SwiftUI idiom.
  • ForEach inside CommandGroup is not explicitly documented by Apple but works in production (CodeEdit) — monitor regressions.

Product / UX

Acceptance Criteria (EPIC-level)

Epic is closed when:

  • All MVP tasks (T-1 through T-15) are merged into main
  • Closing a project returns to Welcome (requirement 1) — manual test pass on macOS 26
  • File → Open Recent + Window → Open Projects menus work (requirement 2) — manual test pass
  • Confirmation sheet appears when opening another project with other windows open (requirement 3) — manual test pass
  • Attempting to open an already-open project (via menu, drag-to-Dock, open CLI) activates the existing window (requirement 4) — manual test pass for all three triggers
  • Two windows with different projects do not leak terminal sessions (T-15 AC) — integration test pass
  • docs/architecture/ and MacApp/ README updated with multi-window model

Out of Scope

  • Android / iOS / web clients (macOS client only)
  • Full NSDocument-based architecture (rejected: Approach B)
  • NSWindowController dictionary (rejected: Approach C)
  • URL scheme beyond basic path parsing (deferred post-MVP)
  • AppleScript full scripting support (deferred)
  • Handoff via NSUserActivity (deferred)
  • Backend (Rust runner) changes — this is a pure frontend epic

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions