diff --git a/apps/web/app/(dashboard)/agents/page.tsx b/apps/web/app/(dashboard)/agents/page.tsx
index 112b559..d00f256 100644
--- a/apps/web/app/(dashboard)/agents/page.tsx
+++ b/apps/web/app/(dashboard)/agents/page.tsx
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
+import { DispatchPanel } from '@/components/agents/dispatch-panel';
interface Agent {
id: string;
@@ -164,11 +165,15 @@ export default function AgentsPage() {
return (
-
-
Agent Activity
-
- Every agent action, visible and reversible.
-
+
+
+
Agent Activity
+
+ Every agent action, visible and reversible.
+
+
+
+
{feedback && (
+ {/* Floating AI dispatch button */}
+
+
+
);
diff --git a/apps/web/app/api/agent-tasks/route.ts b/apps/web/app/api/agent-tasks/route.ts
index 46d7902..5c5c7a6 100644
--- a/apps/web/app/api/agent-tasks/route.ts
+++ b/apps/web/app/api/agent-tasks/route.ts
@@ -4,8 +4,10 @@
// See AGENTS.md for full rules.
import { NextRequest, NextResponse } from 'next/server';
-import { withAuth } from '@/lib/auth';
+import { withAuth, createErrorResponse } from '@/lib/auth';
import { createPocketBaseClient } from '@/lib/pocketbase';
+import { createAgentTaskSchema } from '@project-e/shared';
+import { z } from 'zod';
// GET /api/agent-tasks — List agent tasks
export const GET = withAuth(async (request: NextRequest) => {
@@ -27,3 +29,24 @@ export const GET = withAuth(async (request: NextRequest) => {
perPage: result.perPage,
});
});
+
+// POST /api/agent-tasks — Create a new agent task
+export const POST = withAuth(async (request: NextRequest) => {
+ try {
+ const body = await request.json();
+ const data = createAgentTaskSchema.parse(body);
+
+ const pb = createPocketBaseClient();
+ const task = await pb.collection('agent_tasks').create({
+ ...data,
+ status: 'pending',
+ });
+
+ return NextResponse.json(task, { status: 201 });
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return createErrorResponse('VALIDATION_ERROR', 'Invalid input', 400, error.issues);
+ }
+ throw error;
+ }
+});
diff --git a/apps/web/components/agents/dispatch-panel.tsx b/apps/web/components/agents/dispatch-panel.tsx
new file mode 100644
index 0000000..1469920
--- /dev/null
+++ b/apps/web/components/agents/dispatch-panel.tsx
@@ -0,0 +1,235 @@
+'use client';
+
+import { useState, useEffect, useCallback } from 'react';
+import { Bot, Sparkles, X, Loader2 } from 'lucide-react';
+import { toast } from 'sonner';
+import { Button } from '@/components/ui/button';
+import { Textarea } from '@/components/ui/textarea';
+import { Input } from '@/components/ui/input';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from '@/components/ui/sheet';
+
+interface Agent {
+ id: string;
+ name: string;
+ description?: string;
+ status: 'active' | 'disabled';
+ permission_tier: string;
+}
+
+interface DispatchPanelProps {
+ /** Optional trigger label override */
+ triggerLabel?: string;
+ /** Optional variant for the trigger button */
+ triggerVariant?: 'default' | 'outline' | 'ghost';
+ /** Optional class name for the trigger button */
+ triggerClassName?: string;
+}
+
+export function DispatchPanel({
+ triggerLabel = 'Ask AI',
+ triggerVariant = 'default',
+ triggerClassName,
+}: DispatchPanelProps) {
+ const [open, setOpen] = useState(false);
+ const [agents, setAgents] = useState
([]);
+ const [selectedAgentId, setSelectedAgentId] = useState('');
+ const [prompt, setPrompt] = useState('');
+ const [entityRef, setEntityRef] = useState('');
+ const [dispatching, setDispatching] = useState(false);
+ const [loadingAgents, setLoadingAgents] = useState(false);
+
+ const fetchAgents = useCallback(async () => {
+ setLoadingAgents(true);
+ try {
+ const res = await fetch('/api/agents');
+ if (!res.ok) throw new Error('Failed to fetch agents');
+ const data = await res.json();
+ const activeAgents = (data.items || []).filter(
+ (a: Agent) => a.status === 'active'
+ );
+ setAgents(activeAgents);
+ if (activeAgents.length > 0 && !selectedAgentId) {
+ setSelectedAgentId(activeAgents[0].id);
+ }
+ } catch (err) {
+ console.error('Failed to fetch agents:', err);
+ } finally {
+ setLoadingAgents(false);
+ }
+ }, [selectedAgentId]);
+
+ useEffect(() => {
+ if (open) {
+ fetchAgents();
+ }
+ }, [open, fetchAgents]);
+
+ async function handleDispatch() {
+ if (!selectedAgentId || !prompt.trim()) {
+ toast.error('Please select an agent and enter a prompt');
+ return;
+ }
+
+ setDispatching(true);
+ try {
+ const body: Record = {
+ agent_id: selectedAgentId,
+ task_type: 'custom',
+ input: { prompt: prompt.trim() },
+ };
+
+ if (entityRef.trim()) {
+ // Parse entity reference: "type:id" or just a free-form reference
+ body.entity_type = 'reference';
+ body.entity_id = entityRef.trim();
+ }
+
+ const res = await fetch('/api/agent-tasks', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+
+ if (!res.ok) {
+ const text = await res.text();
+ console.error('Failed to dispatch task:', text);
+ toast.error('Failed to dispatch task');
+ return;
+ }
+
+ toast.success('Task dispatched — see Agent Activity for results');
+ setPrompt('');
+ setEntityRef('');
+ setOpen(false);
+ } catch (err) {
+ console.error('Failed to dispatch task:', err);
+ toast.error('Failed to dispatch task');
+ } finally {
+ setDispatching(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ Dispatch AI Agent
+
+
+ Send a task to an AI agent and view results in Agent Activity.
+
+
+
+
+ {/* Agent selector */}
+
+
+ {loadingAgents ? (
+
+
+ Loading agents...
+
+ ) : agents.length === 0 ? (
+
+ No active agents available
+
+ ) : (
+
+ )}
+
+
+ {/* Prompt */}
+
+
+
+
+ {/* Entity reference (optional) */}
+
+
+
setEntityRef(e.target.value)}
+ placeholder="e.g. task:abc123 or project:xyz"
+ />
+
+ Reference a specific entity the agent should work on.
+
+
+
+ {/* Dispatch button */}
+
+
+
+
+ );
+}