From 2a65cc37e6e4f7faacc2c1db754103699386dc89 Mon Sep 17 00:00:00 2001 From: Harshil Vasoya Date: Wed, 28 Oct 2026 11:44:41 +0530 Subject: [PATCH 1/2] bugfix: add Configure model button for empty catalog in composer --- .changeset/configure-model-empty-catalog.md | 5 +++ .../src/atoms/draft/DraftModelSelector.tsx | 31 +++++++++++++------ .../atoms/draft/DraftModelSelector.test.tsx | 9 +++--- 3 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 .changeset/configure-model-empty-catalog.md diff --git a/.changeset/configure-model-empty-catalog.md b/.changeset/configure-model-empty-catalog.md new file mode 100644 index 0000000000..ca27adf28f --- /dev/null +++ b/.changeset/configure-model-empty-catalog.md @@ -0,0 +1,5 @@ +--- +'@truefoundry/trueforge-ui': patch +--- + +Show a Configure model button that opens Settings when the model catalog is empty, instead of a Select model dropdown with a ? monogram. diff --git a/packages/trueforge-ui/src/atoms/draft/DraftModelSelector.tsx b/packages/trueforge-ui/src/atoms/draft/DraftModelSelector.tsx index 991776e202..0766e959e6 100644 --- a/packages/trueforge-ui/src/atoms/draft/DraftModelSelector.tsx +++ b/packages/trueforge-ui/src/atoms/draft/DraftModelSelector.tsx @@ -3,6 +3,8 @@ import { useTrueForgeAgentSpec, useTrueForgeUpdateAgentSpec } from '@truefoundry/trueforge-assistant-ui-runtime'; import { useEffect, useId, useRef, useState } from 'react'; +import { useTrackAnalytics } from '../../analytics/AnalyticsProvider.js'; +import { AnalyticsEvents } from '../../analytics/events.js'; import { Icon } from '../../icons/Icon.js'; import { useOptionalCatalogServer } from '../../server/ServerContext.js'; import { useOptionalShellMode } from '../../server/ShellModeContext.js'; @@ -11,6 +13,7 @@ import { cn } from '../lib/cn.js'; import { useCompactLayout } from '../lib/CompactLayoutContext.js'; import { useIsMobile } from '../lib/useIsMobile.js'; import { BottomSheet } from '../primitives/BottomSheet.js'; +import { Button } from '../primitives/Button.js'; import { Tooltip } from '../primitives/Tooltip.js'; import { useDraftCatalog } from './DraftCatalogProvider.js'; import { displayModelLabel, DraftModelCatalogPanel, ModelWarningMark, ProviderMark } from './DraftModelCatalogPanel.js'; @@ -27,6 +30,7 @@ export function DraftModelSelector({ disabled, isRunning }: DraftModelSelectorPr const updateAgentSpec = useTrueForgeUpdateAgentSpec(); const catalog = useOptionalCatalogServer(); const shell = useOptionalShellMode(); + const track = useTrackAnalytics(); const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const containerRef = useRef(null); @@ -84,6 +88,24 @@ export function DraftModelSelector({ disabled, isRunning }: DraftModelSelectorPr return () => document.removeEventListener('mousedown', handler); }, [open]); + if (showConfigureSettingsCta) { + return ( + { + if (!shell?.settingsOpen) track(AnalyticsEvents.Settings.OPENED); + shell?.setSettingsOpen(true, 'models'); + }} + > + Configure model + + ); + } + const account = selected?.provider.name ?? selectedName; const content = ( @@ -94,15 +116,6 @@ export function DraftModelSelector({ disabled, isRunning }: DraftModelSelectorPr query={query} onQueryChange={setQuery} listboxId={menuId} - onOpenSettings={ - showConfigureSettingsCta - ? () => { - setOpen(false); - setQuery(''); - shell?.setSettingsOpen(true, 'models'); - } - : undefined - } onSelect={model => { updateAgentSpec?.({ model: modelPatchWithReasoningEffort(model.name, agentSpec?.model?.params, model.properties.reasoningEfforts), diff --git a/packages/trueforge-ui/test/atoms/draft/DraftModelSelector.test.tsx b/packages/trueforge-ui/test/atoms/draft/DraftModelSelector.test.tsx index 4fbd5a9c06..399b646a79 100644 --- a/packages/trueforge-ui/test/atoms/draft/DraftModelSelector.test.tsx +++ b/packages/trueforge-ui/test/atoms/draft/DraftModelSelector.test.tsx @@ -238,7 +238,7 @@ describe('DraftModelSelector', () => { expect(screen.getByRole('button', { name: 'Select model' })).toBeDisabled(); }); - it('shows a settings CTA when the host has a catalog and no models', async () => { + it('shows Configure model when the host has a catalog and no models', async () => { renderSelector( {}, { @@ -247,9 +247,8 @@ describe('DraftModelSelector', () => { }, ); - fireEvent.click(await screen.findByRole('button', { name: 'Select model' })); - const cta = await screen.findByRole('button', { name: /Please configure Models in the settings/i }); - expect(cta.querySelector('.underline')).toHaveTextContent('settings'); + const cta = await screen.findByRole('button', { name: 'Configure model' }); + expect(screen.queryByRole('button', { name: 'Select model' })).not.toBeInTheDocument(); fireEvent.click(cta); expect(screen.queryByRole('listbox', { name: 'Select model' })).not.toBeInTheDocument(); @@ -261,6 +260,6 @@ describe('DraftModelSelector', () => { fireEvent.click(await screen.findByRole('button', { name: 'Select model' })); await waitFor(() => expect(screen.getByText('No models')).toBeInTheDocument()); - expect(screen.queryByRole('button', { name: /Please configure Models/i })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Configure model' })).not.toBeInTheDocument(); }); }); From 6a7b06048e75fcdc1ec4cb6c026b2c5e9f3faeb9 Mon Sep 17 00:00:00 2001 From: Harshil Vasoya Date: Wed, 28 Oct 2026 14:27:49 +0530 Subject: [PATCH 2/2] feat: enhance DraftModelSelector to conditionally show Configure model button based on settings chrome availability --- .../src/atoms/draft/DraftModelSelector.tsx | 9 +++-- .../atoms/draft/DraftModelSelector.test.tsx | 40 ++++++++++++++++++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/trueforge-ui/src/atoms/draft/DraftModelSelector.tsx b/packages/trueforge-ui/src/atoms/draft/DraftModelSelector.tsx index 0766e959e6..0f2f6c1ec2 100644 --- a/packages/trueforge-ui/src/atoms/draft/DraftModelSelector.tsx +++ b/packages/trueforge-ui/src/atoms/draft/DraftModelSelector.tsx @@ -6,7 +6,8 @@ import { useEffect, useId, useRef, useState } from 'react'; import { useTrackAnalytics } from '../../analytics/AnalyticsProvider.js'; import { AnalyticsEvents } from '../../analytics/events.js'; import { Icon } from '../../icons/Icon.js'; -import { useOptionalCatalogServer } from '../../server/ServerContext.js'; +import { isSettingsChromeEnabled } from '../../server/serverChrome.js'; +import { useOptionalCatalogServer, useServerCapabilities } from '../../server/ServerContext.js'; import { useOptionalShellMode } from '../../server/ShellModeContext.js'; import { auiButtonClass } from '../lib/buttonClasses.js'; import { cn } from '../lib/cn.js'; @@ -25,10 +26,11 @@ export type DraftModelSelectorProps = { }; export function DraftModelSelector({ disabled, isRunning }: DraftModelSelectorProps) { - const { models, loading, ensureLoaded } = useDraftCatalog(); + const { models, loading, loaded, ensureLoaded } = useDraftCatalog(); const { agentSpec } = useTrueForgeAgentSpec(); const updateAgentSpec = useTrueForgeUpdateAgentSpec(); const catalog = useOptionalCatalogServer(); + const capabilities = useServerCapabilities(); const shell = useOptionalShellMode(); const track = useTrackAnalytics(); const [open, setOpen] = useState(false); @@ -37,7 +39,8 @@ export function DraftModelSelector({ disabled, isRunning }: DraftModelSelectorPr const menuId = useId(); const isMobile = useIsMobile(); const compactLayout = useCompactLayout(); - const showConfigureSettingsCta = !loading && models.length === 0 && catalog != null; + const settingsChromeEnabled = isSettingsChromeEnabled({ catalog, capabilities }); + const showConfigureSettingsCta = loaded && models.length === 0 && settingsChromeEnabled && shell != null; const seededDefaultModelRef = useRef(null); useEffect(() => { diff --git a/packages/trueforge-ui/test/atoms/draft/DraftModelSelector.test.tsx b/packages/trueforge-ui/test/atoms/draft/DraftModelSelector.test.tsx index 399b646a79..bbb5247e2c 100644 --- a/packages/trueforge-ui/test/atoms/draft/DraftModelSelector.test.tsx +++ b/packages/trueforge-ui/test/atoms/draft/DraftModelSelector.test.tsx @@ -238,7 +238,7 @@ describe('DraftModelSelector', () => { expect(screen.getByRole('button', { name: 'Select model' })).toBeDisabled(); }); - it('shows Configure model when the host has a catalog and no models', async () => { + it('shows Configure model when settings chrome is available and no models', async () => { renderSelector( {}, { @@ -255,6 +255,44 @@ describe('DraftModelSelector', () => { expect(screen.getByTestId('settings-open')).toHaveTextContent('true'); }); + it('does not flash Configure model before the catalog finishes loading', async () => { + let resolveModels!: (value: ModelSelection[]) => void; + const modelsPromise = new Promise(resolve => { + resolveModels = resolve; + }); + + renderSelector( + {}, + { + getModels: () => modelsPromise, + catalog: createMockCatalog(), + }, + ); + + expect(await screen.findByRole('button', { name: 'Select model' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Configure model' })).not.toBeInTheDocument(); + + resolveModels([]); + expect(await screen.findByRole('button', { name: 'Configure model' })).toBeInTheDocument(); + }); + + it('keeps the model picker when settings chrome is disabled', async () => { + renderSelector( + {}, + { + getModels: async () => [], + catalog: createMockCatalog(), + getCapabilities: async () => ({ + data: { sandbox: { enabled: true }, skill: { enabled: true }, settings: { enabled: false } }, + }), + }, + ); + + fireEvent.click(await screen.findByRole('button', { name: 'Select model' })); + await waitFor(() => expect(screen.getByText('No models')).toBeInTheDocument()); + expect(screen.queryByRole('button', { name: 'Configure model' })).not.toBeInTheDocument(); + }); + it('shows No models when the catalog is not configured', async () => { renderSelector({}, { getModels: async () => [] });