Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/configure-model-empty-catalog.md
Original file line number Diff line number Diff line change
@@ -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.
40 changes: 28 additions & 12 deletions packages/trueforge-ui/src/atoms/draft/DraftModelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
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 { 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';
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';
Expand All @@ -22,18 +26,21 @@ 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);
const [query, setQuery] = useState('');
const containerRef = useRef<HTMLDivElement>(null);
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<string | null>(null);

useEffect(() => {
Expand Down Expand Up @@ -84,6 +91,24 @@ export function DraftModelSelector({ disabled, isRunning }: DraftModelSelectorPr
return () => document.removeEventListener('mousedown', handler);
}, [open]);

if (showConfigureSettingsCta) {
return (
<Button.Ghost
type="button"
size="small"
disabled={disabled || isRunning}
aria-label="Configure model"
className="h-8 max-w-48 rounded-full px-2 text-xs font-medium"
onClick={() => {
if (!shell?.settingsOpen) track(AnalyticsEvents.Settings.OPENED);
shell?.setSettingsOpen(true, 'models');
}}
>
Configure model
</Button.Ghost>
);
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

const account = selected?.provider.name ?? selectedName;

const content = (
Expand All @@ -94,15 +119,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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 settings chrome is available and no models', async () => {
renderSelector(
{},
{
Expand All @@ -247,20 +247,57 @@ 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();
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<ModelSelection[]>(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 () => [] });

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();
});
});
Loading