diff --git a/app/src/pages/Conversations.tsx b/app/src/pages/Conversations.tsx index 1b8aeaf86f..dab8c38a9c 100644 --- a/app/src/pages/Conversations.tsx +++ b/app/src/pages/Conversations.tsx @@ -82,6 +82,7 @@ import { THREAD_NOT_FOUND_MESSAGE, updateThreadTitle, } from '../store/threadSlice'; +import type { AgentProfile } from '../types/agentProfile'; import type { ConfirmationModal as ConfirmationModalType } from '../types/intelligence'; import type { ThreadMessage } from '../types/thread'; import { splitAgentMessageIntoBubbles } from '../utils/agentMessageBubbles'; @@ -217,6 +218,14 @@ export function isImeCompositionKeyEvent(event: ImeKeyboardEventLike): boolean { * Exported so the mount-effect's `.catch` stays a one-liner and the message * shape can be unit-tested without mounting the full page. */ +export function sortAgentProfiles(profiles: AgentProfile[], locale: string): AgentProfile[] { + return profiles + .filter(p => !p.builtIn) + .sort( + (a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0) || a.name.localeCompare(b.name, locale) + ); +} + export function formatThreadLoadError(err: unknown): string { if (err instanceof Error) return err.message; if (err && typeof err === 'object' && 'message' in err) { @@ -2952,6 +2961,22 @@ const Conversations = ({ }`}> {t('chat.agentProfile.reasoning')} + {sortAgentProfiles(agentProfiles, uiLocale).map(profile => ( + + ))} {/* Super context is read at thread construction, so it only affects NEW threads. Hide the toggle once the thread has ANY diff --git a/app/src/pages/__tests__/Conversations.test.tsx b/app/src/pages/__tests__/Conversations.test.tsx index d45aad8c44..9426fac558 100644 --- a/app/src/pages/__tests__/Conversations.test.tsx +++ b/app/src/pages/__tests__/Conversations.test.tsx @@ -1,9 +1,11 @@ import { describe, expect, it } from 'vitest'; +import type { AgentProfile } from '../../types/agentProfile'; import { formatThreadLoadError, isComposerInteractionBlocked, isImeCompositionKeyEvent, + sortAgentProfiles, } from '../Conversations'; describe('isComposerInteractionBlocked', () => { @@ -78,3 +80,91 @@ describe('formatThreadLoadError', () => { expect(formatThreadLoadError({ message: 42 })).toBe('[object Object]'); }); }); + +describe('sortAgentProfiles', () => { + it('filters out built-in profiles', () => { + const profiles: AgentProfile[] = [ + { id: '1', name: 'Custom', builtIn: false, description: '', agentId: 'orchestrator' }, + { id: 'default', name: 'Default', builtIn: true, description: '', agentId: 'orchestrator' }, + ]; + expect(sortAgentProfiles(profiles, 'en')).toHaveLength(1); + }); + + it('sorts by sortOrder then name', () => { + const profiles: AgentProfile[] = [ + { + id: 'a', + name: 'Alpha', + builtIn: false, + sortOrder: 2, + description: '', + agentId: 'orchestrator', + }, + { + id: 'b', + name: 'Beta', + builtIn: false, + sortOrder: 1, + description: '', + agentId: 'orchestrator', + }, + { + id: 'c', + name: 'Charlie', + builtIn: false, + sortOrder: 1, + description: '', + agentId: 'orchestrator', + }, + ]; + const sorted = sortAgentProfiles(profiles, 'en'); + expect(sorted[0].id).toBe('b'); + expect(sorted[1].id).toBe('c'); + expect(sorted[2].id).toBe('a'); + }); + + it('treats missing sortOrder as 0', () => { + const profiles: AgentProfile[] = [ + { + id: 'a', + name: 'Alpha', + builtIn: false, + sortOrder: null, + description: '', + agentId: 'orchestrator', + }, + { + id: 'b', + name: 'Beta', + builtIn: false, + sortOrder: 1, + description: '', + agentId: 'orchestrator', + }, + ]; + const sorted = sortAgentProfiles(profiles, 'en'); + expect(sorted[0].id).toBe('a'); + expect(sorted[1].id).toBe('b'); + }); + + it('returns empty array when all profiles are built-in', () => { + expect( + sortAgentProfiles( + [ + { + id: 'default', + name: 'Default', + builtIn: true, + description: '', + agentId: 'orchestrator', + }, + ], + 'en' + ) + ).toHaveLength(0); + }); + + it('returns empty array for empty input', () => { + expect(sortAgentProfiles([], 'en')).toHaveLength(0); + }); +});