git:20260829.b3c4b6f to git:20260907.14c353d
3 added, 2 removed. Audit A to A.
# DOX — packages/kb-plugin/src/client
Files in this directory. One row per source file. See change: add-kb-folder-slot.
| File | Purpose |
|------|---------|
- | `FolderKbSection.tsx` | `sidebar-folder-section` claim. `deriveKbRowState` ordered five-state: error→indexing→not-indexed→stale→populated (error wins over chunks:0). `clientError = reindexError ?? error` forces `error` state (trigger reject OR poll outage) over the derived state; `indexing` spinner survives transient poll blips. Count tooltip `F files · N chunks`. `→` opens kb settings. Pill is STATE-ONLY: `folder-kb-reindex` / `-index-now` / `-retry` and the indexing spinner GONE. ONE `useFolderMenuItem` (`id:"kb-reindex"`, `group:"maintenance"`, `mdiDatabaseRefreshOutline` so it never collides with the menu's plain `mdiRefresh`) varies by state: Retry (error), indexing… (indexing), Index now (not-indexed), Reindex now (stale, populated or loading); `badge` = `N stale` when stale; `disabled = busy`. Scope is `null` when `placement === "card"` (worktree card has no folder menu) — registers nothing. Optimistic `pending` from `useKbStats` renders the `indexing` branch on activation (error still outranks it); `busy` (pending OR polled indexing) covers the whole optimistic window (no double-submit). Inline `folder-kb-stale` marker STAYS in the pill (state, not a control) — badge on the menu item is the action's context, not duplication. See change: add-kb-folder-slot; fix-kb-index-feedback; add-kb-index-optimistic-pending; move-slot-actions-to-menu. |
+ | `FolderKbSection.tsx` | `sidebar-folder-section` claim. `deriveKbRowState` ordered five-state: error→indexing→not-indexed→stale→populated (error wins over chunks:0). `clientError = reindexError ?? error` forces `error` state (trigger reject OR poll outage) over the derived state; `indexing` spinner survives transient poll blips. Count tooltip `F files · N chunks`. `→` opens kb settings. Pill is STATE-ONLY: `folder-kb-reindex` / `-index-now` / `-retry` and the indexing spinner GONE. ONE `useFolderMenuItem` (`id:"kb-reindex"`, `group:"maintenance"`, `mdiDatabaseRefreshOutline` so it never collides with the menu's plain `mdiRefresh`) varies by state: Retry (error), indexing… (indexing), Index now (not-indexed), Reindex now (stale, populated or loading); `badge` = `N stale` when stale; `disabled = busy`. Scope is `null` when `placement === "card"` (worktree card has no folder menu) — registers nothing. Optimistic `pending` from `useKbStats` renders the `indexing` branch on activation (error still outranks it); `busy` (pending OR polled indexing) covers the whole optimistic window (no double-submit). Inline `folder-kb-stale` marker STAYS in the pill (state, not a control) — badge on the menu item is the action's context, not duplication. CARD placement ALSO renders ONE compact `folder-kb-card-reindex` icon-button (`mdiDatabaseRefreshOutline`) as a SIBLING of `SlotPill`, outside the pill root — the card scope has no folder menu. Same state labels as the menu item via `menuLabel` (`aria-label` on the button, `title` on the WRAPPER `span` so the label stays perceivable while `disabled={busy}` swallows mouse events). Click `stopPropagation` + sibling position = the pill's `onActivate`/`onKeyDown` never fires, so it never opens settings. Root div gets `flex items-center gap-1.5 [&>[role=button]]:flex-1` in card placement only; sidebar renders NO sibling. See change: add-kb-folder-slot; fix-kb-index-feedback; add-kb-index-optimistic-pending; move-slot-actions-to-menu; fix-kb-card-refresh-and-shared-stats. |
| `index.tsx` | Client barrel. Exports `FolderKbSection`, `KbSettingsClaim` for plugin-registry. See change: add-kb-folder-slot. |
| `kb-api.ts` | REST client. `fetchKbStats`/`reindexKb`/`fetchKbConfig`/`saveKbConfig`. base64url folder-path codec `encodeFolderPath`/`decodeFolderPath`, `kbSettingsUrl`. Content-type guard: non-JSON body → typed error not parse crash. See change: add-kb-folder-slot. |
+ | `kb-stats-store.ts` | Module-level per-cwd KB stats store — the whole fetch/poll/pending/error machine, hoisted out of the hook. `getKbStatsStore(cwd)` over a never-evicted `Map<cwd, KbStatsStore>`; `resetKbStatsStores()` is TEST-ONLY and MUST run in `beforeEach` of every client test that mounts a consumer (module singleton). Exports `POLL_MS`=1000, `MAX_POLL_MISSES`=3, `REINDEX_GUARD_MS`=4000, `EMPTY_SNAPSHOT`. Refcounted lifecycle: first subscriber → initial fetch; later subscriber → retained snapshot synchronously + background revalidate COALESCED with any in-flight fetch (zero duplicate requests); last unsubscribe → poll stopped, in-flight fetch NOT aborted (result still lands; StrictMode churn harmless), guard LEFT ARMED and self-clears `pending` WITHOUT fetching at zero subscribers. Epoch counter + `AbortController`: non-poll fetches supersede the previous epoch, stale responses discarded. `loading` tracks initial/revalidate/refetch ONLY — poll ticks never toggle it. `reindex()` is a store-level no-op while `pending \|\| stats.indexing` (defense-in-depth behind the consumers' `busy` gates). `update()` replaces the snapshot object ONLY on a real field change — identity is load-bearing for `useSyncExternalStore`. See change: fix-kb-card-refresh-and-shared-stats. |
| `KbSettingsClaim.tsx` | `shell-overlay-route` claim `/folder/:encodedCwd/kb`. Decodes cwd param, renders `KbSettingsPanel`. See change: add-kb-folder-slot. |
| `KbSettingsPanel.tsx` | Per-folder KB path editor. Edits sources[] (add/remove/reorder priority)/include/exclude/dbPath only; other config round-tripped. Shows origin + count. `Save + Reindex`. Worktree bootstrap: `Create project config` + `Copy from parent repo` (`parentRepoOf` derives parent from `.worktrees/` path). Standalone `Reindex now` (`kb-reindex-now`, `mdiDatabaseRefreshOutline`) in BOTH footer branches: gated on `resolvedSources.length > 0` (the disk list `reindexAll` walks) + `saving/busy` — NOT on `dirty`; reason rendered as visible inline text when disabled for zero sources. Error region `kb-settings-error` precedence `bootstrapErr ?? reindexError ?? error(useKbConfig) ?? statsError` (user-initiated before ambient). Bootstrap banner gated on `!isProject && !canIndex`; sources notice two variants keyed on `canIndex` ("(no sources defined)" vs "(no sources — nothing will be indexed)"). Second consumer of `useKbStats` `reindex`/`pending`/`reindexError`. See change: add-kb-folder-slot; fix-kb-settings-reindex-gate. |
| `useKbConfig.ts` | `useKbConfig(cwd)`. GET config, `save(patch)` PUT. Round-trips full config. See change: add-kb-folder-slot. |
- | `useKbStats.ts` | `useKbStats(cwd)`. Fetch `/api/kb/stats`, `reindex()` POST (202→`refetch()` engages poll). Polls every 1000ms while `indexing`, stops on settle. Two error channels: `reindexError` (trigger POST rejected, no job) vs `error` (poll outage, surfaced only after `MAX_POLL_MISSES`=3 consecutive misses — a lone blip keeps the spinner, never `clearPoll`s). Optimistic `pending` boolean: set `true` synchronously in `reindex()` (spinner on click), cleared ONLY on POST reject / a `/stats` poll seeing `indexing:true` / a `REINDEX_GUARD_MS` timeout guard (`refetch()`s) — never on the bare `202`, so no spinner flicker/wedge. TWO consumers of `reindex`/`pending`: the folder slot AND `KbSettingsPanel` — a refactor cannot assume the slot is the only caller; the double-submit `disabled` guard is CONSUMER-side (derived `busy`), not in the hook. See change: add-kb-folder-slot; fix-kb-index-feedback; add-kb-index-optimistic-pending; fix-kb-settings-reindex-gate. |
+ | `useKbStats.ts` | `useKbStats(cwd)` — THIN `useSyncExternalStore` subscription to the per-cwd store in `kb-stats-store.ts`; holds no state of its own. Public `UseKbStatsResult` (`stats/loading/error/reindexError/pending/reindex/refetch`) unchanged; re-exports `POLL_MS`/`MAX_POLL_MISSES`/`REINDEX_GUARD_MS`/`resetKbStatsStores`. Null cwd → inert `EMPTY_SNAPSHOT`, no store entry. THREE consumer shapes (sidebar section, worktree-card section, `KbSettingsPanel`) now share ONE snapshot + ONE poll loop per folder, so a reindex in any surface converges the others live with no remount/reload. DELIBERATE: `error` and `reindexError` are shared FOLDER state — a settings-panel trigger reject flips the sidebar/card row to `error` too. The double-submit `disabled` guard stays CONSUMER-side (derived `busy`); the store's in-flight no-op is defense-in-depth. See change: add-kb-folder-slot; fix-kb-index-feedback; add-kb-index-optimistic-pending; fix-kb-settings-reindex-gate; fix-kb-card-refresh-and-shared-stats. |