From 64b3f984886ddea76068e2819adf457b32338938 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:23:39 -0800 Subject: [PATCH 01/36] feat(copilot): add commands (#2797) * Slash commands v0 * Web tools * Web * Fix popover * Fix commands ui * Fix for context mentions too * Improvem tool names for options and config * Fix thinking text scroll * Fix ishosted * Ui * Ui * Subagent parallelization * Fix ui * Fix lint * Fix superagent * Dont collapse info and super --- apps/sim/app/api/copilot/chat/route.ts | 3 + .../components/markdown-renderer.tsx | 564 ++++++++---------- .../components/smooth-streaming.tsx | 124 ++-- .../components/thinking-block.tsx | 193 ++++-- .../copilot-message/copilot-message.tsx | 60 +- .../components/tool-call/tool-call.tsx | 69 ++- .../components/user-input/components/index.ts | 1 + .../components/slash-menu/slash-menu.tsx | 249 ++++++++ .../hooks/use-context-management.ts | 45 +- .../user-input/hooks/use-mention-menu.ts | 137 ++++- .../user-input/hooks/use-mention-tokens.ts | 17 +- .../components/user-input/user-input.tsx | 234 +++++++- apps/sim/lib/copilot/api.ts | 1 + .../tools/client/blocks/get-block-config.ts | 5 +- .../tools/client/blocks/get-block-options.ts | 5 +- .../copilot/tools/client/init-tool-configs.ts | 1 + .../tools/client/other/crawl-website.ts | 53 ++ .../tools/client/other/get-page-contents.ts | 54 ++ .../copilot/tools/client/other/scrape-page.ts | 53 ++ .../tools/client/other/search-online.ts | 36 +- .../copilot/tools/client/other/superagent.ts | 56 ++ apps/sim/stores/panel/copilot/store.ts | 91 +-- apps/sim/stores/panel/copilot/types.ts | 1 + 23 files changed, 1522 insertions(+), 530 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/slash-menu.tsx create mode 100644 apps/sim/lib/copilot/tools/client/other/crawl-website.ts create mode 100644 apps/sim/lib/copilot/tools/client/other/get-page-contents.ts create mode 100644 apps/sim/lib/copilot/tools/client/other/scrape-page.ts create mode 100644 apps/sim/lib/copilot/tools/client/other/superagent.ts diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index 1c0067e835..2c2fc6c38f 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -97,6 +97,7 @@ const ChatMessageSchema = z.object({ }) ) .optional(), + commands: z.array(z.string()).optional(), }) /** @@ -132,6 +133,7 @@ export async function POST(req: NextRequest) { provider, conversationId, contexts, + commands, } = ChatMessageSchema.parse(body) // Ensure we have a consistent user message ID for this request const userMessageIdToUse = userMessageId || crypto.randomUUID() @@ -462,6 +464,7 @@ export async function POST(req: NextRequest) { ...(integrationTools.length > 0 && { tools: integrationTools }), ...(baseTools.length > 0 && { baseTools }), ...(credentials && { credentials }), + ...(commands && commands.length > 0 && { commands }), } try { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer.tsx index dcc2dffd06..dc3299c50f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useEffect, useMemo, useState } from 'react' +import React, { memo, useCallback, useState } from 'react' import { Check, Copy } from 'lucide-react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' @@ -28,55 +28,95 @@ const getTextContent = (element: React.ReactNode): string => { return '' } -// Global layout fixes for markdown content inside the copilot panel -if (typeof document !== 'undefined') { - const styleId = 'copilot-markdown-fix' - if (!document.getElementById(styleId)) { - const style = document.createElement('style') - style.id = styleId - style.textContent = ` - /* Prevent any markdown content from expanding beyond the panel */ - .copilot-markdown-wrapper, - .copilot-markdown-wrapper * { - max-width: 100% !important; - } +/** + * Maps common language aliases to supported viewer languages + */ +const LANGUAGE_MAP: Record = { + js: 'javascript', + javascript: 'javascript', + jsx: 'javascript', + ts: 'javascript', + typescript: 'javascript', + tsx: 'javascript', + json: 'json', + python: 'python', + py: 'python', + code: 'javascript', +} - .copilot-markdown-wrapper p, - .copilot-markdown-wrapper li { - overflow-wrap: anywhere !important; - word-break: break-word !important; - } +/** + * Normalizes a language string to a supported viewer language + */ +function normalizeLanguage(lang: string): 'javascript' | 'json' | 'python' { + const normalized = (lang || '').toLowerCase() + return LANGUAGE_MAP[normalized] || 'javascript' +} - .copilot-markdown-wrapper a { - overflow-wrap: anywhere !important; - word-break: break-all !important; - } +/** + * Props for the CodeBlock component + */ +interface CodeBlockProps { + /** Code content to display */ + code: string + /** Language identifier from markdown */ + language: string +} - .copilot-markdown-wrapper code:not(pre code) { - white-space: normal !important; - overflow-wrap: anywhere !important; - word-break: break-word !important; - } +/** + * CodeBlock component with isolated copy state + * Prevents full markdown re-renders when copy button is clicked + */ +const CodeBlock = memo(function CodeBlock({ code, language }: CodeBlockProps) { + const [copied, setCopied] = useState(false) + + const handleCopy = useCallback(() => { + if (code) { + navigator.clipboard.writeText(code) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + }, [code]) - /* Reduce top margin for first heading (e.g., right after thinking block) */ - .copilot-markdown-wrapper > h1:first-child, - .copilot-markdown-wrapper > h2:first-child, - .copilot-markdown-wrapper > h3:first-child, - .copilot-markdown-wrapper > h4:first-child { - margin-top: 0.25rem !important; - } - ` - document.head.appendChild(style) - } -} + const viewerLanguage = normalizeLanguage(language) + const displayLanguage = language === 'code' ? viewerLanguage : language + + return ( +
+
+ {displayLanguage} + +
+ +
+ ) +}) /** * Link component with hover preview tooltip - * Displays full URL on hover for better UX - * @param props - Component props with href and children - * @returns Link element with tooltip preview */ -function LinkWithPreview({ href, children }: { href: string; children: React.ReactNode }) { +const LinkWithPreview = memo(function LinkWithPreview({ + href, + children, +}: { + href: string + children: React.ReactNode +}) { return ( @@ -94,7 +134,7 @@ function LinkWithPreview({ href, children }: { href: string; children: React.Rea ) -} +}) /** * Props for the CopilotMarkdownRenderer component @@ -105,274 +145,196 @@ interface CopilotMarkdownRendererProps { } /** - * CopilotMarkdownRenderer renders markdown content with custom styling - * Supports GitHub-flavored markdown, code blocks with syntax highlighting, - * tables, links with preview, and more - * - * @param props - Component props - * @returns Rendered markdown content + * Static markdown component definitions - optimized for LLM chat spacing + * Tighter spacing compared to traditional prose for better chat UX */ -export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRendererProps) { - const [copiedCodeBlocks, setCopiedCodeBlocks] = useState>({}) - - useEffect(() => { - const timers: Record = {} - - Object.keys(copiedCodeBlocks).forEach((key) => { - if (copiedCodeBlocks[key]) { - timers[key] = setTimeout(() => { - setCopiedCodeBlocks((prev) => ({ ...prev, [key]: false })) - }, 2000) - } - }) - - return () => { - Object.values(timers).forEach(clearTimeout) +const markdownComponents = { + // Paragraphs - tight spacing, no margin on last + p: ({ children }: React.HTMLAttributes) => ( +

+ {children} +

+ ), + + // Headings - minimal margins for chat context + h1: ({ children }: React.HTMLAttributes) => ( +

+ {children} +

+ ), + h2: ({ children }: React.HTMLAttributes) => ( +

+ {children} +

+ ), + h3: ({ children }: React.HTMLAttributes) => ( +

+ {children} +

+ ), + h4: ({ children }: React.HTMLAttributes) => ( +

+ {children} +

+ ), + + // Lists - compact spacing + ul: ({ children }: React.HTMLAttributes) => ( +
    + {children} +
+ ), + ol: ({ children }: React.HTMLAttributes) => ( +
    + {children} +
+ ), + li: ({ children }: React.LiHTMLAttributes) => ( +
  • + {children} +
  • + ), + + // Code blocks - handled by CodeBlock component + pre: ({ children }: React.HTMLAttributes) => { + let codeContent: React.ReactNode = children + let language = 'code' + + if ( + React.isValidElement<{ className?: string; children?: React.ReactNode }>(children) && + children.type === 'code' + ) { + const childElement = children as React.ReactElement<{ + className?: string + children?: React.ReactNode + }> + codeContent = childElement.props.children + language = childElement.props.className?.replace('language-', '') || 'code' } - }, [copiedCodeBlocks]) - - const markdownComponents = useMemo( - () => ({ - p: ({ children }: React.HTMLAttributes) => ( -

    - {children} -

    - ), - h1: ({ children }: React.HTMLAttributes) => ( -

    - {children} -

    - ), - h2: ({ children }: React.HTMLAttributes) => ( -

    - {children} -

    - ), - h3: ({ children }: React.HTMLAttributes) => ( -

    - {children} -

    - ), - h4: ({ children }: React.HTMLAttributes) => ( -

    - {children} -

    - ), - - ul: ({ children }: React.HTMLAttributes) => ( -
      - {children} -
    - ), - ol: ({ children }: React.HTMLAttributes) => ( -
      - {children} -
    - ), - li: ({ - children, - ordered, - }: React.LiHTMLAttributes & { ordered?: boolean }) => ( -
  • - {children} -
  • - ), - - pre: ({ children }: React.HTMLAttributes) => { - let codeContent: React.ReactNode = children - let language = 'code' - - if ( - React.isValidElement<{ className?: string; children?: React.ReactNode }>(children) && - children.type === 'code' - ) { - const childElement = children as React.ReactElement<{ - className?: string - children?: React.ReactNode - }> - codeContent = childElement.props.children - language = childElement.props.className?.replace('language-', '') || 'code' - } - - let actualCodeText = '' - if (typeof codeContent === 'string') { - actualCodeText = codeContent - } else if (React.isValidElement(codeContent)) { - actualCodeText = getTextContent(codeContent) - } else if (Array.isArray(codeContent)) { - actualCodeText = codeContent - .map((child) => - typeof child === 'string' - ? child - : React.isValidElement(child) - ? getTextContent(child) - : '' - ) - .join('') - } else { - actualCodeText = String(codeContent || '') - } - - const codeText = actualCodeText || 'code' - const codeBlockKey = `${language}-${codeText.substring(0, 30).replace(/\s/g, '-')}-${codeText.length}` - - const showCopySuccess = copiedCodeBlocks[codeBlockKey] || false - - const handleCopy = () => { - const textToCopy = actualCodeText - if (textToCopy) { - navigator.clipboard.writeText(textToCopy) - setCopiedCodeBlocks((prev) => ({ ...prev, [codeBlockKey]: true })) - } - } - - const normalizedLanguage = (language || '').toLowerCase() - const viewerLanguage: 'javascript' | 'json' | 'python' = - normalizedLanguage === 'json' - ? 'json' - : normalizedLanguage === 'python' || normalizedLanguage === 'py' - ? 'python' - : 'javascript' - - return ( -
    -
    - - {language === 'code' ? viewerLanguage : language} - - -
    - -
    + let actualCodeText = '' + if (typeof codeContent === 'string') { + actualCodeText = codeContent + } else if (React.isValidElement(codeContent)) { + actualCodeText = getTextContent(codeContent) + } else if (Array.isArray(codeContent)) { + actualCodeText = codeContent + .map((child) => + typeof child === 'string' + ? child + : React.isValidElement(child) + ? getTextContent(child) + : '' ) - }, - - code: ({ - inline, - className, - children, - ...props - }: React.HTMLAttributes & { className?: string; inline?: boolean }) => { - if (inline) { - return ( - - {children} - - ) - } - return ( - - {children} - - ) - }, - - strong: ({ children }: React.HTMLAttributes) => ( - {children} - ), - - b: ({ children }: React.HTMLAttributes) => ( - {children} - ), - - em: ({ children }: React.HTMLAttributes) => ( - {children} - ), - - i: ({ children }: React.HTMLAttributes) => ( - {children} - ), - - blockquote: ({ children }: React.HTMLAttributes) => ( -
    - {children} -
    - ), - - hr: () =>
    , - - a: ({ href, children, ...props }: React.AnchorHTMLAttributes) => ( - - {children} - - ), - - table: ({ children }: React.TableHTMLAttributes) => ( -
    - - {children} -
    -
    - ), - thead: ({ children }: React.HTMLAttributes) => ( - - {children} - - ), - tbody: ({ children }: React.HTMLAttributes) => ( - {children} - ), - tr: ({ children }: React.HTMLAttributes) => ( - - {children} - - ), - th: ({ children }: React.ThHTMLAttributes) => ( - - {children} - - ), - td: ({ children }: React.TdHTMLAttributes) => ( - - {children} - - ), + .join('') + } else { + actualCodeText = String(codeContent || '') + } - img: ({ src, alt, ...props }: React.ImgHTMLAttributes) => ( - {alt - ), - }), - [copiedCodeBlocks] - ) + return + }, + + // Inline code + code: ({ + className, + children, + ...props + }: React.HTMLAttributes & { className?: string }) => ( + + {children} + + ), + + // Text formatting + strong: ({ children }: React.HTMLAttributes) => ( + {children} + ), + b: ({ children }: React.HTMLAttributes) => ( + {children} + ), + em: ({ children }: React.HTMLAttributes) => ( + {children} + ), + i: ({ children }: React.HTMLAttributes) => ( + {children} + ), + + // Blockquote - compact + blockquote: ({ children }: React.HTMLAttributes) => ( +
    + {children} +
    + ), + + // Horizontal rule + hr: () =>
    , + + // Links + a: ({ href, children }: React.AnchorHTMLAttributes) => ( + {children} + ), + + // Tables - compact + table: ({ children }: React.TableHTMLAttributes) => ( +
    + + {children} +
    +
    + ), + thead: ({ children }: React.HTMLAttributes) => ( + {children} + ), + tbody: ({ children }: React.HTMLAttributes) => ( + {children} + ), + tr: ({ children }: React.HTMLAttributes) => ( + {children} + ), + th: ({ children }: React.ThHTMLAttributes) => ( + + {children} + + ), + td: ({ children }: React.TdHTMLAttributes) => ( + + {children} + + ), + + // Images + img: ({ src, alt, ...props }: React.ImgHTMLAttributes) => ( + {alt + ), +} +/** + * CopilotMarkdownRenderer renders markdown content with custom styling + * Optimized for LLM chat: tight spacing, memoized components, isolated state + * + * @param props - Component props + * @returns Rendered markdown content + */ +function CopilotMarkdownRenderer({ content }: CopilotMarkdownRendererProps) { return ( -
    +
    {content}
    ) } + +export default memo(CopilotMarkdownRenderer) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming.tsx index 71de980ce9..7dfe9af4ea 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming.tsx @@ -2,18 +2,38 @@ import { memo, useEffect, useRef, useState } from 'react' import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer' /** - * Character animation delay in milliseconds + * Minimum delay between characters (fast catch-up mode) */ -const CHARACTER_DELAY = 3 +const MIN_DELAY = 1 + +/** + * Maximum delay between characters (when waiting for content) + */ +const MAX_DELAY = 12 + +/** + * Default delay when streaming normally + */ +const DEFAULT_DELAY = 4 + +/** + * How far behind (in characters) before we speed up + */ +const CATCH_UP_THRESHOLD = 20 + +/** + * How close to content before we slow down + */ +const SLOW_DOWN_THRESHOLD = 5 /** * StreamingIndicator shows animated dots during message streaming - * Uses CSS classes for animations to follow best practices + * Used as a standalone indicator when no content has arrived yet * * @returns Animated loading indicator */ export const StreamingIndicator = memo(() => ( -
    +
    @@ -34,9 +54,39 @@ interface SmoothStreamingTextProps { isStreaming: boolean } +/** + * Calculates adaptive delay based on how far behind animation is from actual content + * + * @param displayedLength - Current displayed content length + * @param totalLength - Total available content length + * @returns Delay in milliseconds + */ +function calculateAdaptiveDelay(displayedLength: number, totalLength: number): number { + const charsRemaining = totalLength - displayedLength + + if (charsRemaining > CATCH_UP_THRESHOLD) { + // Far behind - speed up to catch up + // Scale from MIN_DELAY to DEFAULT_DELAY based on how far behind + const catchUpFactor = Math.min(1, (charsRemaining - CATCH_UP_THRESHOLD) / 50) + return MIN_DELAY + (DEFAULT_DELAY - MIN_DELAY) * (1 - catchUpFactor) + } + + if (charsRemaining <= SLOW_DOWN_THRESHOLD) { + // Close to content edge - slow down to feel natural + // The closer we are, the slower we go (up to MAX_DELAY) + const slowFactor = 1 - charsRemaining / SLOW_DOWN_THRESHOLD + return DEFAULT_DELAY + (MAX_DELAY - DEFAULT_DELAY) * slowFactor + } + + // Normal streaming speed + return DEFAULT_DELAY +} + /** * SmoothStreamingText component displays text with character-by-character animation - * Creates a smooth streaming effect for AI responses + * Creates a smooth streaming effect for AI responses with adaptive speed + * + * Uses adaptive pacing: speeds up when catching up, slows down near content edge * * @param props - Component props * @returns Streaming text with smooth animation @@ -45,74 +95,73 @@ export const SmoothStreamingText = memo( ({ content, isStreaming }: SmoothStreamingTextProps) => { const [displayedContent, setDisplayedContent] = useState('') const contentRef = useRef(content) - const timeoutRef = useRef(null) + const rafRef = useRef(null) const indexRef = useRef(0) - const streamingStartTimeRef = useRef(null) + const lastFrameTimeRef = useRef(0) const isAnimatingRef = useRef(false) - /** - * Handles content streaming animation - * Updates displayed content character by character during streaming - */ useEffect(() => { contentRef.current = content if (content.length === 0) { setDisplayedContent('') indexRef.current = 0 - streamingStartTimeRef.current = null return } if (isStreaming) { - if (streamingStartTimeRef.current === null) { - streamingStartTimeRef.current = Date.now() - } + if (indexRef.current < content.length && !isAnimatingRef.current) { + isAnimatingRef.current = true + lastFrameTimeRef.current = performance.now() - if (indexRef.current < content.length) { - const animateText = () => { + const animateText = (timestamp: number) => { const currentContent = contentRef.current const currentIndex = indexRef.current + const elapsed = timestamp - lastFrameTimeRef.current + + // Calculate adaptive delay based on how far behind we are + const delay = calculateAdaptiveDelay(currentIndex, currentContent.length) + + if (elapsed >= delay) { + if (currentIndex < currentContent.length) { + const newDisplayed = currentContent.slice(0, currentIndex + 1) + setDisplayedContent(newDisplayed) + indexRef.current = currentIndex + 1 + lastFrameTimeRef.current = timestamp + } + } - if (currentIndex < currentContent.length) { - const chunkSize = 1 - const newDisplayed = currentContent.slice(0, currentIndex + chunkSize) - - setDisplayedContent(newDisplayed) - indexRef.current = currentIndex + chunkSize - - timeoutRef.current = setTimeout(animateText, CHARACTER_DELAY) + if (indexRef.current < currentContent.length) { + rafRef.current = requestAnimationFrame(animateText) } else { isAnimatingRef.current = false } } - if (!isAnimatingRef.current) { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - } - - isAnimatingRef.current = true - animateText() - } + rafRef.current = requestAnimationFrame(animateText) + } else if (indexRef.current < content.length && isAnimatingRef.current) { + // Animation already running, it will pick up new content automatically } } else { + // Streaming ended - show full content immediately + if (rafRef.current) { + cancelAnimationFrame(rafRef.current) + } setDisplayedContent(content) indexRef.current = content.length isAnimatingRef.current = false - streamingStartTimeRef.current = null } return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) + if (rafRef.current) { + cancelAnimationFrame(rafRef.current) } isAnimatingRef.current = false } }, [content, isStreaming]) return ( -
    +
    ) @@ -121,7 +170,6 @@ export const SmoothStreamingText = memo( // Prevent re-renders during streaming unless content actually changed return ( prevProps.content === nextProps.content && prevProps.isStreaming === nextProps.isStreaming - // markdownComponents is now memoized so no need to compare ) } ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx index 54c7042e75..fbb7065f90 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useRef, useState } from 'react' +import { memo, useEffect, useRef, useState } from 'react' import clsx from 'clsx' import { ChevronUp } from 'lucide-react' import CopilotMarkdownRenderer from './markdown-renderer' @@ -8,18 +8,151 @@ import CopilotMarkdownRenderer from './markdown-renderer' /** * Max height for thinking content before internal scrolling kicks in */ -const THINKING_MAX_HEIGHT = 200 +const THINKING_MAX_HEIGHT = 150 + +/** + * Height threshold before gradient fade kicks in + */ +const GRADIENT_THRESHOLD = 100 /** * Interval for auto-scroll during streaming (ms) */ -const SCROLL_INTERVAL = 100 +const SCROLL_INTERVAL = 50 /** * Timer update interval in milliseconds */ const TIMER_UPDATE_INTERVAL = 100 +/** + * Thinking text streaming - much faster than main text + * Essentially instant with minimal delay + */ +const THINKING_DELAY = 0.5 +const THINKING_CHARS_PER_FRAME = 3 + +/** + * Props for the SmoothThinkingText component + */ +interface SmoothThinkingTextProps { + content: string + isStreaming: boolean +} + +/** + * SmoothThinkingText renders thinking content with fast streaming animation + * Uses gradient fade at top when content is tall enough + */ +const SmoothThinkingText = memo( + ({ content, isStreaming }: SmoothThinkingTextProps) => { + const [displayedContent, setDisplayedContent] = useState('') + const [showGradient, setShowGradient] = useState(false) + const contentRef = useRef(content) + const textRef = useRef(null) + const rafRef = useRef(null) + const indexRef = useRef(0) + const lastFrameTimeRef = useRef(0) + const isAnimatingRef = useRef(false) + + useEffect(() => { + contentRef.current = content + + if (content.length === 0) { + setDisplayedContent('') + indexRef.current = 0 + return + } + + if (isStreaming) { + if (indexRef.current < content.length && !isAnimatingRef.current) { + isAnimatingRef.current = true + lastFrameTimeRef.current = performance.now() + + const animateText = (timestamp: number) => { + const currentContent = contentRef.current + const currentIndex = indexRef.current + const elapsed = timestamp - lastFrameTimeRef.current + + if (elapsed >= THINKING_DELAY) { + if (currentIndex < currentContent.length) { + // Reveal multiple characters per frame for faster streaming + const newIndex = Math.min( + currentIndex + THINKING_CHARS_PER_FRAME, + currentContent.length + ) + const newDisplayed = currentContent.slice(0, newIndex) + setDisplayedContent(newDisplayed) + indexRef.current = newIndex + lastFrameTimeRef.current = timestamp + } + } + + if (indexRef.current < currentContent.length) { + rafRef.current = requestAnimationFrame(animateText) + } else { + isAnimatingRef.current = false + } + } + + rafRef.current = requestAnimationFrame(animateText) + } + } else { + // Streaming ended - show full content immediately + if (rafRef.current) { + cancelAnimationFrame(rafRef.current) + } + setDisplayedContent(content) + indexRef.current = content.length + isAnimatingRef.current = false + } + + return () => { + if (rafRef.current) { + cancelAnimationFrame(rafRef.current) + } + isAnimatingRef.current = false + } + }, [content, isStreaming]) + + // Check if content height exceeds threshold for gradient + useEffect(() => { + if (textRef.current && isStreaming) { + const height = textRef.current.scrollHeight + setShowGradient(height > GRADIENT_THRESHOLD) + } else { + setShowGradient(false) + } + }, [displayedContent, isStreaming]) + + // Apply vertical gradient fade at the top only when content is tall enough + const gradientStyle = + isStreaming && showGradient + ? { + maskImage: 'linear-gradient(to bottom, transparent 0%, black 30%, black 100%)', + WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, black 30%, black 100%)', + } + : undefined + + return ( +
    + +
    + ) + }, + (prevProps, nextProps) => { + return ( + prevProps.content === nextProps.content && prevProps.isStreaming === nextProps.isStreaming + ) + } +) + +SmoothThinkingText.displayName = 'SmoothThinkingText' + /** * Props for the ThinkingBlock component */ @@ -66,8 +199,8 @@ export function ThinkingBlock({ * Auto-collapses when streaming ends OR when following content arrives */ useEffect(() => { - // Collapse if streaming ended or if there's following content (like a tool call) - if (!isStreaming || hasFollowingContent) { + // Collapse if streaming ended, there's following content, or special tags arrived + if (!isStreaming || hasFollowingContent || hasSpecialTags) { setIsExpanded(false) userCollapsedRef.current = false setUserHasScrolledAway(false) @@ -77,7 +210,7 @@ export function ThinkingBlock({ if (!userCollapsedRef.current && content && content.trim().length > 0) { setIsExpanded(true) } - }, [isStreaming, content, hasFollowingContent]) + }, [isStreaming, content, hasFollowingContent, hasSpecialTags]) // Reset start time when streaming begins useEffect(() => { @@ -113,14 +246,14 @@ export function ThinkingBlock({ const isNearBottom = distanceFromBottom <= 20 const delta = scrollTop - lastScrollTopRef.current - const movedUp = delta < -2 + const movedUp = delta < -1 if (movedUp && !isNearBottom) { setUserHasScrolledAway(true) } - // Re-stick if user scrolls back to bottom - if (userHasScrolledAway && isNearBottom) { + // Re-stick if user scrolls back to bottom with intent + if (userHasScrolledAway && isNearBottom && delta > 10) { setUserHasScrolledAway(false) } @@ -133,7 +266,7 @@ export function ThinkingBlock({ return () => container.removeEventListener('scroll', handleScroll) }, [isExpanded, userHasScrolledAway]) - // Smart auto-scroll: only scroll if user hasn't scrolled away + // Smart auto-scroll: always scroll to bottom while streaming unless user scrolled away useEffect(() => { if (!isStreaming || !isExpanded || userHasScrolledAway) return @@ -141,20 +274,14 @@ export function ThinkingBlock({ const container = scrollContainerRef.current if (!container) return - const { scrollTop, scrollHeight, clientHeight } = container - const distanceFromBottom = scrollHeight - scrollTop - clientHeight - const isNearBottom = distanceFromBottom <= 50 - - if (isNearBottom) { - programmaticScrollRef.current = true - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth', - }) - window.setTimeout(() => { - programmaticScrollRef.current = false - }, 150) - } + programmaticScrollRef.current = true + container.scrollTo({ + top: container.scrollHeight, + behavior: 'auto', + }) + window.setTimeout(() => { + programmaticScrollRef.current = false + }, 16) }, SCROLL_INTERVAL) return () => window.clearInterval(intervalId) @@ -241,15 +368,11 @@ export function ThinkingBlock({
    - {/* Render markdown during streaming with thinking text styling */} -
    - - -
    +
    ) @@ -281,12 +404,12 @@ export function ThinkingBlock({
    - {/* Use markdown renderer for completed content */} -
    + {/* Completed thinking text - dimmed with markdown */} +
    diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx index 2cba10be86..be3af2f886 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx @@ -187,6 +187,7 @@ const CopilotMessage: FC = memo( ) // Memoize content blocks to avoid re-rendering unchanged blocks + // No entrance animations to prevent layout shift const memoizedContentBlocks = useMemo(() => { if (!message.contentBlocks || message.contentBlocks.length === 0) { return null @@ -205,14 +206,10 @@ const CopilotMessage: FC = memo( // Use smooth streaming for the last text block if we're streaming const shouldUseSmoothing = isStreaming && isLastTextBlock + const blockKey = `text-${index}-${block.timestamp || index}` return ( -
    0 ? 'opacity-100' : 'opacity-70' - } ${shouldUseSmoothing ? 'translate-y-0 transition-transform duration-100 ease-out' : ''}`} - > +
    {shouldUseSmoothing ? ( ) : ( @@ -224,29 +221,33 @@ const CopilotMessage: FC = memo( if (block.type === 'thinking') { // Check if there are any blocks after this one (tool calls, text, etc.) const hasFollowingContent = index < message.contentBlocks!.length - 1 + // Check if special tags (options, plan) are present - should also close thinking + const hasSpecialTags = !!(parsedTags?.options || parsedTags?.plan) + const blockKey = `thinking-${index}-${block.timestamp || index}` + return ( -
    +
    ) } if (block.type === 'tool_call') { + const blockKey = `tool-${block.toolCall.id}` + return ( -
    +
    ) } return null }) - }, [message.contentBlocks, isStreaming]) + }, [message.contentBlocks, isStreaming, parsedTags]) if (isUser) { return ( @@ -279,6 +280,7 @@ const CopilotMessage: FC = memo( onModeChange={setMode} panelWidth={panelWidth} clearOnSubmit={false} + initialContexts={message.contexts} /> {/* Inline Checkpoint Discard Confirmation - shown below input in edit mode */} @@ -346,14 +348,18 @@ const CopilotMessage: FC = memo( const contexts: any[] = Array.isArray((message as any).contexts) ? ((message as any).contexts as any[]) : [] - const labels = contexts - .filter((c) => c?.kind !== 'current_workflow') - .map((c) => c?.label) - .filter(Boolean) as string[] - if (!labels.length) return text + + // Build tokens with their prefixes (@ for mentions, / for commands) + const tokens = contexts + .filter((c) => c?.kind !== 'current_workflow' && c?.label) + .map((c) => { + const prefix = c?.kind === 'slash_command' ? '/' : '@' + return `${prefix}${c.label}` + }) + if (!tokens.length) return text const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - const pattern = new RegExp(`@(${labels.map(escapeRegex).join('|')})`, 'g') + const pattern = new RegExp(`(${tokens.map(escapeRegex).join('|')})`, 'g') const nodes: React.ReactNode[] = [] let lastIndex = 0 @@ -460,17 +466,29 @@ const CopilotMessage: FC = memo( ) } + // Check if there's any visible content in the blocks + const hasVisibleContent = useMemo(() => { + if (!message.contentBlocks || message.contentBlocks.length === 0) return false + return message.contentBlocks.some((block) => { + if (block.type === 'text') { + const parsed = parseSpecialTags(block.content) + return parsed.cleanContent.trim().length > 0 + } + return block.type === 'thinking' || block.type === 'tool_call' + }) + }, [message.contentBlocks]) + if (isAssistant) { return (
    -
    +
    {/* Content blocks in chronological order */} {memoizedContentBlocks} - {/* Always show streaming indicator at the end while streaming */} + {/* Streaming indicator always at bottom during streaming */} {isStreaming && } {message.errorType === 'usage_limit' && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx index c2fdf92647..d646a179b1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx @@ -497,6 +497,11 @@ const ACTION_VERBS = [ 'Accessed', 'Managing', 'Managed', + 'Scraping', + 'Scraped', + 'Crawling', + 'Crawled', + 'Getting', ] as const /** @@ -1061,7 +1066,7 @@ function SubAgentContent({
    @@ -1157,10 +1162,10 @@ function SubAgentThinkingContent({ /** * Subagents that should collapse when done streaming. - * Default behavior is to NOT collapse (stay expanded like edit). - * Only these specific subagents collapse into "Planned for Xs >" style headers. + * Default behavior is to NOT collapse (stay expanded like edit, superagent, info, etc.). + * Only plan, debug, and research collapse into summary headers. */ -const COLLAPSIBLE_SUBAGENTS = new Set(['plan', 'debug', 'research', 'info']) +const COLLAPSIBLE_SUBAGENTS = new Set(['plan', 'debug', 'research']) /** * SubagentContentRenderer handles the rendering of subagent content. @@ -1321,7 +1326,7 @@ function SubagentContentRenderer({
    @@ -1631,10 +1636,8 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) { * Checks if a tool is an integration tool (server-side executed, not a client tool) */ function isIntegrationTool(toolName: string): boolean { - // Check if it's NOT a client tool (not in CLASS_TOOL_METADATA and not in registered tools) - const isClientTool = !!CLASS_TOOL_METADATA[toolName] - const isRegisteredTool = !!getRegisteredTools()[toolName] - return !isClientTool && !isRegisteredTool + // Any tool NOT in CLASS_TOOL_METADATA is an integration tool (server-side execution) + return !CLASS_TOOL_METADATA[toolName] } function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean { @@ -1663,16 +1666,9 @@ function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean { return true } - // Also show buttons for integration tools in pending state (they need user confirmation) - // But NOT if the tool is auto-allowed (it will auto-execute) + // Always show buttons for integration tools in pending state (they need user confirmation) const mode = useCopilotStore.getState().mode - const isAutoAllowed = useCopilotStore.getState().isToolAutoAllowed(toolCall.name) - if ( - mode === 'build' && - isIntegrationTool(toolCall.name) && - toolCall.state === 'pending' && - !isAutoAllowed - ) { + if (mode === 'build' && isIntegrationTool(toolCall.name) && toolCall.state === 'pending') { return true } @@ -1895,15 +1891,20 @@ function RunSkipButtons({ if (buttonsHidden) return null - // Standardized buttons for all interrupt tools: Allow, Always Allow, Skip + // Hide "Always Allow" for integration tools (only show for client tools with interrupts) + const showAlwaysAllow = !isIntegrationTool(toolCall.name) + + // Standardized buttons for all interrupt tools: Allow, (Always Allow for client tools only), Skip return (
    - + {showAlwaysAllow && ( + + )} @@ -1969,6 +1970,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }: 'tour', 'info', 'workflow', + 'superagent', ] const isSubagentTool = SUBAGENT_TOOLS.includes(toolCall.name) @@ -2596,16 +2598,23 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }: } } + // For edit_workflow, hide text display when we have operations (WorkflowEditSummary replaces it) + const isEditWorkflow = toolCall.name === 'edit_workflow' + const hasOperations = Array.isArray(params.operations) && params.operations.length > 0 + const hideTextForEditWorkflow = isEditWorkflow && hasOperations + return (
    -
    - -
    + {!hideTextForEditWorkflow && ( +
    + +
    + )} {isExpandableTool && expanded &&
    {renderPendingDetails()}
    } {showRemoveAutoAllow && isAutoAllowed && (
    diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts index fd7d64cff1..bab808a85b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts @@ -3,3 +3,4 @@ export { ContextPills } from './context-pills/context-pills' export { MentionMenu } from './mention-menu/mention-menu' export { ModeSelector } from './mode-selector/mode-selector' export { ModelSelector } from './model-selector/model-selector' +export { SlashMenu } from './slash-menu/slash-menu' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/slash-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/slash-menu.tsx new file mode 100644 index 0000000000..a50de3c1bd --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/slash-menu.tsx @@ -0,0 +1,249 @@ +'use client' + +import { useMemo } from 'react' +import { + Popover, + PopoverAnchor, + PopoverBackButton, + PopoverContent, + PopoverFolder, + PopoverItem, + PopoverScrollArea, +} from '@/components/emcn' +import type { useMentionMenu } from '../../hooks/use-mention-menu' + +/** + * Top-level slash command options + */ +const TOP_LEVEL_COMMANDS = [ + { id: 'fast', label: 'fast' }, + { id: 'plan', label: 'plan' }, + { id: 'debug', label: 'debug' }, + { id: 'research', label: 'research' }, + { id: 'deploy', label: 'deploy' }, + { id: 'superagent', label: 'superagent' }, +] as const + +/** + * Web submenu commands + */ +const WEB_COMMANDS = [ + { id: 'search', label: 'search' }, + { id: 'read', label: 'read' }, + { id: 'scrape', label: 'scrape' }, + { id: 'crawl', label: 'crawl' }, +] as const + +/** + * All command labels for filtering + */ +const ALL_COMMANDS = [...TOP_LEVEL_COMMANDS, ...WEB_COMMANDS] + +interface SlashMenuProps { + mentionMenu: ReturnType + message: string + onSelectCommand: (command: string) => void +} + +/** + * SlashMenu component for slash command dropdown. + * Shows command options when user types '/'. + * + * @param props - Component props + * @returns Rendered slash menu + */ +export function SlashMenu({ mentionMenu, message, onSelectCommand }: SlashMenuProps) { + const { + mentionMenuRef, + menuListRef, + getActiveSlashQueryAtPosition, + getCaretPos, + submenuActiveIndex, + mentionActiveIndex, + openSubmenuFor, + setOpenSubmenuFor, + } = mentionMenu + + /** + * Get the current query string after / + */ + const currentQuery = useMemo(() => { + const caretPos = getCaretPos() + const active = getActiveSlashQueryAtPosition(caretPos, message) + return active?.query.trim().toLowerCase() || '' + }, [message, getCaretPos, getActiveSlashQueryAtPosition]) + + /** + * Filter commands based on query (search across all commands when there's a query) + */ + const filteredCommands = useMemo(() => { + if (!currentQuery) return null // Show folder view when no query + return ALL_COMMANDS.filter((cmd) => cmd.label.toLowerCase().includes(currentQuery)) + }, [currentQuery]) + + // Show aggregated view when there's a query + const showAggregatedView = currentQuery.length > 0 + + // Compute caret viewport position via mirror technique for precise anchoring + const textareaEl = mentionMenu.textareaRef.current + if (!textareaEl) return null + + const getCaretViewport = (textarea: HTMLTextAreaElement, caretPosition: number, text: string) => { + const textareaRect = textarea.getBoundingClientRect() + const style = window.getComputedStyle(textarea) + + const mirrorDiv = document.createElement('div') + mirrorDiv.style.position = 'absolute' + mirrorDiv.style.visibility = 'hidden' + mirrorDiv.style.whiteSpace = 'pre-wrap' + mirrorDiv.style.wordWrap = 'break-word' + mirrorDiv.style.font = style.font + mirrorDiv.style.padding = style.padding + mirrorDiv.style.border = style.border + mirrorDiv.style.width = style.width + mirrorDiv.style.lineHeight = style.lineHeight + mirrorDiv.style.boxSizing = style.boxSizing + mirrorDiv.style.letterSpacing = style.letterSpacing + mirrorDiv.style.textTransform = style.textTransform + mirrorDiv.style.textIndent = style.textIndent + mirrorDiv.style.textAlign = style.textAlign + + mirrorDiv.textContent = text.substring(0, caretPosition) + + const caretMarker = document.createElement('span') + caretMarker.style.display = 'inline-block' + caretMarker.style.width = '0px' + caretMarker.style.padding = '0' + caretMarker.style.border = '0' + mirrorDiv.appendChild(caretMarker) + + document.body.appendChild(mirrorDiv) + const markerRect = caretMarker.getBoundingClientRect() + const mirrorRect = mirrorDiv.getBoundingClientRect() + document.body.removeChild(mirrorDiv) + + const leftOffset = markerRect.left - mirrorRect.left - textarea.scrollLeft + const topOffset = markerRect.top - mirrorRect.top - textarea.scrollTop + + return { + left: textareaRect.left + leftOffset, + top: textareaRect.top + topOffset, + } + } + + const caretPos = getCaretPos() + const caretViewport = getCaretViewport(textareaEl, caretPos, message) + + // Decide preferred side based on available space + const margin = 8 + const spaceAbove = caretViewport.top - margin + const spaceBelow = window.innerHeight - caretViewport.top - margin + const side: 'top' | 'bottom' = spaceBelow >= spaceAbove ? 'bottom' : 'top' + + // Check if we're in folder navigation mode (no query, not in submenu) + const isInFolderNavigationMode = !openSubmenuFor && !showAggregatedView + + return ( + { + /* controlled externally */ + }} + > + +
    + + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + > + + + {openSubmenuFor === 'Web' ? ( + // Web submenu view + <> + {WEB_COMMANDS.map((cmd, index) => ( + onSelectCommand(cmd.label)} + data-idx={index} + active={index === submenuActiveIndex} + > + {cmd.label} + + ))} + + ) : showAggregatedView ? ( + // Aggregated filtered view + <> + {filteredCommands && filteredCommands.length === 0 ? ( +
    + No commands found +
    + ) : ( + filteredCommands?.map((cmd, index) => ( + onSelectCommand(cmd.label)} + data-idx={index} + active={index === submenuActiveIndex} + > + {cmd.label} + + )) + )} + + ) : ( + // Folder navigation view + <> + {TOP_LEVEL_COMMANDS.map((cmd, index) => ( + onSelectCommand(cmd.label)} + data-idx={index} + active={isInFolderNavigationMode && index === mentionActiveIndex} + > + {cmd.label} + + ))} + + setOpenSubmenuFor('Web')} + active={ + isInFolderNavigationMode && mentionActiveIndex === TOP_LEVEL_COMMANDS.length + } + data-idx={TOP_LEVEL_COMMANDS.length} + > + {WEB_COMMANDS.map((cmd) => ( + onSelectCommand(cmd.label)}> + {cmd.label} + + ))} + + + )} +
    +
    + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts index 72aa6067ca..9e85bbeca6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts @@ -1,9 +1,11 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import type { ChatContext } from '@/stores/panel' interface UseContextManagementProps { /** Current message text */ message: string + /** Initial contexts to populate when editing a message */ + initialContexts?: ChatContext[] } /** @@ -13,8 +15,17 @@ interface UseContextManagementProps { * @param props - Configuration object * @returns Context state and management functions */ -export function useContextManagement({ message }: UseContextManagementProps) { - const [selectedContexts, setSelectedContexts] = useState([]) +export function useContextManagement({ message, initialContexts }: UseContextManagementProps) { + const [selectedContexts, setSelectedContexts] = useState(initialContexts ?? []) + const initializedRef = useRef(false) + + // Initialize with initial contexts when they're first provided (for edit mode) + useEffect(() => { + if (initialContexts && initialContexts.length > 0 && !initializedRef.current) { + setSelectedContexts(initialContexts) + initializedRef.current = true + } + }, [initialContexts]) /** * Adds a context to the selected contexts list, avoiding duplicates @@ -63,6 +74,9 @@ export function useContextManagement({ message }: UseContextManagementProps) { if (c.kind === 'docs') { return true // Only one docs context allowed } + if (c.kind === 'slash_command' && 'command' in context && 'command' in c) { + return c.command === (context as any).command + } } return false @@ -103,6 +117,8 @@ export function useContextManagement({ message }: UseContextManagementProps) { return (c as any).executionId !== (contextToRemove as any).executionId case 'docs': return false // Remove docs (only one docs context) + case 'slash_command': + return (c as any).command !== (contextToRemove as any).command default: return c.label !== contextToRemove.label } @@ -118,7 +134,7 @@ export function useContextManagement({ message }: UseContextManagementProps) { }, []) /** - * Synchronizes selected contexts with inline @label tokens in the message. + * Synchronizes selected contexts with inline @label or /label tokens in the message. * Removes contexts whose labels are no longer present in the message. */ useEffect(() => { @@ -130,17 +146,16 @@ export function useContextManagement({ message }: UseContextManagementProps) { setSelectedContexts((prev) => { if (prev.length === 0) return prev - const presentLabels = new Set() - const labels = prev.map((c) => c.label).filter(Boolean) - - for (const label of labels) { - const token = ` @${label} ` - if (message.includes(token)) { - presentLabels.add(label) - } - } - - const filtered = prev.filter((c) => !!c.label && presentLabels.has(c.label)) + const filtered = prev.filter((c) => { + if (!c.label) return false + // Check for slash command tokens or mention tokens based on kind + const isSlashCommand = c.kind === 'slash_command' + const prefix = isSlashCommand ? '/' : '@' + const tokenWithSpaces = ` ${prefix}${c.label} ` + const tokenAtStart = `${prefix}${c.label} ` + // Token can appear with leading space OR at the start of the message + return message.includes(tokenWithSpaces) || message.startsWith(tokenAtStart) + }) return filtered.length === prev.length ? prev : filtered }) }, [message]) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-menu.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-menu.ts index 12460c060f..8a07146e05 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-menu.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-menu.ts @@ -70,11 +70,25 @@ export function useMentionMenu({ // Ensure '@' starts a token (start or whitespace before) if (atIndex > 0 && !/\s/.test(before.charAt(atIndex - 1))) return null - // Check if this '@' is part of a completed mention token ( @label ) + // Check if this '@' is part of a completed mention token if (selectedContexts.length > 0) { - const labels = selectedContexts.map((c) => c.label).filter(Boolean) as string[] - for (const label of labels) { - // Space-wrapped token: " @label " + // Only check non-slash_command contexts for mentions + const mentionLabels = selectedContexts + .filter((c) => c.kind !== 'slash_command') + .map((c) => c.label) + .filter(Boolean) as string[] + + for (const label of mentionLabels) { + // Check for token at start of text: "@label " + if (atIndex === 0) { + const startToken = `@${label} ` + if (text.startsWith(startToken)) { + // This @ is part of a completed token + return null + } + } + + // Check for space-wrapped token: " @label " const token = ` @${label} ` let fromIndex = 0 while (fromIndex <= text.length) { @@ -88,7 +102,6 @@ export function useMentionMenu({ // Check if the @ we found is the @ of this completed token if (atIndex === atPositionInToken) { // The @ we found is part of a completed mention - // Don't show menu - user is typing after the completed mention return null } @@ -113,6 +126,76 @@ export function useMentionMenu({ [message, selectedContexts] ) + /** + * Finds active slash command query at the given position + * + * @param pos - Position in the text to check + * @param textOverride - Optional text override (for checking during input) + * @returns Active slash query object or null if no active slash command + */ + const getActiveSlashQueryAtPosition = useCallback( + (pos: number, textOverride?: string) => { + const text = textOverride ?? message + const before = text.slice(0, pos) + const slashIndex = before.lastIndexOf('/') + if (slashIndex === -1) return null + + // Ensure '/' starts a token (start or whitespace before) + if (slashIndex > 0 && !/\s/.test(before.charAt(slashIndex - 1))) return null + + // Check if this '/' is part of a completed slash token + if (selectedContexts.length > 0) { + // Only check slash_command contexts + const slashLabels = selectedContexts + .filter((c) => c.kind === 'slash_command') + .map((c) => c.label) + .filter(Boolean) as string[] + + for (const label of slashLabels) { + // Check for token at start of text: "/label " + if (slashIndex === 0) { + const startToken = `/${label} ` + if (text.startsWith(startToken)) { + // This slash is part of a completed token + return null + } + } + + // Check for space-wrapped token: " /label " + const token = ` /${label} ` + let fromIndex = 0 + while (fromIndex <= text.length) { + const idx = text.indexOf(token, fromIndex) + if (idx === -1) break + + const tokenStart = idx + const tokenEnd = idx + token.length + const slashPositionInToken = idx + 1 // position of / in " /label " + + if (slashIndex === slashPositionInToken) { + return null + } + + if (pos > tokenStart && pos < tokenEnd) { + return null + } + + fromIndex = tokenEnd + } + } + } + + const segment = before.slice(slashIndex + 1) + // Close the popup if user types space immediately after / + if (segment.length > 0 && /^\s/.test(segment)) { + return null + } + + return { query: segment, start: slashIndex, end: pos } + }, + [message, selectedContexts] + ) + /** * Gets the submenu query text * @@ -200,9 +283,10 @@ export function useMentionMenu({ const before = message.slice(0, active.start) const after = message.slice(active.end) - // Always include leading space, avoid duplicate if one exists - const needsLeadingSpace = !before.endsWith(' ') - const insertion = `${needsLeadingSpace ? ' ' : ''}@${label} ` + // Add leading space only if not at start and previous char isn't whitespace + const needsLeadingSpace = before.length > 0 && !before.endsWith(' ') + // Always add trailing space for easy continued typing + const insertion = `${needsLeadingSpace ? ' ' : ''}@${label} ` const next = `${before}${insertion}${after}` onMessageChange(next) @@ -217,6 +301,41 @@ export function useMentionMenu({ [message, getActiveMentionQueryAtPosition, onMessageChange] ) + /** + * Replaces active slash command with a label + * + * @param label - Label to replace the slash command with + * @returns True if replacement was successful, false if no active slash command found + */ + const replaceActiveSlashWith = useCallback( + (label: string) => { + const textarea = textareaRef.current + if (!textarea) return false + const pos = textarea.selectionStart ?? message.length + const active = getActiveSlashQueryAtPosition(pos) + if (!active) return false + + const before = message.slice(0, active.start) + const after = message.slice(active.end) + + // Add leading space only if not at start and previous char isn't whitespace + const needsLeadingSpace = before.length > 0 && !before.endsWith(' ') + // Always add trailing space for easy continued typing + const insertion = `${needsLeadingSpace ? ' ' : ''}/${label} ` + + const next = `${before}${insertion}${after}` + onMessageChange(next) + + setTimeout(() => { + const cursorPos = before.length + insertion.length + textarea.setSelectionRange(cursorPos, cursorPos) + textarea.focus() + }, 0) + return true + }, + [message, getActiveSlashQueryAtPosition, onMessageChange] + ) + /** * Scrolls active item into view in the menu * @@ -304,10 +423,12 @@ export function useMentionMenu({ // Operations getCaretPos, getActiveMentionQueryAtPosition, + getActiveSlashQueryAtPosition, getSubmenuQuery, resetActiveMentionQuery, insertAtCursor, replaceActiveMentionWith, + replaceActiveSlashWith, scrollActiveItemIntoView, closeMentionMenu, } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-tokens.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-tokens.ts index ca76abe24d..8d21fe83d0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-tokens.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-tokens.ts @@ -39,7 +39,7 @@ export function useMentionTokens({ setSelectedContexts, }: UseMentionTokensProps) { /** - * Computes all mention ranges in the message + * Computes all mention ranges in the message (both @mentions and /commands) * * @returns Array of mention ranges sorted by start position */ @@ -55,8 +55,19 @@ export function useMentionTokens({ const uniqueLabels = Array.from(new Set(labels)) for (const label of uniqueLabels) { - // Space-wrapped token: " @label " (search from start) - const token = ` @${label} ` + // Find matching context to determine if it's a slash command + const matchingContext = selectedContexts.find((c) => c.label === label) + const isSlashCommand = matchingContext?.kind === 'slash_command' + const prefix = isSlashCommand ? '/' : '@' + + // Check for token at the very start of the message (no leading space) + const tokenAtStart = `${prefix}${label} ` + if (message.startsWith(tokenAtStart)) { + ranges.push({ start: 0, end: tokenAtStart.length, label }) + } + + // Space-wrapped token: " @label " or " /label " (search from start) + const token = ` ${prefix}${label} ` let fromIndex = 0 while (fromIndex <= message.length) { const idx = message.indexOf(token, fromIndex) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx index b8ad537e66..2d16d1c6f8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx @@ -21,6 +21,7 @@ import { MentionMenu, ModelSelector, ModeSelector, + SlashMenu, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components' import { NEAR_TOP_THRESHOLD } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants' import { @@ -67,6 +68,8 @@ interface UserInputProps { hideModeSelector?: boolean /** Disable @mention functionality */ disableMentions?: boolean + /** Initial contexts for editing a message with existing context mentions */ + initialContexts?: ChatContext[] } interface UserInputRef { @@ -103,6 +106,7 @@ const UserInput = forwardRef( onModelChangeOverride, hideModeSelector = false, disableMentions = false, + initialContexts, }, ref ) => { @@ -123,6 +127,7 @@ const UserInput = forwardRef( const [isNearTop, setIsNearTop] = useState(false) const [containerRef, setContainerRef] = useState(null) const [inputContainerRef, setInputContainerRef] = useState(null) + const [showSlashMenu, setShowSlashMenu] = useState(false) // Controlled vs uncontrolled message state const message = controlledValue !== undefined ? controlledValue : internalMessage @@ -140,7 +145,7 @@ const UserInput = forwardRef( // Custom hooks - order matters for ref sharing // Context management (manages selectedContexts state) - const contextManagement = useContextManagement({ message }) + const contextManagement = useContextManagement({ message, initialContexts }) // Mention menu const mentionMenu = useMentionMenu({ @@ -370,20 +375,131 @@ const UserInput = forwardRef( } }, [onAbort, isLoading]) + const handleSlashCommandSelect = useCallback( + (command: string) => { + // Capitalize the command for display + const capitalizedCommand = command.charAt(0).toUpperCase() + command.slice(1) + + // Replace the active slash query with the capitalized command + mentionMenu.replaceActiveSlashWith(capitalizedCommand) + + // Add as a context so it gets highlighted + contextManagement.addContext({ + kind: 'slash_command', + command, + label: capitalizedCommand, + }) + + setShowSlashMenu(false) + mentionMenu.textareaRef.current?.focus() + }, + [mentionMenu, contextManagement] + ) + const handleKeyDown = useCallback( (e: KeyboardEvent) => { // Escape key handling - if (e.key === 'Escape' && mentionMenu.showMentionMenu) { + if (e.key === 'Escape' && (mentionMenu.showMentionMenu || showSlashMenu)) { e.preventDefault() if (mentionMenu.openSubmenuFor) { mentionMenu.setOpenSubmenuFor(null) mentionMenu.setSubmenuQueryStart(null) } else { mentionMenu.closeMentionMenu() + setShowSlashMenu(false) } return } + // Arrow navigation in slash menu + if (showSlashMenu) { + const TOP_LEVEL_COMMANDS = ['fast', 'plan', 'debug', 'research', 'deploy', 'superagent'] + const WEB_COMMANDS = ['search', 'read', 'scrape', 'crawl'] + const ALL_COMMANDS = [...TOP_LEVEL_COMMANDS, ...WEB_COMMANDS] + + const caretPos = mentionMenu.getCaretPos() + const activeSlash = mentionMenu.getActiveSlashQueryAtPosition(caretPos, message) + const query = activeSlash?.query.trim().toLowerCase() || '' + const showAggregatedView = query.length > 0 + + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + e.preventDefault() + + if (mentionMenu.openSubmenuFor === 'Web') { + // Navigate in Web submenu + const last = WEB_COMMANDS.length - 1 + mentionMenu.setSubmenuActiveIndex((prev) => { + const next = + e.key === 'ArrowDown' + ? prev >= last + ? 0 + : prev + 1 + : prev <= 0 + ? last + : prev - 1 + requestAnimationFrame(() => mentionMenu.scrollActiveItemIntoView(next)) + return next + }) + } else if (showAggregatedView) { + // Navigate in filtered view + const filtered = ALL_COMMANDS.filter((cmd) => cmd.includes(query)) + const last = Math.max(0, filtered.length - 1) + mentionMenu.setSubmenuActiveIndex((prev) => { + if (filtered.length === 0) return 0 + const next = + e.key === 'ArrowDown' + ? prev >= last + ? 0 + : prev + 1 + : prev <= 0 + ? last + : prev - 1 + requestAnimationFrame(() => mentionMenu.scrollActiveItemIntoView(next)) + return next + }) + } else { + // Navigate in folder view (top-level + Web folder) + const totalItems = TOP_LEVEL_COMMANDS.length + 1 // +1 for Web folder + const last = totalItems - 1 + mentionMenu.setMentionActiveIndex((prev) => { + const next = + e.key === 'ArrowDown' + ? prev >= last + ? 0 + : prev + 1 + : prev <= 0 + ? last + : prev - 1 + requestAnimationFrame(() => mentionMenu.scrollActiveItemIntoView(next)) + return next + }) + } + return + } + + // Arrow right to enter Web submenu + if (e.key === 'ArrowRight') { + e.preventDefault() + if (!showAggregatedView && !mentionMenu.openSubmenuFor) { + // Check if Web folder is selected (it's after all top-level commands) + if (mentionMenu.mentionActiveIndex === TOP_LEVEL_COMMANDS.length) { + mentionMenu.setOpenSubmenuFor('Web') + mentionMenu.setSubmenuActiveIndex(0) + } + } + return + } + + // Arrow left to exit submenu + if (e.key === 'ArrowLeft') { + e.preventDefault() + if (mentionMenu.openSubmenuFor) { + mentionMenu.setOpenSubmenuFor(null) + } + return + } + } + // Arrow navigation in mention menu if (mentionKeyboard.handleArrowNavigation(e)) return if (mentionKeyboard.handleArrowRight(e)) return @@ -392,6 +508,42 @@ const UserInput = forwardRef( // Enter key handling if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault() + if (showSlashMenu) { + const TOP_LEVEL_COMMANDS = ['fast', 'plan', 'debug', 'research', 'deploy', 'superagent'] + const WEB_COMMANDS = ['search', 'read', 'scrape', 'crawl'] + const ALL_COMMANDS = [...TOP_LEVEL_COMMANDS, ...WEB_COMMANDS] + + const caretPos = mentionMenu.getCaretPos() + const activeSlash = mentionMenu.getActiveSlashQueryAtPosition(caretPos, message) + const query = activeSlash?.query.trim().toLowerCase() || '' + const showAggregatedView = query.length > 0 + + if (mentionMenu.openSubmenuFor === 'Web') { + // Select from Web submenu + const selectedCommand = + WEB_COMMANDS[mentionMenu.submenuActiveIndex] || WEB_COMMANDS[0] + handleSlashCommandSelect(selectedCommand) + } else if (showAggregatedView) { + // Select from filtered view + const filtered = ALL_COMMANDS.filter((cmd) => cmd.includes(query)) + if (filtered.length > 0) { + const selectedCommand = filtered[mentionMenu.submenuActiveIndex] || filtered[0] + handleSlashCommandSelect(selectedCommand) + } + } else { + // Folder navigation view + const selectedIndex = mentionMenu.mentionActiveIndex + if (selectedIndex < TOP_LEVEL_COMMANDS.length) { + // Top-level command selected + handleSlashCommandSelect(TOP_LEVEL_COMMANDS[selectedIndex]) + } else if (selectedIndex === TOP_LEVEL_COMMANDS.length) { + // Web folder selected - open it + mentionMenu.setOpenSubmenuFor('Web') + mentionMenu.setSubmenuActiveIndex(0) + } + } + return + } if (!mentionMenu.showMentionMenu) { handleSubmit() } else { @@ -469,7 +621,15 @@ const UserInput = forwardRef( } } }, - [mentionMenu, mentionKeyboard, handleSubmit, message.length, mentionTokensWithContext] + [ + mentionMenu, + mentionKeyboard, + handleSubmit, + handleSlashCommandSelect, + message, + mentionTokensWithContext, + showSlashMenu, + ] ) const handleInputChange = useCallback( @@ -481,9 +641,14 @@ const UserInput = forwardRef( if (disableMentions) return const caret = e.target.selectionStart ?? newValue.length - const active = mentionMenu.getActiveMentionQueryAtPosition(caret, newValue) - if (active) { + // Check for @ mention trigger + const activeMention = mentionMenu.getActiveMentionQueryAtPosition(caret, newValue) + // Check for / slash command trigger + const activeSlash = mentionMenu.getActiveSlashQueryAtPosition(caret, newValue) + + if (activeMention) { + setShowSlashMenu(false) mentionMenu.setShowMentionMenu(true) mentionMenu.setInAggregated(false) if (mentionMenu.openSubmenuFor) { @@ -492,10 +657,17 @@ const UserInput = forwardRef( mentionMenu.setMentionActiveIndex(0) mentionMenu.setSubmenuActiveIndex(0) } + } else if (activeSlash) { + mentionMenu.setShowMentionMenu(false) + mentionMenu.setOpenSubmenuFor(null) + mentionMenu.setSubmenuQueryStart(null) + setShowSlashMenu(true) + mentionMenu.setSubmenuActiveIndex(0) } else { mentionMenu.setShowMentionMenu(false) mentionMenu.setOpenSubmenuFor(null) mentionMenu.setSubmenuQueryStart(null) + setShowSlashMenu(false) } }, [setMessage, mentionMenu, disableMentions] @@ -542,6 +714,32 @@ const UserInput = forwardRef( mentionMenu.setSubmenuActiveIndex(0) }, [disabled, isLoading, mentionMenu, message, setMessage]) + const handleOpenSlashMenu = useCallback(() => { + if (disabled || isLoading) return + const textarea = mentionMenu.textareaRef.current + if (!textarea) return + textarea.focus() + const pos = textarea.selectionStart ?? message.length + const needsSpaceBefore = pos > 0 && !/\s/.test(message.charAt(pos - 1)) + + const insertText = needsSpaceBefore ? ' /' : '/' + const start = textarea.selectionStart ?? message.length + const end = textarea.selectionEnd ?? message.length + const before = message.slice(0, start) + const after = message.slice(end) + const next = `${before}${insertText}${after}` + setMessage(next) + + setTimeout(() => { + const newPos = before.length + insertText.length + textarea.setSelectionRange(newPos, newPos) + textarea.focus() + }, 0) + + setShowSlashMenu(true) + mentionMenu.setSubmenuActiveIndex(0) + }, [disabled, isLoading, mentionMenu, message, setMessage]) + const canSubmit = message.trim().length > 0 && !disabled && !isLoading const showAbortButton = isLoading && onAbort @@ -643,6 +841,20 @@ const UserInput = forwardRef( + + + / + + + {/* Selected Context Pills */} ( />, document.body )} + + {/* Slash Menu Portal */} + {!disableMentions && + showSlashMenu && + createPortal( + , + document.body + )}
    {/* Bottom Row: Mode Selector + Model Selector + Attach Button + Send Button */} diff --git a/apps/sim/lib/copilot/api.ts b/apps/sim/lib/copilot/api.ts index 581fe0511f..f45cd78660 100644 --- a/apps/sim/lib/copilot/api.ts +++ b/apps/sim/lib/copilot/api.ts @@ -99,6 +99,7 @@ export interface SendMessageRequest { workflowId?: string executionId?: string }> + commands?: string[] } /** diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-block-config.ts b/apps/sim/lib/copilot/tools/client/blocks/get-block-config.ts index 6b3a15c531..be4196c443 100644 --- a/apps/sim/lib/copilot/tools/client/blocks/get-block-config.ts +++ b/apps/sim/lib/copilot/tools/client/blocks/get-block-config.ts @@ -10,6 +10,7 @@ import { GetBlockConfigInput, GetBlockConfigResult, } from '@/lib/copilot/tools/shared/schemas' +import { getBlock } from '@/blocks/registry' interface GetBlockConfigArgs { blockType: string @@ -39,7 +40,9 @@ export class GetBlockConfigClientTool extends BaseClientTool { }, getDynamicText: (params, state) => { if (params?.blockType && typeof params.blockType === 'string') { - const blockName = params.blockType.replace(/_/g, ' ') + // Look up the block config to get the human-readable name + const blockConfig = getBlock(params.blockType) + const blockName = (blockConfig?.name ?? params.blockType.replace(/_/g, ' ')).toLowerCase() const opSuffix = params.operation ? ` (${params.operation})` : '' switch (state) { diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-block-options.ts b/apps/sim/lib/copilot/tools/client/blocks/get-block-options.ts index 41cd7bd8f6..a104688e5f 100644 --- a/apps/sim/lib/copilot/tools/client/blocks/get-block-options.ts +++ b/apps/sim/lib/copilot/tools/client/blocks/get-block-options.ts @@ -10,6 +10,7 @@ import { GetBlockOptionsInput, GetBlockOptionsResult, } from '@/lib/copilot/tools/shared/schemas' +import { getBlock } from '@/blocks/registry' interface GetBlockOptionsArgs { blockId: string @@ -37,7 +38,9 @@ export class GetBlockOptionsClientTool extends BaseClientTool { }, getDynamicText: (params, state) => { if (params?.blockId && typeof params.blockId === 'string') { - const blockName = params.blockId.replace(/_/g, ' ') + // Look up the block config to get the human-readable name + const blockConfig = getBlock(params.blockId) + const blockName = (blockConfig?.name ?? params.blockId.replace(/_/g, ' ')).toLowerCase() switch (state) { case ClientToolCallState.success: diff --git a/apps/sim/lib/copilot/tools/client/init-tool-configs.ts b/apps/sim/lib/copilot/tools/client/init-tool-configs.ts index 821e5ec8d6..b2d480f037 100644 --- a/apps/sim/lib/copilot/tools/client/init-tool-configs.ts +++ b/apps/sim/lib/copilot/tools/client/init-tool-configs.ts @@ -18,6 +18,7 @@ import './other/make-api-request' import './other/plan' import './other/research' import './other/sleep' +import './other/superagent' import './other/test' import './other/tour' import './other/workflow' diff --git a/apps/sim/lib/copilot/tools/client/other/crawl-website.ts b/apps/sim/lib/copilot/tools/client/other/crawl-website.ts new file mode 100644 index 0000000000..5fee1690dd --- /dev/null +++ b/apps/sim/lib/copilot/tools/client/other/crawl-website.ts @@ -0,0 +1,53 @@ +import { Globe, Loader2, MinusCircle, XCircle } from 'lucide-react' +import { + BaseClientTool, + type BaseClientToolMetadata, + ClientToolCallState, +} from '@/lib/copilot/tools/client/base-tool' + +export class CrawlWebsiteClientTool extends BaseClientTool { + static readonly id = 'crawl_website' + + constructor(toolCallId: string) { + super(toolCallId, CrawlWebsiteClientTool.id, CrawlWebsiteClientTool.metadata) + } + + static readonly metadata: BaseClientToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Crawling website', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Crawling website', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Crawling website', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Crawled website', icon: Globe }, + [ClientToolCallState.error]: { text: 'Failed to crawl website', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted crawling website', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped crawling website', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.url && typeof params.url === 'string') { + const url = params.url + const truncated = url.length > 50 ? `${url.slice(0, 50)}...` : url + + switch (state) { + case ClientToolCallState.success: + return `Crawled ${truncated}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Crawling ${truncated}` + case ClientToolCallState.error: + return `Failed to crawl ${truncated}` + case ClientToolCallState.aborted: + return `Aborted crawling ${truncated}` + case ClientToolCallState.rejected: + return `Skipped crawling ${truncated}` + } + } + return undefined + }, + } + + async execute(): Promise { + return + } +} diff --git a/apps/sim/lib/copilot/tools/client/other/get-page-contents.ts b/apps/sim/lib/copilot/tools/client/other/get-page-contents.ts new file mode 100644 index 0000000000..a5ffa6eeb2 --- /dev/null +++ b/apps/sim/lib/copilot/tools/client/other/get-page-contents.ts @@ -0,0 +1,54 @@ +import { FileText, Loader2, MinusCircle, XCircle } from 'lucide-react' +import { + BaseClientTool, + type BaseClientToolMetadata, + ClientToolCallState, +} from '@/lib/copilot/tools/client/base-tool' + +export class GetPageContentsClientTool extends BaseClientTool { + static readonly id = 'get_page_contents' + + constructor(toolCallId: string) { + super(toolCallId, GetPageContentsClientTool.id, GetPageContentsClientTool.metadata) + } + + static readonly metadata: BaseClientToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting page contents', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting page contents', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Getting page contents', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Retrieved page contents', icon: FileText }, + [ClientToolCallState.error]: { text: 'Failed to get page contents', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted getting page contents', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped getting page contents', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.urls && Array.isArray(params.urls) && params.urls.length > 0) { + const firstUrl = String(params.urls[0]) + const truncated = firstUrl.length > 40 ? `${firstUrl.slice(0, 40)}...` : firstUrl + const count = params.urls.length + + switch (state) { + case ClientToolCallState.success: + return count > 1 ? `Retrieved ${count} pages` : `Retrieved ${truncated}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return count > 1 ? `Getting ${count} pages` : `Getting ${truncated}` + case ClientToolCallState.error: + return count > 1 ? `Failed to get ${count} pages` : `Failed to get ${truncated}` + case ClientToolCallState.aborted: + return count > 1 ? `Aborted getting ${count} pages` : `Aborted getting ${truncated}` + case ClientToolCallState.rejected: + return count > 1 ? `Skipped getting ${count} pages` : `Skipped getting ${truncated}` + } + } + return undefined + }, + } + + async execute(): Promise { + return + } +} diff --git a/apps/sim/lib/copilot/tools/client/other/scrape-page.ts b/apps/sim/lib/copilot/tools/client/other/scrape-page.ts new file mode 100644 index 0000000000..0bb5f72a7e --- /dev/null +++ b/apps/sim/lib/copilot/tools/client/other/scrape-page.ts @@ -0,0 +1,53 @@ +import { Globe, Loader2, MinusCircle, XCircle } from 'lucide-react' +import { + BaseClientTool, + type BaseClientToolMetadata, + ClientToolCallState, +} from '@/lib/copilot/tools/client/base-tool' + +export class ScrapePageClientTool extends BaseClientTool { + static readonly id = 'scrape_page' + + constructor(toolCallId: string) { + super(toolCallId, ScrapePageClientTool.id, ScrapePageClientTool.metadata) + } + + static readonly metadata: BaseClientToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Scraping page', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Scraping page', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Scraping page', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Scraped page', icon: Globe }, + [ClientToolCallState.error]: { text: 'Failed to scrape page', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted scraping page', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped scraping page', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.url && typeof params.url === 'string') { + const url = params.url + const truncated = url.length > 50 ? `${url.slice(0, 50)}...` : url + + switch (state) { + case ClientToolCallState.success: + return `Scraped ${truncated}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Scraping ${truncated}` + case ClientToolCallState.error: + return `Failed to scrape ${truncated}` + case ClientToolCallState.aborted: + return `Aborted scraping ${truncated}` + case ClientToolCallState.rejected: + return `Skipped scraping ${truncated}` + } + } + return undefined + }, + } + + async execute(): Promise { + return + } +} diff --git a/apps/sim/lib/copilot/tools/client/other/search-online.ts b/apps/sim/lib/copilot/tools/client/other/search-online.ts index f5022c3f44..fd96c5cc99 100644 --- a/apps/sim/lib/copilot/tools/client/other/search-online.ts +++ b/apps/sim/lib/copilot/tools/client/other/search-online.ts @@ -1,19 +1,9 @@ -import { createLogger } from '@sim/logger' import { Globe, Loader2, MinusCircle, XCircle } from 'lucide-react' import { BaseClientTool, type BaseClientToolMetadata, ClientToolCallState, } from '@/lib/copilot/tools/client/base-tool' -import { ExecuteResponseSuccessSchema } from '@/lib/copilot/tools/shared/schemas' - -interface SearchOnlineArgs { - query: string - num?: number - type?: string - gl?: string - hl?: string -} export class SearchOnlineClientTool extends BaseClientTool { static readonly id = 'search_online' @@ -32,6 +22,7 @@ export class SearchOnlineClientTool extends BaseClientTool { [ClientToolCallState.rejected]: { text: 'Skipped online search', icon: MinusCircle }, [ClientToolCallState.aborted]: { text: 'Aborted online search', icon: XCircle }, }, + interrupt: undefined, getDynamicText: (params, state) => { if (params?.query && typeof params.query === 'string') { const query = params.query @@ -56,28 +47,7 @@ export class SearchOnlineClientTool extends BaseClientTool { }, } - async execute(args?: SearchOnlineArgs): Promise { - const logger = createLogger('SearchOnlineClientTool') - try { - this.setState(ClientToolCallState.executing) - const res = await fetch('/api/copilot/execute-copilot-server-tool', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ toolName: 'search_online', payload: args || {} }), - }) - if (!res.ok) { - const txt = await res.text().catch(() => '') - throw new Error(txt || `Server error (${res.status})`) - } - const json = await res.json() - const parsed = ExecuteResponseSuccessSchema.parse(json) - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, 'Online search complete', parsed.result) - this.setState(ClientToolCallState.success) - } catch (e: any) { - logger.error('execute failed', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Search failed') - } + async execute(): Promise { + return } } diff --git a/apps/sim/lib/copilot/tools/client/other/superagent.ts b/apps/sim/lib/copilot/tools/client/other/superagent.ts new file mode 100644 index 0000000000..99ec1fbfe1 --- /dev/null +++ b/apps/sim/lib/copilot/tools/client/other/superagent.ts @@ -0,0 +1,56 @@ +import { Loader2, Sparkles, XCircle } from 'lucide-react' +import { + BaseClientTool, + type BaseClientToolMetadata, + ClientToolCallState, +} from '@/lib/copilot/tools/client/base-tool' +import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' + +interface SuperagentArgs { + instruction: string +} + +/** + * Superagent tool that spawns a powerful subagent for complex tasks. + * This tool auto-executes and the actual work is done by the superagent. + * The subagent's output is streamed as nested content under this tool call. + */ +export class SuperagentClientTool extends BaseClientTool { + static readonly id = 'superagent' + + constructor(toolCallId: string) { + super(toolCallId, SuperagentClientTool.id, SuperagentClientTool.metadata) + } + + static readonly metadata: BaseClientToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Superagent working', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Superagent working', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Superagent working', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Superagent completed', icon: Sparkles }, + [ClientToolCallState.error]: { text: 'Superagent failed', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Superagent skipped', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Superagent aborted', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Superagent working', + completedLabel: 'Superagent completed', + shouldCollapse: true, + outputArtifacts: [], + }, + }, + } + + /** + * Execute the superagent tool. + * This just marks the tool as executing - the actual work is done server-side + * by the superagent, and its output is streamed as subagent events. + */ + async execute(_args?: SuperagentArgs): Promise { + this.setState(ClientToolCallState.executing) + } +} + +// Register UI config at module load +registerToolUIConfig(SuperagentClientTool.id, SuperagentClientTool.metadata.uiConfig!) diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index 97b785177a..64d1d3e7bf 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -27,11 +27,13 @@ import { import { NavigateUIClientTool } from '@/lib/copilot/tools/client/navigation/navigate-ui' import { AuthClientTool } from '@/lib/copilot/tools/client/other/auth' import { CheckoffTodoClientTool } from '@/lib/copilot/tools/client/other/checkoff-todo' +import { CrawlWebsiteClientTool } from '@/lib/copilot/tools/client/other/crawl-website' import { CustomToolClientTool } from '@/lib/copilot/tools/client/other/custom-tool' import { DebugClientTool } from '@/lib/copilot/tools/client/other/debug' import { DeployClientTool } from '@/lib/copilot/tools/client/other/deploy' import { EditClientTool } from '@/lib/copilot/tools/client/other/edit' import { EvaluateClientTool } from '@/lib/copilot/tools/client/other/evaluate' +import { GetPageContentsClientTool } from '@/lib/copilot/tools/client/other/get-page-contents' import { InfoClientTool } from '@/lib/copilot/tools/client/other/info' import { KnowledgeClientTool } from '@/lib/copilot/tools/client/other/knowledge' import { MakeApiRequestClientTool } from '@/lib/copilot/tools/client/other/make-api-request' @@ -40,6 +42,7 @@ import { OAuthRequestAccessClientTool } from '@/lib/copilot/tools/client/other/o import { PlanClientTool } from '@/lib/copilot/tools/client/other/plan' import { RememberDebugClientTool } from '@/lib/copilot/tools/client/other/remember-debug' import { ResearchClientTool } from '@/lib/copilot/tools/client/other/research' +import { ScrapePageClientTool } from '@/lib/copilot/tools/client/other/scrape-page' import { SearchDocumentationClientTool } from '@/lib/copilot/tools/client/other/search-documentation' import { SearchErrorsClientTool } from '@/lib/copilot/tools/client/other/search-errors' import { SearchLibraryDocsClientTool } from '@/lib/copilot/tools/client/other/search-library-docs' @@ -120,6 +123,9 @@ const CLIENT_TOOL_INSTANTIATORS: Record any> = { search_library_docs: (id) => new SearchLibraryDocsClientTool(id), search_patterns: (id) => new SearchPatternsClientTool(id), search_errors: (id) => new SearchErrorsClientTool(id), + scrape_page: (id) => new ScrapePageClientTool(id), + get_page_contents: (id) => new GetPageContentsClientTool(id), + crawl_website: (id) => new CrawlWebsiteClientTool(id), remember_debug: (id) => new RememberDebugClientTool(id), set_environment_variables: (id) => new SetEnvironmentVariablesClientTool(id), get_credentials: (id) => new GetCredentialsClientTool(id), @@ -179,6 +185,9 @@ export const CLASS_TOOL_METADATA: Record = { } } catch {} - // Integration tools: Check if auto-allowed, otherwise wait for user confirmation - // This handles tools like google_calendar_*, exa_*, etc. that aren't in the client registry + // Integration tools: Stay in pending state until user confirms via buttons + // This handles tools like google_calendar_*, exa_*, gmail_read, etc. that aren't in the client registry // Only relevant if mode is 'build' (agent) - const { mode, workflowId, autoAllowedTools } = get() + const { mode, workflowId } = get() if (mode === 'build' && workflowId) { - // Check if tool was NOT found in client registry (def is undefined from above) + // Check if tool was NOT found in client registry const def = name ? getTool(name) : undefined const inst = getClientTool(id) as any if (!def && !inst && name) { - // Check if this tool is auto-allowed - if (autoAllowedTools.includes(name)) { - logger.info('[build mode] Integration tool auto-allowed, executing', { id, name }) - - // Auto-execute the tool - setTimeout(() => { - get().executeIntegrationTool(id) - }, 0) - } else { - // Integration tools stay in pending state until user confirms - logger.info('[build mode] Integration tool awaiting user confirmation', { - id, - name, - }) - } + // Integration tools stay in pending state until user confirms + logger.info('[build mode] Integration tool awaiting user confirmation', { + id, + name, + }) } } }, @@ -1854,7 +1853,7 @@ const subAgentSSEHandlers: Record = { updateToolCallWithSubAgentData(context, get, set, parentToolCallId) - // Execute client tools (same logic as main tool_call handler) + // Execute client tools in parallel (non-blocking) - same pattern as main tool_call handler try { const def = getTool(name) if (def) { @@ -1863,29 +1862,33 @@ const subAgentSSEHandlers: Record = { ? !!def.hasInterrupt(args || {}) : !!def.hasInterrupt if (!hasInterrupt) { - // Auto-execute tools without interrupts + // Auto-execute tools without interrupts - non-blocking const ctx = createExecutionContext({ toolCallId: id, toolName: name }) - try { - await def.execute(ctx, args || {}) - } catch (execErr: any) { - logger.error('[SubAgent] Tool execution failed', { id, name, error: execErr?.message }) - } + Promise.resolve() + .then(() => def.execute(ctx, args || {})) + .catch((execErr: any) => { + logger.error('[SubAgent] Tool execution failed', { + id, + name, + error: execErr?.message, + }) + }) } } else { - // Fallback to class-based tools + // Fallback to class-based tools - non-blocking const instance = getClientTool(id) if (instance) { const hasInterruptDisplays = !!instance.getInterruptDisplays?.() if (!hasInterruptDisplays) { - try { - await instance.execute(args || {}) - } catch (execErr: any) { - logger.error('[SubAgent] Class tool execution failed', { - id, - name, - error: execErr?.message, + Promise.resolve() + .then(() => instance.execute(args || {})) + .catch((execErr: any) => { + logger.error('[SubAgent] Class tool execution failed', { + id, + name, + error: execErr?.message, + }) }) - } } } } @@ -2515,6 +2518,13 @@ export const useCopilotStore = create()( // Call copilot API const apiMode: 'ask' | 'agent' | 'plan' = mode === 'ask' ? 'ask' : mode === 'plan' ? 'plan' : 'agent' + + // Extract slash commands from contexts (lowercase) and filter them out from contexts + const commands = contexts + ?.filter((c) => c.kind === 'slash_command' && 'command' in c) + .map((c) => (c as any).command.toLowerCase()) as string[] | undefined + const filteredContexts = contexts?.filter((c) => c.kind !== 'slash_command') + const result = await sendStreamingMessage({ message: messageToSend, userMessageId: userMessage.id, @@ -2526,7 +2536,8 @@ export const useCopilotStore = create()( createNewChat: !currentChat, stream, fileAttachments, - contexts, + contexts: filteredContexts, + commands: commands?.length ? commands : undefined, abortSignal: abortController.signal, }) @@ -2618,13 +2629,14 @@ export const useCopilotStore = create()( ), isSendingMessage: false, isAborting: false, - abortController: null, + // Keep abortController so streaming loop can check signal.aborted + // It will be nulled when streaming completes or new message starts })) } else { set({ isSendingMessage: false, isAborting: false, - abortController: null, + // Keep abortController so streaming loop can check signal.aborted }) } @@ -2653,7 +2665,7 @@ export const useCopilotStore = create()( } catch {} } } catch { - set({ isSendingMessage: false, isAborting: false, abortController: null }) + set({ isSendingMessage: false, isAborting: false }) } }, @@ -3154,6 +3166,7 @@ export const useCopilotStore = create()( : msg ), isSendingMessage: false, + isAborting: false, abortController: null, currentUserMessageId: null, })) diff --git a/apps/sim/stores/panel/copilot/types.ts b/apps/sim/stores/panel/copilot/types.ts index fbb6404aac..0ddb9515f8 100644 --- a/apps/sim/stores/panel/copilot/types.ts +++ b/apps/sim/stores/panel/copilot/types.ts @@ -85,6 +85,7 @@ export type ChatContext = | { kind: 'knowledge'; knowledgeId?: string; label: string } | { kind: 'templates'; templateId?: string; label: string } | { kind: 'docs'; label: string } + | { kind: 'slash_command'; command: string; label: string } import type { CopilotChat as ApiCopilotChat } from '@/lib/copilot/api' From eb52f69efd5b320d4d0f47433cc98796d4e7169b Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:31:18 -0800 Subject: [PATCH 02/36] improvement(schedule): default schedule timezone (#2800) --- apps/sim/blocks/blocks/schedule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/blocks/blocks/schedule.ts b/apps/sim/blocks/blocks/schedule.ts index a4490a9cf8..fb757543e3 100644 --- a/apps/sim/blocks/blocks/schedule.ts +++ b/apps/sim/blocks/blocks/schedule.ts @@ -172,7 +172,7 @@ export const ScheduleBlock: BlockConfig = { { label: 'Melbourne (UTC+10)', id: 'Australia/Melbourne' }, { label: 'Auckland (UTC+12)', id: 'Pacific/Auckland' }, ], - value: () => 'UTC', + value: () => Intl.DateTimeFormat().resolvedOptions().timeZone, required: false, mode: 'trigger', condition: { field: 'scheduleType', value: ['minutes', 'hourly'], not: true }, From 3d037c9b74273b3077a8916397b7d06fc6595c93 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:12:31 -0800 Subject: [PATCH 03/36] fix(executor): pattern match more errors to prevent swallow (#2802) --- apps/sim/tools/utils.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/sim/tools/utils.ts b/apps/sim/tools/utils.ts index 4d0a2593c1..fe067e2198 100644 --- a/apps/sim/tools/utils.ts +++ b/apps/sim/tools/utils.ts @@ -3,6 +3,7 @@ import { getBaseUrl } from '@/lib/core/utils/urls' import { AGENT, isCustomTool } from '@/executor/constants' import { useCustomToolsStore } from '@/stores/custom-tools' import { useEnvironmentStore } from '@/stores/settings/environment' +import { extractErrorMessage } from '@/tools/error-extractors' import { tools } from '@/tools/registry' import type { TableRow, ToolConfig, ToolResponse } from '@/tools/types' @@ -162,14 +163,22 @@ export async function executeRequest( const externalResponse = await fetch(url, { method, headers, body }) if (!externalResponse.ok) { - let errorContent + let errorData: any try { - errorContent = await externalResponse.json() + errorData = await externalResponse.json() } catch (_e) { - errorContent = { message: externalResponse.statusText } + try { + errorData = await externalResponse.text() + } catch (_e2) { + errorData = null + } } - const error = errorContent.message || `${toolId} API error: ${externalResponse.statusText}` + const error = extractErrorMessage({ + status: externalResponse.status, + statusText: externalResponse.statusText, + data: errorData, + }) logger.error(`${toolId} error:`, { error }) throw new Error(error) } From 2b49d15ec80134ed9d983f42a4ec35d58356c9a5 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 13 Jan 2026 17:17:23 -0800 Subject: [PATCH 04/36] fix(comparison): add condition to prevent duplicate identical edges (#2799) * fix)comparison): add condition to prevent duplicate identical edges, ignore from workflow change detection * fix failing test * added back store check --- .../[workspaceId]/w/[workflowId]/workflow.tsx | 45 ++++++++++--------- apps/sim/stores/workflows/workflow/store.ts | 15 ++++--- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index f4e2b54883..569ccc8497 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -356,6 +356,9 @@ const WorkflowContent = React.memo(() => { /** Stores source node/handle info when a connection drag starts for drop-on-block detection. */ const connectionSourceRef = useRef<{ nodeId: string; handleId: string } | null>(null) + /** Tracks whether onConnect successfully handled the connection (ReactFlow pattern). */ + const connectionCompletedRef = useRef(false) + /** Stores start positions for multi-node drag undo/redo recording. */ const multiNodeDragStartRef = useRef>( new Map() @@ -2214,7 +2217,8 @@ const WorkflowContent = React.memo(() => { ) /** - * Captures the source handle when a connection drag starts + * Captures the source handle when a connection drag starts. + * Resets connectionCompletedRef to track if onConnect handles this connection. */ const onConnectStart = useCallback((_event: any, params: any) => { const handleId: string | undefined = params?.handleId @@ -2223,6 +2227,7 @@ const WorkflowContent = React.memo(() => { nodeId: params?.nodeId, handleId: params?.handleId, } + connectionCompletedRef.current = false }, []) /** Handles new edge connections with container boundary validation. */ @@ -2283,6 +2288,7 @@ const WorkflowContent = React.memo(() => { isInsideContainer: true, }, }) + connectionCompletedRef.current = true return } @@ -2311,6 +2317,7 @@ const WorkflowContent = React.memo(() => { } : undefined, }) + connectionCompletedRef.current = true } }, [addEdge, getNodes, blocks] @@ -2319,8 +2326,9 @@ const WorkflowContent = React.memo(() => { /** * Handles connection drag end. Detects if the edge was dropped over a block * and automatically creates a connection to that block's target handle. - * Only creates a connection if ReactFlow didn't already handle it (e.g., when - * dropping on the block body instead of a handle). + * + * Uses connectionCompletedRef to check if onConnect already handled this connection + * (ReactFlow pattern for distinguishing handle-to-handle vs handle-to-body drops). */ const onConnectEnd = useCallback( (event: MouseEvent | TouchEvent) => { @@ -2332,6 +2340,12 @@ const WorkflowContent = React.memo(() => { return } + // If onConnect already handled this connection, skip (handle-to-handle case) + if (connectionCompletedRef.current) { + connectionSourceRef.current = null + return + } + // Get cursor position in flow coordinates const clientPos = 'changedTouches' in event ? event.changedTouches[0] : event const flowPosition = screenToFlowPosition({ @@ -2342,25 +2356,14 @@ const WorkflowContent = React.memo(() => { // Find node under cursor const targetNode = findNodeAtPosition(flowPosition) - // Create connection if valid target found AND edge doesn't already exist - // ReactFlow's onConnect fires first when dropping on a handle, so we check - // if that connection already exists to avoid creating duplicates. - // IMPORTANT: We must read directly from the store (not React state) because - // the store update from ReactFlow's onConnect may not have triggered a - // React re-render yet when this callback runs (typically 1-2ms later). + // Create connection if valid target found (handle-to-body case) if (targetNode && targetNode.id !== source.nodeId) { - const currentEdges = useWorkflowStore.getState().edges - const edgeAlreadyExists = currentEdges.some( - (e) => e.source === source.nodeId && e.target === targetNode.id - ) - if (!edgeAlreadyExists) { - onConnect({ - source: source.nodeId, - sourceHandle: source.handleId, - target: targetNode.id, - targetHandle: 'target', - }) - } + onConnect({ + source: source.nodeId, + sourceHandle: source.handleId, + target: targetNode.id, + targetHandle: 'target', + }) } connectionSourceRef.current = null diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 398c662812..9f46b0de6f 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -498,8 +498,6 @@ export const useWorkflowStore = create()( const currentEdges = get().edges const newEdges = [...currentEdges] const existingEdgeIds = new Set(currentEdges.map((e) => e.id)) - // Track existing connections to prevent duplicates (same source->target) - const existingConnections = new Set(currentEdges.map((e) => `${e.source}->${e.target}`)) for (const edge of edges) { // Skip if edge ID already exists @@ -508,9 +506,15 @@ export const useWorkflowStore = create()( // Skip self-referencing edges if (edge.source === edge.target) continue - // Skip if connection already exists (same source and target) - const connectionKey = `${edge.source}->${edge.target}` - if (existingConnections.has(connectionKey)) continue + // Skip if identical connection already exists (same ports) + const connectionExists = newEdges.some( + (e) => + e.source === edge.source && + e.sourceHandle === edge.sourceHandle && + e.target === edge.target && + e.targetHandle === edge.targetHandle + ) + if (connectionExists) continue // Skip if would create a cycle if (wouldCreateCycle([...newEdges], edge.source, edge.target)) continue @@ -525,7 +529,6 @@ export const useWorkflowStore = create()( data: edge.data || {}, }) existingEdgeIds.add(edge.id) - existingConnections.add(connectionKey) } const blocks = get().blocks From ebbe67aae335f00fdeb85d9383683efe4ae04bbf Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 13 Jan 2026 17:48:19 -0800 Subject: [PATCH 05/36] fix(triggers): cleanup trigger outputs formatting, fix display name issues (#2801) * fix(triggers): package lemlist data, cleanup trigger outputs formatting, fix display name issues * cleanup trigger outputs * fix tests * more test fixes * remove branch field for ones where it's not relevant * remove branch from unrelated ops --- .claude/commands/add-trigger.md | 52 ++ apps/sim/blocks/registry.ts | 20 + apps/sim/executor/utils/start-block.ts | 15 +- apps/sim/lib/logs/get-trigger-options.ts | 25 +- apps/sim/lib/webhooks/utils.server.ts | 855 +++--------------- .../lib/workflows/comparison/compare.test.ts | 63 +- apps/sim/triggers/circleback/utils.ts | 20 - apps/sim/triggers/circleback/webhook.ts | 4 +- apps/sim/triggers/constants.ts | 8 +- apps/sim/triggers/github/issue_closed.ts | 5 + apps/sim/triggers/github/issue_comment.ts | 4 + apps/sim/triggers/github/issue_opened.ts | 5 + apps/sim/triggers/github/pr_closed.ts | 4 + apps/sim/triggers/github/pr_comment.ts | 4 + apps/sim/triggers/github/pr_merged.ts | 4 + apps/sim/triggers/github/pr_opened.ts | 4 + apps/sim/triggers/github/pr_reviewed.ts | 4 + apps/sim/triggers/github/push.ts | 8 + apps/sim/triggers/github/release_published.ts | 4 + apps/sim/triggers/github/workflow_run.ts | 4 + apps/sim/triggers/jira/utils.ts | 5 - apps/sim/triggers/lemlist/email_bounced.ts | 4 +- apps/sim/triggers/lemlist/email_clicked.ts | 4 +- apps/sim/triggers/lemlist/email_opened.ts | 4 +- apps/sim/triggers/lemlist/email_replied.ts | 4 +- apps/sim/triggers/lemlist/email_sent.ts | 4 +- apps/sim/triggers/lemlist/interested.ts | 4 +- apps/sim/triggers/lemlist/linkedin_replied.ts | 4 +- apps/sim/triggers/lemlist/not_interested.ts | 4 +- apps/sim/triggers/lemlist/utils.ts | 359 ++++---- apps/sim/triggers/lemlist/webhook.ts | 4 +- apps/sim/triggers/telegram/webhook.ts | 1 + apps/sim/triggers/typeform/webhook.ts | 2 + .../webflow/collection_item_changed.ts | 4 - .../webflow/collection_item_created.ts | 4 - .../webflow/collection_item_deleted.ts | 4 - apps/sim/triggers/webflow/form_submission.ts | 4 +- bun.lock | 1 - 38 files changed, 548 insertions(+), 984 deletions(-) diff --git a/.claude/commands/add-trigger.md b/.claude/commands/add-trigger.md index b65edf42a6..d252bf6166 100644 --- a/.claude/commands/add-trigger.md +++ b/.claude/commands/add-trigger.md @@ -552,6 +552,53 @@ All fields automatically have: - `mode: 'trigger'` - Only shown in trigger mode - `condition: { field: 'selectedTriggerId', value: triggerId }` - Only shown when this trigger is selected +## Trigger Outputs & Webhook Input Formatting + +### Important: Two Sources of Truth + +There are two related but separate concerns: + +1. **Trigger `outputs`** - Schema/contract defining what fields SHOULD be available. Used by UI for tag dropdown. +2. **`formatWebhookInput`** - Implementation that transforms raw webhook payload into actual data. Located in `apps/sim/lib/webhooks/utils.server.ts`. + +**These MUST be aligned.** The fields returned by `formatWebhookInput` should match what's defined in trigger `outputs`. If they differ: +- Tag dropdown shows fields that don't exist (broken variable resolution) +- Or actual data has fields not shown in dropdown (users can't discover them) + +### When to Add a formatWebhookInput Handler + +- **Simple providers**: If the raw webhook payload structure already matches your outputs, you don't need a handler. The generic fallback returns `body` directly. +- **Complex providers**: If you need to transform, flatten, extract nested data, compute fields, or handle conditional logic, add a handler. + +### Adding a Handler + +In `apps/sim/lib/webhooks/utils.server.ts`, add a handler block: + +```typescript +if (foundWebhook.provider === '{service}') { + // Transform raw webhook body to match trigger outputs + return { + eventType: body.type, + resourceId: body.data?.id || '', + timestamp: body.created_at, + resource: body.data, + } +} +``` + +**Key rules:** +- Return fields that match your trigger `outputs` definition exactly +- No wrapper objects like `webhook: { data: ... }` or `{service}: { ... }` +- No duplication (don't spread body AND add individual fields) +- Use `null` for missing optional data, not empty objects with empty strings + +### Verify Alignment + +Run the alignment checker: +```bash +bunx scripts/check-trigger-alignment.ts {service} +``` + ## Trigger Outputs Trigger outputs use the same schema as block outputs (NOT tool outputs). @@ -649,6 +696,11 @@ export const {service}WebhookTrigger: TriggerConfig = { - [ ] Added `delete{Service}Webhook` function to `provider-subscriptions.ts` - [ ] Added provider to `cleanupExternalWebhook` function +### Webhook Input Formatting +- [ ] Added handler in `apps/sim/lib/webhooks/utils.server.ts` (if custom formatting needed) +- [ ] Handler returns fields matching trigger `outputs` exactly +- [ ] Run `bunx scripts/check-trigger-alignment.ts {service}` to verify alignment + ### Testing - [ ] Run `bun run type-check` to verify no TypeScript errors - [ ] Restart dev server to pick up new triggers diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 06752d5d68..865294d20d 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -313,6 +313,26 @@ export const getBlock = (type: string): BlockConfig | undefined => { return registry[normalized] } +export const getLatestBlock = (baseType: string): BlockConfig | undefined => { + const normalized = baseType.replace(/-/g, '_') + + const versionedKeys = Object.keys(registry).filter((key) => { + const match = key.match(new RegExp(`^${normalized}_v(\\d+)$`)) + return match !== null + }) + + if (versionedKeys.length > 0) { + const sorted = versionedKeys.sort((a, b) => { + const versionA = Number.parseInt(a.match(/_v(\d+)$/)?.[1] || '0', 10) + const versionB = Number.parseInt(b.match(/_v(\d+)$/)?.[1] || '0', 10) + return versionB - versionA + }) + return registry[sorted[0]] + } + + return registry[normalized] +} + export const getBlockByToolName = (toolName: string): BlockConfig | undefined => { return Object.values(registry).find((block) => block.tools?.access?.includes(toolName)) } diff --git a/apps/sim/executor/utils/start-block.ts b/apps/sim/executor/utils/start-block.ts index f6c9753bb8..1ed90c3710 100644 --- a/apps/sim/executor/utils/start-block.ts +++ b/apps/sim/executor/utils/start-block.ts @@ -378,21 +378,10 @@ function buildManualTriggerOutput( } function buildIntegrationTriggerOutput( - finalInput: unknown, + _finalInput: unknown, workflowInput: unknown ): NormalizedBlockOutput { - const base: NormalizedBlockOutput = isPlainObject(workflowInput) - ? ({ ...(workflowInput as Record) } as NormalizedBlockOutput) - : {} - - if (isPlainObject(finalInput)) { - Object.assign(base, finalInput as Record) - base.input = { ...(finalInput as Record) } - } else { - base.input = finalInput - } - - return mergeFilesIntoOutput(base, workflowInput) + return isPlainObject(workflowInput) ? (workflowInput as NormalizedBlockOutput) : {} } function extractSubBlocks(block: SerializedBlock): Record | undefined { diff --git a/apps/sim/lib/logs/get-trigger-options.ts b/apps/sim/lib/logs/get-trigger-options.ts index fd704c7755..6aad83ae0d 100644 --- a/apps/sim/lib/logs/get-trigger-options.ts +++ b/apps/sim/lib/logs/get-trigger-options.ts @@ -1,4 +1,4 @@ -import { getBlock } from '@/blocks/registry' +import { getLatestBlock } from '@/blocks/registry' import { getAllTriggers } from '@/triggers' export interface TriggerOption { @@ -49,22 +49,13 @@ export function getTriggerOptions(): TriggerOption[] { continue } - const block = getBlock(provider) - - if (block) { - providerMap.set(provider, { - value: provider, - label: block.name, // Use block's display name (e.g., "Slack", "GitHub") - color: block.bgColor || '#6b7280', // Use block's hex color, fallback to gray - }) - } else { - const label = formatProviderName(provider) - providerMap.set(provider, { - value: provider, - label, - color: '#6b7280', // gray fallback - }) - } + const block = getLatestBlock(provider) + + providerMap.set(provider, { + value: provider, + label: block?.name || formatProviderName(provider), + color: block?.bgColor || '#6b7280', + }) } const integrationOptions = Array.from(providerMap.values()).sort((a, b) => diff --git a/apps/sim/lib/webhooks/utils.server.ts b/apps/sim/lib/webhooks/utils.server.ts index 02fbac769b..923cfc6508 100644 --- a/apps/sim/lib/webhooks/utils.server.ts +++ b/apps/sim/lib/webhooks/utils.server.ts @@ -177,18 +177,10 @@ async function formatTeamsGraphNotification( keys: Object.keys(body || {}), }) return { - input: 'Teams notification received', - webhook: { - data: { - provider: 'microsoft-teams', - path: foundWebhook?.path || '', - providerConfig: foundWebhook?.providerConfig || {}, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + from: null, + message: { raw: body }, + activity: body, + conversation: null, } } const resolvedChatId = chatId as string @@ -431,31 +423,12 @@ async function formatTeamsGraphNotification( hasCredential: !!credentialId, }) return { - input: '', - message_id: messageId, - chat_id: chatId, - from_name: 'Unknown', + message_id: resolvedMessageId, + chat_id: resolvedChatId, + from_name: '', text: '', - created_at: notification.resourceData?.createdDateTime || '', - change_type: changeType, - subscription_id: subscriptionId, + created_at: '', attachments: [], - microsoftteams: { - message: { id: messageId, text: '', timestamp: '', chatId, raw: null }, - from: { id: '', name: 'Unknown', aadObjectId: '' }, - notification: { changeType, subscriptionId, resource }, - }, - webhook: { - data: { - provider: 'microsoft-teams', - path: foundWebhook?.path || '', - providerConfig: foundWebhook?.providerConfig || {}, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -464,45 +437,12 @@ async function formatTeamsGraphNotification( const createdAt = message.createdDateTime || '' return { - input: messageText, - message_id: messageId, - chat_id: chatId, - from_name: from.displayName || 'Unknown', + message_id: resolvedMessageId, + chat_id: resolvedChatId, + from_name: from.displayName || '', text: messageText, created_at: createdAt, - change_type: changeType, - subscription_id: subscriptionId, attachments: rawAttachments, - microsoftteams: { - message: { - id: messageId, - text: messageText, - timestamp: createdAt, - chatId, - raw: message, - }, - from: { - id: from.id, - name: from.displayName, - aadObjectId: from.aadObjectId, - }, - notification: { - changeType, - subscriptionId, - resource, - }, - }, - webhook: { - data: { - provider: 'microsoft-teams', - path: foundWebhook?.path || '', - providerConfig: foundWebhook?.providerConfig || {}, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -587,166 +527,73 @@ export async function formatWebhookInput( if (messages.length > 0) { const message = messages[0] - const phoneNumberId = data.metadata?.phone_number_id - const from = message.from - const messageId = message.id - const timestamp = message.timestamp - const text = message.text?.body - return { - whatsapp: { - data: { - messageId, - from, - phoneNumberId, - text, - timestamp, - raw: message, - }, - }, - webhook: { - data: { - provider: 'whatsapp', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + messageId: message.id, + from: message.from, + phoneNumberId: data.metadata?.phone_number_id, + text: message.text?.body, + timestamp: message.timestamp, + raw: JSON.stringify(message), } } return null } if (foundWebhook.provider === 'telegram') { - const message = + const rawMessage = body?.message || body?.edited_message || body?.channel_post || body?.edited_channel_post - if (message) { - let input = '' - - if (message.text) { - input = message.text - } else if (message.caption) { - input = message.caption - } else if (message.photo) { - input = 'Photo message' - } else if (message.document) { - input = `Document: ${message.document.file_name || 'file'}` - } else if (message.audio) { - input = `Audio: ${message.audio.title || 'audio file'}` - } else if (message.video) { - input = 'Video message' - } else if (message.voice) { - input = 'Voice message' - } else if (message.sticker) { - input = `Sticker: ${message.sticker.emoji || '🎭'}` - } else if (message.location) { - input = 'Location shared' - } else if (message.contact) { - input = `Contact: ${message.contact.first_name || 'contact'}` - } else if (message.poll) { - input = `Poll: ${message.poll.question}` - } else { - input = 'Message received' - } - - const messageObj = { - id: message.message_id, - text: message.text, - caption: message.caption, - date: message.date, - messageType: message.photo - ? 'photo' - : message.document - ? 'document' - : message.audio - ? 'audio' - : message.video - ? 'video' - : message.voice - ? 'voice' - : message.sticker - ? 'sticker' - : message.location - ? 'location' - : message.contact - ? 'contact' - : message.poll - ? 'poll' - : 'text', - raw: message, - } - - const senderObj = message.from - ? { - id: message.from.id, - firstName: message.from.first_name, - lastName: message.from.last_name, - username: message.from.username, - languageCode: message.from.language_code, - isBot: message.from.is_bot, - } - : null - - const chatObj = message.chat - ? { - id: message.chat.id, - type: message.chat.type, - title: message.chat.title, - username: message.chat.username, - firstName: message.chat.first_name, - lastName: message.chat.last_name, - } - : null + const updateType = body.message + ? 'message' + : body.edited_message + ? 'edited_message' + : body.channel_post + ? 'channel_post' + : body.edited_channel_post + ? 'edited_channel_post' + : 'unknown' + + if (rawMessage) { + const messageType = rawMessage.photo + ? 'photo' + : rawMessage.document + ? 'document' + : rawMessage.audio + ? 'audio' + : rawMessage.video + ? 'video' + : rawMessage.voice + ? 'voice' + : rawMessage.sticker + ? 'sticker' + : rawMessage.location + ? 'location' + : rawMessage.contact + ? 'contact' + : rawMessage.poll + ? 'poll' + : 'text' return { - input, - - // Top-level properties for backward compatibility with syntax - message: messageObj, - sender: senderObj, - chat: chatObj, - updateId: body.update_id, - updateType: body.message - ? 'message' - : body.edited_message - ? 'edited_message' - : body.channel_post - ? 'channel_post' - : body.edited_channel_post - ? 'edited_channel_post' - : 'unknown', - - // Keep the nested structure for the new telegram.message.text syntax - telegram: { - message: messageObj, - sender: senderObj, - chat: chatObj, - updateId: body.update_id, - updateType: body.message - ? 'message' - : body.edited_message - ? 'edited_message' - : body.channel_post - ? 'channel_post' - : body.edited_channel_post - ? 'edited_channel_post' - : 'unknown', + message: { + id: rawMessage.message_id, + text: rawMessage.text, + date: rawMessage.date, + messageType, + raw: rawMessage, }, - webhook: { - data: { - provider: 'telegram', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + sender: rawMessage.from + ? { + id: rawMessage.from.id, + username: rawMessage.from.username, + firstName: rawMessage.from.first_name, + lastName: rawMessage.from.last_name, + languageCode: rawMessage.from.language_code, + isBot: rawMessage.from.is_bot, + } + : null, + updateId: body.update_id, + updateType, } } @@ -756,23 +603,8 @@ export async function formatWebhookInput( }) return { - input: 'Telegram update received', - telegram: { - updateId: body.update_id, - updateType: 'unknown', - raw: body, - }, - webhook: { - data: { - provider: 'telegram', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + updateId: body.update_id, + updateType, } } @@ -810,40 +642,15 @@ export async function formatWebhookInput( callerZip: body.CallerZip, callerCountry: body.CallerCountry, callToken: body.CallToken, - - webhook: { - data: { - provider: 'twilio_voice', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + raw: JSON.stringify(body), } } if (foundWebhook.provider === 'gmail') { if (body && typeof body === 'object' && 'email' in body) { - const email = body.email as Record - const timestamp = body.timestamp return { - ...email, - email, - ...(timestamp !== undefined && { timestamp }), - webhook: { - data: { - provider: 'gmail', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + email: body.email, + timestamp: body.timestamp, } } return body @@ -851,23 +658,9 @@ export async function formatWebhookInput( if (foundWebhook.provider === 'outlook') { if (body && typeof body === 'object' && 'email' in body) { - const email = body.email as Record - const timestamp = body.timestamp return { - ...email, - email, - ...(timestamp !== undefined && { timestamp }), - webhook: { - data: { - provider: 'outlook', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + email: body.email, + timestamp: body.timestamp, } } return body @@ -875,26 +668,10 @@ export async function formatWebhookInput( if (foundWebhook.provider === 'rss') { if (body && typeof body === 'object' && 'item' in body) { - const item = body.item as Record - const feed = body.feed as Record - return { - title: item?.title, - link: item?.link, - pubDate: item?.pubDate, - item, - feed, - webhook: { - data: { - provider: 'rss', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + item: body.item, + feed: body.feed, + timestamp: body.timestamp, } } return body @@ -902,32 +679,9 @@ export async function formatWebhookInput( if (foundWebhook.provider === 'imap') { if (body && typeof body === 'object' && 'email' in body) { - const email = body.email as Record return { - messageId: email?.messageId, - subject: email?.subject, - from: email?.from, - to: email?.to, - cc: email?.cc, - date: email?.date, - bodyText: email?.bodyText, - bodyHtml: email?.bodyHtml, - mailbox: email?.mailbox, - hasAttachments: email?.hasAttachments, - attachments: email?.attachments, - email, + email: body.email, timestamp: body.timestamp, - webhook: { - data: { - provider: 'imap', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } return body @@ -952,7 +706,6 @@ export async function formatWebhookInput( payload: body, provider: 'hubspot', providerConfig: foundWebhook.providerConfig, - workflowId: foundWorkflow.id, } } @@ -997,24 +750,10 @@ export async function formatWebhookInput( const activityObj = body || {} return { - input: messageText, - from: fromObj, message: messageObj, activity: activityObj, conversation: conversationObj, - - webhook: { - data: { - provider: 'microsoft-teams', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -1022,46 +761,19 @@ export async function formatWebhookInput( const event = body?.event if (event && body?.type === 'event_callback') { - let input = '' - - if (event.text) { - input = event.text - } else if (event.type === 'app_mention') { - input = 'App mention received' - } else { - input = 'Slack event received' - } - - const eventObj = { - event_type: event.type || '', - channel: event.channel || '', - channel_name: '', - user: event.user || '', - user_name: '', - text: event.text || '', - timestamp: event.ts || event.event_ts || '', - team_id: body.team_id || event.team || '', - event_id: body.event_id || '', - } - return { - input, - - event: eventObj, - slack: { - event: eventObj, - }, - webhook: { - data: { - provider: 'slack', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, + event: { + event_type: event.type || '', + channel: event.channel || '', + channel_name: '', + user: event.user || '', + user_name: '', + text: event.text || '', + timestamp: event.ts || event.event_ts || '', + thread_ts: event.thread_ts || '', + team_id: body.team_id || event.team || '', + event_id: body.event_id || '', }, - workflowId: foundWorkflow.id, } } @@ -1072,80 +784,31 @@ export async function formatWebhookInput( }) return { - input: 'Slack webhook received', - slack: { - event: { - event_type: body?.event?.type || body?.type || 'unknown', - channel: body?.event?.channel || '', - user: body?.event?.user || '', - text: body?.event?.text || '', - timestamp: body?.event?.ts || '', - team_id: body?.team_id || '', - event_id: body?.event_id || '', - }, - }, - webhook: { - data: { - provider: 'slack', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, + event: { + event_type: body?.event?.type || body?.type || 'unknown', + channel: body?.event?.channel || '', + channel_name: '', + user: body?.event?.user || '', + user_name: '', + text: body?.event?.text || '', + timestamp: body?.event?.ts || '', + thread_ts: body?.event?.thread_ts || '', + team_id: body?.team_id || '', + event_id: body?.event_id || '', }, - workflowId: foundWorkflow.id, } } if (foundWebhook.provider === 'webflow') { - const triggerType = body?.triggerType || 'unknown' - const siteId = body?.siteId || '' - const workspaceId = body?.workspaceId || '' - const collectionId = body?.collectionId || '' - const payload = body?.payload || {} - const formId = body?.formId || '' - const formName = body?.name || '' - const formSubmissionId = body?.id || '' - const submittedAt = body?.submittedAt || '' - const formData = body?.data || {} - const schema = body?.schema || {} - return { - siteId, - workspaceId, - collectionId, - payload, - triggerType, - - formId, - name: formName, - id: formSubmissionId, - submittedAt, - data: formData, - schema, + siteId: body?.siteId || '', + formId: body?.formId || '', + name: body?.name || '', + id: body?.id || '', + submittedAt: body?.submittedAt || '', + data: body?.data || {}, + schema: body?.schema || {}, formElementId: body?.formElementId || '', - - webflow: { - siteId, - workspaceId, - collectionId, - payload, - triggerType, - raw: body, - }, - - webhook: { - data: { - provider: 'webflow', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -1156,7 +819,6 @@ export async function formatWebhookInput( if (foundWebhook.provider === 'google_forms') { const providerConfig = (foundWebhook.providerConfig as Record) || {} - // Normalize answers: if value is an array with single element, collapse to scalar; keep multi-select arrays const normalizeAnswers = (src: unknown): Record => { if (!src || typeof src !== 'object') return {} const out: Record = {} @@ -1176,205 +838,47 @@ export async function formatWebhookInput( const formId = body?.formId || providerConfig.formId || '' const includeRaw = providerConfig.includeRawPayload !== false - const normalizedAnswers = normalizeAnswers(body?.answers) - - const summaryCount = Object.keys(normalizedAnswers).length - const input = `Google Form response${responseId ? ` ${responseId}` : ''} (${summaryCount} answers)` - return { - input, responseId, createTime, lastSubmittedTime, formId, - answers: normalizedAnswers, + answers: normalizeAnswers(body?.answers), ...(includeRaw ? { raw: body?.raw ?? body } : {}), - google_forms: { - responseId, - createTime, - lastSubmittedTime, - formId, - answers: normalizedAnswers, - ...(includeRaw ? { raw: body?.raw ?? body } : {}), - }, - webhook: { - data: { - provider: 'google_forms', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: includeRaw ? body : undefined, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } if (foundWebhook.provider === 'github') { - // GitHub webhook input formatting logic const eventType = request.headers.get('x-github-event') || 'unknown' - const delivery = request.headers.get('x-github-delivery') || '' - - // Extract common GitHub properties - const repository = body?.repository || {} - const sender = body?.sender || {} - const action = body?.action || '' - - // Build GitHub-specific variables based on the trigger config outputs - const githubData = { - // Event metadata - event_type: eventType, - action: action, - delivery_id: delivery, - - // Repository information (avoid 'repository' to prevent conflict with the object) - repository_full_name: repository.full_name || '', - repository_name: repository.name || '', - repository_owner: repository.owner?.login || '', - repository_id: repository.id || '', - repository_url: repository.html_url || '', - - // Sender information (avoid 'sender' to prevent conflict with the object) - sender_login: sender.login || '', - sender_id: sender.id || '', - sender_type: sender.type || '', - sender_url: sender.html_url || '', - - // Event-specific data - ...(body?.ref && { - ref: body.ref, - branch: body.ref?.replace('refs/heads/', '') || '', - }), - ...(body?.before && { before: body.before }), - ...(body?.after && { after: body.after }), - ...(body?.commits && { - commits: JSON.stringify(body.commits), - commit_count: body.commits.length || 0, - }), - ...(body?.head_commit && { - commit_message: body.head_commit.message || '', - commit_author: body.head_commit.author?.name || '', - commit_sha: body.head_commit.id || '', - commit_url: body.head_commit.url || '', - }), - ...(body?.pull_request && { - pull_request: JSON.stringify(body.pull_request), - pr_number: body.pull_request.number || '', - pr_title: body.pull_request.title || '', - pr_state: body.pull_request.state || '', - pr_url: body.pull_request.html_url || '', - }), - ...(body?.issue && { - issue: JSON.stringify(body.issue), - issue_number: body.issue.number || '', - issue_title: body.issue.title || '', - issue_state: body.issue.state || '', - issue_url: body.issue.html_url || '', - }), - ...(body?.comment && { - comment: JSON.stringify(body.comment), - comment_body: body.comment.body || '', - comment_url: body.comment.html_url || '', - }), - } - - // Set input based on event type for workflow processing - let input = '' - switch (eventType) { - case 'push': - input = `Push to ${githubData.branch || githubData.ref}: ${githubData.commit_message || 'No commit message'}` - break - case 'pull_request': - input = `${action} pull request: ${githubData.pr_title || 'No title'}` - break - case 'issues': - input = `${action} issue: ${githubData.issue_title || 'No title'}` - break - case 'issue_comment': - case 'pull_request_review_comment': - input = `Comment ${action}: ${githubData.comment_body?.slice(0, 100) || 'No comment body'}${(githubData.comment_body?.length || 0) > 100 ? '...' : ''}` - break - default: - input = `GitHub ${eventType} event${action ? ` (${action})` : ''}` - } + const branch = body?.ref?.replace('refs/heads/', '') || '' return { ...body, - webhook: { - data: { - provider: 'github', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + event_type: eventType, + action: body?.action || '', + branch, } } if (foundWebhook.provider === 'typeform') { - const eventId = body?.event_id || '' - const eventType = body?.event_type || 'form_response' const formResponse = body?.form_response || {} - const formId = formResponse.form_id || '' - const token = formResponse.token || '' - const submittedAt = formResponse.submitted_at || '' - const landedAt = formResponse.landed_at || '' - const calculated = formResponse.calculated || {} - const variables = formResponse.variables || [] - const hidden = formResponse.hidden || {} - const answers = formResponse.answers || [] - const definition = formResponse.definition || {} - const ending = formResponse.ending || {} - const providerConfig = (foundWebhook.providerConfig as Record) || {} const includeDefinition = providerConfig.includeDefinition === true return { - event_id: eventId, - event_type: eventType, - form_id: formId, - token, - submitted_at: submittedAt, - landed_at: landedAt, - calculated, - variables, - hidden, - answers, - ...(includeDefinition ? { definition } : {}), - ending, - - typeform: { - event_id: eventId, - event_type: eventType, - form_id: formId, - token, - submitted_at: submittedAt, - landed_at: landedAt, - calculated, - variables, - hidden, - answers, - ...(includeDefinition ? { definition } : {}), - ending, - }, - + event_id: body?.event_id || '', + event_type: body?.event_type || 'form_response', + form_id: formResponse.form_id || '', + token: formResponse.token || '', + submitted_at: formResponse.submitted_at || '', + landed_at: formResponse.landed_at || '', + calculated: formResponse.calculated || {}, + variables: formResponse.variables || [], + hidden: formResponse.hidden || {}, + answers: formResponse.answers || [], + ...(includeDefinition ? { definition: formResponse.definition || {} } : {}), + ending: formResponse.ending || {}, raw: body, - - webhook: { - data: { - provider: 'typeform', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -1389,17 +893,6 @@ export async function formatWebhookInput( actor: body.actor || null, data: body.data || null, updatedFrom: body.updatedFrom || null, - webhook: { - data: { - provider: 'linear', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -1411,46 +904,17 @@ export async function formatWebhookInput( const providerConfig = (foundWebhook.providerConfig as Record) || {} const triggerId = providerConfig.triggerId as string | undefined - let extractedData if (triggerId === 'jira_issue_commented') { - extractedData = extractCommentData(body) - } else if (triggerId === 'jira_worklog_created') { - extractedData = extractWorklogData(body) - } else { - extractedData = extractIssueData(body) + return extractCommentData(body) } - - return { - ...extractedData, - webhook: { - data: { - provider: 'jira', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + if (triggerId === 'jira_worklog_created') { + return extractWorklogData(body) } + return extractIssueData(body) } if (foundWebhook.provider === 'stripe') { - return { - ...body, - webhook: { - data: { - provider: 'stripe', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, - } + return body } if (foundWebhook.provider === 'calendly') { @@ -1459,17 +923,6 @@ export async function formatWebhookInput( created_at: body.created_at, created_by: body.created_by, payload: body.payload, - webhook: { - data: { - provider: 'calendly', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -1489,17 +942,6 @@ export async function formatWebhookInput( transcript: body.transcript || [], insights: body.insights || {}, meeting: body, - webhook: { - data: { - provider: 'circleback', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -1508,57 +950,18 @@ export async function formatWebhookInput( type: body.type, user_id: body.user_id, data: body.data || {}, - - webhook: { - data: { - provider: 'grain', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } if (foundWebhook.provider === 'fireflies') { - // Fireflies webhook payload uses camelCase: - // { meetingId, eventType, clientReferenceId } return { meetingId: body.meetingId || '', eventType: body.eventType || 'Transcription completed', clientReferenceId: body.clientReferenceId || '', - - webhook: { - data: { - provider: 'fireflies', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } - // Generic format for other providers - return { - webhook: { - data: { - path: foundWebhook.path, - provider: foundWebhook.provider, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, - } + return body } /** diff --git a/apps/sim/lib/workflows/comparison/compare.test.ts b/apps/sim/lib/workflows/comparison/compare.test.ts index d0abb00409..31af020e30 100644 --- a/apps/sim/lib/workflows/comparison/compare.test.ts +++ b/apps/sim/lib/workflows/comparison/compare.test.ts @@ -2290,7 +2290,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, webhookId: { value: null }, }, }), @@ -2302,7 +2302,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, webhookId: { value: 'wh_123456' }, }, }), @@ -2318,7 +2318,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, triggerPath: { value: '' }, }, }), @@ -2330,7 +2330,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, triggerPath: { value: '/api/webhooks/abc123' }, }, }), @@ -2346,7 +2346,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, webhookId: { value: null }, triggerPath: { value: '' }, }, @@ -2359,7 +2359,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, webhookId: { value: 'wh_123456' }, triggerPath: { value: '/api/webhooks/abc123' }, }, @@ -2371,14 +2371,18 @@ describe('hasWorkflowChanged', () => { }) it.concurrent( - 'should detect change when triggerConfig differs but runtime metadata also differs', + 'should detect change when actual config differs but runtime metadata also differs', () => { + // Test that when a real config field changes along with runtime metadata, + // the change is still detected. Using 'model' as the config field since + // triggerConfig is now excluded from comparison (individual trigger fields + // are compared separately). const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, webhookId: { value: null }, }, }), @@ -2390,7 +2394,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'pull_request' } }, + model: { value: 'gpt-4o' }, webhookId: { value: 'wh_123456' }, }, }), @@ -2402,8 +2406,12 @@ describe('hasWorkflowChanged', () => { ) it.concurrent( - 'should not detect change when runtime metadata is added to current state', + 'should not detect change when triggerConfig differs (individual fields compared separately)', () => { + // triggerConfig is excluded from comparison because: + // 1. Individual trigger fields are stored as separate subblocks and compared individually + // 2. The client populates triggerConfig with default values from trigger definitions, + // which aren't present in the deployed state, causing false positive change detection const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { @@ -2420,7 +2428,36 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + triggerConfig: { value: { event: 'pull_request', extraField: true } }, + }, + }), + }, + }) + + expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) + } + ) + + it.concurrent( + 'should not detect change when runtime metadata is added to current state', + () => { + const deployedState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + model: { value: 'gpt-4' }, + }, + }), + }, + }) + + const currentState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + model: { value: 'gpt-4' }, webhookId: { value: 'wh_123456' }, triggerPath: { value: '/api/webhooks/abc123' }, }, @@ -2440,7 +2477,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, webhookId: { value: 'wh_old123' }, triggerPath: { value: '/api/webhooks/old' }, }, @@ -2453,7 +2490,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, }, }), }, diff --git a/apps/sim/triggers/circleback/utils.ts b/apps/sim/triggers/circleback/utils.ts index a9480cc0a8..bee5bd3922 100644 --- a/apps/sim/triggers/circleback/utils.ts +++ b/apps/sim/triggers/circleback/utils.ts @@ -96,23 +96,3 @@ export function buildMeetingOutputs(): Record { }, } as Record } - -/** - * Build output schema for generic webhook events - */ -export function buildGenericOutputs(): Record { - return { - payload: { - type: 'object', - description: 'Raw webhook payload', - }, - headers: { - type: 'object', - description: 'Request headers', - }, - timestamp: { - type: 'string', - description: 'ISO8601 received timestamp', - }, - } as Record -} diff --git a/apps/sim/triggers/circleback/webhook.ts b/apps/sim/triggers/circleback/webhook.ts index 017a066b9e..f618deaf86 100644 --- a/apps/sim/triggers/circleback/webhook.ts +++ b/apps/sim/triggers/circleback/webhook.ts @@ -1,6 +1,6 @@ import { CirclebackIcon } from '@/components/icons' import type { TriggerConfig } from '@/triggers/types' -import { buildGenericOutputs, circlebackSetupInstructions, circlebackTriggerOptions } from './utils' +import { buildMeetingOutputs, circlebackSetupInstructions, circlebackTriggerOptions } from './utils' export const circlebackWebhookTrigger: TriggerConfig = { id: 'circleback_webhook', @@ -74,7 +74,7 @@ export const circlebackWebhookTrigger: TriggerConfig = { }, ], - outputs: buildGenericOutputs(), + outputs: buildMeetingOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/constants.ts b/apps/sim/triggers/constants.ts index 6c082b76f8..354994db04 100644 --- a/apps/sim/triggers/constants.ts +++ b/apps/sim/triggers/constants.ts @@ -31,8 +31,14 @@ export const TRIGGER_PERSISTED_SUBBLOCK_IDS: string[] = [ /** * Trigger-related subblock IDs that represent runtime metadata. They should remain * in the workflow state but must not be modified or cleared by diff operations. + * + * Note: 'triggerConfig' is included because it's an aggregate of individual trigger + * field subblocks. Those individual fields are compared separately, so comparing + * triggerConfig would be redundant. Additionally, the client populates triggerConfig + * with default values from the trigger definition on load, which aren't present in + * the deployed state, causing false positive change detection. */ -export const TRIGGER_RUNTIME_SUBBLOCK_IDS: string[] = ['webhookId', 'triggerPath'] +export const TRIGGER_RUNTIME_SUBBLOCK_IDS: string[] = ['webhookId', 'triggerPath', 'triggerConfig'] /** * Maximum number of consecutive failures before a trigger (schedule/webhook) is auto-disabled. diff --git a/apps/sim/triggers/github/issue_closed.ts b/apps/sim/triggers/github/issue_closed.ts index f2bbe12e39..aa22275a37 100644 --- a/apps/sim/triggers/github/issue_closed.ts +++ b/apps/sim/triggers/github/issue_closed.ts @@ -116,6 +116,11 @@ export const githubIssueClosedTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: + 'GitHub event type from X-GitHub-Event header (e.g., issues, pull_request, push)', + }, action: { type: 'string', description: 'Action performed (opened, closed, reopened, edited, etc.)', diff --git a/apps/sim/triggers/github/issue_comment.ts b/apps/sim/triggers/github/issue_comment.ts index 972d244120..db40982e92 100644 --- a/apps/sim/triggers/github/issue_comment.ts +++ b/apps/sim/triggers/github/issue_comment.ts @@ -117,6 +117,10 @@ export const githubIssueCommentTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., issue_comment)', + }, action: { type: 'string', description: 'Action performed (created, edited, deleted)', diff --git a/apps/sim/triggers/github/issue_opened.ts b/apps/sim/triggers/github/issue_opened.ts index e05caad03b..da4b2e1f2a 100644 --- a/apps/sim/triggers/github/issue_opened.ts +++ b/apps/sim/triggers/github/issue_opened.ts @@ -137,6 +137,11 @@ export const githubIssueOpenedTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: + 'GitHub event type from X-GitHub-Event header (e.g., issues, pull_request, push)', + }, action: { type: 'string', description: 'Action performed (opened, closed, reopened, edited, etc.)', diff --git a/apps/sim/triggers/github/pr_closed.ts b/apps/sim/triggers/github/pr_closed.ts index b60c1043f4..a654c0da43 100644 --- a/apps/sim/triggers/github/pr_closed.ts +++ b/apps/sim/triggers/github/pr_closed.ts @@ -117,6 +117,10 @@ export const githubPRClosedTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request)', + }, action: { type: 'string', description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)', diff --git a/apps/sim/triggers/github/pr_comment.ts b/apps/sim/triggers/github/pr_comment.ts index 2fab088a70..70b5f9a5c9 100644 --- a/apps/sim/triggers/github/pr_comment.ts +++ b/apps/sim/triggers/github/pr_comment.ts @@ -117,6 +117,10 @@ export const githubPRCommentTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., issue_comment)', + }, action: { type: 'string', description: 'Action performed (created, edited, deleted)', diff --git a/apps/sim/triggers/github/pr_merged.ts b/apps/sim/triggers/github/pr_merged.ts index 23ecdad3a8..24b2b8205c 100644 --- a/apps/sim/triggers/github/pr_merged.ts +++ b/apps/sim/triggers/github/pr_merged.ts @@ -116,6 +116,10 @@ export const githubPRMergedTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request)', + }, action: { type: 'string', description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)', diff --git a/apps/sim/triggers/github/pr_opened.ts b/apps/sim/triggers/github/pr_opened.ts index cced084ba5..3288cc0c6f 100644 --- a/apps/sim/triggers/github/pr_opened.ts +++ b/apps/sim/triggers/github/pr_opened.ts @@ -116,6 +116,10 @@ export const githubPROpenedTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request)', + }, action: { type: 'string', description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)', diff --git a/apps/sim/triggers/github/pr_reviewed.ts b/apps/sim/triggers/github/pr_reviewed.ts index a5affcd839..8105f983f0 100644 --- a/apps/sim/triggers/github/pr_reviewed.ts +++ b/apps/sim/triggers/github/pr_reviewed.ts @@ -117,6 +117,10 @@ export const githubPRReviewedTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request_review)', + }, action: { type: 'string', description: 'Action performed (submitted, edited, dismissed)', diff --git a/apps/sim/triggers/github/push.ts b/apps/sim/triggers/github/push.ts index 7892110245..36ce192e5d 100644 --- a/apps/sim/triggers/github/push.ts +++ b/apps/sim/triggers/github/push.ts @@ -116,6 +116,14 @@ export const githubPushTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., push)', + }, + branch: { + type: 'string', + description: 'Branch name derived from ref (e.g., main from refs/heads/main)', + }, ref: { type: 'string', description: 'Git reference that was pushed (e.g., refs/heads/main)', diff --git a/apps/sim/triggers/github/release_published.ts b/apps/sim/triggers/github/release_published.ts index 3d10bb7423..7e8698d5a9 100644 --- a/apps/sim/triggers/github/release_published.ts +++ b/apps/sim/triggers/github/release_published.ts @@ -116,6 +116,10 @@ export const githubReleasePublishedTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., release)', + }, action: { type: 'string', description: diff --git a/apps/sim/triggers/github/workflow_run.ts b/apps/sim/triggers/github/workflow_run.ts index 65e8053301..dc30c81b20 100644 --- a/apps/sim/triggers/github/workflow_run.ts +++ b/apps/sim/triggers/github/workflow_run.ts @@ -117,6 +117,10 @@ export const githubWorkflowRunTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., workflow_run)', + }, action: { type: 'string', description: 'Action performed (requested, in_progress, completed)', diff --git a/apps/sim/triggers/jira/utils.ts b/apps/sim/triggers/jira/utils.ts index 3a3386e9cc..15fdab162b 100644 --- a/apps/sim/triggers/jira/utils.ts +++ b/apps/sim/triggers/jira/utils.ts @@ -265,11 +265,6 @@ function buildBaseWebhookOutputs(): Record { }, }, }, - - webhook: { - type: 'json', - description: 'Webhook metadata including provider, path, and raw payload', - }, } } diff --git a/apps/sim/triggers/lemlist/email_bounced.ts b/apps/sim/triggers/lemlist/email_bounced.ts index cd79184716..cda91d0b6a 100644 --- a/apps/sim/triggers/lemlist/email_bounced.ts +++ b/apps/sim/triggers/lemlist/email_bounced.ts @@ -1,7 +1,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildActivityOutputs, + buildEmailBouncedOutputs, buildLemlistExtraFields, lemlistSetupInstructions, lemlistTriggerOptions, @@ -27,7 +27,7 @@ export const lemlistEmailBouncedTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_email_bounced'), }), - outputs: buildActivityOutputs(), + outputs: buildEmailBouncedOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/email_clicked.ts b/apps/sim/triggers/lemlist/email_clicked.ts index a1da3ff6ba..e618ef0253 100644 --- a/apps/sim/triggers/lemlist/email_clicked.ts +++ b/apps/sim/triggers/lemlist/email_clicked.ts @@ -1,7 +1,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildActivityOutputs, + buildEmailClickedOutputs, buildLemlistExtraFields, lemlistSetupInstructions, lemlistTriggerOptions, @@ -27,7 +27,7 @@ export const lemlistEmailClickedTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_email_clicked'), }), - outputs: buildActivityOutputs(), + outputs: buildEmailClickedOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/email_opened.ts b/apps/sim/triggers/lemlist/email_opened.ts index 12c6638e81..e6ac4c574b 100644 --- a/apps/sim/triggers/lemlist/email_opened.ts +++ b/apps/sim/triggers/lemlist/email_opened.ts @@ -1,7 +1,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildActivityOutputs, + buildEmailOpenedOutputs, buildLemlistExtraFields, lemlistSetupInstructions, lemlistTriggerOptions, @@ -27,7 +27,7 @@ export const lemlistEmailOpenedTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_email_opened'), }), - outputs: buildActivityOutputs(), + outputs: buildEmailOpenedOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/email_replied.ts b/apps/sim/triggers/lemlist/email_replied.ts index bb95476b3a..be2dc41521 100644 --- a/apps/sim/triggers/lemlist/email_replied.ts +++ b/apps/sim/triggers/lemlist/email_replied.ts @@ -1,7 +1,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildEmailReplyOutputs, + buildEmailRepliedOutputs, buildLemlistExtraFields, lemlistSetupInstructions, lemlistTriggerOptions, @@ -30,7 +30,7 @@ export const lemlistEmailRepliedTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_email_replied'), }), - outputs: buildEmailReplyOutputs(), + outputs: buildEmailRepliedOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/email_sent.ts b/apps/sim/triggers/lemlist/email_sent.ts index f45bdf4aaa..7fd38bb951 100644 --- a/apps/sim/triggers/lemlist/email_sent.ts +++ b/apps/sim/triggers/lemlist/email_sent.ts @@ -1,7 +1,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildActivityOutputs, + buildEmailSentOutputs, buildLemlistExtraFields, lemlistSetupInstructions, lemlistTriggerOptions, @@ -27,7 +27,7 @@ export const lemlistEmailSentTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_email_sent'), }), - outputs: buildActivityOutputs(), + outputs: buildEmailSentOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/interested.ts b/apps/sim/triggers/lemlist/interested.ts index e85ea40e9a..a0b2b64289 100644 --- a/apps/sim/triggers/lemlist/interested.ts +++ b/apps/sim/triggers/lemlist/interested.ts @@ -1,7 +1,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildActivityOutputs, + buildInterestOutputs, buildLemlistExtraFields, lemlistSetupInstructions, lemlistTriggerOptions, @@ -27,7 +27,7 @@ export const lemlistInterestedTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_interested'), }), - outputs: buildActivityOutputs(), + outputs: buildInterestOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/linkedin_replied.ts b/apps/sim/triggers/lemlist/linkedin_replied.ts index e3ccae016f..267b562094 100644 --- a/apps/sim/triggers/lemlist/linkedin_replied.ts +++ b/apps/sim/triggers/lemlist/linkedin_replied.ts @@ -2,7 +2,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { buildLemlistExtraFields, - buildLinkedInReplyOutputs, + buildLinkedInRepliedOutputs, lemlistSetupInstructions, lemlistTriggerOptions, } from '@/triggers/lemlist/utils' @@ -27,7 +27,7 @@ export const lemlistLinkedInRepliedTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_linkedin_replied'), }), - outputs: buildLinkedInReplyOutputs(), + outputs: buildLinkedInRepliedOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/not_interested.ts b/apps/sim/triggers/lemlist/not_interested.ts index 5581429012..f53f5c5127 100644 --- a/apps/sim/triggers/lemlist/not_interested.ts +++ b/apps/sim/triggers/lemlist/not_interested.ts @@ -1,7 +1,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildActivityOutputs, + buildInterestOutputs, buildLemlistExtraFields, lemlistSetupInstructions, lemlistTriggerOptions, @@ -27,7 +27,7 @@ export const lemlistNotInterestedTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_not_interested'), }), - outputs: buildActivityOutputs(), + outputs: buildInterestOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/utils.ts b/apps/sim/triggers/lemlist/utils.ts index 69fbc1d4b3..6ecbcce6cf 100644 --- a/apps/sim/triggers/lemlist/utils.ts +++ b/apps/sim/triggers/lemlist/utils.ts @@ -66,203 +66,254 @@ export function buildLemlistExtraFields(triggerId: string) { } /** - * Base activity outputs shared across all Lemlist triggers + * Core fields present in ALL Lemlist webhook payloads + * See: https://help.lemlist.com/en/articles/9423940-use-the-api-to-list-activity-types */ -function buildBaseActivityOutputs(): Record { - return { - type: { - type: 'string', - description: 'Activity type (emailsReplied, linkedinReplied, interested, emailsOpened, etc.)', - }, - _id: { - type: 'string', - description: 'Unique activity identifier', - }, - leadId: { - type: 'string', - description: 'Associated lead ID', - }, - campaignId: { - type: 'string', - description: 'Campaign ID', - }, - campaignName: { - type: 'string', - description: 'Campaign name', - }, - sequenceId: { - type: 'string', - description: 'Sequence ID within the campaign', - }, - stepId: { - type: 'string', - description: 'Step ID that triggered this activity', - }, - createdAt: { - type: 'string', - description: 'When the activity occurred (ISO 8601)', - }, - } -} +const coreOutputs = { + _id: { + type: 'string', + description: 'Unique activity identifier', + }, + type: { + type: 'string', + description: 'Activity type (e.g., emailsSent, emailsReplied)', + }, + createdAt: { + type: 'string', + description: 'Activity creation timestamp (ISO 8601)', + }, + teamId: { + type: 'string', + description: 'Lemlist team identifier', + }, + leadId: { + type: 'string', + description: 'Lead identifier', + }, + campaignId: { + type: 'string', + description: 'Campaign identifier', + }, + campaignName: { + type: 'string', + description: 'Campaign name', + }, +} as const + +/** + * Lead fields present in webhook payloads + */ +const leadOutputs = { + email: { + type: 'string', + description: 'Lead email address', + }, + firstName: { + type: 'string', + description: 'Lead first name', + }, + lastName: { + type: 'string', + description: 'Lead last name', + }, + companyName: { + type: 'string', + description: 'Lead company name', + }, + linkedinUrl: { + type: 'string', + description: 'Lead LinkedIn profile URL', + }, +} as const + +/** + * Sequence/campaign tracking fields for email activities + */ +const sequenceOutputs = { + sequenceId: { + type: 'string', + description: 'Sequence identifier', + }, + sequenceStep: { + type: 'number', + description: 'Current step in the sequence (0-indexed)', + }, + totalSequenceStep: { + type: 'number', + description: 'Total number of steps in the sequence', + }, + isFirst: { + type: 'boolean', + description: 'Whether this is the first activity of this type for this step', + }, +} as const /** - * Lead outputs - information about the lead + * Sender information fields */ -function buildLeadOutputs(): Record { +const senderOutputs = { + sendUserId: { + type: 'string', + description: 'Sender user identifier', + }, + sendUserEmail: { + type: 'string', + description: 'Sender email address', + }, + sendUserName: { + type: 'string', + description: 'Sender display name', + }, +} as const + +/** + * Email content fields + */ +const emailContentOutputs = { + subject: { + type: 'string', + description: 'Email subject line', + }, + text: { + type: 'string', + description: 'Email body content (HTML)', + }, + messageId: { + type: 'string', + description: 'Email message ID (RFC 2822 format)', + }, + emailId: { + type: 'string', + description: 'Lemlist email identifier', + }, +} as const + +/** + * Build outputs for email sent events + */ +export function buildEmailSentOutputs(): Record { return { - lead: { - _id: { - type: 'string', - description: 'Lead unique identifier', - }, - email: { - type: 'string', - description: 'Lead email address', - }, - firstName: { - type: 'string', - description: 'Lead first name', - }, - lastName: { - type: 'string', - description: 'Lead last name', - }, - companyName: { - type: 'string', - description: 'Lead company name', - }, - phone: { - type: 'string', - description: 'Lead phone number', - }, - linkedinUrl: { - type: 'string', - description: 'Lead LinkedIn profile URL', - }, - picture: { - type: 'string', - description: 'Lead profile picture URL', - }, - icebreaker: { - type: 'string', - description: 'Personalized icebreaker text', - }, - timezone: { - type: 'string', - description: 'Lead timezone (e.g., America/New_York)', - }, - isUnsubscribed: { - type: 'boolean', - description: 'Whether the lead is unsubscribed', - }, - }, - } + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, + ...senderOutputs, + ...emailContentOutputs, + } as Record } /** - * Standard activity outputs (activity + lead data) + * Build outputs for email replied events */ -export function buildActivityOutputs(): Record { +export function buildEmailRepliedOutputs(): Record { return { - ...buildBaseActivityOutputs(), - ...buildLeadOutputs(), - webhook: { - type: 'json', - description: 'Full webhook payload with all activity-specific data', - }, - } + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, + ...senderOutputs, + ...emailContentOutputs, + } as Record } /** - * Email-specific outputs (includes message content for replies) + * Build outputs for email opened events */ -export function buildEmailReplyOutputs(): Record { +export function buildEmailOpenedOutputs(): Record { return { - ...buildBaseActivityOutputs(), - ...buildLeadOutputs(), + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, + ...senderOutputs, messageId: { type: 'string', - description: 'Email message ID', - }, - subject: { - type: 'string', - description: 'Email subject line', - }, - text: { - type: 'string', - description: 'Email reply text content', - }, - html: { - type: 'string', - description: 'Email reply HTML content', + description: 'Email message ID that was opened', }, - sentAt: { - type: 'string', - description: 'When the reply was sent', - }, - webhook: { - type: 'json', - description: 'Full webhook payload with all email data', - }, - } + } as Record } /** - * LinkedIn-specific outputs (includes message content) + * Build outputs for email clicked events */ -export function buildLinkedInReplyOutputs(): Record { +export function buildEmailClickedOutputs(): Record { return { - ...buildBaseActivityOutputs(), - ...buildLeadOutputs(), + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, + ...senderOutputs, messageId: { type: 'string', - description: 'LinkedIn message ID', - }, - text: { - type: 'string', - description: 'LinkedIn message text content', + description: 'Email message ID containing the clicked link', }, - sentAt: { + clickedUrl: { type: 'string', - description: 'When the message was sent', + description: 'URL that was clicked', }, - webhook: { - type: 'json', - description: 'Full webhook payload with all LinkedIn data', - }, - } + } as Record } /** - * All outputs for generic webhook (activity + lead + all possible fields) + * Build outputs for email bounced events */ -export function buildAllOutputs(): Record { +export function buildEmailBouncedOutputs(): Record { return { - ...buildBaseActivityOutputs(), - ...buildLeadOutputs(), + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, + ...senderOutputs, messageId: { type: 'string', - description: 'Message ID (for email/LinkedIn events)', + description: 'Email message ID that bounced', }, - subject: { + errorMessage: { type: 'string', - description: 'Email subject (for email events)', + description: 'Bounce error message', }, + } as Record +} + +/** + * Build outputs for LinkedIn replied events + */ +export function buildLinkedInRepliedOutputs(): Record { + return { + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, text: { type: 'string', - description: 'Message text content', + description: 'LinkedIn message content', }, - html: { + } as Record +} + +/** + * Build outputs for interested/not interested events + */ +export function buildInterestOutputs(): Record { + return { + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, + } as Record +} + +/** + * Build outputs for generic webhook (all events) + * Includes all possible fields across event types + */ +export function buildLemlistOutputs(): Record { + return { + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, + ...senderOutputs, + ...emailContentOutputs, + clickedUrl: { type: 'string', - description: 'Message HTML content (for email events)', + description: 'URL that was clicked (for emailsClicked events)', }, - sentAt: { + errorMessage: { type: 'string', - description: 'When the message was sent', - }, - webhook: { - type: 'json', - description: 'Full webhook payload with all data', + description: 'Error message (for bounce/failed events)', }, - } + } as Record } diff --git a/apps/sim/triggers/lemlist/webhook.ts b/apps/sim/triggers/lemlist/webhook.ts index 289d8dead6..ef557b1c59 100644 --- a/apps/sim/triggers/lemlist/webhook.ts +++ b/apps/sim/triggers/lemlist/webhook.ts @@ -1,8 +1,8 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildAllOutputs, buildLemlistExtraFields, + buildLemlistOutputs, lemlistSetupInstructions, lemlistTriggerOptions, } from '@/triggers/lemlist/utils' @@ -27,7 +27,7 @@ export const lemlistWebhookTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_webhook'), }), - outputs: buildAllOutputs(), + outputs: buildLemlistOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/telegram/webhook.ts b/apps/sim/triggers/telegram/webhook.ts index aad6174d6f..a11aaf7190 100644 --- a/apps/sim/triggers/telegram/webhook.ts +++ b/apps/sim/triggers/telegram/webhook.ts @@ -110,6 +110,7 @@ export const telegramWebhookTrigger: TriggerConfig = { }, sender: { id: { type: 'number', description: 'Sender user ID' }, + username: { type: 'string', description: 'Sender username (if available)' }, firstName: { type: 'string', description: 'Sender first name' }, lastName: { type: 'string', description: 'Sender last name' }, languageCode: { type: 'string', description: 'Sender language code (if available)' }, diff --git a/apps/sim/triggers/typeform/webhook.ts b/apps/sim/triggers/typeform/webhook.ts index 90d5069fc8..b846a05396 100644 --- a/apps/sim/triggers/typeform/webhook.ts +++ b/apps/sim/triggers/typeform/webhook.ts @@ -136,6 +136,8 @@ export const typeformWebhookTrigger: TriggerConfig = { 'Array of respondent answers (only includes answered questions). Each answer contains type, value, and field reference.', }, definition: { + description: + 'Form definition (only included when "Include Form Definition" is enabled in trigger settings)', id: { type: 'string', description: 'Form ID', diff --git a/apps/sim/triggers/webflow/collection_item_changed.ts b/apps/sim/triggers/webflow/collection_item_changed.ts index e66590f5c6..aedeae63ad 100644 --- a/apps/sim/triggers/webflow/collection_item_changed.ts +++ b/apps/sim/triggers/webflow/collection_item_changed.ts @@ -96,10 +96,6 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = { type: 'string', description: 'The site ID where the event occurred', }, - workspaceId: { - type: 'string', - description: 'The workspace ID where the event occurred', - }, collectionId: { type: 'string', description: 'The collection ID where the item was changed', diff --git a/apps/sim/triggers/webflow/collection_item_created.ts b/apps/sim/triggers/webflow/collection_item_created.ts index e0fdc7e8a0..777b74b76f 100644 --- a/apps/sim/triggers/webflow/collection_item_created.ts +++ b/apps/sim/triggers/webflow/collection_item_created.ts @@ -109,10 +109,6 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = { type: 'string', description: 'The site ID where the event occurred', }, - workspaceId: { - type: 'string', - description: 'The workspace ID where the event occurred', - }, collectionId: { type: 'string', description: 'The collection ID where the item was created', diff --git a/apps/sim/triggers/webflow/collection_item_deleted.ts b/apps/sim/triggers/webflow/collection_item_deleted.ts index 12a10f3ed9..60fb2805dc 100644 --- a/apps/sim/triggers/webflow/collection_item_deleted.ts +++ b/apps/sim/triggers/webflow/collection_item_deleted.ts @@ -97,10 +97,6 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = { type: 'string', description: 'The site ID where the event occurred', }, - workspaceId: { - type: 'string', - description: 'The workspace ID where the event occurred', - }, collectionId: { type: 'string', description: 'The collection ID where the item was deleted', diff --git a/apps/sim/triggers/webflow/form_submission.ts b/apps/sim/triggers/webflow/form_submission.ts index 7c27599c65..1a6c7640ba 100644 --- a/apps/sim/triggers/webflow/form_submission.ts +++ b/apps/sim/triggers/webflow/form_submission.ts @@ -76,9 +76,9 @@ export const webflowFormSubmissionTrigger: TriggerConfig = { type: 'string', description: 'The site ID where the form was submitted', }, - workspaceId: { + formId: { type: 'string', - description: 'The workspace ID where the event occurred', + description: 'The form ID', }, name: { type: 'string', diff --git a/bun.lock b/bun.lock index 8b136165e7..c7cee56919 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", From 6e0055f84746a4939d4baacd107b4be6c9ac28fa Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 13 Jan 2026 18:37:06 -0800 Subject: [PATCH 06/36] feat(slack): added get message by timestamp and get thread tool (#2803) * feat(slack): added get message tool * added get thread --- apps/docs/components/icons.tsx | 30 ++- apps/docs/content/docs/en/tools/a2a.mdx | 5 - apps/docs/content/docs/en/tools/lemlist.mdx | 3 +- apps/docs/content/docs/en/tools/slack.mdx | 39 ++++ apps/sim/blocks/blocks/slack.ts | 120 +++++++++++ apps/sim/tools/registry.ts | 4 + apps/sim/tools/slack/get_message.ts | 213 +++++++++++++++++++ apps/sim/tools/slack/get_thread.ts | 224 ++++++++++++++++++++ apps/sim/tools/slack/index.ts | 4 + apps/sim/tools/slack/types.ts | 29 +++ 10 files changed, 653 insertions(+), 18 deletions(-) create mode 100644 apps/sim/tools/slack/get_message.ts create mode 100644 apps/sim/tools/slack/get_thread.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 7addb30eaa..0143e517a5 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1855,17 +1855,25 @@ export function LinearIcon(props: React.SVGProps) { export function LemlistIcon(props: SVGProps) { return ( - - - - + + + + + ) } diff --git a/apps/docs/content/docs/en/tools/a2a.mdx b/apps/docs/content/docs/en/tools/a2a.mdx index 558f1f907e..63393fe019 100644 --- a/apps/docs/content/docs/en/tools/a2a.mdx +++ b/apps/docs/content/docs/en/tools/a2a.mdx @@ -208,8 +208,3 @@ Delete the push notification webhook configuration for a task. | `success` | boolean | Whether deletion was successful | - -## Notes - -- Category: `tools` -- Type: `a2a` diff --git a/apps/docs/content/docs/en/tools/lemlist.mdx b/apps/docs/content/docs/en/tools/lemlist.mdx index c3b38bb720..25e1a4ca11 100644 --- a/apps/docs/content/docs/en/tools/lemlist.mdx +++ b/apps/docs/content/docs/en/tools/lemlist.mdx @@ -49,8 +49,7 @@ Retrieves lead information by email address or lead ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Lemlist API key | -| `email` | string | No | Lead email address \(use either email or id\) | -| `id` | string | No | Lead ID \(use either email or id\) | +| `leadIdentifier` | string | Yes | Lead email address or lead ID | #### Output diff --git a/apps/docs/content/docs/en/tools/slack.mdx b/apps/docs/content/docs/en/tools/slack.mdx index 8c806d4de8..1181f24bb7 100644 --- a/apps/docs/content/docs/en/tools/slack.mdx +++ b/apps/docs/content/docs/en/tools/slack.mdx @@ -124,6 +124,45 @@ Read the latest messages from Slack channels. Retrieve conversation history with | --------- | ---- | ----------- | | `messages` | array | Array of message objects from the channel | +### `slack_get_message` + +Retrieve a specific message by its timestamp. Useful for getting a thread parent message. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `channel` | string | Yes | Slack channel ID \(e.g., C1234567890\) | +| `timestamp` | string | Yes | Message timestamp to retrieve \(e.g., 1405894322.002768\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | object | The retrieved message object | + +### `slack_get_thread` + +Retrieve an entire thread including the parent message and all replies. Useful for getting full conversation context. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `channel` | string | Yes | Slack channel ID \(e.g., C1234567890\) | +| `threadTs` | string | Yes | Thread timestamp \(thread_ts\) to retrieve \(e.g., 1405894322.002768\) | +| `limit` | number | No | Maximum number of messages to return \(default: 100, max: 200\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `parentMessage` | object | The thread parent message | + ### `slack_list_channels` List all channels in a Slack workspace. Returns public and private channels the bot has access to. diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index c7c37d7576..7e432f27f0 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -26,6 +26,8 @@ export const SlackBlock: BlockConfig = { { label: 'Send Message', id: 'send' }, { label: 'Create Canvas', id: 'canvas' }, { label: 'Read Messages', id: 'read' }, + { label: 'Get Message', id: 'get_message' }, + { label: 'Get Thread', id: 'get_thread' }, { label: 'List Channels', id: 'list_channels' }, { label: 'List Channel Members', id: 'list_members' }, { label: 'List Users', id: 'list_users' }, @@ -316,6 +318,68 @@ export const SlackBlock: BlockConfig = { }, required: true, }, + // Get Message specific fields + { + id: 'getMessageTimestamp', + title: 'Message Timestamp', + type: 'short-input', + placeholder: 'Message timestamp (e.g., 1405894322.002768)', + condition: { + field: 'operation', + value: 'get_message', + }, + required: true, + wandConfig: { + enabled: true, + prompt: `Extract or generate a Slack message timestamp from the user's input. +Slack message timestamps are in the format: XXXXXXXXXX.XXXXXX (seconds.microseconds since Unix epoch). +Examples: +- "1405894322.002768" -> 1405894322.002768 (already a valid timestamp) +- "thread_ts from the trigger" -> The user wants to reference a variable, output the original text +- A URL like "https://slack.com/archives/C123/p1405894322002768" -> Extract 1405894322.002768 (remove 'p' prefix, add decimal after 10th digit) + +If the input looks like a reference to another block's output (contains < and >) or a variable, return it as-is. +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Paste a Slack message URL or timestamp...', + generationType: 'timestamp', + }, + }, + // Get Thread specific fields + { + id: 'getThreadTimestamp', + title: 'Thread Timestamp', + type: 'short-input', + placeholder: 'Thread timestamp (thread_ts, e.g., 1405894322.002768)', + condition: { + field: 'operation', + value: 'get_thread', + }, + required: true, + wandConfig: { + enabled: true, + prompt: `Extract or generate a Slack thread timestamp from the user's input. +Slack thread timestamps (thread_ts) are in the format: XXXXXXXXXX.XXXXXX (seconds.microseconds since Unix epoch). +Examples: +- "1405894322.002768" -> 1405894322.002768 (already a valid timestamp) +- "thread_ts from the trigger" -> The user wants to reference a variable, output the original text +- A URL like "https://slack.com/archives/C123/p1405894322002768" -> Extract 1405894322.002768 (remove 'p' prefix, add decimal after 10th digit) + +If the input looks like a reference to another block's output (contains < and >) or a variable, return it as-is. +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Paste a Slack thread URL or thread_ts...', + generationType: 'timestamp', + }, + }, + { + id: 'threadLimit', + title: 'Message Limit', + type: 'short-input', + placeholder: '100', + condition: { + field: 'operation', + value: 'get_thread', + }, + }, { id: 'oldest', title: 'Oldest Timestamp', @@ -430,6 +494,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'slack_message', 'slack_canvas', 'slack_message_reader', + 'slack_get_message', + 'slack_get_thread', 'slack_list_channels', 'slack_list_members', 'slack_list_users', @@ -448,6 +514,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, return 'slack_canvas' case 'read': return 'slack_message_reader' + case 'get_message': + return 'slack_get_message' + case 'get_thread': + return 'slack_get_thread' case 'list_channels': return 'slack_list_channels' case 'list_members': @@ -498,6 +568,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, includeDeleted, userLimit, userId, + getMessageTimestamp, + getThreadTimestamp, + threadLimit, ...rest } = params @@ -574,6 +647,27 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, break } + case 'get_message': + if (!getMessageTimestamp) { + throw new Error('Message timestamp is required for get message operation') + } + baseParams.timestamp = getMessageTimestamp + break + + case 'get_thread': { + if (!getThreadTimestamp) { + throw new Error('Thread timestamp is required for get thread operation') + } + baseParams.threadTs = getThreadTimestamp + if (threadLimit) { + const parsedLimit = Number.parseInt(threadLimit, 10) + if (!Number.isNaN(parsedLimit) && parsedLimit > 0) { + baseParams.limit = Math.min(parsedLimit, 200) + } + } + break + } + case 'list_channels': { baseParams.includePrivate = includePrivate !== 'false' baseParams.excludeArchived = true @@ -679,6 +773,14 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, userLimit: { type: 'string', description: 'Maximum number of users to return' }, // Get User inputs userId: { type: 'string', description: 'User ID to look up' }, + // Get Message inputs + getMessageTimestamp: { type: 'string', description: 'Message timestamp to retrieve' }, + // Get Thread inputs + getThreadTimestamp: { type: 'string', description: 'Thread timestamp to retrieve' }, + threadLimit: { + type: 'string', + description: 'Maximum number of messages to return from thread', + }, }, outputs: { // slack_message outputs (send operation) @@ -706,6 +808,24 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'Array of message objects with comprehensive properties: text, user, timestamp, reactions, threads, files, attachments, blocks, stars, pins, and edit history', }, + // slack_get_thread outputs (get_thread operation) + parentMessage: { + type: 'json', + description: 'The thread parent message with all properties', + }, + replies: { + type: 'json', + description: 'Array of reply messages in the thread (excluding the parent)', + }, + replyCount: { + type: 'number', + description: 'Number of replies returned in this response', + }, + hasMore: { + type: 'boolean', + description: 'Whether there are more messages in the thread', + }, + // slack_list_channels outputs (list_channels operation) channels: { type: 'json', diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 6570eea636..c5137da6e5 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1180,6 +1180,8 @@ import { slackCanvasTool, slackDeleteMessageTool, slackDownloadTool, + slackGetMessageTool, + slackGetThreadTool, slackGetUserTool, slackListChannelsTool, slackListMembersTool, @@ -1731,6 +1733,8 @@ export const tools: Record = { slack_list_members: slackListMembersTool, slack_list_users: slackListUsersTool, slack_get_user: slackGetUserTool, + slack_get_message: slackGetMessageTool, + slack_get_thread: slackGetThreadTool, slack_canvas: slackCanvasTool, slack_download: slackDownloadTool, slack_update_message: slackUpdateMessageTool, diff --git a/apps/sim/tools/slack/get_message.ts b/apps/sim/tools/slack/get_message.ts new file mode 100644 index 0000000000..f651f00a1a --- /dev/null +++ b/apps/sim/tools/slack/get_message.ts @@ -0,0 +1,213 @@ +import type { SlackGetMessageParams, SlackGetMessageResponse } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackGetMessageTool: ToolConfig = { + id: 'slack_get_message', + name: 'Slack Get Message', + description: + 'Retrieve a specific message by its timestamp. Useful for getting a thread parent message.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + channel: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Slack channel ID (e.g., C1234567890)', + }, + timestamp: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Message timestamp to retrieve (e.g., 1405894322.002768)', + }, + }, + + request: { + url: (params: SlackGetMessageParams) => { + const url = new URL('https://slack.com/api/conversations.history') + url.searchParams.append('channel', params.channel?.trim() ?? '') + url.searchParams.append('oldest', params.timestamp?.trim() ?? '') + url.searchParams.append('limit', '1') + url.searchParams.append('inclusive', 'true') + return url.toString() + }, + method: 'GET', + headers: (params: SlackGetMessageParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:history, groups:history).' + ) + } + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + if (data.error === 'channel_not_found') { + throw new Error('Channel not found. Please check the channel ID.') + } + throw new Error(data.error || 'Failed to get message from Slack') + } + + const messages = data.messages || [] + if (messages.length === 0) { + throw new Error('Message not found') + } + + const msg = messages[0] + const message = { + type: msg.type ?? 'message', + ts: msg.ts, + text: msg.text ?? '', + user: msg.user ?? null, + bot_id: msg.bot_id ?? null, + username: msg.username ?? null, + channel: msg.channel ?? null, + team: msg.team ?? null, + thread_ts: msg.thread_ts ?? null, + parent_user_id: msg.parent_user_id ?? null, + reply_count: msg.reply_count ?? null, + reply_users_count: msg.reply_users_count ?? null, + latest_reply: msg.latest_reply ?? null, + subscribed: msg.subscribed ?? null, + last_read: msg.last_read ?? null, + unread_count: msg.unread_count ?? null, + subtype: msg.subtype ?? null, + reactions: msg.reactions ?? [], + is_starred: msg.is_starred ?? false, + pinned_to: msg.pinned_to ?? [], + files: (msg.files ?? []).map((f: any) => ({ + id: f.id, + name: f.name, + mimetype: f.mimetype, + size: f.size, + url_private: f.url_private ?? null, + permalink: f.permalink ?? null, + mode: f.mode ?? null, + })), + attachments: msg.attachments ?? [], + blocks: msg.blocks ?? [], + edited: msg.edited ?? null, + permalink: msg.permalink ?? null, + } + + return { + success: true, + output: { + message, + }, + } + }, + + outputs: { + message: { + type: 'object', + description: 'The retrieved message object', + properties: { + type: { type: 'string', description: 'Message type' }, + ts: { type: 'string', description: 'Message timestamp' }, + text: { type: 'string', description: 'Message text content' }, + user: { type: 'string', description: 'User ID who sent the message' }, + bot_id: { type: 'string', description: 'Bot ID if sent by a bot', optional: true }, + username: { type: 'string', description: 'Display username', optional: true }, + channel: { type: 'string', description: 'Channel ID', optional: true }, + team: { type: 'string', description: 'Team ID', optional: true }, + thread_ts: { type: 'string', description: 'Thread parent timestamp', optional: true }, + parent_user_id: { type: 'string', description: 'User ID of thread parent', optional: true }, + reply_count: { type: 'number', description: 'Number of thread replies', optional: true }, + reply_users_count: { + type: 'number', + description: 'Number of users who replied', + optional: true, + }, + latest_reply: { type: 'string', description: 'Timestamp of latest reply', optional: true }, + subtype: { type: 'string', description: 'Message subtype', optional: true }, + reactions: { + type: 'array', + description: 'Array of reactions on this message', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Emoji name' }, + count: { type: 'number', description: 'Number of reactions' }, + users: { + type: 'array', + description: 'User IDs who reacted', + items: { type: 'string' }, + }, + }, + }, + }, + is_starred: { type: 'boolean', description: 'Whether message is starred', optional: true }, + pinned_to: { + type: 'array', + description: 'Channel IDs where message is pinned', + items: { type: 'string' }, + optional: true, + }, + files: { + type: 'array', + description: 'Files attached to message', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'File ID' }, + name: { type: 'string', description: 'File name' }, + mimetype: { type: 'string', description: 'MIME type' }, + size: { type: 'number', description: 'File size in bytes' }, + url_private: { type: 'string', description: 'Private download URL' }, + permalink: { type: 'string', description: 'Permanent link to file' }, + }, + }, + }, + attachments: { + type: 'array', + description: 'Legacy attachments', + items: { type: 'object' }, + }, + blocks: { type: 'array', description: 'Block Kit blocks', items: { type: 'object' } }, + edited: { + type: 'object', + description: 'Edit information if message was edited', + properties: { + user: { type: 'string', description: 'User ID who edited' }, + ts: { type: 'string', description: 'Edit timestamp' }, + }, + optional: true, + }, + permalink: { type: 'string', description: 'Permanent link to message', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/slack/get_thread.ts b/apps/sim/tools/slack/get_thread.ts new file mode 100644 index 0000000000..ab8122cb59 --- /dev/null +++ b/apps/sim/tools/slack/get_thread.ts @@ -0,0 +1,224 @@ +import type { SlackGetThreadParams, SlackGetThreadResponse } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackGetThreadTool: ToolConfig = { + id: 'slack_get_thread', + name: 'Slack Get Thread', + description: + 'Retrieve an entire thread including the parent message and all replies. Useful for getting full conversation context.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + channel: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Slack channel ID (e.g., C1234567890)', + }, + threadTs: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Thread timestamp (thread_ts) to retrieve (e.g., 1405894322.002768)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of messages to return (default: 100, max: 200)', + }, + }, + + request: { + url: (params: SlackGetThreadParams) => { + const url = new URL('https://slack.com/api/conversations.replies') + url.searchParams.append('channel', params.channel?.trim() ?? '') + url.searchParams.append('ts', params.threadTs?.trim() ?? '') + url.searchParams.append('inclusive', 'true') + const limit = params.limit ? Math.min(Number(params.limit), 200) : 100 + url.searchParams.append('limit', String(limit)) + return url.toString() + }, + method: 'GET', + headers: (params: SlackGetThreadParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:history, groups:history).' + ) + } + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + if (data.error === 'channel_not_found') { + throw new Error('Channel not found. Please check the channel ID.') + } + if (data.error === 'thread_not_found') { + throw new Error('Thread not found. Please check the thread timestamp.') + } + throw new Error(data.error || 'Failed to get thread from Slack') + } + + const rawMessages = data.messages || [] + if (rawMessages.length === 0) { + throw new Error('Thread not found') + } + + const messages = rawMessages.map((msg: any) => ({ + type: msg.type ?? 'message', + ts: msg.ts, + text: msg.text ?? '', + user: msg.user ?? null, + bot_id: msg.bot_id ?? null, + username: msg.username ?? null, + channel: msg.channel ?? null, + team: msg.team ?? null, + thread_ts: msg.thread_ts ?? null, + parent_user_id: msg.parent_user_id ?? null, + reply_count: msg.reply_count ?? null, + reply_users_count: msg.reply_users_count ?? null, + latest_reply: msg.latest_reply ?? null, + subscribed: msg.subscribed ?? null, + last_read: msg.last_read ?? null, + unread_count: msg.unread_count ?? null, + subtype: msg.subtype ?? null, + reactions: msg.reactions ?? [], + is_starred: msg.is_starred ?? false, + pinned_to: msg.pinned_to ?? [], + files: (msg.files ?? []).map((f: any) => ({ + id: f.id, + name: f.name, + mimetype: f.mimetype, + size: f.size, + url_private: f.url_private ?? null, + permalink: f.permalink ?? null, + mode: f.mode ?? null, + })), + attachments: msg.attachments ?? [], + blocks: msg.blocks ?? [], + edited: msg.edited ?? null, + permalink: msg.permalink ?? null, + })) + + // First message is always the parent + const parentMessage = messages[0] + // Remaining messages are replies + const replies = messages.slice(1) + + return { + success: true, + output: { + parentMessage, + replies, + messages, + replyCount: replies.length, + hasMore: data.has_more ?? false, + }, + } + }, + + outputs: { + parentMessage: { + type: 'object', + description: 'The thread parent message', + properties: { + type: { type: 'string', description: 'Message type' }, + ts: { type: 'string', description: 'Message timestamp' }, + text: { type: 'string', description: 'Message text content' }, + user: { type: 'string', description: 'User ID who sent the message' }, + bot_id: { type: 'string', description: 'Bot ID if sent by a bot', optional: true }, + username: { type: 'string', description: 'Display username', optional: true }, + reply_count: { type: 'number', description: 'Total number of thread replies' }, + reply_users_count: { type: 'number', description: 'Number of users who replied' }, + latest_reply: { type: 'string', description: 'Timestamp of latest reply' }, + reactions: { + type: 'array', + description: 'Array of reactions on the parent message', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Emoji name' }, + count: { type: 'number', description: 'Number of reactions' }, + users: { + type: 'array', + description: 'User IDs who reacted', + items: { type: 'string' }, + }, + }, + }, + }, + files: { + type: 'array', + description: 'Files attached to the parent message', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'File ID' }, + name: { type: 'string', description: 'File name' }, + mimetype: { type: 'string', description: 'MIME type' }, + size: { type: 'number', description: 'File size in bytes' }, + }, + }, + }, + }, + }, + replies: { + type: 'array', + description: 'Array of reply messages in the thread (excluding the parent)', + items: { + type: 'object', + properties: { + ts: { type: 'string', description: 'Message timestamp' }, + text: { type: 'string', description: 'Message text content' }, + user: { type: 'string', description: 'User ID who sent the reply' }, + reactions: { type: 'array', description: 'Reactions on the reply' }, + files: { type: 'array', description: 'Files attached to the reply' }, + }, + }, + }, + messages: { + type: 'array', + description: 'All messages in the thread (parent + replies) in chronological order', + items: { type: 'object' }, + }, + replyCount: { + type: 'number', + description: 'Number of replies returned in this response', + }, + hasMore: { + type: 'boolean', + description: 'Whether there are more messages in the thread (pagination needed)', + }, + }, +} diff --git a/apps/sim/tools/slack/index.ts b/apps/sim/tools/slack/index.ts index ea99459cd3..2bc0f249ef 100644 --- a/apps/sim/tools/slack/index.ts +++ b/apps/sim/tools/slack/index.ts @@ -2,6 +2,8 @@ import { slackAddReactionTool } from '@/tools/slack/add_reaction' import { slackCanvasTool } from '@/tools/slack/canvas' import { slackDeleteMessageTool } from '@/tools/slack/delete_message' import { slackDownloadTool } from '@/tools/slack/download' +import { slackGetMessageTool } from '@/tools/slack/get_message' +import { slackGetThreadTool } from '@/tools/slack/get_thread' import { slackGetUserTool } from '@/tools/slack/get_user' import { slackListChannelsTool } from '@/tools/slack/list_channels' import { slackListMembersTool } from '@/tools/slack/list_members' @@ -22,4 +24,6 @@ export { slackListMembersTool, slackListUsersTool, slackGetUserTool, + slackGetMessageTool, + slackGetThreadTool, } diff --git a/apps/sim/tools/slack/types.ts b/apps/sim/tools/slack/types.ts index 3cbf79b81d..4271409aa0 100644 --- a/apps/sim/tools/slack/types.ts +++ b/apps/sim/tools/slack/types.ts @@ -71,6 +71,17 @@ export interface SlackGetUserParams extends SlackBaseParams { userId: string } +export interface SlackGetMessageParams extends SlackBaseParams { + channel: string + timestamp: string +} + +export interface SlackGetThreadParams extends SlackBaseParams { + channel: string + threadTs: string + limit?: number +} + export interface SlackMessageResponse extends ToolResponse { output: { // Legacy properties for backward compatibility @@ -305,6 +316,22 @@ export interface SlackGetUserResponse extends ToolResponse { } } +export interface SlackGetMessageResponse extends ToolResponse { + output: { + message: SlackMessage + } +} + +export interface SlackGetThreadResponse extends ToolResponse { + output: { + parentMessage: SlackMessage + replies: SlackMessage[] + messages: SlackMessage[] + replyCount: number + hasMore: boolean + } +} + export type SlackResponse = | SlackCanvasResponse | SlackMessageReaderResponse @@ -317,3 +344,5 @@ export type SlackResponse = | SlackListMembersResponse | SlackListUsersResponse | SlackGetUserResponse + | SlackGetMessageResponse + | SlackGetThreadResponse From e3fa40af11c80943a26cb0b0a3bb882dd09fdc70 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 13 Jan 2026 18:45:41 -0800 Subject: [PATCH 07/36] fix(sockets): redrawing edges should not lead to socket ops (#2804) * fix(sockets): redrawing edges should not lead to socket ops * consolidate --- apps/sim/hooks/use-collaborative-workflow.ts | 16 ++++++---- apps/sim/stores/workflows/utils.ts | 14 +++++++++ .../stores/workflows/workflow/store.test.ts | 13 +------- apps/sim/stores/workflows/workflow/store.ts | 30 +++++-------------- 4 files changed, 34 insertions(+), 39 deletions(-) diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index c2fa032d86..bdfbbae43b 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -22,7 +22,7 @@ import { useUndoRedoStore } from '@/stores/undo-redo' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' -import { mergeSubblockState, normalizeName } from '@/stores/workflows/utils' +import { filterNewEdges, mergeSubblockState, normalizeName } from '@/stores/workflows/utils' import { useWorkflowStore } from '@/stores/workflows/workflow/store' import type { BlockState, Loop, Parallel, Position } from '@/stores/workflows/workflow/types' @@ -242,7 +242,10 @@ export function useCollaborativeWorkflow() { case EDGES_OPERATIONS.BATCH_ADD_EDGES: { const { edges } = payload if (Array.isArray(edges) && edges.length > 0) { - workflowStore.batchAddEdges(edges) + const newEdges = filterNewEdges(edges, workflowStore.edges) + if (newEdges.length > 0) { + workflowStore.batchAddEdges(newEdges) + } } break } @@ -976,6 +979,9 @@ export function useCollaborativeWorkflow() { if (edges.length === 0) return false + const newEdges = filterNewEdges(edges, workflowStore.edges) + if (newEdges.length === 0) return false + const operationId = crypto.randomUUID() addToQueue({ @@ -983,16 +989,16 @@ export function useCollaborativeWorkflow() { operation: { operation: EDGES_OPERATIONS.BATCH_ADD_EDGES, target: OPERATION_TARGETS.EDGES, - payload: { edges }, + payload: { edges: newEdges }, }, workflowId: activeWorkflowId || '', userId: session?.user?.id || 'unknown', }) - workflowStore.batchAddEdges(edges) + workflowStore.batchAddEdges(newEdges) if (!options?.skipUndoRedo) { - edges.forEach((edge) => undoRedo.recordAddEdge(edge.id)) + newEdges.forEach((edge) => undoRedo.recordAddEdge(edge.id)) } return true diff --git a/apps/sim/stores/workflows/utils.ts b/apps/sim/stores/workflows/utils.ts index 2caadeea1a..c0b3f3a9a4 100644 --- a/apps/sim/stores/workflows/utils.ts +++ b/apps/sim/stores/workflows/utils.ts @@ -1,5 +1,19 @@ import type { Edge } from 'reactflow' import { v4 as uuidv4 } from 'uuid' + +export function filterNewEdges(edgesToAdd: Edge[], currentEdges: Edge[]): Edge[] { + return edgesToAdd.filter((edge) => { + if (edge.source === edge.target) return false + return !currentEdges.some( + (e) => + e.source === edge.source && + e.sourceHandle === edge.sourceHandle && + e.target === edge.target && + e.targetHandle === edge.targetHandle + ) + }) +} + import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { getBlock } from '@/blocks' import { normalizeName } from '@/executor/constants' diff --git a/apps/sim/stores/workflows/workflow/store.test.ts b/apps/sim/stores/workflows/workflow/store.test.ts index f1fef5bef7..1ed122c238 100644 --- a/apps/sim/stores/workflows/workflow/store.test.ts +++ b/apps/sim/stores/workflows/workflow/store.test.ts @@ -297,7 +297,7 @@ describe('workflow store', () => { expectEdgeConnects(edges, 'block-1', 'block-2') }) - it('should not add duplicate edges', () => { + it('should not add duplicate connections', () => { const { addBlock, batchAddEdges } = useWorkflowStore.getState() addBlock('block-1', 'starter', 'Start', { x: 0, y: 0 }) @@ -309,17 +309,6 @@ describe('workflow store', () => { const state = useWorkflowStore.getState() expectEdgeCount(state, 1) }) - - it('should prevent self-referencing edges', () => { - const { addBlock, batchAddEdges } = useWorkflowStore.getState() - - addBlock('block-1', 'function', 'Self', { x: 0, y: 0 }) - - batchAddEdges([{ id: 'e1', source: 'block-1', target: 'block-1' }]) - - const state = useWorkflowStore.getState() - expectEdgeCount(state, 0) - }) }) describe('batchRemoveEdges', () => { diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 9f46b0de6f..789e83695e 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -9,7 +9,12 @@ import { getBlock } from '@/blocks' import type { SubBlockConfig } from '@/blocks/types' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' -import { getUniqueBlockName, mergeSubblockState, normalizeName } from '@/stores/workflows/utils' +import { + filterNewEdges, + getUniqueBlockName, + mergeSubblockState, + normalizeName, +} from '@/stores/workflows/utils' import type { Position, SubBlockState, @@ -496,29 +501,11 @@ export const useWorkflowStore = create()( batchAddEdges: (edges: Edge[]) => { const currentEdges = get().edges + const filtered = filterNewEdges(edges, currentEdges) const newEdges = [...currentEdges] - const existingEdgeIds = new Set(currentEdges.map((e) => e.id)) - - for (const edge of edges) { - // Skip if edge ID already exists - if (existingEdgeIds.has(edge.id)) continue - - // Skip self-referencing edges - if (edge.source === edge.target) continue - - // Skip if identical connection already exists (same ports) - const connectionExists = newEdges.some( - (e) => - e.source === edge.source && - e.sourceHandle === edge.sourceHandle && - e.target === edge.target && - e.targetHandle === edge.targetHandle - ) - if (connectionExists) continue - // Skip if would create a cycle + for (const edge of filtered) { if (wouldCreateCycle([...newEdges], edge.source, edge.target)) continue - newEdges.push({ id: edge.id || crypto.randomUUID(), source: edge.source, @@ -528,7 +515,6 @@ export const useWorkflowStore = create()( type: edge.type || 'default', data: edge.data || {}, }) - existingEdgeIds.add(edge.id) } const blocks = get().blocks From d6e4c91e81caaa7c033078d3618b008660cba817 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 13 Jan 2026 20:10:44 -0800 Subject: [PATCH 08/36] fix(invitations): preserve tokens after error (#2806) --- .../invitations/[invitationId]/route.test.ts | 206 ++++++++++++++++-- .../invitations/[invitationId]/route.ts | 18 +- apps/sim/app/invite/[id]/invite.tsx | 43 ++-- 3 files changed, 219 insertions(+), 48 deletions(-) diff --git a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts index 12833c9695..389de676cd 100644 --- a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts +++ b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts @@ -2,13 +2,6 @@ import { createSession, createWorkspaceRecord, loggerMock } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' -/** - * Tests for workspace invitation by ID API route - * Tests GET (details + token acceptance), DELETE (cancellation) - * - * @vitest-environment node - */ - const mockGetSession = vi.fn() const mockHasWorkspaceAdminAccess = vi.fn() @@ -227,7 +220,7 @@ describe('Workspace Invitation [invitationId] API Route', () => { expect(response.headers.get('location')).toBe('https://test.sim.ai/workspace/workspace-456/w') }) - it('should redirect to error page when invitation expired', async () => { + it('should redirect to error page with token preserved when invitation expired', async () => { const session = createSession({ userId: mockUser.id, email: 'invited@example.com', @@ -250,12 +243,13 @@ describe('Workspace Invitation [invitationId] API Route', () => { const response = await GET(request, { params }) expect(response.status).toBe(307) - expect(response.headers.get('location')).toBe( - 'https://test.sim.ai/invite/invitation-789?error=expired' + const location = response.headers.get('location') + expect(location).toBe( + 'https://test.sim.ai/invite/invitation-789?error=expired&token=token-abc123' ) }) - it('should redirect to error page when email mismatch', async () => { + it('should redirect to error page with token preserved when email mismatch', async () => { const session = createSession({ userId: mockUser.id, email: 'wrong@example.com', @@ -277,12 +271,13 @@ describe('Workspace Invitation [invitationId] API Route', () => { const response = await GET(request, { params }) expect(response.status).toBe(307) - expect(response.headers.get('location')).toBe( - 'https://test.sim.ai/invite/invitation-789?error=email-mismatch' + const location = response.headers.get('location') + expect(location).toBe( + 'https://test.sim.ai/invite/invitation-789?error=email-mismatch&token=token-abc123' ) }) - it('should return 404 when invitation not found', async () => { + it('should return 404 when invitation not found (without token)', async () => { const session = createSession({ userId: mockUser.id, email: mockUser.email }) mockGetSession.mockResolvedValue(session) dbSelectResults = [[]] @@ -296,6 +291,189 @@ describe('Workspace Invitation [invitationId] API Route', () => { expect(response.status).toBe(404) expect(data).toEqual({ error: 'Invitation not found or has expired' }) }) + + it('should redirect to error page with token preserved when invitation not found (with token)', async () => { + const session = createSession({ userId: mockUser.id, email: mockUser.email }) + mockGetSession.mockResolvedValue(session) + dbSelectResults = [[]] + + const request = new NextRequest( + 'http://localhost/api/workspaces/invitations/non-existent?token=some-invalid-token' + ) + const params = Promise.resolve({ invitationId: 'non-existent' }) + + const response = await GET(request, { params }) + + expect(response.status).toBe(307) + const location = response.headers.get('location') + expect(location).toBe( + 'https://test.sim.ai/invite/non-existent?error=invalid-token&token=some-invalid-token' + ) + }) + + it('should redirect to error page with token preserved when invitation already processed', async () => { + const session = createSession({ + userId: mockUser.id, + email: 'invited@example.com', + name: mockUser.name, + }) + mockGetSession.mockResolvedValue(session) + + const acceptedInvitation = { + ...mockInvitation, + status: 'accepted', + } + + dbSelectResults = [[acceptedInvitation], [mockWorkspace]] + + const request = new NextRequest( + 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123' + ) + const params = Promise.resolve({ invitationId: 'token-abc123' }) + + const response = await GET(request, { params }) + + expect(response.status).toBe(307) + const location = response.headers.get('location') + expect(location).toBe( + 'https://test.sim.ai/invite/invitation-789?error=already-processed&token=token-abc123' + ) + }) + + it('should redirect to error page with token preserved when workspace not found', async () => { + const session = createSession({ + userId: mockUser.id, + email: 'invited@example.com', + name: mockUser.name, + }) + mockGetSession.mockResolvedValue(session) + + dbSelectResults = [[mockInvitation], []] + + const request = new NextRequest( + 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123' + ) + const params = Promise.resolve({ invitationId: 'token-abc123' }) + + const response = await GET(request, { params }) + + expect(response.status).toBe(307) + const location = response.headers.get('location') + expect(location).toBe( + 'https://test.sim.ai/invite/invitation-789?error=workspace-not-found&token=token-abc123' + ) + }) + + it('should redirect to error page with token preserved when user not found', async () => { + const session = createSession({ + userId: mockUser.id, + email: 'invited@example.com', + name: mockUser.name, + }) + mockGetSession.mockResolvedValue(session) + + dbSelectResults = [[mockInvitation], [mockWorkspace], []] + + const request = new NextRequest( + 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123' + ) + const params = Promise.resolve({ invitationId: 'token-abc123' }) + + const response = await GET(request, { params }) + + expect(response.status).toBe(307) + const location = response.headers.get('location') + expect(location).toBe( + 'https://test.sim.ai/invite/invitation-789?error=user-not-found&token=token-abc123' + ) + }) + + it('should URL encode special characters in token when preserving in error redirects', async () => { + const session = createSession({ + userId: mockUser.id, + email: 'wrong@example.com', + name: mockUser.name, + }) + mockGetSession.mockResolvedValue(session) + + dbSelectResults = [ + [mockInvitation], + [mockWorkspace], + [{ ...mockUser, email: 'wrong@example.com' }], + ] + + const specialToken = 'token+with/special=chars&more' + const request = new NextRequest( + `http://localhost/api/workspaces/invitations/token-abc123?token=${encodeURIComponent(specialToken)}` + ) + const params = Promise.resolve({ invitationId: 'token-abc123' }) + + const response = await GET(request, { params }) + + expect(response.status).toBe(307) + const location = response.headers.get('location') + expect(location).toContain('error=email-mismatch') + expect(location).toContain(`token=${encodeURIComponent(specialToken)}`) + }) + }) + + describe('Token Preservation - Full Flow Scenario', () => { + it('should preserve token through email mismatch so user can retry with correct account', async () => { + const wrongSession = createSession({ + userId: 'wrong-user', + email: 'wrong@example.com', + name: 'Wrong User', + }) + mockGetSession.mockResolvedValue(wrongSession) + + dbSelectResults = [ + [mockInvitation], + [mockWorkspace], + [{ id: 'wrong-user', email: 'wrong@example.com' }], + ] + + const request1 = new NextRequest( + 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123' + ) + const params1 = Promise.resolve({ invitationId: 'token-abc123' }) + + const response1 = await GET(request1, { params: params1 }) + + expect(response1.status).toBe(307) + const location1 = response1.headers.get('location') + expect(location1).toBe( + 'https://test.sim.ai/invite/invitation-789?error=email-mismatch&token=token-abc123' + ) + + vi.clearAllMocks() + dbSelectCallIndex = 0 + + const correctSession = createSession({ + userId: mockUser.id, + email: 'invited@example.com', + name: mockUser.name, + }) + mockGetSession.mockResolvedValue(correctSession) + + dbSelectResults = [ + [mockInvitation], + [mockWorkspace], + [{ ...mockUser, email: 'invited@example.com' }], + [], + ] + + const request2 = new NextRequest( + 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123' + ) + const params2 = Promise.resolve({ invitationId: 'token-abc123' }) + + const response2 = await GET(request2, { params: params2 }) + + expect(response2.status).toBe(307) + expect(response2.headers.get('location')).toBe( + 'https://test.sim.ai/workspace/workspace-456/w' + ) + }) }) describe('DELETE /api/workspaces/invitations/[invitationId]', () => { diff --git a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts index 4d4ac7928f..c7574a61e2 100644 --- a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts +++ b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts @@ -31,7 +31,6 @@ export async function GET( const isAcceptFlow = !!token // If token is provided, this is an acceptance flow if (!session?.user?.id) { - // For token-based acceptance flows, redirect to login if (isAcceptFlow) { return NextResponse.redirect(new URL(`/invite/${invitationId}?token=${token}`, getBaseUrl())) } @@ -51,8 +50,9 @@ export async function GET( if (!invitation) { if (isAcceptFlow) { + const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '' return NextResponse.redirect( - new URL(`/invite/${invitationId}?error=invalid-token`, getBaseUrl()) + new URL(`/invite/${invitationId}?error=invalid-token${tokenParam}`, getBaseUrl()) ) } return NextResponse.json({ error: 'Invitation not found or has expired' }, { status: 404 }) @@ -60,8 +60,9 @@ export async function GET( if (new Date() > new Date(invitation.expiresAt)) { if (isAcceptFlow) { + const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '' return NextResponse.redirect( - new URL(`/invite/${invitation.id}?error=expired`, getBaseUrl()) + new URL(`/invite/${invitation.id}?error=expired${tokenParam}`, getBaseUrl()) ) } return NextResponse.json({ error: 'Invitation has expired' }, { status: 400 }) @@ -75,17 +76,20 @@ export async function GET( if (!workspaceDetails) { if (isAcceptFlow) { + const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '' return NextResponse.redirect( - new URL(`/invite/${invitation.id}?error=workspace-not-found`, getBaseUrl()) + new URL(`/invite/${invitation.id}?error=workspace-not-found${tokenParam}`, getBaseUrl()) ) } return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) } if (isAcceptFlow) { + const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '' + if (invitation.status !== ('pending' as WorkspaceInvitationStatus)) { return NextResponse.redirect( - new URL(`/invite/${invitation.id}?error=already-processed`, getBaseUrl()) + new URL(`/invite/${invitation.id}?error=already-processed${tokenParam}`, getBaseUrl()) ) } @@ -100,7 +104,7 @@ export async function GET( if (!userData) { return NextResponse.redirect( - new URL(`/invite/${invitation.id}?error=user-not-found`, getBaseUrl()) + new URL(`/invite/${invitation.id}?error=user-not-found${tokenParam}`, getBaseUrl()) ) } @@ -108,7 +112,7 @@ export async function GET( if (!isValidMatch) { return NextResponse.redirect( - new URL(`/invite/${invitation.id}?error=email-mismatch`, getBaseUrl()) + new URL(`/invite/${invitation.id}?error=email-mismatch${tokenParam}`, getBaseUrl()) ) } diff --git a/apps/sim/app/invite/[id]/invite.tsx b/apps/sim/app/invite/[id]/invite.tsx index 41a6c20588..caa2659d47 100644 --- a/apps/sim/app/invite/[id]/invite.tsx +++ b/apps/sim/app/invite/[id]/invite.tsx @@ -178,22 +178,24 @@ export default function Invite() { useEffect(() => { const errorReason = searchParams.get('error') - - if (errorReason) { - setError(getInviteError(errorReason)) - setIsLoading(false) - return - } - const isNew = searchParams.get('new') === 'true' setIsNewUser(isNew) const tokenFromQuery = searchParams.get('token') - const effectiveToken = tokenFromQuery || inviteId + if (tokenFromQuery) { + setToken(tokenFromQuery) + sessionStorage.setItem('inviteToken', tokenFromQuery) + } else { + const storedToken = sessionStorage.getItem('inviteToken') + if (storedToken && storedToken !== inviteId) { + setToken(storedToken) + } + } - if (effectiveToken) { - setToken(effectiveToken) - sessionStorage.setItem('inviteToken', effectiveToken) + if (errorReason) { + setError(getInviteError(errorReason)) + setIsLoading(false) + return } }, [searchParams, inviteId]) @@ -203,7 +205,6 @@ export default function Invite() { async function fetchInvitationDetails() { setIsLoading(true) try { - // Fetch invitation details using the invitation ID from the URL path const workspaceInviteResponse = await fetch(`/api/workspaces/invitations/${inviteId}`, { method: 'GET', }) @@ -220,7 +221,6 @@ export default function Invite() { return } - // Handle workspace invitation errors with specific status codes if (!workspaceInviteResponse.ok && workspaceInviteResponse.status !== 404) { const errorCode = parseApiError(null, workspaceInviteResponse.status) const errorData = await workspaceInviteResponse.json().catch(() => ({})) @@ -229,7 +229,6 @@ export default function Invite() { error: errorData, }) - // Refine error code based on response body if available if (errorData.error) { const refinedCode = parseApiError(errorData.error, workspaceInviteResponse.status) setError(getInviteError(refinedCode)) @@ -254,13 +253,11 @@ export default function Invite() { if (data) { setInvitationType('organization') - // Check if user is already in an organization BEFORE showing the invitation const activeOrgResponse = await client.organization .getFullOrganization() .catch(() => ({ data: null })) if (activeOrgResponse?.data) { - // User is already in an organization setCurrentOrgName(activeOrgResponse.data.name) setError(getInviteError('already-in-organization')) setIsLoading(false) @@ -289,7 +286,6 @@ export default function Invite() { throw { code: 'invalid-invitation' } } } catch (orgErr: any) { - // If this is our structured error, use it directly if (orgErr.code) { throw orgErr } @@ -316,7 +312,6 @@ export default function Invite() { window.location.href = `/api/workspaces/invitations/${encodeURIComponent(inviteId)}?token=${encodeURIComponent(token || '')}` } else { try { - // Get the organizationId from invitation details const orgId = invitationDetails?.data?.organizationId if (!orgId) { @@ -325,7 +320,6 @@ export default function Invite() { return } - // Use our custom API endpoint that handles Pro usage snapshot const response = await fetch(`/api/organizations/${orgId}/invitations/${inviteId}`, { method: 'PUT', headers: { @@ -347,7 +341,6 @@ export default function Invite() { return } - // Set the organization as active await client.organization.setActive({ organizationId: orgId, }) @@ -360,7 +353,6 @@ export default function Invite() { } catch (err: any) { logger.error('Error accepting invitation:', err) - // Reset accepted state on error setAccepted(false) const errorCode = parseApiError(err) @@ -371,7 +363,9 @@ export default function Invite() { } const getCallbackUrl = () => { - return `/invite/${inviteId}${token && token !== inviteId ? `?token=${token}` : ''}` + const effectiveToken = + token || sessionStorage.getItem('inviteToken') || searchParams.get('token') + return `/invite/${inviteId}${effectiveToken && effectiveToken !== inviteId ? `?token=${effectiveToken}` : ''}` } if (!session?.user && !isPending) { @@ -435,7 +429,6 @@ export default function Invite() { if (error) { const callbackUrl = encodeURIComponent(getCallbackUrl()) - // Special handling for already in organization if (error.code === 'already-in-organization') { return ( @@ -463,7 +456,6 @@ export default function Invite() { ) } - // Handle email mismatch - user needs to sign in with a different account if (error.code === 'email-mismatch') { return ( @@ -490,7 +482,6 @@ export default function Invite() { ) } - // Handle auth-related errors - prompt user to sign in if (error.requiresAuth) { return ( @@ -518,7 +509,6 @@ export default function Invite() { ) } - // Handle retryable errors const actions: Array<{ label: string onClick: () => void @@ -550,7 +540,6 @@ export default function Invite() { ) } - // Show success only if accepted AND no error if (accepted && !error) { return ( From 70ed19fcdb645840574aa445ce7feb36e3f81d22 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 13 Jan 2026 20:43:37 -0800 Subject: [PATCH 09/36] fix(chat): remove special handling for non-streaming (#2808) --- .../w/[workflowId]/components/chat/chat.tsx | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index feebe135e4..e67f92ab67 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -531,35 +531,6 @@ export function Chat() { return } - if ( - selectedOutputs.length > 0 && - 'logs' in result && - Array.isArray(result.logs) && - activeWorkflowId - ) { - const additionalOutputs: string[] = [] - - for (const outputId of selectedOutputs) { - const blockId = extractBlockIdFromOutputId(outputId) - const path = extractPathFromOutputId(outputId, blockId) - - if (path === 'content') continue - - const outputValue = extractOutputFromLogs(result.logs as BlockLog[], outputId) - if (outputValue !== undefined) { - const formattedValue = - typeof outputValue === 'string' ? outputValue : JSON.stringify(outputValue) - if (formattedValue) { - additionalOutputs.push(`**${path}:** ${formattedValue}`) - } - } - } - - if (additionalOutputs.length > 0) { - appendMessageContent(responseMessageId, `\n\n${additionalOutputs.join('\n\n')}`) - } - } - finalizeMessageStream(responseMessageId) } else if (contentChunk) { accumulatedContent += contentChunk From f6b7c15dc4752b7057556c522c4998286f517833 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 13 Jan 2026 21:01:42 -0800 Subject: [PATCH 10/36] improvement(oauth): added random identifier in unused accountId to bypass betterauth unique constraint (#2807) * improvement(oauth): added random identifier in unnused accountId to bypass betterauth unique constraint * ack pr comments --- apps/sim/lib/auth/auth.ts | 40 +- .../db/migrations/0140_fuzzy_the_twelve.sql | 2 + .../db/migrations/meta/0140_snapshot.json | 10239 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 5 +- 5 files changed, 10270 insertions(+), 23 deletions(-) create mode 100644 packages/db/migrations/0140_fuzzy_the_twelve.sql create mode 100644 packages/db/migrations/meta/0140_snapshot.json diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index d94e38585d..87106ac2dc 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -656,7 +656,7 @@ export const auth = betterAuth({ const now = new Date() return { - id: profile.id.toString(), + id: `${profile.id.toString()}-${crypto.randomUUID()}`, name: profile.name || profile.login, email: profile.email, image: profile.avatar_url, @@ -962,7 +962,7 @@ export const auth = betterAuth({ const now = new Date() return { - id: uniqueId, + id: `${uniqueId}-${crypto.randomUUID()}`, name: 'Wealthbox User', email: `${uniqueId}@wealthbox.user`, emailVerified: false, @@ -1016,7 +1016,7 @@ export const auth = betterAuth({ const user = data.data return { - id: user.id.toString(), + id: `${user.id.toString()}-${crypto.randomUUID()}`, name: user.name, email: user.email, emailVerified: user.activated, @@ -1108,7 +1108,7 @@ export const auth = betterAuth({ }) return { - id: data.user_id || data.hub_id.toString(), + id: `${data.user_id || data.hub_id.toString()}-${crypto.randomUUID()}`, name: data.user || 'HubSpot User', email: data.user || `hubspot-${data.hub_id}@hubspot.com`, emailVerified: true, @@ -1162,7 +1162,7 @@ export const auth = betterAuth({ const data = await response.json() return { - id: data.user_id || data.sub, + id: `${data.user_id || data.sub}-${crypto.randomUUID()}`, name: data.name || 'Salesforce User', email: data.email || `salesforce-${data.user_id}@salesforce.com`, emailVerified: data.email_verified || true, @@ -1221,7 +1221,7 @@ export const auth = betterAuth({ const now = new Date() return { - id: profile.data.id, + id: `${profile.data.id}-${crypto.randomUUID()}`, name: profile.data.name || 'X User', email: `${profile.data.username}@x.com`, image: profile.data.profile_image_url, @@ -1295,7 +1295,7 @@ export const auth = betterAuth({ const now = new Date() return { - id: profile.account_id, + id: `${profile.account_id}-${crypto.randomUUID()}`, name: profile.name || profile.display_name || 'Confluence User', email: profile.email || `${profile.account_id}@atlassian.com`, image: profile.picture || undefined, @@ -1406,7 +1406,7 @@ export const auth = betterAuth({ const now = new Date() return { - id: profile.account_id, + id: `${profile.account_id}-${crypto.randomUUID()}`, name: profile.name || profile.display_name || 'Jira User', email: profile.email || `${profile.account_id}@atlassian.com`, image: profile.picture || undefined, @@ -1456,7 +1456,7 @@ export const auth = betterAuth({ const now = new Date() return { - id: data.id, + id: `${data.id}-${crypto.randomUUID()}`, name: data.email ? data.email.split('@')[0] : 'Airtable User', email: data.email || `${data.id}@airtable.user`, emailVerified: !!data.email, @@ -1505,7 +1505,7 @@ export const auth = betterAuth({ const now = new Date() return { - id: profile.bot?.owner?.user?.id || profile.id, + id: `${profile.bot?.owner?.user?.id || profile.id}-${crypto.randomUUID()}`, name: profile.name || profile.bot?.owner?.user?.name || 'Notion User', email: profile.person?.email || `${profile.id}@notion.user`, emailVerified: !!profile.person?.email, @@ -1572,7 +1572,7 @@ export const auth = betterAuth({ const now = new Date() return { - id: data.id, + id: `${data.id}-${crypto.randomUUID()}`, name: data.name || 'Reddit User', email: `${data.name}@reddit.user`, image: data.icon_img || undefined, @@ -1644,7 +1644,7 @@ export const auth = betterAuth({ const viewer = data.viewer return { - id: viewer.id, + id: `${viewer.id}-${crypto.randomUUID()}`, email: viewer.email, name: viewer.name, emailVerified: true, @@ -1707,7 +1707,7 @@ export const auth = betterAuth({ const data = await response.json() return { - id: data.account_id, + id: `${data.account_id}-${crypto.randomUUID()}`, email: data.email, name: data.name?.display_name || data.email, emailVerified: data.email_verified || false, @@ -1758,7 +1758,7 @@ export const auth = betterAuth({ const now = new Date() return { - id: profile.gid, + id: `${profile.gid}-${crypto.randomUUID()}`, name: profile.name || 'Asana User', email: profile.email || `${profile.gid}@asana.user`, image: profile.photo?.image_128x128 || undefined, @@ -1834,7 +1834,7 @@ export const auth = betterAuth({ logger.info('Slack credential identifier', { teamId, userId, uniqueId, teamName }) return { - id: uniqueId, + id: `${uniqueId}-${crypto.randomUUID()}`, name: teamName, email: `${teamId}-${userId}@slack.bot`, emailVerified: false, @@ -1884,7 +1884,7 @@ export const auth = betterAuth({ const uniqueId = `webflow-${userId}` return { - id: uniqueId, + id: `${uniqueId}-${crypto.randomUUID()}`, name: data.user_name || 'Webflow User', email: `${uniqueId.replace(/[^a-zA-Z0-9]/g, '')}@webflow.user`, emailVerified: false, @@ -1931,7 +1931,7 @@ export const auth = betterAuth({ const profile = await response.json() return { - id: profile.sub, + id: `${profile.sub}-${crypto.randomUUID()}`, name: profile.name || 'LinkedIn User', email: profile.email || `${profile.sub}@linkedin.user`, emailVerified: profile.email_verified || true, @@ -1993,7 +1993,7 @@ export const auth = betterAuth({ const profile = await response.json() return { - id: profile.id, + id: `${profile.id}-${crypto.randomUUID()}`, name: `${profile.first_name || ''} ${profile.last_name || ''}`.trim() || 'Zoom User', email: profile.email || `${profile.id}@zoom.user`, @@ -2060,7 +2060,7 @@ export const auth = betterAuth({ const profile = await response.json() return { - id: profile.id, + id: `${profile.id}-${crypto.randomUUID()}`, name: profile.display_name || 'Spotify User', email: profile.email || `${profile.id}@spotify.user`, emailVerified: true, @@ -2108,7 +2108,7 @@ export const auth = betterAuth({ const profile = await response.json() return { - id: profile.ID?.toString() || profile.id?.toString(), + id: `${profile.ID?.toString() || profile.id?.toString()}-${crypto.randomUUID()}`, name: profile.display_name || profile.username || 'WordPress User', email: profile.email || `${profile.username}@wordpress.com`, emailVerified: profile.email_verified || false, diff --git a/packages/db/migrations/0140_fuzzy_the_twelve.sql b/packages/db/migrations/0140_fuzzy_the_twelve.sql new file mode 100644 index 0000000000..606b0eb723 --- /dev/null +++ b/packages/db/migrations/0140_fuzzy_the_twelve.sql @@ -0,0 +1,2 @@ +DROP INDEX "account_user_provider_account_unique";--> statement-breakpoint +CREATE UNIQUE INDEX "account_user_provider_unique" ON "account" USING btree ("user_id","provider_id"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0140_snapshot.json b/packages/db/migrations/meta/0140_snapshot.json new file mode 100644 index 0000000000..07327f6e77 --- /dev/null +++ b/packages/db/migrations/meta/0140_snapshot.json @@ -0,0 +1,10239 @@ +{ + "id": "604deef4-68ee-4a32-94bd-21ce3e37be38", + "prevId": "c895e678-5c08-44f9-b1be-b1f1021a6603", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_agent_workspace_id_idx": { + "name": "a2a_agent_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_id_idx": { + "name": "a2a_push_notification_config_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_user_provider_unique": { + "name": "account_user_provider_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_organization_id_idx": { + "name": "credential_set_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_set_id_idx": { + "name": "credential_set_member_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form": { + "name": "form", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_namespace_unique": { + "name": "idempotency_key_namespace_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idempotency_key_namespace_idx": { + "name": "idempotency_key_namespace_idx", + "columns": [ + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_idx": { + "name": "mcp_servers_workspace_deleted_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_organization_id_idx": { + "name": "permission_group_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_org_name_unique": { + "name": "permission_group_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_user_id_unique": { + "name": "permission_group_member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_super_user": { + "name": "is_super_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'20'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_block_id_workflow_blocks_id_fk": { + "name": "webhook_block_id_workflow_blocks_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_unique": { + "name": "workflow_schedule_workflow_block_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_block_id_workflow_blocks_id_fk": { + "name": "workflow_schedule_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_files_key_unique": { + "name": "workspace_files_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "workspace_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "org_invitation_id": { + "name": "org_invitation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": ["workflow", "wand", "copilot"] + }, + "public.workspace_invitation_status": { + "name": "workspace_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 30639b8a2e..08cd037e8e 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -974,6 +974,13 @@ "when": 1768260112533, "tag": "0139_late_cargill", "breakpoints": true + }, + { + "idx": 140, + "version": "7", + "when": 1768366574848, + "tag": "0140_fuzzy_the_twelve", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index b596b01168..c5edcfb623 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -89,10 +89,9 @@ export const account = pgTable( table.accountId, table.providerId ), - uniqueUserProviderAccount: uniqueIndex('account_user_provider_account_unique').on( + uniqueUserProvider: uniqueIndex('account_user_provider_unique').on( table.userId, - table.providerId, - table.accountId + table.providerId ), }) ) From 4b026ad54d525ac7ac876302b1f6a28c57bceaff Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 13 Jan 2026 21:08:00 -0800 Subject: [PATCH 11/36] fix(a2a): added file data part and data data part to a2a agents (#2805) * fix(a2a): added file data part and data data part to a2a agents * removed unused streaming tool * ack comment --- apps/docs/content/docs/en/tools/a2a.mdx | 2 + .../tools/a2a/send-message-stream/route.ts | 150 ------------------ .../app/api/tools/a2a/send-message/route.ts | 99 +++++++++++- apps/sim/blocks/blocks/a2a.ts | 25 +++ apps/sim/lib/a2a/utils.ts | 19 ++- apps/sim/tools/a2a/cancel_task.ts | 13 +- .../sim/tools/a2a/delete_push_notification.ts | 16 +- apps/sim/tools/a2a/get_agent_card.ts | 11 +- apps/sim/tools/a2a/get_push_notification.ts | 13 +- apps/sim/tools/a2a/get_task.ts | 15 +- apps/sim/tools/a2a/index.ts | 2 - apps/sim/tools/a2a/resubscribe.ts | 13 +- apps/sim/tools/a2a/send_message.ts | 24 ++- apps/sim/tools/a2a/send_message_stream.ts | 81 ---------- apps/sim/tools/a2a/set_push_notification.ts | 17 +- apps/sim/tools/a2a/types.ts | 9 ++ apps/sim/tools/registry.ts | 2 - 17 files changed, 230 insertions(+), 281 deletions(-) delete mode 100644 apps/sim/app/api/tools/a2a/send-message-stream/route.ts delete mode 100644 apps/sim/tools/a2a/send_message_stream.ts diff --git a/apps/docs/content/docs/en/tools/a2a.mdx b/apps/docs/content/docs/en/tools/a2a.mdx index 63393fe019..9e7ea9ee4e 100644 --- a/apps/docs/content/docs/en/tools/a2a.mdx +++ b/apps/docs/content/docs/en/tools/a2a.mdx @@ -44,6 +44,8 @@ Send a message to an external A2A-compatible agent. | `message` | string | Yes | Message to send to the agent | | `taskId` | string | No | Task ID for continuing an existing task | | `contextId` | string | No | Context ID for conversation continuity | +| `data` | string | No | Structured data to include with the message \(JSON string\) | +| `files` | array | No | Files to include with the message | | `apiKey` | string | No | API key for authentication | #### Output diff --git a/apps/sim/app/api/tools/a2a/send-message-stream/route.ts b/apps/sim/app/api/tools/a2a/send-message-stream/route.ts deleted file mode 100644 index e30689a801..0000000000 --- a/apps/sim/app/api/tools/a2a/send-message-stream/route.ts +++ /dev/null @@ -1,150 +0,0 @@ -import type { - Artifact, - Message, - Task, - TaskArtifactUpdateEvent, - TaskState, - TaskStatusUpdateEvent, -} from '@a2a-js/sdk' -import { createLogger } from '@sim/logger' -import { type NextRequest, NextResponse } from 'next/server' -import { z } from 'zod' -import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils' -import { checkHybridAuth } from '@/lib/auth/hybrid' -import { generateRequestId } from '@/lib/core/utils/request' - -export const dynamic = 'force-dynamic' - -const logger = createLogger('A2ASendMessageStreamAPI') - -const A2ASendMessageStreamSchema = z.object({ - agentUrl: z.string().min(1, 'Agent URL is required'), - message: z.string().min(1, 'Message is required'), - taskId: z.string().optional(), - contextId: z.string().optional(), - apiKey: z.string().optional(), -}) - -export async function POST(request: NextRequest) { - const requestId = generateRequestId() - - try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) - - if (!authResult.success) { - logger.warn( - `[${requestId}] Unauthorized A2A send message stream attempt: ${authResult.error}` - ) - return NextResponse.json( - { - success: false, - error: authResult.error || 'Authentication required', - }, - { status: 401 } - ) - } - - logger.info( - `[${requestId}] Authenticated A2A send message stream request via ${authResult.authType}`, - { - userId: authResult.userId, - } - ) - - const body = await request.json() - const validatedData = A2ASendMessageStreamSchema.parse(body) - - logger.info(`[${requestId}] Sending A2A streaming message`, { - agentUrl: validatedData.agentUrl, - hasTaskId: !!validatedData.taskId, - hasContextId: !!validatedData.contextId, - }) - - const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey) - - const message: Message = { - kind: 'message', - messageId: crypto.randomUUID(), - role: 'user', - parts: [{ kind: 'text', text: validatedData.message }], - ...(validatedData.taskId && { taskId: validatedData.taskId }), - ...(validatedData.contextId && { contextId: validatedData.contextId }), - } - - const stream = client.sendMessageStream({ message }) - - let taskId = '' - let contextId: string | undefined - let state: TaskState = 'working' - let content = '' - let artifacts: Artifact[] = [] - let history: Message[] = [] - - for await (const event of stream) { - if (event.kind === 'message') { - const msg = event as Message - content = extractTextContent(msg) - taskId = msg.taskId || taskId - contextId = msg.contextId || contextId - state = 'completed' - } else if (event.kind === 'task') { - const task = event as Task - taskId = task.id - contextId = task.contextId - state = task.status.state - artifacts = task.artifacts || [] - history = task.history || [] - const lastAgentMessage = history.filter((m) => m.role === 'agent').pop() - if (lastAgentMessage) { - content = extractTextContent(lastAgentMessage) - } - } else if ('status' in event) { - const statusEvent = event as TaskStatusUpdateEvent - state = statusEvent.status.state - } else if ('artifact' in event) { - const artifactEvent = event as TaskArtifactUpdateEvent - artifacts.push(artifactEvent.artifact) - } - } - - logger.info(`[${requestId}] A2A streaming message completed`, { - taskId, - state, - artifactCount: artifacts.length, - }) - - return NextResponse.json({ - success: isTerminalState(state) && state !== 'failed', - output: { - content, - taskId, - contextId, - state, - artifacts, - history, - }, - }) - } catch (error) { - if (error instanceof z.ZodError) { - logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) - return NextResponse.json( - { - success: false, - error: 'Invalid request data', - details: error.errors, - }, - { status: 400 } - ) - } - - logger.error(`[${requestId}] Error in A2A streaming:`, error) - - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Streaming failed', - }, - { status: 500 } - ) - } -} diff --git a/apps/sim/app/api/tools/a2a/send-message/route.ts b/apps/sim/app/api/tools/a2a/send-message/route.ts index 4d52fc710c..a66c2b3d37 100644 --- a/apps/sim/app/api/tools/a2a/send-message/route.ts +++ b/apps/sim/app/api/tools/a2a/send-message/route.ts @@ -1,4 +1,4 @@ -import type { Message, Task } from '@a2a-js/sdk' +import type { DataPart, FilePart, Message, Part, Task, TextPart } from '@a2a-js/sdk' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' @@ -10,11 +10,20 @@ export const dynamic = 'force-dynamic' const logger = createLogger('A2ASendMessageAPI') +const FileInputSchema = z.object({ + type: z.enum(['file', 'url']), + data: z.string(), + name: z.string(), + mime: z.string().optional(), +}) + const A2ASendMessageSchema = z.object({ agentUrl: z.string().min(1, 'Agent URL is required'), message: z.string().min(1, 'Message is required'), taskId: z.string().optional(), contextId: z.string().optional(), + data: z.string().optional(), + files: z.array(FileInputSchema).optional(), apiKey: z.string().optional(), }) @@ -51,18 +60,100 @@ export async function POST(request: NextRequest) { hasContextId: !!validatedData.contextId, }) - const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey) + let client + try { + client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey) + logger.info(`[${requestId}] A2A client created successfully`) + } catch (clientError) { + logger.error(`[${requestId}] Failed to create A2A client:`, clientError) + return NextResponse.json( + { + success: false, + error: `Failed to connect to agent: ${clientError instanceof Error ? clientError.message : 'Unknown error'}`, + }, + { status: 502 } + ) + } + + const parts: Part[] = [] + + const textPart: TextPart = { kind: 'text', text: validatedData.message } + parts.push(textPart) + + if (validatedData.data) { + try { + const parsedData = JSON.parse(validatedData.data) + const dataPart: DataPart = { kind: 'data', data: parsedData } + parts.push(dataPart) + } catch (parseError) { + logger.warn(`[${requestId}] Failed to parse data as JSON, skipping DataPart`, { + error: parseError instanceof Error ? parseError.message : String(parseError), + }) + } + } + + if (validatedData.files && validatedData.files.length > 0) { + for (const file of validatedData.files) { + if (file.type === 'url') { + const filePart: FilePart = { + kind: 'file', + file: { + name: file.name, + mimeType: file.mime, + uri: file.data, + }, + } + parts.push(filePart) + } else if (file.type === 'file') { + let bytes = file.data + let mimeType = file.mime + + if (file.data.startsWith('data:')) { + const match = file.data.match(/^data:([^;]+);base64,(.+)$/) + if (match) { + mimeType = mimeType || match[1] + bytes = match[2] + } else { + bytes = file.data + } + } + + const filePart: FilePart = { + kind: 'file', + file: { + name: file.name, + mimeType: mimeType || 'application/octet-stream', + bytes, + }, + } + parts.push(filePart) + } + } + } const message: Message = { kind: 'message', messageId: crypto.randomUUID(), role: 'user', - parts: [{ kind: 'text', text: validatedData.message }], + parts, ...(validatedData.taskId && { taskId: validatedData.taskId }), ...(validatedData.contextId && { contextId: validatedData.contextId }), } - const result = await client.sendMessage({ message }) + let result + try { + result = await client.sendMessage({ message }) + logger.info(`[${requestId}] A2A sendMessage completed`, { resultKind: result?.kind }) + } catch (sendError) { + logger.error(`[${requestId}] Failed to send A2A message:`, sendError) + return NextResponse.json( + { + success: false, + error: `Failed to send message: ${sendError instanceof Error ? sendError.message : 'Unknown error'}`, + }, + { status: 502 } + ) + } if (result.kind === 'message') { const responseMessage = result as Message diff --git a/apps/sim/blocks/blocks/a2a.ts b/apps/sim/blocks/blocks/a2a.ts index a520028cff..6996b685a4 100644 --- a/apps/sim/blocks/blocks/a2a.ts +++ b/apps/sim/blocks/blocks/a2a.ts @@ -98,6 +98,23 @@ export const A2ABlock: BlockConfig = { condition: { field: 'operation', value: 'a2a_send_message' }, required: true, }, + { + id: 'data', + title: 'Data (JSON)', + type: 'code', + placeholder: '{\n "key": "value"\n}', + description: 'Structured data to include with the message (DataPart)', + condition: { field: 'operation', value: 'a2a_send_message' }, + }, + { + id: 'files', + title: 'Files', + type: 'file-upload', + placeholder: 'Upload files to send', + description: 'Files to include with the message (FilePart)', + condition: { field: 'operation', value: 'a2a_send_message' }, + multiple: true, + }, { id: 'taskId', title: 'Task ID', @@ -208,6 +225,14 @@ export const A2ABlock: BlockConfig = { type: 'string', description: 'Context ID for conversation continuity', }, + data: { + type: 'json', + description: 'Structured data to include with the message', + }, + files: { + type: 'array', + description: 'Files to include with the message', + }, historyLength: { type: 'number', description: 'Number of history messages to include', diff --git a/apps/sim/lib/a2a/utils.ts b/apps/sim/lib/a2a/utils.ts index 119059d994..3eddb5d8d1 100644 --- a/apps/sim/lib/a2a/utils.ts +++ b/apps/sim/lib/a2a/utils.ts @@ -36,9 +36,10 @@ class ApiKeyInterceptor implements CallInterceptor { /** * Create an A2A client from an agent URL with optional API key authentication * - * The agent URL should be the full endpoint URL (e.g., /api/a2a/serve/{agentId}). - * We pass an empty path to createFromUrl so it uses the URL directly for agent card - * discovery (GET on the URL) instead of appending .well-known/agent-card.json. + * Supports both standard A2A agents (agent card at /.well-known/agent.json) + * and Sim Studio agents (agent card at root URL via GET). + * + * Tries standard path first, falls back to root URL for compatibility. */ export async function createA2AClient(agentUrl: string, apiKey?: string): Promise { const factoryOptions = apiKey @@ -49,6 +50,18 @@ export async function createA2AClient(agentUrl: string, apiKey?: string): Promis }) : ClientFactoryOptions.default const factory = new ClientFactory(factoryOptions) + + // Try standard A2A path first (/.well-known/agent.json) + try { + return await factory.createFromUrl(agentUrl, '/.well-known/agent.json') + } catch (standardError) { + logger.debug('Standard agent card path failed, trying root URL', { + agentUrl, + error: standardError instanceof Error ? standardError.message : String(standardError), + }) + } + + // Fall back to root URL (Sim Studio compatibility) return factory.createFromUrl(agentUrl, '') } diff --git a/apps/sim/tools/a2a/cancel_task.ts b/apps/sim/tools/a2a/cancel_task.ts index a43bccc587..6ac1b78937 100644 --- a/apps/sim/tools/a2a/cancel_task.ts +++ b/apps/sim/tools/a2a/cancel_task.ts @@ -30,11 +30,14 @@ export const a2aCancelTaskTool: ToolConfig ({ 'Content-Type': 'application/json', }), - body: (params: A2ACancelTaskParams) => ({ - agentUrl: params.agentUrl, - taskId: params.taskId, - apiKey: params.apiKey, - }), + body: (params: A2ACancelTaskParams) => { + const body: Record = { + agentUrl: params.agentUrl, + taskId: params.taskId, + } + if (params.apiKey) body.apiKey = params.apiKey + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/a2a/delete_push_notification.ts b/apps/sim/tools/a2a/delete_push_notification.ts index 186e9834bc..89052e8cf1 100644 --- a/apps/sim/tools/a2a/delete_push_notification.ts +++ b/apps/sim/tools/a2a/delete_push_notification.ts @@ -38,12 +38,16 @@ export const a2aDeletePushNotificationTool: ToolConfig< headers: () => ({ 'Content-Type': 'application/json', }), - body: (params) => ({ - agentUrl: params.agentUrl, - taskId: params.taskId, - pushNotificationConfigId: params.pushNotificationConfigId, - apiKey: params.apiKey, - }), + body: (params) => { + const body: Record = { + agentUrl: params.agentUrl, + taskId: params.taskId, + } + if (params.pushNotificationConfigId) + body.pushNotificationConfigId = params.pushNotificationConfigId + if (params.apiKey) body.apiKey = params.apiKey + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/a2a/get_agent_card.ts b/apps/sim/tools/a2a/get_agent_card.ts index e6ee38795b..f0e07ff6c5 100644 --- a/apps/sim/tools/a2a/get_agent_card.ts +++ b/apps/sim/tools/a2a/get_agent_card.ts @@ -25,10 +25,13 @@ export const a2aGetAgentCardTool: ToolConfig ({ 'Content-Type': 'application/json', }), - body: (params) => ({ - agentUrl: params.agentUrl, - apiKey: params.apiKey, - }), + body: (params) => { + const body: Record = { + agentUrl: params.agentUrl, + } + if (params.apiKey) body.apiKey = params.apiKey + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/a2a/get_push_notification.ts b/apps/sim/tools/a2a/get_push_notification.ts index e117923674..3682476b32 100644 --- a/apps/sim/tools/a2a/get_push_notification.ts +++ b/apps/sim/tools/a2a/get_push_notification.ts @@ -33,11 +33,14 @@ export const a2aGetPushNotificationTool: ToolConfig< headers: () => ({ 'Content-Type': 'application/json', }), - body: (params) => ({ - agentUrl: params.agentUrl, - taskId: params.taskId, - apiKey: params.apiKey, - }), + body: (params) => { + const body: Record = { + agentUrl: params.agentUrl, + taskId: params.taskId, + } + if (params.apiKey) body.apiKey = params.apiKey + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/a2a/get_task.ts b/apps/sim/tools/a2a/get_task.ts index 1c62b408f4..43e0059dac 100644 --- a/apps/sim/tools/a2a/get_task.ts +++ b/apps/sim/tools/a2a/get_task.ts @@ -34,12 +34,15 @@ export const a2aGetTaskTool: ToolConfig = headers: () => ({ 'Content-Type': 'application/json', }), - body: (params: A2AGetTaskParams) => ({ - agentUrl: params.agentUrl, - taskId: params.taskId, - apiKey: params.apiKey, - historyLength: params.historyLength, - }), + body: (params: A2AGetTaskParams) => { + const body: Record = { + agentUrl: params.agentUrl, + taskId: params.taskId, + } + if (params.apiKey) body.apiKey = params.apiKey + if (params.historyLength) body.historyLength = params.historyLength + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/a2a/index.ts b/apps/sim/tools/a2a/index.ts index 7b78f26424..ccbf235a1b 100644 --- a/apps/sim/tools/a2a/index.ts +++ b/apps/sim/tools/a2a/index.ts @@ -5,7 +5,6 @@ import { a2aGetPushNotificationTool } from './get_push_notification' import { a2aGetTaskTool } from './get_task' import { a2aResubscribeTool } from './resubscribe' import { a2aSendMessageTool } from './send_message' -import { a2aSendMessageStreamTool } from './send_message_stream' import { a2aSetPushNotificationTool } from './set_push_notification' export { @@ -16,6 +15,5 @@ export { a2aGetTaskTool, a2aResubscribeTool, a2aSendMessageTool, - a2aSendMessageStreamTool, a2aSetPushNotificationTool, } diff --git a/apps/sim/tools/a2a/resubscribe.ts b/apps/sim/tools/a2a/resubscribe.ts index e2ed455855..99456b8b53 100644 --- a/apps/sim/tools/a2a/resubscribe.ts +++ b/apps/sim/tools/a2a/resubscribe.ts @@ -30,11 +30,14 @@ export const a2aResubscribeTool: ToolConfig ({ 'Content-Type': 'application/json', }), - body: (params: A2AResubscribeParams) => ({ - agentUrl: params.agentUrl, - taskId: params.taskId, - apiKey: params.apiKey, - }), + body: (params: A2AResubscribeParams) => { + const body: Record = { + agentUrl: params.agentUrl, + taskId: params.taskId, + } + if (params.apiKey) body.apiKey = params.apiKey + return body + }, }, transformResponse: async (response) => { diff --git a/apps/sim/tools/a2a/send_message.ts b/apps/sim/tools/a2a/send_message.ts index 6da9cf11c8..0b317a4413 100644 --- a/apps/sim/tools/a2a/send_message.ts +++ b/apps/sim/tools/a2a/send_message.ts @@ -26,6 +26,14 @@ export const a2aSendMessageTool: ToolConfig ({}), + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + agentUrl: params.agentUrl, + message: params.message, + } + if (params.taskId) body.taskId = params.taskId + if (params.contextId) body.contextId = params.contextId + if (params.data) body.data = params.data + if (params.files && params.files.length > 0) body.files = params.files + if (params.apiKey) body.apiKey = params.apiKey + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/a2a/send_message_stream.ts b/apps/sim/tools/a2a/send_message_stream.ts deleted file mode 100644 index dd44856b0c..0000000000 --- a/apps/sim/tools/a2a/send_message_stream.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { ToolConfig } from '@/tools/types' -import type { A2ASendMessageParams, A2ASendMessageResponse } from './types' - -export const a2aSendMessageStreamTool: ToolConfig = { - id: 'a2a_send_message_stream', - name: 'A2A Send Message (Streaming)', - description: 'Send a message to an external A2A-compatible agent with real-time streaming.', - version: '1.0.0', - - params: { - agentUrl: { - type: 'string', - required: true, - description: 'The A2A agent endpoint URL', - }, - message: { - type: 'string', - required: true, - description: 'Message to send to the agent', - }, - taskId: { - type: 'string', - description: 'Task ID for continuing an existing task', - }, - contextId: { - type: 'string', - description: 'Context ID for conversation continuity', - }, - apiKey: { - type: 'string', - description: 'API key for authentication', - }, - }, - - request: { - url: '/api/tools/a2a/send-message-stream', - method: 'POST', - headers: () => ({ - 'Content-Type': 'application/json', - }), - body: (params) => ({ - agentUrl: params.agentUrl, - message: params.message, - taskId: params.taskId, - contextId: params.contextId, - apiKey: params.apiKey, - }), - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - return data - }, - - outputs: { - content: { - type: 'string', - description: 'The text response from the agent', - }, - taskId: { - type: 'string', - description: 'Task ID for follow-up interactions', - }, - contextId: { - type: 'string', - description: 'Context ID for conversation continuity', - }, - state: { - type: 'string', - description: 'Task state', - }, - artifacts: { - type: 'array', - description: 'Structured output artifacts', - }, - history: { - type: 'array', - description: 'Full message history', - }, - }, -} diff --git a/apps/sim/tools/a2a/set_push_notification.ts b/apps/sim/tools/a2a/set_push_notification.ts index a1a69ed40b..e3dd25360b 100644 --- a/apps/sim/tools/a2a/set_push_notification.ts +++ b/apps/sim/tools/a2a/set_push_notification.ts @@ -42,13 +42,16 @@ export const a2aSetPushNotificationTool: ToolConfig< headers: () => ({ 'Content-Type': 'application/json', }), - body: (params: A2ASetPushNotificationParams) => ({ - agentUrl: params.agentUrl, - taskId: params.taskId, - webhookUrl: params.webhookUrl, - token: params.token, - apiKey: params.apiKey, - }), + body: (params: A2ASetPushNotificationParams) => { + const body: Record = { + agentUrl: params.agentUrl, + taskId: params.taskId, + webhookUrl: params.webhookUrl, + } + if (params.token) body.token = params.token + if (params.apiKey) body.apiKey = params.apiKey + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/a2a/types.ts b/apps/sim/tools/a2a/types.ts index 7230eb563a..82844d6faf 100644 --- a/apps/sim/tools/a2a/types.ts +++ b/apps/sim/tools/a2a/types.ts @@ -25,11 +25,20 @@ export interface A2AGetAgentCardResponse extends ToolResponse { } } +export interface A2ASendMessageFileInput { + type: 'file' | 'url' + data: string + name: string + mime?: string +} + export interface A2ASendMessageParams { agentUrl: string message: string taskId?: string contextId?: string + data?: string + files?: A2ASendMessageFileInput[] apiKey?: string } diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index c5137da6e5..7da777ff96 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -5,7 +5,6 @@ import { a2aGetPushNotificationTool, a2aGetTaskTool, a2aResubscribeTool, - a2aSendMessageStreamTool, a2aSendMessageTool, a2aSetPushNotificationTool, } from '@/tools/a2a' @@ -1543,7 +1542,6 @@ export const tools: Record = { a2a_get_task: a2aGetTaskTool, a2a_resubscribe: a2aResubscribeTool, a2a_send_message: a2aSendMessageTool, - a2a_send_message_stream: a2aSendMessageStreamTool, a2a_set_push_notification: a2aSetPushNotificationTool, arxiv_search: arxivSearchTool, arxiv_get_paper: arxivGetPaperTool, From 258e96d6b56b03684da7ca7fb15b99b9eaf9a748 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 13 Jan 2026 22:03:19 -0800 Subject: [PATCH 12/36] improvement(pricing): drop agent multiplier in docs, change base exec cost --- apps/docs/content/docs/en/execution/costs.mdx | 44 +++++++++---------- apps/sim/lib/billing/constants.ts | 2 +- .../logs/execution/logging-factory.test.ts | 4 +- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/apps/docs/content/docs/en/execution/costs.mdx b/apps/docs/content/docs/en/execution/costs.mdx index 54c8a5dda5..65dcd8a583 100644 --- a/apps/docs/content/docs/en/execution/costs.mdx +++ b/apps/docs/content/docs/en/execution/costs.mdx @@ -12,7 +12,7 @@ Sim automatically calculates costs for all workflow executions, providing transp Every workflow execution includes two cost components: -**Base Execution Charge**: $0.001 per execution +**Base Execution Charge**: $0.005 per execution **AI Model Usage**: Variable cost based on token consumption ```javascript @@ -48,40 +48,40 @@ The model breakdown shows: - **Hosted Models** - Sim provides API keys with a 1.4x pricing multiplier for Agent blocks: + **Hosted Models** - Sim provides API keys with a 1.1x pricing multiplier for Agent blocks: **OpenAI** | Model | Base Price (Input/Output) | Hosted Price (Input/Output) | |-------|---------------------------|----------------------------| - | GPT-5.1 | $1.25 / $10.00 | $1.75 / $14.00 | - | GPT-5 | $1.25 / $10.00 | $1.75 / $14.00 | - | GPT-5 Mini | $0.25 / $2.00 | $0.35 / $2.80 | - | GPT-5 Nano | $0.05 / $0.40 | $0.07 / $0.56 | - | GPT-4o | $2.50 / $10.00 | $3.50 / $14.00 | - | GPT-4.1 | $2.00 / $8.00 | $2.80 / $11.20 | - | GPT-4.1 Mini | $0.40 / $1.60 | $0.56 / $2.24 | - | GPT-4.1 Nano | $0.10 / $0.40 | $0.14 / $0.56 | - | o1 | $15.00 / $60.00 | $21.00 / $84.00 | - | o3 | $2.00 / $8.00 | $2.80 / $11.20 | - | o4 Mini | $1.10 / $4.40 | $1.54 / $6.16 | + | GPT-5.1 | $1.25 / $10.00 | $1.38 / $11.00 | + | GPT-5 | $1.25 / $10.00 | $1.38 / $11.00 | + | GPT-5 Mini | $0.25 / $2.00 | $0.28 / $2.20 | + | GPT-5 Nano | $0.05 / $0.40 | $0.06 / $0.44 | + | GPT-4o | $2.50 / $10.00 | $2.75 / $11.00 | + | GPT-4.1 | $2.00 / $8.00 | $2.20 / $8.80 | + | GPT-4.1 Mini | $0.40 / $1.60 | $0.44 / $1.76 | + | GPT-4.1 Nano | $0.10 / $0.40 | $0.11 / $0.44 | + | o1 | $15.00 / $60.00 | $16.50 / $66.00 | + | o3 | $2.00 / $8.00 | $2.20 / $8.80 | + | o4 Mini | $1.10 / $4.40 | $1.21 / $4.84 | **Anthropic** | Model | Base Price (Input/Output) | Hosted Price (Input/Output) | |-------|---------------------------|----------------------------| - | Claude Opus 4.5 | $5.00 / $25.00 | $7.00 / $35.00 | - | Claude Opus 4.1 | $15.00 / $75.00 | $21.00 / $105.00 | - | Claude Sonnet 4.5 | $3.00 / $15.00 | $4.20 / $21.00 | - | Claude Sonnet 4.0 | $3.00 / $15.00 | $4.20 / $21.00 | - | Claude Haiku 4.5 | $1.00 / $5.00 | $1.40 / $7.00 | + | Claude Opus 4.5 | $5.00 / $25.00 | $5.50 / $27.50 | + | Claude Opus 4.1 | $15.00 / $75.00 | $16.50 / $82.50 | + | Claude Sonnet 4.5 | $3.00 / $15.00 | $3.30 / $16.50 | + | Claude Sonnet 4.0 | $3.00 / $15.00 | $3.30 / $16.50 | + | Claude Haiku 4.5 | $1.00 / $5.00 | $1.10 / $5.50 | **Google** | Model | Base Price (Input/Output) | Hosted Price (Input/Output) | |-------|---------------------------|----------------------------| - | Gemini 3 Pro Preview | $2.00 / $12.00 | $2.80 / $16.80 | - | Gemini 2.5 Pro | $1.25 / $10.00 | $1.75 / $14.00 | - | Gemini 2.5 Flash | $0.30 / $2.50 | $0.42 / $3.50 | + | Gemini 3 Pro Preview | $2.00 / $12.00 | $2.20 / $13.20 | + | Gemini 2.5 Pro | $1.25 / $10.00 | $1.38 / $11.00 | + | Gemini 2.5 Flash | $0.30 / $2.50 | $0.33 / $2.75 | - *The 1.4x multiplier covers infrastructure and API management costs.* + *The 1.1x multiplier covers infrastructure and API management costs.* diff --git a/apps/sim/lib/billing/constants.ts b/apps/sim/lib/billing/constants.ts index 44ee2e513d..142a5ce06a 100644 --- a/apps/sim/lib/billing/constants.ts +++ b/apps/sim/lib/billing/constants.ts @@ -18,7 +18,7 @@ export const DEFAULT_ENTERPRISE_TIER_COST_LIMIT = 200 * Base charge applied to every workflow execution * This charge is applied regardless of whether the workflow uses AI models */ -export const BASE_EXECUTION_CHARGE = 0.001 +export const BASE_EXECUTION_CHARGE = 0.005 /** * Fixed cost for search tool invocation (in dollars) diff --git a/apps/sim/lib/logs/execution/logging-factory.test.ts b/apps/sim/lib/logs/execution/logging-factory.test.ts index 92a5981c75..badc63542d 100644 --- a/apps/sim/lib/logs/execution/logging-factory.test.ts +++ b/apps/sim/lib/logs/execution/logging-factory.test.ts @@ -8,7 +8,7 @@ import { // Mock the billing constants vi.mock('@/lib/billing/constants', () => ({ - BASE_EXECUTION_CHARGE: 0.001, + BASE_EXECUTION_CHARGE: 0.005, })) vi.mock('@sim/logger', () => loggerMock) @@ -148,7 +148,7 @@ describe('createEnvironmentObject', () => { }) describe('calculateCostSummary', () => { - const BASE_EXECUTION_CHARGE = 0.001 + const BASE_EXECUTION_CHARGE = 0.005 test('should return base execution charge for empty trace spans', () => { const result = calculateCostSummary([]) From 4f04b1efea5bb6ca95211f59ae541d4cbd9b96e3 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 13 Jan 2026 22:42:15 -0800 Subject: [PATCH 13/36] feat(terminal): migrate from zustand for console terminal logs to indexedDb, incr limit from 5mb to ~GBs (#2812) * feat(terminal): migrate from zustand for console terminal logs to indexedDb, incr limit from 5mb to ~GBs * ack PR comments --- .../w/[workflowId]/components/chat/chat.tsx | 4 +- .../components/terminal/terminal.tsx | 4 +- apps/sim/package.json | 1 + apps/sim/stores/terminal/console/index.ts | 1 + apps/sim/stores/terminal/console/storage.ts | 81 +++++++++++++++++++ apps/sim/stores/terminal/console/store.ts | 62 ++++++++++---- apps/sim/stores/terminal/console/types.ts | 11 +-- bun.lock | 4 + 8 files changed, 141 insertions(+), 27 deletions(-) create mode 100644 apps/sim/stores/terminal/console/storage.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index e67f92ab67..7518a35c4d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -221,7 +221,9 @@ export function Chat() { exportChatCSV, } = useChatStore() - const { entries } = useTerminalConsoleStore() + const hasConsoleHydrated = useTerminalConsoleStore((state) => state._hasHydrated) + const entriesFromStore = useTerminalConsoleStore((state) => state.entries) + const entries = hasConsoleHydrated ? entriesFromStore : [] const { isExecuting } = useExecutionStore() const { handleRunWorkflow, handleCancelExecution } = useWorkflowExecution() const { data: session } = useSession() diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx index d263456ff2..311c2ff22a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx @@ -320,12 +320,14 @@ export function Terminal() { } = useTerminalStore() const isExpanded = useTerminalStore((state) => state.terminalHeight > NEAR_MIN_THRESHOLD) const { activeWorkflowId } = useWorkflowRegistry() + const hasConsoleHydrated = useTerminalConsoleStore((state) => state._hasHydrated) const workflowEntriesSelector = useCallback( (state: { entries: ConsoleEntry[] }) => state.entries.filter((entry) => entry.workflowId === activeWorkflowId), [activeWorkflowId] ) - const entries = useTerminalConsoleStore(useShallow(workflowEntriesSelector)) + const entriesFromStore = useTerminalConsoleStore(useShallow(workflowEntriesSelector)) + const entries = hasConsoleHydrated ? entriesFromStore : [] const clearWorkflowConsole = useTerminalConsoleStore((state) => state.clearWorkflowConsole) const exportConsoleCSV = useTerminalConsoleStore((state) => state.exportConsoleCSV) const [selectedEntry, setSelectedEntry] = useState(null) diff --git a/apps/sim/package.json b/apps/sim/package.json index 8287aff388..3213602ee9 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -104,6 +104,7 @@ "groq-sdk": "^0.15.0", "html-to-image": "1.11.13", "html-to-text": "^9.0.5", + "idb-keyval": "6.2.2", "imapflow": "1.2.4", "input-otp": "^1.4.2", "ioredis": "^5.6.0", diff --git a/apps/sim/stores/terminal/console/index.ts b/apps/sim/stores/terminal/console/index.ts index 3560970281..d2b6679543 100644 --- a/apps/sim/stores/terminal/console/index.ts +++ b/apps/sim/stores/terminal/console/index.ts @@ -1,2 +1,3 @@ +export { indexedDBStorage } from './storage' export { useTerminalConsoleStore } from './store' export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './types' diff --git a/apps/sim/stores/terminal/console/storage.ts b/apps/sim/stores/terminal/console/storage.ts new file mode 100644 index 0000000000..1a809648f8 --- /dev/null +++ b/apps/sim/stores/terminal/console/storage.ts @@ -0,0 +1,81 @@ +import { createLogger } from '@sim/logger' +import { del, get, set } from 'idb-keyval' +import type { StateStorage } from 'zustand/middleware' + +const logger = createLogger('ConsoleStorage') + +const STORE_KEY = 'terminal-console-store' +const MIGRATION_KEY = 'terminal-console-store-migrated' + +/** + * Promise that resolves when migration is complete. + * Used to ensure getItem waits for migration before reading. + */ +let migrationPromise: Promise | null = null + +/** + * Migrates existing console data from localStorage to IndexedDB. + * Runs once on first load, then marks migration as complete. + */ +async function migrateFromLocalStorage(): Promise { + if (typeof window === 'undefined') return + + try { + const migrated = await get(MIGRATION_KEY) + if (migrated) return + + const localData = localStorage.getItem(STORE_KEY) + if (localData) { + await set(STORE_KEY, localData) + localStorage.removeItem(STORE_KEY) + logger.info('Migrated console store to IndexedDB') + } + + await set(MIGRATION_KEY, true) + } catch (error) { + logger.warn('Migration from localStorage failed', { error }) + } +} + +if (typeof window !== 'undefined') { + migrationPromise = migrateFromLocalStorage().finally(() => { + migrationPromise = null + }) +} + +export const indexedDBStorage: StateStorage = { + getItem: async (name: string): Promise => { + if (typeof window === 'undefined') return null + + // Ensure migration completes before reading + if (migrationPromise) { + await migrationPromise + } + + try { + const value = await get(name) + return value ?? null + } catch (error) { + logger.warn('IndexedDB read failed', { name, error }) + return null + } + }, + + setItem: async (name: string, value: string): Promise => { + if (typeof window === 'undefined') return + try { + await set(name, value) + } catch (error) { + logger.warn('IndexedDB write failed', { name, error }) + } + }, + + removeItem: async (name: string): Promise => { + if (typeof window === 'undefined') return + try { + await del(name) + } catch (error) { + logger.warn('IndexedDB delete failed', { name, error }) + } + }, +} diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index 3049faa92d..2052b51340 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -1,18 +1,22 @@ import { createLogger } from '@sim/logger' import { create } from 'zustand' -import { devtools, persist } from 'zustand/middleware' +import { createJSONStorage, devtools, persist } from 'zustand/middleware' import { redactApiKeys } from '@/lib/core/security/redaction' import type { NormalizedBlockOutput } from '@/executor/types' import { useExecutionStore } from '@/stores/execution' import { useNotificationStore } from '@/stores/notifications' import { useGeneralStore } from '@/stores/settings/general' +import { indexedDBStorage } from '@/stores/terminal/console/storage' import type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from '@/stores/terminal/console/types' const logger = createLogger('TerminalConsoleStore') /** - * Updates a NormalizedBlockOutput with new content + * Maximum number of console entries to keep per workflow. + * Keeps the stored data size reasonable and improves performance. */ +const MAX_ENTRIES_PER_WORKFLOW = 500 + const updateBlockOutput = ( existingOutput: NormalizedBlockOutput | undefined, contentUpdate: string @@ -23,9 +27,6 @@ const updateBlockOutput = ( } } -/** - * Checks if output represents a streaming object that should be skipped - */ const isStreamingOutput = (output: any): boolean => { if (typeof ReadableStream !== 'undefined' && output instanceof ReadableStream) { return true @@ -44,9 +45,6 @@ const isStreamingOutput = (output: any): boolean => { ) } -/** - * Checks if entry should be skipped to prevent duplicates - */ const shouldSkipEntry = (output: any): boolean => { if (typeof output !== 'object' || !output) { return false @@ -69,6 +67,9 @@ export const useTerminalConsoleStore = create()( (set, get) => ({ entries: [], isOpen: false, + _hasHydrated: false, + + setHasHydrated: (hasHydrated) => set({ _hasHydrated: hasHydrated }), addConsole: (entry: Omit) => { set((state) => { @@ -94,7 +95,15 @@ export const useTerminalConsoleStore = create()( timestamp: new Date().toISOString(), } - return { entries: [newEntry, ...state.entries] } + const newEntries = [newEntry, ...state.entries] + const workflowCounts = new Map() + const trimmedEntries = newEntries.filter((entry) => { + const count = workflowCounts.get(entry.workflowId) || 0 + if (count >= MAX_ENTRIES_PER_WORKFLOW) return false + workflowCounts.set(entry.workflowId, count + 1) + return true + }) + return { entries: trimmedEntries } }) const newEntry = get().entries[0] @@ -130,10 +139,6 @@ export const useTerminalConsoleStore = create()( return newEntry }, - /** - * Clears console entries for a specific workflow and clears the run path - * @param workflowId - The workflow ID to clear entries for - */ clearWorkflowConsole: (workflowId: string) => { set((state) => ({ entries: state.entries.filter((entry) => entry.workflowId !== workflowId), @@ -148,9 +153,6 @@ export const useTerminalConsoleStore = create()( return } - /** - * Formats a value for CSV export - */ const formatCSVValue = (value: any): string => { if (value === null || value === undefined) { return '' @@ -297,7 +299,35 @@ export const useTerminalConsoleStore = create()( }), { name: 'terminal-console-store', + storage: createJSONStorage(() => indexedDBStorage), + partialize: (state) => ({ + entries: state.entries, + isOpen: state.isOpen, + }), + onRehydrateStorage: () => (_state, error) => { + if (error) { + logger.error('Failed to rehydrate console store', { error }) + } + }, + merge: (persistedState, currentState) => { + const persisted = persistedState as Partial | undefined + return { + ...currentState, + entries: persisted?.entries ?? currentState.entries, + isOpen: persisted?.isOpen ?? currentState.isOpen, + } + }, } ) ) ) + +if (typeof window !== 'undefined') { + useTerminalConsoleStore.persist.onFinishHydration(() => { + useTerminalConsoleStore.setState({ _hasHydrated: true }) + }) + + if (useTerminalConsoleStore.persist.hasHydrated()) { + useTerminalConsoleStore.setState({ _hasHydrated: true }) + } +} diff --git a/apps/sim/stores/terminal/console/types.ts b/apps/sim/stores/terminal/console/types.ts index 416575fa38..f496c7356c 100644 --- a/apps/sim/stores/terminal/console/types.ts +++ b/apps/sim/stores/terminal/console/types.ts @@ -1,9 +1,6 @@ import type { NormalizedBlockOutput } from '@/executor/types' import type { SubflowType } from '@/stores/workflows/workflow/types' -/** - * Console entry for terminal logs - */ export interface ConsoleEntry { id: string timestamp: string @@ -25,9 +22,6 @@ export interface ConsoleEntry { iterationType?: SubflowType } -/** - * Console update payload for partial updates - */ export interface ConsoleUpdate { content?: string output?: Partial @@ -40,9 +34,6 @@ export interface ConsoleUpdate { input?: any } -/** - * Console store state and actions - */ export interface ConsoleStore { entries: ConsoleEntry[] isOpen: boolean @@ -52,4 +43,6 @@ export interface ConsoleStore { getWorkflowEntries: (workflowId: string) => ConsoleEntry[] toggleConsole: () => void updateConsole: (blockId: string, update: string | ConsoleUpdate, executionId?: string) => void + _hasHydrated: boolean + setHasHydrated: (hasHydrated: boolean) => void } diff --git a/bun.lock b/bun.lock index c7cee56919..cf6913db54 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "simstudio", @@ -133,6 +134,7 @@ "groq-sdk": "^0.15.0", "html-to-image": "1.11.13", "html-to-text": "^9.0.5", + "idb-keyval": "6.2.2", "imapflow": "1.2.4", "input-otp": "^1.4.2", "ioredis": "^5.6.0", @@ -2310,6 +2312,8 @@ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "idb-keyval": ["idb-keyval@6.2.2", "", {}, "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="], From bd7009e316a8b246bcdcb4b8a9f29128f1611ce8 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:48:21 -0800 Subject: [PATCH 14/36] fix(copilot): commands (#2811) --- .../components/mention-menu/mention-menu.tsx | 116 ++++------ .../components/slash-menu/slash-menu.tsx | 165 +++++-------- .../components/user-input/user-input.tsx | 218 ++++++------------ 3 files changed, 166 insertions(+), 333 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/mention-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/mention-menu.tsx index 5ba95d91fc..651e551e25 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/mention-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/mention-menu.tsx @@ -26,26 +26,14 @@ function formatTimestamp(iso: string): string { } } -/** - * Common text styling for loading and empty states - */ const STATE_TEXT_CLASSES = 'px-[8px] py-[8px] text-[12px] text-[var(--text-muted)]' -/** - * Loading state component for mention folders - */ const LoadingState = () =>
    Loading...
    -/** - * Empty state component for mention folders - */ const EmptyState = ({ message }: { message: string }) => (
    {message}
    ) -/** - * Aggregated item type for filtered results - */ interface AggregatedItem { id: string label: string @@ -78,14 +66,6 @@ interface MentionMenuProps { } } -/** - * MentionMenu component for mention menu dropdown. - * Handles rendering of mention options, submenus, and aggregated search results. - * Manages keyboard navigation and selection of mentions. - * - * @param props - Component props - * @returns Rendered mention menu - */ export function MentionMenu({ mentionMenu, mentionData, @@ -100,6 +80,7 @@ export function MentionMenu({ submenuActiveIndex, mentionActiveIndex, openSubmenuFor, + setOpenSubmenuFor, } = mentionMenu const { @@ -308,72 +289,55 @@ export function MentionMenu({ 'Docs', // 7 ] as const - // Get active folder based on navigation when not in submenu and no query const isInFolderNavigationMode = !openSubmenuFor && !showAggregatedView - // Compute caret viewport position via mirror technique for precise anchoring const textareaEl = mentionMenu.textareaRef.current if (!textareaEl) return null - const getCaretViewport = (textarea: HTMLTextAreaElement, caretPosition: number, text: string) => { - const textareaRect = textarea.getBoundingClientRect() - const style = window.getComputedStyle(textarea) - - const mirrorDiv = document.createElement('div') - mirrorDiv.style.position = 'absolute' - mirrorDiv.style.visibility = 'hidden' - mirrorDiv.style.whiteSpace = 'pre-wrap' - mirrorDiv.style.wordWrap = 'break-word' - mirrorDiv.style.font = style.font - mirrorDiv.style.padding = style.padding - mirrorDiv.style.border = style.border - mirrorDiv.style.width = style.width - mirrorDiv.style.lineHeight = style.lineHeight - mirrorDiv.style.boxSizing = style.boxSizing - mirrorDiv.style.letterSpacing = style.letterSpacing - mirrorDiv.style.textTransform = style.textTransform - mirrorDiv.style.textIndent = style.textIndent - mirrorDiv.style.textAlign = style.textAlign - - mirrorDiv.textContent = text.substring(0, caretPosition) - - const caretMarker = document.createElement('span') - caretMarker.style.display = 'inline-block' - caretMarker.style.width = '0px' - caretMarker.style.padding = '0' - caretMarker.style.border = '0' - mirrorDiv.appendChild(caretMarker) - - document.body.appendChild(mirrorDiv) - const markerRect = caretMarker.getBoundingClientRect() - const mirrorRect = mirrorDiv.getBoundingClientRect() - document.body.removeChild(mirrorDiv) - - const leftOffset = markerRect.left - mirrorRect.left - textarea.scrollLeft - const topOffset = markerRect.top - mirrorRect.top - textarea.scrollTop - - return { - left: textareaRect.left + leftOffset, - top: textareaRect.top + topOffset, - } - } - const caretPos = getCaretPos() - const caretViewport = getCaretViewport(textareaEl, caretPos, message) + const textareaRect = textareaEl.getBoundingClientRect() + const style = window.getComputedStyle(textareaEl) + + const mirrorDiv = document.createElement('div') + mirrorDiv.style.position = 'absolute' + mirrorDiv.style.visibility = 'hidden' + mirrorDiv.style.whiteSpace = 'pre-wrap' + mirrorDiv.style.wordWrap = 'break-word' + mirrorDiv.style.font = style.font + mirrorDiv.style.padding = style.padding + mirrorDiv.style.border = style.border + mirrorDiv.style.width = style.width + mirrorDiv.style.lineHeight = style.lineHeight + mirrorDiv.style.boxSizing = style.boxSizing + mirrorDiv.style.letterSpacing = style.letterSpacing + mirrorDiv.style.textTransform = style.textTransform + mirrorDiv.style.textIndent = style.textIndent + mirrorDiv.style.textAlign = style.textAlign + mirrorDiv.textContent = message.substring(0, caretPos) + + const caretMarker = document.createElement('span') + caretMarker.style.display = 'inline-block' + caretMarker.style.width = '0px' + caretMarker.style.padding = '0' + caretMarker.style.border = '0' + mirrorDiv.appendChild(caretMarker) + + document.body.appendChild(mirrorDiv) + const markerRect = caretMarker.getBoundingClientRect() + const mirrorRect = mirrorDiv.getBoundingClientRect() + document.body.removeChild(mirrorDiv) + + const caretViewport = { + left: textareaRect.left + (markerRect.left - mirrorRect.left) - textareaEl.scrollLeft, + top: textareaRect.top + (markerRect.top - mirrorRect.top) - textareaEl.scrollTop, + } - // Decide preferred side based on available space const margin = 8 - const spaceAbove = caretViewport.top - margin const spaceBelow = window.innerHeight - caretViewport.top - margin - const side: 'top' | 'bottom' = spaceBelow >= spaceAbove ? 'bottom' : 'top' + const side: 'top' | 'bottom' = spaceBelow >= caretViewport.top - margin ? 'bottom' : 'top' return ( - { - /* controlled by mentionMenu */ - }} - > + {}}>
    e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()} > - + setOpenSubmenuFor(null)} /> {openSubmenuFor ? ( // Submenu view - showing contents of a specific folder diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/slash-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/slash-menu.tsx index a50de3c1bd..0e6a79588c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/slash-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/slash-menu.tsx @@ -12,31 +12,19 @@ import { } from '@/components/emcn' import type { useMentionMenu } from '../../hooks/use-mention-menu' -/** - * Top-level slash command options - */ const TOP_LEVEL_COMMANDS = [ - { id: 'fast', label: 'fast' }, - { id: 'plan', label: 'plan' }, - { id: 'debug', label: 'debug' }, - { id: 'research', label: 'research' }, - { id: 'deploy', label: 'deploy' }, - { id: 'superagent', label: 'superagent' }, + { id: 'fast', label: 'Fast' }, + { id: 'research', label: 'Research' }, + { id: 'superagent', label: 'Actions' }, ] as const -/** - * Web submenu commands - */ const WEB_COMMANDS = [ - { id: 'search', label: 'search' }, - { id: 'read', label: 'read' }, - { id: 'scrape', label: 'scrape' }, - { id: 'crawl', label: 'crawl' }, + { id: 'search', label: 'Search' }, + { id: 'read', label: 'Read' }, + { id: 'scrape', label: 'Scrape' }, + { id: 'crawl', label: 'Crawl' }, ] as const -/** - * All command labels for filtering - */ const ALL_COMMANDS = [...TOP_LEVEL_COMMANDS, ...WEB_COMMANDS] interface SlashMenuProps { @@ -45,13 +33,6 @@ interface SlashMenuProps { onSelectCommand: (command: string) => void } -/** - * SlashMenu component for slash command dropdown. - * Shows command options when user types '/'. - * - * @param props - Component props - * @returns Rendered slash menu - */ export function SlashMenu({ mentionMenu, message, onSelectCommand }: SlashMenuProps) { const { mentionMenuRef, @@ -64,92 +45,71 @@ export function SlashMenu({ mentionMenu, message, onSelectCommand }: SlashMenuPr setOpenSubmenuFor, } = mentionMenu - /** - * Get the current query string after / - */ const currentQuery = useMemo(() => { const caretPos = getCaretPos() const active = getActiveSlashQueryAtPosition(caretPos, message) return active?.query.trim().toLowerCase() || '' }, [message, getCaretPos, getActiveSlashQueryAtPosition]) - /** - * Filter commands based on query (search across all commands when there's a query) - */ const filteredCommands = useMemo(() => { - if (!currentQuery) return null // Show folder view when no query - return ALL_COMMANDS.filter((cmd) => cmd.label.toLowerCase().includes(currentQuery)) + if (!currentQuery) return null + return ALL_COMMANDS.filter( + (cmd) => + cmd.id.toLowerCase().includes(currentQuery) || + cmd.label.toLowerCase().includes(currentQuery) + ) }, [currentQuery]) - // Show aggregated view when there's a query const showAggregatedView = currentQuery.length > 0 + const isInFolderNavigationMode = !openSubmenuFor && !showAggregatedView - // Compute caret viewport position via mirror technique for precise anchoring const textareaEl = mentionMenu.textareaRef.current if (!textareaEl) return null - const getCaretViewport = (textarea: HTMLTextAreaElement, caretPosition: number, text: string) => { - const textareaRect = textarea.getBoundingClientRect() - const style = window.getComputedStyle(textarea) - - const mirrorDiv = document.createElement('div') - mirrorDiv.style.position = 'absolute' - mirrorDiv.style.visibility = 'hidden' - mirrorDiv.style.whiteSpace = 'pre-wrap' - mirrorDiv.style.wordWrap = 'break-word' - mirrorDiv.style.font = style.font - mirrorDiv.style.padding = style.padding - mirrorDiv.style.border = style.border - mirrorDiv.style.width = style.width - mirrorDiv.style.lineHeight = style.lineHeight - mirrorDiv.style.boxSizing = style.boxSizing - mirrorDiv.style.letterSpacing = style.letterSpacing - mirrorDiv.style.textTransform = style.textTransform - mirrorDiv.style.textIndent = style.textIndent - mirrorDiv.style.textAlign = style.textAlign - - mirrorDiv.textContent = text.substring(0, caretPosition) - - const caretMarker = document.createElement('span') - caretMarker.style.display = 'inline-block' - caretMarker.style.width = '0px' - caretMarker.style.padding = '0' - caretMarker.style.border = '0' - mirrorDiv.appendChild(caretMarker) - - document.body.appendChild(mirrorDiv) - const markerRect = caretMarker.getBoundingClientRect() - const mirrorRect = mirrorDiv.getBoundingClientRect() - document.body.removeChild(mirrorDiv) - - const leftOffset = markerRect.left - mirrorRect.left - textarea.scrollLeft - const topOffset = markerRect.top - mirrorRect.top - textarea.scrollTop - - return { - left: textareaRect.left + leftOffset, - top: textareaRect.top + topOffset, - } - } - const caretPos = getCaretPos() - const caretViewport = getCaretViewport(textareaEl, caretPos, message) + const textareaRect = textareaEl.getBoundingClientRect() + const style = window.getComputedStyle(textareaEl) + + const mirrorDiv = document.createElement('div') + mirrorDiv.style.position = 'absolute' + mirrorDiv.style.visibility = 'hidden' + mirrorDiv.style.whiteSpace = 'pre-wrap' + mirrorDiv.style.wordWrap = 'break-word' + mirrorDiv.style.font = style.font + mirrorDiv.style.padding = style.padding + mirrorDiv.style.border = style.border + mirrorDiv.style.width = style.width + mirrorDiv.style.lineHeight = style.lineHeight + mirrorDiv.style.boxSizing = style.boxSizing + mirrorDiv.style.letterSpacing = style.letterSpacing + mirrorDiv.style.textTransform = style.textTransform + mirrorDiv.style.textIndent = style.textIndent + mirrorDiv.style.textAlign = style.textAlign + mirrorDiv.textContent = message.substring(0, caretPos) + + const caretMarker = document.createElement('span') + caretMarker.style.display = 'inline-block' + caretMarker.style.width = '0px' + caretMarker.style.padding = '0' + caretMarker.style.border = '0' + mirrorDiv.appendChild(caretMarker) + + document.body.appendChild(mirrorDiv) + const markerRect = caretMarker.getBoundingClientRect() + const mirrorRect = mirrorDiv.getBoundingClientRect() + document.body.removeChild(mirrorDiv) + + const caretViewport = { + left: textareaRect.left + (markerRect.left - mirrorRect.left) - textareaEl.scrollLeft, + top: textareaRect.top + (markerRect.top - mirrorRect.top) - textareaEl.scrollTop, + } - // Decide preferred side based on available space const margin = 8 - const spaceAbove = caretViewport.top - margin const spaceBelow = window.innerHeight - caretViewport.top - margin - const side: 'top' | 'bottom' = spaceBelow >= spaceAbove ? 'bottom' : 'top' - - // Check if we're in folder navigation mode (no query, not in submenu) - const isInFolderNavigationMode = !openSubmenuFor && !showAggregatedView + const side: 'top' | 'bottom' = spaceBelow >= caretViewport.top - margin ? 'bottom' : 'top' return ( - { - /* controlled externally */ - }} - > + {}}>
    e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()} > - + setOpenSubmenuFor(null)} /> {openSubmenuFor === 'Web' ? ( - // Web submenu view <> {WEB_COMMANDS.map((cmd, index) => ( onSelectCommand(cmd.label)} + onClick={() => onSelectCommand(cmd.id)} data-idx={index} active={index === submenuActiveIndex} > - {cmd.label} + {cmd.label} ))} ) : showAggregatedView ? ( - // Aggregated filtered view <> {filteredCommands && filteredCommands.length === 0 ? (
    @@ -202,26 +160,25 @@ export function SlashMenu({ mentionMenu, message, onSelectCommand }: SlashMenuPr filteredCommands?.map((cmd, index) => ( onSelectCommand(cmd.label)} + onClick={() => onSelectCommand(cmd.id)} data-idx={index} active={index === submenuActiveIndex} > - {cmd.label} + {cmd.label} )) )} ) : ( - // Folder navigation view <> {TOP_LEVEL_COMMANDS.map((cmd, index) => ( onSelectCommand(cmd.label)} + onClick={() => onSelectCommand(cmd.id)} data-idx={index} active={isInFolderNavigationMode && index === mentionActiveIndex} > - {cmd.label} + {cmd.label} ))} @@ -235,8 +192,8 @@ export function SlashMenu({ mentionMenu, message, onSelectCommand }: SlashMenuPr data-idx={TOP_LEVEL_COMMANDS.length} > {WEB_COMMANDS.map((cmd) => ( - onSelectCommand(cmd.label)}> - {cmd.label} + onSelectCommand(cmd.id)}> + {cmd.label} ))} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx index 2d16d1c6f8..665266bbbd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx @@ -40,6 +40,24 @@ import { useCopilotStore } from '@/stores/panel' const logger = createLogger('CopilotUserInput') +const TOP_LEVEL_COMMANDS = ['fast', 'research', 'superagent'] as const +const WEB_COMMANDS = ['search', 'read', 'scrape', 'crawl'] as const +const ALL_COMMANDS = [...TOP_LEVEL_COMMANDS, ...WEB_COMMANDS] + +const COMMAND_DISPLAY_LABELS: Record = { + superagent: 'Actions', +} + +/** + * Calculates the next index for circular navigation (wraps around at bounds) + */ +function getNextIndex(current: number, direction: 'up' | 'down', maxIndex: number): number { + if (direction === 'down') { + return current >= maxIndex ? 0 : current + 1 + } + return current <= 0 ? maxIndex : current - 1 +} + interface UserInputProps { onSubmit: ( message: string, @@ -110,7 +128,6 @@ const UserInput = forwardRef( }, ref ) => { - // Refs and external hooks const { data: session } = useSession() const params = useParams() const workspaceId = params.workspaceId as string @@ -122,19 +139,16 @@ const UserInput = forwardRef( selectedModelOverride !== undefined ? selectedModelOverride : copilotStore.selectedModel const setSelectedModel = onModelChangeOverride || copilotStore.setSelectedModel - // Internal state const [internalMessage, setInternalMessage] = useState('') const [isNearTop, setIsNearTop] = useState(false) const [containerRef, setContainerRef] = useState(null) const [inputContainerRef, setInputContainerRef] = useState(null) const [showSlashMenu, setShowSlashMenu] = useState(false) - // Controlled vs uncontrolled message state const message = controlledValue !== undefined ? controlledValue : internalMessage const setMessage = controlledValue !== undefined ? onControlledChange || (() => {}) : setInternalMessage - // Effective placeholder const effectivePlaceholder = placeholder || (mode === 'ask' @@ -143,11 +157,8 @@ const UserInput = forwardRef( ? 'Plan your workflow' : 'Plan, search, build anything') - // Custom hooks - order matters for ref sharing - // Context management (manages selectedContexts state) const contextManagement = useContextManagement({ message, initialContexts }) - // Mention menu const mentionMenu = useMentionMenu({ message, selectedContexts: contextManagement.selectedContexts, @@ -155,7 +166,6 @@ const UserInput = forwardRef( onMessageChange: setMessage, }) - // Mention token utilities const mentionTokensWithContext = useMentionTokens({ message, selectedContexts: contextManagement.selectedContexts, @@ -183,7 +193,6 @@ const UserInput = forwardRef( isLoading, }) - // Insert mention handlers const insertHandlers = useMentionInsertHandlers({ mentionMenu, workflowId: workflowId || null, @@ -191,14 +200,12 @@ const UserInput = forwardRef( onContextAdd: contextManagement.addContext, }) - // Keyboard navigation hook const mentionKeyboard = useMentionKeyboard({ mentionMenu, mentionData, insertHandlers, }) - // Expose focus method to parent useImperativeHandle( ref, () => ({ @@ -215,9 +222,6 @@ const UserInput = forwardRef( [mentionMenu.textareaRef] ) - // Note: textarea auto-resize is handled by the useTextareaAutoResize hook - - // Load workflows on mount if we have a workflowId useEffect(() => { if (workflowId) { void mentionData.ensureWorkflowsLoaded() @@ -225,7 +229,6 @@ const UserInput = forwardRef( // eslint-disable-next-line react-hooks/exhaustive-deps }, [workflowId]) - // Detect if input is near top of screen useEffect(() => { const checkPosition = () => { if (containerRef) { @@ -253,7 +256,6 @@ const UserInput = forwardRef( } }, [containerRef]) - // Also check position when mention menu opens useEffect(() => { if (mentionMenu.showMentionMenu && containerRef) { const rect = containerRef.getBoundingClientRect() @@ -261,7 +263,6 @@ const UserInput = forwardRef( } }, [mentionMenu.showMentionMenu, containerRef]) - // Preload mention data when query is active useEffect(() => { if (!mentionMenu.showMentionMenu || mentionMenu.openSubmenuFor) { return @@ -273,7 +274,6 @@ const UserInput = forwardRef( .toLowerCase() if (q && q.length > 0) { - // Prefetch all lists when there's any query for instant filtering void mentionData.ensurePastChatsLoaded() void mentionData.ensureWorkflowsLoaded() void mentionData.ensureWorkflowBlocksLoaded() @@ -282,15 +282,12 @@ const UserInput = forwardRef( void mentionData.ensureTemplatesLoaded() void mentionData.ensureLogsLoaded() - // Reset to first item when query changes mentionMenu.setSubmenuActiveIndex(0) requestAnimationFrame(() => mentionMenu.scrollActiveItemIntoView(0)) } - // Only depend on values that trigger data loading, not the entire objects // eslint-disable-next-line react-hooks/exhaustive-deps }, [mentionMenu.showMentionMenu, mentionMenu.openSubmenuFor, message]) - // When switching into a submenu, select the first item and scroll to it useEffect(() => { if (mentionMenu.openSubmenuFor) { mentionMenu.setSubmenuActiveIndex(0) @@ -299,12 +296,10 @@ const UserInput = forwardRef( // eslint-disable-next-line react-hooks/exhaustive-deps }, [mentionMenu.openSubmenuFor]) - // Handlers const handleSubmit = useCallback( async (overrideMessage?: string, options: { preserveInput?: boolean } = {}) => { const targetMessage = overrideMessage ?? message const trimmedMessage = targetMessage.trim() - // Allow submission even when isLoading - store will queue the message if (!trimmedMessage || disabled) return const failedUploads = fileAttachments.attachedFiles.filter((f) => !f.uploading && !f.key) @@ -377,17 +372,13 @@ const UserInput = forwardRef( const handleSlashCommandSelect = useCallback( (command: string) => { - // Capitalize the command for display - const capitalizedCommand = command.charAt(0).toUpperCase() + command.slice(1) - - // Replace the active slash query with the capitalized command - mentionMenu.replaceActiveSlashWith(capitalizedCommand) - - // Add as a context so it gets highlighted + const displayLabel = + COMMAND_DISPLAY_LABELS[command] || command.charAt(0).toUpperCase() + command.slice(1) + mentionMenu.replaceActiveSlashWith(displayLabel) contextManagement.addContext({ kind: 'slash_command', command, - label: capitalizedCommand, + label: displayLabel, }) setShowSlashMenu(false) @@ -398,7 +389,6 @@ const UserInput = forwardRef( const handleKeyDown = useCallback( (e: KeyboardEvent) => { - // Escape key handling if (e.key === 'Escape' && (mentionMenu.showMentionMenu || showSlashMenu)) { e.preventDefault() if (mentionMenu.openSubmenuFor) { @@ -411,65 +401,33 @@ const UserInput = forwardRef( return } - // Arrow navigation in slash menu if (showSlashMenu) { - const TOP_LEVEL_COMMANDS = ['fast', 'plan', 'debug', 'research', 'deploy', 'superagent'] - const WEB_COMMANDS = ['search', 'read', 'scrape', 'crawl'] - const ALL_COMMANDS = [...TOP_LEVEL_COMMANDS, ...WEB_COMMANDS] - const caretPos = mentionMenu.getCaretPos() const activeSlash = mentionMenu.getActiveSlashQueryAtPosition(caretPos, message) const query = activeSlash?.query.trim().toLowerCase() || '' const showAggregatedView = query.length > 0 + const direction = e.key === 'ArrowDown' ? 'down' : 'up' if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault() if (mentionMenu.openSubmenuFor === 'Web') { - // Navigate in Web submenu - const last = WEB_COMMANDS.length - 1 mentionMenu.setSubmenuActiveIndex((prev) => { - const next = - e.key === 'ArrowDown' - ? prev >= last - ? 0 - : prev + 1 - : prev <= 0 - ? last - : prev - 1 + const next = getNextIndex(prev, direction, WEB_COMMANDS.length - 1) requestAnimationFrame(() => mentionMenu.scrollActiveItemIntoView(next)) return next }) } else if (showAggregatedView) { - // Navigate in filtered view const filtered = ALL_COMMANDS.filter((cmd) => cmd.includes(query)) - const last = Math.max(0, filtered.length - 1) mentionMenu.setSubmenuActiveIndex((prev) => { if (filtered.length === 0) return 0 - const next = - e.key === 'ArrowDown' - ? prev >= last - ? 0 - : prev + 1 - : prev <= 0 - ? last - : prev - 1 + const next = getNextIndex(prev, direction, filtered.length - 1) requestAnimationFrame(() => mentionMenu.scrollActiveItemIntoView(next)) return next }) } else { - // Navigate in folder view (top-level + Web folder) - const totalItems = TOP_LEVEL_COMMANDS.length + 1 // +1 for Web folder - const last = totalItems - 1 mentionMenu.setMentionActiveIndex((prev) => { - const next = - e.key === 'ArrowDown' - ? prev >= last - ? 0 - : prev + 1 - : prev <= 0 - ? last - : prev - 1 + const next = getNextIndex(prev, direction, TOP_LEVEL_COMMANDS.length) requestAnimationFrame(() => mentionMenu.scrollActiveItemIntoView(next)) return next }) @@ -477,11 +435,9 @@ const UserInput = forwardRef( return } - // Arrow right to enter Web submenu if (e.key === 'ArrowRight') { e.preventDefault() if (!showAggregatedView && !mentionMenu.openSubmenuFor) { - // Check if Web folder is selected (it's after all top-level commands) if (mentionMenu.mentionActiveIndex === TOP_LEVEL_COMMANDS.length) { mentionMenu.setOpenSubmenuFor('Web') mentionMenu.setSubmenuActiveIndex(0) @@ -490,7 +446,6 @@ const UserInput = forwardRef( return } - // Arrow left to exit submenu if (e.key === 'ArrowLeft') { e.preventDefault() if (mentionMenu.openSubmenuFor) { @@ -500,44 +455,33 @@ const UserInput = forwardRef( } } - // Arrow navigation in mention menu if (mentionKeyboard.handleArrowNavigation(e)) return if (mentionKeyboard.handleArrowRight(e)) return if (mentionKeyboard.handleArrowLeft(e)) return - // Enter key handling if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault() if (showSlashMenu) { - const TOP_LEVEL_COMMANDS = ['fast', 'plan', 'debug', 'research', 'deploy', 'superagent'] - const WEB_COMMANDS = ['search', 'read', 'scrape', 'crawl'] - const ALL_COMMANDS = [...TOP_LEVEL_COMMANDS, ...WEB_COMMANDS] - const caretPos = mentionMenu.getCaretPos() const activeSlash = mentionMenu.getActiveSlashQueryAtPosition(caretPos, message) const query = activeSlash?.query.trim().toLowerCase() || '' const showAggregatedView = query.length > 0 if (mentionMenu.openSubmenuFor === 'Web') { - // Select from Web submenu const selectedCommand = WEB_COMMANDS[mentionMenu.submenuActiveIndex] || WEB_COMMANDS[0] handleSlashCommandSelect(selectedCommand) } else if (showAggregatedView) { - // Select from filtered view const filtered = ALL_COMMANDS.filter((cmd) => cmd.includes(query)) if (filtered.length > 0) { const selectedCommand = filtered[mentionMenu.submenuActiveIndex] || filtered[0] handleSlashCommandSelect(selectedCommand) } } else { - // Folder navigation view const selectedIndex = mentionMenu.mentionActiveIndex if (selectedIndex < TOP_LEVEL_COMMANDS.length) { - // Top-level command selected handleSlashCommandSelect(TOP_LEVEL_COMMANDS[selectedIndex]) } else if (selectedIndex === TOP_LEVEL_COMMANDS.length) { - // Web folder selected - open it mentionMenu.setOpenSubmenuFor('Web') mentionMenu.setSubmenuActiveIndex(0) } @@ -552,7 +496,6 @@ const UserInput = forwardRef( return } - // Handle mention token behavior (backspace, delete, arrow keys) when menu is closed if (!mentionMenu.showMentionMenu) { const textarea = mentionMenu.textareaRef.current const selStart = textarea?.selectionStart ?? 0 @@ -561,11 +504,8 @@ const UserInput = forwardRef( if (e.key === 'Backspace' || e.key === 'Delete') { if (selectionLength > 0) { - // Multi-character selection: Clean up contexts for any overlapping mentions - // but let the default behavior handle the actual text deletion mentionTokensWithContext.removeContextsInSelection(selStart, selEnd) } else { - // Single character delete - check if cursor is inside/at a mention token const ranges = mentionTokensWithContext.computeMentionRanges() const target = e.key === 'Backspace' @@ -604,7 +544,6 @@ const UserInput = forwardRef( } } - // Prevent typing inside token if (e.key.length === 1 || e.key === 'Space') { const blocked = selectionLength === 0 && !!mentionTokensWithContext.findRangeContaining(selStart) @@ -637,14 +576,10 @@ const UserInput = forwardRef( const newValue = e.target.value setMessage(newValue) - // Skip mention menu logic if mentions are disabled if (disableMentions) return const caret = e.target.selectionStart ?? newValue.length - - // Check for @ mention trigger const activeMention = mentionMenu.getActiveMentionQueryAtPosition(caret, newValue) - // Check for / slash command trigger const activeSlash = mentionMenu.getActiveSlashQueryAtPosition(caret, newValue) if (activeMention) { @@ -686,84 +621,66 @@ const UserInput = forwardRef( } }, [mentionMenu.textareaRef, mentionTokensWithContext]) - const handleOpenMentionMenuWithAt = useCallback(() => { - if (disabled || isLoading) return - const textarea = mentionMenu.textareaRef.current - if (!textarea) return - textarea.focus() - const pos = textarea.selectionStart ?? message.length - const needsSpaceBefore = pos > 0 && !/\s/.test(message.charAt(pos - 1)) - - const insertText = needsSpaceBefore ? ' @' : '@' - const start = textarea.selectionStart ?? message.length - const end = textarea.selectionEnd ?? message.length - const before = message.slice(0, start) - const after = message.slice(end) - const next = `${before}${insertText}${after}` - setMessage(next) - - setTimeout(() => { - const newPos = before.length + insertText.length - textarea.setSelectionRange(newPos, newPos) + const insertTriggerAndOpenMenu = useCallback( + (trigger: '@' | '/') => { + if (disabled || isLoading) return + const textarea = mentionMenu.textareaRef.current + if (!textarea) return + textarea.focus() - }, 0) + const start = textarea.selectionStart ?? message.length + const end = textarea.selectionEnd ?? message.length + const needsSpaceBefore = start > 0 && !/\s/.test(message.charAt(start - 1)) - mentionMenu.setShowMentionMenu(true) - mentionMenu.setOpenSubmenuFor(null) - mentionMenu.setMentionActiveIndex(0) - mentionMenu.setSubmenuActiveIndex(0) - }, [disabled, isLoading, mentionMenu, message, setMessage]) + const insertText = needsSpaceBefore ? ` ${trigger}` : trigger + const before = message.slice(0, start) + const after = message.slice(end) + setMessage(`${before}${insertText}${after}`) - const handleOpenSlashMenu = useCallback(() => { - if (disabled || isLoading) return - const textarea = mentionMenu.textareaRef.current - if (!textarea) return - textarea.focus() - const pos = textarea.selectionStart ?? message.length - const needsSpaceBefore = pos > 0 && !/\s/.test(message.charAt(pos - 1)) - - const insertText = needsSpaceBefore ? ' /' : '/' - const start = textarea.selectionStart ?? message.length - const end = textarea.selectionEnd ?? message.length - const before = message.slice(0, start) - const after = message.slice(end) - const next = `${before}${insertText}${after}` - setMessage(next) - - setTimeout(() => { - const newPos = before.length + insertText.length - textarea.setSelectionRange(newPos, newPos) - textarea.focus() - }, 0) + setTimeout(() => { + const newPos = before.length + insertText.length + textarea.setSelectionRange(newPos, newPos) + textarea.focus() + }, 0) - setShowSlashMenu(true) - mentionMenu.setSubmenuActiveIndex(0) - }, [disabled, isLoading, mentionMenu, message, setMessage]) + if (trigger === '@') { + mentionMenu.setShowMentionMenu(true) + mentionMenu.setOpenSubmenuFor(null) + mentionMenu.setMentionActiveIndex(0) + } else { + setShowSlashMenu(true) + } + mentionMenu.setSubmenuActiveIndex(0) + }, + [disabled, isLoading, mentionMenu, message, setMessage] + ) + + const handleOpenMentionMenuWithAt = useCallback( + () => insertTriggerAndOpenMenu('@'), + [insertTriggerAndOpenMenu] + ) + + const handleOpenSlashMenu = useCallback( + () => insertTriggerAndOpenMenu('/'), + [insertTriggerAndOpenMenu] + ) const canSubmit = message.trim().length > 0 && !disabled && !isLoading const showAbortButton = isLoading && onAbort - // Render overlay content with highlighted mentions const renderOverlayContent = useCallback(() => { const contexts = contextManagement.selectedContexts - // Handle empty message if (!message) { return {'\u00A0'} } - // If no contexts, render the message directly with proper newline handling if (contexts.length === 0) { - // Add a zero-width space at the end if message ends with newline - // This ensures the newline is rendered and height is calculated correctly const displayText = message.endsWith('\n') ? `${message}\u200B` : message return {displayText} } const elements: React.ReactNode[] = [] - const labels = contexts.map((c) => c.label).filter(Boolean) - - // Build ranges for all mentions to highlight them including spaces const ranges = mentionTokensWithContext.computeMentionRanges() if (ranges.length === 0) { @@ -775,14 +692,11 @@ const UserInput = forwardRef( for (let i = 0; i < ranges.length; i++) { const range = ranges[i] - // Add text before mention if (range.start > lastIndex) { const before = message.slice(lastIndex, range.start) elements.push({before}) } - // Add highlighted mention (including spaces) - // Use index + start + end to ensure unique keys even with duplicate contexts const mentionText = message.slice(range.start, range.end) elements.push( ( const tail = message.slice(lastIndex) if (tail) { - // Add a zero-width space at the end if tail ends with newline const displayTail = tail.endsWith('\n') ? `${tail}\u200B` : tail elements.push({displayTail}) } - // Ensure there's always something to render for height calculation return elements.length > 0 ? elements : {'\u00A0'} }, [message, contextManagement.selectedContexts, mentionTokensWithContext]) From d5bd97de329505067025498baaa70a9ec02385a4 Mon Sep 17 00:00:00 2001 From: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com> Date: Wed, 14 Jan 2026 00:25:54 -0800 Subject: [PATCH 15/36] feat(tinybird): added tinybird block (#2781) --- apps/docs/components/icons.tsx | 13 ++ apps/docs/components/ui/icon-mapping.ts | 3 + apps/docs/content/docs/en/tools/meta.json | 2 + apps/docs/content/docs/en/tools/tinybird.mdx | 70 +++++++ apps/sim/blocks/blocks/tinybird.ts | 207 +++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 13 ++ apps/sim/tools/registry.ts | 3 + apps/sim/tools/tinybird/events.ts | 128 ++++++++++++ apps/sim/tools/tinybird/index.ts | 5 + apps/sim/tools/tinybird/query.ts | 139 +++++++++++++ apps/sim/tools/tinybird/types.ts | 59 ++++++ 12 files changed, 644 insertions(+) create mode 100644 apps/docs/content/docs/en/tools/tinybird.mdx create mode 100644 apps/sim/blocks/blocks/tinybird.ts create mode 100644 apps/sim/tools/tinybird/events.ts create mode 100644 apps/sim/tools/tinybird/index.ts create mode 100644 apps/sim/tools/tinybird/query.ts create mode 100644 apps/sim/tools/tinybird/types.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 0143e517a5..f47d1b8792 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1897,6 +1897,19 @@ export function TelegramIcon(props: SVGProps) { ) } +export function TinybirdIcon(props: SVGProps) { + return ( + + + + + + + + + ) +} + export function ClayIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index fe03d578c1..549bbc6e70 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -107,6 +107,7 @@ import { SupabaseIcon, TavilyIcon, TelegramIcon, + TinybirdIcon, TranslateIcon, TrelloIcon, TTSIcon, @@ -230,6 +231,8 @@ export const blockTypeToIconMap: Record = { supabase: SupabaseIcon, tavily: TavilyIcon, telegram: TelegramIcon, + thinking: BrainIcon, + tinybird: TinybirdIcon, translate: TranslateIcon, trello: TrelloIcon, tts: TTSIcon, diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index ea445d4488..5d24380339 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -103,6 +103,8 @@ "supabase", "tavily", "telegram", + "thinking", + "tinybird", "translate", "trello", "tts", diff --git a/apps/docs/content/docs/en/tools/tinybird.mdx b/apps/docs/content/docs/en/tools/tinybird.mdx new file mode 100644 index 0000000000..9da20cce93 --- /dev/null +++ b/apps/docs/content/docs/en/tools/tinybird.mdx @@ -0,0 +1,70 @@ +--- +title: Tinybird +description: Send events and query data with Tinybird +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Interact with Tinybird using the Events API to stream JSON or NDJSON events, or use the Query API to execute SQL queries against Pipes and Data Sources. + + + +## Tools + +### `tinybird_events` + +Send events to a Tinybird Data Source using the Events API. Supports JSON and NDJSON formats with optional gzip compression. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co or https://api.us-east.tinybird.co\) | +| `datasource` | string | Yes | Name of the Tinybird Data Source to send events to | +| `data` | string | Yes | Data to send as NDJSON \(newline-delimited JSON\) or JSON string. Each event should be a valid JSON object. | +| `wait` | boolean | No | Wait for database acknowledgment before responding. Enables safer retries but introduces latency. Defaults to false. | +| `format` | string | No | Format of the events data: "ndjson" \(default\) or "json" | +| `compression` | string | No | Compression format: "none" \(default\) or "gzip" | +| `token` | string | Yes | Tinybird API Token with DATASOURCE:APPEND or DATASOURCE:CREATE scope | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `successful_rows` | number | Number of rows successfully ingested | +| `quarantined_rows` | number | Number of rows quarantined \(failed validation\) | + +### `tinybird_query` + +Execute SQL queries against Tinybird Pipes and Data Sources using the Query API. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co\) | +| `query` | string | Yes | SQL query to execute. Specify your desired output format \(e.g., FORMAT JSON, FORMAT CSV, FORMAT TSV\). JSON format provides structured data, while other formats return raw text. | +| `pipeline` | string | No | Optional pipe name. When provided, enables SELECT * FROM _ syntax | +| `token` | string | Yes | Tinybird API Token with PIPE:READ scope | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `data` | json | Query result data. For FORMAT JSON: array of objects. For other formats \(CSV, TSV, etc.\): raw text string. | +| `rows` | number | Number of rows returned \(only available with FORMAT JSON\) | +| `statistics` | json | Query execution statistics - elapsed time, rows read, bytes read \(only available with FORMAT JSON\) | + + + +## Notes + +- Category: `tools` +- Type: `tinybird` diff --git a/apps/sim/blocks/blocks/tinybird.ts b/apps/sim/blocks/blocks/tinybird.ts new file mode 100644 index 0000000000..436543de76 --- /dev/null +++ b/apps/sim/blocks/blocks/tinybird.ts @@ -0,0 +1,207 @@ +import { TinybirdIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { TinybirdResponse } from '@/tools/tinybird/types' + +export const TinybirdBlock: BlockConfig = { + type: 'tinybird', + name: 'Tinybird', + description: 'Send events and query data with Tinybird', + authMode: AuthMode.ApiKey, + longDescription: + 'Interact with Tinybird using the Events API to stream JSON or NDJSON events, or use the Query API to execute SQL queries against Pipes and Data Sources.', + docsLink: 'https://www.tinybird.co/docs/api-reference', + category: 'tools', + bgColor: '#2EF598', + icon: TinybirdIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Send Events', id: 'tinybird_events' }, + { label: 'Query', id: 'tinybird_query' }, + ], + value: () => 'tinybird_events', + }, + { + id: 'base_url', + title: 'Base URL', + type: 'short-input', + placeholder: 'https://api.tinybird.co', + required: true, + }, + { + id: 'token', + title: 'API Token', + type: 'short-input', + placeholder: 'Enter your Tinybird API token', + password: true, + required: true, + }, + // Send Events operation inputs + { + id: 'datasource', + title: 'Data Source', + type: 'short-input', + placeholder: 'my_events_datasource', + condition: { field: 'operation', value: 'tinybird_events' }, + required: true, + }, + { + id: 'data', + title: 'Data', + type: 'code', + placeholder: + '{"event": "click", "timestamp": "2024-01-01T12:00:00Z"}\n{"event": "view", "timestamp": "2024-01-01T12:00:01Z"}', + condition: { field: 'operation', value: 'tinybird_events' }, + required: true, + }, + { + id: 'format', + title: 'Format', + type: 'dropdown', + options: [ + { label: 'NDJSON (Newline-delimited JSON)', id: 'ndjson' }, + { label: 'JSON', id: 'json' }, + ], + value: () => 'ndjson', + condition: { field: 'operation', value: 'tinybird_events' }, + }, + { + id: 'compression', + title: 'Compression', + type: 'dropdown', + options: [ + { label: 'None', id: 'none' }, + { label: 'Gzip', id: 'gzip' }, + ], + value: () => 'none', + mode: 'advanced', + condition: { field: 'operation', value: 'tinybird_events' }, + }, + { + id: 'wait', + title: 'Wait for Acknowledgment', + type: 'switch', + value: () => 'false', + mode: 'advanced', + condition: { field: 'operation', value: 'tinybird_events' }, + }, + // Query operation inputs + { + id: 'query', + title: 'SQL Query', + type: 'code', + placeholder: 'SELECT * FROM my_pipe FORMAT JSON\nOR\nSELECT * FROM my_pipe FORMAT CSV', + condition: { field: 'operation', value: 'tinybird_query' }, + required: true, + }, + { + id: 'pipeline', + title: 'Pipeline Name', + type: 'short-input', + placeholder: 'my_pipe (optional)', + condition: { field: 'operation', value: 'tinybird_query' }, + }, + ], + tools: { + access: ['tinybird_events', 'tinybird_query'], + config: { + tool: (params) => params.operation || 'tinybird_events', + params: (params) => { + const operation = params.operation || 'tinybird_events' + const result: Record = { + base_url: params.base_url, + token: params.token, + } + + if (operation === 'tinybird_events') { + // Send Events operation + if (!params.datasource) { + throw new Error('Data Source is required for Send Events operation') + } + if (!params.data) { + throw new Error('Data is required for Send Events operation') + } + + result.datasource = params.datasource + result.data = params.data + result.format = params.format || 'ndjson' + result.compression = params.compression || 'none' + + // Convert wait from string to boolean + // Convert wait from string to boolean + if (params.wait !== undefined) { + const waitValue = + typeof params.wait === 'string' ? params.wait.toLowerCase() : params.wait + result.wait = waitValue === 'true' || waitValue === true + } + } else if (operation === 'tinybird_query') { + // Query operation + if (!params.query) { + throw new Error('SQL Query is required for Query operation') + } + + result.query = params.query + if (params.pipeline) { + result.pipeline = params.pipeline + } + } + + return result + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + base_url: { type: 'string', description: 'Tinybird API base URL' }, + // Send Events inputs + datasource: { + type: 'string', + description: 'Name of the Tinybird Data Source', + }, + data: { + type: 'string', + description: 'Data to send as JSON or NDJSON string', + }, + wait: { type: 'boolean', description: 'Wait for database acknowledgment' }, + format: { + type: 'string', + description: 'Format of the events (ndjson or json)', + }, + compression: { + type: 'string', + description: 'Compression format (none or gzip)', + }, + // Query inputs + query: { type: 'string', description: 'SQL query to execute' }, + pipeline: { type: 'string', description: 'Optional pipeline name' }, + // Common + token: { type: 'string', description: 'Tinybird API Token' }, + }, + outputs: { + // Send Events outputs + successful_rows: { + type: 'number', + description: 'Number of rows successfully ingested', + }, + quarantined_rows: { + type: 'number', + description: 'Number of rows quarantined (failed validation)', + }, + // Query outputs + data: { + type: 'json', + description: + 'Query result data. FORMAT JSON: array of objects. Other formats (CSV, TSV, etc.): raw text string.', + }, + rows: { type: 'number', description: 'Number of rows returned (only with FORMAT JSON)' }, + statistics: { + type: 'json', + description: + 'Query execution statistics - elapsed time, rows read, bytes read (only with FORMAT JSON)', + }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 865294d20d..f5504a7a16 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -121,6 +121,7 @@ import { SupabaseBlock } from '@/blocks/blocks/supabase' import { TavilyBlock } from '@/blocks/blocks/tavily' import { TelegramBlock } from '@/blocks/blocks/telegram' import { ThinkingBlock } from '@/blocks/blocks/thinking' +import { TinybirdBlock } from '@/blocks/blocks/tinybird' import { TranslateBlock } from '@/blocks/blocks/translate' import { TrelloBlock } from '@/blocks/blocks/trello' import { TtsBlock } from '@/blocks/blocks/tts' @@ -281,6 +282,7 @@ export const registry: Record = { tavily: TavilyBlock, telegram: TelegramBlock, thinking: ThinkingBlock, + tinybird: TinybirdBlock, translate: TranslateBlock, trello: TrelloBlock, twilio_sms: TwilioSMSBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 0143e517a5..f47d1b8792 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1897,6 +1897,19 @@ export function TelegramIcon(props: SVGProps) { ) } +export function TinybirdIcon(props: SVGProps) { + return ( + + + + + + + + + ) +} + export function ClayIcon(props: SVGProps) { return ( diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 7da777ff96..7d9a6816d0 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1381,6 +1381,7 @@ import { telegramSendVideoTool, } from '@/tools/telegram' import { thinkingTool } from '@/tools/thinking' +import { tinybirdEventsTool, tinybirdQueryTool } from '@/tools/tinybird' import { trelloAddCommentTool, trelloCreateCardTool, @@ -2237,6 +2238,8 @@ export const tools: Record = { apollo_email_accounts: apolloEmailAccountsTool, mistral_parser: mistralParserTool, thinking_tool: thinkingTool, + tinybird_events: tinybirdEventsTool, + tinybird_query: tinybirdQueryTool, stagehand_extract: stagehandExtractTool, stagehand_agent: stagehandAgentTool, mem0_add_memories: mem0AddMemoriesTool, diff --git a/apps/sim/tools/tinybird/events.ts b/apps/sim/tools/tinybird/events.ts new file mode 100644 index 0000000000..6de9137044 --- /dev/null +++ b/apps/sim/tools/tinybird/events.ts @@ -0,0 +1,128 @@ +import { gzipSync } from 'zlib' +import { createLogger } from '@sim/logger' +import type { TinybirdEventsParams, TinybirdEventsResponse } from '@/tools/tinybird/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('tinybird-events') + +export const eventsTool: ToolConfig = { + id: 'tinybird_events', + name: 'Tinybird Events', + description: + 'Send events to a Tinybird Data Source using the Events API. Supports JSON and NDJSON formats with optional gzip compression.', + version: '1.0.0', + errorExtractor: 'nested-error-object', + + params: { + base_url: { + type: 'string', + required: true, + visibility: 'user-only', + description: + 'Tinybird API base URL (e.g., https://api.tinybird.co or https://api.us-east.tinybird.co)', + }, + datasource: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the Tinybird Data Source to send events to', + }, + data: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Data to send as NDJSON (newline-delimited JSON) or JSON string. Each event should be a valid JSON object.', + }, + wait: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: + 'Wait for database acknowledgment before responding. Enables safer retries but introduces latency. Defaults to false.', + }, + format: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Format of the events data: "ndjson" (default) or "json"', + }, + compression: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Compression format: "none" (default) or "gzip"', + }, + token: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tinybird API Token with DATASOURCE:APPEND or DATASOURCE:CREATE scope', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.base_url.endsWith('/') ? params.base_url.slice(0, -1) : params.base_url + const url = new URL(`${baseUrl}/v0/events`) + url.searchParams.set('name', params.datasource) + if (params.wait) { + url.searchParams.set('wait', 'true') + } + return url.toString() + }, + method: 'POST', + headers: (params) => { + const headers: Record = { + Authorization: `Bearer ${params.token}`, + } + + if (params.compression === 'gzip') { + headers['Content-Encoding'] = 'gzip' + } + + if (params.format === 'json') { + headers['Content-Type'] = 'application/json' + } else { + headers['Content-Type'] = 'application/x-ndjson' + } + + return headers + }, + body: (params) => { + const data = params.data + if (params.compression === 'gzip') { + return gzipSync(Buffer.from(data, 'utf-8')) + } + return data + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + logger.info('Successfully sent events to Tinybird', { + successful: data.successful_rows, + quarantined: data.quarantined_rows, + }) + + return { + success: true, + output: { + successful_rows: data.successful_rows ?? 0, + quarantined_rows: data.quarantined_rows ?? 0, + }, + } + }, + + outputs: { + successful_rows: { + type: 'number', + description: 'Number of rows successfully ingested', + }, + quarantined_rows: { + type: 'number', + description: 'Number of rows quarantined (failed validation)', + }, + }, +} diff --git a/apps/sim/tools/tinybird/index.ts b/apps/sim/tools/tinybird/index.ts new file mode 100644 index 0000000000..5eb7e6af0b --- /dev/null +++ b/apps/sim/tools/tinybird/index.ts @@ -0,0 +1,5 @@ +import { eventsTool } from '@/tools/tinybird/events' +import { queryTool } from '@/tools/tinybird/query' + +export const tinybirdEventsTool = eventsTool +export const tinybirdQueryTool = queryTool diff --git a/apps/sim/tools/tinybird/query.ts b/apps/sim/tools/tinybird/query.ts new file mode 100644 index 0000000000..7046f9a67d --- /dev/null +++ b/apps/sim/tools/tinybird/query.ts @@ -0,0 +1,139 @@ +import { createLogger } from '@sim/logger' +import type { TinybirdQueryParams, TinybirdQueryResponse } from '@/tools/tinybird/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('tinybird-query') + +/** + * Tinybird Query Tool + * + * Executes SQL queries against Tinybird and returns results in the format specified in the query. + * - FORMAT JSON: Returns structured data with rows/statistics metadata + * - FORMAT CSV/TSV/etc: Returns raw text string + * + * The tool automatically detects the response format based on Content-Type headers. + */ +export const queryTool: ToolConfig = { + id: 'tinybird_query', + name: 'Tinybird Query', + description: 'Execute SQL queries against Tinybird Pipes and Data Sources using the Query API.', + version: '1.0.0', + errorExtractor: 'nested-error-object', + + params: { + base_url: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tinybird API base URL (e.g., https://api.tinybird.co)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'SQL query to execute. Specify your desired output format (e.g., FORMAT JSON, FORMAT CSV, FORMAT TSV). JSON format provides structured data, while other formats return raw text.', + }, + pipeline: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional pipe name. When provided, enables SELECT * FROM _ syntax', + }, + token: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tinybird API Token with PIPE:READ scope', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.base_url.endsWith('/') ? params.base_url.slice(0, -1) : params.base_url + return `${baseUrl}/v0/sql` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Bearer ${params.token}`, + }), + body: (params) => { + const searchParams = new URLSearchParams() + searchParams.set('q', params.query) + if (params.pipeline) { + searchParams.set('pipeline', params.pipeline) + } + return searchParams.toString() + }, + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + const contentType = response.headers.get('content-type') || '' + + // Check if response is JSON based on content-type or try parsing + const isJson = contentType.includes('application/json') || contentType.includes('text/json') + + if (isJson) { + try { + const data = JSON.parse(responseText) + logger.info('Successfully executed Tinybird query (JSON)', { + rows: data.rows, + elapsed: data.statistics?.elapsed, + }) + + return { + success: true, + output: { + data: data.data || [], + rows: data.rows || 0, + statistics: data.statistics + ? { + elapsed: data.statistics.elapsed, + rows_read: data.statistics.rows_read, + bytes_read: data.statistics.bytes_read, + } + : undefined, + }, + } + } catch (parseError) { + logger.error('Failed to parse JSON response', { + contentType, + parseError: parseError instanceof Error ? parseError.message : String(parseError), + }) + throw new Error( + `Invalid JSON response: ${parseError instanceof Error ? parseError.message : 'Parse error'}` + ) + } + } + + // For non-JSON formats (CSV, TSV, etc.), return as raw text + logger.info('Successfully executed Tinybird query (non-JSON)', { contentType }) + return { + success: true, + output: { + data: responseText, + rows: undefined, + statistics: undefined, + }, + } + }, + + outputs: { + data: { + type: 'json', + description: + 'Query result data. For FORMAT JSON: array of objects. For other formats (CSV, TSV, etc.): raw text string.', + }, + rows: { + type: 'number', + description: 'Number of rows returned (only available with FORMAT JSON)', + }, + statistics: { + type: 'json', + description: + 'Query execution statistics - elapsed time, rows read, bytes read (only available with FORMAT JSON)', + }, + }, +} diff --git a/apps/sim/tools/tinybird/types.ts b/apps/sim/tools/tinybird/types.ts new file mode 100644 index 0000000000..2e681bf411 --- /dev/null +++ b/apps/sim/tools/tinybird/types.ts @@ -0,0 +1,59 @@ +import type { ToolResponse } from '@/tools/types' + +/** + * Base parameters for Tinybird API tools + */ +export interface TinybirdBaseParams { + token: string +} + +/** + * Parameters for sending events to Tinybird + */ +export interface TinybirdEventsParams extends TinybirdBaseParams { + base_url: string + datasource: string + data: string + wait?: boolean + format?: 'ndjson' | 'json' + compression?: 'none' | 'gzip' +} + +/** + * Response from sending events to Tinybird + */ +export interface TinybirdEventsResponse extends ToolResponse { + output: { + successful_rows: number + quarantined_rows: number + } +} + +/** + * Parameters for querying Tinybird + */ +export interface TinybirdQueryParams extends TinybirdBaseParams { + base_url: string + query: string + pipeline?: string +} + +/** + * Response from querying Tinybird + */ +export interface TinybirdQueryResponse extends ToolResponse { + output: { + data: unknown[] | string + rows?: number + statistics?: { + elapsed: number + rows_read: number + bytes_read: number + } + } +} + +/** + * Union type for all possible Tinybird responses + */ +export type TinybirdResponse = TinybirdEventsResponse | TinybirdQueryResponse From a3535639f19601a043ad9e355e108f06dabeae3c Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 14 Jan 2026 11:04:53 -0800 Subject: [PATCH 16/36] fix(copilot): rewrote user input popover to optimize UX (#2814) * fix(copilot): rewrote user input popover to optimize UX * cleanup * make keyboard and moues share state * escape goes one level up on slash popover --- .../components/tool-call/tool-call.tsx | 1 - .../components/user-input/components/index.ts | 4 +- .../mention-menu/folder-content.tsx | 151 +++ .../components/mention-menu/mention-menu.tsx | 881 +++++------------- .../components/slash-menu/slash-menu.tsx | 267 +++--- .../components/user-input/constants.ts | 264 +++++- .../components/user-input/hooks/index.ts | 1 + .../user-input/hooks/use-caret-viewport.ts | 77 ++ .../hooks/use-context-management.ts | 85 +- .../user-input/hooks/use-mention-data.ts | 58 +- .../hooks/use-mention-insert-handlers.ts | 376 ++------ .../user-input/hooks/use-mention-keyboard.ts | 619 ++++-------- .../user-input/hooks/use-mention-menu.ts | 5 +- .../hooks/use-textarea-auto-resize.ts | 22 - .../components/user-input/user-input.tsx | 94 +- .../copilot/components/user-input/utils.ts | 149 +++ apps/sim/lib/core/utils/formatting.ts | 30 +- 17 files changed, 1330 insertions(+), 1754 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/folder-content.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-caret-viewport.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx index d646a179b1..83ff87eb90 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx @@ -8,7 +8,6 @@ import { Button, Code, getCodeEditorProps, highlight, languages } from '@/compon import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool' import { getClientTool } from '@/lib/copilot/tools/client/manager' import { getRegisteredTools } from '@/lib/copilot/tools/client/registry' -// Initialize all tool UI configs import '@/lib/copilot/tools/client/init-tool-configs' import { getSubagentLabels as getSubagentLabelsFromConfig, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts index bab808a85b..1d0da42d4c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts @@ -1,6 +1,6 @@ export { AttachedFilesDisplay } from './attached-files-display/attached-files-display' export { ContextPills } from './context-pills/context-pills' -export { MentionMenu } from './mention-menu/mention-menu' +export { type MentionFolderNav, MentionMenu } from './mention-menu/mention-menu' export { ModeSelector } from './mode-selector/mode-selector' export { ModelSelector } from './model-selector/model-selector' -export { SlashMenu } from './slash-menu/slash-menu' +export { type SlashFolderNav, SlashMenu } from './slash-menu/slash-menu' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/folder-content.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/folder-content.tsx new file mode 100644 index 0000000000..bb45e83d9b --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/folder-content.tsx @@ -0,0 +1,151 @@ +'use client' + +import type { ComponentType, ReactNode, SVGProps } from 'react' +import { PopoverItem } from '@/components/emcn' +import { formatCompactTimestamp } from '@/lib/core/utils/formatting' +import { + FOLDER_CONFIGS, + MENU_STATE_TEXT_CLASSES, + type MentionFolderId, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants' + +const ICON_CONTAINER = + 'relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]' + +export function BlockIcon({ + bgColor, + Icon, +}: { + bgColor?: string + Icon?: ComponentType> +}) { + return ( +
    + {Icon && } +
    + ) +} + +export function WorkflowColorDot({ color }: { color?: string }) { + return
    +} + +interface FolderContentProps { + /** Folder ID to render content for */ + folderId: MentionFolderId + /** Items to render (already filtered) */ + items: any[] + /** Whether data is loading */ + isLoading: boolean + /** Current search query (for determining empty vs no-match message) */ + currentQuery: string + /** Currently active item index (for keyboard navigation) */ + activeIndex: number + /** Callback when an item is clicked */ + onItemClick: (item: any) => void +} + +export function renderItemIcon(folderId: MentionFolderId, item: any): ReactNode { + switch (folderId) { + case 'workflows': + return + case 'blocks': + case 'workflow-blocks': + return + default: + return null + } +} + +function renderItemSuffix(folderId: MentionFolderId, item: any): ReactNode { + switch (folderId) { + case 'templates': + return {item.stars} + case 'logs': + return ( + <> + · + + {formatCompactTimestamp(item.createdAt)} + + · + {(item.trigger || 'manual').toLowerCase()} + + ) + default: + return null + } +} + +export function FolderContent({ + folderId, + items, + isLoading, + currentQuery, + activeIndex, + onItemClick, +}: FolderContentProps) { + const config = FOLDER_CONFIGS[folderId] + + if (isLoading) { + return
    Loading...
    + } + + if (items.length === 0) { + return ( +
    + {currentQuery ? config.noMatchMessage : config.emptyMessage} +
    + ) + } + + return ( + <> + {items.map((item, index) => ( + onItemClick(item)} + data-idx={index} + active={index === activeIndex} + > + {renderItemIcon(folderId, item)} + + {config.getLabel(item)} + + {renderItemSuffix(folderId, item)} + + ))} + + ) +} + +export function FolderPreviewContent({ + folderId, + items, + isLoading, + onItemClick, +}: Omit) { + const config = FOLDER_CONFIGS[folderId] + + if (isLoading) { + return
    Loading...
    + } + + if (items.length === 0) { + return
    {config.emptyMessage}
    + } + + return ( + <> + {items.map((item) => ( + onItemClick(item)}> + {renderItemIcon(folderId, item)} + + {config.getLabel(item)} + + {renderItemSuffix(folderId, item)} + + ))} + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/mention-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/mention-menu.tsx index 651e551e25..89dbafa4b2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/mention-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/mention-menu.tsx @@ -1,6 +1,6 @@ 'use client' -import { useMemo } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import { Popover, PopoverAnchor, @@ -9,47 +9,43 @@ import { PopoverFolder, PopoverItem, PopoverScrollArea, + usePopoverContext, } from '@/components/emcn' -import type { useMentionData } from '../../hooks/use-mention-data' -import type { useMentionMenu } from '../../hooks/use-mention-menu' - -function formatTimestamp(iso: string): string { - try { - const d = new Date(iso) - const mm = String(d.getMonth() + 1).padStart(2, '0') - const dd = String(d.getDate()).padStart(2, '0') - const hh = String(d.getHours()).padStart(2, '0') - const min = String(d.getMinutes()).padStart(2, '0') - return `${mm}-${dd} ${hh}:${min}` - } catch { - return iso - } -} - -const STATE_TEXT_CLASSES = 'px-[8px] py-[8px] text-[12px] text-[var(--text-muted)]' - -const LoadingState = () =>
    Loading...
    - -const EmptyState = ({ message }: { message: string }) => ( -
    {message}
    -) +import { formatCompactTimestamp } from '@/lib/core/utils/formatting' +import { + FOLDER_CONFIGS, + FOLDER_ORDER, + MENU_STATE_TEXT_CLASSES, + type MentionCategory, + type MentionFolderId, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants' +import { + useCaretViewport, + type useMentionData, + type useMentionMenu, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks' +import { + getFolderData as getFolderDataUtil, + getFolderEnsureLoaded as getFolderEnsureLoadedUtil, + getFolderLoading as getFolderLoadingUtil, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils' +import { FolderContent, FolderPreviewContent, renderItemIcon } from './folder-content' interface AggregatedItem { id: string label: string - category: - | 'chats' - | 'workflows' - | 'knowledge' - | 'blocks' - | 'workflow-blocks' - | 'templates' - | 'logs' - | 'docs' + category: MentionCategory data: any icon?: React.ReactNode } +export interface MentionFolderNav { + isInFolder: boolean + currentFolder: string | null + openFolder: (id: string, title: string) => void + closeFolder: () => void +} + interface MentionMenuProps { mentionMenu: ReturnType mentionData: ReturnType @@ -64,170 +60,124 @@ interface MentionMenuProps { insertLogMention: (log: any) => void insertDocsMention: () => void } + onFolderNavChange?: (nav: MentionFolderNav) => void } -export function MentionMenu({ +type InsertHandlerMap = Record void> + +function MentionMenuContent({ mentionMenu, mentionData, message, insertHandlers, + onFolderNavChange, }: MentionMenuProps) { + const { currentFolder, openFolder, closeFolder } = usePopoverContext() + const { - mentionMenuRef, menuListRef, getActiveMentionQueryAtPosition, getCaretPos, submenuActiveIndex, mentionActiveIndex, - openSubmenuFor, - setOpenSubmenuFor, + setSubmenuActiveIndex, } = mentionMenu - const { - insertPastChatMention, - insertWorkflowMention, - insertKnowledgeMention, - insertBlockMention, - insertWorkflowBlockMention, - insertTemplateMention, - insertLogMention, - insertDocsMention, - } = insertHandlers - - /** - * Get the current query string after @ - */ const currentQuery = useMemo(() => { const caretPos = getCaretPos() const active = getActiveMentionQueryAtPosition(caretPos, message) return active?.query.trim().toLowerCase() || '' }, [message, getCaretPos, getActiveMentionQueryAtPosition]) - /** - * Collect and filter all available items based on query - */ - const filteredAggregatedItems = useMemo(() => { - if (!currentQuery) return [] - - const items: AggregatedItem[] = [] + const isInFolder = currentFolder !== null + const showAggregatedView = currentQuery.length > 0 + const isInFolderNavigationMode = !isInFolder && !showAggregatedView + + useEffect(() => { + setSubmenuActiveIndex(0) + }, [isInFolder, setSubmenuActiveIndex]) + + useEffect(() => { + if (onFolderNavChange) { + onFolderNavChange({ + isInFolder, + currentFolder, + openFolder, + closeFolder, + }) + } + }, [onFolderNavChange, isInFolder, currentFolder, openFolder, closeFolder]) + + const insertHandlerMap = useMemo( + (): InsertHandlerMap => ({ + chats: insertHandlers.insertPastChatMention, + workflows: insertHandlers.insertWorkflowMention, + knowledge: insertHandlers.insertKnowledgeMention, + blocks: insertHandlers.insertBlockMention, + 'workflow-blocks': insertHandlers.insertWorkflowBlockMention, + templates: insertHandlers.insertTemplateMention, + logs: insertHandlers.insertLogMention, + }), + [insertHandlers] + ) - // Chats - mentionData.pastChats.forEach((chat) => { - const label = chat.title || 'New Chat' - if (label.toLowerCase().includes(currentQuery)) { - items.push({ - id: `chat-${chat.id}`, - label, - category: 'chats', - data: chat, - }) - } - }) + const getFolderData = useCallback( + (folderId: MentionFolderId) => getFolderDataUtil(mentionData, folderId), + [mentionData] + ) - // Workflows - mentionData.workflows.forEach((wf) => { - const label = wf.name || 'Untitled Workflow' - if (label.toLowerCase().includes(currentQuery)) { - items.push({ - id: `workflow-${wf.id}`, - label, - category: 'workflows', - data: wf, - icon: ( -
    - ), - }) - } - }) + const getFolderLoading = useCallback( + (folderId: MentionFolderId) => getFolderLoadingUtil(mentionData, folderId), + [mentionData] + ) - // Knowledge bases - mentionData.knowledgeBases.forEach((kb) => { - const label = kb.name || 'Untitled' - if (label.toLowerCase().includes(currentQuery)) { - items.push({ - id: `knowledge-${kb.id}`, - label, - category: 'knowledge', - data: kb, - }) - } - }) + const getEnsureLoaded = useCallback( + (folderId: MentionFolderId) => getFolderEnsureLoadedUtil(mentionData, folderId), + [mentionData] + ) - // Blocks - mentionData.blocksList.forEach((blk) => { - const label = blk.name || blk.id - if (label.toLowerCase().includes(currentQuery)) { - const Icon = blk.iconComponent - items.push({ - id: `block-${blk.id}`, - label, - category: 'blocks', - data: blk, - icon: ( -
    - {Icon && } -
    - ), - }) - } - }) + const filterFolderItems = useCallback( + (folderId: MentionFolderId, query: string): any[] => { + const config = FOLDER_CONFIGS[folderId] + const items = getFolderData(folderId) + if (!query) return items + const q = query.toLowerCase() + return items.filter((item) => config.filterFn(item, q)) + }, + [getFolderData] + ) - // Workflow blocks - mentionData.workflowBlocks.forEach((blk) => { - const label = blk.name || blk.id - if (label.toLowerCase().includes(currentQuery)) { - const Icon = blk.iconComponent - items.push({ - id: `workflow-block-${blk.id}`, - label, - category: 'workflow-blocks', - data: blk, - icon: ( -
    - {Icon && } -
    - ), - }) - } - }) + const getFilteredFolderItems = useCallback( + (folderId: MentionFolderId): any[] => { + return isInFolder ? filterFolderItems(folderId, currentQuery) : getFolderData(folderId) + }, + [isInFolder, currentQuery, filterFolderItems, getFolderData] + ) - // Templates - mentionData.templatesList.forEach((tpl) => { - const label = tpl.name - if (label.toLowerCase().includes(currentQuery)) { - items.push({ - id: `template-${tpl.id}`, - label, - category: 'templates', - data: tpl, - }) - } - }) + const filteredAggregatedItems = useMemo(() => { + if (!currentQuery) return [] - // Logs - mentionData.logsList.forEach((log) => { - const label = log.workflowName - if (label.toLowerCase().includes(currentQuery)) { - items.push({ - id: `log-${log.id}`, - label, - category: 'logs', - data: log, - }) - } - }) + const items: AggregatedItem[] = [] + const q = currentQuery.toLowerCase() + + for (const folderId of FOLDER_ORDER) { + const config = FOLDER_CONFIGS[folderId] + const folderData = getFolderData(folderId) + + folderData.forEach((item) => { + if (config.filterFn(item, q)) { + items.push({ + id: `${folderId}-${config.getId(item)}`, + label: config.getLabel(item), + category: folderId as MentionCategory, + data: item, + icon: renderItemIcon(folderId, item), + }) + } + }) + } - // Docs - if ('docs'.includes(currentQuery)) { + if ('docs'.includes(q)) { items.push({ id: 'docs', label: 'Docs', @@ -237,107 +187,114 @@ export function MentionMenu({ } return items - }, [currentQuery, mentionData]) - - /** - * Handle click on aggregated item - */ - const handleAggregatedItemClick = (item: AggregatedItem) => { - switch (item.category) { - case 'chats': - insertPastChatMention(item.data) - break - case 'workflows': - insertWorkflowMention(item.data) - break - case 'knowledge': - insertKnowledgeMention(item.data) - break - case 'blocks': - insertBlockMention(item.data) - break - case 'workflow-blocks': - insertWorkflowBlockMention(item.data) - break - case 'templates': - insertTemplateMention(item.data) - break - case 'logs': - insertLogMention(item.data) - break - case 'docs': - insertDocsMention() - break - } - } + }, [currentQuery, getFolderData]) - // Open state derived directly from mention menu - const open = !!mentionMenu.showMentionMenu - - // Show filtered aggregated view when there's a query - const showAggregatedView = currentQuery.length > 0 - - // Folder order for keyboard navigation - matches render order - const FOLDER_ORDER = [ - 'Chats', // 0 - 'Workflows', // 1 - 'Knowledge', // 2 - 'Blocks', // 3 - 'Workflow Blocks', // 4 - 'Templates', // 5 - 'Logs', // 6 - 'Docs', // 7 - ] as const + const handleAggregatedItemClick = useCallback( + (item: AggregatedItem) => { + if (item.category === 'docs') { + insertHandlers.insertDocsMention() + return + } + const handler = insertHandlerMap[item.category as MentionFolderId] + if (handler) { + handler(item.data) + } + }, + [insertHandlerMap, insertHandlers] + ) - const isInFolderNavigationMode = !openSubmenuFor && !showAggregatedView + return ( + + {isInFolder ? ( + + ) : showAggregatedView ? ( + <> + {filteredAggregatedItems.length === 0 ? ( +
    No results found
    + ) : ( + filteredAggregatedItems.map((item, index) => ( + handleAggregatedItemClick(item)} + data-idx={index} + active={index === submenuActiveIndex} + > + {item.icon} + {item.label} + {item.category === 'logs' && ( + <> + · + + {formatCompactTimestamp(item.data.createdAt)} + + + )} + + )) + )} + + ) : ( + <> + {FOLDER_ORDER.map((folderId, folderIndex) => { + const config = FOLDER_CONFIGS[folderId] + const ensureLoaded = getEnsureLoaded(folderId) + + return ( + ensureLoaded?.()} + active={isInFolderNavigationMode && mentionActiveIndex === folderIndex} + data-idx={folderIndex} + > + + + ) + })} + + insertHandlers.insertDocsMention()} + active={isInFolderNavigationMode && mentionActiveIndex === FOLDER_ORDER.length} + data-idx={FOLDER_ORDER.length} + > + Docs + + + )} +
    + ) +} - const textareaEl = mentionMenu.textareaRef.current - if (!textareaEl) return null +export function MentionMenu({ + mentionMenu, + mentionData, + message, + insertHandlers, + onFolderNavChange, +}: MentionMenuProps) { + const { mentionMenuRef, textareaRef, getCaretPos } = mentionMenu const caretPos = getCaretPos() - const textareaRect = textareaEl.getBoundingClientRect() - const style = window.getComputedStyle(textareaEl) - - const mirrorDiv = document.createElement('div') - mirrorDiv.style.position = 'absolute' - mirrorDiv.style.visibility = 'hidden' - mirrorDiv.style.whiteSpace = 'pre-wrap' - mirrorDiv.style.wordWrap = 'break-word' - mirrorDiv.style.font = style.font - mirrorDiv.style.padding = style.padding - mirrorDiv.style.border = style.border - mirrorDiv.style.width = style.width - mirrorDiv.style.lineHeight = style.lineHeight - mirrorDiv.style.boxSizing = style.boxSizing - mirrorDiv.style.letterSpacing = style.letterSpacing - mirrorDiv.style.textTransform = style.textTransform - mirrorDiv.style.textIndent = style.textIndent - mirrorDiv.style.textAlign = style.textAlign - mirrorDiv.textContent = message.substring(0, caretPos) + const { caretViewport, side } = useCaretViewport({ textareaRef, message, caretPos }) - const caretMarker = document.createElement('span') - caretMarker.style.display = 'inline-block' - caretMarker.style.width = '0px' - caretMarker.style.padding = '0' - caretMarker.style.border = '0' - mirrorDiv.appendChild(caretMarker) - - document.body.appendChild(mirrorDiv) - const markerRect = caretMarker.getBoundingClientRect() - const mirrorRect = mirrorDiv.getBoundingClientRect() - document.body.removeChild(mirrorDiv) - - const caretViewport = { - left: textareaRect.left + (markerRect.left - mirrorRect.left) - textareaEl.scrollLeft, - top: textareaRect.top + (markerRect.top - mirrorRect.top) - textareaEl.scrollTop, - } - - const margin = 8 - const spaceBelow = window.innerHeight - caretViewport.top - margin - const side: 'top' | 'bottom' = spaceBelow >= caretViewport.top - margin ? 'bottom' : 'top' + if (!caretViewport) return null return ( - {}}> + {}}>
    e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()} + onMouseDown={(e) => e.preventDefault()} > - setOpenSubmenuFor(null)} /> - - {openSubmenuFor ? ( - // Submenu view - showing contents of a specific folder - <> - {openSubmenuFor === 'Chats' && ( - <> - {mentionData.isLoadingPastChats ? ( - - ) : mentionData.pastChats.length === 0 ? ( - - ) : ( - mentionData.pastChats.map((chat, index) => ( - insertPastChatMention(chat)} - data-idx={index} - active={index === submenuActiveIndex} - > - {chat.title || 'New Chat'} - - )) - )} - - )} - {openSubmenuFor === 'Workflows' && ( - <> - {mentionData.isLoadingWorkflows ? ( - - ) : mentionData.workflows.length === 0 ? ( - - ) : ( - mentionData.workflows.map((wf, index) => ( - insertWorkflowMention(wf)} - data-idx={index} - active={index === submenuActiveIndex} - > -
    - {wf.name || 'Untitled Workflow'} - - )) - )} - - )} - {openSubmenuFor === 'Knowledge' && ( - <> - {mentionData.isLoadingKnowledge ? ( - - ) : mentionData.knowledgeBases.length === 0 ? ( - - ) : ( - mentionData.knowledgeBases.map((kb, index) => ( - insertKnowledgeMention(kb)} - data-idx={index} - active={index === submenuActiveIndex} - > - {kb.name || 'Untitled'} - - )) - )} - - )} - {openSubmenuFor === 'Blocks' && ( - <> - {mentionData.isLoadingBlocks ? ( - - ) : mentionData.blocksList.length === 0 ? ( - - ) : ( - mentionData.blocksList.map((blk, index) => { - const Icon = blk.iconComponent - return ( - insertBlockMention(blk)} - data-idx={index} - active={index === submenuActiveIndex} - > -
    - {Icon && } -
    - {blk.name || blk.id} -
    - ) - }) - )} - - )} - {openSubmenuFor === 'Workflow Blocks' && ( - <> - {mentionData.isLoadingWorkflowBlocks ? ( - - ) : mentionData.workflowBlocks.length === 0 ? ( - - ) : ( - mentionData.workflowBlocks.map((blk, index) => { - const Icon = blk.iconComponent - return ( - insertWorkflowBlockMention(blk)} - data-idx={index} - active={index === submenuActiveIndex} - > -
    - {Icon && } -
    - {blk.name || blk.id} -
    - ) - }) - )} - - )} - {openSubmenuFor === 'Templates' && ( - <> - {mentionData.isLoadingTemplates ? ( - - ) : mentionData.templatesList.length === 0 ? ( - - ) : ( - mentionData.templatesList.map((tpl, index) => ( - insertTemplateMention(tpl)} - data-idx={index} - active={index === submenuActiveIndex} - > - {tpl.name} - {tpl.stars} - - )) - )} - - )} - {openSubmenuFor === 'Logs' && ( - <> - {mentionData.isLoadingLogs ? ( - - ) : mentionData.logsList.length === 0 ? ( - - ) : ( - mentionData.logsList.map((log, index) => ( - insertLogMention(log)} - data-idx={index} - active={index === submenuActiveIndex} - > - {log.workflowName} - · - - {formatTimestamp(log.createdAt)} - - · - - {(log.trigger || 'manual').toLowerCase()} - - - )) - )} - - )} - - ) : showAggregatedView ? ( - // Aggregated filtered view - <> - {filteredAggregatedItems.length === 0 ? ( - - ) : ( - filteredAggregatedItems.map((item, index) => ( - handleAggregatedItemClick(item)} - data-idx={index} - active={index === submenuActiveIndex} - > - {item.icon} - {item.label} - {item.category === 'logs' && ( - <> - · - - {formatTimestamp(item.data.createdAt)} - - - )} - - )) - )} - - ) : ( - // Folder navigation view - <> - mentionData.ensurePastChatsLoaded()} - active={isInFolderNavigationMode && mentionActiveIndex === 0} - data-idx={0} - > - {mentionData.isLoadingPastChats ? ( - - ) : mentionData.pastChats.length === 0 ? ( - - ) : ( - mentionData.pastChats.map((chat) => ( - insertPastChatMention(chat)}> - {chat.title || 'New Chat'} - - )) - )} - - - mentionData.ensureWorkflowsLoaded()} - active={isInFolderNavigationMode && mentionActiveIndex === 1} - data-idx={1} - > - {mentionData.isLoadingWorkflows ? ( - - ) : mentionData.workflows.length === 0 ? ( - - ) : ( - mentionData.workflows.map((wf) => ( - insertWorkflowMention(wf)}> -
    - {wf.name || 'Untitled Workflow'} - - )) - )} - - - mentionData.ensureKnowledgeLoaded()} - active={isInFolderNavigationMode && mentionActiveIndex === 2} - data-idx={2} - > - {mentionData.isLoadingKnowledge ? ( - - ) : mentionData.knowledgeBases.length === 0 ? ( - - ) : ( - mentionData.knowledgeBases.map((kb) => ( - insertKnowledgeMention(kb)}> - {kb.name || 'Untitled'} - - )) - )} - - - mentionData.ensureBlocksLoaded()} - active={isInFolderNavigationMode && mentionActiveIndex === 3} - data-idx={3} - > - {mentionData.isLoadingBlocks ? ( - - ) : mentionData.blocksList.length === 0 ? ( - - ) : ( - mentionData.blocksList.map((blk) => { - const Icon = blk.iconComponent - return ( - insertBlockMention(blk)}> -
    - {Icon && } -
    - {blk.name || blk.id} -
    - ) - }) - )} -
    - - mentionData.ensureWorkflowBlocksLoaded()} - active={isInFolderNavigationMode && mentionActiveIndex === 4} - data-idx={4} - > - {mentionData.isLoadingWorkflowBlocks ? ( - - ) : mentionData.workflowBlocks.length === 0 ? ( - - ) : ( - mentionData.workflowBlocks.map((blk) => { - const Icon = blk.iconComponent - return ( - insertWorkflowBlockMention(blk)}> -
    - {Icon && } -
    - {blk.name || blk.id} -
    - ) - }) - )} -
    - - mentionData.ensureTemplatesLoaded()} - active={isInFolderNavigationMode && mentionActiveIndex === 5} - data-idx={5} - > - {mentionData.isLoadingTemplates ? ( - - ) : mentionData.templatesList.length === 0 ? ( - - ) : ( - mentionData.templatesList.map((tpl) => ( - insertTemplateMention(tpl)}> - {tpl.name} - {tpl.stars} - - )) - )} - - - mentionData.ensureLogsLoaded()} - active={isInFolderNavigationMode && mentionActiveIndex === 6} - data-idx={6} - > - {mentionData.isLoadingLogs ? ( - - ) : mentionData.logsList.length === 0 ? ( - - ) : ( - mentionData.logsList.map((log) => ( - insertLogMention(log)}> - {log.workflowName} - · - - {formatTimestamp(log.createdAt)} - - · - - {(log.trigger || 'manual').toLowerCase()} - - - )) - )} - - - insertDocsMention()} - active={isInFolderNavigationMode && mentionActiveIndex === 7} - data-idx={7} - > - Docs - - - )} - + + ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/slash-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/slash-menu.tsx index 0e6a79588c..a51067057f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/slash-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/slash-menu.tsx @@ -1,6 +1,6 @@ 'use client' -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import { Popover, PopoverAnchor, @@ -9,51 +9,57 @@ import { PopoverFolder, PopoverItem, PopoverScrollArea, + usePopoverContext, } from '@/components/emcn' +import { + ALL_SLASH_COMMANDS, + MENU_STATE_TEXT_CLASSES, + TOP_LEVEL_COMMANDS, + WEB_COMMANDS, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants' +import { useCaretViewport } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks' import type { useMentionMenu } from '../../hooks/use-mention-menu' -const TOP_LEVEL_COMMANDS = [ - { id: 'fast', label: 'Fast' }, - { id: 'research', label: 'Research' }, - { id: 'superagent', label: 'Actions' }, -] as const - -const WEB_COMMANDS = [ - { id: 'search', label: 'Search' }, - { id: 'read', label: 'Read' }, - { id: 'scrape', label: 'Scrape' }, - { id: 'crawl', label: 'Crawl' }, -] as const - -const ALL_COMMANDS = [...TOP_LEVEL_COMMANDS, ...WEB_COMMANDS] +export interface SlashFolderNav { + isInFolder: boolean + openWebFolder: () => void + closeFolder: () => void +} interface SlashMenuProps { mentionMenu: ReturnType message: string onSelectCommand: (command: string) => void + onFolderNavChange?: (nav: SlashFolderNav) => void } -export function SlashMenu({ mentionMenu, message, onSelectCommand }: SlashMenuProps) { +function SlashMenuContent({ + mentionMenu, + message, + onSelectCommand, + onFolderNavChange, +}: SlashMenuProps) { + const { currentFolder, openFolder, closeFolder } = usePopoverContext() + const { - mentionMenuRef, menuListRef, getActiveSlashQueryAtPosition, getCaretPos, submenuActiveIndex, mentionActiveIndex, - openSubmenuFor, - setOpenSubmenuFor, + setSubmenuActiveIndex, } = mentionMenu + const caretPos = getCaretPos() + const currentQuery = useMemo(() => { - const caretPos = getCaretPos() const active = getActiveSlashQueryAtPosition(caretPos, message) return active?.query.trim().toLowerCase() || '' - }, [message, getCaretPos, getActiveSlashQueryAtPosition]) + }, [message, caretPos, getActiveSlashQueryAtPosition]) const filteredCommands = useMemo(() => { if (!currentQuery) return null - return ALL_COMMANDS.filter( + return ALL_SLASH_COMMANDS.filter( (cmd) => cmd.id.toLowerCase().includes(currentQuery) || cmd.label.toLowerCase().includes(currentQuery) @@ -61,52 +67,106 @@ export function SlashMenu({ mentionMenu, message, onSelectCommand }: SlashMenuPr }, [currentQuery]) const showAggregatedView = currentQuery.length > 0 - const isInFolderNavigationMode = !openSubmenuFor && !showAggregatedView + const isInFolder = currentFolder !== null + const isInFolderNavigationMode = !isInFolder && !showAggregatedView + + useEffect(() => { + if (onFolderNavChange) { + onFolderNavChange({ + isInFolder, + openWebFolder: () => { + openFolder('web', 'Web') + setSubmenuActiveIndex(0) + }, + closeFolder: () => { + closeFolder() + setSubmenuActiveIndex(0) + }, + }) + } + }, [onFolderNavChange, isInFolder, openFolder, closeFolder, setSubmenuActiveIndex]) + + return ( + + {isInFolder ? ( + <> + {WEB_COMMANDS.map((cmd, index) => ( + onSelectCommand(cmd.id)} + data-idx={index} + active={index === submenuActiveIndex} + > + {cmd.label} + + ))} + + ) : showAggregatedView ? ( + <> + {filteredCommands && filteredCommands.length === 0 ? ( +
    No commands found
    + ) : ( + filteredCommands?.map((cmd, index) => ( + onSelectCommand(cmd.id)} + data-idx={index} + active={index === submenuActiveIndex} + > + {cmd.label} + + )) + )} + + ) : ( + <> + {TOP_LEVEL_COMMANDS.map((cmd, index) => ( + onSelectCommand(cmd.id)} + data-idx={index} + active={isInFolderNavigationMode && index === mentionActiveIndex} + > + {cmd.label} + + ))} + + setSubmenuActiveIndex(0)} + active={isInFolderNavigationMode && mentionActiveIndex === TOP_LEVEL_COMMANDS.length} + data-idx={TOP_LEVEL_COMMANDS.length} + > + {WEB_COMMANDS.map((cmd) => ( + onSelectCommand(cmd.id)}> + {cmd.label} + + ))} + + + )} +
    + ) +} - const textareaEl = mentionMenu.textareaRef.current - if (!textareaEl) return null +export function SlashMenu({ + mentionMenu, + message, + onSelectCommand, + onFolderNavChange, +}: SlashMenuProps) { + const { mentionMenuRef, textareaRef, getCaretPos } = mentionMenu const caretPos = getCaretPos() - const textareaRect = textareaEl.getBoundingClientRect() - const style = window.getComputedStyle(textareaEl) - - const mirrorDiv = document.createElement('div') - mirrorDiv.style.position = 'absolute' - mirrorDiv.style.visibility = 'hidden' - mirrorDiv.style.whiteSpace = 'pre-wrap' - mirrorDiv.style.wordWrap = 'break-word' - mirrorDiv.style.font = style.font - mirrorDiv.style.padding = style.padding - mirrorDiv.style.border = style.border - mirrorDiv.style.width = style.width - mirrorDiv.style.lineHeight = style.lineHeight - mirrorDiv.style.boxSizing = style.boxSizing - mirrorDiv.style.letterSpacing = style.letterSpacing - mirrorDiv.style.textTransform = style.textTransform - mirrorDiv.style.textIndent = style.textIndent - mirrorDiv.style.textAlign = style.textAlign - mirrorDiv.textContent = message.substring(0, caretPos) - - const caretMarker = document.createElement('span') - caretMarker.style.display = 'inline-block' - caretMarker.style.width = '0px' - caretMarker.style.padding = '0' - caretMarker.style.border = '0' - mirrorDiv.appendChild(caretMarker) - - document.body.appendChild(mirrorDiv) - const markerRect = caretMarker.getBoundingClientRect() - const mirrorRect = mirrorDiv.getBoundingClientRect() - document.body.removeChild(mirrorDiv) - - const caretViewport = { - left: textareaRect.left + (markerRect.left - mirrorRect.left) - textareaEl.scrollLeft, - top: textareaRect.top + (markerRect.top - mirrorRect.top) - textareaEl.scrollTop, - } - - const margin = 8 - const spaceBelow = window.innerHeight - caretViewport.top - margin - const side: 'top' | 'bottom' = spaceBelow >= caretViewport.top - margin ? 'bottom' : 'top' + + const { caretViewport, side } = useCaretViewport({ + textareaRef, + message, + caretPos, + }) + + if (!caretViewport) return null return ( {}}> @@ -129,77 +189,18 @@ export function SlashMenu({ mentionMenu, message, onSelectCommand }: SlashMenuPr collisionPadding={6} maxHeight={360} className='pointer-events-auto' - style={{ - width: `180px`, - }} + style={{ width: '180px' }} onOpenAutoFocus={(e) => e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()} + onMouseDown={(e) => e.preventDefault()} > - setOpenSubmenuFor(null)} /> - - {openSubmenuFor === 'Web' ? ( - <> - {WEB_COMMANDS.map((cmd, index) => ( - onSelectCommand(cmd.id)} - data-idx={index} - active={index === submenuActiveIndex} - > - {cmd.label} - - ))} - - ) : showAggregatedView ? ( - <> - {filteredCommands && filteredCommands.length === 0 ? ( -
    - No commands found -
    - ) : ( - filteredCommands?.map((cmd, index) => ( - onSelectCommand(cmd.id)} - data-idx={index} - active={index === submenuActiveIndex} - > - {cmd.label} - - )) - )} - - ) : ( - <> - {TOP_LEVEL_COMMANDS.map((cmd, index) => ( - onSelectCommand(cmd.id)} - data-idx={index} - active={isInFolderNavigationMode && index === mentionActiveIndex} - > - {cmd.label} - - ))} - - setOpenSubmenuFor('Web')} - active={ - isInFolderNavigationMode && mentionActiveIndex === TOP_LEVEL_COMMANDS.length - } - data-idx={TOP_LEVEL_COMMANDS.length} - > - {WEB_COMMANDS.map((cmd) => ( - onSelectCommand(cmd.id)}> - {cmd.label} - - ))} - - - )} -
    + +
    ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants.ts index 5872144aae..74c5f275a0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants.ts @@ -1,42 +1,245 @@ +import type { ChatContext } from '@/stores/panel' + /** - * Constants for user input component + * Mention folder types */ +export type MentionFolderId = + | 'chats' + | 'workflows' + | 'knowledge' + | 'blocks' + | 'workflow-blocks' + | 'templates' + | 'logs' /** - * Mention menu options in order (matches visual render order) + * Menu item category types for mention menu (includes folders + docs item) */ -export const MENTION_OPTIONS = [ - 'Chats', - 'Workflows', - 'Knowledge', - 'Blocks', - 'Workflow Blocks', - 'Templates', - 'Logs', - 'Docs', +export type MentionCategory = MentionFolderId | 'docs' + +/** + * Configuration interface for folder types + */ +export interface FolderConfig { + /** Display title in menu */ + title: string + /** Data source key in useMentionData return */ + dataKey: string + /** Loading state key in useMentionData return */ + loadingKey: string + /** Ensure loaded function key in useMentionData return (optional - some folders auto-load) */ + ensureLoadedKey?: string + /** Extract label from an item */ + getLabel: (item: TItem) => string + /** Extract unique ID from an item */ + getId: (item: TItem) => string + /** Empty state message */ + emptyMessage: string + /** No match message (when filtering) */ + noMatchMessage: string + /** Filter function for matching query */ + filterFn: (item: TItem, query: string) => boolean + /** Build the ChatContext object from an item */ + buildContext: (item: TItem, workflowId?: string | null) => ChatContext + /** Whether to use insertAtCursor fallback when replaceActiveMentionWith fails */ + useInsertFallback?: boolean +} + +/** + * Configuration for all folder types in the mention menu + */ +export const FOLDER_CONFIGS: Record = { + chats: { + title: 'Chats', + dataKey: 'pastChats', + loadingKey: 'isLoadingPastChats', + ensureLoadedKey: 'ensurePastChatsLoaded', + getLabel: (item) => item.title || 'New Chat', + getId: (item) => item.id, + emptyMessage: 'No past chats', + noMatchMessage: 'No matching chats', + filterFn: (item, q) => (item.title || 'New Chat').toLowerCase().includes(q), + buildContext: (item) => ({ + kind: 'past_chat', + chatId: item.id, + label: item.title || 'New Chat', + }), + useInsertFallback: false, + }, + workflows: { + title: 'All workflows', + dataKey: 'workflows', + loadingKey: 'isLoadingWorkflows', + // No ensureLoadedKey - workflows auto-load from registry store + getLabel: (item) => item.name || 'Untitled Workflow', + getId: (item) => item.id, + emptyMessage: 'No workflows', + noMatchMessage: 'No matching workflows', + filterFn: (item, q) => (item.name || 'Untitled Workflow').toLowerCase().includes(q), + buildContext: (item) => ({ + kind: 'workflow', + workflowId: item.id, + label: item.name || 'Untitled Workflow', + }), + useInsertFallback: true, + }, + knowledge: { + title: 'Knowledge Bases', + dataKey: 'knowledgeBases', + loadingKey: 'isLoadingKnowledge', + ensureLoadedKey: 'ensureKnowledgeLoaded', + getLabel: (item) => item.name || 'Untitled', + getId: (item) => item.id, + emptyMessage: 'No knowledge bases', + noMatchMessage: 'No matching knowledge bases', + filterFn: (item, q) => (item.name || 'Untitled').toLowerCase().includes(q), + buildContext: (item) => ({ + kind: 'knowledge', + knowledgeId: item.id, + label: item.name || 'Untitled', + }), + useInsertFallback: false, + }, + blocks: { + title: 'Blocks', + dataKey: 'blocksList', + loadingKey: 'isLoadingBlocks', + ensureLoadedKey: 'ensureBlocksLoaded', + getLabel: (item) => item.name || item.id, + getId: (item) => item.id, + emptyMessage: 'No blocks found', + noMatchMessage: 'No matching blocks', + filterFn: (item, q) => (item.name || item.id).toLowerCase().includes(q), + buildContext: (item) => ({ + kind: 'blocks', + blockIds: [item.id], + label: item.name || item.id, + }), + useInsertFallback: false, + }, + 'workflow-blocks': { + title: 'Workflow Blocks', + dataKey: 'workflowBlocks', + loadingKey: 'isLoadingWorkflowBlocks', + // No ensureLoadedKey - workflow blocks auto-sync from store + getLabel: (item) => item.name || item.id, + getId: (item) => item.id, + emptyMessage: 'No blocks in this workflow', + noMatchMessage: 'No matching blocks', + filterFn: (item, q) => (item.name || item.id).toLowerCase().includes(q), + buildContext: (item, workflowId) => ({ + kind: 'workflow_block', + workflowId: workflowId || '', + blockId: item.id, + label: item.name || item.id, + }), + useInsertFallback: true, + }, + templates: { + title: 'Templates', + dataKey: 'templatesList', + loadingKey: 'isLoadingTemplates', + ensureLoadedKey: 'ensureTemplatesLoaded', + getLabel: (item) => item.name || 'Untitled Template', + getId: (item) => item.id, + emptyMessage: 'No templates found', + noMatchMessage: 'No matching templates', + filterFn: (item, q) => (item.name || 'Untitled Template').toLowerCase().includes(q), + buildContext: (item) => ({ + kind: 'templates', + templateId: item.id, + label: item.name || 'Untitled Template', + }), + useInsertFallback: false, + }, + logs: { + title: 'Logs', + dataKey: 'logsList', + loadingKey: 'isLoadingLogs', + ensureLoadedKey: 'ensureLogsLoaded', + getLabel: (item) => item.workflowName, + getId: (item) => item.id, + emptyMessage: 'No executions found', + noMatchMessage: 'No matching executions', + filterFn: (item, q) => + [item.workflowName, item.trigger || ''].join(' ').toLowerCase().includes(q), + buildContext: (item) => ({ + kind: 'logs', + executionId: item.executionId || item.id, + label: item.workflowName, + }), + useInsertFallback: false, + }, +} + +/** + * Order of folders in the mention menu + */ +export const FOLDER_ORDER: MentionFolderId[] = [ + 'chats', + 'workflows', + 'knowledge', + 'blocks', + 'workflow-blocks', + 'templates', + 'logs', +] + +/** + * Docs item configuration (special case - not a folder) + */ +export const DOCS_CONFIG = { + getLabel: () => 'Docs', + buildContext: (): ChatContext => ({ kind: 'docs', label: 'Docs' }), +} as const + +/** + * Total number of items in root menu (folders + docs) + */ +export const ROOT_MENU_ITEM_COUNT = FOLDER_ORDER.length + 1 + +/** + * Slash command configuration + */ +export interface SlashCommand { + id: string + label: string +} + +export const TOP_LEVEL_COMMANDS: readonly SlashCommand[] = [ + { id: 'fast', label: 'Fast' }, + { id: 'research', label: 'Research' }, + { id: 'superagent', label: 'Actions' }, +] as const + +export const WEB_COMMANDS: readonly SlashCommand[] = [ + { id: 'search', label: 'Search' }, + { id: 'read', label: 'Read' }, + { id: 'scrape', label: 'Scrape' }, + { id: 'crawl', label: 'Crawl' }, ] as const +export const ALL_SLASH_COMMANDS: readonly SlashCommand[] = [...TOP_LEVEL_COMMANDS, ...WEB_COMMANDS] + +export const ALL_COMMAND_IDS = ALL_SLASH_COMMANDS.map((cmd) => cmd.id) + +/** + * Get display label for a command ID + */ +export function getCommandDisplayLabel(commandId: string): string { + const command = ALL_SLASH_COMMANDS.find((cmd) => cmd.id === commandId) + return command?.label || commandId.charAt(0).toUpperCase() + commandId.slice(1) +} + /** * Model configuration options */ export const MODEL_OPTIONS = [ { value: 'claude-4.5-opus', label: 'Claude 4.5 Opus' }, { value: 'claude-4.5-sonnet', label: 'Claude 4.5 Sonnet' }, - // { value: 'claude-4-sonnet', label: 'Claude 4 Sonnet' }, { value: 'claude-4.5-haiku', label: 'Claude 4.5 Haiku' }, - // { value: 'claude-4.1-opus', label: 'Claude 4.1 Opus' }, { value: 'gpt-5.1-codex', label: 'GPT 5.1 Codex' }, - // { value: 'gpt-5-codex', label: 'GPT 5 Codex' }, { value: 'gpt-5.1-medium', label: 'GPT 5.1 Medium' }, - // { value: 'gpt-5-fast', label: 'GPT 5 Fast' }, - // { value: 'gpt-5', label: 'GPT 5' }, - // { value: 'gpt-5.1-fast', label: 'GPT 5.1 Fast' }, - // { value: 'gpt-5.1', label: 'GPT 5.1' }, - // { value: 'gpt-5.1-high', label: 'GPT 5.1 High' }, - // { value: 'gpt-5-high', label: 'GPT 5 High' }, - // { value: 'gpt-4o', label: 'GPT 4o' }, - // { value: 'gpt-4.1', label: 'GPT 4.1' }, - // { value: 'o3', label: 'o3' }, { value: 'gemini-3-pro', label: 'Gemini 3 Pro' }, ] as const @@ -49,3 +252,18 @@ export const NEAR_TOP_THRESHOLD = 300 * Scroll tolerance for mention menu positioning (in pixels) */ export const SCROLL_TOLERANCE = 8 + +/** + * Shared CSS classes for menu state text (loading, empty states) + */ +export const MENU_STATE_TEXT_CLASSES = 'px-[8px] py-[8px] text-[12px] text-[var(--text-muted)]' + +/** + * Calculates the next index for circular navigation (wraps around at bounds) + */ +export function getNextIndex(current: number, direction: 'up' | 'down', maxIndex: number): number { + if (direction === 'down') { + return current >= maxIndex ? 0 : current + 1 + } + return current <= 0 ? maxIndex : current - 1 +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts index 6631a13c75..858a39c136 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts @@ -1,3 +1,4 @@ +export { useCaretViewport } from './use-caret-viewport' export { useContextManagement } from './use-context-management' export { useFileAttachments } from './use-file-attachments' export { useMentionData } from './use-mention-data' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-caret-viewport.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-caret-viewport.ts new file mode 100644 index 0000000000..51cc921228 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-caret-viewport.ts @@ -0,0 +1,77 @@ +import { useMemo } from 'react' + +interface CaretViewportPosition { + left: number + top: number +} + +interface UseCaretViewportResult { + caretViewport: CaretViewportPosition | null + side: 'top' | 'bottom' +} + +interface UseCaretViewportProps { + textareaRef: React.RefObject + message: string + caretPos: number +} + +/** + * Calculates the viewport position of the caret in a textarea using the mirror div technique. + * This hook memoizes the calculation to prevent unnecessary DOM manipulation on every render. + */ +export function useCaretViewport({ + textareaRef, + message, + caretPos, +}: UseCaretViewportProps): UseCaretViewportResult { + return useMemo(() => { + const textareaEl = textareaRef.current + if (!textareaEl) { + return { caretViewport: null, side: 'bottom' as const } + } + + const textareaRect = textareaEl.getBoundingClientRect() + const style = window.getComputedStyle(textareaEl) + + const mirrorDiv = document.createElement('div') + mirrorDiv.style.position = 'absolute' + mirrorDiv.style.visibility = 'hidden' + mirrorDiv.style.whiteSpace = 'pre-wrap' + mirrorDiv.style.overflowWrap = 'break-word' + mirrorDiv.style.font = style.font + mirrorDiv.style.padding = style.padding + mirrorDiv.style.border = style.border + mirrorDiv.style.width = style.width + mirrorDiv.style.lineHeight = style.lineHeight + mirrorDiv.style.boxSizing = style.boxSizing + mirrorDiv.style.letterSpacing = style.letterSpacing + mirrorDiv.style.textTransform = style.textTransform + mirrorDiv.style.textIndent = style.textIndent + mirrorDiv.style.textAlign = style.textAlign + mirrorDiv.textContent = message.substring(0, caretPos) + + const caretMarker = document.createElement('span') + caretMarker.style.display = 'inline-block' + caretMarker.style.width = '0px' + caretMarker.style.padding = '0' + caretMarker.style.border = '0' + mirrorDiv.appendChild(caretMarker) + + document.body.appendChild(mirrorDiv) + const markerRect = caretMarker.getBoundingClientRect() + const mirrorRect = mirrorDiv.getBoundingClientRect() + document.body.removeChild(mirrorDiv) + + const caretViewport = { + left: textareaRect.left + (markerRect.left - mirrorRect.left) - textareaEl.scrollLeft, + top: textareaRect.top + (markerRect.top - mirrorRect.top) - textareaEl.scrollTop, + } + + const margin = 8 + const spaceBelow = window.innerHeight - caretViewport.top - margin + const side: 'top' | 'bottom' = spaceBelow >= caretViewport.top - margin ? 'bottom' : 'top' + + return { caretViewport, side } + }, [textareaRef, message, caretPos]) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts index 9e85bbeca6..6b062e13f0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts @@ -1,4 +1,8 @@ import { useCallback, useEffect, useRef, useState } from 'react' +import { + filterOutContext, + isContextAlreadySelected, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils' import type { ChatContext } from '@/stores/panel' interface UseContextManagementProps { @@ -35,53 +39,7 @@ export function useContextManagement({ message, initialContexts }: UseContextMan */ const addContext = useCallback((context: ChatContext) => { setSelectedContexts((prev) => { - // CRITICAL: Check label collision FIRST - // The token system uses @label format, so we cannot have duplicate labels - // regardless of kind or ID differences - const exists = prev.some((c) => { - // Primary check: label collision - // This prevents duplicate @Label tokens which would break the overlay - if (c.label && context.label && c.label === context.label) { - return true - } - - // Secondary check: exact duplicate by ID fields based on kind - // This prevents the same entity from being added twice even with different labels - if (c.kind === context.kind) { - if (c.kind === 'past_chat' && 'chatId' in context && 'chatId' in c) { - return c.chatId === (context as any).chatId - } - if (c.kind === 'workflow' && 'workflowId' in context && 'workflowId' in c) { - return c.workflowId === (context as any).workflowId - } - if (c.kind === 'blocks' && 'blockId' in context && 'blockId' in c) { - return c.blockId === (context as any).blockId - } - if (c.kind === 'workflow_block' && 'blockId' in context && 'blockId' in c) { - return ( - c.workflowId === (context as any).workflowId && c.blockId === (context as any).blockId - ) - } - if (c.kind === 'knowledge' && 'knowledgeId' in context && 'knowledgeId' in c) { - return c.knowledgeId === (context as any).knowledgeId - } - if (c.kind === 'templates' && 'templateId' in context && 'templateId' in c) { - return c.templateId === (context as any).templateId - } - if (c.kind === 'logs' && 'executionId' in context && 'executionId' in c) { - return c.executionId === (context as any).executionId - } - if (c.kind === 'docs') { - return true // Only one docs context allowed - } - if (c.kind === 'slash_command' && 'command' in context && 'command' in c) { - return c.command === (context as any).command - } - } - - return false - }) - if (exists) return prev + if (isContextAlreadySelected(context, prev)) return prev return [...prev, context] }) }, []) @@ -92,38 +50,7 @@ export function useContextManagement({ message, initialContexts }: UseContextMan * @param contextToRemove - Context to remove */ const removeContext = useCallback((contextToRemove: ChatContext) => { - setSelectedContexts((prev) => - prev.filter((c) => { - // Match by kind and specific ID fields - if (c.kind !== contextToRemove.kind) return true - - switch (c.kind) { - case 'past_chat': - return (c as any).chatId !== (contextToRemove as any).chatId - case 'workflow': - return (c as any).workflowId !== (contextToRemove as any).workflowId - case 'blocks': - return (c as any).blockId !== (contextToRemove as any).blockId - case 'workflow_block': - return ( - (c as any).workflowId !== (contextToRemove as any).workflowId || - (c as any).blockId !== (contextToRemove as any).blockId - ) - case 'knowledge': - return (c as any).knowledgeId !== (contextToRemove as any).knowledgeId - case 'templates': - return (c as any).templateId !== (contextToRemove as any).templateId - case 'logs': - return (c as any).executionId !== (contextToRemove as any).executionId - case 'docs': - return false // Remove docs (only one docs context) - case 'slash_command': - return (c as any).command !== (contextToRemove as any).command - default: - return c.label !== contextToRemove.label - } - }) - ) + setSelectedContexts((prev) => filterOutContext(prev, contextToRemove)) }, []) /** diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data.ts index 476623e8b7..cea9a5ce34 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data.ts @@ -83,6 +83,36 @@ interface UseMentionDataProps { workspaceId: string } +/** + * Return type for useMentionData hook + */ +export interface MentionDataReturn { + // Data arrays + pastChats: PastChat[] + workflows: WorkflowItem[] + knowledgeBases: KnowledgeItem[] + blocksList: BlockItem[] + workflowBlocks: WorkflowBlockItem[] + templatesList: TemplateItem[] + logsList: LogItem[] + + // Loading states + isLoadingPastChats: boolean + isLoadingWorkflows: boolean + isLoadingKnowledge: boolean + isLoadingBlocks: boolean + isLoadingWorkflowBlocks: boolean + isLoadingTemplates: boolean + isLoadingLogs: boolean + + // Ensure loaded functions + ensurePastChatsLoaded: () => Promise + ensureKnowledgeLoaded: () => Promise + ensureBlocksLoaded: () => Promise + ensureTemplatesLoaded: () => Promise + ensureLogsLoaded: () => Promise +} + /** * Custom hook to fetch and manage data for mention suggestions * Loads data from APIs for chats, workflows, knowledge bases, blocks, templates, and logs @@ -90,7 +120,7 @@ interface UseMentionDataProps { * @param props - Configuration including workflow and workspace IDs * @returns Mention data state and loading operations */ -export function useMentionData(props: UseMentionDataProps) { +export function useMentionData(props: UseMentionDataProps): MentionDataReturn { const { workflowId, workspaceId } = props const { config, isBlockAllowed } = usePermissionConfig() @@ -104,7 +134,6 @@ export function useMentionData(props: UseMentionDataProps) { const [blocksList, setBlocksList] = useState([]) const [isLoadingBlocks, setIsLoadingBlocks] = useState(false) - // Reset blocks list when permission config changes useEffect(() => { setBlocksList([]) }, [config.allowedIntegrations]) @@ -118,12 +147,10 @@ export function useMentionData(props: UseMentionDataProps) { const [workflowBlocks, setWorkflowBlocks] = useState([]) const [isLoadingWorkflowBlocks, setIsLoadingWorkflowBlocks] = useState(false) - // Only subscribe to block keys to avoid re-rendering on position updates const blockKeys = useWorkflowStore( useShallow(useCallback((state) => Object.keys(state.blocks), [])) ) - // Use workflow registry as source of truth for workflows const registryWorkflows = useWorkflowRegistry((state) => state.workflows) const hydrationPhase = useWorkflowRegistry((state) => state.hydration.phase) const isLoadingWorkflows = @@ -131,7 +158,6 @@ export function useMentionData(props: UseMentionDataProps) { hydrationPhase === 'metadata-loading' || hydrationPhase === 'state-loading' - // Convert registry workflows to mention format, filtered by workspace and sorted const workflows: WorkflowItem[] = Object.values(registryWorkflows) .filter((w) => w.workspaceId === workspaceId) .sort((a, b) => { @@ -219,14 +245,6 @@ export function useMentionData(props: UseMentionDataProps) { } }, [isLoadingPastChats, pastChats.length, workflowId]) - /** - * Ensures workflows are loaded (now using registry store) - */ - const ensureWorkflowsLoaded = useCallback(() => { - // Workflows are now automatically loaded from the registry store - // No manual fetching needed - }, []) - /** * Ensures knowledge bases are loaded */ @@ -348,18 +366,6 @@ export function useMentionData(props: UseMentionDataProps) { } }, [isLoadingLogs, logsList.length, workspaceId]) - /** - * Ensures workflow blocks are loaded (synced from store) - */ - const ensureWorkflowBlocksLoaded = useCallback(async () => { - if (!workflowId) return - logger.debug('ensureWorkflowBlocksLoaded called', { - workflowId, - storeBlocksCount: blockKeys.length, - workflowBlocksCount: workflowBlocks.length, - }) - }, [workflowId, blockKeys.length, workflowBlocks.length]) - return { // State pastChats, @@ -379,11 +385,9 @@ export function useMentionData(props: UseMentionDataProps) { // Operations ensurePastChatsLoaded, - ensureWorkflowsLoaded, ensureKnowledgeLoaded, ensureBlocksLoaded, ensureTemplatesLoaded, ensureLogsLoaded, - ensureWorkflowBlocksLoaded, } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-insert-handlers.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-insert-handlers.ts index cd631781d7..478331b3c7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-insert-handlers.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-insert-handlers.ts @@ -1,5 +1,12 @@ -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' +import type { MentionFolderNav } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components' +import { + DOCS_CONFIG, + FOLDER_CONFIGS, + type FolderConfig, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants' import type { useMentionMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-menu' +import { isContextAlreadySelected } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils' import type { ChatContext } from '@/stores/panel' interface UseMentionInsertHandlersProps { @@ -11,12 +18,12 @@ interface UseMentionInsertHandlersProps { selectedContexts: ChatContext[] /** Callback to update selected contexts */ onContextAdd: (context: ChatContext) => void + /** Folder navigation state exposed from MentionMenu via callback */ + mentionFolderNav?: MentionFolderNav | null } /** * Custom hook to provide insert handlers for different mention types. - * Consolidates the logic for inserting mentions and updating selected contexts. - * Prevents duplicate mentions from being inserted. * * @param props - Configuration object * @returns Insert handler functions for each mention type @@ -26,6 +33,7 @@ export function useMentionInsertHandlers({ workflowId, selectedContexts, onContextAdd, + mentionFolderNav, }: UseMentionInsertHandlersProps) { const { replaceActiveMentionWith, @@ -36,342 +44,94 @@ export function useMentionInsertHandlers({ } = mentionMenu /** - * Checks if a context already exists in selected contexts - * CRITICAL: Prioritizes label checking to prevent token system breakage - * - * @param context - Context to check - * @returns True if context already exists or label is already used + * Closes all menus and resets state */ - const isContextAlreadySelected = useCallback( - (context: ChatContext): boolean => { - return selectedContexts.some((c) => { - // CRITICAL: Check label collision FIRST - // The token system uses @label format, so we cannot have duplicate labels - // regardless of kind or ID differences - if (c.label && context.label && c.label === context.label) { - return true + const closeMenus = useCallback(() => { + setShowMentionMenu(false) + if (mentionFolderNav?.isInFolder) { + mentionFolderNav.closeFolder() + } + setOpenSubmenuFor(null) + }, [setShowMentionMenu, setOpenSubmenuFor, mentionFolderNav]) + + const createInsertHandler = useCallback( + (config: FolderConfig) => { + return (item: TItem) => { + const label = config.getLabel(item) + const context = config.buildContext(item, workflowId) + + if (isContextAlreadySelected(context, selectedContexts)) { + resetActiveMentionQuery() + closeMenus() + return } - // Secondary check: exact duplicate by ID fields - if (c.kind === context.kind) { - if (c.kind === 'past_chat' && 'chatId' in context && 'chatId' in c) { - return c.chatId === (context as any).chatId - } - if (c.kind === 'workflow' && 'workflowId' in context && 'workflowId' in c) { - return c.workflowId === (context as any).workflowId - } - if (c.kind === 'blocks' && 'blockId' in context && 'blockId' in c) { - return c.blockId === (context as any).blockId - } - if (c.kind === 'workflow_block' && 'blockId' in context && 'blockId' in c) { - return ( - c.workflowId === (context as any).workflowId && c.blockId === (context as any).blockId - ) - } - if (c.kind === 'knowledge' && 'knowledgeId' in context && 'knowledgeId' in c) { - return c.knowledgeId === (context as any).knowledgeId - } - if (c.kind === 'templates' && 'templateId' in context && 'templateId' in c) { - return c.templateId === (context as any).templateId - } - if (c.kind === 'logs' && 'executionId' in context && 'executionId' in c) { - return c.executionId === (context as any).executionId - } - if (c.kind === 'docs') { - return true + if (config.useInsertFallback) { + if (!replaceActiveMentionWith(label)) { + insertAtCursor(` @${label} `) } + } else { + replaceActiveMentionWith(label) } - return false - }) - }, - [selectedContexts] - ) - - /** - * Inserts a past chat mention - * - * @param chat - Chat object to mention - */ - const insertPastChatMention = useCallback( - (chat: { id: string; title: string | null }) => { - const label = chat.title || 'New Chat' - const context = { kind: 'past_chat', chatId: chat.id, label } as ChatContext - - // Prevent duplicate insertion - if (isContextAlreadySelected(context)) { - // Clear the partial mention text (e.g., "@Unti") before closing - resetActiveMentionQuery() - setShowMentionMenu(false) - setOpenSubmenuFor(null) - return - } - - replaceActiveMentionWith(label) - onContextAdd(context) - setShowMentionMenu(false) - setOpenSubmenuFor(null) - }, - [ - replaceActiveMentionWith, - onContextAdd, - setShowMentionMenu, - setOpenSubmenuFor, - isContextAlreadySelected, - resetActiveMentionQuery, - ] - ) - - /** - * Inserts a workflow mention - * - * @param wf - Workflow object to mention - */ - const insertWorkflowMention = useCallback( - (wf: { id: string; name: string }) => { - const label = wf.name || 'Untitled Workflow' - const context = { kind: 'workflow', workflowId: wf.id, label } as ChatContext - - // Prevent duplicate insertion - if (isContextAlreadySelected(context)) { - // Clear the partial mention text before closing - resetActiveMentionQuery() - setShowMentionMenu(false) - setOpenSubmenuFor(null) - return - } - - if (!replaceActiveMentionWith(label)) insertAtCursor(` @${label} `) - onContextAdd(context) - setShowMentionMenu(false) - setOpenSubmenuFor(null) - }, - [ - replaceActiveMentionWith, - insertAtCursor, - onContextAdd, - setShowMentionMenu, - setOpenSubmenuFor, - isContextAlreadySelected, - resetActiveMentionQuery, - ] - ) - - /** - * Inserts a knowledge base mention - * - * @param kb - Knowledge base object to mention - */ - const insertKnowledgeMention = useCallback( - (kb: { id: string; name: string }) => { - const label = kb.name || 'Untitled' - const context = { kind: 'knowledge', knowledgeId: kb.id, label } as any - - // Prevent duplicate insertion - if (isContextAlreadySelected(context)) { - // Clear the partial mention text before closing - resetActiveMentionQuery() - setShowMentionMenu(false) - setOpenSubmenuFor(null) - return - } - - replaceActiveMentionWith(label) - onContextAdd(context) - setShowMentionMenu(false) - setOpenSubmenuFor(null) - }, - [ - replaceActiveMentionWith, - onContextAdd, - setShowMentionMenu, - setOpenSubmenuFor, - isContextAlreadySelected, - resetActiveMentionQuery, - ] - ) - - /** - * Inserts a block mention - * - * @param blk - Block object to mention - */ - const insertBlockMention = useCallback( - (blk: { id: string; name: string }) => { - const label = blk.name || blk.id - const context = { kind: 'blocks', blockId: blk.id, label } as any - - // Prevent duplicate insertion - if (isContextAlreadySelected(context)) { - // Clear the partial mention text before closing - resetActiveMentionQuery() - setShowMentionMenu(false) - setOpenSubmenuFor(null) - return + onContextAdd(context) + closeMenus() } - - replaceActiveMentionWith(label) - onContextAdd(context) - setShowMentionMenu(false) - setOpenSubmenuFor(null) }, [ - replaceActiveMentionWith, - onContextAdd, - setShowMentionMenu, - setOpenSubmenuFor, - isContextAlreadySelected, - resetActiveMentionQuery, - ] - ) - - /** - * Inserts a workflow block mention - * - * @param blk - Workflow block object to mention - */ - const insertWorkflowBlockMention = useCallback( - (blk: { id: string; name: string }) => { - const label = blk.name - const context = { - kind: 'workflow_block', - workflowId: workflowId as string, - blockId: blk.id, - label, - } as any - - // Prevent duplicate insertion - if (isContextAlreadySelected(context)) { - // Clear the partial mention text before closing - resetActiveMentionQuery() - setShowMentionMenu(false) - setOpenSubmenuFor(null) - return - } - - if (!replaceActiveMentionWith(label)) insertAtCursor(` @${label} `) - onContextAdd(context) - setShowMentionMenu(false) - setOpenSubmenuFor(null) - }, - [ - replaceActiveMentionWith, - insertAtCursor, workflowId, - onContextAdd, - setShowMentionMenu, - setOpenSubmenuFor, - isContextAlreadySelected, - resetActiveMentionQuery, - ] - ) - - /** - * Inserts a template mention - * - * @param tpl - Template object to mention - */ - const insertTemplateMention = useCallback( - (tpl: { id: string; name: string }) => { - const label = tpl.name || 'Untitled Template' - const context = { kind: 'templates', templateId: tpl.id, label } as any - - // Prevent duplicate insertion - if (isContextAlreadySelected(context)) { - // Clear the partial mention text before closing - resetActiveMentionQuery() - setShowMentionMenu(false) - setOpenSubmenuFor(null) - return - } - - replaceActiveMentionWith(label) - onContextAdd(context) - setShowMentionMenu(false) - setOpenSubmenuFor(null) - }, - [ - replaceActiveMentionWith, - onContextAdd, - setShowMentionMenu, - setOpenSubmenuFor, - isContextAlreadySelected, - resetActiveMentionQuery, - ] - ) - - /** - * Inserts a log mention - * - * @param log - Log object to mention - */ - const insertLogMention = useCallback( - (log: { id: string; executionId?: string; workflowName: string }) => { - const label = log.workflowName - const context = { kind: 'logs' as const, executionId: log.executionId, label } - - // Prevent duplicate insertion - if (isContextAlreadySelected(context)) { - // Clear the partial mention text before closing - resetActiveMentionQuery() - setShowMentionMenu(false) - setOpenSubmenuFor(null) - return - } - - replaceActiveMentionWith(label) - onContextAdd(context) - setShowMentionMenu(false) - setOpenSubmenuFor(null) - }, - [ + selectedContexts, replaceActiveMentionWith, + insertAtCursor, onContextAdd, - setShowMentionMenu, - setOpenSubmenuFor, - isContextAlreadySelected, resetActiveMentionQuery, + closeMenus, ] ) /** - * Inserts a docs mention + * Special handler for Docs (no item parameter, uses DOCS_CONFIG) */ const insertDocsMention = useCallback(() => { - const label = 'Docs' - const context = { kind: 'docs', label } as any + const label = DOCS_CONFIG.getLabel() + const context = DOCS_CONFIG.buildContext() // Prevent duplicate insertion - if (isContextAlreadySelected(context)) { - // Clear the partial mention text before closing + if (isContextAlreadySelected(context, selectedContexts)) { resetActiveMentionQuery() - setShowMentionMenu(false) - setOpenSubmenuFor(null) + closeMenus() return } - if (!replaceActiveMentionWith(label)) insertAtCursor(` @${label} `) + // Docs uses fallback insertion + if (!replaceActiveMentionWith(label)) { + insertAtCursor(` @${label} `) + } + onContextAdd(context) - setShowMentionMenu(false) - setOpenSubmenuFor(null) + closeMenus() }, [ + selectedContexts, replaceActiveMentionWith, insertAtCursor, onContextAdd, - setShowMentionMenu, - setOpenSubmenuFor, - isContextAlreadySelected, resetActiveMentionQuery, + closeMenus, ]) - return { - insertPastChatMention, - insertWorkflowMention, - insertKnowledgeMention, - insertBlockMention, - insertWorkflowBlockMention, - insertTemplateMention, - insertLogMention, - insertDocsMention, - } + const handlers = useMemo( + () => ({ + insertPastChatMention: createInsertHandler(FOLDER_CONFIGS.chats), + insertWorkflowMention: createInsertHandler(FOLDER_CONFIGS.workflows), + insertKnowledgeMention: createInsertHandler(FOLDER_CONFIGS.knowledge), + insertBlockMention: createInsertHandler(FOLDER_CONFIGS.blocks), + insertWorkflowBlockMention: createInsertHandler(FOLDER_CONFIGS['workflow-blocks']), + insertTemplateMention: createInsertHandler(FOLDER_CONFIGS.templates), + insertLogMention: createInsertHandler(FOLDER_CONFIGS.logs), + insertDocsMention, + }), + [createInsertHandler, insertDocsMention] + ) + + return handlers } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-keyboard.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-keyboard.ts index b36e73a286..27e4385410 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-keyboard.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-keyboard.ts @@ -1,56 +1,19 @@ -import { type KeyboardEvent, useCallback } from 'react' -import type { useMentionData } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data' -import type { useMentionMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-menu' -import { MENTION_OPTIONS } from '../constants' - -/** - * Chat item for mention insertion - */ -interface ChatItem { - id: string - title: string | null -} - -/** - * Workflow item for mention insertion - */ -interface WorkflowItem { - id: string - name: string -} - -/** - * Knowledge base item for mention insertion - */ -interface KnowledgeItem { - id: string - name: string -} - -/** - * Block item for mention insertion - */ -interface BlockItem { - id: string - name: string -} - -/** - * Template item for mention insertion - */ -interface TemplateItem { - id: string - name: string -} - -/** - * Log item for mention insertion - */ -interface LogItem { - id: string - executionId?: string - workflowName: string -} +import { type KeyboardEvent, useCallback, useMemo } from 'react' +import type { MentionFolderNav } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components' +import { + FOLDER_CONFIGS, + FOLDER_ORDER, + type MentionFolderId, + ROOT_MENU_ITEM_COUNT, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants' +import type { + useMentionData, + useMentionMenu, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks' +import { + getFolderData as getFolderDataUtil, + getFolderEnsureLoaded as getFolderEnsureLoadedUtil, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils' interface UseMentionKeyboardProps { /** Mention menu hook instance */ @@ -59,37 +22,34 @@ interface UseMentionKeyboardProps { mentionData: ReturnType /** Callback to insert specific mention types */ insertHandlers: { - insertPastChatMention: (chat: ChatItem) => void - insertWorkflowMention: (wf: WorkflowItem) => void - insertKnowledgeMention: (kb: KnowledgeItem) => void - insertBlockMention: (blk: BlockItem) => void - insertWorkflowBlockMention: (blk: BlockItem) => void - insertTemplateMention: (tpl: TemplateItem) => void - insertLogMention: (log: LogItem) => void + insertPastChatMention: (chat: any) => void + insertWorkflowMention: (wf: any) => void + insertKnowledgeMention: (kb: any) => void + insertBlockMention: (blk: any) => void + insertWorkflowBlockMention: (blk: any) => void + insertTemplateMention: (tpl: any) => void + insertLogMention: (log: any) => void insertDocsMention: () => void } + /** Folder navigation state exposed from MentionMenu via callback */ + mentionFolderNav: MentionFolderNav | null } /** * Custom hook to handle keyboard navigation in the mention menu. - * Manages Arrow Up/Down/Left/Right and Enter key navigation through menus and submenus. - * - * @param props - Configuration object - * @returns Keyboard handler for mention menu */ export function useMentionKeyboard({ mentionMenu, mentionData, insertHandlers, + mentionFolderNav, }: UseMentionKeyboardProps) { const { showMentionMenu, - openSubmenuFor, mentionActiveIndex, submenuActiveIndex, setMentionActiveIndex, setSubmenuActiveIndex, - setOpenSubmenuFor, setSubmenuQueryStart, getCaretPos, getActiveMentionQueryAtPosition, @@ -98,65 +58,101 @@ export function useMentionKeyboard({ scrollActiveItemIntoView, } = mentionMenu - const { - pastChats, - workflows, - knowledgeBases, - blocksList, - workflowBlocks, - templatesList, - logsList, - ensurePastChatsLoaded, - ensureWorkflowsLoaded, - ensureKnowledgeLoaded, - ensureBlocksLoaded, - ensureWorkflowBlocksLoaded, - ensureTemplatesLoaded, - ensureLogsLoaded, - } = mentionData + const currentFolder = mentionFolderNav?.currentFolder ?? null + const isInFolder = mentionFolderNav?.isInFolder ?? false - const { - insertPastChatMention, - insertWorkflowMention, - insertKnowledgeMention, - insertBlockMention, - insertWorkflowBlockMention, - insertTemplateMention, - insertLogMention, - insertDocsMention, - } = insertHandlers + /** + * Map of folder IDs to insert handlers + */ + const insertHandlerMap = useMemo( + (): Record void> => ({ + chats: insertHandlers.insertPastChatMention, + workflows: insertHandlers.insertWorkflowMention, + knowledge: insertHandlers.insertKnowledgeMention, + blocks: insertHandlers.insertBlockMention, + 'workflow-blocks': insertHandlers.insertWorkflowBlockMention, + templates: insertHandlers.insertTemplateMention, + logs: insertHandlers.insertLogMention, + }), + [insertHandlers] + ) + + /** + * Get data array for a folder from mentionData + */ + const getFolderData = useCallback( + (folderId: MentionFolderId) => getFolderDataUtil(mentionData, folderId), + [mentionData] + ) + + /** + * Filter items for a folder based on query using config's filterFn + */ + const filterFolderItems = useCallback( + (folderId: MentionFolderId, query: string): any[] => { + const config = FOLDER_CONFIGS[folderId] + const items = getFolderData(folderId) + if (!query) return items + const q = query.toLowerCase() + return items.filter((item) => config.filterFn(item, q)) + }, + [getFolderData] + ) + + /** + * Ensure data is loaded for a folder + */ + const ensureFolderLoaded = useCallback( + (folderId: MentionFolderId): void => { + const ensureFn = getFolderEnsureLoadedUtil(mentionData, folderId) + if (ensureFn) void ensureFn() + }, + [mentionData] + ) /** * Build aggregated list matching the portal's ordering */ const buildAggregatedList = useCallback( - (query: string) => { + (query: string): Array<{ type: MentionFolderId | 'docs'; value: any }> => { const q = query.toLowerCase() - return [ - ...pastChats - .filter((c) => (c.title || 'New Chat').toLowerCase().includes(q)) - .map((c) => ({ type: 'Chats' as const, value: c })), - ...workflows - .filter((w) => (w.name || 'Untitled Workflow').toLowerCase().includes(q)) - .map((w) => ({ type: 'Workflows' as const, value: w })), - ...knowledgeBases - .filter((k) => (k.name || 'Untitled').toLowerCase().includes(q)) - .map((k) => ({ type: 'Knowledge' as const, value: k })), - ...blocksList - .filter((b) => (b.name || b.id).toLowerCase().includes(q)) - .map((b) => ({ type: 'Blocks' as const, value: b })), - ...workflowBlocks - .filter((b) => (b.name || b.id).toLowerCase().includes(q)) - .map((b) => ({ type: 'Workflow Blocks' as const, value: b })), - ...templatesList - .filter((t) => (t.name || 'Untitled Template').toLowerCase().includes(q)) - .map((t) => ({ type: 'Templates' as const, value: t })), - ...logsList - .filter((l) => (l.workflowName || 'Untitled Workflow').toLowerCase().includes(q)) - .map((l) => ({ type: 'Logs' as const, value: l })), - ] + const result: Array<{ type: MentionFolderId | 'docs'; value: any }> = [] + + for (const folderId of FOLDER_ORDER) { + const filtered = filterFolderItems(folderId, q) + filtered.forEach((item) => { + result.push({ type: folderId, value: item }) + }) + } + + if ('docs'.includes(q)) { + result.push({ type: 'docs', value: null }) + } + + return result }, - [pastChats, workflows, knowledgeBases, blocksList, workflowBlocks, templatesList, logsList] + [filterFolderItems] + ) + + /** + * Generic navigation helper for navigating through items + */ + const navigateItems = useCallback( + ( + direction: 'up' | 'down', + itemCount: number, + setIndex: (fn: (prev: number) => number) => void + ) => { + setIndex((prev) => { + const last = Math.max(0, itemCount - 1) + if (itemCount === 0) return 0 + const next = + direction === 'down' ? (prev >= last ? 0 : prev + 1) : prev <= 0 ? last : prev - 1 + requestAnimationFrame(() => scrollActiveItemIntoView(next)) + return next + }) + }, + [scrollActiveItemIntoView] ) /** @@ -169,143 +165,36 @@ export function useMentionKeyboard({ e.preventDefault() const caretPos = getCaretPos() const active = getActiveMentionQueryAtPosition(caretPos) - const mainQ = (!openSubmenuFor ? active?.query || '' : '').toLowerCase() + const mainQ = (!isInFolder ? active?.query || '' : '').toLowerCase() + const direction = e.key === 'ArrowDown' ? 'down' : 'up' - // When there's a query, we show aggregated filtered view (no folders) const showAggregatedView = mainQ.length > 0 - const aggregatedList = showAggregatedView ? buildAggregatedList(mainQ) : [] - - // When showing aggregated filtered view, navigate through the aggregated list - if (showAggregatedView && !openSubmenuFor) { - setSubmenuActiveIndex((prev) => { - const last = Math.max(0, aggregatedList.length - 1) - if (aggregatedList.length === 0) return 0 - const next = - e.key === 'ArrowDown' ? (prev >= last ? 0 : prev + 1) : prev <= 0 ? last : prev - 1 - requestAnimationFrame(() => scrollActiveItemIntoView(next)) - return next - }) + if (showAggregatedView && !isInFolder) { + const aggregatedList = buildAggregatedList(mainQ) + navigateItems(direction, aggregatedList.length, setSubmenuActiveIndex) return true } - // Handle submenu navigation - if (openSubmenuFor === 'Chats') { - const q = getSubmenuQuery().toLowerCase() - const filtered = pastChats.filter((c) => (c.title || 'New Chat').toLowerCase().includes(q)) - setSubmenuActiveIndex((prev) => { - const last = Math.max(0, filtered.length - 1) - if (filtered.length === 0) return 0 - const next = - e.key === 'ArrowDown' ? (prev >= last ? 0 : prev + 1) : prev <= 0 ? last : prev - 1 - requestAnimationFrame(() => scrollActiveItemIntoView(next)) - return next - }) - } else if (openSubmenuFor === 'Workflows') { - const q = getSubmenuQuery().toLowerCase() - const filtered = workflows.filter((w) => - (w.name || 'Untitled Workflow').toLowerCase().includes(q) - ) - setSubmenuActiveIndex((prev) => { - const last = Math.max(0, filtered.length - 1) - if (filtered.length === 0) return 0 - const next = - e.key === 'ArrowDown' ? (prev >= last ? 0 : prev + 1) : prev <= 0 ? last : prev - 1 - requestAnimationFrame(() => scrollActiveItemIntoView(next)) - return next - }) - } else if (openSubmenuFor === 'Knowledge') { - const q = getSubmenuQuery().toLowerCase() - const filtered = knowledgeBases.filter((k) => - (k.name || 'Untitled').toLowerCase().includes(q) - ) - setSubmenuActiveIndex((prev) => { - const last = Math.max(0, filtered.length - 1) - if (filtered.length === 0) return 0 - const next = - e.key === 'ArrowDown' ? (prev >= last ? 0 : prev + 1) : prev <= 0 ? last : prev - 1 - requestAnimationFrame(() => scrollActiveItemIntoView(next)) - return next - }) - } else if (openSubmenuFor === 'Blocks') { + if (currentFolder && FOLDER_CONFIGS[currentFolder as MentionFolderId]) { const q = getSubmenuQuery().toLowerCase() - const filtered = blocksList.filter((b) => (b.name || b.id).toLowerCase().includes(q)) - setSubmenuActiveIndex((prev) => { - const last = Math.max(0, filtered.length - 1) - if (filtered.length === 0) return 0 - const next = - e.key === 'ArrowDown' ? (prev >= last ? 0 : prev + 1) : prev <= 0 ? last : prev - 1 - requestAnimationFrame(() => scrollActiveItemIntoView(next)) - return next - }) - } else if (openSubmenuFor === 'Workflow Blocks') { - const q = getSubmenuQuery().toLowerCase() - const filtered = workflowBlocks.filter((b) => (b.name || b.id).toLowerCase().includes(q)) - setSubmenuActiveIndex((prev) => { - const last = Math.max(0, filtered.length - 1) - if (filtered.length === 0) return 0 - const next = - e.key === 'ArrowDown' ? (prev >= last ? 0 : prev + 1) : prev <= 0 ? last : prev - 1 - requestAnimationFrame(() => scrollActiveItemIntoView(next)) - return next - }) - } else if (openSubmenuFor === 'Templates') { - const q = getSubmenuQuery().toLowerCase() - const filtered = templatesList.filter((t) => - (t.name || 'Untitled Template').toLowerCase().includes(q) - ) - setSubmenuActiveIndex((prev) => { - const last = Math.max(0, filtered.length - 1) - if (filtered.length === 0) return 0 - const next = - e.key === 'ArrowDown' ? (prev >= last ? 0 : prev + 1) : prev <= 0 ? last : prev - 1 - requestAnimationFrame(() => scrollActiveItemIntoView(next)) - return next - }) - } else if (openSubmenuFor === 'Logs') { - const q = getSubmenuQuery().toLowerCase() - const filtered = logsList.filter((l) => - [l.workflowName, l.trigger || ''].join(' ').toLowerCase().includes(q) - ) - setSubmenuActiveIndex((prev) => { - const last = Math.max(0, filtered.length - 1) - if (filtered.length === 0) return 0 - const next = - e.key === 'ArrowDown' ? (prev >= last ? 0 : prev + 1) : prev <= 0 ? last : prev - 1 - requestAnimationFrame(() => scrollActiveItemIntoView(next)) - return next - }) - } else { - // Navigate through folder options when no query - const filteredMain = MENTION_OPTIONS.filter((o) => o.toLowerCase().includes(mainQ)) - setMentionActiveIndex((prev) => { - const last = Math.max(0, filteredMain.length - 1) - if (filteredMain.length === 0) return 0 - const next = - e.key === 'ArrowDown' ? (prev >= last ? 0 : prev + 1) : prev <= 0 ? last : prev - 1 - requestAnimationFrame(() => scrollActiveItemIntoView(next)) - return next - }) + const filtered = filterFolderItems(currentFolder as MentionFolderId, q) + navigateItems(direction, filtered.length, setSubmenuActiveIndex) + return true } + navigateItems(direction, ROOT_MENU_ITEM_COUNT, setMentionActiveIndex) return true }, [ showMentionMenu, - openSubmenuFor, - mentionActiveIndex, - submenuActiveIndex, + isInFolder, + currentFolder, buildAggregatedList, - pastChats, - workflows, - knowledgeBases, - blocksList, - workflowBlocks, - templatesList, - logsList, + filterFolderItems, + navigateItems, getCaretPos, getActiveMentionQueryAtPosition, getSubmenuQuery, - scrollActiveItemIntoView, setMentionActiveIndex, setSubmenuActiveIndex, ] @@ -316,65 +205,30 @@ export function useMentionKeyboard({ */ const handleArrowRight = useCallback( (e: KeyboardEvent) => { - if (!showMentionMenu || e.key !== 'ArrowRight') return false + if (!showMentionMenu || e.key !== 'ArrowRight' || !mentionFolderNav) return false const caretPos = getCaretPos() const active = getActiveMentionQueryAtPosition(caretPos) const mainQ = (active?.query || '').toLowerCase() - const showAggregatedView = mainQ.length > 0 - // Don't handle arrow right in aggregated view (user is filtering, not navigating folders) - if (showAggregatedView) return false + if (mainQ.length > 0) return false e.preventDefault() - const filteredMain = MENTION_OPTIONS.filter((o) => o.toLowerCase().includes(mainQ)) - const selected = filteredMain[mentionActiveIndex] - if (selected === 'Chats') { - resetActiveMentionQuery() - setOpenSubmenuFor('Chats') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensurePastChatsLoaded() - } else if (selected === 'Workflows') { - resetActiveMentionQuery() - setOpenSubmenuFor('Workflows') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureWorkflowsLoaded() - } else if (selected === 'Knowledge') { - resetActiveMentionQuery() - setOpenSubmenuFor('Knowledge') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureKnowledgeLoaded() - } else if (selected === 'Blocks') { - resetActiveMentionQuery() - setOpenSubmenuFor('Blocks') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureBlocksLoaded() - } else if (selected === 'Workflow Blocks') { + const isDocsSelected = mentionActiveIndex === FOLDER_ORDER.length + if (isDocsSelected) { resetActiveMentionQuery() - setOpenSubmenuFor('Workflow Blocks') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureWorkflowBlocksLoaded() - } else if (selected === 'Docs') { - resetActiveMentionQuery() - insertDocsMention() - } else if (selected === 'Templates') { - resetActiveMentionQuery() - setOpenSubmenuFor('Templates') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureTemplatesLoaded() - } else if (selected === 'Logs') { + insertHandlers.insertDocsMention() + return true + } + + const selectedFolderId = FOLDER_ORDER[mentionActiveIndex] + if (selectedFolderId) { + const config = FOLDER_CONFIGS[selectedFolderId] resetActiveMentionQuery() - setOpenSubmenuFor('Logs') - setSubmenuActiveIndex(0) + mentionFolderNav.openFolder(selectedFolderId, config.title) setSubmenuQueryStart(getCaretPos()) - void ensureLogsLoaded() + ensureFolderLoaded(selectedFolderId) } return true @@ -382,21 +236,13 @@ export function useMentionKeyboard({ [ showMentionMenu, mentionActiveIndex, - openSubmenuFor, + mentionFolderNav, getCaretPos, getActiveMentionQueryAtPosition, resetActiveMentionQuery, - setOpenSubmenuFor, - setSubmenuActiveIndex, setSubmenuQueryStart, - ensurePastChatsLoaded, - ensureWorkflowsLoaded, - ensureKnowledgeLoaded, - ensureBlocksLoaded, - ensureWorkflowBlocksLoaded, - ensureTemplatesLoaded, - ensureLogsLoaded, - insertDocsMention, + ensureFolderLoaded, + insertHandlers, ] ) @@ -407,16 +253,16 @@ export function useMentionKeyboard({ (e: KeyboardEvent) => { if (!showMentionMenu || e.key !== 'ArrowLeft') return false - if (openSubmenuFor) { + if (isInFolder && mentionFolderNav) { e.preventDefault() - setOpenSubmenuFor(null) + mentionFolderNav.closeFolder() setSubmenuQueryStart(null) return true } return false }, - [showMentionMenu, openSubmenuFor, setOpenSubmenuFor, setSubmenuQueryStart] + [showMentionMenu, isInFolder, mentionFolderNav, setSubmenuQueryStart] ) /** @@ -429,179 +275,74 @@ export function useMentionKeyboard({ e.preventDefault() const caretPos = getCaretPos() const active = getActiveMentionQueryAtPosition(caretPos) - const mainQ = (active?.query || '').toLowerCase() + const mainQ = (!isInFolder ? active?.query || '' : '').toLowerCase() const showAggregatedView = mainQ.length > 0 - const filteredMain = MENTION_OPTIONS.filter((o) => o.toLowerCase().includes(mainQ)) - const selected = filteredMain[mentionActiveIndex] - // Handle selection in aggregated filtered view - if (showAggregatedView && !openSubmenuFor) { + if (showAggregatedView && !isInFolder) { const aggregated = buildAggregatedList(mainQ) const idx = Math.max(0, Math.min(submenuActiveIndex, aggregated.length - 1)) const chosen = aggregated[idx] if (chosen) { - if (chosen.type === 'Chats') insertPastChatMention(chosen.value as ChatItem) - else if (chosen.type === 'Workflows') insertWorkflowMention(chosen.value as WorkflowItem) - else if (chosen.type === 'Knowledge') - insertKnowledgeMention(chosen.value as KnowledgeItem) - else if (chosen.type === 'Workflow Blocks') - insertWorkflowBlockMention(chosen.value as BlockItem) - else if (chosen.type === 'Blocks') insertBlockMention(chosen.value as BlockItem) - else if (chosen.type === 'Templates') insertTemplateMention(chosen.value as TemplateItem) - else if (chosen.type === 'Logs') insertLogMention(chosen.value as LogItem) + if (chosen.type === 'docs') { + insertHandlers.insertDocsMention() + } else { + const handler = insertHandlerMap[chosen.type] + handler(chosen.value) + } } return true } - // Handle folder navigation when no query - if (!openSubmenuFor && selected === 'Chats') { - resetActiveMentionQuery() - setOpenSubmenuFor('Chats') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensurePastChatsLoaded() - } else if (openSubmenuFor === 'Chats') { - const q = getSubmenuQuery().toLowerCase() - const filtered = pastChats.filter((c) => (c.title || 'New Chat').toLowerCase().includes(q)) - if (filtered.length > 0) { - const chosen = filtered[Math.max(0, Math.min(submenuActiveIndex, filtered.length - 1))] - insertPastChatMention(chosen) - setSubmenuQueryStart(null) - } - } else if (!openSubmenuFor && selected === 'Workflows') { - resetActiveMentionQuery() - setOpenSubmenuFor('Workflows') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureWorkflowsLoaded() - } else if (openSubmenuFor === 'Workflows') { - const q = getSubmenuQuery().toLowerCase() - const filtered = workflows.filter((w) => - (w.name || 'Untitled Workflow').toLowerCase().includes(q) - ) - if (filtered.length > 0) { - const chosen = filtered[Math.max(0, Math.min(submenuActiveIndex, filtered.length - 1))] - insertWorkflowMention(chosen) - setSubmenuQueryStart(null) - } - } else if (!openSubmenuFor && selected === 'Knowledge') { - resetActiveMentionQuery() - setOpenSubmenuFor('Knowledge') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureKnowledgeLoaded() - } else if (openSubmenuFor === 'Knowledge') { - const q = getSubmenuQuery().toLowerCase() - const filtered = knowledgeBases.filter((k) => - (k.name || 'Untitled').toLowerCase().includes(q) - ) - if (filtered.length > 0) { - const chosen = filtered[Math.max(0, Math.min(submenuActiveIndex, filtered.length - 1))] - insertKnowledgeMention(chosen) - setSubmenuQueryStart(null) - } - } else if (!openSubmenuFor && selected === 'Blocks') { - resetActiveMentionQuery() - setOpenSubmenuFor('Blocks') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureBlocksLoaded() - } else if (openSubmenuFor === 'Blocks') { - const q = getSubmenuQuery().toLowerCase() - const filtered = blocksList.filter((b) => (b.name || b.id).toLowerCase().includes(q)) - if (filtered.length > 0) { - const chosen = filtered[Math.max(0, Math.min(submenuActiveIndex, filtered.length - 1))] - insertBlockMention(chosen) - setSubmenuQueryStart(null) - } - } else if (!openSubmenuFor && selected === 'Workflow Blocks') { - resetActiveMentionQuery() - setOpenSubmenuFor('Workflow Blocks') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureWorkflowBlocksLoaded() - } else if (openSubmenuFor === 'Workflow Blocks') { + if (isInFolder && currentFolder && FOLDER_CONFIGS[currentFolder as MentionFolderId]) { + const folderId = currentFolder as MentionFolderId const q = getSubmenuQuery().toLowerCase() - const filtered = workflowBlocks.filter((b) => (b.name || b.id).toLowerCase().includes(q)) + const filtered = filterFolderItems(folderId, q) if (filtered.length > 0) { const chosen = filtered[Math.max(0, Math.min(submenuActiveIndex, filtered.length - 1))] - insertWorkflowBlockMention(chosen) + const handler = insertHandlerMap[folderId] + handler(chosen) setSubmenuQueryStart(null) } - } else if (!openSubmenuFor && selected === 'Docs') { - resetActiveMentionQuery() - insertDocsMention() - } else if (!openSubmenuFor && selected === 'Templates') { + return true + } + + const isDocsSelected = mentionActiveIndex === FOLDER_ORDER.length + if (isDocsSelected) { resetActiveMentionQuery() - setOpenSubmenuFor('Templates') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureTemplatesLoaded() - } else if (!openSubmenuFor && selected === 'Logs') { + insertHandlers.insertDocsMention() + return true + } + + const selectedFolderId = FOLDER_ORDER[mentionActiveIndex] + if (selectedFolderId && mentionFolderNav) { + const config = FOLDER_CONFIGS[selectedFolderId] resetActiveMentionQuery() - setOpenSubmenuFor('Logs') + mentionFolderNav.openFolder(selectedFolderId, config.title) setSubmenuActiveIndex(0) setSubmenuQueryStart(getCaretPos()) - void ensureLogsLoaded() - } else if (openSubmenuFor === 'Templates') { - const q = getSubmenuQuery().toLowerCase() - const filtered = templatesList.filter((t) => - (t.name || 'Untitled Template').toLowerCase().includes(q) - ) - if (filtered.length > 0) { - const chosen = filtered[Math.max(0, Math.min(submenuActiveIndex, filtered.length - 1))] - insertTemplateMention(chosen) - setSubmenuQueryStart(null) - } - } else if (openSubmenuFor === 'Logs') { - const q = getSubmenuQuery().toLowerCase() - const filtered = logsList.filter((l) => - [l.workflowName, l.trigger || ''].join(' ').toLowerCase().includes(q) - ) - if (filtered.length > 0) { - const chosen = filtered[Math.max(0, Math.min(submenuActiveIndex, filtered.length - 1))] - insertLogMention(chosen) - setSubmenuQueryStart(null) - } + ensureFolderLoaded(selectedFolderId) } return true }, [ showMentionMenu, - openSubmenuFor, + isInFolder, + currentFolder, mentionActiveIndex, submenuActiveIndex, + mentionFolderNav, buildAggregatedList, - pastChats, - workflows, - knowledgeBases, - blocksList, - workflowBlocks, - templatesList, - logsList, + filterFolderItems, + insertHandlerMap, getCaretPos, getActiveMentionQueryAtPosition, getSubmenuQuery, resetActiveMentionQuery, - setOpenSubmenuFor, setSubmenuActiveIndex, setSubmenuQueryStart, - ensurePastChatsLoaded, - ensureWorkflowsLoaded, - ensureKnowledgeLoaded, - ensureBlocksLoaded, - ensureWorkflowBlocksLoaded, - ensureTemplatesLoaded, - ensureLogsLoaded, - insertPastChatMention, - insertWorkflowMention, - insertKnowledgeMention, - insertBlockMention, - insertWorkflowBlockMention, - insertTemplateMention, - insertLogMention, - insertDocsMention, + ensureFolderLoaded, + insertHandlers, ] ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-menu.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-menu.ts index 8a07146e05..4859409219 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-menu.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-menu.ts @@ -1,9 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' -import { createLogger } from '@sim/logger' +import { SCROLL_TOLERANCE } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants' import type { ChatContext } from '@/stores/panel' -import { SCROLL_TOLERANCE } from '../constants' - -const logger = createLogger('useMentionMenu') interface UseMentionMenuProps { /** Current message text */ diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-textarea-auto-resize.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-textarea-auto-resize.ts index 7fabea1da9..82ee7107ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-textarea-auto-resize.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-textarea-auto-resize.ts @@ -49,7 +49,6 @@ export function useTextareaAutoResize({ const styles = window.getComputedStyle(textarea) - // Copy all text rendering properties exactly (but NOT color - overlay needs visible text) overlay.style.font = styles.font overlay.style.fontSize = styles.fontSize overlay.style.fontFamily = styles.fontFamily @@ -66,7 +65,6 @@ export function useTextareaAutoResize({ overlay.style.textTransform = styles.textTransform overlay.style.textIndent = styles.textIndent - // Copy box model properties exactly to ensure identical text flow overlay.style.padding = styles.padding overlay.style.paddingTop = styles.paddingTop overlay.style.paddingRight = styles.paddingRight @@ -80,7 +78,6 @@ export function useTextareaAutoResize({ overlay.style.border = styles.border overlay.style.borderWidth = styles.borderWidth - // Copy text wrapping and breaking properties overlay.style.whiteSpace = styles.whiteSpace overlay.style.wordBreak = styles.wordBreak overlay.style.wordWrap = styles.wordWrap @@ -91,20 +88,17 @@ export function useTextareaAutoResize({ overlay.style.direction = styles.direction overlay.style.hyphens = (styles as any).hyphens ?? '' - // Critical: Match dimensions exactly const textareaWidth = textarea.clientWidth const textareaHeight = textarea.clientHeight overlay.style.width = `${textareaWidth}px` overlay.style.height = `${textareaHeight}px` - // Match max-height behavior const computedMaxHeight = styles.maxHeight if (computedMaxHeight && computedMaxHeight !== 'none') { overlay.style.maxHeight = computedMaxHeight } - // Ensure scroll positions are perfectly synced overlay.scrollTop = textarea.scrollTop overlay.scrollLeft = textarea.scrollLeft }) @@ -119,25 +113,20 @@ export function useTextareaAutoResize({ const overlay = overlayRef.current if (!textarea || !overlay) return - // Store current cursor position to determine if user is typing at the end const cursorPos = textarea.selectionStart ?? 0 const isAtEnd = cursorPos === message.length const wasScrolledToBottom = textarea.scrollHeight - textarea.scrollTop - textarea.clientHeight < 5 - // Reset height to auto to get proper scrollHeight textarea.style.height = 'auto' overlay.style.height = 'auto' - // Force a reflow to ensure accurate scrollHeight void textarea.offsetHeight void overlay.offsetHeight - // Get the scroll height (this includes all content, including trailing newlines) const scrollHeight = textarea.scrollHeight const nextHeight = Math.min(scrollHeight, MAX_TEXTAREA_HEIGHT) - // Apply height to BOTH elements simultaneously const heightString = `${nextHeight}px` const overflowString = scrollHeight > MAX_TEXTAREA_HEIGHT ? 'auto' : 'hidden' @@ -146,22 +135,18 @@ export function useTextareaAutoResize({ overlay.style.height = heightString overlay.style.overflowY = overflowString - // Force another reflow after height change void textarea.offsetHeight void overlay.offsetHeight - // Maintain scroll behavior: if user was at bottom or typing at end, keep them at bottom if ((isAtEnd || wasScrolledToBottom) && scrollHeight > nextHeight) { const scrollValue = scrollHeight textarea.scrollTop = scrollValue overlay.scrollTop = scrollValue } else { - // Otherwise, sync scroll positions overlay.scrollTop = textarea.scrollTop overlay.scrollLeft = textarea.scrollLeft } - // Sync all other styles after height change syncOverlayStyles.current() }, [message, selectedContexts, textareaRef]) @@ -192,19 +177,15 @@ export function useTextareaAutoResize({ const overlay = overlayRef.current if (!textarea || !overlay || !containerRef || typeof window === 'undefined') return - // Initial sync syncOverlayStyles.current() - // Observe the CONTAINER - when pills wrap, container height changes if (typeof ResizeObserver !== 'undefined' && !containerResizeObserverRef.current) { containerResizeObserverRef.current = new ResizeObserver(() => { - // Container size changed (pills wrapped) - sync immediately syncOverlayStyles.current() }) containerResizeObserverRef.current.observe(containerRef) } - // ALSO observe the textarea for its own size changes if (typeof ResizeObserver !== 'undefined' && !textareaResizeObserverRef.current) { textareaResizeObserverRef.current = new ResizeObserver(() => { syncOverlayStyles.current() @@ -212,7 +193,6 @@ export function useTextareaAutoResize({ textareaResizeObserverRef.current.observe(textarea) } - // Setup MutationObserver to detect style changes const mutationObserver = new MutationObserver(() => { syncOverlayStyles.current() }) @@ -221,11 +201,9 @@ export function useTextareaAutoResize({ attributeFilter: ['style', 'class'], }) - // Listen to window resize events (for browser window resizing) const handleResize = () => syncOverlayStyles.current() window.addEventListener('resize', handleResize) - // Cleanup return () => { mutationObserver.disconnect() window.removeEventListener('resize', handleResize) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx index 665266bbbd..a5e19fd130 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx @@ -18,12 +18,21 @@ import { cn } from '@/lib/core/utils/cn' import { AttachedFilesDisplay, ContextPills, + type MentionFolderNav, MentionMenu, ModelSelector, ModeSelector, + type SlashFolderNav, SlashMenu, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components' -import { NEAR_TOP_THRESHOLD } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants' +import { + ALL_COMMAND_IDS, + getCommandDisplayLabel, + getNextIndex, + NEAR_TOP_THRESHOLD, + TOP_LEVEL_COMMANDS, + WEB_COMMANDS, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants' import { useContextManagement, useFileAttachments, @@ -40,24 +49,6 @@ import { useCopilotStore } from '@/stores/panel' const logger = createLogger('CopilotUserInput') -const TOP_LEVEL_COMMANDS = ['fast', 'research', 'superagent'] as const -const WEB_COMMANDS = ['search', 'read', 'scrape', 'crawl'] as const -const ALL_COMMANDS = [...TOP_LEVEL_COMMANDS, ...WEB_COMMANDS] - -const COMMAND_DISPLAY_LABELS: Record = { - superagent: 'Actions', -} - -/** - * Calculates the next index for circular navigation (wraps around at bounds) - */ -function getNextIndex(current: number, direction: 'up' | 'down', maxIndex: number): number { - if (direction === 'down') { - return current >= maxIndex ? 0 : current + 1 - } - return current <= 0 ? maxIndex : current - 1 -} - interface UserInputProps { onSubmit: ( message: string, @@ -144,6 +135,8 @@ const UserInput = forwardRef( const [containerRef, setContainerRef] = useState(null) const [inputContainerRef, setInputContainerRef] = useState(null) const [showSlashMenu, setShowSlashMenu] = useState(false) + const [slashFolderNav, setSlashFolderNav] = useState(null) + const [mentionFolderNav, setMentionFolderNav] = useState(null) const message = controlledValue !== undefined ? controlledValue : internalMessage const setMessage = @@ -198,12 +191,14 @@ const UserInput = forwardRef( workflowId: workflowId || null, selectedContexts: contextManagement.selectedContexts, onContextAdd: contextManagement.addContext, + mentionFolderNav, }) const mentionKeyboard = useMentionKeyboard({ mentionMenu, mentionData, insertHandlers, + mentionFolderNav, }) useImperativeHandle( @@ -222,13 +217,6 @@ const UserInput = forwardRef( [mentionMenu.textareaRef] ) - useEffect(() => { - if (workflowId) { - void mentionData.ensureWorkflowsLoaded() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [workflowId]) - useEffect(() => { const checkPosition = () => { if (containerRef) { @@ -264,7 +252,7 @@ const UserInput = forwardRef( }, [mentionMenu.showMentionMenu, containerRef]) useEffect(() => { - if (!mentionMenu.showMentionMenu || mentionMenu.openSubmenuFor) { + if (!mentionMenu.showMentionMenu || mentionFolderNav?.isInFolder) { return } @@ -275,8 +263,7 @@ const UserInput = forwardRef( if (q && q.length > 0) { void mentionData.ensurePastChatsLoaded() - void mentionData.ensureWorkflowsLoaded() - void mentionData.ensureWorkflowBlocksLoaded() + // workflows and workflow-blocks auto-load from stores void mentionData.ensureKnowledgeLoaded() void mentionData.ensureBlocksLoaded() void mentionData.ensureTemplatesLoaded() @@ -286,15 +273,15 @@ const UserInput = forwardRef( requestAnimationFrame(() => mentionMenu.scrollActiveItemIntoView(0)) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [mentionMenu.showMentionMenu, mentionMenu.openSubmenuFor, message]) + }, [mentionMenu.showMentionMenu, mentionFolderNav?.isInFolder, message]) useEffect(() => { - if (mentionMenu.openSubmenuFor) { + if (mentionFolderNav?.isInFolder) { mentionMenu.setSubmenuActiveIndex(0) requestAnimationFrame(() => mentionMenu.scrollActiveItemIntoView(0)) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [mentionMenu.openSubmenuFor]) + }, [mentionFolderNav?.isInFolder]) const handleSubmit = useCallback( async (overrideMessage?: string, options: { preserveInput?: boolean } = {}) => { @@ -372,8 +359,7 @@ const UserInput = forwardRef( const handleSlashCommandSelect = useCallback( (command: string) => { - const displayLabel = - COMMAND_DISPLAY_LABELS[command] || command.charAt(0).toUpperCase() + command.slice(1) + const displayLabel = getCommandDisplayLabel(command) mentionMenu.replaceActiveSlashWith(displayLabel) contextManagement.addContext({ kind: 'slash_command', @@ -391,9 +377,11 @@ const UserInput = forwardRef( (e: KeyboardEvent) => { if (e.key === 'Escape' && (mentionMenu.showMentionMenu || showSlashMenu)) { e.preventDefault() - if (mentionMenu.openSubmenuFor) { - mentionMenu.setOpenSubmenuFor(null) + if (mentionFolderNav?.isInFolder) { + mentionFolderNav.closeFolder() mentionMenu.setSubmenuQueryStart(null) + } else if (slashFolderNav?.isInFolder) { + slashFolderNav.closeFolder() } else { mentionMenu.closeMentionMenu() setShowSlashMenu(false) @@ -407,18 +395,19 @@ const UserInput = forwardRef( const query = activeSlash?.query.trim().toLowerCase() || '' const showAggregatedView = query.length > 0 const direction = e.key === 'ArrowDown' ? 'down' : 'up' + const isInFolder = slashFolderNav?.isInFolder ?? false if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault() - if (mentionMenu.openSubmenuFor === 'Web') { + if (isInFolder) { mentionMenu.setSubmenuActiveIndex((prev) => { const next = getNextIndex(prev, direction, WEB_COMMANDS.length - 1) requestAnimationFrame(() => mentionMenu.scrollActiveItemIntoView(next)) return next }) } else if (showAggregatedView) { - const filtered = ALL_COMMANDS.filter((cmd) => cmd.includes(query)) + const filtered = ALL_COMMAND_IDS.filter((cmd) => cmd.includes(query)) mentionMenu.setSubmenuActiveIndex((prev) => { if (filtered.length === 0) return 0 const next = getNextIndex(prev, direction, filtered.length - 1) @@ -437,10 +426,9 @@ const UserInput = forwardRef( if (e.key === 'ArrowRight') { e.preventDefault() - if (!showAggregatedView && !mentionMenu.openSubmenuFor) { + if (!showAggregatedView && !isInFolder) { if (mentionMenu.mentionActiveIndex === TOP_LEVEL_COMMANDS.length) { - mentionMenu.setOpenSubmenuFor('Web') - mentionMenu.setSubmenuActiveIndex(0) + slashFolderNav?.openWebFolder() } } return @@ -448,8 +436,8 @@ const UserInput = forwardRef( if (e.key === 'ArrowLeft') { e.preventDefault() - if (mentionMenu.openSubmenuFor) { - mentionMenu.setOpenSubmenuFor(null) + if (isInFolder) { + slashFolderNav?.closeFolder() } return } @@ -466,13 +454,14 @@ const UserInput = forwardRef( const activeSlash = mentionMenu.getActiveSlashQueryAtPosition(caretPos, message) const query = activeSlash?.query.trim().toLowerCase() || '' const showAggregatedView = query.length > 0 + const isInFolder = slashFolderNav?.isInFolder ?? false - if (mentionMenu.openSubmenuFor === 'Web') { + if (isInFolder) { const selectedCommand = - WEB_COMMANDS[mentionMenu.submenuActiveIndex] || WEB_COMMANDS[0] + WEB_COMMANDS[mentionMenu.submenuActiveIndex]?.id || WEB_COMMANDS[0].id handleSlashCommandSelect(selectedCommand) } else if (showAggregatedView) { - const filtered = ALL_COMMANDS.filter((cmd) => cmd.includes(query)) + const filtered = ALL_COMMAND_IDS.filter((cmd) => cmd.includes(query)) if (filtered.length > 0) { const selectedCommand = filtered[mentionMenu.submenuActiveIndex] || filtered[0] handleSlashCommandSelect(selectedCommand) @@ -480,10 +469,9 @@ const UserInput = forwardRef( } else { const selectedIndex = mentionMenu.mentionActiveIndex if (selectedIndex < TOP_LEVEL_COMMANDS.length) { - handleSlashCommandSelect(TOP_LEVEL_COMMANDS[selectedIndex]) + handleSlashCommandSelect(TOP_LEVEL_COMMANDS[selectedIndex].id) } else if (selectedIndex === TOP_LEVEL_COMMANDS.length) { - mentionMenu.setOpenSubmenuFor('Web') - mentionMenu.setSubmenuActiveIndex(0) + slashFolderNav?.openWebFolder() } } return @@ -568,6 +556,8 @@ const UserInput = forwardRef( message, mentionTokensWithContext, showSlashMenu, + slashFolderNav, + mentionFolderNav, ] ) @@ -586,7 +576,7 @@ const UserInput = forwardRef( setShowSlashMenu(false) mentionMenu.setShowMentionMenu(true) mentionMenu.setInAggregated(false) - if (mentionMenu.openSubmenuFor) { + if (mentionFolderNav?.isInFolder) { mentionMenu.setSubmenuActiveIndex(0) } else { mentionMenu.setMentionActiveIndex(0) @@ -605,7 +595,7 @@ const UserInput = forwardRef( setShowSlashMenu(false) } }, - [setMessage, mentionMenu, disableMentions] + [setMessage, mentionMenu, disableMentions, mentionFolderNav] ) const handleSelectAdjust = useCallback(() => { @@ -838,6 +828,7 @@ const UserInput = forwardRef( mentionData={mentionData} message={message} insertHandlers={insertHandlers} + onFolderNavChange={setMentionFolderNav} />, document.body )} @@ -850,6 +841,7 @@ const UserInput = forwardRef( mentionMenu={mentionMenu} message={message} onSelectCommand={handleSlashCommandSelect} + onFolderNavChange={setSlashFolderNav} />, document.body )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils.ts new file mode 100644 index 0000000000..89902729d4 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils.ts @@ -0,0 +1,149 @@ +import { + FOLDER_CONFIGS, + type MentionFolderId, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants' +import type { MentionDataReturn } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data' +import type { ChatContext } from '@/stores/panel' + +/** + * Gets the data array for a folder ID from mentionData. + * Uses FOLDER_CONFIGS as the source of truth for key mapping. + * Returns any[] since item types vary by folder and are used with dynamic config.filterFn + */ +export function getFolderData(mentionData: MentionDataReturn, folderId: MentionFolderId): any[] { + const config = FOLDER_CONFIGS[folderId] + return (mentionData[config.dataKey as keyof MentionDataReturn] as any[]) || [] +} + +/** + * Gets the loading state for a folder ID from mentionData. + * Uses FOLDER_CONFIGS as the source of truth for key mapping. + */ +export function getFolderLoading( + mentionData: MentionDataReturn, + folderId: MentionFolderId +): boolean { + const config = FOLDER_CONFIGS[folderId] + return mentionData[config.loadingKey as keyof MentionDataReturn] as boolean +} + +/** + * Gets the ensure loaded function for a folder ID from mentionData. + * Uses FOLDER_CONFIGS as the source of truth for key mapping. + */ +export function getFolderEnsureLoaded( + mentionData: MentionDataReturn, + folderId: MentionFolderId +): (() => Promise) | undefined { + const config = FOLDER_CONFIGS[folderId] + if (!config.ensureLoadedKey) return undefined + return mentionData[config.ensureLoadedKey as keyof MentionDataReturn] as + | (() => Promise) + | undefined +} + +/** + * Extract specific ChatContext types for type-safe narrowing + */ +type PastChatContext = Extract +type WorkflowContext = Extract +type CurrentWorkflowContext = Extract +type BlocksContext = Extract +type WorkflowBlockContext = Extract +type KnowledgeContext = Extract +type TemplatesContext = Extract +type LogsContext = Extract +type SlashCommandContext = Extract + +/** + * Checks if two contexts of the same kind are equal by their ID fields. + * Assumes c.kind === context.kind (must be checked before calling). + */ +export function areContextsEqual(c: ChatContext, context: ChatContext): boolean { + switch (c.kind) { + case 'past_chat': { + const ctx = context as PastChatContext + return c.chatId === ctx.chatId + } + case 'workflow': { + const ctx = context as WorkflowContext + return c.workflowId === ctx.workflowId + } + case 'current_workflow': { + const ctx = context as CurrentWorkflowContext + return c.workflowId === ctx.workflowId + } + case 'blocks': { + const ctx = context as BlocksContext + const existingIds = c.blockIds || [] + const newIds = ctx.blockIds || [] + return existingIds.some((id) => newIds.includes(id)) + } + case 'workflow_block': { + const ctx = context as WorkflowBlockContext + return c.workflowId === ctx.workflowId && c.blockId === ctx.blockId + } + case 'knowledge': { + const ctx = context as KnowledgeContext + return c.knowledgeId === ctx.knowledgeId + } + case 'templates': { + const ctx = context as TemplatesContext + return c.templateId === ctx.templateId + } + case 'logs': { + const ctx = context as LogsContext + return c.executionId === ctx.executionId + } + case 'docs': + return true // Only one docs context allowed + case 'slash_command': { + const ctx = context as SlashCommandContext + return c.command === ctx.command + } + default: + return false + } +} + +/** + * Removes a context from a list, returning a new filtered list. + */ +export function filterOutContext( + contexts: ChatContext[], + contextToRemove: ChatContext +): ChatContext[] { + return contexts.filter((c) => { + if (c.kind !== contextToRemove.kind) return true + return !areContextsEqual(c, contextToRemove) + }) +} + +/** + * Checks if a context already exists in selected contexts. + * + * The token system uses @label format, so we cannot have duplicate labels + * regardless of kind or ID differences. + * + * @param context - Context to check + * @param selectedContexts - Currently selected contexts + * @returns True if context already exists or label is already used + */ +export function isContextAlreadySelected( + context: ChatContext, + selectedContexts: ChatContext[] +): boolean { + return selectedContexts.some((c) => { + // CRITICAL: Check label collision FIRST + // The token system uses @label format, so we cannot have duplicate labels + // regardless of kind or ID differences + if (c.label && context.label && c.label === context.label) { + return true + } + + // Secondary check: exact duplicate by ID fields + if (c.kind !== context.kind) return false + + return areContextsEqual(c, context) + }) +} diff --git a/apps/sim/lib/core/utils/formatting.ts b/apps/sim/lib/core/utils/formatting.ts index 5543026f56..3d6d1e9029 100644 --- a/apps/sim/lib/core/utils/formatting.ts +++ b/apps/sim/lib/core/utils/formatting.ts @@ -7,7 +7,6 @@ export function getTimezoneAbbreviation(timezone: string, date: Date = new Date()): string { if (timezone === 'UTC') return 'UTC' - // Common timezone mappings const timezoneMap: Record = { 'America/Los_Angeles': { standard: 'PST', daylight: 'PDT' }, 'America/Denver': { standard: 'MST', daylight: 'MDT' }, @@ -20,30 +19,22 @@ export function getTimezoneAbbreviation(timezone: string, date: Date = new Date( 'Asia/Singapore': { standard: 'SGT', daylight: 'SGT' }, // Singapore doesn't use DST } - // If we have a mapping for this timezone if (timezone in timezoneMap) { - // January 1 is guaranteed to be standard time in northern hemisphere - // July 1 is guaranteed to be daylight time in northern hemisphere (if observed) const januaryDate = new Date(date.getFullYear(), 0, 1) const julyDate = new Date(date.getFullYear(), 6, 1) - // Get offset in January (standard time) const januaryFormatter = new Intl.DateTimeFormat('en-US', { timeZone: timezone, timeZoneName: 'short', }) - // Get offset in July (likely daylight time) const julyFormatter = new Intl.DateTimeFormat('en-US', { timeZone: timezone, timeZoneName: 'short', }) - // If offsets are different, timezone observes DST const isDSTObserved = januaryFormatter.format(januaryDate) !== julyFormatter.format(julyDate) - // If DST is observed, check if current date is in DST by comparing its offset - // with January's offset (standard time) if (isDSTObserved) { const currentFormatter = new Intl.DateTimeFormat('en-US', { timeZone: timezone, @@ -54,11 +45,9 @@ export function getTimezoneAbbreviation(timezone: string, date: Date = new Date( return isDST ? timezoneMap[timezone].daylight : timezoneMap[timezone].standard } - // If DST is not observed, always use standard return timezoneMap[timezone].standard } - // For unknown timezones, use full IANA name return timezone } @@ -79,7 +68,6 @@ export function formatDateTime(date: Date, timezone?: string): string { timeZone: timezone || undefined, }) - // If timezone is provided, add a friendly timezone abbreviation if (timezone) { const tzAbbr = getTimezoneAbbreviation(timezone, date) return `${formattedDate} ${tzAbbr}` @@ -114,6 +102,24 @@ export function formatTime(date: Date): string { }) } +/** + * Format an ISO timestamp into a compact format for UI display + * @param iso - ISO timestamp string + * @returns A formatted string in "MM-DD HH:mm" format + */ +export function formatCompactTimestamp(iso: string): string { + try { + const d = new Date(iso) + const mm = String(d.getMonth() + 1).padStart(2, '0') + const dd = String(d.getDate()).padStart(2, '0') + const hh = String(d.getHours()).padStart(2, '0') + const min = String(d.getMinutes()).padStart(2, '0') + return `${mm}-${dd} ${hh}:${min}` + } catch { + return iso + } +} + /** * Format a duration in milliseconds to a human-readable format * @param durationMs - The duration in milliseconds From 51477c12ccc8432fe613a71bca5e9ced1a7f1d5f Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 14 Jan 2026 11:05:38 -0800 Subject: [PATCH 17/36] fix(terminal): pop all entries from a single execution when the limit is exceeded (#2815) --- apps/sim/stores/terminal/console/store.ts | 58 ++++++++++++++++++++--- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index 2052b51340..d6becc4929 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -15,7 +15,7 @@ const logger = createLogger('TerminalConsoleStore') * Maximum number of console entries to keep per workflow. * Keeps the stored data size reasonable and improves performance. */ -const MAX_ENTRIES_PER_WORKFLOW = 500 +const MAX_ENTRIES_PER_WORKFLOW = 1000 const updateBlockOutput = ( existingOutput: NormalizedBlockOutput | undefined, @@ -96,13 +96,57 @@ export const useTerminalConsoleStore = create()( } const newEntries = [newEntry, ...state.entries] - const workflowCounts = new Map() - const trimmedEntries = newEntries.filter((entry) => { - const count = workflowCounts.get(entry.workflowId) || 0 - if (count >= MAX_ENTRIES_PER_WORKFLOW) return false - workflowCounts.set(entry.workflowId, count + 1) - return true + + const executionsToRemove = new Set() + + const workflowGroups = new Map() + for (const e of newEntries) { + const group = workflowGroups.get(e.workflowId) || [] + group.push(e) + workflowGroups.set(e.workflowId, group) + } + + for (const [workflowId, entries] of workflowGroups) { + if (entries.length <= MAX_ENTRIES_PER_WORKFLOW) continue + + const execOrder: string[] = [] + const seen = new Set() + for (const e of entries) { + const execId = e.executionId ?? e.id + if (!seen.has(execId)) { + execOrder.push(execId) + seen.add(execId) + } + } + + const counts = new Map() + for (const e of entries) { + const execId = e.executionId ?? e.id + counts.set(execId, (counts.get(execId) || 0) + 1) + } + + let total = 0 + const toKeep = new Set() + for (const execId of execOrder) { + const c = counts.get(execId) || 0 + if (total + c <= MAX_ENTRIES_PER_WORKFLOW) { + toKeep.add(execId) + total += c + } + } + + for (const execId of execOrder) { + if (!toKeep.has(execId)) { + executionsToRemove.add(`${workflowId}:${execId}`) + } + } + } + + const trimmedEntries = newEntries.filter((e) => { + const key = `${e.workflowId}:${e.executionId ?? e.id}` + return !executionsToRemove.has(key) }) + return { entries: trimmedEntries } }) From d7e0d9ba432a9fba3b24d505dceb32a25da66988 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 14 Jan 2026 11:23:26 -0800 Subject: [PATCH 18/36] fix(i18n): update translations action to run once per week on sunday (#2816) --- .github/workflows/i18n.yml | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/i18n.yml b/.github/workflows/i18n.yml index bc01dfa94a..486504b174 100644 --- a/.github/workflows/i18n.yml +++ b/.github/workflows/i18n.yml @@ -1,11 +1,10 @@ name: 'Auto-translate Documentation' on: - push: - branches: [ staging ] - paths: - - 'apps/docs/content/docs/en/**' - - 'apps/docs/i18n.json' + schedule: + # Run every Sunday at midnight UTC + - cron: '0 0 * * 0' + workflow_dispatch: # Allow manual triggers permissions: contents: write @@ -20,6 +19,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: + ref: staging token: ${{ secrets.GH_PAT }} fetch-depth: 0 @@ -68,12 +68,11 @@ jobs: title: "feat(i18n): update translations" body: | ## Summary - Automated translation updates triggered by changes to documentation. - - This PR was automatically created after content changes were made, updating translations for all supported languages using Lingo.dev AI translation engine. - - **Original trigger**: ${{ github.event.head_commit.message }} - **Commit**: ${{ github.sha }} + Automated weekly translation updates for documentation. + + This PR was automatically created by the scheduled weekly i18n workflow, updating translations for all supported languages using Lingo.dev AI translation engine. + + **Triggered**: Weekly scheduled run **Workflow**: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} ## Type of Change @@ -107,7 +106,7 @@ jobs: ## Screenshots/Videos - branch: auto-translate/staging-merge-${{ github.run_id }} + branch: auto-translate/weekly-${{ github.run_id }} base: staging labels: | i18n @@ -145,6 +144,8 @@ jobs: bun install --frozen-lockfile - name: Build documentation to verify translations + env: + DATABASE_URL: postgresql://dummy:dummy@localhost:5432/dummy run: | cd apps/docs bun run build @@ -153,7 +154,7 @@ jobs: run: | cd apps/docs echo "## Translation Status Report" >> $GITHUB_STEP_SUMMARY - echo "**Triggered by merge to staging branch**" >> $GITHUB_STEP_SUMMARY + echo "**Weekly scheduled translation run**" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY en_count=$(find content/docs/en -name "*.mdx" | wc -l) From 468ec2ea81f64986ce7f7ffa85c2d2d8c8228751 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 14 Jan 2026 12:06:02 -0800 Subject: [PATCH 19/36] fix(terminal-colors): change algo to compute colors based on hash of execution id and pointer from bottom (#2817) --- .../components/terminal/terminal.tsx | 117 ++++++++++-------- apps/sim/lib/core/utils/formatting.ts | 16 +++ 2 files changed, 79 insertions(+), 54 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx index 311c2ff22a..56b9dc8bbe 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx @@ -36,6 +36,7 @@ import { Tooltip, } from '@/components/emcn' import { getEnv, isTruthy } from '@/lib/core/config/env' +import { formatTimeWithSeconds } from '@/lib/core/utils/formatting' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils' import { @@ -82,18 +83,6 @@ const COLUMN_WIDTHS = { OUTPUT_PANEL: 'w-[400px]', } as const -/** - * Color palette for run IDs - matching code syntax highlighting colors - */ -const RUN_ID_COLORS = [ - { text: '#4ADE80' }, // Green - { text: '#F472B6' }, // Pink - { text: '#60C5FF' }, // Blue - { text: '#FF8533' }, // Orange - { text: '#C084FC' }, // Purple - { text: '#FCD34D' }, // Yellow -] as const - /** * Shared styling constants */ @@ -183,22 +172,6 @@ const ToggleButton = ({ ) -/** - * Formats timestamp to H:MM:SS AM/PM TZ format - */ -const formatTimestamp = (timestamp: string): string => { - const date = new Date(timestamp) - const fullString = date.toLocaleTimeString('en-US', { - hour: 'numeric', - minute: '2-digit', - second: '2-digit', - hour12: true, - timeZoneName: 'short', - }) - // Format: "5:54:55 PM PST" - return as is - return fullString -} - /** * Truncates execution ID for display as run ID */ @@ -208,16 +181,25 @@ const formatRunId = (executionId?: string): string => { } /** - * Gets color for a run ID based on its index in the execution ID order map + * Run ID colors + */ +const RUN_ID_COLORS = [ + '#4ADE80', // Green + '#F472B6', // Pink + '#60C5FF', // Blue + '#FF8533', // Orange + '#C084FC', // Purple + '#EAB308', // Yellow + '#2DD4BF', // Teal + '#FB7185', // Rose +] as const + +/** + * Gets color for a run ID from the precomputed color map. */ -const getRunIdColor = ( - executionId: string | undefined, - executionIdOrderMap: Map -) => { +const getRunIdColor = (executionId: string | undefined, colorMap: Map) => { if (!executionId) return null - const colorIndex = executionIdOrderMap.get(executionId) - if (colorIndex === undefined) return null - return RUN_ID_COLORS[colorIndex % RUN_ID_COLORS.length] + return colorMap.get(executionId) ?? null } /** @@ -464,25 +446,52 @@ export function Terminal() { }, [allWorkflowEntries]) /** - * Create stable execution ID to color index mapping based on order of first appearance. - * Once an execution ID is assigned a color index, it keeps that index. - * Uses all workflow entries to maintain consistent colors regardless of active filters. + * Track color offset - increments when old executions are trimmed + * so remaining executions keep their colors. */ - const executionIdOrderMap = useMemo(() => { - const orderMap = new Map() - let colorIndex = 0 + const colorStateRef = useRef<{ executionIds: string[]; offset: number }>({ + executionIds: [], + offset: 0, + }) - // Process entries in reverse order (oldest first) since entries array is newest-first - // Use allWorkflowEntries to ensure colors remain consistent when filters change + /** + * Compute colors for each execution ID using sequential assignment. + * Colors cycle through RUN_ID_COLORS based on position + offset. + * When old executions are trimmed, offset increments to preserve colors. + */ + const executionColorMap = useMemo(() => { + const currentIds: string[] = [] + const seen = new Set() for (let i = allWorkflowEntries.length - 1; i >= 0; i--) { - const entry = allWorkflowEntries[i] - if (entry.executionId && !orderMap.has(entry.executionId)) { - orderMap.set(entry.executionId, colorIndex) - colorIndex++ + const execId = allWorkflowEntries[i].executionId + if (execId && !seen.has(execId)) { + currentIds.push(execId) + seen.add(execId) } } - return orderMap + const { executionIds: prevIds, offset: prevOffset } = colorStateRef.current + let newOffset = prevOffset + + if (prevIds.length > 0 && currentIds.length > 0) { + const currentOldest = currentIds[0] + if (prevIds[0] !== currentOldest) { + const trimmedCount = prevIds.indexOf(currentOldest) + if (trimmedCount > 0) { + newOffset = (prevOffset + trimmedCount) % RUN_ID_COLORS.length + } + } + } + + const colorMap = new Map() + for (let i = 0; i < currentIds.length; i++) { + const colorIndex = (newOffset + i) % RUN_ID_COLORS.length + colorMap.set(currentIds[i], RUN_ID_COLORS[colorIndex]) + } + + colorStateRef.current = { executionIds: currentIds, offset: newOffset } + + return colorMap }, [allWorkflowEntries]) /** @@ -1128,7 +1137,7 @@ export function Terminal() { {uniqueRunIds.map((runId, index) => { const isSelected = filters.runIds.has(runId) - const runIdColor = getRunIdColor(runId, executionIdOrderMap) + const runIdColor = getRunIdColor(runId, executionColorMap) return ( {formatRunId(runId)} @@ -1335,7 +1344,7 @@ export function Terminal() { const statusInfo = getStatusInfo(entry.success, entry.error) const isSelected = selectedEntry?.id === entry.id const BlockIcon = getBlockIcon(entry.blockType) - const runIdColor = getRunIdColor(entry.executionId, executionIdOrderMap) + const runIdColor = getRunIdColor(entry.executionId, executionColorMap) return (
    {formatRunId(entry.executionId)} @@ -1411,7 +1420,7 @@ export function Terminal() { ROW_TEXT_CLASS )} > - {formatTimestamp(entry.timestamp)} + {formatTimeWithSeconds(new Date(entry.timestamp))}
    ) diff --git a/apps/sim/lib/core/utils/formatting.ts b/apps/sim/lib/core/utils/formatting.ts index 3d6d1e9029..3fcf3d6f1b 100644 --- a/apps/sim/lib/core/utils/formatting.ts +++ b/apps/sim/lib/core/utils/formatting.ts @@ -102,6 +102,22 @@ export function formatTime(date: Date): string { }) } +/** + * Format a time with seconds and timezone + * @param date - The date to format + * @param includeTimezone - Whether to include the timezone abbreviation + * @returns A formatted time string in the format "h:mm:ss AM/PM TZ" + */ +export function formatTimeWithSeconds(date: Date, includeTimezone = true): string { + return date.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hour12: true, + timeZoneName: includeTimezone ? 'short' : undefined, + }) +} + /** * Format an ISO timestamp into a compact format for UI display * @param iso - ISO timestamp string From 3f1dccd6aa145decd8796cdb16156654996a49a6 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 14 Jan 2026 13:01:26 -0800 Subject: [PATCH 20/36] fix(batch-add): on batch add persist subblock values (#2819) * fix(batch-add): on batch add persist subblock values * consolidate merge subblock * consolidate more code --- apps/sim/background/webhook-execution.ts | 2 +- apps/sim/lib/workflows/subblocks.ts | 80 ++++++++++++++++++ apps/sim/socket/database/operations.ts | 43 ++++++---- apps/sim/stores/workflows/server-utils.ts | 70 +--------------- apps/sim/stores/workflows/utils.ts | 92 ++++++++------------- apps/sim/stores/workflows/workflow/store.ts | 4 +- 6 files changed, 147 insertions(+), 144 deletions(-) create mode 100644 apps/sim/lib/workflows/subblocks.ts diff --git a/apps/sim/background/webhook-execution.ts b/apps/sim/background/webhook-execution.ts index 55cb3e0300..2a97480f33 100644 --- a/apps/sim/background/webhook-execution.ts +++ b/apps/sim/background/webhook-execution.ts @@ -172,7 +172,7 @@ async function executeWebhookJobInternal( const workflowVariables = (wfRows[0]?.variables as Record) || {} // Merge subblock states (matching workflow-execution pattern) - const mergedStates = mergeSubblockState(blocks, {}) + const mergedStates = mergeSubblockState(blocks) // Create serialized workflow const serializer = new Serializer() diff --git a/apps/sim/lib/workflows/subblocks.ts b/apps/sim/lib/workflows/subblocks.ts new file mode 100644 index 0000000000..6f5bdb9222 --- /dev/null +++ b/apps/sim/lib/workflows/subblocks.ts @@ -0,0 +1,80 @@ +import type { BlockState, SubBlockState } from '@/stores/workflows/workflow/types' + +export const DEFAULT_SUBBLOCK_TYPE = 'short-input' + +/** + * Merges subblock values into the provided subblock structures. + * Falls back to a default subblock shape when a value has no structure. + * @param subBlocks - Existing subblock definitions from the workflow + * @param values - Stored subblock values keyed by subblock id + * @returns Merged subblock structures with updated values + */ +export function mergeSubBlockValues( + subBlocks: Record | undefined, + values: Record | undefined +): Record { + const merged = { ...(subBlocks || {}) } as Record + + if (!values) return merged + + Object.entries(values).forEach(([subBlockId, value]) => { + if (merged[subBlockId] && typeof merged[subBlockId] === 'object') { + merged[subBlockId] = { + ...(merged[subBlockId] as Record), + value, + } + return + } + + merged[subBlockId] = { + id: subBlockId, + type: DEFAULT_SUBBLOCK_TYPE, + value, + } + }) + + return merged +} + +/** + * Merges workflow block states with explicit subblock values while maintaining block structure. + * Values that are null or undefined do not override existing subblock values. + * @param blocks - Block configurations from workflow state + * @param subBlockValues - Subblock values keyed by blockId -> subBlockId -> value + * @param blockId - Optional specific block ID to merge (merges all if not provided) + * @returns Merged block states with updated subblocks + */ +export function mergeSubblockStateWithValues( + blocks: Record, + subBlockValues: Record> = {}, + blockId?: string +): Record { + const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks + + return Object.entries(blocksToProcess).reduce( + (acc, [id, block]) => { + if (!block) { + return acc + } + + const blockSubBlocks = block.subBlocks || {} + const blockValues = subBlockValues[id] || {} + const filteredValues = Object.fromEntries( + Object.entries(blockValues).filter(([, value]) => value !== null && value !== undefined) + ) + + const mergedSubBlocks = mergeSubBlockValues(blockSubBlocks, filteredValues) as Record< + string, + SubBlockState + > + + acc[id] = { + ...block, + subBlocks: mergedSubBlocks, + } + + return acc + }, + {} as Record + ) +} diff --git a/apps/sim/socket/database/operations.ts b/apps/sim/socket/database/operations.ts index 1f52d46ef9..f5960e4b59 100644 --- a/apps/sim/socket/database/operations.ts +++ b/apps/sim/socket/database/operations.ts @@ -7,6 +7,7 @@ import postgres from 'postgres' import { env } from '@/lib/core/config/env' import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' +import { mergeSubBlockValues } from '@/lib/workflows/subblocks' import { BLOCK_OPERATIONS, BLOCKS_OPERATIONS, @@ -455,7 +456,7 @@ async function handleBlocksOperationTx( } case BLOCKS_OPERATIONS.BATCH_ADD_BLOCKS: { - const { blocks, edges, loops, parallels } = payload + const { blocks, edges, loops, parallels, subBlockValues } = payload logger.info(`Batch adding blocks to workflow ${workflowId}`, { blockCount: blocks?.length || 0, @@ -465,22 +466,30 @@ async function handleBlocksOperationTx( }) if (blocks && blocks.length > 0) { - const blockValues = blocks.map((block: Record) => ({ - id: block.id as string, - workflowId, - type: block.type as string, - name: block.name as string, - positionX: (block.position as { x: number; y: number }).x, - positionY: (block.position as { x: number; y: number }).y, - data: (block.data as Record) || {}, - subBlocks: (block.subBlocks as Record) || {}, - outputs: (block.outputs as Record) || {}, - enabled: (block.enabled as boolean) ?? true, - horizontalHandles: (block.horizontalHandles as boolean) ?? true, - advancedMode: (block.advancedMode as boolean) ?? false, - triggerMode: (block.triggerMode as boolean) ?? false, - height: (block.height as number) || 0, - })) + const blockValues = blocks.map((block: Record) => { + const blockId = block.id as string + const mergedSubBlocks = mergeSubBlockValues( + block.subBlocks as Record, + subBlockValues?.[blockId] + ) + + return { + id: blockId, + workflowId, + type: block.type as string, + name: block.name as string, + positionX: (block.position as { x: number; y: number }).x, + positionY: (block.position as { x: number; y: number }).y, + data: (block.data as Record) || {}, + subBlocks: mergedSubBlocks, + outputs: (block.outputs as Record) || {}, + enabled: (block.enabled as boolean) ?? true, + horizontalHandles: (block.horizontalHandles as boolean) ?? true, + advancedMode: (block.advancedMode as boolean) ?? false, + triggerMode: (block.triggerMode as boolean) ?? false, + height: (block.height as number) || 0, + } + }) await tx.insert(workflowBlocks).values(blockValues) diff --git a/apps/sim/stores/workflows/server-utils.ts b/apps/sim/stores/workflows/server-utils.ts index 3f4e1641ca..5e8fa19f07 100644 --- a/apps/sim/stores/workflows/server-utils.ts +++ b/apps/sim/stores/workflows/server-utils.ts @@ -8,7 +8,8 @@ * or React hooks, making it safe for use in Next.js API routes. */ -import type { BlockState, SubBlockState } from '@/stores/workflows/workflow/types' +import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks' +import type { BlockState } from '@/stores/workflows/workflow/types' /** * Server-safe version of mergeSubblockState for API routes @@ -26,72 +27,7 @@ export function mergeSubblockState( subBlockValues: Record> = {}, blockId?: string ): Record { - const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks - - return Object.entries(blocksToProcess).reduce( - (acc, [id, block]) => { - // Skip if block is undefined - if (!block) { - return acc - } - - // Initialize subBlocks if not present - const blockSubBlocks = block.subBlocks || {} - - // Get stored values for this block - const blockValues = subBlockValues[id] || {} - - // Create a deep copy of the block's subBlocks to maintain structure - const mergedSubBlocks = Object.entries(blockSubBlocks).reduce( - (subAcc, [subBlockId, subBlock]) => { - // Skip if subBlock is undefined - if (!subBlock) { - return subAcc - } - - // Get the stored value for this subblock - const storedValue = blockValues[subBlockId] - - // Create a new subblock object with the same structure but updated value - subAcc[subBlockId] = { - ...subBlock, - value: storedValue !== undefined && storedValue !== null ? storedValue : subBlock.value, - } - - return subAcc - }, - {} as Record - ) - - // Return the full block state with updated subBlocks - acc[id] = { - ...block, - subBlocks: mergedSubBlocks, - } - - // Add any values that exist in the provided values but aren't in the block structure - // This handles cases where block config has been updated but values still exist - Object.entries(blockValues).forEach(([subBlockId, value]) => { - if (!mergedSubBlocks[subBlockId] && value !== null && value !== undefined) { - // Create a minimal subblock structure - mergedSubBlocks[subBlockId] = { - id: subBlockId, - type: 'short-input', // Default type that's safe to use - value: value, - } - } - }) - - // Update the block with the final merged subBlocks (including orphaned values) - acc[id] = { - ...block, - subBlocks: mergedSubBlocks, - } - - return acc - }, - {} as Record - ) + return mergeSubblockStateWithValues(blocks, subBlockValues, blockId) } /** diff --git a/apps/sim/stores/workflows/utils.ts b/apps/sim/stores/workflows/utils.ts index c0b3f3a9a4..6fb1f71798 100644 --- a/apps/sim/stores/workflows/utils.ts +++ b/apps/sim/stores/workflows/utils.ts @@ -1,20 +1,7 @@ import type { Edge } from 'reactflow' import { v4 as uuidv4 } from 'uuid' - -export function filterNewEdges(edgesToAdd: Edge[], currentEdges: Edge[]): Edge[] { - return edgesToAdd.filter((edge) => { - if (edge.source === edge.target) return false - return !currentEdges.some( - (e) => - e.source === edge.source && - e.sourceHandle === edge.sourceHandle && - e.target === edge.target && - e.targetHandle === edge.targetHandle - ) - }) -} - import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' +import { mergeSubBlockValues, mergeSubblockStateWithValues } from '@/lib/workflows/subblocks' import { getBlock } from '@/blocks' import { normalizeName } from '@/executor/constants' import { useSubBlockStore } from '@/stores/workflows/subblock/store' @@ -32,6 +19,19 @@ const WEBHOOK_SUBBLOCK_FIELDS = ['webhookId', 'triggerPath'] export { normalizeName } +export function filterNewEdges(edgesToAdd: Edge[], currentEdges: Edge[]): Edge[] { + return edgesToAdd.filter((edge) => { + if (edge.source === edge.target) return false + return !currentEdges.some( + (e) => + e.source === edge.source && + e.sourceHandle === edge.sourceHandle && + e.target === edge.target && + e.targetHandle === edge.targetHandle + ) + }) +} + export interface RegeneratedState { blocks: Record edges: Edge[] @@ -201,27 +201,20 @@ export function prepareDuplicateBlockState(options: PrepareDuplicateBlockStateOp Object.entries(subBlockValues).filter(([key]) => !WEBHOOK_SUBBLOCK_FIELDS.includes(key)) ) - const mergedSubBlocks: Record = sourceBlock.subBlocks + const baseSubBlocks: Record = sourceBlock.subBlocks ? JSON.parse(JSON.stringify(sourceBlock.subBlocks)) : {} WEBHOOK_SUBBLOCK_FIELDS.forEach((field) => { - if (field in mergedSubBlocks) { - delete mergedSubBlocks[field] + if (field in baseSubBlocks) { + delete baseSubBlocks[field] } }) - Object.entries(filteredSubBlockValues).forEach(([subblockId, value]) => { - if (mergedSubBlocks[subblockId]) { - mergedSubBlocks[subblockId].value = value as SubBlockState['value'] - } else { - mergedSubBlocks[subblockId] = { - id: subblockId, - type: 'short-input', - value: value as SubBlockState['value'], - } - } - }) + const mergedSubBlocks = mergeSubBlockValues(baseSubBlocks, filteredSubBlockValues) as Record< + string, + SubBlockState + > const block: BlockState = { id: newId, @@ -256,11 +249,16 @@ export function mergeSubblockState( workflowId?: string, blockId?: string ): Record { - const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks const subBlockStore = useSubBlockStore.getState() const workflowSubblockValues = workflowId ? subBlockStore.workflowValues[workflowId] || {} : {} + if (workflowId) { + return mergeSubblockStateWithValues(blocks, workflowSubblockValues, blockId) + } + + const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks + return Object.entries(blocksToProcess).reduce( (acc, [id, block]) => { if (!block) { @@ -339,9 +337,15 @@ export async function mergeSubblockStateAsync( workflowId?: string, blockId?: string ): Promise> { - const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks const subBlockStore = useSubBlockStore.getState() + if (workflowId) { + const workflowValues = subBlockStore.workflowValues[workflowId] || {} + return mergeSubblockStateWithValues(blocks, workflowValues, blockId) + } + + const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks + // Process blocks in parallel for better performance const processedBlockEntries = await Promise.all( Object.entries(blocksToProcess).map(async ([id, block]) => { @@ -358,16 +362,7 @@ export async function mergeSubblockStateAsync( return null } - let storedValue = null - - if (workflowId) { - const workflowValues = subBlockStore.workflowValues[workflowId] - if (workflowValues?.[id]) { - storedValue = workflowValues[id][subBlockId] - } - } else { - storedValue = subBlockStore.getValue(id, subBlockId) - } + const storedValue = subBlockStore.getValue(id, subBlockId) return [ subBlockId, @@ -386,23 +381,6 @@ export async function mergeSubblockStateAsync( subBlockEntries.filter((entry): entry is readonly [string, SubBlockState] => entry !== null) ) as Record - // Add any values that exist in the store but aren't in the block structure - // This handles cases where block config has been updated but values still exist - // IMPORTANT: This includes runtime subblock IDs like webhookId, triggerPath, etc. - if (workflowId) { - const workflowValues = subBlockStore.workflowValues[workflowId] - const blockValues = workflowValues?.[id] || {} - Object.entries(blockValues).forEach(([subBlockId, value]) => { - if (!mergedSubBlocks[subBlockId] && value !== null && value !== undefined) { - mergedSubBlocks[subBlockId] = { - id: subBlockId, - type: 'short-input', - value: value as SubBlockState['value'], - } - } - }) - } - // Return the full block state with updated subBlocks (including orphaned values) return [ id, diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 789e83695e..5d2c21563c 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -639,7 +639,8 @@ export const useWorkflowStore = create()( const newName = getUniqueBlockName(block.name, get().blocks) - const mergedBlock = mergeSubblockState(get().blocks, id)[id] + const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId + const mergedBlock = mergeSubblockState(get().blocks, activeWorkflowId || undefined, id)[id] const newSubBlocks = Object.entries(mergedBlock.subBlocks).reduce( (acc, [subId, subBlock]) => ({ @@ -668,7 +669,6 @@ export const useWorkflowStore = create()( parallels: get().generateParallelBlocks(), } - const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId if (activeWorkflowId) { const subBlockValues = useSubBlockStore.getState().workflowValues[activeWorkflowId]?.[id] || {} From 6c8c3d63684de9f9f73dcf5984917673d55cd795 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 14 Jan 2026 15:52:27 -0800 Subject: [PATCH 21/36] feat(reorder): allow workflow/folder reordering (#2818) * feat(reorder): allow workflow/folder reordering * progress * fix edge cases * add migration * fix bun lock * updated to use brand tertiary color, allow worfklows to be dropped above/below folders at the same level * cahnged color, removed flicker on folder container * optimized * ack pr comments * removed empty placeholder images for drag, removed redundant local sanitization helper --------- Co-authored-by: waleed --- apps/sim/app/api/folders/[id]/route.ts | 7 +- apps/sim/app/api/folders/reorder/route.ts | 91 + apps/sim/app/api/folders/route.ts | 35 +- apps/sim/app/api/workflows/[id]/route.ts | 5 +- apps/sim/app/api/workflows/reorder/route.ts | 91 + apps/sim/app/api/workflows/route.ts | 30 +- apps/sim/app/api/workspaces/route.ts | 58 +- .../components/folder-item/folder-item.tsx | 30 +- .../workflow-item/workflow-item.tsx | 73 +- .../workflow-list/workflow-list.tsx | 269 +- .../w/components/sidebar/hooks/index.ts | 2 +- .../components/sidebar/hooks/use-drag-drop.ts | 606 +- .../w/hooks/use-export-workspace.ts | 1 + .../w/hooks/use-import-workflow.ts | 67 +- .../w/hooks/use-import-workspace.ts | 64 +- apps/sim/hooks/queries/folders.ts | 68 +- apps/sim/hooks/queries/workflows.ts | 134 +- .../lib/workflows/operations/import-export.ts | 30 +- .../workflows/sanitization/json-sanitizer.ts | 2 + apps/sim/stores/workflows/registry/store.ts | 6 +- apps/sim/stores/workflows/registry/types.ts | 1 + bun.lock | 1 - .../0141_daffy_marten_broadcloak.sql | 2 + .../db/migrations/meta/0141_snapshot.json | 10267 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 2 + 26 files changed, 11505 insertions(+), 444 deletions(-) create mode 100644 apps/sim/app/api/folders/reorder/route.ts create mode 100644 apps/sim/app/api/workflows/reorder/route.ts create mode 100644 packages/db/migrations/0141_daffy_marten_broadcloak.sql create mode 100644 packages/db/migrations/meta/0141_snapshot.json diff --git a/apps/sim/app/api/folders/[id]/route.ts b/apps/sim/app/api/folders/[id]/route.ts index ebd44f9816..35d41f6939 100644 --- a/apps/sim/app/api/folders/[id]/route.ts +++ b/apps/sim/app/api/folders/[id]/route.ts @@ -14,6 +14,7 @@ const updateFolderSchema = z.object({ color: z.string().optional(), isExpanded: z.boolean().optional(), parentId: z.string().nullable().optional(), + sortOrder: z.number().int().min(0).optional(), }) // PUT - Update a folder @@ -38,7 +39,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: `Validation failed: ${errorMessages}` }, { status: 400 }) } - const { name, color, isExpanded, parentId } = validationResult.data + const { name, color, isExpanded, parentId, sortOrder } = validationResult.data // Verify the folder exists const existingFolder = await db @@ -81,12 +82,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ } } - // Update the folder - const updates: any = { updatedAt: new Date() } + const updates: Record = { updatedAt: new Date() } if (name !== undefined) updates.name = name.trim() if (color !== undefined) updates.color = color if (isExpanded !== undefined) updates.isExpanded = isExpanded if (parentId !== undefined) updates.parentId = parentId || null + if (sortOrder !== undefined) updates.sortOrder = sortOrder const [updatedFolder] = await db .update(workflowFolder) diff --git a/apps/sim/app/api/folders/reorder/route.ts b/apps/sim/app/api/folders/reorder/route.ts new file mode 100644 index 0000000000..653d830165 --- /dev/null +++ b/apps/sim/app/api/folders/reorder/route.ts @@ -0,0 +1,91 @@ +import { db } from '@sim/db' +import { workflowFolder } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq, inArray } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { generateRequestId } from '@/lib/core/utils/request' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('FolderReorderAPI') + +const ReorderSchema = z.object({ + workspaceId: z.string(), + updates: z.array( + z.object({ + id: z.string(), + sortOrder: z.number().int().min(0), + parentId: z.string().nullable().optional(), + }) + ), +}) + +export async function PUT(req: NextRequest) { + const requestId = generateRequestId() + const session = await getSession() + + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthorized folder reorder attempt`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + try { + const body = await req.json() + const { workspaceId, updates } = ReorderSchema.parse(body) + + const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (!permission || permission === 'read') { + logger.warn( + `[${requestId}] User ${session.user.id} lacks write permission for workspace ${workspaceId}` + ) + return NextResponse.json({ error: 'Write access required' }, { status: 403 }) + } + + const folderIds = updates.map((u) => u.id) + const existingFolders = await db + .select({ id: workflowFolder.id, workspaceId: workflowFolder.workspaceId }) + .from(workflowFolder) + .where(inArray(workflowFolder.id, folderIds)) + + const validIds = new Set( + existingFolders.filter((f) => f.workspaceId === workspaceId).map((f) => f.id) + ) + + const validUpdates = updates.filter((u) => validIds.has(u.id)) + + if (validUpdates.length === 0) { + return NextResponse.json({ error: 'No valid folders to update' }, { status: 400 }) + } + + await db.transaction(async (tx) => { + for (const update of validUpdates) { + const updateData: Record = { + sortOrder: update.sortOrder, + updatedAt: new Date(), + } + if (update.parentId !== undefined) { + updateData.parentId = update.parentId + } + await tx.update(workflowFolder).set(updateData).where(eq(workflowFolder.id, update.id)) + } + }) + + logger.info( + `[${requestId}] Reordered ${validUpdates.length} folders in workspace ${workspaceId}` + ) + + return NextResponse.json({ success: true, updated: validUpdates.length }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid folder reorder data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Error reordering folders`, error) + return NextResponse.json({ error: 'Failed to reorder folders' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/folders/route.ts b/apps/sim/app/api/folders/route.ts index e976f1a945..13f07f520f 100644 --- a/apps/sim/app/api/folders/route.ts +++ b/apps/sim/app/api/folders/route.ts @@ -58,7 +58,7 @@ export async function POST(request: NextRequest) { } const body = await request.json() - const { name, workspaceId, parentId, color } = body + const { name, workspaceId, parentId, color, sortOrder: providedSortOrder } = body if (!name || !workspaceId) { return NextResponse.json({ error: 'Name and workspace ID are required' }, { status: 400 }) @@ -81,25 +81,26 @@ export async function POST(request: NextRequest) { // Generate a new ID const id = crypto.randomUUID() - // Use transaction to ensure sortOrder consistency const newFolder = await db.transaction(async (tx) => { - // Get the next sort order for the parent (or root level) - // Consider all folders in the workspace, not just those created by current user - const existingFolders = await tx - .select({ sortOrder: workflowFolder.sortOrder }) - .from(workflowFolder) - .where( - and( - eq(workflowFolder.workspaceId, workspaceId), - parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId) + let sortOrder: number + if (providedSortOrder !== undefined) { + sortOrder = providedSortOrder + } else { + const existingFolders = await tx + .select({ sortOrder: workflowFolder.sortOrder }) + .from(workflowFolder) + .where( + and( + eq(workflowFolder.workspaceId, workspaceId), + parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId) + ) ) - ) - .orderBy(desc(workflowFolder.sortOrder)) - .limit(1) + .orderBy(desc(workflowFolder.sortOrder)) + .limit(1) - const nextSortOrder = existingFolders.length > 0 ? existingFolders[0].sortOrder + 1 : 0 + sortOrder = existingFolders.length > 0 ? existingFolders[0].sortOrder + 1 : 0 + } - // Insert the new folder within the same transaction const [folder] = await tx .insert(workflowFolder) .values({ @@ -109,7 +110,7 @@ export async function POST(request: NextRequest) { workspaceId, parentId: parentId || null, color: color || '#6B7280', - sortOrder: nextSortOrder, + sortOrder, }) .returning() diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index 968593e94f..0172f6d682 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -20,6 +20,7 @@ const UpdateWorkflowSchema = z.object({ description: z.string().optional(), color: z.string().optional(), folderId: z.string().nullable().optional(), + sortOrder: z.number().int().min(0).optional(), }) /** @@ -438,12 +439,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } - // Build update object - const updateData: any = { updatedAt: new Date() } + const updateData: Record = { updatedAt: new Date() } if (updates.name !== undefined) updateData.name = updates.name if (updates.description !== undefined) updateData.description = updates.description if (updates.color !== undefined) updateData.color = updates.color if (updates.folderId !== undefined) updateData.folderId = updates.folderId + if (updates.sortOrder !== undefined) updateData.sortOrder = updates.sortOrder // Update the workflow const [updatedWorkflow] = await db diff --git a/apps/sim/app/api/workflows/reorder/route.ts b/apps/sim/app/api/workflows/reorder/route.ts new file mode 100644 index 0000000000..3989be4e99 --- /dev/null +++ b/apps/sim/app/api/workflows/reorder/route.ts @@ -0,0 +1,91 @@ +import { db } from '@sim/db' +import { workflow } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq, inArray } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { generateRequestId } from '@/lib/core/utils/request' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('WorkflowReorderAPI') + +const ReorderSchema = z.object({ + workspaceId: z.string(), + updates: z.array( + z.object({ + id: z.string(), + sortOrder: z.number().int().min(0), + folderId: z.string().nullable().optional(), + }) + ), +}) + +export async function PUT(req: NextRequest) { + const requestId = generateRequestId() + const session = await getSession() + + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthorized reorder attempt`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + try { + const body = await req.json() + const { workspaceId, updates } = ReorderSchema.parse(body) + + const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (!permission || permission === 'read') { + logger.warn( + `[${requestId}] User ${session.user.id} lacks write permission for workspace ${workspaceId}` + ) + return NextResponse.json({ error: 'Write access required' }, { status: 403 }) + } + + const workflowIds = updates.map((u) => u.id) + const existingWorkflows = await db + .select({ id: workflow.id, workspaceId: workflow.workspaceId }) + .from(workflow) + .where(inArray(workflow.id, workflowIds)) + + const validIds = new Set( + existingWorkflows.filter((w) => w.workspaceId === workspaceId).map((w) => w.id) + ) + + const validUpdates = updates.filter((u) => validIds.has(u.id)) + + if (validUpdates.length === 0) { + return NextResponse.json({ error: 'No valid workflows to update' }, { status: 400 }) + } + + await db.transaction(async (tx) => { + for (const update of validUpdates) { + const updateData: Record = { + sortOrder: update.sortOrder, + updatedAt: new Date(), + } + if (update.folderId !== undefined) { + updateData.folderId = update.folderId + } + await tx.update(workflow).set(updateData).where(eq(workflow.id, update.id)) + } + }) + + logger.info( + `[${requestId}] Reordered ${validUpdates.length} workflows in workspace ${workspaceId}` + ) + + return NextResponse.json({ success: true, updated: validUpdates.length }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid reorder data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Error reordering workflows`, error) + return NextResponse.json({ error: 'Failed to reorder workflows' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts index 81d4c885b9..407e9f0368 100644 --- a/apps/sim/app/api/workflows/route.ts +++ b/apps/sim/app/api/workflows/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { eq } from 'drizzle-orm' +import { and, eq, isNull, max } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' @@ -17,6 +17,7 @@ const CreateWorkflowSchema = z.object({ color: z.string().optional().default('#3972F6'), workspaceId: z.string().optional(), folderId: z.string().nullable().optional(), + sortOrder: z.number().int().optional(), }) // GET /api/workflows - Get workflows for user (optionally filtered by workspaceId) @@ -89,7 +90,14 @@ export async function POST(req: NextRequest) { try { const body = await req.json() - const { name, description, color, workspaceId, folderId } = CreateWorkflowSchema.parse(body) + const { + name, + description, + color, + workspaceId, + folderId, + sortOrder: providedSortOrder, + } = CreateWorkflowSchema.parse(body) if (workspaceId) { const workspacePermission = await getUserEntityPermissions( @@ -127,11 +135,28 @@ export async function POST(req: NextRequest) { // Silently fail }) + let sortOrder: number + if (providedSortOrder !== undefined) { + sortOrder = providedSortOrder + } else { + const folderCondition = folderId ? eq(workflow.folderId, folderId) : isNull(workflow.folderId) + const [maxResult] = await db + .select({ maxOrder: max(workflow.sortOrder) }) + .from(workflow) + .where( + workspaceId + ? and(eq(workflow.workspaceId, workspaceId), folderCondition) + : and(eq(workflow.userId, session.user.id), folderCondition) + ) + sortOrder = (maxResult?.maxOrder ?? -1) + 1 + } + await db.insert(workflow).values({ id: workflowId, userId: session.user.id, workspaceId: workspaceId || null, folderId: folderId || null, + sortOrder, name, description, color, @@ -152,6 +177,7 @@ export async function POST(req: NextRequest) { color, workspaceId, folderId, + sortOrder, createdAt: now, updatedAt: now, }) diff --git a/apps/sim/app/api/workspaces/route.ts b/apps/sim/app/api/workspaces/route.ts index f9172d9c30..492922b79b 100644 --- a/apps/sim/app/api/workspaces/route.ts +++ b/apps/sim/app/api/workspaces/route.ts @@ -13,6 +13,7 @@ const logger = createLogger('Workspaces') const createWorkspaceSchema = z.object({ name: z.string().trim().min(1, 'Name is required'), + skipDefaultWorkflow: z.boolean().optional().default(false), }) // Get all workspaces for the current user @@ -63,9 +64,9 @@ export async function POST(req: Request) { } try { - const { name } = createWorkspaceSchema.parse(await req.json()) + const { name, skipDefaultWorkflow } = createWorkspaceSchema.parse(await req.json()) - const newWorkspace = await createWorkspace(session.user.id, name) + const newWorkspace = await createWorkspace(session.user.id, name, skipDefaultWorkflow) return NextResponse.json({ workspace: newWorkspace }) } catch (error) { @@ -80,7 +81,7 @@ async function createDefaultWorkspace(userId: string, userName?: string | null) return createWorkspace(userId, workspaceName) } -async function createWorkspace(userId: string, name: string) { +async function createWorkspace(userId: string, name: string, skipDefaultWorkflow = false) { const workspaceId = crypto.randomUUID() const workflowId = crypto.randomUUID() const now = new Date() @@ -97,7 +98,6 @@ async function createWorkspace(userId: string, name: string) { updatedAt: now, }) - // Create admin permissions for the workspace owner await tx.insert(permissions).values({ id: crypto.randomUUID(), entityType: 'workspace' as const, @@ -108,37 +108,41 @@ async function createWorkspace(userId: string, name: string) { updatedAt: now, }) - // Create initial workflow for the workspace (empty canvas) - // Create the workflow - await tx.insert(workflow).values({ - id: workflowId, - userId, - workspaceId, - folderId: null, - name: 'default-agent', - description: 'Your first workflow - start building here!', - color: '#3972F6', - lastSynced: now, - createdAt: now, - updatedAt: now, - isDeployed: false, - runCount: 0, - variables: {}, - }) + if (!skipDefaultWorkflow) { + await tx.insert(workflow).values({ + id: workflowId, + userId, + workspaceId, + folderId: null, + name: 'default-agent', + description: 'Your first workflow - start building here!', + color: '#3972F6', + lastSynced: now, + createdAt: now, + updatedAt: now, + isDeployed: false, + runCount: 0, + variables: {}, + }) + } logger.info( - `Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}` + skipDefaultWorkflow + ? `Created workspace ${workspaceId} for user ${userId}` + : `Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}` ) }) - const { workflowState } = buildDefaultWorkflowArtifacts() - const seedResult = await saveWorkflowToNormalizedTables(workflowId, workflowState) + if (!skipDefaultWorkflow) { + const { workflowState } = buildDefaultWorkflowArtifacts() + const seedResult = await saveWorkflowToNormalizedTables(workflowId, workflowState) - if (!seedResult.success) { - throw new Error(seedResult.error || 'Failed to seed default workflow state') + if (!seedResult.success) { + throw new Error(seedResult.error || 'Failed to seed default workflow state') + } } } catch (error) { - logger.error(`Failed to create workspace ${workspaceId} with initial workflow:`, error) + logger.error(`Failed to create workspace ${workspaceId}:`, error) throw error } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx index 7bef1cee4b..68719f8815 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx @@ -36,6 +36,8 @@ interface FolderItemProps { onDragEnter?: (e: React.DragEvent) => void onDragLeave?: (e: React.DragEvent) => void } + onDragStart?: () => void + onDragEnd?: () => void } /** @@ -46,7 +48,13 @@ interface FolderItemProps { * @param props - Component props * @returns Folder item with drag and expand support */ -export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) { +export function FolderItem({ + folder, + level, + hoverHandlers, + onDragStart: onDragStartProp, + onDragEnd: onDragEndProp, +}: FolderItemProps) { const params = useParams() const router = useRouter() const workspaceId = params.workspaceId as string @@ -135,11 +143,6 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) { } }, [createFolderMutation, workspaceId, folder.id, expandFolder]) - /** - * Drag start handler - sets folder data for drag operation - * - * @param e - React drag event - */ const onDragStart = useCallback( (e: React.DragEvent) => { if (isEditing) { @@ -149,14 +152,25 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) { e.dataTransfer.setData('folder-id', folder.id) e.dataTransfer.effectAllowed = 'move' + onDragStartProp?.() }, - [folder.id] + [folder.id, onDragStartProp] ) - const { isDragging, shouldPreventClickRef, handleDragStart, handleDragEnd } = useItemDrag({ + const { + isDragging, + shouldPreventClickRef, + handleDragStart, + handleDragEnd: handleDragEndBase, + } = useItemDrag({ onDragStart, }) + const handleDragEnd = useCallback(() => { + handleDragEndBase() + onDragEndProp?.() + }, [handleDragEndBase, onDragEndProp]) + const { isOpen: isContextMenuOpen, position, diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx index ec12c8b294..52aac85189 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx @@ -29,6 +29,8 @@ interface WorkflowItemProps { active: boolean level: number onWorkflowClick: (workflowId: string, shiftKey: boolean, metaKey: boolean) => void + onDragStart?: () => void + onDragEnd?: () => void } /** @@ -38,7 +40,14 @@ interface WorkflowItemProps { * @param props - Component props * @returns Workflow item with drag and selection support */ -export function WorkflowItem({ workflow, active, level, onWorkflowClick }: WorkflowItemProps) { +export function WorkflowItem({ + workflow, + active, + level, + onWorkflowClick, + onDragStart: onDragStartProp, + onDragEnd: onDragEndProp, +}: WorkflowItemProps) { const params = useParams() const workspaceId = params.workspaceId as string const { selectedWorkflows } = useFolderStore() @@ -104,30 +113,7 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf [workflow.id, updateWorkflow] ) - /** - * Drag start handler - handles workflow dragging with multi-selection support - * - * @param e - React drag event - */ - const onDragStart = useCallback( - (e: React.DragEvent) => { - if (isEditing) { - e.preventDefault() - return - } - - const workflowIds = - isSelected && selectedWorkflows.size > 1 ? Array.from(selectedWorkflows) : [workflow.id] - - e.dataTransfer.setData('workflow-ids', JSON.stringify(workflowIds)) - e.dataTransfer.effectAllowed = 'move' - }, - [isSelected, selectedWorkflows, workflow.id] - ) - - const { isDragging, shouldPreventClickRef, handleDragStart, handleDragEnd } = useItemDrag({ - onDragStart, - }) + const isEditingRef = useRef(false) const { isOpen: isContextMenuOpen, @@ -232,6 +218,43 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf itemId: workflow.id, }) + isEditingRef.current = isEditing + + const onDragStart = useCallback( + (e: React.DragEvent) => { + if (isEditingRef.current) { + e.preventDefault() + return + } + + const currentSelection = useFolderStore.getState().selectedWorkflows + const isCurrentlySelected = currentSelection.has(workflow.id) + const workflowIds = + isCurrentlySelected && currentSelection.size > 1 + ? Array.from(currentSelection) + : [workflow.id] + + e.dataTransfer.setData('workflow-ids', JSON.stringify(workflowIds)) + e.dataTransfer.effectAllowed = 'move' + onDragStartProp?.() + }, + [workflow.id, onDragStartProp] + ) + + const { + isDragging, + shouldPreventClickRef, + handleDragStart, + handleDragEnd: handleDragEndBase, + } = useItemDrag({ + onDragStart, + }) + + const handleDragEnd = useCallback(() => { + handleDragEndBase() + onDragEndProp?.() + }, [handleDragEndBase, onDragEndProp]) + /** * Handle double-click on workflow name to enter rename mode */ diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx index 468960c20a..a79a2b2024 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useEffect, useMemo } from 'react' +import { memo, useCallback, useEffect, useMemo } from 'react' import clsx from 'clsx' import { useParams, usePathname } from 'next/navigation' import { FolderItem } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item' @@ -14,9 +14,6 @@ import { useFolderStore } from '@/stores/folders/store' import type { FolderTreeNode } from '@/stores/folders/types' import type { WorkflowMetadata } from '@/stores/workflows/registry/types' -/** - * Constants for tree layout and styling - */ const TREE_SPACING = { INDENT_PER_LEVEL: 20, } as const @@ -29,12 +26,24 @@ interface WorkflowListProps { scrollContainerRef: React.RefObject } -/** - * WorkflowList component displays workflows organized by folders with drag-and-drop support. - * - * @param props - Component props - * @returns Workflow list with folders and drag-drop support - */ +const DropIndicatorLine = memo(function DropIndicatorLine({ + show, + level = 0, +}: { + show: boolean + level?: number +}) { + if (!show) return null + return ( +
    +
    +
    + ) +}) + export function WorkflowList({ regularWorkflows, isLoading = false, @@ -48,20 +57,21 @@ export function WorkflowList({ const workflowId = params.workflowId as string const { isLoading: foldersLoading } = useFolders(workspaceId) - const { getFolderTree, expandedFolders, getFolderPath, setExpanded } = useFolderStore() const { - dropTargetId, + dropIndicator, isDragging, setScrollContainer, + createWorkflowDragHandlers, createFolderDragHandlers, - createItemDragHandlers, - createRootDragHandlers, - createFolderHeaderHoverHandlers, + createEmptyFolderDropZone, + createFolderContentDropZone, + createRootDropZone, + handleDragStart, + handleDragEnd, } = useDragDrop() - // Set scroll container when ref changes useEffect(() => { if (scrollContainerRef.current) { setScrollContainer(scrollContainerRef.current) @@ -76,23 +86,22 @@ export function WorkflowList({ return activeWorkflow?.folderId || null }, [workflowId, regularWorkflows, isLoading, foldersLoading]) - const workflowsByFolder = useMemo( - () => - regularWorkflows.reduce( - (acc, workflow) => { - const folderId = workflow.folderId || 'root' - if (!acc[folderId]) acc[folderId] = [] - acc[folderId].push(workflow) - return acc - }, - {} as Record - ), - [regularWorkflows] - ) + const workflowsByFolder = useMemo(() => { + const grouped = regularWorkflows.reduce( + (acc, workflow) => { + const folderId = workflow.folderId || 'root' + if (!acc[folderId]) acc[folderId] = [] + acc[folderId].push(workflow) + return acc + }, + {} as Record + ) + for (const folderId of Object.keys(grouped)) { + grouped[folderId].sort((a, b) => a.sortOrder - b.sortOrder) + } + return grouped + }, [regularWorkflows]) - /** - * Build a flat list of all workflow IDs in display order for range selection - */ const orderedWorkflowIds = useMemo(() => { const ids: string[] = [] @@ -106,12 +115,10 @@ export function WorkflowList({ } } - // Collect from folders first for (const folder of folderTree) { collectWorkflowIds(folder) } - // Then collect root workflows const rootWorkflows = workflowsByFolder.root || [] for (const workflow of rootWorkflows) { ids.push(workflow.id) @@ -120,30 +127,24 @@ export function WorkflowList({ return ids }, [folderTree, workflowsByFolder]) - // Workflow selection hook - uses active workflow ID as anchor for range selection const { handleWorkflowClick } = useWorkflowSelection({ workflowIds: orderedWorkflowIds, activeWorkflowId: workflowId, }) const isWorkflowActive = useCallback( - (workflowId: string) => pathname === `/workspace/${workspaceId}/w/${workflowId}`, + (wfId: string) => pathname === `/workspace/${workspaceId}/w/${wfId}`, [pathname, workspaceId] ) - /** - * Auto-expand folders and select active workflow. - */ useEffect(() => { if (!workflowId || isLoading || foldersLoading) return - // Expand folder path to reveal workflow if (activeWorkflowFolderId) { const folderPath = getFolderPath(activeWorkflowFolderId) folderPath.forEach((folder) => setExpanded(folder.id, true)) } - // Select workflow if not already selected const { selectedWorkflows, selectOnly } = useFolderStore.getState() if (!selectedWorkflows.has(workflowId)) { selectOnly(workflowId) @@ -151,23 +152,40 @@ export function WorkflowList({ }, [workflowId, activeWorkflowFolderId, isLoading, foldersLoading, getFolderPath, setExpanded]) const renderWorkflowItem = useCallback( - (workflow: WorkflowMetadata, level: number, parentFolderId: string | null = null) => ( -
    -
    - + (workflow: WorkflowMetadata, level: number, folderId: string | null = null) => { + const showBefore = + dropIndicator?.targetId === workflow.id && dropIndicator?.position === 'before' + const showAfter = + dropIndicator?.targetId === workflow.id && dropIndicator?.position === 'after' + + return ( +
    + +
    + handleDragStart('workflow', folderId)} + onDragEnd={handleDragEnd} + /> +
    +
    -
    - ), - [isWorkflowActive, createItemDragHandlers, handleWorkflowClick] + ) + }, + [ + dropIndicator, + isWorkflowActive, + createWorkflowDragHandlers, + handleWorkflowClick, + handleDragStart, + handleDragEnd, + ] ) const renderFolderSection = useCallback( @@ -179,45 +197,75 @@ export function WorkflowList({ const workflowsInFolder = workflowsByFolder[folder.id] || [] const isExpanded = expandedFolders.has(folder.id) const hasChildren = workflowsInFolder.length > 0 || folder.children.length > 0 - const isDropTarget = dropTargetId === folder.id + + const showBefore = + dropIndicator?.targetId === folder.id && dropIndicator?.position === 'before' + const showAfter = dropIndicator?.targetId === folder.id && dropIndicator?.position === 'after' + const showInside = + dropIndicator?.targetId === folder.id && dropIndicator?.position === 'inside' + + const childItems: Array<{ + type: 'folder' | 'workflow' + id: string + sortOrder: number + data: FolderTreeNode | WorkflowMetadata + }> = [] + for (const childFolder of folder.children) { + childItems.push({ + type: 'folder', + id: childFolder.id, + sortOrder: childFolder.sortOrder, + data: childFolder, + }) + } + for (const workflow of workflowsInFolder) { + childItems.push({ + type: 'workflow', + id: workflow.id, + sortOrder: workflow.sortOrder, + data: workflow, + }) + } + childItems.sort((a, b) => a.sortOrder - b.sortOrder) return ( -
    - {/* Drop target highlight overlay - always rendered for stable DOM */} +
    + + {/* Drop target highlight overlay - covers entire folder section */}
    -
    handleDragStart('folder', parentFolderId)} + onDragEnd={handleDragEnd} />
    + - {isExpanded && hasChildren && ( -
    - {/* Vertical line - positioned to align under folder chevron */} + {isExpanded && (hasChildren || isDragging) && ( +
    - {workflowsInFolder.map((workflow: WorkflowMetadata) => - renderWorkflowItem(workflow, level + 1, folder.id) + {childItems.map((item) => + item.type === 'folder' + ? renderFolderSection(item.data as FolderTreeNode, level + 1, folder.id) + : renderWorkflowItem(item.data as WorkflowMetadata, level + 1, folder.id) + )} + {!hasChildren && isDragging && ( +
    )} - {folder.children.map((childFolder) => ( -
    - {renderFolderSection(childFolder, level + 1, folder.id)} -
    - ))}
    )} @@ -227,29 +275,47 @@ export function WorkflowList({ [ workflowsByFolder, expandedFolders, - dropTargetId, + dropIndicator, isDragging, createFolderDragHandlers, - createItemDragHandlers, - createFolderHeaderHoverHandlers, + createEmptyFolderDropZone, + createFolderContentDropZone, + handleDragStart, + handleDragEnd, renderWorkflowItem, ] ) - const handleRootDragEvents = createRootDragHandlers() + const rootDropZoneHandlers = createRootDropZone() const rootWorkflows = workflowsByFolder.root || [] - const isRootDropTarget = dropTargetId === 'root' - const hasRootWorkflows = rootWorkflows.length > 0 - const hasFolders = folderTree.length > 0 - /** - * Handle click on empty space to revert to active workflow selection - */ + const rootItems = useMemo(() => { + const items: Array<{ + type: 'folder' | 'workflow' + id: string + sortOrder: number + data: FolderTreeNode | WorkflowMetadata + }> = [] + for (const folder of folderTree) { + items.push({ type: 'folder', id: folder.id, sortOrder: folder.sortOrder, data: folder }) + } + for (const workflow of rootWorkflows) { + items.push({ + type: 'workflow', + id: workflow.id, + sortOrder: workflow.sortOrder, + data: workflow, + }) + } + return items.sort((a, b) => a.sortOrder - b.sortOrder) + }, [folderTree, rootWorkflows]) + + const hasRootItems = rootItems.length > 0 + const showRootInside = dropIndicator?.targetId === 'root' && dropIndicator?.position === 'inside' + const handleContainerClick = useCallback( (e: React.MouseEvent) => { - // Only handle clicks directly on the container (empty space) if (e.target !== e.currentTarget) return - const { selectOnly, clearSelection } = useFolderStore.getState() workflowId ? selectOnly(workflowId) : clearSelection() }, @@ -258,36 +324,23 @@ export function WorkflowList({ return (
    - {/* Folders Section */} - {hasFolders && ( -
    - {folderTree.map((folder) => renderFolderSection(folder, 0))} -
    - )} - - {/* Root Workflows Section - Expands to fill remaining space */}
    - {/* Root drop target highlight overlay - always rendered for stable DOM */} + {/* Root drop target highlight overlay */}
    -
    - {rootWorkflows.map((workflow: WorkflowMetadata) => ( - - ))} + {rootItems.map((item) => + item.type === 'folder' + ? renderFolderSection(item.data as FolderTreeNode, 0, null) + : renderWorkflowItem(item.data as WorkflowMetadata, 0, null) + )}
    diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts index ba77cd7d31..beae8fc3a5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts @@ -1,6 +1,6 @@ export { useAutoScroll } from './use-auto-scroll' export { useContextMenu } from './use-context-menu' -export { useDragDrop } from './use-drag-drop' +export { type DropIndicator, useDragDrop } from './use-drag-drop' export { useFolderExpand } from './use-folder-expand' export { useFolderOperations } from './use-folder-operations' export { useItemDrag } from './use-item-drag' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts index 91ec56aa72..d6b4696933 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts @@ -1,47 +1,40 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useParams } from 'next/navigation' -import { useUpdateFolder } from '@/hooks/queries/folders' +import { useReorderFolders } from '@/hooks/queries/folders' +import { useReorderWorkflows } from '@/hooks/queries/workflows' import { useFolderStore } from '@/stores/folders/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' const logger = createLogger('WorkflowList:DragDrop') -/** - * Constants for auto-scroll behavior - */ -const SCROLL_THRESHOLD = 60 // Distance from edge to trigger scroll -const SCROLL_SPEED = 8 // Pixels per frame - -/** - * Constants for folder auto-expand on hover during drag - */ -const HOVER_EXPAND_DELAY = 400 // Milliseconds to wait before expanding folder - -/** - * Custom hook for handling drag and drop operations for workflows and folders. - * Includes auto-scrolling, drop target highlighting, and hover-to-expand. - * - * @returns Drag and drop state and event handlers - */ +const SCROLL_THRESHOLD = 60 +const SCROLL_SPEED = 8 +const HOVER_EXPAND_DELAY = 400 + +export interface DropIndicator { + targetId: string + position: 'before' | 'after' | 'inside' + folderId: string | null +} + export function useDragDrop() { - const [dropTargetId, setDropTargetId] = useState(null) + const [dropIndicator, setDropIndicator] = useState(null) const [isDragging, setIsDragging] = useState(false) const [hoverFolderId, setHoverFolderId] = useState(null) const scrollContainerRef = useRef(null) const scrollIntervalRef = useRef(null) const hoverExpandTimerRef = useRef(null) const lastDragYRef = useRef(0) + const draggedTypeRef = useRef<'workflow' | 'folder' | null>(null) + const draggedSourceFolderRef = useRef(null) const params = useParams() const workspaceId = params.workspaceId as string | undefined - const updateFolderMutation = useUpdateFolder() + const reorderWorkflowsMutation = useReorderWorkflows() + const reorderFoldersMutation = useReorderFolders() const { setExpanded, expandedFolders } = useFolderStore() - const { updateWorkflow } = useWorkflowRegistry() - /** - * Auto-scroll handler - scrolls container when dragging near edges - */ const handleAutoScroll = useCallback(() => { if (!scrollContainerRef.current || !isDragging) return @@ -49,22 +42,17 @@ export function useDragDrop() { const rect = container.getBoundingClientRect() const mouseY = lastDragYRef.current - // Only scroll if mouse is within container bounds if (mouseY < rect.top || mouseY > rect.bottom) return - // Calculate distance from top and bottom edges const distanceFromTop = mouseY - rect.top const distanceFromBottom = rect.bottom - mouseY let scrollDelta = 0 - // Scroll up if near top and not at scroll top if (distanceFromTop < SCROLL_THRESHOLD && container.scrollTop > 0) { const intensity = Math.max(0, Math.min(1, 1 - distanceFromTop / SCROLL_THRESHOLD)) scrollDelta = -SCROLL_SPEED * intensity - } - // Scroll down if near bottom and not at scroll bottom - else if (distanceFromBottom < SCROLL_THRESHOLD) { + } else if (distanceFromBottom < SCROLL_THRESHOLD) { const maxScroll = container.scrollHeight - container.clientHeight if (container.scrollTop < maxScroll) { const intensity = Math.max(0, Math.min(1, 1 - distanceFromBottom / SCROLL_THRESHOLD)) @@ -77,12 +65,9 @@ export function useDragDrop() { } }, [isDragging]) - /** - * Start auto-scroll animation loop - */ useEffect(() => { if (isDragging) { - scrollIntervalRef.current = window.setInterval(handleAutoScroll, 10) // ~100fps for smoother response + scrollIntervalRef.current = window.setInterval(handleAutoScroll, 10) } else { if (scrollIntervalRef.current) { clearInterval(scrollIntervalRef.current) @@ -97,30 +82,17 @@ export function useDragDrop() { } }, [isDragging, handleAutoScroll]) - /** - * Handle hover folder changes - start/clear expand timer - */ useEffect(() => { - // Clear existing timer when hover folder changes if (hoverExpandTimerRef.current) { clearTimeout(hoverExpandTimerRef.current) hoverExpandTimerRef.current = null } - // Don't start timer if not dragging or no folder is hovered - if (!isDragging || !hoverFolderId) { - return - } - - // Don't expand if folder is already expanded - if (expandedFolders.has(hoverFolderId)) { - return - } + if (!isDragging || !hoverFolderId) return + if (expandedFolders.has(hoverFolderId)) return - // Start timer to expand folder after delay hoverExpandTimerRef.current = window.setTimeout(() => { setExpanded(hoverFolderId, true) - logger.info(`Auto-expanded folder ${hoverFolderId} during drag`) }, HOVER_EXPAND_DELAY) return () => { @@ -131,249 +103,471 @@ export function useDragDrop() { } }, [hoverFolderId, isDragging, expandedFolders, setExpanded]) - /** - * Cleanup hover state when dragging stops - */ useEffect(() => { if (!isDragging) { setHoverFolderId(null) + setDropIndicator(null) + draggedTypeRef.current = null } }, [isDragging]) - /** - * Moves one or more workflows to a target folder - * - * @param workflowIds - Array of workflow IDs to move - * @param targetFolderId - Target folder ID or null for root - */ - const handleWorkflowDrop = useCallback( - async (workflowIds: string[], targetFolderId: string | null) => { - if (!workflowIds.length) { - logger.warn('No workflows to move') - return + const calculateDropPosition = useCallback( + (e: React.DragEvent, element: HTMLElement): 'before' | 'after' => { + const rect = element.getBoundingClientRect() + const midY = rect.top + rect.height / 2 + return e.clientY < midY ? 'before' : 'after' + }, + [] + ) + + const calculateFolderDropPosition = useCallback( + (e: React.DragEvent, element: HTMLElement): 'before' | 'inside' | 'after' => { + const rect = element.getBoundingClientRect() + const relativeY = e.clientY - rect.top + const height = rect.height + // Top 25% = before, middle 50% = inside, bottom 25% = after + if (relativeY < height * 0.25) return 'before' + if (relativeY > height * 0.75) return 'after' + return 'inside' + }, + [] + ) + + type SiblingItem = { type: 'folder' | 'workflow'; id: string; sortOrder: number } + + const getDestinationFolderId = useCallback((indicator: DropIndicator): string | null => { + return indicator.position === 'inside' + ? indicator.targetId === 'root' + ? null + : indicator.targetId + : indicator.folderId + }, []) + + const calculateInsertIndex = useCallback( + (remaining: SiblingItem[], indicator: DropIndicator): number => { + return indicator.position === 'inside' + ? remaining.length + : remaining.findIndex((item) => item.id === indicator.targetId) + + (indicator.position === 'after' ? 1 : 0) + }, + [] + ) + + const buildAndSubmitUpdates = useCallback( + async (newOrder: SiblingItem[], destinationFolderId: string | null) => { + const indexed = newOrder.map((item, i) => ({ ...item, sortOrder: i })) + + const folderUpdates = indexed + .filter((item) => item.type === 'folder') + .map((item) => ({ id: item.id, sortOrder: item.sortOrder, parentId: destinationFolderId })) + + const workflowUpdates = indexed + .filter((item) => item.type === 'workflow') + .map((item) => ({ id: item.id, sortOrder: item.sortOrder, folderId: destinationFolderId })) + + await Promise.all( + [ + folderUpdates.length > 0 && + reorderFoldersMutation.mutateAsync({ + workspaceId: workspaceId!, + updates: folderUpdates, + }), + workflowUpdates.length > 0 && + reorderWorkflowsMutation.mutateAsync({ + workspaceId: workspaceId!, + updates: workflowUpdates, + }), + ].filter(Boolean) + ) + }, + [workspaceId, reorderFoldersMutation, reorderWorkflowsMutation] + ) + + const isLeavingElement = useCallback((e: React.DragEvent): boolean => { + const relatedTarget = e.relatedTarget as HTMLElement | null + const currentTarget = e.currentTarget as HTMLElement + return !relatedTarget || !currentTarget.contains(relatedTarget) + }, []) + + const initDragOver = useCallback((e: React.DragEvent, stopPropagation = true) => { + e.preventDefault() + if (stopPropagation) e.stopPropagation() + lastDragYRef.current = e.clientY + setIsDragging(true) + }, []) + + const getSiblingItems = useCallback((folderId: string | null): SiblingItem[] => { + const currentFolders = useFolderStore.getState().folders + const currentWorkflows = useWorkflowRegistry.getState().workflows + return [ + ...Object.values(currentFolders) + .filter((f) => f.parentId === folderId) + .map((f) => ({ type: 'folder' as const, id: f.id, sortOrder: f.sortOrder })), + ...Object.values(currentWorkflows) + .filter((w) => w.folderId === folderId) + .map((w) => ({ type: 'workflow' as const, id: w.id, sortOrder: w.sortOrder })), + ].sort((a, b) => a.sortOrder - b.sortOrder) + }, []) + + const setNormalizedDropIndicator = useCallback( + (indicator: DropIndicator | null) => { + setDropIndicator((prev) => { + let next: DropIndicator | null = indicator + + if (indicator && indicator.position === 'after' && indicator.targetId !== 'root') { + const siblings = getSiblingItems(indicator.folderId) + const currentIdx = siblings.findIndex((s) => s.id === indicator.targetId) + const nextSibling = siblings[currentIdx + 1] + if (nextSibling) { + next = { + targetId: nextSibling.id, + position: 'before', + folderId: indicator.folderId, + } + } + } + + if ( + prev?.targetId === next?.targetId && + prev?.position === next?.position && + prev?.folderId === next?.folderId + ) { + return prev + } + + return next + }) + }, + [getSiblingItems] + ) + + const isNoOpMove = useCallback( + ( + indicator: DropIndicator, + draggedIds: string[], + draggedType: 'folder' | 'workflow', + destinationFolderId: string | null, + currentFolderId: string | null | undefined + ): boolean => { + if (indicator.position !== 'inside' && draggedIds.includes(indicator.targetId)) { + return true + } + if (currentFolderId !== destinationFolderId) { + return false } + const siblingItems = getSiblingItems(destinationFolderId) + const remaining = siblingItems.filter( + (item) => !(item.type === draggedType && draggedIds.includes(item.id)) + ) + const insertAt = calculateInsertIndex(remaining, indicator) + const originalIdx = siblingItems.findIndex( + (item) => item.type === draggedType && item.id === draggedIds[0] + ) + return insertAt === originalIdx + }, + [getSiblingItems, calculateInsertIndex] + ) + + const handleWorkflowDrop = useCallback( + async (workflowIds: string[], indicator: DropIndicator) => { + if (!workflowIds.length || !workspaceId) return try { - await Promise.all( - workflowIds.map((workflowId) => updateWorkflow(workflowId, { folderId: targetFolderId })) + const destinationFolderId = getDestinationFolderId(indicator) + const currentWorkflows = useWorkflowRegistry.getState().workflows + const firstWorkflow = currentWorkflows[workflowIds[0]] + + if ( + isNoOpMove( + indicator, + workflowIds, + 'workflow', + destinationFolderId, + firstWorkflow?.folderId + ) + ) { + return + } + + const siblingItems = getSiblingItems(destinationFolderId) + const movingSet = new Set(workflowIds) + const remaining = siblingItems.filter( + (item) => !(item.type === 'workflow' && movingSet.has(item.id)) ) - logger.info(`Moved ${workflowIds.length} workflow(s)`) + const moving = workflowIds + .map((id) => ({ + type: 'workflow' as const, + id, + sortOrder: currentWorkflows[id]?.sortOrder ?? 0, + })) + .sort((a, b) => a.sortOrder - b.sortOrder) + + const insertAt = calculateInsertIndex(remaining, indicator) + + const newOrder: SiblingItem[] = [ + ...remaining.slice(0, insertAt), + ...moving, + ...remaining.slice(insertAt), + ] + + await buildAndSubmitUpdates(newOrder, destinationFolderId) } catch (error) { - logger.error('Failed to move workflows:', error) + logger.error('Failed to reorder workflows:', error) } }, - [updateWorkflow] + [ + getDestinationFolderId, + getSiblingItems, + calculateInsertIndex, + isNoOpMove, + buildAndSubmitUpdates, + ] ) - /** - * Moves a folder to a new parent folder, with validation - * - * @param draggedFolderId - ID of the folder being moved - * @param targetFolderId - Target folder ID or null for root - */ - const handleFolderMove = useCallback( - async (draggedFolderId: string, targetFolderId: string | null) => { - if (!draggedFolderId) { - logger.warn('No folder to move') - return - } + const handleFolderDrop = useCallback( + async (draggedFolderId: string, indicator: DropIndicator) => { + if (!draggedFolderId || !workspaceId) return try { const folderStore = useFolderStore.getState() - const draggedFolderPath = folderStore.getFolderPath(draggedFolderId) + const currentFolders = folderStore.folders - // Prevent moving folder into its own descendant - if ( - targetFolderId && - draggedFolderPath.some((ancestor) => ancestor.id === targetFolderId) - ) { - logger.info('Cannot move folder into its own descendant') - return - } + const targetParentId = getDestinationFolderId(indicator) - // Prevent moving folder into itself - if (draggedFolderId === targetFolderId) { + if (draggedFolderId === targetParentId) { logger.info('Cannot move folder into itself') return } - if (!workspaceId) { - logger.warn('No workspaceId available for folder move') + if (targetParentId) { + const targetPath = folderStore.getFolderPath(targetParentId) + if (targetPath.some((f) => f.id === draggedFolderId)) { + logger.info('Cannot move folder into its own descendant') + return + } + } + + const draggedFolder = currentFolders[draggedFolderId] + if ( + isNoOpMove( + indicator, + [draggedFolderId], + 'folder', + targetParentId, + draggedFolder?.parentId + ) + ) { return } - await updateFolderMutation.mutateAsync({ - workspaceId, - id: draggedFolderId, - updates: { parentId: targetFolderId }, - }) - logger.info(`Moved folder to ${targetFolderId ? `folder ${targetFolderId}` : 'root'}`) + + const siblingItems = getSiblingItems(targetParentId) + const remaining = siblingItems.filter( + (item) => !(item.type === 'folder' && item.id === draggedFolderId) + ) + + const insertAt = calculateInsertIndex(remaining, indicator) + + const newOrder: SiblingItem[] = [ + ...remaining.slice(0, insertAt), + { type: 'folder', id: draggedFolderId, sortOrder: 0 }, + ...remaining.slice(insertAt), + ] + + await buildAndSubmitUpdates(newOrder, targetParentId) } catch (error) { - logger.error('Failed to move folder:', error) + logger.error('Failed to reorder folder:', error) } }, - [updateFolderMutation, workspaceId] + [ + workspaceId, + getDestinationFolderId, + getSiblingItems, + calculateInsertIndex, + isNoOpMove, + buildAndSubmitUpdates, + ] ) - /** - * Handles drop events for both workflows and folders - * - * @param e - React drag event - * @param targetFolderId - Target folder ID or null for root - */ - const handleFolderDrop = useCallback( - async (e: React.DragEvent, targetFolderId: string | null) => { + const handleDrop = useCallback( + async (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() - setDropTargetId(null) + + const indicator = dropIndicator + setDropIndicator(null) setIsDragging(false) + if (!indicator) return + try { - // Check if dropping workflows const workflowIdsData = e.dataTransfer.getData('workflow-ids') if (workflowIdsData) { const workflowIds = JSON.parse(workflowIdsData) as string[] - await handleWorkflowDrop(workflowIds, targetFolderId) + await handleWorkflowDrop(workflowIds, indicator) return } - // Check if dropping a folder const folderIdData = e.dataTransfer.getData('folder-id') - if (folderIdData && targetFolderId !== folderIdData) { - await handleFolderMove(folderIdData, targetFolderId) + if (folderIdData) { + await handleFolderDrop(folderIdData, indicator) } } catch (error) { logger.error('Failed to handle drop:', error) } }, - [handleWorkflowDrop, handleFolderMove] + [dropIndicator, handleWorkflowDrop, handleFolderDrop] ) - /** - * Creates drag event handlers for a specific folder section - * These handlers are attached to the entire folder section container - * - * @param folderId - Folder ID to create handlers for - * @returns Object containing drag event handlers - */ - const createFolderDragHandlers = useCallback( - (folderId: string) => ({ - onDragEnter: (e: React.DragEvent) => { - e.preventDefault() - setIsDragging(true) + const createWorkflowDragHandlers = useCallback( + (workflowId: string, folderId: string | null) => ({ + onDragOver: (e: React.DragEvent) => { + initDragOver(e) + const isSameFolder = draggedSourceFolderRef.current === folderId + if (isSameFolder) { + const position = calculateDropPosition(e, e.currentTarget) + setNormalizedDropIndicator({ targetId: workflowId, position, folderId }) + } else { + setNormalizedDropIndicator({ + targetId: folderId || 'root', + position: 'inside', + folderId: null, + }) + } }, + onDrop: handleDrop, + }), + [initDragOver, calculateDropPosition, setNormalizedDropIndicator, handleDrop] + ) + + const createFolderDragHandlers = useCallback( + (folderId: string, parentFolderId: string | null) => ({ onDragOver: (e: React.DragEvent) => { - e.preventDefault() - lastDragYRef.current = e.clientY - setDropTargetId(folderId) - setIsDragging(true) + initDragOver(e) + if (draggedTypeRef.current === 'folder') { + const isSameParent = draggedSourceFolderRef.current === parentFolderId + if (isSameParent) { + const position = calculateDropPosition(e, e.currentTarget) + setNormalizedDropIndicator({ targetId: folderId, position, folderId: parentFolderId }) + } else { + setNormalizedDropIndicator({ + targetId: folderId, + position: 'inside', + folderId: parentFolderId, + }) + setHoverFolderId(folderId) + } + } else { + // Workflow being dragged over a folder + const isSameParent = draggedSourceFolderRef.current === parentFolderId + if (isSameParent) { + // Same level - use three zones: top=before, middle=inside, bottom=after + const position = calculateFolderDropPosition(e, e.currentTarget) + setNormalizedDropIndicator({ targetId: folderId, position, folderId: parentFolderId }) + if (position === 'inside') { + setHoverFolderId(folderId) + } else { + setHoverFolderId(null) + } + } else { + // Different container - drop into folder + setNormalizedDropIndicator({ + targetId: folderId, + position: 'inside', + folderId: parentFolderId, + }) + setHoverFolderId(folderId) + } + } }, onDragLeave: (e: React.DragEvent) => { - e.preventDefault() - const relatedTarget = e.relatedTarget as HTMLElement | null - const currentTarget = e.currentTarget as HTMLElement - // Only clear if we're leaving the folder section completely - if (!relatedTarget || !currentTarget.contains(relatedTarget)) { - setDropTargetId(null) - } + if (isLeavingElement(e)) setHoverFolderId(null) }, - onDrop: (e: React.DragEvent) => handleFolderDrop(e, folderId), + onDrop: handleDrop, }), - [handleFolderDrop] + [ + initDragOver, + calculateDropPosition, + calculateFolderDropPosition, + setNormalizedDropIndicator, + isLeavingElement, + handleDrop, + ] ) - /** - * Creates drag event handlers for items (workflows/folders) that belong to a parent folder - * When dragging over an item, highlights the parent folder section - * - * @param parentFolderId - Parent folder ID or null for root - * @returns Object containing drag event handlers - */ - const createItemDragHandlers = useCallback( - (parentFolderId: string | null) => ({ + const createEmptyFolderDropZone = useCallback( + (folderId: string) => ({ onDragOver: (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - lastDragYRef.current = e.clientY - setDropTargetId(parentFolderId || 'root') - setIsDragging(true) + initDragOver(e) + setNormalizedDropIndicator({ targetId: folderId, position: 'inside', folderId }) }, + onDrop: handleDrop, }), - [] + [initDragOver, setNormalizedDropIndicator, handleDrop] ) - /** - * Creates drag event handlers for the root drop zone - * - * @returns Object containing drag event handlers for root - */ - const createRootDragHandlers = useCallback( - () => ({ - onDragEnter: (e: React.DragEvent) => { - e.preventDefault() - setIsDragging(true) - }, + const createFolderContentDropZone = useCallback( + (folderId: string) => ({ onDragOver: (e: React.DragEvent) => { e.preventDefault() + e.stopPropagation() lastDragYRef.current = e.clientY - setDropTargetId('root') setIsDragging(true) - }, - onDragLeave: (e: React.DragEvent) => { - e.preventDefault() - const relatedTarget = e.relatedTarget as HTMLElement | null - const currentTarget = e.currentTarget as HTMLElement - // Only clear if we're leaving the root completely - if (!relatedTarget || !currentTarget.contains(relatedTarget)) { - setDropTargetId(null) + if (e.target === e.currentTarget && draggedSourceFolderRef.current !== folderId) { + setNormalizedDropIndicator({ targetId: folderId, position: 'inside', folderId: null }) } }, - onDrop: (e: React.DragEvent) => handleFolderDrop(e, null), + onDrop: handleDrop, }), - [handleFolderDrop] + [setNormalizedDropIndicator, handleDrop] ) - /** - * Creates drag event handlers for folder header (the clickable part) - * These handlers trigger folder expansion on hover during drag - * - * @param folderId - Folder ID to handle hover for - * @returns Object containing drag event handlers for folder header - */ - const createFolderHeaderHoverHandlers = useCallback( - (folderId: string) => ({ - onDragEnter: (e: React.DragEvent) => { - if (isDragging) { - setHoverFolderId(folderId) + const createRootDropZone = useCallback( + () => ({ + onDragOver: (e: React.DragEvent) => { + initDragOver(e, false) + if (e.target === e.currentTarget) { + setNormalizedDropIndicator({ targetId: 'root', position: 'inside', folderId: null }) } }, onDragLeave: (e: React.DragEvent) => { - const relatedTarget = e.relatedTarget as HTMLElement | null - const currentTarget = e.currentTarget as HTMLElement - // Only clear if we're leaving the folder header completely - if (!relatedTarget || !currentTarget.contains(relatedTarget)) { - setHoverFolderId(null) - } + if (isLeavingElement(e)) setNormalizedDropIndicator(null) }, + onDrop: handleDrop, }), - [isDragging] + [initDragOver, setNormalizedDropIndicator, isLeavingElement, handleDrop] ) - /** - * Set the scroll container ref for auto-scrolling - * - * @param element - Scrollable container element - */ + const handleDragStart = useCallback( + (type: 'workflow' | 'folder', sourceFolderId: string | null) => { + draggedTypeRef.current = type + draggedSourceFolderRef.current = sourceFolderId + setIsDragging(true) + }, + [] + ) + + const handleDragEnd = useCallback(() => { + setIsDragging(false) + setDropIndicator(null) + draggedTypeRef.current = null + draggedSourceFolderRef.current = null + setHoverFolderId(null) + }, []) + const setScrollContainer = useCallback((element: HTMLDivElement | null) => { scrollContainerRef.current = element }, []) return { - dropTargetId, + dropIndicator, isDragging, setScrollContainer, + createWorkflowDragHandlers, createFolderDragHandlers, - createItemDragHandlers, - createRootDragHandlers, - createFolderHeaderHoverHandlers, + createEmptyFolderDropZone, + createFolderContentDropZone, + createRootDropZone, + handleDragStart, + handleDragEnd, } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workspace.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workspace.ts index 1f855d99c1..44fab2c685 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workspace.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workspace.ts @@ -64,6 +64,7 @@ export function useExportWorkspace({ onSuccess }: UseExportWorkspaceProps = {}) id: folder.id, name: folder.name, parentId: folder.parentId, + sortOrder: folder.sortOrder, }) ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts index a5e190f05e..e186f1279c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts @@ -7,6 +7,7 @@ import { extractWorkflowsFromFiles, extractWorkflowsFromZip, parseWorkflowJson, + sanitizePathSegment, } from '@/lib/workflows/operations/import-export' import { folderKeys, useCreateFolder } from '@/hooks/queries/folders' import { useCreateWorkflow, workflowKeys } from '@/hooks/queries/workflows' @@ -40,7 +41,7 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { * Import a single workflow */ const importSingleWorkflow = useCallback( - async (content: string, filename: string, folderId?: string) => { + async (content: string, filename: string, folderId?: string, sortOrder?: number) => { const { data: workflowData, errors: parseErrors } = parseWorkflowJson(content) if (!workflowData || parseErrors.length > 0) { @@ -60,6 +61,7 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { description: workflowData.metadata?.description || 'Imported from JSON', workspaceId, folderId: folderId || undefined, + sortOrder, }) const newWorkflowId = result.id @@ -140,6 +142,55 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { }) const folderMap = new Map() + if (metadata?.folders && metadata.folders.length > 0) { + type ExportedFolder = { + id: string + name: string + parentId: string | null + sortOrder?: number + } + const foldersById = new Map( + metadata.folders.map((f) => [f.id, f]) + ) + const oldIdToNewId = new Map() + + const buildPath = (folderId: string): string => { + const pathParts: string[] = [] + let currentId: string | null = folderId + while (currentId && foldersById.has(currentId)) { + const folder: ExportedFolder = foldersById.get(currentId)! + pathParts.unshift(sanitizePathSegment(folder.name)) + currentId = folder.parentId + } + return pathParts.join('/') + } + + const createFolderRecursive = async (folder: ExportedFolder): Promise => { + if (oldIdToNewId.has(folder.id)) { + return oldIdToNewId.get(folder.id)! + } + + let parentId = importFolder.id + if (folder.parentId && foldersById.has(folder.parentId)) { + parentId = await createFolderRecursive(foldersById.get(folder.parentId)!) + } + + const newFolder = await createFolderMutation.mutateAsync({ + name: folder.name, + workspaceId, + parentId, + sortOrder: folder.sortOrder, + }) + oldIdToNewId.set(folder.id, newFolder.id) + folderMap.set(buildPath(folder.id), newFolder.id) + return newFolder.id + } + + for (const folder of metadata.folders) { + await createFolderRecursive(folder) + } + } + for (const workflow of extractedWorkflows) { try { let targetFolderId = importFolder.id @@ -147,15 +198,17 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { if (workflow.folderPath.length > 0) { const folderPathKey = workflow.folderPath.join('/') - if (!folderMap.has(folderPathKey)) { + if (folderMap.has(folderPathKey)) { + targetFolderId = folderMap.get(folderPathKey)! + } else { let parentId = importFolder.id - for (let i = 0; i < workflow.folderPath.length; i++) { const pathSegment = workflow.folderPath.slice(0, i + 1).join('/') + const folderNameForSegment = workflow.folderPath[i] if (!folderMap.has(pathSegment)) { const subFolder = await createFolderMutation.mutateAsync({ - name: workflow.folderPath[i], + name: folderNameForSegment, workspaceId, parentId, }) @@ -165,15 +218,15 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { parentId = folderMap.get(pathSegment)! } } + targetFolderId = folderMap.get(folderPathKey)! } - - targetFolderId = folderMap.get(folderPathKey)! } const workflowId = await importSingleWorkflow( workflow.content, workflow.name, - targetFolderId + targetFolderId, + workflow.sortOrder ) if (workflowId) importedWorkflowIds.push(workflowId) } catch (error) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workspace.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workspace.ts index d79add0c23..87d2e793a8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workspace.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workspace.ts @@ -5,6 +5,7 @@ import { extractWorkflowName, extractWorkflowsFromZip, parseWorkflowJson, + sanitizePathSegment, } from '@/lib/workflows/operations/import-export' import { useCreateFolder } from '@/hooks/queries/folders' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' @@ -59,7 +60,7 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {}) const createResponse = await fetch('/api/workspaces', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: workspaceName }), + body: JSON.stringify({ name: workspaceName, skipDefaultWorkflow: true }), }) if (!createResponse.ok) { @@ -71,6 +72,55 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {}) const folderMap = new Map() + if (metadata?.folders && metadata.folders.length > 0) { + type ExportedFolder = { + id: string + name: string + parentId: string | null + sortOrder?: number + } + const foldersById = new Map( + metadata.folders.map((f) => [f.id, f]) + ) + const oldIdToNewId = new Map() + + const buildPath = (folderId: string): string => { + const pathParts: string[] = [] + let currentId: string | null = folderId + while (currentId && foldersById.has(currentId)) { + const folder: ExportedFolder = foldersById.get(currentId)! + pathParts.unshift(sanitizePathSegment(folder.name)) + currentId = folder.parentId + } + return pathParts.join('/') + } + + const createFolderRecursive = async (folder: ExportedFolder): Promise => { + if (oldIdToNewId.has(folder.id)) { + return oldIdToNewId.get(folder.id)! + } + + let parentId: string | undefined + if (folder.parentId && foldersById.has(folder.parentId)) { + parentId = await createFolderRecursive(foldersById.get(folder.parentId)!) + } + + const newFolder = await createFolderMutation.mutateAsync({ + name: folder.name, + workspaceId: newWorkspace.id, + parentId, + sortOrder: folder.sortOrder, + }) + oldIdToNewId.set(folder.id, newFolder.id) + folderMap.set(buildPath(folder.id), newFolder.id) + return newFolder.id + } + + for (const folder of metadata.folders) { + await createFolderRecursive(folder) + } + } + for (const workflow of extractedWorkflows) { try { const { data: workflowData, errors: parseErrors } = parseWorkflowJson(workflow.content) @@ -84,9 +134,10 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {}) if (workflow.folderPath.length > 0) { const folderPathKey = workflow.folderPath.join('/') - if (!folderMap.has(folderPathKey)) { - let parentId: string | null = null - + if (folderMap.has(folderPathKey)) { + targetFolderId = folderMap.get(folderPathKey)! + } else { + let parentId: string | undefined for (let i = 0; i < workflow.folderPath.length; i++) { const pathSegment = workflow.folderPath.slice(0, i + 1).join('/') @@ -94,7 +145,7 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {}) const subFolder = await createFolderMutation.mutateAsync({ name: workflow.folderPath[i], workspaceId: newWorkspace.id, - parentId: parentId || undefined, + parentId, }) folderMap.set(pathSegment, subFolder.id) parentId = subFolder.id @@ -102,9 +153,8 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {}) parentId = folderMap.get(pathSegment)! } } + targetFolderId = folderMap.get(folderPathKey) || null } - - targetFolderId = folderMap.get(folderPathKey) || null } const workflowName = extractWorkflowName(workflow.content, workflow.name) diff --git a/apps/sim/hooks/queries/folders.ts b/apps/sim/hooks/queries/folders.ts index 566b70e6b8..61289ed658 100644 --- a/apps/sim/hooks/queries/folders.ts +++ b/apps/sim/hooks/queries/folders.ts @@ -68,6 +68,7 @@ interface CreateFolderVariables { name: string parentId?: string color?: string + sortOrder?: number } interface UpdateFolderVariables { @@ -160,18 +161,20 @@ export function useCreateFolder() { parentId: variables.parentId || null, color: variables.color || '#808080', isExpanded: false, - sortOrder: getNextSortOrder(previousFolders, variables.workspaceId, variables.parentId), + sortOrder: + variables.sortOrder ?? + getNextSortOrder(previousFolders, variables.workspaceId, variables.parentId), createdAt: new Date(), updatedAt: new Date(), }) ) return useMutation({ - mutationFn: async ({ workspaceId, ...payload }: CreateFolderVariables) => { + mutationFn: async ({ workspaceId, sortOrder, ...payload }: CreateFolderVariables) => { const response = await fetch('/api/folders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ...payload, workspaceId }), + body: JSON.stringify({ ...payload, workspaceId, sortOrder }), }) if (!response.ok) { @@ -285,9 +288,66 @@ export function useDuplicateFolderMutation() { }, ...handlers, onSettled: (_data, _error, variables) => { - // Invalidate both folders and workflows (duplicated folder may contain workflows) queryClient.invalidateQueries({ queryKey: folderKeys.list(variables.workspaceId) }) queryClient.invalidateQueries({ queryKey: workflowKeys.list(variables.workspaceId) }) }, }) } + +interface ReorderFoldersVariables { + workspaceId: string + updates: Array<{ + id: string + sortOrder: number + parentId?: string | null + }> +} + +export function useReorderFolders() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (variables: ReorderFoldersVariables): Promise => { + const response = await fetch('/api/folders/reorder', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(variables), + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.error || 'Failed to reorder folders') + } + }, + onMutate: async (variables) => { + await queryClient.cancelQueries({ queryKey: folderKeys.list(variables.workspaceId) }) + + const snapshot = { ...useFolderStore.getState().folders } + + useFolderStore.setState((state) => { + const updated = { ...state.folders } + for (const update of variables.updates) { + if (updated[update.id]) { + updated[update.id] = { + ...updated[update.id], + sortOrder: update.sortOrder, + parentId: + update.parentId !== undefined ? update.parentId : updated[update.id].parentId, + } + } + } + return { folders: updated } + }) + + return { snapshot } + }, + onError: (_error, _variables, context) => { + if (context?.snapshot) { + useFolderStore.setState({ folders: context.snapshot }) + } + }, + onSettled: (_data, _error, variables) => { + queryClient.invalidateQueries({ queryKey: folderKeys.list(variables.workspaceId) }) + }, + }) +} diff --git a/apps/sim/hooks/queries/workflows.ts b/apps/sim/hooks/queries/workflows.ts index eaabbab66e..6a119bdc9d 100644 --- a/apps/sim/hooks/queries/workflows.ts +++ b/apps/sim/hooks/queries/workflows.ts @@ -32,6 +32,7 @@ function mapWorkflow(workflow: any): WorkflowMetadata { color: workflow.color, workspaceId: workflow.workspaceId, folderId: workflow.folderId, + sortOrder: workflow.sortOrder ?? 0, createdAt: new Date(workflow.createdAt), lastModified: new Date(workflow.updatedAt || workflow.createdAt), } @@ -91,6 +92,7 @@ interface CreateWorkflowVariables { description?: string color?: string folderId?: string | null + sortOrder?: number } interface CreateWorkflowResult { @@ -100,6 +102,7 @@ interface CreateWorkflowResult { color: string workspaceId: string folderId?: string | null + sortOrder: number } interface DuplicateWorkflowVariables { @@ -118,6 +121,7 @@ interface DuplicateWorkflowResult { color: string workspaceId: string folderId?: string | null + sortOrder: number blocksCount: number edgesCount: number subflowsCount: number @@ -161,6 +165,7 @@ function createWorkflowMutationHandlers( queryClient, 'CreateWorkflow', - (variables, tempId) => ({ - id: tempId, - name: variables.name || generateCreativeWorkflowName(), - lastModified: new Date(), - createdAt: new Date(), - description: variables.description || 'New workflow', - color: variables.color || getNextWorkflowColor(), - workspaceId: variables.workspaceId, - folderId: variables.folderId || null, - }) + (variables, tempId) => { + let sortOrder: number + if (variables.sortOrder !== undefined) { + sortOrder = variables.sortOrder + } else { + const currentWorkflows = useWorkflowRegistry.getState().workflows + const targetFolderId = variables.folderId || null + const workflowsInFolder = Object.values(currentWorkflows).filter( + (w) => w.folderId === targetFolderId + ) + sortOrder = workflowsInFolder.reduce((max, w) => Math.max(max, w.sortOrder ?? 0), -1) + 1 + } + + return { + id: tempId, + name: variables.name || generateCreativeWorkflowName(), + lastModified: new Date(), + createdAt: new Date(), + description: variables.description || 'New workflow', + color: variables.color || getNextWorkflowColor(), + workspaceId: variables.workspaceId, + folderId: variables.folderId || null, + sortOrder, + } + } ) return useMutation({ mutationFn: async (variables: CreateWorkflowVariables): Promise => { - const { workspaceId, name, description, color, folderId } = variables + const { workspaceId, name, description, color, folderId, sortOrder } = variables logger.info(`Creating new workflow in workspace: ${workspaceId}`) @@ -206,6 +226,7 @@ export function useCreateWorkflow() { color: color || getNextWorkflowColor(), workspaceId, folderId: folderId || null, + sortOrder, }), }) @@ -243,13 +264,13 @@ export function useCreateWorkflow() { color: createdWorkflow.color, workspaceId, folderId: createdWorkflow.folderId, + sortOrder: createdWorkflow.sortOrder ?? 0, } }, ...handlers, onSuccess: (data, variables, context) => { handlers.onSuccess(data, variables, context) - // Initialize subblock values for new workflow const { subBlockValues } = buildDefaultWorkflowArtifacts() useSubBlockStore.setState((state) => ({ workflowValues: { @@ -267,16 +288,26 @@ export function useDuplicateWorkflowMutation() { const handlers = createWorkflowMutationHandlers( queryClient, 'DuplicateWorkflow', - (variables, tempId) => ({ - id: tempId, - name: variables.name, - lastModified: new Date(), - createdAt: new Date(), - description: variables.description, - color: variables.color, - workspaceId: variables.workspaceId, - folderId: variables.folderId || null, - }) + (variables, tempId) => { + const currentWorkflows = useWorkflowRegistry.getState().workflows + const targetFolderId = variables.folderId || null + const workflowsInFolder = Object.values(currentWorkflows).filter( + (w) => w.folderId === targetFolderId + ) + const maxSortOrder = workflowsInFolder.reduce((max, w) => Math.max(max, w.sortOrder ?? 0), -1) + + return { + id: tempId, + name: variables.name, + lastModified: new Date(), + createdAt: new Date(), + description: variables.description, + color: variables.color, + workspaceId: variables.workspaceId, + folderId: targetFolderId, + sortOrder: maxSortOrder + 1, + } + } ) return useMutation({ @@ -317,6 +348,7 @@ export function useDuplicateWorkflowMutation() { color: duplicatedWorkflow.color || color, workspaceId, folderId: duplicatedWorkflow.folderId ?? folderId, + sortOrder: duplicatedWorkflow.sortOrder ?? 0, blocksCount: duplicatedWorkflow.blocksCount || 0, edgesCount: duplicatedWorkflow.edgesCount || 0, subflowsCount: duplicatedWorkflow.subflowsCount || 0, @@ -398,3 +430,61 @@ export function useRevertToVersion() { }, }) } + +interface ReorderWorkflowsVariables { + workspaceId: string + updates: Array<{ + id: string + sortOrder: number + folderId?: string | null + }> +} + +export function useReorderWorkflows() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (variables: ReorderWorkflowsVariables): Promise => { + const response = await fetch('/api/workflows/reorder', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(variables), + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.error || 'Failed to reorder workflows') + } + }, + onMutate: async (variables) => { + await queryClient.cancelQueries({ queryKey: workflowKeys.list(variables.workspaceId) }) + + const snapshot = { ...useWorkflowRegistry.getState().workflows } + + useWorkflowRegistry.setState((state) => { + const updated = { ...state.workflows } + for (const update of variables.updates) { + if (updated[update.id]) { + updated[update.id] = { + ...updated[update.id], + sortOrder: update.sortOrder, + folderId: + update.folderId !== undefined ? update.folderId : updated[update.id].folderId, + } + } + } + return { workflows: updated } + }) + + return { snapshot } + }, + onError: (_error, _variables, context) => { + if (context?.snapshot) { + useWorkflowRegistry.setState({ workflows: context.snapshot }) + } + }, + onSettled: (_data, _error, variables) => { + queryClient.invalidateQueries({ queryKey: workflowKeys.list(variables.workspaceId) }) + }, + }) +} diff --git a/apps/sim/lib/workflows/operations/import-export.ts b/apps/sim/lib/workflows/operations/import-export.ts index d2fb95628c..fb741f7c44 100644 --- a/apps/sim/lib/workflows/operations/import-export.ts +++ b/apps/sim/lib/workflows/operations/import-export.ts @@ -16,6 +16,7 @@ export interface WorkflowExportData { description?: string color?: string folderId?: string | null + sortOrder?: number } state: WorkflowState variables?: Record @@ -25,6 +26,7 @@ export interface FolderExportData { id: string name: string parentId: string | null + sortOrder?: number } export interface WorkspaceExportStructure { @@ -186,7 +188,12 @@ export async function exportWorkspaceToZip( name: workspaceName, exportedAt: new Date().toISOString(), }, - folders: folders.map((f) => ({ id: f.id, name: f.name, parentId: f.parentId })), + folders: folders.map((f) => ({ + id: f.id, + name: f.name, + parentId: f.parentId, + sortOrder: f.sortOrder, + })), } zip.file('_workspace.json', JSON.stringify(metadata, null, 2)) @@ -199,6 +206,7 @@ export async function exportWorkspaceToZip( name: workflow.workflow.name, description: workflow.workflow.description, color: workflow.workflow.color, + sortOrder: workflow.workflow.sortOrder, exportedAt: new Date().toISOString(), }, variables: workflow.variables, @@ -279,11 +287,27 @@ export interface ImportedWorkflow { content: string name: string folderPath: string[] + sortOrder?: number } export interface WorkspaceImportMetadata { workspaceName: string exportedAt?: string + folders?: Array<{ + id: string + name: string + parentId: string | null + sortOrder?: number + }> +} + +function extractSortOrder(content: string): number | undefined { + try { + const parsed = JSON.parse(content) + return parsed.state?.metadata?.sortOrder ?? parsed.metadata?.sortOrder + } catch { + return undefined + } } export async function extractWorkflowsFromZip( @@ -303,6 +327,7 @@ export async function extractWorkflowsFromZip( metadata = { workspaceName: parsed.workspace?.name || 'Imported Workspace', exportedAt: parsed.workspace?.exportedAt, + folders: parsed.folders, } } catch (error) { logger.error('Failed to parse workspace metadata:', error) @@ -321,6 +346,7 @@ export async function extractWorkflowsFromZip( content, name: filename, folderPath: pathParts, + sortOrder: extractSortOrder(content), }) } catch (error) { logger.error(`Failed to extract ${path}:`, error) @@ -338,10 +364,12 @@ export async function extractWorkflowsFromFiles(files: File[]): Promise()( // Use the server-generated ID const id = duplicatedWorkflow.id - // Generate new workflow metadata using the server-generated ID const newWorkflow: WorkflowMetadata = { id, name: `${sourceWorkflow.name} (Copy)`, @@ -484,8 +483,9 @@ export const useWorkflowRegistry = create()( createdAt: new Date(), description: sourceWorkflow.description, color: getNextWorkflowColor(), - workspaceId, // Include the workspaceId in the new workflow - folderId: sourceWorkflow.folderId, // Include the folderId from source workflow + workspaceId, + folderId: sourceWorkflow.folderId, + sortOrder: duplicatedWorkflow.sortOrder ?? 0, } // Get the current workflow state to copy from diff --git a/apps/sim/stores/workflows/registry/types.ts b/apps/sim/stores/workflows/registry/types.ts index 80180a4518..8ea62dde69 100644 --- a/apps/sim/stores/workflows/registry/types.ts +++ b/apps/sim/stores/workflows/registry/types.ts @@ -26,6 +26,7 @@ export interface WorkflowMetadata { color: string workspaceId?: string folderId?: string | null + sortOrder: number } export type HydrationPhase = diff --git a/bun.lock b/bun.lock index cf6913db54..1237b32b18 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", diff --git a/packages/db/migrations/0141_daffy_marten_broadcloak.sql b/packages/db/migrations/0141_daffy_marten_broadcloak.sql new file mode 100644 index 0000000000..6566fe6b06 --- /dev/null +++ b/packages/db/migrations/0141_daffy_marten_broadcloak.sql @@ -0,0 +1,2 @@ +ALTER TABLE "workflow" ADD COLUMN "sort_order" integer DEFAULT 0 NOT NULL;--> statement-breakpoint +CREATE INDEX "workflow_folder_sort_idx" ON "workflow" USING btree ("folder_id","sort_order"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0141_snapshot.json b/packages/db/migrations/meta/0141_snapshot.json new file mode 100644 index 0000000000..06255b2821 --- /dev/null +++ b/packages/db/migrations/meta/0141_snapshot.json @@ -0,0 +1,10267 @@ +{ + "id": "cffb4dda-dfcc-474d-a6d8-fdb2b749edaf", + "prevId": "604deef4-68ee-4a32-94bd-21ce3e37be38", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_agent_workspace_id_idx": { + "name": "a2a_agent_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_id_idx": { + "name": "a2a_push_notification_config_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_user_provider_unique": { + "name": "account_user_provider_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_organization_id_idx": { + "name": "credential_set_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_set_id_idx": { + "name": "credential_set_member_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form": { + "name": "form", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_namespace_unique": { + "name": "idempotency_key_namespace_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idempotency_key_namespace_idx": { + "name": "idempotency_key_namespace_idx", + "columns": [ + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_idx": { + "name": "mcp_servers_workspace_deleted_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_organization_id_idx": { + "name": "permission_group_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_org_name_unique": { + "name": "permission_group_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_user_id_unique": { + "name": "permission_group_member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_super_user": { + "name": "is_super_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'20'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_block_id_workflow_blocks_id_fk": { + "name": "webhook_block_id_workflow_blocks_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_unique": { + "name": "workflow_schedule_workflow_block_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_block_id_workflow_blocks_id_fk": { + "name": "workflow_schedule_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_files_key_unique": { + "name": "workspace_files_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "workspace_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "org_invitation_id": { + "name": "org_invitation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": ["workflow", "wand", "copilot"] + }, + "public.workspace_invitation_status": { + "name": "workspace_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 08cd037e8e..a65e10be81 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -981,6 +981,13 @@ "when": 1768366574848, "tag": "0140_fuzzy_the_twelve", "breakpoints": true + }, + { + "idx": 141, + "version": "7", + "when": 1768421319400, + "tag": "0141_daffy_marten_broadcloak", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index c5edcfb623..78ac81829c 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -149,6 +149,7 @@ export const workflow = pgTable( .references(() => user.id, { onDelete: 'cascade' }), workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }), folderId: text('folder_id').references(() => workflowFolder.id, { onDelete: 'set null' }), + sortOrder: integer('sort_order').notNull().default(0), name: text('name').notNull(), description: text('description'), color: text('color').notNull().default('#3972F6'), @@ -165,6 +166,7 @@ export const workflow = pgTable( userIdIdx: index('workflow_user_id_idx').on(table.userId), workspaceIdIdx: index('workflow_workspace_id_idx').on(table.workspaceId), userWorkspaceIdx: index('workflow_user_workspace_idx').on(table.userId, table.workspaceId), + folderSortIdx: index('workflow_folder_sort_idx').on(table.folderId, table.sortOrder), }) ) From 41f9374b5c222ceabb0f5fc985bc7a21b6588420 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 14 Jan 2026 15:53:42 -0800 Subject: [PATCH 22/36] fix(agent-tools): added special handling for workflow tool in agent tool input, added react grab and feature flag (#2820) * fix(agent-tools): added special handling for workflow tool in agent tool input, added react grab * FF react grab * ack comments * updated to account for workflow input tool on top of just workflow as well --- apps/sim/app/layout.tsx | 16 +++++++++++++++- .../components/tool-input/tool-input.tsx | 19 +++++++++++++------ .../emcn/components/combobox/combobox.tsx | 4 ++++ apps/sim/lib/core/config/env.ts | 3 +++ apps/sim/lib/core/config/feature-flags.ts | 6 ++++++ bun.lock | 6 ++++++ 6 files changed, 47 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/layout.tsx b/apps/sim/app/layout.tsx index 6be1e579c5..327a519291 100644 --- a/apps/sim/app/layout.tsx +++ b/apps/sim/app/layout.tsx @@ -1,12 +1,13 @@ import type { Metadata, Viewport } from 'next' +import Script from 'next/script' import { PublicEnvScript } from 'next-runtime-env' import { BrandedLayout } from '@/components/branded-layout' import { generateThemeCSS } from '@/lib/branding/inject-theme' import { generateBrandedMetadata, generateStructuredData } from '@/lib/branding/metadata' import { PostHogProvider } from '@/app/_shell/providers/posthog-provider' import '@/app/_styles/globals.css' - import { OneDollarStats } from '@/components/analytics/onedollarstats' +import { isReactGrabEnabled } from '@/lib/core/config/feature-flags' import { HydrationErrorHandler } from '@/app/_shell/hydration-error-handler' import { QueryProvider } from '@/app/_shell/providers/query-provider' import { SessionProvider } from '@/app/_shell/providers/session-provider' @@ -33,6 +34,19 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( + {isReactGrabEnabled && ( +