From d3f611e14e14b73858bac15dfec975a13ab9bd4c Mon Sep 17 00:00:00 2001 From: Pedram Amini Date: Tue, 30 Dec 2025 05:14:50 -0600 Subject: [PATCH] ## CHANGES MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pick an SSH remote when creating a new group chat! 🛰️ - Edit existing group chats with top-level SSH remote execution controls! 🛠️ - Agent edit modal now uses dedicated SSH Remote selector UI! 🎛️ - Wizard agent selection adds compact SSH Remote selector in config view! 🧙 - SSH Remote selector UX revamped: “local vs selected remote” clarity! 🔍 - Removed global default SSH remote logic for simpler, explicit choices! 🧹 - Selector styling now matches form fields for cleaner modal layouts! 🎨 - Status indicator now reflects selected remote, not “effective” fallback! 📡 - SSH remotes are loaded automatically in group chat modals on open! ⚡ - SSH remote config resets cleanly when modals reset state! 🔄 --- .../components/EditGroupChatModal.tsx | 31 +++ src/renderer/components/NewGroupChatModal.tsx | 31 +++ src/renderer/components/NewInstanceModal.tsx | 28 +- .../Wizard/screens/AgentSelectionScreen.tsx | 23 +- .../components/shared/SshRemoteSelector.tsx | 261 +++++++++--------- 5 files changed, 223 insertions(+), 151 deletions(-) diff --git a/src/renderer/components/EditGroupChatModal.tsx b/src/renderer/components/EditGroupChatModal.tsx index 2784e02d..58cbc5b7 100644 --- a/src/renderer/components/EditGroupChatModal.tsx +++ b/src/renderer/components/EditGroupChatModal.tsx @@ -12,10 +12,12 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { Check, X, Settings, ArrowLeft } 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 { AgentConfigPanel } from './shared/AgentConfigPanel'; +import { SshRemoteSelector } from './shared/SshRemoteSelector'; interface EditGroupChatModalProps { theme: Theme; @@ -52,6 +54,10 @@ export function EditGroupChatModal({ // Track if user has visited/modified the config panel (agent-level settings like model) const [configWasModified, setConfigWasModified] = useState(false); + // SSH Remote configuration state + const [sshRemotes, setSshRemotes] = useState([]); + const [sshRemoteConfig, setSshRemoteConfig] = useState(undefined); + const nameInputRef = useRef(null); // Ref to track latest agentConfig for async save operations const agentConfigRef = useRef>({}); @@ -91,6 +97,7 @@ export function EditGroupChatModal({ setLoadingModels(false); setRefreshingAgent(false); setConfigWasModified(false); + setSshRemoteConfig(undefined); }, []); // Detect agents on mount @@ -112,7 +119,19 @@ export function EditGroupChatModal({ } } + async function loadSshRemotes() { + try { + const configsResult = await window.maestro.sshRemote.getConfigs(); + if (configsResult.success && configsResult.configs) { + setSshRemotes(configsResult.configs); + } + } catch (error) { + console.error('Failed to load SSH remotes:', error); + } + } + detect(); + loadSshRemotes(); }, [isOpen, resetState]); // Focus name input when agents detected @@ -500,6 +519,18 @@ export function EditGroupChatModal({ )} + {/* SSH Remote Execution - Top Level */} + {sshRemotes.length > 0 && ( +
+ +
+ )} + {/* Warning about changing moderator */} {groupChat && selectedAgent !== groupChat.moderatorAgentId && (
([]); + const [sshRemoteConfig, setSshRemoteConfig] = useState(undefined); + const nameInputRef = useRef(null); // Ref to track latest agentConfig for async save operations const agentConfigRef = useRef>({}); @@ -66,6 +72,7 @@ export function NewGroupChatModal({ setAvailableModels([]); setLoadingModels(false); setRefreshingAgent(false); + setSshRemoteConfig(undefined); }, []); // Detect agents on mount @@ -101,7 +108,19 @@ export function NewGroupChatModal({ } } + async function loadSshRemotes() { + try { + const configsResult = await window.maestro.sshRemote.getConfigs(); + if (configsResult.success && configsResult.configs) { + setSshRemotes(configsResult.configs); + } + } catch (error) { + console.error('Failed to load SSH remotes:', error); + } + } + detect(); + loadSshRemotes(); }, [isOpen, resetState]); // Focus name input when agents detected @@ -503,6 +522,18 @@ export function NewGroupChatModal({ )}
+ {/* SSH Remote Execution - Top Level */} + {sshRemotes.length > 0 && ( +
+ +
+ )} + {/* Name Input */} ([]); - const [globalDefaultSshRemoteId, setGlobalDefaultSshRemoteId] = useState(null); const [agentSshRemoteConfigs, setAgentSshRemoteConfigs] = useState>({}); const nameInputRef = useRef(null); @@ -162,10 +161,6 @@ export function NewInstanceModal({ isOpen, onClose, onCreate, theme, existingSes if (sshConfigsResult.success && sshConfigsResult.configs) { setSshRemotes(sshConfigsResult.configs); } - const sshDefaultResult = await window.maestro.sshRemote.getDefaultId(); - if (sshDefaultResult.success) { - setGlobalDefaultSshRemoteId(sshDefaultResult.id ?? null); - } } catch (sshError) { console.error('Failed to load SSH remote configs:', sshError); } @@ -708,7 +703,6 @@ export function NewInstanceModal({ isOpen, onClose, onCreate, theme, existingSes [selectedAgent]: config })); }} - globalDefaultSshRemoteId={globalDefaultSshRemoteId} /> )} @@ -764,7 +758,6 @@ export function EditAgentModal({ isOpen, onClose, onSave, theme, session, existi const [refreshingAgent, setRefreshingAgent] = useState(false); // SSH Remote configuration const [sshRemotes, setSshRemotes] = useState([]); - const [globalDefaultSshRemoteId, setGlobalDefaultSshRemoteId] = useState(null); const [sshRemoteConfig, setSshRemoteConfig] = useState(undefined); const nameInputRef = useRef(null); @@ -806,13 +799,6 @@ export function EditAgentModal({ isOpen, onClose, onSave, theme, session, existi } }) .catch((err) => console.error('Failed to load SSH remotes:', err)); - window.maestro.sshRemote.getDefaultId() - .then((result) => { - if (result.success) { - setGlobalDefaultSshRemoteId(result.id ?? null); - } - }) - .catch((err) => console.error('Failed to load SSH default ID:', err)); // Load per-session config (stored on the session/agent instance) // No provider-level fallback - each agent has its own config @@ -1096,13 +1082,19 @@ export function EditAgentModal({ isOpen, onClose, onSave, theme, session, existi onRefreshAgent={handleRefreshAgent} refreshingAgent={refreshingAgent} showBuiltInEnvVars - sshRemotes={sshRemotes} - sshRemoteConfig={sshRemoteConfig} - onSshRemoteConfigChange={setSshRemoteConfig} - globalDefaultSshRemoteId={globalDefaultSshRemoteId} /> )} + + {/* SSH Remote Execution - Top Level */} + {sshRemotes.length > 0 && ( + + )} diff --git a/src/renderer/components/Wizard/screens/AgentSelectionScreen.tsx b/src/renderer/components/Wizard/screens/AgentSelectionScreen.tsx index 437647ae..2e4c1583 100644 --- a/src/renderer/components/Wizard/screens/AgentSelectionScreen.tsx +++ b/src/renderer/components/Wizard/screens/AgentSelectionScreen.tsx @@ -19,6 +19,7 @@ import type { SshRemoteConfig, AgentSshRemoteConfig } from '../../../../shared/t import { useWizard } from '../WizardContext'; import { ScreenReaderAnnouncement } from '../ScreenReaderAnnouncement'; import { AgentConfigPanel } from '../../shared/AgentConfigPanel'; +import { SshRemoteSelector } from '../../shared/SshRemoteSelector'; interface AgentSelectionScreenProps { theme: Theme; @@ -322,7 +323,6 @@ export function AgentSelectionScreen({ theme }: AgentSelectionScreenProps): JSX. // SSH Remote configuration state const [sshRemotes, setSshRemotes] = useState([]); const [sshRemoteConfig, setSshRemoteConfig] = useState(undefined); - const [globalDefaultSshRemoteId, setGlobalDefaultSshRemoteId] = useState(null); // Refs const containerRef = useRef(null); @@ -390,10 +390,6 @@ export function AgentSelectionScreen({ theme }: AgentSelectionScreenProps): JSX. if (mounted && configsResult.success && configsResult.configs) { setSshRemotes(configsResult.configs); } - const defaultResult = await window.maestro.sshRemote.getDefaultId(); - if (mounted && defaultResult.success) { - setGlobalDefaultSshRemoteId(defaultResult.id ?? null); - } } catch (error) { console.error('Failed to load SSH remotes:', error); } @@ -816,11 +812,20 @@ export function AgentSelectionScreen({ theme }: AgentSelectionScreenProps): JSX. refreshingAgent={refreshingAgent} compact showBuiltInEnvVars - sshRemotes={sshRemotes} - sshRemoteConfig={sshRemoteConfig} - onSshRemoteConfigChange={setSshRemoteConfig} - globalDefaultSshRemoteId={globalDefaultSshRemoteId} /> + + {/* SSH Remote Execution - at config view level */} + {sshRemotes.length > 0 && ( +
+ +
+ )} diff --git a/src/renderer/components/shared/SshRemoteSelector.tsx b/src/renderer/components/shared/SshRemoteSelector.tsx index cee55a55..2550d2df 100644 --- a/src/renderer/components/shared/SshRemoteSelector.tsx +++ b/src/renderer/components/shared/SshRemoteSelector.tsx @@ -6,7 +6,7 @@ * * Displays: * - Dropdown to select SSH remote (or local execution) - * - Status indicator showing effective remote + * - Status indicator showing selected remote * - Hint when no remotes are configured */ @@ -19,8 +19,7 @@ export interface SshRemoteSelectorProps { sshRemotes: SshRemoteConfig[]; sshRemoteConfig?: AgentSshRemoteConfig; onSshRemoteConfigChange: (config: AgentSshRemoteConfig) => void; - globalDefaultSshRemoteId?: string | null; - /** Optional: compact mode with less padding */ + /** Optional: compact mode with less padding (for use inside config panels) */ compact?: boolean; } @@ -29,134 +28,148 @@ export function SshRemoteSelector({ sshRemotes, sshRemoteConfig, onSshRemoteConfigChange, - globalDefaultSshRemoteId, compact = false, }: SshRemoteSelectorProps): JSX.Element { - const padding = compact ? 'p-2' : 'p-3'; + // Compact mode uses bordered container style (for nested use in config panels) + // Non-compact mode uses simple label + input style (for top-level modal use) + if (compact) { + return ( +
+ + +

+ Execute this agent on a remote host via SSH instead of locally +

+
+ ); + } + // Non-compact: simple label + input style matching other form fields return ( -
-