feat: multi-repo workspace support (project registry + pickers + dashboard)#974
feat: multi-repo workspace support (project registry + pickers + dashboard)#974njbrake merged 21 commits intonjbrake:mainfrom
Conversation
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>
|
@njbrake what is this commit you pushed ? 👀 Edit: nevermind I was able to read the commit body ! Thanks ! 🥰 |
|
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:
Tests, clippy, fmt, tsc all clean. Architecture maps cleanly to the 3-piece plan in #917. Two asks before flipping out of draft:
If anything else surfaces, file a follow-up and I'll fast-track it. Concrete punt targets so you don't feel cut off:
Thanks for the iteration so far. The cross-scope override and case-insensitivity fixes were good catches. |
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 looks like failing playwright test to address |
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>
|
@njbrake Fix pushed 😀 |
Description
First-class multi-repo workspace support for
agent-of-empires.Closes #917
The multi-repo plumbing (
WorkspaceInfo.repos,--repoflag, 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:<app_dir>/projects.jsonvisible 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.Ctrl+Ron the Extra Repos field to open a registered-project picker; web wizard gets a chip-based multi-select with free-text fallback.SessionResponsenow carriesworkspace_reposso the frontend can show them.Also fixed along the way:
extra_repo_paths, so the dashboard could not create multi-repo sessions even though the API accepted them.SessionResponsedid 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--scopealways wins.aoe project removeand an--allow-overrideopt-in. Without this guard a re-add silently created a phantom duplicate.adddedup,remove,aoe add --project NAME) is ASCII case-insensitive:aoe project remove smartcallerfindsSmartCaller.aoe add --project NAME -w branch -bis a shortcut for typed--repo /long/path/...invocations.aoe project listpluralizes correctly.value_hint = DirPath/AnyPathon the path positionals so zsh and fish shell completion wire_filesfor~/<TAB>expansion (bash hits a known clap_complete upstream limitation).What's not in scope (per njbrake's comment)
PR Type
Checklist
(Recordings/screenshots: pending — opening as draft so reviewers can request specific captures.)
AI Usage
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.Manual Testing TODO
Setup once at the top of a session:
Reset registry state between scenarios with:
rm -rf "$TMPHOME/.config/agent-of-empires" "$TMPHOME/.agent-of-empires".1. CLI:
aoe project$AOE project liston empty registry shows "No projects registered." + hint$AOE project add "$TMPHOME/repoA"registers as[global], namerepoA$AOE project listshowsrepoA [global] /…/repoA$AOE project list --jsonreturns valid JSON array withscope: "global"$AOE project add "$TMPHOME/repoB" --scope profileshows[profile]badge$AOE project list --scope globalshows only repoA$AOE project list --scope profileshows only repoB$AOE project list(default merged) shows both$AOE project add /not/a/repoerrors with "not a git repository" +.gitfolder tip$AOE project add "$TMPHOME/repoA"(duplicate) errors with "already registered in global scope"$AOE project add "$TMPHOME/repoA" --name something --scope profile --allow-overridesucceeds; profile shadows global--allow-override, the same cross-scope add errors with the tip pointing ataoe project remove --scope global$AOE project listafter 1.11 shows the profile entry (one row, namesomething)$AOE project remove repoAremoves from global; profile entry survives$AOE project remove SOMETHING --scope profile(uppercase) finds the lowercase entry — case-insensitive$AOE project remove "$TMPHOME/repoB" --scope profileremoves by full path$AOE project remove ghosterrors with "no project 'ghost' in global scope"$AOE -p custom project add "$TMPHOME/repoC"(no--scope) defaults to PROFILE scope$AOE -p custom project add "$TMPHOME/repoC" --scope globalhonors the explicit override$AOE project list(default) vs$AOE -p custom project listshows different visible sets$AOE project listafter registering 1 project prints "Total: 1 project" (singular)aoe completion zshsourced,$AOE project add ~/<TAB>expands paths2. CLI:
aoe add --projectPre-reqs: register repoB as global, repoC as profile-only.
$AOE add "$TMPHOME/repoA" --project repoB -w feat -bcreates a 2-worktree workspace$AOE add "$TMPHOME/repoA" --project repoB --project repoC -w feat2 -bcreates a 3-worktree workspace$AOE add "$TMPHOME/repoA" --project ghost -w feat3 -bfails fast with "Unknown project 'ghost'. Available: repoB, repoC"$AOE add "$TMPHOME/repoA" --project repoB(no-w) fails:--repo/--projectrequires--worktree$AOE add "$TMPHOME/repoA" --project REPOB -w feat5 -bresolves case-insensitively$AOE add "$TMPHOME/repoA" --repo "$TMPHOME/repoB" --project repoB -w feat4 -bfails the duplicate-name guard (same repo via two routes)$AOE add "$TMPHOME/repoA" -w solo -b(no project flag) regression: single-repo session unchangedaoe listshows the session and stored JSON has 2 repos inworkspace_info3. Web API (
aoe serve --no-auth --port 41234 --host 127.0.0.1)BASE=http://127.0.0.1:41234.curl $BASE/api/projectsreturns[]initiallyPOST /api/projectswith{"path":".../repoA"}returns 201 +scope: "global"POSTwith"scope":"profile"returns 201 +scope: "profile"POSTwith"scope":"weird"returns 400bad_scopePOSTwith a non-git path returns 400not_a_git_repoPOSTduplicate within scope returns 409add_failedPOSTcross-scope dup withoutallow_overridereturns 409POSTcross-scope dup with"allow_override": truereturns 201GET /api/projects?scope=globalreturns only globalsGET /api/projects?scope=profilereturns only profile entriesGET /api/projects?scope=invalidreturns 400DELETE /api/projects/repoA?scope=globalreturns 200 with the removed entryDELETE /api/projects/ghost?scope=globalreturns 404--read-only:POST/DELETEreturn 403read_onlyGET /api/sessionsfor a multi-repo session populatesworkspace_repos; single-repo sessions return[], nevernull/missing4. Web Wizard (
localhost:41234, "+ New session")[global]/[profile]badge×buttonextra_repo_pathsin POSTextra_repo_paths: [...]; session created with workspace/api/sessionsbody includesextra_repo_pathsonly when non-empty5. Web Projects page (
/projects, folder icon in sidebar footer)[global]badge--read-onlyserver: Add button hidden, Remove button hidden/projectsand/session/:id: no flicker, sidebar reappears when leaving6. Web dashboard cards
titleworkspace_repos)7. TUI projects picker (in new-session dialog)
aoeto open TUI, pressnfor new session.Ctrl+Pon Worktree: worktree config overlay opensC-r pick projectCtrl+Ron empty registry: error message "No registered projects available…"Ctrl+R: picker overlay opens with "Add registered project" titleCtrl+Ragain: already-added project filtered out8. TUI projects panel
pon home screen: Projects dialog opensa: add form appears with Path field focusedaagain, Tab to Scope field: scope label underlined[ ]↔[x][profile]badgej/kon the list: cursor movesd(or Delete): removes selected entry, status line shows "Removed '…'"qor Esc: dialog closesd: list reflects removalP(capital): profile picker opens (regression — no collision withp)9. Cross-surface consistency
aoe project list-p other): profile-scoped entries from default not visible; globals still visible--project NAMEmay fail (documented limitation)10. Edge / regression
workspace_repos: []; sidebar unchangedaoe add /path -w feat -b(no--repo, no--project): single worktree path unchangedaoe add /path -r /other -w feat -b(only--repo): multi-repo workspace built, no registry interaction--repoand--projecttogether: both merged into extras; duplicate-name guard fires only on real duplicatesdefault; profile-scoped projects.json lives atprofiles/default/projects.json.baksibling existsprojects.jsonto invalid JSON: loaders return error; CLI list shows error; TUI/web log warning, no panicprojects.jsonwhile server running: next API call returns empty list, no crash11. Build / CI
cargo fmt --checkcleancargo clippy --all-targets --features serveno warningscargo testpasses (2 known pre-existing Dockercontainers::runtimefailures unrelated)cargo build --features serve --releasecleancd web && npx tsc --noEmitcleancd web && npx vite buildcleancargo xtask gen-docsthengit diff docs/cli/reference.mdshows no diff