diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/README.md b/docs/design/evidence/byo-hosted-offer-2026-10-24/README.md
new file mode 100644
index 0000000000..45624455af
--- /dev/null
+++ b/docs/design/evidence/byo-hosted-offer-2026-10-24/README.md
@@ -0,0 +1,41 @@
+# Stalled BYO hosted bar
+
+TASK-482/483, 2026-10-24. Real isolated Chromium with synthetic API responses
+and the literal `fixture-only-key`. No real user session, credential, or
+production write is used.
+
+Lily's 16:28Z ruling supersedes the earlier in-flow placement: the offer is a
+sticky bottom bar with an ink action, a top border and no shadow. Its 96px desktop
+or 128px phone height is reserved from the first render; a negative top margin
+means inserting the bar adds no flow height. The initial content pane padding
+includes that same height, so full scroll reveals the final control above it.
+
+Each language (`en`, `zh-CN`) and viewport (`1440×1000`, `390×844`) has:
+
+- `offer-*.png`: the bar visible immediately at the initial scroll position.
+- `full-scroll-*.png`: the last existing control remains above the bar.
+- `copied-*.png`: command copy dismissed the offer.
+- `reload-*.png`: same-tab reload, reissue/render the same agent's command,
+ advance past 30 seconds; the offer remains absent.
+- `result-*.json`: bar/button dimensions, scroll/client widths, fixed event
+ names and browser errors. Phone captures advance the browser clock; desktop
+ captures use top-edge exit intent.
+
+`command-tops-*.json` and `sticky-clearance-*.json` come from
+`e2e/byo-offer-layout.spec.ts`. At 1440×1000 and 390×844, all four commands,
+the memory textarea, final CTA row and both final buttons retain identical tops
+when the bar appears. Memory and the final buttons are fully on screen while
+the timer fires. The bar stays in view at the top and bottom of the pane; at
+full scroll the last button clears its top edge. Reserved height equals measured
+bar height both before key issuance and after the bar appears.
+
+Run against the normal frontend dev server from the repository root:
+`E2E_BASE_URL=http://localhost:3000 npx playwright test e2e/byo-offer-layout.spec.ts`.
+All API responses and authentication are fixtures; no backend is required.
+
+Chinese action: `改为在这里运行`.
+Chinese supporting text: `无需终端。在这个 Pod 中启动一个新的托管智能体。`
+
+### Provision error at 390 px
+
+`provision-error-en-390.png` shows the intercepted 503 response with a two-line error. The sticky bar uses a minimum height and grows for error text; it has no nested scroller. The Chromium spec reproduced clipping before the CSS fix and passes after it, including the existing stationary controls, viewport and full-scroll clearance assertions.
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/command-tops-1440.json b/docs/design/evidence/byo-hosted-offer-2026-10-24/command-tops-1440.json
new file mode 100644
index 0000000000..a5343c8a8e
--- /dev/null
+++ b/docs/design/evidence/byo-hosted-offer-2026-10-24/command-tops-1440.json
@@ -0,0 +1,27 @@
+{
+ "width": 1440,
+ "before": [
+ -595,
+ -258,
+ -61,
+ 77
+ ],
+ "after": [
+ -595,
+ -258,
+ -61,
+ 77
+ ],
+ "controlsBefore": [
+ 512,
+ 703,
+ 703,
+ 786
+ ],
+ "controlsAfter": [
+ 512,
+ 703,
+ 703,
+ 786
+ ]
+}
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/command-tops-390.json b/docs/design/evidence/byo-hosted-offer-2026-10-24/command-tops-390.json
new file mode 100644
index 0000000000..7383ad54a3
--- /dev/null
+++ b/docs/design/evidence/byo-hosted-offer-2026-10-24/command-tops-390.json
@@ -0,0 +1,27 @@
+{
+ "width": 390,
+ "before": [
+ -1168,
+ -591,
+ -314,
+ -136
+ ],
+ "after": [
+ -1168,
+ -591,
+ -314,
+ -136
+ ],
+ "controlsBefore": [
+ 399,
+ 621,
+ 621,
+ 724
+ ],
+ "controlsAfter": [
+ 399,
+ 621,
+ 621,
+ 724
+ ]
+}
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/copied-en-1440.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/copied-en-1440.png
new file mode 100644
index 0000000000..f3b9eb5e73
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/copied-en-1440.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/copied-en-390.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/copied-en-390.png
new file mode 100644
index 0000000000..58270f8660
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/copied-en-390.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/copied-zh-CN-1440.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/copied-zh-CN-1440.png
new file mode 100644
index 0000000000..7437994b13
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/copied-zh-CN-1440.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/copied-zh-CN-390.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/copied-zh-CN-390.png
new file mode 100644
index 0000000000..27cd52b712
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/copied-zh-CN-390.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/full-scroll-en-1440.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/full-scroll-en-1440.png
new file mode 100644
index 0000000000..117ba5fbb0
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/full-scroll-en-1440.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/full-scroll-en-390.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/full-scroll-en-390.png
new file mode 100644
index 0000000000..c1d816b59b
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/full-scroll-en-390.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/full-scroll-zh-CN-1440.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/full-scroll-zh-CN-1440.png
new file mode 100644
index 0000000000..fdc0f271e7
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/full-scroll-zh-CN-1440.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/full-scroll-zh-CN-390.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/full-scroll-zh-CN-390.png
new file mode 100644
index 0000000000..2698db4027
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/full-scroll-zh-CN-390.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/offer-en-1440.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/offer-en-1440.png
new file mode 100644
index 0000000000..4d019eb241
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/offer-en-1440.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/offer-en-390.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/offer-en-390.png
new file mode 100644
index 0000000000..3e60d86f36
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/offer-en-390.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/offer-zh-CN-1440.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/offer-zh-CN-1440.png
new file mode 100644
index 0000000000..a864cb2db1
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/offer-zh-CN-1440.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/offer-zh-CN-390.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/offer-zh-CN-390.png
new file mode 100644
index 0000000000..611b83e59c
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/offer-zh-CN-390.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/provision-error-en-390.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/provision-error-en-390.png
new file mode 100644
index 0000000000..bfe86afac8
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/provision-error-en-390.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/reload-en-1440.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/reload-en-1440.png
new file mode 100644
index 0000000000..c391ac32d0
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/reload-en-1440.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/reload-en-390.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/reload-en-390.png
new file mode 100644
index 0000000000..58e2f29cb7
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/reload-en-390.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/reload-zh-CN-1440.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/reload-zh-CN-1440.png
new file mode 100644
index 0000000000..15f3388e07
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/reload-zh-CN-1440.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/reload-zh-CN-390.png b/docs/design/evidence/byo-hosted-offer-2026-10-24/reload-zh-CN-390.png
new file mode 100644
index 0000000000..de34fbe332
Binary files /dev/null and b/docs/design/evidence/byo-hosted-offer-2026-10-24/reload-zh-CN-390.png differ
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/result-en-1440.json b/docs/design/evidence/byo-hosted-offer-2026-10-24/result-en-1440.json
new file mode 100644
index 0000000000..87d006d5d8
--- /dev/null
+++ b/docs/design/evidence/byo-hosted-offer-2026-10-24/result-en-1440.json
@@ -0,0 +1,36 @@
+{
+ "lang": "en",
+ "width": 1440,
+ "metrics": {
+ "buttonHeight": 40,
+ "buttonWidth": 160.09375,
+ "rowWidth": 1240,
+ "barHeight": 96,
+ "position": "sticky",
+ "bg": "rgb(16, 24, 40)",
+ "afterCommand": true,
+ "scrollWidth": 1440,
+ "clientWidth": 1440
+ },
+ "copySuppressed": true,
+ "reloadSuppressed": true,
+ "phoneTimerOnly": false,
+ "events": [
+ {
+ "step": "page-viewed"
+ },
+ {
+ "step": "seat-created"
+ },
+ {
+ "step": "cli-command-copied"
+ },
+ {
+ "step": "page-viewed"
+ },
+ {
+ "step": "seat-created"
+ }
+ ],
+ "errors": []
+}
\ No newline at end of file
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/result-en-390.json b/docs/design/evidence/byo-hosted-offer-2026-10-24/result-en-390.json
new file mode 100644
index 0000000000..f7e64f2ca3
--- /dev/null
+++ b/docs/design/evidence/byo-hosted-offer-2026-10-24/result-en-390.json
@@ -0,0 +1,36 @@
+{
+ "lang": "en",
+ "width": 390,
+ "metrics": {
+ "buttonHeight": 44,
+ "buttonWidth": 290,
+ "rowWidth": 322,
+ "barHeight": 128,
+ "position": "sticky",
+ "bg": "rgb(16, 24, 40)",
+ "afterCommand": true,
+ "scrollWidth": 390,
+ "clientWidth": 390
+ },
+ "copySuppressed": true,
+ "reloadSuppressed": true,
+ "phoneTimerOnly": true,
+ "events": [
+ {
+ "step": "page-viewed"
+ },
+ {
+ "step": "seat-created"
+ },
+ {
+ "step": "cli-command-copied"
+ },
+ {
+ "step": "page-viewed"
+ },
+ {
+ "step": "seat-created"
+ }
+ ],
+ "errors": []
+}
\ No newline at end of file
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/result-zh-CN-1440.json b/docs/design/evidence/byo-hosted-offer-2026-10-24/result-zh-CN-1440.json
new file mode 100644
index 0000000000..a2aae8101b
--- /dev/null
+++ b/docs/design/evidence/byo-hosted-offer-2026-10-24/result-zh-CN-1440.json
@@ -0,0 +1,36 @@
+{
+ "lang": "zh-CN",
+ "width": 1440,
+ "metrics": {
+ "buttonHeight": 40,
+ "buttonWidth": 133.96875,
+ "rowWidth": 1240,
+ "barHeight": 96,
+ "position": "sticky",
+ "bg": "rgb(16, 24, 40)",
+ "afterCommand": true,
+ "scrollWidth": 1440,
+ "clientWidth": 1440
+ },
+ "copySuppressed": true,
+ "reloadSuppressed": true,
+ "phoneTimerOnly": false,
+ "events": [
+ {
+ "step": "page-viewed"
+ },
+ {
+ "step": "seat-created"
+ },
+ {
+ "step": "cli-command-copied"
+ },
+ {
+ "step": "page-viewed"
+ },
+ {
+ "step": "seat-created"
+ }
+ ],
+ "errors": []
+}
\ No newline at end of file
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/result-zh-CN-390.json b/docs/design/evidence/byo-hosted-offer-2026-10-24/result-zh-CN-390.json
new file mode 100644
index 0000000000..1ee51e661a
--- /dev/null
+++ b/docs/design/evidence/byo-hosted-offer-2026-10-24/result-zh-CN-390.json
@@ -0,0 +1,36 @@
+{
+ "lang": "zh-CN",
+ "width": 390,
+ "metrics": {
+ "buttonHeight": 44,
+ "buttonWidth": 290,
+ "rowWidth": 322,
+ "barHeight": 128,
+ "position": "sticky",
+ "bg": "rgb(16, 24, 40)",
+ "afterCommand": true,
+ "scrollWidth": 390,
+ "clientWidth": 390
+ },
+ "copySuppressed": true,
+ "reloadSuppressed": true,
+ "phoneTimerOnly": true,
+ "events": [
+ {
+ "step": "page-viewed"
+ },
+ {
+ "step": "seat-created"
+ },
+ {
+ "step": "cli-command-copied"
+ },
+ {
+ "step": "page-viewed"
+ },
+ {
+ "step": "seat-created"
+ }
+ ],
+ "errors": []
+}
\ No newline at end of file
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/sticky-clearance-1440.json b/docs/design/evidence/byo-hosted-offer-2026-10-24/sticky-clearance-1440.json
new file mode 100644
index 0000000000..c2ce2f73b4
--- /dev/null
+++ b/docs/design/evidence/byo-hosted-offer-2026-10-24/sticky-clearance-1440.json
@@ -0,0 +1,7 @@
+{
+ "width": 1440,
+ "barHeight": 96,
+ "reservedHeight": 96,
+ "barTop": 889,
+ "lastControlBottom": 825
+}
diff --git a/docs/design/evidence/byo-hosted-offer-2026-10-24/sticky-clearance-390.json b/docs/design/evidence/byo-hosted-offer-2026-10-24/sticky-clearance-390.json
new file mode 100644
index 0000000000..9e1adbabaa
--- /dev/null
+++ b/docs/design/evidence/byo-hosted-offer-2026-10-24/sticky-clearance-390.json
@@ -0,0 +1,7 @@
+{
+ "width": 390,
+ "barHeight": 128,
+ "reservedHeight": 128,
+ "barTop": 716,
+ "lastControlBottom": 652
+}
diff --git a/docs/development/byo-funnel-telemetry.md b/docs/development/byo-funnel-telemetry.md
new file mode 100644
index 0000000000..1fd545fe19
--- /dev/null
+++ b/docs/development/byo-funnel-telemetry.md
@@ -0,0 +1,39 @@
+# BYO funnel events
+
+The authenticated BYO page posts fixed event names to `POST /api/stats/byo-step`.
+The endpoint stores `userId`, `step`, and server time in `byo_telemetry`.
+Commands, keys, names, and clipboard contents are never sent.
+
+As of TASK-483 (2026-10-24), the browser sends:
+
+| Event | Observed state |
+| --- | --- |
+| `page-viewed` | An authenticated user mounted the BYO page, including its hosted entry variant. Once per mount, including React StrictMode effect replay. |
+| `seat-created` | A runtime key was issued and its command rendered. Once per issued command. This is the command-exposure denominator. |
+| `mcp-command-copied` | The Claude or Cursor copy button successfully wrote the command. |
+| `cli-command-copied` | The listener copy button successfully wrote the command. |
+| `token-copied` | The token copy button successfully wrote the key. |
+| `listen-confirmed` | The connection poll observed this agent using its key after issuance (with existing clock-skew allowance). |
+| `listen-timeout` | The page's connection polling window expired. |
+
+Before TASK-483, the client never emitted `page-viewed` or `seat-created`.
+Historical counts of those events cannot measure page or command exposure.
+A selected-text copy from a command or key block dismisses the hosted offer but does not claim that a complete
+command was copied in telemetry. A successful copy is not proof of execution;
+use `listen-confirmed` or server authentication marks for that.
+
+TASK-482 offers a new hosted agent in the same pod after 30 seconds with an
+issued command and no copy, or desktop (over 760px) exit through the top edge. At 390px only the timer triggers it. The offer is a sticky bottom bar with an ink action, a top border and a status announcement. Its height is reserved from the first render, and its negative margin adds no flow height when it appears. It is shown once per owner/agent/pod panel per browser-tab session; a prior display or copy suppresses it after reload. Only a non-secret seen marker is stored in sessionStorage. Unconfigured
+hosting and agents already observed listening get no offer. Existing BYO identity
+and credentials are retained. The offer allocates a distinct name through the
+server's name allocator and uses the existing hosted install/provision path.
+Hosting errors preserve the command and expose a retry beside the offer. The allocated hosted name is retained for the issued key, so a provision failure retries the existing installation instead of allocating another cap slot.
+
+The placement invariant is checked in real Chromium by
+`e2e/byo-offer-layout.spec.ts`: with all command blocks expanded, each block's
+viewport top must be identical before and after the offer appears. The memory
+textarea, final action row and both action buttons must also stay stationary,
+with those controls fully in the viewport while the timer fires. The bar must
+remain visible at both scroll extremes and must not cover the last control at
+full scroll; the reserved height must equal its measured height. The DOM order
+is also pinned in the BYO render and v2 layout-invariant Jest tests.
diff --git a/e2e/byo-offer-layout.spec.ts b/e2e/byo-offer-layout.spec.ts
new file mode 100644
index 0000000000..1861b354d3
--- /dev/null
+++ b/e2e/byo-offer-layout.spec.ts
@@ -0,0 +1,107 @@
+import { test, expect } from '@playwright/test';
+
+// Real layout with a synthetic user and intercepted APIs; no backend write or
+// runtime credential is needed. Run against the frontend's normal dev server.
+for (const width of [1440, 390]) {
+ test(`BYO offer leaves existing commands and controls stationary at ${width}px`, async ({ page }, testInfo) => {
+ await page.setViewportSize({ width, height: width === 390 ? 844 : 1000 });
+ await page.clock.install();
+ await page.addInitScript(() => {
+ localStorage.setItem('token', 'fixture-only-user');
+ localStorage.setItem('v2.lang', 'en');
+ });
+ await page.route('**/api/**', async (route) => {
+ const { pathname, searchParams } = new URL(route.request().url());
+ let data: unknown = {};
+ if (pathname === '/api/auth/user') data = { _id: 'fixture-owner', username: 'alex', email: 'fixture@example.invalid', verified: true };
+ else if (pathname === '/api/pods') data = [{ _id: 'fixture-pod', name: 'Workspace', type: 'chat', createdBy: { _id: 'fixture-owner' } }];
+ else if (pathname === '/api/hosted/availability') data = { configured: true, caps: { agentsPerUser: 1, turnsPerDay: 200 } };
+ else if (pathname === '/api/machines') data = { machines: [] };
+ else if (pathname.endsWith('next-free-name')) data = { name: searchParams.get('base') };
+ else if (pathname.includes('runtime-tokens')) data = { token: 'fixture-only-key' };
+ else if (pathname.includes('attention')) data = { items: [], count: 0 };
+ await route.fulfill({ json: data });
+ });
+ await page.goto('/v2/agents/byo');
+ const initialReservedHeight = await page.locator('.v2-byo__layout').evaluate((el) =>
+ parseFloat(getComputedStyle(el).paddingBottom) - 40);
+ await page.getByTestId('byo-mode-byo').click();
+ await page.getByRole('button', { name: 'Get my connect command', exact: true }).click();
+ const result = page.locator('.v2-byo__result');
+ await expect(result).toBeVisible();
+ // Fix the setup OS, independently of the runner's emulated user agent.
+ await page.getByRole('button', { name: 'Mac', exact: true }).click();
+ // Include commands inside Other ways: checking only the first command
+ // missed the original regression, where later copy targets moved.
+ await result.locator('details').evaluateAll((details) => details.forEach((el) => { (el as HTMLDetailsElement).open = true; }));
+ await page.evaluate(() => document.fonts.ready);
+ // Put the memory field and action buttons on screen while the timer fires.
+ await result.locator('.v2-byo__memory-text').scrollIntoViewIfNeeded();
+ await expect(result.locator('.v2-byo__memory-text')).toBeInViewport({ ratio: 1 });
+ for (const button of await result.locator('.v2-byo__cta-row button').all()) {
+ await expect(button).toBeInViewport({ ratio: 1 });
+ }
+ const commands = result.locator('.v2-byo__command');
+ await expect(commands).toHaveCount(4);
+ const before = await commands.evaluateAll((els) => els.map((el) => el.getBoundingClientRect().top));
+ const controls = result.locator('.v2-byo__memory-text, .v2-byo__cta-row, .v2-byo__cta-row button');
+ await expect(controls).toHaveCount(4);
+ const controlsBefore = await controls.evaluateAll((els) => els.map((el) => el.getBoundingClientRect().top));
+ await expect(page.locator('.v2-byo__hosted-offer')).toHaveCount(0);
+ await page.clock.fastForward(30_001);
+ await expect(page.getByRole('status', { name: 'Run it here instead' })).toBeAttached();
+ const after = await commands.evaluateAll((els) => els.map((el) => el.getBoundingClientRect().top));
+ const controlsAfter = await controls.evaluateAll((els) => els.map((el) => el.getBoundingClientRect().top));
+ await testInfo.attach('command-top-positions', {
+ body: Buffer.from(JSON.stringify({ width, before, after, controlsBefore, controlsAfter }, null, 2)),
+ contentType: 'application/json',
+ });
+ expect(after).toEqual(before);
+ expect(controlsAfter).toEqual(controlsBefore);
+ const bar = page.getByRole('status', { name: 'Run it here instead' });
+ await expect(bar).toBeInViewport({ ratio: 1 });
+ const pane = page.locator('.v2-feature__body');
+ await pane.evaluate((el) => { el.scrollTop = 0; });
+ await expect(bar).toBeInViewport({ ratio: 1 });
+ await pane.evaluate((el) => { el.scrollTop = el.scrollHeight; });
+ await expect(bar).toBeInViewport({ ratio: 1 });
+ const lastControl = result.locator('.v2-byo__cta-row button').last();
+ await expect(lastControl).toBeInViewport({ ratio: 1 });
+ const controlBox = await lastControl.boundingBox();
+ const barBox = await bar.boundingBox();
+ expect(controlBox!.y + controlBox!.height).toBeLessThanOrEqual(barBox!.y);
+ const reservedHeight = await page.locator('.v2-byo__layout').evaluate((el) =>
+ parseFloat(getComputedStyle(el).paddingBottom) - 40);
+ expect(reservedHeight).toBe(barBox!.height);
+ expect(initialReservedHeight).toBe(barBox!.height);
+ const paneBox = await pane.boundingBox();
+ expect(Math.abs(barBox!.y + barBox!.height - paneBox!.y - paneBox!.height)).toBeLessThanOrEqual(1);
+ expect(await bar.evaluate((el) => getComputedStyle(el).boxShadow)).toBe('none');
+ await testInfo.attach('sticky-bar-clearance', {
+ body: Buffer.from(JSON.stringify({ width, barHeight: barBox!.height, reservedHeight,
+ barTop: barBox!.y, lastControlBottom: controlBox!.y + controlBox!.height }, null, 2)),
+ contentType: 'application/json',
+ });
+
+ if (width === 390) {
+ const provisionError = 'The hosted service could not start this agent. Please try again in a few minutes.';
+ await page.route('**/api/registry/agents/next-free-name?*', (route) => route.fulfill({ json: { name: 'alex-hosted-agent' } }));
+ await page.route('**/api/hosted/provision', (route) => route.fulfill({ status: 503, json: { error: provisionError } }));
+ await bar.getByRole('button', { name: 'Run it here instead', exact: true }).click();
+ const error = bar.locator('.v2-byo__error');
+ await expect(error).toHaveText(provisionError);
+ await expect(error).toBeInViewport({ ratio: 1 });
+ const errorGeometry = await error.evaluate((el) => ({
+ height: el.getBoundingClientRect().height,
+ lineHeight: parseFloat(getComputedStyle(el).lineHeight),
+ }));
+ expect(errorGeometry.height).toBeGreaterThanOrEqual(errorGeometry.lineHeight * 2);
+ expect(await bar.evaluate((el) => el.scrollHeight <= el.clientHeight + 1)).toBe(true);
+ expect(await bar.evaluate((el) => getComputedStyle(el).overflowY)).toBe('visible');
+ await testInfo.attach('phone-two-line-provision-error', { body: await page.screenshot({ path: testInfo.outputPath('phone-two-line-provision-error.png') }), contentType: 'image/png' });
+ await expect(bar).toBeInViewport({ ratio: 1 });
+ await expect(bar.getByRole('button', { name: 'Run it here instead', exact: true })).toBeInViewport({ ratio: 1 });
+ }
+
+ });
+}
diff --git a/frontend/design-system/preview/components-byo-hosted-bar.html b/frontend/design-system/preview/components-byo-hosted-bar.html
new file mode 100644
index 0000000000..4bb0180428
--- /dev/null
+++ b/frontend/design-system/preview/components-byo-hosted-bar.html
@@ -0,0 +1,8 @@
+
BYO hosted bar · SignalBYO hosted alternative
Permanent bottom space equals the sticky bar height. Scroll to the final control.
Your connect command
commonly agent run my-agent
No terminal needed. Start a new hosted agent in this pod.
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index 9350553c83..67424d3ef1 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -1557,6 +1557,8 @@
"machineMeta": "Needs the Commonly daemon running there"
},
"hosted": {
+ "offerAction": "Run it here instead",
+ "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 2301cbdc4b..3a8f8dd24d 100644
--- a/frontend/src/i18n/locales/zh-CN.json
+++ b/frontend/src/i18n/locales/zh-CN.json
@@ -1617,6 +1617,8 @@
"machineMeta": "需要那台电脑运行 Commonly 守护进程"
},
"hosted": {
+ "offerAction": "改为在这里运行",
+ "offerBody": "无需终端。在这个 Pod 中启动一个新的托管智能体。",
"heading": "运行中:",
"live": "{{name}} 已在 {{pod}} 上线。在那里 @ 它即可得到回复。",
"starting": "正在启动 {{name}}…",
diff --git a/frontend/src/v2/__tests__/V2AgentBYOFootnote.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOFootnote.test.tsx
index fdc9ddabd2..46703dc364 100644
--- a/frontend/src/v2/__tests__/V2AgentBYOFootnote.test.tsx
+++ b/frontend/src/v2/__tests__/V2AgentBYOFootnote.test.tsx
@@ -1,4 +1,6 @@
// @ts-nocheck
+// Keep setup request assertions independent of funnel telemetry.
+jest.mock('../utils/byoTelemetry', () => ({ recordByoStep: jest.fn() }));
// The CLI footnote must describe the connection available after install.
// `agent init` scaffolds a Python webhook and does not connect Claude/Codex.
import React from 'react';
diff --git a/frontend/src/v2/__tests__/V2AgentBYOFromCard.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOFromCard.test.tsx
index 4938430d02..db96709fe4 100644
--- a/frontend/src/v2/__tests__/V2AgentBYOFromCard.test.tsx
+++ b/frontend/src/v2/__tests__/V2AgentBYOFromCard.test.tsx
@@ -1,4 +1,6 @@
// @ts-nocheck
+// Keep setup request assertions independent of funnel telemetry.
+jest.mock('../utils/byoTelemetry', () => ({ recordByoStep: jest.fn() }));
// QA BYO walk 2 (2026-10-16): after approving Scout's connect card, the page
// still asked "Where should it run?" and showed the name and pod fields, 127
// words before the button, most of them re-asking what the card settled.
diff --git a/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx
index e0657fcd15..45220a6f4d 100644
--- a/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx
+++ b/frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx
@@ -1,4 +1,6 @@
// @ts-nocheck
+// Keep setup request assertions independent of funnel telemetry.
+jest.mock('../utils/byoTelemetry', () => ({ recordByoStep: jest.fn() }));
// ADR-023 W2 "Run it here" on the BYO page. The instance reports whether it
// can host; when it can, the page asks where the agent should run with
// nothing preselected (ruled 2026-10-14), and choosing "Run it here" installs
diff --git a/frontend/src/v2/__tests__/V2AgentBYOHostedOffer.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOHostedOffer.test.tsx
new file mode 100644
index 0000000000..7484c32a10
--- /dev/null
+++ b/frontend/src/v2/__tests__/V2AgentBYOHostedOffer.test.tsx
@@ -0,0 +1,168 @@
+// @ts-nocheck
+// ADR-023 W2 "Run it here" on the BYO page. The instance reports whether it
+// can host; when it can, the page asks where the agent should run with
+// nothing preselected (ruled 2026-10-14), and choosing "Run it here" installs
+// with runtimeType 'hosted', asks the kernel to provision, and never renders a
+// token. When it cannot, the page is exactly the BYO page it was.
+import React, { StrictMode } from 'react';
+import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
+import { MemoryRouter, Routes, Route, useLocation } from 'react-router-dom';
+import V2AgentBYO from '../components/V2AgentBYO';
+import { AuthContext } from '../../context/AuthContext';
+
+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 pods = [{ _id: 'p1', name: 'Workspace', type: 'chat', createdBy: { _id: 'u1' } }];
+
+const mockGet = ({ configured, status }) => {
+ axios.get.mockImplementation((url) => {
+ if (url.startsWith('/api/registry/agents/next-free-name?')) return Promise.resolve({ data: { name: new URLSearchParams(url.split('?')[1]).get('base') } });
+ if (url.startsWith('/api/hosted/availability')) {
+ return Promise.resolve({ data: { configured, caps: { agentsPerUser: 1, turnsPerDay: 200 } } });
+ }
+ if (url.startsWith('/api/hosted/status')) return Promise.resolve({ data: status || { runtime: {} } });
+ if (url === '/api/pods') return Promise.resolve({ data: pods });
+ return Promise.resolve({ data: {} });
+ });
+};
+
+const renderPage = () => render(
+
+
+
+
+ ,
+);
+
+beforeEach(() => {
+ sessionStorage.clear();
+ window.matchMedia = jest.fn().mockReturnValue({ matches: true });
+ jest.useFakeTimers(); mockGet({ configured: true });
+ axios.post.mockImplementation((url) => Promise.resolve({ data: url.includes('runtime-tokens') ? { token: 'fixture-only-key' } : { ready: true } }));
+});
+afterEach(() => { jest.useRealTimers(); jest.resetAllMocks(); });
+const issue = async () => {
+ const view = renderPage(); await screen.findByTestId('byo-mode-byo'); fireEvent.click(screen.getByTestId('byo-mode-byo'));
+ await waitFor(() => expect(screen.getByRole('button', { name: /get my connect command/i })).toBeEnabled());
+ fireEvent.click(screen.getByRole('button', { name: /get my connect command/i })); await screen.findByText(/your connect command for/i);
+ return view;
+};
+const offer = () => screen.queryByRole('button', { name: 'Run it here instead' });
+const tick = async (ms) => act(async () => { jest.advanceTimersByTime(ms); });
+test('30s offer starts a distinct hosted identity in the same pod', async () => {
+ await issue(); await tick(29999); expect(offer()).not.toBeInTheDocument(); await tick(1); fireEvent.click(offer());
+ await waitFor(() => expect(axios.post).toHaveBeenCalledWith('/api/hosted/provision', { agentName: 'sam-agent' }, expect.anything()));
+ expect(axios.post).toHaveBeenCalledWith('/api/registry/install', expect.objectContaining({ agentName: 'sam-agent', podId: 'p1', config: { runtime: { runtimeType: 'hosted' } } }), expect.anything());
+ await waitFor(() => expect(screen.queryByText(/your connect command for/i)).not.toBeInTheDocument());
+});
+test('exit intent offers immediately; manual copy dismisses permanently', async () => {
+ await issue(); fireEvent.mouseLeave(document.documentElement, { clientY: 0 }); expect(offer()).toBeInTheDocument();
+ fireEvent.copy(screen.getByTestId('byo-listen').querySelector('.v2-byo__command')); await tick(60000); fireEvent.mouseLeave(document.documentElement, { clientY: 0 }); expect(offer()).not.toBeInTheDocument();
+});
+test('button copy suppresses the offer beyond the Copied label', async () => {
+ Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: jest.fn().mockResolvedValue(undefined) } });
+ await issue(); await act(async () => fireEvent.click(screen.getByTestId('byo-listen').querySelector('button.v2-byo__copy'))); await tick(60000); expect(offer()).not.toBeInTheDocument();
+ expect(axios.post).toHaveBeenCalledWith('/api/stats/byo-step', { step: 'cli-command-copied' }, expect.anything());
+});
+test('hosting errors preserve the command and a retryable offer', async () => {
+ await issue(); axios.post.mockImplementation((url) => url === '/api/registry/install' ? Promise.reject({ response: { data: { code: 'hosted_cap_reached', cap: 1 } } }) : Promise.resolve({ data: {} }));
+ await tick(30000); fireEvent.click(offer()); expect(await screen.findByRole('alert')).toHaveTextContent(/hosted/i); expect(screen.getByText(/your connect command for/i)).toBeInTheDocument(); expect(offer()).toBeEnabled();
+ expect(axios.post.mock.calls.some(([url]) => url === '/api/hosted/provision')).toBe(false);
+});
+test('page entry is recorded once under StrictMode; command rendering is separate and contains no key', async () => {
+ render();
+ await screen.findByTestId('byo-mode-byo'); fireEvent.click(screen.getByTestId('byo-mode-byo'));
+ await waitFor(() => expect(screen.getByRole('button', { name: /get my connect command/i })).toBeEnabled()); fireEvent.click(screen.getByRole('button', { name: /get my connect command/i })); await screen.findByText(/your connect command for/i);
+ expect(axios.post.mock.calls.filter(([url]) => url === '/api/stats/byo-step').map(([, body]) => body)).toEqual([{ step: 'page-viewed' }, { step: 'seat-created' }]);
+});
+test('a failed clipboard write leaves the fallback available', async () => {
+ Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: jest.fn().mockRejectedValue(new Error('Denied')) } });
+ await issue(); await act(async () => fireEvent.click(screen.getByTestId('byo-listen').querySelector('button.v2-byo__copy'))); await tick(30000); expect(offer()).toBeInTheDocument();
+});
+test('unavailable hosting never gets offered', async () => {
+ mockGet({ configured: false }); renderPage();
+ await waitFor(() => expect(screen.getByRole('button', { name: /get my connect command/i })).toBeEnabled());
+ fireEvent.click(screen.getByRole('button', { name: /get my connect command/i })); await screen.findByText(/your connect command for/i);
+ await tick(30000); fireEvent.mouseLeave(document.documentElement, { clientY: 0 }); expect(offer()).not.toBeInTheDocument();
+});
+test('an agent that connects before the deadline needs no hosted offer', async () => {
+ await issue(); const get = axios.get.getMockImplementation();
+ axios.get.mockImplementation((url) => url === '/api/users/me/agent-connection' ? Promise.resolve({ data: { connected: true, connectedAgent: { agentName: 'sam-claude' }, lastUsedAt: new Date(Date.now() + 1).toISOString() } }) : get(url));
+ await tick(4000); await screen.findByTestId('byo-listen-ok'); await tick(30000); expect(offer()).not.toBeInTheDocument();
+});
+test('double activation waits for one name lookup and one hosted provision', async () => {
+ await issue(); await tick(30000); const get = axios.get.getMockImplementation(); let resolveName;
+ axios.get.mockImplementation((url) => url.startsWith('/api/registry/agents/next-free-name?') ? new Promise(resolve => { resolveName = resolve; }) : get(url));
+ const button = offer(); fireEvent.click(button); fireEvent.click(button);
+ expect(button).toBeDisabled(); await act(async () => resolveName({ data: { name: 'sam-agent' } }));
+ await waitFor(() => expect(axios.post.mock.calls.filter(([url]) => url === '/api/hosted/provision')).toHaveLength(1));
+});
+test('name lookup failure never provisions or changes the issued command', async () => {
+ await issue(); await tick(30000); const get = axios.get.getMockImplementation();
+ axios.get.mockImplementation((url) => url.startsWith('/api/registry/agents/next-free-name?') ? Promise.reject(new Error('Offline')) : get(url));
+ fireEvent.click(offer()); expect(await screen.findByRole('alert')).toHaveTextContent(/name/i);
+ expect(screen.getByText(/your connect command for/i)).toBeInTheDocument(); expect(offer()).toBeEnabled(); expect(axios.post.mock.calls.some(([url]) => url === '/api/hosted/provision')).toBe(false);
+});
+test('offer follows every command and existing control and stays absent after remount in the same session', async () => {
+ const view = await issue(); await tick(30000);
+ const row = screen.getByRole('status', { name: 'Run it here instead' });
+ expect(row.parentElement.lastElementChild).toBe(row);
+ document.querySelectorAll('.v2-byo__command, .v2-byo__memory-text, .v2-byo__cta-row').forEach((command) => {
+ expect(command.compareDocumentPosition(row) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
+ });
+ view.unmount(); await issue(); await tick(30000); fireEvent.mouseLeave(document.documentElement, { clientY: 0 }); expect(offer()).not.toBeInTheDocument();
+});
+test('copy before the offer also suppresses it across a remount', async () => {
+ const view = await issue(); fireEvent.copy(screen.getByTestId('byo-listen').querySelector('.v2-byo__command'));
+ view.unmount(); await issue(); await tick(30000); expect(offer()).not.toBeInTheDocument();
+});
+test('phone exit does not trigger the offer; its timer still does', async () => {
+ window.matchMedia = jest.fn().mockReturnValue({ matches: false });
+ await issue(); fireEvent.mouseLeave(document.documentElement, { clientY: 0 }); expect(offer()).not.toBeInTheDocument(); await tick(30000); expect(offer()).toBeInTheDocument();
+});
+test('failed provisioning retries the same allocated hosted identity instead of consuming another cap slot', async () => {
+ await issue(); await tick(30000); const get = axios.get.getMockImplementation(); let lookups = 0; let installs = 0; let provisions = 0;
+ axios.get.mockImplementation((url) => url.startsWith('/api/registry/agents/next-free-name?') ? Promise.resolve({ data: { name: ++lookups === 1 ? 'sam-agent' : 'sam-agent-2' } }) : get(url));
+ axios.post.mockImplementation((url) => {
+ if (url === '/api/registry/install' && ++installs > 1) return Promise.reject({ response: { data: { error: 'Agent already installed in this pod' } } });
+ if (url === '/api/hosted/provision' && ++provisions === 1) return Promise.reject({ response: { data: { error: 'Runtime temporarily unreachable' } } });
+ return Promise.resolve({ data: { ready: true } });
+ });
+ fireEvent.click(offer()); await screen.findByRole('alert'); fireEvent.click(offer());
+ await waitFor(() => expect(screen.queryByText(/your connect command for/i)).not.toBeInTheDocument());
+ expect(lookups).toBe(1);
+ expect(axios.post.mock.calls.filter(([url]) => url === '/api/hosted/provision').map(([,body]) => body.agentName)).toEqual(['sam-agent', 'sam-agent']);
+});
+test('copying a heading or memory text does not count as copying the command', async () => {
+ await issue(); fireEvent.copy(screen.getByText(/your connect command for/i));
+ fireEvent.copy(document.querySelector('.v2-byo__memory-text'));
+ await tick(30000); expect(offer()).toBeInTheDocument();
+});
diff --git a/frontend/src/v2/__tests__/V2AgentBYOListenSnippet.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOListenSnippet.test.tsx
index c86e6b97aa..e21c79987b 100644
--- a/frontend/src/v2/__tests__/V2AgentBYOListenSnippet.test.tsx
+++ b/frontend/src/v2/__tests__/V2AgentBYOListenSnippet.test.tsx
@@ -1,4 +1,6 @@
// @ts-nocheck
+// Keep setup request assertions independent of funnel telemetry.
+jest.mock('../utils/byoTelemetry', () => ({ recordByoStep: jest.fn() }));
// The listener snippet is what a stranger pastes to make the agent answer
// @mentions. Every line of it has to execute: the CLI-install line was a shell
// comment until 2026-10-09, so a whole-snippet paste on a machine without the
diff --git a/frontend/src/v2/__tests__/V2AgentBYOListenVerify.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOListenVerify.test.tsx
index 652997ccd3..322c9c0b32 100644
--- a/frontend/src/v2/__tests__/V2AgentBYOListenVerify.test.tsx
+++ b/frontend/src/v2/__tests__/V2AgentBYOListenVerify.test.tsx
@@ -1,4 +1,6 @@
// @ts-nocheck
+// Keep setup request assertions independent of funnel telemetry.
+jest.mock('../utils/byoTelemetry', () => ({ recordByoStep: jest.fn() }));
// The listening-verification loop on the BYO page (#887 class, second half).
// #888 made the listening step legible; this pins the step being CHECKABLE:
// the page polls /api/users/me/agent-connection until THIS agent's runtime
diff --git a/frontend/src/v2/__tests__/V2AgentBYOMachine.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOMachine.test.tsx
index ff2c219172..c1f6e88dc9 100644
--- a/frontend/src/v2/__tests__/V2AgentBYOMachine.test.tsx
+++ b/frontend/src/v2/__tests__/V2AgentBYOMachine.test.tsx
@@ -1,4 +1,6 @@
// @ts-nocheck
+// Keep setup request assertions independent of funnel telemetry.
+jest.mock('../utils/byoTelemetry', () => ({ recordByoStep: jest.fn() }));
// ADR-026 Phase 2 slice 3: the "on my computer" path. Pinned: the mode card
// appears only when the user has daemon machines; submit installs with
// runtimeType 'wrapper' and files a placement request (never a binding); the
diff --git a/frontend/src/v2/__tests__/V2AgentBYOMemory.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOMemory.test.tsx
index 67283c8dc8..7cc779eef0 100644
--- a/frontend/src/v2/__tests__/V2AgentBYOMemory.test.tsx
+++ b/frontend/src/v2/__tests__/V2AgentBYOMemory.test.tsx
@@ -1,4 +1,6 @@
// @ts-nocheck
+// Keep setup request assertions independent of funnel telemetry.
+jest.mock('../utils/byoTelemetry', () => ({ recordByoStep: jest.fn() }));
// Retention plan Phase C: the optional "bring its memory" step on the BYO
// page. Pins the wire contract: the import uses the freshly issued
// cm_agent_* runtime token (NOT the user JWT), patch mode, sourceRuntime
diff --git a/frontend/src/v2/__tests__/V2AgentBYOName.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOName.test.tsx
index 73b48d8af5..f615ee0f61 100644
--- a/frontend/src/v2/__tests__/V2AgentBYOName.test.tsx
+++ b/frontend/src/v2/__tests__/V2AgentBYOName.test.tsx
@@ -1,4 +1,6 @@
// @ts-nocheck
+// Keep setup request assertions independent of funnel telemetry.
+jest.mock('../utils/byoTelemetry', () => ({ recordByoStep: jest.fn() }));
// The default agent name follows where it runs (C14 rehearsal, 2026-10-20).
// "Run it here" takes `-agent`. When "Use my own agent" defaulted to the
// same name, a person who had just started a hosted agent was refused at the
diff --git a/frontend/src/v2/__tests__/V2AgentBYOPersona.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOPersona.test.tsx
index b3e6206ff9..c025c2634f 100644
--- a/frontend/src/v2/__tests__/V2AgentBYOPersona.test.tsx
+++ b/frontend/src/v2/__tests__/V2AgentBYOPersona.test.tsx
@@ -1,4 +1,6 @@
// @ts-nocheck
+// Keep setup request assertions independent of funnel telemetry.
+jest.mock('../utils/byoTelemetry', () => ({ recordByoStep: jest.fn() }));
// Sam (2026-09-01): "Hire an agent" and "Add a computer" converged on this
// page with the persona silently dropped. Pinned: a ?persona= param renders
// a context card, names the agent after the persona, and the
diff --git a/frontend/src/v2/__tests__/V2AgentBYOPodPicker.test.tsx b/frontend/src/v2/__tests__/V2AgentBYOPodPicker.test.tsx
index 8a6443fa95..1e2afb9908 100644
--- a/frontend/src/v2/__tests__/V2AgentBYOPodPicker.test.tsx
+++ b/frontend/src/v2/__tests__/V2AgentBYOPodPicker.test.tsx
@@ -1,4 +1,6 @@
// @ts-nocheck
+// Keep setup request assertions independent of funnel telemetry.
+jest.mock('../utils/byoTelemetry', () => ({ recordByoStep: jest.fn() }));
// Guards the BYO pod-picker default after the 2026-07-24 launch incident:
// new users auto-join Commonly HQ (publicRead), it sorts first by activity in
// /api/pods, and the old `installablePods[0]` default silently attached
diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts
index 8134ecdf92..1a43be48e0 100644
--- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts
+++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts
@@ -4938,3 +4938,28 @@ test('TASK-443 lease typography outranks the ownership cell font size', () => {
expect(ruleBody(read('../v2.css'), '.v2-board__ownership .v2-board__lease-value'))
.toContain('font: 500 12px/16px var(--v2-font-mono)');
});
+
+test('TASK-482 sticky bar overlays reserved space with a top border and no shadow', () => {
+ const css = read('../v2.css');
+ const bar = ruleBody(css, '.v2-byo__hosted-offer');
+ expect(bar).toContain('position: sticky');
+ expect(bar).toContain('bottom: 0');
+ expect(bar).toContain('min-height: var(--v2-byo-offer-height)');
+ expect(bar).not.toMatch(/(?:^|[;\s])height:/);
+ expect(bar).not.toContain('overflow:');
+ expect(bar).toContain('margin-top: calc(-1 * var(--v2-byo-offer-height))');
+ expect(bar).toContain('border-top: 1px solid var(--v2-border)');
+ expect(bar).toContain('background: var(--v2-surface)');
+ expect(bar).not.toContain('box-shadow');
+ expect(ruleBody(css, '.v2-byo__layout')).toContain('padding: 8px 24px calc(40px + var(--v2-byo-offer-height, 0px))');
+});
+
+test('TASK-482 offer follows the final CTA so existing commands and controls cannot move', () => {
+ const source = read('../components/V2AgentBYO.tsx');
+ const finalCommand = source.lastIndexOf('className="v2-byo__command"');
+ const offer = source.indexOf('role="status" className="v2-byo__hosted-offer"');
+ expect(finalCommand).toBeGreaterThan(-1);
+ expect(offer).toBeGreaterThan(finalCommand);
+ expect(offer).toBeGreaterThan(source.lastIndexOf('className="v2-byo__cta-row"'));
+ // Actual before/after top coordinates are asserted by e2e/byo-offer-layout.spec.ts.
+});
diff --git a/frontend/src/v2/components/V2AgentBYO.tsx b/frontend/src/v2/components/V2AgentBYO.tsx
index 22937647c0..9dcd85ab79 100644
--- a/frontend/src/v2/components/V2AgentBYO.tsx
+++ b/frontend/src/v2/components/V2AgentBYO.tsx
@@ -15,10 +15,11 @@
// expected to paste into their MCP host config immediately. If lost, they
// can explicitly replace the old key without changing the agent identity.
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import axios from '../../utils/axiosConfig';
+import { recordByoStep, type ByoStep } from '../utils/byoTelemetry';
import getApiBaseUrl from '../../utils/apiBaseUrl';
import V2FeaturePage from './V2FeaturePage';
import V2Avatar from './V2Avatar';
@@ -180,6 +181,16 @@ const V2AgentBYO: React.FC = () => {
const [error, setError] = useState(null);
const [errorField, setErrorField] = useState<'machine' | 'name' | 'pod' | null>(null);
const [issued, setIssued] = useState<{ token: string; agentName: string; podId: string; issuedAt: number } | null>(null);
+ const [hasCopied, setHasCopied] = useState(false);
+ const [showHostedOffer, setShowHostedOffer] = useState(false);
+ const offeredPanels = useRef(new Set());
+ const offerSessionKey = issued
+ ? `commonly:byo-hosted-offer:${currentUser?._id || ''}:${issued.podId}:${issued.agentName}`
+ : null;
+ const viewed = useRef(false);
+ const commandViewed = useRef