diff --git a/apps/sim/app/api/copilot/api-keys/validate/route.ts b/apps/sim/app/api/copilot/api-keys/validate/route.ts index 15a604c8704..16f00aad875 100644 --- a/apps/sim/app/api/copilot/api-keys/validate/route.ts +++ b/apps/sim/app/api/copilot/api-keys/validate/route.ts @@ -65,6 +65,7 @@ export async function POST(req: NextRequest) { if (!Number.isNaN(limit) && limit > 0 && currentUsage >= limit) { // Usage exceeded + logger.info('[API VALIDATION] Usage exceeded', { userId, currentUsage, limit }) return new NextResponse(null, { status: 402 }) } } diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index a7f2bec118a..b13ed91667a 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -371,7 +371,42 @@ export async function POST(req: NextRequest) { (currentChat?.conversationId as string | undefined) || conversationId // If we have a conversationId, only send the most recent user message; else send full history - const messagesForAgent = effectiveConversationId ? [messages[messages.length - 1]] : messages + const latestUserMessage = + [...messages].reverse().find((m) => m?.role === 'user') || messages[messages.length - 1] + const messagesForAgent = effectiveConversationId ? [latestUserMessage] : messages + + const requestPayload = { + messages: messagesForAgent, + workflowId, + userId: authenticatedUserId, + stream: stream, + streamToolCalls: true, + mode: mode, + provider: providerToUse, + ...(effectiveConversationId ? { conversationId: effectiveConversationId } : {}), + ...(typeof depth === 'number' ? { depth } : {}), + ...(session?.user?.name && { userName: session.user.name }), + } + + // Log the payload being sent to the streaming endpoint + try { + logger.info(`[${tracker.requestId}] Sending payload to sim agent streaming endpoint`, { + url: `${SIM_AGENT_API_URL}/api/chat-completion-streaming`, + provider: providerToUse, + mode, + stream, + workflowId, + hasConversationId: !!effectiveConversationId, + depth: typeof depth === 'number' ? depth : undefined, + messagesCount: requestPayload.messages.length, + }) + // Full payload as JSON string + logger.info( + `[${tracker.requestId}] Full streaming payload: ${JSON.stringify(requestPayload)}` + ) + } catch (e) { + logger.warn(`[${tracker.requestId}] Failed to log payload preview for streaming endpoint`, e) + } const simAgentResponse = await fetch(`${SIM_AGENT_API_URL}/api/chat-completion-streaming`, { method: 'POST', @@ -379,18 +414,7 @@ export async function POST(req: NextRequest) { 'Content-Type': 'application/json', ...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}), }, - body: JSON.stringify({ - messages: messagesForAgent, - workflowId, - userId: authenticatedUserId, - stream: stream, - streamToolCalls: true, - mode: mode, - provider: providerToUse, - ...(effectiveConversationId ? { conversationId: effectiveConversationId } : {}), - ...(typeof depth === 'number' ? { depth } : {}), - ...(session?.user?.name && { userName: session.user.name }), - }), + body: JSON.stringify(requestPayload), }) if (!simAgentResponse.ok) { @@ -690,7 +714,7 @@ export async function POST(req: NextRequest) { ) } - const responseId = responseIdFromDone || responseIdFromStart + const responseId = responseIdFromDone // Update chat in database immediately (without title) await db diff --git a/apps/sim/app/api/logs/[executionId]/frozen-canvas/route.ts b/apps/sim/app/api/logs/[executionId]/frozen-canvas/route.ts index b9410b4f726..be596d034bc 100644 --- a/apps/sim/app/api/logs/[executionId]/frozen-canvas/route.ts +++ b/apps/sim/app/api/logs/[executionId]/frozen-canvas/route.ts @@ -46,20 +46,7 @@ export async function GET( startedAt: workflowLog.startedAt.toISOString(), endedAt: workflowLog.endedAt?.toISOString(), totalDurationMs: workflowLog.totalDurationMs, - blockStats: { - total: workflowLog.blockCount, - success: workflowLog.successCount, - error: workflowLog.errorCount, - skipped: workflowLog.skippedCount, - }, - cost: { - total: workflowLog.totalCost ? Number.parseFloat(workflowLog.totalCost) : null, - input: workflowLog.totalInputCost ? Number.parseFloat(workflowLog.totalInputCost) : null, - output: workflowLog.totalOutputCost - ? Number.parseFloat(workflowLog.totalOutputCost) - : null, - }, - totalTokens: workflowLog.totalTokens, + cost: workflowLog.cost || null, }, } diff --git a/apps/sim/app/api/logs/by-id/[id]/route.ts b/apps/sim/app/api/logs/by-id/[id]/route.ts new file mode 100644 index 00000000000..02817f1e4e6 --- /dev/null +++ b/apps/sim/app/api/logs/by-id/[id]/route.ts @@ -0,0 +1,102 @@ +import { and, eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console/logger' +import { db } from '@/db' +import { permissions, workflow, workflowExecutionLogs } from '@/db/schema' + +const logger = createLogger('LogDetailsByIdAPI') + +export const revalidate = 0 + +export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const session = await getSession() + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthorized log details access attempt`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + const { id } = await params + + const rows = await db + .select({ + id: workflowExecutionLogs.id, + workflowId: workflowExecutionLogs.workflowId, + executionId: workflowExecutionLogs.executionId, + stateSnapshotId: workflowExecutionLogs.stateSnapshotId, + level: workflowExecutionLogs.level, + trigger: workflowExecutionLogs.trigger, + startedAt: workflowExecutionLogs.startedAt, + endedAt: workflowExecutionLogs.endedAt, + totalDurationMs: workflowExecutionLogs.totalDurationMs, + executionData: workflowExecutionLogs.executionData, + cost: workflowExecutionLogs.cost, + files: workflowExecutionLogs.files, + createdAt: workflowExecutionLogs.createdAt, + workflowName: workflow.name, + workflowDescription: workflow.description, + workflowColor: workflow.color, + workflowFolderId: workflow.folderId, + workflowUserId: workflow.userId, + workflowWorkspaceId: workflow.workspaceId, + workflowCreatedAt: workflow.createdAt, + workflowUpdatedAt: workflow.updatedAt, + }) + .from(workflowExecutionLogs) + .innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id)) + .innerJoin( + permissions, + and( + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workflow.workspaceId), + eq(permissions.userId, userId) + ) + ) + .where(eq(workflowExecutionLogs.id, id)) + .limit(1) + + const log = rows[0] + if (!log) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }) + } + + const workflowSummary = { + id: log.workflowId, + name: log.workflowName, + description: log.workflowDescription, + color: log.workflowColor, + folderId: log.workflowFolderId, + userId: log.workflowUserId, + workspaceId: log.workflowWorkspaceId, + createdAt: log.workflowCreatedAt, + updatedAt: log.workflowUpdatedAt, + } + + const response = { + id: log.id, + workflowId: log.workflowId, + executionId: log.executionId, + level: log.level, + duration: log.totalDurationMs ? `${log.totalDurationMs}ms` : null, + trigger: log.trigger, + createdAt: log.startedAt.toISOString(), + files: log.files || undefined, + workflow: workflowSummary, + executionData: { + totalDuration: log.totalDurationMs, + ...(log.executionData as any), + enhanced: true, + }, + cost: log.cost as any, + } + + return NextResponse.json({ data: response }) + } catch (error: any) { + logger.error(`[${requestId}] log details fetch error`, error) + return NextResponse.json({ error: error.message }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/logs/cleanup/route.ts b/apps/sim/app/api/logs/cleanup/route.ts index 823a8d7c668..55ca2b35b50 100644 --- a/apps/sim/app/api/logs/cleanup/route.ts +++ b/apps/sim/app/api/logs/cleanup/route.ts @@ -99,21 +99,13 @@ export async function GET(request: NextRequest) { executionId: workflowExecutionLogs.executionId, stateSnapshotId: workflowExecutionLogs.stateSnapshotId, level: workflowExecutionLogs.level, - message: workflowExecutionLogs.message, trigger: workflowExecutionLogs.trigger, startedAt: workflowExecutionLogs.startedAt, endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, - blockCount: workflowExecutionLogs.blockCount, - successCount: workflowExecutionLogs.successCount, - errorCount: workflowExecutionLogs.errorCount, - skippedCount: workflowExecutionLogs.skippedCount, - totalCost: workflowExecutionLogs.totalCost, - totalInputCost: workflowExecutionLogs.totalInputCost, - totalOutputCost: workflowExecutionLogs.totalOutputCost, - totalTokens: workflowExecutionLogs.totalTokens, + executionData: workflowExecutionLogs.executionData, + cost: workflowExecutionLogs.cost, files: workflowExecutionLogs.files, - metadata: workflowExecutionLogs.metadata, createdAt: workflowExecutionLogs.createdAt, }) .from(workflowExecutionLogs) diff --git a/apps/sim/app/api/logs/route.ts b/apps/sim/app/api/logs/route.ts index e559ca3e06e..7ef3460a68d 100644 --- a/apps/sim/app/api/logs/route.ts +++ b/apps/sim/app/api/logs/route.ts @@ -1,4 +1,4 @@ -import { and, desc, eq, gte, inArray, lte, or, type SQL, sql } from 'drizzle-orm' +import { and, desc, eq, gte, inArray, lte, type SQL, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' @@ -44,8 +44,7 @@ function extractBlockExecutionsFromTraceSpans(traceSpans: any[]): any[] { export const revalidate = 0 const QueryParamsSchema = z.object({ - includeWorkflow: z.coerce.boolean().optional().default(false), - includeBlocks: z.coerce.boolean().optional().default(false), + details: z.enum(['basic', 'full']).optional().default('basic'), limit: z.coerce.number().optional().default(100), offset: z.coerce.number().optional().default(0), level: z.string().optional(), @@ -81,20 +80,12 @@ export async function GET(request: NextRequest) { executionId: workflowExecutionLogs.executionId, stateSnapshotId: workflowExecutionLogs.stateSnapshotId, level: workflowExecutionLogs.level, - message: workflowExecutionLogs.message, trigger: workflowExecutionLogs.trigger, startedAt: workflowExecutionLogs.startedAt, endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, - blockCount: workflowExecutionLogs.blockCount, - successCount: workflowExecutionLogs.successCount, - errorCount: workflowExecutionLogs.errorCount, - skippedCount: workflowExecutionLogs.skippedCount, - totalCost: workflowExecutionLogs.totalCost, - totalInputCost: workflowExecutionLogs.totalInputCost, - totalOutputCost: workflowExecutionLogs.totalOutputCost, - totalTokens: workflowExecutionLogs.totalTokens, - metadata: workflowExecutionLogs.metadata, + executionData: workflowExecutionLogs.executionData, + cost: workflowExecutionLogs.cost, files: workflowExecutionLogs.files, createdAt: workflowExecutionLogs.createdAt, workflowName: workflow.name, @@ -163,13 +154,8 @@ export async function GET(request: NextRequest) { // Filter by search query if (params.search) { const searchTerm = `%${params.search}%` - conditions = and( - conditions, - or( - sql`${workflowExecutionLogs.message} ILIKE ${searchTerm}`, - sql`${workflowExecutionLogs.executionId} ILIKE ${searchTerm}` - ) - ) + // With message removed, restrict search to executionId only + conditions = and(conditions, sql`${workflowExecutionLogs.executionId} ILIKE ${searchTerm}`) } // Execute the query using the optimized join @@ -290,31 +276,20 @@ export async function GET(request: NextRequest) { const enhancedLogs = logs.map((log) => { const blockExecutions = blockExecutionsByExecution[log.executionId] || [] - // Use stored trace spans from metadata if available, otherwise create from block executions - const storedTraceSpans = (log.metadata as any)?.traceSpans + // Use stored trace spans if available, otherwise create from block executions + const storedTraceSpans = (log.executionData as any)?.traceSpans const traceSpans = storedTraceSpans && Array.isArray(storedTraceSpans) && storedTraceSpans.length > 0 ? storedTraceSpans : createTraceSpans(blockExecutions) - // Use extracted cost summary if available, otherwise use stored values + // Prefer stored cost JSON; otherwise synthesize from blocks const costSummary = - blockExecutions.length > 0 - ? extractCostSummary(blockExecutions) - : { - input: Number(log.totalInputCost) || 0, - output: Number(log.totalOutputCost) || 0, - total: Number(log.totalCost) || 0, - tokens: { - total: log.totalTokens || 0, - prompt: (log.metadata as any)?.tokenBreakdown?.prompt || 0, - completion: (log.metadata as any)?.tokenBreakdown?.completion || 0, - }, - models: (log.metadata as any)?.models || {}, - } + log.cost && Object.keys(log.cost as any).length > 0 + ? (log.cost as any) + : extractCostSummary(blockExecutions) - // Build workflow object from joined data - const workflow = { + const workflowSummary = { id: log.workflowId, name: log.workflowName, description: log.workflowDescription, @@ -329,67 +304,28 @@ export async function GET(request: NextRequest) { return { id: log.id, workflowId: log.workflowId, - executionId: log.executionId, + executionId: params.details === 'full' ? log.executionId : undefined, level: log.level, - message: log.message, duration: log.totalDurationMs ? `${log.totalDurationMs}ms` : null, trigger: log.trigger, createdAt: log.startedAt.toISOString(), - files: log.files || undefined, - workflow: params.includeWorkflow ? workflow : undefined, - metadata: { - totalDuration: log.totalDurationMs, - cost: costSummary, - blockStats: { - total: log.blockCount, - success: log.successCount, - error: log.errorCount, - skipped: log.skippedCount, - }, - traceSpans, - blockExecutions, - enhanced: true, - }, + files: params.details === 'full' ? log.files || undefined : undefined, + workflow: workflowSummary, + executionData: + params.details === 'full' + ? { + totalDuration: log.totalDurationMs, + traceSpans, + blockExecutions, + enhanced: true, + } + : undefined, + cost: + params.details === 'full' + ? (costSummary as any) + : { total: (costSummary as any)?.total || 0 }, } }) - - // Include block execution data if requested - if (params.includeBlocks) { - // Block executions are now extracted from stored trace spans in metadata - const blockLogsByExecution: Record = {} - - logs.forEach((log) => { - const storedTraceSpans = (log.metadata as any)?.traceSpans - if (storedTraceSpans && Array.isArray(storedTraceSpans)) { - blockLogsByExecution[log.executionId] = - extractBlockExecutionsFromTraceSpans(storedTraceSpans) - } else { - blockLogsByExecution[log.executionId] = [] - } - }) - - // Add block logs to metadata - const logsWithBlocks = enhancedLogs.map((log) => ({ - ...log, - metadata: { - ...log.metadata, - blockExecutions: blockLogsByExecution[log.executionId] || [], - }, - })) - - return NextResponse.json( - { - data: logsWithBlocks, - total: Number(count), - page: Math.floor(params.offset / params.limit) + 1, - pageSize: params.limit, - totalPages: Math.ceil(Number(count) / params.limit), - }, - { status: 200 } - ) - } - - // Return basic logs return NextResponse.json( { data: enhancedLogs, diff --git a/apps/sim/app/api/templates/[id]/use/route.ts b/apps/sim/app/api/templates/[id]/use/route.ts index 69de688be39..7587ada95a1 100644 --- a/apps/sim/app/api/templates/[id]/use/route.ts +++ b/apps/sim/app/api/templates/[id]/use/route.ts @@ -80,7 +80,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ workspaceId: workspaceId, name: `${templateData.name} (copy)`, description: templateData.description, - state: templateData.state, color: templateData.color, userId: session.user.id, createdAt: now, @@ -158,9 +157,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ })) } - // Update the workflow with the corrected state - await tx.update(workflow).set({ state: updatedState }).where(eq(workflow.id, newWorkflowId)) - // Insert blocks and edges if (blockEntries.length > 0) { await tx.insert(workflowBlocks).values(blockEntries) diff --git a/apps/sim/app/api/workflows/[id]/duplicate/route.ts b/apps/sim/app/api/workflows/[id]/duplicate/route.ts index c4af8963304..abd96baae93 100644 --- a/apps/sim/app/api/workflows/[id]/duplicate/route.ts +++ b/apps/sim/app/api/workflows/[id]/duplicate/route.ts @@ -7,7 +7,7 @@ import { createLogger } from '@/lib/logs/console/logger' import { getUserEntityPermissions } from '@/lib/permissions/utils' import { db } from '@/db' import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema' -import type { LoopConfig, ParallelConfig, WorkflowState } from '@/stores/workflows/workflow/types' +import type { LoopConfig, ParallelConfig } from '@/stores/workflows/workflow/types' const logger = createLogger('WorkflowDuplicateAPI') @@ -90,7 +90,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: folderId: folderId || source.folderId, name, description: description || source.description, - state: source.state, // We'll update this later with new block IDs color: color || source.color, lastSynced: now, createdAt: now, @@ -112,9 +111,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: // Create a mapping from old block IDs to new block IDs const blockIdMapping = new Map() - // Initialize state for updating with new block IDs - let updatedState: WorkflowState = source.state as WorkflowState - if (sourceBlocks.length > 0) { // First pass: Create all block ID mappings sourceBlocks.forEach((block) => { @@ -265,86 +261,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: ) } - // Update the JSON state to use new block IDs - if (updatedState && typeof updatedState === 'object') { - updatedState = JSON.parse(JSON.stringify(updatedState)) as WorkflowState - - // Update blocks object keys - if (updatedState.blocks && typeof updatedState.blocks === 'object') { - const newBlocks = {} as Record - for (const [oldId, blockData] of Object.entries(updatedState.blocks)) { - const newId = blockIdMapping.get(oldId) || oldId - newBlocks[newId] = { - ...blockData, - id: newId, - // Update data.parentId and extent in the JSON state as well - data: (() => { - const block = blockData as any - if (block.data && typeof block.data === 'object' && block.data.parentId) { - return { - ...block.data, - parentId: blockIdMapping.get(block.data.parentId) || block.data.parentId, - extent: 'parent', // Ensure extent is set for child blocks - } - } - return block.data - })(), - } - } - updatedState.blocks = newBlocks - } - - // Update edges array - if (updatedState.edges && Array.isArray(updatedState.edges)) { - updatedState.edges = updatedState.edges.map((edge) => ({ - ...edge, - id: crypto.randomUUID(), - source: blockIdMapping.get(edge.source) || edge.source, - target: blockIdMapping.get(edge.target) || edge.target, - })) - } - - // Update loops and parallels if they exist - if (updatedState.loops && typeof updatedState.loops === 'object') { - const newLoops = {} as Record - for (const [oldId, loopData] of Object.entries(updatedState.loops)) { - const newId = blockIdMapping.get(oldId) || oldId - const loopConfig = loopData as any - newLoops[newId] = { - ...loopConfig, - id: newId, - // Update node references in loop config - nodes: loopConfig.nodes - ? loopConfig.nodes.map((nodeId: string) => blockIdMapping.get(nodeId) || nodeId) - : [], - } - } - updatedState.loops = newLoops - } - - if (updatedState.parallels && typeof updatedState.parallels === 'object') { - const newParallels = {} as Record - for (const [oldId, parallelData] of Object.entries(updatedState.parallels)) { - const newId = blockIdMapping.get(oldId) || oldId - const parallelConfig = parallelData as any - newParallels[newId] = { - ...parallelConfig, - id: newId, - // Update node references in parallel config - nodes: parallelConfig.nodes - ? parallelConfig.nodes.map((nodeId: string) => blockIdMapping.get(nodeId) || nodeId) - : [], - } - } - updatedState.parallels = newParallels - } - } - - // Update the workflow state with the new block IDs + // Update the workflow timestamp await tx .update(workflow) .set({ - state: updatedState, updatedAt: now, }) .where(eq(workflow.id, newWorkflowId)) diff --git a/apps/sim/app/api/workflows/[id]/route.test.ts b/apps/sim/app/api/workflows/[id]/route.test.ts index 930c3fb0d61..4ed6bf1140d 100644 --- a/apps/sim/app/api/workflows/[id]/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/route.test.ts @@ -89,7 +89,14 @@ describe('Workflow By ID API Route', () => { userId: 'user-123', name: 'Test Workflow', workspaceId: null, - state: { blocks: {}, edges: [] }, + } + + const mockNormalizedData = { + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + isFromNormalizedTables: true, } vi.doMock('@/lib/auth', () => ({ @@ -110,6 +117,10 @@ describe('Workflow By ID API Route', () => { }, })) + vi.doMock('@/lib/workflows/db-helpers', () => ({ + loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue(mockNormalizedData), + })) + const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123') const params = Promise.resolve({ id: 'workflow-123' }) @@ -127,7 +138,14 @@ describe('Workflow By ID API Route', () => { userId: 'other-user', name: 'Test Workflow', workspaceId: 'workspace-456', - state: { blocks: {}, edges: [] }, + } + + const mockNormalizedData = { + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + isFromNormalizedTables: true, } vi.doMock('@/lib/auth', () => ({ @@ -148,6 +166,10 @@ describe('Workflow By ID API Route', () => { }, })) + vi.doMock('@/lib/workflows/db-helpers', () => ({ + loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue(mockNormalizedData), + })) + vi.doMock('@/lib/permissions/utils', () => ({ getUserEntityPermissions: vi.fn().mockResolvedValue('read'), hasAdminPermission: vi.fn().mockResolvedValue(false), @@ -170,7 +192,6 @@ describe('Workflow By ID API Route', () => { userId: 'other-user', name: 'Test Workflow', workspaceId: 'workspace-456', - state: { blocks: {}, edges: [] }, } vi.doMock('@/lib/auth', () => ({ @@ -213,7 +234,6 @@ describe('Workflow By ID API Route', () => { userId: 'user-123', name: 'Test Workflow', workspaceId: null, - state: { blocks: {}, edges: [] }, } const mockNormalizedData = { diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index 269e8de4823..0a782dc9b30 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -120,8 +120,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ logger.debug(`[${requestId}] Attempting to load workflow ${workflowId} from normalized tables`) const normalizedData = await loadWorkflowFromNormalizedTables(workflowId) - const finalWorkflowData = { ...workflowData } - if (normalizedData) { logger.debug(`[${requestId}] Found normalized data for workflow ${workflowId}:`, { blocksCount: Object.keys(normalizedData.blocks).length, @@ -131,38 +129,31 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ loops: normalizedData.loops, }) - // Use normalized table data - reconstruct complete state object - // First get any existing state properties, then override with normalized data - const existingState = - workflowData.state && typeof workflowData.state === 'object' ? workflowData.state : {} - - finalWorkflowData.state = { - // Default values for expected properties - deploymentStatuses: {}, - hasActiveWebhook: false, - // Preserve any existing state properties - ...existingState, - // Override with normalized data (this takes precedence) - blocks: normalizedData.blocks, - edges: normalizedData.edges, - loops: normalizedData.loops, - parallels: normalizedData.parallels, - lastSaved: Date.now(), - isDeployed: workflowData.isDeployed || false, - deployedAt: workflowData.deployedAt, + // Construct response object with workflow data and state from normalized tables + const finalWorkflowData = { + ...workflowData, + state: { + // Default values for expected properties + deploymentStatuses: {}, + hasActiveWebhook: false, + // Data from normalized tables + blocks: normalizedData.blocks, + edges: normalizedData.edges, + loops: normalizedData.loops, + parallels: normalizedData.parallels, + lastSaved: Date.now(), + isDeployed: workflowData.isDeployed || false, + deployedAt: workflowData.deployedAt, + }, } - logger.info(`[${requestId}] Loaded workflow ${workflowId} from normalized tables`) - } else { - // Fallback to JSON blob - logger.info( - `[${requestId}] Using JSON blob for workflow ${workflowId} - no normalized data found` - ) - } - const elapsed = Date.now() - startTime - logger.info(`[${requestId}] Successfully fetched workflow ${workflowId} in ${elapsed}ms`) + logger.info(`[${requestId}] Loaded workflow ${workflowId} from normalized tables`) + const elapsed = Date.now() - startTime + logger.info(`[${requestId}] Successfully fetched workflow ${workflowId} in ${elapsed}ms`) - return NextResponse.json({ data: finalWorkflowData }, { status: 200 }) + return NextResponse.json({ data: finalWorkflowData }, { status: 200 }) + } + return NextResponse.json({ error: 'Workflow has no normalized data' }, { status: 400 }) } catch (error: any) { const elapsed = Date.now() - startTime logger.error(`[${requestId}] Error fetching workflow ${workflowId} after ${elapsed}ms`, error) diff --git a/apps/sim/app/api/workflows/[id]/state/route.ts b/apps/sim/app/api/workflows/[id]/state/route.ts index b181d88ecf8..a0afac7b24c 100644 --- a/apps/sim/app/api/workflows/[id]/state/route.ts +++ b/apps/sim/app/api/workflows/[id]/state/route.ts @@ -220,7 +220,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ .set({ lastSynced: new Date(), updatedAt: new Date(), - state: saveResult.jsonBlob, // Also update JSON blob for backward compatibility }) .where(eq(workflow.id, workflowId)) diff --git a/apps/sim/app/api/workflows/[id]/yaml/route.ts b/apps/sim/app/api/workflows/[id]/yaml/route.ts index bf4c42765f8..86f7dc091f8 100644 --- a/apps/sim/app/api/workflows/[id]/yaml/route.ts +++ b/apps/sim/app/api/workflows/[id]/yaml/route.ts @@ -18,14 +18,12 @@ import { db } from '@/db' import { workflowCheckpoints, workflow as workflowTable } from '@/db/schema' import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils' -// Sim Agent API configuration const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT export const dynamic = 'force-dynamic' const logger = createLogger('WorkflowYamlAPI') -// Request schema for YAML workflow operations const YamlWorkflowRequestSchema = z.object({ yamlContent: z.string().min(1, 'YAML content is required'), description: z.string().optional(), @@ -647,14 +645,13 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ .set({ lastSynced: new Date(), updatedAt: new Date(), - state: saveResult.jsonBlob, }) .where(eq(workflowTable.id, workflowId)) // Notify socket server for real-time collaboration (for copilot and editor) if (source === 'copilot' || source === 'editor') { try { - const socketUrl = process.env.SOCKET_URL || 'http://localhost:3002' + const socketUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002' await fetch(`${socketUrl}/api/copilot-workflow-edit`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts index 3a5e5465fc5..f10f50b246f 100644 --- a/apps/sim/app/api/workflows/route.ts +++ b/apps/sim/app/api/workflows/route.ts @@ -151,7 +151,6 @@ export async function POST(req: NextRequest) { folderId: folderId || null, name, description, - state: initialState, color, lastSynced: now, createdAt: now, diff --git a/apps/sim/app/api/workflows/yaml/export/route.ts b/apps/sim/app/api/workflows/yaml/export/route.ts index f50f71317f1..3c1cc9bc288 100644 --- a/apps/sim/app/api/workflows/yaml/export/route.ts +++ b/apps/sim/app/api/workflows/yaml/export/route.ts @@ -85,14 +85,10 @@ export async function GET(request: NextRequest) { edgesCount: normalizedData.edges.length, }) - // Use normalized table data - reconstruct complete state object - const existingState = - workflowData.state && typeof workflowData.state === 'object' ? workflowData.state : {} - + // Use normalized table data - construct state from normalized tables workflowState = { deploymentStatuses: {}, hasActiveWebhook: false, - ...existingState, blocks: normalizedData.blocks, edges: normalizedData.edges, loops: normalizedData.loops, @@ -116,33 +112,10 @@ export async function GET(request: NextRequest) { logger.info(`[${requestId}] Loaded workflow ${workflowId} from normalized tables`) } else { - // Fallback to JSON blob - logger.info( - `[${requestId}] Using JSON blob for workflow ${workflowId} - no normalized data found` + return NextResponse.json( + { success: false, error: 'Workflow has no normalized data' }, + { status: 400 } ) - - if (!workflowData.state || typeof workflowData.state !== 'object') { - return NextResponse.json( - { success: false, error: 'Workflow has no valid state data' }, - { status: 400 } - ) - } - - workflowState = workflowData.state as any - - // Extract subblock values from JSON blob state - if (workflowState.blocks) { - Object.entries(workflowState.blocks).forEach(([blockId, block]: [string, any]) => { - subBlockValues[blockId] = {} - if (block.subBlocks) { - Object.entries(block.subBlocks).forEach(([subBlockId, subBlock]: [string, any]) => { - if (subBlock && typeof subBlock === 'object' && 'value' in subBlock) { - subBlockValues[blockId][subBlockId] = subBlock.value - } - }) - } - }) - } } // Gather block registry and utilities for sim-agent diff --git a/apps/sim/app/api/workspaces/route.ts b/apps/sim/app/api/workspaces/route.ts index ce15c76218d..b184ca6e864 100644 --- a/apps/sim/app/api/workspaces/route.ts +++ b/apps/sim/app/api/workspaces/route.ts @@ -113,64 +113,6 @@ async function createWorkspace(userId: string, name: string) { // Create initial workflow for the workspace with start block const starterId = crypto.randomUUID() - const initialState = { - blocks: { - [starterId]: { - id: starterId, - type: 'starter', - name: 'Start', - position: { x: 100, y: 100 }, - subBlocks: { - startWorkflow: { - id: 'startWorkflow', - type: 'dropdown', - value: 'manual', - }, - webhookPath: { - id: 'webhookPath', - type: 'short-input', - value: '', - }, - webhookSecret: { - id: 'webhookSecret', - type: 'short-input', - value: '', - }, - scheduleType: { - id: 'scheduleType', - type: 'dropdown', - value: 'daily', - }, - minutesInterval: { - id: 'minutesInterval', - type: 'short-input', - value: '', - }, - minutesStartingAt: { - id: 'minutesStartingAt', - type: 'short-input', - value: '', - }, - }, - outputs: { - response: { type: { input: 'any' } }, - }, - enabled: true, - horizontalHandles: true, - isWide: false, - advancedMode: false, - height: 95, - }, - }, - edges: [], - subflows: {}, - variables: {}, - metadata: { - version: '1.0.0', - createdAt: now.toISOString(), - updatedAt: now.toISOString(), - }, - } // Create the workflow await tx.insert(workflow).values({ @@ -180,7 +122,6 @@ async function createWorkspace(userId: string, name: string) { folderId: null, name: 'default-agent', description: 'Your first workflow - start building here!', - state: initialState, color: '#3972F6', lastSynced: now, createdAt: now, diff --git a/apps/sim/app/globals.css b/apps/sim/app/globals.css index 84891c05535..24dbca36cb9 100644 --- a/apps/sim/app/globals.css +++ b/apps/sim/app/globals.css @@ -14,7 +14,8 @@ } .workflow-container .react-flow__node-loopNode, -.workflow-container .react-flow__node-parallelNode { +.workflow-container .react-flow__node-parallelNode, +.workflow-container .react-flow__node-subflowNode { z-index: -1 !important; } diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx index 69c5800f6ae..214772865d2 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useMemo, useRef, useState } from 'react' -import { ChevronDown, ChevronUp, Eye, X } from 'lucide-react' +import { ChevronDown, ChevronUp, Eye, Loader2, X } from 'lucide-react' import { Button } from '@/components/ui/button' import { CopyButton } from '@/components/ui/copy-button' import { ScrollArea } from '@/components/ui/scroll-area' @@ -209,29 +209,30 @@ export function Sidebar({ } }, [log?.id]) + const isLoadingDetails = useMemo(() => { + if (!log) return false + // Only show while we expect details to arrive (has executionId) + if (!log.executionId) return false + const hasEnhanced = !!log.executionData?.enhanced + const hasAnyDetails = hasEnhanced || !!log.cost || Array.isArray(log.executionData?.traceSpans) + return !hasAnyDetails + }, [log]) + const formattedContent = useMemo(() => { if (!log) return null let blockInput: Record | undefined - if (log.metadata?.blockInput) { - blockInput = log.metadata.blockInput - } else if (log.metadata?.traceSpans) { - const blockIdMatch = log.message.match(/Block .+?(\d+)/i) - const blockId = blockIdMatch ? blockIdMatch[1] : null - - if (blockId) { - const matchingSpan = log.metadata.traceSpans.find( - (span) => span.blockId === blockId || span.name.includes(`Block ${blockId}`) - ) - - if (matchingSpan?.input) { - blockInput = matchingSpan.input - } + if (log.executionData?.blockInput) { + blockInput = log.executionData.blockInput + } else if (log.executionData?.traceSpans) { + const firstSpanWithInput = log.executionData.traceSpans.find((s) => s.input) + if (firstSpanWithInput?.input) { + blockInput = firstSpanWithInput.input as any } } - return formatJsonContent(log.message, blockInput) + return null }, [log]) useEffect(() => { @@ -243,22 +244,16 @@ export function Sidebar({ // Determine if this is a workflow execution log const isWorkflowExecutionLog = useMemo(() => { if (!log) return false - // Check if message contains workflow execution phrases (success or failure) return ( - log.message.toLowerCase().includes('workflow executed') || - log.message.toLowerCase().includes('execution completed') || - log.message.toLowerCase().includes('workflow execution failed') || - log.message.toLowerCase().includes('execution failed') || - (log.trigger === 'manual' && log.duration) || - // Also check if we have enhanced logging metadata with trace spans - (log.metadata?.enhanced && log.metadata?.traceSpans) + (log.trigger === 'manual' && !!log.duration) || + (log.executionData?.enhanced && log.executionData?.traceSpans) ) }, [log]) // Helper to determine if we have cost information to display // All workflow executions now have cost info (base charge + any model costs) const hasCostInfo = useMemo(() => { - return isWorkflowExecutionLog && log?.metadata?.cost + return isWorkflowExecutionLog && log?.cost }, [log, isWorkflowExecutionLog]) const isWorkflowWithCost = useMemo(() => { @@ -490,6 +485,14 @@ export function Sidebar({ )} + {/* Suspense while details load (positioned after summary fields) */} + {isLoadingDetails && ( +
+ + Loading details… +
+ )} + {/* Files */} {log.files && log.files.length > 0 && (
@@ -541,19 +544,15 @@ export function Sidebar({
)} - {/* Message Content */} -
-

Message

-
{formattedContent}
-
+ {/* end suspense */} {/* Trace Spans (if available and this is a workflow execution log) */} - {isWorkflowExecutionLog && log.metadata?.traceSpans && ( + {isWorkflowExecutionLog && log.executionData?.traceSpans && (
@@ -561,11 +560,11 @@ export function Sidebar({ )} {/* Tool Calls (if available) */} - {log.metadata?.toolCalls && log.metadata.toolCalls.length > 0 && ( + {log.executionData?.toolCalls && log.executionData.toolCalls.length > 0 && (

Tool Calls

- +
)} @@ -584,86 +583,80 @@ export function Sidebar({
Model Input: - - {formatCost(log.metadata?.cost?.input || 0)} - + {formatCost(log.cost?.input || 0)}
Model Output: - - {formatCost(log.metadata?.cost?.output || 0)} - + {formatCost(log.cost?.output || 0)}
Total: - {formatCost(log.metadata?.cost?.total || 0)} + {formatCost(log.cost?.total || 0)}
Tokens: - {log.metadata?.cost?.tokens?.prompt || 0} in /{' '} - {log.metadata?.cost?.tokens?.completion || 0} out + {log.cost?.tokens?.prompt || 0} in / {log.cost?.tokens?.completion || 0}{' '} + out
{/* Models Breakdown */} - {log.metadata?.cost?.models && - Object.keys(log.metadata?.cost?.models).length > 0 && ( -
- - - {isModelsExpanded && ( -
- {Object.entries(log.metadata?.cost?.models || {}).map( - ([model, cost]: [string, any]) => ( -
-
{model}
-
-
- Input: - {formatCost(cost.input || 0)} -
-
- Output: - {formatCost(cost.output || 0)} -
-
- Total: - - {formatCost(cost.total || 0)} - -
-
- Tokens: - - {cost.tokens?.prompt || 0} in /{' '} - {cost.tokens?.completion || 0} out - -
+ {log.cost?.models && Object.keys(log.cost?.models).length > 0 && ( +
+ + + {isModelsExpanded && ( +
+ {Object.entries(log.cost?.models || {}).map( + ([model, cost]: [string, any]) => ( +
+
{model}
+
+
+ Input: + {formatCost(cost.input || 0)} +
+
+ Output: + {formatCost(cost.output || 0)} +
+
+ Total: + + {formatCost(cost.total || 0)} + +
+
+ Tokens: + + {cost.tokens?.prompt || 0} in /{' '} + {cost.tokens?.completion || 0} out +
- ) - )} -
- )} -
- )} +
+ ) + )} +
+ )} +
+ )} {isWorkflowWithCost && (
@@ -688,7 +681,7 @@ export function Sidebar({ executionId={log.executionId} workflowName={log.workflow?.name} trigger={log.trigger || undefined} - traceSpans={log.metadata?.traceSpans} + traceSpans={log.executionData?.traceSpans} isOpen={isFrozenCanvasOpen} onClose={() => setIsFrozenCanvasOpen(false)} /> diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index bedf3111013..68a46a7d9ee 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -85,6 +85,10 @@ export default function Logs() { const [selectedLog, setSelectedLog] = useState(null) const [selectedLogIndex, setSelectedLogIndex] = useState(-1) const [isSidebarOpen, setIsSidebarOpen] = useState(false) + const [isDetailsLoading, setIsDetailsLoading] = useState(false) + const detailsCacheRef = useRef>(new Map()) + const detailsAbortRef = useRef(null) + const currentDetailsIdRef = useRef(null) const selectedRowRef = useRef(null) const loaderRef = useRef(null) const scrollContainerRef = useRef(null) @@ -116,13 +120,122 @@ export default function Logs() { const index = logs.findIndex((l) => l.id === log.id) setSelectedLogIndex(index) setIsSidebarOpen(true) + setIsDetailsLoading(true) + + // Fetch details for current, previous, and next concurrently with cache + const currentId = log.id + const prevId = index > 0 ? logs[index - 1]?.id : undefined + const nextId = index < logs.length - 1 ? logs[index + 1]?.id : undefined + + // Abort any previous details fetch batch + if (detailsAbortRef.current) { + try { + detailsAbortRef.current.abort() + } catch { + /* no-op */ + } + } + const controller = new AbortController() + detailsAbortRef.current = controller + currentDetailsIdRef.current = currentId + + const idsToFetch: Array<{ id: string; merge: boolean }> = [] + const cachedCurrent = currentId ? detailsCacheRef.current.get(currentId) : undefined + if (currentId && !cachedCurrent) idsToFetch.push({ id: currentId, merge: true }) + if (prevId && !detailsCacheRef.current.has(prevId)) + idsToFetch.push({ id: prevId, merge: false }) + if (nextId && !detailsCacheRef.current.has(nextId)) + idsToFetch.push({ id: nextId, merge: false }) + + // Merge cached current immediately + if (cachedCurrent) { + setSelectedLog((prev) => + prev && prev.id === currentId + ? ({ ...(prev as any), ...(cachedCurrent as any) } as any) + : prev + ) + setIsDetailsLoading(false) + } + if (idsToFetch.length === 0) return + + Promise.all( + idsToFetch.map(async ({ id, merge }) => { + try { + const res = await fetch(`/api/logs/by-id/${id}`, { signal: controller.signal }) + if (!res.ok) return + const body = await res.json() + const detailed = body?.data + if (detailed) { + detailsCacheRef.current.set(id, detailed) + if (merge && id === currentId) { + setSelectedLog((prev) => + prev && prev.id === id ? ({ ...(prev as any), ...(detailed as any) } as any) : prev + ) + if (currentDetailsIdRef.current === id) setIsDetailsLoading(false) + } + } + } catch (e: any) { + if (e?.name === 'AbortError') return + } + }) + ).catch(() => {}) } const handleNavigateNext = useCallback(() => { if (selectedLogIndex < logs.length - 1) { const nextIndex = selectedLogIndex + 1 setSelectedLogIndex(nextIndex) - setSelectedLog(logs[nextIndex]) + const nextLog = logs[nextIndex] + setSelectedLog(nextLog) + // Abort any previous details fetch batch + if (detailsAbortRef.current) { + try { + detailsAbortRef.current.abort() + } catch { + /* no-op */ + } + } + const controller = new AbortController() + detailsAbortRef.current = controller + + const cached = detailsCacheRef.current.get(nextLog.id) + if (cached) { + setSelectedLog((prev) => + prev && prev.id === nextLog.id ? ({ ...(prev as any), ...(cached as any) } as any) : prev + ) + } else { + const prevId = nextIndex > 0 ? logs[nextIndex - 1]?.id : undefined + const afterId = nextIndex < logs.length - 1 ? logs[nextIndex + 1]?.id : undefined + const idsToFetch: Array<{ id: string; merge: boolean }> = [] + if (nextLog.id && !detailsCacheRef.current.has(nextLog.id)) + idsToFetch.push({ id: nextLog.id, merge: true }) + if (prevId && !detailsCacheRef.current.has(prevId)) + idsToFetch.push({ id: prevId, merge: false }) + if (afterId && !detailsCacheRef.current.has(afterId)) + idsToFetch.push({ id: afterId, merge: false }) + Promise.all( + idsToFetch.map(async ({ id, merge }) => { + try { + const res = await fetch(`/api/logs/by-id/${id}`, { signal: controller.signal }) + if (!res.ok) return + const body = await res.json() + const detailed = body?.data + if (detailed) { + detailsCacheRef.current.set(id, detailed) + if (merge && id === nextLog.id) { + setSelectedLog((prev) => + prev && prev.id === id + ? ({ ...(prev as any), ...(detailed as any) } as any) + : prev + ) + } + } + } catch (e: any) { + if (e?.name === 'AbortError') return + } + }) + ).catch(() => {}) + } } }, [selectedLogIndex, logs]) @@ -130,7 +243,57 @@ export default function Logs() { if (selectedLogIndex > 0) { const prevIndex = selectedLogIndex - 1 setSelectedLogIndex(prevIndex) - setSelectedLog(logs[prevIndex]) + const prevLog = logs[prevIndex] + setSelectedLog(prevLog) + // Abort any previous details fetch batch + if (detailsAbortRef.current) { + try { + detailsAbortRef.current.abort() + } catch { + /* no-op */ + } + } + const controller = new AbortController() + detailsAbortRef.current = controller + + const cached = detailsCacheRef.current.get(prevLog.id) + if (cached) { + setSelectedLog((prev) => + prev && prev.id === prevLog.id ? ({ ...(prev as any), ...(cached as any) } as any) : prev + ) + } else { + const beforeId = prevIndex > 0 ? logs[prevIndex - 1]?.id : undefined + const afterId = prevIndex < logs.length - 1 ? logs[prevIndex + 1]?.id : undefined + const idsToFetch: Array<{ id: string; merge: boolean }> = [] + if (prevLog.id && !detailsCacheRef.current.has(prevLog.id)) + idsToFetch.push({ id: prevLog.id, merge: true }) + if (beforeId && !detailsCacheRef.current.has(beforeId)) + idsToFetch.push({ id: beforeId, merge: false }) + if (afterId && !detailsCacheRef.current.has(afterId)) + idsToFetch.push({ id: afterId, merge: false }) + Promise.all( + idsToFetch.map(async ({ id, merge }) => { + try { + const res = await fetch(`/api/logs/by-id/${id}`, { signal: controller.signal }) + if (!res.ok) return + const body = await res.json() + const detailed = body?.data + if (detailed) { + detailsCacheRef.current.set(id, detailed) + if (merge && id === prevLog.id) { + setSelectedLog((prev) => + prev && prev.id === id + ? ({ ...(prev as any), ...(detailed as any) } as any) + : prev + ) + } + } + } catch (e: any) { + if (e?.name === 'AbortError') return + } + }) + ).catch(() => {}) + } } }, [selectedLogIndex, logs]) @@ -160,7 +323,7 @@ export default function Logs() { // Get fresh query params by calling buildQueryParams from store const { buildQueryParams: getCurrentQueryParams } = useFilterStore.getState() const queryParams = getCurrentQueryParams(pageNum, LOGS_PER_PAGE) - const response = await fetch(`/api/logs?${queryParams}`) + const response = await fetch(`/api/logs?${queryParams}&details=basic`) if (!response.ok) { throw new Error(`Error fetching logs: ${response.statusText}`) @@ -262,7 +425,7 @@ export default function Logs() { // Build query params inline to avoid dependency issues const params = new URLSearchParams() - params.set('includeWorkflow', 'true') + params.set('details', 'basic') params.set('limit', LOGS_PER_PAGE.toString()) params.set('offset', '0') // Always start from page 1 params.set('workspaceId', workspaceId) @@ -482,7 +645,7 @@ export default function Logs() { {/* Header */}
-
+
Time
@@ -493,14 +656,12 @@ export default function Logs() { Workflow
- ID + Cost
Trigger
-
- Message -
+
Duration
@@ -547,7 +708,7 @@ export default function Logs() { }`} onClick={() => handleLogClick(log)} > -
+
{/* Time */}
@@ -584,10 +745,12 @@ export default function Logs() {
- {/* ID */} + {/* Cost */}
- #{log.id.slice(-4)} + {typeof (log as any)?.cost?.total === 'number' + ? `$${((log as any).cost.total as number).toFixed(4)}` + : 'β€”'}
@@ -614,11 +777,6 @@ export default function Logs() { )}
- {/* Message */} -
-
{log.message}
-
- {/* Duration */}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx index 166e0195b6c..e95b427dbb3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx @@ -341,10 +341,11 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) { * Handle deleting the current workflow */ const handleDeleteWorkflow = () => { - if (!activeWorkflowId || !userPermissions.canEdit) return + const currentWorkflowId = params.workflowId as string + if (!currentWorkflowId || !userPermissions.canEdit) return const sidebarWorkflows = getSidebarOrderedWorkflows() - const currentIndex = sidebarWorkflows.findIndex((w) => w.id === activeWorkflowId) + const currentIndex = sidebarWorkflows.findIndex((w) => w.id === currentWorkflowId) // Find next workflow: try next, then previous let nextWorkflowId: string | null = null @@ -363,8 +364,8 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) { router.push(`/workspace/${workspaceId}`) } - // Remove the workflow from the registry - useWorkflowRegistry.getState().removeWorkflow(activeWorkflowId) + // Remove the workflow from the registry using the URL parameter + useWorkflowRegistry.getState().removeWorkflow(currentWorkflowId) } // Helper function to open subscription settings diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls.tsx index 1af3380b8bc..1943bb9938b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls.tsx @@ -196,22 +196,17 @@ export function DiffControls() { logger.warn('Failed to clear preview YAML:', error) }) - // Accept changes with automatic backup and rollback on failure - await acceptChanges() + // Accept changes without blocking the UI; errors will be logged by the store handler + acceptChanges().catch((error) => { + logger.error('Failed to accept changes (background):', error) + }) - logger.info('Successfully accepted and saved workflow changes') - // Show success feedback if needed + logger.info('Accept triggered; UI will update optimistically') } catch (error) { logger.error('Failed to accept changes:', error) - // Show error notification to user - // Note: The acceptChanges function has already rolled back the state const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' - - // You could add toast notification here console.error('Workflow update failed:', errorMessage) - - // Optionally show user-facing error dialog alert(`Failed to save workflow changes: ${errorMessage}`) } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/error/index.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/error/index.tsx index dbdd7fa4b24..e0ac6ce7f4c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/error/index.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/error/index.tsx @@ -4,6 +4,8 @@ import { Component, type ReactNode, useEffect } from 'react' import { BotIcon } from 'lucide-react' import { Card } from '@/components/ui/card' import { createLogger } from '@/lib/logs/console/logger' +import { ControlBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar' +import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel' const logger = createLogger('ErrorBoundary') @@ -22,18 +24,32 @@ export function ErrorUI({ fullScreen = false, }: ErrorUIProps) { const containerClass = fullScreen - ? 'flex items-center justify-center w-full h-screen bg-muted/40' - : 'flex items-center justify-center w-full h-full bg-muted/40' + ? 'flex flex-col w-full h-screen bg-muted/40' + : 'flex flex-col w-full h-full bg-muted/40' return (
- -
- + {/* Control bar */} + + + {/* Main content area */} +
+ {/* Error message */} +
+ +
+ +
+

{title}

+

{message}

+
-

{title}

-

{message}

- + + {/* Console panel */} +
+ +
+
) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts index fe1e8a21671..832d40c1604 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts @@ -2,8 +2,7 @@ export { ControlBar } from './control-bar/control-bar' export { ErrorBoundary } from './error/index' export { Panel } from './panel/panel' export { SkeletonLoading } from './skeleton-loading/skeleton-loading' -export { LoopNodeComponent } from './subflows/loop/loop-node' -export { ParallelNodeComponent } from './subflows/parallel/parallel-node' +export { SubflowNodeComponent } from './subflows/subflow-node' export { WandPromptBar } from './wand-prompt-bar/wand-prompt-bar' export { WorkflowBlock } from './workflow-block/workflow-block' export { WorkflowEdge } from './workflow-edge/workflow-edge' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx index 8992dee65df..35950868cb3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx @@ -10,12 +10,12 @@ import { } from 'react' import { ArrowUp, - Boxes, + Brain, BrainCircuit, - BrainCog, Check, FileText, Image, + Infinity as InfinityIcon, Loader2, MessageCircle, Package, @@ -435,14 +435,14 @@ const UserInput = forwardRef( } const getDepthLabel = () => { - if (agentDepth === 0) return 'Lite' + if (agentDepth === 0) return 'Fast' if (agentDepth === 1) return 'Auto' if (agentDepth === 2) return 'Pro' return 'Max' } const getDepthLabelFor = (value: 0 | 1 | 2 | 3) => { - if (value === 0) return 'Lite' + if (value === 0) return 'Fast' if (value === 1) return 'Auto' if (value === 2) return 'Pro' return 'Max' @@ -459,9 +459,9 @@ const UserInput = forwardRef( const getDepthIconFor = (value: 0 | 1 | 2 | 3) => { if (value === 0) return - if (value === 1) return - if (value === 2) return - return + if (value === 1) return + if (value === 2) return + return } const getDepthIcon = () => getDepthIconFor(agentDepth) @@ -654,7 +654,7 @@ const UserInput = forwardRef( )} > - + Auto {agentDepth === 1 && ( @@ -682,7 +682,7 @@ const UserInput = forwardRef( > - Lite + Fast {agentDepth === 0 && ( @@ -709,7 +709,7 @@ const UserInput = forwardRef( )} > - + Pro {agentDepth === 2 && ( @@ -737,7 +737,7 @@ const UserInput = forwardRef( )} > - + Max {agentDepth === 3 && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/components/iteration-badges/iteration-badges.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/components/iteration-badges/iteration-badges.test.tsx new file mode 100644 index 00000000000..9071f639533 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/components/iteration-badges/iteration-badges.test.tsx @@ -0,0 +1,388 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// Mock hooks +const mockCollaborativeUpdates = { + collaborativeUpdateLoopType: vi.fn(), + collaborativeUpdateParallelType: vi.fn(), + collaborativeUpdateIterationCount: vi.fn(), + collaborativeUpdateIterationCollection: vi.fn(), +} + +const mockStoreData = { + loops: {}, + parallels: {}, +} + +vi.mock('@/hooks/use-collaborative-workflow', () => ({ + useCollaborativeWorkflow: () => mockCollaborativeUpdates, +})) + +vi.mock('@/stores/workflows/workflow/store', () => ({ + useWorkflowStore: () => mockStoreData, +})) + +vi.mock('@/components/ui/badge', () => ({ + Badge: ({ children, ...props }: any) => ( +
+ {children} +
+ ), +})) + +vi.mock('@/components/ui/input', () => ({ + Input: (props: any) => , +})) + +vi.mock('@/components/ui/popover', () => ({ + Popover: ({ children }: any) =>
{children}
, + PopoverContent: ({ children }: any) =>
{children}
, + PopoverTrigger: ({ children }: any) =>
{children}
, +})) + +vi.mock('@/components/ui/tag-dropdown', () => ({ + checkTagTrigger: vi.fn(() => ({ show: false })), + TagDropdown: ({ children }: any) =>
{children}
, +})) + +vi.mock('react-simple-code-editor', () => ({ + default: (props: any) =>