show ACP login methods for all desktop agents

This commit is contained in:
tianma-if committed 2026-10-01 17:21:04 +08:00
1 parent 46dfbeb98d
commit 7a4dce8f86
8 files changed
+49 -15

No files matched your search

+3 -3
View File
@@ -789,7 +789,7 @@ export function createAcpHostRuntime(options = {}) {
const prepared = id === "piAgent" ? withPiPath(command, resolutionDeps(commandDeps))
: id === "antigravity" ? withAntigravityMacProxy(command, commandDeps) : command;
connected = await withHandshakeTimeout((signal) => connect(prepared, `install-${id}`, () => {}, signal), 90_000);
return { ...adapterShell(id), state: "available", promptCapabilities: connected.promptCapabilities };
return { ...adapterShell(id), state: "available", promptCapabilities: connected.promptCapabilities, authMethods: connected.authMethods };
} catch (error) {
return { ...adapterShell(id), ...failureFields(classifyAcpFailure(error)), ...(isAuthRequiredError(error) ? { authMethods: error.authMethods ?? [] } : {}) };
} finally {
@@ -860,7 +860,7 @@ export function createAcpHostRuntime(options = {}) {
...adapterShell(id),
state: "available",
promptCapabilities: connected.promptCapabilities,
...((id === "workbuddyCn" || id === "workbuddyIntl") ? { authMethods: connected.authMethods } : {}),
authMethods: connected.authMethods,
...(resolved.version ? { version: resolved.version, managed: true } : {}),
};
latestStatus.set(id, adapter);
@@ -878,7 +878,7 @@ export function createAcpHostRuntime(options = {}) {
let connected;
try {
connected = await withHandshakeTimeout((signal) => connect(resolved.command, `auth-${id}`, () => {}, signal, input.methodId), 5 * 60_000);
const adapter = { ...adapterShell(id), state: "available", promptCapabilities: connected.promptCapabilities, ...((id === "workbuddyCn" || id === "workbuddyIntl") ? { authMethods: connected.authMethods } : {}), ...(resolved.version ? { version: resolved.version, managed: true } : {}) };
const adapter = { ...adapterShell(id), state: "available", promptCapabilities: connected.promptCapabilities, authMethods: connected.authMethods, ...(resolved.version ? { version: resolved.version, managed: true } : {}) };
latestStatus.set(id, adapter);
return adapter;
} catch (error) {
+23 -2
View File
@@ -68,7 +68,7 @@ const collector = () => {
};
};
const fakeAgentSource = ({ reportPath, secretPath, allowImage, allowEmbedded, hold, requireAuth, promptRefusal }) => `#!/usr/bin/env bun
const fakeAgentSource = ({ reportPath, secretPath, allowImage, allowEmbedded, hold, requireAuth, advertiseAuth, promptRefusal }) => `#!/usr/bin/env bun
import * as acp from ${JSON.stringify(sdkHref)};
import { writeFileSync } from "node:fs";
@@ -78,6 +78,7 @@ const allowImage = ${allowImage ? "true" : "false"};
const allowEmbedded = ${allowEmbedded ? "true" : "false"};
const hold = ${hold ? "true" : "false"};
const requireAuth = ${requireAuth ? "true" : "false"};
const advertiseAuth = ${advertiseAuth ? "true" : "false"};
const promptRefusal = ${promptRefusal ? JSON.stringify(promptRefusal) : "null"};
let authenticated = false;
const report = { initialize: null, newSession: null, permission: null, readError: null, readResult: null, prompt: null, authMethod: null };
@@ -119,7 +120,7 @@ acp.agent({ name: "edgeever-fake-agent" })
return {
protocolVersion: ctx.params.protocolVersion,
agentCapabilities: { promptCapabilities: { image: allowImage, embeddedContext: allowEmbedded } },
authMethods: requireAuth ? [{ id: "browser", name: "Browser" }] : [],
authMethods: requireAuth || advertiseAuth ? [{ id: "browser", name: "Browser" }] : [],
};
})
.onRequest("authenticate", (ctx) => {
@@ -708,6 +709,26 @@ describe("ACP stdio session", () => {
}
}, 15_000);
sessionTest("keeps ACP login methods visible when another agent can already create a session", async () => {
const directory = await mkdtemp(path.join(tmpdir(), "edgeever-acp-optional-auth-"));
try {
const scriptPath = await writeFakeAgent(directory, {
reportPath: path.join(directory, "report.json"),
secretPath: path.join(directory, "secret.txt"),
allowImage: false,
allowEmbedded: false,
hold: false,
advertiseAuth: true,
});
const runtime = createAcpHostRuntime();
const probed = await runtime.probeAdapter({ id: "antigravity", path: scriptPath });
expect(probed.state).toBe("available");
expect(probed.authMethods).toEqual([{ id: "browser", name: "Browser" }]);
} finally {
await rm(directory, { recursive: true, force: true });
}
}, 15_000);
sessionTest("reports a prompt-time authentication refusal instead of an empty reply", async () => {
const directory = await mkdtemp(path.join(tmpdir(), "edgeever-acp-refusal-"));
try {
@@ -126,15 +126,15 @@ const DesktopAcpAgentCardBody = ({ bridge }: { bridge: boolean }) => {
installError,
state: shown?.state,
});
const hasDetails = Boolean(
shown?.state === "not_installed" ||
shown?.state === "needs_login" ||
(isWorkBuddy && shown?.state === "available" && Boolean(shown.authMethods?.length)) ||
shown?.updateError
);
const authMethods = shown?.authMethods?.filter((method) => (
adapterId === "workbuddyCn" ? method.id !== "external" : adapterId === "workbuddyIntl" ? method.id !== "internal" : true
)) ?? [];
const hasDetails = Boolean(
shown?.state === "not_installed" ||
shown?.state === "needs_login" ||
(shown?.state === "available" && authMethods.length > 0) ||
shown?.updateError
);
const selectAdapter = (id: DesktopAcpAdapterId) => {
setAdapterId(id);
@@ -369,7 +369,10 @@ const DesktopAcpAgentCardBody = ({ bridge }: { bridge: boolean }) => {
{(shown?.state === "needs_login" || shown?.state === "available") && isWorkBuddy ? (
<p className="text-xs leading-relaxed text-slate-600">{t(adapterId === "workbuddyCn" ? "aiAssistant.agentSource.workbuddyCnLoginHint" : "aiAssistant.agentSource.workbuddyIntlLoginHint")}</p>
) : null}
{(shown?.state === "needs_login" || (isWorkBuddy && shown?.state === "available")) && authMethods.length ? (
{shown?.state === "available" && !isWorkBuddy && authMethods.length ? (
<p className="text-xs leading-relaxed text-slate-600">{t("aiAssistant.agentSource.authAvailableHint")}</p>
) : null}
{(shown?.state === "needs_login" || shown?.state === "available") && authMethods.length ? (
<div className="flex flex-wrap gap-2 pt-0.5">
{authMethods.map((method) => (
<Button key={method.id} type="button" variant="outline" size="sm" className="h-8 bg-card text-xs font-normal" disabled={authenticating} onClick={() => void authenticate(method.id)}>
+6
View File
@@ -15,3 +15,9 @@ test("a newly installed managed connector takes priority over an older probe", (
const previous = { id: "antigravity", label: "Antigravity", state: "failed", version: "1.2.1", managed: true };
expect(displayedDesktopAcpAdapter({ id: "antigravity", path: "", listed: [current], probed: previous })).toBe(current);
});
test("a prompt-time login failure takes priority over an earlier successful session probe", () => {
const current = { id: "grokBuild", label: "Grok Build", state: "needs_login", authMethods: [{ id: "browser", name: "Browser" }] };
const earlierProbe = { id: "grokBuild", label: "Grok Build", state: "available" };
expect(displayedDesktopAcpAdapter({ id: "grokBuild", path: "", listed: [current], probed: earlierProbe })).toBe(current);
});
+1
View File
@@ -33,6 +33,7 @@ export const displayedDesktopAcpAdapter = ({
const checked = probed?.id === id ? probed : undefined;
if (id === "antigravity" && path.trim()) return checked;
if (current?.state === "installing" || (current?.managed && (!checked?.managed || current.version !== checked.version))) return current;
if (current?.state === "needs_login" && checked?.state === "available") return current;
return checked ?? current;
};
+2 -1
View File
@@ -1439,11 +1439,12 @@ export const enUS = {
updateFailed: "Automatic update failed. The previous version remains in use.",
authenticating: "Signing in…",
authenticateWith: "Sign in with {{method}}",
authAvailableHint: "This agent offers ACP sign-in. If sending a message requires login, complete it here.",
loginUnavailable: "This ACP connector requires authentication but provides no ACP sign-in method. Authenticate in the agent, then check the connection again.",
autoUpdated: "Auto-updated",
probe: "Check connection",
probing: "Checking…",
probeHint: "Opens a temporary session and closes it. Your notes are not sent.",
probeHint: "Opens and closes a temporary session without sending your notes. This check does not verify model sign-in.",
notProbed: "Not checked yet",
invalidPath: "Enter an absolute path to an executable file.",
states: {
+2 -1
View File
@@ -1439,11 +1439,12 @@ export const ja = {
updateFailed: "自動更新に失敗しました。以前のバージョンを継続して使用します。",
authenticating: "ログイン中…",
authenticateWith: "{{method}} でログイン",
authAvailableHint: "この Agent は ACP ログインに対応しています。メッセージ送信時にログインを求められた場合は、ここで認証してください。",
loginUnavailable: "この ACP 接続コンポーネントは認証が必要ですが、ACP のログイン方法を提供していません。エージェント側で認証してから再確認してください。",
autoUpdated: "自動更新",
probe: "接続を確認",
probing: "確認中…",
probeHint: "一時的なセッションを開いてすぐ閉じます。ノートは送信しません。",
probeHint: "一時的なセッションを開いてすぐ閉じます。ノートは送信されず、モデルへのログイン状態も確認しません。",
notProbed: "未確認",
invalidPath: "実行ファイルの絶対パスを入力してください。",
states: {
+2 -1
View File
@@ -1439,11 +1439,12 @@ export const zhCN = {
updateFailed: "自动更新失败,正在继续使用原版本。",
authenticating: "正在登录…",
authenticateWith: "使用 {{method}} 登录",
authAvailableHint: "此 Agent 提供 ACP 登录方式。若发消息时提示需要登录,可在这里完成认证。",
loginUnavailable: "这个 ACP 连接组件要求认证,但没有提供 ACP 登录入口。请在对应代理中完成认证后重试连接。",
autoUpdated: "自动更新",
probe: "检查连接",
probing: "正在检查…",
probeHint: "会临时建立会话并立即关闭,不会发送你的笔记。",
probeHint: "会临时建立会话并立即关闭,不会发送你的笔记;此检查不验证模型请求是否已登录。",
notProbed: "尚未检查",
invalidPath: "请填写可执行文件的绝对路径。",
states: {