diff --git a/plane-src/apps/web/core/components/ai-workspace/global-control.tsx b/plane-src/apps/web/core/components/ai-workspace/global-control.tsx index ac3c5a9..2bc33c7 100644 --- a/plane-src/apps/web/core/components/ai-workspace/global-control.tsx +++ b/plane-src/apps/web/core/components/ai-workspace/global-control.tsx @@ -8,11 +8,10 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import type { FormEvent, MouseEvent } from "react"; import { createPortal } from "react-dom"; import useSWR from "swr"; -import { CheckCircle2, Download, MessageCircle, Monitor, Plus, RefreshCw, Settings, Trash2, X } from "lucide-react"; +import { MessageCircle } from "lucide-react"; // plane imports import { Tooltip } from "@plane/propel/tooltip"; import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; -import { cn } from "@plane/utils"; // services import { WorkspaceAIWorkspaceService } from "@/services/workspace-ai-workspace.service"; import type { @@ -24,14 +23,14 @@ import type { const workspaceAIWorkspaceService = new WorkspaceAIWorkspaceService(); const DEFAULT_WINDOWS_AGENT_PORT = "8787"; const capabilityOptions = [ + { value: "chat", label: "Chat" }, + { value: "code", label: "Code" }, { value: "engine", label: "Engine" }, { value: "ops", label: "Ops" }, - { value: "ndc-agent-core", label: "NDC Agent" }, + { value: "mcp", label: "MCP" }, + { value: "workflows", label: "Workflows" }, ]; -const aiWorkspaceCloseButtonClassName = - "absolute top-0.5 right-0.5 flex h-12 w-12 items-center justify-center rounded-full border-0 bg-[#17181B] text-white shadow-none ring-0 transition-transform outline-none hover:scale-[1.03] hover:bg-[#0F1012]"; - type TAIWorkspaceGlobalControlProps = { workspaceSlug: string; }; @@ -67,7 +66,8 @@ function draftFromExecutor(executor: TAIWorkspaceExecutor | null | undefined): T workspacePath: executor.workspacePath || "", agentPort: String(executor.agentPort || DEFAULT_WINDOWS_AGENT_PORT), accountLabel: executor.accountLabel || "", - capabilities: Array.isArray(executor.capabilities) && executor.capabilities.length ? executor.capabilities : ["ops"], + capabilities: + Array.isArray(executor.capabilities) && executor.capabilities.length ? executor.capabilities : ["ops"], }; } @@ -86,11 +86,11 @@ function toInput(draft: TExecutorDraft): TAIWorkspaceExecutorInput { } function statusLabel(status: string | null | undefined) { - if (status === "online") return "online"; - if (status === "offline") return "offline"; - if (status === "checking") return "checking"; - if (status === "error") return "error"; - return "unknown"; + if (status === "online") return "Online"; + if (status === "offline") return "Offline"; + if (status === "checking") return "Checking"; + if (status === "error") return "Error"; + return "Unknown"; } export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalControlProps) { @@ -157,22 +157,21 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo setActionError(""); }, []); - const runMutation = useCallback( - async (id: string, action: () => Promise, successText: string) => { - setBusyId(id); - setActionError(""); - setNotice(""); - try { - await action(); - setNotice(successText); - } catch (mutationError: any) { - setActionError(String(mutationError?.message || mutationError?.error || mutationError || "AI Workspace request failed")); - } finally { - setBusyId(""); - } - }, - [] - ); + const runMutation = useCallback(async (id: string, action: () => Promise, successText: string) => { + setBusyId(id); + setActionError(""); + setNotice(""); + try { + await action(); + setNotice(successText); + } catch (mutationError: any) { + setActionError( + String(mutationError?.message || mutationError?.error || mutationError || "AI Workspace request failed") + ); + } finally { + setBusyId(""); + } + }, []); const startNew = () => { setDetailsMode("new"); @@ -215,30 +214,43 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo return; } if (detailsMode === "new") { - await runMutation("save", async () => { - const payload = await workspaceAIWorkspaceService.createExecutor(workspaceSlug, input); - await mutate(payload, { revalidate: false }); - const next = payload.executors.find((executor) => executor.name === input.name) ?? payload.executors[0] ?? null; - setEditingId(next?.id || ""); - setDetailsMode(next ? "edit" : "context"); - }, "Устройство создано"); + await runMutation( + "save", + async () => { + const payload = await workspaceAIWorkspaceService.createExecutor(workspaceSlug, input); + await mutate(payload, { revalidate: false }); + const next = + payload.executors.find((executor) => executor.name === input.name) ?? payload.executors[0] ?? null; + setEditingId(next?.id || ""); + setDetailsMode(next ? "edit" : "context"); + }, + "Устройство создано" + ); return; } if (!editingExecutor) return; - await runMutation("save", async () => { - const payload = await workspaceAIWorkspaceService.updateExecutor(workspaceSlug, editingExecutor.id, input); - await mutate(payload, { revalidate: false }); - setDetailsMode("context"); - }, "Устройство сохранено"); + await runMutation( + "save", + async () => { + const payload = await workspaceAIWorkspaceService.updateExecutor(workspaceSlug, editingExecutor.id, input); + await mutate(payload, { revalidate: false }); + setDetailsMode("context"); + }, + "Устройство сохранено" + ); }; const deleteEditingExecutor = async () => { if (!editingExecutor) return; - await runMutation(`delete:${editingExecutor.id}`, async () => { - const payload = await workspaceAIWorkspaceService.deleteExecutor(workspaceSlug, editingExecutor.id); - await mutate(payload, { revalidate: false }); - setDetailsMode("context"); - }, "Устройство удалено"); + await runMutation( + `delete:${editingExecutor.id}`, + async () => { + const payload = await workspaceAIWorkspaceService.deleteExecutor(workspaceSlug, editingExecutor.id); + await mutate(payload, { revalidate: false }); + setDetailsMode("context"); + }, + "Устройство удалено" + ); }; const toggleCapability = (value: string) => { @@ -253,13 +265,25 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo const downloadAgent = async (event: MouseEvent) => { if (!editingExecutor) return; event.preventDefault(); - await runMutation("download", async () => { - const payload = await workspaceAIWorkspaceService.updateExecutor(workspaceSlug, editingExecutor.id, toInput(draft)); - await mutate(payload, { revalidate: false }); - window.location.href = workspaceAIWorkspaceService.getWindowsAgentInstallerUrl(workspaceSlug, editingExecutor.id, { - port: draft.agentPort || DEFAULT_WINDOWS_AGENT_PORT, - }); - }, "Installer подготовлен"); + await runMutation( + "download", + async () => { + const payload = await workspaceAIWorkspaceService.updateExecutor( + workspaceSlug, + editingExecutor.id, + toInput(draft) + ); + await mutate(payload, { revalidate: false }); + window.location.href = workspaceAIWorkspaceService.getWindowsAgentInstallerUrl( + workspaceSlug, + editingExecutor.id, + { + port: draft.agentPort || DEFAULT_WINDOWS_AGENT_PORT, + } + ); + }, + "Installer подготовлен" + ); }; return ( @@ -287,214 +311,301 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo width={EModalWidth.XXL} className="overflow-visible" > -
-
-
-
AI Space
-

AI Workspace

+
+
+
+
AI Workspace Settings
+
+ MODEL CONTEXT +
-
-
- - -
-
-
-
{detailsMode === "new" ? "Новое устройство" : "Рабочий контекст"}
-
Общий пользовательский ассистент для Ops и Engine
-
- {activeExecutor ? {statusLabel(activeExecutor.status)} : null} -
- - {activeExecutor && detailsMode !== "new" ? ( - <> -
- Активное устройство - {activeExecutor.name} - {activeExecutor.connectionMode === "hub" ? activeExecutor.pairingCode || "Cloud Hub" : activeExecutor.endpoint} -
- -
- Это устройство сохранено в общем AI Workspace registry. Оно будет видно в Engine и Ops для этого же пользователя. -
- -
- - - -
- - ) : detailsMode === "new" ? ( -
- Новое устройство - Заполни параметры справа и нажми «Подключить». После сохранения появятся pairing code и installer агента. -
- ) : error ? ( -
- Registry backend недоступен - {String((error as any)?.message || (error as any)?.error || error)} - -
- ) : ( -
- Устройство не выбрано - Подключи Codex worker. Если устройство уже добавлено в Engine, оно появится здесь автоматически. - -
- )} - - {notice ?
{notice}
: null} - {actionError ?
{actionError}
: null} -
- - {detailsOpen ? ( -
-
+
+
+