fix(web): surface "why nothing happened" on context Sync/Import#549
Merged
fix(web): surface "why nothing happened" on context Sync/Import#549
Conversation
…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]>
Owner
Author
|
Addressed review in
Cache-bust bumped to Tests: 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]>
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
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 → clickSync 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/) directorieswere empty in cwd. The UI just didn't tell the user that.
Changes
memtomem.context.{skills,commands,agents}): skipped itemsare now
(name, reason, reason_code)triples. New modulememtomem.context._skip_reasonsenumerates the codes:no_canonical_root,unknown_runtime,parse_error,invalid_name,already_imported,canonical_exists,toml_parse_error. Humanreasonstrings stay (CLI/log output unchanged byte-for-byte).canonical_root: str. Importresponses gain
project_root: str+scanned_dirs: list[str]. Eachskipped[]entry carriesreason_code. All additive.data.skipped(was onlyreading
data.dropped, which is empty for skills); onreason_code === "no_canonical_root"it shows"No canonical {type} under {canonical}. Create one first."infotoast. 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.
empty_hint,import_no_runtimes,sync_empty_canonical) cover all 3 types via{type}substitution — 3 keys instead of 9. en.json + ko.jsonupdated.
context-gateway.js?v=3.route assertions verify
reason_code,canonical_root,project_root,scanned_dirsin sync/import responses for theempty-canonical and empty-runtime paths.
Compatibility
HTTP wire shape — all changes are additive. Existing clients that
read
imported/generated/skipped[].name|runtime|reasonkeepworking.
Python API tuple shape — breaking for external destructuring.
SkillSyncResult.skipped,CommandSyncResult.skipped,AgentSyncResult.skipped, and the sharedExtractResult.skippedaretyped
list[tuple[str, str]]→list[tuple[str, str, str]]. Allin-tree callers were updated in the same commit (CLI
cli/context_cmd.py, MCPserver/tools/context.py, web routes,tests). External Python consumers that unpack
for name, reason in result.skipped:would need to add a thirdbinding — 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 MCPcontexttool also unpack the triple via
_codediscard so their output isbyte-identical.
Test plan
uv run pytest packages/memtomem/tests/ -m "not ollama"— 3152passed.
uv run ruff check packages/memtomem/src && uv run ruff format --check packages/memtomem/src— clean.mm webagainst an empty cwd → both Sync andImport responses verified via curl. Sync returns
skippedwithreason_code: "no_canonical_root"+canonical_root: ".memtomem/skills". Import returnsproject_root+scanned_dirs: [".claude/skills", ".gemini/skills", ".agents/skills"].project — see
No canonical skills under .memtomem/skills/. Create one first.info toast (was: green "Sync completed"). ClickImport — 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)
docs/rfc/multi-project-context-ui.md— multi-project aggregateview design.
~/.claude/skills/).verification).
Optional polish (not blocking PR1):
reason_codetoLiteral[...]/StrEnumfor strictertyping of the
codefield in dataclass + route response builders.test_import_empty_carries_metato assert theproject_rootvalue matchestmp_path, not just key presence.🤖 Generated with Claude Code