diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Rescan/RescanButton.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Rescan/RescanButton.tsx index cc73d7f9..cee13a84 100644 --- a/src/apps/officer-web/Screens/Dashboard/Layout/Rescan/RescanButton.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Layout/Rescan/RescanButton.tsx @@ -33,7 +33,14 @@ export function RescanButton() { await Promise.all(ITEM_QUERY_KEYS.map((key) => qc.invalidateQueries({ queryKey: [key], refetchType: 'all' }))); const c = res.counts ?? {}; toast.success( - `Rescanned items — ${c.tasks ?? 0} tasks, ${c.agents ?? 0} agents, ${c.tools ?? 0} tools, ${c.skills ?? 0} skills, ${c.processes ?? 0} processes`, + `Rescanned items — ${c.tasks ?? 0} tasks, ${c.agents ?? 0} agents, ${c.tools ?? 0} tools, ${c.skills ?? 0} skills, ${c.processes ?? 0} processes, ${c.affordances ?? 0} affordances`, + { + // Said here rather than left to be discovered: a rescan cannot reach a chat that is already + // open, because its system prompt was assembled when it started. + description: c.affordances + ? 'Affordances apply to new chats — an open one keeps the set it started with.' + : undefined, + }, ); } catch { toast.error('Failed to rescan items'); diff --git a/src/servers/api/items/rescan.ts b/src/servers/api/items/rescan.ts index 50ae2ce0..55b3ae71 100644 --- a/src/servers/api/items/rescan.ts +++ b/src/servers/api/items/rescan.ts @@ -1,13 +1,25 @@ -import { readdirSync, existsSync } from 'node:fs'; +import { readdirSync, existsSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { createRouter } from '../../create-router'; import { ITEM_TYPES, itemsDir, ensureItemDirs, type ItemType } from '../../data-path'; import { ensureToolLoader } from '../../ensure-tool-loader'; +import { AFFORDANCES_DIR, loadAffordances } from '../../affordances/load'; // Re-applies the boot-time item setup so changes under OFFICER_ITEMS_DIR take effect without a // server restart. Item lists are already read fresh from disk on every request; this ensures the // type dirs exist and the tool-loader extension is current, and reports the live item counts so the // UI can refetch. +// +// ── Affordances are counted, NOT applied ── +// +// They are not an ITEM_TYPE — no directory per item, no canonical file — so they were invisible here +// and a newly written one looked like nothing had happened. +// +// Counting them is as far as this can honestly go. `affordanceBlock()` is read when a chat session is +// CREATED (claude-manager.ts), so a new affordance is already live without a server restart — for the +// next session. Nothing can retrofit one into a conversation that is already running, because its +// system prompt was assembled when it started. Reporting the count says "the file is on disk and the +// platform can see it", which is the part that was genuinely unknowable from the UI. const CANONICAL: Record = { skills: 'SKILL.md', @@ -32,9 +44,16 @@ export const rescanRouter = createRouter(); rescanRouter.post('/', async (ctx) => { ensureItemDirs(); + // So the directory is discoverable on a machine that has never had one, the same way the item dirs are. + mkdirSync(AFFORDANCES_DIR, { recursive: true }); ensureToolLoader(); const counts = Object.fromEntries(ITEM_TYPES.map((type) => [type, countItems(type)])) as Record; - return ctx.json({ ok: true, counts }); + // Loaded rather than file-counted, so the number means "affordances the agent will actually receive" + // rather than "markdown files present" — one with no body is skipped by the loader and should not be + // reported as though it were working. + const affordances = loadAffordances().length; + + return ctx.json({ ok: true, counts: { ...counts, affordances } }); });