Skip to content

Commit fed8259

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 7d3a1a4 commit fed8259

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
@@ -195,7 +195,7 @@ export const POST = withRouteHandler(
195195

196196
const parsed = await parseRequest(v2ChatContract, req, {}, { ...V2_PARSE_DEFAULTS })
197197
if (!parsed.success) return parsed.response
198-
const { workspaceId, message, conversationId } = parsed.data.body
198+
const { workspaceId, message, conversationId, effort } = parsed.data.body
199199

200200
const messageId = generateId()
201201
const requestId = generateId()
@@ -346,6 +346,7 @@ export const POST = withRouteHandler(
346346
chatId,
347347
messageId,
348348
...(integrationTools.length > 0 ? { integrationTools } : {}),
349+
...(effort ? { effort } : {}),
349350
}
350351

351352
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'
@@ -42,6 +42,11 @@ import { mentionifyIntegrations } from '@/blocks/integration-matcher'
4242
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
4343
import { type SpeechToTextError, useSpeechToText } from '@/hooks/use-speech-to-text'
4444
import { type DraftPayload, useMothershipDraftsStore } from '@/stores/mothership-drafts/store'
45+
import {
46+
MOTHERSHIP_EFFORT_OPTIONS,
47+
type MothershipEffort,
48+
useMothershipEffortStore,
49+
} from '@/stores/mothership-effort/store'
4550
import type { ChatContext } from '@/stores/panel'
4651

4752
export type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types'
@@ -649,6 +654,9 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
649654
editorRef.current.openResourceMenu({ left: rect.left, top: rect.top })
650655
}, [])
651656

657+
const effort = useMothershipEffortStore((state) => state.effort)
658+
const setEffort = useMothershipEffortStore((state) => state.setEffort)
659+
652660
const handleSlashTriggerClick = useCallback(() => {
653661
editorRef.current.insertSlashTrigger()
654662
}, [])
@@ -733,6 +741,12 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
733741
</Tooltip.Trigger>
734742
<Tooltip.Content side='top'>Skills</Tooltip.Content>
735743
</Tooltip.Root>
744+
<ChipDropdown
745+
options={MOTHERSHIP_EFFORT_OPTIONS}
746+
value={effort}
747+
placeholder='Effort'
748+
onChange={(value) => setEffort(value as MothershipEffort)}
749+
/>
736750
</div>
737751
<div className='flex items-center gap-1.5'>
738752
{canSearch && <ModeSwitcher onLeaveSearch={handleLeaveSearch} />}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ import { workflowKeys } from '@/hooks/queries/workflows'
119119
import { useExecutionStream } from '@/hooks/use-execution-stream'
120120
import { snapAllSmoothText } from '@/hooks/use-smooth-text'
121121
import { useExecutionStore } from '@/stores/execution/store'
122+
import { useMothershipEffortStore } from '@/stores/mothership-effort/store'
122123
import { useMothershipQueueStore } from '@/stores/mothership-queue/store'
123124
import type {
124125
QueuedMothershipMessage,
@@ -3401,6 +3402,7 @@ export function useChat(
34013402
// subagent) — the server gates the features on these flags.
34023403
...desktopChatCapabilities,
34033404
userTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
3405+
effort: useMothershipEffortStore.getState().effort,
34043406
}),
34053407
signal: abortController.signal,
34063408
})

‎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
@@ -428,6 +430,7 @@ export async function buildCopilotRequestPayload(
428430
...(integrationTools.length > 0 ? { integrationTools } : {}),
429431
...(mothershipTools.length > 0 ? { mothershipTools } : {}),
430432
...(params.userTimezone ? { userTimezone: params.userTimezone } : {}),
433+
...(params.effort ? { effort: params.effort } : {}),
431434
// The mounted chat view executes client-routed workflow tools (run panel UX), so the
432435
// UI declares that capability explicitly; headless callers omit or send [] and the
433436
// 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
@@ -287,6 +287,8 @@ const ChatMessageSchema = z.object({
287287
contexts: z.array(ChatContextSchema).optional(),
288288
commands: z.array(z.string()).optional(),
289289
userTimezone: z.string().optional(),
290+
/** Per-turn model effort dial; forwarded verbatim to the worker. */
291+
effort: z.enum(['low', 'medium', 'high', 'xhigh', 'max']).optional(),
290292
/**
291293
* Contract ChatRequest.clientCapabilities: what this caller can execute client-side.
292294
* PRESENT = explicit declaration (empty array → no client pickup, dispatch runs
@@ -354,6 +356,7 @@ type UnifiedChatBranch =
354356
fileAttachments?: UnifiedChatRequest['fileAttachments']
355357
userPermission?: string
356358
userTimezone?: string
359+
effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max'
357360
workflowId: string
358361
workflowName?: string
359362
workspaceId?: string
@@ -394,6 +397,7 @@ type UnifiedChatBranch =
394397
fileAttachments?: UnifiedChatRequest['fileAttachments']
395398
userPermission?: string
396399
userTimezone?: string
400+
effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max'
397401
desktopLocalFilesystem?: boolean
398402
browser?: boolean
399403
terminalCapable?: boolean
@@ -878,6 +882,7 @@ async function resolveBranch(params: {
878882
implicitFeedback: payloadParams.implicitFeedback,
879883
userPermission: payloadParams.userPermission,
880884
userTimezone: payloadParams.userTimezone,
885+
effort: payloadParams.effort,
881886
desktopLocalFilesystem: payloadParams.desktopLocalFilesystem,
882887
browser: payloadParams.browser,
883888
terminalCapable: payloadParams.terminalCapable,
@@ -936,6 +941,7 @@ async function resolveBranch(params: {
936941
chatId: payloadParams.chatId,
937942
userPermission: payloadParams.userPermission,
938943
userTimezone: payloadParams.userTimezone,
944+
effort: payloadParams.effort,
939945
desktopLocalFilesystem: payloadParams.desktopLocalFilesystem,
940946
browser: payloadParams.browser,
941947
terminalCapable: payloadParams.terminalCapable,
@@ -1372,6 +1378,7 @@ export async function handleUnifiedChatPost(req: NextRequest) {
13721378
fileAttachments: body.fileAttachments,
13731379
userPermission: userPermission ?? undefined,
13741380
userTimezone: body.userTimezone,
1381+
effort: body.effort,
13751382
workflowId: branch.workflowId,
13761383
workflowName: branch.workflowName,
13771384
workspaceId: branch.workspaceId,
@@ -1396,6 +1403,7 @@ export async function handleUnifiedChatPost(req: NextRequest) {
13961403
fileAttachments: body.fileAttachments,
13971404
userPermission: userPermission ?? undefined,
13981405
userTimezone: body.userTimezone,
1406+
effort: body.effort,
13991407
desktopLocalFilesystem: body.desktopCapabilities?.localFilesystem === true,
14001408
browser: body.desktopCapabilities?.browser === true,
14011409
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)