diff --git a/client/src/components/Layout.jsx b/client/src/components/Layout.jsx index 0d3c4cb501..6ff188b289 100644 --- a/client/src/components/Layout.jsx +++ b/client/src/components/Layout.jsx @@ -305,6 +305,7 @@ export const NAV_PRESENTATION = { '/settings/general': { icon: Settings }, '/settings/mortalloom': { icon: Activity }, '/openclaw': { icon: MessagesSquare }, + '/settings/orchestration': { icon: Cpu }, '/prompts': { icon: FileText }, '/ai': { icon: Bot }, '/settings/security': { icon: Lock }, diff --git a/client/src/components/cos/TaskAddForm.jsx b/client/src/components/cos/TaskAddForm.jsx index fa7b293a69..932c6d058e 100644 --- a/client/src/components/cos/TaskAddForm.jsx +++ b/client/src/components/cos/TaskAddForm.jsx @@ -21,6 +21,13 @@ import { reviewerModelsFromDefaults, reviewerEffortsFromDefaults } from '../../l import { PORTOS_APP_ID } from '../../lib/appIdentity'; import { safeReadJsonStorage, safeReadStorage, safeRemoveStorage, safeWriteJsonStorage } from '../../lib/safeStorage'; +const ORCHESTRATION_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra']; +const ORCHESTRATION_ROLES_META = [ + { key: 'architect', label: 'Architect', hint: 'Planning & spec authoring' }, + { key: 'implementer', label: 'Implementer', hint: 'Spec execution' }, + { key: 'reviewer', label: 'Reviewer', hint: 'Spec verification' }, +]; + const TASK_DESCRIPTION_DRAFT_KEY = 'portos-cos-task-description-draft'; const INVALID_DRAFT = Symbol('invalid task description draft'); @@ -121,6 +128,66 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o // Bare slashdo command a quick-template pinned (`plan-task`), never a rendered // `/do:x` string — see server/lib/slashdoInvocation.js for why. const [slashdoCommand, setSlashdoCommand] = useState(''); + const [orchestrationMode, setOrchestrationMode] = useState('direct'); + const [orchestrationProfiles, setOrchestrationProfiles] = useState([]); + const [selectedProfileId, setSelectedProfileId] = useState(''); + const [orchestrationProfile, setOrchestrationProfile] = useState({ + architect: { provider: '', model: '', effort: '' }, + implementer: { provider: '', model: '', effort: '' }, + reviewer: { provider: '', model: '', effort: '' }, + }); + + useEffect(() => { + api.getOrchestrationProfiles?.({ silent: true }) + ?.then((res) => { + const list = Array.isArray(res) ? res : res?.profiles || []; + setOrchestrationProfiles(list); + }) + ?.catch(() => {}); + }, []); + + const handleSelectOrchestrationProfile = (profileId) => { + setSelectedProfileId(profileId); + if (!profileId) return; + const found = orchestrationProfiles.find((p) => p.id === profileId); + if (found?.profile) { + setOrchestrationProfile({ + architect: { + provider: found.profile.architect?.provider || '', + model: found.profile.architect?.model || '', + effort: found.profile.architect?.effort || '', + }, + implementer: { + provider: found.profile.implementer?.provider || '', + model: found.profile.implementer?.model || '', + effort: found.profile.implementer?.effort || '', + }, + reviewer: { + provider: found.profile.reviewer?.provider || '', + model: found.profile.reviewer?.model || '', + effort: found.profile.reviewer?.effort || '', + }, + }); + } + }; + + const updateOrchestrationRoleField = (roleKey, field, val) => { + setOrchestrationProfile((prev) => { + const roleData = prev[roleKey] || {}; + let updatedRole = { ...roleData, [field]: val }; + if (field === 'provider') { + updatedRole.model = ''; + updatedRole.effort = ''; + } else if (field === 'model') { + const prov = providers?.find((p) => p.id === roleData.provider); + updatedRole.effort = effortSurvivingModel(prov, val, roleData.effort); + } + return { + ...prev, + [roleKey]: updatedRole, + }; + }); + }; // Resolved model lists for the reviewer table's Model column. Owned here (not by // ReviewerPicker) so the picker stays fetch-free — see its `modelOptions` prop. const reviewerModelOptions = useReviewerModelOptions(); @@ -573,6 +640,8 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o model: newTask.model || undefined, provider: newTask.provider || undefined, effort: newTask.effort || undefined, + orchestrationMode: orchestrationMode === 'orchestrated' ? 'orchestrated' : undefined, + orchestrationProfile: orchestrationMode === 'orchestrated' ? orchestrationProfile : undefined, temperature: newTask.temperature === '' ? undefined : Number(newTask.temperature), thinking: newTask.thinking === '' ? undefined : newTask.thinking === 'true', app: newTask.app || undefined, @@ -970,58 +1039,164 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o > )} -