diff --git a/backend/__tests__/unit/config/scoutWelcome.test.js b/backend/__tests__/unit/config/scoutWelcome.test.js index 90c839059d..2d432219ca 100644 --- a/backend/__tests__/unit/config/scoutWelcome.test.js +++ b/backend/__tests__/unit/config/scoutWelcome.test.js @@ -36,8 +36,20 @@ describe('Scout welcome locale', () => { it('keeps the start labels in English, since Scout reads them', () => { expect(SCOUT_STARTS.map((s) => s.label)).toEqual([ + 'Show me what you can do', 'Connect an agent I already use', 'Start a pod for a project', ]); }); + + // Staging walk 2026-11-02: a start that asks for nothing leads the sent + // starts, and Scout's prompt says how to answer it. + it('leads the sent starts with the one that asks nothing, and tells Scout how to answer it', () => { + expect(SCOUT_STARTS[0]).toEqual({ id: 'ask-scout', label: 'Show me what you can do' }); + // eslint-disable-next-line global-require + const { scoutApp } = require('../../../config/native-agents/scout'); + expect(scoutApp.systemPrompt).toContain('For "Show me what you can do", answer in two or three short sentences'); + expect(scoutApp.systemPrompt).toContain('For "Connect an agent I already use", find out which tool they run'); + expect(scoutApp.systemPrompt).toContain('For "Start a pod for a project", ask what the project is'); + }); }); diff --git a/backend/__tests__/unit/controllers/authController.guideIdentity.test.js b/backend/__tests__/unit/controllers/authController.guideIdentity.test.js index bb3cec8095..cb8c99c8cb 100644 --- a/backend/__tests__/unit/controllers/authController.guideIdentity.test.js +++ b/backend/__tests__/unit/controllers/authController.guideIdentity.test.js @@ -123,6 +123,7 @@ describe('workspace onboarding guide identity fork (2026-08-13)', () => { expect(welcome.payload).toEqual({ kind: 'starts', starts: [ + { id: 'ask-scout', label: 'Show me what you can do' }, { id: 'connect-agent', label: 'Connect an agent I already use' }, { id: 'start-pod', label: 'Start a pod for a project' }, ], diff --git a/backend/config/native-agents/scout.ts b/backend/config/native-agents/scout.ts index 735444d648..2da10e8b97 100644 --- a/backend/config/native-agents/scout.ts +++ b/backend/config/native-agents/scout.ts @@ -28,10 +28,17 @@ import UI_CARD_REVISION from '../uiCardCatalogueRevision.json'; * the start a new person picked (services/onboardingMarks.ts, by id). */ export const SCOUT_STARTS = [ + // The one start that asks nothing of the person (staging walk, 2026-11-02): + // the other starts each ask for a tool or a project before a first message. + { id: 'ask-scout', label: 'Show me what you can do' }, { id: 'connect-agent', label: 'Connect an agent I already use' }, { id: 'start-pod', label: 'Start a pod for a project' }, ] as const; +const startLabel = (id: typeof SCOUT_STARTS[number]['id']): string => ( + SCOUT_STARTS.find((start) => start.id === id)?.label ?? id +); + /** * The start that leads the welcome where the instance can host (QA walk 3, * 2026-10-16): nothing to install, so it goes first. Unlike SCOUT_STARTS it @@ -97,12 +104,15 @@ export const scoutApp = { + 'page in between; only if that cannot run does it open Bring your own with ' + '"Run it here" chosen. Either way it sends you nothing. The others send the start as ' + 'the person\'s own message: ' - + `"${SCOUT_STARTS[0].label}" and "${SCOUT_STARTS[1].label}". For ` - + `"${SCOUT_STARTS[0].label}", find out which tool they run ` + + `"${startLabel('ask-scout')}", "${startLabel('connect-agent')}" and "${startLabel('start-pod')}". ` + + `For "${startLabel('ask-scout')}", answer in two or three short sentences: ` + + 'name what you can do for them here, then offer one thing you can do right ' + + 'now in this pod, such as putting their first task on the board. ' + + `For "${startLabel('connect-agent')}", find out which tool they run ` + '(Claude Code or Codex) and their operating system, one question per turn, ' + 'then propose the connect card when the card rule below allows it; in that first reply, also say in one short ' + 'clause that if they would rather install nothing, Commonly can run one here. ' - + `For "${SCOUT_STARTS[1].label}", ask what the project is if ` + + `For "${startLabel('start-pod')}", ask what the project is if ` + 'they have not said, then propose create_pod with a short name for it. ' + 'Set create_pod params.locale to the person\'s language (en or zh-CN). ' + 'Once they approve it, reply with the exact one-line wording in the ' diff --git a/docs/design/artboards/d215/index.html b/docs/design/artboards/d215/index.html index 82e646114d..f43790ab0a 100644 --- a/docs/design/artboards/d215/index.html +++ b/docs/design/artboards/d215/index.html @@ -1306,23 +1306,25 @@

Same look. Less said.

First run

/v2/pods/:id · Scout welcome + Guide dialog
helper words welcome – → 16dialog – → 68scroll 390 – → 1.0×
-
Scout welcome · 1440new pod, first message
NWNorthwind1now
Northwind
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Message Northwind
Needs you 0
Agents 1
Files 0
Who can join
-
Dialog · waiting · 1440step 2 not connected yet
NWNorthwind1now
Northwind
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Message Northwind
Needs you 0
Agents 1
Files 0
Who can join
+
Scout welcome · 1440new pod, first message
NWNorthwind1now
Northwind3 to do
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Tell Scout: I’m working on…
Needs you 0
Agents 1
Files 0
Who can join
+
Dialog · waiting · 1440step 2 not connected yet
NWNorthwind1now
Northwind3 to do
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Tell Scout: I’m working on…
Needs you 0
Agents 1
Files 0
Who can join
-
Dialog · connected · 1440steps 1–2 done
NWNorthwind1now
Northwind
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Message Northwind
Needs you 0
Agents 1
Files 0
Who can join
-
Dialog · hello failed · 1440room did not open
NWNorthwind1now
Northwind
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Message Northwind
Needs you 0
Agents 1
Files 0
Who can join
+
Dialog · connected · 1440steps 1–2 done
NWNorthwind1now
Northwind3 to do
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Tell Scout: I’m working on…
Needs you 0
Agents 1
Files 0
Who can join
+
Dialog · hello failed · 1440room did not open
NWNorthwind1now
Northwind3 to do
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Tell Scout: I’m working on…
Needs you 0
Agents 1
Files 0
Who can join
-
Scout welcome · 390
Northwind
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Message Northwind
Pods
Needs you
Team
Settings
-
Dialog · waiting · 390
Northwind
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Message Northwind
Pods
Needs you
Team
Settings
+
Scout welcome · 390
Northwind3 to do
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Tell Scout: I’m working on…
Pods
Needs you
Team
Settings
+
Dialog · waiting · 390
Northwind3 to do
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Tell Scout: I’m working on…
Pods
Needs you
Team
Settings
    -
  • Two doors, one pod. Scout's welcome is the first offer and needs no dialog: three starts under one message. Run an agent here is the one ink key; the other two are bordered. The dialog is the longer route for someone who wants to bring their own tool.
  • +
  • Two doors, one pod. Scout's welcome is the first offer and needs no dialog: four starts under one message. Run an agent here is the one ink key; the other three are bordered. Show me what you can do asks nothing of you: one tap sends it as your first message. Where there is no hosting, or once you hold a hosted agent, it becomes the ink key, so the welcome always has a default. The dialog is the longer route for someone who wants to bring their own tool.
  • The dialog is the live component: v2-first-run__* names and the same three steps. Marks that were unicode (✓, ↗) become 16px icons. The overlay is a flat --scrim tint; the card uses the page's pop edge, not a drop shadow of its own.
  • Connected is the only cobalt: the step-1 and step-2 marks and the word Connected. The waiting line pulses its dot, the same cue as the Bring-your-own handoff.
  • At 390 the card is inset 12px, Skip for now and every act are 44px, and the steps scroll inside the card.

Each state, one line

+ + @@ -1330,8 +1332,14 @@

Same look. Less said.

Scout welcomeStarts stay until this person sends that start. Run an agent here runs in place for the pod's creator and disappears once they hold a hosted agent; others open Bring your own.
ComposerWhile the welcome's starts are open for the pod's creator and they have sent nothing since it, the composer reads Tell Scout: I'm working on…, naming the agent that sent the welcome. After their first message it goes back to Message <pod>.
Board chipThe count carries its noun at both widths: 3 to do, the open tasks it counts. A bare 3 read as an unread count on the 11-02 walk.
Dialog · waitingStep 3 is disabled. If the status check fails, the waiting line is replaced by "Connection status is temporarily unavailable. We'll keep checking." in the error style.
Dialog · connectedSteps 1–2 show the cobalt check; Say hello is the one ink key. While the 1:1 opens, the key reads "Opening…" and is disabled.
Dialog · hello failed"Could not open your 1:1 yet. Try again." under the key, alert glyph first. The key stays live.
-
Dialog · connected · 390
Northwind
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Message Northwind
Pods
Needs you
Team
Settings
-
Dialog · hello failed · 390
Northwind
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Message Northwind
Pods
Needs you
Team
Settings
+
Dialog · connected · 390
Northwind3 to do
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Tell Scout: I’m working on…
Pods
Needs you
Team
Settings
+
Dialog · hello failed · 390
Northwind3 to do
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Tell Scout: I’m working on…
Pods
Needs you
Team
Settings
+
+
+
Scout welcome · no hosting · 1440self-hosted, or a hosted agent already running
NWNorthwind1now
Northwind3 to do
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Tell Scout: I’m working on…
Needs you 0
Agents 1
Files 0
Who can join
+
+
+
Scout welcome · no hosting · 390self-hosted, or a hosted agent already running
Northwind3 to do
Scoutguide · now

I'm Scout, your first agent here. Pick one below, or just tell me what you're working on.

Tell Scout: I’m working on…
Pods
Needs you
Team
Settings
diff --git a/frontend/src/i18n/__tests__/scoutStartLabels.test.ts b/frontend/src/i18n/__tests__/scoutStartLabels.test.ts index 0d4df717a9..e80e4294ed 100644 --- a/frontend/src/i18n/__tests__/scoutStartLabels.test.ts +++ b/frontend/src/i18n/__tests__/scoutStartLabels.test.ts @@ -4,7 +4,7 @@ import i18n, { i18nReady, LANGUAGE_STORAGE_KEY } from '..'; // TASK-550: Scout's welcome starts are sent to Scout by their English labels // (backend/config/native-agents/scout.ts), so only the displayed text is // translated, by id. Every id the welcome can offer needs a zh-CN label. -const START_IDS = ['connect-agent', 'start-pod', 'run-hosted']; +const START_IDS = ['ask-scout', 'connect-agent', 'start-pod', 'run-hosted']; describe('Scout welcome start labels', () => { beforeAll(async () => { diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 0f0c3fd4b1..1c873b7be6 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -609,6 +609,7 @@ "label": "Ways to start", "openByo": "Open Bring your own", "labels": { + "ask-scout": "Show me what you can do", "connect-agent": "Connect an agent I already use", "start-pod": "Start a pod for a project", "run-hosted": "Run an agent here" @@ -716,6 +717,8 @@ "agents_one": "{{count}} agent", "agents_other": "{{count}} agents", "boardOpen": "board {{count}} open", + "boardTasks_one": "{{count}} to do", + "boardTasks_other": "{{count}} to do", "backToPods": "Back to pods", "invite": "Invite", "dm": "DM", @@ -746,6 +749,7 @@ }, "composer": { "placeholder": "Message {{podName}}…", + "placeholderAgent": "Tell {{agent}}: I'm working on…", "uploading": "Uploading…", "attachFile": "Attach file", "sending": "Sending…", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index e809799bd9..bb88eeb067 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -609,6 +609,7 @@ "label": "开始方式", "openByo": "去接入自己的智能体", "labels": { + "ask-scout": "看看你能做什么", "connect-agent": "接入我在用的智能体", "start-pod": "新建项目 Pod", "run-hosted": "在这里运行智能体" @@ -715,6 +716,7 @@ "agents_one": "{{count}}个智能体", "agents_other": "{{count}}个智能体", "boardOpen": "看板{{count}}项待办", + "boardTasks_other": "{{count}}项待办", "backToPods": "返回 Pod 列表", "invite": "邀请", "dm": "私信", @@ -745,6 +747,7 @@ }, "composer": { "placeholder": "发消息到 {{podName}}…", + "placeholderAgent": "告诉 {{agent}}:我在做…", "uploading": "正在上传…", "attachFile": "添加文件", "sending": "正在发送…", diff --git a/frontend/src/v2/__tests__/V2ThreadPodRename.test.tsx b/frontend/src/v2/__tests__/V2ThreadPodRename.test.tsx index b46ed6acfd..1d364a3b25 100644 --- a/frontend/src/v2/__tests__/V2ThreadPodRename.test.tsx +++ b/frontend/src/v2/__tests__/V2ThreadPodRename.test.tsx @@ -115,7 +115,8 @@ describe('V2Thread — owner-only pod rename (TASK-419)', () => { expect(chips).toHaveLength(2); chips.forEach((chip) => { expect(chip).toHaveAttribute('title', 'board 2 open'); - expect(chip).toHaveTextContent(/^2$/); + // The count carries its noun: a bare "2" read as an unread count (walk 11-02). + expect(chip).toHaveTextContent(/^2 to do$/); expect(chip.querySelector('svg')).toBeTruthy(); }); fireEvent.click(chips[0]); diff --git a/frontend/src/v2/__tests__/V2ThreadStarts.test.tsx b/frontend/src/v2/__tests__/V2ThreadStarts.test.tsx index c2296f61e0..0d63308498 100644 --- a/frontend/src/v2/__tests__/V2ThreadStarts.test.tsx +++ b/frontend/src/v2/__tests__/V2ThreadStarts.test.tsx @@ -144,7 +144,7 @@ describe("V2Thread — Scout's starts", () => { test('a click sends the start as a root message, leaves the draft alone, and that start goes away', async () => { const sendSpy = renderThread([welcome]); - const composer = screen.getByPlaceholderText(/message my workspace/i); + const composer = screen.getByPlaceholderText(/tell scout: i'm working on/i); fireEvent.change(composer, { target: { value: 'half typed' } }); fireEvent.click(within(startsGroup()).getByRole('button', { name: POD_START })); @@ -171,7 +171,7 @@ describe("V2Thread — Scout's starts", () => { test('typing something else leaves the starts in place, so connecting stays one click away', async () => { const sendSpy = renderThread([welcome]); - fireEvent.change(screen.getByPlaceholderText(/message my workspace/i), { target: { value: 'hello' } }); + fireEvent.change(screen.getByPlaceholderText(/tell scout: i'm working on/i), { target: { value: 'hello' } }); fireEvent.click(screen.getByRole('button', { name: /send message/i })); expect(await screen.findByText('hello', { selector: '.v2-msg *' })).toBeInTheDocument(); @@ -213,6 +213,57 @@ describe("V2Thread — Scout's starts", () => { // message, from the moment it is sent. const OPEN_BYO = 'Open Bring your own'; +// Staging walk 2026-11-02: every start asked for a tool or a project, and the +// composer named the pod rather than the agent who had just spoken. +describe('V2Thread — a first message that asks nothing', () => { + const ASK = 'Show me what you can do'; + const withAsk = (extra = []) => ({ + ...welcome, + payload: { kind: 'starts', starts: [...extra, { id: 'ask-scout', label: ASK }, ...welcome.payload.starts] }, + }); + + test('where no hosted start is offered, the ask start is the one ink key', () => { + renderThread([withAsk()]); + const primary = startsGroup().querySelectorAll('.v2-chat__start--primary'); + expect(primary).toHaveLength(1); + expect(primary[0]).toHaveTextContent(ASK); + }); + + test('where the hosted start is offered, it keeps the ink key and the ask start is bordered', () => { + renderThread([withAsk([{ id: 'run-hosted', label: 'Run an agent here', href: '/v2/agents/byo?pod=pod-1&mode=hosted', action: 'run-hosted' }])]); + const primary = startsGroup().querySelectorAll('.v2-chat__start--primary'); + expect(primary).toHaveLength(1); + expect(primary[0]).toHaveTextContent('Run an agent here'); + }); + + test('one tap sends it as the person\'s own message', async () => { + const sendSpy = renderThread([withAsk()]); + fireEvent.click(screen.getByRole('button', { name: ASK })); + await waitFor(() => expect(sendSpy).toHaveBeenCalled()); + expect(sendSpy.mock.calls[0][0]).toBe(ASK); + }); + + test('the composer names the agent who welcomed them until they send anything', () => { + renderThread([withAsk()]); + expect(screen.getByPlaceholderText("Tell scout: I'm working on…")).toBeInTheDocument(); + }); + + test('after their first message the composer names the pod again', () => { + renderThread([withAsk(), message('mine', 'u1', 'maya', 'hello', 3)]); + expect(screen.getByPlaceholderText('Message My Workspace…')).toBeInTheDocument(); + }); + + test('an older welcome without the ask start still gets the agent composer', () => { + renderThread([welcome]); + expect(screen.getByPlaceholderText("Tell scout: I'm working on…")).toBeInTheDocument(); + }); + + test("in a pod the viewer didn't create, the composer names the pod", () => { + renderThread([withAsk()], jest.fn(), { pod: { ...OWN_POD, createdBy: 'someone-else' } }); + expect(screen.getByPlaceholderText('Message My Workspace…')).toBeInTheDocument(); + }); +}); + describe('V2Thread — the connect start links Bring your own', () => { test("clicking the connect start shows the link under the person's message at once", async () => { const send = renderThread([welcome]); @@ -440,7 +491,7 @@ describe('Scout starts and the composer after a hosted agent goes live', () => { , ); - const composer = await screen.findByPlaceholderText(/message my workspace/i); + const composer = await screen.findByPlaceholderText(/tell scout: i'm working on/i); await waitFor(() => expect(composer).toHaveValue('@maya-agent ')); }); @@ -452,7 +503,7 @@ describe('Scout starts and the composer after a hosted agent goes live', () => { , ); - const composer = await screen.findByPlaceholderText(/message my workspace/i); + const composer = await screen.findByPlaceholderText(/tell scout: i'm working on/i); await waitFor(() => expect(composer).toHaveValue('@maya-agent ')); }); }); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index a4d4b208a3..b25fef235e 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -3259,7 +3259,9 @@ describe('v2 layout invariants (CSS rule presence)', () => { // row under Scout's message, in the slot the delivery cue and the // no-agent row share, so they land on the message text column too. expect(threadMessages).toContain('className="v2-chat__starts"'); - expect(threadMessages).toContain('v2-chat__start${start.action === WELCOME_HOSTED_ACTION ? \' v2-chat__start--primary\' : \'\'}'); + expect(threadMessages).toContain('v2-chat__start${primary ? \' v2-chat__start--primary\' : \'\'}'); + // One ink key: the hosted start where offered, otherwise the start that asks nothing (walk 11-02). + expect(threadMessages).toMatch(/const primary = start\.action === WELCOME_HOSTED_ACTION\s*\|\| \(start\.id === ASK_START_ID && !starts\.some/); // The 44px phone minimum must come AFTER the 32px base in the sheet: at // equal specificity the later rule wins, and a base rule placed after // the media block would silently render 32 on a phone. diff --git a/frontend/src/v2/components/V2Composer.tsx b/frontend/src/v2/components/V2Composer.tsx index b01219a53e..985f2035b6 100644 --- a/frontend/src/v2/components/V2Composer.tsx +++ b/frontend/src/v2/components/V2Composer.tsx @@ -22,6 +22,8 @@ export interface V2ComposerWarning { interface V2ComposerProps { podName: string; + /** Replaces "Message " when nothing is being replied to. */ + placeholder?: string; authorName: string; draft: string; sending: boolean; @@ -61,6 +63,7 @@ const FILE_ACCEPT = 'image/*,.pdf,.md,.txt,.csv,.json,.doc,.docx,.xls,.xlsx,.ppt /** The one bounded input surface for a workspace thread (direction C). */ const V2Composer: React.FC = ({ podName, + placeholder, authorName, draft, sending, @@ -171,7 +174,7 @@ const V2Composer: React.FC = ({