diff --git a/client/src/App.tsx b/client/src/App.tsx
index 57a89c0..b1b4d76 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -9,6 +9,7 @@ import { SoeMotorProvider } from "@/contexts/SoeMotorContext";
import { ProtectedRoute } from "@/lib/protected-route";
import { CommandPalette } from "@/components/CommandPalette";
import { KnowledgeCollectorInit } from "@/components/KnowledgeCollectorInit";
+import { OpenClawWidget } from "@/components/openclaw/OpenClawWidget";
import NotFound from "@/pages/not-found";
import AuthPage from "@/pages/auth-page";
@@ -164,6 +165,7 @@ function App() {
+
diff --git a/client/src/components/openclaw/OpenClawWidget.tsx b/client/src/components/openclaw/OpenClawWidget.tsx
new file mode 100644
index 0000000..e84e3bd
--- /dev/null
+++ b/client/src/components/openclaw/OpenClawWidget.tsx
@@ -0,0 +1,131 @@
+/**
+ * OpenClawWidget.tsx
+ *
+ * Widget flutuante do OpenClaw — aparece quando há sugestões pendentes.
+ * Posicionado no canto inferior direito, fora das rotas.
+ * Abre o modal SkillSuggestion para confirmação.
+ *
+ * Fase 4: OpenClaw Sprint 2
+ */
+
+import { useState } from "react";
+import { Sparkles, X, ChevronUp, ChevronDown } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { useAgentEmergence } from "@/hooks/useAgentEmergence";
+import { SkillSuggestion } from "./SkillSuggestion";
+import { useAuth } from "@/hooks/use-auth";
+
+export function OpenClawWidget() {
+ const { user } = useAuth();
+ const {
+ suggestions,
+ activeSuggestion,
+ pendingCount,
+ confirmSkill,
+ rejectSkill,
+ openSuggestion,
+ closeSuggestion,
+ } = useAgentEmergence();
+
+ const [collapsed, setCollapsed] = useState(false);
+ const [dismissed, setDismissed] = useState(false);
+
+ // Não renderiza se não há usuário, não há sugestões, ou foi dispensado
+ if (!user || pendingCount === 0 || dismissed) return null;
+
+ return (
+ <>
+ {/* Widget flutuante */}
+
+ {/* Painel expandido */}
+ {!collapsed && (
+
+
+
+
+
+ OpenClaw
+
+
+ {pendingCount}
+
+
+
+
+
+
+
+
+
+
+ Detectei padrões de uso que podem virar skills automáticas:
+
+ {suggestions.map((s) => (
+
+ ))}
+
+
+ )}
+
+ {/* Botão minimizado */}
+ {collapsed && (
+
+ )}
+
+
+ {/* Modal de detalhes */}
+ {activeSuggestion && (
+
+ )}
+ >
+ );
+}
diff --git a/client/src/components/openclaw/SkillSuggestion.tsx b/client/src/components/openclaw/SkillSuggestion.tsx
new file mode 100644
index 0000000..77f2671
--- /dev/null
+++ b/client/src/components/openclaw/SkillSuggestion.tsx
@@ -0,0 +1,101 @@
+/**
+ * SkillSuggestion.tsx
+ *
+ * Modal com preview da skill sugerida pelo OpenClaw.
+ * Apresenta padrão detectado, nome, descrição e preview de automação.
+ * Usuário pode confirmar ou rejeitar.
+ *
+ * Fase 4: OpenClaw Sprint 2
+ */
+
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Sparkles, X, Check, Zap, BarChart2 } from "lucide-react";
+import type { SkillSuggestionData } from "@/hooks/useAgentEmergence";
+
+interface SkillSuggestionProps {
+ suggestion: SkillSuggestionData;
+ onConfirm: (id: string) => void;
+ onReject: (id: string) => void;
+ onClose: () => void;
+}
+
+export function SkillSuggestion({ suggestion, onConfirm, onReject, onClose }: SkillSuggestionProps) {
+ const confidencePct = Math.round(suggestion.confidence * 100);
+
+ return (
+
+ );
+}
diff --git a/client/src/hooks/useAgentEmergence.ts b/client/src/hooks/useAgentEmergence.ts
new file mode 100644
index 0000000..0a392a7
--- /dev/null
+++ b/client/src/hooks/useAgentEmergence.ts
@@ -0,0 +1,143 @@
+/**
+ * useAgentEmergence.ts
+ *
+ * Hook para gerenciar sugestões de skills do OpenClaw.
+ * Escuta eventos Socket.IO e coordena confirmações/rejeições.
+ *
+ * Fase 4: OpenClaw Sprint 2
+ */
+
+import { useState, useEffect, useCallback } from "react";
+import { useAuth } from "./use-auth";
+
+export interface SkillSuggestionData {
+ id: string;
+ pattern: {
+ id: string;
+ action_type: string;
+ frequency: number;
+ confidence: number;
+ description: string;
+ };
+ suggested_skill_name: string;
+ suggested_description: string;
+ estimated_automation: string;
+ confidence: number;
+ created_at: string;
+ status: "pending" | "accepted" | "rejected";
+}
+
+interface UseAgentEmergenceReturn {
+ suggestions: SkillSuggestionData[];
+ activeSuggestion: SkillSuggestionData | null;
+ isOpen: boolean;
+ pendingCount: number;
+ confirmSkill: (suggestionId: string) => Promise;
+ rejectSkill: (suggestionId: string) => Promise;
+ openSuggestion: (suggestion: SkillSuggestionData) => void;
+ closeSuggestion: () => void;
+ dismissWidget: () => void;
+}
+
+export function useAgentEmergence(): UseAgentEmergenceReturn {
+ const { user } = useAuth();
+ const [suggestions, setSuggestions] = useState([]);
+ const [activeSuggestion, setActiveSuggestion] = useState(null);
+ const [isOpen, setIsOpen] = useState(false);
+
+ // Carrega sugestões pendentes do servidor
+ const fetchPendingSuggestions = useCallback(async () => {
+ if (!user) return;
+ try {
+ const res = await fetch("/api/openclaw/suggestions");
+ if (res.ok) {
+ const data = await res.json();
+ setSuggestions(data.suggestions || []);
+ }
+ } catch {
+ // silencioso — não bloqueia o usuário
+ }
+ }, [user]);
+
+ // Escuta eventos Socket.IO via window (padrão da suite)
+ useEffect(() => {
+ if (!user) return;
+
+ fetchPendingSuggestions();
+
+ const handleOpenClawEvent = (event: CustomEvent) => {
+ const suggestion: SkillSuggestionData = event.detail;
+ setSuggestions((prev) => {
+ const exists = prev.find((s) => s.id === suggestion.id);
+ if (exists) return prev;
+ return [suggestion, ...prev];
+ });
+ };
+
+ window.addEventListener("openclaw:suggestion" as any, handleOpenClawEvent);
+ return () => {
+ window.removeEventListener("openclaw:suggestion" as any, handleOpenClawEvent);
+ };
+ }, [user, fetchPendingSuggestions]);
+
+ const confirmSkill = useCallback(async (suggestionId: string) => {
+ try {
+ const res = await fetch("/api/openclaw/confirm-skill", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ suggestion_id: suggestionId, action: "confirm" }),
+ });
+ if (res.ok) {
+ setSuggestions((prev) => prev.filter((s) => s.id !== suggestionId));
+ setActiveSuggestion(null);
+ setIsOpen(false);
+ }
+ } catch {
+ // erro tratado na UI via toast externo
+ }
+ }, []);
+
+ const rejectSkill = useCallback(async (suggestionId: string) => {
+ try {
+ const res = await fetch("/api/openclaw/confirm-skill", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ suggestion_id: suggestionId, action: "reject" }),
+ });
+ if (res.ok) {
+ setSuggestions((prev) => prev.filter((s) => s.id !== suggestionId));
+ setActiveSuggestion(null);
+ setIsOpen(false);
+ }
+ } catch {
+ // silencioso
+ }
+ }, []);
+
+ const openSuggestion = useCallback((suggestion: SkillSuggestionData) => {
+ setActiveSuggestion(suggestion);
+ setIsOpen(true);
+ }, []);
+
+ const closeSuggestion = useCallback(() => {
+ setActiveSuggestion(null);
+ setIsOpen(false);
+ }, []);
+
+ const dismissWidget = useCallback(() => {
+ setIsOpen(false);
+ setActiveSuggestion(null);
+ }, []);
+
+ return {
+ suggestions,
+ activeSuggestion,
+ isOpen,
+ pendingCount: suggestions.length,
+ confirmSkill,
+ rejectSkill,
+ openSuggestion,
+ closeSuggestion,
+ dismissWidget,
+ };
+}