Skip to content
Open
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
25 changes: 25 additions & 0 deletions app/src/pages/Conversations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -2952,6 +2961,22 @@ const Conversations = ({
}`}>
{t('chat.agentProfile.reasoning')}
</button>
{sortAgentProfiles(agentProfiles, uiLocale).map(profile => (
<button
key={profile.id}
type="button"
role="radio"
aria-checked={selectedAgentProfileId === profile.id}
data-analytics-id={`chat-header-mode-${profile.id}`}
onClick={() => void handleSelectAgentProfile(profile.id)}
className={`rounded-full px-2.5 py-0.5 text-xs font-medium transition-all ${
selectedAgentProfileId === profile.id
? 'bg-surface text-content shadow-sm'
: 'text-content-muted hover:text-content-secondary'
}`}>
{profile.name}
</button>
))}
</div>
{/* Super context is read at thread construction, so it only
affects NEW threads. Hide the toggle once the thread has ANY
Expand Down
90 changes: 90 additions & 0 deletions app/src/pages/__tests__/Conversations.test.tsx
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
Loading