mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
## 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:
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user