From 57dd25993c13cbafacf8e4511d517b819b717562 Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Tue, 22 Sep 2026 14:04:15 +0300 Subject: [PATCH] Research tool row grays on the registry's own search signal (Teacher flag) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The teacher registry drops the research tool when no search engine is live (chat-handler's searchAvailable, 2026-09-20) — the settings row now grays on the SAME /api/search signal, so the row and the actual registry availability can never disagree (Teacher-stream flag, 2026-09-22, routed by master). - researchToolUnavailable (pure): research grays exactly when the live status says unavailable; a loading/failed fetch grays nothing; the other tools never touch the search port. - The row: whole-row dim (graph-view graying) + disabled toggle + the status reason as the hint — the same line the search-engine row under it speaks from. Two live-caught fixes while here: - The reason line refreshed IN the pick handler raced the section PUT (the hint could contradict the trigger it sits under — caught with a Brave pin: trigger 'Brave', hint still 'tavily (auto)'). The refresh now keys on the agent prop's engine pin, so it reads post-write state. - The engine menu options render from a map without React keys (console warning) — keyed. Live-verified on 3001: no engine key → research dimmed with reason, siblings untouched; Tavily key saved + section reopened → row alive ('search engine: tavily (auto)'); Brave pinned without its key → both rows gray with the honest missing-key reason; Automatic restored. Gates: typecheck, lint, 803/803, build. --- components/settings/agent-section.tsx | 56 +++++++++++++++++++++------ tests/settings-pure.test.ts | 23 ++++++++++- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/components/settings/agent-section.tsx b/components/settings/agent-section.tsx index 72e053d..5a3608a 100644 --- a/components/settings/agent-section.tsx +++ b/components/settings/agent-section.tsx @@ -96,6 +96,19 @@ export function composeTool(agent: AgentSettings | undefined, id: string, on: bo return { ...agent, tools: { ...agent?.tools, [id]: on } }; } +/** The research tool runs on the search port — the teacher registry DROPS + * it when no engine is live (chat-handler's searchAvailable, the same + * /api/search signal the section fetches). Pure — exported for tests. + * Teacher-stream flag 2026-09-22: the settings row grays on the SAME + * signal, so the row and the actual registry availability can never + * disagree. `search === null` (loading/failed) grays NOTHING. */ +export function researchToolUnavailable( + toolId: string, + search: { available: boolean } | null, +): boolean { + return toolId === "research" && search !== null && !search.available; +} + /** Compose: one slot's model/thinking override. Empty model = absent * override (Default — falls to the factory pin); empty thinking = the * tier keeps its resolved thinking budget. Per-field independence, same @@ -295,7 +308,6 @@ export function AgentSection({ .then((r) => r.json()) .then((body: { prober: SegmentInfo[]; teacher: SegmentInfo[] }) => setPrompts(body)) .catch(() => setPrompts(null)); - refreshSearch(); fetch("/api/keys") .then((r) => r.json()) .then((body: { keys: { id: string; present: boolean }[] }) => { @@ -309,6 +321,14 @@ export function AgentSection({ .catch(() => {}); }, []); + // The reason line re-reads /api/search AFTER an engine write lands — + // the write updates the `agent` prop (the dialog's record mirror), so + // keying on the pin avoids the race where the fetch beat the PUT and + // the hint contradicted the trigger it sits under. + useEffect(() => { + refreshSearch(); + }, [agent?.search?.engine]); + // The dropdown contract (the chat model selector's, mirrored): any // click outside the open menu — or an Esc — closes it. Esc is CLAIMED // with stopPropagation so the settings dialog behind does NOT also @@ -393,6 +413,7 @@ export function AgentSection({ onPick: () => void, ) => (