Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/sim/app/api/copilot/chat/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
readLiveTurnSnapshot,
} from '@/lib/mothership/chat/live-turn-snapshot'
import { normalizeMessage } from '@/lib/mothership/chat/persisted-message'
import type { MothershipEffort } from '@/lib/mothership/model-options'
import {
authenticateCopilotRequestSessionOnly,
createBadRequestResponse,
Expand All @@ -32,6 +33,7 @@ function transformChat(chat: {
model: string | null
messages: unknown
config?: unknown
effort?: MothershipEffort | null
conversationId?: string | null
resources?: unknown
createdAt: Date | null
Expand All @@ -44,6 +46,7 @@ function transformChat(chat: {
messages: Array.isArray(chat.messages) ? chat.messages : [],
messageCount: Array.isArray(chat.messages) ? chat.messages.length : 0,
config: chat.config || null,
effort: chat.effort ?? null,
...('conversationId' in chat ? { activeStreamId: chat.conversationId || null } : {}),
...('resources' in chat
? { resources: Array.isArray(chat.resources) ? chat.resources : [] }
Expand Down
23 changes: 23 additions & 0 deletions apps/sim/app/api/mothership/chats/[chatId]/effort/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { setMothershipChatEffortContract } from '@/lib/api/contracts/mothership-chats'
import {
defineInternalJsonRoute,
internalJsonPresenters,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { setChatEffort } from '@/lib/mothership/chat/application/set-effort'

/** Records the reasoning effort the owner picked for one of their chats. */
export const PUT = defineInternalJsonRoute({
contract: setMothershipChatEffortContract,
operation: setChatEffort.operation,
auth: internalSessionAuth,
rateLimit: internalRateLimits.none({
reason: 'Personal chat settings updates have no separate rate bucket.',
}),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ params, body }) => ({ chatId: params.chatId, effort: body.effort }),
useCase: setChatEffort,
Comment thread
waleedlatif1 marked this conversation as resolved.
present: internalJsonPresenters.withSuccess,
})
1 change: 1 addition & 0 deletions apps/sim/app/api/mothership/chats/[chatId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export const GET = withRouteHandler(
messages: effectiveMessages,
activeStreamId: liveStreamId,
resources: Array.isArray(chat.resources) ? chat.resources : [],
effort: chat.effort,
createdAt: chat.createdAt,
updatedAt: chat.updatedAt,
// Events stay out of the payload (the resume endpoint replays them),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,7 @@ let queryClient: QueryClient

beforeEach(() => {
mocks.advanced = false
useMothershipEffortStore.setState({
effort: 'high',
modelSelection: { model: 'gpt-6-astra', fastMode: false },
})
useMothershipEffortStore.getState().reset()
mocks.plan = false
vi.clearAllMocks()
mocks.workspaces = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import { useEffect } from 'react'
import {
DropdownMenu,
DropdownMenuContent,
Expand All @@ -11,12 +12,18 @@ import { Brain, Check, Sparkles } from '@sim/emcn/icons'
import {
MOTHERSHIP_MODEL_OPTIONS,
MOTHERSHIP_SIMPLE_EFFORT_OPTIONS,
type MothershipEffort,
mothershipEffortOptions,
resolveMothershipModelSettings,
} from '@/lib/mothership/model-options'
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
import { FastModeToggle } from '@/app/workspace/[workspaceId]/home/components/user-input/components/fast-mode-toggle'
import { ModelSettingTrigger } from '@/app/workspace/[workspaceId]/home/components/user-input/components/model-setting-trigger'
import { useFeatureFlag } from '@/app/workspace/[workspaceId]/providers/feature-flags-provider'
import {
useMothershipChatHistory,
useSetMothershipChatEffort,
} from '@/hooks/queries/mothership-chats'
import { useMothershipEffortStore } from '@/stores/mothership-effort/store'

/** Model, reasoning effort, and Fast mode for Build chat composers. */
Expand All @@ -25,15 +32,32 @@ export function ModelSelector() {
const selection = useMothershipEffortStore((state) => state.modelSelection)
const setModel = useMothershipEffortStore((state) => state.setModel)
const setFastMode = useMothershipEffortStore((state) => state.setFastMode)
const storedEffort = useMothershipEffortStore((state) => state.effort)
const { chatId } = useChatSurface()
const { data: chatHistory } = useMothershipChatHistory(chatId)
const chatPick = useMothershipEffortStore((state) =>
chatId ? state.chatEfforts[chatId] : undefined
)
const newChatEffort = useMothershipEffortStore((state) => state.newChatEffort)
const setNewChatEffort = useMothershipEffortStore((state) => state.setNewChatEffort)
const { mutate: saveChatEffort } = useSetMothershipChatEffort(chatId)
const effortChoice = chatId ? (chatPick ?? chatHistory?.effort) : newChatEffort
const { effort, modelSelection } = resolveMothershipModelSettings(
{ effort: storedEffort, modelSelection: selection },
{ effort: effortChoice ?? undefined, modelSelection: selection },
advanced
)
const options = advanced
? mothershipEffortOptions(modelSelection.model)
: MOTHERSHIP_SIMPLE_EFFORT_OPTIONS
const setEffort = useMothershipEffortStore((state) => state.setEffort)
const setEffort = (choice: MothershipEffort) => {
if (chatId) saveChatEffort(choice)
else setNewChatEffort(choice)
}

useEffect(() => {
if (chatId) return
return () => useMothershipEffortStore.getState().setNewChatEffort(null)
}, [chatId])

const effortLabel = options.find((option) => option.value === effort)?.label ?? effort
const modelLabel =
MOTHERSHIP_MODEL_OPTIONS.find((option) => option.value === modelSelection.model)?.label ??
Expand Down
40 changes: 38 additions & 2 deletions apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,14 @@ export function useChat(
new Set())
const streamReaderRef = useRef<ReadableStreamDefaultReader<Uint8Array> | null>(null)
const chatIdRef = useRef<string | undefined>(initialChatId)
/** Cleared on unmount, so a late rollback cannot hand a pick to a surface the user left. */
const surfaceMountedRef = useRef(true)
useEffect(() => {
surfaceMountedRef.current = true
return () => {
surfaceMountedRef.current = false
}
}, [])
const tableViewContextsRef = useRef({
scopeId: desktopScopeId,
views: new Map<string, MothershipTableViewContext>(),
Expand Down Expand Up @@ -3396,6 +3404,18 @@ export function useChat(

let requestChatId =
queuedSendHandoff?.chatId ?? selectedChatIdRef.current ?? chatIdRef.current
// Read before the composer can unmount. Sent only when picked; otherwise the server
// uses the chat's stored pick or the default.
const effortStore = useMothershipEffortStore.getState()
const effortChoice =
options?.requestMode === 'assistant'
? undefined
: requestChatId
? (effortStore.chatEfforts[requestChatId] ??
queryClient.getQueryData<MothershipChatHistory>(
mothershipChatKeys.detail(requestChatId)
)?.effort)
: effortStore.newChatEffort
Comment thread
waleedlatif1 marked this conversation as resolved.
const writeQueuedSendHandoff = (chatId?: string) => {
if (!queuedSendHandoff) return
if (!chatId && !queuedSendHandoff.supersededStreamId) return
Expand Down Expand Up @@ -3530,6 +3550,16 @@ export function useChat(
}

const rollbackOptimisticSend = () => {
// A withdrawn first send hands its pick back to the new-chat composer for the retry,
// only while that surface is still open on the new chat.
if (
!requestChatId &&
effortChoice &&
surfaceMountedRef.current &&
!chatIdRef.current &&
!selectedChatIdRef.current
)
useMothershipEffortStore.getState().setNewChatEffort(effortChoice)
if (requestChatId) {
upsertChatHistory(requestChatId, (current) => ({
...current,
Expand Down Expand Up @@ -3707,10 +3737,11 @@ export function useChat(
userTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
...(options?.requestMode !== 'assistant'
? {
...resolveMothershipModelSettings(
modelSelection: resolveMothershipModelSettings(
useMothershipEffortStore.getState(),
modelSelectorEnabled
),
).modelSelection,
...(effortChoice ? { effort: effortChoice } : {}),
Comment thread
waleedlatif1 marked this conversation as resolved.
}
: {}),
}),
Expand All @@ -3727,6 +3758,8 @@ export function useChat(
return consumedByTranscript
}
if (admittedChatId && !requestChatId) {
if (effortChoice)
useMothershipEffortStore.getState().adoptNewChatEffort(admittedChatId, effortChoice)
requestChatId = admittedChatId
streamTargetChatId = admittedChatId
adoptResolvedChatId(admittedChatId, { replaceHomeHistory: true, invalidateList: true })
Expand Down Expand Up @@ -3775,6 +3808,9 @@ export function useChat(
const conflictChatId =
typeof errorData.chatId === 'string' ? errorData.chatId : undefined
if (conflictChatId && !streamTargetChatId) {
// The retry carries the same pick the first attempt stored on that chat.
if (effortChoice)
useMothershipEffortStore.getState().adoptNewChatEffort(conflictChatId, effortChoice)
adoptResolvedChatId(conflictChatId, {
replaceHomeHistory: true,
invalidateList: true,
Expand Down
8 changes: 6 additions & 2 deletions apps/sim/blocks/blocks/mothership.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Blimp } from '@sim/emcn/icons'
import { MOTHERSHIP_EFFORT_OPTIONS, MOTHERSHIP_MODEL_OPTIONS } from '@/lib/mothership/model-options'
import {
DEFAULT_MOTHERSHIP_EFFORT,
MOTHERSHIP_EFFORT_OPTIONS,
MOTHERSHIP_MODEL_OPTIONS,
} from '@/lib/mothership/model-options'
import type { BlockConfig } from '@/blocks/types'

export const MothershipBlock: BlockConfig = {
Expand Down Expand Up @@ -45,7 +49,7 @@ export const MothershipBlock: BlockConfig = {
title: 'Reasoning Effort',
type: 'dropdown',
options: MOTHERSHIP_EFFORT_OPTIONS.map(({ value, label }) => ({ id: value, label })),
value: () => 'high',
value: () => DEFAULT_MOTHERSHIP_EFFORT,
},
{
id: 'fastMode',
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/executor/handlers/mothership/mothership-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { assertValidMcpServerToolBindings, MCP_SERVER_ADVANCED_TOOL_TYPE } from
import { resolveMcpToolBinding } from '@/lib/mcp/tool-binding'
import { resolveMothershipConversation } from '@/lib/mothership/conversation-id'
import { ChatPayloadSchema, ModelSelectionSchema } from '@/lib/mothership/generated/protocol'
import { DEFAULT_MOTHERSHIP_EFFORT } from '@/lib/mothership/model-options'
import { normalizeSecretMountPolicy } from '@/lib/mothership/secret-mount-policy'
import {
areModelSafeWorkspaceFileKeys,
Expand Down Expand Up @@ -971,7 +972,7 @@ export class MothershipBlockHandler implements BlockHandler {
model: inputs.model ?? 'gpt-6-astra',
fastMode: inputs.model === 'claude-opus-5' ? false : (inputs.fastMode ?? false),
})
const effort = ChatPayloadSchema.shape.effort.parse(inputs.effort ?? 'high')
const effort = ChatPayloadSchema.shape.effort.parse(inputs.effort ?? DEFAULT_MOTHERSHIP_EFFORT)
const body: Record<string, unknown> = {
messages,
useConversationHistory: true,
Expand Down
44 changes: 44 additions & 0 deletions apps/sim/hooks/queries/mothership-chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,22 @@ import {
type MothershipChatScope,
markMothershipChatReadContract,
restoreMothershipChatContract,
type SetMothershipChatEffortBody,
setMothershipChatEffortContract,
updateMothershipChatContract,
} from '@/lib/api/contracts/mothership-chats'
import { mothershipResourceSchema } from '@/lib/api/contracts/mothership-resources'
import { suspendDesktopChatScopes } from '@/lib/desktop/chat-scope'
import type { PersistedMessage } from '@/lib/mothership/chat/persisted-message'
import { normalizeMessage } from '@/lib/mothership/chat/persisted-message'
import type { MothershipEffort } from '@/lib/mothership/model-options'
import {
type FilePreviewSession,
isFilePreviewSession,
} from '@/lib/mothership/request/session/file-preview-session-contract'
import { isStreamBatchEvent, type StreamBatchEvent } from '@/lib/mothership/request/session/types'
import type { MothershipResource } from '@/lib/mothership/resources/types'
import { useMothershipEffortStore } from '@/stores/mothership-effort/store'
import { useMothershipQueueStore } from '@/stores/mothership-queue/store'

export interface MothershipChatMetadata {
Expand All @@ -53,6 +57,8 @@ export interface MothershipChatHistory {
messages: PersistedMessage[]
activeStreamId: string | null
resources: MothershipResource[]
/** The effort the user picked for this chat; null or absent while it follows the default. */
effort?: MothershipEffort | null
streamSnapshot?: {
events: StreamBatchEvent[]
previewSessions: FilePreviewSession[]
Expand Down Expand Up @@ -200,6 +206,7 @@ function parseChatHistory(value: unknown): MothershipChatHistory {
messages: normalizeMessages(chat.messages),
activeStreamId: chat.activeStreamId,
resources: parseResources(chat.resources, `${chatContext}.resources`),
effort: getMothershipChatResponseSchema.shape.chat.shape.effort.parse(chat.effort ?? null),
streamSnapshot: parseStrictStreamSnapshot(chat.streamSnapshot, `${chatContext}.streamSnapshot`),
}
}
Expand Down Expand Up @@ -581,6 +588,43 @@ export function useSetMothershipChatPinned(owner?: MothershipChatOwner) {
})
}

async function setChatEffort({
chatId,
effort,
}: SetMothershipChatEffortBody & { chatId: string }): Promise<void> {
await requestJson(setMothershipChatEffortContract, {
params: { chatId },
body: { effort },
})
}

/**
* Records the effort the user picked for a chat. The pick shows and sends at once from the
* session's pick map; saves for one chat run one at a time so the last pick is the one stored.
*/
export function useSetMothershipChatEffort(chatId: string | undefined) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (effort: MothershipEffort) => {
if (!chatId) throw new Error('A chat effort needs a chat')
return setChatEffort({ chatId, effort })
},
scope: { id: `mothership-chat-effort:${chatId ?? ''}` },
onMutate: (effort) => {
if (chatId) useMothershipEffortStore.getState().setChatEffort(chatId, effort)
},
onError: (_error, effort) => {
if (chatId) useMothershipEffortStore.getState().dropChatEffort(chatId, effort)
},
onSuccess: (_data, effort) => {
queryClient.setQueryData<MothershipChatHistory>(
mothershipChatKeys.detail(chatId),
(current) => current && { ...current, effort }
)
},
})
}

async function forkChat(params: {
chatId: string
upToMessageId: string
Expand Down
22 changes: 22 additions & 0 deletions apps/sim/lib/api/contracts/mothership-chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ export const mothershipChatParamsSchema = z.object({
chatId: z.string().min(1),
})

/** The effort the user picked for a chat; null while the chat follows the default. */
const mothershipChatEffortChoiceSchema = ChatPayloadSchema.shape.effort.unwrap().nullable()

const setMothershipChatEffortBodySchema = z.object({
effort: ChatPayloadSchema.shape.effort.unwrap(),
})
export type SetMothershipChatEffortBody = z.input<typeof setMothershipChatEffortBodySchema>

export const updateMothershipChatBodySchema = z
.object({
title: z.string().trim().min(1).max(200).optional(),
Expand Down Expand Up @@ -309,6 +317,19 @@ export const updateMothershipChatContract = defineRouteContract({
},
})

export const setMothershipChatEffortContract = defineRouteContract({
method: 'PUT',
path: '/api/mothership/chats/[chatId]/effort',
params: mothershipChatParamsSchema,
body: setMothershipChatEffortBodySchema,
response: {
mode: 'json',
schema: z.object({
success: z.literal(true),
}),
},
})

export const deleteMothershipChatContract = defineRouteContract({
method: 'DELETE',
path: '/api/mothership/chats/[chatId]',
Expand Down Expand Up @@ -399,6 +420,7 @@ export const getMothershipChatResponseSchema = z.object({
messages: z.array(z.unknown()),
activeStreamId: z.string().nullable(),
resources: z.array(z.unknown()),
effort: mothershipChatEffortChoiceSchema,
createdAt: z.union([z.string(), z.date()]).nullable().optional(),
updatedAt: z.union([z.string(), z.date()]).nullable().optional(),
streamSnapshot: mothershipChatStreamSnapshotSchema.optional(),
Expand Down
Loading
Loading