-
Agent Activity
-
- Every agent action, visible and reversible.
-
- {feedback && (
-
- {feedback.message}
+
+
+
Agent Activity
+
+ Every agent action, visible and reversible.
- )}
+
+
+ {feedback && (
+
+ {feedback.message}
+
+ )}
{/* Agents list */}
diff --git a/apps/web/app/(dashboard)/canvas/page.tsx b/apps/web/app/(dashboard)/canvas/page.tsx
new file mode 100644
index 0000000..7207141
--- /dev/null
+++ b/apps/web/app/(dashboard)/canvas/page.tsx
@@ -0,0 +1,523 @@
+'use client';
+
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { LayoutGrid, Plus, Trash2, GripVertical, X, ZoomIn, ZoomOut, Maximize2 } from 'lucide-react';
+import { toast } from 'sonner';
+import { Button } from '@/components/ui/button';
+import { Card } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+
+interface CanvasCard {
+ id: string;
+ canvas_id: string;
+ type: 'note' | 'task' | 'image' | 'entity';
+ entity_id?: string;
+ title?: string;
+ content?: string;
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ rotation: number;
+ color?: string;
+ z_index: number;
+ created: string;
+ updated: string;
+}
+
+interface CanvasConnection {
+ id: string;
+ source_card_id: string;
+ target_card_id: string;
+ label?: string;
+ style: 'solid' | 'dashed' | 'dotted';
+}
+
+interface Canvas {
+ id: string;
+ name: string;
+ description?: string;
+ mode: 'freeform' | 'graph';
+ domain: string;
+ tags: string[];
+ cards: CanvasCard[];
+ connections: CanvasConnection[];
+ viewport?: { x: number; y: number; zoom: number };
+ background?: string;
+ created: string;
+ updated: string;
+}
+
+function CanvasBoard({ canvas, onBack }: { canvas: Canvas; onBack: () => void }) {
+ const [cards, setCards] = useState
(canvas.cards || []);
+ const [dragging, setDragging] = useState(null);
+ const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
+ const [viewport, setViewport] = useState(canvas.viewport || { x: 0, y: 0, zoom: 1 });
+ const [editingCard, setEditingCard] = useState(null);
+ const [editTitle, setEditTitle] = useState('');
+ const [editContent, setEditContent] = useState('');
+ const boardRef = useRef(null);
+
+ const handlePointerDown = useCallback(
+ (e: React.PointerEvent, cardId: string) => {
+ e.preventDefault();
+ const card = cards.find((c) => c.id === cardId);
+ if (!card) return;
+ setDragging(cardId);
+ setDragOffset({
+ x: e.clientX - card.x * viewport.zoom,
+ y: e.clientY - card.y * viewport.zoom,
+ });
+ (e.target as HTMLElement).setPointerCapture(e.pointerId);
+ },
+ [cards, viewport.zoom]
+ );
+
+ const handlePointerMove = useCallback(
+ (e: React.PointerEvent) => {
+ if (!dragging) return;
+ const newX = (e.clientX - dragOffset.x) / viewport.zoom;
+ const newY = (e.clientY - dragOffset.y) / viewport.zoom;
+ setCards((prev) =>
+ prev.map((c) => (c.id === dragging ? { ...c, x: Math.max(0, newX), y: Math.max(0, newY) } : c))
+ );
+ },
+ [dragging, dragOffset, viewport.zoom]
+ );
+
+ const handlePointerUp = useCallback(() => {
+ setDragging(null);
+ }, []);
+
+ async function saveCardPosition(card: CanvasCard) {
+ try {
+ await fetch(`/api/canvases/${canvas.id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ cards: cards.map((c) =>
+ c.id === card.id
+ ? { ...c, x: card.x, y: card.y }
+ : c
+ ),
+ }),
+ });
+ } catch (err) {
+ console.error('Failed to save card position:', err);
+ }
+ }
+
+ async function addCard() {
+ const newCard: CanvasCard = {
+ id: crypto.randomUUID(),
+ canvas_id: canvas.id,
+ type: 'note',
+ title: 'New note',
+ content: '',
+ x: 50 + Math.random() * 200,
+ y: 50 + Math.random() * 200,
+ width: 200,
+ height: 150,
+ rotation: 0,
+ z_index: cards.length,
+ created: new Date().toISOString(),
+ updated: new Date().toISOString(),
+ };
+ const updatedCards = [...cards, newCard];
+ setCards(updatedCards);
+ try {
+ await fetch(`/api/canvases/${canvas.id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ cards: updatedCards }),
+ });
+ toast.success('Card added');
+ } catch (err) {
+ console.error('Failed to add card:', err);
+ toast.error('Failed to add card');
+ }
+ }
+
+ async function deleteCard(cardId: string) {
+ const updatedCards = cards.filter((c) => c.id !== cardId);
+ setCards(updatedCards);
+ try {
+ await fetch(`/api/canvases/${canvas.id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ cards: updatedCards }),
+ });
+ toast.success('Card removed');
+ } catch (err) {
+ console.error('Failed to delete card:', err);
+ toast.error('Failed to delete card');
+ }
+ }
+
+ async function saveCardEdit() {
+ if (!editingCard) return;
+ const updatedCards = cards.map((c) =>
+ c.id === editingCard.id
+ ? { ...c, title: editTitle, content: editContent }
+ : c
+ );
+ setCards(updatedCards);
+ setEditingCard(null);
+ try {
+ await fetch(`/api/canvases/${canvas.id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ cards: updatedCards }),
+ });
+ } catch (err) {
+ console.error('Failed to save card:', err);
+ }
+ }
+
+ function openEdit(card: CanvasCard) {
+ setEditingCard(card);
+ setEditTitle(card.title || '');
+ setEditContent(card.content || '');
+ }
+
+ return (
+
+ {/* Toolbar */}
+
+
+
+
{canvas.name}
+
+
+
+
+ {Math.round(viewport.zoom * 100)}%
+
+
+
+
+
+
+
+ {/* Board */}
+
+
+ {/* Connections */}
+
+
+ {/* Cards */}
+ {cards.map((card) => (
+
+ {/* Drag handle */}
+
handlePointerDown(e, card.id)}
+ style={{ touchAction: 'none' }}
+ >
+
+
+ {card.title || 'Untitled'}
+
+
+
+
+ {/* Content */}
+
+ {card.content || 'No content'}
+
+
+ ))}
+
+ {cards.length === 0 && (
+
+
+
No cards yet
+
+
+ )}
+
+
+
+ {/* Edit dialog */}
+
+
+ );
+}
+
+export default function CanvasPage() {
+ const [canvases, setCanvases] = useState
+ );
+}
diff --git a/apps/web/app/(dashboard)/layout.tsx b/apps/web/app/(dashboard)/layout.tsx
index 044fa2c..96286f1 100644
--- a/apps/web/app/(dashboard)/layout.tsx
+++ b/apps/web/app/(dashboard)/layout.tsx
@@ -4,6 +4,7 @@ import { NetworkErrorBanner } from '@/components/network-error-banner';
import { KeyboardShortcutsProvider } from '@/components/keyboard-shortcuts-provider';
import { WebVitalsTracker } from '@/components/web-vitals-tracker';
import { MobileBottomNav } from '@/components/mobile-bottom-nav';
+import { DispatchPanel } from '@/components/agents/dispatch-panel';
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
@@ -23,6 +24,14 @@ export default function DashboardLayout({ children }: { children: React.ReactNod