Skip to content

Commit e04080d

Browse files
committed
feat(mothership): keep each chat's reasoning effort, default to medium, restore Low
The simple picker offers Low / Medium / High / Extra High again, each sending exactly that effort. New chats and chats never changed run at medium instead of high. An effort the user picks is stored on the chat (copilot_chats.config) through a new PUT /api/mothership/chats/[chatId]/effort and at turn admission, and later turns of that chat keep it. The global last-used effort is no longer persisted. The Sim Chat block defaults to medium.
1 parent ea88578 commit e04080d

21 files changed

Lines changed: 446 additions & 70 deletions

File tree

‎apps/sim/app/api/copilot/chat/queries.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
readLiveTurnSnapshot,
1313
} from '@/lib/mothership/chat/live-turn-snapshot'
1414
import { normalizeMessage } from '@/lib/mothership/chat/persisted-message'
15+
import type { MothershipEffort } from '@/lib/mothership/model-options'
1516
import {
1617
authenticateCopilotRequestSessionOnly,
1718
createBadRequestResponse,
@@ -32,6 +33,7 @@ function transformChat(chat: {
3233
model: string | null
3334
messages: unknown
3435
config?: unknown
36+
effort?: MothershipEffort | null
3537
conversationId?: string | null
3638
resources?: unknown
3739
createdAt: Date | null
@@ -44,6 +46,7 @@ function transformChat(chat: {
4446
messages: Array.isArray(chat.messages) ? chat.messages : [],
4547
messageCount: Array.isArray(chat.messages) ? chat.messages.length : 0,
4648
config: chat.config || null,
49+
effort: chat.effort ?? null,
4750
...('conversationId' in chat ? { activeStreamId: chat.conversationId || null } : {}),
4851
...('resources' in chat
4952
? { resources: Array.isArray(chat.resources) ? chat.resources : [] }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { setMothershipChatEffortContract } from '@/lib/api/contracts/mothership-chats'
2+
import {
3+
defineInternalJsonRoute,
4+
internalJsonPresenters,
5+
internalOrchestrationErrorPolicy,
6+
internalRateLimits,
7+
internalSessionAuth,
8+
} from '@/lib/api/server/routes'
9+
import { setChatEffort } from '@/lib/mothership/chat/application/set-effort'
10+
11+
/** Records the reasoning effort the owner picked for one of their chats. */
12+
export const PUT = defineInternalJsonRoute({
13+
contract: setMothershipChatEffortContract,
14+
operation: setChatEffort.operation,
15+
auth: internalSessionAuth,
16+
rateLimit: internalRateLimits.none({
17+
reason: 'Personal chat settings updates have no separate rate bucket.',
18+
}),
19+
errorPolicy: internalOrchestrationErrorPolicy,
20+
mapInput: ({ params, body }) => ({ chatId: params.chatId, effort: body.effort }),
21+
useCase: setChatEffort,
22+
present: internalJsonPresenters.withSuccess,
23+
})

‎apps/sim/app/api/mothership/chats/[chatId]/route.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export const GET = withRouteHandler(
9494
messages: effectiveMessages,
9595
activeStreamId: liveStreamId,
9696
resources: Array.isArray(chat.resources) ? chat.resources : [],
97+
effort: chat.effort,
9798
createdAt: chat.createdAt,
9899
updatedAt: chat.updatedAt,
99100
// Events stay out of the payload (the resume endpoint replays them),

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,7 @@ let queryClient: QueryClient
102102

103103
beforeEach(() => {
104104
mocks.advanced = false
105-
useMothershipEffortStore.setState({
106-
effort: 'high',
107-
modelSelection: { model: 'gpt-6-astra', fastMode: false },
108-
})
105+
useMothershipEffortStore.getState().reset()
109106
mocks.plan = false
110107
vi.clearAllMocks()
111108
mocks.workspaces = [

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ import { Brain, Check, Sparkles } from '@sim/emcn/icons'
1111
import {
1212
MOTHERSHIP_MODEL_OPTIONS,
1313
MOTHERSHIP_SIMPLE_EFFORT_OPTIONS,
14+
type MothershipEffort,
1415
mothershipEffortOptions,
1516
resolveMothershipModelSettings,
1617
} from '@/lib/mothership/model-options'
18+
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
1719
import { FastModeToggle } from '@/app/workspace/[workspaceId]/home/components/user-input/components/fast-mode-toggle'
1820
import { ModelSettingTrigger } from '@/app/workspace/[workspaceId]/home/components/user-input/components/model-setting-trigger'
1921
import { useFeatureFlag } from '@/app/workspace/[workspaceId]/providers/feature-flags-provider'
22+
import {
23+
useMothershipChatHistory,
24+
useSetMothershipChatEffort,
25+
} from '@/hooks/queries/mothership-chats'
2026
import { useMothershipEffortStore } from '@/stores/mothership-effort/store'
2127

2228
/** Model, reasoning effort, and Fast mode for Build chat composers. */
@@ -25,15 +31,23 @@ export function ModelSelector() {
2531
const selection = useMothershipEffortStore((state) => state.modelSelection)
2632
const setModel = useMothershipEffortStore((state) => state.setModel)
2733
const setFastMode = useMothershipEffortStore((state) => state.setFastMode)
28-
const storedEffort = useMothershipEffortStore((state) => state.effort)
34+
const { chatId } = useChatSurface()
35+
const { data: chatHistory } = useMothershipChatHistory(chatId)
36+
const newChatEffort = useMothershipEffortStore((state) => state.newChatEffort)
37+
const setNewChatEffort = useMothershipEffortStore((state) => state.setNewChatEffort)
38+
const { mutate: setChatEffort } = useSetMothershipChatEffort()
39+
const effortChoice = chatId ? chatHistory?.effort : newChatEffort
2940
const { effort, modelSelection } = resolveMothershipModelSettings(
30-
{ effort: storedEffort, modelSelection: selection },
41+
{ effort: effortChoice ?? undefined, modelSelection: selection },
3142
advanced
3243
)
3344
const options = advanced
3445
? mothershipEffortOptions(modelSelection.model)
3546
: MOTHERSHIP_SIMPLE_EFFORT_OPTIONS
36-
const setEffort = useMothershipEffortStore((state) => state.setEffort)
47+
const setEffort = (choice: MothershipEffort) => {
48+
if (chatId) setChatEffort({ chatId, effort: choice })
49+
else setNewChatEffort(choice)
50+
}
3751
const effortLabel = options.find((option) => option.value === effort)?.label ?? effort
3852
const modelLabel =
3953
MOTHERSHIP_MODEL_OPTIONS.find((option) => option.value === modelSelection.model)?.label ??

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3681,6 +3681,12 @@ export function useChat(
36813681
? {}
36823682
: await getDesktopChatCapabilities(desktopScopeIdRef.current)
36833683

3684+
// Sent only when picked; otherwise the server uses the chat's stored pick or the default.
3685+
const effortChoice = requestChatId
3686+
? queryClient.getQueryData<MothershipChatHistory>(
3687+
mothershipChatKeys.detail(requestChatId)
3688+
)?.effort
3689+
: useMothershipEffortStore.getState().newChatEffort
36843690
const response = await fetch(apiPathRef.current, {
36853691
method: 'POST',
36863692
headers: { 'Content-Type': 'application/json' },
@@ -3707,10 +3713,11 @@ export function useChat(
37073713
userTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
37083714
...(options?.requestMode !== 'assistant'
37093715
? {
3710-
...resolveMothershipModelSettings(
3716+
modelSelection: resolveMothershipModelSettings(
37113717
useMothershipEffortStore.getState(),
37123718
modelSelectorEnabled
3713-
),
3719+
).modelSelection,
3720+
...(effortChoice ? { effort: effortChoice } : {}),
37143721
}
37153722
: {}),
37163723
}),
@@ -3727,6 +3734,13 @@ export function useChat(
37273734
return consumedByTranscript
37283735
}
37293736
if (admittedChatId && !requestChatId) {
3737+
if (effortChoice) {
3738+
queryClient.setQueryData<MothershipChatHistory>(
3739+
mothershipChatKeys.detail(admittedChatId),
3740+
(current) => current && { ...current, effort: effortChoice }
3741+
)
3742+
useMothershipEffortStore.getState().setNewChatEffort(null)
3743+
}
37303744
requestChatId = admittedChatId
37313745
streamTargetChatId = admittedChatId
37323746
adoptResolvedChatId(admittedChatId, { replaceHomeHistory: true, invalidateList: true })

‎apps/sim/blocks/blocks/mothership.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { Blimp } from '@sim/emcn/icons'
2-
import { MOTHERSHIP_EFFORT_OPTIONS, MOTHERSHIP_MODEL_OPTIONS } from '@/lib/mothership/model-options'
2+
import {
3+
DEFAULT_MOTHERSHIP_EFFORT,
4+
MOTHERSHIP_EFFORT_OPTIONS,
5+
MOTHERSHIP_MODEL_OPTIONS,
6+
} from '@/lib/mothership/model-options'
37
import type { BlockConfig } from '@/blocks/types'
48

59
export const MothershipBlock: BlockConfig = {
@@ -45,7 +49,7 @@ export const MothershipBlock: BlockConfig = {
4549
title: 'Reasoning Effort',
4650
type: 'dropdown',
4751
options: MOTHERSHIP_EFFORT_OPTIONS.map(({ value, label }) => ({ id: value, label })),
48-
value: () => 'high',
52+
value: () => DEFAULT_MOTHERSHIP_EFFORT,
4953
},
5054
{
5155
id: 'fastMode',

‎apps/sim/executor/handlers/mothership/mothership-handler.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { assertValidMcpServerToolBindings, MCP_SERVER_ADVANCED_TOOL_TYPE } from
2929
import { resolveMcpToolBinding } from '@/lib/mcp/tool-binding'
3030
import { resolveMothershipConversation } from '@/lib/mothership/conversation-id'
3131
import { ChatPayloadSchema, ModelSelectionSchema } from '@/lib/mothership/generated/protocol'
32+
import { DEFAULT_MOTHERSHIP_EFFORT } from '@/lib/mothership/model-options'
3233
import { normalizeSecretMountPolicy } from '@/lib/mothership/secret-mount-policy'
3334
import {
3435
areModelSafeWorkspaceFileKeys,
@@ -971,7 +972,7 @@ export class MothershipBlockHandler implements BlockHandler {
971972
model: inputs.model ?? 'gpt-6-astra',
972973
fastMode: inputs.model === 'claude-opus-5' ? false : (inputs.fastMode ?? false),
973974
})
974-
const effort = ChatPayloadSchema.shape.effort.parse(inputs.effort ?? 'high')
975+
const effort = ChatPayloadSchema.shape.effort.parse(inputs.effort ?? DEFAULT_MOTHERSHIP_EFFORT)
975976
const body: Record<string, unknown> = {
976977
messages,
977978
useConversationHistory: true,

‎apps/sim/hooks/queries/mothership-chats.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ import {
2020
type MothershipChatScope,
2121
markMothershipChatReadContract,
2222
restoreMothershipChatContract,
23+
type SetMothershipChatEffortBody,
24+
setMothershipChatEffortContract,
2325
updateMothershipChatContract,
2426
} from '@/lib/api/contracts/mothership-chats'
2527
import { mothershipResourceSchema } from '@/lib/api/contracts/mothership-resources'
2628
import { suspendDesktopChatScopes } from '@/lib/desktop/chat-scope'
2729
import type { PersistedMessage } from '@/lib/mothership/chat/persisted-message'
2830
import { normalizeMessage } from '@/lib/mothership/chat/persisted-message'
31+
import type { MothershipEffort } from '@/lib/mothership/model-options'
2932
import {
3033
type FilePreviewSession,
3134
isFilePreviewSession,
@@ -53,6 +56,8 @@ export interface MothershipChatHistory {
5356
messages: PersistedMessage[]
5457
activeStreamId: string | null
5558
resources: MothershipResource[]
59+
/** The effort the user picked for this chat; null or absent while it follows the default. */
60+
effort?: MothershipEffort | null
5661
streamSnapshot?: {
5762
events: StreamBatchEvent[]
5863
previewSessions: FilePreviewSession[]
@@ -200,6 +205,7 @@ function parseChatHistory(value: unknown): MothershipChatHistory {
200205
messages: normalizeMessages(chat.messages),
201206
activeStreamId: chat.activeStreamId,
202207
resources: parseResources(chat.resources, `${chatContext}.resources`),
208+
effort: getMothershipChatResponseSchema.shape.chat.shape.effort.parse(chat.effort ?? null),
203209
streamSnapshot: parseStrictStreamSnapshot(chat.streamSnapshot, `${chatContext}.streamSnapshot`),
204210
}
205211
}
@@ -581,6 +587,43 @@ export function useSetMothershipChatPinned(owner?: MothershipChatOwner) {
581587
})
582588
}
583589

590+
async function setChatEffort({
591+
chatId,
592+
effort,
593+
}: SetMothershipChatEffortBody & { chatId: string }): Promise<void> {
594+
await requestJson(setMothershipChatEffortContract, {
595+
params: { chatId },
596+
body: { effort },
597+
})
598+
}
599+
600+
/**
601+
* Records the effort the user picked for a chat, shown immediately. Success needs no
602+
* refetch: the server stores exactly the picked value, and refetching the detail would
603+
* reload the whole transcript, possibly mid-stream.
604+
*/
605+
export function useSetMothershipChatEffort() {
606+
const queryClient = useQueryClient()
607+
return useMutation({
608+
mutationFn: setChatEffort,
609+
onMutate: async ({ chatId, effort }) => {
610+
const queryKey = mothershipChatKeys.detail(chatId)
611+
const previousEffort = queryClient.getQueryData<MothershipChatHistory>(queryKey)?.effort
612+
queryClient.setQueryData<MothershipChatHistory>(
613+
queryKey,
614+
(current) => current && { ...current, effort }
615+
)
616+
return { previousEffort }
617+
},
618+
onError: (_error, { chatId }, context) => {
619+
queryClient.setQueryData<MothershipChatHistory>(
620+
mothershipChatKeys.detail(chatId),
621+
(current) => current && { ...current, effort: context?.previousEffort }
622+
)
623+
},
624+
})
625+
}
626+
584627
async function forkChat(params: {
585628
chatId: string
586629
upToMessageId: string

‎apps/sim/lib/api/contracts/mothership-chats.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ export const mothershipChatParamsSchema = z.object({
3939
chatId: z.string().min(1),
4040
})
4141

42+
/** The effort the user picked for a chat; null while the chat follows the default. */
43+
const mothershipChatEffortChoiceSchema = ChatPayloadSchema.shape.effort.unwrap().nullable()
44+
45+
const setMothershipChatEffortBodySchema = z.object({
46+
effort: ChatPayloadSchema.shape.effort.unwrap(),
47+
})
48+
export type SetMothershipChatEffortBody = z.input<typeof setMothershipChatEffortBodySchema>
49+
4250
export const updateMothershipChatBodySchema = z
4351
.object({
4452
title: z.string().trim().min(1).max(200).optional(),
@@ -309,6 +317,19 @@ export const updateMothershipChatContract = defineRouteContract({
309317
},
310318
})
311319

320+
export const setMothershipChatEffortContract = defineRouteContract({
321+
method: 'PUT',
322+
path: '/api/mothership/chats/[chatId]/effort',
323+
params: mothershipChatParamsSchema,
324+
body: setMothershipChatEffortBodySchema,
325+
response: {
326+
mode: 'json',
327+
schema: z.object({
328+
success: z.literal(true),
329+
}),
330+
},
331+
})
332+
312333
export const deleteMothershipChatContract = defineRouteContract({
313334
method: 'DELETE',
314335
path: '/api/mothership/chats/[chatId]',
@@ -399,6 +420,7 @@ export const getMothershipChatResponseSchema = z.object({
399420
messages: z.array(z.unknown()),
400421
activeStreamId: z.string().nullable(),
401422
resources: z.array(z.unknown()),
423+
effort: mothershipChatEffortChoiceSchema,
402424
createdAt: z.union([z.string(), z.date()]).nullable().optional(),
403425
updatedAt: z.union([z.string(), z.date()]).nullable().optional(),
404426
streamSnapshot: mothershipChatStreamSnapshotSchema.optional(),

0 commit comments

Comments
 (0)