Skip to content

Commit 8406f9b

Browse files
committed
feat(mothership): simplify Build picker to Astra effort levels
1 parent a09adba commit 8406f9b

4 files changed

Lines changed: 134 additions & 74 deletions

File tree

‎apps/sim/app/o/[organizationId]/home/components/composer/composer.test.tsx‎

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ describe('organization voice composer', () => {
232232
async (isInitialView) => {
233233
await render(isInitialView)
234234
const mic = container.querySelector<HTMLButtonElement>('button[aria-label="Voice input"]')!
235-
expect(container.querySelector('[aria-label="Model and reasoning effort"]')).toBeNull()
235+
expect(container.querySelector('[aria-label="Reasoning effort"]')).toBeNull()
236236
expect(container.querySelector('[aria-label="Fast mode"]')).toBeNull()
237237
expect(mic.previousElementSibling?.getAttribute('aria-label')).toBe('Search level')
238238
expect(mic.parentElement?.nextElementSibling?.getAttribute('aria-label')).toBe('Send')
@@ -405,7 +405,7 @@ it('uploads an agent document with explicit mode while Assistant remains image-o
405405
})
406406
)
407407
expect(container.querySelector('input[type="file"]')?.getAttribute('accept')).toContain('.txt')
408-
expect(container.querySelector('button[aria-label="Model and reasoning effort"]')).not.toBeNull()
408+
expect(container.querySelector('button[aria-label="Reasoning effort"]')).not.toBeNull()
409409
})
410410

411411
it('shows a flat custom skill row with its workspace label and sends scoped context', async () => {
@@ -493,7 +493,7 @@ it.each([true, false])(
493493
expect(mocks.skillQuery).not.toHaveBeenCalled()
494494
expect(container.querySelector('[aria-label="Skills"]')).toBeNull()
495495
expect(container.querySelector('[aria-label="Add resources"]')).toBeNull()
496-
expect(container.querySelector('[aria-label="Model and reasoning effort"]')).toBeNull()
496+
expect(container.querySelector('[aria-label="Reasoning effort"]')).toBeNull()
497497
expect(container.querySelector('[aria-label="Fast mode"]')).toBeNull()
498498
expect(Boolean(container.querySelector('[aria-label="Conversation mode"]'))).toBe(canChoose)
499499
expect(container.querySelector('[aria-label="Attach images"]')).toBeNull()
@@ -772,15 +772,44 @@ describe('Search levels', () => {
772772
})
773773
})
774774

775+
it('offers only the five Build efforts and changes effort without losing the draft', async () => {
776+
useMothershipEffortStore.getState().setEffort('high')
777+
await render(true, 'Build draft', 'agent')
778+
const picker = container.querySelector<HTMLButtonElement>('[aria-label="Reasoning effort"]')!
779+
expect(picker?.textContent).toBe('High')
780+
expect(container.textContent).not.toMatch(/GPT-6 Astra|Opus/)
781+
await act(async () =>
782+
picker.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
783+
)
784+
const options = [...document.querySelectorAll<HTMLElement>('[role="menuitemradio"]')]
785+
expect(options.map((item) => item.textContent)).toEqual([
786+
'Low',
787+
'Medium',
788+
'High',
789+
'Extra High',
790+
'Max',
791+
])
792+
expect(options[2].getAttribute('aria-checked')).toBe('true')
793+
await act(async () => options[3].click())
794+
expect(picker.textContent).toBe('Extra High')
795+
expect(useMothershipEffortStore.getState()).toMatchObject({
796+
effort: 'xhigh',
797+
modelSelection: { model: 'gpt-6-astra' },
798+
})
799+
expect(container.querySelector<HTMLInputElement>('[aria-label="Ask Sim"]')!.value).toBe(
800+
'Build draft'
801+
)
802+
expect(mocks.submit).not.toHaveBeenCalled()
803+
})
804+
775805
it('keeps Build Fast independent of Search levels', async () => {
776-
useMothershipEffortStore.getState().setModel('gpt-6-astra')
777806
useMothershipEffortStore.getState().setFastMode(false)
778807
await render(true, 'Build', 'agent')
779808
expect(container.querySelector('[aria-label="Search level"]')).toBeNull()
780809
const fast = container.querySelector<HTMLButtonElement>('[aria-label="Fast mode"]')!
781810
await act(async () => fast.click())
782811
expect(useMothershipEffortStore.getState().modelSelection.fastMode).toBe(true)
783-
expect(container.querySelector('[aria-label="Model and reasoning effort"]')).not.toBeNull()
812+
expect(container.querySelector('[aria-label="Reasoning effort"]')).not.toBeNull()
784813
await act(async () => useMothershipEffortStore.getState().setFastMode(false))
785814
})
786815

‎apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/model-selector.tsx‎

Lines changed: 22 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,73 +5,45 @@ import {
55
DropdownMenu,
66
DropdownMenuContent,
77
DropdownMenuItem,
8-
DropdownMenuItemLabel,
98
DropdownMenuRadioGroup,
10-
DropdownMenuSub,
11-
DropdownMenuSubContent,
12-
DropdownMenuSubTrigger,
139
DropdownMenuTrigger,
1410
} from '@sim/emcn'
15-
import { MOTHERSHIP_EFFORT_OPTIONS, MOTHERSHIP_MODEL_OPTIONS } from '@/lib/mothership/model-options'
11+
import { MOTHERSHIP_EFFORT_OPTIONS } from '@/lib/mothership/model-options'
1612
import { FastModeToggle } from '@/app/workspace/[workspaceId]/home/components/user-input/components/fast-mode-toggle'
1713
import { useMothershipEffortStore } from '@/stores/mothership-effort/store'
1814

19-
/** Shared model, reasoning effort, and fast-mode controls for chat composers. */
15+
/** Reasoning effort and Fast mode for Build chat composers. */
2016
export function ModelSelector() {
21-
const modelSelection = useMothershipEffortStore((state) => state.modelSelection)
22-
const setModel = useMothershipEffortStore((state) => state.setModel)
17+
const fastMode = useMothershipEffortStore((state) => state.modelSelection.fastMode)
2318
const setFastMode = useMothershipEffortStore((state) => state.setFastMode)
2419
const effort = useMothershipEffortStore((state) => state.effort)
2520
const setEffort = useMothershipEffortStore((state) => state.setEffort)
2621
return (
2722
<div className='flex items-center'>
28-
{modelSelection.model === 'gpt-6-astra' && (
29-
<FastModeToggle
30-
enabled={modelSelection.fastMode}
31-
onChange={setFastMode}
32-
description='Faster responses at a higher price'
33-
/>
34-
)}
23+
<FastModeToggle
24+
enabled={fastMode}
25+
onChange={setFastMode}
26+
description='Faster responses at a higher price'
27+
/>
3528
<DropdownMenu>
3629
<DropdownMenuTrigger asChild>
37-
<Chip
38-
aria-label='Model and reasoning effort'
39-
className={modelSelection.model === 'gpt-6-astra' ? '-ml-2' : undefined}
40-
>
41-
{
42-
MOTHERSHIP_MODEL_OPTIONS.find((option) => option.value === modelSelection.model)
43-
?.label
44-
}{' '}
45-
<span className='text-[var(--text-muted)]'>
46-
{MOTHERSHIP_EFFORT_OPTIONS.find((option) => option.value === effort)?.label}
47-
</span>
30+
<Chip aria-label='Reasoning effort' className='-ml-2'>
31+
{MOTHERSHIP_EFFORT_OPTIONS.find((option) => option.value === effort)?.label}
4832
</Chip>
4933
</DropdownMenuTrigger>
5034
<DropdownMenuContent side='top' align='start'>
51-
{MOTHERSHIP_MODEL_OPTIONS.map((model) => (
52-
<DropdownMenuSub key={model.value}>
53-
<DropdownMenuSubTrigger>
54-
<DropdownMenuItemLabel label={model.label} />
55-
</DropdownMenuSubTrigger>
56-
<DropdownMenuSubContent>
57-
<DropdownMenuRadioGroup aria-label='Reasoning effort'>
58-
{MOTHERSHIP_EFFORT_OPTIONS.map((option) => (
59-
<DropdownMenuItem
60-
key={option.value}
61-
role='menuitemradio'
62-
aria-checked={modelSelection.model === model.value && effort === option.value}
63-
onSelect={() => {
64-
setModel(model.value)
65-
setEffort(option.value)
66-
}}
67-
>
68-
{option.label}
69-
</DropdownMenuItem>
70-
))}
71-
</DropdownMenuRadioGroup>
72-
</DropdownMenuSubContent>
73-
</DropdownMenuSub>
74-
))}
35+
<DropdownMenuRadioGroup aria-label='Reasoning effort'>
36+
{MOTHERSHIP_EFFORT_OPTIONS.map((option) => (
37+
<DropdownMenuItem
38+
key={option.value}
39+
role='menuitemradio'
40+
aria-checked={effort === option.value}
41+
onSelect={() => setEffort(option.value)}
42+
>
43+
{option.label}
44+
</DropdownMenuItem>
45+
))}
46+
</DropdownMenuRadioGroup>
7547
</DropdownMenuContent>
7648
</DropdownMenu>
7749
</div>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/** @vitest-environment jsdom */
2+
import { beforeEach, describe, expect, it } from 'vitest'
3+
import { useMothershipEffortStore } from '@/stores/mothership-effort/store'
4+
5+
beforeEach(() => {
6+
localStorage.clear()
7+
useMothershipEffortStore.setState({
8+
effort: 'high',
9+
modelSelection: { model: 'gpt-6-astra', fastMode: false },
10+
})
11+
})
12+
13+
describe('Build reasoning preferences', () => {
14+
it('replaces a saved Opus selection with Astra while preserving effort', async () => {
15+
localStorage.setItem(
16+
'mothership-effort',
17+
JSON.stringify({
18+
version: 0,
19+
state: { effort: 'xhigh', modelSelection: { model: 'claude-opus-5', fastMode: false } },
20+
})
21+
)
22+
await useMothershipEffortStore.persist.rehydrate()
23+
expect(useMothershipEffortStore.getState()).toMatchObject({
24+
effort: 'xhigh',
25+
modelSelection: { model: 'gpt-6-astra', fastMode: false },
26+
})
27+
useMothershipEffortStore.getState().setFastMode(true)
28+
expect(JSON.parse(localStorage.getItem('mothership-effort')!).state).toEqual({
29+
effort: 'xhigh',
30+
modelSelection: { model: 'gpt-6-astra', fastMode: true },
31+
})
32+
})
33+
34+
it('restores effort and Fast mode together without changing the Build model', async () => {
35+
useMothershipEffortStore.getState().setEffort('max')
36+
useMothershipEffortStore.getState().setFastMode(true)
37+
const saved = localStorage.getItem('mothership-effort')!
38+
useMothershipEffortStore.setState({
39+
effort: 'high',
40+
modelSelection: { model: 'gpt-6-astra', fastMode: false },
41+
})
42+
localStorage.setItem('mothership-effort', saved)
43+
await useMothershipEffortStore.persist.rehydrate()
44+
expect(useMothershipEffortStore.getState()).toMatchObject({
45+
effort: 'max',
46+
modelSelection: { model: 'gpt-6-astra', fastMode: true },
47+
})
48+
})
49+
})

‎apps/sim/stores/mothership-effort/store.ts‎

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,51 @@
1+
import { toRecord } from '@sim/utils/object'
12
import { create } from 'zustand'
23
import { devtools, persist } from 'zustand/middleware'
34
import type { ModelSelection } from '@/lib/mothership/generated/protocol'
4-
import type { MothershipEffort } from '@/lib/mothership/model-options'
5+
import { MOTHERSHIP_EFFORT_OPTIONS, type MothershipEffort } from '@/lib/mothership/model-options'
56

67
interface MothershipEffortState {
7-
modelSelection: ModelSelection
8-
setModel: (model: ModelSelection['model']) => void
8+
modelSelection: ModelSelection & { model: 'gpt-6-astra' }
99
setFastMode: (fastMode: boolean) => void
1010
effort: MothershipEffort
1111
setEffort: (effort: MothershipEffort) => void
12+
reset: () => void
1213
}
1314

15+
const initialState = {
16+
effort: 'high',
17+
modelSelection: { model: 'gpt-6-astra', fastMode: false },
18+
} satisfies Pick<MothershipEffortState, 'effort' | 'modelSelection'>
19+
1420
export const useMothershipEffortStore = create<MothershipEffortState>()(
1521
devtools(
1622
persist(
1723
(set) => ({
18-
effort: 'high',
19-
modelSelection: { model: 'gpt-6-astra', fastMode: false },
20-
setModel: (model) =>
21-
set((state) => ({
22-
modelSelection: {
23-
model,
24-
fastMode: model === 'gpt-6-astra' && state.modelSelection.fastMode,
25-
},
26-
})),
27-
setFastMode: (fastMode) =>
28-
set((state) => ({
29-
modelSelection: {
30-
...state.modelSelection,
31-
fastMode: state.modelSelection.model === 'gpt-6-astra' && fastMode,
32-
},
33-
})),
24+
...initialState,
25+
setFastMode: (fastMode) => set({ modelSelection: { model: 'gpt-6-astra', fastMode } }),
3426
setEffort: (effort) => set({ effort }),
27+
reset: () => set(initialState),
3528
}),
3629
{
3730
name: 'mothership-effort',
3831
partialize: ({ effort, modelSelection }) => ({ effort, modelSelection }),
32+
merge: (persistedState, currentState) => {
33+
const persisted = toRecord(persistedState)
34+
const selection = toRecord(persisted.modelSelection)
35+
return {
36+
...currentState,
37+
effort:
38+
MOTHERSHIP_EFFORT_OPTIONS.find((option) => option.value === persisted.effort)
39+
?.value ?? currentState.effort,
40+
modelSelection: {
41+
model: 'gpt-6-astra',
42+
fastMode:
43+
typeof selection.fastMode === 'boolean'
44+
? selection.fastMode
45+
: currentState.modelSelection.fastMode,
46+
},
47+
}
48+
},
3949
}
4050
),
4151
{ name: 'mothership-effort-store' }

0 commit comments

Comments
 (0)