## CHANGES

- 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! 🔄
This commit is contained in:
Pedram Amini
2025-12-30 05:14:50 -06:00
parent f3499ab7f0
commit d3f611e14e
5 changed files with 223 additions and 151 deletions

View File

@@ -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<SshRemoteConfig[]>([]);
const [sshRemoteConfig, setSshRemoteConfig] = useState<AgentSshRemoteConfig | undefined>(undefined);
const nameInputRef = useRef<HTMLInputElement>(null);
// Ref to track latest agentConfig for async save operations
const agentConfigRef = useRef<Record<string, any>>({});
@@ -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({
)}
</div>
{/* SSH Remote Execution - Top Level */}
{sshRemotes.length > 0 && (
<div className="mb-4">
<SshRemoteSelector
theme={theme}
sshRemotes={sshRemotes}
sshRemoteConfig={sshRemoteConfig}
onSshRemoteConfigChange={setSshRemoteConfig}
/>
</div>
)}
{/* Warning about changing moderator */}
{groupChat && selectedAgent !== groupChat.moderatorAgentId && (
<div

View File

@@ -12,10 +12,12 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { Check, X, Settings, ArrowLeft } from 'lucide-react';
import type { Theme, AgentConfig, ModeratorConfig } 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 NewGroupChatModalProps {
theme: Theme;
@@ -48,6 +50,10 @@ export function NewGroupChatModal({
const [loadingModels, setLoadingModels] = useState(false);
const [refreshingAgent, setRefreshingAgent] = useState(false);
// SSH Remote configuration state
const [sshRemotes, setSshRemotes] = useState<SshRemoteConfig[]>([]);
const [sshRemoteConfig, setSshRemoteConfig] = useState<AgentSshRemoteConfig | undefined>(undefined);
const nameInputRef = useRef<HTMLInputElement>(null);
// Ref to track latest agentConfig for async save operations
const agentConfigRef = useRef<Record<string, any>>({});
@@ -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({
)}
</div>
{/* SSH Remote Execution - Top Level */}
{sshRemotes.length > 0 && (
<div className="mb-6">
<SshRemoteSelector
theme={theme}
sshRemotes={sshRemotes}
sshRemoteConfig={sshRemoteConfig}
onSshRemoteConfigChange={setSshRemoteConfig}
/>
</div>
)}
{/* Name Input */}
<FormInput
ref={nameInputRef}

View File

@@ -82,7 +82,6 @@ export function NewInstanceModal({ isOpen, onClose, onCreate, theme, existingSes
const [directoryWarningAcknowledged, setDirectoryWarningAcknowledged] = useState(false);
// SSH Remote configuration
const [sshRemotes, setSshRemotes] = useState<SshRemoteConfig[]>([]);
const [globalDefaultSshRemoteId, setGlobalDefaultSshRemoteId] = useState<string | null>(null);
const [agentSshRemoteConfigs, setAgentSshRemoteConfigs] = useState<Record<string, AgentSshRemoteConfig>>({});
const nameInputRef = useRef<HTMLInputElement>(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<SshRemoteConfig[]>([]);
const [globalDefaultSshRemoteId, setGlobalDefaultSshRemoteId] = useState<string | null>(null);
const [sshRemoteConfig, setSshRemoteConfig] = useState<AgentSshRemoteConfig | undefined>(undefined);
const nameInputRef = useRef<HTMLInputElement>(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}
/>
</div>
)}
{/* SSH Remote Execution - Top Level */}
{sshRemotes.length > 0 && (
<SshRemoteSelector
theme={theme}
sshRemotes={sshRemotes}
sshRemoteConfig={sshRemoteConfig}
onSshRemoteConfigChange={setSshRemoteConfig}
/>
)}
</div>
</Modal>
</div>

View File

@@ -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<SshRemoteConfig[]>([]);
const [sshRemoteConfig, setSshRemoteConfig] = useState<AgentSshRemoteConfig | undefined>(undefined);
const [globalDefaultSshRemoteId, setGlobalDefaultSshRemoteId] = useState<string | null>(null);
// Refs
const containerRef = useRef<HTMLDivElement>(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 && (
<div className="mt-3">
<SshRemoteSelector
theme={theme}
sshRemotes={sshRemotes}
sshRemoteConfig={sshRemoteConfig}
onSshRemoteConfigChange={setSshRemoteConfig}
compact
/>
</div>
)}
</div>
</div>

View File

@@ -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 (
<div
className="p-2 rounded border"
style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgMain }}
>
<label className="block text-xs font-medium mb-2" style={{ color: theme.colors.textDim }}>
SSH Remote Execution
</label>
<SshRemoteDropdown
theme={theme}
sshRemotes={sshRemotes}
sshRemoteConfig={sshRemoteConfig}
onSshRemoteConfigChange={onSshRemoteConfigChange}
/>
<p className="text-xs opacity-50 mt-2">
Execute this agent on a remote host via SSH instead of locally
</p>
</div>
);
}
// Non-compact: simple label + input style matching other form fields
return (
<div
className={`${padding} rounded border`}
style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgMain }}
>
<label className="block text-xs font-medium mb-2" style={{ color: theme.colors.textDim }}>
<div>
<label
className="block text-xs font-bold opacity-70 uppercase mb-2"
style={{ color: theme.colors.textMain }}
>
SSH Remote Execution
</label>
{/* SSH Remote Selection */}
<div className="space-y-3">
{/* Dropdown to select remote */}
<div className="relative">
<select
value={
sshRemoteConfig?.enabled === false
? 'disabled'
: sshRemoteConfig?.remoteId || 'default'
}
onChange={(e) => {
const value = e.target.value;
if (value === 'disabled') {
// Explicitly disable SSH for this agent (run locally even if global default is set)
onSshRemoteConfigChange({
enabled: false,
remoteId: null,
});
} else if (value === 'default') {
// Use global default (or local if no global default)
onSshRemoteConfigChange({
enabled: true,
remoteId: null,
});
} else {
// Use specific remote
onSshRemoteConfigChange({
enabled: true,
remoteId: value,
});
}
}}
onClick={(e) => e.stopPropagation()}
className="w-full p-2 rounded border bg-transparent outline-none text-xs appearance-none cursor-pointer pr-8"
style={{
borderColor: theme.colors.border,
color: theme.colors.textMain,
backgroundColor: theme.colors.bgMain,
}}
>
<option value="default">
{globalDefaultSshRemoteId
? `Use Global Default (${sshRemotes.find(r => r.id === globalDefaultSshRemoteId)?.name || 'Unknown'})`
: 'Local Execution (No SSH Remote)'}
</option>
<option value="disabled">Force Local Execution</option>
{sshRemotes.filter(r => r.enabled).map((remote) => (
<option key={remote.id} value={remote.id}>
{remote.name} ({remote.host})
</option>
))}
</select>
<ChevronDown
className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none"
style={{ color: theme.colors.textDim }}
/>
</div>
{/* Status indicator showing effective remote */}
{(() => {
const effectiveRemoteId = sshRemoteConfig?.enabled === false
? null
: sshRemoteConfig?.remoteId || globalDefaultSshRemoteId || null;
const effectiveRemote = effectiveRemoteId
? sshRemotes.find(r => r.id === effectiveRemoteId && r.enabled)
: null;
const isForceLocal = sshRemoteConfig?.enabled === false;
return (
<div
className="flex items-center gap-2 px-2 py-1.5 rounded text-xs"
style={{ backgroundColor: theme.colors.bgActivity }}
>
{isForceLocal ? (
<>
<Monitor className="w-3 h-3" style={{ color: theme.colors.textDim }} />
<span style={{ color: theme.colors.textDim }}>
Agent will run locally (SSH disabled)
</span>
</>
) : effectiveRemote ? (
<>
<Cloud className="w-3 h-3" style={{ color: theme.colors.success }} />
<span style={{ color: theme.colors.textMain }}>
Agent will run on <span className="font-medium">{effectiveRemote.name}</span>
<span style={{ color: theme.colors.textDim }}> ({effectiveRemote.host})</span>
</span>
</>
) : (
<>
<Monitor className="w-3 h-3" style={{ color: theme.colors.textDim }} />
<span style={{ color: theme.colors.textDim }}>
Agent will run locally
</span>
</>
)}
</div>
);
})()}
{/* No remotes configured hint */}
{sshRemotes.filter(r => r.enabled).length === 0 && (
<p className="text-xs" style={{ color: theme.colors.textDim }}>
No SSH remotes configured.{' '}
<span style={{ color: theme.colors.accent }}>
Configure remotes in Settings SSH Remotes.
</span>
</p>
)}
</div>
<p className="text-xs opacity-50 mt-2">
Execute this agent on a remote host via SSH instead of locally
<SshRemoteDropdown
theme={theme}
sshRemotes={sshRemotes}
sshRemoteConfig={sshRemoteConfig}
onSshRemoteConfigChange={onSshRemoteConfigChange}
/>
<p className="mt-1 text-xs" style={{ color: theme.colors.textDim }}>
Execute this agent on a remote host via SSH instead of locally.
</p>
</div>
);
}
/** Internal component for the dropdown and status indicator */
function SshRemoteDropdown({
theme,
sshRemotes,
sshRemoteConfig,
onSshRemoteConfigChange,
}: {
theme: Theme;
sshRemotes: SshRemoteConfig[];
sshRemoteConfig?: AgentSshRemoteConfig;
onSshRemoteConfigChange: (config: AgentSshRemoteConfig) => void;
}): JSX.Element {
// Get the currently selected remote (if any)
const selectedRemoteId = sshRemoteConfig?.enabled && sshRemoteConfig?.remoteId
? sshRemoteConfig.remoteId
: null;
const selectedRemote = selectedRemoteId
? sshRemotes.find(r => r.id === selectedRemoteId && r.enabled)
: null;
return (
<div className="space-y-2">
{/* Dropdown to select remote */}
<div className="relative">
<select
value={selectedRemoteId || 'local'}
onChange={(e) => {
const value = e.target.value;
if (value === 'local') {
// Run locally
onSshRemoteConfigChange({
enabled: false,
remoteId: null,
});
} else {
// Use specific remote
onSshRemoteConfigChange({
enabled: true,
remoteId: value,
});
}
}}
onClick={(e) => e.stopPropagation()}
className="w-full p-2 rounded border bg-transparent outline-none text-sm appearance-none cursor-pointer pr-8"
style={{
borderColor: theme.colors.border,
color: theme.colors.textMain,
}}
>
<option value="local">Local Execution</option>
{sshRemotes.filter(r => r.enabled).map((remote) => (
<option key={remote.id} value={remote.id}>
{remote.name} ({remote.host})
</option>
))}
</select>
<ChevronDown
className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none"
style={{ color: theme.colors.textDim }}
/>
</div>
{/* Status indicator showing selected remote */}
<div
className="flex items-center gap-2 px-2 py-1.5 rounded text-xs"
style={{ backgroundColor: theme.colors.bgActivity }}
>
{selectedRemote ? (
<>
<Cloud className="w-3 h-3" style={{ color: theme.colors.success }} />
<span style={{ color: theme.colors.textMain }}>
Agent will run on <span className="font-medium">{selectedRemote.name}</span>
<span style={{ color: theme.colors.textDim }}> ({selectedRemote.host})</span>
</span>
</>
) : (
<>
<Monitor className="w-3 h-3" style={{ color: theme.colors.textDim }} />
<span style={{ color: theme.colors.textDim }}>
Agent will run locally
</span>
</>
)}
</div>
{/* No remotes configured hint */}
{sshRemotes.filter(r => r.enabled).length === 0 && (
<p className="text-xs" style={{ color: theme.colors.textDim }}>
No SSH remotes configured.{' '}
<span style={{ color: theme.colors.accent }}>
Configure remotes in Settings SSH Remotes.
</span>
</p>
)}
</div>
);
}