- ))}
-
-
- }
- />
- );
-}
-
-/** B — single muted one-liner with arrows. */
-export function MockTeamStepsB(): ReactNode {
- return (
-
- Next:
- {STEPS.join(" → ")}
-
- }
- />
- );
-}
-
-/**
- * Ceremonial welcome — a centered "this is a moment" treatment that builds
- * anticipation: prominent brand mark, a warm headline, a payoff-teasing subline
- * (your coding agent working alongside your team, in minutes), the single
- * naming action, and a low-effort expectation ("3 quick steps · ~2 minutes").
- * Vertically centered, generous spacing, center-aligned hero.
- */
-export function MockWelcomeCeremonial(): ReactNode {
- const [name, setName] = useState("Gandy's team");
- return (
-
-
-
-
- First Tree
-
-
-
-
- {/* Three deliberate zones with their own rhythm: HERO (brand + value),
- ROADMAP (quiet "what's next"), ACTION (name + a restrained CTA). */}
-
- {/* ── Hero ── (wide enough for the subtitle to sit on ONE line) */}
-
-
- Welcome to First Tree
-
-
- You and your local coding agent (Claude Code, Codex) join a First Tree team to work together.
-
-
- {/* ── Roadmap ── quiet, refined: a faint eyebrow + delicate numerals
- (no filled chips) so it reads as a light roadmap, not a heavy list. */}
-
- What's next
-
-
- {STEPS.map((s, i) => (
-
-
- {i + 1}
-
- {s}
-
- ))}
-
-
- {/* ── Action ── (clear gap above separates it from orientation). The
- CTA is auto-width + centered (a restrained pill, not a heavy
- full-width black bar). */}
-
- {/* Inline field: the preset team name as the value, with a muted
- "← rename it freely" hint trailing it — signals editability on the
- same line, no separate label row. The input sizes to its content
- so the hint sits right after the name. */}
-
-
-
-
-
-
- );
-}
diff --git a/packages/web/src/pages/onboarding/__tests__/copy.test.ts b/packages/web/src/pages/onboarding/__tests__/copy.test.ts
index ac6893d4f..ee068175d 100644
--- a/packages/web/src/pages/onboarding/__tests__/copy.test.ts
+++ b/packages/web/src/pages/onboarding/__tests__/copy.test.ts
@@ -47,7 +47,7 @@ describe("get-started progressive copy", () => {
}
expect(g.joinedTeam("Acme")).toBe("You've joined Acme");
expect(g.recommendedTitle).toBe("Set up your First Tree agent");
- expect(g.personalSteps).toEqual(["Connect computer", "Create agent", "Start first chat"]);
+ expect(g.personalSteps).toEqual(["Connect computer", "Create agent", "Meet your agent"]);
expect(g.continueWithout).toBe("Continue without my own agent");
expect(g.runBy("Zhang Wei")).toBe("Run by Zhang Wei");
expect(g.teamAgentExecution("Zhang Wei")).toContain("Zhang Wei's connected computer");
@@ -64,9 +64,9 @@ describe("get-started progressive copy", () => {
});
describe("onboarding vocabulary (connect-agent reframe)", () => {
- // The reframe retires "runtime" from UI copy in favour of "coding agent" /
- // the tool's own name. Guard against it creeping back into the two steps
- // that used to say it.
+ // The reframe retires "runtime" from UI copy in favour of plain descriptions
+ // and the detected option's own name. Guard against it creeping back into
+ // the two steps that used to say it.
it("connect-computer + create-agent copy never says 'runtime'", () => {
const cc = COPY.connectComputer;
const ca = COPY.createAgent;
@@ -92,37 +92,39 @@ describe("onboarding vocabulary (connect-agent reframe)", () => {
}
});
- it("keeps the computer bridge provider-neutral and names the managed agent", () => {
- expect(COPY.connectComputer.whyWaiting).toContain("local coding agent");
- expect(COPY.connectComputer.whyWaiting).not.toContain("Claude Code");
- expect(COPY.connectComputer.whyWaiting).not.toContain("Codex");
+ it("explains the computer action without inventing a category users must learn", () => {
+ expect(COPY.connectComputer.whyWaiting).toBe(
+ "Install the First Tree app to connect this computer and detect what your agents can run.",
+ );
+ expect(COPY.connectComputer.whyConnected).toBe("");
+ expect(COPY.connectComputer.detectedLabel).toBe("Available on this computer");
expect(COPY.connectComputer.detectedBridge).toBe("Next, create your First Tree agent.");
expect(STEP_COPY["create-agent"].title).toContain("First Tree agent");
- // The subtitle keeps the First Tree teammate distinct from the local
- // provider selected in the field below.
- expect(COPY.createAgent.subtitle).toContain("First Tree teammate");
- expect(COPY.createAgent.subtitle).toContain("local coding agent");
+ expect(COPY.createAgent.subtitle).toBe(
+ "Build your own group of agents for different work in this team. Let’s create your first one.",
+ );
+ expect(COPY.createAgent.codingAgentLabel).toBe("This agent will run");
});
- it("keeps the start-chat finale action-oriented and consistent", () => {
- expect(COPY.startChat.newTitle).toBe("Start your first Agent Chat");
- expect(COPY.startChat.existingTitle).toBe("Start your first Agent Chat");
- expect(COPY.startChat.noProjectTitle).toBe("Start your first Agent Chat");
- expect(COPY.startChat.inviteeReadyTitle).toBe("Start your first Agent Chat");
- expect(COPY.invitee.notReadyTitle).toBe("Start your first Agent Chat");
+ it("keeps the meet-your-agent finale action-oriented and consistent", () => {
+ expect(COPY.startChat.newTitle).toBe("Meet your agent");
+ expect(COPY.startChat.existingTitle).toBe("Meet your agent");
+ expect(COPY.startChat.noProjectTitle).toBe("Meet your agent");
+ expect(COPY.startChat.inviteeReadyTitle).toBe("Meet your agent");
+ expect(COPY.invitee.notReadyTitle).toBe("Meet your agent");
- expect(COPY.startChat.startBuilding).toBe("Start chat");
- expect(COPY.startChat.startExisting).toBe("Start chat");
- expect(COPY.startChat.startChatting).toBe("Start chat");
- expect(COPY.startChat.startWorking).toBe("Start chat");
- expect(COPY.invitee.startAnyway).toBe("Start chat");
+ expect(COPY.startChat.startBuilding).toBe("Start exploring");
+ expect(COPY.startChat.startExisting).toBe("Start exploring");
+ expect(COPY.startChat.startChatting).toBe("Start exploring");
+ expect(COPY.startChat.startWorking).toBe("Start exploring");
+ expect(COPY.invitee.startAnyway).toBe("Start exploring");
});
it("shows one plain launch subtitle across every start-chat state", () => {
// The finale intentionally reads the same regardless of role or team/tree
// state: that state is invisible to the user (Context Tree is introduced
// later, in chat), so the subtitle stays a single plain launch line.
- const launch = "Your agent is ready. Delegate work, follow progress, and review results with your team.";
+ const launch = "Explore First Tree together, then choose what you’d like to try first.";
expect(COPY.startChat.noProjectBody).toBe(launch);
expect(COPY.startChat.inviteeReadyBody).toBe(launch);
expect(COPY.invitee.notReadyBody).toBe(launch);
diff --git a/packages/web/src/pages/onboarding/copy.ts b/packages/web/src/pages/onboarding/copy.ts
index 0b05253f4..5724215d5 100644
--- a/packages/web/src/pages/onboarding/copy.ts
+++ b/packages/web/src/pages/onboarding/copy.ts
@@ -9,9 +9,9 @@
* agent journey. Only after they explicitly continue without one do Team-agent
* quick start and external Context access appear. BYO gives the coding agent
* one self-contained prompt that connects the computer and enables Team
- * Context without creating a First Tree agent or completing onboarding. A
- * local coding agent is a provider, never something silently added to First
- * Tree Chat.
+ * Context without creating a First Tree agent or completing onboarding. In
+ * the recommended path, the detected executable is named directly instead of
+ * introducing a category term users must learn before creating an agent.
* "repo" stays (GitHub access / start-chat can still involve repos, and
* "project" is ambiguous next to GitHub's own "Projects"). "binding" and
* other deep internals still never leak.
@@ -70,11 +70,11 @@ export const STEP_COPY: Record = {
},
};
-/** One plain launch line for every start-chat finale — admin or invitee, team
- * ready or not. It teaches the product value without leaking Team/Tree
- * readiness or implying that a personal Claude Code / Codex conversation is
- * joining First Tree Chat. */
-const START_CHAT_LAUNCH_WHY = "Your agent is ready. Delegate work, follow progress, and review results with your team.";
+/** One plain exploration line for every first-chat finale — admin or invitee,
+ * team ready or not. The page introduces the managed First Tree agent without
+ * exposing repo / Context Tree readiness or turning Chat into the concept the
+ * member has to understand first. */
+const START_CHAT_LAUNCH_WHY = "Explore First Tree together, then choose what you’d like to try first.";
/** Shared phrases reused across steps so wording stays consistent. */
export const COPY = {
@@ -180,21 +180,16 @@ export const COPY = {
// The Client connects the computer and lets a managed First Tree agent run
// there. It does not itself enable Team Context inside a personal provider
// session, so keep that separate in the user-facing mental model.
- whyWaiting: "A background app that lets First Tree agents run here using a local coding agent.",
- whyConnected: "A background app that lets First Tree agents run here using a local coding agent.",
- // Two install paths (waiting state): run the bare command in a terminal, or
- // paste a ready prompt to the coding agent the user already has — the prompt
- // wraps the command in a "please run this" line so the agent executes it
- // instead of just explaining a bare command.
+ whyWaiting: "Install the First Tree app to connect this computer and detect what your agents can run.",
+ // Once connected, the status row and detected options carry the result.
+ // Repeating the install explanation would describe work the user has
+ // already completed, so the connected state has no separate lead sentence.
+ whyConnected: "",
+ // One canonical install path: run the server-authored command in a terminal.
terminalBoxLabel: "Run this command in your terminal",
- agentBoxLabel: "Or paste this to your Claude Code, Codex, or Cursor agent",
- agentPromptPrefix: "Help me install First Tree by running the command below:",
- // Quiet caption naming the nested coding-agent list, so the indented rows
- // read as "found ON this computer" (the relationship the nesting implies)
- // rather than as an unlabelled cluster. Count-aware so a single detection
- // doesn't read as a plural label.
- detectedLabel: (count: number) =>
- count === 1 ? "Coding agent on this computer" : "Coding agents on this computer",
+ // Quiet caption naming the nested executable list without asking the user
+ // to learn another category term such as "AI tool" or "coding agent".
+ detectedLabel: "Available on this computer",
// Bridge below the detected-agents list → the next step (create-agent).
detectedBridge: "Next, create your First Tree agent.",
waiting: "Waiting for your computer…",
@@ -203,8 +198,8 @@ export const COPY = {
// connected (so no "Your computer is connected, but…" lead-in), and this is a
// live polling state (so the dropped "it'll appear here automatically" tail is
// implied — a detected agent just shows up). Problem + fix only.
- noRuntime: "No coding agent found yet. Install one (like Claude Code) and sign in.",
- detecting: "Looking for coding agents on it…",
+ noRuntime: "Nothing your agents can run was found yet. Install Codex, Claude Code, or another supported option.",
+ detecting: "Detecting what your agents can run…",
/** Token-mint failure (POST /me/connect-tokens threw, after silent retries).
Calm + recoverable: the auto-retry handles transient blips, so by the
time this shows it's worth a manual Try again. */
@@ -213,18 +208,13 @@ export const COPY = {
},
/** create-agent states */
createAgent: {
- // A First Tree agent is the managed teammate; the local coding
- // agent is the provider it uses on this computer. Keep both nouns visible
- // so onboarding never implies that a personal provider conversation joins
- // First Tree Chat.
- subtitle: "A First Tree teammate you can delegate work to. It runs here using your local coding agent.",
- // Coding-agent picker (moved here from connect-computer): always a list, even
- // for one, default-selected to Claude Code when present. Verb-leading to
- // match the imperative `nameLabel` ("Name your agent") below; "local" keeps
- // the subtitle's vocabulary and frames the pick as the user's own
- // machine-side tool — connect-computer already showed which machine, so no
- // "Detected on " sub-label is repeated here.
- codingAgentLabel: "Choose which local coding agent it uses",
+ // Establish the durable product model first: this is the user's first of
+ // potentially several agents in the current Team. The selected executable
+ // is explained immediately below, where the choice is made.
+ subtitle: "Build your own group of agents for different work in this team. Let’s create your first one.",
+ // The detected executables define how this First Tree agent runs, without
+ // collapsing the managed teammate identity into a renamed local tool.
+ codingAgentLabel: "This agent will run",
// Amber "not ready" badge beside the label when the computer dropped — so the
// disabled picker reads AS unavailable (action needed: reconnect) at a glance,
// not just a quietly greyed pill.
@@ -274,47 +264,39 @@ export const COPY = {
// admin · new tree (the default — the team has none yet). `newWhy`/`existingWhy`
// are only read by the dormant repo-aware branch (StepConnectCode is out of the
// live sequence); kept as functions so that call site's shape is unchanged.
- newTitle: "Start your first Agent Chat",
+ newTitle: "Meet your agent",
newWhy: (_repoCount: number): string => START_CHAT_LAUNCH_WHY,
- startBuilding: "Start chat",
+ startBuilding: "Start exploring",
// admin · the team already has a Context Tree (re-run / second admin /
// CLI-bound). Detected silently; also part of the dormant repo-aware branch.
- existingTitle: "Start your first Agent Chat",
+ existingTitle: "Meet your agent",
existingWhy: (_repoCount: number): string => START_CHAT_LAUNCH_WHY,
- startExisting: "Start chat",
+ startExisting: "Start exploring",
// admin · no repo connected (the live default path).
- noProjectTitle: "Start your first Agent Chat",
+ noProjectTitle: "Meet your agent",
noProjectBody: START_CHAT_LAUNCH_WHY,
- startChatting: "Start chat",
+ startChatting: "Start exploring",
// invitee · ready (team has a tree + a GitHub connection). The agent inherits
// the team's recommended repos automatically, so there is nothing to select.
- inviteeReadyTitle: "Start your first Agent Chat",
+ inviteeReadyTitle: "Meet your agent",
inviteeReadyBody: START_CHAT_LAUNCH_WHY,
- startWorking: "Start chat",
+ startWorking: "Start exploring",
// shared launch transition
- starting: "Starting your agent…",
-
- /** Heading of the community footer under the launch CTA (every finale).
- * The channel cards themselves live in components/community-channels.tsx
- * (shared with the top-bar SupportMenu), so only the onboarding-surface
- * heading is copy here. */
- community: {
- title: "Stay connected",
- },
+ starting: "Opening your first Chat…",
},
/** Invitee not-ready (blocked-on-admin) state. The not-ready screen covers
* both "no Context Tree" and "no GitHub connection" — the invitee can't act
* on either, and it advances on its own once the admin finishes. */
invitee: {
- notReadyTitle: "Start your first Agent Chat",
+ notReadyTitle: "Meet your agent",
notReadyBody: START_CHAT_LAUNCH_WHY,
// The primary action on the not-ready screen — start a simple first chat now
// instead of waiting on the team.
- startAnyway: "Start chat",
+ startAnyway: "Start exploring",
},
/** Progressive Member entry: one recommended personal-agent path first,
* then Team-agent and external Context access after explicit continuation. */
@@ -323,7 +305,7 @@ export const COPY = {
recommendedTitle: "Set up your First Tree agent",
recommendedWhy: "Create your own agent for ongoing work with your team.",
computerReady: "Your computer is connected. Next, create your agent.",
- personalSteps: ["Connect computer", "Create agent", "Start first chat"],
+ personalSteps: ["Connect computer", "Create agent", "Meet your agent"],
continueWithout: "Continue without my own agent",
personal: {
cta: "Set up my agent",
@@ -376,7 +358,7 @@ export const COPY = {
/** failure recovery, shared */
errors: {
generic: "Something went wrong. Try again in a moment.",
- chatFailed: "Couldn't start the first task. Try again.",
+ chatFailed: "Couldn't open your first Chat. Try again.",
agentFailed: "Couldn't add your agent to the team — please try again.",
noAgent: "We couldn't find your agent. Go back a step and add one.",
},
diff --git a/packages/web/src/pages/onboarding/steps/__tests__/step-connect-computer-dom.test.tsx b/packages/web/src/pages/onboarding/steps/__tests__/step-connect-computer-dom.test.tsx
index 721fc7991..8921a4d85 100644
--- a/packages/web/src/pages/onboarding/steps/__tests__/step-connect-computer-dom.test.tsx
+++ b/packages/web/src/pages/onboarding/steps/__tests__/step-connect-computer-dom.test.tsx
@@ -120,8 +120,16 @@ describe("StepConnectComputer", () => {
const container = await renderStep(value);
+ expect(container.textContent).toContain(
+ "Install the First Tree app to connect this computer and detect what your agents can run.",
+ );
+ expect(container.textContent).toContain("Run this command in your terminal");
expect(container.textContent).toContain("https://download.first-tree.ai/releases/prod/install.sh");
expect(container.textContent).toContain("~/.local/bin/first-tree login abc123");
+ expect(
+ [...container.querySelectorAll("button")].filter((button) => button.textContent?.includes("Copy")),
+ ).toHaveLength(1);
+ expect(container.textContent).not.toContain("Or paste this");
expect(container.textContent).not.toContain("npm install");
expect(container.textContent).not.toContain("Node.js");
await click(buttonByText(container, "Copy"));
@@ -158,23 +166,26 @@ describe("StepConnectComputer", () => {
const detecting = await renderStep(flow({ computer: computer({ connectedClient }) }));
expect(detecting.textContent).toContain("workstation");
- expect(detecting.textContent).toContain("Looking for coding agents");
+ expect(detecting.textContent).toContain("Detecting what your agents can run");
+ expect(detecting.textContent).not.toContain("Install the First Tree app");
expect(buttonByText(detecting, "Continue")?.disabled).toBe(true);
const noRuntime = await renderStep(
flow({ computer: computer({ connectedClient, capabilitiesLoaded: true, okRuntimes: [] }) }),
);
- expect(noRuntime.textContent).toContain("No coding agent found yet");
+ expect(noRuntime.textContent).toContain("Nothing your agents can run was found yet");
const goNext = vi.fn();
const ready = await renderStep(
flow({
goNext,
- computer: computer({ connectedClient, capabilitiesLoaded: true, okRuntimes: ["codex", "claude-code"] }),
+ computer: computer({ connectedClient, capabilitiesLoaded: true, okRuntimes: ["claude-code", "codex"] }),
}),
);
expect(ready.textContent).toContain("Codex");
expect(ready.textContent).toContain("Claude Code");
+ expect(ready.textContent).toContain("Available on this computer");
+ expect((ready.textContent ?? "").indexOf("Codex")).toBeLessThan((ready.textContent ?? "").indexOf("Claude Code"));
expect(buttonByText(ready, "Continue")?.disabled).toBe(false);
await click(buttonByText(ready, "Continue"));
expect(goNext).toHaveBeenCalledTimes(1);
diff --git a/packages/web/src/pages/onboarding/steps/__tests__/step-get-started-dom.test.tsx b/packages/web/src/pages/onboarding/steps/__tests__/step-get-started-dom.test.tsx
index 741425bef..e8e3227fc 100644
--- a/packages/web/src/pages/onboarding/steps/__tests__/step-get-started-dom.test.tsx
+++ b/packages/web/src/pages/onboarding/steps/__tests__/step-get-started-dom.test.tsx
@@ -193,7 +193,7 @@ describe("StepGetStarted", () => {
expect(container.textContent).toContain("You've joined Acme");
expect(container.textContent).toContain("Set up your First Tree agent");
expect(container.textContent).toContain("Connect computer");
- expect(container.textContent).toContain("Start first chat");
+ expect(container.textContent).toContain("Meet your agent");
expect(container.textContent).not.toContain("Pick a team agent");
expect(container.textContent).not.toContain("Use the Context Tree in Claude Code or Codex");
expect(mocks.listAgents).not.toHaveBeenCalled();
diff --git a/packages/web/src/pages/onboarding/steps/step-connect-computer.tsx b/packages/web/src/pages/onboarding/steps/step-connect-computer.tsx
index d5487ab7e..8c9d2ff35 100644
--- a/packages/web/src/pages/onboarding/steps/step-connect-computer.tsx
+++ b/packages/web/src/pages/onboarding/steps/step-connect-computer.tsx
@@ -1,5 +1,6 @@
import { ArrowRight } from "lucide-react";
import { Button } from "../../../components/ui/button.js";
+import { orderRuntimesByPreference } from "../../../features/agent-setup/runtime-preference.js";
import { runtimeProviderLabel } from "../../clients/cards/shared/providers.js";
import { COPY } from "../copy.js";
import { CommandBox, FlowHint, StatusRow } from "../flow-ui.js";
@@ -7,10 +8,9 @@ import { useOnboardingFlow } from "../onboarding-flow.js";
/**
* Install the First Tree client (a small background app) on the user's computer.
- * Two install paths: run the command block in a terminal, OR paste a ready prompt to
- * the coding agent the user already has (Claude Code / Codex) and let it install.
- * We poll until the computer shows up, then list the coding agents detected on it
- * (read-only — picking which one to use moves to the next step, create-agent).
+ * The user runs the server-authored command in a terminal. We poll until the
+ * computer shows up, then list what First Tree agents can run on it (read-only
+ * — choosing one moves to the next step, create-agent).
*
* No "Need help?" disclosure / example terminal: the normal state is just the
* server-provided command(s) + status.
@@ -21,17 +21,16 @@ export function StepConnectComputer() {
const noRuntime = !!connectedClient && capabilitiesLoaded && okRuntimes.length === 0;
const ready = !!connectedClient && okRuntimes.length > 0;
-
- // Box 2 hands the SAME command to the user's coding agent as a paste-able
- // prompt, with a natural-language "please run this" wrapper so the agent
- // actually executes it (a bare command pasted in might only get explained).
- const agentPrompt = cliCommand ? `${COPY.connectComputer.agentPromptPrefix}\n${cliCommand}` : null;
+ const orderedRuntimes = orderRuntimesByPreference(okRuntimes);
+ const stepBody = connectedClient ? COPY.connectComputer.whyConnected : COPY.connectComputer.whyWaiting;
return (
)
) : (
<>
@@ -97,10 +90,10 @@ export function StepConnectComputer() {
{/* Names the nested group so the relationship is stated, not
only implied by the indent. */}
- {/* Collapsed-model subtitle: the agent you create IS your local coding
- agent given a team identity — no two-layer "powered by" framing. */}
{COPY.createAgent.subtitle}
@@ -258,66 +315,9 @@ export function StepCreateAgent() {
{COPY.createAgent.templateIntentUnavailable}
)}
- {/* Coding agent — always a list (even for one), default Claude Code.
+ {/* Coding agent — always a list (even for one), default Codex when available.
Stays visible (disabled) when the computer drops, so the field never
vanishes from under the user. */}
- {displayProviders.length > 0 && (
-
- )}
-
+ {toolPicker}
+
-
);
@@ -283,7 +281,6 @@ function AdminStartChat() {
-
);
@@ -421,7 +418,6 @@ function InviteeReady() {
-
);
@@ -489,7 +485,6 @@ function InviteeNotReady() {
-
);
@@ -500,29 +495,3 @@ function InviteeNotReady() {
function StartingState() {
return ;
}
-
-/**
- * "Stay connected" — the mobile install and community cards as a footer under
- * the launch CTA. Rendered only in the
- * stable finale bodies (never during loading/starting transitions), separated
- * from the primary action by a hairline so it reads as a footer and can't
- * compete with "Start chat".
- */
-function CommunityBlock() {
- return (
-