Add fx as a provider you can opt in to from the catalog (#9160)

* feat(providers): add fx as an opt-in bundled ACP provider

fx ships a native ACP server, so it follows the catalog opt-in shape of the
other bundled ACP provider and needs no vendor protocol code.

fx files its provider selector first under ACP's "model" category, then the
model, Fast, and Ultrafast. The ACP adapter took the first option in that
category as the model, so the model picker listed fx's providers. It now
prefers the option with id "model" and keeps the rest as settings.

* fix(providers): keep the chosen model across ACP provider changes

fx's provider option also selects that provider's saved model. Paseo applied
the model before the option, so a new agent lost the requested model. It also
kept the old model as the agent's preference after a switch, and reopening the
agent sent that model to the new connection.

Apply opening configuration in the order the agent lists its options, and store
the model an option moved the session to.
This commit is contained in:
Mohamed Boudra
2026-11-09 20:58:01 +07:00
committed by GitHub
parent e260f93cc7
commit 0fa7f840b4
27 changed files with 499 additions and 48 deletions
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="166.241 0 155.861 155.75" fill="currentColor"><path d="M237.892 0C243.178 8.73262e-05 249.383 1.41857 253.025 3.07422L255.093 4.01367L250.076 18.6299L247.677 17.7461C244.901 16.7234 241.938 15.7998 238.491 15.7998C234.984 15.7998 232.787 16.5601 231.08 18.3213C229.231 20.2297 227.629 23.6395 226.229 29.7578L226.227 29.7617L226.228 29.7627L225.136 34.8506H241.669L260.432 34.9502L260.694 34.9512L260.842 35.1689L278.854 61.6328L296.736 34.9502H320.866L291.676 76.7402L322.102 119.75H299.331L299.182 119.546L241.144 40.4775L239.35 49.4004H222.069L205.688 127.208C203.927 135.709 201.185 142.838 196.777 147.866C192.27 153.008 186.201 155.75 178.342 155.75C174.181 155.75 170.746 155.109 167.909 154.107L166.241 153.519V137.182L166.899 137.4L169.531 138.278C172.182 139.162 174.414 139.8 177.142 139.8C178.528 139.8 179.703 139.526 180.732 138.98C181.764 138.434 182.677 137.6 183.52 136.442C185.304 133.989 186.716 130.135 187.897 124.672L203.76 49.4004H189.874L191.759 39.4375L192.039 39.3457L206.818 34.4668L208.154 28.6416C210.516 18.2065 213.772 10.9381 218.708 6.31836C223.737 1.61217 230.127 3.08752e-05 237.892 0ZM273.987 99.084L260.073 120.25H234.541L261.003 82.0215L273.987 99.084Z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+7
View File
@@ -0,0 +1,7 @@
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { createFxProvider } from "./server/provider.js";
export default function contribute(server: PluginServerContext) {
server.registerProvider(createFxProvider());
return () => {};
}
+1
View File
@@ -0,0 +1 @@
{ "id": "fx-provider", "requirements": { "paseo": ">=0.11.0" } }
@@ -0,0 +1,47 @@
import type { AcpToolCallSnapshot } from "@getpaseo/plugin/server/acp";
import { describe, expect, it } from "vitest";
import { fileToolTransformer } from "./file-tools.js";
const context = { sessionId: "session-1" };
// Shapes captured from `fx acp` 0.0.16.
function fxToolCall(name: string, input: AcpToolCallSnapshot["input"]): AcpToolCallSnapshot {
return {
id: "call_1",
name,
title: "Writing",
kind: "edit",
status: "pending",
input,
output: null,
locations: [],
};
}
function transform(toolCall: AcpToolCallSnapshot): AcpToolCallSnapshot {
return fileToolTransformer.toolCall!(toolCall, context);
}
describe("fx file tool calls", () => {
it("presents write_file as a write to its path", () => {
expect(
transform(fxToolCall("write_file", { path: "hello.txt", content: "hi\n" })).input,
).toEqual({ filePath: "hello.txt", newString: "hi\n" });
});
it("presents edit_file as a replacement in its path", () => {
expect(
transform(fxToolCall("edit_file", { path: "a.ts", old_string: "a", new_string: "b" })).input,
).toEqual({ filePath: "a.ts", oldString: "a", newString: "b" });
});
it("keeps the presentation when a later update reaches the same call", () => {
const presented = transform(fxToolCall("write_file", { path: "hello.txt", content: "hi\n" }));
expect(transform({ ...presented, status: "completed" }).input).toEqual(presented.input);
});
it("leaves other tools alone", () => {
const shell = fxToolCall("shell", { path: "/tmp", command: "ls" });
expect(transform(shell)).toEqual(shell);
});
});
+26
View File
@@ -0,0 +1,26 @@
import type { AcpToolCallSnapshot, AcpTransformer } from "@getpaseo/plugin/server/acp";
type ToolInput = AcpToolCallSnapshot["input"];
// fx names its file arguments for the model (`path`, `old_string`), so its file tools need the
// argument names Paseo's edit rows read. The adapter reapplies this to each update of a call.
export const fileToolTransformer: AcpTransformer = {
toolCall(toolCall) {
const input = toEditRowInput(toolCall.name, toolCall.input);
return input ? { ...toolCall, input } : toolCall;
},
};
function toEditRowInput(tool: string | undefined, input: ToolInput): ToolInput | null {
if (typeof input !== "object" || input === null || Array.isArray(input)) return null;
if (typeof input.path !== "string") return null;
if (tool === "write_file") return { filePath: input.path, newString: input.content ?? null };
if (tool === "edit_file") {
return {
filePath: input.path,
oldString: input.old_string ?? null,
newString: input.new_string ?? null,
};
}
return null;
}
+17
View File
@@ -0,0 +1,17 @@
import { runAcpProvider } from "@getpaseo/plugin/server/acp";
import type { ProviderRegistration } from "@getpaseo/plugin/server/provider";
import { fileToolTransformer } from "./file-tools.js";
export function createFxProvider(): ProviderRegistration {
return {
requiresConfiguration: true,
...runAcpProvider({
id: "fx",
label: "fx",
description: "Vercel Labs' native coding agent through `fx acp`",
icon: "icon.svg",
command: ["fx", "acp"],
transformers: [fileToolTransformer],
}),
};
}