22 * useAiSurfaceEnabled
33 *
44 * Single source of truth for "should the in-UI AI surface be shown on this
5- * deployment?". The console ships under MIT and is edition-agnostic: it never
6- * knows at build time whether the runtime is a Community Edition (framework
7- * only, no cloud AI package) or a full cloud install. It decides purely at
8- * runtime from what the server reports — no `VITE_EDITION` flag, no tree-shake.
5+ * deployment?". The console ships under MIT and is edition-agnostic: it decides
6+ * purely at runtime from what the server reports — no `VITE_EDITION` flag, no
7+ * tree-shake.
98 *
10- * The signal is **a non-empty agent catalog** (`GET /api/v1/ai/agents`), NOT
11- * the discovery `services.ai` flag. That distinction matters:
9+ * The signal is whether the **`@objectstack/service-ai` capability is present**,
10+ * as reported by discovery (`/discovery` → `services.ai.enabled &&
11+ * status === 'available'`, i.e. `isAiEnabled`).
1212 *
13- * The `ask`/`build` agent *personas* are a commercial feature that moved to
14- * the cloud-only `@objectstack/service-ai-studio` package; the open-source
15- * framework keeps a HEADLESS `@objectstack/service-ai` that still
16- * `registerService('ai')`s. So on a Community Edition runtime discovery can
17- * STILL report `services.ai` as available (the service is running) while the
18- * agent catalog is empty (no persona attached). Gating on `isAiEnabled` would
19- * then leave the FAB / "Ask AI" affordances visible with nothing to talk to —
20- * a dead end. The catalog is the real "is there an agent to answer?" signal,
21- * and it's exactly what the Home "Build/Ask AI" CTAs already gate on, so every
22- * AI entry point now agrees.
13+ * `service-ai` is an ENTERPRISE capability: a Community-Edition runtime does not
14+ * depend on it, so the framework never registers the AI service and discovery
15+ * reports `services.ai` unavailable → the whole AI surface hides. An install
16+ * that ships `service-ai` reports it available → AI shows. It is the presence of
17+ * the CAPABILITY that gates, NOT whether any specific agent happens to be
18+ * configured yet (an install with `service-ai` but no agents has AI "available";
19+ * AiChatPage degrades gracefully if the catalog is empty).
2320 *
24- * The `VITE_AI_BASE_URL` opt-in flows through naturally: {@link resolveAiApiBase}
25- * points the catalog fetch at the configured server, so an external AI server
26- * with agents lights the surface up and an agent-less one keeps it hidden.
21+ * The framework only registers the AI service when the host app declares
22+ * `@objectstack/service-ai`, so discovery's `services.ai` is an honest edition
23+ * signal (see objectstack-ai/framework#2311). Earlier this hook gated on the
24+ * agent catalog as a workaround for the headless service reporting itself
25+ * available in CE; with #2311 that no longer happens, so discovery is correct.
2726 *
28- * `isLoading` is surfaced so the `/ai` route guard can wait for the catalog to
27+ * `VITE_AI_BASE_URL` is an explicit opt-in: it points the console at an external
28+ * AI server and is trusted even when local discovery reports AI unavailable.
29+ *
30+ * `isLoading` is surfaced so the `/ai` route guard can wait for discovery to
2931 * resolve before redirecting — otherwise a stale bookmark would flash a redirect
30- * to home before the fetch even starts. Entry-point buttons (FAB, top-bar link,
31- * designer "Ask AI") ignore it: staying hidden during the brief load is the
32- * correct, flash-free behaviour for a control that must not appear unless AI can
33- * actually answer.
32+ * to home before the server's answer is in.
3433 *
3534 * @module
3635 */
3736
38- import { useRef } from 'react' ;
39- import { useAgents } from '@object-ui/plugin-chatbot' ;
37+ import { useDiscovery } from '@object-ui/react' ;
4038
4139/**
4240 * Resolve the AI service base URL, mirroring AiChatPage / the Home CTAs:
4341 * an explicit `VITE_AI_BASE_URL` wins, otherwise `${VITE_SERVER_URL}/api/v1/ai`.
44- * Shared so every catalog fetch (route guard, layouts, Home ) hits the same URL.
42+ * Shared so every AI fetch (Home catalog, AiChatPage ) hits the same URL.
4543 */
4644export function resolveAiApiBase ( ) : string {
4745 const env = ( import . meta as any ) . env ?? { } ;
@@ -52,31 +50,22 @@ export function resolveAiApiBase(): string {
5250}
5351
5452export interface AiSurfaceState {
55- /** True when the AI UI should be rendered ( the server serves ≥1 agent) . */
53+ /** True when the AI capability (`service-ai`) is available, so the AI UI shows . */
5654 enabled : boolean ;
57- /** True until the agent catalog has resolved; route guards wait on this. */
55+ /** True until discovery resolves (and no `VITE_AI_BASE_URL` opt-in); guards wait on this. */
5856 isLoading : boolean ;
5957}
6058
6159/**
6260 * Whether the console's AI surface (FAB, `/ai` routes, "Ask AI" affordances)
63- * should be shown, driven off the live agent catalog.
61+ * should be shown — driven off the presence of the `service-ai` capability in
62+ * discovery.
6463 */
6564export function useAiSurfaceEnabled ( ) : AiSurfaceState {
66- const { agents, isLoading } = useAgents ( { apiBase : resolveAiApiBase ( ) } ) ;
67-
68- // useAgents starts `isLoading=false` and only kicks off the fetch in an effect
69- // a tick later, so the first render's empty list means "not fetched yet", not
70- // "no agents". Latch whether a fetch has actually been in flight so the route
71- // guard treats that initial frame as loading (not a definitive empty → redirect).
72- const fetchStartedRef = useRef ( false ) ;
73- if ( isLoading ) fetchStartedRef . current = true ;
74-
75- const enabled = agents . length > 0 ;
76- return {
77- enabled,
78- // Agents present → resolved/available. Otherwise we're loading until a fetch
79- // has both started and finished with an empty result.
80- isLoading : enabled ? false : isLoading || ! fetchStartedRef . current ,
81- } ;
65+ const { isAiEnabled, isLoading } = useDiscovery ( ) ;
66+ // An explicit external-AI opt-in is trusted even if local discovery reports AI
67+ // unavailable; it's synchronous, so there's nothing to wait for.
68+ const aiBaseUrlConfigured = Boolean ( ( import . meta as any ) . env ?. VITE_AI_BASE_URL ) ;
69+ if ( aiBaseUrlConfigured ) return { enabled : true , isLoading : false } ;
70+ return { enabled : isAiEnabled , isLoading } ;
8271}
0 commit comments