From 342df60c249713d9cdfd377dd854210d3d18f091 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Tue, 9 Jun 2026 14:06:14 +0300 Subject: [PATCH] refactor: split ops ai workspace settings modal --- .../ai-workspace/global-control.tsx | 623 +++--------------- .../ai-workspace/product-settings-modal.tsx | 512 ++++++++++++++ 2 files changed, 613 insertions(+), 522 deletions(-) create mode 100644 plane-src/apps/web/core/components/ai-workspace/product-settings-modal.tsx 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 2bc33c7..4cd8014 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 @@ -4,101 +4,31 @@ * See the LICENSE file for details. */ -import { useCallback, useEffect, useMemo, useState } from "react"; -import type { FormEvent, MouseEvent } from "react"; +import { useCallback, useEffect, useState } from "react"; import { createPortal } from "react-dom"; import useSWR from "swr"; import { MessageCircle } from "lucide-react"; // plane imports import { Tooltip } from "@plane/propel/tooltip"; -import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; +// components +import { AIWorkspaceProductSettingsModal } from "@/components/ai-workspace/product-settings-modal"; // services import { WorkspaceAIWorkspaceService } from "@/services/workspace-ai-workspace.service"; import type { TAIWorkspaceExecutor, - TAIWorkspaceExecutorConnectionMode, TAIWorkspaceExecutorInput, + TAIWorkspaceExecutorListResponse, } from "@/services/workspace-ai-workspace.service"; 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: "mcp", label: "MCP" }, - { value: "workflows", label: "Workflows" }, -]; type TAIWorkspaceGlobalControlProps = { workspaceSlug: string; }; -type TExecutorDraft = { - name: string; - connectionMode: TAIWorkspaceExecutorConnectionMode; - endpoint: string; - workspacePath: string; - agentPort: string; - accountLabel: string; - capabilities: string[]; -}; - -function emptyDraft(): TExecutorDraft { - return { - name: "", - connectionMode: "hub", - endpoint: "", - workspacePath: "", - agentPort: DEFAULT_WINDOWS_AGENT_PORT, - accountLabel: "", - capabilities: ["ops"], - }; -} - -function draftFromExecutor(executor: TAIWorkspaceExecutor | null | undefined): TExecutorDraft { - if (!executor) return emptyDraft(); - return { - name: executor.name || "", - connectionMode: executor.connectionMode === "direct" ? "direct" : "hub", - endpoint: executor.endpoint || "", - 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"], - }; -} - -function toInput(draft: TExecutorDraft): TAIWorkspaceExecutorInput { - const port = Number(draft.agentPort || DEFAULT_WINDOWS_AGENT_PORT); - return { - name: draft.name.trim(), - type: "codex-remote", - connectionMode: draft.connectionMode, - endpoint: draft.connectionMode === "direct" ? draft.endpoint.trim() || null : null, - workspacePath: draft.workspacePath.trim() || null, - agentPort: Number.isInteger(port) ? port : Number(DEFAULT_WINDOWS_AGENT_PORT), - accountLabel: draft.accountLabel.trim() || null, - capabilities: Array.from(new Set(["ops", ...draft.capabilities])).filter(Boolean), - }; -} - -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"; -} - export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalControlProps) { const [isOpen, setIsOpen] = useState(false); const [dockSlot, setDockSlot] = useState(null); - const [detailsMode, setDetailsMode] = useState<"context" | "new" | "edit">("context"); - const [editingId, setEditingId] = useState(""); - const [draft, setDraft] = useState(() => emptyDraft()); const [busyId, setBusyId] = useState(""); const [notice, setNotice] = useState(""); const [actionError, setActionError] = useState(""); @@ -128,163 +58,97 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo }; }, []); - const executors = useMemo(() => data?.executors ?? [], [data?.executors]); - const activeExecutor = useMemo( - () => executors.find((executor) => executor.id === data?.selectedExecutorId) ?? executors[0] ?? null, - [data?.selectedExecutorId, executors] - ); - const editingExecutor = useMemo( - () => executors.find((executor) => executor.id === editingId) ?? activeExecutor, - [activeExecutor, editingId, executors] - ); - const detailsOpen = detailsMode === "new" || detailsMode === "edit"; - const isBusy = Boolean(busyId); - - useEffect(() => { - if (!isOpen || detailsMode === "new") return; - setEditingId(activeExecutor?.id || ""); - setDraft(draftFromExecutor(activeExecutor)); - }, [activeExecutor, detailsMode, isOpen]); - - const handleOpen = useCallback(() => { - setIsOpen(true); - }, []); - const handleClose = useCallback(() => { setIsOpen(false); - setDetailsMode("context"); setNotice(""); 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): Promise => { + setBusyId(id); + setActionError(""); + setNotice(""); + try { + const result = await action(); + setNotice(successText); + return result; + } catch (mutationError: any) { + setActionError( + String(mutationError?.message || mutationError?.error || mutationError || "AI Workspace request failed") + ); + return null; + } finally { + setBusyId(""); + } + }, + [] + ); - const startNew = () => { - setDetailsMode("new"); - setEditingId(""); - setDraft(emptyDraft()); - setNotice(""); - setActionError(""); - }; + const updateRegistry = useCallback( + async (payload: TAIWorkspaceExecutorListResponse) => { + await mutate(payload, { revalidate: false }); + return payload; + }, + [mutate] + ); - const selectExecutor = (executor: TAIWorkspaceExecutor) => { - setDetailsMode("context"); - setEditingId(executor.id); - setDraft(draftFromExecutor(executor)); - if (executor.id !== data?.selectedExecutorId) { - void runMutation( + const selectExecutor = useCallback( + async (executor: TAIWorkspaceExecutor) => + runMutation( `select:${executor.id}`, - async () => { - const payload = await workspaceAIWorkspaceService.selectExecutor(workspaceSlug, executor.id); - await mutate(payload, { revalidate: false }); - }, + async () => updateRegistry(await workspaceAIWorkspaceService.selectExecutor(workspaceSlug, executor.id)), "Устройство выбрано" - ); - } - }; + ), + [runMutation, updateRegistry, workspaceSlug] + ); - const editActive = () => { - if (!activeExecutor) return; - setDetailsMode("edit"); - setEditingId(activeExecutor.id); - setDraft(draftFromExecutor(activeExecutor)); - setNotice(""); - setActionError(""); - }; - - const saveDraft = async (event: FormEvent) => { - event.preventDefault(); - const input = toInput(draft); - if (!input.name) { - setActionError("Укажи имя устройства"); - return; - } - if (detailsMode === "new") { - await runMutation( + const createExecutor = useCallback( + async (input: TAIWorkspaceExecutorInput) => + 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"); - }, + async () => updateRegistry(await workspaceAIWorkspaceService.createExecutor(workspaceSlug, input)), "Устройство создано" + ), + [runMutation, updateRegistry, workspaceSlug] + ); + + const updateExecutor = useCallback( + async (executorId: string, input: TAIWorkspaceExecutorInput) => + runMutation( + "save", + async () => updateRegistry(await workspaceAIWorkspaceService.updateExecutor(workspaceSlug, executorId, input)), + "Устройство сохранено" + ), + [runMutation, updateRegistry, workspaceSlug] + ); + + const deleteExecutor = useCallback( + async (executorId: string) => + runMutation( + `delete:${executorId}`, + async () => updateRegistry(await workspaceAIWorkspaceService.deleteExecutor(workspaceSlug, executorId)), + "Устройство удалено" + ), + [runMutation, updateRegistry, workspaceSlug] + ); + + const downloadAgent = useCallback( + async (executorId: string, input: TAIWorkspaceExecutorInput, port: string) => { + await runMutation( + "download", + async () => { + const payload = await workspaceAIWorkspaceService.updateExecutor(workspaceSlug, executorId, input); + await updateRegistry(payload); + window.location.href = workspaceAIWorkspaceService.getWindowsAgentInstallerUrl(workspaceSlug, executorId, { + port, + }); + }, + "Installer подготовлен" ); - return; - } - if (!editingExecutor) return; - 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"); - }, - "Устройство удалено" - ); - }; - - const toggleCapability = (value: string) => { - setDraft((current) => { - const set = new Set(current.capabilities); - if (set.has(value)) set.delete(value); - else set.add(value); - return { ...current, capabilities: Array.from(set) }; - }); - }; - - 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 подготовлен" - ); - }; + }, + [runMutation, updateRegistry, workspaceSlug] + ); return ( <> @@ -294,7 +158,7 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo - - -
-
-