diff --git a/.gitignore b/.gitignore index 05d8971..6f04d85 100644 --- a/.gitignore +++ b/.gitignore @@ -112,6 +112,11 @@ temp/ # Manus version file (auto-generated, not part of source) client/public/__manus__/version.json +# Secrets — NEVER commit +.deploy-secrets +deploy-secrets +*.secret + # Kilocode config and working directories .kilo/ .manus/ diff --git a/client/src/components/TasksPanel.tsx b/client/src/components/TasksPanel.tsx index 4312fa8..b3aa707 100644 --- a/client/src/components/TasksPanel.tsx +++ b/client/src/components/TasksPanel.tsx @@ -1,23 +1,28 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { trpc } from "@/lib/trpc"; import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; -import { AlertCircle, CheckCircle2, Clock, Trash2, Plus } from "lucide-react"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + AlertCircle, + CheckCircle2, + Clock, + Trash2, + Plus, + ChevronDown, + ChevronRight, + Zap, + Loader2, + Terminal, +} from "lucide-react"; export interface TasksPanelProps { agentId?: number; conversationId?: string; } -/** - * TasksPanel — правая панель для отображения и управления задачами - */ -export function TasksPanel({ - agentId, - conversationId, -}: TasksPanelProps) { +export function TasksPanel({ agentId, conversationId }: TasksPanelProps) { const [tasks, setTasks] = useState([]); const [expandedTaskId, setExpandedTaskId] = useState(null); @@ -50,11 +55,8 @@ export function TasksPanel({ ...(newStatus === "completed" && { completedAt: new Date() }), ...(newStatus === "in_progress" && { startedAt: new Date() }), }); - if (result) { - setTasks((prev) => - prev.map((t) => (t.id === taskId ? result : t)) - ); + setTasks(prev => prev.map(t => (t.id === taskId ? result : t))); } } catch (error) { console.error("Failed to update task:", error); @@ -65,7 +67,7 @@ export function TasksPanel({ try { const success = await deleteTaskMutation.mutateAsync({ taskId }); if (success) { - setTasks((prev) => prev.filter((t) => t.id !== taskId)); + setTasks(prev => prev.filter(t => t.id !== taskId)); } } catch (error) { console.error("Failed to delete task:", error); @@ -75,173 +77,204 @@ export function TasksPanel({ const getStatusIcon = (status: string) => { switch (status) { case "completed": - return ; + return ; case "failed": - return ; + return ; case "in_progress": - return ; + return ; default: - return ; + return ; } }; - const getStatusColor = (status: string) => { - switch (status) { - case "completed": - return "bg-green-100 text-green-800"; - case "failed": - return "bg-red-100 text-red-800"; - case "in_progress": - return "bg-blue-100 text-blue-800"; - case "blocked": - return "bg-orange-100 text-orange-800"; - default: - return "bg-gray-100 text-gray-800"; - } + const getStatusBadge = (status: string) => { + const styles: Record = { + completed: "bg-[#00FF88]/10 text-[#00FF88] border-[#00FF88]/30", + failed: "bg-[#FF3366]/10 text-[#FF3366] border-[#FF3366]/30", + in_progress: "bg-[#00D4FF]/10 text-[#00D4FF] border-[#00D4FF]/30", + blocked: "bg-[#FFB800]/10 text-[#FFB800] border-[#FFB800]/30", + pending: "bg-[#FFB800]/10 text-[#FFB800]/70 border-[#FFB800]/30", + }; + return ( + styles[status] ?? "bg-[#FFB800]/10 text-[#FFB800]/70 border-[#FFB800]/30" + ); }; - const getPriorityColor = (priority: string) => { - switch (priority) { - case "critical": - return "bg-red-100 text-red-800"; - case "high": - return "bg-orange-100 text-orange-800"; - case "medium": - return "bg-yellow-100 text-yellow-800"; - default: - return "bg-gray-100 text-gray-800"; - } + const getPriorityBadge = (priority: string) => { + const styles: Record = { + critical: "bg-[#FF3366]/10 text-[#FF3366] border-[#FF3366]/30", + high: "bg-[#FFB800]/10 text-[#FFB800] border-[#FFB800]/30", + medium: "bg-[#00D4FF]/10 text-[#00D4FF]/70 border-[#00D4FF]/30", + low: "bg-muted/20 text-muted-foreground border-border/30", + }; + return ( + styles[priority] ?? "bg-muted/20 text-muted-foreground border-border/30" + ); }; return ( -
-
-
-

Tasks

- +
+ {/* Header */} +
+
+ + + TASKS + + {tasks.length}
-

- {agentId ? `Agent #${agentId}` : conversationId ? "Conversation" : "No selection"} -

+ + {agentId ? `Agent #${agentId}` : conversationId ? "Session" : "—"} +
-
+ {/* Task List */} + {tasks.length === 0 ? ( -
- No tasks yet +
+ +

+ No tasks yet +

+

+ Tasks will appear as the orchestrator works +

) : ( -
- {tasks.map((task) => ( - + {tasks.map(task => ( +
- setExpandedTaskId( - expandedTaskId === task.id ? null : task.id - ) - } + className="rounded border border-border/30 bg-secondary/20 hover:bg-secondary/30 transition-colors overflow-hidden" > -
- { - handleStatusChange( - task.id, - checked ? "completed" : "pending" - ); - }} - onClick={(e) => e.stopPropagation()} - className="mt-1" - /> + -
+
+ + {expandedTaskId === task.id ? ( + + ) : ( + + )} +
+ {expandedTaskId === task.id && ( -
+
{task.description && ( -
-

+

+

Description

-

+

{task.description}

)} {task.result && (
-

+

Result

-

+

                           {task.result}
-                        

+
)} {task.errorMessage && (
-

+

Error

-

+

{task.errorMessage}

)} {task.createdAt && ( -
- Created: {new Date(task.createdAt).toLocaleString()} -
+

+ {new Date(task.createdAt).toLocaleString()} +

)}
)} - +
))}
)} -
+ -
+ {/* Footer */} +
diff --git a/client/src/pages/Chat.tsx b/client/src/pages/Chat.tsx index 133523c..64767f4 100644 --- a/client/src/pages/Chat.tsx +++ b/client/src/pages/Chat.tsx @@ -1,13 +1,13 @@ -import { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect, useCallback } from "react"; import { Streamdown } from "streamdown"; import { motion, AnimatePresence } from "framer-motion"; import { trpc } from "@/lib/trpc"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { TasksPanel } from "@/components/TasksPanel"; +import { WebResearchPanel } from "@/components/WebResearchPanel"; import { Terminal, Send, @@ -28,6 +28,8 @@ import { Shell, Network, Database, + Search, + ListChecks, } from "lucide-react"; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -49,10 +51,16 @@ interface ChatMessage { timestamp: string; toolCalls?: ToolCallStep[]; model?: string; - usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number }; + usage?: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; isError?: boolean; } +type SidebarTab = "console" | "tasks" | "research"; + // ─── Tool Icon Map ───────────────────────────────────────────────────────────── function ToolIcon({ tool }: { tool: string }) { @@ -68,7 +76,11 @@ function ToolIcon({ tool }: { tool: string }) { install_skill: , docker_exec: , }; - return {icons[tool] ?? }; + return ( + + {icons[tool] ?? } + + ); } function toolLabel(tool: string): string { @@ -94,10 +106,14 @@ function ToolCallCard({ step, index }: { step: ToolCallStep; index: number }) { const argsSummary = () => { if (step.tool === "shell_exec") return step.args.command?.slice(0, 60); - if (step.tool === "file_read" || step.tool === "file_write") return step.args.path; - if (step.tool === "http_request") return `${step.args.method || "GET"} ${step.args.url?.slice(0, 50)}`; - if (step.tool === "delegate_to_agent") return `Agent #${step.args.agentId}: ${step.args.message?.slice(0, 40)}`; - if (step.tool === "docker_exec") return `docker ${step.args.command?.slice(0, 50)}`; + if (step.tool === "file_read" || step.tool === "file_write") + return step.args.path; + if (step.tool === "http_request") + return `${step.args.method || "GET"} ${step.args.url?.slice(0, 50)}`; + if (step.tool === "delegate_to_agent") + return `Agent #${step.args.agentId}: ${step.args.message?.slice(0, 40)}`; + if (step.tool === "docker_exec") + return `docker ${step.args.command?.slice(0, 50)}`; return JSON.stringify(step.args).slice(0, 60); }; @@ -113,14 +129,20 @@ function ToolCallCard({ step, index }: { step: ToolCallStep; index: number }) { className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-secondary/40 transition-colors" > - {toolLabel(step.tool)} - {argsSummary()} + + {toolLabel(step.tool)} + + + {argsSummary()} +
- {step.durationMs}ms + + {step.durationMs}ms + {step.success ? ( - + ) : ( - + )} {expanded ? ( @@ -133,13 +155,17 @@ function ToolCallCard({ step, index }: { step: ToolCallStep; index: number }) { {expanded && (
-

INPUT

+

+ INPUT +

               {JSON.stringify(step.args, null, 2)}
             
-

OUTPUT

+

+ OUTPUT +

               {step.success
                 ? typeof step.result === "string"
@@ -172,26 +198,35 @@ function MessageBubble({ msg }: { msg: ChatMessage }) {
           isUser
             ? "bg-primary/15 border-primary/30"
             : isSystem
-            ? "bg-yellow-500/10 border-yellow-500/30"
-            : "bg-cyan-500/10 border-cyan-500/30"
+              ? "bg-[#FFB800]/10 border-[#FFB800]/30"
+              : "bg-[#00D4FF]/10 border-[#00D4FF]/30"
         }`}
       >
         {isUser ? (
           
         ) : isSystem ? (
-          
+          
         ) : (
-          
+          
         )}
       
{/* Content */} -
+
{/* Meta */} -
- {msg.timestamp} +
+ + {msg.timestamp} + {msg.model && ( - + {msg.model} )} @@ -207,7 +242,8 @@ function MessageBubble({ msg }: { msg: ChatMessage }) {

- {msg.toolCalls.length} tool call{msg.toolCalls.length > 1 ? "s" : ""} + {msg.toolCalls.length} tool call + {msg.toolCalls.length > 1 ? "s" : ""}

{msg.toolCalls.map((step, i) => ( @@ -222,14 +258,16 @@ function MessageBubble({ msg }: { msg: ChatMessage }) { isUser ? "bg-primary/15 border border-primary/20 text-foreground" : isSystem - ? "bg-yellow-500/10 border border-yellow-500/20 text-yellow-200" - : msg.isError - ? "bg-red-500/10 border border-red-500/20 text-red-300" - : "bg-secondary/50 border border-border/40 text-foreground" + ? "bg-[#FFB800]/10 border border-[#FFB800]/20 text-[#FFB800]" + : msg.isError + ? "bg-[#FF3366]/10 border border-[#FF3366]/20 text-[#FF3366]" + : "bg-secondary/50 border border-border/40 text-foreground" }`} > {isUser || isSystem || msg.isError ? ( -
{msg.content}
+
+                {msg.content}
+              
) : (
{msg.content} @@ -242,6 +280,139 @@ function MessageBubble({ msg }: { msg: ChatMessage }) { ); } +// ─── Console Panel ──────────────────────────────────────────────────────────── + +function ConsolePanel({ messages }: { messages: ChatMessage[] }) { + const consoleRef = useRef(null); + + useEffect(() => { + if (consoleRef.current) { + consoleRef.current.scrollTop = consoleRef.current.scrollHeight; + } + }, [messages]); + + const consoleEntries = messages.filter( + m => m.toolCalls && m.toolCalls.length > 0 + ); + + return ( +
+
+
+ + + CONSOLE + + + {consoleEntries.reduce( + (acc, m) => acc + (m.toolCalls?.length ?? 0), + 0 + )} + +
+ + tool output + +
+ +
+ {consoleEntries.length === 0 ? ( +
+ +

+ No tool output yet +

+

+ Tool calls will appear here +

+
+ ) : ( + consoleEntries.map(msg => + msg.toolCalls?.map((step, i) => ( +
+
+ + + {toolLabel(step.tool)} + + + {step.args.command || step.args.path || ""} + + + {step.durationMs}ms + + {step.success ? ( + + ) : ( + + )} +
+
+                  {step.success
+                    ? typeof step.result === "string"
+                      ? step.result.slice(0, 500)
+                      : JSON.stringify(step.result, null, 2).slice(0, 500)
+                    : `ERROR: ${step.result?.error ?? "Unknown"}`}
+                
+
+ )) + ) + )} +
+
+ ); +} + +// ─── Sidebar Tab Button ────────────────────────────────────────────────────── + +function SidebarTabButton({ + active, + onClick, + icon, + label, + count, +}: { + active: boolean; + onClick: () => void; + icon: React.ReactNode; + label: string; + count?: number; +}) { + return ( + + ); +} + // ─── Main Chat Component ────────────────────────────────────────────────────── export default function Chat() { @@ -252,16 +423,21 @@ export default function Chat() { const [input, setInput] = useState(""); const [isThinking, setIsThinking] = useState(false); const [retryAttempt, setRetryAttempt] = useState(0); - const [lastError, setLastError] = useState<{ message: string; isRetryable: boolean } | null>(null); + const [lastError, setLastError] = useState<{ + message: string; + isRetryable: boolean; + } | null>(null); const [conversationId] = useState(`conv-${Date.now()}`); + const [activeTab, setActiveTab] = useState("console"); const scrollRef = useRef(null); - const inputRef = useRef(null); + const textareaRef = useRef(null); - const agentsQuery = trpc.agents.list.useQuery(undefined, { refetchInterval: 30000 }); + const agentsQuery = trpc.agents.list.useQuery(undefined, { + refetchInterval: 30000, + }); const orchestratorMutation = trpc.orchestrator.chat.useMutation(); const orchestratorConfigQuery = trpc.orchestrator.getConfig.useQuery(); - // Initialize welcome message with orchestrator name from DB useEffect(() => { if (orchestratorConfigQuery.data && messages.length === 0) { const cfg = orchestratorConfigQuery.data; @@ -270,7 +446,10 @@ export default function Chat() { id: "welcome", role: "system", content: `${cfg.name} ready. Model: ${cfg.model}\nI have access to all agents, tools, and skills.\nType a command or ask anything.`, - timestamp: new Date().toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" }), + timestamp: new Date().toLocaleTimeString("ru-RU", { + hour: "2-digit", + minute: "2-digit", + }), }, ]); } @@ -282,8 +461,24 @@ export default function Chat() { } }, [messages]); + // Auto-resize textarea + const adjustTextareaHeight = useCallback(() => { + if (textareaRef.current) { + textareaRef.current.style.height = "auto"; + textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 150)}px`; + } + }, []); + + useEffect(() => { + adjustTextareaHeight(); + }, [input, adjustTextareaHeight]); + const getTs = () => - new Date().toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit", second: "2-digit" }); + new Date().toLocaleTimeString("ru-RU", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); const sendMessage = async () => { if (!input.trim() || isThinking) return; @@ -291,14 +486,13 @@ export default function Chat() { const userContent = input.trim(); const ts = getTs(); - // Add user message const userMsg: ChatMessage = { id: `user-${Date.now()}`, role: "user", content: userContent, timestamp: ts, }; - setMessages((prev) => [...prev, userMsg]); + setMessages(prev => [...prev, userMsg]); const newHistory = [ ...conversationHistory, @@ -308,9 +502,11 @@ export default function Chat() { setInput(""); setIsThinking(true); - // Add thinking indicator + // Switch to console tab when sending a message + setActiveTab("console"); + const thinkingId = `thinking-${Date.now()}`; - setMessages((prev) => [ + setMessages(prev => [ ...prev, { id: thinkingId, @@ -323,28 +519,23 @@ export default function Chat() { try { const result = await orchestratorMutation.mutateAsync({ messages: newHistory, - // model is loaded from DB config — do not override here maxIterations: 10, }); - // Remove thinking indicator - setMessages((prev) => prev.filter((m) => m.id !== thinkingId)); + setMessages(prev => prev.filter(m => m.id !== thinkingId)); const respTs = getTs(); - // Clear error state on success setLastError(null); setRetryAttempt(0); if (result.success) { - // Update conversation history - setConversationHistory((prev) => [ + setConversationHistory(prev => [ ...prev, { role: "assistant" as const, content: result.response }, ]); - // Add assistant message with tool calls - setMessages((prev) => [ + setMessages(prev => [ ...prev, { id: `resp-${Date.now()}`, @@ -357,7 +548,7 @@ export default function Chat() { }, ]); } else { - setMessages((prev) => [ + setMessages(prev => [ ...prev, { id: `err-${Date.now()}`, @@ -369,14 +560,17 @@ export default function Chat() { ]); } } catch (err: any) { - setMessages((prev) => prev.filter((m) => m.id !== thinkingId)); + setMessages(prev => prev.filter(m => m.id !== thinkingId)); const errorMsg = err.message || "Unknown error"; - const isRetryable = errorMsg.includes("timeout") || errorMsg.includes("unavailable") || errorMsg.includes("ECONNREFUSED"); - + const isRetryable = + errorMsg.includes("timeout") || + errorMsg.includes("unavailable") || + errorMsg.includes("ECONNREFUSED"); + setLastError({ message: errorMsg, isRetryable }); - setRetryAttempt((prev) => prev + 1); - - setMessages((prev) => [ + setRetryAttempt(prev => prev + 1); + + setMessages(prev => [ ...prev, { id: `err-${Date.now()}`, @@ -386,29 +580,46 @@ export default function Chat() { isError: true, }, ]); - - // Auto-retry if retryable and under max attempts + if (isRetryable && retryAttempt < 2) { - setTimeout(() => { - sendMessage(); - }, 1000 * Math.pow(2, retryAttempt)); + setTimeout( + () => { + sendMessage(); + }, + 1000 * Math.pow(2, retryAttempt) + ); } } finally { setIsThinking(false); - setTimeout(() => inputRef.current?.focus(), 100); - } }; + setTimeout(() => textareaRef.current?.focus(), 100); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }; const agents = agentsQuery.data ?? []; - const activeAgents = agents.filter((a) => a.isActive && !(a as any).isOrchestrator); + const activeAgents = agents.filter( + a => a.isActive && !(a as any).isOrchestrator + ); const orchConfig = orchestratorConfigQuery.data; + const toolCallCount = messages.reduce( + (acc, m) => acc + (m.toolCalls?.length ?? 0), + 0 + ); + return (
{/* Header */}
-
- +
+

@@ -417,8 +628,10 @@ export default function Chat() {

{orchConfig ? ( - {orchConfig.model} - {" · "}{activeAgents.length} agents · {ORCHESTRATOR_TOOLS_COUNT} tools + {orchConfig.model} + {" · "} + {activeAgents.length} agents · {ORCHESTRATOR_TOOLS_COUNT}{" "} + tools ) : ( `Main AI · ${activeAgents.length} agents · ${ORCHESTRATOR_TOOLS_COUNT} tools` @@ -427,25 +640,30 @@ export default function Chat() {

- {/* Active agents badges + Configure link */}
- {activeAgents.slice(0, 3).map((agent) => ( + {activeAgents.slice(0, 3).map(agent => ( - {agent.role === "browser" && } - {agent.role === "tool_builder" && } - {agent.role === "agent_compiler" && } + {agent.role === "browser" && ( + + )} + {agent.role === "tool_builder" && ( + + )} + {agent.role === "agent_compiler" && ( + + )} {agent.name} ))}
Configure @@ -457,85 +675,124 @@ export default function Chat() { {/* Chat area */} - -
- - {messages.map((msg) => ( - - ))} - + +
+ + {messages.map(msg => ( + + ))} + - {/* Thinking indicator */} - {isThinking && ( - - - Orchestrator thinking... - - )} -
-
- - {/* Input area */} -
- {/* Quick commands */} -
- {[ - "Список агентов", - "Покажи файлы проекта", - "Статус Docker", - "Создай инструмент", - "Скомпилируй агента", - ].map((cmd) => ( - - ))} -
- -
- $ - setInput(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && sendMessage()} - placeholder={isThinking ? "Ожидание ответа оркестратора..." : "Введите команду или вопрос..."} - disabled={isThinking} - className="bg-transparent border-none text-foreground font-mono text-sm placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0 h-8" - /> - -
-
- - +
+
- {/* Tasks Panel */} -
- + {/* Input area */} +
+ {/* Quick commands */} +
+ {[ + "Список агентов", + "Покажи файлы проекта", + "Статус Docker", + "Создай инструмент", + "Скомпилируй агента", + ].map(cmd => ( + + ))} +
+ +
+ + $ + +