Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2287,6 +2287,8 @@
"gateSwitch": "Relay {{pod}}",
"gatesTitle": "Pods that reach this channel",
"hostedMcpManualRevoke": "Your previous {{provider}} account may still have Commonly connected. Switch to it on {{provider}}, then remove Commonly there.",
"hostedMcpManualRevoke_linear": "Your previous Linear account may still have an MCP grant connected. Switch to it in Linear, then revoke the MCP grant under Linear's connected apps.",
"hostedMcpManualRevoke_sentry": "Your previous Sentry account may still have Sentry's MCP server app connected. Switch to it in Sentry, then revoke that app there. Revoking it ends every client that server holds for the account, not only Commonly.",
"hostedMcpManualRevoke_supabase": "Commonly may still be authorized in the Supabase organization you connected before. Removing Commonly under Authorized apps disconnects everyone who connected that organization.",
"hostedMcpReviewAuthorization": "Open {{provider}}'s connected apps",
"hostedMcpReviewAuthorization_supabase": "Open Supabase Authorized apps",
Expand Down Expand Up @@ -2472,7 +2474,7 @@
"confirmRemoveConnection": "Yes, remove",
"removeConnectionError": "Could not remove the connection.",
"reviewProviderAccess": "Review access at {{label}}",
"removedReviewProvider": "Connection removed. Review Commonly access at {{label}}.",
"removedReviewProvider": "Connection removed. Review its access at {{label}}.",
"openProviderSettings": "Open provider settings",
"noConnection": "install the GitHub App first · an admin does this once",
"noSeats": "no agent in this pod yet",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2278,6 +2278,8 @@
"gateSwitch": "转发 {{pod}}",
"gatesTitle": "接入此频道的 Pod",
"hostedMcpManualRevoke": "你之前的 {{provider}} 账号可能仍保留对 Commonly 的授权。请在 {{provider}} 切换到那个账号,再在那里移除 Commonly。",
"hostedMcpManualRevoke_linear": "你之前的 Linear 账号可能仍连着一个 MCP 授权。请在 Linear 切换到那个账号,再在其已连接应用中撤销该 MCP 授权。",
"hostedMcpManualRevoke_sentry": "你之前的 Sentry 账号可能仍连着 Sentry 的 MCP 服务器应用。请在 Sentry 切换到那个账号,再在那里撤销该应用。撤销后,此账号下该服务器的所有客户端都会断开,不只是 Commonly。",
"hostedMcpManualRevoke_supabase": "Commonly 可能仍在你之前连接的 Supabase 组织中获得授权。在“已授权应用”中移除 Commonly,会断开所有连接该组织的成员。",
"hostedMcpReviewAuthorization": "打开 {{provider}} 的已连接应用",
"hostedMcpReviewAuthorization_supabase": "打开 Supabase 已授权应用",
Expand Down Expand Up @@ -2463,7 +2465,7 @@
"confirmRemoveConnection": "确认移除",
"removeConnectionError": "无法移除连接。",
"reviewProviderAccess": "查看 {{label}} 的授权",
"removedReviewProvider": "连接已移除。请检查 {{label}} 中 Commonly 的授权。",
"removedReviewProvider": "连接已移除。请检查 {{label}} 中的授权。",
"openProviderSettings": "打开服务提供方设置",
"noConnection": "请先安装 GitHub App · 管理员只需安装一次",
"noSeats": "此 Pod 中还没有智能体",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/v2/__tests__/V2ConnectorTools.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,7 @@ test('a hidden hosted entry has no offering while its existing connection can be
fireEvent.click(screen.getByRole('button', { name: 'Remove connection' }));
fireEvent.click(screen.getByRole('button', { name: 'Yes, remove' }));
await waitFor(() => expect(axios.delete).toHaveBeenCalledWith('/api/integrations/conn-hidden', expect.anything()));
expect(await screen.findByText(/Connection removed. Review Commonly access at Airtable/)).toBeInTheDocument();
expect(await screen.findByText(/Connection removed. Review its access at Airtable/)).toBeInTheDocument();
});

test('a hosted connect start error uses fixed copy instead of response text', async () => {
Expand Down
50 changes: 48 additions & 2 deletions frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ describe('V2ConnectorsPage', () => {
expect(reviewLink).toHaveAttribute('rel', 'noopener noreferrer');
const notice = screen.getByRole('status');
expect(notice).toHaveTextContent(
'Your previous Linear account may still have Commonly connected. Switch to it on Linear, then remove Commonly there.',
'Your previous Linear account may still have an MCP grant connected. Switch to it in Linear, then revoke the MCP grant under Linear\'s connected apps.',
);
expect(notice.closest('.v2-connector-row')).toHaveTextContent('Linear');
expect(window.location.search).toBe('');
Expand All @@ -770,7 +770,7 @@ describe('V2ConnectorsPage', () => {
expect(reviewLink).toHaveAttribute('href', revokePage);
const notice = screen.getByRole('status');
expect(notice).toHaveTextContent(
'你之前的 Linear 账号可能仍保留对 Commonly 的授权。请在 Linear 切换到那个账号,再在那里移除 Commonly。',
'你之前的 Linear 账号可能仍连着一个 MCP 授权。请在 Linear 切换到那个账号,再在其已连接应用中撤销该 MCP 授权。',
);
expect(notice.closest('.v2-connector-row')).toHaveTextContent('Linear');
expect(window.location.search).toBe('');
Expand Down Expand Up @@ -803,6 +803,52 @@ describe('V2ConnectorsPage', () => {
expect(screen.getByRole('status')).not.toHaveTextContent('remove Commonly there');
});

it('warns that revoking the Sentry MCP app ends every client that server holds, and does not name Commonly', async () => {
const revokePage = 'https://sentry.io/settings/account/api/authorizations/';
window.history.replaceState(
{},
'',
`/v2/connectors?hostedMcp=connected&entryId=sentry&revokeAt=${encodeURIComponent(revokePage)}`,
);
mockGets([{
_id: 'i-sentry', type: 'hosted-mcp', status: 'connected',
config: { entryId: 'sentry', revokePage },
}], [{
...hostedLinearCatalogEntry('i-sentry'),
installableId: 'sentry', label: 'Sentry', entryId: 'sentry',
}]);
renderPage();

const reviewLink = await screen.findByRole('link', { name: "Open Sentry's connected apps" });
expect(reviewLink).toHaveAttribute('href', revokePage);
expect(screen.getByRole('status')).toHaveTextContent(
"Revoking it ends every client that server holds for the account, not only Commonly.",
);
expect(screen.getByRole('status')).not.toHaveTextContent('remove Commonly there');
});

it('keeps the generic manual-revoke copy for an entry without its own', async () => {
const revokePage = 'https://airtable.com/account/security';
window.history.replaceState(
{},
'',
`/v2/connectors?hostedMcp=connected&entryId=airtable&revokeAt=${encodeURIComponent(revokePage)}`,
);
mockGets([{
_id: 'i-airtable', type: 'hosted-mcp', status: 'connected',
config: { entryId: 'airtable', revokePage },
}], [{
...hostedLinearCatalogEntry('i-airtable'),
installableId: 'airtable', label: 'Airtable', entryId: 'airtable',
}]);
renderPage();

expect(await screen.findByRole('link', { name: "Open Airtable's connected apps" })).toHaveAttribute('href', revokePage);
expect(screen.getByRole('status')).toHaveTextContent(
'Your previous Airtable account may still have Commonly connected. Switch to it on Airtable, then remove Commonly there.',
);
});

it('does not turn a callback query into a provider link unless it matches the row', async () => {
window.history.replaceState(
{},
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/v2/components/V2ConnectorTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ const ModeGlyph: React.FC<{ mode: GrantWriteMode }> = ({ mode }) => {
return <G><path d="M12 3 4 6v6c0 5 3.4 8.4 8 9 4.6-.6 8-4 8-9V6z" /><path d="m9 12 2 2 4-4" /></G>;
};

const PER_ENTRY_REVOKE_COPY = new Set(['supabase', 'linear', 'sentry']);

const V2ConnectorTools: React.FC<Props> = ({ pods, manualRevokeNotice = null }) => {
const { t } = useTranslation();
const api = useV2Api();
Expand Down Expand Up @@ -300,7 +302,8 @@ const V2ConnectorTools: React.FC<Props> = ({ pods, manualRevokeNotice = null })
);
const renderManualRevokeNotice = (entry: ToolCatalogEntry | null) => {
if (!manualRevokeNotice || !noticeMatchesEntry(entry)) return null;
const context = manualRevokeNotice.entryId === 'supabase' ? 'supabase' : undefined;
// Per-entry copy names only what that vendor's page lists; entries without it fall back to the generic line.
const context = PER_ENTRY_REVOKE_COPY.has(manualRevokeNotice.entryId) ? manualRevokeNotice.entryId : undefined;
return (
<p className="v2-connector-row__refusal" role="status">
{t('connectors.hostedMcpManualRevoke', {
Expand Down Expand Up @@ -1110,7 +1113,7 @@ const V2ConnectorTools: React.FC<Props> = ({ pods, manualRevokeNotice = null })
{error && !draft && !selected && !githubAppSetup && (
<p className="v2-connector-aside__note" role="alert">{error}</p>
)}
{removedHiddenNotice && <p className="v2-connector-aside__note" role="status">{t('tools.removedReviewProvider', { defaultValue: 'Connection removed. Review Commonly access at {{label}}.', label: removedHiddenNotice.label })} <a href={removedHiddenNotice.revokePage} target="_blank" rel="noopener noreferrer">{t('tools.openProviderSettings', { defaultValue: 'Open provider settings' })}</a></p>}
{removedHiddenNotice && <p className="v2-connector-aside__note" role="status">{t('tools.removedReviewProvider', { defaultValue: 'Connection removed. Review its access at {{label}}.', label: removedHiddenNotice.label })} <a href={removedHiddenNotice.revokePage} target="_blank" rel="noopener noreferrer">{t('tools.openProviderSettings', { defaultValue: 'Open provider settings' })}</a></p>}
<div className="v2-connectors__rows">
{liveRows.map(renderRow)}
{notYet.map(renderNotYet)}
Expand Down
Loading