From 0c2cabe702ef6a45d0b2906498f6130427077ba0 Mon Sep 17 00:00:00 2001 From: Guy Ben-Aharon Date: Sun, 8 Feb 2026 15:33:48 +0200 Subject: [PATCH 1/3] fix: add activity --- apps/web/src/app/(dashboard)/board/page.tsx | 26 +-- apps/web/src/app/(dashboard)/layout.tsx | 4 +- apps/web/src/app/globals.css | 7 + apps/web/src/components/kanban/CLAUDE.md | 6 +- .../src/components/kanban/kanban-board.tsx | 8 +- .../src/components/kanban/kanban-column.tsx | 7 +- apps/web/src/components/kanban/types.ts | 15 +- apps/web/src/components/live-feed/index.ts | 3 + .../components/live-feed/live-feed-item.tsx | 162 +++++++++++++++++ .../src/components/live-feed/live-feed.tsx | 167 ++++++++++++++++++ apps/web/src/components/live-feed/types.ts | 20 +++ apps/web/src/hooks/use-chat.ts | 5 + 12 files changed, 408 insertions(+), 22 deletions(-) create mode 100644 apps/web/src/components/live-feed/index.ts create mode 100644 apps/web/src/components/live-feed/live-feed-item.tsx create mode 100644 apps/web/src/components/live-feed/live-feed.tsx create mode 100644 apps/web/src/components/live-feed/types.ts diff --git a/apps/web/src/app/(dashboard)/board/page.tsx b/apps/web/src/app/(dashboard)/board/page.tsx index ba5e2c1..9af6987 100644 --- a/apps/web/src/app/(dashboard)/board/page.tsx +++ b/apps/web/src/app/(dashboard)/board/page.tsx @@ -12,6 +12,7 @@ import { type KanbanTask, type KanbanColumnDef, } from "@/components/kanban"; +import { LiveFeed } from "@/components/live-feed"; // Map priority from Convex to Kanban format function mapPriority(priority?: string): "low" | "medium" | "high" { @@ -78,6 +79,7 @@ const BoardPage = () => { done: [], }; + // Add real tasks from Convex if (tasks) { for (const task of tasks) { if (isValidStatus(task.status)) { @@ -90,13 +92,13 @@ const BoardPage = () => { { id: "inbox", title: "Inbox", - variant: "todo", + variant: "inbox", tasks: groupedTasks.inbox, }, { id: "assigned", title: "Assigned", - variant: "todo", + variant: "assigned", tasks: groupedTasks.assigned, }, { @@ -108,7 +110,7 @@ const BoardPage = () => { { id: "review", title: "Review", - variant: "in-review", + variant: "review", tasks: groupedTasks.review, }, { @@ -121,20 +123,22 @@ const BoardPage = () => { return ( <> - + Board -
- {!tasks ? ( -
- Loading... -
- ) : ( +
+ {/* Kanban Board */} +
- )} +
+ + {/* Live Feed Sidebar */} +
+ +
); diff --git a/apps/web/src/app/(dashboard)/layout.tsx b/apps/web/src/app/(dashboard)/layout.tsx index d08c2d2..1c5639e 100644 --- a/apps/web/src/app/(dashboard)/layout.tsx +++ b/apps/web/src/app/(dashboard)/layout.tsx @@ -54,7 +54,9 @@ const DashboardLayout = ({ children, header }: DashboardLayoutProps) => { {header} {fullHeight ? ( -
{children}
+
+ {children} +
) : (
{children}
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 052c2bb..c915b6f 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -1 +1,8 @@ /* App-specific styles (base styles are in @clawe/ui/globals.css) */ + +/* Kanban board: Override Radix ScrollArea's display:table to allow full-height columns */ +[data-kanban-board] [data-radix-scroll-area-viewport] > div { + display: flex !important; + flex-direction: column; + height: 100%; +} diff --git a/apps/web/src/components/kanban/CLAUDE.md b/apps/web/src/components/kanban/CLAUDE.md index d7efb10..220b979 100644 --- a/apps/web/src/components/kanban/CLAUDE.md +++ b/apps/web/src/components/kanban/CLAUDE.md @@ -17,9 +17,11 @@ Display-only task kanban board with subtask support. import { KanbanBoard, type KanbanTask, type KanbanColumnDef } from "@/components/kanban"; const columns: KanbanColumnDef[] = [ - { id: "pending", title: "To Do", variant: "todo", tasks: [...] }, + { id: "inbox", title: "Inbox", variant: "inbox", tasks: [...] }, + { id: "assigned", title: "Assigned", variant: "assigned", tasks: [...] }, { id: "in_progress", title: "In Progress", variant: "in-progress", tasks: [...] }, - { id: "completed", title: "Done", variant: "done", tasks: [...] }, + { id: "review", title: "Review", variant: "review", tasks: [...] }, + { id: "done", title: "Done", variant: "done", tasks: [...] }, ]; diff --git a/apps/web/src/components/kanban/kanban-board.tsx b/apps/web/src/components/kanban/kanban-board.tsx index 75c4ce5..b5951fe 100644 --- a/apps/web/src/components/kanban/kanban-board.tsx +++ b/apps/web/src/components/kanban/kanban-board.tsx @@ -22,8 +22,12 @@ export const KanbanBoard = ({ columns, className }: KanbanBoardProps) => { return ( <> - -
+ +
{columns.map((column) => ( = { - todo: , + inbox: , + assigned: , "in-progress": , - "in-review": , + review: , done: , }; diff --git a/apps/web/src/components/kanban/types.ts b/apps/web/src/components/kanban/types.ts index 56ab26f..500bd4c 100644 --- a/apps/web/src/components/kanban/types.ts +++ b/apps/web/src/components/kanban/types.ts @@ -9,7 +9,12 @@ export type KanbanTask = { }; // Predefined column variants with built-in styling -export type ColumnVariant = "todo" | "in-progress" | "in-review" | "done"; +export type ColumnVariant = + | "inbox" + | "assigned" + | "in-progress" + | "review" + | "done"; export type KanbanColumnDef = { id: string; @@ -28,15 +33,19 @@ export const columnVariants: Record< ColumnVariant, { badge: string; column: string } > = { - todo: { + inbox: { badge: "bg-gray-200 text-gray-600 dark:bg-gray-800 dark:text-gray-300", column: "bg-gray-100 dark:bg-zinc-900", }, + assigned: { + badge: "bg-amber-500 text-white", + column: "bg-amber-50 dark:bg-amber-950/30", + }, "in-progress": { badge: "bg-blue-500 text-white", column: "bg-blue-50 dark:bg-blue-950/30", }, - "in-review": { + review: { badge: "bg-purple-500 text-white", column: "bg-purple-50 dark:bg-purple-950/30", }, diff --git a/apps/web/src/components/live-feed/index.ts b/apps/web/src/components/live-feed/index.ts new file mode 100644 index 0000000..b63e381 --- /dev/null +++ b/apps/web/src/components/live-feed/index.ts @@ -0,0 +1,3 @@ +export { LiveFeed } from "./live-feed"; +export { LiveFeedItem } from "./live-feed-item"; +export type * from "./types"; diff --git a/apps/web/src/components/live-feed/live-feed-item.tsx b/apps/web/src/components/live-feed/live-feed-item.tsx new file mode 100644 index 0000000..753201d --- /dev/null +++ b/apps/web/src/components/live-feed/live-feed-item.tsx @@ -0,0 +1,162 @@ +"use client"; + +import { + Heart, + CheckCircle2, + MessageSquare, + FileText, + Bell, + Zap, +} from "lucide-react"; +import { cn } from "@clawe/ui/lib/utils"; +import type { FeedActivity } from "./types"; + +const formatRelativeTime = (timestamp: number): string => { + const now = Date.now(); + const diff = now - timestamp; + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(diff / 3600000); + const days = Math.floor(diff / 86400000); + + if (seconds < 60) return "just now"; + if (minutes < 60) return `${minutes}m ago`; + if (hours < 24) return `${hours}h ago`; + if (days === 1) return "yesterday"; + return `${days}d ago`; +}; + +// Generate a consistent color based on agent name +const getAgentColor = (name: string): string => { + // Light, colorful pastels + const colors = [ + "bg-violet-300 dark:bg-violet-400", + "bg-blue-300 dark:bg-blue-400", + "bg-emerald-300 dark:bg-emerald-400", + "bg-amber-300 dark:bg-amber-400", + "bg-rose-300 dark:bg-rose-400", + "bg-cyan-300 dark:bg-cyan-400", + "bg-fuchsia-300 dark:bg-fuchsia-400", + "bg-teal-300 dark:bg-teal-400", + ] as const; + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash); + } + return ( + colors[Math.abs(hash) % colors.length] ?? "bg-violet-300 dark:bg-violet-400" + ); +}; + +// Get activity type config +const getActivityConfig = ( + type: FeedActivity["type"], +): { icon: React.ReactNode; color: string; verb: string } => { + switch (type) { + case "agent_heartbeat": + return { + icon: , + color: "text-emerald-500", + verb: "is online", + }; + case "task_created": + return { + icon: , + color: "text-blue-500", + verb: "created", + }; + case "task_assigned": + return { + icon: , + color: "text-amber-500", + verb: "was assigned", + }; + case "task_status_changed": + return { + icon: , + color: "text-violet-500", + verb: "updated", + }; + case "subtask_completed": + return { + icon: , + color: "text-emerald-500", + verb: "completed", + }; + case "message_sent": + return { + icon: , + color: "text-blue-500", + verb: "commented", + }; + case "notification_sent": + return { + icon: , + color: "text-amber-500", + verb: "notified", + }; + default: + return { + icon: , + color: "text-muted-foreground", + verb: "", + }; + } +}; + +export type LiveFeedItemProps = { + activity: FeedActivity; + isLast?: boolean; + className?: string; +}; + +export const LiveFeedItem = ({ + activity, + isLast = false, + className, +}: LiveFeedItemProps) => { + const agentName = activity.agent?.name || "Unknown"; + const agentEmoji = activity.agent?.emoji || "🤖"; + const agentColor = getAgentColor(agentName); + const config = getActivityConfig(activity.type); + + const taskTitle = activity.task?.title; + + return ( +
+ {/* Timeline line */} + {!isLast && ( +
+ )} + + {/* Avatar with emoji */} +
+ {agentEmoji} +
+ + {/* Content */} +
+
+ {agentName} + + {config.icon} + {config.verb} + + {taskTitle && ( + + {taskTitle} + + )} +
+ + {formatRelativeTime(activity.createdAt)} + +
+
+ ); +}; diff --git a/apps/web/src/components/live-feed/live-feed.tsx b/apps/web/src/components/live-feed/live-feed.tsx new file mode 100644 index 0000000..3b01eab --- /dev/null +++ b/apps/web/src/components/live-feed/live-feed.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { useQuery } from "convex/react"; +import { api } from "@clawe/backend"; +import { cn } from "@clawe/ui/lib/utils"; +import { ScrollArea } from "@clawe/ui/components/scroll-area"; +import { Activity, Loader2 } from "lucide-react"; +import { LiveFeedItem } from "./live-feed-item"; +import type { FeedActivity, FeedFilter } from "./types"; + +const FILTER_CONFIG: { + id: FeedFilter; + label: string; + types: FeedActivity["type"][]; +}[] = [ + { id: "all", label: "All", types: [] }, + { + id: "tasks", + label: "Tasks", + types: [ + "task_created", + "task_assigned", + "task_status_changed", + "subtask_completed", + ], + }, + { + id: "status", + label: "Messages", + types: ["message_sent", "notification_sent"], + }, + { id: "heartbeats", label: "Online", types: ["agent_heartbeat"] }, +]; + +export type LiveFeedProps = { + className?: string; + limit?: number; +}; + +export const LiveFeed = ({ className, limit = 50 }: LiveFeedProps) => { + const [activeFilter, setActiveFilter] = useState("all"); + + const activities = useQuery(api.activities.feed, { limit }); + + const filteredActivities = useMemo(() => { + if (!activities) return []; + + const filterConfig = FILTER_CONFIG.find((f) => f.id === activeFilter); + if (!filterConfig || filterConfig.types.length === 0) { + return activities as FeedActivity[]; + } + + return (activities as FeedActivity[]).filter((activity) => + filterConfig.types.includes(activity.type), + ); + }, [activities, activeFilter]); + + const filterCounts = useMemo((): Record => { + if (!activities) { + return { all: 0, tasks: 0, status: 0, heartbeats: 0 }; + } + + const counts: Record = { + all: activities.length, + tasks: 0, + status: 0, + heartbeats: 0, + }; + + for (const activity of activities as FeedActivity[]) { + for (const filter of FILTER_CONFIG) { + if (filter.types.includes(activity.type)) { + counts[filter.id]++; + } + } + } + + return counts; + }, [activities]); + + return ( +
+ {/* Header */} +
+
+
+
+ + + + +
+

Activity

+
+ + {filteredActivities.length} + +
+ + {/* Filter tabs */} +
+ {FILTER_CONFIG.map((filter) => { + const count = filterCounts[filter.id] ?? 0; + const isActive = activeFilter === filter.id; + + return ( + + ); + })} +
+
+ + {/* Feed list */} + +
+ {!activities ? ( +
+ + + Loading activity... + +
+ ) : filteredActivities.length === 0 ? ( +
+ + + No activity yet + +
+ ) : ( + filteredActivities.map((activity, index) => ( + + )) + )} +
+
+
+ ); +}; diff --git a/apps/web/src/components/live-feed/types.ts b/apps/web/src/components/live-feed/types.ts new file mode 100644 index 0000000..c6358bd --- /dev/null +++ b/apps/web/src/components/live-feed/types.ts @@ -0,0 +1,20 @@ +import type { Doc, Id } from "@clawe/backend/dataModel"; + +// Activity type from backend schema +export type ActivityType = Doc<"activities">["type"]; + +// Enriched activity returned by activities.feed query +export type FeedActivity = Doc<"activities"> & { + agent: { + _id: Id<"agents">; + name: string; + emoji?: string; + } | null; + task: { + _id: Id<"tasks">; + title: string; + status: string; + } | null; +}; + +export type FeedFilter = "all" | "tasks" | "status" | "heartbeats"; diff --git a/apps/web/src/hooks/use-chat.ts b/apps/web/src/hooks/use-chat.ts index d8836c9..08dad09 100644 --- a/apps/web/src/hooks/use-chat.ts +++ b/apps/web/src/hooks/use-chat.ts @@ -43,6 +43,11 @@ const SYSTEM_MESSAGE_PATTERNS = [ /^System:\s*\[\d{4}-\d{2}-\d{2}/i, /^Cron:/i, /HEARTBEAT_OK/i, + // Agent startup/system file content + /^#\s*WORKING\.md/i, + /---EXIT---/i, + /clawe not found/i, + /This file is the shared memory across all agent/i, ]; const isSystemMessage = (content: string): boolean => { From 66b15185b20d95f469a7575468f9bdacb4142472 Mon Sep 17 00:00:00 2001 From: Guy Ben-Aharon Date: Sun, 8 Feb 2026 17:01:28 +0200 Subject: [PATCH 2/3] fix: add activity --- apps/web/package.json | 3 + .../(dashboard)/_components/nav-settings.tsx | 7 +- .../_components/business-settings-form.tsx | 201 ++++ .../(dashboard)/settings/business/page.tsx | 26 + .../_components/general-settings-form.tsx | 2 +- .../web/src/app/api/business/context/route.ts | 109 ++ apps/web/src/app/setup/business/page.tsx | 58 ++ apps/web/src/app/setup/complete/page.tsx | 4 +- apps/web/src/app/setup/layout.tsx | 2 +- apps/web/src/app/setup/telegram/page.tsx | 4 +- apps/web/src/app/setup/welcome/page.tsx | 16 +- apps/web/src/components/chat/chat-message.tsx | 132 ++- apps/web/src/components/chat/chat.tsx | 26 +- docker/openclaw/templates/shared/CLAWE-CLI.md | 58 +- .../templates/workspaces/clawe/BOOTSTRAP.md | 86 +- packages/backend/convex/_generated/api.d.ts | 2 + packages/backend/convex/businessContext.ts | 114 +++ packages/backend/convex/schema.ts | 22 + packages/cli/src/commands/business-get.ts | 31 + packages/cli/src/commands/business-set.ts | 81 ++ packages/cli/src/index.ts | 39 + packages/ui/src/components/textarea.tsx | 18 + pnpm-lock.yaml | 962 ++++++++++++++++++ 23 files changed, 1963 insertions(+), 40 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/settings/business/_components/business-settings-form.tsx create mode 100644 apps/web/src/app/(dashboard)/settings/business/page.tsx create mode 100644 apps/web/src/app/api/business/context/route.ts create mode 100644 apps/web/src/app/setup/business/page.tsx create mode 100644 packages/backend/convex/businessContext.ts create mode 100644 packages/cli/src/commands/business-get.ts create mode 100644 packages/cli/src/commands/business-set.ts create mode 100644 packages/ui/src/components/textarea.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 8d2484c..8b031cc 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -44,6 +44,9 @@ "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-markdown": "^10.1.0", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", "tippy.js": "^6.3.7", "ws": "^8.19.0", "zod": "^4.3.6" diff --git a/apps/web/src/app/(dashboard)/_components/nav-settings.tsx b/apps/web/src/app/(dashboard)/_components/nav-settings.tsx index eeb8fc3..c4db36c 100644 --- a/apps/web/src/app/(dashboard)/_components/nav-settings.tsx +++ b/apps/web/src/app/(dashboard)/_components/nav-settings.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; -import { ChevronLeft, Settings2, AlertTriangle } from "lucide-react"; +import { ChevronLeft, Settings2, AlertTriangle, Globe } from "lucide-react"; import { SidebarGroup, SidebarGroupLabel, @@ -19,6 +19,11 @@ const settingsItems = [ url: "/settings/general", icon: Settings2, }, + { + title: "Business", + url: "/settings/business", + icon: Globe, + }, { title: "Danger zone", url: "/settings/danger-zone", diff --git a/apps/web/src/app/(dashboard)/settings/business/_components/business-settings-form.tsx b/apps/web/src/app/(dashboard)/settings/business/_components/business-settings-form.tsx new file mode 100644 index 0000000..c71e513 --- /dev/null +++ b/apps/web/src/app/(dashboard)/settings/business/_components/business-settings-form.tsx @@ -0,0 +1,201 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useQuery, useMutation } from "convex/react"; +import { api } from "@clawe/backend"; +import { Button } from "@clawe/ui/components/button"; +import { Input } from "@clawe/ui/components/input"; +import { Label } from "@clawe/ui/components/label"; +import { Textarea } from "@clawe/ui/components/textarea"; +import { Spinner } from "@clawe/ui/components/spinner"; +import { Globe, Building2, Users, Palette } from "lucide-react"; + +export const BusinessSettingsForm = () => { + const businessContext = useQuery(api.businessContext.get); + const saveBusinessContext = useMutation(api.businessContext.save); + + const [url, setUrl] = useState(""); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [industry, setIndustry] = useState(""); + const [targetAudience, setTargetAudience] = useState(""); + const [tone, setTone] = useState(""); + const [isDirty, setIsDirty] = useState(false); + const [isSaving, setIsSaving] = useState(false); + + // Load existing business context + useEffect(() => { + if (businessContext) { + setUrl(businessContext.url ?? ""); + setName(businessContext.name ?? ""); + setDescription(businessContext.description ?? ""); + setIndustry(businessContext.metadata?.industry ?? ""); + setTargetAudience(businessContext.metadata?.targetAudience ?? ""); + setTone(businessContext.metadata?.tone ?? ""); + setIsDirty(false); + } + }, [businessContext]); + + const handleChange = ( + setter: React.Dispatch>, + ) => { + return (e: React.ChangeEvent) => { + setter(e.target.value); + setIsDirty(true); + }; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!isDirty || !url) return; + + setIsSaving(true); + try { + await saveBusinessContext({ + url, + name: name || undefined, + description: description || undefined, + metadata: { + industry: industry || undefined, + targetAudience: targetAudience || undefined, + tone: tone || undefined, + }, + approved: true, + }); + setIsDirty(false); + } finally { + setIsSaving(false); + } + }; + + // Loading state + if (businessContext === undefined) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Website URL - Primary field */} +
+ + +

+ Your business website. This helps agents understand your brand and + context. +

+
+ + {/* Business Name */} +
+ + +

+ The name of your business or brand. +

+
+ + {/* Description */} +
+ +