Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5fe5c9f
ai: extract the agent picker into a shared content component
dimitrieh Aug 27, 2026
43af411
ai: move the agent picker's look into unlayered CSS so it survives prose
dimitrieh Aug 27, 2026
030fe4f
Merge branch 'ai/self-hosted-mcp-address' into ai/agent-setup-tabs
dimitrieh Aug 27, 2026
9c669f1
ai: size the agent panel by its own width, not the viewport
dimitrieh Aug 27, 2026
9e8802e
ai: shrink the step CTAs to their labels when the steps stack
dimitrieh Aug 27, 2026
c685e6f
ai: give the card room in prose, and one width for the stacked CTAs
dimitrieh Aug 27, 2026
2d56f76
Merge branch 'main' into ai/agent-setup-tabs
dimitrieh Aug 27, 2026
df86891
ai: keep the changelog entry out of this branch, it lands separately
dimitrieh Aug 27, 2026
0362931
changelog: use the shared agent picker instead of listing the agents
dimitrieh Aug 27, 2026
cf3ecc6
Merge branch 'main' into ai/agent-setup-tabs
dimitrieh Aug 28, 2026
b747230
Merge branch 'ai/agent-setup-tabs' into changelog/agent-setup-tabs
dimitrieh Aug 28, 2026
3b535b7
Merge branch 'main' into ai/agent-setup-tabs
dimitrieh Aug 28, 2026
15fcbb9
Merge branch 'ai/agent-setup-tabs' into changelog/agent-setup-tabs
dimitrieh Aug 28, 2026
88076d3
Merge pull request #5700 from FlowFuse/changelog/agent-setup-tabs
dimitrieh Aug 28, 2026
9dc3f0f
Merge branch 'main' into ai/agent-setup-tabs
dimitrieh Aug 28, 2026
d6e113a
ai: correct the ChatGPT route to developer mode
dimitrieh Aug 28, 2026
de9094a
ai: spell the ChatGPT menu label as it appears on screen
dimitrieh Aug 28, 2026
914b135
Merge branch 'main' into ai/agent-setup-tabs
dimitrieh Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions nuxt/components/content/AgentSetupTabs.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<script setup lang="ts">
// The agent picker: a tab per agent, and the three setup steps that apply to the
// one selected. Lives in components/content/ so markdown can use it as
// ::agent-setup-tabs as well as pages using it directly, which is the point: the
// changelog entry and the third-party-agents docs page need the same steps /ai has,
// and hand-copying them into each surface is how they drift apart.
//
// Styling comes from the .ff-agent-* rules in src/css/style.css, which nuxt.config.ts
// links on every Nuxt page, rather than from utility classes on the elements. That is
// forced: this renders inside .prose on the changelog and docs pages, and the site's
// prose rules sit in @layer overrides, which outranks Tailwind's @layer utilities
// whatever the specificity. As utilities, `h-4` on a tab logo lost to .prose img's
// width:100% and the marks blew up to full column width.
const props = withDefaults(defineProps<{
// Drops the FlowFuse Expert tab. For surfaces that are specifically about
// connecting your own agent, where Expert is not one of the options.
excludeExpert?: boolean
// Whether step 03 ends on a sign-up CTA. Off for documentation, where the
// reader already has an account.
signup?: boolean
// Distinguishes this instance in PostHog. 'ai' keeps the positions the /ai
// page already reports, so its existing numbers stay comparable.
surface?: string
}>(), { excludeExpert: false, signup: true, surface: 'ai' })

const capture = useCapture()

// The Cloud address. Self-hosted platforms answer on their own domain, so the block
// takes one (host-swap) rather than telling the reader in prose to edit what they
// have just copied.
const ENDPOINT = 'https://app.flowfuse.com/mcp'

const STEP1 = {
title: 'Copy the FlowFuse connector URL',
description: 'You will paste this into your agent in the next step.',
}
const STEP3 = {
title: 'Sign in and choose what it reaches',
description: 'Which teams the agent may act on, and whether it has editing rights or read access only.',
}

// A client renders `logo` as a brand mark when set, `icon` as our own glyph when not.
// A brand mark keeps its own colours so it stays an <img>; the glyph inherits
// currentColor and turns white with the label when its tab is active.
const CLIENTS = [
{
id: 'expert',
logo: '/images/ai/agents/flowfuse-expert.svg',
name: 'FlowFuse Expert',
builtIn: true,
step1Title: 'Sign in to FlowFuse',
step1Body: 'A new account puts you in a hosted instance with Expert already in the editor. Nothing to add, no connector, no token.',
step2Title: 'Or start with the Device Agent',
step2Body: 'Already running Node-RED on your own hardware? Connect it as a remote instance and Expert works there in the same way.',
step2Label: 'Install the Device Agent',
step2Url: '/docs/device-agent/quickstart/',
step3Title: 'Ask for what you need',
step3Body: 'Build a flow, explain one you inherited, write the Function node, or ask what is running on the floor. Every write waits for you to approve, edit or reject.',
noStep3Cta: true,
},
{
id: 'copilot',
logo: '/images/ai/agents/microsoft-copilot.svg',
name: 'Microsoft Copilot',
step2Title: 'Copilot Studio, Tools, Add a tool',
step2Body: 'Choose Model Context Protocol and paste the URL. Describe what it is for: the orchestrator reads that to decide when to call it.',
step2Label: 'Open Copilot Studio',
step2Url: 'https://copilotstudio.microsoft.com/',
},
{
id: 'chatgpt',
logo: '/images/ai/agents/chatgpt.svg',
name: 'ChatGPT',
step2Title: 'Settings, Apps & Connectors, Advanced settings',
step2Body: 'Turn on developer mode there, then add FlowFuse by URL. Developer mode needs a paid plan, so it is not on the free tier.',
step2Label: 'Open ChatGPT',
step2Url: 'https://chatgpt.com/',
},
{
id: 'claude',
logo: '/images/ai/agents/claude.svg',
name: 'Claude',
step2Title: 'Add a custom connector',
step2Body: 'Where custom connectors are available on your plan, add one and paste the URL. On Team and Enterprise an owner adds it once for everyone.',
step2Label: 'Open Claude',
step2Url: 'https://claude.ai/',
},
{
id: 'local',
icon: 'i-lucide-server',
name: 'Local and Custom Agents',
step2Title: "Your MCP client's config",
step2Body: 'Any MCP-capable client works, pointed at your own model, so nothing has to leave your network.',
step2Label: 'See the documentation',
step2Url: '/docs/user/expert/',
},
]

const clients = computed(() => props.excludeExpert ? CLIENTS.filter(client => !client.builtIn) : CLIENTS)

// Prefix only off the /ai page, so the positions that page already reports to
// PostHog keep their current values and its existing insights stay valid.
function pos (id: string) {
return props.surface === 'ai' ? id : `${props.surface}-${id}`
}

const activeClient = ref(clients.value[0].id)

// A surface can switch excludeExpert at runtime in dev; keep the selection valid.
watch(clients, (list) => {
if (!list.some(client => client.id === activeClient.value)) activeClient.value = list[0].id
})

function selectClient (id: string) {
activeClient.value = id
capture('cta-ai-agent-tab', { position: pos(id) })
}
</script>

<template>
<div class="ff-agent-card overflow-hidden rounded-xl border border-indigo-100 bg-white shadow-sm">
<div class="ff-agent-tabs border-b border-indigo-100 bg-indigo-50/60" role="tablist" aria-label="Choose your AI agent">
<button
v-for="client in clients"
:id="`ff-tab-${client.id}`"
:key="client.id"
type="button"
role="tab"
class="ff-agent-tab"
:class="{ 'ff-agent-tab--active': activeClient === client.id }"
:aria-controls="`ff-panel-${client.id}`"
:aria-selected="activeClient === client.id"
@click="selectClient(client.id)"
>
<UIcon v-if="client.icon" :name="client.icon" class="ff-agent-tab__glyph" aria-hidden="true" />
<img v-else-if="client.logo" :src="client.logo" alt="" aria-hidden="true">
<span>{{ client.name }}</span>
</button>
</div>

<div
v-for="client in clients"
v-show="activeClient === client.id"
:id="`ff-panel-${client.id}`"
:key="`panel-${client.id}`"
role="tabpanel"
:aria-labelledby="`ff-tab-${client.id}`"
class="ff-agent-panel"
>
<div class="ff-agent-step">
<p class="ff-agent-step__num">01</p>
<p class="ff-agent-step__title">{{ client.builtIn ? client.step1Title : STEP1.title }}</p>
<p class="ff-agent-step__body">{{ client.builtIn ? client.step1Body : STEP1.description }}</p>
<div v-if="client.builtIn" class="ff-agent-step__cta">
<CtaSignUp variant="primary" :position="`${surface}-tab-expert`" class="w-full" />
</div>
<div v-else class="ff-agent-step__cta">
<FfCommand :command="ENDPOINT" event="cta-copy-mcp-endpoint" :position="pos(client.id)" stacked host-swap />
</div>
</div>

<div class="ff-agent-step">
<p class="ff-agent-step__num">02</p>
<p class="ff-agent-step__title">{{ client.step2Title }}</p>
<p class="ff-agent-step__body">{{ client.step2Body }}</p>
<div class="ff-agent-step__cta">
<a
:href="client.step2Url"
class="ff-btn ff-btn--primary-outlined flex w-full"
:target="client.step2Url.startsWith('http') ? '_blank' : undefined"
:rel="client.step2Url.startsWith('http') ? 'noopener' : undefined"
@click="capture('cta-ai-open-client', { position: pos(client.id) })"
>{{ client.step2Label }}</a>
</div>
</div>

<div class="ff-agent-step">
<p class="ff-agent-step__num">03</p>
<p class="ff-agent-step__title">{{ client.step3Title || STEP3.title }}</p>
<p class="ff-agent-step__body">{{ client.step3Body || STEP3.description }}</p>
<div v-if="signup && !client.noStep3Cta" class="ff-agent-step__cta">
<CtaSignUp variant="primary" :position="`${surface}-connect-step3`" class="w-full" />
</div>
</div>
</div>
</div>
</template>
147 changes: 4 additions & 143 deletions nuxt/pages/ai/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { getAllBlogPosts } from '~/utils/sharedContent'
// What the port changes on purpose:
// - The two hand-rolled DOM scripts (agent tabs, showcase rotation) become
// reactive state. That was ~120 lines of querySelectorAll in the .njk.
// - The copy button becomes <FfCommand>, shared with the changelog and any other
// markdown page, instead of markup plus a copy helper pasted per page.
// - The copy button becomes <FfCommand>, and the whole agent picker becomes
// <AgentSetupTabs>, both shared with the changelog and the docs page instead
// of markup plus a copy helper pasted per page.
// - jsonld.njk's meta.faq becomes useSchemaOrg, which escapes properly. The 11ty
// partial interpolated answers straight into a JSON string.
// - Sign-up buttons become <CtaSignUp>, so the copy is the site's fixed "Try it
Expand Down Expand Up @@ -55,83 +56,8 @@ useSchemaOrg([
// The Cloud address. Self-hosted platforms answer on their own domain, so the block
// takes one (host-swap) rather than telling the reader in prose to edit what they
// have just copied.
const ENDPOINT = 'https://app.flowfuse.com/mcp'

const STEP1 = {
title: 'Copy the FlowFuse connector URL',
description: 'You will paste this into your agent in the next step.',
}
const STEP3 = {
title: 'Sign in and choose what it reaches',
description: 'Which teams the agent may act on, and whether it has editing rights or read access only.',
}

// A client renders `logo` as a brand mark when set, `icon` as our own glyph when not.
// A brand mark keeps its own colours so it stays an <img>; the glyph inherits
// currentColor and turns white with the label when its tab is active.
const CLIENTS = [
{
id: 'expert',
logo: '/images/ai/agents/flowfuse-expert.svg',
name: 'FlowFuse Expert',
builtIn: true,
step1Title: 'Sign in to FlowFuse',
step1Body: 'A new account puts you in a hosted instance with Expert already in the editor. Nothing to add, no connector, no token.',
step2Title: 'Or start with the Device Agent',
step2Body: 'Already running Node-RED on your own hardware? Connect it as a remote instance and Expert works there in the same way.',
step2Label: 'Install the Device Agent',
step2Url: '/docs/device-agent/quickstart/',
step3Title: 'Ask for what you need',
step3Body: 'Build a flow, explain one you inherited, write the Function node, or ask what is running on the floor. Every write waits for you to approve, edit or reject.',
noStep3Cta: true,
},
{
id: 'copilot',
logo: '/images/ai/agents/microsoft-copilot.svg',
name: 'Microsoft Copilot',
step2Title: 'Copilot Studio, Tools, Add a tool',
step2Body: 'Choose Model Context Protocol and paste the URL. Describe what it is for: the orchestrator reads that to decide when to call it.',
step2Label: 'Open Copilot Studio',
step2Url: 'https://copilotstudio.microsoft.com/',
},
{
id: 'chatgpt',
logo: '/images/ai/agents/chatgpt.svg',
name: 'ChatGPT',
step2Title: 'Enable developer mode, then add a connector',
step2Body: 'Custom connectors sit behind developer mode, which a workspace administrator turns on. The connector itself is added from the prompt dashboard.',
step2Label: 'Open ChatGPT',
step2Url: 'https://chatgpt.com/',
},
{
id: 'claude',
logo: '/images/ai/agents/claude.svg',
name: 'Claude',
step2Title: 'Add a custom connector',
step2Body: 'Where custom connectors are available on your plan, add one and paste the URL. On Team and Enterprise an owner adds it once for everyone.',
step2Label: 'Open Claude',
step2Url: 'https://claude.ai/',
},
{
id: 'local',
icon: 'i-lucide-server',
name: 'Local and Custom Agents',
step2Title: "Your MCP client's config",
step2Body: 'Any MCP-capable client works, pointed at your own model, so nothing has to leave your network.',
step2Label: 'See the documentation',
step2Url: '/docs/user/expert/',
},
]

const CODING_NOTE = 'Command-line and editor agents such as Claude Code, Cursor, Visual Studio Code and Gemini CLI connect to the same URL.'

const activeClient = ref(CLIENTS[0].id)

function selectClient (id: string) {
activeClient.value = id
capture('cta-ai-agent-tab', { position: id })
}

const GOVERNANCE = {
title: 'AI Governance you can <span class="text-indigo-600">prove</span>',
subtitle: 'The control plane every AI action runs through, the same one that governs your teams.',
Expand Down Expand Up @@ -283,72 +209,7 @@ onUnmounted(() => {

<!-- CONNECTOR: pick your agent, then three steps specific to it, so the
reader only sees the instructions that apply to them. -->
<div class="mt-12 overflow-hidden rounded-xl border border-indigo-100 bg-white shadow-sm">
<div class="ff-agent-tabs flex flex-nowrap gap-1 overflow-x-auto border-b border-indigo-100 bg-indigo-50/60 p-2" role="tablist" aria-label="Choose your AI agent">
<button
v-for="client in CLIENTS"
:id="`ff-tab-${client.id}`"
:key="client.id"
type="button"
role="tab"
:aria-controls="`ff-panel-${client.id}`"
:aria-selected="activeClient === client.id"
class="ff-agent-tab flex flex-none items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm font-medium transition"
:class="activeClient === client.id ? 'bg-indigo-600 text-white shadow-sm' : 'text-gray-600 hover:bg-white hover:text-gray-900'"
@click="selectClient(client.id)"
>
<UIcon v-if="client.icon" :name="client.icon" class="flex-none size-4" aria-hidden="true" />
<img v-else-if="client.logo" :src="client.logo" alt="" class="h-4 w-auto flex-none" aria-hidden="true">
<span>{{ client.name }}</span>
</button>
</div>

<div
v-for="client in CLIENTS"
v-show="activeClient === client.id"
:id="`ff-panel-${client.id}`"
:key="`panel-${client.id}`"
role="tabpanel"
:aria-labelledby="`ff-tab-${client.id}`"
class="ff-agent-panel grid grid-cols-1 gap-8 p-6 md:grid-cols-3 md:gap-10 md:p-8"
>
<div class="flex flex-col">
<p class="font-mono text-xs font-semibold text-indigo-600 m-0">01</p>
<p class="text-lg font-medium text-gray-900 mt-2 mb-0">{{ client.builtIn ? client.step1Title : STEP1.title }}</p>
<p class="text-gray-600 font-light text-sm mt-2 mb-0">{{ client.builtIn ? client.step1Body : STEP1.description }}</p>
<div v-if="client.builtIn" class="mt-auto pt-5">
<CtaSignUp variant="primary" position="ai-tab-expert" class="w-full" />
</div>
<div v-else class="mt-auto pt-5">
<FfCommand :command="ENDPOINT" event="cta-copy-mcp-endpoint" :position="client.id" stacked host-swap />
</div>
</div>

<div class="flex flex-col">
<p class="font-mono text-xs font-semibold text-indigo-600 m-0">02</p>
<p class="text-lg font-medium text-gray-900 mt-2 mb-0">{{ client.step2Title }}</p>
<p class="text-gray-600 font-light text-sm mt-2 mb-0">{{ client.step2Body }}</p>
<div class="mt-auto pt-5">
<a
:href="client.step2Url"
class="ff-btn ff-btn--primary-outlined flex w-full"
:target="client.step2Url.startsWith('http') ? '_blank' : undefined"
:rel="client.step2Url.startsWith('http') ? 'noopener' : undefined"
@click="capture('cta-ai-open-client', { position: client.id })"
>{{ client.step2Label }}</a>
</div>
</div>

<div class="flex flex-col">
<p class="font-mono text-xs font-semibold text-indigo-600 m-0">03</p>
<p class="text-lg font-medium text-gray-900 mt-2 mb-0">{{ client.step3Title || STEP3.title }}</p>
<p class="text-gray-600 font-light text-sm mt-2 mb-0">{{ client.step3Body || STEP3.description }}</p>
<div v-if="!client.noStep3Cta" class="mt-auto pt-5">
<CtaSignUp variant="primary" position="ai-connect-step3" class="w-full" />
</div>
</div>
</div>
</div>
<AgentSetupTabs class="mt-12" />
<!-- Docs pointer, not a step. Carded in the same tinted, hover-lifting
treatment the platform page uses for its How-it-works blocks, so it
reads as somewhere to go rather than as fine print under step 03.
Expand Down
7 changes: 6 additions & 1 deletion src/changelog/2026/08/connect-your-own-ai-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ You can now connect your own AI agent to FlowFuse. Your company-approved AI can

Add the FlowFuse address to connect, then decide which teams the agent reaches and whether it can make changes.

The address is `https://app.flowfuse.com/mcp`. [Pick your agent on the AI page](/ai/) for the exact steps in [Microsoft Copilot](https://copilotstudio.microsoft.com/), [ChatGPT](https://chatgpt.com/) or [Claude](https://claude.ai/). Any MCP client that supports the HTTP transport works too, including command-line and editor agents and local models. Sign-in uses OAuth where the client supports it, with a token fallback where it does not.
Pick your agent, and follow the three steps:

::agent-setup-tabs{:exclude-expert="true" surface="changelog"}
::

Any MCP client that supports the HTTP transport works, including command-line and editor agents and local models. Sign-in uses OAuth where the client supports it, with a token fallback where it does not.

Your own agent gets the full, growing set of FlowFuse platform automation and flow-building tools to run. That matters most where company policy only permits an approved AI agent, so FlowFuse Expert wasn't an option.

Expand Down
Loading