From d03421becce08ca352bac86c39790ed64d1145d5 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:49:11 -0700 Subject: [PATCH 1/8] Cut helper text that the (i) icons already cover (D150, partial) BYO mode cards: drop the visible hint line on hosted, machine and BYO; fold each fact into that card's (i). Beta turn cap kept verbatim. Also cut the invite modal "new link" hint, shorten the people and agent hints, shorten the Your Team own-agent text and the BYO offer body, drop the Connectors "choose a channel" line, and shorten the avatar no-face hint. en and zh-CN move together. Not in this commit (see PR body): kicker lines, compact.limits, claudeCodeBody, nameHint, Settings avatar Paper variant, screenshots, zh-CN two-pass review. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/__tests__/timingClaims.test.ts | 2 +- frontend/src/i18n/__tests__/i18n.test.ts | 4 ++-- frontend/src/i18n/locales/en.json | 21 +++++++------------ frontend/src/i18n/locales/zh-CN.json | 21 +++++++------------ .../v2/__tests__/V2AgentBYOHosted.test.tsx | 4 ++-- .../src/v2/__tests__/V2SettingsPage.test.tsx | 8 +++---- frontend/src/v2/components/V2AgentBYO.tsx | 3 --- .../src/v2/components/V2ConnectorsPage.tsx | 2 -- frontend/src/v2/components/V2InviteModal.tsx | 3 --- 9 files changed, 24 insertions(+), 44 deletions(-) diff --git a/frontend/src/__tests__/timingClaims.test.ts b/frontend/src/__tests__/timingClaims.test.ts index 3ef2898c0c..fbefdc4d7f 100644 --- a/frontend/src/__tests__/timingClaims.test.ts +++ b/frontend/src/__tests__/timingClaims.test.ts @@ -15,7 +15,7 @@ describe('timing claims in the copy', () => { // at 2.54 s (install 1.83 + provision 0.63); walk 4: 2.47 s. // QA report qa-hosted-oob-5, timing table. test('click to live says "a few seconds", in both languages', () => { - expect(en.agentByo.mode.hostedMeta).toBe('Live in a few seconds · Beta: {{turns}} turns a day'); + expect(en.agentByo.mode.hostedMeta).toBe('Nothing to install. Live in a few seconds. Beta: {{turns}} turns a day.'); expect(en.agentByo.compact.lead).toBe('Live in a few seconds.'); expect(zh.agentByo.mode.hostedMeta).toContain('几秒'); expect(zh.agentByo.compact.lead).toContain('几秒'); diff --git a/frontend/src/i18n/__tests__/i18n.test.ts b/frontend/src/i18n/__tests__/i18n.test.ts index 1687a57b2b..a90d183c4e 100644 --- a/frontend/src/i18n/__tests__/i18n.test.ts +++ b/frontend/src/i18n/__tests__/i18n.test.ts @@ -133,8 +133,8 @@ describe('i18n configuration', () => { }); it('uses a Chinese comma in the BYO machine hint', () => { - const hint = i18n.t('agentByo.mode.machineHint', { lng: 'zh-CN' }); - expect(hint).toBe('在你已添加的电脑上运行,你的文件、你的 CLI'); + const hint = i18n.t('agentByo.mode.machineMeta', { lng: 'zh-CN' }); + expect(hint).toBe('在你已添加的电脑上运行,用你的文件和 CLI。需要那台电脑运行 Commonly 守护进程。'); expect(hint).not.toContain('—'); }); }); diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 15d85018a9..d627fb2a5f 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -134,7 +134,7 @@ "avatar": { "regenerate": "Regenerate", "uploadPhoto": "Upload photo", - "description": "No face and no skin tone until you choose one. Regenerate for another, or upload a photo.", + "description": "Regenerate for another, or upload a photo.", "descriptionAfterChoice": "Regenerate for another, or upload a photo.", "uploadTitle": "Upload photo", "cropPreview": "Avatar crop preview", @@ -495,7 +495,7 @@ "people": "Invite people", "agent": "Add agent" }, - "peopleHint": "Create a link anyone with a Commonly account can use to join this pod.", + "peopleHint": "Anyone with the link can join.", "podMember": "Pod member", "options": { "expiresAfter": "Expires after", @@ -513,7 +513,6 @@ "generate": "Generate invite link", "generating": "Generating…", "newLinkLabel": "New invite link", - "newLinkHint": "The new link also appears under Active links below.", "activeLinks": "Active links", "loadingLinks": "Loading links…", "noActiveLinks": "No active invite links.", @@ -541,7 +540,7 @@ "unavailable": "Expiry unavailable", "expiresOn": "Expires {{date}}" }, - "agentHint": "Add an agent — Claude Code or Codex, or run one here — and it joins this pod. Takes about two minutes.", + "agentHint": "Claude Code, Codex, or one that runs here.", "connectOwnAgent": "Connect your own agent →", "errors": { "loadFailed": "Could not load invite links.", @@ -1738,21 +1737,18 @@ "mode": { "label": "Where should it run?", "hosted": "Run it here", - "hostedHint": "Commonly runs it for you. Nothing to install. Beta: {{turns}} turns a day.", "byo": "Use my own agent", - "byoHint": "Your Claude Code or Codex, on your computer.", "onCommonly": "on Commonly", "yours": "your machine", - "hostedMeta": "Live in a few seconds · Beta: {{turns}} turns a day", - "byoMeta": "One command in Terminal", + "hostedMeta": "Nothing to install. Live in a few seconds. Beta: {{turns}} turns a day.", + "byoMeta": "Your Claude Code or Codex, on your computer. One command in Terminal.", "moreAbout": "More about {{mode}}", "machine": "On my computer", - "machineHint": "Runs on a computer you've added — your files, your CLIs", - "machineMeta": "Needs the Commonly daemon running there" + "machineMeta": "Runs on a computer you've added, with your files and CLIs. Needs the Commonly daemon running there." }, "hosted": { "offerAction": "Run it here instead", - "offerBody": "No terminal needed. Start a new hosted agent in this pod.", + "offerBody": "No terminal needed.", "heading": "Running:", "live": "{{name}} is live in {{pod}}. Mention it there and it answers.", "starting": "Starting {{name}}…", @@ -2224,7 +2220,7 @@ }, "own": { "title": "Your own agent", - "text": "Claude Code, Codex, or anything that speaks the protocol. One command on your machine; it keeps its identity here.", + "text": "Claude Code or Codex, from your own machine.", "command": "npx @commonlyai/cli agent attach claude", "setup": "Set it up" } @@ -2306,7 +2302,6 @@ "codeExpired": "The enable code expired.", "connect": "Connect", "connectChannel": "Connect a channel", - "connectChannelHint": "Choose a channel and the pod it should join.", "copied": "Copied", "copyCommand": "Copy command", "description": "Each channel gets one agent from a pod. It answers there in its own name; the rest of the team stays behind it.", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 9984f7b282..80a9c79717 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -134,7 +134,7 @@ "avatar": { "regenerate": "换一个", "uploadPhoto": "上传照片", - "description": "选择前不会显示面部或肤色。你可以换一个造型,或上传照片。", + "description": "你可以换一个造型,或上传照片。", "descriptionAfterChoice": "你可以换一个造型,或上传照片。", "uploadTitle": "上传照片", "cropPreview": "头像裁剪预览", @@ -495,7 +495,7 @@ "people": "邀请成员", "agent": "添加智能体" }, - "peopleHint": "创建邀请链接,拥有 Commonly 账号的人都能用它加入这个 Pod。", + "peopleHint": "拿到链接的人都能加入。", "podMember": "Pod 成员", "options": { "expiresAfter": "有效期", @@ -513,7 +513,6 @@ "generate": "生成邀请链接", "generating": "正在生成…", "newLinkLabel": "新邀请链接", - "newLinkHint": "新链接也会出现在下方的有效链接中。", "activeLinks": "有效链接", "loadingLinks": "正在加载链接…", "noActiveLinks": "暂无有效邀请链接。", @@ -541,7 +540,7 @@ "unavailable": "无法查看有效期", "expiresOn": "{{date}} 到期" }, - "agentHint": "添加一个智能体 —— Claude Code 或 Codex,或在这里运行一个 —— 它就会加入这个 Pod。大约需要两分钟。", + "agentHint": "Claude Code、Codex,或在这里运行一个。", "connectOwnAgent": "连接你自己的智能体 →", "errors": { "loadFailed": "邀请链接加载失败。", @@ -1797,21 +1796,18 @@ "mode": { "label": "在哪里运行?", "hosted": "在这里运行", - "hostedHint": "由 Commonly 代为运行,无需安装。测试期:每天{{turns}}轮。", "byo": "用我自己的智能体", - "byoHint": "你电脑上的 Claude Code 或 Codex。", "onCommonly": "在 Commonly 上", "yours": "你的电脑", - "hostedMeta": "几秒即可上线 · 测试期每天{{turns}}轮", - "byoMeta": "只需一条终端命令", + "hostedMeta": "无需安装,几秒即可上线。测试期每天{{turns}}轮。", + "byoMeta": "你电脑上的 Claude Code 或 Codex,只需一条终端命令。", "moreAbout": "更多关于{{mode}}的信息", "machine": "在我的电脑上", - "machineHint": "在你已添加的电脑上运行,你的文件、你的 CLI", - "machineMeta": "需要那台电脑运行 Commonly 守护进程" + "machineMeta": "在你已添加的电脑上运行,用你的文件和 CLI。需要那台电脑运行 Commonly 守护进程。" }, "hosted": { "offerAction": "改为在这里运行", - "offerBody": "无需终端。在这个 Pod 中启动一个新的托管智能体。", + "offerBody": "无需终端。", "heading": "运行中:", "live": "{{name}} 已在 {{pod}} 上线。在那里 @ 它即可得到回复。", "starting": "正在启动 {{name}}…", @@ -2214,7 +2210,7 @@ }, "own": { "title": "你自己的智能体", - "text": "Claude Code、Codex,或任何支持该协议的工具。在你的机器上运行一条命令,它的身份保留在这里。", + "text": "你自己电脑上的 Claude Code 或 Codex。", "command": "npx @commonlyai/cli agent attach claude", "setup": "开始设置" } @@ -2296,7 +2292,6 @@ "codeExpired": "启用码已过期。", "connect": "连接", "connectChannel": "连接频道", - "connectChannelHint": "选择频道,以及它要加入的 Pod。", "copied": "已复制", "copyCommand": "复制命令", "description": "每个频道从一个 Pod 接入一个智能体。它会在那里以自己的名义回复,其余团队成员都在幕后。", diff --git a/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx index 52b9bb3cf5..e69ee21b6d 100644 --- a/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx @@ -79,7 +79,7 @@ describe('V2AgentBYO — Run it here', () => { await waitFor(() => expect(screen.getByTestId('byo-mode-hosted')).toBeInTheDocument()); expect(screen.getByRole('group', { name: 'Where should it run?' })).toBeInTheDocument(); - expect(screen.getByText('Live in a few seconds · Beta: 200 turns a day')).toBeInTheDocument(); + expect(screen.getByText('Nothing to install. Live in a few seconds. Beta: 200 turns a day.')).toBeInTheDocument(); expect(document.body.textContent).not.toContain('{{'); expect(screen.getByTestId('byo-mode-hosted')).toHaveAttribute('aria-pressed', 'false'); expect(screen.getByTestId('byo-mode-byo')).toHaveAttribute('aria-pressed', 'false'); @@ -148,7 +148,7 @@ describe('V2AgentBYO — Run it here', () => { renderPage(); await waitFor(() => expect(screen.getByTestId('byo-mode-hosted')).toBeInTheDocument()); - expect(screen.getAllByText(/beta: 200 turns a day/i)).toHaveLength(2); + expect(screen.getAllByText(/beta: 200 turns a day/i)).toHaveLength(1); fireEvent.click(screen.getByTestId('byo-mode-hosted')); fireEvent.click(screen.getByRole('button', { name: /^run it here$/i })); diff --git a/frontend/src/v2/__tests__/V2SettingsPage.test.tsx b/frontend/src/v2/__tests__/V2SettingsPage.test.tsx index 73e73a97f5..7176f681c4 100644 --- a/frontend/src/v2/__tests__/V2SettingsPage.test.tsx +++ b/frontend/src/v2/__tests__/V2SettingsPage.test.tsx @@ -322,11 +322,11 @@ describe('V2SettingsPage', () => { expect(regenerateButton).toHaveFocus(); }); - test('keeps the no-face explanation for Paper and shortens it for a picked face or photo', () => { + test('shows the same short avatar hint for Paper and for a picked face', () => { (axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } }); const { rerender } = renderSettings({ ...auth.currentUser, profilePicture: 'default' }); - expect(screen.getByText('No face and no skin tone until you choose one. Regenerate for another, or upload a photo.')) + expect(screen.getByText('Regenerate for another, or upload a photo.')) .toBeInTheDocument(); rerender( @@ -334,7 +334,7 @@ describe('V2SettingsPage', () => {
, ); - expect(screen.getByText('No face and no skin tone until you choose one. Regenerate for another, or upload a photo.')) + expect(screen.getByText('Regenerate for another, or upload a photo.')) .toBeInTheDocument(); rerender( @@ -343,8 +343,6 @@ describe('V2SettingsPage', () => { , ); expect(screen.getByText('Regenerate for another, or upload a photo.')).toBeInTheDocument(); - expect(screen.queryByText('No face and no skin tone until you choose one. Regenerate for another, or upload a photo.')) - .not.toBeInTheDocument(); }); test('Regenerate reports a profile save error without losing the current avatar', async () => { diff --git a/frontend/src/v2/components/V2AgentBYO.tsx b/frontend/src/v2/components/V2AgentBYO.tsx index e324763a2e..2f4ef54c86 100644 --- a/frontend/src/v2/components/V2AgentBYO.tsx +++ b/frontend/src/v2/components/V2AgentBYO.tsx @@ -1241,7 +1241,6 @@ const V2AgentBYO: React.FC = () => { choice (ruled 2026-10-15). */} {t('agentByo.mode.onCommonly')} {t('agentByo.mode.hosted')} - {t('agentByo.mode.hostedHint', { turns: hosting.caps.turnsPerDay })} {t('agentByo.mode.hostedMeta', { turns: hosting.caps.turnsPerDay })} @@ -1258,7 +1257,6 @@ const V2AgentBYO: React.FC = () => { > {t('agentByo.mode.yours')} {t('agentByo.mode.machine')} - {t('agentByo.mode.machineHint')} {t('agentByo.mode.machineMeta')} @@ -1275,7 +1273,6 @@ const V2AgentBYO: React.FC = () => { > {t('agentByo.mode.yours')} {t('agentByo.mode.byo')} - {t('agentByo.mode.byoHint')} {t('agentByo.mode.byoMeta')} diff --git a/frontend/src/v2/components/V2ConnectorsPage.tsx b/frontend/src/v2/components/V2ConnectorsPage.tsx index f4032ee912..635bb14fb8 100644 --- a/frontend/src/v2/components/V2ConnectorsPage.tsx +++ b/frontend/src/v2/components/V2ConnectorsPage.tsx @@ -1690,7 +1690,6 @@ const V2ConnectorsPage: React.FC = () => { - {(pods === null || pods.length > 0) &&

{t('connectors.connectChannelHint', { defaultValue: 'Choose a channel and the pod it should join.' })}

} )} @@ -1707,7 +1706,6 @@ const V2ConnectorsPage: React.FC = () => { diff --git a/frontend/src/v2/components/V2InviteModal.tsx b/frontend/src/v2/components/V2InviteModal.tsx index 8a8697068b..bcac6f174a 100644 --- a/frontend/src/v2/components/V2InviteModal.tsx +++ b/frontend/src/v2/components/V2InviteModal.tsx @@ -264,9 +264,6 @@ const V2InviteModal: React.FC = ({ {copiedToken === 'new' ? t('common.copied') : t('common.copy')} -

- {t('inviteModal.newLinkHint')} -

)} {error &&
{error}
} From cc30b0c16996f69aeee6a471bbfbefc28fb7c821 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:20:36 -0700 Subject: [PATCH 2/8] Finish D150 helper-text cuts: kickers, limits, Node 20 Remove the BYO mode kickers ("on Commonly", "your machine") and their dead CSS. Fold the beta cap into the compact page-head (i) and drop the visible limits line; the page-head lead now shows on the one-click hosted page too, so the cap is still visible there. Shorten the Claude Code snippet body and the agent name hint. Listen requirement is Node 20+, matching cli engines. en and zh-CN move together. Tests: the hosted act button is the "Run it here" button that is not a mode toggle; queries now target it through one helper, since the mode card's name is no longer disambiguated by its kicker. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/__tests__/timingClaims.test.ts | 2 +- frontend/src/i18n/locales/en.json | 8 ++-- frontend/src/i18n/locales/zh-CN.json | 8 ++-- .../v2/__tests__/V2AgentBYOHosted.test.tsx | 39 +++++++++++-------- .../v2/__tests__/v2-layout-invariants.test.ts | 15 ------- frontend/src/v2/components/V2AgentBYO.tsx | 12 +----- frontend/src/v2/v2.css | 15 ------- 7 files changed, 31 insertions(+), 68 deletions(-) diff --git a/frontend/src/__tests__/timingClaims.test.ts b/frontend/src/__tests__/timingClaims.test.ts index fbefdc4d7f..bcee9be227 100644 --- a/frontend/src/__tests__/timingClaims.test.ts +++ b/frontend/src/__tests__/timingClaims.test.ts @@ -16,7 +16,7 @@ describe('timing claims in the copy', () => { // QA report qa-hosted-oob-5, timing table. test('click to live says "a few seconds", in both languages', () => { expect(en.agentByo.mode.hostedMeta).toBe('Nothing to install. Live in a few seconds. Beta: {{turns}} turns a day.'); - expect(en.agentByo.compact.lead).toBe('Live in a few seconds.'); + expect(en.agentByo.compact.lead).toBe('Live in a few seconds. Beta: {{agents}} hosted agent per account, {{turns}} turns a day.'); expect(zh.agentByo.mode.hostedMeta).toContain('几秒'); expect(zh.agentByo.compact.lead).toContain('几秒'); }); diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index d627fb2a5f..54379c8428 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1626,7 +1626,7 @@ "description": "A running agent answers @mentions in its pod.", "form": { "nameLabel": "Agent name", - "nameHint": "Lower-case letters, digits, and dashes. It posts under this name.", + "nameHint": "Lowercase letters, digits and dashes.", "podLabel": "Pod it joins", "loadingPods": "Loading pods…", "machineLabel": "Computer", @@ -1681,7 +1681,7 @@ "cursorInstall": "Add to Cursor", "cursorInstallHint": "The link adds Commonly with a placeholder key. Replace it in Cursor’s MCP settings with the key above.", "memory": "Bring its memory (optional)", - "claudeCodeBody": "Gives your own Claude Code sessions Commonly’s tools. It does not make {{name}} answer @mentions; the command above does.", + "claudeCodeBody": "Gives your Claude Code sessions the pod’s tools. It won’t answer @mentions.", "claudeCodeBodyOwn": "Gives your own Claude Code sessions Commonly’s tools.", "claudeCodeBodyWindows": "Paste both lines in PowerShell. They give your own Claude Code sessions on this machine Commonly’s tools.", "claudeCodeMain": "Let Claude Code use the pod in your own sessions" @@ -1738,8 +1738,6 @@ "label": "Where should it run?", "hosted": "Run it here", "byo": "Use my own agent", - "onCommonly": "on Commonly", - "yours": "your machine", "hostedMeta": "Nothing to install. Live in a few seconds. Beta: {{turns}} turns a day.", "byoMeta": "Your Claude Code or Codex, on your computer. One command in Terminal.", "moreAbout": "More about {{mode}}", @@ -1794,7 +1792,7 @@ }, "compact": { "title": "Run an agent here", - "lead": "Live in a few seconds.", + "lead": "Live in a few seconds. Beta: {{agents}} hosted agent per account, {{turns}} turns a day.", "joins": "It joins {{pod}} as {{name}}.", "change": "Change", "limits": "Beta: {{agents}} hosted agent per account, {{turns}} turns a day.", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 80a9c79717..1912a79119 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -1685,7 +1685,7 @@ "description": "智能体运行期间会在所在 Pod 回复 @提及。", "form": { "nameLabel": "智能体名称", - "nameHint": "仅支持小写字母、数字和连字符。它会用这个名字发言。", + "nameHint": "仅支持小写字母、数字和连字符。", "podLabel": "要加入的 Pod", "loadingPods": "正在加载 Pod…", "machineLabel": "电脑", @@ -1740,7 +1740,7 @@ "cursorInstall": "添加到 Cursor", "cursorInstallHint": "该链接会以占位密钥添加 Commonly。请在 Cursor 的 MCP 设置中将其替换为上面的密钥。", "memory": "导入它的记忆(可选)", - "claudeCodeBody": "为你自己的 Claude Code 会话加上 Commonly 的工具。它不会让 {{name}} 回复 @提及,上面那条命令才会。", + "claudeCodeBody": "为你的 Claude Code 会话加上 Commonly 的工具。它不会回复 @提及。", "claudeCodeBodyOwn": "为你自己的 Claude Code 会话加上 Commonly 的工具。", "claudeCodeBodyWindows": "两行都粘贴到 PowerShell 里运行。这样你在这台电脑上的 Claude Code 会话就能用上 Commonly 的工具。", "claudeCodeMain": "让 Claude Code 在你自己的会话里使用这个 Pod" @@ -1797,8 +1797,6 @@ "label": "在哪里运行?", "hosted": "在这里运行", "byo": "用我自己的智能体", - "onCommonly": "在 Commonly 上", - "yours": "你的电脑", "hostedMeta": "无需安装,几秒即可上线。测试期每天{{turns}}轮。", "byoMeta": "你电脑上的 Claude Code 或 Codex,只需一条终端命令。", "moreAbout": "更多关于{{mode}}的信息", @@ -1853,7 +1851,7 @@ }, "compact": { "title": "在这里运行智能体", - "lead": "几秒后即可上线。", + "lead": "几秒后即可上线。测试期:每个账号{{agents}}个托管智能体,每天{{turns}}轮。", "joins": "它会以 {{name}} 的身份加入 {{pod}}。", "change": "更改", "limits": "测试期:每个账号{{agents}}个托管智能体,每天{{turns}}轮。", diff --git a/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx index e69ee21b6d..bbe48cea6c 100644 --- a/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx @@ -56,6 +56,11 @@ const mockGet = ({ configured, status }) => { }); }; +// The hosted mode card is titled "Run it here" too, so the act button is the +// "Run it here" button that is not a mode toggle (no aria-pressed). +const actButton = () => screen.queryAllByRole('button', { name: /^run it here$/i }) + .find((b) => !b.hasAttribute('aria-pressed')) ?? null; + const renderPage = () => render( @@ -84,7 +89,7 @@ describe('V2AgentBYO — Run it here', () => { expect(screen.getByTestId('byo-mode-hosted')).toHaveAttribute('aria-pressed', 'false'); expect(screen.getByTestId('byo-mode-byo')).toHaveAttribute('aria-pressed', 'false'); expect(screen.getByTestId('byo-mode-unchosen')).toHaveTextContent('Choose where it runs first'); - expect(screen.queryByRole('button', { name: /^run it here$/i })).not.toBeInTheDocument(); + expect(actButton()).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /get my connect command/i })).not.toBeInTheDocument(); fireEvent.click(screen.getByTestId('byo-mode-byo')); @@ -150,7 +155,7 @@ describe('V2AgentBYO — Run it here', () => { await waitFor(() => expect(screen.getByTestId('byo-mode-hosted')).toBeInTheDocument()); expect(screen.getAllByText(/beta: 200 turns a day/i)).toHaveLength(1); fireEvent.click(screen.getByTestId('byo-mode-hosted')); - fireEvent.click(screen.getByRole('button', { name: /^run it here$/i })); + fireEvent.click(actButton()); await screen.findByTestId('byo-hosted-result'); const installCall = axios.post.mock.calls.find(([url]) => url === '/api/registry/install'); @@ -172,7 +177,7 @@ describe('V2AgentBYO — Run it here', () => { await waitFor(() => expect(screen.getByTestId('byo-mode-hosted')).toBeInTheDocument()); fireEvent.click(screen.getByTestId('byo-mode-hosted')); - const act = screen.getByRole('button', { name: /^run it here$/i }); + const act = actButton(); const addComputer = await screen.findByTestId('byo-add-computer'); // eslint-disable-next-line no-bitwise expect(act.compareDocumentPosition(addComputer) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); @@ -223,12 +228,13 @@ describe('V2AgentBYO — Run it here', () => { const compact = await screen.findByTestId('byo-hosted-compact'); expect(screen.getByRole('heading', { name: 'Run an agent here' })).toBeInTheDocument(); await waitFor(() => expect(compact).toHaveTextContent('It joins Workspace as sam-agent.')); - await waitFor(() => expect(compact).toHaveTextContent('Beta: 1 hosted agent per account, 200 turns a day.')); + // The beta cap lives in the page-head (i), not in the body. + expect(screen.getByText(/Beta: 1 hosted agent per account, 200 turns a day\./)).toBeInTheDocument(); // None of the full page's choice and setup copy is in the way. expect(screen.queryByTestId('byo-mode-hosted')).not.toBeInTheDocument(); expect(screen.queryByTestId('byo-add-computer')).not.toBeInTheDocument(); - const act = screen.getByRole('button', { name: /^run it here$/i }); + const act = actButton(); await waitFor(() => expect(act).toBeEnabled()); fireEvent.click(act); await screen.findByTestId('byo-hosted-result'); @@ -266,7 +272,8 @@ describe('V2AgentBYO — Run it here', () => { , ); - const act = await screen.findByRole('button', { name: /^run it here$/i }); + await waitFor(() => expect(actButton()).not.toBeNull()); + const act = actButton(); await waitFor(() => expect(act).toBeEnabled()); fireEvent.click(act); fireEvent.click(await screen.findByRole('button', { name: /go to your pod/i })); @@ -290,10 +297,10 @@ describe('V2AgentBYO — Run it here', () => { // The compact page shows at once, with the act disabled until the // instance answers that it can host. expect(await screen.findByTestId('byo-hosted-compact')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /^run it here$/i })).toBeDisabled(); + expect(actButton()).toBeDisabled(); await act(async () => { answerAvailability({ data: { configured: true, caps: { agentsPerUser: 1, turnsPerDay: 30 } } }); }); - await waitFor(() => expect(screen.getByRole('button', { name: /^run it here$/i })).toBeEnabled()); + await waitFor(() => expect(actButton()).toBeEnabled()); window.history.pushState({}, '', '/'); }); @@ -303,7 +310,7 @@ describe('V2AgentBYO — Run it here', () => { await waitFor(() => expect(screen.getByRole('button', { name: /get my connect command/i })).toBeEnabled()); expect(screen.queryByTestId('byo-mode-hosted')).not.toBeInTheDocument(); - expect(screen.queryByRole('button', { name: /^run it here$/i })).not.toBeInTheDocument(); + expect(actButton()).not.toBeInTheDocument(); window.history.pushState({}, '', '/'); }); @@ -323,7 +330,7 @@ describe('V2AgentBYO — Run it here', () => { // A persona link says what to hire, not where it runs. expect(screen.getByTestId('byo-mode-hosted')).toHaveAttribute('aria-pressed', 'false'); fireEvent.click(screen.getByTestId('byo-mode-hosted')); - fireEvent.click(screen.getByRole('button', { name: /^run it here$/i })); + fireEvent.click(actButton()); await screen.findByTestId('byo-hosted-result'); const installCall = axios.post.mock.calls.find(([url]) => url === '/api/registry/install'); expect(installCall[1]).toMatchObject({ @@ -341,7 +348,7 @@ describe('V2AgentBYO — Run it here', () => { renderPage(); await waitFor(() => expect(screen.getByTestId('byo-mode-hosted')).toBeInTheDocument()); fireEvent.click(screen.getByTestId('byo-mode-hosted')); - fireEvent.click(screen.getByRole('button', { name: /^run it here$/i })); + fireEvent.click(actButton()); await screen.findByTestId('byo-hosted-starting'); await act(async () => { jest.advanceTimersByTime(4000); }); @@ -361,7 +368,7 @@ describe('V2AgentBYO — Run it here', () => { renderPage(); await waitFor(() => expect(screen.getByTestId('byo-mode-hosted')).toBeInTheDocument()); fireEvent.click(screen.getByTestId('byo-mode-hosted')); - fireEvent.click(screen.getByRole('button', { name: /^run it here$/i })); + fireEvent.click(actButton()); expect(await screen.findByTestId('byo-hosted-running')).toBeInTheDocument(); expect(screen.queryByTestId('byo-hosted-starting')).not.toBeInTheDocument(); @@ -375,7 +382,7 @@ describe('V2AgentBYO — Run it here', () => { renderPage(); await waitFor(() => expect(screen.getByTestId('byo-mode-hosted')).toBeInTheDocument()); fireEvent.click(screen.getByTestId('byo-mode-hosted')); - fireEvent.click(screen.getByRole('button', { name: /^run it here$/i })); + fireEvent.click(actButton()); // No timer advanced: the first check runs on its own. expect(await screen.findByTestId('byo-hosted-running')).toBeInTheDocument(); @@ -389,7 +396,7 @@ describe('V2AgentBYO — Run it here', () => { renderPage(); await waitFor(() => expect(screen.getByTestId('byo-mode-hosted')).toBeInTheDocument()); fireEvent.click(screen.getByTestId('byo-mode-hosted')); - fireEvent.click(screen.getByRole('button', { name: /^run it here$/i })); + fireEvent.click(actButton()); await screen.findByTestId('byo-hosted-starting'); await act(async () => { jest.advanceTimersByTime(4000); }); @@ -411,7 +418,7 @@ describe('V2AgentBYO — Run it here', () => { renderPage(); await waitFor(() => expect(screen.getByTestId('byo-mode-hosted')).toBeInTheDocument()); fireEvent.click(screen.getByTestId('byo-mode-hosted')); - fireEvent.click(screen.getByRole('button', { name: /^run it here$/i })); + fireEvent.click(actButton()); await screen.findByText(/already have a hosted agent \(beta allows 1\)/i); expect(axios.post.mock.calls.some(([url]) => url === '/api/hosted/provision')).toBe(false); }); @@ -427,7 +434,7 @@ describe('V2AgentBYO — Run it here', () => { renderPage(); await waitFor(() => expect(screen.getByTestId('byo-mode-hosted')).toBeInTheDocument()); fireEvent.click(screen.getByTestId('byo-mode-hosted')); - fireEvent.click(screen.getByRole('button', { name: /^run it here$/i })); + fireEvent.click(actButton()); await screen.findByText(/maya-agent already holds your hosted agent slot \(beta allows 1\)/i); expect(axios.post.mock.calls.some(([url]) => url === '/api/hosted/provision')).toBe(false); }); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 7f96ca6c82..a2735b56d4 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -3208,21 +3208,6 @@ describe('Bring your own agent onto Signal (TASK-166)', () => { ).toContain('box-shadow: var(--v2-focus-ring)'); }); - test('the mode kicker is mono 500 at 11px, lowercase, cobalt only when chosen (item 3)', () => { - const kicker = cssDeclarations(ruleBody(v2, '.v2-byo__mode-kicker')); - expect(kicker.font).toBe('500 11px/16px var(--v2-font-mono)'); - expect(kicker['text-transform']).toBeUndefined(); - expect(kicker.color).toBe('var(--v2-text-muted)'); - const chosen = cssDeclarations( - ruleBody(v2, '.v2-root button.v2-byo__mode[aria-pressed="true"] .v2-byo__mode-kicker'), - ); - expect(chosen.color).toBe('var(--v2-accent-text)'); - // The caps were data, not only CSS, so the data is where they must not come - // back from: a lowercase transform over zh-CN copy would be the wrong fix. - expect(en).toContain('"onCommonly": "on Commonly"'); - expect(en).toContain('"yours": "your machine"'); - }); - test('the preview label is mono 500 at 11px with no caps transform (item 4)', () => { const label = cssDeclarations(ruleBody(v2, '.v2-byo__preview-label')); expect(label.font).toBe('500 11px/16px var(--v2-font-mono)'); diff --git a/frontend/src/v2/components/V2AgentBYO.tsx b/frontend/src/v2/components/V2AgentBYO.tsx index 2f4ef54c86..bac31d7724 100644 --- a/frontend/src/v2/components/V2AgentBYO.tsx +++ b/frontend/src/v2/components/V2AgentBYO.tsx @@ -1112,7 +1112,7 @@ const V2AgentBYO: React.FC = () => { })()} hint={(() => { if (issued) return undefined; - if (showCompactHosted && !hosted) return t('agentByo.compact.lead'); + if (showCompactHosted) return t('agentByo.compact.lead',{ agents: hosting?.caps.agentsPerUser, turns: hosting?.caps.turnsPerDay }); if (showCompactByo) return t('agentByo.compactByo.lead'); return t('agentByo.description'); })()} @@ -1159,11 +1159,6 @@ const V2AgentBYO: React.FC = () => { > {submitting ? t('agentByo.actions.starting') : t('agentByo.actions.runHere')} - {hosting?.configured && ( -

- {t('agentByo.compact.limits', { agents: hosting.caps.agentsPerUser, turns: hosting.caps.turnsPerDay })} -

- )} @@ -1255,7 +1247,6 @@ const V2AgentBYO: React.FC = () => { onClick={() => chooseMode('machine')} data-testid="byo-mode-machine" > - {t('agentByo.mode.yours')} {t('agentByo.mode.machine')} @@ -1271,7 +1262,6 @@ const V2AgentBYO: React.FC = () => { onClick={() => chooseMode('byo')} data-testid="byo-mode-byo" > - {t('agentByo.mode.yours')} {t('agentByo.mode.byo')} diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index a27973a1b0..d88f5c80fe 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -6490,21 +6490,6 @@ body.modern-ui.v2-canvas { cursor: pointer; transition: border-color 0.08s ease, background 0.08s ease; } -/* Signal rule 3 — meta is mono 500 at 11px and lowercase; the caps lived in - en.json, which now carries lowercase strings, so no transform is needed - (and a transform over zh-CN copy would be wrong). */ -.v2-byo__mode-kicker { - color: var(--v2-text-muted); - font: 500 11px/16px var(--v2-font-mono); - letter-spacing: 0; -} -.v2-byo__mode-kicker--quiet { color: var(--v2-text-muted); } - -/* The chosen card is the row's one cobalt mark (Signal rule 1); the modifier - above is what the untouched cards wear. */ -.v2-root button.v2-byo__mode[aria-pressed="true"] .v2-byo__mode-kicker { - color: var(--v2-accent-text); -} .v2-byo__mode-option > .v2-hint { margin-top: 0; } .v2-root button.v2-byo__mode:hover { background: var(--v2-surface-hover); From 8876b24bd1f7138720f2ffd82b3de68544d8149c Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:16:12 -0700 Subject: [PATCH 3/8] Restore 'or run one here' in the invite agent hint The D150 cut dropped the hosted-offer phrase from inviteModal.agentHint, which runtimeClaims.test.ts requires on every surface that leads to the Bring your own page. Fails the Frontend tests check on #2943. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/i18n/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 54379c8428..629cd8340c 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -540,7 +540,7 @@ "unavailable": "Expiry unavailable", "expiresOn": "Expires {{date}}" }, - "agentHint": "Claude Code, Codex, or one that runs here.", + "agentHint": "Claude Code, Codex, or run one here.", "connectOwnAgent": "Connect your own agent →", "errors": { "loadFailed": "Could not load invite links.", From 9b3b0014034431bf5ee0772eff379de543351419 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:19:17 -0700 Subject: [PATCH 4/8] Add route-mock render harness for D150 before/after screenshots Sample-name fixtures (Alex) answer every /api call, so the signed-in BYO pages and the pod invite modal render without a backend. Test tooling only; no app code changes. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/scripts/d150-renders/package.json | 8 ++ frontend/scripts/d150-renders/render.mjs | 127 +++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 frontend/scripts/d150-renders/package.json create mode 100644 frontend/scripts/d150-renders/render.mjs diff --git a/frontend/scripts/d150-renders/package.json b/frontend/scripts/d150-renders/package.json new file mode 100644 index 0000000000..8109e75505 --- /dev/null +++ b/frontend/scripts/d150-renders/package.json @@ -0,0 +1,8 @@ +{ + "name": "commonly-d150-renders", + "private": true, + "type": "module", + "description": "Route-mocked 390/1440 renders for the D150 helper-text cut (TASK-588). Run against a before and an after dev server.", + "scripts": { "render": "node render.mjs" }, + "dependencies": { "playwright": "1.48.2" } +} diff --git a/frontend/scripts/d150-renders/render.mjs b/frontend/scripts/d150-renders/render.mjs new file mode 100644 index 0000000000..01800f73b1 --- /dev/null +++ b/frontend/scripts/d150-renders/render.mjs @@ -0,0 +1,127 @@ +// Route-mocked renders for the D150 helper-text cut (TASK-588). +// +// Usage: node render.mjs --base http://localhost:5173 --label after --out ./renders +// +// Every /api/* call is answered from the fixtures below, so the signed-in BYO +// pages render without a backend and without real pod data. Names are sample +// names only (Alex, Maya, Jo); the token is an unsigned fixture the frontend +// only decodes for its expiry. Nothing here is app code. +import { chromium } from 'playwright'; +import { mkdirSync } from 'node:fs'; +import { join } from 'node:path'; + +const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => { + if (a.startsWith('--')) acc.push([a.slice(2), all[i + 1]]); + return acc; +}, [])); +const base = args.base || 'http://localhost:5173'; +const label = args.label || 'run'; +const outDir = args.out || './renders'; +const chromiumPath = process.env.PW_CHROMIUM_PATH || undefined; + +const b64url = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url'); +// exp must be near-term: the app schedules a refresh at exp - 5min, and a delay +// past setTimeout's ~24.8-day ceiling fires at once and drops the session. +const FIXTURE_EXP = Math.floor(Date.now() / 1000) + 6 * 60 * 60; +const FIXTURE_TOKEN = `${b64url({ alg: 'none', typ: 'JWT' })}.${b64url({ sub: 'fixture-alex', exp: FIXTURE_EXP })}.`; + +const USER = { _id: 'fixture-alex', username: 'alex', displayName: 'Alex', email: 'alex@example.test', role: 'user' }; +const POD = { _id: 'fixture-pod', id: 'fixture-pod', name: 'Launch room', type: 'chat', members: ['fixture-alex'], createdBy: 'fixture-alex' }; + +// Path → JSON. Anything unlisted gets {} so an unexpected call cannot crash the page. +const FIXTURES = [ + [/^\/api\/auth\/user$/, USER], + [/^\/api\/pods$/, [POD]], + [/^\/api\/hosted\/availability$/, { configured: true, caps: { agentsPerUser: 1, turnsPerDay: 200 } }], + [/^\/api\/machines$/, { machines: [] }], + [/^\/api\/registry\/pods\/[^/]+\/agents$/, { agents: [] }], + [/^\/api\/registry\/agents\/next-free-name/, { name: 'alex-agent' }], + [/^\/api\/users\/me\/agent-connection$/, {}], + [/^\/api\/pods\/fixture-pod$/, POD], + [/^\/api\/messages\//, []], + [/^\/api\/posts/, []], + [/^\/api\/activity\/decision-queue/, []], + [/^\/api\/pg\/status$/, { ok: true }], +]; + +const fixtureFor = (path) => { + const hit = FIXTURES.find(([re]) => re.test(path)); + return hit ? hit[1] : {}; +}; + +const VIEWPORTS = [ + { name: '390', width: 390, height: 844 }, + { name: '1440', width: 1440, height: 900 }, +]; + +// Each capture is a route the BYO work touches. The invite modal is opened +// from the pod page; its trigger is resolved by text inside the harness. +const SHOTS = [ + { name: 'byo', path: '/v2/agents/byo' }, + { name: 'byo-hosted', path: '/v2/agents/byo?mode=hosted' }, + { name: 'invite', path: '/v2/pods/fixture-pod', openInvite: true }, +]; + +const run = async () => { + mkdirSync(outDir, { recursive: true }); + const browser = await chromium.launch({ headless: true, executablePath: chromiumPath }); + try { + for (const vp of VIEWPORTS) { + for (const shot of SHOTS) { + // The inspector (and its invite control) is not rendered at the 390 width, + // so the invite modal is captured at 1440 only until a mobile trigger is verified. + if (shot.openInvite && vp.name === '390') { + console.log('skip invite at 390: no verified mobile trigger'); + continue; + } + const context = await browser.newContext({ + viewport: { width: vp.width, height: vp.height }, + reducedMotion: 'reduce', + deviceScaleFactor: 1, + }); + await context.addInitScript((token) => { + localStorage.setItem('token', token); + }, FIXTURE_TOKEN); + const page = await context.newPage(); + // The app calls its API on another origin (localhost:5000 in dev), so every + // answer carries CORS headers and preflights are answered here. + const cors = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Allow-Methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS', + }; + await page.route('**/api/**', async (route) => { + if (route.request().method() === 'OPTIONS') { + await route.fulfill({ status: 204, headers: cors }); + return; + } + const url = new URL(route.request().url()); + await route.fulfill({ + status: 200, + headers: cors, + contentType: 'application/json', + body: JSON.stringify(fixtureFor(url.pathname)), + }); + }); + await page.goto(`${base}${shot.path}`, { waitUntil: 'networkidle' }); + await page.addStyleTag({ content: '*,*::before,*::after{transition:none!important;animation:none!important;caret-color:transparent!important}' }); + if (shot.openInvite) { + // Opened from the pod inspector's "members" control (V2Inspector onOpenInvite). + await page.getByRole('button', { name: /^members$/i }).first().click(); + } + await page.waitForTimeout(400); + const file = join(outDir, `${label}-${shot.name}-${vp.name}.png`); + await page.screenshot({ path: file, fullPage: true }); + console.log(`wrote ${file}`); + await context.close(); + } + } + } finally { + await browser.close(); + } +}; + +run().catch((e) => { + console.error(e); + process.exit(1); +}); From d794a322b90802ea01d40d4e7ec9d948d4e8ff45 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:53:47 -0700 Subject: [PATCH 5/8] Stretch BYO mode cards and guard the compact hosted lead With only the title left in each mode card, the cards sized to their text and the chooser collapsed. Stretch them across their cell and pin the rule in the layout-invariant test. The hosted lead interpolated caps before availability loaded, so it read 'Beta: hosted agent' until the call returned; use it only once hosting is configured. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/v2/__tests__/v2-layout-invariants.test.ts | 3 +++ frontend/src/v2/components/V2AgentBYO.tsx | 2 +- frontend/src/v2/v2.css | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index a2735b56d4..31a678ab93 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -3193,6 +3193,9 @@ describe('Bring your own agent onto Signal (TASK-166)', () => { // The 1px-per-side compensation is derived from the base shorthand, so it is // pinned to it rather than restated as a magic number. expect(base.padding).toBe('16px 18px 14px'); + // With only the title left, a card sizes to its text unless it stretches + // across its cell (the chooser collapsed at 1440, 720 and 390 without this). + expect(base['align-self']).toBe('stretch'); const pressed = cssDeclarations( ruleBody(v2, '.v2-root button.v2-byo__mode[aria-pressed="true"]'), diff --git a/frontend/src/v2/components/V2AgentBYO.tsx b/frontend/src/v2/components/V2AgentBYO.tsx index bac31d7724..2e5d1b1b25 100644 --- a/frontend/src/v2/components/V2AgentBYO.tsx +++ b/frontend/src/v2/components/V2AgentBYO.tsx @@ -1112,7 +1112,7 @@ const V2AgentBYO: React.FC = () => { })()} hint={(() => { if (issued) return undefined; - if (showCompactHosted) return t('agentByo.compact.lead',{ agents: hosting?.caps.agentsPerUser, turns: hosting?.caps.turnsPerDay }); + if (showCompactHosted && hosting?.configured) return t('agentByo.compact.lead',{ agents: hosting?.caps.agentsPerUser, turns: hosting?.caps.turnsPerDay }); if (showCompactByo) return t('agentByo.compactByo.lead'); return t('agentByo.description'); })()} diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index d88f5c80fe..a4cf11cd9a 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -6480,6 +6480,7 @@ body.modern-ui.v2-canvas { display: flex; flex-direction: column; align-items: flex-start; + align-self: stretch; gap: 5px; text-align: left; padding: 16px 18px 14px; From 2a7abd0bcf586c496898ad1f85229b450d9c5d78 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:54:39 -0700 Subject: [PATCH 6/8] Fix the snippet body wording and test the unconfigured hosted lead claudeCodeBody read as if the agent would not answer @mentions; the limit is that the command above is needed for that. Use Commonly's tools in both locales, matching the sibling keys. Pins the compact hosted lead to never show undefined caps when hosting is not configured. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/i18n/locales/en.json | 2 +- frontend/src/i18n/locales/zh-CN.json | 2 +- frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 629cd8340c..643d312f1e 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1681,7 +1681,7 @@ "cursorInstall": "Add to Cursor", "cursorInstallHint": "The link adds Commonly with a placeholder key. Replace it in Cursor’s MCP settings with the key above.", "memory": "Bring its memory (optional)", - "claudeCodeBody": "Gives your Claude Code sessions the pod’s tools. It won’t answer @mentions.", + "claudeCodeBody": "Gives your Claude Code sessions Commonly’s tools. Answering @mentions needs the command above.", "claudeCodeBodyOwn": "Gives your own Claude Code sessions Commonly’s tools.", "claudeCodeBodyWindows": "Paste both lines in PowerShell. They give your own Claude Code sessions on this machine Commonly’s tools.", "claudeCodeMain": "Let Claude Code use the pod in your own sessions" diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 1912a79119..0e0d258ac4 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -1740,7 +1740,7 @@ "cursorInstall": "添加到 Cursor", "cursorInstallHint": "该链接会以占位密钥添加 Commonly。请在 Cursor 的 MCP 设置中将其替换为上面的密钥。", "memory": "导入它的记忆(可选)", - "claudeCodeBody": "为你的 Claude Code 会话加上 Commonly 的工具。它不会回复 @提及。", + "claudeCodeBody": "为你的 Claude Code 会话加上 Commonly 的工具。回复 @提及需要上面的命令。", "claudeCodeBodyOwn": "为你自己的 Claude Code 会话加上 Commonly 的工具。", "claudeCodeBodyWindows": "两行都粘贴到 PowerShell 里运行。这样你在这台电脑上的 Claude Code 会话就能用上 Commonly 的工具。", "claudeCodeMain": "让 Claude Code 在你自己的会话里使用这个 Pod" diff --git a/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx index bbe48cea6c..a7db131b17 100644 --- a/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx @@ -243,6 +243,18 @@ describe('V2AgentBYO — Run it here', () => { window.history.pushState({}, '', '/'); }); + // Before availability resolves, or when hosting is not configured, the lead + // must not interpolate caps: the (i) would read "Beta: undefined ...". + test('the compact hosted lead never shows undefined caps when hosting is not configured', async () => { + mockGet({ configured: false, status: { runtime: { lastPollAt: 1 } } }); + renderAt('/v2/agents/byo?pod=p1&mode=hosted'); + + await screen.findByTestId('byo-hosted-compact'); + expect(screen.queryByText(/undefined/)).not.toBeInTheDocument(); + expect(screen.queryByText(/Beta:/)).not.toBeInTheDocument(); + window.history.pushState({}, '', '/'); + }); + test('"Other ways to connect" opens the full page with Run it here still chosen', async () => { mockGet({ configured: true, status: { runtime: { lastPollAt: 1 } } }); renderAt('/v2/agents/byo?pod=p1&mode=hosted'); From d03f1a5f3bde239d969a011cdf0c2f7204a36d5e Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:58:08 -0700 Subject: [PATCH 7/8] Move the D150 render harness to its own branch The one-off route-mock harness is kept on task-588-renders with the renders it produced, not on main. See the PR body for the branch and SHA. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/scripts/d150-renders/package.json | 8 -- frontend/scripts/d150-renders/render.mjs | 127 --------------------- 2 files changed, 135 deletions(-) delete mode 100644 frontend/scripts/d150-renders/package.json delete mode 100644 frontend/scripts/d150-renders/render.mjs diff --git a/frontend/scripts/d150-renders/package.json b/frontend/scripts/d150-renders/package.json deleted file mode 100644 index 8109e75505..0000000000 --- a/frontend/scripts/d150-renders/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "commonly-d150-renders", - "private": true, - "type": "module", - "description": "Route-mocked 390/1440 renders for the D150 helper-text cut (TASK-588). Run against a before and an after dev server.", - "scripts": { "render": "node render.mjs" }, - "dependencies": { "playwright": "1.48.2" } -} diff --git a/frontend/scripts/d150-renders/render.mjs b/frontend/scripts/d150-renders/render.mjs deleted file mode 100644 index 01800f73b1..0000000000 --- a/frontend/scripts/d150-renders/render.mjs +++ /dev/null @@ -1,127 +0,0 @@ -// Route-mocked renders for the D150 helper-text cut (TASK-588). -// -// Usage: node render.mjs --base http://localhost:5173 --label after --out ./renders -// -// Every /api/* call is answered from the fixtures below, so the signed-in BYO -// pages render without a backend and without real pod data. Names are sample -// names only (Alex, Maya, Jo); the token is an unsigned fixture the frontend -// only decodes for its expiry. Nothing here is app code. -import { chromium } from 'playwright'; -import { mkdirSync } from 'node:fs'; -import { join } from 'node:path'; - -const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => { - if (a.startsWith('--')) acc.push([a.slice(2), all[i + 1]]); - return acc; -}, [])); -const base = args.base || 'http://localhost:5173'; -const label = args.label || 'run'; -const outDir = args.out || './renders'; -const chromiumPath = process.env.PW_CHROMIUM_PATH || undefined; - -const b64url = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url'); -// exp must be near-term: the app schedules a refresh at exp - 5min, and a delay -// past setTimeout's ~24.8-day ceiling fires at once and drops the session. -const FIXTURE_EXP = Math.floor(Date.now() / 1000) + 6 * 60 * 60; -const FIXTURE_TOKEN = `${b64url({ alg: 'none', typ: 'JWT' })}.${b64url({ sub: 'fixture-alex', exp: FIXTURE_EXP })}.`; - -const USER = { _id: 'fixture-alex', username: 'alex', displayName: 'Alex', email: 'alex@example.test', role: 'user' }; -const POD = { _id: 'fixture-pod', id: 'fixture-pod', name: 'Launch room', type: 'chat', members: ['fixture-alex'], createdBy: 'fixture-alex' }; - -// Path → JSON. Anything unlisted gets {} so an unexpected call cannot crash the page. -const FIXTURES = [ - [/^\/api\/auth\/user$/, USER], - [/^\/api\/pods$/, [POD]], - [/^\/api\/hosted\/availability$/, { configured: true, caps: { agentsPerUser: 1, turnsPerDay: 200 } }], - [/^\/api\/machines$/, { machines: [] }], - [/^\/api\/registry\/pods\/[^/]+\/agents$/, { agents: [] }], - [/^\/api\/registry\/agents\/next-free-name/, { name: 'alex-agent' }], - [/^\/api\/users\/me\/agent-connection$/, {}], - [/^\/api\/pods\/fixture-pod$/, POD], - [/^\/api\/messages\//, []], - [/^\/api\/posts/, []], - [/^\/api\/activity\/decision-queue/, []], - [/^\/api\/pg\/status$/, { ok: true }], -]; - -const fixtureFor = (path) => { - const hit = FIXTURES.find(([re]) => re.test(path)); - return hit ? hit[1] : {}; -}; - -const VIEWPORTS = [ - { name: '390', width: 390, height: 844 }, - { name: '1440', width: 1440, height: 900 }, -]; - -// Each capture is a route the BYO work touches. The invite modal is opened -// from the pod page; its trigger is resolved by text inside the harness. -const SHOTS = [ - { name: 'byo', path: '/v2/agents/byo' }, - { name: 'byo-hosted', path: '/v2/agents/byo?mode=hosted' }, - { name: 'invite', path: '/v2/pods/fixture-pod', openInvite: true }, -]; - -const run = async () => { - mkdirSync(outDir, { recursive: true }); - const browser = await chromium.launch({ headless: true, executablePath: chromiumPath }); - try { - for (const vp of VIEWPORTS) { - for (const shot of SHOTS) { - // The inspector (and its invite control) is not rendered at the 390 width, - // so the invite modal is captured at 1440 only until a mobile trigger is verified. - if (shot.openInvite && vp.name === '390') { - console.log('skip invite at 390: no verified mobile trigger'); - continue; - } - const context = await browser.newContext({ - viewport: { width: vp.width, height: vp.height }, - reducedMotion: 'reduce', - deviceScaleFactor: 1, - }); - await context.addInitScript((token) => { - localStorage.setItem('token', token); - }, FIXTURE_TOKEN); - const page = await context.newPage(); - // The app calls its API on another origin (localhost:5000 in dev), so every - // answer carries CORS headers and preflights are answered here. - const cors = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Headers': '*', - 'Access-Control-Allow-Methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS', - }; - await page.route('**/api/**', async (route) => { - if (route.request().method() === 'OPTIONS') { - await route.fulfill({ status: 204, headers: cors }); - return; - } - const url = new URL(route.request().url()); - await route.fulfill({ - status: 200, - headers: cors, - contentType: 'application/json', - body: JSON.stringify(fixtureFor(url.pathname)), - }); - }); - await page.goto(`${base}${shot.path}`, { waitUntil: 'networkidle' }); - await page.addStyleTag({ content: '*,*::before,*::after{transition:none!important;animation:none!important;caret-color:transparent!important}' }); - if (shot.openInvite) { - // Opened from the pod inspector's "members" control (V2Inspector onOpenInvite). - await page.getByRole('button', { name: /^members$/i }).first().click(); - } - await page.waitForTimeout(400); - const file = join(outDir, `${label}-${shot.name}-${vp.name}.png`); - await page.screenshot({ path: file, fullPage: true }); - console.log(`wrote ${file}`); - await context.close(); - } - } - } finally { - await browser.close(); - } -}; - -run().catch((e) => { - console.error(e); - process.exit(1); -}); From ca61901b07b54e3a7f6b0208342d56446ba0ff93 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 31 Oct 2026 01:16:30 -0700 Subject: [PATCH 8/8] Restore hosted offer line and compact head guard (Vera HOLD on #2943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Put "Start a new hosted agent in this pod" back in agentByo.hosted.offerBody (en and zh-CN, matching main). The offer must say the hosted start is new, since "Run it here instead" allocates a fresh name and takes the one slot. - Guard the compact head lead with !hosted, matching the title guard, so a hosted start doesn't keep "Live in a few seconds. Beta: ..." above the live line. Written by Kite, a Commonly agent — https://commonly.me/v2/pods/6a692a1be833c668acdb84cf Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/i18n/locales/en.json | 2 +- frontend/src/i18n/locales/zh-CN.json | 2 +- frontend/src/v2/components/V2AgentBYO.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 643d312f1e..fc064be479 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1746,7 +1746,7 @@ }, "hosted": { "offerAction": "Run it here instead", - "offerBody": "No terminal needed.", + "offerBody": "No terminal needed. Start a new hosted agent in this pod.", "heading": "Running:", "live": "{{name}} is live in {{pod}}. Mention it there and it answers.", "starting": "Starting {{name}}…", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 0e0d258ac4..73513cecd1 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -1805,7 +1805,7 @@ }, "hosted": { "offerAction": "改为在这里运行", - "offerBody": "无需终端。", + "offerBody": "无需终端。在这个 Pod 中启动一个新的托管智能体。", "heading": "运行中:", "live": "{{name}} 已在 {{pod}} 上线。在那里 @ 它即可得到回复。", "starting": "正在启动 {{name}}…", diff --git a/frontend/src/v2/components/V2AgentBYO.tsx b/frontend/src/v2/components/V2AgentBYO.tsx index 2e5d1b1b25..8eb80142f8 100644 --- a/frontend/src/v2/components/V2AgentBYO.tsx +++ b/frontend/src/v2/components/V2AgentBYO.tsx @@ -1112,7 +1112,7 @@ const V2AgentBYO: React.FC = () => { })()} hint={(() => { if (issued) return undefined; - if (showCompactHosted && hosting?.configured) return t('agentByo.compact.lead',{ agents: hosting?.caps.agentsPerUser, turns: hosting?.caps.turnsPerDay }); + if (showCompactHosted && !hosted && hosting?.configured) return t('agentByo.compact.lead',{ agents: hosting?.caps.agentsPerUser, turns: hosting?.caps.turnsPerDay }); if (showCompactByo) return t('agentByo.compactByo.lead'); return t('agentByo.description'); })()}