Skip to content

Commit e79310f

Browse files
AlexsJonesclaude
andcommitted
feat: auto-detect node probe providers and allow changing ensemble provider
Wizard now auto-switches to "Installed on node" mode when matching providers are discovered by the node-probe DaemonSet, instead of defaulting to manual base URL entry. Users can still toggle back. Adds a "Change Provider" button on the ensemble detail page so providers can be reconfigured after initial activation without disabling/re-enabling the ensemble. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent abd0f5d commit e79310f

2 files changed

Lines changed: 108 additions & 5 deletions

File tree

web/src/components/onboarding-wizard.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useMemo, useState } from "react";
1+
import { useEffect, useMemo, useRef, useState } from "react";
22
import { useModelList } from "@/hooks/use-model-list";
33
import { useProviderNodes } from "@/hooks/use-provider-nodes";
44
import { Input } from "@/components/ui/input";
@@ -499,9 +499,30 @@ export function OnboardingWizard({
499499
}
500500
return probeName === form.provider;
501501
};
502-
const { data: providerNodes, isLoading: nodesLoading } = useProviderNodes(
503-
isLocalProvider && inferenceMode === "node",
504-
);
502+
const { data: providerNodes, isLoading: nodesLoading } =
503+
useProviderNodes(isLocalProvider);
504+
// Track whether the user manually toggled inference mode so we don't override it.
505+
const userOverrodeInferenceMode = useRef(false);
506+
// Auto-switch to "node" mode when matching providers are discovered.
507+
useEffect(() => {
508+
if (
509+
!isLocalProvider ||
510+
nodesLoading ||
511+
!providerNodes ||
512+
userOverrodeInferenceMode.current
513+
)
514+
return;
515+
const hasMatch = providerNodes.some((n) =>
516+
n.providers.some((p) => nodeProviderMatches(p.name)),
517+
);
518+
if (hasMatch) {
519+
setInferenceMode("node");
520+
}
521+
}, [providerNodes, nodesLoading, form.provider]);
522+
// Reset the override flag when the provider changes.
523+
useEffect(() => {
524+
userOverrodeInferenceMode.current = false;
525+
}, [form.provider]);
505526

506527
const stepIdx = steps.indexOf(step);
507528

@@ -734,6 +755,7 @@ export function OnboardingWizard({
734755
<button
735756
type="button"
736757
onClick={() => {
758+
userOverrodeInferenceMode.current = true;
737759
setInferenceMode("workload");
738760
setForm({ ...form, nodeSelector: undefined });
739761
}}
@@ -748,7 +770,10 @@ export function OnboardingWizard({
748770
</button>
749771
<button
750772
type="button"
751-
onClick={() => setInferenceMode("node")}
773+
onClick={() => {
774+
userOverrodeInferenceMode.current = true;
775+
setInferenceMode("node");
776+
}}
752777
className={cn(
753778
"flex-1 flex items-center justify-center gap-1.5 rounded-md border px-3 py-2 text-xs transition-colors",
754779
inferenceMode === "node"

web/src/pages/ensemble-detail.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,16 @@ import {
2626
Check,
2727
Workflow,
2828
Database,
29+
Settings,
2930
} from "lucide-react";
3031
import { Breadcrumbs } from "@/components/breadcrumbs";
3132
import { formatAge } from "@/lib/utils";
3233
import { YamlButton, ensembleYamlFromResource } from "@/components/yaml-panel";
3334
import { EnsembleCanvas } from "@/components/ensemble-canvas";
35+
import {
36+
OnboardingWizard,
37+
type WizardResult,
38+
} from "@/components/onboarding-wizard";
3439

3540
interface PersonaEditState {
3641
systemPrompt: string;
@@ -43,6 +48,8 @@ export function EnsembleDetailPage() {
4348
const { data: skillPacks } = useSkills();
4449
const patchMutation = useActivateEnsemble();
4550

51+
const [wizardOpen, setWizardOpen] = useState(false);
52+
4653
// Track which persona is being edited (by name), and its draft state
4754
const [editingPersona, setEditingPersona] = useState<string | null>(null);
4855
const [editState, setEditState] = useState<PersonaEditState>({
@@ -97,6 +104,43 @@ export function EnsembleDetailPage() {
97104
}));
98105
};
99106

107+
function handleProviderChange(result: WizardResult) {
108+
if (!name) return;
109+
let skillParams: Record<string, Record<string, string>> | undefined;
110+
if (result.skills.includes("github-gitops") && result.githubRepo) {
111+
skillParams = { "github-gitops": { repo: result.githubRepo } };
112+
}
113+
patchMutation.mutate(
114+
{
115+
name,
116+
provider: result.provider,
117+
secretName: result.secretName || undefined,
118+
apiKey: result.apiKey || undefined,
119+
awsRegion: result.awsRegion || undefined,
120+
awsAccessKeyId: result.awsAccessKeyId || undefined,
121+
awsSecretAccessKey: result.awsSecretAccessKey || undefined,
122+
awsSessionToken: result.awsSessionToken || undefined,
123+
model: result.model,
124+
baseURL: result.baseURL || undefined,
125+
channels: result.channels.length > 0 ? result.channels : undefined,
126+
channelConfigs:
127+
Object.keys(result.channelConfigs).length > 0
128+
? result.channelConfigs
129+
: undefined,
130+
heartbeatInterval: result.heartbeatInterval || undefined,
131+
skillParams,
132+
githubToken: result.githubToken || undefined,
133+
agentSandbox: result.agentSandboxEnabled
134+
? {
135+
enabled: true,
136+
runtimeClass: result.agentSandboxRuntimeClass || "gvisor",
137+
}
138+
: undefined,
139+
},
140+
{ onSuccess: () => setWizardOpen(false) },
141+
);
142+
}
143+
100144
// Collect all available skill names from SkillPacks
101145
const availableSkills = skillPacks?.flatMap((sp) => sp.metadata.name) ?? [];
102146

@@ -133,6 +177,17 @@ export function EnsembleDetailPage() {
133177
<div className="flex items-center gap-2 text-sm text-muted-foreground">
134178
{pack.spec.description && <span>{pack.spec.description}</span>}
135179
<StatusBadge phase={pack.status?.phase} />
180+
{pack.spec.enabled && (
181+
<Button
182+
variant="outline"
183+
size="sm"
184+
className="gap-1.5 text-xs"
185+
onClick={() => setWizardOpen(true)}
186+
>
187+
<Settings className="h-3.5 w-3.5" />
188+
Change Provider
189+
</Button>
190+
)}
136191
{pack.spec.category && (
137192
<Badge variant="outline" className="capitalize">
138193
{pack.spec.category}
@@ -690,6 +745,29 @@ export function EnsembleDetailPage() {
690745
)}
691746
</TabsContent>
692747
</Tabs>
748+
749+
<OnboardingWizard
750+
open={wizardOpen}
751+
onClose={() => setWizardOpen(false)}
752+
mode="persona"
753+
targetName={pack.metadata.name}
754+
personaCount={pack.spec.personas?.length ?? 0}
755+
availableSkills={(skillPacks || []).map((s) => s.metadata.name)}
756+
defaults={{
757+
provider: pack.spec.authRefs?.[0]?.provider || "",
758+
secretName: pack.spec.authRefs?.[0]?.secret || "",
759+
model: pack.spec.personas?.[0]?.model || "",
760+
skills: Array.from(
761+
new Set((pack.spec.personas || []).flatMap((p) => p.skills || [])),
762+
),
763+
channelConfigs: pack.spec.channelConfigs || {},
764+
channels:
765+
pack.spec.personas?.[0]?.channels ||
766+
Object.keys(pack.spec.channelConfigs || {}),
767+
}}
768+
onComplete={handleProviderChange}
769+
isPending={patchMutation.isPending}
770+
/>
693771
</div>
694772
);
695773
}

0 commit comments

Comments
 (0)