pipeline executor improvements, proxy refresh, task runner step list

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 07:25:11 +00:00
co-authored by Claude Opus 4.6
parent 0f1e1f35f1
commit 92da03fcff
6 changed files with 280 additions and 86 deletions
@@ -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>
@@ -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(() => {