From 111c745942f1c1420bf66dcd3ec67002b7f00a45 Mon Sep 17 00:00:00 2001 From: David Anyatonwu <51977119+onyedikachi-david@users.noreply.github.com> Date: Wed, 6 May 2026 11:30:17 +0100 Subject: [PATCH 1/9] chore(savvycal): bump version to 0.1.0 (#13114) --- packages/pieces/community/savvycal/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pieces/community/savvycal/package.json b/packages/pieces/community/savvycal/package.json index 5c4fa0c9543..a6068ef7dba 100644 --- a/packages/pieces/community/savvycal/package.json +++ b/packages/pieces/community/savvycal/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-savvycal", - "version": "0.0.2", + "version": "0.1.0", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "scripts": { From ed88d572e1dabed59b79cf4a9d457fbe9cc3f013 Mon Sep 17 00:00:00 2001 From: Hazem Adel Date: Wed, 6 May 2026 13:48:01 +0300 Subject: [PATCH 2/9] fix(chat): default to Sonnet instead of Opus for chat model (#13122) --- bun.lock | 6 ++---- packages/shared/package.json | 2 +- packages/shared/src/lib/management/ai-providers/index.ts | 6 +++--- packages/web/src/features/agents/ai-model/hooks.ts | 9 ++++++++- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/bun.lock b/bun.lock index ce26e738090..5d8e9b18075 100644 --- a/bun.lock +++ b/bun.lock @@ -6007,7 +6007,7 @@ }, "packages/pieces/community/savvycal": { "name": "@activepieces/piece-savvycal", - "version": "0.0.2", + "version": "0.1.0", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -8213,7 +8213,6 @@ "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "isolated-vm": "6.0.2", - "mime-types": "2.1.35", "nanoid": "3.3.8", "socket.io-client": "4.8.1", "tslib": "2.6.2", @@ -8221,7 +8220,6 @@ "zod": "4.3.6", }, "devDependencies": { - "@types/mime-types": "2.1.1", "@types/node": "24.11.0", "vitest": "3.0.8", }, @@ -8280,7 +8278,7 @@ }, "packages/shared": { "name": "@activepieces/shared", - "version": "0.71.1", + "version": "0.71.4", "dependencies": { "dayjs": "1.11.9", "deepmerge-ts": "7.1.0", diff --git a/packages/shared/package.json b/packages/shared/package.json index e2e4bcc5414..cd1f7c0025f 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/shared", - "version": "0.71.3", + "version": "0.71.4", "type": "commonjs", "sideEffects": false, "main": "./dist/src/index.js", diff --git a/packages/shared/src/lib/management/ai-providers/index.ts b/packages/shared/src/lib/management/ai-providers/index.ts index 401b771b879..be1ed8edd54 100644 --- a/packages/shared/src/lib/management/ai-providers/index.ts +++ b/packages/shared/src/lib/management/ai-providers/index.ts @@ -256,8 +256,8 @@ export type AIErrorResponse = z.infer * wrong-but-confident answer. */ const OPENAI_CHAT_MODELS = ['gpt-5.5', 'gpt-5.4-mini', 'gpt-5.4-nano', 'gpt-4.1', 'gpt-4.1-mini'] as const -const ANTHROPIC_CHAT_MODELS = ['claude-opus-4-7', 'claude-sonnet-4-6', 'claude-haiku-4-5'] as const -const ANTHROPIC_OPENROUTER_CHAT_MODELS = ['claude-opus-4.7', 'claude-sonnet-4.6', 'claude-haiku-4.5'] as const +const ANTHROPIC_CHAT_MODELS = ['claude-sonnet-4-6', 'claude-opus-4-7', 'claude-haiku-4-5'] as const +const ANTHROPIC_OPENROUTER_CHAT_MODELS = ['claude-sonnet-4.6', 'claude-opus-4.7', 'claude-haiku-4.5'] as const const GOOGLE_CHAT_MODELS = ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-3.1-pro-preview', 'gemini-3-flash-preview'] as const const X_AI_OPENROUTER_CHAT_MODELS = ['grok-4.20', 'grok-4.1-fast'] as const @@ -266,8 +266,8 @@ export const ALLOWED_CHAT_MODELS_BY_PROVIDER: Partial `${AIProviderName.OPENAI}/${m}`), ...ANTHROPIC_OPENROUTER_CHAT_MODELS.map((m) => `${AIProviderName.ANTHROPIC}/${m}`), + ...OPENAI_CHAT_MODELS.map((m) => `${AIProviderName.OPENAI}/${m}`), ...GOOGLE_CHAT_MODELS.map((m) => `${AIProviderName.GOOGLE}/${m}`), ...X_AI_OPENROUTER_CHAT_MODELS.map((m) => `x-ai/${m}`), ], diff --git a/packages/web/src/features/agents/ai-model/hooks.ts b/packages/web/src/features/agents/ai-model/hooks.ts index 8a7719a5baf..8cffba459e8 100644 --- a/packages/web/src/features/agents/ai-model/hooks.ts +++ b/packages/web/src/features/agents/ai-model/hooks.ts @@ -26,7 +26,14 @@ function getAllowedModelsForProvider( return allowedIds.includes(model.id); }) - .sort((a, b) => a.name.localeCompare(b.name)); + .sort((a, b) => { + if (isNil(allowedIds)) { + return a.name.localeCompare(b.name); + } + const aIndex = allowedIds.indexOf(a.id); + const bIndex = allowedIds.indexOf(b.id); + return aIndex - bIndex; + }); } export const aiModelHooks = { From 1913734a780a7d4f2b72362c35f248a79161f966 Mon Sep 17 00:00:00 2001 From: Hazem Adel Date: Wed, 6 May 2026 14:51:47 +0300 Subject: [PATCH 3/9] feat(chat): show connection status and reconnect in picker (#13123) --- .../api/src/app/chat/tools/chat-tools.ts | 5 +- .../web/public/locales/en/translation.json | 2 + .../components/connection-picker-card.tsx | 209 +++++++++++++++--- .../chat-with-ai/lib/message-parsers.ts | 16 ++ 4 files changed, 198 insertions(+), 34 deletions(-) diff --git a/packages/server/api/src/app/chat/tools/chat-tools.ts b/packages/server/api/src/app/chat/tools/chat-tools.ts index 5ea3a35557a..0e427f9e89b 100644 --- a/packages/server/api/src/app/chat/tools/chat-tools.ts +++ b/packages/server/api/src/app/chat/tools/chat-tools.ts @@ -131,10 +131,10 @@ function pieceDisplayLabel(shortName: string): string { function buildConnectionPickerBlock({ shortName, displayName, connections }: { shortName: string displayName: string - connections: Array<{ displayName: string, project: string, externalId: string, projectId: string }> + connections: Array<{ displayName: string, project: string, externalId: string, projectId: string, status: string }> }): string { const connLines = connections.map((c) => - `- label: ${c.displayName}\n project: ${c.project}\n externalId: ${c.externalId}\n projectId: ${c.projectId}`, + `- label: ${c.displayName}\n project: ${c.project}\n externalId: ${c.externalId}\n projectId: ${c.projectId}\n status: ${c.status}`, ).join('\n') return `\`\`\`connection-picker\npiece: ${shortName}\ndisplayName: ${displayName}\nconnections:\n${connLines}\n\`\`\`` } @@ -165,6 +165,7 @@ async function findConnectionsForPiece({ pieceName, projects, platformId, log }: externalId: c.externalId, project: chatPrompt.projectDisplayName(project), projectId: project.id, + status: c.status, })) }), ) diff --git a/packages/web/public/locales/en/translation.json b/packages/web/public/locales/en/translation.json index 4aa1d2c27da..df07903ef56 100644 --- a/packages/web/public/locales/en/translation.json +++ b/packages/web/public/locales/en/translation.json @@ -205,6 +205,7 @@ "Select an array of items": "Select an array of items", "Select a connection": "Select a connection", "Reconnect": "Reconnect", + "Reconnect & Use": "Reconnect & Use", "Create Connection": "Create Connection", "Incomplete settings": "Incomplete settings", "Rename": "Rename", @@ -1163,6 +1164,7 @@ "Try Again": "Try Again", "Redirecting to billing in {countdown} seconds...": "Redirecting to billing in {countdown} seconds...", "Expired": "Expired", + "Missing": "Missing", "Expires soon": "Expires soon", "Active": "Active", "License Key": "License Key", diff --git a/packages/web/src/app/routes/chat-with-ai/components/connection-picker-card.tsx b/packages/web/src/app/routes/chat-with-ai/components/connection-picker-card.tsx index 1945e001c36..c3d9e7f7662 100644 --- a/packages/web/src/app/routes/chat-with-ai/components/connection-picker-card.tsx +++ b/packages/web/src/app/routes/chat-with-ai/components/connection-picker-card.tsx @@ -1,19 +1,35 @@ +import { + AppConnectionStatus, + AppConnectionWithoutSensitiveData, +} from '@activepieces/shared'; import { useQueryClient } from '@tanstack/react-query'; import { t } from 'i18next'; -import { Check, Plus } from 'lucide-react'; +import { Check, Plus, RefreshCw } from 'lucide-react'; import { motion } from 'motion/react'; -import { useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { CreateOrEditConnectionDialog } from '@/app/connections/create-edit-connection-dialog'; import { Button } from '@/components/ui/button'; +import { appConnectionsApi } from '@/features/connections/api/app-connections'; import { piecesHooks } from '@/features/pieces'; import { PieceIconWithPieceName } from '@/features/pieces/components/piece-icon-from-name'; +import { authenticationSession } from '@/lib/authentication-session'; import { ConnectionPickerData, normalizePieceName, } from '../lib/message-parsers'; +function isConnectionHealthy(status: AppConnectionStatus): boolean { + return status === AppConnectionStatus.ACTIVE; +} + +function connectionStatusLabel(status: AppConnectionStatus): string | null { + if (status === AppConnectionStatus.ERROR) return t('Expired'); + if (status === AppConnectionStatus.MISSING) return t('Missing'); + return null; +} + function SelectedState({ pieceName, connection, @@ -53,6 +69,78 @@ function SelectedState({ ); } +function useLiveConnections({ + connections, + pieceName, + enabled, +}: { + connections: ConnectionPickerData['connections']; + pieceName: string; + enabled: boolean; +}): { + statuses: Record; + fullConnections: Record; + isLoading: boolean; +} { + const [statuses, setStatuses] = useState>( + {}, + ); + const [isLoading, setIsLoading] = useState(false); + const fullConnectionsRef = useRef< + Record + >({}); + + const projectIdsKey = useMemo( + () => [...new Set(connections.map((c) => c.projectId))].sort().join(','), + [connections], + ); + + useEffect(() => { + if (!enabled || !projectIdsKey) return; + let cancelled = false; + setIsLoading(true); + + const projectIds = projectIdsKey.split(','); + + void Promise.all( + projectIds.map(async (projectId) => { + const effectiveProjectId = + projectId || authenticationSession.getProjectId(); + if (!effectiveProjectId) return []; + const result = await appConnectionsApi.list({ + projectId: effectiveProjectId, + pieceName, + limit: 100, + }); + return result.data; + }), + ) + .then((results) => { + if (cancelled) return; + const statusMap: Record = {}; + const connMap: Record = {}; + for (const conns of results) { + for (const conn of conns) { + statusMap[conn.externalId] = conn.status; + connMap[conn.externalId] = conn; + } + } + fullConnectionsRef.current = connMap; + setStatuses(statusMap); + setIsLoading(false); + }) + .catch(() => { + if (!cancelled) setIsLoading(false); + }); + + return () => { + cancelled = true; + }; + }, [projectIdsKey, pieceName, enabled]); + + return { statuses, fullConnections: fullConnectionsRef.current, isLoading }; +} + export function ConnectionPickerCard({ picker, onSelect, @@ -64,10 +152,34 @@ export function ConnectionPickerCard({ name: pieceName, }); const [connectDialogOpen, setConnectDialogOpen] = useState(false); + const [reconnectConnection, setReconnectConnection] = + useState(null); const [selectedConnection, setSelectedConnection] = useState< ConnectionPickerData['connections'][number] | null >(null); + const { + statuses: liveStatuses, + fullConnections, + isLoading: isLoadingStatuses, + } = useLiveConnections({ + connections: picker.connections, + pieceName, + enabled: isInteractive && !selectedConnection, + }); + + const handleReconnect = (externalId: string) => { + const fullConnection = fullConnections[externalId]; + if (!fullConnection) return; + setReconnectConnection(fullConnection); + setConnectDialogOpen(true); + }; + + const handleNewConnection = () => { + setReconnectConnection(null); + setConnectDialogOpen(true); + }; + if (!isInteractive) { return (
- {picker.connections.map((conn) => ( -
- -
-
{conn.label}
-
- {conn.project} + {picker.connections.map((conn) => { + const status = liveStatuses[conn.externalId] ?? conn.status; + const healthy = isConnectionHealthy(status); + return ( +
+ +
+
+ {conn.label} +
+
+ {healthy + ? conn.project + : `${conn.project} · ${connectionStatusLabel(status)}`} +
+ {healthy ? ( + + ) : status === AppConnectionStatus.MISSING ? ( + + ) : ( + + )}
- -
- ))} + ); + })}
@@ -178,7 +322,7 @@ export function ConnectionPickerCard({ size="sm" className="shrink-0 gap-1.5" disabled={isPieceLoading} - onClick={() => setConnectDialogOpen(true)} + onClick={handleNewConnection} > {t('Connect')} @@ -201,11 +345,12 @@ export function ConnectionPickerCard({ project: '', externalId: createdConnection.externalId, projectId: '', + status: AppConnectionStatus.ACTIVE, }); onSelect(`Connected ${createdConnection.displayName}`); } }} - reconnectConnection={null} + reconnectConnection={reconnectConnection} isGlobalConnection={false} /> )} diff --git a/packages/web/src/app/routes/chat-with-ai/lib/message-parsers.ts b/packages/web/src/app/routes/chat-with-ai/lib/message-parsers.ts index 1005f38ec05..546e1360e89 100644 --- a/packages/web/src/app/routes/chat-with-ai/lib/message-parsers.ts +++ b/packages/web/src/app/routes/chat-with-ai/lib/message-parsers.ts @@ -1,7 +1,20 @@ +import { AppConnectionStatus } from '@activepieces/shared'; + import { ChatUIMessage } from '@/features/chat/lib/chat-types'; import { ProposalStep, stepVisuals } from './step-visuals'; +const CONNECTION_STATUS_VALUES: ReadonlySet = new Set( + Object.values(AppConnectionStatus), +); + +function toConnectionStatus(value: string): AppConnectionStatus { + if (CONNECTION_STATUS_VALUES.has(value)) { + return value as AppConnectionStatus; + } + return AppConnectionStatus.ACTIVE; +} + export function normalizePieceName(piece: string): string { const shortName = piece.replace(/[^a-z0-9-]/gi, ''); return piece.startsWith('@activepieces/') @@ -266,6 +279,7 @@ export function parseConnectionPicker(content: string): { const projectMatch = /^\s+project:\s*(.+)$/m.exec(connBlock); const externalIdMatch = /^\s+externalId:\s*(.+)$/m.exec(connBlock); const projectIdMatch = /^\s+projectId:\s*(.+)$/m.exec(connBlock); + const statusMatch = /^\s+status:\s*(.+)$/m.exec(connBlock); const externalId = externalIdMatch?.[1].trim() ?? ''; const projectId = projectIdMatch?.[1].trim() ?? ''; @@ -276,6 +290,7 @@ export function parseConnectionPicker(content: string): { project: projectMatch?.[1].trim() ?? '', externalId, projectId, + status: toConnectionStatus(statusMatch?.[1].trim() ?? ''), }); } @@ -297,5 +312,6 @@ export type ConnectionPickerData = { project: string; externalId: string; projectId: string; + status: AppConnectionStatus; }>; }; From 236911f5dd83f2d5d32de6920a7054e5509369c8 Mon Sep 17 00:00:00 2001 From: sanket-a11y Date: Wed, 6 May 2026 17:31:07 +0530 Subject: [PATCH 4/9] fix(gmail): default Find Email to latest emails when no filters set (#13124) Co-authored-by: Hazem Adel --- packages/pieces/community/gmail/package.json | 2 +- .../gmail/src/lib/actions/search-email-action.ts | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/pieces/community/gmail/package.json b/packages/pieces/community/gmail/package.json index 0e9dd748781..5c42d6e0e86 100644 --- a/packages/pieces/community/gmail/package.json +++ b/packages/pieces/community/gmail/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-gmail", - "version": "0.12.2", + "version": "0.12.3", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "dependencies": { diff --git a/packages/pieces/community/gmail/src/lib/actions/search-email-action.ts b/packages/pieces/community/gmail/src/lib/actions/search-email-action.ts index 9663a4fd233..73cb0321469 100644 --- a/packages/pieces/community/gmail/src/lib/actions/search-email-action.ts +++ b/packages/pieces/community/gmail/src/lib/actions/search-email-action.ts @@ -10,7 +10,7 @@ export const gmailSearchMailAction = createAction({ name: 'gmail_search_mail', displayName: 'Find Email', description: - 'Find emails using advanced search criteria. At least one search filter (from, to, subject, label, category, date, content, or attachment) is required.', + 'Find emails using advanced search criteria. If no filters are provided, the latest emails are returned.', props: { from: GmailProps.from, to: GmailProps.to, @@ -114,10 +114,6 @@ export const gmailSearchMailAction = createAction({ const searchQuery = queryParts.join(' '); - if (!searchQuery.trim()) { - throw new Error('Please provide at least one search criterion'); - } - const maxResults = Math.min( Math.max(context.propsValue.max_results || 10, 1), 500 @@ -126,7 +122,7 @@ export const gmailSearchMailAction = createAction({ try { const searchResponse = await gmail.users.messages.list({ userId: 'me', - q: searchQuery, + ...(searchQuery.trim() ? { q: searchQuery } : {}), maxResults: maxResults, includeSpamTrash: context.propsValue.include_spam_trash, }); From 4bfad273564b97fb174d213d810663f805593606 Mon Sep 17 00:00:00 2001 From: Mo AbuAboud Date: Wed, 6 May 2026 14:10:46 +0200 Subject: [PATCH 5/9] =?UTF-8?q?fix(workers):=20backfill=20streamStepProgre?= =?UTF-8?q?ss=20in=20v7=E2=86=92v8=20migration=20and=20reject=20poisoned?= =?UTF-8?q?=20jobs=20(#13125)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/workers/job-queue/job-broker.ts | 17 ++- .../workers/migrations/job-data-migrations.ts | 37 ++++- .../workers/job-broker-invalid-schema.test.ts | 89 ++++++++++++ .../migrations/job-data-migrations.test.ts | 128 +++++++++++++++--- packages/shared/package.json | 2 +- .../src/lib/automation/workers/job-data.ts | 2 +- 6 files changed, 247 insertions(+), 28 deletions(-) create mode 100644 packages/server/api/test/integration/ce/workers/job-broker-invalid-schema.test.ts diff --git a/packages/server/api/src/app/workers/job-queue/job-broker.ts b/packages/server/api/src/app/workers/job-queue/job-broker.ts index 8a16d819d84..c9b17f71ca4 100644 --- a/packages/server/api/src/app/workers/job-queue/job-broker.ts +++ b/packages/server/api/src/app/workers/job-queue/job-broker.ts @@ -1,5 +1,5 @@ import { ConsumeJobRequest, ConsumeJobResponse, EngineResponseStatus, isNil, JobData, tryCatch } from '@activepieces/shared' -import { Worker as BullMQWorker, Job } from 'bullmq' +import { Worker as BullMQWorker, Job, UnrecoverableError } from 'bullmq' import { BullMQOtel } from 'bullmq-otel' import { FastifyBaseLogger } from 'fastify' import { accessTokenManager } from '../../authentication/lib/access-token-manager' @@ -115,6 +115,21 @@ async function tryDequeue(worker: BullMQWorker, queueName: string, log: FastifyB await job.updateData(migratedData) } + const parseResult = JobData.safeParse(migratedData) + if (!parseResult.success) { + const issues = parseResult.error.issues.map(issue => `${issue.path.join('.') || ''}: ${issue.message}`).join('; ') + const reason = `Job data failed schema validation after migration: ${issues}` + log.error( + { queueName, jobId, schemaVersion: migratedData.schemaVersion, jobType: migratedData.jobType, issues: parseResult.error.issues }, + '[jobBroker#tryDequeue] Failing job with invalid schema as unrecoverable', + ) + const { error: failError } = await tryCatch(() => job.moveToFailed(new UnrecoverableError(reason), token, false)) + if (failError) { + log.error({ queueName, jobId, error: String(failError) }, '[jobBroker#tryDequeue] Failed to fail invalid-schema job') + } + return tryDequeue(worker, queueName, log) + } + const interceptorResult = await runInterceptors({ jobId, jobData: migratedData, job, log }) if (interceptorResult === 'DISCARD') { await job.moveToCompleted(null, token, false) diff --git a/packages/server/api/src/app/workers/migrations/job-data-migrations.ts b/packages/server/api/src/app/workers/migrations/job-data-migrations.ts index 7daa3b442a2..0f34e02befc 100644 --- a/packages/server/api/src/app/workers/migrations/job-data-migrations.ts +++ b/packages/server/api/src/app/workers/migrations/job-data-migrations.ts @@ -1,9 +1,25 @@ import { apId, JobData, StreamStepProgress, UploadLogsBehavior, UploadLogsToken, WorkerJobType } from '@activepieces/shared' import { FastifyBaseLogger } from 'fastify' +import { z } from 'zod' import { flowRunLogsService } from '../../flows/flow-run/logs/flow-run-logs-service' import { flowVersionService } from '../../flows/flow-version/flow-version.service' import { jwtUtils } from '../../helper/jwt-utils' +const LegacyExecuteFlowFields = z.object({ + streamStepProgress: z.enum(StreamStepProgress).optional(), + progressUpdateType: z.string().optional(), + workerHandlerId: z.string().nullish(), + synchronousHandlerId: z.string().nullish(), +}) + +function deriveExecuteFlowMigrationFields(job: JobData): { streamStepProgress: StreamStepProgress, workerHandlerId: string | null } { + const legacy = LegacyExecuteFlowFields.parse(job) + return { + streamStepProgress: legacy.streamStepProgress ?? migrateProgressUpdateType(legacy.progressUpdateType), + workerHandlerId: legacy.workerHandlerId ?? legacy.synchronousHandlerId ?? null, + } +} + function createMigrations(log: FastifyBaseLogger): JobMigration[] { const enrichFlowIdAndLogsUrl: JobMigration = { runAtSchemaVersion: 0, @@ -48,14 +64,10 @@ function createMigrations(log: FastifyBaseLogger): JobMigration[] { runAtSchemaVersion: 5, migrate: async (job: JobData) => { if (job.jobType === WorkerJobType.EXECUTE_FLOW) { - const legacy = job as Record - const streamStepProgress: StreamStepProgress = (legacy['streamStepProgress'] as StreamStepProgress | undefined) ?? migrateProgressUpdateType(legacy['progressUpdateType'] as string | undefined) - const workerHandlerId: string | null = (legacy['workerHandlerId'] as string | undefined) ?? (legacy['synchronousHandlerId'] as string | undefined) ?? null return { ...job, schemaVersion: 6, - streamStepProgress, - workerHandlerId, + ...deriveExecuteFlowMigrationFields(job), } } return { ...job, schemaVersion: 6 } @@ -81,8 +93,21 @@ function createMigrations(log: FastifyBaseLogger): JobMigration[] { } }, } + const backfillRequiredExecuteFlowFields: JobMigration = { + runAtSchemaVersion: 7, + migrate: async (job: JobData) => { + if (job.jobType !== WorkerJobType.EXECUTE_FLOW) { + return { ...job, schemaVersion: 8 } + } + return { + ...job, + schemaVersion: 8, + ...deriveExecuteFlowMigrationFields(job), + } + }, + } - return [enrichFlowIdAndLogsUrl, migratePayloadToUnion, renameProgressAndHandlerFields, reSignLogsUploadUrlWithAudience] + return [enrichFlowIdAndLogsUrl, migratePayloadToUnion, renameProgressAndHandlerFields, reSignLogsUploadUrlWithAudience, backfillRequiredExecuteFlowFields] } function extractLogsBehaviorFromUrl(url: string): UploadLogsBehavior | null { diff --git a/packages/server/api/test/integration/ce/workers/job-broker-invalid-schema.test.ts b/packages/server/api/test/integration/ce/workers/job-broker-invalid-schema.test.ts new file mode 100644 index 00000000000..81c3e0f3770 --- /dev/null +++ b/packages/server/api/test/integration/ce/workers/job-broker-invalid-schema.test.ts @@ -0,0 +1,89 @@ +import { + apId, + ExecuteFlowJobData, + ExecutionType, + LATEST_JOB_DATA_SCHEMA_VERSION, + RunEnvironment, + StreamStepProgress, + WorkerJobType, +} from '@activepieces/shared' +import { FastifyInstance } from 'fastify' +import { redisConnections } from '../../../../src/app/database/redis-connections' +import { QueueName } from '../../../../src/app/workers/job' +import { jobBroker } from '../../../../src/app/workers/job-queue/job-broker' +import { jobQueue, JobType } from '../../../../src/app/workers/job-queue/job-queue' +import { mockAndSaveBasicSetup } from '../../../helpers/mocks' +import { setupTestEnvironment, teardownTestEnvironment } from '../../../helpers/test-setup' + +let app: FastifyInstance + +beforeAll(async () => { + app = await setupTestEnvironment() + await jobBroker(app.log).init() +}) + +afterAll(async () => { + await jobBroker(app.log).close() + await teardownTestEnvironment() +}) + +const jobKey = (jobId: string): string => `bull:${QueueName.WORKER_JOBS}:${jobId}` +const activeKey = (): string => `bull:${QueueName.WORKER_JOBS}:active` +const failedKey = (): string => `bull:${QueueName.WORKER_JOBS}:failed` +const waitKey = (): string => `bull:${QueueName.WORKER_JOBS}:wait` + +describe('jobBroker.tryDequeue — invalid-schema poison handling', () => { + it('fails the job as unrecoverable when migrated data still fails JobData.parse, instead of recycling', async () => { + const { mockPlatform, mockProject } = await mockAndSaveBasicSetup() + + const validJobData: ExecuteFlowJobData = { + jobType: WorkerJobType.EXECUTE_FLOW, + schemaVersion: LATEST_JOB_DATA_SCHEMA_VERSION, + projectId: mockProject.id, + platformId: mockPlatform.id, + flowId: apId(), + flowVersionId: apId(), + runId: apId(), + environment: RunEnvironment.PRODUCTION, + executionType: ExecutionType.BEGIN, + streamStepProgress: StreamStepProgress.NONE, + payload: { type: 'inline', value: null }, + logsFileId: apId(), + logsUploadUrl: 'https://example.invalid/v1/flow-runs/logs?token=x', + } + + const jobId = apId() + await jobQueue(app.log).add({ + type: JobType.ONE_TIME, + id: jobId, + data: validJobData, + }) + + const redis = await redisConnections.useExisting() + + const poisonedRaw = JSON.stringify({ + jobType: WorkerJobType.EXECUTE_FLOW, + schemaVersion: LATEST_JOB_DATA_SCHEMA_VERSION, + projectId: mockProject.id, + platformId: mockPlatform.id, + runId: apId(), + executionType: 'BEGIN', + }) + await redis.hset(jobKey(jobId), 'data', poisonedRaw) + + const polled = await jobBroker(app.log).poll() + + expect(polled).toBeNull() + + const failedAfter = await redis.zrange(failedKey(), 0, -1) + const activeAfter = await redis.lrange(activeKey(), 0, -1) + const waitAfter = await redis.lrange(waitKey(), 0, -1) + + expect(failedAfter).toContain(jobId) + expect(activeAfter).not.toContain(jobId) + expect(waitAfter).not.toContain(jobId) + + const failedReason = await redis.hget(jobKey(jobId), 'failedReason') + expect(failedReason).toContain('Job data failed schema validation after migration') + }) +}) diff --git a/packages/server/api/test/unit/app/workers/migrations/job-data-migrations.test.ts b/packages/server/api/test/unit/app/workers/migrations/job-data-migrations.test.ts index 3bc4d08dedd..834908dd5ba 100644 --- a/packages/server/api/test/unit/app/workers/migrations/job-data-migrations.test.ts +++ b/packages/server/api/test/unit/app/workers/migrations/job-data-migrations.test.ts @@ -1,8 +1,8 @@ -import { ExecuteFlowJobData, UploadLogsBehavior, WorkerJobType } from '@activepieces/shared' +import { ExecuteFlowJobData, ExecutionType, JobData, RunEnvironment, StreamStepProgress, UploadLogsBehavior, WorkerJobType } from '@activepieces/shared' import { FastifyBaseLogger } from 'fastify' import { beforeEach, describe, expect, it, vi } from 'vitest' -const TARGET_SCHEMA_VERSION = 7 +const TARGET_SCHEMA_VERSION = 8 const SECRET = 'job-data-migrations-test-secret' @@ -52,7 +52,7 @@ async function makeLegacyLogsUrl(behavior: UploadLogsBehavior): Promise return `https://old-api.example.com/v1/flow-runs/logs?token=${legacyToken}` } -function baseFlowJob(overrides?: Partial): ExecuteFlowJobData { +function baseFlowJob(overrides: Partial = {}): ExecuteFlowJobData { return { jobType: WorkerJobType.EXECUTE_FLOW, schemaVersion: 6, @@ -61,14 +61,14 @@ function baseFlowJob(overrides?: Partial): ExecuteFlowJobDat flowId: 'flow-1', flowVersionId: 'fv-1', runId: 'run-1', - environment: 'PRODUCTION', - executionType: 'BEGIN', - streamStepProgress: 'NONE', + environment: RunEnvironment.PRODUCTION, + executionType: ExecutionType.BEGIN, + streamStepProgress: StreamStepProgress.NONE, payload: { type: 'inline', value: {} }, logsUploadUrl: 'placeholder', logsFileId: 'file-1', ...overrides, - } as unknown as ExecuteFlowJobData + } } describe('jobMigrations reSignLogsUploadUrlWithAudience', () => { @@ -78,9 +78,8 @@ describe('jobMigrations reSignLogsUploadUrlWithAudience', () => { it('re-signs logsUploadUrl for EXECUTE_FLOW at schemaVersion 6 and bumps to latest', async () => { const logsUploadUrl = await makeLegacyLogsUrl(UploadLogsBehavior.UPLOAD_DIRECTLY) - const job = baseFlowJob({ logsUploadUrl }) - const migrated = await jobMigrations(mockLog).apply(job) as ExecuteFlowJobData + const migrated = await jobMigrations(mockLog).apply(baseFlowJob({ logsUploadUrl })) expect(mockConstructUploadUrl).toHaveBeenCalledWith({ logsFileId: 'file-1', @@ -88,8 +87,10 @@ describe('jobMigrations reSignLogsUploadUrlWithAudience', () => { flowRunId: 'run-1', behavior: UploadLogsBehavior.UPLOAD_DIRECTLY, }) - expect(migrated.logsUploadUrl).toContain('new-api.example.com') - expect(migrated.schemaVersion).toBe(TARGET_SCHEMA_VERSION) + expect(migrated).toMatchObject({ + schemaVersion: TARGET_SCHEMA_VERSION, + logsUploadUrl: expect.stringContaining('new-api.example.com'), + }) }) it('preserves REDIRECT_TO_S3 behavior from the legacy token', async () => { @@ -136,7 +137,7 @@ describe('jobMigrations reSignLogsUploadUrlWithAudience', () => { }) it('bumps schemaVersion without re-signing for non-EXECUTE_FLOW jobs', async () => { - const webhookJob = { + const migrated = await jobMigrations(mockLog).apply({ jobType: WorkerJobType.EXECUTE_WEBHOOK, schemaVersion: 6, projectId: 'proj-1', @@ -144,20 +145,109 @@ describe('jobMigrations reSignLogsUploadUrlWithAudience', () => { flowId: 'flow-1', requestId: 'req-1', payload: { type: 'inline', value: {} }, - } as unknown as ExecuteFlowJobData - - const migrated = await jobMigrations(mockLog).apply(webhookJob) + }) expect(mockConstructUploadUrl).not.toHaveBeenCalled() - expect((migrated as { schemaVersion: number }).schemaVersion).toBe(TARGET_SCHEMA_VERSION) + expect(migrated).toMatchObject({ schemaVersion: TARGET_SCHEMA_VERSION }) }) it('is a no-op when job is already at latest schemaVersion', async () => { - const job = baseFlowJob({ schemaVersion: TARGET_SCHEMA_VERSION, logsUploadUrl: 'https://ok/v1?token=x' }) + const migrated = await jobMigrations(mockLog).apply(baseFlowJob({ + schemaVersion: TARGET_SCHEMA_VERSION, + logsUploadUrl: 'https://ok/v1?token=x', + })) + + expect(mockConstructUploadUrl).not.toHaveBeenCalled() + expect(migrated).toMatchObject({ schemaVersion: TARGET_SCHEMA_VERSION }) + }) +}) + +describe('jobMigrations backfillRequiredExecuteFlowFields (v7 -> v8)', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('backfills missing streamStepProgress to NONE on v7 EXECUTE_FLOW jobs and bumps to v8', async () => { + const { streamStepProgress: _omit, ...withoutProgress } = baseFlowJob({ schemaVersion: 7, logsUploadUrl: 'https://ok/v1?token=x' }) + + const migrated = await jobMigrations(mockLog).apply(withoutProgress) + + expect(migrated).toMatchObject({ + schemaVersion: TARGET_SCHEMA_VERSION, + streamStepProgress: StreamStepProgress.NONE, + }) + expect(JobData.safeParse(migrated).success).toBe(true) + }) + + it('backfills streamStepProgress from legacy progressUpdateType on v7 jobs', async () => { + const { streamStepProgress: _omit, ...withoutProgress } = baseFlowJob({ schemaVersion: 7, logsUploadUrl: 'https://ok/v1?token=x' }) + + const migrated = await jobMigrations(mockLog).apply({ ...withoutProgress, progressUpdateType: 'TEST_FLOW' }) + + expect(migrated).toMatchObject({ + schemaVersion: TARGET_SCHEMA_VERSION, + streamStepProgress: StreamStepProgress.WEBSOCKET, + }) + }) - const migrated = await jobMigrations(mockLog).apply(job) as ExecuteFlowJobData + it('backfills missing workerHandlerId from legacy synchronousHandlerId on v7 jobs', async () => { + const { workerHandlerId: _omit, ...withoutWorker } = baseFlowJob({ schemaVersion: 7, logsUploadUrl: 'https://ok/v1?token=x' }) + + const migrated = await jobMigrations(mockLog).apply({ ...withoutWorker, synchronousHandlerId: 'handler-1' }) + + expect(migrated).toMatchObject({ + schemaVersion: TARGET_SCHEMA_VERSION, + workerHandlerId: 'handler-1', + }) + }) + + it('preserves existing streamStepProgress when already present on v7 jobs', async () => { + const migrated = await jobMigrations(mockLog).apply(baseFlowJob({ + schemaVersion: 7, + streamStepProgress: StreamStepProgress.WEBSOCKET, + logsUploadUrl: 'https://ok/v1?token=x', + })) + + expect(migrated).toMatchObject({ streamStepProgress: StreamStepProgress.WEBSOCKET }) + }) + + it('bumps schemaVersion without backfilling for non-EXECUTE_FLOW v7 jobs', async () => { + const migrated = await jobMigrations(mockLog).apply({ + jobType: WorkerJobType.EXECUTE_WEBHOOK, + schemaVersion: 7, + projectId: 'proj-1', + platformId: 'plat-1', + flowId: 'flow-1', + requestId: 'req-1', + payload: { type: 'inline', value: {} }, + }) expect(mockConstructUploadUrl).not.toHaveBeenCalled() - expect(migrated.schemaVersion).toBe(TARGET_SCHEMA_VERSION) + expect(migrated).toMatchObject({ schemaVersion: TARGET_SCHEMA_VERSION }) + expect(migrated).not.toHaveProperty('streamStepProgress') + }) + + it('repairs the production poison case (v7 EXECUTE_FLOW with no streamStepProgress nor legacy field) so JobData.parse succeeds', async () => { + const productionPoisonShape = { + jobType: WorkerJobType.EXECUTE_FLOW, + schemaVersion: 7, + projectId: 'RgJbQBPvuKsHxEf8pYvWH', + platformId: 'e6o1zp3Md80n0m9zPKs2c', + flowVersionId: 'z9NHiAw5LYzU3fyl0W6oN', + flowId: 'flow-x', + runId: '7o88bEwi3MuYCUJXYer9J', + environment: RunEnvironment.PRODUCTION, + executionType: ExecutionType.BEGIN, + payload: { type: 'inline', value: {} }, + logsFileId: 'file-1', + logsUploadUrl: 'https://ok/v1?token=x', + } + + expect(JobData.safeParse(productionPoisonShape).success).toBe(false) + + const migrated = await jobMigrations(mockLog).apply(productionPoisonShape) + + expect(migrated).toMatchObject({ schemaVersion: TARGET_SCHEMA_VERSION }) + expect(JobData.safeParse(migrated).success).toBe(true) }) }) diff --git a/packages/shared/package.json b/packages/shared/package.json index cd1f7c0025f..7b67347f316 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/shared", - "version": "0.71.4", + "version": "0.71.5", "type": "commonjs", "sideEffects": false, "main": "./dist/src/index.js", diff --git a/packages/shared/src/lib/automation/workers/job-data.ts b/packages/shared/src/lib/automation/workers/job-data.ts index 6071add4cbe..c784f4a0ab5 100644 --- a/packages/shared/src/lib/automation/workers/job-data.ts +++ b/packages/shared/src/lib/automation/workers/job-data.ts @@ -8,7 +8,7 @@ import { FlowVersion } from '../flows/flow-version' import { FlowTriggerType } from '../flows/triggers/trigger' import { PiecePackage } from '../pieces/piece' -export const LATEST_JOB_DATA_SCHEMA_VERSION = 7 +export const LATEST_JOB_DATA_SCHEMA_VERSION = 8 export const InlineJobPayload = z.object({ type: z.literal('inline'), From cac71804a326cc8647920b5d0868534d956eb46d Mon Sep 17 00:00:00 2001 From: Hazem Adel Date: Wed, 6 May 2026 15:23:52 +0300 Subject: [PATCH 6/9] fix(chat): fix markdown table rendering when header contains # (#13126) --- packages/web/src/components/prompt-kit/markdown.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/web/src/components/prompt-kit/markdown.tsx b/packages/web/src/components/prompt-kit/markdown.tsx index a378a4c9d62..5b36067c62d 100644 --- a/packages/web/src/components/prompt-kit/markdown.tsx +++ b/packages/web/src/components/prompt-kit/markdown.tsx @@ -19,8 +19,9 @@ export type MarkdownProps = { function normalizeMarkdownSpacing(markdown: string): string { let text = markdown; - // Ensure headings have a newline before them (fixes "text.## Heading" streaming artifact) - text = text.replace(/([^\n])(#{1,6}\s)/g, '$1\n\n$2'); + // Ensure headings have a blank line before them (fixes "text\n## Heading" streaming artifact). + // Uses multiline flag so ^ matches after every \n; only touches lines that start with #. + text = text.replace(/^(#{1,6}\s)/gm, '\n$1'); // Ensure blank lines between non-empty content lines (except inside tables/code/lists) const lines = text.split('\n'); From d326f0d1252e6f2e9a06a2d373dde7822b047aef Mon Sep 17 00:00:00 2001 From: Mo AbuAboud Date: Wed, 6 May 2026 14:29:36 +0200 Subject: [PATCH 7/9] fix(workers): drain BullMQ deferredFailure jobs to terminal failed state (#13120) --- .../api/src/app/workers/job-queue/job-broker.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/server/api/src/app/workers/job-queue/job-broker.ts b/packages/server/api/src/app/workers/job-queue/job-broker.ts index c9b17f71ca4..df827af2f5f 100644 --- a/packages/server/api/src/app/workers/job-queue/job-broker.ts +++ b/packages/server/api/src/app/workers/job-queue/job-broker.ts @@ -95,7 +95,7 @@ async function tryDequeue(worker: BullMQWorker, queueName: string, log: FastifyB { queueName, jobId: job.id, jobName: job.name, deferredFailure: job.deferredFailure }, '[jobBroker#tryDequeue] Failing job with deferred failure (BullMQ stalled limit exceeded)', ) - const { error: failError } = await tryCatch(() => job.moveToFailed(new Error(job.deferredFailure), token, false)) + const { error: failError } = await tryCatch(() => job.moveToFailed(new UnrecoverableError(job.deferredFailure), token, false)) if (failError) { log.error( { queueName, jobId: job.id, error: String(failError) }, @@ -202,11 +202,6 @@ async function runInterceptors({ jobId, jobData, job, log }: { jobId: string, jo return null } -function isStalledJobError(error: unknown): boolean { - const msg = error instanceof Error ? error.message : String(error) - return msg.includes('Missing lock') || msg.includes('job stalled') || msg.includes('Cannot read properties of null (reading \'moveToFinishedArgs\')') -} - function buildFailedReason(errorMessage: string, logs?: string): string { if (!logs) return errorMessage return `${errorMessage}\n${logs}` @@ -260,12 +255,7 @@ export const jobBroker = (log: FastifyBaseLogger) => ({ } }) if (error) { - if (isStalledJobError(error)) { - log.warn({ jobId: input.jobId, error: String(error), originalError: input.errorMessage }, '[jobBroker] Stalled job error during completeJob') - } - else { - log.error({ jobId: input.jobId, error: String(error), originalError: input.errorMessage }, '[jobBroker] Failed to move job to final state') - } + log.error({ jobId: input.jobId, error: String(error), originalError: input.errorMessage }, '[jobBroker] Failed to move job to final state — leaving for stalled-scan recovery') if (userJobData) { await engineResponseWatcher(log).publish(userJobData.webserverId, userJobData.requestId, { status: EngineResponseStatus.INTERNAL_ERROR, From f877468527c00b5e9136ed6cc945d22e449074af Mon Sep 17 00:00:00 2001 From: Hazem Adel Date: Wed, 6 May 2026 16:45:38 +0300 Subject: [PATCH 8/9] feat(chat): move chat feature from CE to EE (#13127) --- .agents/features/chat.md | 84 +++++++++++++++++++ packages/server/api/src/app/app.ts | 5 +- .../src/app/database/database-connection.ts | 2 +- .../app/{ => ee}/chat/chat-approval-gate.ts | 4 +- .../src/app/{ => ee}/chat/chat-compaction.ts | 0 .../src/app/{ => ee}/chat/chat-controller.ts | 2 +- .../{ => ee}/chat/chat-conversation-entity.ts | 2 +- .../src/app/{ => ee}/chat/chat-file-utils.ts | 0 .../app/{ => ee}/chat/chat-model-factory.ts | 0 .../api/src/app/{ => ee}/chat/chat-service.ts | 20 ++--- .../api/src/app/{ => ee}/chat/chat.module.ts | 2 +- .../app/{ => ee}/chat/history/chat-history.ts | 0 .../api/src/app/{ => ee}/chat/mcp/chat-mcp.ts | 6 +- .../app/{ => ee}/chat/prompt/chat-prompt.ts | 0 .../src/app/{ => ee}/chat/tools/chat-tools.ts | 14 ++-- .../app/ee/flags/enterprise-flags.hooks.ts | 1 + .../server/api/src/app/flags/flag.service.ts | 6 ++ packages/shared/package.json | 2 +- packages/shared/src/index.ts | 2 +- .../lib/core/common/security/permission.ts | 2 - packages/shared/src/lib/core/flag/flag.ts | 1 + .../src/lib/ee/authn/access-control-list.ts | 5 -- .../src/lib/{automation => ee}/chat/index.ts | 0 .../components/sidebar/dashboard/index.tsx | 11 ++- packages/web/src/lib/route-utils.ts | 3 - 25 files changed, 130 insertions(+), 44 deletions(-) create mode 100644 .agents/features/chat.md rename packages/server/api/src/app/{ => ee}/chat/chat-approval-gate.ts (92%) rename packages/server/api/src/app/{ => ee}/chat/chat-compaction.ts (100%) rename packages/server/api/src/app/{ => ee}/chat/chat-controller.ts (98%) rename packages/server/api/src/app/{ => ee}/chat/chat-conversation-entity.ts (96%) rename packages/server/api/src/app/{ => ee}/chat/chat-file-utils.ts (100%) rename packages/server/api/src/app/{ => ee}/chat/chat-model-factory.ts (100%) rename packages/server/api/src/app/{ => ee}/chat/chat-service.ts (95%) rename packages/server/api/src/app/{ => ee}/chat/chat.module.ts (80%) rename packages/server/api/src/app/{ => ee}/chat/history/chat-history.ts (100%) rename packages/server/api/src/app/{ => ee}/chat/mcp/chat-mcp.ts (95%) rename packages/server/api/src/app/{ => ee}/chat/prompt/chat-prompt.ts (100%) rename packages/server/api/src/app/{ => ee}/chat/tools/chat-tools.ts (96%) rename packages/shared/src/lib/{automation => ee}/chat/index.ts (100%) diff --git a/.agents/features/chat.md b/.agents/features/chat.md new file mode 100644 index 00000000000..3001c77bfd7 --- /dev/null +++ b/.agents/features/chat.md @@ -0,0 +1,84 @@ +# Chat Module + +## Summary +A platform-level AI chat assistant that lets users interact with an LLM to manage their Activepieces projects through natural language. The chat connects to the platform's configured AI provider, streams responses via the Vercel AI SDK, and exposes Activepieces resources (flows, tables, connections, runs) as callable tools through the project's MCP server. Conversations are persisted per-user with support for message compaction, file attachments, multi-project context switching, and a tool approval gate for destructive operations. + +## Key Files +- `packages/server/api/src/app/ee/chat/chat.module.ts` — module registration with `chatEnabled` plan gate +- `packages/server/api/src/app/ee/chat/chat-controller.ts` — HTTP endpoints (conversations CRUD, messages, tool approvals) +- `packages/server/api/src/app/ee/chat/chat-service.ts` — core business logic (conversation management, message streaming) +- `packages/server/api/src/app/ee/chat/chat-conversation-entity.ts` — ChatConversation TypeORM entity +- `packages/server/api/src/app/ee/chat/chat-model-factory.ts` — creates AI SDK `LanguageModel` from provider config (OpenAI, Anthropic, Google, Azure, Bedrock, Cloudflare, Custom) +- `packages/server/api/src/app/ee/chat/chat-compaction.ts` — long-conversation context management via summarization +- `packages/server/api/src/app/ee/chat/chat-approval-gate.ts` — Redis pub/sub gate for tool execution approval (5-min timeout) +- `packages/server/api/src/app/ee/chat/chat-file-utils.ts` — file attachment processing (base64, MIME validation, 10MB limit) +- `packages/server/api/src/app/ee/chat/tools/chat-tools.ts` — local LLM tools (title, project selection, action execution, cross-project listing) +- `packages/server/api/src/app/ee/chat/mcp/chat-mcp.ts` — connects to Activepieces MCP server for project-scoped tools with approval wrapping +- `packages/server/api/src/app/ee/chat/history/chat-history.ts` — reconstructs chat history from AI SDK `ModelMessage` format +- `packages/server/api/src/app/ee/chat/prompt/chat-prompt.ts` — builds system prompt from markdown templates in `src/assets/prompts/` +- `packages/shared/src/lib/ee/chat/index.ts` — shared Zod schemas and types (ChatConversation, request DTOs, ChatHistoryMessage) +- `packages/web/src/app/routes/chat-with-ai/index.tsx` — main chat page component +- `packages/web/src/app/routes/chat-with-ai/ai-chat-box.tsx` — chat interface with provider check, message streaming, tool approvals +- `packages/web/src/app/routes/chat-with-ai/conversation-list.tsx` — conversation history sidebar +- `packages/web/src/app/routes/chat-with-ai/components/` — sub-components (input, messages, model selector, project selector, tool approval form) +- `packages/web/src/features/chat/lib/chat-api.ts` — API client for `/v1/chat/*` endpoints +- `packages/web/src/features/chat/lib/use-chat.ts` — `useAgentChat()` hook managing conversation state +- `packages/web/src/features/chat/lib/use-tool-approval.ts` — hook for tool approval requests + +## Edition Availability +- Community (CE): not available (module not registered) +- Enterprise (EE): available when `platform.plan.chatEnabled` is true +- Cloud: available when `platform.plan.chatEnabled` is true + +## Domain Terms +- **ChatConversation** — a persisted conversation between a user and the AI assistant, scoped to a platform and user; optionally scoped to a project for tool access +- **Message compaction** — when a conversation exceeds a token threshold, older messages are summarized by the LLM and replaced with a summary to keep context within the model's window +- **Tool approval gate** — a Redis pub/sub mechanism that pauses destructive tool executions (delete, test, publish) until the user explicitly approves or denies in the UI; times out after 5 minutes +- **Local tools** — chat-specific tools not part of MCP: `ap_set_session_title`, `ap_select_project`, `ap_run_one_time_action`, `ap_list_across_projects` +- **MCP tools** — project-scoped tools loaded from the Activepieces MCP server when a project is selected; destructive ones are wrapped with the approval gate +- **AI provider** — a platform-configured LLM provider with an `enabledForChat` flag; the chat resolves the first enabled provider and its default model +- **Project context** — the currently selected project for a conversation; determines which MCP tools are available and scopes resource access + +## Data Model + +**ChatConversation**: id, platformId, userId, projectId (nullable), title (nullable), modelName (nullable), messages (JSONB array of `ModelMessage`), summary (text, nullable — compaction summary), summarizedUpToIndex (int, nullable — index up to which messages are summarized). +- Relations: platform (many-to-one), project (many-to-one, SET NULL on delete), user (many-to-one, CASCADE on delete) +- Index: `idx_chat_conversation_platform_user_created_id` on (platformId, userId, created, id) + +## Key Service Methods +- `createConversation()` — creates a new conversation for a user on a platform +- `listConversations()` — cursor-paginated list of user's conversations, ordered by creation date descending +- `getConversationOrThrow()` — fetches a conversation, enforcing ownership (platformId + userId) +- `updateConversation()` — updates title and/or modelName +- `deleteConversation()` — deletes a conversation after ownership check +- `getMessages()` — reconstructs `ChatHistoryMessage[]` from stored `ModelMessage[]` +- `setProjectContext()` — sets or clears the project scope, verifying user has access +- `sendMessage()` — the main streaming flow: resolves provider, connects MCP, builds prompt, runs `streamText()` with compaction, persists assistant response on completion + +## Local Tools +- `ap_set_session_title` — auto-names the conversation after the first exchange +- `ap_select_project` — switches project context (scopes MCP tools to that project) +- `ap_run_one_time_action` — executes a single piece action ad-hoc (e.g. "check my inbox"); auto-discovers connections across projects +- `ap_list_across_projects` — lists flows, tables, runs, or connections across all user-accessible projects + +## Endpoints +- `POST /v1/chat/conversations` — create conversation +- `GET /v1/chat/conversations` — list conversations (cursor, limit) +- `GET /v1/chat/conversations/:id` — get conversation +- `POST /v1/chat/conversations/:id` — update conversation (title, modelName) +- `DELETE /v1/chat/conversations/:id` — delete conversation +- `GET /v1/chat/conversations/:id/messages` — get conversation messages +- `POST /v1/chat/conversations/:id/messages` — send message (streaming response) +- `POST /v1/chat/tool-approvals/:gateId` — approve or deny a tool execution +- `POST /v1/chat/conversations/:id/project-context` — set project context + +All endpoints require `PrincipalType.USER` authentication at the platform level. + +## Message Flow +1. User sends message via `POST /conversations/:id/messages` +2. Service resolves AI provider, connects MCP client, builds system prompt with project list +3. If conversation is long, compaction summarizes older messages +4. `streamText()` streams the LLM response with local tools + MCP tools available +5. Destructive MCP tool calls pause and emit an approval request to the UI via the stream +6. User approves/denies via `POST /tool-approvals/:gateId`, unblocking the gate via Redis pub/sub +7. On stream completion, assistant messages are appended to the stored conversation diff --git a/packages/server/api/src/app/app.ts b/packages/server/api/src/app/app.ts index 43ecdec51ea..23877153999 100644 --- a/packages/server/api/src/app/app.ts +++ b/packages/server/api/src/app/app.ts @@ -12,7 +12,6 @@ import { platformAnalyticsModule } from './analytics/platform-analytics.module' import { setPlatformOAuthService } from './app-connection/app-connection-service/oauth2' import { appConnectionModule } from './app-connection/app-connection.module' import { authenticationModule } from './authentication/authentication.module' -import { chatModule } from './chat/chat.module' import { canaryRoutingMiddleware } from './core/canary/canary-routing.middleware' import { collaborativeModule } from './core/collaborative/collaborative.module' import { rateLimitModule } from './core/security/rate-limit' @@ -30,6 +29,7 @@ import { federatedAuthModule } from './ee/authentication/federated-authn/federat import { otpModule } from './ee/authentication/otp/otp-module' import { rbacMiddleware } from './ee/authentication/project-role/rbac-middleware' import { authnSsoSamlModule } from './ee/authentication/saml-authn/authn-sso-saml-module' +import { chatModule } from './ee/chat/chat.module' import { connectionKeyModule } from './ee/connection-keys/connection-key.module' import { customDomainModule } from './ee/custom-domains/custom-domain.module' import { domainHelper } from './ee/custom-domains/domain-helper' @@ -228,7 +228,6 @@ export const setupApp = async (app: FastifyInstance): Promise = await app.register(licenseKeysModule) await app.register(tablesModule) await app.register(knowledgeBaseModule) - await app.register(chatModule) await app.register(userModule) await app.register(templateModule) await app.register(userBadgeModule) @@ -289,6 +288,7 @@ export const setupApp = async (app: FastifyInstance): Promise = await app.register(globalConnectionModule) await app.register(secretManagersModule) await app.register(scimModule) + await app.register(chatModule) setPlatformOAuthService(platformOAuth2Service(app.log)) projectHooks.set(projectEnterpriseHooks) flagHooks.set(enterpriseFlagsHooks) @@ -318,6 +318,7 @@ export const setupApp = async (app: FastifyInstance): Promise = await app.register(globalConnectionModule) await app.register(secretManagersModule) await app.register(scimModule) + await app.register(chatModule) setPlatformOAuthService(platformOAuth2Service(app.log)) projectHooks.set(projectEnterpriseHooks) flagHooks.set(enterpriseFlagsHooks) diff --git a/packages/server/api/src/app/database/database-connection.ts b/packages/server/api/src/app/database/database-connection.ts index 2204f033529..86691c5ca7c 100644 --- a/packages/server/api/src/app/database/database-connection.ts +++ b/packages/server/api/src/app/database/database-connection.ts @@ -7,13 +7,13 @@ import { AIProviderEntity } from '../ai/ai-provider-entity' import { PlatformAnalyticsReportEntity } from '../analytics/platform-analytics-report.entity' import { AppConnectionEntity } from '../app-connection/app-connection.entity' import { UserIdentityEntity } from '../authentication/user-identity/user-identity-entity' -import { ChatConversationEntity } from '../chat/chat-conversation-entity' import { AlertEntity } from '../ee/alerts/alerts-entity' import { ApiKeyEntity } from '../ee/api-keys/api-key-entity' import { AppCredentialEntity } from '../ee/app-credentials/app-credentials.entity' import { AppSumoEntity } from '../ee/appsumo/appsumo.entity' import { AuditEventEntity } from '../ee/audit-logs/audit-event-entity' import { OtpEntity } from '../ee/authentication/otp/otp-entity' +import { ChatConversationEntity } from '../ee/chat/chat-conversation-entity' import { ConnectionKeyEntity } from '../ee/connection-keys/connection-key.entity' import { CustomDomainEntity } from '../ee/custom-domains/custom-domain.entity' import { OAuthAppEntity } from '../ee/oauth-apps/oauth-app.entity' diff --git a/packages/server/api/src/app/chat/chat-approval-gate.ts b/packages/server/api/src/app/ee/chat/chat-approval-gate.ts similarity index 92% rename from packages/server/api/src/app/chat/chat-approval-gate.ts rename to packages/server/api/src/app/ee/chat/chat-approval-gate.ts index c5a737524b3..63b55d21bd0 100644 --- a/packages/server/api/src/app/chat/chat-approval-gate.ts +++ b/packages/server/api/src/app/ee/chat/chat-approval-gate.ts @@ -1,5 +1,5 @@ -import { redisConnections } from '../database/redis-connections' -import { pubsub } from '../helper/pubsub' +import { redisConnections } from '../../database/redis-connections' +import { pubsub } from '../../helper/pubsub' const GATE_TIMEOUT_MS = 5 * 60 * 1000 const CHANNEL_PREFIX = 'tool-approval:' diff --git a/packages/server/api/src/app/chat/chat-compaction.ts b/packages/server/api/src/app/ee/chat/chat-compaction.ts similarity index 100% rename from packages/server/api/src/app/chat/chat-compaction.ts rename to packages/server/api/src/app/ee/chat/chat-compaction.ts diff --git a/packages/server/api/src/app/chat/chat-controller.ts b/packages/server/api/src/app/ee/chat/chat-controller.ts similarity index 98% rename from packages/server/api/src/app/chat/chat-controller.ts rename to packages/server/api/src/app/ee/chat/chat-controller.ts index 32accf834a2..c14f6f1aa6c 100644 --- a/packages/server/api/src/app/chat/chat-controller.ts +++ b/packages/server/api/src/app/ee/chat/chat-controller.ts @@ -10,7 +10,7 @@ import { pipeUIMessageStreamToResponse } from 'ai' import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod' import { StatusCodes } from 'http-status-codes' import { z } from 'zod' -import { securityAccess } from '../core/security/authorization/fastify-security' +import { securityAccess } from '../../core/security/authorization/fastify-security' import { chatApprovalGate } from './chat-approval-gate' import { chatService } from './chat-service' diff --git a/packages/server/api/src/app/chat/chat-conversation-entity.ts b/packages/server/api/src/app/ee/chat/chat-conversation-entity.ts similarity index 96% rename from packages/server/api/src/app/chat/chat-conversation-entity.ts rename to packages/server/api/src/app/ee/chat/chat-conversation-entity.ts index 797c24c146a..0f754bad005 100644 --- a/packages/server/api/src/app/chat/chat-conversation-entity.ts +++ b/packages/server/api/src/app/ee/chat/chat-conversation-entity.ts @@ -1,6 +1,6 @@ import { ChatConversation, Platform, Project } from '@activepieces/shared' import { EntitySchema } from 'typeorm' -import { ApIdSchema, BaseColumnSchemaPart } from '../database/database-common' +import { ApIdSchema, BaseColumnSchemaPart } from '../../database/database-common' type ChatConversationWithRelations = ChatConversation & { platform: Platform diff --git a/packages/server/api/src/app/chat/chat-file-utils.ts b/packages/server/api/src/app/ee/chat/chat-file-utils.ts similarity index 100% rename from packages/server/api/src/app/chat/chat-file-utils.ts rename to packages/server/api/src/app/ee/chat/chat-file-utils.ts diff --git a/packages/server/api/src/app/chat/chat-model-factory.ts b/packages/server/api/src/app/ee/chat/chat-model-factory.ts similarity index 100% rename from packages/server/api/src/app/chat/chat-model-factory.ts rename to packages/server/api/src/app/ee/chat/chat-model-factory.ts diff --git a/packages/server/api/src/app/chat/chat-service.ts b/packages/server/api/src/app/ee/chat/chat-service.ts similarity index 95% rename from packages/server/api/src/app/chat/chat-service.ts rename to packages/server/api/src/app/ee/chat/chat-service.ts index ed4d9a4ac73..9ae4fd09d61 100644 --- a/packages/server/api/src/app/chat/chat-service.ts +++ b/packages/server/api/src/app/ee/chat/chat-service.ts @@ -17,16 +17,16 @@ import { } from '@activepieces/shared' import { createUIMessageStream, LanguageModel, ModelMessage, stepCountIs, streamText } from 'ai' import { FastifyBaseLogger } from 'fastify' -import { aiProviderService } from '../ai/ai-provider-service' -import { repoFactory } from '../core/db/repo-factory' -import { buildPaginator } from '../helper/pagination/build-paginator' -import { paginationHelper } from '../helper/pagination/pagination-utils' -import { Order } from '../helper/pagination/paginator' -import { system } from '../helper/system/system' -import { AppSystemProp } from '../helper/system/system-props' -import { mcpProjectSelection } from '../mcp/mcp-project-selection' -import { projectService } from '../project/project-service' -import { userService } from '../user/user-service' +import { aiProviderService } from '../../ai/ai-provider-service' +import { repoFactory } from '../../core/db/repo-factory' +import { buildPaginator } from '../../helper/pagination/build-paginator' +import { paginationHelper } from '../../helper/pagination/pagination-utils' +import { Order } from '../../helper/pagination/paginator' +import { system } from '../../helper/system/system' +import { AppSystemProp } from '../../helper/system/system-props' +import { mcpProjectSelection } from '../../mcp/mcp-project-selection' +import { projectService } from '../../project/project-service' +import { userService } from '../../user/user-service' import { chatCompaction } from './chat-compaction' import { ChatConversationEntity } from './chat-conversation-entity' import { buildUserContentWithFiles } from './chat-file-utils' diff --git a/packages/server/api/src/app/chat/chat.module.ts b/packages/server/api/src/app/ee/chat/chat.module.ts similarity index 80% rename from packages/server/api/src/app/chat/chat.module.ts rename to packages/server/api/src/app/ee/chat/chat.module.ts index 9b7f64a3b80..b8055279e13 100644 --- a/packages/server/api/src/app/chat/chat.module.ts +++ b/packages/server/api/src/app/ee/chat/chat.module.ts @@ -1,5 +1,5 @@ import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod' -import { platformMustHaveFeatureEnabled } from '../ee/authentication/ee-authorization' +import { platformMustHaveFeatureEnabled } from '../authentication/ee-authorization' import { chatController } from './chat-controller' export const chatModule: FastifyPluginAsyncZod = async (app) => { diff --git a/packages/server/api/src/app/chat/history/chat-history.ts b/packages/server/api/src/app/ee/chat/history/chat-history.ts similarity index 100% rename from packages/server/api/src/app/chat/history/chat-history.ts rename to packages/server/api/src/app/ee/chat/history/chat-history.ts diff --git a/packages/server/api/src/app/chat/mcp/chat-mcp.ts b/packages/server/api/src/app/ee/chat/mcp/chat-mcp.ts similarity index 95% rename from packages/server/api/src/app/chat/mcp/chat-mcp.ts rename to packages/server/api/src/app/ee/chat/mcp/chat-mcp.ts index 2f3178545a8..b7912683528 100644 --- a/packages/server/api/src/app/chat/mcp/chat-mcp.ts +++ b/packages/server/api/src/app/ee/chat/mcp/chat-mcp.ts @@ -1,9 +1,9 @@ import { apId, isNil, tryCatch } from '@activepieces/shared' import { createMCPClient } from '@ai-sdk/mcp' import { FastifyBaseLogger } from 'fastify' -import { system } from '../../helper/system/system' -import { AppSystemProp } from '../../helper/system/system-props' -import { mcpOAuthTokenService } from '../../mcp/oauth/token/mcp-oauth-token.service' +import { system } from '../../../helper/system/system' +import { AppSystemProp } from '../../../helper/system/system-props' +import { mcpOAuthTokenService } from '../../../mcp/oauth/token/mcp-oauth-token.service' import { chatApprovalGate } from '../chat-approval-gate' type StreamWriter = { diff --git a/packages/server/api/src/app/chat/prompt/chat-prompt.ts b/packages/server/api/src/app/ee/chat/prompt/chat-prompt.ts similarity index 100% rename from packages/server/api/src/app/chat/prompt/chat-prompt.ts rename to packages/server/api/src/app/ee/chat/prompt/chat-prompt.ts diff --git a/packages/server/api/src/app/chat/tools/chat-tools.ts b/packages/server/api/src/app/ee/chat/tools/chat-tools.ts similarity index 96% rename from packages/server/api/src/app/chat/tools/chat-tools.ts rename to packages/server/api/src/app/ee/chat/tools/chat-tools.ts index 0e427f9e89b..9ed799f54ca 100644 --- a/packages/server/api/src/app/chat/tools/chat-tools.ts +++ b/packages/server/api/src/app/ee/chat/tools/chat-tools.ts @@ -2,13 +2,13 @@ import { FlowRunStatus, FlowStatus, Project, RunEnvironment } from '@activepiece import { tool } from 'ai' import { FastifyBaseLogger } from 'fastify' import { z } from 'zod' -import { appConnectionService } from '../../app-connection/app-connection-service/app-connection-service' -import { flowService } from '../../flows/flow/flow.service' -import { flowRunService } from '../../flows/flow-run/flow-run-service' -import { formatFlowLine } from '../../mcp/tools/ap-list-flows' -import { executeAdhocAction, formatRunSummary } from '../../mcp/tools/flow-run-utils' -import { mcpUtils } from '../../mcp/tools/mcp-utils' -import { tableService } from '../../tables/table/table.service' +import { appConnectionService } from '../../../app-connection/app-connection-service/app-connection-service' +import { flowService } from '../../../flows/flow/flow.service' +import { flowRunService } from '../../../flows/flow-run/flow-run-service' +import { formatFlowLine } from '../../../mcp/tools/ap-list-flows' +import { executeAdhocAction, formatRunSummary } from '../../../mcp/tools/flow-run-utils' +import { mcpUtils } from '../../../mcp/tools/mcp-utils' +import { tableService } from '../../../tables/table/table.service' import { chatPrompt } from '../prompt/chat-prompt' const RESOURCE_TYPES = ['flows', 'tables', 'runs', 'connections'] as const diff --git a/packages/server/api/src/app/ee/flags/enterprise-flags.hooks.ts b/packages/server/api/src/app/ee/flags/enterprise-flags.hooks.ts index 183ee686f3f..2313d160bcf 100644 --- a/packages/server/api/src/app/ee/flags/enterprise-flags.hooks.ts +++ b/packages/server/api/src/app/ee/flags/enterprise-flags.hooks.ts @@ -46,6 +46,7 @@ export const enterpriseFlagsHooks: FlagsServiceHooks = { modifiedFlags[ApFlagId.SHOW_BILLING_PAGE] = flags[ApFlagId.SHOW_BILLING_PAGE] && !platformUtils.isCustomerOnDedicatedDomain(platformWithPlan) modifiedFlags[ApFlagId.CLOUD_AUTH_ENABLED] = platform.cloudAuthEnabled modifiedFlags[ApFlagId.SHOW_BADGES] = !platformWithPlan.plan.embeddingEnabled + modifiedFlags[ApFlagId.SHOW_CHAT] = platformWithPlan.plan.chatEnabled modifiedFlags[ApFlagId.SHOW_PROJECT_MEMBERS] = platformWithPlan.plan.projectRolesEnabled modifiedFlags[ApFlagId.PUBLIC_URL] = await domainHelper.getPublicUrl({ path: '', diff --git a/packages/server/api/src/app/flags/flag.service.ts b/packages/server/api/src/app/flags/flag.service.ts index 60bf8fafb56..c9a8b621dd3 100644 --- a/packages/server/api/src/app/flags/flag.service.ts +++ b/packages/server/api/src/app/flags/flag.service.ts @@ -83,6 +83,12 @@ export const flagService = (log: FastifyBaseLogger) => ({ created, updated, }, + { + id: ApFlagId.SHOW_CHAT, + value: system.getEdition() !== ApEdition.COMMUNITY, + created, + updated, + }, { id: ApFlagId.SHOW_PROJECT_MEMBERS, value: system.getEdition() !== ApEdition.COMMUNITY, diff --git a/packages/shared/package.json b/packages/shared/package.json index 7b67347f316..79e03620051 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/shared", - "version": "0.71.5", + "version": "0.71.6", "type": "commonjs", "sideEffects": false, "main": "./dist/src/index.js", diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index ad753d67491..2a887e58113 100755 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -82,7 +82,7 @@ export * from './lib/automation/forms' export * from './lib/automation/mcp' export * from './lib/automation/agents' export * from './lib/automation/knowledge-base' -export * from './lib/automation/chat' +export * from './lib/ee/chat' export * from './lib/automation/tables' export * from './lib/automation/project-release/project-release' export * from './lib/automation/project-release/project-release.request' diff --git a/packages/shared/src/lib/core/common/security/permission.ts b/packages/shared/src/lib/core/common/security/permission.ts index 18b3e864b93..44241e4f885 100644 --- a/packages/shared/src/lib/core/common/security/permission.ts +++ b/packages/shared/src/lib/core/common/security/permission.ts @@ -25,8 +25,6 @@ export enum Permission { WRITE_TABLE = 'WRITE_TABLE', READ_KNOWLEDGE_BASE = 'READ_KNOWLEDGE_BASE', WRITE_KNOWLEDGE_BASE = 'WRITE_KNOWLEDGE_BASE', - READ_CHAT = 'READ_CHAT', - WRITE_CHAT = 'WRITE_CHAT', } export enum RoleType { diff --git a/packages/shared/src/lib/core/flag/flag.ts b/packages/shared/src/lib/core/flag/flag.ts index b6a60bea773..b3f5eba42b1 100755 --- a/packages/shared/src/lib/core/flag/flag.ts +++ b/packages/shared/src/lib/core/flag/flag.ts @@ -58,6 +58,7 @@ export enum ApFlagId { MAX_MCPS_PER_PROJECT = 'MAX_MCPS_PER_PROJECT', ENABLE_FLOW_ON_PUBLISH = 'ENABLE_FLOW_ON_PUBLISH', SHOW_ALERTS = 'SHOW_ALERTS', + SHOW_CHAT = 'SHOW_CHAT', SHOW_PROJECT_MEMBERS = 'SHOW_PROJECT_MEMBERS', TEMPLATES_CATEGORIES = 'TEMPLATES_CATEGORIES', diff --git a/packages/shared/src/lib/ee/authn/access-control-list.ts b/packages/shared/src/lib/ee/authn/access-control-list.ts index 139d289ee6e..a0d962a35f1 100644 --- a/packages/shared/src/lib/ee/authn/access-control-list.ts +++ b/packages/shared/src/lib/ee/authn/access-control-list.ts @@ -28,8 +28,6 @@ export const rolePermissions: Record = { Permission.WRITE_MCP, Permission.READ_KNOWLEDGE_BASE, Permission.WRITE_KNOWLEDGE_BASE, - Permission.READ_CHAT, - Permission.WRITE_CHAT, ], [DefaultProjectRole.EDITOR]: [ Permission.READ_APP_CONNECTION, @@ -52,8 +50,6 @@ export const rolePermissions: Record = { Permission.WRITE_MCP, Permission.READ_KNOWLEDGE_BASE, Permission.WRITE_KNOWLEDGE_BASE, - Permission.READ_CHAT, - Permission.WRITE_CHAT, ], [DefaultProjectRole.VIEWER]: [ Permission.READ_APP_CONNECTION, @@ -66,6 +62,5 @@ export const rolePermissions: Record = { Permission.READ_TABLE, Permission.READ_MCP, Permission.READ_KNOWLEDGE_BASE, - Permission.READ_CHAT, ], } diff --git a/packages/shared/src/lib/automation/chat/index.ts b/packages/shared/src/lib/ee/chat/index.ts similarity index 100% rename from packages/shared/src/lib/automation/chat/index.ts rename to packages/shared/src/lib/ee/chat/index.ts diff --git a/packages/web/src/app/components/sidebar/dashboard/index.tsx b/packages/web/src/app/components/sidebar/dashboard/index.tsx index 3eb8b3b4346..75fbe6e9141 100644 --- a/packages/web/src/app/components/sidebar/dashboard/index.tsx +++ b/packages/web/src/app/components/sidebar/dashboard/index.tsx @@ -1,4 +1,5 @@ import { + ApFlagId, isNil, PROJECT_COLOR_PALETTE, PlatformRole, @@ -44,6 +45,7 @@ import { } from '@/features/projects'; import { templatesTelemetryApi } from '@/features/templates'; import { useIsPlatformAdmin } from '@/hooks/authorization-hooks'; +import { flagsHooks } from '@/hooks/flags-hooks'; import { platformHooks } from '@/hooks/platform-hooks'; import { userHooks } from '@/hooks/user-hooks'; import { cn } from '@/lib/utils'; @@ -71,6 +73,7 @@ export function ProjectDashboardSidebar({ const navigate = useNavigate(); const { data: currentUser } = userHooks.useCurrentUser(); const { platform } = platformHooks.useCurrentPlatform(); + const { data: showChat } = flagsHooks.useFlag(ApFlagId.SHOW_CHAT); useEffect(() => { if (!searchOpen) { @@ -151,7 +154,7 @@ export function ProjectDashboardSidebar({ type: 'link', to: '/chat', label: t('Chat'), - show: platform.plan.chatEnabled, + show: showChat ?? false, icon: SendIcon, hasPermission: true, isSubItem: false, @@ -219,9 +222,9 @@ export function ProjectDashboardSidebar({ }, }; - const items = [chatLink, exploreLink, impactLink, leaderboardLink].filter( - permissionFilter, - ); + const items = [chatLink, exploreLink, impactLink, leaderboardLink] + .filter((item) => item.show !== false) + .filter(permissionFilter); return ( !embedState.hideSideNav && ( diff --git a/packages/web/src/lib/route-utils.ts b/packages/web/src/lib/route-utils.ts index 83737df8fd0..95440cd3659 100644 --- a/packages/web/src/lib/route-utils.ts +++ b/packages/web/src/lib/route-utils.ts @@ -23,9 +23,6 @@ export const determineDefaultRoute = ( if (checkAccess(Permission.READ_FLOW) || checkAccess(Permission.READ_TABLE)) { return authenticationSession.appendProjectRoutePrefix('/automations'); } - if (checkAccess(Permission.READ_CHAT)) { - return '/chat'; - } if (checkAccess(Permission.READ_RUN)) { return authenticationSession.appendProjectRoutePrefix('/runs'); } From 49d5bbca6001fa314a87292e1d6ece4060f7407a Mon Sep 17 00:00:00 2001 From: Abdul <106555838+AbdulTheActivePiecer@users.noreply.github.com> Date: Wed, 6 May 2026 16:46:35 +0300 Subject: [PATCH 9/9] feat: add grill me and design skills to codebase (#13128) --- .agents/skills/design/README.md | 192 +++++++++++++++++++++++++++++++ .agents/skills/design/SKILL.md | 180 +++++++++++++++++++++++++++++ .agents/skills/grill-me/SKILL.md | 13 +++ 3 files changed, 385 insertions(+) create mode 100644 .agents/skills/design/README.md create mode 100644 .agents/skills/design/SKILL.md create mode 100644 .agents/skills/grill-me/SKILL.md diff --git a/.agents/skills/design/README.md b/.agents/skills/design/README.md new file mode 100644 index 00000000000..3c1664f9f29 --- /dev/null +++ b/.agents/skills/design/README.md @@ -0,0 +1,192 @@ +# Activepieces Design System + +A design system for **Activepieces** — an open-source AI automation platform ("an open source replacement for Zapier"). The product lets both technical and non-technical users build automated workflows with 280+ integrations ("pieces") and exposes every piece as an MCP server for use with Claude, Cursor, Windsurf, etc. + +This folder contains the brand/visual foundations, CSS tokens, fonts, icon conventions, HTML preview cards for the Design System tab, and a UI kit that recreates core Activepieces product surfaces. + +--- + +## Source materials + +| Source | Location | +| --- | --- | +| Shadcn UI Kit for Figma + Pro Blocks (Oct 2025) | mounted `.fig` VFS — browse via `fig_ls /`, screenshot via `fig_screenshot` | +| Activepieces codebase | `github.com/yazeed-prog/activepieces` (`packages/web` is the React UI) | +| Canonical brand stylesheet | `packages/web/src/styles.css` (Tailwind v4 + Shadcn "new-york" style) | +| Brand logo | `packages/web/public/logo.svg` — purple mark `#8142E3` | +| Brand fonts | Inter (400/500/600/700/800) — provided in `uploads/` and `fonts/` | +| `Sentient-Variable.woff2` | provided as a display/display-alt exploration font (not used in shipping UI) | + +Activepieces uses **Shadcn/Radix UI** primitives on top of Tailwind, with **Lucide** as its icon library (confirmed in `packages/web/components.json` → `"iconLibrary": "lucide"`). Shadcn base color is `"neutral"`. + +--- + +## Index (what's in this folder) + +- `README.md` — this file +- `colors_and_type.css` — CSS variables for colors, fonts, spacing, radii, shadows, type ramp +- `SKILL.md` — cross-compatible Agent Skill definition for reuse in Claude Code +- `fonts/` — Inter family (.woff2/.ttf) + Sentient variable +- `assets/` — `logo.svg` (brand mark), piece-tile SVGs +- `preview/` — ~700px-wide HTML cards that populate the Design System tab +- `ui_kits/web/` — UI kit for the Activepieces web app (builder, dashboard, sidebar, forms showcase) +- `insights/` — Interactive Insights page (`Insights.html`) with reusable chart primitives (`InsightsCharts.jsx`) and page-scoped styles (`insights.css`). Composes the `ui_kits/web/` shell + primitives; adds Sparkline / LineChart / BarChart / Donut / Heatmap. Two layout variations (Classic dashboard + Editorial narrative) switchable via Tweaks. Respects light/dark toggle. + +### `ui_kits/web/` component inventory + +The kit is plain React via Babel standalone — no build step. Single `app.css` defines the `ap-*` class vocabulary for both light and dark modes. + +**Primitives** (`Primitives.jsx`): + +| Component | Variants / sizes | Notes | +| --- | --- | --- | +| `Button` | `default / secondary / outline / ghost / destructive / link` × `xs / sm / default / lg / icon / icon-sm / icon-lg` | Matches Shadcn button API. Supports `loading`, leading/trailing slots. | +| `Badge` | `default / secondary / outline / destructive / success / warning` | Optional `dot`. | +| `Input` | default 36px, `thin` 32px | Optional left `icon`. | +| `Textarea` | auto-grow 1–5 rows | | +| `Label` | `required` flag adds red asterisk | Paired via `htmlFor`. | +| `Checkbox` | `checked = true / false / "indeterminate"` | `Minus` glyph on indeterminate. | +| `Switch` | optional `checkedIcon` + `uncheckedIcon` | Thumb translates 20px. | +| `RadioGroup` + `RadioGroupItem` | Context-driven selection | | +| `Slider` | single-thumb, 0..100 | Filled track in primary. | +| `Progress` | 0..100 | | +| `Skeleton` | — | Animated-pulse placeholder. | +| `Avatar` | `size`, `initials`, `src`, `color` | | +| `Kbd` | — | Inline keyboard-key chip. | +| `Alert` | `default / primary / warning / destructive / success` | Optional `icon` + `title`. | +| `Tabs` + `TabsList` + `TabsTrigger` + `TabsContent` | `default / pills / underline` | | +| `Separator` | `horizontal / vertical` | | + +**Overlays** (`Overlays.jsx`) — portal-based, all dismiss on Esc + outside click: + +| Component | Notes | +| --- | --- | +| `Popover` | Anchored to trigger; `placement` + `align` + `offset`; clamps to viewport. | +| `DropdownMenu` | `items` support `icon`, `shortcut`, `destructive`, `separator`, `disabled`. | +| `Modal` | Sizes `sm / md / lg`; `title` + `description` + `footer`; `black/50` backdrop, no blur. | +| `Tooltip` | 400ms show delay, 120ms fade. | +| `ToastProvider` + `useToast()` + `ToastHost` | Bottom-right stack; `.toast / .success / .error / .warning / .info` helpers; auto-dismiss. | + +**Screen views**: `Sidebar.jsx`, `TopBar.jsx`, `FlowsView.jsx`, `BuilderView.jsx`, `RunsView.jsx`, `ConnectionsView.jsx`, `AskAIView.jsx`, `Icons.jsx`. + +**Entry points**: `ui_kits/web/index.html` (full app shell) and `ui_kits/web/forms.html` (form-controls showcase). + +--- + +## Brand & product context + +**Product**: Activepieces is an all-in-one AI automation platform. The core surface is a visual **flow builder** (React + XYFlow) where users assemble triggers + actions from 280+ open-source **pieces** into runnable flows. Every piece doubles as an MCP server, so LLM agents can call them directly. + +**Audience**: mixed — "developers set up the tools, and anyone in the organization can use the no-code builder" (from README). Non-technical users live in the builder; developers contribute new pieces as typed npm packages. + +**Products / surfaces represented in this design system**: +1. **Web app** (`packages/web`) — the authenticated product: flow builder, runs, connections, tables, agents, settings. This is the only UI in scope; the marketing site is not in the repo. + +--- + +## CONTENT FUNDAMENTALS + +Activepieces copy is **functional, direct, and product-led**. It talks about workflows, pieces, and runs in concrete terms — no marketing puffery inside the app. + +- **Voice**: second-person ("**you** can build", "**your** flows"). Feature names and verbs lead; adjectives are rare. +- **Casing**: **Sentence case** for every UI string — headings, buttons, menu items, page titles. Proper nouns are the feature itself: "Pieces", "Flows", "Runs", "MCP", "Agents", "Connections". +- **Tone**: matter-of-fact and a little nerdy. The product README uses emoji headers (🤯 🔥 🧠 🛠️) but the *in-app UI does not* — inside the app, emoji are essentially absent and all iconography is Lucide. +- **Buttons**: verb-first, terse. "New flow", "Publish", "Connect", "Test step", "Run", "Save". No "Click here", no "Please". +- **Empty states / errors**: explain the state, then say what the user can do. Example pattern: *"No flows yet. Create your first flow to start automating."* +- **Microcopy examples (from repo strings & feature names)**: "Create a Piece", "Deploy", "Hot reloading for local piece development", "Chat Interface", "Form Interface", "Ask AI in Code Piece", "Human in the Loop". +- **Docs / README vibe**: slightly more playful, uses emoji section markers ("💖 Loved by Everyone", "🔒 Secure by Design"), short bullet explainers, bold lead-ins. Good for landing/docs — **not** for in-product UI. + +**Do**: "Your flow is live.", "Add a step", "Connect your Google account" +**Don't**: "Awesome! 🎉 Your flow is now live!", "Click here to add a step", "Please authorize Google" + +--- + +## VISUAL FOUNDATIONS + +### Palette +- **Primary — Purple** `hsl(257 74% 57%)` ≈ `#8142E3`. Used for: primary buttons, the logo, active sidebar item, add-step affordance, selection glow in the builder, key links. + - `--primary-100` `hsl(257 75% 85%)` for soft backgrounds / selection washes + - `--primary-300` `hsl(257 74% 25%)` for deep accents +- **Neutrals**: Tailwind `neutral` scale (50→950). `950` is near-black (`#0a0a0a`), used for body text and the dark-mode background. The whole app reads as crisp **white + near-black** with purple as the only accent. +- **Semantics**: Emerald (`success` 160 84% 39%), Rose (`destructive` 350 89% 60%), Amber (`warning` 38 92% 50%). Only the 500 step is used for the solid colour; 50/100 are the soft fill for alerts/toasts; 700 is for high-contrast text on light chips. +- **Dark mode**: `neutral-950` background, `neutral-800` popovers/secondary, `hsla(0,0%,100%,0.1)` borders. In the shipping app primary *shifts to blue* in dark mode — we keep it purple by default in this design system for brand consistency, but document the blue variant. + +### Typography +- **Inter** across the entire product (400 / 500 / 600 / 700 / 800). `font-feature-settings: 'rlig' 1, 'calt' 1`. +- **Display**: we also include **Sentient** (uploaded variable font) as an *optional* display/marketing alt. Not used in shipping product UI. +- **Ramp**: text-xss 10.4 / xs 12 / sm 14 / base 16 / lg 18 / xl 20 / 2xl 24 / **3xl 28** / **4xl 32** (the repo overrides 3xl/4xl to tighter sizes). +- **Body default is 14px (sm)**, not 16 — this is important. Dense, tool-like feel. +- **Headings** use `-0.01em` to `-0.02em` letter-spacing. + +### Spacing & layout +- **Base unit 4px**. Gap scale: 4, 8, 12, 16, 24, 32, 48, 64. +- **Borders are 1px** everywhere, colour `neutral-200` (light) / `white/10` (dark). Never thicker. +- **Radius**: base `0.625rem` (10px). `lg` = 10, `md` = 8, `sm` = 6, `xs` = 2. Inputs + buttons use `md`; cards and dialogs use `lg`. +- **No negative margins.** Repo AGENTS.md bans them explicitly — use `gap` or `padding`. + +### Surfaces +- Cards: white fill, 1px `neutral-200` border, `radius-lg`, **no shadow by default**. Shadow only on floating surfaces (popovers, dialogs, dropdowns). +- **The builder canvas** has a dotted pattern: background `#FBFBFB`, pattern dot `#b2b2b2`. This is a signature look. +- Shadows are subtle: `0 1px 3px rgba(0,0,0,0.06)` for cards-that-lift, `0 10px 15px -3px rgba(0,0,0,0.08)` for menus. **No coloured shadows** except the add-step button glow (`0 0 0 6px var(--primary-100)`). + +### Animation +- Tailwind `tw-animate-css` is in. Custom easing: `--ease-expand-out: cubic-bezier(0.35, 0, 0.25, 1)`. +- Durations are short: **200ms default**, 150ms hover. Long-running animations (typing, highlight) are rare and intentional. +- Common keyframes in the repo: `accordion-down/up`, `fade`, `highlight` (primary-100 → secondary fade), `typing`, `slide-in-from-bottom`, `primary-color-pulse`. +- **No bounces, no springs**, no "pop" scale easing. Everything glides. + +### States +- **Hover**: primary buttons darken to `/90`; secondary to `/80`; ghost buttons get `bg-gray-300/30`. Links underline. No scale, no elevation change. +- **Press/active**: same as hover — the product does **not** shrink or translate on press. +- **Focus-visible**: `3px` ring at `ring-color/50`, with `border-ring` — very visible, accessibility-first. +- **Disabled**: `opacity: 0.5`, `pointer-events: none`. No greyed-out variant swap. +- **Aria-invalid**: `border-destructive`, `ring-destructive/20`. + +### Imagery & backgrounds +- **Minimal imagery** inside the product. No hero photos, no illustrations in the main app. +- **Piece tiles**: small rounded-square icons (48×48) with an 8% tinted background and the piece's own logo. Code piece uses amber `#E5AE43`, etc. Each piece owns its colour. +- Marketing/docs imagery (not in-app): screenshots of the builder with the dotted canvas, animated GIFs showing flow creation. No abstract gradients, no AI "bluish-purple glow" tropes — just the real UI. +- **No full-bleed photography** anywhere in the app. + +### Transparency & blur +- Used very sparingly. Sidebar accent fill is `color-mix(in srgb, neutral-200 60%, transparent)` — a tinted translucent wash. Overlays on dialogs use `black/50`. **No backdrop-blur** in the shipping UI. + +### Layout rules +- Fixed left sidebar, fluid content. Top bar only in the builder (it shows flow name + publish/test). +- Max content width ~1400px; dense tables break out wider. +- **`cn()` from `@/lib/utils`** is mandatory for className composition (clsx + tailwind-merge). + +--- + +## ICONOGRAPHY + +**Lucide** (https://lucide.dev) is the canonical icon set — confirmed via `components.json` (`"iconLibrary": "lucide"`). Stroke-based, 1.5px strokes, 24×24 viewBox, rounded line caps. + +- **In HTML/JSX prototypes in this system, link Lucide from CDN**: + ```html + + ``` + Or use inline SVGs from https://lucide.dev. Sizes: default `16` (`size-4`), small `12` (`size-3`), large `20` (`size-5`). +- **Icon conventions**: icons sit left of text with 8px gap (`gap-2`). Ghost buttons and xs buttons get `size-3` icons, default `size-4`. +- **Piece icons**: each integration has its own SVG (Google, OpenAI, Slack, …). These live at `packages/web/src/assets/img/piece/` and as npm-published per-piece packages. **Copy the real SVG** — do not redraw. +- **Custom product glyphs**: a small set of custom SVGs for MCP, Cursor, Claude, Windsurf, auth providers. These live at `packages/web/src/assets/img/custom/`. We copy the MCP and code glyphs into `assets/`. +- **Emoji**: not used in product UI. Used lightly in the public README (🔥🤯🧠). Do not use in app. +- **Unicode icon chars** (✓, ×, arrows): not used — always a Lucide ``, ``, ``. + +--- + +## Tailwind + `cn()` conventions + +Activepieces is a Tailwind v4 codebase. When generating production-style code off this system: +- Always `cn(...classes)` from `@/lib/utils` — never template literals for `className`. +- Use design-token class names (`bg-primary`, `text-muted-foreground`, `border-border`, `rounded-md`) not raw hex / raw radii. +- Ban negative margins. Use `gap-*`, `p-*`, `space-*`. +- Prefer extending existing `/ui` components before creating new ones. + +--- + +## Caveats + +- The **Pro-Blocks** Figma pages (Landing, Application, etc.) are Shadcn's stock templates and do NOT reflect the real Activepieces marketing site (which isn't in the repo). We use them as secondary reference for Shadcn patterns only. +- Figma file says primary purple is `rgb(151,71,255)` (`#9747FF`). The **actual shipping** primary per `styles.css` is `hsl(257 74% 57%)` ≈ `#8142E3` (matches the logo). We use the shipping value — the Figma swatch is a slightly lighter preview variant. +- **Sentient** (uploaded) is included as a display option but **is not used in shipping Activepieces UI**. Treat as optional branding exploration only. diff --git a/.agents/skills/design/SKILL.md b/.agents/skills/design/SKILL.md new file mode 100644 index 00000000000..5530fbb21c7 --- /dev/null +++ b/.agents/skills/design/SKILL.md @@ -0,0 +1,180 @@ +--- +name: activepieces-design-system +description: Design system for Activepieces (open-source AI automation platform, "open source replacement for Zapier"). Use whenever designing, mocking, or building UI for Activepieces — the web app (flow builder, runs, connections, dashboard), docs, or marketing surfaces. Provides brand purple `#8142E3`, Inter type ramp with `sm` (14px) as body default, Tailwind neutrals, Lucide icons, Shadcn/Radix primitive conventions, the signature dotted-canvas builder background, and a recreated web UI kit. +--- + +# Activepieces Design System + +You are designing for **Activepieces** — an open-source AI automation platform where users assemble triggers + actions from 280+ "pieces" into automated flows. Every piece doubles as an MCP server. + +Read `README.md` in this folder **first** — it is the canonical reference. This file is a fast-loading summary for agent use. + +## Inventory + +- `README.md` — full spec (content fundamentals, visual foundations, iconography, caveats). Read first. +- `colors_and_type.css` — CSS tokens (colors, fonts, radii, shadows, spacing, type ramp). Import in every HTML file. +- `fonts/` — Inter 400/500/600/700/800 + `Sentient-Variable.woff2` (display alt, not used in app). +- `assets/logo.svg` — brand mark. Purple `#8142E3`. +- `assets/` — piece-tile SVGs (Shopify, Airtable, Google, OpenAI, Slack, Gmail) + MCP/code glyphs. +- `preview/` — ~700px-wide design-system cards (type, color, spacing, components). +- `ui_kits/web/` — high-fidelity recreation of the Activepieces web app. Entry `ui_kits/web/index.html`. Form-controls showcase at `ui_kits/web/forms.html`. Modular JSX via Babel standalone. Screens: Flows dashboard, Builder (dotted canvas + step panel), Runs table, Connections list, **Ask AI chat overlay** (Lottie-animated "thinking" loader at `assets/ai-loader.lottie.json`). Supports **light + dark mode** via `.dark` class on `` — toggle lives in the sidebar footer. +- `insights/` — Interactive **Insights** page. Entry `insights/Insights.html`. Composes the `ui_kits/web/` shell (Sidebar, TopBar, Primitives, Overlays) and adds chart primitives in `InsightsCharts.jsx`: `Sparkline`, `LineChart` (multi-series + hover crosshair + tooltip), `BarChart` (stacked/grouped), `Donut`, `Heatmap` (days × hours). Page-scoped styles in `insights.css` define the `ins-*` class vocabulary (stat cards, AI summary card, quota meter, flow/error/team rows, piece grid, narrative hero). Two layout variations — **Classic dashboard** (hero stats + chart + donut + quota + top flows/errors + heatmap + team + live feed) and **Editorial** (dark narrative hero that tells the week's story + 3 highlight cards + heatmap/feed). Time range picker (24h/7d/30d/90d), compare-to-previous toggle, scope tabs (Workspace/Flow/Piece/Teammate), clickable stat cards, hover tooltips. **Tweaks**: layout (Classic ↔ Editorial), chart style (line/bar), AI summary on/off, density. Respects light/dark toggle. + +### Component inventory (`ui_kits/web/`) + +- **`Primitives.jsx`** — form & content building blocks: + `Button` (default / secondary / outline / ghost / destructive / link; sizes xs / sm / default / lg / icon / icon-sm / icon-lg), + `Badge` (default / secondary / outline / destructive / success / warning, optional `dot`), + `Input` (with optional left `icon`, `thin` 32px variant), + `Textarea` (auto-grows 1–5 rows), + `Label` (supports `required` asterisk), + `Checkbox` (supports `checked="indeterminate"`), + `Switch` (optional checked/unchecked icons), + `RadioGroup` + `RadioGroupItem`, + `Slider` (single-thumb, 0..100 by default), + `Progress` (0..100), + `Skeleton`, + `Avatar`, `Kbd`, + `Alert` (default / primary / warning / destructive / success, with `icon` + `title`), + `Tabs` + `TabsList` + `TabsTrigger` + `TabsContent` (default / pills / underline), + `Separator` (horizontal / vertical). +- **`Overlays.jsx`** — portal-based floating surfaces, all with Esc + outside-click dismiss: + `Popover` (anchored, placement + align + offset), + `DropdownMenu` (items support `icon`, `shortcut`, `destructive`, `separator`, `disabled`), + `Modal` (sizes sm / md / lg, `title` + `description` + `footer`, black/50 backdrop, no blur), + `Tooltip` (400ms delay, 120ms fade), + `ToastProvider` + `useToast()` + `ToastHost` (bottom-right stack; `.toast / .success / .error / .warning / .info`). +- **Screen views** — `Sidebar.jsx`, `TopBar.jsx`, `FlowsView.jsx`, `BuilderView.jsx`, `RunsView.jsx`, `ConnectionsView.jsx`, `AskAIView.jsx`, `Icons.jsx`. +- **Styling** — single `app.css` defines the `ap-*` class vocabulary and light/dark tokens. No build step. + +## Hard rules (never violate) + +1. **Primary is purple `hsl(257 74% 57%)` / `#8142E3`** — the shipping value from `packages/web/src/styles.css`. Not the `#9747FF` swatch some Figma files show. **Primary stays purple in dark mode** too (brand continuity) — use `.dark.blue-primary` to opt back into the repo's blue-in-dark behaviour. +2. **Body text is 14px (`text-sm`), not 16**. Activepieces feels dense and tool-like. Headings use `-0.01em` to `-0.02em` tracking. +3. **Sentence case everywhere**: headings, buttons, menu items, page titles. Proper nouns only for feature names (Flows, Runs, Pieces, MCP, Agents, Connections). +4. **Lucide icons only**, 1.5–2px stroke, rounded caps. Default size `16` (`size-4`). Icon + text → `gap-2` (8px). No emoji in the product UI. No Unicode glyphs (✓ × ←) — always a Lucide component. +5. **Borders are 1px**, color `neutral-200` (light) / `white/14` (dark). Never thicker. (Note: repo ships `white/10` in dark — we bump to `14%` so dividers stay readable against `neutral-900` surfaces.) +6. **No negative margins.** Use `gap-*`, `p-*`, `space-*`. Explicitly banned in the repo's AGENTS.md. +7. **Cards: white fill, 1px border, `radius-lg` (10px), NO shadow by default.** Shadows only on floating surfaces (popovers, menus, dialogs). +8. **Main content is a floating card**: the sidebar blends with the outer shell (`neutral-50` in light, `neutral-950` in dark); the content area sits inside with `radius-xl` (12px), 1px border, `shadow-xs`, and 8px inset from the viewport edges. Matches the shipping app layout. +9. **Builder canvas has the dotted look** — background `#FBFBFB` (light) / `#171717` (dark), radial-gradient dots `#b2b2b2 1px` at `16px 16px`. This is signature. +10. **Hover states darken only** — primary buttons go to `/90`, secondary `/80`, ghost `bg-gray-300/30`. No scale, no translate, no elevation change on hover or press. +11. **Focus-visible is a `3px` ring at `ring-color/50`** — accessibility-first, very visible. +12. **Disabled: `opacity: 0.5; pointer-events: none`.** No greyed-out variant. +13. **`cn()` from `@/lib/utils`** is mandatory for className composition in production-style code. Use design-token classes (`bg-primary`, `text-muted-foreground`, `border-border`, `rounded-md`), never raw hex. + +## Voice & copy + +Matter-of-fact, second-person, verb-first. No "Click here". No "Please". No hype. + +- **Buttons**: "New flow", "Publish", "Connect", "Test step", "Save", "Run" +- **Empty states**: state the fact, then the action. *"No flows yet. Create your first flow to start automating."* +- **Do**: "Your flow is live.", "Add a step", "Connect your Google account" +- **Don't**: "Awesome! 🎉 Your flow is now live!", "Click here to add a step", "Please authorize Google" + +## Quick-reference tokens + +### Colors +```css +/* Primary */ +--ap-primary: hsl(257 74% 57%); /* #8142E3 — the brand purple */ +--ap-primary-100: hsl(257 75% 85%); /* soft wash, selection, add-step glow */ +--ap-primary-300: hsl(257 74% 25%); /* deep accent, text on primary-100 */ + +/* Neutrals (Tailwind neutral) */ +--ap-neutral-50: #fafafa; +--ap-neutral-100: #f5f5f5; +--ap-neutral-200: #e5e5e5; /* borders */ +--ap-neutral-600: #525252; /* muted text */ +--ap-neutral-700: #404040; +--ap-neutral-900: #171717; +--ap-neutral-950: #0a0a0a; /* body text, dark bg */ + +/* Semantics (only 500 used as solid) */ +--ap-success: hsl(160 84% 39%); /* emerald */ +--ap-destructive: hsl(350 89% 60%); /* rose */ +--ap-warning: hsl(38 92% 50%); /* amber */ +``` + +### Type ramp (Inter) +| Token | Size | Usage | +|---|---|---| +| `xs` | 12px | metadata, badges | +| `sm` | 14px | **body default**, buttons, inputs | +| `base` | 16px | larger body | +| `lg` | 18px | card titles | +| `xl` | 20px | section titles | +| `2xl` | 24px | page titles | +| `3xl` | 28px | display (repo overrides from 30) | +| `4xl` | 32px | display (repo overrides from 36) | + +### Spacing / radius / shadow +- Gap scale: **4, 8, 12, 16, 24, 32, 48, 64** (base 4px). +- Radii: `xs 2` / `sm 6` / `md 8` / `lg 10` / `xl 12`. Inputs & buttons = `md`. Cards & dialogs = `lg`. +- Shadows: `sm` `0 1px 3px rgba(0,0,0,0.06)` (cards that lift), `md` `0 10px 15px -3px rgba(0,0,0,0.08)` (menus). **No colored shadows** except the add-step button: `box-shadow: 0 0 0 6px var(--primary-100)`. + +### Button variants +`default` (purple), `secondary` (near-black), `outline` (white + border), `ghost` (transparent), `destructive` (rose), `link` (underlined purple). Sizes: `xs 24` / `sm 30` / `default 36` / `lg 40` (height in px) + `icon` / `icon-sm` / `icon-lg` (square). Icon-only buttons: square with `radius-md`. + +### Form controls (ships in `Primitives.jsx`) +- **Input** 36px default, 32px `thin`; supports left icon. +- **Textarea** auto-grows 1–5 rows. +- **Checkbox** supports `checked="indeterminate"` (shows `Minus` glyph). +- **Switch** 36×20px track, 20px thumb translates on check. +- **RadioGroup** / **RadioGroupItem** with context-driven selection. +- **Slider** single-thumb, filled track in primary. +- **Progress** `value` 0..100, indicator translates. +- **Alert** `default / primary / warning / destructive / success` with optional icon + title. +- **Tabs** `default` (boxed), `pills`, `underline` — use `pills` for segmented controls, `underline` for page-level nav. +- All controls respect the global focus-visible ring (`3px ring/50`) and the `opacity-50 / pointer-events-none` disabled pattern. + +### Animation +- Default `200ms`, hover `150ms`. Custom ease: `cubic-bezier(0.35, 0, 0.25, 1)` (`--ease-expand-out`). +- **No bounces, no springs, no pop-scale.** Everything glides. + +## Iconography + +```html + + +``` +Or inline SVGs from https://lucide.dev. Piece/integration icons: copy the real SVG from `packages/web/src/assets/img/piece/` — do not redraw. Piece tile is 38–48px rounded-square with an 8% tinted background of the piece's brand color. + +## Surfaces & layout + +- Fixed 260px left sidebar, fluid content. Brand + workspace switcher top-left, user pill bottom-left, **theme toggle** (☀️/🌙) just above Settings. +- The content area is a **floating card** — 8px inset, `radius-xl`, 1px border, `shadow-xs`. Sidebar blends with the outer shell. +- Top bar only appears **inside the builder** (flow name + Test/Publish). Other screens use a page header with title + subtitle + actions. +- Max content width ~1400px; dense tables may break out wider. +- **Dialogs**: `radius-lg`, white, `shadow-md`, `black/50` overlay, no backdrop-blur. +- **Ask AI overlay**: centered sheet, 520px wide, conversational. Uses the Lottie loader (`assets/ai-loader.lottie.json`) while awaiting a response. + +## Dark mode + +- Toggle via `.dark` on ``. Persist with `localStorage['ap-theme']`. +- Surfaces: outer shell `neutral-950`, floating card `neutral-900`, popovers/muted `neutral-800`, accent `neutral-700`. +- **Primary stays purple** (`hsl(257 74% 57%)`) — brand continuity. Opt back into the repo's blue-in-dark with `.dark.blue-primary`. +- Semantics lift from `-500` → `-400` for legibility on dark backgrounds (`success hsl(160 60% 52%)`, `destructive hsl(351 95% 72%)`, `warning hsl(43 97% 56%)`). +- Dividers: `hsla(0, 0%, 100%, 0.14)` — 14% white, bumped from the repo's 10% so borders stay readable against `neutral-900`. + +## When to use the UI kit vs. build fresh + +- **Use `ui_kits/web/` as your starting point** for any web-app screen (Flows list, Builder, Runs, Connections). Copy components, don't re-derive. It uses vanilla React + Babel standalone and a single `app.css` — no build step. +- **Tweak-compose**: match the CSS class vocabulary (`ap-page`, `ap-topbar`, `ap-btn`, `ap-badge`, `ap-side-item`, `ap-step`, `ap-canvas`, `ap-table`, `ap-trow`). +- **For production React code** targeting the real repo: write Tailwind v4 + Shadcn components and use `cn()`. The CSS variables in `colors_and_type.css` mirror the repo's `:root` tokens so values stay consistent. + +## Known gotchas + +- Figma file shows primary as `#9747FF`. **Ignore it.** Shipping primary is `#8142E3` (from `packages/web/src/styles.css` and the logo). +- The repo's dark mode shifts primary to **blue**. This system keeps primary **purple** in both modes for brand consistency; document the blue variant only if the user explicitly asks for dark-mode fidelity. +- `Sentient-Variable.woff2` is an *optional* display/marketing font — **not used in shipping product UI**. Only use if explicitly doing branding/marketing exploration. +- The Pro-Blocks Figma pages (Landing, Application, etc.) are Shadcn stock templates, not Activepieces marketing. Use as Shadcn pattern reference only. +- `packages/web` is the only shipping UI surface in the repo. There is no marketing-site code to reference. + +## Starting a new design + +1. Read `README.md` for depth. +2. Import `colors_and_type.css` in your HTML. +3. Load Inter (already in `fonts/`) and Lucide via CDN. +4. If building a web-app screen: open `ui_kits/web/index.html`, copy the relevant component file(s), and compose. +5. Use sentence case, 14px body, 1px borders, purple `#8142E3` only for primary action + brand. Nothing else. diff --git a/.agents/skills/grill-me/SKILL.md b/.agents/skills/grill-me/SKILL.md new file mode 100644 index 00000000000..b961f39bef9 --- /dev/null +++ b/.agents/skills/grill-me/SKILL.md @@ -0,0 +1,13 @@ +--- +name: grill-me +description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me". +--- + +Interview me relentlessly about every aspect of this plan until +we reach a shared understanding. Walk down each branch of the design +tree resolving dependencies between decisions one by one. + +If a question can be answered by exploring the codebase, explore +the codebase instead. + +For each question, provide your recommended answer. \ No newline at end of file