From d318e0fa2ad1982ec43c4435a341490af84b76d0 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Mon, 1 Jun 2026 02:38:35 -0700 Subject: [PATCH] feat(fleet): engine picker offers only engines the factory advertises MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add GET /fleet/factories (lists a product's factory docs with capabilities) — also fixes the fleet map's empty factory cards (listFactories had no route and silently returned []). The New-Job form now loads the selected factory's engine:* capabilities and constrains the engine dropdown to those (e.g. hides codex when the host doesn't have it), keeping the current pick valid; falls back to all engines when capabilities are unknown. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/app/dashboard/fleet/jobs/page.tsx | 34 ++++++++++++++++++- .../tracker-web/src/lib/fleet-client.ts | 22 ++++++++++-- .../src/modules/fleet/routes.ts | 8 +++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/dashboards/tracker-web/src/app/dashboard/fleet/jobs/page.tsx b/dashboards/tracker-web/src/app/dashboard/fleet/jobs/page.tsx index b81f2a78..a98bc996 100644 --- a/dashboards/tracker-web/src/app/dashboard/fleet/jobs/page.tsx +++ b/dashboards/tracker-web/src/app/dashboard/fleet/jobs/page.tsx @@ -8,6 +8,7 @@ import { useAuth } from '@/lib/auth-context'; import { listJobs, submitJob, + availableEnginesForProduct, FLEET_ENGINES, type FleetJob, type FleetEngine, @@ -99,6 +100,9 @@ export default function FleetJobsPage() { const [body, setBody] = useState(''); const [priority, setPriority] = useState<'critical' | 'high' | 'medium' | 'low'>('high'); const [engine, setEngine] = useState('devin'); + // Engines the selected factory's product actually advertises ([] ⇒ unknown, + // offer all). Keeps users from picking an engine the host can't run. + const [engineOptions, setEngineOptions] = useState([]); // Empty by default: no agent-queue factory advertises a `build` capability // (caps are os:* / engine:* / node:* / has:*), so a non-empty default here makes // the job unroutable. Leave blank ⇒ any capable factory for the product claims it. @@ -133,6 +137,31 @@ export default function FleetJobsPage() { useEffect(() => { setHideShipped(localStorage.getItem('fleet_hide_shipped') === '1'); }, []); + + // When the target factory changes, learn which engines its product advertises + // and constrain the picker (so you can't pick an engine the host can't run). + useEffect(() => { + if (!token || !showForm) return; + const factory = FLEET_FACTORIES.find(f => f.id === factoryId) ?? FLEET_FACTORIES[0]; + let cancelled = false; + availableEnginesForProduct(factory.productId) + .then(engines => { + if (cancelled) return; + setEngineOptions(engines); + // Keep the selection valid: prefer the current pick, else devin, else first. + if (engines.length > 0) { + setEngine(prev => + engines.includes(prev) ? prev : engines.includes('devin') ? 'devin' : engines[0]! + ); + } + }) + .catch(() => { + if (!cancelled) setEngineOptions([]); + }); + return () => { + cancelled = true; + }; + }, [token, showForm, factoryId]); const toggleHideShipped = useCallback((next: boolean) => { setHideShipped(next); localStorage.setItem('fleet_hide_shipped', next ? '1' : '0'); @@ -290,12 +319,15 @@ export default function FleetJobsPage() { onChange={e => setEngine(e.target.value as FleetEngine)} className="rounded border bg-background px-2 py-1 text-sm" > - {FLEET_ENGINES.map(e => ( + {(engineOptions.length > 0 ? engineOptions : FLEET_ENGINES).map(e => ( ))} + {engineOptions.length > 0 && ( +

advertised by this factory

+ )}