Superagent

This commit is contained in:
Siddharth Ganesan
2026-01-05 15:48:08 -08:00
parent c369e04189
commit 97839473c5
12 changed files with 110 additions and 38 deletions

View File

@@ -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 (
<Tooltip.Provider delayDuration={600} skipDelayDuration={0}>
<div className='flex h-screen flex-col bg-[var(--surface-1)]'>
{/* Header */}
<header className='flex h-14 flex-shrink-0 items-center justify-between border-b border-[var(--border)] px-4'>
<div className='flex items-center gap-3'>
<h1 className='font-semibold text-lg text-[var(--text-primary)]'>Superagent</h1>
<span className='rounded-full bg-[var(--accent)]/10 px-2 py-0.5 font-medium text-[var(--accent)] text-xs'>
Full Access
</span>
</div>
</header>
{/* Copilot - exact same component in standalone mode */}
<div className='flex-1 overflow-hidden p-4'>
<div className='mx-auto h-full max-w-4xl'>
<Copilot panelWidth={800} standalone />
</div>
</div>
</div>
</Tooltip.Provider>
)
}

View File

@@ -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 = [

View File

@@ -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
})
/**

View File

@@ -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 =

View File

@@ -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 <ListTree className='h-3 w-3' />
}
if (mode === 'superagent') {
return <Zap className='h-3 w-3' />
}
return <Package className='h-3 w-3' />
}
@@ -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)
}

View File

@@ -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

View File

@@ -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'
}
/**

View File

@@ -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<CopilotRef, CopilotProps>(({ panelWidth }, ref) => {
export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth, standalone = false }, ref) => {
const userInputRef = useRef<UserInputRef>(null)
const copilotContainerRef = useRef<HTMLDivElement>(null)
const cancelEditCallbackRef = useRef<(() => void) | null>(null)
@@ -122,6 +124,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
loadAutoAllowedTools,
currentChat,
isSendingMessage,
standalone,
})
// Handle scroll management
@@ -298,7 +301,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ 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<CopilotRef, CopilotProps>(({ 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<CopilotRef, CopilotProps>(({ 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<CopilotRef, CopilotProps>(({ 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}

View File

@@ -15,6 +15,8 @@ interface UseCopilotInitializationProps {
loadAutoAllowedTools: () => Promise<void>
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

View File

@@ -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'

View File

@@ -1073,16 +1073,16 @@ const sseHandlers: Record<string, SSEHandler> = {
// 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<string, SSEHandler> = {
}, 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<CopilotStore>()(
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<CopilotStore>()(
}
// 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<CopilotStore>()(
},
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

View File

@@ -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