diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 6aa13d30..1f136cec 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -228,8 +228,9 @@ "unavailable": "Expiry unavailable", "expiresOn": "Expires {{date}}" }, - "agentHint": "Pick an agent from the catalog to install into this pod.", - "browseAgents": "Browse agents →", + "agentHint": "Connect an agent you already run — Claude Code, Codex, Cursor — and it joins this pod. Takes about two minutes.", + "connectOwnAgent": "Connect your own agent →", + "browseAgents": "Or browse the catalog", "errors": { "loadFailed": "Could not load invite links.", "generateFailed": "Could not generate invite.", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index c1f539e4..87828b35 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -228,8 +228,9 @@ "unavailable": "无法查看有效期", "expiresOn": "{{date}} 到期" }, - "agentHint": "从目录中选择要安装到这个 Pod 的智能体。", - "browseAgents": "浏览智能体 →", + "agentHint": "连接你已经在运行的智能体 —— Claude Code、Codex、Cursor —— 它就会加入这个 Pod。大约需要两分钟。", + "connectOwnAgent": "连接你自己的智能体 →", + "browseAgents": "或浏览目录", "errors": { "loadFailed": "邀请链接加载失败。", "generateFailed": "邀请链接生成失败。", diff --git a/frontend/src/v2/__tests__/V2InviteModal.test.tsx b/frontend/src/v2/__tests__/V2InviteModal.test.tsx index 96a0b232..0b5a2ab5 100644 --- a/frontend/src/v2/__tests__/V2InviteModal.test.tsx +++ b/frontend/src/v2/__tests__/V2InviteModal.test.tsx @@ -84,7 +84,12 @@ describe('V2InviteModal', () => { renderModal('agent'); expect(screen.getByRole('tab', { name: 'Add agent' })).toHaveAttribute('aria-selected', 'true'); - expect(screen.getByRole('button', { name: 'Browse agents →' })).toBeInTheDocument(); + // The primary action is connect-your-own, not the v1 catalog. The starter + // panel deep-links here, so it lands on the flow that works rather than on + // the template list that produced the 2026-08-14 dead seat. + expect(screen.getByRole('button', { name: 'Connect your own agent →' })).toBeInTheDocument(); + // The catalog is demoted, not removed. + expect(screen.getByRole('button', { name: 'Or browse the catalog' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Generate invite link' })).not.toBeInTheDocument(); }); }); diff --git a/frontend/src/v2/__tests__/V2InviteModalAgentPath.test.tsx b/frontend/src/v2/__tests__/V2InviteModalAgentPath.test.tsx new file mode 100644 index 00000000..d612aae7 --- /dev/null +++ b/frontend/src/v2/__tests__/V2InviteModalAgentPath.test.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import V2InviteModal from '../components/V2InviteModal'; +import en from '../../i18n/locales/en.json'; +import zhCN from '../../i18n/locales/zh-CN.json'; + +/** + * The in-pod "add an agent" path must lead to the flow that works. + * + * It used to offer exactly one button, into `/v2/agents/browse` — the v1 + * AgentsHub, whose catalog still serves 21 internal and smoke-test rows + * (`smoke-claude`, `demo-target`, `pod-architect`, `cl-critic`, …) beside real + * entries. On 2026-08-14 a real user took that door, installed the + * `claude-code` template, never started a local session, asked it the same + * question three times and got silence. They had a working Scout in the next + * room the entire time. + * + * So: connect-your-own leads, the catalog stays reachable underneath. + */ + +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { ...actual, useNavigate: () => mockNavigate }; +}); + +jest.mock('../hooks/useV2Api', () => ({ + useV2Api: () => ({ + get: jest.fn().mockResolvedValue({ invites: [] }), + post: jest.fn(), patch: jest.fn(), del: jest.fn(), + }), +})); + +jest.mock('../../context/AuthContext', () => ({ + useAuth: () => ({ currentUser: { _id: 'u1', username: 'sam' } }), +})); + +const POD = '6a7d154b0ec237d4b15dd28b'; + +const open = () => render( + + + , +); + +describe('the in-pod add-agent path', () => { + beforeEach(() => jest.clearAllMocks()); + + test('the primary action goes to BYO connect, with the pod prefilled', () => { + open(); + + const cta = screen.queryByText(en.inviteModal.connectOwnAgent); + expect(cta).toBeTruthy(); + fireEvent.click(cta as HTMLElement); + + // `?pod=` — V2AgentBYO reads `pod`, NOT `podId`. Getting this wrong sends + // the user to an unprefilled form, which is how a two-minute flow becomes + // a dead end. + expect(mockNavigate).toHaveBeenCalledWith(`/v2/agents/byo?pod=${POD}`); + }); + + test('the v1 catalog is demoted, not the only door', () => { + open(); + + const browse = screen.queryByText(en.inviteModal.browseAgents); + expect(browse).toBeTruthy(); + fireEvent.click(browse as HTMLElement); + + expect(mockNavigate).toHaveBeenCalledWith(`/v2/agents/browse?podId=${POD}`); + }); +}); + +describe('both locales carry the new primary action', () => { + test.each([['en', en as any], ['zh-CN', zhCN as any]])('%s', (_l, bundle) => { + // Two of the users this change is for wrote Chinese. + expect(bundle.inviteModal.connectOwnAgent).toBeTruthy(); + expect(bundle.inviteModal.browseAgents).toBeTruthy(); + // The hint must describe connecting your own agent, not picking from a + // catalog — the sentence is what sets the expectation before the click. + expect(bundle.inviteModal.agentHint).not.toMatch(/catalog|目录/); + }); +}); diff --git a/frontend/src/v2/components/V2InviteModal.tsx b/frontend/src/v2/components/V2InviteModal.tsx index 1696c6f1..02257958 100644 --- a/frontend/src/v2/components/V2InviteModal.tsx +++ b/frontend/src/v2/components/V2InviteModal.tsx @@ -337,12 +337,34 @@ const V2InviteModal: React.FC = ({ )} {tab === 'agent' && ( <> + {/* The primary action is CONNECT YOUR OWN, not the catalog. + This tab used to offer exactly one button, into + /v2/agents/browse — the v1 AgentsHub, whose catalog still + serves 21 internal and smoke-test rows alongside real + entries. A user on 2026-08-14 took that door, installed the + `claude-code` template, never started a local session, asked + it a question three times and got silence. Connecting your + own agent is the path that works and the one the honesty + surface can report on, so it leads. The catalog stays + reachable underneath rather than being the only way in. + + Note `?pod=` — V2AgentBYO reads `pod`, not `podId`. */}

{t('inviteModal.agentHint')}

+