diff --git a/src/__tests__/renderer/components/GroupChatModals.test.tsx b/src/__tests__/renderer/components/GroupChatModals.test.tsx index acf9f446..1fd05c3b 100644 --- a/src/__tests__/renderer/components/GroupChatModals.test.tsx +++ b/src/__tests__/renderer/components/GroupChatModals.test.tsx @@ -7,7 +7,7 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { NewGroupChatModal } from '../../../renderer/components/NewGroupChatModal'; import { EditGroupChatModal } from '../../../renderer/components/EditGroupChatModal'; import type { Theme, GroupChat, AgentConfig } from '../../../renderer/types'; @@ -167,18 +167,17 @@ describe('Group Chat Modals', () => { /> ); - // Wait for agent detection + // Wait for agent detection and verify dropdown is rendered await waitFor(() => { - expect(screen.getByText('Claude Code')).toBeInTheDocument(); + expect(screen.getByRole('combobox', { name: /select moderator/i })).toBeInTheDocument(); }); - // Select the agent first (the tile is now a div with role="button") - const agentTile = screen.getByText('Claude Code').closest('[role="button"]'); - expect(agentTile).not.toBeNull(); - fireEvent.click(agentTile!); + // Verify Claude Code is selected in dropdown + const dropdown = screen.getByRole('combobox', { name: /select moderator/i }); + expect(dropdown).toHaveValue('claude-code'); - // Click the Customize button to open config panel - const customizeButton = screen.getByText('Customize'); + // Click the Customize button to expand config panel + const customizeButton = screen.getByRole('button', { name: /customize/i }); fireEvent.click(customizeButton); // Wait for config panel to appear and verify MAESTRO_SESSION_RESUMED is displayed @@ -189,6 +188,39 @@ describe('Group Chat Modals', () => { // Also verify the value hint is shown expect(screen.getByText('1 (when resuming)')).toBeInTheDocument(); }); + + it('should show all available agents in dropdown', async () => { + // Setup multiple agents + vi.mocked(window.maestro.agents.detect).mockResolvedValue([ + createMockAgent({ id: 'claude-code', name: 'Claude Code' }), + createMockAgent({ id: 'codex', name: 'Codex' }), + createMockAgent({ id: 'opencode', name: 'OpenCode' }), + createMockAgent({ id: 'factory-droid', name: 'Factory Droid' }), + ]); + + const onCreate = vi.fn(); + const onClose = vi.fn(); + + render( + + ); + + // Wait for dropdown to be rendered + await waitFor(() => { + expect(screen.getByRole('combobox', { name: /select moderator/i })).toBeInTheDocument(); + }); + + // Verify all agents appear as options + expect(screen.getByRole('option', { name: /Claude Code/i })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: /Codex.*Beta/i })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: /OpenCode.*Beta/i })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: /Factory Droid.*Beta/i })).toBeInTheDocument(); + }); }); describe('EditGroupChatModal', () => { @@ -207,13 +239,17 @@ describe('Group Chat Modals', () => { /> ); - // Wait for agent detection + // Wait for dropdown to be rendered await waitFor(() => { - expect(screen.getByText('Claude Code')).toBeInTheDocument(); + expect(screen.getByRole('combobox', { name: /select moderator/i })).toBeInTheDocument(); }); - // Click the Customize button to open config panel - const customizeButton = screen.getByText('Customize'); + // Verify Claude Code is pre-selected + const dropdown = screen.getByRole('combobox', { name: /select moderator/i }); + expect(dropdown).toHaveValue('claude-code'); + + // Click the Customize button to expand config panel + const customizeButton = screen.getByRole('button', { name: /customize/i }); fireEvent.click(customizeButton); // Wait for config panel to appear and verify MAESTRO_SESSION_RESUMED is displayed @@ -224,5 +260,41 @@ describe('Group Chat Modals', () => { // Also verify the value hint is shown expect(screen.getByText('1 (when resuming)')).toBeInTheDocument(); }); + + it('should show warning when changing moderator agent', async () => { + // Setup multiple agents + vi.mocked(window.maestro.agents.detect).mockResolvedValue([ + createMockAgent({ id: 'claude-code', name: 'Claude Code' }), + createMockAgent({ id: 'codex', name: 'Codex' }), + ]); + + const onSave = vi.fn(); + const onClose = vi.fn(); + const groupChat = createMockGroupChat({ moderatorAgentId: 'claude-code' }); + + render( + + ); + + // Wait for dropdown + await waitFor(() => { + expect(screen.getByRole('combobox', { name: /select moderator/i })).toBeInTheDocument(); + }); + + // Change to different agent + const dropdown = screen.getByRole('combobox', { name: /select moderator/i }); + fireEvent.change(dropdown, { target: { value: 'codex' } }); + + // Verify warning message appears + await waitFor(() => { + expect(screen.getByText(/changing the moderator agent/i)).toBeInTheDocument(); + }); + }); }); }); diff --git a/src/renderer/components/EditGroupChatModal.tsx b/src/renderer/components/EditGroupChatModal.tsx index 5685c6ca..f6069a3d 100644 --- a/src/renderer/components/EditGroupChatModal.tsx +++ b/src/renderer/components/EditGroupChatModal.tsx @@ -3,19 +3,19 @@ * * Modal for editing an existing Group Chat. Allows user to: * - Change the name of the group chat - * - Change the moderator agent - * - Customize moderator settings (CLI args, path, ENV vars) + * - Change the moderator agent via dropdown + * - Customize moderator settings (CLI args, path, ENV vars) via expandable panel * * Similar to NewGroupChatModal but pre-populated with existing values. */ import { useState, useEffect, useRef, useCallback } from 'react'; -import { Check, X, Settings, ArrowLeft } from 'lucide-react'; +import { X, Settings, ChevronDown, Check } from 'lucide-react'; import type { Theme, AgentConfig, ModeratorConfig, GroupChat } from '../types'; import type { SshRemoteConfig, AgentSshRemoteConfig } from '../../shared/types'; import { MODAL_PRIORITIES } from '../constants/modalPriorities'; import { Modal, ModalFooter, FormInput } from './ui'; -import { AgentLogo, AGENT_TILES } from './Wizard/screens/AgentSelectionScreen'; +import { AGENT_TILES } from './Wizard/screens/AgentSelectionScreen'; import { AgentConfigPanel } from './shared/AgentConfigPanel'; import { SshRemoteSelector } from './shared/SshRemoteSelector'; @@ -44,9 +44,8 @@ export function EditGroupChatModal({ const [detectedAgents, setDetectedAgents] = useState([]); const [isDetecting, setIsDetecting] = useState(true); - // View mode for switching between grid and config - const [viewMode, setViewMode] = useState<'grid' | 'config'>('grid'); - const [isTransitioning, setIsTransitioning] = useState(false); + // Configuration panel state - expandable below dropdown + const [isConfigExpanded, setIsConfigExpanded] = useState(false); // Custom moderator configuration state const [customPath, setCustomPath] = useState(''); @@ -81,8 +80,7 @@ export function EditGroupChatModal({ setCustomPath(groupChat.moderatorConfig?.customPath || ''); setCustomArgs(groupChat.moderatorConfig?.customArgs || ''); setCustomEnvVars(groupChat.moderatorConfig?.customEnvVars || {}); - setViewMode('grid'); - setIsTransitioning(false); + setIsConfigExpanded(false); setAgentConfig({}); setAvailableModels([]); setLoadingModels(false); @@ -94,8 +92,7 @@ export function EditGroupChatModal({ setName(''); setSelectedAgent(null); setIsDetecting(true); - setViewMode('grid'); - setIsTransitioning(false); + setIsConfigExpanded(false); setCustomPath(''); setCustomArgs(''); setCustomEnvVars({}); @@ -143,10 +140,41 @@ export function EditGroupChatModal({ // Focus name input when agents detected useEffect(() => { - if (!isDetecting && isOpen && viewMode === 'grid') { + if (!isDetecting && isOpen) { nameInputRef.current?.focus(); } - }, [isDetecting, isOpen, viewMode]); + }, [isDetecting, isOpen]); + + // Load agent config when expanding configuration panel + useEffect(() => { + if (isConfigExpanded && selectedAgent) { + loadAgentConfig(selectedAgent); + } + }, [isConfigExpanded, selectedAgent]); + + // Load agent configuration + const loadAgentConfig = useCallback( + async (agentId: string) => { + const config = await window.maestro.agents.getConfig(agentId); + setAgentConfig(config || {}); + agentConfigRef.current = config || {}; + + // Load models if agent supports it + const agent = detectedAgents.find((a) => a.id === agentId); + if (agent?.capabilities?.supportsModelSelection) { + setLoadingModels(true); + try { + const models = await window.maestro.agents.getModels(agentId); + setAvailableModels(models); + } catch (err) { + console.error('Failed to load models:', err); + } finally { + setLoadingModels(false); + } + } + }, + [detectedAgents] + ); // Build moderator config from state const buildModeratorConfig = useCallback((): ModeratorConfig | undefined => { @@ -194,45 +222,9 @@ export function EditGroupChatModal({ const canSave = name.trim().length > 0 && selectedAgent !== null && hasChanges(); - // Open configuration panel for the selected agent - const handleOpenConfig = useCallback(async () => { - if (!selectedAgent) return; - - // Load agent config - const config = await window.maestro.agents.getConfig(selectedAgent); - setAgentConfig(config || {}); - agentConfigRef.current = config || {}; - - // Load models if agent supports it - const agent = detectedAgents.find((a) => a.id === selectedAgent); - // Note: capabilities is added by agent-detector but not in the TypeScript type - if ((agent as any)?.capabilities?.supportsModelSelection) { - setLoadingModels(true); - try { - const models = await window.maestro.agents.getModels(selectedAgent); - setAvailableModels(models); - } catch (err) { - console.error('Failed to load models:', err); - } finally { - setLoadingModels(false); - } - } - - // Transition to config view - setIsTransitioning(true); - setTimeout(() => { - setViewMode('config'); - setIsTransitioning(false); - }, 150); - }, [selectedAgent, detectedAgents]); - - // Close configuration panel - const handleCloseConfig = useCallback(() => { - setIsTransitioning(true); - setTimeout(() => { - setViewMode('grid'); - setIsTransitioning(false); - }, 150); + // Toggle configuration panel + const handleToggleConfig = useCallback(() => { + setIsConfigExpanded((prev) => !prev); }, []); // Refresh agent detection after config changes @@ -266,6 +258,22 @@ export function EditGroupChatModal({ } }, [selectedAgent]); + // Handle agent selection change + const handleAgentChange = useCallback( + (agentId: string) => { + setSelectedAgent(agentId); + // Reset customizations when changing agent + setCustomPath(''); + setCustomArgs(''); + setCustomEnvVars({}); + // If config is expanded, reload config for new agent + if (isConfigExpanded) { + loadAgentConfig(agentId); + } + }, + [isConfigExpanded, loadAgentConfig] + ); + if (!isOpen || !groupChat) return null; // Filter AGENT_TILES to only show supported + detected agents @@ -281,127 +289,6 @@ export function EditGroupChatModal({ // Check if there's any customization set const hasCustomization = customPath || customArgs || Object.keys(customEnvVars).length > 0; - // Render configuration view - if (viewMode === 'config' && selectedAgentConfig && selectedTile) { - return ( - -
- -

- Configure {selectedTile.name} -

-
- - - } - footer={ - - } - > -
- { - /* Local state only */ - }} - onCustomPathClear={() => setCustomPath('')} - customArgs={customArgs} - onCustomArgsChange={setCustomArgs} - onCustomArgsBlur={() => { - /* Local state only */ - }} - onCustomArgsClear={() => setCustomArgs('')} - customEnvVars={customEnvVars} - onEnvVarKeyChange={(oldKey, newKey, value) => { - const newVars = { ...customEnvVars }; - delete newVars[oldKey]; - newVars[newKey] = value; - setCustomEnvVars(newVars); - }} - onEnvVarValueChange={(key, value) => { - setCustomEnvVars({ ...customEnvVars, [key]: value }); - }} - onEnvVarRemove={(key) => { - const newVars = { ...customEnvVars }; - delete newVars[key]; - setCustomEnvVars(newVars); - }} - onEnvVarAdd={() => { - let newKey = 'NEW_VAR'; - let counter = 1; - while (customEnvVars[newKey]) { - newKey = `NEW_VAR_${counter}`; - counter++; - } - setCustomEnvVars({ ...customEnvVars, [newKey]: '' }); - }} - onEnvVarsBlur={() => { - /* Local state only */ - }} - agentConfig={agentConfig} - onConfigChange={(key, value) => { - const newConfig = { ...agentConfig, [key]: value }; - setAgentConfig(newConfig); - agentConfigRef.current = newConfig; - setConfigWasModified(true); - }} - onConfigBlur={async () => { - if (selectedAgent) { - // Use ref to get latest config (state may be stale in async callback) - await window.maestro.agents.setConfig(selectedAgent, agentConfigRef.current); - setConfigWasModified(true); - } - }} - availableModels={availableModels} - loadingModels={loadingModels} - onRefreshModels={handleRefreshModels} - onRefreshAgent={handleRefreshAgent} - refreshingAgent={refreshingAgent} - compact - showBuiltInEnvVars - /> -
-
- ); - } - - // Render grid view return ( } > -
+
{/* Name Input */}
- {/* Agent Selection */} -
+ {/* Moderator Selection - Dropdown with Customize button */} +
{isDetecting ? ( -
+
+ + Detecting agents... +
) : availableTiles.length === 0 ? ( -
- No agents available. Please install Claude Code, OpenCode, or Codex. +
+ No agents available. Please install Claude Code, OpenCode, Codex, or Factory Droid.
) : ( -
- {availableTiles.map((tile) => { - const isSelected = selectedAgent === tile.id; +
+ {/* Dropdown */} +
+ + +
- return ( -
setSelectedAgent(tile.id)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - setSelectedAgent(tile.id); - } - }} - className="relative flex flex-col items-center p-4 pb-10 rounded-lg border-2 transition-all outline-none cursor-pointer" - style={{ - backgroundColor: isSelected ? `${tile.brandColor}15` : theme.colors.bgMain, - borderColor: isSelected ? tile.brandColor : theme.colors.border, - }} - > - {isSelected && ( -
- -
- )} - - - {tile.name} + {/* Customize button */} + +
+ )} + + {/* Expandable Configuration Panel */} + {isConfigExpanded && selectedAgentConfig && selectedTile && ( +
+
+ + {selectedTile.name} Configuration + + {hasCustomization && ( +
+ + + Customized - - {/* Customize button */} -
- ); - })} + )} +
+ { + /* Local state only */ + }} + onCustomPathClear={() => setCustomPath('')} + customArgs={customArgs} + onCustomArgsChange={setCustomArgs} + onCustomArgsBlur={() => { + /* Local state only */ + }} + onCustomArgsClear={() => setCustomArgs('')} + customEnvVars={customEnvVars} + onEnvVarKeyChange={(oldKey, newKey, value) => { + const newVars = { ...customEnvVars }; + delete newVars[oldKey]; + newVars[newKey] = value; + setCustomEnvVars(newVars); + }} + onEnvVarValueChange={(key, value) => { + setCustomEnvVars({ ...customEnvVars, [key]: value }); + }} + onEnvVarRemove={(key) => { + const newVars = { ...customEnvVars }; + delete newVars[key]; + setCustomEnvVars(newVars); + }} + onEnvVarAdd={() => { + let newKey = 'NEW_VAR'; + let counter = 1; + while (customEnvVars[newKey]) { + newKey = `NEW_VAR_${counter}`; + counter++; + } + setCustomEnvVars({ ...customEnvVars, [newKey]: '' }); + }} + onEnvVarsBlur={() => { + /* Local state only */ + }} + agentConfig={agentConfig} + onConfigChange={(key, value) => { + const newConfig = { ...agentConfig, [key]: value }; + setAgentConfig(newConfig); + agentConfigRef.current = newConfig; + setConfigWasModified(true); + }} + onConfigBlur={async () => { + if (selectedAgent) { + // Use ref to get latest config (state may be stale in async callback) + await window.maestro.agents.setConfig(selectedAgent, agentConfigRef.current); + setConfigWasModified(true); + } + }} + availableModels={availableModels} + loadingModels={loadingModels} + onRefreshModels={handleRefreshModels} + onRefreshAgent={handleRefreshAgent} + refreshingAgent={refreshingAgent} + compact + showBuiltInEnvVars + />
)}
{/* SSH Remote Execution - Top Level */} {sshRemotes.length > 0 && ( -
+
([]); const [isDetecting, setIsDetecting] = useState(true); - // View mode for switching between grid and config - const [viewMode, setViewMode] = useState<'grid' | 'config'>('grid'); - const [isTransitioning, setIsTransitioning] = useState(false); + // Configuration panel state - expandable below dropdown + const [isConfigExpanded, setIsConfigExpanded] = useState(false); // Custom moderator configuration state const [customPath, setCustomPath] = useState(''); @@ -65,8 +64,7 @@ export function NewGroupChatModal({ setName(''); setSelectedAgent(null); setIsDetecting(true); - setViewMode('grid'); - setIsTransitioning(false); + setIsConfigExpanded(false); setCustomPath(''); setCustomArgs(''); setCustomEnvVars({}); @@ -127,10 +125,41 @@ export function NewGroupChatModal({ // Focus name input when agents detected useEffect(() => { - if (!isDetecting && isOpen && viewMode === 'grid') { + if (!isDetecting && isOpen) { nameInputRef.current?.focus(); } - }, [isDetecting, isOpen, viewMode]); + }, [isDetecting, isOpen]); + + // Load agent config when expanding configuration panel + useEffect(() => { + if (isConfigExpanded && selectedAgent) { + loadAgentConfig(selectedAgent); + } + }, [isConfigExpanded, selectedAgent]); + + // Load agent configuration + const loadAgentConfig = useCallback( + async (agentId: string) => { + const config = await window.maestro.agents.getConfig(agentId); + setAgentConfig(config || {}); + agentConfigRef.current = config || {}; + + // Load models if agent supports it + const agent = detectedAgents.find((a) => a.id === agentId); + if (agent?.capabilities?.supportsModelSelection) { + setLoadingModels(true); + try { + const models = await window.maestro.agents.getModels(agentId); + setAvailableModels(models); + } catch (err) { + console.error('Failed to load models:', err); + } finally { + setLoadingModels(false); + } + } + }, + [detectedAgents] + ); // Build moderator config from state const buildModeratorConfig = useCallback((): ModeratorConfig | undefined => { @@ -155,44 +184,9 @@ export function NewGroupChatModal({ const canCreate = name.trim().length > 0 && selectedAgent !== null; - // Open configuration panel for the selected agent - const handleOpenConfig = useCallback(async () => { - if (!selectedAgent) return; - - // Load agent config - const config = await window.maestro.agents.getConfig(selectedAgent); - setAgentConfig(config || {}); - agentConfigRef.current = config || {}; - - // Load models if agent supports it - const agent = detectedAgents.find((a) => a.id === selectedAgent); - if (agent?.capabilities?.supportsModelSelection) { - setLoadingModels(true); - try { - const models = await window.maestro.agents.getModels(selectedAgent); - setAvailableModels(models); - } catch (err) { - console.error('Failed to load models:', err); - } finally { - setLoadingModels(false); - } - } - - // Transition to config view - setIsTransitioning(true); - setTimeout(() => { - setViewMode('config'); - setIsTransitioning(false); - }, 150); - }, [selectedAgent, detectedAgents]); - - // Close configuration panel - const handleCloseConfig = useCallback(() => { - setIsTransitioning(true); - setTimeout(() => { - setViewMode('grid'); - setIsTransitioning(false); - }, 150); + // Toggle configuration panel + const handleToggleConfig = useCallback(() => { + setIsConfigExpanded((prev) => !prev); }, []); // Refresh agent detection after config changes @@ -226,6 +220,22 @@ export function NewGroupChatModal({ } }, [selectedAgent]); + // Handle agent selection change + const handleAgentChange = useCallback( + (agentId: string) => { + setSelectedAgent(agentId); + // Reset customizations when changing agent + setCustomPath(''); + setCustomArgs(''); + setCustomEnvVars({}); + // If config is expanded, reload config for new agent + if (isConfigExpanded) { + loadAgentConfig(agentId); + } + }, + [isConfigExpanded, loadAgentConfig] + ); + if (!isOpen) return null; // Filter AGENT_TILES to only show supported + detected agents @@ -241,125 +251,6 @@ export function NewGroupChatModal({ // Check if there's any customization set const hasCustomization = customPath || customArgs || Object.keys(customEnvVars).length > 0; - // Render configuration view - if (viewMode === 'config' && selectedAgentConfig && selectedTile) { - return ( - -
- -

- Configure {selectedTile.name} -

-
- -
- } - footer={ - - } - > -
- { - /* Local state only */ - }} - onCustomPathClear={() => setCustomPath('')} - customArgs={customArgs} - onCustomArgsChange={setCustomArgs} - onCustomArgsBlur={() => { - /* Local state only */ - }} - onCustomArgsClear={() => setCustomArgs('')} - customEnvVars={customEnvVars} - onEnvVarKeyChange={(oldKey, newKey, value) => { - const newVars = { ...customEnvVars }; - delete newVars[oldKey]; - newVars[newKey] = value; - setCustomEnvVars(newVars); - }} - onEnvVarValueChange={(key, value) => { - setCustomEnvVars({ ...customEnvVars, [key]: value }); - }} - onEnvVarRemove={(key) => { - const newVars = { ...customEnvVars }; - delete newVars[key]; - setCustomEnvVars(newVars); - }} - onEnvVarAdd={() => { - let newKey = 'NEW_VAR'; - let counter = 1; - while (customEnvVars[newKey]) { - newKey = `NEW_VAR_${counter}`; - counter++; - } - setCustomEnvVars({ ...customEnvVars, [newKey]: '' }); - }} - onEnvVarsBlur={() => { - /* Local state only */ - }} - agentConfig={agentConfig} - onConfigChange={(key, value) => { - const newConfig = { ...agentConfig, [key]: value }; - setAgentConfig(newConfig); - agentConfigRef.current = newConfig; - }} - onConfigBlur={async () => { - if (selectedAgent) { - // Use ref to get latest config (state may be stale in async callback) - await window.maestro.agents.setConfig(selectedAgent, agentConfigRef.current); - } - }} - availableModels={availableModels} - loadingModels={loadingModels} - onRefreshModels={handleRefreshModels} - onRefreshAgent={handleRefreshAgent} - refreshingAgent={refreshingAgent} - compact - showBuiltInEnvVars - /> -
- - ); - } - - // Render grid view return ( } > -
+
{/* Description */}
A Group Chat lets you collaborate with multiple AI agents in a single conversation. The{' '} @@ -422,98 +311,170 @@ export function NewGroupChatModal({ Claude appears to be the best performing moderator.
- {/* Agent Selection */} + {/* Moderator Selection - Dropdown with Customize button */}
{isDetecting ? ( -
+
+ + Detecting agents... +
) : availableTiles.length === 0 ? ( -
- No agents available. Please install Claude Code, OpenCode, or Codex. +
+ No agents available. Please install Claude Code, OpenCode, Codex, or Factory Droid.
) : ( -
- {availableTiles.map((tile) => { - const isSelected = selectedAgent === tile.id; +
+ {/* Dropdown */} +
+ + +
- return ( -
setSelectedAgent(tile.id)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - setSelectedAgent(tile.id); - } - }} - className="relative flex flex-col items-center p-4 pb-10 rounded-lg border-2 transition-all outline-none cursor-pointer" - style={{ - backgroundColor: isSelected ? `${tile.brandColor}15` : theme.colors.bgMain, - borderColor: isSelected ? tile.brandColor : theme.colors.border, - }} - > - {isSelected && ( -
- -
- )} - - - {tile.name} + {/* Customize button */} + +
+ )} + + {/* Expandable Configuration Panel */} + {isConfigExpanded && selectedAgentConfig && selectedTile && ( +
+
+ + {selectedTile.name} Configuration + + {hasCustomization && ( +
+ + + Customized - - {/* Customize button */} -
- ); - })} + )} +
+ { + /* Local state only */ + }} + onCustomPathClear={() => setCustomPath('')} + customArgs={customArgs} + onCustomArgsChange={setCustomArgs} + onCustomArgsBlur={() => { + /* Local state only */ + }} + onCustomArgsClear={() => setCustomArgs('')} + customEnvVars={customEnvVars} + onEnvVarKeyChange={(oldKey, newKey, value) => { + const newVars = { ...customEnvVars }; + delete newVars[oldKey]; + newVars[newKey] = value; + setCustomEnvVars(newVars); + }} + onEnvVarValueChange={(key, value) => { + setCustomEnvVars({ ...customEnvVars, [key]: value }); + }} + onEnvVarRemove={(key) => { + const newVars = { ...customEnvVars }; + delete newVars[key]; + setCustomEnvVars(newVars); + }} + onEnvVarAdd={() => { + let newKey = 'NEW_VAR'; + let counter = 1; + while (customEnvVars[newKey]) { + newKey = `NEW_VAR_${counter}`; + counter++; + } + setCustomEnvVars({ ...customEnvVars, [newKey]: '' }); + }} + onEnvVarsBlur={() => { + /* Local state only */ + }} + agentConfig={agentConfig} + onConfigChange={(key, value) => { + const newConfig = { ...agentConfig, [key]: value }; + setAgentConfig(newConfig); + agentConfigRef.current = newConfig; + }} + onConfigBlur={async () => { + if (selectedAgent) { + // Use ref to get latest config (state may be stale in async callback) + await window.maestro.agents.setConfig(selectedAgent, agentConfigRef.current); + } + }} + availableModels={availableModels} + loadingModels={loadingModels} + onRefreshModels={handleRefreshModels} + onRefreshAgent={handleRefreshAgent} + refreshingAgent={refreshingAgent} + compact + showBuiltInEnvVars + />
)}