diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 4dccb3e4c3..59842da9f5 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2819,7 +2819,8 @@ "generic": "Could not add the key. Try again." }, "agentsUnavailable": "couldn't read your agents just now · reload to try again" - } + }, + "chooseKey": "Choose a key" }, "agentManage": { "runtime": { diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 14063b8d34..57fc27502a 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2820,7 +2820,8 @@ "generic": "无法添加密钥,请重试。" }, "agentsUnavailable": "暂时无法读取你的智能体,请刷新后重试" - } + }, + "chooseKey": "选择密钥" }, "agentManage": { "runtime": { diff --git a/frontend/src/v2/__tests__/ConnectorsPage.operatorKey.test.tsx b/frontend/src/v2/__tests__/ConnectorsPage.operatorKey.test.tsx index 0c171eca39..7ef4bb4ce2 100644 --- a/frontend/src/v2/__tests__/ConnectorsPage.operatorKey.test.tsx +++ b/frontend/src/v2/__tests__/ConnectorsPage.operatorKey.test.tsx @@ -406,12 +406,28 @@ describe('Connectors page: granting an operator-key connection', () => { it('Grant again on a key grant whose key is gone starts with nobody, never another agent\'s key', async () => { const dead = keyGrant({ grantId: 'grant_gone', installationId: 'conn-deleted', target: { kind: 'pod', id: 'p2' }, effectiveAudience: [REED], ...revoked }); const form = await openGrantAgain([scoutKey, reedKey], dead); - expect(within(form).queryAllByRole('checkbox', { checked: true })).toHaveLength(0); + // The picker says no key is chosen, rather than showing Scout's as picked. + expect(within(form).getByLabelText('connection')).toHaveValue(''); + expect(within(form).getByRole('option', { name: 'Choose a key' })).toBeDisabled(); + expect(within(form).queryAllByRole('checkbox')).toHaveLength(0); expect(within(form).getByRole('button', { name: 'Grant' })).toBeDisabled(); fireEvent.click(within(form).getByRole('button', { name: 'Grant' })); expect(grantPost()).toBeUndefined(); }); + it('a removed key with one key left still offers the picker, and choosing it enables Grant', async () => { + const dead = keyGrant({ grantId: 'grant_gone', installationId: 'conn-deleted', effectiveAudience: [REED], ...revoked }); + const form = await openGrantAgain([scoutKey], dead); + const picker = within(form).getByLabelText('connection'); + expect(picker).toHaveValue(''); + expect(within(form).getByRole('button', { name: 'Grant' })).toBeDisabled(); + fireEvent.change(picker, { target: { value: 'conn-scout' } }); + expect(within(form).getByRole('checkbox', { name: 'Scout' })).toBeChecked(); + fireEvent.click(within(form).getByRole('button', { name: 'Grant' })); + await waitFor(() => expect(grantPost()).toBeDefined()); + expect(grantPost()[1]).toEqual(expect.objectContaining({ connectionId: 'conn-scout', target: { kind: 'pod', id: 'p1' }, audience: [SCOUT] })); + }); + it('in a pod with several agents, the first Grant reaches only the key\'s agent', async () => { const form = await openKeyDraft([scoutKey, reedKey]); // Launch pod holds Scout and Nova; only Scout's checkbox is offered. diff --git a/frontend/src/v2/connectors/ToolPanels.tsx b/frontend/src/v2/connectors/ToolPanels.tsx index a11b9ff44f..b4187aa716 100644 --- a/frontend/src/v2/connectors/ToolPanels.tsx +++ b/frontend/src/v2/connectors/ToolPanels.tsx @@ -530,6 +530,9 @@ export const useToolPanels = (m: ToolConnectorsModel) => { const offeredSeats = draftBoundAgent ? podSeats.filter((seat) => seat.userId === draftBoundAgent) : podSeats; const boundAbsent = Boolean(draftBoundAgent) && offeredSeats.length === 0; const boundName = draftBoundAgent ? (agentNameById(draftBoundAgent) || memberName(draftBoundAgent)) : null; + // A key grant whose key was removed opens with no key chosen (openDraft): + // the picker shows even for one remaining key, and the seats wait for a key. + const keyPending = draftEntry.credentialMode === 'operator_key' && !draft.connectionId; const tools = draftTools(draftEntry, draft.writeMode); const irreversible = irreversibleTools(draftEntry, tools); const asks = draft.writeMode === 'read' @@ -552,7 +555,7 @@ export const useToolPanels = (m: ToolConnectorsModel) => { )} - {draftEntry.connections.length > 1 && ( + {(draftEntry.connections.length > 1 || (keyPending && draftEntry.connections.length > 0)) && (