From 97839473c57960dcc6b4f85ab4601417b063c0e4 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 5 Jan 2026 15:48:08 -0800 Subject: [PATCH] Superagent --- apps/sim/app/agent/page.tsx | 42 +++++++++++++++++++ apps/sim/app/api/copilot/chat/route.ts | 6 +-- .../sim/app/api/copilot/execute-tool/route.ts | 2 +- .../components/tool-call/tool-call.tsx | 8 ++-- .../mode-selector/mode-selector.tsx | 16 ++++--- .../components/user-input/user-input.tsx | 4 +- .../copilot/components/welcome/welcome.tsx | 4 +- .../panel/components/copilot/copilot.tsx | 17 ++++---- .../hooks/use-copilot-initialization.ts | 22 +++++++++- apps/sim/lib/copilot/api.ts | 4 +- apps/sim/stores/panel/copilot/store.ts | 21 ++++++---- apps/sim/stores/panel/copilot/types.ts | 2 +- 12 files changed, 110 insertions(+), 38 deletions(-) create mode 100644 apps/sim/app/agent/page.tsx diff --git a/apps/sim/app/agent/page.tsx b/apps/sim/app/agent/page.tsx new file mode 100644 index 000000000..a393361c1 --- /dev/null +++ b/apps/sim/app/agent/page.tsx @@ -0,0 +1,42 @@ +'use client' + +import { useEffect } from 'react' +import { Tooltip } from '@/components/emcn' +import { Copilot } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot' +import { useCopilotStore } from '@/stores/panel/copilot/store' + +/** + * Superagent page - standalone AI agent with full credential access + * Uses the exact same Copilot UI but with superagent mode forced + */ +export default function AgentPage() { + const { setMode } = useCopilotStore() + + // Set superagent mode on mount + useEffect(() => { + setMode('superagent') + }, [setMode]) + + return ( + +
+ {/* Header */} +
+
+

Superagent

+ + Full Access + +
+
+ + {/* Copilot - exact same component in standalone mode */} +
+
+ +
+
+
+
+ ) +} diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index b14feb495..15a2555ce 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -38,7 +38,7 @@ const ChatMessageSchema = z.object({ message: z.string().min(1, 'Message is required'), userMessageId: z.string().optional(), // ID from frontend for the user message chatId: z.string().optional(), - workflowId: z.string().min(1, 'Workflow ID is required'), + workflowId: z.string().optional(), model: z .enum([ 'gpt-5-fast', @@ -63,7 +63,7 @@ const ChatMessageSchema = z.object({ ]) .optional() .default('claude-4.5-opus'), - mode: z.enum(['ask', 'agent', 'plan']).optional().default('agent'), + mode: z.enum(['ask', 'agent', 'plan', 'superagent']).optional().default('agent'), prefetch: z.boolean().optional(), createNewChat: z.boolean().optional().default(false), stream: z.boolean().optional().default(true), @@ -339,7 +339,7 @@ export async function POST(req: NextRequest) { } } | null = null - if (mode === 'agent') { + if (mode === 'agent' || mode === 'superagent') { // Build base tools (executed locally, not deferred) // Include function_execute for code execution capability baseTools = [ diff --git a/apps/sim/app/api/copilot/execute-tool/route.ts b/apps/sim/app/api/copilot/execute-tool/route.ts index adb88071e..3185a34be 100644 --- a/apps/sim/app/api/copilot/execute-tool/route.ts +++ b/apps/sim/app/api/copilot/execute-tool/route.ts @@ -25,7 +25,7 @@ const ExecuteToolSchema = z.object({ toolCallId: z.string(), toolName: z.string(), arguments: z.record(z.any()).optional().default({}), - workflowId: z.string().optional(), + workflowId: z.string().nullish(), // Accept undefined or null for superagent mode }) /** 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 3df44fe0c..fa795c427 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 @@ -276,7 +276,7 @@ function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean { const mode = useCopilotStore.getState().mode const isAutoAllowed = useCopilotStore.getState().isToolAutoAllowed(toolCall.name) if ( - mode === 'build' && + (mode === 'build' || mode === 'superagent') && isIntegrationTool(toolCall.name) && toolCall.state === 'pending' && !isAutoAllowed @@ -564,11 +564,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }: // Allow rendering if: // 1. Tool is in CLASS_TOOL_METADATA (client tools), OR - // 2. We're in build mode (integration tools are executed server-side) + // 2. We're in build or superagent mode (integration tools are executed server-side) const isClientTool = !!CLASS_TOOL_METADATA[toolCall.name] - const isIntegrationToolInBuildMode = mode === 'build' && !isClientTool + const isIntegrationToolInAgentMode = (mode === 'build' || mode === 'superagent') && !isClientTool - if (!isClientTool && !isIntegrationToolInBuildMode) { + if (!isClientTool && !isIntegrationToolInAgentMode) { return null } const isExpandableTool = diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/mode-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/mode-selector.tsx index c6cc61ad2..4dc8ce662 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/mode-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/mode-selector.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useRef, useState } from 'react' -import { ListTree, MessageSquare, Package } from 'lucide-react' +import { ListTree, MessageSquare, Package, Zap } from 'lucide-react' import { Badge, Popover, @@ -13,10 +13,10 @@ import { import { cn } from '@/lib/core/utils/cn' interface ModeSelectorProps { - /** Current mode - 'ask', 'build', or 'plan' */ - mode: 'ask' | 'build' | 'plan' + /** Current mode - 'ask', 'build', 'plan', or 'superagent' */ + mode: 'ask' | 'build' | 'plan' | 'superagent' /** Callback when mode changes */ - onModeChange?: (mode: 'ask' | 'build' | 'plan') => void + onModeChange?: (mode: 'ask' | 'build' | 'plan' | 'superagent') => void /** Whether the input is near the top of viewport (affects dropdown direction) */ isNearTop: boolean /** Whether the selector is disabled */ @@ -42,6 +42,9 @@ export function ModeSelector({ mode, onModeChange, isNearTop, disabled }: ModeSe if (mode === 'plan') { return } + if (mode === 'superagent') { + return + } return } @@ -52,10 +55,13 @@ export function ModeSelector({ mode, onModeChange, isNearTop, disabled }: ModeSe if (mode === 'plan') { return 'Plan' } + if (mode === 'superagent') { + return 'Superagent' + } return 'Build' } - const handleSelect = (selectedMode: 'ask' | 'build' | 'plan') => { + const handleSelect = (selectedMode: 'ask' | 'build' | 'plan' | 'superagent') => { onModeChange?.(selectedMode) setOpen(false) } 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 c88aed141..c0e736492 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 @@ -51,8 +51,8 @@ interface UserInputProps { isAborting?: boolean placeholder?: string className?: string - mode?: 'ask' | 'build' | 'plan' - onModeChange?: (mode: 'ask' | 'build' | 'plan') => void + mode?: 'ask' | 'build' | 'plan' | 'superagent' + onModeChange?: (mode: 'ask' | 'build' | 'plan' | 'superagent') => void value?: string onChange?: (value: string) => void panelWidth?: number diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/welcome.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/welcome.tsx index b4c9dc249..13ae08448 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/welcome.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/welcome.tsx @@ -8,8 +8,8 @@ import { Button } from '@/components/emcn' interface WelcomeProps { /** Callback when a suggested question is clicked */ onQuestionClick?: (question: string) => void - /** Current copilot mode ('ask' for Q&A, 'plan' for planning, 'build' for workflow building) */ - mode?: 'ask' | 'build' | 'plan' + /** Current copilot mode ('ask' for Q&A, 'plan' for planning, 'build' for workflow building, 'superagent' for full access) */ + mode?: 'ask' | 'build' | 'plan' | 'superagent' } /** diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx index cd47ec91a..2a60504e5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx @@ -49,6 +49,8 @@ const logger = createLogger('Copilot') interface CopilotProps { /** Width of the copilot panel in pixels */ panelWidth: number + /** If true, runs in standalone mode without workflow context (for superagent) */ + standalone?: boolean } /** @@ -67,7 +69,7 @@ interface CopilotRef { * Copilot component - AI-powered assistant for workflow management * Provides chat interface, message history, and intelligent workflow suggestions */ -export const Copilot = forwardRef(({ panelWidth }, ref) => { +export const Copilot = forwardRef(({ panelWidth, standalone = false }, ref) => { const userInputRef = useRef(null) const copilotContainerRef = useRef(null) const cancelEditCallbackRef = useRef<(() => void) | null>(null) @@ -122,6 +124,7 @@ export const Copilot = forwardRef(({ panelWidth }, ref loadAutoAllowedTools, currentChat, isSendingMessage, + standalone, }) // Handle scroll management @@ -298,7 +301,7 @@ export const Copilot = forwardRef(({ panelWidth }, ref */ const handleSubmit = useCallback( async (query: string, fileAttachments?: MessageFileAttachment[], contexts?: any[]) => { - if (!query || isSendingMessage || !activeWorkflowId) return + if (!query || isSendingMessage || (!activeWorkflowId && !standalone)) return if (showPlanTodos) { const store = useCopilotStore.getState() @@ -316,7 +319,7 @@ export const Copilot = forwardRef(({ panelWidth }, ref logger.error('Failed to send message:', error) } }, - [isSendingMessage, activeWorkflowId, sendMessage, showPlanTodos] + [isSendingMessage, activeWorkflowId, sendMessage, showPlanTodos, standalone] ) /** @@ -487,11 +490,11 @@ export const Copilot = forwardRef(({ panelWidth }, ref ref={userInputRef} onSubmit={handleSubmit} onAbort={handleAbort} - disabled={!activeWorkflowId} + disabled={!activeWorkflowId && !standalone} isLoading={isSendingMessage} isAborting={isAborting} mode={mode} - onModeChange={setMode} + onModeChange={standalone ? undefined : setMode} value={inputValue} onChange={setInputValue} panelWidth={panelWidth} @@ -594,11 +597,11 @@ export const Copilot = forwardRef(({ panelWidth }, ref ref={userInputRef} onSubmit={handleSubmit} onAbort={handleAbort} - disabled={!activeWorkflowId} + disabled={!activeWorkflowId && !standalone} isLoading={isSendingMessage} isAborting={isAborting} mode={mode} - onModeChange={setMode} + onModeChange={standalone ? undefined : setMode} value={inputValue} onChange={setInputValue} panelWidth={panelWidth} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/hooks/use-copilot-initialization.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/hooks/use-copilot-initialization.ts index 719760ce2..fc215e9ed 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/hooks/use-copilot-initialization.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/hooks/use-copilot-initialization.ts @@ -15,6 +15,8 @@ interface UseCopilotInitializationProps { loadAutoAllowedTools: () => Promise currentChat: any isSendingMessage: boolean + /** If true, initializes without requiring a workflowId (for standalone agent mode) */ + standalone?: boolean } /** @@ -34,6 +36,7 @@ export function useCopilotInitialization(props: UseCopilotInitializationProps) { loadAutoAllowedTools, currentChat, isSendingMessage, + standalone = false, } = props const [isInitialized, setIsInitialized] = useState(false) @@ -46,6 +49,14 @@ export function useCopilotInitialization(props: UseCopilotInitializationProps) { * Never loads during message streaming to prevent interrupting active conversations */ useEffect(() => { + // Standalone mode: initialize immediately without workflow + if (standalone && !hasMountedRef.current && !isSendingMessage) { + hasMountedRef.current = true + setIsInitialized(true) + logger.info('Standalone mode initialized') + return + } + if (activeWorkflowId && !hasMountedRef.current && !isSendingMessage) { hasMountedRef.current = true setIsInitialized(false) @@ -55,7 +66,7 @@ export function useCopilotInitialization(props: UseCopilotInitializationProps) { // Use false to let the store decide if a reload is needed based on cache loadChats(false) } - }, [activeWorkflowId, setCopilotWorkflowId, loadChats, isSendingMessage]) + }, [activeWorkflowId, setCopilotWorkflowId, loadChats, isSendingMessage, standalone]) /** * Initialize the component - only on mount and genuine workflow changes @@ -63,6 +74,9 @@ export function useCopilotInitialization(props: UseCopilotInitializationProps) { * Never reloads during message streaming to preserve active conversations */ useEffect(() => { + // Skip workflow tracking in standalone mode + if (standalone) return + // Handle genuine workflow changes (not initial mount, not same workflow) // Only reload if not currently streaming to avoid interrupting conversations if ( @@ -100,19 +114,23 @@ export function useCopilotInitialization(props: UseCopilotInitializationProps) { setCopilotWorkflowId, loadChats, isSendingMessage, + standalone, ]) /** * Fetch context usage when component is initialized and has a current chat */ useEffect(() => { + // In standalone mode, skip context usage fetch (no workflow context) + if (standalone) return + if (isInitialized && currentChat?.id && activeWorkflowId) { logger.info('[Copilot] Component initialized, fetching context usage') fetchContextUsage().catch((err) => { logger.warn('[Copilot] Failed to fetch context usage on mount', err) }) } - }, [isInitialized, currentChat?.id, activeWorkflowId, fetchContextUsage]) + }, [isInitialized, currentChat?.id, activeWorkflowId, fetchContextUsage, standalone]) /** * Load auto-allowed tools once on mount diff --git a/apps/sim/lib/copilot/api.ts b/apps/sim/lib/copilot/api.ts index 581fe0511..889204b85 100644 --- a/apps/sim/lib/copilot/api.ts +++ b/apps/sim/lib/copilot/api.ts @@ -27,7 +27,7 @@ export interface CopilotMessage { * Chat config stored in database */ export interface CopilotChatConfig { - mode?: 'ask' | 'build' | 'plan' + mode?: 'ask' | 'build' | 'plan' | 'superagent' model?: string } @@ -65,7 +65,7 @@ export interface SendMessageRequest { userMessageId?: string // ID from frontend for the user message chatId?: string workflowId?: string - mode?: 'ask' | 'agent' | 'plan' + mode?: 'ask' | 'agent' | 'plan' | 'superagent' model?: | 'gpt-5-fast' | 'gpt-5' diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index 8a5a634af..8feba9438 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -1073,16 +1073,16 @@ const sseHandlers: Record = { // 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 - // Only relevant if mode is 'build' (agent) + // Relevant in 'build' mode (with workflow) or 'superagent' mode (standalone) const { mode, workflowId, autoAllowedTools } = get() - if (mode === 'build' && workflowId) { + if ((mode === 'build' && workflowId) || mode === 'superagent') { // Check if tool was NOT found in client registry (def is undefined from above) 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 }) + logger.info('[copilot] Integration tool auto-allowed, executing', { id, name, mode }) // Auto-execute the tool setTimeout(() => { @@ -1090,9 +1090,10 @@ const sseHandlers: Record = { }, 0) } else { // Integration tools stay in pending state until user confirms - logger.info('[build mode] Integration tool awaiting user confirmation', { + logger.info('[copilot] Integration tool awaiting user confirmation', { id, name, + mode, }) } } @@ -1982,7 +1983,8 @@ export const useCopilotStore = create()( messageId?: string } - if (!workflowId) return + // Allow sending without workflowId in superagent mode + if (!workflowId && mode !== 'superagent') return const abortController = new AbortController() set({ isSendingMessage: true, error: null, abortController }) @@ -2053,8 +2055,8 @@ export const useCopilotStore = create()( } // Call copilot API - const apiMode: 'ask' | 'agent' | 'plan' = - mode === 'ask' ? 'ask' : mode === 'plan' ? 'plan' : 'agent' + const apiMode: 'ask' | 'agent' | 'plan' | 'superagent' = + mode === 'ask' ? 'ask' : mode === 'plan' ? 'plan' : mode === 'superagent' ? 'superagent' : 'agent' const result = await sendStreamingMessage({ message: messageToSend, userMessageId: userMessage.id, @@ -2916,9 +2918,10 @@ export const useCopilotStore = create()( }, executeIntegrationTool: async (toolCallId: string) => { - const { toolCallsById, workflowId } = get() + const { toolCallsById, workflowId, mode } = get() const toolCall = toolCallsById[toolCallId] - if (!toolCall || !workflowId) return + // In superagent mode, workflowId is optional + if (!toolCall || (!workflowId && mode !== 'superagent')) return const { id, name, params } = toolCall diff --git a/apps/sim/stores/panel/copilot/types.ts b/apps/sim/stores/panel/copilot/types.ts index f021aa717..20a918e68 100644 --- a/apps/sim/stores/panel/copilot/types.ts +++ b/apps/sim/stores/panel/copilot/types.ts @@ -58,7 +58,7 @@ import type { CopilotChat as ApiCopilotChat } from '@/lib/copilot/api' export type CopilotChat = ApiCopilotChat -export type CopilotMode = 'ask' | 'build' | 'plan' +export type CopilotMode = 'ask' | 'build' | 'plan' | 'superagent' export interface CopilotState { mode: CopilotMode