Skip to content

fix(web): surface "why nothing happened" on context Sync/Import#549

Merged
memtomem merged 3 commits intomainfrom
feat/web-ctx-empty-state-reason-code
Apr 29, 2026
Merged

fix(web): surface "why nothing happened" on context Sync/Import#549
memtomem merged 3 commits intomainfrom
feat/web-ctx-empty-state-reason-code

Conversation

@memtomem
Copy link
Copy Markdown
Owner

@memtomem memtomem commented Apr 29, 2026

Summary

PR1 of the multi-project context UI series (single-project UX gap fix —
no behavior change for users with populated .memtomem/skills/ etc.).

User reported that mm web → Settings → Skills/Commands/Agents → click
Sync or Import showed a green-toast "completed" message but
nothing actually moved on disk. Root cause was a wire shape mismatch
plus missing metadata in the response, not a wiring bug. Both buttons
were correctly wired to backend handlers; the handlers correctly did
nothing because the canonical (.memtomem/skills/) and runtime
(.claude/skills/, .gemini/skills/, .agents/skills/) directories
were empty in cwd. The UI just didn't tell the user that.

Changes

  • Core (memtomem.context.{skills,commands,agents}): skipped items
    are now (name, reason, reason_code) triples. New module
    memtomem.context._skip_reasons enumerates the codes:
    no_canonical_root, unknown_runtime, parse_error, invalid_name,
    already_imported, canonical_exists, toml_parse_error. Human
    reason strings stay (CLI/log output unchanged byte-for-byte).
  • Web routes: sync responses gain canonical_root: str. Import
    responses gain project_root: str + scanned_dirs: list[str]. Each
    skipped[] entry carries reason_code. All additive.
  • Web client: sync handler now reads data.skipped (was only
    reading data.dropped, which is empty for skills); on
    reason_code === "no_canonical_root" it shows
    "No canonical {type} under {canonical}. Create one first." info
    toast. Import handler on a 0/0 result shows
    "No runtime {type} found in {project_root}. Scanned: {scan_dirs}."
    info toast. Empty list state hint points at the canonical + scan
    paths instead of generic "Create one or import" copy.
  • i18n: three new placeholder-form keys (empty_hint,
    import_no_runtimes, sync_empty_canonical) cover all 3 types via
    {type} substitution — 3 keys instead of 9. en.json + ko.json
    updated.
  • Cache-bust: context-gateway.js?v=3.
  • Tests: 3 core test suites updated for the 3-tuple shape; new web
    route assertions verify reason_code, canonical_root,
    project_root, scanned_dirs in sync/import responses for the
    empty-canonical and empty-runtime paths.

Compatibility

HTTP wire shape — all changes are additive. Existing clients that
read imported / generated / skipped[].name|runtime|reason keep
working.

Python API tuple shape — breaking for external destructuring.
SkillSyncResult.skipped, CommandSyncResult.skipped,
AgentSyncResult.skipped, and the shared ExtractResult.skipped are
typed list[tuple[str, str]]list[tuple[str, str, str]]. All
in-tree callers were updated in the same commit (CLI
cli/context_cmd.py, MCP server/tools/context.py, web routes,
tests). External Python consumers that unpack
for name, reason in result.skipped: would need to add a third
binding — we don't expect any (these dataclasses are not part of the
documented public API), but flagging for completeness.

CLI (mm context skills/agents/commands ...) and the MCP context
tool also unpack the triple via _code discard so their output is
byte-identical.

Test plan

  • uv run pytest packages/memtomem/tests/ -m "not ollama" — 3152
    passed.
  • uv run ruff check packages/memtomem/src && uv run ruff format --check packages/memtomem/src — clean.
  • Manual smoke: mm web against an empty cwd → both Sync and
    Import responses verified via curl. Sync returns skipped with
    reason_code: "no_canonical_root" + canonical_root: ".memtomem/skills". Import returns project_root +
    scanned_dirs: [".claude/skills", ".gemini/skills", ".agents/skills"].
  • Browser smoke (reviewer): clear cache, click Sync on an empty
    project — see No canonical skills under .memtomem/skills/. Create one first. info toast (was: green "Sync completed"). Click
    Import — see No runtime skills found in <cwd>. Scanned: .claude/skills, .gemini/skills, .agents/skills. info toast (was:
    green "Import completed").

Follow-ups (separate PRs per the approved RFC sequence)

  • RFC docs/rfc/multi-project-context-ui.md — multi-project aggregate
    view design.
  • PR2 — multi-project read-only (Add Project + lazy GET groups).
  • PR3 — multi-project write + Claude user-tier (~/.claude/skills/).
  • PR4 — Gemini/Codex user-tier + alias unification (after CLI
    verification).

Optional polish (not blocking PR1):

  • Tighten reason_code to Literal[...] / StrEnum for stricter
    typing of the code field in dataclass + route response builders.
  • Strengthen test_import_empty_carries_meta to assert the
    project_root value matches tmp_path, not just key presence.

🤖 Generated with Claude Code

pandas-studio and others added 2 commits April 29, 2026 17:35
…nc+Import

Users reported "Sync 도 Import 도 동작이 이상함" — both buttons in
mm web → Settings → Skills/Commands/Agents looked successful (a green
toast popped) but nothing actually moved on disk. The root cause was a
shape mismatch in the response handling, not a wiring bug:

- For skills, the sync response carries `skipped` (e.g. "no canonical
  skills") but never `dropped`. The client only read `dropped`, so the
  skipped reason was silently dropped on the floor.
- The import response carried no metadata at all, so a 0-imported,
  0-skipped result (the common case when no `.claude/skills/` exists in
  the project) just rendered "Import completed" with no clue that the
  scanner found nothing because the directories didn't exist.

This change keeps cwd-bound single-project behavior unchanged and adds
machine-readable plumbing the UI can match against:

- All three context types' `Sync` and `Import` core layers
  (`memtomem.context.{skills,commands,agents}`) now record skipped items
  as `(name, reason, reason_code)` triples. Reason codes come from a new
  `memtomem.context._skip_reasons` module: `no_canonical_root`,
  `unknown_runtime`, `parse_error`, `invalid_name`, `already_imported`,
  `canonical_exists`, `toml_parse_error`. Human `reason` strings stay
  for CLI/log output.
- Web route handlers (`web/routes/context_{skills,commands,agents}.py`)
  surface `reason_code` per skipped item, plus `canonical_root` on sync
  responses and `project_root` + `scanned_dirs` on import responses.
- The web client (`context-gateway.js`) reads `data.skipped` (in
  addition to `data.dropped` for commands/agents field-level drops),
  matches on `reason_code === "no_canonical_root"` to show
  "No canonical {type} under {canonical}. Create one first." instead of
  a generic success, and on a 0/0 import shows "No runtime {type} found
  in {project_root}. Scanned: {scan_dirs}." so users see exactly which
  paths the detector inspected.
- The empty list state hint now points at the canonical and runtime
  paths instead of "Create one or import from existing runtimes." — a
  fresh user can drop a `SKILL.md` into the right directory without
  guessing.
- Three new placeholder-form i18n keys (`empty_hint`,
  `import_no_runtimes`, `sync_empty_canonical`) cover all three types
  via `{type}` / `{canonical}` / `{root}` / `{scan_dirs}` substitution
  rather than 9 type-specific keys.
- `context-gateway.js?v=2` cache-bust.

All response changes are additive — existing clients that only read
`imported`/`generated`/`skipped[].name|runtime|reason` keep working
unchanged. The CLI (`mm context skills/agents/commands ...`) and the
MCP `context` tool also unpack the new triple shape via `_code` discard
so their human-readable output is byte-identical.

Tests:
- 3 core test suites (`test_context_{skills,commands,agents}.py`)
  updated to the 3-tuple shape.
- New web route assertions verify `reason_code`, `canonical_root`,
  `project_root`, and `scanned_dirs` are present in sync/import
  responses for the empty-canonical and empty-runtime paths.
- Full suite green: `pytest -m "not ollama"` 3152 passed; `ruff check`
  + `ruff format --check` clean.

Co-Authored-By: Claude <[email protected]>
… in toast, align canonical_root style

Follow-up to PR #549 review:

- **#1 (low-medium)**: drop the hardcoded `_CTX_EMPTY_HINT_META` JS map.
  GET `/api/context/{skills,commands,agents}` now also returns
  `canonical_root: str` and `scanned_dirs: list[str]` (computed once at
  module import from `SKILL_DIRS` / `AGENT_DIRS` / `COMMAND_DIRS`), so the
  empty-state hint pulls from the wire instead of duplicating the
  detector layout client-side. The JS still has a one-line fallback
  (`.memtomem/${type}` + empty list) for older backends, but it never
  fires when the cache-bust takes effect.
- **#2 (cosmetic)**: skills sync response now uses the
  `CANONICAL_SKILL_ROOT = ".memtomem/skills"` constant directly,
  matching the `CANONICAL_AGENT_ROOT` / `CANONICAL_COMMAND_ROOT` pattern
  in agents/commands routes. Drops the `_safe_rel(canonical_skills_root(
  project_root), project_root)` round-trip — both forms resolve to the
  same string today, but a constant won't drift if a future PR adds env
  / scope resolution to `canonical_skills_root()`.
- **#3 (low)**: import-no-runtimes toast renders
  `basename(project_root)` instead of the absolute path. The wire still
  carries the absolute string (consistent with `/api/system/*` and
  useful for debugging / reverse-proxy contexts) but the user-facing
  toast keeps it short — `scanned_dirs` already gives full orientation.
  New `_ctxBasename()` helper handles the POSIX path split.

Cache-bust bumped to `context-gateway.js?v=3`.

Tests:
- New assertions on `TestListSkills.test_empty`,
  `TestListCommands.test_empty`, `TestListAgents.test_empty` verify
  GET response now includes `canonical_root` + `scanned_dirs`.
- `pytest -m "not ollama"` 3152 passed; `ruff check` + `ruff format
  --check` clean.

Skipped per reviewer guidance:
- #4 `Final[str]` → `Literal` / `StrEnum` (mypy is advisory).
- #5 additional positive route assertions for `invalid_name` /
  `already_imported` / `toml_parse_error` (route layer is a trivial
  dict comprehension; covered at the core layer).
- #6 Korean `<이름>` literal angle-bracket (intentional placeholder
  guide, not an unsubstituted variable).

Co-Authored-By: Claude <[email protected]>
@memtomem
Copy link
Copy Markdown
Owner Author

Addressed review in f48b43a:

  • chore: prepare for open-source release #1 (low-medium) — dropped the hardcoded _CTX_EMPTY_HINT_META JS map. GET /api/context/{type} now returns canonical_root + scanned_dirs (computed once at module import from SKILL_DIRS / AGENT_DIRS / COMMAND_DIRS), so the empty-state hint reads from the wire. The JS keeps a one-line .memtomem/${type} fallback for older backends but it never fires once the cache-bust takes effect.
  • refactor(stm): decouple surfacing via remote-only MCP #2 (cosmetic) — skills sync response now uses the CANONICAL_SKILL_ROOT constant directly, matching the CANONICAL_AGENT_ROOT / CANONICAL_COMMAND_ROOT pattern in the other two routes. Drops the _safe_rel(canonical_skills_root(...), ...) round-trip — same string today, but won't drift if a future PR adds env/scope resolution to canonical_skills_root().
  • chore(core): strip STM integration residue #3 (low) — import-no-runtimes toast renders basename(project_root). Wire stays absolute (consistent with /api/system/*); UI keeps the toast short since scanned_dirs already gives orientation. New _ctxBasename() helper.

Cache-bust bumped to context-gateway.js?v=3.

Tests: TestListSkills.test_empty / TestListCommands.test_empty / TestListAgents.test_empty now assert the new GET fields. pytest -m "not ollama" 3152 passed; ruff check + ruff format --check clean.

Skipped per your guidance:

Tightens the typing on the third element of `(name, reason, reason_code)`
triples produced by `SkillSyncResult.skipped`, `CommandSyncResult.skipped`,
`AgentSyncResult.skipped`, and `ExtractResult.skipped`. The new
`memtomem.context._skip_reasons.SkipCode` is a `Literal` of the seven
codes already enumerated in that module, so a typo at the construction
site fails type-check instead of slipping through to the wire.

Also strengthens the import-empty web-route tests for skills, commands,
and agents to assert `data["project_root"] == str(tmp_path)` instead of
just key presence — confirms the response actually echoes the
fixture's project root rather than some unrelated path.

No runtime behavior change; addresses follow-up polish flagged in the
review of this PR.

Co-Authored-By: Claude <[email protected]>
@memtomem memtomem merged commit 73713ad 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