mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-11 23:14:58 -05:00
fix(variables): fix tag dropdown and cursor alignment in variables block (#3199)
This commit is contained in:
committed by
Siddharth Ganesan
parent
d5a756c9f2
commit
cec74e09c2
@@ -113,6 +113,7 @@ const ChatMessageSchema = z.object({
|
||||
workflowId: z.string().optional(),
|
||||
knowledgeId: z.string().optional(),
|
||||
blockId: z.string().optional(),
|
||||
blockIds: z.array(z.string()).optional(),
|
||||
templateId: z.string().optional(),
|
||||
executionId: z.string().optional(),
|
||||
// For workflow_block, provide both workflowId and blockId
|
||||
@@ -159,6 +160,20 @@ export async function POST(req: NextRequest) {
|
||||
commands,
|
||||
} = ChatMessageSchema.parse(body)
|
||||
|
||||
const normalizedContexts = Array.isArray(contexts)
|
||||
? contexts.map((ctx) => {
|
||||
if (ctx.kind !== 'blocks') return ctx
|
||||
if (Array.isArray(ctx.blockIds) && ctx.blockIds.length > 0) return ctx
|
||||
if (ctx.blockId) {
|
||||
return {
|
||||
...ctx,
|
||||
blockIds: [ctx.blockId],
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
})
|
||||
: contexts
|
||||
|
||||
// Resolve workflowId - if not provided, use first workflow or find by name
|
||||
const resolved = await resolveWorkflowIdForUser(
|
||||
authenticatedUserId,
|
||||
@@ -176,10 +191,10 @@ export async function POST(req: NextRequest) {
|
||||
const userMessageIdToUse = userMessageId || crypto.randomUUID()
|
||||
try {
|
||||
logger.info(`[${tracker.requestId}] Received chat POST`, {
|
||||
hasContexts: Array.isArray(contexts),
|
||||
contextsCount: Array.isArray(contexts) ? contexts.length : 0,
|
||||
contextsPreview: Array.isArray(contexts)
|
||||
? contexts.map((c: any) => ({
|
||||
hasContexts: Array.isArray(normalizedContexts),
|
||||
contextsCount: Array.isArray(normalizedContexts) ? normalizedContexts.length : 0,
|
||||
contextsPreview: Array.isArray(normalizedContexts)
|
||||
? normalizedContexts.map((c: any) => ({
|
||||
kind: c?.kind,
|
||||
chatId: c?.chatId,
|
||||
workflowId: c?.workflowId,
|
||||
@@ -191,17 +206,25 @@ export async function POST(req: NextRequest) {
|
||||
} catch {}
|
||||
// Preprocess contexts server-side
|
||||
let agentContexts: Array<{ type: string; content: string }> = []
|
||||
if (Array.isArray(contexts) && contexts.length > 0) {
|
||||
if (Array.isArray(normalizedContexts) && normalizedContexts.length > 0) {
|
||||
try {
|
||||
const { processContextsServer } = await import('@/lib/copilot/process-contents')
|
||||
const processed = await processContextsServer(contexts as any, authenticatedUserId, message)
|
||||
const processed = await processContextsServer(
|
||||
normalizedContexts as any,
|
||||
authenticatedUserId,
|
||||
message
|
||||
)
|
||||
agentContexts = processed
|
||||
logger.info(`[${tracker.requestId}] Contexts processed for request`, {
|
||||
processedCount: agentContexts.length,
|
||||
kinds: agentContexts.map((c) => c.type),
|
||||
lengthPreview: agentContexts.map((c) => c.content?.length ?? 0),
|
||||
})
|
||||
if (Array.isArray(contexts) && contexts.length > 0 && agentContexts.length === 0) {
|
||||
if (
|
||||
Array.isArray(normalizedContexts) &&
|
||||
normalizedContexts.length > 0 &&
|
||||
agentContexts.length === 0
|
||||
) {
|
||||
logger.warn(
|
||||
`[${tracker.requestId}] Contexts provided but none processed. Check executionId for logs contexts.`
|
||||
)
|
||||
@@ -246,11 +269,13 @@ export async function POST(req: NextRequest) {
|
||||
mode,
|
||||
model: selectedModel,
|
||||
provider,
|
||||
conversationId: effectiveConversationId,
|
||||
conversationHistory,
|
||||
contexts: agentContexts,
|
||||
fileAttachments,
|
||||
commands,
|
||||
chatId: actualChatId,
|
||||
prefetch,
|
||||
implicitFeedback,
|
||||
},
|
||||
{
|
||||
@@ -432,10 +457,15 @@ export async function POST(req: NextRequest) {
|
||||
content: message,
|
||||
timestamp: new Date().toISOString(),
|
||||
...(fileAttachments && fileAttachments.length > 0 && { fileAttachments }),
|
||||
...(Array.isArray(contexts) && contexts.length > 0 && { contexts }),
|
||||
...(Array.isArray(contexts) &&
|
||||
contexts.length > 0 && {
|
||||
contentBlocks: [{ type: 'contexts', contexts: contexts as any, timestamp: Date.now() }],
|
||||
...(Array.isArray(normalizedContexts) &&
|
||||
normalizedContexts.length > 0 && {
|
||||
contexts: normalizedContexts,
|
||||
}),
|
||||
...(Array.isArray(normalizedContexts) &&
|
||||
normalizedContexts.length > 0 && {
|
||||
contentBlocks: [
|
||||
{ type: 'contexts', contexts: normalizedContexts as any, timestamp: Date.now() },
|
||||
],
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflow } from '@sim/db/schema'
|
||||
import { customTools, workflow } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { and, desc, eq, isNull, or } from 'drizzle-orm'
|
||||
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
|
||||
import type {
|
||||
ExecutionContext,
|
||||
@@ -12,6 +12,7 @@ import { routeExecution } from '@/lib/copilot/tools/server/router'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
|
||||
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
|
||||
import { getTool, resolveToolId } from '@/tools/utils'
|
||||
import {
|
||||
executeCheckDeploymentStatus,
|
||||
@@ -76,6 +77,247 @@ import {
|
||||
|
||||
const logger = createLogger('CopilotToolExecutor')
|
||||
|
||||
type ManageCustomToolOperation = 'add' | 'edit' | 'delete' | 'list'
|
||||
|
||||
interface ManageCustomToolSchema {
|
||||
type: 'function'
|
||||
function: {
|
||||
name: string
|
||||
description?: string
|
||||
parameters: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
interface ManageCustomToolParams {
|
||||
operation?: string
|
||||
toolId?: string
|
||||
schema?: ManageCustomToolSchema
|
||||
code?: string
|
||||
title?: string
|
||||
workspaceId?: string
|
||||
}
|
||||
|
||||
async function executeManageCustomTool(
|
||||
rawParams: Record<string, unknown>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
const params = rawParams as ManageCustomToolParams
|
||||
const operation = String(params.operation || '').toLowerCase() as ManageCustomToolOperation
|
||||
const workspaceId = params.workspaceId || context.workspaceId
|
||||
|
||||
if (!operation) {
|
||||
return { success: false, error: "Missing required 'operation' argument" }
|
||||
}
|
||||
|
||||
try {
|
||||
if (operation === 'list') {
|
||||
const toolsForUser = workspaceId
|
||||
? await db
|
||||
.select()
|
||||
.from(customTools)
|
||||
.where(
|
||||
or(
|
||||
eq(customTools.workspaceId, workspaceId),
|
||||
and(isNull(customTools.workspaceId), eq(customTools.userId, context.userId))
|
||||
)
|
||||
)
|
||||
.orderBy(desc(customTools.createdAt))
|
||||
: await db
|
||||
.select()
|
||||
.from(customTools)
|
||||
.where(and(isNull(customTools.workspaceId), eq(customTools.userId, context.userId)))
|
||||
.orderBy(desc(customTools.createdAt))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
operation,
|
||||
tools: toolsForUser,
|
||||
count: toolsForUser.length,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'add') {
|
||||
if (!workspaceId) {
|
||||
return {
|
||||
success: false,
|
||||
error: "workspaceId is required for operation 'add'",
|
||||
}
|
||||
}
|
||||
if (!params.schema || !params.code) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Both 'schema' and 'code' are required for operation 'add'",
|
||||
}
|
||||
}
|
||||
|
||||
const title = params.title || params.schema.function?.name
|
||||
if (!title) {
|
||||
return { success: false, error: "Missing tool title or schema.function.name for 'add'" }
|
||||
}
|
||||
|
||||
const resultTools = await upsertCustomTools({
|
||||
tools: [
|
||||
{
|
||||
title,
|
||||
schema: params.schema,
|
||||
code: params.code,
|
||||
},
|
||||
],
|
||||
workspaceId,
|
||||
userId: context.userId,
|
||||
})
|
||||
const created = resultTools.find((tool) => tool.title === title)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
operation,
|
||||
toolId: created?.id,
|
||||
title,
|
||||
message: `Created custom tool "${title}"`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'edit') {
|
||||
if (!workspaceId) {
|
||||
return {
|
||||
success: false,
|
||||
error: "workspaceId is required for operation 'edit'",
|
||||
}
|
||||
}
|
||||
if (!params.toolId) {
|
||||
return { success: false, error: "'toolId' is required for operation 'edit'" }
|
||||
}
|
||||
if (!params.schema && !params.code) {
|
||||
return {
|
||||
success: false,
|
||||
error: "At least one of 'schema' or 'code' is required for operation 'edit'",
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceTool = await db
|
||||
.select()
|
||||
.from(customTools)
|
||||
.where(and(eq(customTools.id, params.toolId), eq(customTools.workspaceId, workspaceId)))
|
||||
.limit(1)
|
||||
|
||||
const legacyTool =
|
||||
workspaceTool.length === 0
|
||||
? await db
|
||||
.select()
|
||||
.from(customTools)
|
||||
.where(
|
||||
and(
|
||||
eq(customTools.id, params.toolId),
|
||||
isNull(customTools.workspaceId),
|
||||
eq(customTools.userId, context.userId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
: []
|
||||
|
||||
const existing = workspaceTool[0] || legacyTool[0]
|
||||
if (!existing) {
|
||||
return { success: false, error: `Custom tool not found: ${params.toolId}` }
|
||||
}
|
||||
|
||||
const mergedSchema = params.schema || (existing.schema as ManageCustomToolSchema)
|
||||
const mergedCode = params.code || existing.code
|
||||
const title = params.title || mergedSchema.function?.name || existing.title
|
||||
|
||||
await upsertCustomTools({
|
||||
tools: [
|
||||
{
|
||||
id: params.toolId,
|
||||
title,
|
||||
schema: mergedSchema,
|
||||
code: mergedCode,
|
||||
},
|
||||
],
|
||||
workspaceId,
|
||||
userId: context.userId,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
operation,
|
||||
toolId: params.toolId,
|
||||
title,
|
||||
message: `Updated custom tool "${title}"`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'delete') {
|
||||
if (!params.toolId) {
|
||||
return { success: false, error: "'toolId' is required for operation 'delete'" }
|
||||
}
|
||||
|
||||
const workspaceDelete =
|
||||
workspaceId != null
|
||||
? await db
|
||||
.delete(customTools)
|
||||
.where(
|
||||
and(eq(customTools.id, params.toolId), eq(customTools.workspaceId, workspaceId))
|
||||
)
|
||||
.returning({ id: customTools.id })
|
||||
: []
|
||||
|
||||
const legacyDelete =
|
||||
workspaceDelete.length === 0
|
||||
? await db
|
||||
.delete(customTools)
|
||||
.where(
|
||||
and(
|
||||
eq(customTools.id, params.toolId),
|
||||
isNull(customTools.workspaceId),
|
||||
eq(customTools.userId, context.userId)
|
||||
)
|
||||
)
|
||||
.returning({ id: customTools.id })
|
||||
: []
|
||||
|
||||
const deleted = workspaceDelete[0] || legacyDelete[0]
|
||||
if (!deleted) {
|
||||
return { success: false, error: `Custom tool not found: ${params.toolId}` }
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
operation,
|
||||
toolId: params.toolId,
|
||||
message: 'Deleted custom tool',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `Unsupported operation for manage_custom_tool: ${operation}`,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('manage_custom_tool execution failed', {
|
||||
operation,
|
||||
workspaceId,
|
||||
userId: context.userId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to manage custom tool',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SERVER_TOOLS = new Set<string>([
|
||||
'get_blocks_and_tools',
|
||||
'get_blocks_metadata',
|
||||
@@ -161,6 +403,19 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
|
||||
}
|
||||
}
|
||||
},
|
||||
oauth_request_access: async (p, _c) => {
|
||||
const providerName = (p.providerName || p.provider_name || 'the provider') as string
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
status: 'requested',
|
||||
providerName,
|
||||
message: `Requested ${providerName} OAuth connection. The user should complete the OAuth modal in the UI, then retry credential-dependent actions.`,
|
||||
},
|
||||
}
|
||||
},
|
||||
manage_custom_tool: (p, c) => executeManageCustomTool(p, c),
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -310,6 +310,50 @@ function parseModelKey(compositeKey: string): { provider: string; modelId: strin
|
||||
return { provider: compositeKey.slice(0, slashIdx), modelId: compositeKey.slice(slashIdx + 1) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert legacy/variant Claude IDs into the canonical ID shape used by the model catalog.
|
||||
*
|
||||
* Examples:
|
||||
* - claude-4.5-opus -> claude-opus-4-5
|
||||
* - claude-opus-4.6 -> claude-opus-4-6
|
||||
* - anthropic.claude-opus-4-5-20251101-v1:0 -> claude-opus-4-5 (match key only)
|
||||
*/
|
||||
function canonicalizeModelMatchKey(modelId: string): string {
|
||||
if (!modelId) return modelId
|
||||
const normalized = modelId.trim().toLowerCase()
|
||||
|
||||
const toCanonicalClaude = (tier: string, version: string): string => {
|
||||
const normalizedVersion = version.replace(/\./g, '-')
|
||||
return `claude-${tier}-${normalizedVersion}`
|
||||
}
|
||||
|
||||
const tierFirstExact = normalized.match(/^claude-(opus|sonnet|haiku)-(\d+(?:[.-]\d+)?)$/)
|
||||
if (tierFirstExact) {
|
||||
const [, tier, version] = tierFirstExact
|
||||
return toCanonicalClaude(tier, version)
|
||||
}
|
||||
|
||||
const versionFirstExact = normalized.match(/^claude-(\d+(?:[.-]\d+)?)-(opus|sonnet|haiku)$/)
|
||||
if (versionFirstExact) {
|
||||
const [, version, tier] = versionFirstExact
|
||||
return toCanonicalClaude(tier, version)
|
||||
}
|
||||
|
||||
const tierFirstEmbedded = normalized.match(/claude-(opus|sonnet|haiku)-(\d+(?:[.-]\d+)?)/)
|
||||
if (tierFirstEmbedded) {
|
||||
const [, tier, version] = tierFirstEmbedded
|
||||
return toCanonicalClaude(tier, version)
|
||||
}
|
||||
|
||||
const versionFirstEmbedded = normalized.match(/claude-(\d+(?:[.-]\d+)?)-(opus|sonnet|haiku)/)
|
||||
if (versionFirstEmbedded) {
|
||||
const [, version, tier] = versionFirstEmbedded
|
||||
return toCanonicalClaude(tier, version)
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
const MODEL_PROVIDER_PRIORITY = [
|
||||
'anthropic',
|
||||
'bedrock',
|
||||
@@ -350,12 +394,23 @@ function normalizeSelectedModelKey(selectedModel: string, models: AvailableModel
|
||||
|
||||
const { provider, modelId } = parseModelKey(selectedModel)
|
||||
const targetModelId = modelId || selectedModel
|
||||
const targetMatchKey = canonicalizeModelMatchKey(targetModelId)
|
||||
|
||||
const matches = models.filter((m) => m.id.endsWith(`/${targetModelId}`))
|
||||
const matches = models.filter((m) => {
|
||||
const candidateModelId = parseModelKey(m.id).modelId || m.id
|
||||
const candidateMatchKey = canonicalizeModelMatchKey(candidateModelId)
|
||||
return (
|
||||
candidateModelId === targetModelId ||
|
||||
m.id.endsWith(`/${targetModelId}`) ||
|
||||
candidateMatchKey === targetMatchKey
|
||||
)
|
||||
})
|
||||
if (matches.length === 0) return selectedModel
|
||||
|
||||
if (provider) {
|
||||
const sameProvider = matches.find((m) => m.provider === provider)
|
||||
const sameProvider = matches.find(
|
||||
(m) => m.provider === provider || m.id.startsWith(`${provider}/`)
|
||||
)
|
||||
if (sameProvider) return sameProvider.id
|
||||
}
|
||||
|
||||
@@ -1093,11 +1148,12 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
const chatConfig = chat.config ?? {}
|
||||
const chatMode = chatConfig.mode || get().mode
|
||||
const chatModel = chatConfig.model || get().selectedModel
|
||||
const normalizedChatModel = normalizeSelectedModelKey(chatModel, get().availableModels)
|
||||
|
||||
logger.debug('[Chat] Restoring chat config', {
|
||||
chatId: chat.id,
|
||||
mode: chatMode,
|
||||
model: chatModel,
|
||||
model: normalizedChatModel,
|
||||
hasPlanArtifact: !!planArtifact,
|
||||
})
|
||||
|
||||
@@ -1119,7 +1175,7 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
showPlanTodos: false,
|
||||
streamingPlanContent: planArtifact,
|
||||
mode: chatMode,
|
||||
selectedModel: chatModel as CopilotStore['selectedModel'],
|
||||
selectedModel: normalizedChatModel as CopilotStore['selectedModel'],
|
||||
suppressAutoSelect: false,
|
||||
})
|
||||
|
||||
@@ -1292,6 +1348,10 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
const refreshedConfig = updatedCurrentChat.config ?? {}
|
||||
const refreshedMode = refreshedConfig.mode || get().mode
|
||||
const refreshedModel = refreshedConfig.model || get().selectedModel
|
||||
const normalizedRefreshedModel = normalizeSelectedModelKey(
|
||||
refreshedModel,
|
||||
get().availableModels
|
||||
)
|
||||
const toolCallsById = buildToolCallsById(normalizedMessages)
|
||||
|
||||
set({
|
||||
@@ -1300,7 +1360,7 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
toolCallsById,
|
||||
streamingPlanContent: refreshedPlanArtifact,
|
||||
mode: refreshedMode,
|
||||
selectedModel: refreshedModel as CopilotStore['selectedModel'],
|
||||
selectedModel: normalizedRefreshedModel as CopilotStore['selectedModel'],
|
||||
})
|
||||
}
|
||||
try {
|
||||
@@ -1320,11 +1380,15 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
const chatConfig = mostRecentChat.config ?? {}
|
||||
const chatMode = chatConfig.mode || get().mode
|
||||
const chatModel = chatConfig.model || get().selectedModel
|
||||
const normalizedChatModel = normalizeSelectedModelKey(
|
||||
chatModel,
|
||||
get().availableModels
|
||||
)
|
||||
|
||||
logger.info('[Chat] Auto-selecting most recent chat with config', {
|
||||
chatId: mostRecentChat.id,
|
||||
mode: chatMode,
|
||||
model: chatModel,
|
||||
model: normalizedChatModel,
|
||||
hasPlanArtifact: !!planArtifact,
|
||||
})
|
||||
|
||||
@@ -1336,7 +1400,7 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
toolCallsById,
|
||||
streamingPlanContent: planArtifact,
|
||||
mode: chatMode,
|
||||
selectedModel: chatModel as CopilotStore['selectedModel'],
|
||||
selectedModel: normalizedChatModel as CopilotStore['selectedModel'],
|
||||
})
|
||||
try {
|
||||
await get().loadMessageCheckpoints(mostRecentChat.id)
|
||||
@@ -2268,7 +2332,8 @@ export const useCopilotStore = create<CopilotStore>()(
|
||||
},
|
||||
|
||||
setSelectedModel: async (model) => {
|
||||
set({ selectedModel: model })
|
||||
const normalizedModel = normalizeSelectedModelKey(model, get().availableModels)
|
||||
set({ selectedModel: normalizedModel as CopilotStore['selectedModel'] })
|
||||
},
|
||||
setAgentPrefetch: (prefetch) => set({ agentPrefetch: prefetch }),
|
||||
loadAvailableModels: async () => {
|
||||
|
||||
Reference in New Issue
Block a user