Skip to content

Commit cec74e0

Browse files
waleedlatif1Sg312
authored andcommitted
fix(variables): fix tag dropdown and cursor alignment in variables block (#3199)
1 parent d5a756c commit cec74e0

File tree

3 files changed

+372
-22
lines changed

3 files changed

+372
-22
lines changed

apps/sim/app/api/copilot/chat/route.ts

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ const ChatMessageSchema = z.object({
113113
workflowId: z.string().optional(),
114114
knowledgeId: z.string().optional(),
115115
blockId: z.string().optional(),
116+
blockIds: z.array(z.string()).optional(),
116117
templateId: z.string().optional(),
117118
executionId: z.string().optional(),
118119
// For workflow_block, provide both workflowId and blockId
@@ -159,6 +160,20 @@ export async function POST(req: NextRequest) {
159160
commands,
160161
} = ChatMessageSchema.parse(body)
161162

163+
const normalizedContexts = Array.isArray(contexts)
164+
? contexts.map((ctx) => {
165+
if (ctx.kind !== 'blocks') return ctx
166+
if (Array.isArray(ctx.blockIds) && ctx.blockIds.length > 0) return ctx
167+
if (ctx.blockId) {
168+
return {
169+
...ctx,
170+
blockIds: [ctx.blockId],
171+
}
172+
}
173+
return ctx
174+
})
175+
: contexts
176+
162177
// Resolve workflowId - if not provided, use first workflow or find by name
163178
const resolved = await resolveWorkflowIdForUser(
164179
authenticatedUserId,
@@ -176,10 +191,10 @@ export async function POST(req: NextRequest) {
176191
const userMessageIdToUse = userMessageId || crypto.randomUUID()
177192
try {
178193
logger.info(`[${tracker.requestId}] Received chat POST`, {
179-
hasContexts: Array.isArray(contexts),
180-
contextsCount: Array.isArray(contexts) ? contexts.length : 0,
181-
contextsPreview: Array.isArray(contexts)
182-
? contexts.map((c: any) => ({
194+
hasContexts: Array.isArray(normalizedContexts),
195+
contextsCount: Array.isArray(normalizedContexts) ? normalizedContexts.length : 0,
196+
contextsPreview: Array.isArray(normalizedContexts)
197+
? normalizedContexts.map((c: any) => ({
183198
kind: c?.kind,
184199
chatId: c?.chatId,
185200
workflowId: c?.workflowId,
@@ -191,17 +206,25 @@ export async function POST(req: NextRequest) {
191206
} catch {}
192207
// Preprocess contexts server-side
193208
let agentContexts: Array<{ type: string; content: string }> = []
194-
if (Array.isArray(contexts) && contexts.length > 0) {
209+
if (Array.isArray(normalizedContexts) && normalizedContexts.length > 0) {
195210
try {
196211
const { processContextsServer } = await import('@/lib/copilot/process-contents')
197-
const processed = await processContextsServer(contexts as any, authenticatedUserId, message)
212+
const processed = await processContextsServer(
213+
normalizedContexts as any,
214+
authenticatedUserId,
215+
message
216+
)
198217
agentContexts = processed
199218
logger.info(`[${tracker.requestId}] Contexts processed for request`, {
200219
processedCount: agentContexts.length,
201220
kinds: agentContexts.map((c) => c.type),
202221
lengthPreview: agentContexts.map((c) => c.content?.length ?? 0),
203222
})
204-
if (Array.isArray(contexts) && contexts.length > 0 && agentContexts.length === 0) {
223+
if (
224+
Array.isArray(normalizedContexts) &&
225+
normalizedContexts.length > 0 &&
226+
agentContexts.length === 0
227+
) {
205228
logger.warn(
206229
`[${tracker.requestId}] Contexts provided but none processed. Check executionId for logs contexts.`
207230
)
@@ -246,11 +269,13 @@ export async function POST(req: NextRequest) {
246269
mode,
247270
model: selectedModel,
248271
provider,
272+
conversationId: effectiveConversationId,
249273
conversationHistory,
250274
contexts: agentContexts,
251275
fileAttachments,
252276
commands,
253277
chatId: actualChatId,
278+
prefetch,
254279
implicitFeedback,
255280
},
256281
{
@@ -432,10 +457,15 @@ export async function POST(req: NextRequest) {
432457
content: message,
433458
timestamp: new Date().toISOString(),
434459
...(fileAttachments && fileAttachments.length > 0 && { fileAttachments }),
435-
...(Array.isArray(contexts) && contexts.length > 0 && { contexts }),
436-
...(Array.isArray(contexts) &&
437-
contexts.length > 0 && {
438-
contentBlocks: [{ type: 'contexts', contexts: contexts as any, timestamp: Date.now() }],
460+
...(Array.isArray(normalizedContexts) &&
461+
normalizedContexts.length > 0 && {
462+
contexts: normalizedContexts,
463+
}),
464+
...(Array.isArray(normalizedContexts) &&
465+
normalizedContexts.length > 0 && {
466+
contentBlocks: [
467+
{ type: 'contexts', contexts: normalizedContexts as any, timestamp: Date.now() },
468+
],
439469
}),
440470
}
441471

apps/sim/lib/copilot/orchestrator/tool-executor/index.ts

Lines changed: 257 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { db } from '@sim/db'
2-
import { workflow } from '@sim/db/schema'
2+
import { customTools, workflow } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
4-
import { eq } from 'drizzle-orm'
4+
import { and, desc, eq, isNull, or } from 'drizzle-orm'
55
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
66
import type {
77
ExecutionContext,
@@ -12,6 +12,7 @@ import { routeExecution } from '@/lib/copilot/tools/server/router'
1212
import { env } from '@/lib/core/config/env'
1313
import { getBaseUrl } from '@/lib/core/utils/urls'
1414
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
15+
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
1516
import { getTool, resolveToolId } from '@/tools/utils'
1617
import {
1718
executeCheckDeploymentStatus,
@@ -76,6 +77,247 @@ import {
7677

7778
const logger = createLogger('CopilotToolExecutor')
7879

80+
type ManageCustomToolOperation = 'add' | 'edit' | 'delete' | 'list'
81+
82+
interface ManageCustomToolSchema {
83+
type: 'function'
84+
function: {
85+
name: string
86+
description?: string
87+
parameters: Record<string, unknown>
88+
}
89+
}
90+
91+
interface ManageCustomToolParams {
92+
operation?: string
93+
toolId?: string
94+
schema?: ManageCustomToolSchema
95+
code?: string
96+
title?: string
97+
workspaceId?: string
98+
}
99+
100+
async function executeManageCustomTool(
101+
rawParams: Record<string, unknown>,
102+
context: ExecutionContext
103+
): Promise<ToolCallResult> {
104+
const params = rawParams as ManageCustomToolParams
105+
const operation = String(params.operation || '').toLowerCase() as ManageCustomToolOperation
106+
const workspaceId = params.workspaceId || context.workspaceId
107+
108+
if (!operation) {
109+
return { success: false, error: "Missing required 'operation' argument" }
110+
}
111+
112+
try {
113+
if (operation === 'list') {
114+
const toolsForUser = workspaceId
115+
? await db
116+
.select()
117+
.from(customTools)
118+
.where(
119+
or(
120+
eq(customTools.workspaceId, workspaceId),
121+
and(isNull(customTools.workspaceId), eq(customTools.userId, context.userId))
122+
)
123+
)
124+
.orderBy(desc(customTools.createdAt))
125+
: await db
126+
.select()
127+
.from(customTools)
128+
.where(and(isNull(customTools.workspaceId), eq(customTools.userId, context.userId)))
129+
.orderBy(desc(customTools.createdAt))
130+
131+
return {
132+
success: true,
133+
output: {
134+
success: true,
135+
operation,
136+
tools: toolsForUser,
137+
count: toolsForUser.length,
138+
},
139+
}
140+
}
141+
142+
if (operation === 'add') {
143+
if (!workspaceId) {
144+
return {
145+
success: false,
146+
error: "workspaceId is required for operation 'add'",
147+
}
148+
}
149+
if (!params.schema || !params.code) {
150+
return {
151+
success: false,
152+
error: "Both 'schema' and 'code' are required for operation 'add'",
153+
}
154+
}
155+
156+
const title = params.title || params.schema.function?.name
157+
if (!title) {
158+
return { success: false, error: "Missing tool title or schema.function.name for 'add'" }
159+
}
160+
161+
const resultTools = await upsertCustomTools({
162+
tools: [
163+
{
164+
title,
165+
schema: params.schema,
166+
code: params.code,
167+
},
168+
],
169+
workspaceId,
170+
userId: context.userId,
171+
})
172+
const created = resultTools.find((tool) => tool.title === title)
173+
174+
return {
175+
success: true,
176+
output: {
177+
success: true,
178+
operation,
179+
toolId: created?.id,
180+
title,
181+
message: `Created custom tool "${title}"`,
182+
},
183+
}
184+
}
185+
186+
if (operation === 'edit') {
187+
if (!workspaceId) {
188+
return {
189+
success: false,
190+
error: "workspaceId is required for operation 'edit'",
191+
}
192+
}
193+
if (!params.toolId) {
194+
return { success: false, error: "'toolId' is required for operation 'edit'" }
195+
}
196+
if (!params.schema && !params.code) {
197+
return {
198+
success: false,
199+
error: "At least one of 'schema' or 'code' is required for operation 'edit'",
200+
}
201+
}
202+
203+
const workspaceTool = await db
204+
.select()
205+
.from(customTools)
206+
.where(and(eq(customTools.id, params.toolId), eq(customTools.workspaceId, workspaceId)))
207+
.limit(1)
208+
209+
const legacyTool =
210+
workspaceTool.length === 0
211+
? await db
212+
.select()
213+
.from(customTools)
214+
.where(
215+
and(
216+
eq(customTools.id, params.toolId),
217+
isNull(customTools.workspaceId),
218+
eq(customTools.userId, context.userId)
219+
)
220+
)
221+
.limit(1)
222+
: []
223+
224+
const existing = workspaceTool[0] || legacyTool[0]
225+
if (!existing) {
226+
return { success: false, error: `Custom tool not found: ${params.toolId}` }
227+
}
228+
229+
const mergedSchema = params.schema || (existing.schema as ManageCustomToolSchema)
230+
const mergedCode = params.code || existing.code
231+
const title = params.title || mergedSchema.function?.name || existing.title
232+
233+
await upsertCustomTools({
234+
tools: [
235+
{
236+
id: params.toolId,
237+
title,
238+
schema: mergedSchema,
239+
code: mergedCode,
240+
},
241+
],
242+
workspaceId,
243+
userId: context.userId,
244+
})
245+
246+
return {
247+
success: true,
248+
output: {
249+
success: true,
250+
operation,
251+
toolId: params.toolId,
252+
title,
253+
message: `Updated custom tool "${title}"`,
254+
},
255+
}
256+
}
257+
258+
if (operation === 'delete') {
259+
if (!params.toolId) {
260+
return { success: false, error: "'toolId' is required for operation 'delete'" }
261+
}
262+
263+
const workspaceDelete =
264+
workspaceId != null
265+
? await db
266+
.delete(customTools)
267+
.where(
268+
and(eq(customTools.id, params.toolId), eq(customTools.workspaceId, workspaceId))
269+
)
270+
.returning({ id: customTools.id })
271+
: []
272+
273+
const legacyDelete =
274+
workspaceDelete.length === 0
275+
? await db
276+
.delete(customTools)
277+
.where(
278+
and(
279+
eq(customTools.id, params.toolId),
280+
isNull(customTools.workspaceId),
281+
eq(customTools.userId, context.userId)
282+
)
283+
)
284+
.returning({ id: customTools.id })
285+
: []
286+
287+
const deleted = workspaceDelete[0] || legacyDelete[0]
288+
if (!deleted) {
289+
return { success: false, error: `Custom tool not found: ${params.toolId}` }
290+
}
291+
292+
return {
293+
success: true,
294+
output: {
295+
success: true,
296+
operation,
297+
toolId: params.toolId,
298+
message: 'Deleted custom tool',
299+
},
300+
}
301+
}
302+
303+
return {
304+
success: false,
305+
error: `Unsupported operation for manage_custom_tool: ${operation}`,
306+
}
307+
} catch (error) {
308+
logger.error('manage_custom_tool execution failed', {
309+
operation,
310+
workspaceId,
311+
userId: context.userId,
312+
error: error instanceof Error ? error.message : String(error),
313+
})
314+
return {
315+
success: false,
316+
error: error instanceof Error ? error.message : 'Failed to manage custom tool',
317+
}
318+
}
319+
}
320+
79321
const SERVER_TOOLS = new Set<string>([
80322
'get_blocks_and_tools',
81323
'get_blocks_metadata',
@@ -161,6 +403,19 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
161403
}
162404
}
163405
},
406+
oauth_request_access: async (p, _c) => {
407+
const providerName = (p.providerName || p.provider_name || 'the provider') as string
408+
return {
409+
success: true,
410+
output: {
411+
success: true,
412+
status: 'requested',
413+
providerName,
414+
message: `Requested ${providerName} OAuth connection. The user should complete the OAuth modal in the UI, then retry credential-dependent actions.`,
415+
},
416+
}
417+
},
418+
manage_custom_tool: (p, c) => executeManageCustomTool(p, c),
164419
}
165420

166421
/**

0 commit comments

Comments
 (0)