Skip to content

Commit e66af75

Browse files
committed
Default Plan chats to independently saved Opus 5.5 Medium preferences
1 parent 5aeefe2 commit e66af75

12 files changed

Lines changed: 223 additions & 78 deletions

File tree

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

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
44
import { createRoot, type Root } from 'react-dom/client'
55
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
66
import type { useSpeechToText } from '@/hooks/use-speech-to-text'
7-
import { useMothershipEffortStore } from '@/stores/mothership-effort/store'
7+
import {
8+
useMothershipEffortStore,
9+
useMothershipPlanEffortStore,
10+
} from '@/stores/mothership-effort/store'
811

912
const mocks = vi.hoisted(() => ({
1013
live: false,
@@ -95,6 +98,7 @@ let queryClient: QueryClient
9598

9699
beforeEach(() => {
97100
mocks.advanced = false
101+
useMothershipPlanEffortStore.getState().reset()
98102
useMothershipEffortStore.setState({
99103
effort: 'high',
100104
modelSelection: { model: 'gpt-6-astra', fastMode: false },
@@ -189,7 +193,7 @@ afterEach(async () => {
189193
async function render(
190194
isInitialView: boolean,
191195
initialValue = 'Summarize',
192-
requestMode: 'agent' | 'assistant' = 'assistant',
196+
requestMode: 'agent' | 'assistant' | 'plan' = 'assistant',
193197
controls: Pick<
194198
ComponentProps<typeof Composer>,
195199
'isSending' | 'showModeSelector' | 'onModeChange' | 'restoredContexts' | 'onSendQueuedHead'
@@ -366,3 +370,22 @@ it.each([
366370
expect(input.value).toBe('')
367371
}
368372
)
373+
374+
it('renders and restores separate Plan and Build model preferences when changing modes', async () => {
375+
mocks.plan = true
376+
mocks.advanced = true
377+
await render(false, '', 'plan')
378+
expect(container.textContent).toContain('Opus 5.5')
379+
expect(container.textContent).toContain('Medium')
380+
await act(async () => {
381+
useMothershipPlanEffortStore.getState().setModel('gpt-6-astra')
382+
useMothershipPlanEffortStore.getState().setEffort('xhigh')
383+
})
384+
expect(container.textContent).toContain('GPT-6 Astra')
385+
expect(container.textContent).toContain('Extra High')
386+
await render(false, '', 'agent')
387+
expect(container.textContent).toContain('GPT-6 Astra')
388+
expect(container.textContent).not.toContain('Extra High')
389+
await render(false, '', 'plan')
390+
expect(container.textContent).toContain('Extra High')
391+
})

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ export function Composer({
308308
/>
309309
) : (
310310
<InputToolbar
311+
plan={requestMode === 'plan'}
311312
leadingControls={leadingControls}
312313
voiceControl={voiceControl}
313314
submitControl={submitControl}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ModelSelector } from '@/app/workspace/[workspaceId]/home/components/use
77
interface InputToolbarProps {
88
leadingControls: ReactNode
99
showModelSelector?: boolean
10+
plan?: boolean
1011
selectionControl?: ReactNode
1112
voiceControl?: ReactNode
1213
beforeSubmitControl?: ReactNode
@@ -22,6 +23,7 @@ interface InputToolbarProps {
2223
export function InputToolbar({
2324
leadingControls,
2425
showModelSelector = true,
26+
plan = false,
2527
selectionControl,
2628
voiceControl,
2729
beforeSubmitControl,
@@ -69,7 +71,7 @@ export function InputToolbar({
6971
<>
7072
{(selectionControl || showModelSelector || voiceControl) && (
7173
<div className='flex items-center gap-[inherit]'>
72-
{selectionControl ?? (showModelSelector && <ModelSelector />)}
74+
{selectionControl ?? (showModelSelector && <ModelSelector plan={plan} />)}
7375
{voiceControl}
7476
</div>
7577
)}

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

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,33 @@ import {
1717
import { FastModeToggle } from '@/app/workspace/[workspaceId]/home/components/user-input/components/fast-mode-toggle'
1818
import { ModelSettingTrigger } from '@/app/workspace/[workspaceId]/home/components/user-input/components/model-setting-trigger'
1919
import { useFeatureFlag } from '@/app/workspace/[workspaceId]/providers/feature-flags-provider'
20-
import { useMothershipEffortStore } from '@/stores/mothership-effort/store'
20+
import {
21+
useMothershipEffortStore,
22+
useMothershipPlanEffortStore,
23+
} from '@/stores/mothership-effort/store'
24+
25+
interface ModelSelectorProps {
26+
plan?: boolean
27+
}
2128

22-
/** Model, reasoning effort, and Fast mode for Build chat composers. */
23-
export function ModelSelector() {
29+
/** Mode-specific preferences keep planning choices separate from Build chats. */
30+
export function ModelSelector({ plan = false }: ModelSelectorProps) {
31+
const usePreferenceStore = plan ? useMothershipPlanEffortStore : useMothershipEffortStore
2432
const advanced = useFeatureFlag('mothership-model-selector')
25-
const selection = useMothershipEffortStore((state) => state.modelSelection)
26-
const setModel = useMothershipEffortStore((state) => state.setModel)
27-
const setFastMode = useMothershipEffortStore((state) => state.setFastMode)
28-
const storedEffort = useMothershipEffortStore((state) => state.effort)
33+
const selection = usePreferenceStore((state) => state.modelSelection)
34+
const setModel = usePreferenceStore((state) => state.setModel)
35+
const setFastMode = usePreferenceStore((state) => state.setFastMode)
36+
const storedEffort = usePreferenceStore((state) => state.effort)
2937
const { effort, modelSelection } = resolveMothershipModelSettings(
3038
{ effort: storedEffort, modelSelection: selection },
31-
advanced
39+
advanced,
40+
plan
3241
)
33-
const options = advanced
34-
? mothershipEffortOptions(modelSelection.model)
35-
: MOTHERSHIP_SIMPLE_EFFORT_OPTIONS
36-
const setEffort = useMothershipEffortStore((state) => state.setEffort)
42+
const options =
43+
advanced || plan
44+
? mothershipEffortOptions(modelSelection.model)
45+
: MOTHERSHIP_SIMPLE_EFFORT_OPTIONS
46+
const setEffort = usePreferenceStore((state) => state.setEffort)
3747
const effortLabel = options.find((option) => option.value === effort)?.label ?? effort
3848
const modelLabel =
3949
MOTHERSHIP_MODEL_OPTIONS.find((option) => option.value === modelSelection.model)?.label ??

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,7 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
585585
/>
586586

587587
<InputToolbar
588+
plan={requestMode === 'plan'}
588589
leadingControls={
589590
<>
590591
<Tooltip.Root>

‎apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ import { getWorkflowById, getWorkflows } from '@/hooks/queries/utils/workflow-ca
122122
import { getWorkflowListQueryOptions } from '@/hooks/queries/utils/workflow-list-query'
123123
import { workflowKeys } from '@/hooks/queries/workflows'
124124
import { snapAllSmoothText } from '@/hooks/use-smooth-text'
125-
import { useMothershipEffortStore } from '@/stores/mothership-effort/store'
125+
import {
126+
useMothershipEffortStore,
127+
useMothershipPlanEffortStore,
128+
} from '@/stores/mothership-effort/store'
126129
import { useMothershipQueueStore } from '@/stores/mothership-queue/store'
127130
import type {
128131
QueuedMothershipMessage,
@@ -3673,8 +3676,12 @@ export function useChat(
36733676
...(options?.requestMode !== 'assistant'
36743677
? {
36753678
...resolveMothershipModelSettings(
3676-
useMothershipEffortStore.getState(),
3677-
modelSelectorEnabled
3679+
(options?.requestMode === 'plan'
3680+
? useMothershipPlanEffortStore
3681+
: useMothershipEffortStore
3682+
).getState(),
3683+
modelSelectorEnabled,
3684+
options?.requestMode === 'plan'
36783685
),
36793686
}
36803687
: {}),

‎apps/sim/lib/mothership/chat/post.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,33 @@ describe('handleUnifiedChatPost', () => {
841841
}
842842
)
843843

844+
it.each([false, true])(
845+
'defaults Plan admission to Opus Medium with model selection %s',
846+
async (advanced) => {
847+
flags.models.mockResolvedValue(advanced)
848+
flags.plan.mockResolvedValue(true)
849+
const response = await handleUnifiedChatPost(
850+
new NextRequest('http://localhost/api/mothership/chat', {
851+
method: 'POST',
852+
body: JSON.stringify({
853+
message: 'Plan this automation',
854+
workspaceId: 'ws-1',
855+
mode: 'plan',
856+
}),
857+
})
858+
)
859+
expect(response.status).toBe(200)
860+
expect(buildCopilotRequestPayload).toHaveBeenCalledWith(
861+
expect.objectContaining({
862+
mode: 'plan',
863+
effort: 'medium',
864+
modelSelection: { model: 'claude-opus-5-5', fastMode: false },
865+
}),
866+
expect.anything()
867+
)
868+
}
869+
)
870+
844871
it('routes workflow-attached chat requests through the copilot backend path', async () => {
845872
const response = await handleUnifiedChatPost(
846873
new NextRequest('http://localhost/api/copilot/chat', {

‎apps/sim/lib/mothership/chat/post.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,10 @@ export async function handleUnifiedChatPost(req: NextRequest) {
998998
isMothershipModelSelectorEnabled(),
999999
body.mode === 'plan' ? isPlanModeEnabled() : false,
10001000
])
1001-
Object.assign(body, resolveMothershipModelSettings(body, modelSelectorEnabled))
1001+
Object.assign(
1002+
body,
1003+
resolveMothershipModelSettings(body, modelSelectorEnabled, body.mode === 'plan')
1004+
)
10021005
if (body.mode === 'plan' && !planEnabled)
10031006
return createBadRequestResponse('Plan mode is disabled')
10041007
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/** @vitest-environment node */
2+
import { describe, expect, it } from 'vitest'
3+
import { resolveMothershipModelSettings } from '@/lib/mothership/model-options'
4+
5+
describe('Plan model defaults', () => {
6+
it.each([true, false])('defaults Plan to Opus Medium with advanced=%s', (advanced) => {
7+
expect(resolveMothershipModelSettings({}, advanced, true)).toEqual({
8+
effort: 'medium',
9+
modelSelection: { model: 'claude-opus-5-5', fastMode: false },
10+
})
11+
expect(resolveMothershipModelSettings({}, advanced)).toEqual({
12+
effort: 'high',
13+
modelSelection: { model: 'gpt-6-astra', fastMode: false },
14+
})
15+
})
16+
17+
it.each(['low', 'medium', 'high', 'xhigh', 'max'] as const)(
18+
'preserves the visible Plan effort %s with model selection hidden',
19+
(effort) => expect(resolveMothershipModelSettings({ effort }, false, true).effort).toBe(effort)
20+
)
21+
22+
it('preserves an explicit Plan comparison choice when model selection is enabled', () => {
23+
const chosen = {
24+
effort: 'xhigh' as const,
25+
modelSelection: { model: 'gpt-6-astra' as const, fastMode: false },
26+
}
27+
expect(resolveMothershipModelSettings(chosen, true, true)).toEqual(chosen)
28+
expect(resolveMothershipModelSettings(chosen, false, true).modelSelection.model).toBe(
29+
'claude-opus-5-5'
30+
)
31+
})
32+
})

‎apps/sim/lib/mothership/model-options.ts‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,22 @@ export function mothershipEffortOptions(model: ModelSelection['model']) {
3333
/** Shared by the visible controls, send path and server admission so hidden preferences cannot leak. */
3434
export function resolveMothershipModelSettings(
3535
settings: { effort?: MothershipEffort; modelSelection?: ModelSelection },
36-
advanced: boolean
36+
advanced: boolean,
37+
plan = false
3738
): { effort: MothershipEffort; modelSelection: ModelSelection } {
38-
let effort = settings.effort ?? 'high'
39+
let effort = settings.effort ?? (plan ? 'medium' : 'high')
3940
if (!advanced) {
40-
if (effort === 'none' || effort === 'low') effort = 'medium'
41-
if (effort === 'max') effort = 'xhigh'
42-
return { effort, modelSelection: { model: 'gpt-6-astra', fastMode: false } }
41+
if (effort === 'none' || (!plan && effort === 'low')) effort = 'medium'
42+
if (!plan && effort === 'max') effort = 'xhigh'
43+
return {
44+
effort,
45+
modelSelection: { model: plan ? 'claude-opus-5-5' : 'gpt-6-astra', fastMode: false },
46+
}
47+
}
48+
const stored = settings.modelSelection ?? {
49+
model: plan ? 'claude-opus-5-5' : 'gpt-6-astra',
50+
fastMode: false,
4351
}
44-
const stored = settings.modelSelection ?? { model: 'gpt-6-astra', fastMode: false }
4552
const model = stored.model === 'claude-opus-5' ? 'claude-opus-5-5' : stored.model
4653
if (effort === 'none' && model !== 'gpt-6-sol') effort = 'medium'
4754
return {

0 commit comments

Comments
 (0)