feat(web): multi-project read-only discovery for context-gateway tabs#553
Merged
feat(web): multi-project read-only discovery for context-gateway tabs#553
Conversation
PR2 of the multi-project context UI series — see
``memtomem-docs/memtomem/planning/multi-project-context-ui-rfc.md``.
A user running ``mm web`` from the ``memtomem`` repo could previously
only see skills / commands / agents under that one cwd; ``~/Edu/inflearn/
.claude/skills/`` was invisible without restarting the server. PR2 adds
discovery for additional project roots (server cwd, user-registered
``known_projects.json``, opt-in ``~/.claude/projects/`` reverse-decode)
and renders one collapsible ``<details>`` group per scope inside each
tab. Mutating routes stay cwd-only — multi-scope writes ship in PR3.
Backend
-------
- ``memtomem.context.projects`` is the new discovery module:
- ``ProjectScope`` carries ``{scope_id, label, root, tier, sources,
missing, experimental, counts}``.
- ``compute_scope_id`` derives ``"p-" + sha256(case-normalized
Path.resolve())[:12]`` so refresh / restart preserve the id (RFC
§Decision 4). On macOS the input is also lowercased: APFS is
case-insensitive but ``Path.resolve()`` does not canonicalize case.
- ``KnownProjectsStore`` writes ``~/.memtomem/known_projects.json``
via ``tempfile + os.replace`` plus a ``.<name>.lock`` sidecar
fcntl lock — locking the data file directly does not survive
``os.replace`` (PR #548 issue).
- ``discover_project_scopes`` unions cwd + known-projects + (opt-in)
claude-projects, dedupes by resolved path, and clears
``experimental`` on union with a trusted source.
- ``ContextGatewayConfig`` (new) carries ``known_projects_path``,
``experimental_claude_projects_scan: bool = False``, and
``user_tier_enabled: bool = False`` (forward-compat for PR3).
Routes
------
New ``web/routes/context_projects.py``:
- ``GET /api/context/projects`` — full scope list with per-type counts.
- ``POST /api/context/known-projects`` — register; absolute + ``is_dir()``
validation; warning (HTTP 200) when no ``.claude``/``.gemini``/``.agents``/
``.memtomem`` marker is present so users can pre-register an empty
checkout.
- ``DELETE /api/context/known-projects/{scope_id}`` — drop, including
stale entries (matching is path-derived).
- ``resolve_scope_root`` is the dependency the existing
``/api/context/{skills,commands,agents}`` GETs now use; without
``?scope_id=`` it falls back to the server cwd so PR1's mutating
cwd flow keeps working unchanged. Unknown / stale scope_id → 404.
UI
--
Each tab gains an "Add Project" button; the list area renders one
``<details class="ctx-scope-group">`` per scope, lazy-fetching items
on expand. Server CWD opens by default; non-cwd scope items render as
read-only cards (PR2 keeps mutating buttons targeting cwd only). Each
non-cwd group has an ✕ button that calls DELETE
``/api/context/known-projects/{scope_id}``.
Three new i18n keys (placeholder-form, en + ko parity).
Tests
-----
PR2 minimum-bar from RFC §Test obligations:
- ``tests/test_context_projects.py`` — scope_id stability + collision
sanity, trailing-slash invariance, macOS case-insensitivity,
symlink dedup, ``experimental`` flag clears on union, both
``experimental_claude_projects_scan`` defaults, ``known_projects.json``
corruption / unknown-version recovery, stale-entry removal,
multiprocessing.Process atomic-write race.
- ``tests/test_web_routes_context_projects.py`` — HTTP shape, unknown
scope_id 404, POST 400 paths (relative / nonexistent / file),
marker warning, idempotent registration, DELETE round-trip + 404.
Manual smoke
------------
``HOME=/tmp/.../home uv run mm web --port 8088``: GET projects shows
cwd; POST /tmp returns 200 + warning; GET projects shows two scopes
(``Server CWD`` + ``tmp``); skills?scope_id=p-bogus → 404; DELETE
round-trip works; second DELETE → 404.
Refs RFC ``multi-project-context-ui``; gates PR3 (multi-project
mutating + Claude user-tier).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Self-review of PR #553 surfaced five real issues — fixing them keeps the surface honest about what's actually wired up. - Drop dead helpers in ``context/projects.py`` (#1): - ``_is_eaccess`` was never called. - ``is_project_scope_id`` claimed to support route error formatting but no route used it. - ``_new_temp_known_projects_path`` claimed test convenience but no test imported it. - ``errno`` and ``tempfile`` imports follow. - Drop ``ProjectScope.counts`` dataclass field (#2). Discovery never populated it; the route layer (``_scope_to_dict``) always overrides via ``_counts_for(root)``. Forward-design pollution. - ``POST /api/context/known-projects`` now returns ``warning_code: "no_runtime_marker"`` alongside the prose ``warning`` (#4). Matches PR1's (#549) machine-readable ``reason_code`` pattern so client matching is i18n-stable; the prose stays for back-compat. - ``_counts_for`` gains a cost comment (#3): 3 × (canonical scan + N runtime scans) per scope on every projects GET. Acceptable at <30 scopes; cache when discovery growth pushes that ceiling. - JS scope badge no longer carries a redundant ``data-i18n`` attribute (#5). The inline ``t()`` already renders text at construction; the attribute would let the i18n DOM walker re-translate and clobber. - ``ctx-scope-summary`` now carries a ``title="${scope.root}"`` (#8) so same-name scopes (``Edu/inflearn`` vs ``Work/inflearn``) disambiguate on hover without bloating the visible label. Cache-bust: ``context-gateway.js?v=4`` → ``?v=5``. Tests updated: ``test_post_warns_on_missing_marker`` asserts the new ``warning_code``; ``test_post_no_warning_when_marker_present`` asserts both fields are absent. 110/110 green; ruff + format clean. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR2 of the multi-project context UI series (RFC). PR1 (#549) shipped the empty-state surfacing for the single-cwd model; this PR opens the model itself to multiple project roots while keeping every mutating route on cwd. PR3 will then layer multi-scope writes + Claude user-tier on top.
Behavior change
mm webSkills/Commands/Agents tabs now render one collapsible<details>group per discovered project scope. The first group ("Server CWD") opens by default and keeps PR1's clickable / editable card behavior; additional scopes lazy-fetch on expand and render as read-only cards.~/.memtomem/known_projects.json(atomic write + sidecar lockfile) and survive server restart.API additions
GET/api/context/projects{scopes: [{scope_id, label, root, tier, sources, missing, experimental, counts: {skills, commands, agents}}]}GET/api/context/{skills|commands|agents}?scope_id=<sid>POST/api/context/known-projects{root, label?}; abs+is_dir()validation; HTTP 200 +warningwhen no.claude/.gemini/.agents/.memtomemmarkerDELETE/api/context/known-projects/{scope_id}scope_id = "p-" + sha256(case-normalized resolve)[:12](RFC §Decision 4). Unknown / stalescope_id→ HTTP 404. Theexperimentalflag istrueonly whenclaude-projectsis the sole discovery source — union withserver-cwdclears it.Config
New
[context_gateway]section:known_projects_path(default~/.memtomem/known_projects.json)experimental_claude_projects_scan: bool = False(gates the opt-in~/.claude/projects/reverse-decode)user_tier_enabled: bool = False(forward-compat for PR3 — has no PR2 effect)Tests
PR2 minimum-bar from RFC §Test obligations:
tests/test_context_projects.py(25 tests) — scope_id stability, collision sanity, trailing-slash invariance, macOS case-insensitivity (os.path.normcaseis a no-op on POSIX so I lowercase explicitly on darwin — APFS is case-insensitive butPath.resolve()does not canonicalize case), symlink dedup, bothexperimental_claude_projects_scandefaults, atomic-write race viamultiprocessing.Process(real OS-level concurrency, not GIL-bound threads), corruption / unknown-version recovery, stale-entry removal.tests/test_web_routes_context_projects.py(13 tests) — HTTP shape, unknown scope_id 404, POST validation matrix, marker warning behavior, idempotent registration, DELETE round-trip + 404 + stale.uv run pytest -m "not ollama": 3201 passed, 46 deselected.uv run ruff check && ruff format --check: green.uv run mypyon new files: green.Test plan
uv run pytest -m "not ollama"greenuv run ruff check + format --checkgreenuv run mypyadvisory green on new modulesHOME): GET projects → cwd-only; POST/tmp→ 200 + warning; GET projects → 2 scopes;?scope_id=p-bogus→ 404; DELETE round-trip + 404 on second DELETERefs RFC
multi-project-context-ui; gates PR3 (multi-scope mutating + Claude user-tier).🤖 Generated with Claude Code