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';