feat(multi-run): Agent Selector (#103)
* Add Agent Selector * Send on Enter * Allow multi run with 1 Model for Agent Manager * Update packages/ui/src/stores/useMultiRunStore.ts * Update Changelog
This commit is contained in:
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Multi Run / Agent Manager: Select the Agent you want to run for the Worktree sessions
|
||||||
|
- Agent Manager now submits the promt, if valid similar to the Chat interfaces
|
||||||
|
|
||||||
## [1.4.3] - 2026-01-04
|
## [1.4.3] - 2026-01-04
|
||||||
|
|
||||||
- VS Code extension: added Agent Manager panel to run the same prompt across up to 5 models in parallel (thanks to @wienans).
|
- VS Code extension: added Agent Manager panel to run the same prompt across up to 5 models in parallel (thanks to @wienans).
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
|
|
||||||
|
export interface AgentSelectorProps {
|
||||||
|
/** Currently selected agent name (empty string for no agent) */
|
||||||
|
value: string;
|
||||||
|
/** Called when agent selection changes */
|
||||||
|
onChange: (agentName: string) => void;
|
||||||
|
/** Optional className for the trigger */
|
||||||
|
className?: string;
|
||||||
|
/** Whether the selector is disabled */
|
||||||
|
disabled?: boolean;
|
||||||
|
/** ID for accessibility */
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent selector dropdown for selecting an agent for multi-run sessions.
|
||||||
|
* Uses getVisibleAgents from useConfigStore to show available agents.
|
||||||
|
*/
|
||||||
|
export const AgentSelector: React.FC<AgentSelectorProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
id,
|
||||||
|
}) => {
|
||||||
|
const getVisibleAgents = useConfigStore((state) => state.getVisibleAgents);
|
||||||
|
const loadAgents = useConfigStore((state) => state.loadAgents);
|
||||||
|
const agents = getVisibleAgents();
|
||||||
|
|
||||||
|
// Load agents on mount
|
||||||
|
React.useEffect(() => {
|
||||||
|
loadAgents();
|
||||||
|
}, [loadAgents]);
|
||||||
|
|
||||||
|
// Use empty string to represent "no agent" selection
|
||||||
|
const handleValueChange = (newValue: string) => {
|
||||||
|
onChange(newValue === '__none__' ? '' : newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert empty value to __none__ for the Select component (which doesn't handle empty strings well)
|
||||||
|
const selectValue = value || '__none__';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={selectValue}
|
||||||
|
onValueChange={handleValueChange}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id={id}
|
||||||
|
size="lg"
|
||||||
|
className={className ?? 'max-w-full typography-meta text-foreground'}
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Select an agent (optional)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent fitContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Default</SelectLabel>
|
||||||
|
<SelectItem value="__none__" className="w-auto whitespace-nowrap">
|
||||||
|
No agent (default)
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
|
||||||
|
{agents.length > 0 && (
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Agents</SelectLabel>
|
||||||
|
{agents.map((agent) => (
|
||||||
|
<SelectItem
|
||||||
|
key={agent.name}
|
||||||
|
value={agent.name}
|
||||||
|
className="w-auto whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{agent.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@ import { useUIStore } from '@/stores/useUIStore';
|
|||||||
import type { CreateMultiRunParams, MultiRunModelSelection } from '@/types/multirun';
|
import type { CreateMultiRunParams, MultiRunModelSelection } from '@/types/multirun';
|
||||||
import { ModelMultiSelect, generateInstanceId, type ModelSelectionWithId } from './ModelMultiSelect';
|
import { ModelMultiSelect, generateInstanceId, type ModelSelectionWithId } from './ModelMultiSelect';
|
||||||
import { BranchSelector, useBranchOptions } from './BranchSelector';
|
import { BranchSelector, useBranchOptions } from './BranchSelector';
|
||||||
|
import { AgentSelector } from './AgentSelector';
|
||||||
|
|
||||||
/** Max file size in bytes (10MB) */
|
/** Max file size in bytes (10MB) */
|
||||||
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
||||||
@@ -50,6 +51,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
|||||||
const [name, setName] = React.useState('');
|
const [name, setName] = React.useState('');
|
||||||
const [prompt, setPrompt] = React.useState(() => initialPrompt ?? '');
|
const [prompt, setPrompt] = React.useState(() => initialPrompt ?? '');
|
||||||
const [selectedModels, setSelectedModels] = React.useState<ModelSelectionWithId[]>([]);
|
const [selectedModels, setSelectedModels] = React.useState<ModelSelectionWithId[]>([]);
|
||||||
|
const [selectedAgent, setSelectedAgent] = React.useState<string>('');
|
||||||
const [attachedFiles, setAttachedFiles] = React.useState<MultiRunAttachedFile[]>([]);
|
const [attachedFiles, setAttachedFiles] = React.useState<MultiRunAttachedFile[]>([]);
|
||||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||||
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
@@ -191,6 +193,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
|||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
prompt: prompt.trim(),
|
prompt: prompt.trim(),
|
||||||
models: modelsForStore,
|
models: modelsForStore,
|
||||||
|
agent: selectedAgent || undefined,
|
||||||
worktreeBaseBranch,
|
worktreeBaseBranch,
|
||||||
files: filesForStore.length > 0 ? filesForStore : undefined,
|
files: filesForStore.length > 0 ? filesForStore : undefined,
|
||||||
};
|
};
|
||||||
@@ -303,6 +306,24 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Agent selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label
|
||||||
|
className="typography-ui-label font-medium text-foreground"
|
||||||
|
htmlFor="multirun-agent"
|
||||||
|
>
|
||||||
|
Agent
|
||||||
|
</label>
|
||||||
|
<AgentSelector
|
||||||
|
value={selectedAgent}
|
||||||
|
onChange={setSelectedAgent}
|
||||||
|
id="multirun-agent"
|
||||||
|
/>
|
||||||
|
<p className="typography-micro text-muted-foreground">
|
||||||
|
Optional agent to use for all runs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Prompt */}
|
{/* Prompt */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="prompt" className="typography-ui-label font-medium text-foreground">
|
<label htmlFor="prompt" className="typography-ui-label font-medium text-foreground">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export { MultiRunLauncher } from './MultiRunLauncher';
|
export { MultiRunLauncher } from './MultiRunLauncher';
|
||||||
export { ModelMultiSelect, ModelChip, generateInstanceId, type ModelSelectionWithId, type ModelSelection, type ModelMultiSelectProps } from './ModelMultiSelect';
|
export { ModelMultiSelect, ModelChip, generateInstanceId, type ModelSelectionWithId, type ModelSelection, type ModelMultiSelectProps } from './ModelMultiSelect';
|
||||||
export { BranchSelector, useBranchOptions, type BranchSelectorProps, type BranchSelectorState, type WorktreeBaseOption } from './BranchSelector';
|
export { BranchSelector, useBranchOptions, type BranchSelectorProps, type BranchSelectorState, type WorktreeBaseOption } from './BranchSelector';
|
||||||
|
export { AgentSelector, type AgentSelectorProps } from './AgentSelector';
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Textarea } from '@/components/ui/textarea';
|
|||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
import { ModelMultiSelect, generateInstanceId, type ModelSelectionWithId } from '@/components/multirun/ModelMultiSelect';
|
import { ModelMultiSelect, generateInstanceId, type ModelSelectionWithId } from '@/components/multirun/ModelMultiSelect';
|
||||||
import { BranchSelector, useBranchOptions } from '@/components/multirun/BranchSelector';
|
import { BranchSelector, useBranchOptions } from '@/components/multirun/BranchSelector';
|
||||||
|
import { AgentSelector } from '@/components/multirun/AgentSelector';
|
||||||
import type { CreateMultiRunParams, MultiRunFileAttachment } from '@/types/multirun';
|
import type { CreateMultiRunParams, MultiRunFileAttachment } from '@/types/multirun';
|
||||||
|
|
||||||
/** Max file size in bytes (10MB) */
|
/** Max file size in bytes (10MB) */
|
||||||
@@ -47,6 +48,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
|
|||||||
const [groupName, setGroupName] = React.useState('');
|
const [groupName, setGroupName] = React.useState('');
|
||||||
const [prompt, setPrompt] = React.useState('');
|
const [prompt, setPrompt] = React.useState('');
|
||||||
const [selectedModels, setSelectedModels] = React.useState<ModelSelectionWithId[]>([]);
|
const [selectedModels, setSelectedModels] = React.useState<ModelSelectionWithId[]>([]);
|
||||||
|
const [selectedAgent, setSelectedAgent] = React.useState<string>('');
|
||||||
const [baseBranch, setBaseBranch] = React.useState('HEAD');
|
const [baseBranch, setBaseBranch] = React.useState('HEAD');
|
||||||
const [attachedFiles, setAttachedFiles] = React.useState<AttachedFile[]>([]);
|
const [attachedFiles, setAttachedFiles] = React.useState<AttachedFile[]>([]);
|
||||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||||
@@ -154,6 +156,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
|
|||||||
name: groupName.trim(),
|
name: groupName.trim(),
|
||||||
prompt: prompt.trim(),
|
prompt: prompt.trim(),
|
||||||
models,
|
models,
|
||||||
|
agent: selectedAgent || undefined,
|
||||||
worktreeBaseBranch: baseBranch,
|
worktreeBaseBranch: baseBranch,
|
||||||
files,
|
files,
|
||||||
});
|
});
|
||||||
@@ -162,6 +165,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
|
|||||||
setGroupName('');
|
setGroupName('');
|
||||||
setPrompt('');
|
setPrompt('');
|
||||||
setSelectedModels([]);
|
setSelectedModels([]);
|
||||||
|
setSelectedAgent('');
|
||||||
setAttachedFiles([]);
|
setAttachedFiles([]);
|
||||||
setBaseBranch('HEAD');
|
setBaseBranch('HEAD');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -172,6 +176,18 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
// Enter submits if valid, Shift+Enter adds newline
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (isValid && !isSubmittingOrCreating) {
|
||||||
|
handleSubmit(e as unknown as React.FormEvent);
|
||||||
|
}
|
||||||
|
// If not valid, do nothing (no newline, no submit)
|
||||||
|
}
|
||||||
|
// Shift+Enter: default textarea behavior (adds newline)
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-col items-center justify-center h-full w-full p-4', className)}>
|
<div className={cn('flex flex-col items-center justify-center h-full w-full p-4', className)}>
|
||||||
<form onSubmit={handleSubmit} className="w-full max-w-2xl space-y-4">
|
<form onSubmit={handleSubmit} className="w-full max-w-2xl space-y-4">
|
||||||
@@ -208,6 +224,20 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Agent Selection */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="typography-ui-label font-medium text-foreground">
|
||||||
|
Agent
|
||||||
|
</label>
|
||||||
|
<AgentSelector
|
||||||
|
value={selectedAgent}
|
||||||
|
onChange={setSelectedAgent}
|
||||||
|
/>
|
||||||
|
<p className="typography-micro text-muted-foreground">
|
||||||
|
Optional agent to use for all runs
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Model Selection */}
|
{/* Model Selection */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="typography-ui-label font-medium text-foreground">
|
<label className="typography-ui-label font-medium text-foreground">
|
||||||
@@ -235,6 +265,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
|
|||||||
id="prompt"
|
id="prompt"
|
||||||
value={prompt}
|
value={prompt}
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Ask anything..."
|
placeholder="Ask anything..."
|
||||||
className="min-h-[100px] max-h-[300px] resize-none border-0 bg-transparent px-4 py-3 typography-markdown focus-visible:ring-0 focus-visible:ring-offset-0"
|
className="min-h-[100px] max-h-[300px] resize-none border-0 bg-transparent px-4 py-3 typography-markdown focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ export const useMultiRunStore = create<MultiRunStore>()(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (models.length < 2) {
|
if (models.length < 1) {
|
||||||
set({ error: 'Select at least 2 models' });
|
set({ error: 'Select at least 1 model' });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user