From 154d693116e0b9fb0367231d6c94e5013a889798 Mon Sep 17 00:00:00 2001 From: arc53-machine <232052973+arc53-machine@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:57:27 +0100 Subject: [PATCH] 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. --- .../src/agents/ApiWriteAllowlist.test.tsx | 16 ++++++++++++++++ frontend/src/agents/ApiWriteAllowlist.tsx | 7 +++++-- .../components/AgentUsesSection.test.tsx | 19 +++++++++++++++++++ .../agents/components/AgentUsesSection.tsx | 7 ++++++- frontend/src/locale/de.json | 5 +++-- frontend/src/locale/en.json | 5 +++-- frontend/src/locale/es.json | 5 +++-- frontend/src/locale/jp.json | 5 +++-- frontend/src/locale/ru.json | 5 +++-- frontend/src/locale/zh-TW.json | 3 ++- frontend/src/locale/zh.json | 3 ++- 11 files changed, 65 insertions(+), 15 deletions(-) diff --git a/frontend/src/agents/ApiWriteAllowlist.test.tsx b/frontend/src/agents/ApiWriteAllowlist.test.tsx index 98465b1f..7caa2608 100644 --- a/frontend/src/agents/ApiWriteAllowlist.test.tsx +++ b/frontend/src/agents/ApiWriteAllowlist.test.tsx @@ -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()); diff --git a/frontend/src/agents/ApiWriteAllowlist.tsx b/frontend/src/agents/ApiWriteAllowlist.tsx index 921d2409..f5c6a87a 100644 --- a/frontend/src/agents/ApiWriteAllowlist.tsx +++ b/frontend/src/agents/ApiWriteAllowlist.tsx @@ -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={{tool.name}} 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 diff --git a/frontend/src/agents/components/AgentUsesSection.test.tsx b/frontend/src/agents/components/AgentUsesSection.test.tsx index afabf5fb..d68c3692 100644 --- a/frontend/src/agents/components/AgentUsesSection.test.tsx +++ b/frontend/src/agents/components/AgentUsesSection.test.tsx @@ -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'] }), diff --git a/frontend/src/agents/components/AgentUsesSection.tsx b/frontend/src/agents/components/AgentUsesSection.tsx index 605e525c..0b5a8868 100644 --- a/frontend/src/agents/components/AgentUsesSection.tsx +++ b/frontend/src/agents/components/AgentUsesSection.tsx @@ -233,7 +233,12 @@ export default function AgentUsesSection({ {writesNote} {/* Only the owner sets the allowlist. */} {ownerReads && onOpenAccessDetails && ( -
+
+ {/* Access details lists these writes once there is a key + (a draft gets one when it is published). */} + {!agent.key && ( +

{t(`${K}.accessDetailsNeedsKey`)}

+ )}