From 61aba41e77f91f429cfb83342d02e7edab39b19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 16 Apr 2026 23:33:55 -0400 Subject: [PATCH] refactor(renderer): rename EntityWorkspace to TwoPaneLayout for clarity --- ...{EntityWorkspace.tsx => TwoPaneLayout.tsx} | 6 +- .../organization/agent-form/AgentForm.tsx | 111 ++++++ .../agent-form-fields/CreateFormBody.tsx | 254 +++++++++++++ .../agent-form-fields/EditFormBody.tsx | 351 ++++++++++++++++++ .../organization/agent-form/index.ts | 3 + .../organization/agent-form/useAgentForm.ts | 226 +++++++++++ .../views/sections/ExecutionSections.tsx | 4 +- .../views/sections/OperationsSections.tsx | 4 +- 8 files changed, 952 insertions(+), 7 deletions(-) rename src/renderer/components/{EntityWorkspace.tsx => TwoPaneLayout.tsx} (74%) create mode 100644 src/renderer/components/organization/agent-form/AgentForm.tsx create mode 100644 src/renderer/components/organization/agent-form/agent-form-fields/CreateFormBody.tsx create mode 100644 src/renderer/components/organization/agent-form/agent-form-fields/EditFormBody.tsx create mode 100644 src/renderer/components/organization/agent-form/index.ts create mode 100644 src/renderer/components/organization/agent-form/useAgentForm.ts diff --git a/src/renderer/components/EntityWorkspace.tsx b/src/renderer/components/TwoPaneLayout.tsx similarity index 74% rename from src/renderer/components/EntityWorkspace.tsx rename to src/renderer/components/TwoPaneLayout.tsx index e2a722b9..c6119e30 100644 --- a/src/renderer/components/EntityWorkspace.tsx +++ b/src/renderer/components/TwoPaneLayout.tsx @@ -2,10 +2,10 @@ import type { ReactNode } from "react"; import { Eyebrow } from "./ui"; /** - * Two-slot workspace layout with a section header. - * Use `Panel` for single-slot sections; use this when rendering a primary + secondary pane. + * Two-pane layout with a section header. + * Use `Panel` (from ui/) for single-slot sections. Use this when rendering a primary + secondary pane. */ -export function EntityWorkspace({ +export function TwoPaneLayout({ title, eyebrow, primary, diff --git a/src/renderer/components/organization/agent-form/AgentForm.tsx b/src/renderer/components/organization/agent-form/AgentForm.tsx new file mode 100644 index 00000000..1e2a92d0 --- /dev/null +++ b/src/renderer/components/organization/agent-form/AgentForm.tsx @@ -0,0 +1,111 @@ +import { useMemo } from "react"; +import type { + AgentRecord, + ConnectorRecord, + WorkspaceRecord, +} from "@shared/types"; +import { + getConnectorExecutionReadinessIssue, + isConnectorExecutionReady, +} from "@shared/connector-policy"; +import { useAgentForm, type AgentFormMode } from "./useAgentForm"; +import { CreateFormBody } from "./agent-form-fields/CreateFormBody"; +import { EditFormBody } from "./agent-form-fields/EditFormBody"; + +interface AgentFormProps { + mode: AgentFormMode; + companyId: string; + connectors: ConnectorRecord[]; + workspaces: WorkspaceRecord[]; + agents: AgentRecord[]; + agent?: AgentRecord; + onSaved: () => Promise; + onDeleted?: () => Promise; +} + +export function AgentForm({ + mode, + companyId, + connectors, + workspaces, + agents, + agent, + onSaved, + onDeleted, +}: AgentFormProps) { + const form = useAgentForm({ + mode, + agent: agent ?? null, + connectors, + workspaces, + companyId, + }); + const { executionReadyConnectors, selectedConnectorRecord } = form; + + const selectedConnectorIssue = + mode === "edit" + ? selectedConnectorRecord + ? getConnectorExecutionReadinessIssue(selectedConnectorRecord) + : "Select a connector." + : null; + const persistedConnectorRecord = + mode === "edit" && agent + ? connectors.find((connector) => connector.id === agent.connectorId) ?? null + : null; + const persistedConnectorIssue = + mode === "edit" + ? persistedConnectorRecord + ? getConnectorExecutionReadinessIssue(persistedConnectorRecord) + : "Current connector is unavailable." + : null; + + const connectorOptions = useMemo(() => { + const getDisplayLabel = (c: ConnectorRecord) => + c.id.startsWith("claude_compat") && c.notes ? c.notes : c.label; + const readyOptions = executionReadyConnectors.map((connector) => ({ + value: connector.id, + label: getDisplayLabel(connector), + })); + if (mode !== "edit") return readyOptions; + if (!selectedConnectorRecord || isConnectorExecutionReady(selectedConnectorRecord)) { + return readyOptions; + } + return [ + { + value: selectedConnectorRecord.id, + label: `${getDisplayLabel(selectedConnectorRecord)} (current, blocked)`, + }, + ...readyOptions.filter((option) => option.value !== selectedConnectorRecord.id), + ]; + }, [executionReadyConnectors, selectedConnectorRecord, mode]); + + if (mode === "create") { + return ( + + ); + } + + if (!agent) return null; + return ( + + ); +} diff --git a/src/renderer/components/organization/agent-form/agent-form-fields/CreateFormBody.tsx b/src/renderer/components/organization/agent-form/agent-form-fields/CreateFormBody.tsx new file mode 100644 index 00000000..6d7bca3e --- /dev/null +++ b/src/renderer/components/organization/agent-form/agent-form-fields/CreateFormBody.tsx @@ -0,0 +1,254 @@ +import { useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { Robot } from "@phosphor-icons/react"; +import { DEPARTMENT_OPTIONS } from "@shared/constants"; +import type { + AgentRecord, + ConnectorRecord, + DepartmentId, + WorkspaceRecord, +} from "@shared/types"; +import { unwrap } from "../../../../lib/desktop"; +import { useT } from "../../../../i18n"; +import { ActionButton, Input, Select, TextArea } from "../../../ui"; +import type { AgentFormState } from "../useAgentForm"; + +interface CreateFormBodyProps { + companyId: string; + agents: AgentRecord[]; + workspaces: WorkspaceRecord[]; + connectorOptions: Array<{ value: string; label: string }>; + form: AgentFormState; + onSaved: () => Promise; +} + +export function CreateFormBody({ + companyId, + agents, + workspaces, + connectorOptions, + form, + onSaved, +}: CreateFormBodyProps) { + const t = useT(); + const { + name, + setName, + role, + setRole, + title, + setTitle, + department, + setDepartment, + connectorId, + setConnectorId, + model, + setModel, + availableModels, + workspaceId, + setWorkspaceId, + budget, + setBudget, + capabilities, + setCapabilities, + reportsTo, + setReportsTo, + thinkingEffort, + setThinkingEffort, + supportsEffort, + executionReadyConnectors, + selectedConnectorRecord, + selectedDepartment, + } = form; + + const [showAdvanced, setShowAdvanced] = useState(false); + const [requestError, setRequestError] = useState(null); + + async function handleCreate() { + try { + setRequestError(null); + if (!selectedConnectorRecord) { + setRequestError(t("org.selectConnectorFirst")); + return; + } + const meta: Record = {}; + if (thinkingEffort) meta.thinkingEffort = thinkingEffort; + unwrap( + await window.agentCompany.requestHire({ + companyId, + requestedByAgentId: null, + name: name.trim(), + role: role.trim(), + title: title.trim(), + department: selectedDepartment, + connectorId: connectorId as ConnectorRecord["id"], + workspaceId: workspaceId || null, + reportsTo: reportsTo || null, + model: model || null, + metadataJson: JSON.stringify(meta), + capabilities: capabilities.trim(), + budgetMonthlyUsd: Number(budget || 0), + }), + ); + setName(""); + setRole(""); + setTitle(""); + setDepartment(""); + setCapabilities(""); + setReportsTo(""); + setRequestError(null); + await onSaved(); + } catch (error) { + setRequestError( + error instanceof Error ? error.message : t("org.hireRequestFailed"), + ); + } + } + + return ( +
+ + + + ({ value: m.id, label: m.label }))} + /> + ) : null} + {supportsEffort ? ( + setDepartment(value as DepartmentId | "")} + options={DEPARTMENT_OPTIONS.map((option) => ({ + value: option.value, + label: + option.value === "" ? t("comm.selectDepartment") : option.label, + }))} + /> + ({ value: a.id, label: a.name })), + ]} + /> + +