From f9a7c4538ec8d3d0d408ea527c8a5d04a03a7557 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 4 Apr 2026 19:18:46 -0700 Subject: [PATCH 1/7] fix(settings): align skeleton loading states with actual page layouts (#3967) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(settings): align skeleton loading states with actual page layouts * lint * fix(settings): address PR feedback — deduplicate skeleton, fix import order, remove inline comments --- .../settings/[section]/settings.tsx | 6 +- .../components/admin/admin-skeleton.tsx | 53 +++++++++++++- .../components/api-keys/api-key-skeleton.tsx | 29 ++++++-- .../settings/components/api-keys/api-keys.tsx | 19 +++-- .../components/byok/byok-skeleton.tsx | 9 ++- .../credentials/credential-skeleton.tsx | 60 +++++++++++----- .../custom-tools/custom-tool-skeleton.tsx | 6 +- .../components/general/general-skeleton.tsx | 4 +- .../components/inbox/inbox-skeleton.tsx | 70 +++++++++++++------ .../integrations/integrations-skeleton.tsx | 23 ++++++ .../mcp-server-skeleton.tsx | 6 +- .../deleted-item-skeleton.tsx | 2 +- .../recently-deleted-skeleton.tsx | 33 +++++++++ 13 files changed, 255 insertions(+), 65 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-skeleton.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted-skeleton.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx index 352a0cfd334..ddda31f2a41 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx @@ -16,7 +16,9 @@ import { CredentialsSkeleton } from '@/app/workspace/[workspaceId]/settings/comp import { CustomToolsSkeleton } from '@/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tool-skeleton' import { GeneralSkeleton } from '@/app/workspace/[workspaceId]/settings/components/general/general-skeleton' import { InboxSkeleton } from '@/app/workspace/[workspaceId]/settings/components/inbox/inbox-skeleton' +import { IntegrationsSkeleton } from '@/app/workspace/[workspaceId]/settings/components/integrations/integrations-skeleton' import { McpSkeleton } from '@/app/workspace/[workspaceId]/settings/components/mcp/mcp-skeleton' +import { RecentlyDeletedSkeleton } from '@/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted-skeleton' import { SkillsSkeleton } from '@/app/workspace/[workspaceId]/settings/components/skills/skill-skeleton' import { WorkflowMcpServersSkeleton } from '@/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers-skeleton' import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation' @@ -52,7 +54,7 @@ const Integrations = dynamic( import('@/app/workspace/[workspaceId]/settings/components/integrations/integrations').then( (m) => m.Integrations ), - { loading: () => } + { loading: () => } ) const Credentials = dynamic( () => @@ -145,7 +147,7 @@ const RecentlyDeleted = dynamic( import( '@/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted' ).then((m) => m.RecentlyDeleted), - { loading: () => } + { loading: () => } ) const AccessControl = dynamic( () => import('@/ee/access-control/components/access-control').then((m) => m.AccessControl), diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton.tsx index 5fc7bfcf968..5115edac650 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton.tsx @@ -1,5 +1,9 @@ import { Skeleton } from '@/components/emcn' +/** + * Skeleton component for admin settings loading state. + * Matches the exact layout structure of the Admin component. + */ export function AdminSkeleton() { return (
@@ -7,6 +11,9 @@ export function AdminSkeleton() {
+ +
+
@@ -14,9 +21,51 @@ export function AdminSkeleton() {
-
+ +
+ +
- + +
+ + +
+ +
+
+ + + + + +
+ + {Array.from({ length: 5 }).map((_, i) => ( +
+ + + + +
+ + + +
+
+ ))} +
+ +
+ +
+ + +
+
) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton.tsx index 2840852df87..a4131c5cb77 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton.tsx @@ -25,12 +25,31 @@ export function ApiKeysSkeleton() { return (
- - + +
-
- - + +
+
+
+ + +
+ +
+ + + +
+
+
+ +
+
+ + +
+
) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-keys.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-keys.tsx index d60b76eeffd..5db8c7d592f 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-keys.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-keys.tsx @@ -158,14 +158,14 @@ export function ApiKeys() {
{isLoading ? (
+ {/* Workspace section header */}
- -
- -
+ +
+ {/* Personal section header + keys */}
- +
@@ -310,6 +310,15 @@ export function ApiKeys() {
{/* Allow Personal API Keys Toggle - Fixed at bottom */} + {isLoading && canManageWorkspaceKeys && ( +
+
+ + +
+ +
+ )} {!isLoading && canManageWorkspaceKeys && (
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton.tsx index de080c3c23b..ead1b66fbc9 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton.tsx @@ -9,11 +9,14 @@ export function BYOKKeySkeleton() {
- - + +
- +
+ + +
) } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credential-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credential-skeleton.tsx index 32823978561..c6812a3d2de 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credential-skeleton.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credential-skeleton.tsx @@ -1,46 +1,70 @@ import { Skeleton } from '@/components/emcn' -const GRID_COLS = 'grid grid-cols-[minmax(0,1fr)_8px_minmax(0,1fr)_auto] items-center' +const GRID_COLS = 'grid grid-cols-[minmax(0,1fr)_8px_minmax(0,1fr)_auto_auto] items-center' +const COL_SPAN_ALL = 'col-span-5' /** - * Skeleton component for a single secret row in the grid layout. + * Skeleton for a single integration credential row. */ export function CredentialSkeleton() { return ( -
+
+
+ +
+ + +
+
+
+ + +
+
+ ) +} + +/** + * Skeleton for a single secret row matching the credentials grid layout. + */ +function CredentialRowSkeleton() { + return ( +
-
- - -
+ +
) } /** - * Skeleton for the Secrets section shown during dynamic import loading. + * Skeleton for the Credentials (Secrets) page shown during dynamic import loading. */ export function CredentialsSkeleton() { return (
-
-
- -
- + +
+
+
+ + + + +
+ + + + +
-
- - - -
) } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tool-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tool-skeleton.tsx index 93f2895d768..6fe69f3630c 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tool-skeleton.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tool-skeleton.tsx @@ -7,11 +7,11 @@ export function CustomToolSkeleton() { return (
- - + +
- +
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general-skeleton.tsx index e58ef57af15..dae014f0bf4 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general-skeleton.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general-skeleton.tsx @@ -11,10 +11,10 @@ export function GeneralSkeleton() {
- +
- +
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox-skeleton.tsx index 70768951c2d..1948ccb1146 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox-skeleton.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/inbox/inbox-skeleton.tsx @@ -8,7 +8,7 @@ export function InboxTaskSkeleton() {
- +
@@ -20,36 +20,64 @@ export function InboxTaskSkeleton() { } /** - * Skeleton for the full Inbox section shown during dynamic import loading. + * Skeleton for the full Inbox section shown while data is loading. */ export function InboxSkeleton() { return (
- - - -
- - - +
+
+ + +
+
-
- - - + +
+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+
+ +
+
+ +
+
-
- - -
-
- - - + +
+
+ + +
+
+ + + +
) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-skeleton.tsx new file mode 100644 index 00000000000..223c274568d --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-skeleton.tsx @@ -0,0 +1,23 @@ +import { Skeleton } from '@/components/emcn' +import { CredentialSkeleton } from '@/app/workspace/[workspaceId]/settings/components/credentials/credential-skeleton' + +/** + * Skeleton for the Integrations section shown during dynamic import loading. + */ +export function IntegrationsSkeleton() { + return ( +
+
+ + +
+
+
+ + + +
+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-skeleton/mcp-server-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-skeleton/mcp-server-skeleton.tsx index 87b6f0ce01a..f32b4591c3e 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-skeleton/mcp-server-skeleton.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-skeleton/mcp-server-skeleton.tsx @@ -9,10 +9,10 @@ export function McpServerSkeleton() {
- - + +
- +
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/deleted-item-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/deleted-item-skeleton.tsx index 58a62e1f52a..e6cf2de3306 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/deleted-item-skeleton.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/deleted-item-skeleton.tsx @@ -5,7 +5,7 @@ import { Skeleton } from '@/components/emcn' */ export function DeletedItemSkeleton() { return ( -
+
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted-skeleton.tsx new file mode 100644 index 00000000000..59846faac9a --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted-skeleton.tsx @@ -0,0 +1,33 @@ +import { Skeleton } from '@/components/emcn' +import { DeletedItemSkeleton } from '@/app/workspace/[workspaceId]/settings/components/recently-deleted/deleted-item-skeleton' + +/** + * Skeleton component for the entire Recently Deleted settings section. + * Renders placeholder UI for the search bar, sort dropdown, tabs, and item list. + */ +export function RecentlyDeletedSkeleton() { + return ( +
+
+ + +
+ +
+ + + + + +
+ +
+
+ + + +
+
+
+ ) +} From adfcb67dc2af2369678cba85929ca21d5372010b Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 4 Apr 2026 19:59:37 -0700 Subject: [PATCH 2/7] feat(cursor): add list artifacts and download artifact tools (#3970) * feat(cursor): add list artifacts and download artifact tools * fix(cursor): resolve build errors in cursor block and download artifact types - Remove invalid wandConfig with unsupported generationType 'json-array' from promptImages subBlock - Remove invalid 'optional' property from summary output definition - Split DownloadArtifactResponse into v1 (content/metadata) and v2 (file) response types * fix(cursor): address PR review feedback - Remove redundant Array.isArray guards in list_artifacts.ts - Pass through actual HTTP status on presigned URL download failure instead of hardcoded 400 --- apps/docs/content/docs/en/tools/cursor.mdx | 38 +++++ .../integrations/data/integrations.json | 10 +- .../tools/cursor/download-artifact/route.ts | 146 ++++++++++++++++++ apps/sim/blocks/blocks/cursor.ts | 48 +++++- apps/sim/tools/cursor/add_followup.ts | 2 +- apps/sim/tools/cursor/delete_agent.ts | 2 +- apps/sim/tools/cursor/download_artifact.ts | 113 ++++++++++++++ apps/sim/tools/cursor/get_agent.ts | 2 +- apps/sim/tools/cursor/get_conversation.ts | 2 +- apps/sim/tools/cursor/index.ts | 6 + apps/sim/tools/cursor/list_agents.ts | 9 +- apps/sim/tools/cursor/list_artifacts.ts | 113 ++++++++++++++ apps/sim/tools/cursor/stop_agent.ts | 3 +- apps/sim/tools/cursor/types.ts | 47 +++++- apps/sim/tools/registry.ts | 8 + 15 files changed, 540 insertions(+), 9 deletions(-) create mode 100644 apps/sim/app/api/tools/cursor/download-artifact/route.ts create mode 100644 apps/sim/tools/cursor/download_artifact.ts create mode 100644 apps/sim/tools/cursor/list_artifacts.ts diff --git a/apps/docs/content/docs/en/tools/cursor.mdx b/apps/docs/content/docs/en/tools/cursor.mdx index 09bcd4644f4..4e6f165c014 100644 --- a/apps/docs/content/docs/en/tools/cursor.mdx +++ b/apps/docs/content/docs/en/tools/cursor.mdx @@ -45,6 +45,7 @@ List all cloud agents for the authenticated user with optional pagination. Retur | `apiKey` | string | Yes | Cursor API key | | `limit` | number | No | Number of agents to return \(default: 20, max: 100\) | | `cursor` | string | No | Pagination cursor from previous response | +| `prUrl` | string | No | Filter agents by pull request URL | #### Output @@ -173,4 +174,41 @@ Permanently delete a cloud agent. Returns API-aligned fields only. | --------- | ---- | ----------- | | `id` | string | Agent ID | +### `cursor_list_artifacts` + +List generated artifact files for a cloud agent. Returns API-aligned fields only. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Cursor API key | +| `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `artifacts` | array | List of artifact files | +| ↳ `path` | string | Artifact file path | +| ↳ `size` | number | File size in bytes | + +### `cursor_download_artifact` + +Download a generated artifact file from a cloud agent. Returns the file for execution storage. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Cursor API key | +| `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) | +| `path` | string | Yes | Absolute path of the artifact to download \(e.g., /src/index.ts\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | file | Downloaded artifact file stored in execution files | + diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 32881c28d13..9db82b6d349 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -2327,9 +2327,17 @@ { "name": "Delete Agent", "description": "Permanently delete a cloud agent. This action cannot be undone." + }, + { + "name": "List Artifacts", + "description": "List generated artifact files for a cloud agent." + }, + { + "name": "Download Artifact", + "description": "Download a generated artifact file from a cloud agent." } ], - "operationCount": 7, + "operationCount": 9, "triggers": [], "triggerCount": 0, "authType": "api-key", diff --git a/apps/sim/app/api/tools/cursor/download-artifact/route.ts b/apps/sim/app/api/tools/cursor/download-artifact/route.ts new file mode 100644 index 00000000000..bc185d1d86a --- /dev/null +++ b/apps/sim/app/api/tools/cursor/download-artifact/route.ts @@ -0,0 +1,146 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + secureFetchWithPinnedIP, + validateUrlWithDNS, +} from '@/lib/core/security/input-validation.server' +import { generateRequestId } from '@/lib/core/utils/request' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('CursorDownloadArtifactAPI') + +const DownloadArtifactSchema = z.object({ + apiKey: z.string().min(1, 'API key is required'), + agentId: z.string().min(1, 'Agent ID is required'), + path: z.string().min(1, 'Artifact path is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn( + `[${requestId}] Unauthorized Cursor download artifact attempt: ${authResult.error}` + ) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + logger.info( + `[${requestId}] Authenticated Cursor download artifact request via ${authResult.authType}`, + { + userId: authResult.userId, + } + ) + + const body = await request.json() + const { apiKey, agentId, path } = DownloadArtifactSchema.parse(body) + + const authHeader = `Basic ${Buffer.from(`${apiKey}:`).toString('base64')}` + + logger.info(`[${requestId}] Requesting presigned URL for artifact`, { agentId, path }) + + const artifactResponse = await fetch( + `https://api.cursor.com/v0/agents/${encodeURIComponent(agentId)}/artifacts/download?path=${encodeURIComponent(path)}`, + { + method: 'GET', + headers: { + Authorization: authHeader, + }, + } + ) + + if (!artifactResponse.ok) { + const errorText = await artifactResponse.text().catch(() => '') + logger.error(`[${requestId}] Failed to get artifact presigned URL`, { + status: artifactResponse.status, + error: errorText, + }) + return NextResponse.json( + { + success: false, + error: errorText || `Failed to get artifact URL (${artifactResponse.status})`, + }, + { status: artifactResponse.status } + ) + } + + const artifactData = await artifactResponse.json() + const downloadUrl = artifactData.url || artifactData.downloadUrl || artifactData.presignedUrl + + if (!downloadUrl) { + logger.error(`[${requestId}] No download URL in artifact response`, { artifactData }) + return NextResponse.json( + { success: false, error: 'No download URL returned for artifact' }, + { status: 400 } + ) + } + + const urlValidation = await validateUrlWithDNS(downloadUrl, 'downloadUrl') + if (!urlValidation.isValid) { + return NextResponse.json({ success: false, error: urlValidation.error }, { status: 400 }) + } + + logger.info(`[${requestId}] Downloading artifact from presigned URL`, { agentId, path }) + + const downloadResponse = await secureFetchWithPinnedIP( + downloadUrl, + urlValidation.resolvedIP!, + {} + ) + + if (!downloadResponse.ok) { + logger.error(`[${requestId}] Failed to download artifact content`, { + status: downloadResponse.status, + statusText: downloadResponse.statusText, + }) + return NextResponse.json( + { + success: false, + error: `Failed to download artifact content (${downloadResponse.status}: ${downloadResponse.statusText})`, + }, + { status: downloadResponse.status } + ) + } + + const contentType = downloadResponse.headers.get('content-type') || 'application/octet-stream' + const arrayBuffer = await downloadResponse.arrayBuffer() + const fileBuffer = Buffer.from(arrayBuffer) + + const fileName = path.split('/').pop() || 'artifact' + + logger.info(`[${requestId}] Artifact downloaded successfully`, { + agentId, + path, + name: fileName, + size: fileBuffer.length, + mimeType: contentType, + }) + + return NextResponse.json({ + success: true, + output: { + file: { + name: fileName, + mimeType: contentType, + data: fileBuffer.toString('base64'), + size: fileBuffer.length, + }, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error downloading Cursor artifact:`, error) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/blocks/blocks/cursor.ts b/apps/sim/blocks/blocks/cursor.ts index 60ceec13e32..b6ef6a52411 100644 --- a/apps/sim/blocks/blocks/cursor.ts +++ b/apps/sim/blocks/blocks/cursor.ts @@ -31,6 +31,8 @@ export const CursorBlock: BlockConfig = { { label: 'List Agents', id: 'cursor_list_agents' }, { label: 'Stop Agent', id: 'cursor_stop_agent' }, { label: 'Delete Agent', id: 'cursor_delete_agent' }, + { label: 'List Artifacts', id: 'cursor_list_artifacts' }, + { label: 'Download Artifact', id: 'cursor_download_artifact' }, ], value: () => 'cursor_launch_agent', }, @@ -48,6 +50,7 @@ export const CursorBlock: BlockConfig = { type: 'short-input', placeholder: 'main (optional)', condition: { field: 'operation', value: 'cursor_launch_agent' }, + mode: 'advanced', }, { id: 'promptText', @@ -57,12 +60,21 @@ export const CursorBlock: BlockConfig = { condition: { field: 'operation', value: 'cursor_launch_agent' }, required: true, }, + { + id: 'promptImages', + title: 'Prompt Images', + type: 'long-input', + placeholder: '[{"data": "base64...", "dimension": {"width": 1024, "height": 768}}]', + condition: { field: 'operation', value: ['cursor_launch_agent', 'cursor_add_followup'] }, + mode: 'advanced', + }, { id: 'model', title: 'Model', type: 'short-input', placeholder: 'Auto-selection by default', condition: { field: 'operation', value: 'cursor_launch_agent' }, + mode: 'advanced', }, { id: 'branchName', @@ -70,6 +82,7 @@ export const CursorBlock: BlockConfig = { type: 'short-input', placeholder: 'Custom branch name (optional)', condition: { field: 'operation', value: 'cursor_launch_agent' }, + mode: 'advanced', }, { id: 'autoCreatePr', @@ -82,12 +95,14 @@ export const CursorBlock: BlockConfig = { title: 'Open as Cursor GitHub App', type: 'switch', condition: { field: 'operation', value: 'cursor_launch_agent' }, + mode: 'advanced', }, { id: 'skipReviewerRequest', title: 'Skip Reviewer Request', type: 'switch', condition: { field: 'operation', value: 'cursor_launch_agent' }, + mode: 'advanced', }, { id: 'agentId', @@ -102,10 +117,20 @@ export const CursorBlock: BlockConfig = { 'cursor_add_followup', 'cursor_stop_agent', 'cursor_delete_agent', + 'cursor_list_artifacts', + 'cursor_download_artifact', ], }, required: true, }, + { + id: 'path', + title: 'Artifact Path', + type: 'short-input', + placeholder: '/opt/cursor/artifacts/screenshot.png', + condition: { field: 'operation', value: 'cursor_download_artifact' }, + required: true, + }, { id: 'followupPromptText', title: 'Follow-up Prompt', @@ -114,12 +139,21 @@ export const CursorBlock: BlockConfig = { condition: { field: 'operation', value: 'cursor_add_followup' }, required: true, }, + { + id: 'prUrl', + title: 'PR URL Filter', + type: 'short-input', + placeholder: 'Filter by pull request URL (optional)', + condition: { field: 'operation', value: 'cursor_list_agents' }, + mode: 'advanced', + }, { id: 'limit', title: 'Limit', type: 'short-input', placeholder: '20 (default, max 100)', condition: { field: 'operation', value: 'cursor_list_agents' }, + mode: 'advanced', }, { id: 'cursor', @@ -127,6 +161,7 @@ export const CursorBlock: BlockConfig = { type: 'short-input', placeholder: 'Cursor from previous response', condition: { field: 'operation', value: 'cursor_list_agents' }, + mode: 'advanced', }, { id: 'apiKey', @@ -146,6 +181,8 @@ export const CursorBlock: BlockConfig = { 'cursor_add_followup', 'cursor_stop_agent', 'cursor_delete_agent', + 'cursor_list_artifacts', + 'cursor_download_artifact', ], config: { tool: (params) => params.operation || 'cursor_launch_agent', @@ -157,15 +194,20 @@ export const CursorBlock: BlockConfig = { ref: { type: 'string', description: 'Branch, tag, or commit reference' }, promptText: { type: 'string', description: 'Instruction text for the agent' }, followupPromptText: { type: 'string', description: 'Follow-up instruction text for the agent' }, - promptImages: { type: 'string', description: 'JSON array of image objects' }, + promptImages: { + type: 'string', + description: 'JSON array of image objects with base64 data and dimensions', + }, model: { type: 'string', description: 'Model to use (empty for auto-selection)' }, branchName: { type: 'string', description: 'Custom branch name' }, autoCreatePr: { type: 'boolean', description: 'Auto-create PR when done' }, openAsCursorGithubApp: { type: 'boolean', description: 'Open PR as Cursor GitHub App' }, skipReviewerRequest: { type: 'boolean', description: 'Skip reviewer request' }, agentId: { type: 'string', description: 'Agent identifier' }, + prUrl: { type: 'string', description: 'Filter agents by pull request URL' }, limit: { type: 'number', description: 'Number of results to return' }, cursor: { type: 'string', description: 'Pagination cursor' }, + path: { type: 'string', description: 'Absolute path of the artifact to download' }, apiKey: { type: 'string', description: 'Cursor API key' }, }, outputs: { @@ -192,6 +234,8 @@ export const CursorV2Block: BlockConfig = { 'cursor_add_followup_v2', 'cursor_stop_agent_v2', 'cursor_delete_agent_v2', + 'cursor_list_artifacts_v2', + 'cursor_download_artifact_v2', ], config: { tool: createVersionedToolSelector({ @@ -213,5 +257,7 @@ export const CursorV2Block: BlockConfig = { agents: { type: 'json', description: 'Array of agent objects (list operation)' }, nextCursor: { type: 'string', description: 'Pagination cursor (list operation)' }, messages: { type: 'json', description: 'Conversation messages (get conversation operation)' }, + artifacts: { type: 'json', description: 'List of artifact files (list artifacts operation)' }, + file: { type: 'file', description: 'Downloaded artifact file (download artifact operation)' }, }, } diff --git a/apps/sim/tools/cursor/add_followup.ts b/apps/sim/tools/cursor/add_followup.ts index 5e161fc27f9..8dcf46c3b20 100644 --- a/apps/sim/tools/cursor/add_followup.ts +++ b/apps/sim/tools/cursor/add_followup.ts @@ -30,7 +30,7 @@ const addFollowupBase = { }, request: { url: (params: AddFollowupParams) => - `https://api.cursor.com/v0/agents/${params.agentId}/followup`, + `https://api.cursor.com/v0/agents/${params.agentId.trim()}/followup`, method: 'POST', headers: (params: AddFollowupParams) => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/cursor/delete_agent.ts b/apps/sim/tools/cursor/delete_agent.ts index 2acf9ea0dec..a3b8e51a45d 100644 --- a/apps/sim/tools/cursor/delete_agent.ts +++ b/apps/sim/tools/cursor/delete_agent.ts @@ -17,7 +17,7 @@ const deleteAgentBase = { }, }, request: { - url: (params: DeleteAgentParams) => `https://api.cursor.com/v0/agents/${params.agentId}`, + url: (params: DeleteAgentParams) => `https://api.cursor.com/v0/agents/${params.agentId.trim()}`, method: 'DELETE', headers: (params: DeleteAgentParams) => ({ Authorization: `Basic ${Buffer.from(`${params.apiKey}:`).toString('base64')}`, diff --git a/apps/sim/tools/cursor/download_artifact.ts b/apps/sim/tools/cursor/download_artifact.ts new file mode 100644 index 00000000000..5b891688410 --- /dev/null +++ b/apps/sim/tools/cursor/download_artifact.ts @@ -0,0 +1,113 @@ +import type { + DownloadArtifactParams, + DownloadArtifactResponse, + DownloadArtifactV2Response, +} from '@/tools/cursor/types' +import type { ToolConfig } from '@/tools/types' + +const downloadArtifactBase = { + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Cursor API key', + }, + agentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Unique identifier for the cloud agent (e.g., bc_abc123)', + }, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Absolute path of the artifact to download (e.g., /src/index.ts)', + }, + }, + request: { + url: '/api/tools/cursor/download-artifact', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params: DownloadArtifactParams) => ({ + apiKey: params.apiKey, + agentId: params.agentId?.trim(), + path: params.path?.trim(), + }), + }, +} satisfies Pick, 'params' | 'request'> + +export const downloadArtifactTool: ToolConfig = { + id: 'cursor_download_artifact', + name: 'Cursor Download Artifact', + description: 'Download a generated artifact file from a cloud agent.', + version: '1.0.0', + + ...downloadArtifactBase, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.success) { + throw new Error(data.error || 'Failed to download artifact') + } + + return { + success: true, + output: { + content: `Downloaded artifact: ${data.output.file.name}`, + metadata: data.output.file, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable download result' }, + metadata: { + type: 'object', + description: 'Downloaded file metadata', + properties: { + name: { type: 'string', description: 'File name' }, + mimeType: { type: 'string', description: 'MIME type' }, + size: { type: 'number', description: 'File size in bytes' }, + }, + }, + }, +} + +export const downloadArtifactV2Tool: ToolConfig< + DownloadArtifactParams, + DownloadArtifactV2Response +> = { + ...downloadArtifactBase, + id: 'cursor_download_artifact_v2', + name: 'Cursor Download Artifact', + description: + 'Download a generated artifact file from a cloud agent. Returns the file for execution storage.', + version: '2.0.0', + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.success) { + throw new Error(data.error || 'Failed to download artifact') + } + + return { + success: true, + output: { + file: data.output.file, + }, + } + }, + + outputs: { + file: { + type: 'file', + description: 'Downloaded artifact file stored in execution files', + }, + }, +} diff --git a/apps/sim/tools/cursor/get_agent.ts b/apps/sim/tools/cursor/get_agent.ts index 920dec4a29f..9ac829c26fd 100644 --- a/apps/sim/tools/cursor/get_agent.ts +++ b/apps/sim/tools/cursor/get_agent.ts @@ -17,7 +17,7 @@ const getAgentBase = { }, }, request: { - url: (params: GetAgentParams) => `https://api.cursor.com/v0/agents/${params.agentId}`, + url: (params: GetAgentParams) => `https://api.cursor.com/v0/agents/${params.agentId.trim()}`, method: 'GET', headers: (params: GetAgentParams) => ({ Authorization: `Basic ${Buffer.from(`${params.apiKey}:`).toString('base64')}`, diff --git a/apps/sim/tools/cursor/get_conversation.ts b/apps/sim/tools/cursor/get_conversation.ts index 32752ea7b31..c271e857823 100644 --- a/apps/sim/tools/cursor/get_conversation.ts +++ b/apps/sim/tools/cursor/get_conversation.ts @@ -18,7 +18,7 @@ const getConversationBase = { }, request: { url: (params: GetConversationParams) => - `https://api.cursor.com/v0/agents/${params.agentId}/conversation`, + `https://api.cursor.com/v0/agents/${params.agentId.trim()}/conversation`, method: 'GET', headers: (params: GetConversationParams) => ({ Authorization: `Basic ${Buffer.from(`${params.apiKey}:`).toString('base64')}`, diff --git a/apps/sim/tools/cursor/index.ts b/apps/sim/tools/cursor/index.ts index 2ec9cebfd15..80573009eec 100644 --- a/apps/sim/tools/cursor/index.ts +++ b/apps/sim/tools/cursor/index.ts @@ -1,9 +1,11 @@ import { addFollowupTool, addFollowupV2Tool } from '@/tools/cursor/add_followup' import { deleteAgentTool, deleteAgentV2Tool } from '@/tools/cursor/delete_agent' +import { downloadArtifactTool, downloadArtifactV2Tool } from '@/tools/cursor/download_artifact' import { getAgentTool, getAgentV2Tool } from '@/tools/cursor/get_agent' import { getConversationTool, getConversationV2Tool } from '@/tools/cursor/get_conversation' import { launchAgentTool, launchAgentV2Tool } from '@/tools/cursor/launch_agent' import { listAgentsTool, listAgentsV2Tool } from '@/tools/cursor/list_agents' +import { listArtifactsTool, listArtifactsV2Tool } from '@/tools/cursor/list_artifacts' import { stopAgentTool, stopAgentV2Tool } from '@/tools/cursor/stop_agent' export const cursorListAgentsTool = listAgentsTool @@ -13,6 +15,8 @@ export const cursorLaunchAgentTool = launchAgentTool export const cursorAddFollowupTool = addFollowupTool export const cursorStopAgentTool = stopAgentTool export const cursorDeleteAgentTool = deleteAgentTool +export const cursorDownloadArtifactTool = downloadArtifactTool +export const cursorListArtifactsTool = listArtifactsTool export const cursorListAgentsV2Tool = listAgentsV2Tool export const cursorGetAgentV2Tool = getAgentV2Tool @@ -21,3 +25,5 @@ export const cursorLaunchAgentV2Tool = launchAgentV2Tool export const cursorAddFollowupV2Tool = addFollowupV2Tool export const cursorStopAgentV2Tool = stopAgentV2Tool export const cursorDeleteAgentV2Tool = deleteAgentV2Tool +export const cursorDownloadArtifactV2Tool = downloadArtifactV2Tool +export const cursorListArtifactsV2Tool = listArtifactsV2Tool diff --git a/apps/sim/tools/cursor/list_agents.ts b/apps/sim/tools/cursor/list_agents.ts index 9c52b754afa..6caa4686f92 100644 --- a/apps/sim/tools/cursor/list_agents.ts +++ b/apps/sim/tools/cursor/list_agents.ts @@ -21,12 +21,19 @@ const listAgentsBase = { visibility: 'user-or-llm', description: 'Pagination cursor from previous response', }, + prUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter agents by pull request URL', + }, }, request: { url: (params: ListAgentsParams) => { const url = new URL('https://api.cursor.com/v0/agents') if (params.limit) url.searchParams.set('limit', String(params.limit)) if (params.cursor) url.searchParams.set('cursor', params.cursor) + if (params.prUrl) url.searchParams.set('prUrl', params.prUrl) return url.toString() }, method: 'GET', @@ -53,7 +60,7 @@ export const listAgentsTool: ToolConfig = content: `Found ${data.agents.length} agents`, metadata: { agents: data.agents, - nextCursor: data.nextCursor, + nextCursor: data.nextCursor ?? null, }, }, } diff --git a/apps/sim/tools/cursor/list_artifacts.ts b/apps/sim/tools/cursor/list_artifacts.ts new file mode 100644 index 00000000000..99580f7d6a7 --- /dev/null +++ b/apps/sim/tools/cursor/list_artifacts.ts @@ -0,0 +1,113 @@ +import type { ListArtifactsParams, ListArtifactsResponse } from '@/tools/cursor/types' +import type { ToolConfig } from '@/tools/types' + +const listArtifactsBase = { + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Cursor API key', + }, + agentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Unique identifier for the cloud agent (e.g., bc_abc123)', + }, + }, + request: { + url: (params: ListArtifactsParams) => + `https://api.cursor.com/v0/agents/${params.agentId.trim()}/artifacts`, + method: 'GET', + headers: (params: ListArtifactsParams) => ({ + Authorization: `Basic ${Buffer.from(`${params.apiKey}:`).toString('base64')}`, + }), + }, +} satisfies Pick, 'params' | 'request'> + +export const listArtifactsTool: ToolConfig = { + id: 'cursor_list_artifacts', + name: 'Cursor List Artifacts', + description: 'List generated artifact files for a cloud agent.', + version: '1.0.0', + + ...listArtifactsBase, + + transformResponse: async (response) => { + const data = await response.json() + const artifacts = data.artifacts ?? [] + + return { + success: true, + output: { + content: `Found ${artifacts.length} artifact(s)`, + metadata: { + artifacts, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable artifact count' }, + metadata: { + type: 'object', + description: 'Artifacts metadata', + properties: { + artifacts: { + type: 'array', + description: 'List of artifacts', + items: { + type: 'object', + properties: { + path: { type: 'string', description: 'Artifact file path' }, + size: { type: 'number', description: 'File size in bytes', optional: true }, + }, + }, + }, + }, + }, + }, +} + +interface ListArtifactsV2Response { + success: boolean + output: { + artifacts: Array<{ path: string; size?: number }> + } +} + +export const listArtifactsV2Tool: ToolConfig = { + ...listArtifactsBase, + id: 'cursor_list_artifacts_v2', + name: 'Cursor List Artifacts', + description: 'List generated artifact files for a cloud agent. Returns API-aligned fields only.', + version: '2.0.0', + + transformResponse: async (response) => { + const data = await response.json() + const artifacts = data.artifacts ?? [] + + return { + success: true, + output: { + artifacts: Array.isArray(artifacts) ? artifacts : [], + }, + } + }, + + outputs: { + artifacts: { + type: 'array', + description: 'List of artifact files', + items: { + type: 'object', + properties: { + path: { type: 'string', description: 'Artifact file path' }, + size: { type: 'number', description: 'File size in bytes', optional: true }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/cursor/stop_agent.ts b/apps/sim/tools/cursor/stop_agent.ts index 728ca9af608..04373c16644 100644 --- a/apps/sim/tools/cursor/stop_agent.ts +++ b/apps/sim/tools/cursor/stop_agent.ts @@ -17,7 +17,8 @@ const stopAgentBase = { }, }, request: { - url: (params: StopAgentParams) => `https://api.cursor.com/v0/agents/${params.agentId}/stop`, + url: (params: StopAgentParams) => + `https://api.cursor.com/v0/agents/${params.agentId.trim()}/stop`, method: 'POST', headers: (params: StopAgentParams) => ({ Authorization: `Basic ${Buffer.from(`${params.apiKey}:`).toString('base64')}`, diff --git a/apps/sim/tools/cursor/types.ts b/apps/sim/tools/cursor/types.ts index 4ec843ce280..3cce5f362ca 100644 --- a/apps/sim/tools/cursor/types.ts +++ b/apps/sim/tools/cursor/types.ts @@ -7,6 +7,7 @@ export interface BaseCursorParams { export interface ListAgentsParams extends BaseCursorParams { limit?: number cursor?: string + prUrl?: string } export interface GetAgentParams extends BaseCursorParams { @@ -60,7 +61,7 @@ interface AgentTarget { interface AgentMetadata { id: string name: string - status: 'RUNNING' | 'FINISHED' | 'STOPPED' | 'FAILED' + status: 'CREATING' | 'RUNNING' | 'FINISHED' | 'STOPPED' | 'FAILED' source: AgentSource target: AgentTarget summary?: string @@ -174,6 +175,47 @@ export interface ListRepositoriesResponse extends ToolResponse { } } +export interface ListArtifactsParams extends BaseCursorParams { + agentId: string +} + +export interface ArtifactMetadata { + path: string + size?: number +} + +export interface ListArtifactsResponse extends ToolResponse { + output: { + content: string + metadata: { + artifacts: ArtifactMetadata[] + } + } +} + +export interface DownloadArtifactParams extends BaseCursorParams { + agentId: string + path: string +} + +export interface DownloadArtifactResponse extends ToolResponse { + output: { + content: string + metadata: Record + } +} + +export interface DownloadArtifactV2Response extends ToolResponse { + output: { + file: { + name: string + mimeType: string + data: string + size: number + } + } +} + export type CursorResponse = | ListAgentsResponse | GetAgentResponse @@ -185,3 +227,6 @@ export type CursorResponse = | GetApiKeyInfoResponse | ListModelsResponse | ListRepositoriesResponse + | ListArtifactsResponse + | DownloadArtifactResponse + | DownloadArtifactV2Response diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index d5065039edd..b39ab298a63 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -346,6 +346,8 @@ import { cursorAddFollowupV2Tool, cursorDeleteAgentTool, cursorDeleteAgentV2Tool, + cursorDownloadArtifactTool, + cursorDownloadArtifactV2Tool, cursorGetAgentTool, cursorGetAgentV2Tool, cursorGetConversationTool, @@ -354,6 +356,8 @@ import { cursorLaunchAgentV2Tool, cursorListAgentsTool, cursorListAgentsV2Tool, + cursorListArtifactsTool, + cursorListArtifactsV2Tool, cursorStopAgentTool, cursorStopAgentV2Tool, } from '@/tools/cursor' @@ -4123,6 +4127,10 @@ export const tools: Record = { cursor_stop_agent_v2: cursorStopAgentV2Tool, cursor_delete_agent: cursorDeleteAgentTool, cursor_delete_agent_v2: cursorDeleteAgentV2Tool, + cursor_download_artifact: cursorDownloadArtifactTool, + cursor_download_artifact_v2: cursorDownloadArtifactV2Tool, + cursor_list_artifacts: cursorListArtifactsTool, + cursor_list_artifacts_v2: cursorListArtifactsV2Tool, trello_list_lists: trelloListListsTool, trello_list_cards: trelloListCardsTool, trello_create_card: trelloCreateCardTool, From 33e6921954203bf6acc4e1571de6346db6588a93 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 4 Apr 2026 20:05:22 -0700 Subject: [PATCH 3/7] improvement(execution): multiple response blocks (#3918) * improvement(execution): multiple response blocks * address comments --- apps/docs/content/docs/en/blocks/response.mdx | 12 +- .../docs/content/docs/en/execution/basics.mdx | 5 +- apps/sim/blocks/blocks/response.ts | 5 +- apps/sim/executor/execution/engine.test.ts | 291 ++++++++++++++++++ apps/sim/executor/execution/engine.ts | 22 +- 5 files changed, 324 insertions(+), 11 deletions(-) diff --git a/apps/docs/content/docs/en/blocks/response.mdx b/apps/docs/content/docs/en/blocks/response.mdx index 327bbaa6eb0..6ec1872fe14 100644 --- a/apps/docs/content/docs/en/blocks/response.mdx +++ b/apps/docs/content/docs/en/blocks/response.mdx @@ -20,7 +20,7 @@ The Response block formats and sends structured HTTP responses back to API calle
- Response blocks are terminal blocks - they end workflow execution and cannot connect to other blocks. + Response blocks are exit points — when a Response block executes, it ends the workflow and sends the HTTP response immediately. Multiple Response blocks can be placed on different branches (e.g. after a Router or Condition), but only the first one to execute determines the API response. ## Configuration Options @@ -77,7 +77,11 @@ Condition (Error Detected) → Router → Response (400/500, Error Details) ## Outputs -Response blocks are terminal — no downstream blocks execute after them. However, the block does define outputs (`data`, `status`, `headers`) which are used to construct the HTTP response sent back to the API caller. +Response blocks are exit points — when one executes, no further blocks run. The block defines outputs (`data`, `status`, `headers`) which are used to construct the HTTP response sent back to the API caller. + + + If a Response block is placed on a parallel branch, there are no guarantees about whether other parallel blocks will run or not. Execution order across parallel branches is non-deterministic, so a parallel block may execute before or after the Response block on any given run. Avoid placing Response blocks in parallel with blocks that have important side effects. + ## Variable References @@ -110,10 +114,10 @@ Use the `` syntax to dynamically insert workflow variables into y - **Validate variable references**: Ensure all referenced variables exist and contain the expected data types before the Response block executes diff --git a/apps/docs/content/docs/en/execution/basics.mdx b/apps/docs/content/docs/en/execution/basics.mdx index 1541831e770..1777b7fdcfb 100644 --- a/apps/docs/content/docs/en/execution/basics.mdx +++ b/apps/docs/content/docs/en/execution/basics.mdx @@ -96,8 +96,9 @@ Understanding these core principles will help you build better workflows: 2. **Automatic Parallelization**: Independent blocks run concurrently without configuration 3. **Smart Data Flow**: Outputs flow automatically to connected blocks 4. **Error Handling**: Failed blocks stop their execution path but don't affect independent paths -5. **State Persistence**: All block outputs and execution details are preserved for debugging -6. **Cycle Protection**: Workflows that call other workflows (via Workflow blocks, MCP tools, or API blocks) are tracked with a call chain. If the chain exceeds 25 hops, execution is stopped to prevent infinite loops +5. **Response Blocks as Exit Points**: When a Response block executes, the entire workflow stops and the API response is sent immediately. Multiple Response blocks can exist on different branches — the first one to execute wins +6. **State Persistence**: All block outputs and execution details are preserved for debugging +7. **Cycle Protection**: Workflows that call other workflows (via Workflow blocks, MCP tools, or API blocks) are tracked with a call chain. If the chain exceeds 25 hops, execution is stopped to prevent infinite loops ## Next Steps diff --git a/apps/sim/blocks/blocks/response.ts b/apps/sim/blocks/blocks/response.ts index 82f5ddb58bb..d11f6814850 100644 --- a/apps/sim/blocks/blocks/response.ts +++ b/apps/sim/blocks/blocks/response.ts @@ -12,12 +12,13 @@ export const ResponseBlock: BlockConfig = { bestPractices: ` - Only use this if the trigger block is the API Trigger. - Prefer the builder mode over the editor mode. - - This is usually used as the last block in the workflow. + - The Response block is an exit point. When it executes, the workflow stops and the API response is sent immediately. + - Multiple Response blocks can be placed on different branches (e.g. after a Router or Condition). The first one to execute determines the API response and ends the workflow. + - If a Response block is on a parallel branch, there are no guarantees about whether other parallel blocks will run. Avoid placing Response blocks in parallel with blocks that have important side effects. `, category: 'blocks', bgColor: '#2F55FF', icon: ResponseIcon, - singleInstance: true, subBlocks: [ { id: 'dataMode', diff --git a/apps/sim/executor/execution/engine.test.ts b/apps/sim/executor/execution/engine.test.ts index ce62d78e33d..f9c0ab41209 100644 --- a/apps/sim/executor/execution/engine.test.ts +++ b/apps/sim/executor/execution/engine.test.ts @@ -957,6 +957,297 @@ describe('ExecutionEngine', () => { }) }) + describe('Response block exit-point behavior', () => { + it('should lock finalOutput and stop execution when a terminal Response block fires', async () => { + const startNode = createMockNode('start', 'starter') + const responseNode = createMockNode('response', 'response') + + startNode.outgoingEdges.set('edge1', { target: 'response' }) + + const dag = createMockDAG([startNode, responseNode]) + const context = createMockContext() + const edgeManager = createMockEdgeManager((node) => { + if (node.id === 'start') return ['response'] + return [] + }) + + const nodeOrchestrator = { + executionCount: 0, + executeNode: vi.fn().mockImplementation(async (_ctx: ExecutionContext, nodeId: string) => { + nodeOrchestrator.executionCount++ + if (nodeId === 'response') { + return { + nodeId, + output: { data: { message: 'ok' }, status: 200, headers: {} }, + isFinalOutput: true, + } + } + return { nodeId, output: {}, isFinalOutput: false } + }), + handleNodeCompletion: vi.fn(), + } as unknown as MockNodeOrchestrator + + const engine = new ExecutionEngine(context, dag, edgeManager, nodeOrchestrator) + const result = await engine.run('start') + + expect(result.success).toBe(true) + expect(result.output).toEqual({ data: { message: 'ok' }, status: 200, headers: {} }) + expect(nodeOrchestrator.executionCount).toBe(2) + }) + + it('should stop execution after Response block on a branch (Router)', async () => { + const startNode = createMockNode('start', 'starter') + const routerNode = createMockNode('router', 'router') + const successResponse = createMockNode('success-response', 'response') + const errorResponse = createMockNode('error-response', 'response') + + startNode.outgoingEdges.set('edge1', { target: 'router' }) + routerNode.outgoingEdges.set('success', { target: 'success-response' }) + routerNode.outgoingEdges.set('error', { target: 'error-response' }) + + const dag = createMockDAG([startNode, routerNode, successResponse, errorResponse]) + const context = createMockContext() + const edgeManager = createMockEdgeManager((node) => { + if (node.id === 'start') return ['router'] + if (node.id === 'router') return ['success-response'] + return [] + }) + + const executedNodes: string[] = [] + const nodeOrchestrator = { + executionCount: 0, + executeNode: vi.fn().mockImplementation(async (_ctx: ExecutionContext, nodeId: string) => { + executedNodes.push(nodeId) + nodeOrchestrator.executionCount++ + if (nodeId === 'success-response') { + return { + nodeId, + output: { data: { result: 'success' }, status: 200, headers: {} }, + isFinalOutput: true, + } + } + return { nodeId, output: {}, isFinalOutput: false } + }), + handleNodeCompletion: vi.fn(), + } as unknown as MockNodeOrchestrator + + const engine = new ExecutionEngine(context, dag, edgeManager, nodeOrchestrator) + const result = await engine.run('start') + + expect(result.success).toBe(true) + expect(result.output).toEqual({ data: { result: 'success' }, status: 200, headers: {} }) + expect(executedNodes).not.toContain('error-response') + }) + + it('should stop all branches when a parallel Response block fires first', async () => { + const startNode = createMockNode('start', 'starter') + const responseNode = createMockNode('fast-response', 'response') + const slowNode = createMockNode('slow-work', 'function') + const afterSlowNode = createMockNode('after-slow', 'function') + + startNode.outgoingEdges.set('edge1', { target: 'fast-response' }) + startNode.outgoingEdges.set('edge2', { target: 'slow-work' }) + slowNode.outgoingEdges.set('edge3', { target: 'after-slow' }) + + const dag = createMockDAG([startNode, responseNode, slowNode, afterSlowNode]) + const context = createMockContext() + const edgeManager = createMockEdgeManager((node) => { + if (node.id === 'start') return ['fast-response', 'slow-work'] + if (node.id === 'slow-work') return ['after-slow'] + return [] + }) + + const executedNodes: string[] = [] + const nodeOrchestrator = { + executionCount: 0, + executeNode: vi.fn().mockImplementation(async (_ctx: ExecutionContext, nodeId: string) => { + executedNodes.push(nodeId) + nodeOrchestrator.executionCount++ + if (nodeId === 'fast-response') { + return { + nodeId, + output: { data: { fast: true }, status: 200, headers: {} }, + isFinalOutput: true, + } + } + if (nodeId === 'slow-work') { + await new Promise((resolve) => setTimeout(resolve, 1)) + return { nodeId, output: { slow: true }, isFinalOutput: false } + } + return { nodeId, output: {}, isFinalOutput: true } + }), + handleNodeCompletion: vi.fn(), + } as unknown as MockNodeOrchestrator + + const engine = new ExecutionEngine(context, dag, edgeManager, nodeOrchestrator) + const result = await engine.run('start') + + expect(result.success).toBe(true) + expect(result.output).toEqual({ data: { fast: true }, status: 200, headers: {} }) + expect(executedNodes).not.toContain('after-slow') + }) + + it('should use standard finalOutput logic when no Response block exists', async () => { + const startNode = createMockNode('start', 'starter') + const endNode = createMockNode('end', 'function') + startNode.outgoingEdges.set('edge1', { target: 'end' }) + + const dag = createMockDAG([startNode, endNode]) + const context = createMockContext() + const edgeManager = createMockEdgeManager((node) => { + if (node.id === 'start') return ['end'] + return [] + }) + + const nodeOrchestrator = { + executionCount: 0, + executeNode: vi.fn().mockImplementation(async (_ctx: ExecutionContext, nodeId: string) => { + nodeOrchestrator.executionCount++ + if (nodeId === 'end') { + return { nodeId, output: { result: 'done' }, isFinalOutput: true } + } + return { nodeId, output: {}, isFinalOutput: false } + }), + handleNodeCompletion: vi.fn(), + } as unknown as MockNodeOrchestrator + + const engine = new ExecutionEngine(context, dag, edgeManager, nodeOrchestrator) + const result = await engine.run('start') + + expect(result.success).toBe(true) + expect(result.output).toEqual({ result: 'done' }) + }) + + it('should not let a second Response block overwrite the first', async () => { + const startNode = createMockNode('start', 'starter') + const response1 = createMockNode('response1', 'response') + const response2 = createMockNode('response2', 'response') + + startNode.outgoingEdges.set('edge1', { target: 'response1' }) + startNode.outgoingEdges.set('edge2', { target: 'response2' }) + + const dag = createMockDAG([startNode, response1, response2]) + const context = createMockContext() + const edgeManager = createMockEdgeManager((node) => { + if (node.id === 'start') return ['response1', 'response2'] + return [] + }) + + const nodeOrchestrator = { + executionCount: 0, + executeNode: vi.fn().mockImplementation(async (_ctx: ExecutionContext, nodeId: string) => { + nodeOrchestrator.executionCount++ + if (nodeId === 'response1') { + return { + nodeId, + output: { data: { first: true }, status: 200, headers: {} }, + isFinalOutput: true, + } + } + if (nodeId === 'response2') { + return { + nodeId, + output: { data: { second: true }, status: 201, headers: {} }, + isFinalOutput: true, + } + } + return { nodeId, output: {}, isFinalOutput: false } + }), + handleNodeCompletion: vi.fn(), + } as unknown as MockNodeOrchestrator + + const engine = new ExecutionEngine(context, dag, edgeManager, nodeOrchestrator) + const result = await engine.run('start') + + expect(result.success).toBe(true) + expect(result.output).toEqual({ data: { first: true }, status: 200, headers: {} }) + }) + + it('should not let non-Response terminals overwrite a Response block output', async () => { + const startNode = createMockNode('start', 'starter') + const responseNode = createMockNode('response', 'response') + const otherTerminal = createMockNode('other', 'function') + + startNode.outgoingEdges.set('edge1', { target: 'response' }) + startNode.outgoingEdges.set('edge2', { target: 'other' }) + + const dag = createMockDAG([startNode, responseNode, otherTerminal]) + const context = createMockContext() + const edgeManager = createMockEdgeManager((node) => { + if (node.id === 'start') return ['response', 'other'] + return [] + }) + + const nodeOrchestrator = { + executionCount: 0, + executeNode: vi.fn().mockImplementation(async (_ctx: ExecutionContext, nodeId: string) => { + nodeOrchestrator.executionCount++ + if (nodeId === 'response') { + return { + nodeId, + output: { data: { response: true }, status: 200, headers: {} }, + isFinalOutput: true, + } + } + if (nodeId === 'other') { + await new Promise((resolve) => setTimeout(resolve, 1)) + return { nodeId, output: { other: true }, isFinalOutput: true } + } + return { nodeId, output: {}, isFinalOutput: false } + }), + handleNodeCompletion: vi.fn(), + } as unknown as MockNodeOrchestrator + + const engine = new ExecutionEngine(context, dag, edgeManager, nodeOrchestrator) + const result = await engine.run('start') + + expect(result.success).toBe(true) + expect(result.output).toEqual({ data: { response: true }, status: 200, headers: {} }) + }) + + it('should honor locked Response output even when a parallel node throws an error', async () => { + const startNode = createMockNode('start', 'starter') + const responseNode = createMockNode('response', 'response') + const errorNode = createMockNode('error-node', 'function') + + startNode.outgoingEdges.set('edge1', { target: 'response' }) + startNode.outgoingEdges.set('edge2', { target: 'error-node' }) + + const dag = createMockDAG([startNode, responseNode, errorNode]) + const context = createMockContext() + const edgeManager = createMockEdgeManager((node) => { + if (node.id === 'start') return ['response', 'error-node'] + return [] + }) + + const nodeOrchestrator = { + executionCount: 0, + executeNode: vi.fn().mockImplementation(async (_ctx: ExecutionContext, nodeId: string) => { + nodeOrchestrator.executionCount++ + if (nodeId === 'response') { + return { + nodeId, + output: { data: { ok: true }, status: 200, headers: {} }, + isFinalOutput: true, + } + } + if (nodeId === 'error-node') { + await new Promise((resolve) => setTimeout(resolve, 1)) + throw new Error('Parallel branch failed') + } + return { nodeId, output: {}, isFinalOutput: false } + }), + handleNodeCompletion: vi.fn(), + } as unknown as MockNodeOrchestrator + + const engine = new ExecutionEngine(context, dag, edgeManager, nodeOrchestrator) + const result = await engine.run('start') + + expect(result.success).toBe(true) + expect(result.output).toEqual({ data: { ok: true }, status: 200, headers: {} }) + }) + }) + describe('Cancellation flag behavior', () => { it('should set cancelledFlag when abort signal fires', async () => { const abortController = new AbortController() diff --git a/apps/sim/executor/execution/engine.ts b/apps/sim/executor/execution/engine.ts index a420c5df7dd..756ab0a03b3 100644 --- a/apps/sim/executor/execution/engine.ts +++ b/apps/sim/executor/execution/engine.ts @@ -23,6 +23,7 @@ export class ExecutionEngine { private executing = new Set>() private queueLock = Promise.resolve() private finalOutput: NormalizedBlockOutput = {} + private responseOutputLocked = false private pausedBlocks: Map = new Map() private allowResumeTriggers: boolean private cancelledFlag = false @@ -127,8 +128,7 @@ export class ExecutionEngine { await this.waitForAllExecutions() } - // Rethrow the captured error so it's handled by the catch block - if (this.errorFlag && this.executionError) { + if (this.errorFlag && this.executionError && !this.responseOutputLocked) { throw this.executionError } @@ -399,6 +399,12 @@ export class ExecutionEngine { return } + if (this.stoppedEarlyFlag && this.responseOutputLocked) { + // Workflow already ended via Response block. Skip state persistence (setBlockOutput), + // parallel/loop scope tracking, and edge propagation — no downstream blocks will run. + return + } + if (output._pauseMetadata) { const pauseMetadata = output._pauseMetadata this.pausedBlocks.set(pauseMetadata.contextId, pauseMetadata) @@ -410,7 +416,17 @@ export class ExecutionEngine { await this.nodeOrchestrator.handleNodeCompletion(this.context, nodeId, output) - if (isFinalOutput) { + const isResponseBlock = node.block.metadata?.id === BlockType.RESPONSE + if (isResponseBlock) { + if (!this.responseOutputLocked) { + this.finalOutput = output + this.responseOutputLocked = true + } + this.stoppedEarlyFlag = true + return + } + + if (isFinalOutput && !this.responseOutputLocked) { this.finalOutput = output } From ebc19484f20512589fcac3a1b13396b50d66db8a Mon Sep 17 00:00:00 2001 From: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:22:35 -0700 Subject: [PATCH 4/7] improvement(landing, blog): ui/ux (#3972) * refactor: moved home into landing * improvement(landing): blog dropdown and content updates * improvement: ui/ux * improvement: footer, enterprise, templates,etc. * improvement: blog * fix: reset feature flags to dynamic values * fix: remove unused mobileLabel reference in features tabs * improvement(auth): match oauth button styling with inputs and navbar login * fix: resolve TypeScript errors in landing components - Add explicit FeatureTab interface type for FEATURE_TABS with optional mobileLabel property - Remove reference to undefined MOBILE_STEPS in landing-preview (mobile uses static display anyway) Co-authored-by: Emir Karabeg * fix(demo-request): remove region from schema and route, delete unused PostGrid --------- Co-authored-by: Cursor Agent Co-authored-by: Emir Karabeg --- apps/sim/app/(auth)/auth-layout-client.tsx | 2 +- .../components/social-login-buttons.tsx | 4 +- .../(auth)/components/sso-login-button.tsx | 4 +- .../(auth)/components/status-page-layout.tsx | 2 +- .../hero/components/animated-blocks.tsx | 584 ------------------ apps/sim/app/(home)/components/hero/hero.tsx | 135 ---- apps/sim/app/(home)/components/index.ts | 23 - .../landing-preview-home.tsx | 99 --- .../landing-preview-panel.tsx | 169 ----- .../landing-preview/landing-preview.tsx | 147 ----- apps/sim/app/(home)/layout.tsx | 18 - .../{(home) => (landing)}/actions/github.ts | 0 apps/sim/app/(landing)/blog/layout.tsx | 4 +- apps/sim/app/(landing)/blog/loading.tsx | 65 +- apps/sim/app/(landing)/blog/page.tsx | 187 ++++-- apps/sim/app/(landing)/blog/post-grid.tsx | 92 --- .../collaboration/collaboration.tsx | 55 +- .../components/demo-request/consts.ts | 25 - .../demo-request/demo-request-modal.tsx | 133 ++-- .../components/access-control-panel.tsx | 0 .../components/audit-log-preview.tsx | 0 .../components/enterprise/enterprise.tsx | 51 +- .../features/components/features-preview.tsx | 174 +----- .../components/features/features.tsx | 152 ++--- .../components/footer/footer-cta.tsx | 30 +- .../components/footer/footer.tsx | 120 ++-- .../app/(landing)/components/hero/hero.tsx | 97 +++ apps/sim/app/(landing)/components/index.ts | 25 +- .../landing-preview-files.tsx | 4 +- .../landing-preview-home.tsx | 479 ++++++++++++++ .../landing-preview-knowledge.tsx | 4 +- .../landing-preview-logs.tsx | 12 +- .../landing-preview-panel.tsx | 474 ++++++++++++++ .../landing-preview-resource.tsx | 0 .../landing-preview-scheduled-tasks.tsx | 4 +- .../landing-preview-sidebar.tsx | 2 +- .../landing-preview-tables.tsx | 76 ++- .../landing-preview-workflow.tsx | 40 +- .../preview-block-node.tsx | 8 +- .../landing-preview-workflow/workflow-data.ts | 108 +++- .../landing-preview/landing-preview.tsx | 322 ++++++++++ .../app/(landing)/components/legal-layout.tsx | 4 +- .../navbar/components/blog-dropdown.tsx | 6 +- .../navbar/components/docs-dropdown.tsx | 4 +- .../navbar/components/github-stars.tsx | 4 +- .../components/navbar/navbar.tsx | 78 +-- .../components/pricing/pricing.tsx | 30 +- .../components/structured-data.tsx | 35 +- .../templates/template-workflows.ts | 2 +- .../components/templates/templates.tsx | 249 ++------ .../components/testimonials/testimonials.tsx | 0 .../sim/app/(landing)/integrations/layout.tsx | 4 +- .../landing-analytics.tsx | 0 .../sim/app/{(home) => (landing)}/landing.tsx | 27 +- apps/sim/app/(landing)/layout.tsx | 13 +- apps/sim/app/(landing)/models/layout.tsx | 4 +- apps/sim/app/(landing)/partners/page.tsx | 4 +- apps/sim/app/academy/(catalog)/layout.tsx | 4 +- apps/sim/app/api/demo-requests/route.ts | 7 +- apps/sim/app/changelog/layout.tsx | 4 +- apps/sim/app/chat/[identifier]/chat.tsx | 2 +- .../chat/components/auth/email/email-auth.tsx | 2 +- .../auth/password/password-auth.tsx | 2 +- .../[identifier]/components/loading-state.tsx | 2 +- .../[identifier]/components/password-auth.tsx | 2 +- apps/sim/app/form/[identifier]/form.tsx | 2 +- apps/sim/app/invite/components/layout.tsx | 2 +- apps/sim/app/not-found.tsx | 2 +- apps/sim/app/page.tsx | 2 +- .../[executionId]/resume-page-client.tsx | 2 +- .../emcn/components/modal/modal.tsx | 61 +- apps/sim/content/blog/enterprise/index.mdx | 2 +- apps/sim/content/blog/multiplayer/index.mdx | 2 +- apps/sim/ee/sso/components/sso-auth.tsx | 2 +- apps/sim/lib/blog/registry.ts | 2 +- apps/sim/public/blog/copilot/cover.png | Bin 45245 -> 197514 bytes apps/sim/public/blog/enterprise/cover.png | Bin 28434 -> 79309 bytes apps/sim/public/blog/executor/cover.png | Bin 253625 -> 184115 bytes apps/sim/public/blog/mothership/cover.png | Bin 3896641 -> 1122393 bytes apps/sim/public/blog/multiplayer/cover.png | Bin 783627 -> 28453 bytes .../blog/openai-vs-n8n-vs-sim/workflow.png | Bin 147372 -> 108885 bytes apps/sim/public/blog/series-a/cover.png | Bin 837114 -> 123400 bytes apps/sim/public/blog/v0-5/cover.png | Bin 1212592 -> 281930 bytes 83 files changed, 2346 insertions(+), 2152 deletions(-) delete mode 100644 apps/sim/app/(home)/components/hero/components/animated-blocks.tsx delete mode 100644 apps/sim/app/(home)/components/hero/hero.tsx delete mode 100644 apps/sim/app/(home)/components/index.ts delete mode 100644 apps/sim/app/(home)/components/landing-preview/components/landing-preview-home/landing-preview-home.tsx delete mode 100644 apps/sim/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel.tsx delete mode 100644 apps/sim/app/(home)/components/landing-preview/landing-preview.tsx delete mode 100644 apps/sim/app/(home)/layout.tsx rename apps/sim/app/{(home) => (landing)}/actions/github.ts (100%) delete mode 100644 apps/sim/app/(landing)/blog/post-grid.tsx rename apps/sim/app/{(home) => (landing)}/components/collaboration/collaboration.tsx (87%) rename apps/sim/app/{(home) => (landing)}/components/demo-request/consts.ts (73%) rename apps/sim/app/{(home) => (landing)}/components/demo-request/demo-request-modal.tsx (65%) rename apps/sim/app/{(home) => (landing)}/components/enterprise/components/access-control-panel.tsx (100%) rename apps/sim/app/{(home) => (landing)}/components/enterprise/components/audit-log-preview.tsx (100%) rename apps/sim/app/{(home) => (landing)}/components/enterprise/enterprise.tsx (87%) rename apps/sim/app/{(home) => (landing)}/components/features/components/features-preview.tsx (86%) rename apps/sim/app/{(home) => (landing)}/components/features/features.tsx (62%) rename apps/sim/app/{(home) => (landing)}/components/footer/footer-cta.tsx (68%) rename apps/sim/app/{(home) => (landing)}/components/footer/footer.tsx (66%) create mode 100644 apps/sim/app/(landing)/components/hero/hero.tsx rename apps/sim/app/{(home) => (landing)}/components/landing-preview/components/landing-preview-files/landing-preview-files.tsx (94%) create mode 100644 apps/sim/app/(landing)/components/landing-preview/components/landing-preview-home/landing-preview-home.tsx rename apps/sim/app/{(home) => (landing)}/components/landing-preview/components/landing-preview-knowledge/landing-preview-knowledge.tsx (90%) rename apps/sim/app/{(home) => (landing)}/components/landing-preview/components/landing-preview-logs/landing-preview-logs.tsx (98%) create mode 100644 apps/sim/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel.tsx rename apps/sim/app/{(home) => (landing)}/components/landing-preview/components/landing-preview-resource/landing-preview-resource.tsx (100%) rename apps/sim/app/{(home) => (landing)}/components/landing-preview/components/landing-preview-scheduled-tasks/landing-preview-scheduled-tasks.tsx (89%) rename apps/sim/app/{(home) => (landing)}/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar.tsx (98%) rename apps/sim/app/{(home) => (landing)}/components/landing-preview/components/landing-preview-tables/landing-preview-tables.tsx (89%) rename apps/sim/app/{(home) => (landing)}/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow.tsx (75%) rename apps/sim/app/{(home) => (landing)}/components/landing-preview/components/landing-preview-workflow/preview-block-node.tsx (96%) rename apps/sim/app/{(home) => (landing)}/components/landing-preview/components/landing-preview-workflow/workflow-data.ts (65%) create mode 100644 apps/sim/app/(landing)/components/landing-preview/landing-preview.tsx rename apps/sim/app/{(home) => (landing)}/components/navbar/components/blog-dropdown.tsx (90%) rename apps/sim/app/{(home) => (landing)}/components/navbar/components/docs-dropdown.tsx (96%) rename apps/sim/app/{(home) => (landing)}/components/navbar/components/github-stars.tsx (81%) rename apps/sim/app/{(home) => (landing)}/components/navbar/navbar.tsx (83%) rename apps/sim/app/{(home) => (landing)}/components/pricing/pricing.tsx (85%) rename apps/sim/app/{(home) => (landing)}/components/structured-data.tsx (84%) rename apps/sim/app/{(home) => (landing)}/components/templates/template-workflows.ts (99%) rename apps/sim/app/{(home) => (landing)}/components/templates/templates.tsx (66%) rename apps/sim/app/{(home) => (landing)}/components/testimonials/testimonials.tsx (100%) rename apps/sim/app/{(home) => (landing)}/landing-analytics.tsx (100%) rename apps/sim/app/{(home) => (landing)}/landing.tsx (74%) diff --git a/apps/sim/app/(auth)/auth-layout-client.tsx b/apps/sim/app/(auth)/auth-layout-client.tsx index 3aee420922f..89aeb3a89e7 100644 --- a/apps/sim/app/(auth)/auth-layout-client.tsx +++ b/apps/sim/app/(auth)/auth-layout-client.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react' import AuthBackground from '@/app/(auth)/components/auth-background' -import Navbar from '@/app/(home)/components/navbar/navbar' +import Navbar from '@/app/(landing)/components/navbar/navbar' export default function AuthLayoutClient({ children }: { children: React.ReactNode }) { useEffect(() => { diff --git a/apps/sim/app/(auth)/components/social-login-buttons.tsx b/apps/sim/app/(auth)/components/social-login-buttons.tsx index 3545840681e..bf1d112c504 100644 --- a/apps/sim/app/(auth)/components/social-login-buttons.tsx +++ b/apps/sim/app/(auth)/components/social-login-buttons.tsx @@ -81,7 +81,7 @@ export function SocialLoginButtons({ const githubButton = ( - - - Get started - -
-
- - - - - -
- - - - -
- -
-
- - - - ) -} diff --git a/apps/sim/app/(home)/components/index.ts b/apps/sim/app/(home)/components/index.ts deleted file mode 100644 index b05b10953e2..00000000000 --- a/apps/sim/app/(home)/components/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Collaboration from '@/app/(home)/components/collaboration/collaboration' -import Enterprise from '@/app/(home)/components/enterprise/enterprise' -import Features from '@/app/(home)/components/features/features' -import Footer from '@/app/(home)/components/footer/footer' -import Hero from '@/app/(home)/components/hero/hero' -import Navbar from '@/app/(home)/components/navbar/navbar' -import Pricing from '@/app/(home)/components/pricing/pricing' -import StructuredData from '@/app/(home)/components/structured-data' -import Templates from '@/app/(home)/components/templates/templates' -import Testimonials from '@/app/(home)/components/testimonials/testimonials' - -export { - Collaboration, - Enterprise, - Features, - Footer, - Hero, - Navbar, - Pricing, - StructuredData, - Templates, - Testimonials, -} diff --git a/apps/sim/app/(home)/components/landing-preview/components/landing-preview-home/landing-preview-home.tsx b/apps/sim/app/(home)/components/landing-preview/components/landing-preview-home/landing-preview-home.tsx deleted file mode 100644 index 3a7b88d2ff6..00000000000 --- a/apps/sim/app/(home)/components/landing-preview/components/landing-preview-home/landing-preview-home.tsx +++ /dev/null @@ -1,99 +0,0 @@ -'use client' - -import { memo, useCallback, useRef, useState } from 'react' -import { ArrowUp } from 'lucide-react' -import { useLandingSubmit } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel' -import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder' - -const C = { - SURFACE: '#292929', - BORDER: '#3d3d3d', - TEXT_PRIMARY: '#e6e6e6', -} as const - -/** - * Landing preview replica of the workspace Home initial view. - * Shows a greeting heading and a minimal chat input (no + or mic). - * On submit, stores the prompt and redirects to /signup. - */ -export const LandingPreviewHome = memo(function LandingPreviewHome() { - const landingSubmit = useLandingSubmit() - const [inputValue, setInputValue] = useState('') - const textareaRef = useRef(null) - const animatedPlaceholder = useAnimatedPlaceholder() - - const isEmpty = inputValue.trim().length === 0 - - const handleSubmit = useCallback(() => { - if (isEmpty) return - landingSubmit(inputValue) - }, [isEmpty, inputValue, landingSubmit]) - - const MAX_HEIGHT = 200 - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault() - handleSubmit() - } - }, - [handleSubmit] - ) - - const handleInput = useCallback((e: React.FormEvent) => { - const target = e.target as HTMLTextAreaElement - target.style.height = 'auto' - target.style.height = `${Math.min(target.scrollHeight, MAX_HEIGHT)}px` - }, []) - - return ( -
-

- What should we get done? -

- -
-
textareaRef.current?.focus()} - > -