feat(web): global indexing-progress state + cross-tab indicator (#582)#602
Merged
feat(web): global indexing-progress state + cross-tab indicator (#582)#602
Conversation
memtomem
pushed a commit
that referenced
this pull request
Apr 30, 2026
Self-review on PR #602 surfaced four items, three code-level + one PR-body. Code changes: 1. (must-fix) ``runIndexStream`` does not wrap ``new EventSource`` in try/catch. ``EventSource`` can throw synchronously (malformed URL, browser storage policy edge cases, ``SecurityError`` in cross-origin contexts). Without a guard, a throw between ``_indexingTryStart`` and the handler registration leaves ``STATE.indexing`` stuck on ``true`` and blocks every subsequent indexing trigger across all surfaces until page reload. Added a try/catch around the ``URLSearchParams`` + ``new EventSource`` setup that mirrors the existing failure-path code (toast + hide progress + ``btnLoading off`` + ``_indexingEnd``). 2. (smaller observation) ``var(--accent, #4a90e2)`` fallbacks in ``.indexing-pill`` are dead code — ``--accent`` is defined in both theme blocks (``style.css:12, :1927, :1949``) and the fallback colour ``#4a90e2`` is also from neither theme. Dropped the fallbacks to match the rest of the file. 3. (smaller observation) Renamed i18n key ``indexing.indicator`` → ``index.indicator`` so it co-locates with the other ``index.*`` keys in the same JSON neighbourhood. ``toast.indexing_in_progress`` stays in the toast group, which matches every other toast key. (PR body framing — concurrency claim, double-click vs cross-surface, reload caveat — applied via ``gh pr edit`` separately so the diff stays focused on code.) Verified: - ``node --check`` on the modified JS file - ``uv run pytest -k i18n`` — 13 passed (parity holds with renamed key) - ``ruff check`` + ``ruff format --check`` — clean Co-Authored-By: Claude <[email protected]>
Issue #582 item 4.11. Three indexing entry points (Index tab streaming ``runIndexStream``, Sources card per-dir ``mdReindexOne``, Sources "Reindex All" ``mdReindexAll``) used local ``btnLoading`` state only, so a user mid-index on one surface could click a second indexing trigger on another and start a concurrent run that races on the same DB rows. There was also no signal that indexing was still in flight once the user navigated away from the Index tab. Centralizes the state in a single ``STATE.indexing`` flag plus two helpers: - ``_indexingTryStart()`` — toast + early-return if a run is already active; otherwise sets the flag and shows the header indicator. - ``_indexingEnd()`` — clears the flag and hides the indicator. Called from each handler's success / error / SSE-fallback paths. All three handlers now gate on ``_indexingTryStart`` and reset via ``_indexingEnd`` in their finally / completion branches. A new ``#indexing-indicator`` pill in the header (a third ``stat-pill`` next to chunks/sources counts) is the cross-tab signal — styled as a pulsing variant of the existing pill (respects ``prefers-reduced-motion``). ``aria-live="polite"`` announces it to assistive tech without interrupting other speech. i18n: two new keys with en/ko parity (``test_i18n.py`` 13 passed): - ``indexing.indicator``: "⏳ Indexing…" / "⏳ 인덱싱 중…" - ``toast.indexing_in_progress``: "Indexing already in progress" / "이미 인덱싱이 진행 중입니다" ``/api/config`` race-safety check (per the umbrella plan): grep confirmed the config refresh is event-driven (initial fetch + ``visibilitychange`` on Config tab + post-action ``fetchServerConfig`` calls in ``app.js:863`` / ``settings-config.js:1414, 1424``) — no ``setInterval`` polling. ``STATE.indexing`` is independent from ``STATE.serverConfig``, so a Config-tab refresh during indexing cannot clobber the flag. A single boolean is sufficient; no epoch/version bookkeeping needed. Static asset cache-bust (per feedback_static_asset_cache_bust.md): ``style.css?v=68→69``, ``app.js?v=72→73``, ``sources-memory-dirs.js?v=6→7``. Verified: - ``node --check`` passes on both modified JS files - ``uv run pytest -m "not ollama"`` — 3309 passed - ``ruff check`` + ``ruff format --check`` — clean Co-Authored-By: Claude <[email protected]>
Self-review on PR #602 surfaced four items, three code-level + one PR-body. Code changes: 1. (must-fix) ``runIndexStream`` does not wrap ``new EventSource`` in try/catch. ``EventSource`` can throw synchronously (malformed URL, browser storage policy edge cases, ``SecurityError`` in cross-origin contexts). Without a guard, a throw between ``_indexingTryStart`` and the handler registration leaves ``STATE.indexing`` stuck on ``true`` and blocks every subsequent indexing trigger across all surfaces until page reload. Added a try/catch around the ``URLSearchParams`` + ``new EventSource`` setup that mirrors the existing failure-path code (toast + hide progress + ``btnLoading off`` + ``_indexingEnd``). 2. (smaller observation) ``var(--accent, #4a90e2)`` fallbacks in ``.indexing-pill`` are dead code — ``--accent`` is defined in both theme blocks (``style.css:12, :1927, :1949``) and the fallback colour ``#4a90e2`` is also from neither theme. Dropped the fallbacks to match the rest of the file. 3. (smaller observation) Renamed i18n key ``indexing.indicator`` → ``index.indicator`` so it co-locates with the other ``index.*`` keys in the same JSON neighbourhood. ``toast.indexing_in_progress`` stays in the toast group, which matches every other toast key. (PR body framing — concurrency claim, double-click vs cross-surface, reload caveat — applied via ``gh pr edit`` separately so the diff stays focused on code.) Verified: - ``node --check`` on the modified JS file - ``uv run pytest -k i18n`` — 13 passed (parity holds with renamed key) - ``ruff check`` + ``ruff format --check`` — clean Co-Authored-By: Claude <[email protected]>
b69e009 to
4d5d6e4
Compare
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
Issue #582 item 4.11. Adds a single
STATE.indexingflag + cross-tab header indicator so indexing runs from any of the three trigger surfaces (Index tab streaming, Sources card per-dir, Sources "Reindex All") (a) don't trigger redundant runs that queue behind the server-side_index_lock(engine.py:476) and produce a confusing two-progress-stream UX, and (b) stay visible after the user navigates away from the launching tab.The actual gap this closes is cross-surface trigger gating: same-button rapid-fire was already handled by
btn.disabled = true(browsers don't dispatch click on disabled buttons), but Index tab → Sources card or Sources card → Index tab go through different DOM elements with independent disabled state. This PR makes them share one global guard.What changed
web/static/app.js:STATE.indexing: falseadded to the global state object.btnLoading:_indexingTryStart()— early-returns with a toast if a run is already active; otherwise sets the flag and shows the header indicator._indexingEnd()— clears the flag and hides the indicator. Called from each handler's success / error / SSE-fallback paths.runIndexStreamgates entry on_indexingTryStartand resets via_indexingEndoncomplete/onerror/ SSE-malformed-3-strike paths. Thenew EventSource(...)setup is wrapped in try/catch — a synchronous throw there would leaveSTATE.indexingstuck ontrueand block every subsequent indexing trigger across all surfaces until page reload.web/static/sources-memory-dirs.js:mdReindexOneandmdReindexAllgate on_indexingTryStartand call_indexingEndin theirfinallyblocks. Cross-file references usetypeof === 'function'defensive checks consistent with existing patterns in the file.web/static/index.html:<span id="indexing-indicator" class="stat-pill indexing-pill" hidden ... aria-live="polite">inheader-info-bar, alongside the existing chunks/sources stat-pills.style.css?v=68→69,app.js?v=72→73,sources-memory-dirs.js?v=6→7.web/static/style.css:.indexing-pill— accent-colored variant of.stat-pillwith a 1.6s pulse animation. Respectsprefers-reduced-motion.web/static/locales/{en,ko}.json:index.indicator— pill texttoast.indexing_in_progress— early-return toast/api/configrace model — settledThe umbrella plan asked for a pre-investigation: is
/api/configpolled (setInterval) or push-based? Grep acrossapp.jsandsettings-config.jsconfirms it's event-driven, not polled:settings-config.js:1414visibilitychange(Config tab only):settings-config.js:1420-1425app.js:863(after embedding-reset)app.js:701(if (sectionName === 'config') loadConfig())No
setIntervalon/api/config. The fetch writesSTATE.serverConfigand does not touchSTATE.indexing, so even if a user switches to Config mid-indexing, the indicator state survives. Single boolean flag is sufficient — no epoch/version bookkeeping needed.Limitations (not fixed in this PR)
STATE.indexingresets tofalseand the indicator hides — but the indexing run continues server-side. The user loses the visual signal until the run completes and an unrelated event re-fetches/api/stats. Closing this gap properly needs a server endpoint likeGET /api/indexing/activethat the page queries on load. Tracked as a follow-up in the umbrella; out of scope for this PR.Test plan
uv run pytest -m "not ollama"— 3309 passed, 46 deselecteduv run pytest -k i18n— 13 passed (en/ko parity check picks up the two new keys)uv run ruff check packages/memtomem/src && uv run ruff format --check packages/memtomem/src— cleannode --checkon both modified JS files — cleanReferences
~/.claude/plans/https-github-com-memtomem-memtomem-issue-structured-anchor.md("PR docs: slim READMEs, add configuration + embeddings guides #6 (4.11)")feedback_static_asset_cache_bust.mdproject_web_i18n.md🤖 Generated with Claude Code