Skip to content

feat: multi-repo workspace support (project registry + pickers + dashboard)#974

Merged
njbrake merged 21 commits intonjbrake:mainfrom
Seluj78:multi-repository
May 8, 2026
Merged

feat: multi-repo workspace support (project registry + pickers + dashboard)#974
njbrake merged 21 commits intonjbrake:mainfrom
Seluj78:multi-repository

Conversation

@Seluj78
Copy link
Copy Markdown
Contributor

@Seluj78 Seluj78 commented May 8, 2026

Description

First-class multi-repo workspace support for agent-of-empires.

Closes #917

The multi-repo plumbing (WorkspaceInfo.repos, --repo flag, TUI Extra Repos field) already existed. This PR adds the three pieces called out in njbrake's final comment on the issue, plus closes two adjacent gaps that were blocking dashboard parity:

  1. Project registry — saved repos with full CRUD across CLI, TUI, and web. Hybrid storage: a global <app_dir>/projects.json visible from every profile, and a per-profile <app_dir>/profiles/{profile}/projects.json. Loader merges them and lets profile entries shadow global ones on path collision.
  2. Multi-select picker at session creation — TUI new-session dialog gets Ctrl+R on the Extra Repos field to open a registered-project picker; web wizard gets a chip-based multi-select with free-text fallback.
  3. Dashboard surfaces all repos — multi-repo sessions render an inline chip row under the title; SessionResponse now carries workspace_repos so the frontend can show them.

Also fixed along the way:

  • The web wizard previously did not POST extra_repo_paths, so the dashboard could not create multi-repo sessions even though the API accepted them.
  • SessionResponse did not expose any workspace-repo info, so existing multi-repo sessions had no way to render correctly on the dashboard.

Defaults & ergonomics

  • aoe project add <path> defaults to --scope global. When -p <profile> is passed at the top level, the default flips to --scope profile (the user is already in profile context, so registering globally would be surprising). Explicit --scope always wins.
  • Cross-scope path collisions error by default with a tip pointing at aoe project remove and an --allow-override opt-in. Without this guard a re-add silently created a phantom duplicate.
  • Name matching (add dedup, remove, aoe add --project NAME) is ASCII case-insensitive: aoe project remove smartcaller finds SmartCaller.
  • aoe add --project NAME -w branch -b is a shortcut for typed --repo /long/path/... invocations.
  • aoe project list pluralizes correctly.
  • value_hint = DirPath/AnyPath on the path positionals so zsh and fish shell completion wire _files for ~/<TAB> expansion (bash hits a known clap_complete upstream limitation).

What's not in scope (per njbrake's comment)

PR Type

  • New Feature
  • Bug Fix
  • Refactor
  • Documentation
  • Infrastructure / CI

Checklist

  • I understand the code I am submitting
  • New and existing tests pass
  • Documentation was updated where necessary
  • For UI changes: included screenshot or recording

(Recordings/screenshots: pending — opening as draft so reviewers can request specific captures.)

AI Usage

  • No AI was used
  • AI was used for drafting/refactoring
  • This is fully AI-generated

AI Model/Tool used: Claude Code (Sonnet/Opus) drove planning, exploration of the existing multi-repo plumbing, scaffolding of the new modules, and incremental commits. Each commit was reviewed and tested locally before being staged.

Any Additional AI Details you'd like to share: Plan was iterated in plan mode before implementation began. After landing, the user (Seluj78) walked the surfaces by hand and reported several rough edges (cross-scope override behaviour, plural noun, unclear "worktree" tip wording, case-sensitivity on remove, scope-default ergonomics under -p); each was diagnosed and fixed in a follow-up commit.

  • I am an AI Agent filling out this form (check box if true)

Manual Testing TODO

Setup once at the top of a session:

TMPHOME=$(mktemp -d)
export HOME="$TMPHOME"
export XDG_CONFIG_HOME="$TMPHOME/.config"

mkdir -p "$TMPHOME/repoA" "$TMPHOME/repoB" "$TMPHOME/repoC"
for d in repoA repoB repoC; do
  git -C "$TMPHOME/$d" init -q -b main
  git -C "$TMPHOME/$d" -c user.email=t@t -c user.name=t commit --allow-empty -q -m init
done

cargo build --features serve
AOE=./target/debug/aoe

Reset registry state between scenarios with: rm -rf "$TMPHOME/.config/agent-of-empires" "$TMPHOME/.agent-of-empires".

1. CLI: aoe project

  • 1.1 — $AOE project list on empty registry shows "No projects registered." + hint
  • 1.2 — $AOE project add "$TMPHOME/repoA" registers as [global], name repoA
  • 1.3 — $AOE project list shows repoA [global] /…/repoA
  • 1.4 — $AOE project list --json returns valid JSON array with scope: "global"
  • 1.5 — $AOE project add "$TMPHOME/repoB" --scope profile shows [profile] badge
  • 1.6 — $AOE project list --scope global shows only repoA
  • 1.7 — $AOE project list --scope profile shows only repoB
  • 1.8 — $AOE project list (default merged) shows both
  • 1.9 — $AOE project add /not/a/repo errors with "not a git repository" + .git folder tip
  • 1.10 — $AOE project add "$TMPHOME/repoA" (duplicate) errors with "already registered in global scope"
  • 1.11 — $AOE project add "$TMPHOME/repoA" --name something --scope profile --allow-override succeeds; profile shadows global
  • 1.12 — Without --allow-override, the same cross-scope add errors with the tip pointing at aoe project remove --scope global
  • 1.13 — $AOE project list after 1.11 shows the profile entry (one row, name something)
  • 1.14 — $AOE project remove repoA removes from global; profile entry survives
  • 1.15 — $AOE project remove SOMETHING --scope profile (uppercase) finds the lowercase entry — case-insensitive
  • 1.16 — $AOE project remove "$TMPHOME/repoB" --scope profile removes by full path
  • 1.17 — $AOE project remove ghost errors with "no project 'ghost' in global scope"
  • 1.18 — $AOE -p custom project add "$TMPHOME/repoC" (no --scope) defaults to PROFILE scope
  • 1.19 — $AOE -p custom project add "$TMPHOME/repoC" --scope global honors the explicit override
  • 1.20 — $AOE project list (default) vs $AOE -p custom project list shows different visible sets
  • 1.21 — $AOE project list after registering 1 project prints "Total: 1 project" (singular)
  • 1.22 — Shell completion: with aoe completion zsh sourced, $AOE project add ~/<TAB> expands paths

2. CLI: aoe add --project

Pre-reqs: register repoB as global, repoC as profile-only.

  • 2.1 — $AOE add "$TMPHOME/repoA" --project repoB -w feat -b creates a 2-worktree workspace
  • 2.2 — $AOE add "$TMPHOME/repoA" --project repoB --project repoC -w feat2 -b creates a 3-worktree workspace
  • 2.3 — $AOE add "$TMPHOME/repoA" --project ghost -w feat3 -b fails fast with "Unknown project 'ghost'. Available: repoB, repoC"
  • 2.4 — $AOE add "$TMPHOME/repoA" --project repoB (no -w) fails: --repo/--project requires --worktree
  • 2.5 — $AOE add "$TMPHOME/repoA" --project REPOB -w feat5 -b resolves case-insensitively
  • 2.6 — $AOE add "$TMPHOME/repoA" --repo "$TMPHOME/repoB" --project repoB -w feat4 -b fails the duplicate-name guard (same repo via two routes)
  • 2.7 — $AOE add "$TMPHOME/repoA" -w solo -b (no project flag) regression: single-repo session unchanged
  • 2.8 — After 2.1, aoe list shows the session and stored JSON has 2 repos in workspace_info

3. Web API (aoe serve --no-auth --port 41234 --host 127.0.0.1)

BASE=http://127.0.0.1:41234.

  • 3.1 — curl $BASE/api/projects returns [] initially
  • 3.2 — POST /api/projects with {"path":".../repoA"} returns 201 + scope: "global"
  • 3.3 — POST with "scope":"profile" returns 201 + scope: "profile"
  • 3.4 — POST with "scope":"weird" returns 400 bad_scope
  • 3.5 — POST with a non-git path returns 400 not_a_git_repo
  • 3.6 — POST duplicate within scope returns 409 add_failed
  • 3.7 — POST cross-scope dup without allow_override returns 409
  • 3.8 — POST cross-scope dup with "allow_override": true returns 201
  • 3.9 — GET /api/projects?scope=global returns only globals
  • 3.10 — GET /api/projects?scope=profile returns only profile entries
  • 3.11 — GET /api/projects?scope=invalid returns 400
  • 3.12 — DELETE /api/projects/repoA?scope=global returns 200 with the removed entry
  • 3.13 — DELETE /api/projects/ghost?scope=global returns 404
  • 3.14 — Restart with --read-only: POST/DELETE return 403 read_only
  • 3.15 — GET /api/sessions for a multi-repo session populates workspace_repos; single-repo sessions return [], never null/missing

4. Web Wizard (localhost:41234, "+ New session")

  • 4.1 — Step 1, no project selected: "Extra repos" picker hidden
  • 4.2 — Pick a repo via Browse tab: picker appears below
  • 4.3 — Pick again from Recent: picker still appears
  • 4.4 — Picker with empty registry: "No registered projects yet." hint
  • 4.5 — Picker with registered projects: each shown as a chip with [global]/[profile] badge
  • 4.6 — Click a project chip: toggles to "selected" with brand color
  • 4.7 — Click again: deselects
  • 4.8 — Free-text input + "Add" button: path appears as a selected chip with × button
  • 4.9 — Free-text input + Enter: same
  • 4.10 — Primary repo path is filtered out of the registered list
  • 4.11 — Submit wizard with 0 extras: single-repo session created, no extra_repo_paths in POST
  • 4.12 — Submit with 2 extras: POST body includes extra_repo_paths: [...]; session created with workspace
  • 4.13 — Submit with primary repo path also in extras: backend hard-fails, error bubbles to wizard
  • 4.14 — DevTools network tab confirms POST /api/sessions body includes extra_repo_paths only when non-empty

5. Web Projects page (/projects, folder icon in sidebar footer)

  • 5.1 — Empty state: "No registered projects yet." + CLI hint
  • 5.2 — Click "+ Add project": form expands inline
  • 5.3 — DirectoryBrowser opens via "Browse"; selection writes path back into the form
  • 5.4 — Empty path: Add button disabled
  • 5.5 — Non-git path: error banner with "not a git repository"
  • 5.6 — Valid path, scope=global, no name: adds with directory basename, [global] badge
  • 5.7 — Same path, scope=global again: 409 conflict error
  • 5.8 — Same path, scope=profile, allow_override unchecked: 409
  • 5.9 — Same path, scope=profile, allow_override checked: 201, profile shadows global in merged view
  • 5.10 — List shows global = brand-tinted badge, profile = neutral
  • 5.11 — Click Remove on a profile entry: confirm prompt; on accept the row disappears
  • 5.12 — --read-only server: Add button hidden, Remove button hidden
  • 5.13 — Resize to mobile (DevTools iPhone preset): page usable, no overflow, tap-friendly
  • 5.14 — Close button returns to last route (session or dashboard)
  • 5.15 — Back/forward nav between /projects and /session/:id: no flicker, sidebar reappears when leaving

6. Web dashboard cards

  • 6.1 — Single-repo session in sidebar renders unchanged (label + branch subtitle)
  • 6.2 — Multi-repo session shows a small chip row with each repo name under the label
  • 6.3 — Hovering the chip row exposes full source paths via title
  • 6.4 — Two sessions in the same multi-repo workspace render chips independently
  • 6.5 — Reload page: chips persist (server returns workspace_repos)

7. TUI projects picker (in new-session dialog)

aoe to open TUI, press n for new session.

  • 7.1 — Fill path + worktree branch, Tab to "Worktree" field: field shows
  • 7.2 — Ctrl+P on Worktree: worktree config overlay opens
  • 7.3 — Tab to "Extra Repos" field: hint row shows C-r pick project
  • 7.4 — Ctrl+R on empty registry: error message "No registered projects available…"
  • 7.5 — After registering one project, Ctrl+R: picker overlay opens with "Add registered project" title
  • 7.6 — Pick a project via Enter: project's path appended to repo list
  • 7.7 — Ctrl+R again: already-added project filtered out
  • 7.8 — Primary repo (not in registry) is filtered out of picker
  • 7.9 — Esc out of picker: closes, no change
  • 7.10 — Submit dialog: multi-repo session created with selected project's path
  • 7.11 — Free-text path entry still works (regression for typed paths)

8. TUI projects panel

  • 8.1 — Press p on home screen: Projects dialog opens
  • 8.2 — Empty registry: "No registered projects. Press 'a' to add one."
  • 8.3 — Press a: add form appears with Path field focused
  • 8.4 — Type non-git path, Enter: inline error "Not a git repository"
  • 8.5 — Type valid path, Enter: added with default scope=global, returns to browse mode
  • 8.6 — Press a again, Tab to Scope field: scope label underlined
  • 8.7 — Space / ← / → on Scope field: toggles global ↔ profile-only
  • 8.8 — Tab to Override field, Space toggles [ ][x]
  • 8.9 — Submit with profile scope: adds with [profile] badge
  • 8.10 — j/k on the list: cursor moves
  • 8.11 — d (or Delete): removes selected entry, status line shows "Removed '…'"
  • 8.12 — q or Esc: dialog closes
  • 8.13 — Re-open after d: list reflects removal
  • 8.14 — Press P (capital): profile picker opens (regression — no collision with p)
  • 8.15 — While dialog is open, other home keybinds are ignored (dialog has focus)

9. Cross-surface consistency

  • 9.1 — Add via CLI, observe in TUI panel after dialog reload
  • 9.2 — Add via web Projects page, observe in aoe project list
  • 9.3 — Add via TUI, refresh dashboard: chip appears in wizard picker
  • 9.4 — Same path added in global + profile (different names): merged shows one entry; profile name wins; CLI scoped lists show both
  • 9.5 — Switch profile (-p other): profile-scoped entries from default not visible; globals still visible
  • 9.6 — Remove a project that a session is using: session unaffected (paths stored on session, not by lookup)
  • 9.7 — Move a registered repo on disk, then re-list: path still shown; new sessions with --project NAME may fail (documented limitation)

10. Edge / regression

  • 10.1 — Single-repo session created before this branch loads fine; web returns workspace_repos: []; sidebar unchanged
  • 10.2 — aoe add /path -w feat -b (no --repo, no --project): single worktree path unchanged
  • 10.3 — aoe add /path -r /other -w feat -b (only --repo): multi-repo workspace built, no registry interaction
  • 10.4 — --repo and --project together: both merged into extras; duplicate-name guard fires only on real duplicates
  • 10.5 — Empty profile name (env unset): defaults to default; profile-scoped projects.json lives at profiles/default/projects.json
  • 10.6 — After 2nd save in either scope, .bak sibling exists
  • 10.7 — Hand-edit projects.json to invalid JSON: loaders return error; CLI list shows error; TUI/web log warning, no panic
  • 10.8 — Delete projects.json while server running: next API call returns empty list, no crash
  • 10.9 — Path with spaces / special chars: canonicalizes correctly; chips render the basename
  • 10.10 — Symlinked repo path: canonicalizes to target; same-repo dedup matches

11. Build / CI

  • 11.1 — cargo fmt --check clean
  • 11.2 — cargo clippy --all-targets --features serve no warnings
  • 11.3 — cargo test passes (2 known pre-existing Docker containers::runtime failures unrelated)
  • 11.4 — cargo build --features serve --release clean
  • 11.5 — cd web && npx tsc --noEmit clean
  • 11.6 — cd web && npx vite build clean
  • 11.7 — cargo xtask gen-docs then git diff docs/cli/reference.md shows no diff

@Seluj78 Seluj78 force-pushed the multi-repository branch from da14687 to 6d3f1a7 Compare May 8, 2026 08:34
njbrake added a commit to Seluj78/agent-of-empires that referenced this pull request May 8, 2026
Bundles six review fixes:

- TUI strict-mode: add 'p' to the bare-lowercase blocklist so the new
  Projects dialog does not bypass the strict-mode contract. In strict
  mode, reach Projects via the command palette.
- TUI discoverability: add ("p", "Projects") to the help overlay
  (non-strict) and a "projects" entry to the command palette so the
  feature is reachable from `?` and Ctrl+K. Bumped help DIALOG_HEIGHT
  43 -> 44 to fit the extra row.
- Web API: introduce typed RegistryError (Conflict / NotFound / Other)
  in projects::add and projects::remove. Server now returns 409 only
  on conflicts and 500 on I/O errors; delete returns 404 only on
  missing entries. CLI and TUI callers compile unchanged via
  Into<anyhow::Error>.
- Refactor: extract apply_picked_project() and open_projects_picker()
  out of handle_worktree_config_key, drop the
  #[allow(clippy::too_many_lines)].
- Docs: add docs/guides/multi-repo-workspaces.md with the full user
  story (registry, scopes, CLI/TUI/web flows, limitations). Wire it
  into website sync-docs PAGES, URL_MAP, and docsNav.
- E2E: add tests/e2e/project_registry.rs with six tests covering
  add/list/remove round-trip, JSON output, non-git rejection,
  duplicate-within-scope, cross-scope override, --project requires
  --worktree, and unknown --project name fast-fail.

Verified: cargo fmt, cargo clippy --all-targets --features serve,
cargo test --features serve --lib (1604 pass), cargo test --test e2e
project_registry (6 pass), web/tsc --noEmit.

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

Seluj78 commented May 8, 2026

@njbrake what is this commit you pushed ? 👀

Edit: nevermind I was able to read the commit body ! Thanks ! 🥰

@njbrake
Copy link
Copy Markdown
Owner

njbrake commented May 8, 2026

Note: this comment was drafted by Claude via back-and-forth with @njbrake. The reasoning and decisions are his; the prose is Claude's.

Substantive code review came back positive. Claude did the deep dive, I spot-checked, and pushed one follow-up commit (49dbe7a) fixing a few structural items I wanted in before merge:

  • Strict-mode p blocklist gap (bare p was bypassing the strict-mode contract).
  • ? overlay and Ctrl+K command palette entries for the new Projects panel.
  • Typed RegistryError in projects::add / remove so the API returns 409 only on conflict and 500 on actual I/O (was 409 for everything).
  • Extracted the picker logic out of handle_worktree_config_key and dropped the #[allow(clippy::too_many_lines)].
  • docs/guides/multi-repo-workspaces.md plus website wiring.
  • 6 e2e tests in tests/e2e/project_registry.rs covering aoe project round-trip, the typed error paths, and --project flag interactions.

Tests, clippy, fmt, tsc all clean. Architecture maps cleanly to the 3-piece plan in #917.

Two asks before flipping out of draft:

  1. Lock scope. Bugs you find during your remaining manual testing should land here. Anything that feels like a new feature or a different surface should go to a follow-up issue. The PR is already at +2,600 lines and I'd rather merge a tight scope than let it balloon. You've been disciplined already (the "out of scope" list in the PR body is exactly right); I'm just reinforcing the boundary so you don't feel pressure to keep adding "while I'm here" items.

  2. Finish the manual checks. Sections 7 (TUI picker), 8 (TUI panel), 9 (cross-surface consistency), 10 (edge cases), and 6.4 (two sessions in the same multi-repo workspace) are still unchecked. One screenshot or short recording of the dashboard chips and the TUI picker would help for posterity.

If anything else surfaces, file a follow-up and I'll fast-track it. Concrete punt targets so you don't feel cut off:

  • Per-repo branch names (you flagged it yourself as low priority).
  • Saved workspace templates / named bundles.
  • Per-repo PR tracking (track opened PRs by the session #927 territory).
  • TUI Projects panel polish (delete confirm, name editing).

Thanks for the iteration so far. The cross-scope override and case-insensitivity fixes were good catches.

Seluj78 pushed a commit to Seluj78/agent-of-empires that referenced this pull request May 8, 2026
Bundles six review fixes:

- TUI strict-mode: add 'p' to the bare-lowercase blocklist so the new
  Projects dialog does not bypass the strict-mode contract. In strict
  mode, reach Projects via the command palette.
- TUI discoverability: add ("p", "Projects") to the help overlay
  (non-strict) and a "projects" entry to the command palette so the
  feature is reachable from `?` and Ctrl+K. Bumped help DIALOG_HEIGHT
  43 -> 44 to fit the extra row.
- Web API: introduce typed RegistryError (Conflict / NotFound / Other)
  in projects::add and projects::remove. Server now returns 409 only
  on conflicts and 500 on I/O errors; delete returns 404 only on
  missing entries. CLI and TUI callers compile unchanged via
  Into<anyhow::Error>.
- Refactor: extract apply_picked_project() and open_projects_picker()
  out of handle_worktree_config_key, drop the
  #[allow(clippy::too_many_lines)].
- Docs: add docs/guides/multi-repo-workspaces.md with the full user
  story (registry, scopes, CLI/TUI/web flows, limitations). Wire it
  into website sync-docs PAGES, URL_MAP, and docsNav.
- E2E: add tests/e2e/project_registry.rs with six tests covering
  add/list/remove round-trip, JSON output, non-git rejection,
  duplicate-within-scope, cross-scope override, --project requires
  --worktree, and unknown --project name fast-fail.

Verified: cargo fmt, cargo clippy --all-targets --features serve,
cargo test --features serve --lib (1604 pass), cargo test --test e2e
project_registry (6 pass), web/tsc --noEmit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Seluj78 Seluj78 force-pushed the multi-repository branch from 49dbe7a to 25c4c75 Compare May 8, 2026 19:05
@Seluj78 Seluj78 marked this pull request as ready for review May 8, 2026 19:28
@Seluj78 Seluj78 requested a review from njbrake as a code owner May 8, 2026 19:28
@njbrake
Copy link
Copy Markdown
Owner

njbrake commented May 8, 2026

@Seluj78 looks like failing playwright test to address

Seluj78 and others added 21 commits May 8, 2026 22:58
Two-scope project list (global + per-profile) with JSON storage
parallel to groups.json. Profile entries shadow global on path
collision. Includes load/add/remove/resolve_names helpers used by
upcoming CLI, TUI, and web layers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
list/add/remove with --scope {global,profile}. Default scope is
global so multi-profile users opt in to per-profile entries with
`--scope profile`. Scope is its own flag (not `--profile`) to avoid
shadowing the top-level `--profile` flag that selects the active
profile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves project names against the merged registry (global ∪ profile)
and merges the resulting paths with --repo entries before delegating
to builder::create_workspace. Unknown names fail fast with the
available list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Project WorkspaceInfo.repos into a new WorkspaceRepoSummary list on
the API response so the dashboard can render multi-repo session
membership. Empty array for single-repo sessions. TS types mirrored
manually.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GET /api/projects[?scope=global|profile] returns the merged registry
(or one scope on filter). POST validates the path is a git repo and
defaults to global scope. DELETE /api/projects/{name}?scope=... removes
from the chosen scope. Mutations are blocked when the server is in
read-only mode, matching the existing session endpoints.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The wizard's CreateSessionRequest body had a typed slot for
extra_repo_paths but the form never populated it, so multi-repo
sessions were unreachable from the dashboard. Add an
ExtraReposPicker shown under the selected project: registered
projects appear as toggleable chips (sourced from /api/projects),
free-text paths still work via a text input. The primary repo is
hidden from the picker to avoid the builder's duplicate-name guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a session's workspace contains more than one repo, list the
repo names as small chips beneath the row label. Hover surfaces
the full source paths via the title attribute. Single-repo
sessions render unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New top-level route, parallel to Sessions and Settings, opened from
a folder icon in the sidebar footer. Lists registered projects with
their global/profile scope badge, supports adding (path picker +
optional name + scope toggle) and removing entries via /api/projects.
Read-only servers hide the destructive controls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inside the worktree config overlay, with Extra Repos focused, Ctrl+R
opens a picker over the merged registry (global ∪ profile). Selecting
a project appends its path to the workspace_repos list. The primary
repo and already-selected entries are filtered out so the builder's
duplicate-name guard does not fire. Hint row updated to advertise C-r.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ProjectsDialog renders the merged registry, supports add (path +
scope toggle) and remove inline. Reachable from the home screen via
lowercase `p` (uppercase `P` already opens the profile picker).
Validation matches the CLI/web paths: rejects non-git paths and
duplicates within a scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ect`

Auto-generated from clap definitions via `cargo xtask gen-docs`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adding a project at the same canonical path that already exists in the
other scope now errors by default with a tip to either remove the prior
entry or pass `--allow-override` to deliberately shadow it. Without this
guard the second registration silently created a duplicate that surfaced
later (e.g. removing a profile entry would resurrect a long-forgotten
global one).

- src/session/projects.rs: extra cross-scope path check in `add`,
  gated by a new `allow_override` parameter; +1 unit test.
- CLI: new `--allow-override` flag on `aoe project add`.
- Web API: `allow_override` field on POST /api/projects.
- TUI Projects dialog: third focusable form field with a checkbox
  for the override.
- Web Projects view: matching checkbox in the inline add form.
- docs/cli/reference.md regenerated.
`aoe project add <PATH>` and `aoe project remove <NAME-OR-PATH>` now set
`value_hint = DirPath` and `AnyPath` respectively, so clap_complete-
generated zsh and fish completions wire `_files`/`_files -/` for the
positional argument. Without this hint zsh falls back to argument-name
completion only, and `aoe project add ~/<TAB>` does not expand the path.

Note: bash completion still short-circuits the positional via the
hardcoded `COMP_CWORD == N` check that clap_complete's bash backend
emits — that's a known upstream limitation, not something this hint can
work around. zsh and fish are fixed; bash users need to fall back to
`compopt -o default` shell config.
- `aoe project list` no longer prints "Total: 1 projects"; uses
  "project"/"projects" based on the count.
- The error tip when `aoe project add` rejects a path now reads
  "Tip: pass the path to a directory that contains a `.git` folder
  (i.e. the root of a cloned repository)." The previous wording
  ("a repo's working tree (or a linked worktree)") was confusing —
  users read "working tree" as referring to git worktrees and
  worried they were registering the wrong thing.
`aoe project remove smartcaller` now finds an entry registered as
`SmartCaller`. The same applies to:
- `aoe project add` duplicate-name detection (within-scope), so
  registering "SMARTCALLER" while "SmartCaller" exists errors with a
  hint pointing at the actual stored name.
- `aoe add --project NAME` resolution.

Path matching already canonicalized; only name comparisons were
sensitive. Switched to `eq_ignore_ascii_case`.
`aoe -p custom project add <path>` now registers under the profile
scope by default, instead of falling back to global. Rationale: if
the user is already addressing a specific profile via `-p`, the
common intent is to scope the registration to that profile too;
having to also pass `--scope profile` was redundant.

Behavior table (no `--scope` passed):
- `aoe project add <path>`              → global   (unchanged)
- `aoe -p custom project add <path>`    → profile  (was: global)

Explicit `--scope` always wins, so `aoe -p custom project add … --scope global`
still adds to the global registry. Same logic applies to
`aoe project remove`. List behavior unchanged.

Threaded `profile_explicit` from main.rs into cli::project::run; the
flag is just `cli.profile.is_some()` captured before unwrap. Scope arg
type changed from `ScopeArg` (with `default_value_t`) to
`Option<ScopeArg>`, with the default resolved at handler dispatch.
Sessions created with `--repo`/`--project` already persist their
workspace info on disk, but `aoe list --json` only emitted the primary
path, leaving CLI consumers no way to discover the extra repos. The
web `SessionResponse` already carried `workspace_repos`; this brings
the CLI surface to parity.

Single-repo sessions return `workspace_repos: []` so the field is
always present and never `null`.
Cap inner list height at 50vh with overflow-y-auto so tall listings
don't push the projects add form off-screen on small viewports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multi-repo sessions previously grouped by their primary repo path, so
different combos (or different choices of primary) ended up in
unrelated groups. Move them all into a single virtual "Multi-repo"
group at the bottom of the sidebar. Single-repo sessions keep
grouping by their repo path. The "+ new session" button on the
Multi-repo header opens the wizard with no project prefill.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles six review fixes:

- TUI strict-mode: add 'p' to the bare-lowercase blocklist so the new
  Projects dialog does not bypass the strict-mode contract. In strict
  mode, reach Projects via the command palette.
- TUI discoverability: add ("p", "Projects") to the help overlay
  (non-strict) and a "projects" entry to the command palette so the
  feature is reachable from `?` and Ctrl+K. Bumped help DIALOG_HEIGHT
  43 -> 44 to fit the extra row.
- Web API: introduce typed RegistryError (Conflict / NotFound / Other)
  in projects::add and projects::remove. Server now returns 409 only
  on conflicts and 500 on I/O errors; delete returns 404 only on
  missing entries. CLI and TUI callers compile unchanged via
  Into<anyhow::Error>.
- Refactor: extract apply_picked_project() and open_projects_picker()
  out of handle_worktree_config_key, drop the
  #[allow(clippy::too_many_lines)].
- Docs: add docs/guides/multi-repo-workspaces.md with the full user
  story (registry, scopes, CLI/TUI/web flows, limitations). Wire it
  into website sync-docs PAGES, URL_MAP, and docsNav.
- E2E: add tests/e2e/project_registry.rs with six tests covering
  add/list/remove round-trip, JSON output, non-git rejection,
  duplicate-within-scope, cross-scope override, --project requires
  --worktree, and unknown --project name fast-fail.

Verified: cargo fmt, cargo clippy --all-targets --features serve,
cargo test --features serve --lib (1604 pass), cargo test --test e2e
project_registry (6 pass), web/tsc --noEmit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Playwright session mocks predate the SessionResponse.workspace_repos
field. The new SessionRow chip + multi-repo group hook both read
.length on the array, so an undefined value crashed the sidebar
render and clickSidebarSession could not find any session row.
Add workspace_repos: [] to both mocks. Also harden the two readers
with ?.length ?? 0 so future API skew degrades gracefully.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Seluj78 Seluj78 force-pushed the multi-repository branch from 1d79c00 to d2737e8 Compare May 8, 2026 20:58
@Seluj78
Copy link
Copy Markdown
Contributor Author

Seluj78 commented May 8, 2026

@njbrake Fix pushed 😀

@njbrake njbrake merged commit 598549e into njbrake:main May 8, 2026
11 checks passed
@Seluj78 Seluj78 deleted the multi-repository branch May 8, 2026 22:07
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.

Support multi-repository workspaces

2 participants