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 && (