add header rescan button to apply officer-items changes without restart

New POST /api/rescan re-runs the boot item setup (ensureItemDirs +
ensureToolLoader) and returns live item counts; the header button calls it
and invalidates the item query caches so the UI refetches from disk.

Also fix ensure-tool-loader to write into OFFICER_ITEMS_DIR/extensions (the
runtime read path) instead of the now-unread DATA_PATH/extensions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 01:31:51 +00:00
co-authored by Claude Opus 4.8
parent 86a547b216
commit b9eb78c919
5 changed files with 89 additions and 2 deletions
@@ -6,6 +6,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sh
import type { DockItem } from '../Dock';
import { useIsTouch } from '../useIsTouch';
import { UserMenu } from './UserMenu';
import { RescanButton } from '../Rescan/RescanButton';
// import { BugReportButton } from '../BugReport/BugReportButton';
type HeaderProps = {
@@ -52,6 +53,7 @@ export function Header({ dockItems }: HeaderProps) {
*/}
{/* Bug Report — hidden */}
{/* <BugReportButton /> */}
<RescanButton />
<UserMenu />
</div>
</div>
@@ -0,0 +1,44 @@
import { useState } from 'react';
import { RotateCw } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { useClient } from 'hooks/useClient';
type RescanResponse = { ok: boolean; counts: Record<string, number> };
// Item query caches the header refreshes after a rescan (matches the queryKeys used by the
// Automation and Capability pages).
const ITEM_QUERY_KEYS = ['tasks', 'skills', 'tools', 'processes'];
export function RescanButton() {
const client = useClient();
const qc = useQueryClient();
const [loading, setLoading] = useState(false);
const rescan = async () => {
if (loading) return;
setLoading(true);
try {
const res = await client.post<RescanResponse>('/rescan', {});
await Promise.all(ITEM_QUERY_KEYS.map((key) => qc.invalidateQueries({ queryKey: [key] })));
const c = res.counts ?? {};
toast.success(`Rescanned items — ${c.tasks ?? 0} tasks, ${c.tools ?? 0} tools, ${c.skills ?? 0} skills, ${c.processes ?? 0} processes`);
} catch {
toast.error('Failed to rescan items');
} finally {
setLoading(false);
}
};
return (
<button
onClick={rescan}
disabled={loading}
className="flex h-8 w-8 items-center justify-center rounded-full bg-black/20 text-duck-dark transition-transform hover:scale-110 disabled:opacity-50"
aria-label="Rescan items"
title="Rescan officer-items (apply changes without restarting)"
>
<RotateCw size={16} className={loading ? 'animate-spin' : ''} />
</button>
);
}