Skip to content

Commit ecba0b3

Browse files
committed
feat(mothership): per-turn effort dial — composer dropdown, request pass-through
The composer gains an effort dropdown (low..max, default high, persisted preference); the selection rides the chat POST and the v2 chat API (new optional effort field) through to the worker, which applies it per turn and keeps it across continuations. Claude-Session: https://claude.ai/code/session_01CgaxNAaeD3taGdghbXn17w
1 parent d728509 commit ecba0b3

7 files changed

Lines changed: 65 additions & 2 deletions

File tree

‎apps/sim/app/api/v2/chat/route.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export const POST = withRouteHandler(
153153

154154
const parsed = await parseRequest(v2ChatContract, req, {}, { ...V2_PARSE_DEFAULTS })
155155
if (!parsed.success) return parsed.response
156-
const { workspaceId, message, conversationId } = parsed.data.body
156+
const { workspaceId, message, conversationId, effort } = parsed.data.body
157157

158158
const messageId = generateId()
159159
const requestId = generateId()
@@ -253,6 +253,7 @@ export const POST = withRouteHandler(
253253
chatId,
254254
messageId,
255255
...(integrationTools.length > 0 ? { integrationTools } : {}),
256+
...(effort ? { effort } : {}),
256257
}
257258

258259
let allowExplicitAbort = true

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
useRef,
1111
useState,
1212
} from 'react'
13-
import { Button, cn, Paperclip, Plus, Slash, Tooltip, toast } from '@sim/emcn'
13+
import { Button, ChipDropdown, cn, Paperclip, Plus, Slash, Tooltip, toast } from '@sim/emcn'
1414
import { createLogger } from '@sim/logger'
1515
import { useParams } from 'next/navigation'
1616
import { getDesktopBridge } from '@/lib/desktop'
@@ -41,6 +41,11 @@ import { mentionifyIntegrations } from '@/blocks/integration-matcher'
4141
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
4242
import { type SpeechToTextError, useSpeechToText } from '@/hooks/use-speech-to-text'
4343
import { type DraftPayload, useMothershipDraftsStore } from '@/stores/mothership-drafts/store'
44+
import {
45+
MOTHERSHIP_EFFORT_OPTIONS,
46+
type MothershipEffort,
47+
useMothershipEffortStore,
48+
} from '@/stores/mothership-effort/store'
4449
import type { ChatContext } from '@/stores/panel'
4550

4651
export type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types'
@@ -592,6 +597,9 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
592597
editorRef.current.openResourceMenu({ left: rect.left, top: rect.top })
593598
}, [])
594599

600+
const effort = useMothershipEffortStore((state) => state.effort)
601+
const setEffort = useMothershipEffortStore((state) => state.setEffort)
602+
595603
const handleSlashTriggerClick = useCallback(() => {
596604
editorRef.current.insertSlashTrigger()
597605
}, [])
@@ -676,6 +684,12 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
676684
</Tooltip.Trigger>
677685
<Tooltip.Content side='top'>Skills</Tooltip.Content>
678686
</Tooltip.Root>
687+
<ChipDropdown
688+
options={MOTHERSHIP_EFFORT_OPTIONS}
689+
value={effort}
690+
placeholder='Effort'
691+
onChange={(value) => setEffort(value as MothershipEffort)}
692+
/>
679693
</div>
680694
<div className='flex items-center gap-1.5'>
681695
{isSttSupported && (

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ import { workflowKeys } from '@/hooks/queries/workflows'
115115
import { useExecutionStream } from '@/hooks/use-execution-stream'
116116
import { snapAllSmoothText } from '@/hooks/use-smooth-text'
117117
import { useExecutionStore } from '@/stores/execution/store'
118+
import { useMothershipEffortStore } from '@/stores/mothership-effort/store'
118119
import { useMothershipQueueStore } from '@/stores/mothership-queue/store'
119120
import type {
120121
QueuedMothershipMessage,
@@ -3373,6 +3374,7 @@ export function useChat(
33733374
// subagent) — the server gates the features on these flags.
33743375
...desktopChatCapabilities,
33753376
userTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
3377+
effort: useMothershipEffortStore.getState().effort,
33763378
}),
33773379
signal: abortController.signal,
33783380
})

‎apps/sim/lib/api/contracts/v2/chat.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export const v2ChatBodySchema = z.object({
2727
.uuid('conversationId must be a valid conversation id')
2828
.optional()
2929
.describe('Conversation to continue; a new one starts when omitted.'),
30+
effort: z
31+
.enum(['low', 'medium', 'high', 'xhigh', 'max'])
32+
.optional()
33+
.describe('Model effort for this turn; defaults to the deployment default (high).'),
3034
})
3135

3236
const v2ChatTokensSchema = z.object({

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ interface BuildPayloadParams {
5555
userPermission?: string
5656
/** Plan/flag-gated org capabilities (e.g. "custom-blocks") the mothership gates tools/prompts on. */
5757
userTimezone?: string
58+
/** Per-turn model effort dial (user-selected in the composer). */
59+
effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max'
5860
desktopLocalFilesystem?: boolean
5961
browser?: boolean
6062
terminalCapable?: boolean
@@ -424,6 +426,7 @@ export async function buildCopilotRequestPayload(
424426
...(integrationTools.length > 0 ? { integrationTools } : {}),
425427
...(mothershipTools.length > 0 ? { mothershipTools } : {}),
426428
...(params.userTimezone ? { userTimezone: params.userTimezone } : {}),
429+
...(params.effort ? { effort: params.effort } : {}),
427430
// The mounted chat view executes client-routed workflow tools (run panel UX), so the
428431
// UI declares that capability explicitly; headless callers omit or send [] and the
429432
// server runs those tools immediately instead of waiting out the pickup grace.

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,8 @@ const ChatMessageSchema = z.object({
283283
contexts: z.array(ChatContextSchema).optional(),
284284
commands: z.array(z.string()).optional(),
285285
userTimezone: z.string().optional(),
286+
/** Per-turn model effort dial; forwarded verbatim to the worker. */
287+
effort: z.enum(['low', 'medium', 'high', 'xhigh', 'max']).optional(),
286288
/**
287289
* Contract ChatRequest.clientCapabilities: what this caller can execute client-side.
288290
* PRESENT = explicit declaration (empty array → no client pickup, dispatch runs
@@ -350,6 +352,7 @@ type UnifiedChatBranch =
350352
fileAttachments?: UnifiedChatRequest['fileAttachments']
351353
userPermission?: string
352354
userTimezone?: string
355+
effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max'
353356
workflowId: string
354357
workflowName?: string
355358
workspaceId?: string
@@ -390,6 +393,7 @@ type UnifiedChatBranch =
390393
fileAttachments?: UnifiedChatRequest['fileAttachments']
391394
userPermission?: string
392395
userTimezone?: string
396+
effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max'
393397
desktopLocalFilesystem?: boolean
394398
browser?: boolean
395399
terminalCapable?: boolean
@@ -874,6 +878,7 @@ async function resolveBranch(params: {
874878
implicitFeedback: payloadParams.implicitFeedback,
875879
userPermission: payloadParams.userPermission,
876880
userTimezone: payloadParams.userTimezone,
881+
effort: payloadParams.effort,
877882
desktopLocalFilesystem: payloadParams.desktopLocalFilesystem,
878883
browser: payloadParams.browser,
879884
terminalCapable: payloadParams.terminalCapable,
@@ -932,6 +937,7 @@ async function resolveBranch(params: {
932937
chatId: payloadParams.chatId,
933938
userPermission: payloadParams.userPermission,
934939
userTimezone: payloadParams.userTimezone,
940+
effort: payloadParams.effort,
935941
desktopLocalFilesystem: payloadParams.desktopLocalFilesystem,
936942
browser: payloadParams.browser,
937943
terminalCapable: payloadParams.terminalCapable,
@@ -1324,6 +1330,7 @@ export async function handleUnifiedChatPost(req: NextRequest) {
13241330
fileAttachments: body.fileAttachments,
13251331
userPermission: userPermission ?? undefined,
13261332
userTimezone: body.userTimezone,
1333+
effort: body.effort,
13271334
workflowId: branch.workflowId,
13281335
workflowName: branch.workflowName,
13291336
workspaceId: branch.workspaceId,
@@ -1348,6 +1355,7 @@ export async function handleUnifiedChatPost(req: NextRequest) {
13481355
fileAttachments: body.fileAttachments,
13491356
userPermission: userPermission ?? undefined,
13501357
userTimezone: body.userTimezone,
1358+
effort: body.effort,
13511359
desktopLocalFilesystem: body.desktopCapabilities?.localFilesystem === true,
13521360
browser: body.desktopCapabilities?.browser === true,
13531361
terminalCapable: body.desktopCapabilities?.terminal === true,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { create } from 'zustand'
2+
import { devtools, persist } from 'zustand/middleware'
3+
4+
/** The composer's model-effort dial, forwarded per request to the mothership. */
5+
export type MothershipEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max'
6+
7+
export const MOTHERSHIP_EFFORT_OPTIONS: Array<{ value: MothershipEffort; label: string }> = [
8+
{ value: 'low', label: 'Low effort' },
9+
{ value: 'medium', label: 'Medium effort' },
10+
{ value: 'high', label: 'High effort' },
11+
{ value: 'xhigh', label: 'X-high effort' },
12+
{ value: 'max', label: 'Max effort' },
13+
]
14+
15+
interface MothershipEffortState {
16+
effort: MothershipEffort
17+
setEffort: (effort: MothershipEffort) => void
18+
}
19+
20+
export const useMothershipEffortStore = create<MothershipEffortState>()(
21+
devtools(
22+
persist(
23+
(set) => ({
24+
effort: 'high',
25+
setEffort: (effort) => set({ effort }),
26+
}),
27+
{ name: 'mothership-effort' }
28+
),
29+
{ name: 'mothership-effort-store' }
30+
)
31+
)

0 commit comments

Comments
 (0)