Name each Customize link's tool and say when the allowlist needs a key

Each tool's Customize link is described by the tool's name. The partial
badge reads "Not all via API". When the agent has no API key yet, the
note under What this agent uses says Access Details lists the changes
once it has one.
This commit is contained in:
arc53-machine committed 2026-09-29 18:57:27 +01:00
1 parent 736564198c
commit 154d693116
11 files changed
+65 -15

No files matched your search

@@ -315,6 +315,22 @@ describe('ApiWriteAllowlist', () => {
expect(savedConfig().api_write_allowlist).toEqual(['old:gone_action']);
});
// Every tool has a Customize link; each names its tool to screen readers.
it('ties each Customize link to its tool', async () => {
await render(agent({ tools: ['tg', 'crm'] }), { defaultOpen: true });
for (const [id, name] of [
['tg', 'Telegram'],
['crm', 'CRM API'],
]) {
const link = Array.from(group(id).querySelectorAll('button')).find(
(b) => b.getAttribute('aria-expanded') === 'false',
)!;
const describedBy = link.getAttribute('aria-describedby');
expect(describedBy).toBeTruthy();
expect(document.getElementById(describedBy!)?.textContent).toBe(name);
}
});
it('allows one action under Customize, leaving the tool choice mixed', async () => {
updateAgent.mockResolvedValue({ ok: true });
await render(agent());
+5 -2
View File
@@ -1,5 +1,5 @@
import { ChevronRight } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useEffect, useId, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -56,6 +56,8 @@ function ToolAllowlist({
}) {
const { t } = useTranslation();
const [unfolded, setUnfolded] = useState(false);
// Names the tool for its Customize link, which reads the same in each.
const titleId = useId();
const allowedCount = tool.actions.filter((item) =>
allowed.has(item.entry),
).length;
@@ -73,7 +75,7 @@ function ToolAllowlist({
as="h4"
size="xs"
className="min-w-0 flex-1"
title={tool.name}
title={<span id={titleId}>{tool.name}</span>}
description={t(`${K}.toolCount`, {
allowed: formatCount(allowedCount),
formatted: formatCount(tool.actions.length),
@@ -107,6 +109,7 @@ function ToolAllowlist({
size="inline"
className="self-start"
aria-expanded={unfolded}
aria-describedby={titleId}
onClick={() => setUnfolded(!unfolded)}
>
{unfolded
@@ -505,6 +505,25 @@ describe('AgentUsesSection', () => {
expect(openDetails).toHaveBeenCalledTimes(1);
});
// Access details lists the writes only once the agent has an API key, so
// without one the note says to create it there first.
it('says the allowlist needs an API key when the agent has none', async () => {
const blocked = [
item({ id: 't1', name: 'Blocked', owner_credential_writes: ['x'] }),
];
await render(agentWith(blocked), 'me', vi.fn());
await open();
const alert = () => container.querySelector('[data-slot="alert"]');
expect(alert()?.textContent).toContain(`${K}.accessDetailsNeedsKey`);
act(() => root.unmount());
root = createRoot(container);
await render(agentWith(blocked, { key: 'abcd...wxyz' }), 'me', vi.fn());
await open();
expect(alert()?.textContent).not.toContain(`${K}.accessDetailsNeedsKey`);
expect(alert()?.textContent).toContain(`${K}.openAccessDetails`);
});
it('offers no Access details button to an editor or without a way there', async () => {
const blocked = [
item({ id: 't1', name: 'Blocked', owner_credential_writes: ['x'] }),
@@ -233,7 +233,12 @@ export default function AgentUsesSection({
<AlertDescription>{writesNote}</AlertDescription>
{/* Only the owner sets the allowlist. */}
{ownerReads && onOpenAccessDetails && (
<div className="mt-2">
<div className="mt-2 flex flex-col items-start gap-1.5">
{/* Access details lists these writes once there is a key
(a draft gets one when it is published). */}
{!agent.key && (
<p className="text-xs">{t(`${K}.accessDetailsNeedsKey`)}</p>
)}
<Button
type="button"
size="sm"
+3 -2
View File
@@ -789,12 +789,13 @@
"personCredentials": "Gespeicherte Zugangsdaten von {{person}}",
"otherCredentials": "Gespeicherte Zugangsdaten einer anderen Person"
},
"writesSomeOff": "Teils nicht über API",
"writesSomeOff": "Nicht alles über API",
"adminOff": "Änderungen von einem Admin deaktiviert",
"writesNoteApi": "„Erlauben“ bei einem Connector gilt für Chats. Mit „Nicht über API“ markierte Änderungen bleiben für API- und Widget-Nutzer aus, bis du sie unter Zugangsdaten erlaubst.",
"writesNoteApiEditor": "„Erlauben“ bei einem Connector gilt für Chats. Mit „Nicht über API“ markierte Änderungen bleiben für API- und Widget-Nutzer aus, bis der Eigentümer sie unter Zugangsdaten erlaubt.",
"writesNoteMemberTail": "Bei Werkzeugen, bei denen jede Person ihr eigenes Konto nutzt, ändern Link-Nutzer mit ihrem eigenen Konto.",
"openAccessDetails": "Zugangsdaten öffnen"
"openAccessDetails": "Zugangsdaten öffnen",
"accessDetailsNeedsKey": "Die Zugangsdaten listen diese Änderungen auf, sobald der Agent einen API-Schlüssel hat."
}
},
"memberCount_one": "{{formatted}} Mitglied",
+3 -2
View File
@@ -795,12 +795,13 @@
"personCredentials": "{{person}}'s saved credentials",
"otherCredentials": "Someone else's saved credentials"
},
"writesSomeOff": "Some not via API",
"writesSomeOff": "Not all via API",
"adminOff": "Changes off by an admin",
"writesNoteApi": "“Allow” on a connector applies to chats. Changes marked “Not via API” stay off for API and widget users until you allow them in Access Details.",
"writesNoteApiEditor": "“Allow” on a connector applies to chats. Changes marked “Not via API” stay off for API and widget users until the owner allows them in Access Details.",
"writesNoteMemberTail": "On tools where each person uses their own account, public-link users make changes with their own account.",
"openAccessDetails": "Open Access Details"
"openAccessDetails": "Open Access Details",
"accessDetailsNeedsKey": "Access Details lists these changes once the agent has an API key."
}
},
"memberCount_one": "{{formatted}} member",
+3 -2
View File
@@ -789,12 +789,13 @@
"personCredentials": "Credenciales guardadas de {{person}}",
"otherCredentials": "Credenciales guardadas de otra persona"
},
"writesSomeOff": "Algunos no por API",
"writesSomeOff": "No todo por API",
"adminOff": "Cambios desactivados por un administrador",
"writesNoteApi": "«Permitir» en un conector se aplica a los chats. Los cambios marcados «No por API» siguen desactivados para los usuarios de la API y el widget hasta que los permitas en Access Details.",
"writesNoteApiEditor": "«Permitir» en un conector se aplica a los chats. Los cambios marcados «No por API» siguen desactivados para los usuarios de la API y el widget hasta que el propietario los permita en Access Details.",
"writesNoteMemberTail": "En las herramientas donde cada persona usa su propia cuenta, los usuarios del enlace público hacen cambios con su propia cuenta.",
"openAccessDetails": "Abrir Access Details"
"openAccessDetails": "Abrir Access Details",
"accessDetailsNeedsKey": "Access Details muestra estos cambios cuando el agente tiene una clave de API."
}
},
"memberCount_one": "{{formatted}} miembro",
+3 -2
View File
@@ -788,12 +788,13 @@
"personCredentials": "{{person}} の保存済み認証情報",
"otherCredentials": "他の人の保存済み認証情報"
},
"writesSomeOff": "一部 API 経由は不可",
"writesSomeOff": "一部は API 経由不可",
"adminOff": "管理者が変更をオフにしています",
"writesNoteApi": "コネクタの「許可」はチャットに適用されます。「API 経由は不可」と示された変更は、Access Details で許可するまで、API とウィジェットの利用者には行えません。",
"writesNoteApiEditor": "コネクタの「許可」はチャットに適用されます。「API 経由は不可」と示された変更は、オーナーが Access Details で許可するまで、API とウィジェットの利用者には行えません。",
"writesNoteMemberTail": "各自のアカウントを使うツールでは、公開リンクの利用者は自分のアカウントで変更します。",
"openAccessDetails": "Access Details を開く"
"openAccessDetails": "Access Details を開く",
"accessDetailsNeedsKey": "エージェントに API キーがあると、Access Details にこれらの変更が表示されます。"
}
},
"memberCount_one": "{{formatted}}人のメンバー",
+3 -2
View File
@@ -833,12 +833,13 @@
"personCredentials": "Сохранённые учётные данные {{person}}",
"otherCredentials": "Сохранённые учётные данные другого пользователя"
},
"writesSomeOff": "Частично не через API",
"writesSomeOff": "Не всё через API",
"adminOff": "Изменения выключены администратором",
"writesNoteApi": "«Разрешать» у коннектора действует в чатах. Изменения с отметкой «Не через API» остаются выключены для пользователей API и виджета, пока вы не разрешите их в Access Details.",
"writesNoteApiEditor": "«Разрешать» у коннектора действует в чатах. Изменения с отметкой «Не через API» остаются выключены для пользователей API и виджета, пока владелец не разрешит их в Access Details.",
"writesNoteMemberTail": "В инструментах, где у каждого свой аккаунт, пользователи публичной ссылки вносят изменения через свой аккаунт.",
"openAccessDetails": "Открыть Access Details"
"openAccessDetails": "Открыть Access Details",
"accessDetailsNeedsKey": "Access Details покажет эти изменения, когда у агента появится API-ключ."
}
},
"memberCount_one": "{{formatted}} участник",
+2 -1
View File
@@ -793,7 +793,8 @@
"writesNoteApi": "連接器上的「允許」適用於聊天。標示為「無法透過 API」的變更,在你於 Access Details 中允許之前,API 和小工具的使用者無法進行。",
"writesNoteApiEditor": "連接器上的「允許」適用於聊天。標示為「無法透過 API」的變更,在擁有者於 Access Details 中允許之前,API 和小工具的使用者無法進行。",
"writesNoteMemberTail": "在每個人使用自己帳號的工具上,公開連結的使用者以自己的帳號進行變更。",
"openAccessDetails": "開啟 Access Details"
"openAccessDetails": "開啟 Access Details",
"accessDetailsNeedsKey": "代理有 API 金鑰後,Access Details 中會列出這些變更。"
}
},
"memberCount_one": "{{formatted}} 位成員",
+2 -1
View File
@@ -793,7 +793,8 @@
"writesNoteApi": "连接器上的“允许”适用于聊天。标记为“不可通过 API”的更改,在你于 Access Details 中允许之前,API 和小组件的用户无法进行。",
"writesNoteApiEditor": "连接器上的“允许”适用于聊天。标记为“不可通过 API”的更改,在所有者于 Access Details 中允许之前,API 和小组件的用户无法进行。",
"writesNoteMemberTail": "在每个人使用自己账号的工具上,公开链接的用户用自己的账号进行更改。",
"openAccessDetails": "打开 Access Details"
"openAccessDetails": "打开 Access Details",
"accessDetailsNeedsKey": "智能体有 API 密钥后,Access Details 中会列出这些更改。"
}
},
"memberCount_one": "{{formatted}} 名成员",