Skip to content

feat(web): multi-project read-only discovery for context-gateway tabs#553

Merged
memtomem merged 2 commits intomainfrom
feat/multi-project-read-only
Apr 29, 2026
Merged

feat(web): multi-project read-only discovery for context-gateway tabs#553
memtomem merged 2 commits intomainfrom
feat/multi-project-read-only

Conversation

@memtomem
Copy link
Copy Markdown
Owner

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 web Skills/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.
  • New "Add Project" button in each tab opens a path prompt. Registered roots persist to ~/.memtomem/known_projects.json (atomic write + sidecar lockfile) and survive server restart.
  • No mutating-route changes. Create / Sync / Import still target the server cwd. PR2 is read-only by design.

API additions

Method Path Notes
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> optional query; without it, legacy single-cwd contract preserved exactly
POST /api/context/known-projects body {root, label?}; abs+is_dir() validation; HTTP 200 + warning when no .claude/.gemini/.agents/.memtomem marker
DELETE /api/context/known-projects/{scope_id} drops registration; stale entries removable

scope_id = "p-" + sha256(case-normalized resolve)[:12] (RFC §Decision 4). Unknown / stale scope_id → HTTP 404. The experimental flag is true only when claude-projects is the sole discovery source — union with server-cwd clears 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.normcase is a no-op on POSIX so I lowercase explicitly on darwin — APFS is case-insensitive but Path.resolve() does not canonicalize case), symlink dedup, both experimental_claude_projects_scan defaults, atomic-write race via multiprocessing.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 mypy on new files: green.

Test plan

  • uv run pytest -m "not ollama" green
  • uv run ruff check + format --check green
  • uv run mypy advisory green on new modules
  • Manual smoke (isolated HOME): GET projects → cwd-only; POST /tmp → 200 + warning; GET projects → 2 scopes; ?scope_id=p-bogus → 404; DELETE round-trip + 404 on second DELETE
  • CI green
  • Reviewer to confirm RFC §Test obligations PR2 bar covered

Refs RFC multi-project-context-ui; gates PR3 (multi-scope mutating + Claude user-tier).

🤖 Generated with Claude Code

pandas-studio and others added 2 commits April 29, 2026 20:23
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]>
@memtomem memtomem merged commit 35834e4 into main Apr 29, 2026
7 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 29, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants