diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index d33af655a8..1768f2edab 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -1711,6 +1711,8 @@
"waiting": "Waiting for {{name}} to check in… this turns green the moment the command connects.",
"verified": "{{name}} is listening — mentions will wake it.",
"stillWaiting": "Still no check-in from {{name}}. Run the command above in Terminal on your computer, and this page will confirm it.",
+ "stopsOnClose": "This stops when this Terminal window closes. For an agent that keeps running, add this computer under Other ways to connect, then choose On my computer.",
+ "stopsOnCloseWithComputer": "This stops when this Terminal window closes. For an agent that keeps running, start another agent and choose On my computer.",
"npmPermission": "If npm stops with EACCES (permission denied), don’t add sudo. Reinstall Node with a version manager such as nvm, then run the four lines again.",
"ctaWarning": "{{name}} isn’t listening yet: it won’t answer mentions until the command above is running.",
"needs": "Needs Node 20+ with npm and {{tool}} signed in. Run all four lines in one window: the exports last only there.",
@@ -1771,9 +1773,11 @@
"slow": "Still waiting — check that commonly daemon run is active on {{machine}}. The agent starts the moment it's back.",
"openPod": "Open the pod"
},
+ "quickTry": "Quick try: this command runs in Terminal and stops when that window closes. For an agent that should keep running, add this computer once with the daemon, under Other ways to connect below.",
"addComputer": {
"title": "Add a computer",
- "after": "Reload to select this computer.",
+ "recommended": "Recommended for an agent that keeps running. Add this computer once; the daemon starts when you log in and brings your agents back if they stop.",
+ "after": "Reload this page once it connects. Your computer appears in the list by its name, marked online.",
"copy": "Copy",
"copied": "Copied"
},
diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json
index de522bc6a6..b7b04172ff 100644
--- a/frontend/src/i18n/locales/zh-CN.json
+++ b/frontend/src/i18n/locales/zh-CN.json
@@ -1771,6 +1771,8 @@
"waiting": "正在等待 {{name}} 接入…命令一接通,这里就会变绿。",
"verified": "{{name}} 已在监听 —— @提及 会唤醒它。",
"stillWaiting": "{{name}} 还没接入。请在你电脑的终端里运行上面的命令,这里会自动确认。",
+ "stopsOnClose": "关闭这个终端窗口后就会停止。如果希望智能体持续运行,请在「其他接入方式」里添加这台电脑,然后选择「在我的电脑上」。",
+ "stopsOnCloseWithComputer": "关闭这个终端窗口后就会停止。如果希望智能体持续运行,请再添加一个智能体,然后选择「在我的电脑上」。",
"npmPermission": "如果 npm 报 EACCES(权限被拒绝),不要加 sudo。请用版本管理工具(如 nvm)重新安装 Node,然后重新运行这四行。",
"ctaWarning": "{{name}} 尚未监听:上面的命令运行起来之前,它不会回复 @提及。",
"needs": "需要 Node 20+(含 npm)和已登录的 {{tool}}。四行都要在同一个窗口里运行,换了窗口 export 就失效。",
@@ -1831,9 +1833,11 @@
"slow": "仍在等待——请确认 {{machine}} 上正在运行 commonly daemon run,恢复后会立即启动该智能体。",
"openPod": "打开 pod"
},
+ "quickTry": "快速试用:这条命令在终端里运行,关闭那个窗口就会停止。如果希望智能体持续运行,请在下方「其他接入方式」里用守护进程把这台电脑添加一次。",
"addComputer": {
"title": "添加电脑",
- "after": "刷新页面后即可选择这台电脑。",
+ "recommended": "推荐用于需要持续运行的智能体。把这台电脑添加一次,守护进程会在你登录时启动,智能体停止后也会自动恢复。",
+ "after": "连接完成后刷新页面,这台电脑会以名称出现在列表中,并标记为在线。",
"copy": "复制",
"copied": "已复制"
},
diff --git a/frontend/src/v2/__tests__/V2AgentBYOConnectPaths.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOConnectPaths.test.tsx
new file mode 100644
index 0000000000..1cf4405b06
--- /dev/null
+++ b/frontend/src/v2/__tests__/V2AgentBYOConnectPaths.test.tsx
@@ -0,0 +1,170 @@
+// @ts-nocheck
+// Keep setup request assertions independent of funnel telemetry.
+jest.mock('../utils/byoTelemetry', () => ({ recordByoStep: jest.fn() }));
+// TASK-645 item 3 and 4: the two connect paths. The run command is the quick
+// try and dies with its Terminal; the daemon is the one that keeps an agent
+// running. Each state is pinned by its own copy, so a rewording can't drop it.
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import V2AgentBYO from '../components/V2AgentBYO';
+import { AuthContext } from '../../context/AuthContext';
+
+jest.mock('../components/V2NavRail', () => () => null);
+jest.mock('../hooks/useV2PodAttention', () => ({
+ ...jest.requireActual('../hooks/useV2PodAttention'),
+ useV2PodAttention: () => ({ items: [], count: null, countByPod: {}, refresh: async () => {} }),
+}));
+jest.mock('axios', () => {
+ const mock = {
+ get: jest.fn(),
+ post: jest.fn(),
+ patch: jest.fn(),
+ delete: jest.fn(),
+ defaults: { baseURL: '', headers: { common: {} } },
+ interceptors: {
+ request: { use: jest.fn(), eject: jest.fn() },
+ response: { use: jest.fn(), eject: jest.fn() },
+ },
+ };
+ return { __esModule: true, default: mock, ...mock };
+});
+
+const axios = jest.requireMock('axios').default;
+
+const authValue = {
+ currentUser: { _id: 'u1', username: 'sam' },
+ user: { _id: 'u1', username: 'sam' },
+ token: 'user-jwt',
+ loading: false,
+ error: null,
+ isAuthenticated: true,
+ register: jest.fn(),
+ login: jest.fn(),
+ logout: jest.fn(),
+ updateProfile: jest.fn(),
+};
+
+const MAC = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36';
+const WINDOWS = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36';
+const realUserAgent = window.navigator.userAgent;
+const setUserAgent = (value) => Object.defineProperty(window.navigator, 'userAgent', { value, configurable: true });
+
+const mockPage = ({ machines = [] } = {}) => {
+ axios.get.mockImplementation((url) => {
+ if (url.startsWith('/api/hosted/availability')) return Promise.resolve({ data: { configured: false, caps: { agentsPerUser: 1, turnsPerDay: 200 } } });
+ if (url === '/api/pods') return Promise.resolve({ data: [{ _id: 'p1', name: 'Workspace', type: 'chat' }] });
+ if (url === '/api/machines') return Promise.resolve({ data: { machines, offlineAfterMs: 90000 } });
+ if (url.startsWith('/api/registry/agents/next-free-name?')) return Promise.resolve({ data: { name: new URLSearchParams(url.split('?')[1]).get('base') } });
+ return Promise.resolve({ data: {} });
+ });
+ axios.post
+ .mockResolvedValueOnce({ data: { ok: true } })
+ .mockResolvedValueOnce({ data: { token: 'cm_agent_test_token' } });
+};
+
+const renderPage = () => render(
+
{t('agentByo.addComputer.recommended')}
+{daemonCommand}
+
+ {t('agentByo.addComputer.after')}
+{t('agentByo.form.adapterLabel')}{' '} @@ -1407,6 +1427,11 @@ const V2AgentBYO: React.FC = () => { })()} )} + {/* The run command is the quick try; it dies with its Terminal. The + daemon is the one that keeps the agent running (TASK-645 item 3). */} + {mode === 'byo' && machines.length === 0 && listenerAvailable && ( +
{t('agentByo.quickTry')}
+ )} {/* One command is THE path (QA BYO walk 1): the daemon and the SDK are real alternatives, but shown as equals beside the button they read as a choice a stranger has to make before starting. */} @@ -1415,22 +1440,7 @@ const V2AgentBYO: React.FC = () => { if ((event.target as Element).closest('summary, button, a')) event.preventDefault(); }}>{daemonCommand}
-
- {t('agentByo.addComputer.after')}
-{t(listenerAvailable ? 'agentByo.footnote.cliRoute' : listenAdapter === 'claude' ? 'agentByo.footnote.cliRouteClaudeUnavailable' @@ -1618,6 +1628,11 @@ const V2AgentBYO: React.FC = () => { {t('agentByo.listen.npmPermission')}
)} + {listenerAvailable && ( ++ {t(machines.length === 0 ? 'agentByo.listen.stopsOnClose' : 'agentByo.listen.stopsOnCloseWithComputer')} +
+ )} {!listenerAvailable ? null : listenState === 'listening' ? (
{t('agentByo.listen.verified', { name: issued.agentName })}
@@ -1672,6 +1687,19 @@ const V2AgentBYO: React.FC = () => {
{t('agentByo.otherWays.title')}
+ {addComputerBlock}
+ {machines.length > 0 && (
+ // A computer on file: the machine choice is the start form's mode
+ // picker, which this result page does not show, so offer it here.
+
+ )}