From 15a795579d00ea5b61e96a92e1360c9088ad842e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 03:21:07 +0000 Subject: [PATCH 1/3] Initial plan From ba4283e6d2b6ad3094c683f7a22ee5c6928f9620 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 03:28:51 +0000 Subject: [PATCH 2/3] feat: conditionally render console chatbot based on discovery.services.ai availability - Add `ai` service type to `DiscoveryInfo.services` interface with enabled, status, route fields - Add `isAiEnabled` convenience property to `useDiscovery()` hook (true only when ai.enabled && ai.status === 'available') - Conditionally render `ConsoleFloatingChatbot` in `ConsoleLayout` based on `isAiEnabled` - Add 5 new tests for isAiEnabled covering all edge cases - Update CHANGELOG.md and README.md Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/14bca4e2-4027-410b-bc6a-5ef0dc86a4fa Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com> --- CHANGELOG.md | 4 + apps/console/src/components/ConsoleLayout.tsx | 6 +- packages/react/README.md | 9 +- .../src/hooks/__tests__/useDiscovery.test.tsx | 98 +++++++++++++++++++ packages/react/src/hooks/useDiscovery.ts | 14 +++ 5 files changed, 127 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d590d3bfe..4b38e6267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **AI service discovery** (`@object-ui/react`): Added `ai` service type to `DiscoveryInfo.services` interface with `enabled`, `status`, and `route` fields. Added `isAiEnabled` convenience property to `useDiscovery()` hook return value — returns `true` only when `services.ai.enabled === true` and `services.ai.status === 'available'`, defaults to `false` otherwise. + +- **Conditional chatbot rendering** (`@object-ui/console`): Console floating chatbot (FAB) now only renders when the AI service is detected as available via `useDiscovery().isAiEnabled`. Previously the chatbot was always visible; now it is hidden when the server has no AI plugin installed. + - **Home page user menu** (`@object-ui/console`): Added complete user menu dropdown (Profile, Settings, Sign Out) to the Home Dashboard via new `HomeLayout` shell component. Users can now access account actions directly from the `/home` page without navigating elsewhere. - **"Return to Home" navigation** (`@object-ui/console`): Added a "Home" entry in the AppSidebar app switcher dropdown, allowing users to navigate back to `/home` from any application context. Previously, the only way to return to the Home Dashboard was to manually edit the URL. diff --git a/apps/console/src/components/ConsoleLayout.tsx b/apps/console/src/components/ConsoleLayout.tsx index 53cf3f9ff..8e5f2391f 100644 --- a/apps/console/src/components/ConsoleLayout.tsx +++ b/apps/console/src/components/ConsoleLayout.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { AppShell } from '@object-ui/layout'; import { FloatingChatbot, useObjectChat, type ChatMessage } from '@object-ui/plugin-chatbot'; +import { useDiscovery } from '@object-ui/react'; import { AppSidebar } from './AppSidebar'; import { AppHeader } from './AppHeader'; import { useResponsiveSidebar } from '../hooks/useResponsiveSidebar'; @@ -96,6 +97,7 @@ export function ConsoleLayout({ connectionState }: ConsoleLayoutProps) { const appLabel = resolveI18nLabel(activeApp?.label) || activeAppName; + const { isAiEnabled } = useDiscovery(); return ( - {/* Global floating chatbot — available on every page */} - + {/* Global floating chatbot — rendered only when AI service is available */} + {isAiEnabled && } ); } diff --git a/packages/react/README.md b/packages/react/README.md index 34616d3d4..3c651ceac 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -120,13 +120,18 @@ Access server discovery information including preview mode detection: import { useDiscovery } from '@object-ui/react' function MyComponent() { - const { discovery, isLoading, isAuthEnabled } = useDiscovery() + const { discovery, isLoading, isAuthEnabled, isAiEnabled } = useDiscovery() // Check if the server is in preview mode if (discovery?.mode === 'preview') { console.log('Preview mode active:', discovery.previewMode) } + // Check if AI service is available + if (isAiEnabled) { + console.log('AI service route:', discovery?.services?.ai?.route) + } + return
Server: {discovery?.name}
} ``` @@ -139,7 +144,7 @@ function MyComponent() { | `version` | `string` | Server version | | `mode` | `string` | Runtime mode (e.g. `'development'`, `'production'`, `'preview'`) | | `previewMode` | `object` | Preview mode configuration (present when mode is `'preview'`) | -| `services` | `object` | Service availability status (auth, data, metadata) | +| `services` | `object` | Service availability status (auth, data, metadata, ai) | | `capabilities` | `string[]` | API capabilities | The `previewMode` object contains: diff --git a/packages/react/src/hooks/__tests__/useDiscovery.test.tsx b/packages/react/src/hooks/__tests__/useDiscovery.test.tsx index 1c85ab364..95ea32aec 100644 --- a/packages/react/src/hooks/__tests__/useDiscovery.test.tsx +++ b/packages/react/src/hooks/__tests__/useDiscovery.test.tsx @@ -122,6 +122,104 @@ describe('useDiscovery', () => { expect(result.current.isAuthEnabled).toBe(false); }); + it('isAiEnabled defaults to false when no discovery', async () => { + const { result } = renderHook(() => useDiscovery()); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.isAiEnabled).toBe(false); + }); + + it('isAiEnabled is true when ai service is enabled and available', async () => { + const discoveryData = { + services: { + ai: { enabled: true, status: 'available' as const, route: '/api/v1/ai' }, + }, + }; + + const dataSource = { + getDiscovery: vi.fn().mockResolvedValue(discoveryData), + }; + + const { result } = renderHook(() => useDiscovery(), { + wrapper: createWrapper(dataSource), + }); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.isAiEnabled).toBe(true); + }); + + it('isAiEnabled is false when ai service is enabled but unavailable', async () => { + const discoveryData = { + services: { + ai: { enabled: true, status: 'unavailable' as const }, + }, + }; + + const dataSource = { + getDiscovery: vi.fn().mockResolvedValue(discoveryData), + }; + + const { result } = renderHook(() => useDiscovery(), { + wrapper: createWrapper(dataSource), + }); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.isAiEnabled).toBe(false); + }); + + it('isAiEnabled is false when ai service is disabled', async () => { + const discoveryData = { + services: { + ai: { enabled: false, status: 'available' as const }, + }, + }; + + const dataSource = { + getDiscovery: vi.fn().mockResolvedValue(discoveryData), + }; + + const { result } = renderHook(() => useDiscovery(), { + wrapper: createWrapper(dataSource), + }); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.isAiEnabled).toBe(false); + }); + + it('isAiEnabled is false when ai service has no status', async () => { + const discoveryData = { + services: { + ai: { enabled: true }, + }, + }; + + const dataSource = { + getDiscovery: vi.fn().mockResolvedValue(discoveryData), + }; + + const { result } = renderHook(() => useDiscovery(), { + wrapper: createWrapper(dataSource), + }); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.isAiEnabled).toBe(false); + }); + it('cleans up on unmount (cancelled flag)', async () => { let resolveDiscovery: (value: any) => void; const discoveryPromise = new Promise((resolve) => { diff --git a/packages/react/src/hooks/useDiscovery.ts b/packages/react/src/hooks/useDiscovery.ts index 14855e65e..b9d8d6127 100644 --- a/packages/react/src/hooks/useDiscovery.ts +++ b/packages/react/src/hooks/useDiscovery.ts @@ -49,6 +49,13 @@ export interface DiscoveryInfo { enabled: boolean; status?: 'available' | 'unavailable'; }; + /** AI service status */ + ai?: { + enabled: boolean; + status?: 'available' | 'unavailable'; + /** AI service endpoint route (e.g. '/api/v1/ai') */ + route?: string; + }; [key: string]: any; }; @@ -149,6 +156,13 @@ export function useDiscovery() { * Defaults to true if discovery data is not available. */ isAuthEnabled: discovery?.services?.auth?.enabled ?? true, + /** + * Check if AI service is enabled and available on the server. + * Defaults to false if discovery data is not available. + */ + isAiEnabled: + discovery?.services?.ai?.enabled === true && + discovery?.services?.ai?.status === 'available', }; } From 185e195c55b13c9d4e5745de263523f90eb94bbb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 03:31:20 +0000 Subject: [PATCH 3/3] docs: improve JSDoc comment for AI service type in DiscoveryInfo Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/14bca4e2-4027-410b-bc6a-5ef0dc86a4fa Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com> --- packages/react/src/hooks/useDiscovery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/hooks/useDiscovery.ts b/packages/react/src/hooks/useDiscovery.ts index b9d8d6127..3468e6226 100644 --- a/packages/react/src/hooks/useDiscovery.ts +++ b/packages/react/src/hooks/useDiscovery.ts @@ -49,7 +49,7 @@ export interface DiscoveryInfo { enabled: boolean; status?: 'available' | 'unavailable'; }; - /** AI service status */ + /** AI service configuration */ ai?: { enabled: boolean; status?: 'available' | 'unavailable';