diff --git a/client/src/lib/chatStore.ts b/client/src/lib/chatStore.ts new file mode 100644 index 0000000..c85199a --- /dev/null +++ b/client/src/lib/chatStore.ts @@ -0,0 +1,359 @@ +/** + * chatStore — Global singleton for chat conversations. + * + * Stores conversations in localStorage, supports: + * - Create/delete/switch conversations + * - Persist messages across navigation + * - Uses synchronous orchestrator.chat tRPC endpoint + */ + +import { nanoid } from "nanoid"; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +export interface ToolCallStep { + tool: string; + args: Record; + result: any; + success: boolean; + durationMs: number; +} + +export interface ChatMessage { + id: string; + role: "user" | "assistant" | "system"; + content: string; + timestamp: string; + toolCalls?: ToolCallStep[]; + model?: string; + usage?: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; + isError?: boolean; +} + +export interface Conversation { + id: string; + title: string; + createdAt: number; + messages: ChatMessage[]; + history: Array<{ role: "user" | "assistant" | "system"; content: string }>; +} + +// ─── Active Agent Tracking ─────────────────────────────────────────────────── + +export interface AgentActivity { + id: string; + agentName: string; + agentRole: string; + containerId?: string; + startedAt: number; + status: "running" | "done" | "error"; + result?: string; + error?: string; +} + +type StoreEvent = "update"; +type UpdateHandler = () => void; + +// ─── Persistence ────────────────────────────────────────────────────────────── + +const STORAGE_KEY = "goclaw-conversations-v4"; + +function loadConversations(): Conversation[] { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return []; + return JSON.parse(raw); + } catch { + return []; + } +} + +function persistConversations(convs: Conversation[]) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(convs.slice(0, 50))); + } catch {} +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function getTs(): string { + return new Date().toLocaleTimeString("ru-RU", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); +} + +// ─── Store ──────────────────────────────────────────────────────────────────── + +class ChatStore { + private conversations: Conversation[] = loadConversations(); + private activeId: string = ""; + private isThinking = false; + private activeAgents: AgentActivity[] = []; + private listeners = new Set(); + + constructor() { + if (this.conversations.length > 0) { + this.activeId = this.conversations[0].id; + } + } + + // ─── Subscriptions ────────────────────────────────────────────────────────── + + on(_event: "update", handler: UpdateHandler): void { + this.listeners.add(handler); + } + + off(_event: "update", handler: UpdateHandler): void { + this.listeners.delete(handler); + } + + private emit() { + this.listeners.forEach(h => h()); + } + + // ─── Selectors ────────────────────────────────────────────────────────────── + + getConversations(): Conversation[] { + return this.conversations; + } + getActiveId(): string { + return this.activeId; + } + getActive(): Conversation | null { + return this.conversations.find(c => c.id === this.activeId) ?? null; + } + getIsThinking(): boolean { + return this.isThinking; + } + + // ─── Mutations ────────────────────────────────────────────────────────────── + + setActiveId(id: string) { + this.activeId = id; + this.emit(); + } + + createConversation(orchName = "GoClaw Orchestrator"): string { + const id = `conv-${nanoid(8)}`; + const welcome: ChatMessage = { + id: "welcome", + role: "system", + content: `${orchName} ready.\nI have access to all agents, tools, and skills.\nType a command or ask anything.`, + timestamp: getTs(), + }; + const conv: Conversation = { + id, + title: "New Chat", + createdAt: Date.now(), + messages: [welcome], + history: [], + }; + this.conversations = [conv, ...this.conversations]; + this.activeId = id; + persistConversations(this.conversations); + this.emit(); + return id; + } + + deleteConversation(id: string) { + this.conversations = this.conversations.filter(c => c.id !== id); + if (this.activeId === id) { + if (this.conversations.length > 0) { + this.activeId = this.conversations[0].id; + } else { + this.createConversation(); + return; + } + } + persistConversations(this.conversations); + this.emit(); + } + + setThinking(v: boolean) { + this.isThinking = v; + this.emit(); + } + + /** Add a user message to the active conversation. Returns the conv ID. */ + addUserMessage( + content: string, + convId?: string + ): { + convId: string; + userMsg: ChatMessage; + newHistory: Array<{ + role: "user" | "assistant" | "system"; + content: string; + }>; + } { + let id = convId ?? this.activeId; + if (!id || !this.conversations.find(c => c.id === id)) { + id = this.createConversation(); + } + + const userMsg: ChatMessage = { + id: `user-${Date.now()}`, + role: "user", + content: content.trim(), + timestamp: getTs(), + }; + + const conv = this.conversations.find(c => c.id === id); + const newHistory = [ + ...(conv?.history ?? []), + { role: "user" as const, content: content.trim() }, + ]; + + this.conversations = this.conversations.map(c => + c.id === id + ? { + ...c, + title: + c.history.length === 0 + ? content.trim().slice(0, 40) + (content.length > 40 ? "…" : "") + : c.title, + messages: [...c.messages, userMsg], + history: newHistory, + } + : c + ); + persistConversations(this.conversations); + this.emit(); + return { convId: id, userMsg, newHistory }; + } + + /** Add a thinking/processing placeholder. Returns thinkingMsgId. */ + addThinkingMessage(convId: string): string { + const thinkingId = `thinking-${Date.now()}`; + const thinkingMsg: ChatMessage = { + id: thinkingId, + role: "system", + content: "Orchestrator is processing...", + timestamp: getTs(), + }; + + this.conversations = this.conversations.map(c => + c.id === convId ? { ...c, messages: [...c.messages, thinkingMsg] } : c + ); + persistConversations(this.conversations); + this.emit(); + return thinkingId; + } + + /** Remove the thinking placeholder. */ + removeThinkingMessage(convId: string, thinkingId: string) { + this.conversations = this.conversations.map(c => + c.id === convId + ? { ...c, messages: c.messages.filter(m => m.id !== thinkingId) } + : c + ); + this.emit(); + } + + /** Add assistant response after successful chat. */ + addAssistantMessage( + convId: string, + response: string, + toolCalls: any[] | undefined, + model: string | undefined, + usage: any | undefined, + history: Array<{ role: "user" | "assistant" | "system"; content: string }> + ) { + const msg: ChatMessage = { + id: `resp-${Date.now()}`, + role: "assistant", + content: response, + timestamp: getTs(), + toolCalls, + model, + usage, + }; + + this.conversations = this.conversations.map(c => + c.id === convId + ? { ...c, messages: [...c.messages, msg], history: history } + : c + ); + persistConversations(this.conversations); + this.isThinking = false; + this.emit(); + } + + /** Add error message on failed chat. */ + addErrorMessage(convId: string, errorMsg: string) { + const msg: ChatMessage = { + id: `err-${Date.now()}`, + role: "assistant", + content: errorMsg, + timestamp: getTs(), + isError: true, + }; + + this.conversations = this.conversations.map(c => + c.id === convId ? { ...c, messages: [...c.messages, msg] } : c + ); + persistConversations(this.conversations); + this.isThinking = false; + this.emit(); + } + + // ─── Active Agent Tracking ────────────────────────────────────────────────── + + getActiveAgents(): AgentActivity[] { + return this.activeAgents; + } + + /** Start tracking an agent activity. Returns the activity id. */ + startAgentActivity( + agentName: string, + agentRole: string, + containerId?: string + ): string { + const id = `agent-${nanoid(6)}`; + const activity: AgentActivity = { + id, + agentName, + agentRole, + containerId, + startedAt: Date.now(), + status: "running", + }; + this.activeAgents = [...this.activeAgents, activity]; + this.emit(); + return id; + } + + /** Update agent activity status. */ + updateAgentActivity( + activityId: string, + updates: Partial< + Pick + > + ) { + this.activeAgents = this.activeAgents.map(a => + a.id === activityId ? { ...a, ...updates } : a + ); + this.emit(); + } + + /** Remove a completed agent activity. */ + removeAgentActivity(activityId: string) { + this.activeAgents = this.activeAgents.filter(a => a.id !== activityId); + this.emit(); + } + + /** Clear all agent activities. */ + clearAgentActivities() { + this.activeAgents = []; + this.emit(); + } +} + +// Singleton +export const chatStore = new ChatStore(); diff --git a/client/src/pages/Chat.tsx b/client/src/pages/Chat.tsx index 64767f4..e86005e 100644 --- a/client/src/pages/Chat.tsx +++ b/client/src/pages/Chat.tsx @@ -2,6 +2,12 @@ import { useState, useRef, useEffect, useCallback } from "react"; import { Streamdown } from "streamdown"; import { motion, AnimatePresence } from "framer-motion"; import { trpc } from "@/lib/trpc"; +import { + chatStore, + type ChatMessage, + type Conversation, + type AgentActivity, +} from "@/lib/chatStore"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -21,46 +27,65 @@ import { ChevronRight, CheckCircle, XCircle, - Clock, Zap, Code, FileText, - Shell, Network, Database, + Plus, + Trash2, + MessageSquare, Search, ListChecks, + Radio, + Clock, + Activity, + Container, } from "lucide-react"; // ─── Types ──────────────────────────────────────────────────────────────────── -interface ToolCallStep { - tool: string; - args: Record; - result: any; - success: boolean; - durationMs: number; -} - -type MessageRole = "user" | "assistant" | "system"; - -interface ChatMessage { - id: string; - role: MessageRole; - content: string; - timestamp: string; - toolCalls?: ToolCallStep[]; - model?: string; - usage?: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; - isError?: boolean; -} - type SidebarTab = "console" | "tasks" | "research"; +// ─── useChatStore hook ──────────────────────────────────────────────────────── + +function useChatStore() { + const [, forceRender] = useState(0); + useEffect(() => { + const handler = () => forceRender(n => n + 1); + chatStore.on("update", handler); + return () => chatStore.off("update", handler); + }, []); + return { + conversations: chatStore.getConversations(), + activeId: chatStore.getActiveId(), + active: chatStore.getActive(), + isThinking: chatStore.getIsThinking(), + activeAgents: chatStore.getActiveAgents(), + }; +} + +// ─── Live Timer Hook ────────────────────────────────────────────────────────── + +function useElapsedTime(startedAt: number, status: string): string { + const [elapsed, setElapsed] = useState(() => + status === "running" ? Math.floor((Date.now() - startedAt) / 1000) : 0 + ); + + useEffect(() => { + if (status !== "running") return; + const interval = setInterval(() => { + setElapsed(Math.floor((Date.now() - startedAt) / 1000)); + }, 1000); + return () => clearInterval(interval); + }, [startedAt, status]); + + if (status !== "running") return ""; + const m = Math.floor(elapsed / 60); + const s = elapsed % 60; + return `${m}:${s.toString().padStart(2, "0")}`; +} + // ─── Tool Icon Map ───────────────────────────────────────────────────────────── function ToolIcon({ tool }: { tool: string }) { @@ -101,7 +126,7 @@ function toolLabel(tool: string): string { // ─── Tool Call Card ─────────────────────────────────────────────────────────── -function ToolCallCard({ step, index }: { step: ToolCallStep; index: number }) { +function ToolCallCard({ step, index }: { step: any; index: number }) { const [expanded, setExpanded] = useState(false); const argsSummary = () => { @@ -151,7 +176,6 @@ function ToolCallCard({ step, index }: { step: ToolCallStep; index: number }) { )} - {expanded && (
@@ -192,7 +216,6 @@ function MessageBubble({ msg }: { msg: ChatMessage }) { animate={{ opacity: 1, y: 0 }} className={`flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"}`} > - {/* Avatar */}
)}
- - {/* Content */}
- {/* Meta */}
@@ -236,8 +256,6 @@ function MessageBubble({ msg }: { msg: ChatMessage }) { )}
- - {/* Tool calls */} {msg.toolCalls && msg.toolCalls.length > 0 && (

@@ -250,8 +268,6 @@ function MessageBubble({ msg }: { msg: ChatMessage }) { ))}

)} - - {/* Message text */} {msg.content && (
+
+ + + {activity.agentName} + + + {activity.agentRole} + +
+
+ {activity.containerId && ( + + + {activity.containerId.slice(0, 12)} + + )} + {activity.status === "running" && ( + + + {elapsed} + + )} + {activity.status === "running" && ( + + + ping... + + )} + {activity.status === "done" && ( + completed + )} + {activity.status === "error" && ( + error + )} +
+ + ); +} + +// ─── Console Panel ───────────────────────────────────────────────────────────── + +function ConsolePanel({ + messages, + activeAgents, +}: { + messages: ChatMessage[]; + activeAgents: AgentActivity[]; +}) { const consoleRef = useRef(null); useEffect(() => { - if (consoleRef.current) { + if (consoleRef.current) consoleRef.current.scrollTop = consoleRef.current.scrollHeight; - } - }, [messages]); + }, [messages, activeAgents]); const consoleEntries = messages.filter( m => m.toolCalls && m.toolCalls.length > 0 @@ -303,34 +379,39 @@ function ConsolePanel({ messages }: { messages: ChatMessage[] }) { CONSOLE - - {consoleEntries.reduce( - (acc, m) => acc + (m.toolCalls?.length ?? 0), - 0 - )} - + {activeAgents.filter(a => a.status === "running").length > 0 && ( + + {activeAgents.filter(a => a.status === "running").length} active + + )}
- - tool output -
-
- {consoleEntries.length === 0 ? ( +
+ {/* Active agents section */} + {activeAgents.length > 0 && ( +
+

+ Active Agents +

+
+ {activeAgents.map(a => ( + + ))} +
+
+ )} + + {/* Tool output */} + {consoleEntries.length === 0 && activeAgents.length === 0 ? (
-

+

No tool output yet

-

- Tool calls will appear here -

) : ( consoleEntries.map(msg => @@ -341,13 +422,13 @@ function ConsolePanel({ messages }: { messages: ChatMessage[] }) { >
- + {toolLabel(step.tool)} - + {step.args.command || step.args.path || ""} - + {step.durationMs}ms {step.success ? ( @@ -356,7 +437,7 @@ function ConsolePanel({ messages }: { messages: ChatMessage[] }) { )}
-
+                
                   {step.success
                     ? typeof step.result === "string"
                       ? step.result.slice(0, 500)
@@ -372,7 +453,7 @@ function ConsolePanel({ messages }: { messages: ChatMessage[] }) {
   );
 }
 
-// ─── Sidebar Tab Button ──────────────────────────────────────────────────────
+// ─── Sidebar Tab Button ───────────────────────────────────────────────────────
 
 function SidebarTabButton({
   active,
@@ -400,11 +481,7 @@ function SidebarTabButton({
       {label}
       {count !== undefined && count > 0 && (
         
           {count}
         
@@ -413,21 +490,82 @@ function SidebarTabButton({
   );
 }
 
+// ─── Conversation List Item ────────────────────────────────────────────────────
+
+function ConversationItem({
+  conv,
+  isActive,
+  onClick,
+  onDelete,
+}: {
+  conv: Conversation;
+  isActive: boolean;
+  onClick: () => void;
+  onDelete: () => void;
+}) {
+  const lastMsg = conv.messages[conv.messages.length - 1];
+  const lastContent =
+    lastMsg?.role === "user"
+      ? lastMsg.content
+      : lastMsg?.role === "assistant"
+        ? lastMsg.content.slice(0, 30)
+        : "";
+  const msgCount = conv.messages.filter(m => m.role !== "system").length;
+
+  return (
+    
+ +
+

+ {conv.title} +

+ {lastContent && ( +

+ {lastContent} +

+ )} +
+ + {msgCount} msg + + + {new Date(conv.createdAt).toLocaleDateString("ru-RU", { + day: "numeric", + month: "short", + })} + +
+
+ +
+ ); +} + // ─── Main Chat Component ────────────────────────────────────────────────────── export default function Chat() { - const [messages, setMessages] = useState([]); - const [conversationHistory, setConversationHistory] = useState< - Array<{ role: "user" | "assistant" | "system"; content: string }> - >([]); + const { conversations, activeId, active, isThinking, activeAgents } = + useChatStore(); 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 [conversationId] = useState(`conv-${Date.now()}`); const [activeTab, setActiveTab] = useState("console"); const scrollRef = useRef(null); const textareaRef = useRef(null); @@ -437,29 +575,22 @@ export default function Chat() { }); const orchestratorMutation = trpc.orchestrator.chat.useMutation(); const orchestratorConfigQuery = trpc.orchestrator.getConfig.useQuery(); + const researchMutation = trpc.research.search.useMutation(); + // Initialize first conversation if empty useEffect(() => { - if (orchestratorConfigQuery.data && messages.length === 0) { - const cfg = orchestratorConfigQuery.data; - setMessages([ - { - 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", - }), - }, - ]); + if (orchestratorConfigQuery.data && conversations.length === 0) { + chatStore.createConversation( + orchestratorConfigQuery.data.name ?? "GoClaw Orchestrator" + ); } - }, [orchestratorConfigQuery.data]); + }, [orchestratorConfigQuery.data, conversations.length]); + // Auto-scroll useEffect(() => { - if (scrollRef.current) { + if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }, [messages]); + }, [active?.messages]); // Auto-resize textarea const adjustTextareaHeight = useCallback(() => { @@ -473,124 +604,86 @@ export default function Chat() { adjustTextareaHeight(); }, [input, adjustTextareaHeight]); - const getTs = () => - new Date().toLocaleTimeString("ru-RU", { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); + const messages = active?.messages ?? []; + const convId = activeId; + + // Track research activity + useEffect(() => { + if (researchMutation.isPending && activeId) { + const activityId = chatStore.startAgentActivity( + "Researcher", + "browser", + "goclaw-agent-1" + ); + return () => { + if (researchMutation.isSuccess) { + chatStore.updateAgentActivity(activityId, { + status: "done", + result: "Research completed", + }); + setTimeout(() => chatStore.removeAgentActivity(activityId), 3000); + } else if (researchMutation.isError) { + chatStore.updateAgentActivity(activityId, { + status: "error", + error: "Research failed", + }); + setTimeout(() => chatStore.removeAgentActivity(activityId), 5000); + } + }; + } + }, [ + researchMutation.isPending, + researchMutation.isSuccess, + researchMutation.isError, + activeId, + ]); const sendMessage = async () => { if (!input.trim() || isThinking) return; - const userContent = input.trim(); - const ts = getTs(); - - const userMsg: ChatMessage = { - id: `user-${Date.now()}`, - role: "user", - content: userContent, - timestamp: ts, - }; - setMessages(prev => [...prev, userMsg]); - - const newHistory = [ - ...conversationHistory, - { role: "user" as const, content: userContent }, - ]; - setConversationHistory(newHistory); + const { convId: cid, newHistory } = chatStore.addUserMessage( + input.trim(), + convId + ); setInput(""); - setIsThinking(true); - - // Switch to console tab when sending a message + chatStore.setThinking(true); setActiveTab("console"); - const thinkingId = `thinking-${Date.now()}`; - setMessages(prev => [ - ...prev, - { - id: thinkingId, - role: "system" as const, - content: "Orchestrator is processing...", - timestamp: getTs(), - }, - ]); + const thinkingId = chatStore.addThinkingMessage(cid); try { const result = await orchestratorMutation.mutateAsync({ messages: newHistory, maxIterations: 10, }); - - setMessages(prev => prev.filter(m => m.id !== thinkingId)); - - const respTs = getTs(); - - setLastError(null); - setRetryAttempt(0); + chatStore.removeThinkingMessage(cid, thinkingId); if (result.success) { - setConversationHistory(prev => [ - ...prev, + const updatedHistory = [ + ...newHistory, { role: "assistant" as const, content: result.response }, - ]); - - setMessages(prev => [ - ...prev, - { - id: `resp-${Date.now()}`, - role: "assistant" as const, - content: result.response, - timestamp: respTs, - toolCalls: result.toolCalls, - model: result.model, - usage: result.usage, - }, - ]); + ]; + chatStore.addAssistantMessage( + cid, + result.response, + result.toolCalls, + result.model, + result.usage, + updatedHistory + ); } else { - setMessages(prev => [ - ...prev, - { - id: `err-${Date.now()}`, - role: "assistant" as const, - content: `Error: ${result.error || "Unknown error"}`, - timestamp: respTs, - isError: true, - }, - ]); - } - } catch (err: any) { - 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"); - - setLastError({ message: errorMsg, isRetryable }); - setRetryAttempt(prev => prev + 1); - - setMessages(prev => [ - ...prev, - { - id: `err-${Date.now()}`, - role: "assistant" as const, - content: `Network Error (Attempt ${retryAttempt + 1}): ${errorMsg}${isRetryable ? "\n\nRetrying automatically..." : ""}`, - timestamp: getTs(), - isError: true, - }, - ]); - - if (isRetryable && retryAttempt < 2) { - setTimeout( - () => { - sendMessage(); - }, - 1000 * Math.pow(2, retryAttempt) + chatStore.addErrorMessage( + cid, + `Error: ${result.error || "Unknown error"}` ); } + } catch (err: any) { + chatStore.removeThinkingMessage(cid, thinkingId); + chatStore.addErrorMessage( + cid, + `Network Error: ${err.message || "Unknown error"}` + ); } finally { - setIsThinking(false); setTimeout(() => textareaRef.current?.focus(), 100); } }; @@ -603,20 +696,22 @@ export default function Chat() { }; const agents = agentsQuery.data ?? []; - const activeAgents = agents.filter( - a => a.isActive && !(a as any).isOrchestrator + const activeAgentsList = agents.filter( + (a: any) => a.isActive && !a.isOrchestrator ); const orchConfig = orchestratorConfigQuery.data; - const toolCallCount = messages.reduce( (acc, m) => acc + (m.toolCalls?.length ?? 0), 0 ); + const runningAgentCount = activeAgents.filter( + a => a.status === "running" + ).length; return ( -
+
{/* Header */} -
+
@@ -627,22 +722,26 @@ export default function Chat() {

{orchConfig ? ( - + <> {orchConfig.model} {" · "} - {activeAgents.length} agents · {ORCHESTRATOR_TOOLS_COUNT}{" "} + {activeAgentsList.length} agents · {ORCHESTRATOR_TOOLS_COUNT}{" "} tools - + ) : ( - `Main AI · ${activeAgents.length} agents · ${ORCHESTRATOR_TOOLS_COUNT} tools` + `Main AI · ${activeAgentsList.length} agents · ${ORCHESTRATOR_TOOLS_COUNT} tools` + )} + {runningAgentCount > 0 && ( + + ● {runningAgentCount} running + )}

-
- {activeAgents.slice(0, 3).map(agent => ( + {activeAgentsList.slice(0, 3).map((agent: any) => (
- {/* Main Content Area */} -
- {/* Chat area */} - + {/* Main 3-panel layout */} +
+ {/* ─── Left Panel — Conversations ─── */} +
+
+ + Chats + + +
+ +
+ {conversations.map(c => ( + chatStore.setActiveId(c.id)} + onDelete={() => chatStore.deleteConversation(c.id)} + /> + ))} +
+
+
+ + {/* ─── Center — Chat Area ─── */} +
@@ -682,7 +815,6 @@ export default function Chat() { ))} - {isThinking && ( Orchestrator thinking... + {runningAgentCount > 0 && ( + + ({runningAgentCount} agent + {runningAgentCount > 1 ? "s" : ""} active) + + )} )}
@@ -700,7 +838,6 @@ export default function Chat() { {/* Input area */}
- {/* Quick commands */}
{[ "Список агентов", @@ -718,7 +855,6 @@ export default function Chat() { ))}
-
$ @@ -730,8 +866,8 @@ export default function Chat() { onKeyDown={handleKeyDown} placeholder={ isThinking - ? "Ожидание ответа оркестратора..." - : "Введите команду или вопрос... (Shift+Enter для новой строки)" + ? "Ожидание ответа..." + : "Введите команду... (Shift+Enter для новой строки)" } disabled={isThinking} rows={1} @@ -754,16 +890,15 @@ export default function Chat() { - {/* Right Sidebar — Tabbed Panel */} -
- {/* Tab bar */} + {/* ─── Right Sidebar — Tabbed Panel ─── */} +
setActiveTab("console")} icon={} label="Console" - count={toolCallCount} + count={toolCallCount + runningAgentCount} />
- - {/* Tab content */}
- {activeTab === "console" && } - {activeTab === "tasks" && ( - + {activeTab === "console" && ( + )} + {activeTab === "tasks" && } {activeTab === "research" && ( - + )}