Skip to content

feat(web): namespace cluster — preview echo + honest placeholder + helper text (#581)#595

Merged
memtomem merged 4 commits intomainfrom
feat/581-namespace-cluster-polish
Apr 30, 2026
Merged

feat(web): namespace cluster — preview echo + honest placeholder + helper text (#581)#595
memtomem merged 4 commits intomainfrom
feat/581-namespace-cluster-polish

Conversation

@memtomem
Copy link
Copy Markdown
Owner

Summary

Closes #581. Bundles four corrective items in the namespace mental-model
cluster — splitting them touches the same code surface
(_syncIndexHints, _resolve_namespace, the Default NS helper text,
#index-result) four separate times, and UX prose drifts across
releases.

What changed

  • 4.3 Auto-NS preview echo + honest placeholder

    • GET /api/index/preview-namespace?path=...&recursive=true (new)
      returns {resolved_namespaces: list[str | None], truncated, scanned_files}.
      Capped at 200 files so the focus event stays snappy on big dirs.
    • IndexEngine.discover_indexable_files() and resolve_namespaces_for()
      are new public helpers. Both index_path and the preview route call
      them, so the preview cannot silently disagree with the actually-applied
      walk. (engine.py:348-450)
    • IndexingStats.resolved_namespaces (models.py:172) and
      IndexResponse.resolved_namespaces (web/schemas/memory.py:53)
      carry the distinct list end-to-end. The SSE complete event from
      index_path_stream includes it too.
    • Namespace row added to #index-result (index.html:525),
      populated via the shared renderResolvedNamespaces() in app.js
      using the _applied i18n variant.
    • Frontend wiring (settings-config.js:1326-1418) — three event
      surfaces:
      • namespace input focusimmediate preview
      • path input input300ms-debounced preview (skipped when
        the namespace input has an explicit value)
      • namespace input input → invalidate cached preview state
        Late-arrival guard via dataset.previewPath drops stale responses.
    • Placeholder text: ${default_namespace} (auto-determined from path)
      replaces the ambiguous (auto-ns active).
  • 4.13 Hot-reload placeholder when Settings change
    Already wired before this PR: settings-config.js:1172 calls
    _syncConfigToUI() after PATCH success, which calls _syncIndexHints()
    (line 291). With this PR's i18n-keyed suffix, the placeholder
    re-renders correctly when enable_auto_ns flips. The JS reflow is
    not CI-tested
    (pytest-Playwright not in the suite); manual smoke
    is the only gate. See "Verification" below.

  • 4.14 Default NS backward-compat helper text
    Tightened the Default NS field-guide text in _CONFIG_GUIDES
    (settings-config.js:600):

    "Leaving this as 'default' results in untagged chunks (backward
    compatibility — chunks indexed before namespaces existed)."
    English only. _CONFIG_GUIDES bypasses the t() resolver entirely —
    shipping a one-string KO translation in a 6-string-only guide would
    be incoherent. Migrating _CONFIG_GUIDES to i18n keys as a unit is
    tracked as a follow-up.

  • 4.15 Memory_dir root edge case
    No code change. The auto-NS root-skip in _resolve_namespace
    (engine.py:498-503) becomes visible because the new echo surfaces
    the fallback automatically.

Decisions / non-obvious bits

  • Server-side preview, not client-side rule re-impl (option A from
    the issue). Server is the source of truth for resolution rules;
    client-side reimplementation drifts on every server change.
  • List shape, not scalar. A folder with rule-divergent files
    (e.g. alpha/**ns-alpha, beta/**ns-beta) is exactly the
    case where a scalar would silently lie. The list is sorted with
    None last so named NSes render before the untagged sentinel.
  • Helper-text path, not code unification. Making "default" a
    real tag (instead of the current backward-compat None carve-out)
    would force a chunk migration. Documenting the asymmetry is the
    zero-migration value lever.
  • 403 (not 422) for out-of-memory_dirs on the preview route — same
    trust gate as POST /api/index. Comment in
    web/routes/system.py:861-863.
  • File-count cap = 200 chosen so a synchronous focus event doesn't
    stall on memory_dirs with thousands of files. truncated=true lets
    the UI render , scanned 200+. P95 measurement on a 250-file
    fixture in the test suite passes; live measurement on real dirs is
    expected to be sub-100ms.
  • Single PR is the revert unit. Cluster framing means a rollback
    yanks all four sub-tasks together — intended; the entire argument
    for bundling is that the legs disagree if revert-able independently.

Test plan

  • uv run ruff check packages/memtomem/src — clean
  • uv run ruff format --check packages/memtomem/src — clean
  • uv run pytest -m "not ollama" packages/memtomem/tests/ — 3299 passed
  • Backend smoke (live) — isolated HOME, real mm web:
    • Preview leaf file → ["notes"]
    • Preview directory uniform → ["notes"]
    • Preview multi-NS root → ["notes", "scratch"]
    • Preview /etc → 403
    • Preview missing path → 422
    • POST /api/index against multi-NS root →
      {"resolved_namespaces": ["notes", "scratch"], ...}
  • Manual browser smoke (must run before merge — not CI-tested):
    • Settings → toggle enable_auto_ns ON → Save → switch to Index
      tab without page reload → focus Namespace input → placeholder
      reads default (auto-determined from path). Toggle OFF + Save →
      placeholder reverts to default (from config).
    • With Auto-NS ON, paste a path under a memory_dir into Index
      path → focus Namespace input → placeholder shows <folder> (preview)
      immediately on focus.
    • Type a path character-by-character into Index path with
      Namespace empty → DevTools network tab confirms preview fires only
      after typing pauses (~300ms), not on every keystroke.
    • Folder with rule-divergent files → focus shows
      ns_a, ns_b (preview, 2 namespaces).
    • Run an index → result table shows the Namespace row.
    • Toggle locale to KO → repeat above, all new strings
      translated.

Known limitation / risk

If a future PR refactors _syncConfigToUI and drops the
_syncIndexHints call, no automated test will catch the regression
— manual smoke is the only gate. Documented here so a future bisect
notices.

Out of scope (per #581)

  • 4.10a Search NS filter prod surface — different decision axis
    ("NS CRUD model"), tracked in docs: post-STM-extraction cleanup + ground-truth count fixes #5.2.
  • 4.10b NS CRUD prod exposure — needs ADR.
  • 4.14 code-unification (make "default" a real tag) — has migration
    cost, separate PR.
  • _CONFIG_GUIDES → i18n migration — tracked as follow-up so 4.14's
    KO gap can be closed in a coherent unit.

🤖 Generated with Claude Code

pandas-studio and others added 4 commits April 30, 2026 13:59
…er text (#581)

Surfaces what `_resolve_namespace()` actually applies, end-to-end. The
Index-tab input → indexing → result-display path looked coherent but
broke asymmetrically on every leg: a placeholder that lied about the
default, no echo of what was actually applied, and a backward-compat
carve-out (`default_namespace == "default"` → untagged) invisible in
the UI. The cluster gets one PR because UX writing drifts when the same
surface is touched four times.

4.3 — Auto-NS preview echo + honest placeholder
- `IndexEngine.discover_indexable_files()` (new) and
  `resolve_namespaces_for()` (new) — the public file-walk + namespace
  resolution helpers shared by both `index_path` and the new preview
  route, locking file-set parity at the helper layer.
- `GET /api/index/preview-namespace?path=...&recursive=true` returns
  `{resolved_namespaces: list[str | None], truncated, scanned_files}`.
  Capped at 200 files so focus-event latency stays bounded on large
  memory_dirs; `truncated=true` flags the cap.
- `IndexingStats.resolved_namespaces` and `IndexResponse` now carry
  the same list. `index_path_stream` complete event includes it too.
- `index.html`: new `Namespace` row in `#index-result`.
- `app.js`: shared `renderResolvedNamespaces()` honors 1/2/N elements
  and the truncated suffix; result-row populator uses it with `applied`.
- `settings-config.js`: three event surfaces — namespace `focus`
  (immediate preview), path `input` (300ms-debounced, namespace-empty
  guard), namespace `input` (cache invalidation). Per-input late-arrival
  guard via `dataset.previewPath` so stale responses don't overwrite
  fresh ones.
- Placeholder reads `${default_namespace} (auto-determined from path)`
  instead of `(auto-ns active)` — the previous wording sounded like
  *the* default name, hiding that auto-NS would resolve to something
  else.

4.13 — Hot-reload placeholder when Settings change
- Already wired: `_syncConfigToUI()` (called after PATCH /api/config)
  invokes `_syncIndexHints()` which now re-derives the i18n-keyed
  suffix. No new wiring; covered by existing config-reload tests for
  the plumbing layer. JS reflow itself isn't pytest-Playwright-tested
  in CI — manual smoke is the only gate.

4.14 — Default NS backward-compat helper text
- Tightened the `Default NS` field-guide text in `_CONFIG_GUIDES`:
  "Leaving this as 'default' results in untagged chunks (backward
  compatibility — chunks indexed before namespaces existed)." English
  only; the dict bypasses the `t()` resolver — KO migration tracked
  separately so we don't ship a one-string KO translation in a
  6-string-only guide.

4.15 — Memory_dir root edge case
- No code change; the new echo automatically shows the fallback
  (default NS or `(untagged)`) when the user enters a memory_dir root,
  surfacing the auto-NS root-skip rule without a special case.

i18n: 11 new keys in en.json + ko.json (parity required, same commit)
covering placeholder suffixes, the `Namespace` row label, and the
`ns_render.{untagged,single,multi}_{preview,applied}` family that
backs both the placeholder hint and the result-row echo.

Tests: 8 new route-layer tests (leaf/uniform/rule-variance/truncated/
out-of-memory_dirs/missing-path/walk-matches-index-walk for preview;
`resolved_namespaces` in trigger_index response) and 7 new engine
tests (rule-variance distinct list, None-sorts-last, walk parity).

Co-Authored-By: Claude <[email protected]>
Local checks ran ``ruff format --check`` against ``packages/memtomem/src``
only; CI checks the wider set (``src tests tools``) and flagged two
files. Format-only — no behavior change.

Co-Authored-By: Claude <[email protected]>
Two cleanups surfaced in self-review of #595:

1. ``_NS_PREVIEW_STATE`` WeakMap was set in five places and read in zero.
   The intent was tracking "is this placeholder a fresh preview vs.
   config-derived?" so the input handler could be selective, but the
   handler unconditionally invalidates regardless. Drop it; race-guard
   functionality lives in ``dataset.previewPath`` and that still works.

2. The compose tab's ``add-namespace`` input was wired against
   ``add-file``, but ``add-file`` is the *target save path* of a memory
   being composed — it doesn't exist on disk yet.
   ``discover_indexable_files`` only enumerates existing files, so
   focusing the namespace input would always 0-result and render
   ``(untagged) (preview)``, which is exactly the kind of quiet
   misrepresentation this PR is trying to eliminate. Remove the wire;
   leave a comment explaining why a phantom-path API is the right path
   forward (follow-up). The compose tab still gets the config-derived
   placeholder via ``_syncIndexHints``.

Co-Authored-By: Claude <[email protected]>
…ynamic suffix

Manual smoke uncovered a bug introduced by leaving ``data-i18n-placeholder``
on ``<input id="index-namespace">`` and ``<input id="add-namespace">``:
i18n.js ``applyDOM`` ran on every ``langchange`` event and overwrote
``_syncIndexHints``'s dynamic placeholder with the static fallback
``index.ns_placeholder`` value. After a single locale toggle the user
saw ``default (from config)`` even when ``enable_auto_ns=true``.

The fix mirrors the existing ``_refreshAddFilePlaceholder`` pattern (the
codebase already documented this gotcha for ``add-file``):

- Drop ``data-i18n-placeholder`` from both namespace inputs in the HTML;
  keep the static ``placeholder=...`` as a pre-JS fallback.
- Hook ``_syncIndexHints()`` into the ``langchange`` handler so the
  dynamic auto/config suffix re-renders in the new locale.

Verified end-to-end via Playwright on isolated HOME with multi-NS
fixture: A) honest placeholder reflects auto_ns config, B) debounced
typing fires preview, C) focus on namespace fires immediate preview,
D) multi-NS folder renders ``notes, scratch (preview, 2 namespaces)``,
E) KO locale renders Korean strings, F) Settings → Save flips placeholder
without page reload, G) result row after index shows
``notes, scratch (2 namespaces)``, plus live EN↔KO toggle re-renders
the dynamic suffix.

Co-Authored-By: Claude <[email protected]>
@memtomem memtomem merged commit 9e61cb2 into main Apr 30, 2026
7 checks passed
@memtomem memtomem deleted the feat/581-namespace-cluster-polish branch April 30, 2026 05:18
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 30, 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.

feat(web): Namespace cluster — placeholder/echo/hot-reload/default polish (single PR)

2 participants