From 0d9da4318ffb58ae5f95ed837ca987aa0efb883c Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:35:35 -0700 Subject: [PATCH] Shorten Scout welcome before hosted start Move the beta limits from the first read to the named hosted agent's live status beside Scout's start. Keep the short welcome in English, following Sam's copy ruling; Scout still matches the user's language in replies. Written by Sprint Impl, a Commonly agent Co-authored-by: sprint-impl Agent-Model: codex/gpt-6-sol/medium --- .../nativeAgents.scoutKnowledge.test.js | 3 +- .../authController.guideIdentity.test.js | 7 ++-- .../unit/services/welcomeHostedStart.test.js | 2 +- backend/config/native-agents/scout.ts | 5 ++- backend/controllers/authController.ts | 6 ++-- .../src/v2/__tests__/V2ThreadStarts.test.tsx | 32 +++++++++++++------ .../__tests__/useWelcomeHostedStart.test.tsx | 2 +- .../src/v2/components/V2ThreadMessages.tsx | 17 ++++++---- frontend/src/v2/hooks/useV2PodDetail.ts | 2 +- .../src/v2/hooks/useWelcomeHostedStart.ts | 4 +-- 10 files changed, 47 insertions(+), 33 deletions(-) diff --git a/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js b/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js index 84505478e2..c47e080175 100644 --- a/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js +++ b/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js @@ -182,8 +182,7 @@ describe('Scout\'s welcome', () => { const { SCOUT_WELCOME } = require('../../../config/native-agents/scout'); test('is in English, with no invitation to write in another language', () => { - expect(SCOUT_WELCOME).toBe('I\'m Scout. This pod is yours, and I work in it with you. ' - + 'Start with one of these, or tell me what you\'re working on.'); + expect(SCOUT_WELCOME).toBe('I\'m Scout. Pick a start or tell me your goal.'); expect(SCOUT_WELCOME).not.toMatch(/[\u3040-\u30ff\u3400-\u9fff\uac00-\ud7af\uff00-\uffef]/); }); }); diff --git a/backend/__tests__/unit/controllers/authController.guideIdentity.test.js b/backend/__tests__/unit/controllers/authController.guideIdentity.test.js index 1238b0476e..ffdb788df7 100644 --- a/backend/__tests__/unit/controllers/authController.guideIdentity.test.js +++ b/backend/__tests__/unit/controllers/authController.guideIdentity.test.js @@ -119,6 +119,7 @@ describe('workspace onboarding guide identity fork (2026-08-13)', () => { const welcome = mockPostMessage.mock.calls[0][0]; expect(welcome.content).toBe(SCOUT_WELCOME); + expect(welcome.content.split(/\s+/)).toHaveLength(10); expect(welcome.payload).toEqual({ kind: 'starts', starts: [ @@ -131,7 +132,7 @@ describe('workspace onboarding guide identity fork (2026-08-13)', () => { // QA walk 3 (2026-10-16): where the instance can host, the welcome leads with // hosting, nothing to install. It sends no message: on the person's own - // welcome it runs in place (TASK-290), with the beta limits under it, and its + // welcome it runs in place (TASK-290), and its // link opens Bring your own with hosting chosen, in this workspace, for // anyone it does not run for. test('leads the starts with Run an agent here, aimed at this workspace, where the instance can host', async () => { @@ -144,15 +145,13 @@ describe('workspace onboarding guide identity fork (2026-08-13)', () => { const { SCOUT_STARTS, SCOUT_HOSTED_START } = require('../../../config/native-agents/scout'); await authController.finishWorkspaceOnboarding({ _id: 'pod-123' }, 'User-1'); - // eslint-disable-next-line global-require - const { hostedCaps } = require('../../../services/hostedRuntimeService'); const { starts } = mockPostMessage.mock.calls[0][0].payload; expect(starts[0]).toEqual({ id: 'run-hosted', label: SCOUT_HOSTED_START.label, href: '/v2/agents/byo?pod=pod-123&mode=hosted', action: 'run-hosted', - caps: hostedCaps(), + caps: { agentsPerUser: 1, turnsPerDay: 30 }, }); expect(starts.slice(1)).toEqual(SCOUT_STARTS.map(({ id, label }) => ({ id, label }))); } finally { diff --git a/backend/__tests__/unit/services/welcomeHostedStart.test.js b/backend/__tests__/unit/services/welcomeHostedStart.test.js index faba1e8c52..3f0bfd13ac 100644 --- a/backend/__tests__/unit/services/welcomeHostedStart.test.js +++ b/backend/__tests__/unit/services/welcomeHostedStart.test.js @@ -30,7 +30,7 @@ const welcomePayload = () => ({ starts: [ { id: 'run-hosted', - label: 'Run an agent here, nothing to install', + label: 'Run an agent here', href: HREF, action: 'run-hosted', caps: { agentsPerUser: 1, turnsPerDay: 30 }, diff --git a/backend/config/native-agents/scout.ts b/backend/config/native-agents/scout.ts index e52e428d52..1e68062d93 100644 --- a/backend/config/native-agents/scout.ts +++ b/backend/config/native-agents/scout.ts @@ -38,11 +38,10 @@ export const SCOUT_STARTS = [ * in place (TASK-290, services/welcomeHostedStart.ts); anywhere else its href, * added by the signup path, opens Bring your own with hosting chosen. */ -export const SCOUT_HOSTED_START = { id: 'run-hosted', label: 'Run an agent here, nothing to install' } as const; +export const SCOUT_HOSTED_START = { id: 'run-hosted', label: 'Run an agent here' } as const; /** Scout's first message in a new workspace; the starts render under it. */ -export const SCOUT_WELCOME = 'I\'m Scout. This pod is yours, and I work in it with you. ' - + 'Start with one of these, or tell me what you\'re working on.'; +export const SCOUT_WELCOME = 'I\'m Scout. Pick a start or tell me your goal.'; export const scoutApp = { agentName: 'scout', diff --git a/backend/controllers/authController.ts b/backend/controllers/authController.ts index d0a6bce507..512ff6dbf9 100644 --- a/backend/controllers/authController.ts +++ b/backend/controllers/authController.ts @@ -304,9 +304,9 @@ const finishWorkspaceOnboarding = async (pod: any, userId: any) => { const AgentMessageService = require('../services/agentMessageService'); // Lead with hosting where this instance can host (QA walk 3, 10-16): // nothing to install. One click starts a hosted agent right here - // (TASK-290, services/welcomeHostedStart.ts), with the beta limits - // under the button. The href stays for anyone the thread will not - // run it for: it opens Bring your own with hosting chosen. + // (TASK-290, services/welcomeHostedStart.ts). The href stays for + // anyone the thread will not run it for: it opens Bring your own + // with hosting chosen. // eslint-disable-next-line global-require const { hostedEngineAvailable } = require('../services/nativeRuntimeService'); // eslint-disable-next-line global-require diff --git a/frontend/src/v2/__tests__/V2ThreadStarts.test.tsx b/frontend/src/v2/__tests__/V2ThreadStarts.test.tsx index 1a53b73ede..d83f8a9aca 100644 --- a/frontend/src/v2/__tests__/V2ThreadStarts.test.tsx +++ b/frontend/src/v2/__tests__/V2ThreadStarts.test.tsx @@ -62,7 +62,7 @@ const welcome = { id: 'welcome', pod_id: 'pod-1', user_id: 'scout-1', - content: "I'm Scout. This pod is yours, and I work in it with you. Start with one of these, or tell me what you're working on.", + content: "I'm Scout. Pick a start or tell me your goal.", message_type: 'text', created_at: at(0), user: { username: 'scout', isBot: true }, @@ -270,7 +270,7 @@ describe("V2Thread — a new pod's next steps", () => { // Walk 4 (2026-10-16): two first-minute frictions on the hosted path. describe('Scout starts and the composer after a hosted agent goes live', () => { const axios = jest.requireMock('axios').default; - const HOSTED = 'Run an agent here, nothing to install'; + const HOSTED = 'Run an agent here'; const welcomeWithHosted = { ...welcome, payload: { @@ -355,7 +355,7 @@ describe('Scout starts and the composer after a hosted agent goes live', () => { // gets the same agent, and anything refused falls back to the link. describe('"Run an agent here" runs in place on the owner\'s own welcome (TASK-290)', () => { const axios = jest.requireMock('axios').default; - const HOSTED = 'Run an agent here, nothing to install'; + const HOSTED = 'Run an agent here'; const HREF = '/v2/agents/byo?pod=pod-1&mode=hosted'; const welcomeRunsHere = { ...welcome, @@ -385,10 +385,12 @@ describe('"Run an agent here" runs in place on the owner\'s own welcome (TASK-29 beforeEach(() => { axios.post.mockClear(); }); afterEach(() => { axios.post.mockImplementation(() => Promise.resolve({ data: {} })); }); - test('the beta limits sit under the button, and describe it', () => { + test('the welcome skips the beta limits even for older messages carrying caps', () => { renderThread([welcomeRunsHere]); - expect(screen.getByRole('button', { name: HOSTED })) - .toHaveAccessibleDescription('Beta: 1 hosted agent per account, 30 turns a day.'); + const button = screen.getByRole('button', { name: HOSTED }); + expect(button).not.toHaveAccessibleDescription(); + expect(button).not.toHaveAttribute('aria-describedby'); + expect(screen.queryByText('Beta: 1 hosted agent per account, 30 turns a day.')).not.toBeInTheDocument(); }); test('one click installs and starts the agent in this pod, sends nothing, and the start goes away', async () => { @@ -397,10 +399,22 @@ describe('"Run an agent here" runs in place on the owner\'s own welcome (TASK-29 '/api/registry/install': [{ success: true }], '/api/hosted/provision': [{ ready: true }], }); - const sendSpy = renderThread([welcomeRunsHere]); - fireEvent.click(screen.getByRole('button', { name: HOSTED })); + // A second payload with the same caps must not inherit this welcome's live note. + const otherStart = { + ...welcomeRunsHere, + id: 'other-start', + created_at: at(1), + user_id: 'u2', + user: { username: 'lee', isBot: false }, + }; + const sendSpy = renderThread([welcomeRunsHere, otherStart]); + fireEvent.click(screen.getAllByRole('button', { name: HOSTED })[0]); expect(await screen.findByRole('button', { name: 'Starting…' })).toBeDisabled(); await waitFor(() => expect(screen.queryByRole('button', { name: /Run an agent here|Starting/ })).not.toBeInTheDocument()); + expect(screen.getByText('maya-agent is listening. Beta: 1 hosted agent per account, 30 turns a day.')) + .toHaveAttribute('role', 'status'); + expect(screen.getAllByText('maya-agent is listening. Beta: 1 hosted agent per account, 30 turns a day.')) + .toHaveLength(1); expect(calls('/api/hosted/welcome-start').map(([, body]) => body)).toEqual([{ messageId: 'welcome' }]); expect(calls('/api/registry/install')).toHaveLength(1); @@ -410,7 +424,7 @@ describe('"Run an agent here" runs in place on the owner\'s own welcome (TASK-29 expect(calls('/api/hosted/provision').map(([, body]) => body)).toEqual([{ agentName: 'maya-agent' }]); expect(sendSpy).not.toHaveBeenCalled(); expect(screen.getByTestId('location')).toHaveTextContent(/^\/$/); - expect(screen.getByRole('button', { name: CONNECT })).toBeInTheDocument(); + expect(screen.getAllByRole('button', { name: CONNECT })).toHaveLength(2); }); test('a double click starts one agent', async () => { diff --git a/frontend/src/v2/__tests__/useWelcomeHostedStart.test.tsx b/frontend/src/v2/__tests__/useWelcomeHostedStart.test.tsx index 4b5247f5b5..0a3bdedb13 100644 --- a/frontend/src/v2/__tests__/useWelcomeHostedStart.test.tsx +++ b/frontend/src/v2/__tests__/useWelcomeHostedStart.test.tsx @@ -34,5 +34,5 @@ test('a second run while the first is in flight sends nothing', async () => { '/api/hosted/welcome-start', '/api/registry/install', '/api/hosted/provision', ]); expect(onLive).toHaveBeenCalledTimes(1); - expect(result.current.state).toEqual({ messageId: '41', status: 'live' }); + expect(result.current.state).toEqual({ messageId: '41', status: 'live', agentName: 'maya-agent' }); }); diff --git a/frontend/src/v2/components/V2ThreadMessages.tsx b/frontend/src/v2/components/V2ThreadMessages.tsx index 4138352e75..e8ecb1577e 100644 --- a/frontend/src/v2/components/V2ThreadMessages.tsx +++ b/frontend/src/v2/components/V2ThreadMessages.tsx @@ -261,6 +261,9 @@ const V2ThreadMessages: React.FC = ({ {emptyState} {threadView.map((item, index, view) => { const renderMessage = (message: V2Message, previous: ThreadViewItem | undefined) => { + const liveHostedCaps = hostedStart?.status === 'live' && hostedStart.messageId === String(message.id) + ? message.payload?.starts?.find((start) => start.action === WELCOME_HOSTED_ACTION)?.caps + : undefined; const settledRuling = settledDecisionByMessageId.get(String(message.id)); const renderedMessage = settledRuling ? rulingMessage(message, settledRuling) : message; const previousMessage = previous && previous.kind === 'message' @@ -293,7 +296,6 @@ const V2ThreadMessages: React.FC = ({ const hostedHere = start.action === WELCOME_HOSTED_ACTION && hostedStart?.messageId === String(message.id) ? hostedStart : null; const starting = hostedHere?.status === 'starting'; - const noteId = start.caps ? `v2-start-note-${message.id}-${start.id}` : undefined; return ( - {start.caps && ( -

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

- )} {hostedHere?.status === 'failed' && (

{hostedHere.error}

)} @@ -319,6 +315,13 @@ const V2ThreadMessages: React.FC = ({ })} )} + {liveHostedCaps && ( +

+ {t('agentByo.hosted.running', { name: hostedStart?.status === 'live' ? hostedStart.agentName : '' })} + {' '} + {t('agentByo.compact.limits', { agents: liveHostedCaps.agentsPerUser, turns: liveHostedCaps.turnsPerDay })} +

+ )} {agentDeliveryHint?.messageId === message.id && (