From b5a26fc33a8cd578847ec02a5865d96b65fb2a8a Mon Sep 17 00:00:00 2001 From: zaridan <1617679+zaridan@users.noreply.github.com> Date: Tue, 23 Jun 2026 21:08:30 -0700 Subject: [PATCH] feat(orchestration): director-type picker + DirectorBackend abstraction (#11, #8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #8 — new src/renderer/src/lib/director-backend.ts: a thin DirectorBackend abstraction wrapping the two existing launchers, not reimplementing them. LlmDirectorBackend wraps launchOrchestratorForProject (the Smart/LLM director); RecipeDirectorBackend carries a Recipe and wraps launchRecipeDirector (#9). It earns its keep because the picker dispatches the user's choice through it. #11 — the ORCASTRATORS `+` modal now picks the director TYPE first via a new DirectorTypePicker (shadcn cards + recipe Select, tokens, i18n): Smart director (default LLM + /orcastrate) vs Recipe director ("No director LLM; runs a fixed workflow"), with a recipe dropdown sourced from getRecipes(). The picker is the gate for experimentalOrchestrators — launchRecipeDirector is ungated by design, so the Recipe option only appears under the flag; with it off the modal behaves exactly as before. On confirm the modal dispatches via the DirectorBackend. Tests: director-backend (Smart -> LlmDirectorBackend -> launchOrchestratorForProject; Recipe+recipe -> RecipeDirectorBackend(recipe) -> launchRecipeDirector with that recipe) and DirectorTypePicker (Smart always; Recipe only under the flag; dropdown lists getRecipes()). vitest + typecheck + electron-vite build + oxlint green. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/OrchestratorLaunchModal.tsx | 113 ++++++++---- .../director/DirectorTypePicker.test.tsx | 74 ++++++++ .../director/DirectorTypePicker.tsx | 173 ++++++++++++++++++ src/renderer/src/i18n/locales/en.json | 11 ++ src/renderer/src/i18n/locales/es.json | 11 ++ src/renderer/src/i18n/locales/ja.json | 11 ++ src/renderer/src/i18n/locales/ko.json | 11 ++ src/renderer/src/i18n/locales/zh.json | 11 ++ src/renderer/src/lib/director-backend.test.ts | 63 +++++++ src/renderer/src/lib/director-backend.ts | 51 ++++++ 10 files changed, 496 insertions(+), 33 deletions(-) create mode 100644 src/renderer/src/components/director/DirectorTypePicker.test.tsx create mode 100644 src/renderer/src/components/director/DirectorTypePicker.tsx create mode 100644 src/renderer/src/lib/director-backend.test.ts create mode 100644 src/renderer/src/lib/director-backend.ts diff --git a/src/renderer/src/components/OrchestratorLaunchModal.tsx b/src/renderer/src/components/OrchestratorLaunchModal.tsx index e7a74900bf4..ed1695eaef0 100644 --- a/src/renderer/src/components/OrchestratorLaunchModal.tsx +++ b/src/renderer/src/components/OrchestratorLaunchModal.tsx @@ -17,7 +17,13 @@ import { buildNewWorkspaceCreateTargetOptions } from '@/lib/new-workspace-projec import { getComposerEligibleRepos } from '@/lib/new-workspace-composer-repo' import { getAgentCatalog } from '@/lib/agent-catalog' import { filterEnabledTuiAgents } from '../../../shared/tui-agent-selection' -import { launchOrchestratorForProject } from '@/lib/orchestrator-launch' +import { DirectorTypePicker } from '@/components/director/DirectorTypePicker' +import { + LlmDirectorBackend, + RecipeDirectorBackend, + type DirectorKind +} from '@/lib/director-backend' +import { getRecipes } from '@/lib/recipe-director-recipes' import { translate } from '@/i18n/i18n' import type { TuiAgent } from '../../../shared/types' @@ -49,6 +55,11 @@ export default function OrchestratorLaunchModal(): React.JSX.Element | null { const detectedAgentIds = useAppStore((s) => s.detectedAgentIds) const disabledTuiAgents = useAppStore((s) => s.settings?.disabledTuiAgents) const defaultTuiAgent = useAppStore((s) => s.settings?.defaultTuiAgent ?? null) + // Why: #11 owns the gate — the Recipe director option only appears under the + // experimental flag; with it off the modal behaves exactly as it did before. + const experimentalOrchestrators = useAppStore( + (s) => s.settings?.experimentalOrchestrators ?? false + ) const nameId = useId() const promptId = useId() @@ -90,10 +101,13 @@ export default function OrchestratorLaunchModal(): React.JSX.Element | null { : null, [projectOptions, prefillProjectId] ) + const recipes = useMemo(() => getRecipes(), []) const [selectedOptionId, setSelectedOptionId] = useState(null) const [name, setName] = useState('') const [agent, setAgent] = useState(null) const [prompt, setPrompt] = useState('') + const [directorKind, setDirectorKind] = useState('llm') + const [recipeName, setRecipeName] = useState(null) useEffect(() => { if (visible) { @@ -101,6 +115,8 @@ export default function OrchestratorLaunchModal(): React.JSX.Element | null { setAgent(defaultTuiAgent && defaultTuiAgent !== 'blank' ? defaultTuiAgent : null) setName(prefillName) setPrompt(prefillPrompt) + setDirectorKind('llm') + setRecipeName(recipes[0]?.name ?? null) } }, [ visible, @@ -108,7 +124,8 @@ export default function OrchestratorLaunchModal(): React.JSX.Element | null { prefilledProjectOptionId, defaultTuiAgent, prefillName, - prefillPrompt + prefillPrompt, + recipes ]) if (!visible) { @@ -121,15 +138,28 @@ export default function OrchestratorLaunchModal(): React.JSX.Element | null { ? (projects.find((p) => p.id === selectedOption.projectId) ?? null) : null + // The Recipe director is gated; if the flag is off, only the Smart path is live. + const isRecipe = directorKind === 'recipe' && experimentalOrchestrators + const handleLaunch = (): void => { if (!project) { return } - void launchOrchestratorForProject(project, { - name: name.trim() || undefined, - agent: agent ?? undefined, - prompt: prompt.trim() || undefined - }) + // Dispatch through the DirectorBackend abstraction (#8) instead of branching + // on the kind at the call site. + if (isRecipe) { + const recipe = recipes.find((entry) => entry.name === recipeName) + if (!recipe) { + return + } + void new RecipeDirectorBackend(recipe).launch(project, { name: name.trim() || undefined }) + } else { + void new LlmDirectorBackend().launch(project, { + name: name.trim() || undefined, + agent: agent ?? undefined, + prompt: prompt.trim() || undefined + }) + } closeModal() } @@ -184,6 +214,16 @@ export default function OrchestratorLaunchModal(): React.JSX.Element | null { )} /> + {experimentalOrchestrators && ( + + )}
-
- - -
-
- -