updateRuntimeParam(index, { placeholder: e.target.value })}
/>
@@ -1013,7 +1013,7 @@ export function NodeInspector({
updateRuntimeParam(index, { default_value: e.target.value })}
/>
@@ -1030,7 +1030,7 @@ export function NodeInspector({
+ }>
+
+ {/* Human-readable summary */}
+
+
+ {description}
+
+
+ {/* Frequency selector */}
+
+
Repeat
+
+ {([
+ { v: 'every_n_minutes' as Frequency, l: 'Minutes' },
+ { v: 'hourly' as Frequency, l: 'Hourly' },
+ { v: 'daily' as Frequency, l: 'Daily' },
+ { v: 'weekly' as Frequency, l: 'Weekly' },
+ { v: 'monthly' as Frequency, l: 'Monthly' },
+ ]).map(f => (
+
+ ))}
+
+
+
+ {/* Every N minutes */}
+ {state.frequency === 'every_n_minutes' && (
+
+
Every
+
+ setState(s => ({ ...s, minutes: Math.max(1, Math.min(59, Number(e.target.value) || 1)) }))}
+ style={{ width: 70 }}
+ />
+ minutes
+
+
+ )}
+
+ {/* Hourly — minute offset */}
+ {state.frequency === 'hourly' && (
+
+
At minute
+
+ setState(s => ({ ...s, minute: Math.max(0, Math.min(59, Number(e.target.value) || 0)) }))}
+ style={{ width: 70 }}
+ />
+ past the hour
+
+
+ )}
+
+ {/* Daily / weekly / monthly — time picker */}
+ {(state.frequency === 'daily' || state.frequency === 'weekly' || state.frequency === 'monthly') && (
+
+ )}
+
+ {/* Weekly — day of week selector */}
+ {state.frequency === 'weekly' && (
+
+
On days
+
+ {DAYS_OF_WEEK.map(d => {
+ const active = state.daysOfWeek.includes(d.value)
+ return (
+
+ )
+ })}
+
+
+ )}
+
+ {/* Monthly — day of month */}
+ {state.frequency === 'monthly' && (
+
+
Day of month
+
setState(s => ({ ...s, dayOfMonth: Math.max(1, Math.min(28, Number(e.target.value) || 1)) }))}
+ style={{ width: 70 }}
+ />
+
+ )}
+
+ {/* Timezone */}
+
+
Timezone
+
+
+
+
+ )
+}
diff --git a/src/pages/workflows/components/inspector/TypeaheadSelect.tsx b/src/pages/workflows/components/inspector/TypeaheadSelect.tsx
new file mode 100644
index 0000000..e99ffc8
--- /dev/null
+++ b/src/pages/workflows/components/inspector/TypeaheadSelect.tsx
@@ -0,0 +1,189 @@
+/**
+ * TypeaheadSelect — reusable type-ahead search + select dropdown.
+ *
+ * Used for agent selection, event type selection, and template (workflow) selection.
+ * Fetches options lazily on focus, filters on typing.
+ */
+
+import { useState, useRef, useEffect, useCallback } from 'react'
+import { Search, X, ChevronDown } from 'lucide-react'
+
+export interface TypeaheadOption {
+ value: string
+ label: string
+ description?: string
+}
+
+interface TypeaheadSelectProps {
+ value: string
+ onChange: (value: string) => void
+ options: TypeaheadOption[]
+ loading?: boolean
+ onSearch?: (query: string) => void
+ placeholder?: string
+ /** Show the label next to the value when a selection is made */
+ showLabelOnSelect?: boolean
+ className?: string
+}
+
+export function TypeaheadSelect({
+ value,
+ onChange,
+ options,
+ loading,
+ onSearch,
+ placeholder = 'Search...',
+ showLabelOnSelect,
+}: TypeaheadSelectProps) {
+ const [open, setOpen] = useState(false)
+ const [query, setQuery] = useState('')
+ const inputRef = useRef
(null)
+ const containerRef = useRef(null)
+
+ const selectedOption = options.find(o => o.value === value)
+
+ // Close on outside click
+ useEffect(() => {
+ if (!open) return
+ const handler = (e: MouseEvent) => {
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
+ setOpen(false)
+ setQuery('')
+ }
+ }
+ document.addEventListener('mousedown', handler)
+ return () => document.removeEventListener('mousedown', handler)
+ }, [open])
+
+ const handleQueryChange = useCallback((q: string) => {
+ setQuery(q)
+ onSearch?.(q)
+ }, [onSearch])
+
+ const filtered = query
+ ? options.filter(o =>
+ o.label.toLowerCase().includes(query.toLowerCase()) ||
+ o.value.toLowerCase().includes(query.toLowerCase()) ||
+ (o.description?.toLowerCase().includes(query.toLowerCase()))
+ )
+ : options
+
+ return (
+
+ {/* Display field */}
+ {!open ? (
+
+ ) : (
+
+
+ handleQueryChange(e.target.value)}
+ placeholder={placeholder}
+ style={{ paddingLeft: 26, width: '100%' }}
+ onKeyDown={e => {
+ if (e.key === 'Escape') { setOpen(false); setQuery('') }
+ if (e.key === 'Enter' && filtered.length === 1) {
+ onChange(filtered[0].value)
+ setOpen(false)
+ setQuery('')
+ }
+ }}
+ />
+
+ )}
+
+ {/* Dropdown */}
+ {open && (
+
+ {loading && (
+
+ Loading...
+
+ )}
+ {!loading && filtered.length === 0 && (
+
+ {query ? `No results for "${query}"` : 'No options available'}
+
+ )}
+ {filtered.map(opt => (
+
+ ))}
+
+ )}
+
+ )
+}
diff --git a/src/pages/workflows/components/inspector/WorkflowInspector.tsx b/src/pages/workflows/components/inspector/WorkflowInspector.tsx
index 77ba1b3..f590610 100644
--- a/src/pages/workflows/components/inspector/WorkflowInspector.tsx
+++ b/src/pages/workflows/components/inspector/WorkflowInspector.tsx
@@ -1,11 +1,21 @@
/**
- * WorkflowInspector — workflow-level settings panel (trigger, provider, template, stats).
+ * WorkflowInspector — workflow-level settings panel (template, trigger, provider, stats).
*
* Shown when no node is selected.
+ *
+ * Layout order:
+ * 1. Template (select an existing workflow as template — typeahead search)
+ * 2. Trigger (manual / webhook / schedule / event / chat)
+ * 3. Trigger-specific config (with agent typeahead, schedule modal, event typeahead)
*/
+import { useState, useEffect, useCallback, useMemo } from 'react'
+import { Calendar } from 'lucide-react'
import { InfoTip } from '@/components/ui/Tooltip'
+import { agentsApi, workflowsApi } from '@/api/client'
import type { WfDefinition, WfTrigger, WfProvider, WfTriggerType } from '../../types'
+import { TypeaheadSelect, type TypeaheadOption } from './TypeaheadSelect'
+import { ScheduleModal } from './ScheduleModal'
const FIELD_LABEL_STYLE = {
fontSize: 10,
@@ -23,6 +33,118 @@ const SECTION_TITLE_STYLE = {
type TriggerMode = WfTriggerType
+/* ── Known Agos event types ───────────────────────────────────────────── */
+const AGOS_EVENT_TYPES: TypeaheadOption[] = [
+ // Agent events
+ { value: 'agent_spawned', label: 'Agent Spawned', description: 'An agent was created and started' },
+ { value: 'agent_completed', label: 'Agent Completed', description: 'An agent finished its task successfully' },
+ { value: 'agent_failed', label: 'Agent Failed', description: 'An agent encountered an error and stopped' },
+ { value: 'agent_cancelled', label: 'Agent Cancelled', description: 'An agent was manually cancelled' },
+ // Task events
+ { value: 'task_started', label: 'Task Started', description: 'A task began execution' },
+ { value: 'task_completed', label: 'Task Completed', description: 'A task completed successfully' },
+ { value: 'task_failed', label: 'Task Failed', description: 'A task failed with an error' },
+ { value: 'task_timeout', label: 'Task Timeout', description: 'A task exceeded its time limit' },
+ // Tool events
+ { value: 'tool_called', label: 'Tool Called', description: 'A tool was invoked' },
+ { value: 'tool_completed', label: 'Tool Completed', description: 'A tool finished execution' },
+ { value: 'tool_policy_denied', label: 'Tool Policy Denied', description: 'A tool call was blocked by policy' },
+ { value: 'tool_error', label: 'Tool Error', description: 'A tool call failed' },
+ // Workflow events
+ { value: 'workflow_started', label: 'Workflow Started', description: 'A workflow execution began' },
+ { value: 'workflow.execution.completed', label: 'Workflow Completed', description: 'A workflow execution finished' },
+ { value: 'workflow.execution.failed', label: 'Workflow Failed', description: 'A workflow execution failed' },
+ { value: 'workflow.node.completed', label: 'Workflow Node Completed', description: 'A workflow node finished' },
+ // Policy events
+ { value: 'policy_checked', label: 'Policy Checked', description: 'A policy was evaluated' },
+ { value: 'policy_violation', label: 'Policy Violation', description: 'A policy violation was detected' },
+ { value: 'approval_requested', label: 'Approval Requested', description: 'A human approval was requested' },
+ { value: 'approval_resolved', label: 'Approval Resolved', description: 'A human approval was resolved' },
+ // Plugin events
+ { value: 'plugin_installed', label: 'Plugin Installed', description: 'A plugin was installed' },
+ { value: 'plugin_executed', label: 'Plugin Executed', description: 'A plugin was executed' },
+ // Memory events
+ { value: 'memory_stored', label: 'Memory Stored', description: 'A memory was persisted' },
+ { value: 'memory_retrieved', label: 'Memory Retrieved', description: 'A memory was retrieved' },
+ // Security events
+ { value: 'auth_failure', label: 'Auth Failure', description: 'An authentication attempt failed' },
+ { value: 'permission_denied', label: 'Permission Denied', description: 'A permission check failed' },
+]
+
+/* ── Hooks for lazy data fetching ─────────────────────────────────────── */
+
+function useAgentOptions() {
+ const [options, setOptions] = useState([])
+ const [loading, setLoading] = useState(false)
+
+ useEffect(() => {
+ let cancelled = false
+ setLoading(true)
+ agentsApi.list({ page_size: 100 })
+ .then(res => {
+ if (cancelled) return
+ const agents = res.items ?? []
+ setOptions(agents.map(a => ({
+ value: a.id,
+ label: a.name || a.id,
+ description: a.status ?? 'active',
+ })))
+ })
+ .catch(() => { /* fail silently — user can still type */ })
+ .finally(() => { if (!cancelled) setLoading(false) })
+ return () => { cancelled = true }
+ }, [])
+
+ return { options, loading }
+}
+
+function useWorkflowOptions() {
+ const [options, setOptions] = useState([])
+ const [loading, setLoading] = useState(false)
+
+ const fetchWorkflows = useCallback((search?: string) => {
+ setLoading(true)
+ workflowsApi.list({ page_size: 50, search: search || undefined })
+ .then(res => {
+ const items = res.items ?? []
+ setOptions(items.map(w => ({
+ value: w.id,
+ label: w.name || w.id,
+ description: w.description || w.status || '',
+ })))
+ })
+ .catch(() => { /* fail silently */ })
+ .finally(() => setLoading(false))
+ }, [])
+
+ useEffect(() => { fetchWorkflows() }, [fetchWorkflows])
+
+ return { options, loading, onSearch: fetchWorkflows }
+}
+
+/* ── Cron description helper ──────────────────────────────────────────── */
+
+function describeCronSimple(cron?: string, tz?: string): string | null {
+ if (!cron) return null
+ const parts = cron.trim().split(/\s+/)
+ if (parts.length < 5) return cron
+ const [min, hr, dom, , dow] = parts
+ const tzLabel = tz || 'UTC'
+ const pad = (n: number) => String(n).padStart(2, '0')
+
+ if (min.startsWith('*/')) return `Every ${min.slice(2)} minutes`
+ if (min !== '*' && hr === '*') return `Every hour at :${pad(parseInt(min) || 0)}`
+ if (dow !== '*' && dow !== '?') {
+ const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
+ const days = dow.split(',').map(d => dayNames[parseInt(d)] ?? d).join(', ')
+ return `Every ${days} at ${pad(parseInt(hr) || 0)}:${pad(parseInt(min) || 0)} ${tzLabel}`
+ }
+ if (dom !== '*') return `Day ${dom} at ${pad(parseInt(hr) || 0)}:${pad(parseInt(min) || 0)} ${tzLabel}`
+ return `Daily at ${pad(parseInt(hr) || 0)}:${pad(parseInt(min) || 0)} ${tzLabel}`
+}
+
+/* ── Component ────────────────────────────────────────────────────────── */
+
interface WorkflowInspectorProps {
definition: WfDefinition
onUpdateTrigger: (patch: Partial) => void
@@ -39,6 +161,25 @@ export function WorkflowInspector({
const trigger = definition.trigger ?? { type: 'manual', enabled: true, config: {} }
const template = definition.template ?? null
+ const [scheduleModalOpen, setScheduleModalOpen] = useState(false)
+
+ // Lazy-loaded options
+ const agentData = useAgentOptions()
+ const workflowData = useWorkflowOptions()
+
+ // Selected event types as a set for multi-select
+ const selectedEvents = useMemo(() => trigger.config?.event_types ?? [], [trigger.config?.event_types])
+
+ const handleToggleEvent = useCallback((value: string) => {
+ const current = trigger.config?.event_types ?? []
+ const next = current.includes(value)
+ ? current.filter(v => v !== value)
+ : [...current, value]
+ onUpdateTrigger({ config: { ...trigger.config, event_types: next } })
+ }, [trigger, onUpdateTrigger])
+
+ const cronDescription = describeCronSimple(trigger.config?.cron, trigger.config?.timezone)
+
// Provider selection (agos-native vs n8n) is planned but not yet exposed to users
return (
@@ -47,7 +188,34 @@ export function WorkflowInspector({
Click a step to edit it. The canvas stays primary; config lives here.
- {/* Trigger */}
+ {/* ─── Template (above trigger) ──────────────────────────────────── */}
+
+
+ {/* ─── Trigger ───────────────────────────────────────────────────── */}
- {/* Template reference — use an existing workflow as a base */}
-
-
- {/* Webhook config */}
+ {/* ─── Webhook config ────────────────────────────────────────────── */}
{trigger.type === 'webhook' && (
<>