pipeline executor improvements, proxy refresh, task runner step list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+75
-5
@@ -491,11 +491,18 @@ const PipelineRunner = ({ taskDirName, context, cwd }: PipelineRunnerProps) => {
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null);
|
||||
const [inputDefs, setInputDefs] = useState<Record<string, TaskInputDef> | null>(null);
|
||||
const [formValues, setFormValues] = useState<Record<string, string>>({});
|
||||
const [hasConcurrentSteps, setHasConcurrentSteps] = useState(false);
|
||||
const [concurrency, setConcurrency] = useState('1');
|
||||
const [pipelineSteps, setPipelineSteps] = useState<Array<{ task: string; foreach?: string }>>([]);
|
||||
const [startAt, setStartAt] = useState(0);
|
||||
const { settings } = useSettings();
|
||||
const availableModels = useUserVisibleModels();
|
||||
const [selectedModel, setSelectedModel] = useState(settings.tasks.defaultModel ?? 'claude-haiku-4-5-20251001');
|
||||
|
||||
// Fetch task detail for inputs
|
||||
// Fetch task detail for inputs + check for concurrent steps
|
||||
useEffect(() => {
|
||||
client.get<{ inputs?: Record<string, TaskInputDef> }>(`/tasks/${taskDirName}`).then((task) => {
|
||||
const defs = task.inputs ?? {};
|
||||
client.get<{ inputs?: Record<string, TaskInputDef>; config?: { steps?: Array<{ task: string; foreach?: string; concurrency?: string | boolean }> } }>(`/tasks/${taskDirName}`).then((task) => {
|
||||
const defs: Record<string, TaskInputDef> = task.inputs ?? {};
|
||||
setInputDefs(defs);
|
||||
const initial: Record<string, string> = {};
|
||||
for (const [key, def] of Object.entries(defs)) {
|
||||
@@ -503,6 +510,10 @@ const PipelineRunner = ({ taskDirName, context, cwd }: PipelineRunnerProps) => {
|
||||
else if (def.default !== undefined) initial[key] = def.default;
|
||||
}
|
||||
setFormValues(initial);
|
||||
|
||||
const steps = task.config?.steps ?? [];
|
||||
setPipelineSteps(steps.map((s) => ({ task: s.task, foreach: s.foreach })));
|
||||
setHasConcurrentSteps(steps.some((s: { concurrency?: string | boolean }) => s.concurrency));
|
||||
});
|
||||
}, [taskDirName]);
|
||||
|
||||
@@ -525,7 +536,11 @@ const PipelineRunner = ({ taskDirName, context, cwd }: PipelineRunnerProps) => {
|
||||
}, [pipeline.phase]);
|
||||
|
||||
const handleRun = () => {
|
||||
pipeline.run(taskDirName, formValues, cwd);
|
||||
const allInputs = { ...formValues };
|
||||
if (hasConcurrentSteps && parseInt(concurrency, 10) > 1) {
|
||||
allInputs._concurrency = concurrency;
|
||||
}
|
||||
pipeline.run(taskDirName, allInputs, cwd, selectedModel, startAt > 0 ? startAt : undefined);
|
||||
};
|
||||
|
||||
const hasConfigurableInputs = inputDefs && Object.keys(inputDefs).length > 0;
|
||||
@@ -541,6 +556,61 @@ const PipelineRunner = ({ taskDirName, context, cwd }: PipelineRunnerProps) => {
|
||||
autoFilledKeys={new Set()}
|
||||
/>
|
||||
)}
|
||||
{hasConcurrentSteps && (
|
||||
<div className="px-5 py-3 border-b border-duck-dark/10 flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<span className="text-sm font-medium text-duck-dark dark:text-foreground">Concurrency</span>
|
||||
<span className="text-xs text-duck-dark/50 dark:text-foreground/50 ml-2">parallel runners per step</span>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={20}
|
||||
value={concurrency}
|
||||
onChange={(e) => setConcurrency(e.target.value)}
|
||||
className="w-16 px-2 py-1 text-sm text-center rounded-lg border border-duck-dark/15 dark:border-foreground/15 bg-background focus:outline-none focus:ring-1 focus:ring-duck-teal"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-5 py-3 border-b border-duck-dark/10 flex items-center justify-between gap-4">
|
||||
<span className="text-sm font-medium text-duck-dark dark:text-foreground">Model</span>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(ev) => setSelectedModel(ev.target.value)}
|
||||
className="px-2 py-1 text-sm rounded-lg border border-duck-dark/15 dark:border-foreground/15 bg-background focus:outline-none focus:ring-1 focus:ring-duck-teal"
|
||||
>
|
||||
{availableModels.map((m) => (
|
||||
<option key={`${m.provider}:${m.id}`} value={m.id}>
|
||||
{m.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{pipelineSteps.length > 0 && (
|
||||
<div className="px-5 py-3 border-b border-duck-dark/10">
|
||||
<span className="text-sm font-medium text-duck-dark dark:text-foreground mb-2 block">Steps</span>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{pipelineSteps.map((step, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setStartAt(i)}
|
||||
className={`text-left text-xs px-2.5 py-1.5 rounded-md transition-colors cursor-pointer flex items-center gap-2 ${
|
||||
i < startAt
|
||||
? 'text-duck-dark/30 dark:text-foreground/30 line-through'
|
||||
: i === startAt
|
||||
? 'bg-duck-teal/10 text-duck-teal font-medium'
|
||||
: 'text-duck-dark/70 dark:text-foreground/70 hover:bg-duck-dark/5'
|
||||
}`}
|
||||
>
|
||||
<span className="w-4 text-center font-mono text-[10px]">{i + 1}</span>
|
||||
<span>{step.task}</span>
|
||||
{step.foreach && <span className="text-duck-dark/40 dark:text-foreground/40">(foreach)</span>}
|
||||
{i === startAt && i > 0 && <span className="ml-auto text-[10px] text-duck-teal/70">start here</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<button
|
||||
onClick={handleRun}
|
||||
@@ -548,7 +618,7 @@ const PipelineRunner = ({ taskDirName, context, cwd }: PipelineRunnerProps) => {
|
||||
className="flex items-center gap-2 px-6 py-2.5 rounded-lg bg-duck-teal text-white font-medium text-sm hover:bg-duck-teal/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
Run Pipeline
|
||||
{startAt > 0 ? `Run from step ${startAt + 1}` : 'Run Pipeline'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -290,7 +290,7 @@ export function usePipelineRunner() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const run = useCallback((taskDirName: string, inputs: Record<string, string>, cwd?: string) => {
|
||||
const run = useCallback((taskDirName: string, inputs: Record<string, string>, cwd?: string, model?: string, startAt?: number) => {
|
||||
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
setPhase('running');
|
||||
@@ -313,7 +313,7 @@ export function usePipelineRunner() {
|
||||
setElapsed(Math.floor((Date.now() - startTimeRef.current) / 1000));
|
||||
}, 1000);
|
||||
|
||||
wsRef.current.send(JSON.stringify({ type: 'run', taskDirName, inputs, cwd }));
|
||||
wsRef.current.send(JSON.stringify({ type: 'run', taskDirName, inputs, cwd, model, startAt }));
|
||||
}, []);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user