fix(frontend): address onboarding copy review + hide Board header on welcome

Per review on #2432: welcome body now names the kanban board explicitly,
step copy tightened to the reviewer's wording, the project empty state
says "No worker sessions yet" (an orchestrator session may already
exist), and the "Board" subhead is hidden on the first-launch welcome
since it described a board that isn't rendered there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dhruv Sharma 2026-07-06 03:25:11 +05:30
parent 699469bee4
commit acb959c059
3 changed files with 25 additions and 18 deletions

View File

@ -118,6 +118,8 @@ describe("global board first launch", () => {
expect(screen.getByText("Describe a task")).toBeInTheDocument(); expect(screen.getByText("Describe a task")).toBeInTheDocument();
expect(screen.getByText("Ready to merge")).toBeInTheDocument(); expect(screen.getByText("Ready to merge")).toBeInTheDocument();
expect(columnCount()).toBe(0); expect(columnCount()).toBe(0);
// The welcome carries its own orientation — no dangling "Board" header.
expect(screen.queryByText("Board")).not.toBeInTheDocument();
}); });
it("opens the native folder picker from the welcome CTA", async () => { it("opens the native folder picker from the welcome CTA", async () => {
@ -154,7 +156,7 @@ describe("project board with no sessions", () => {
respondWith([project], []); respondWith([project], []);
renderBoard(<SessionsBoard projectId="proj-1" />); renderBoard(<SessionsBoard projectId="proj-1" />);
expect(await screen.findByText("No sessions yet")).toBeInTheDocument(); expect(await screen.findByText("No worker sessions yet")).toBeInTheDocument();
// Board header + empty state each offer the pair; the orchestrator is primary in both. // Board header + empty state each offer the pair; the orchestrator is primary in both.
expect(screen.getAllByRole("button", { name: "Spawn Orchestrator" }).length).toBeGreaterThan(0); expect(screen.getAllByRole("button", { name: "Spawn Orchestrator" }).length).toBeGreaterThan(0);
expect(screen.getAllByRole("button", { name: "New task" }).length).toBeGreaterThan(0); expect(screen.getAllByRole("button", { name: "New task" }).length).toBeGreaterThan(0);
@ -167,7 +169,7 @@ describe("project board with no sessions", () => {
spawnOrchestratorMock.mockRejectedValue(new Error("branch is already checked out in another worktree")); spawnOrchestratorMock.mockRejectedValue(new Error("branch is already checked out in another worktree"));
renderBoard(<SessionsBoard projectId="proj-1" />); renderBoard(<SessionsBoard projectId="proj-1" />);
await screen.findByText("No sessions yet"); await screen.findByText("No worker sessions yet");
const [spawnButton] = screen.getAllByRole("button", { name: "Spawn Orchestrator" }); const [spawnButton] = screen.getAllByRole("button", { name: "Spawn Orchestrator" });
await userEvent.click(spawnButton); await userEvent.click(spawnButton);
@ -229,7 +231,7 @@ describe("project board with no sessions", () => {
</QueryClientProvider>, </QueryClientProvider>,
); );
await screen.findByText("No sessions yet"); await screen.findByText("No worker sessions yet");
await waitFor(() => expect(useUiStore.getState().orchestratorStartupErrors["proj-1"]).toBeUndefined()); await waitFor(() => expect(useUiStore.getState().orchestratorStartupErrors["proj-1"]).toBeUndefined());
expect(screen.queryByText(/Project added, but orchestrator did not start/)).not.toBeInTheDocument(); expect(screen.queryByText(/Project added, but orchestrator did not start/)).not.toBeInTheDocument();
}); });
@ -244,7 +246,7 @@ describe("project board with no sessions", () => {
); );
renderBoard(<SessionsBoard projectId="proj-1" />); renderBoard(<SessionsBoard projectId="proj-1" />);
await screen.findByText("No sessions yet"); await screen.findByText("No worker sessions yet");
await waitFor(() => expect(useUiStore.getState().orchestratorStartupErrors["proj-1"]).toBeUndefined()); await waitFor(() => expect(useUiStore.getState().orchestratorStartupErrors["proj-1"]).toBeUndefined());
expect(screen.queryByText(/Project added, but orchestrator did not start/)).not.toBeInTheDocument(); expect(screen.queryByText(/Project added, but orchestrator did not start/)).not.toBeInTheDocument();
}); });
@ -255,7 +257,7 @@ describe("project board with no sessions", () => {
spawnOrchestratorMock.mockRejectedValue(new Error("branch is already checked out in another worktree")); spawnOrchestratorMock.mockRejectedValue(new Error("branch is already checked out in another worktree"));
const { rerender } = renderBoard(<SessionsBoard projectId="proj-1" />); const { rerender } = renderBoard(<SessionsBoard projectId="proj-1" />);
await screen.findByText("No sessions yet"); await screen.findByText("No worker sessions yet");
const [spawnButton] = screen.getAllByRole("button", { name: "Spawn Orchestrator" }); const [spawnButton] = screen.getAllByRole("button", { name: "Spawn Orchestrator" });
await userEvent.click(spawnButton); await userEvent.click(spawnButton);
await screen.findByText(/branch is already checked out/); await screen.findByText(/branch is already checked out/);
@ -267,7 +269,7 @@ describe("project board with no sessions", () => {
</ShellProvider> </ShellProvider>
</QueryClientProvider>, </QueryClientProvider>,
); );
await screen.findByText("No sessions yet"); await screen.findByText("No worker sessions yet");
expect(screen.queryByText(/branch is already checked out/)).not.toBeInTheDocument(); expect(screen.queryByText(/branch is already checked out/)).not.toBeInTheDocument();
}); });
@ -276,7 +278,7 @@ describe("project board with no sessions", () => {
renderBoard(<SessionsBoard projectId="proj-1" />); renderBoard(<SessionsBoard projectId="proj-1" />);
expect(await screen.findByText("fix the bug")).toBeInTheDocument(); expect(await screen.findByText("fix the bug")).toBeInTheDocument();
expect(screen.queryByText("No sessions yet")).not.toBeInTheDocument(); expect(screen.queryByText("No worker sessions yet")).not.toBeInTheDocument();
expect(columnCount()).toBe(4); expect(columnCount()).toBe(4);
}); });
}); });

View File

@ -29,19 +29,19 @@ export function BoardWelcome() {
Welcome to Agent Orchestrator Welcome to Agent Orchestrator
</h2> </h2>
<p className="mt-2 max-w-[400px] text-[12.5px] leading-[1.6] text-muted-foreground"> <p className="mt-2 max-w-[400px] text-[12.5px] leading-[1.6] text-muted-foreground">
Point it at a git repository and delegate. Agents do the work on isolated branches; you review and merge the Add a git repository, describe the work, and AO coordinates agent sessions on isolated branches. This kanban
PRs from this board. board tracks each session from work through review to merge.
</p> </p>
<ol className="mt-7 w-full divide-y divide-border rounded-[13px] border border-border bg-surface text-left"> <ol className="mt-7 w-full divide-y divide-border rounded-[13px] border border-border bg-surface text-left">
<WelcomeStep n="01" title="Add a project"> <WelcomeStep n="01" title="Add a project">
Pick a local git repository and choose which agents run it. Choose a local git repository and select the agents AO should use.
</WelcomeStep> </WelcomeStep>
<WelcomeStep n="02" title="Describe a task"> <WelcomeStep n="02" title="Describe a task">
The orchestrator plans the work and spawns worker sessions, each on its own branch. Tell the orchestrator what you want done; it creates worker sessions on isolated branches.
</WelcomeStep> </WelcomeStep>
<WelcomeStep n="03" title="Review and merge"> <WelcomeStep n="03" title="Review and merge">
Sessions move across this board as they progress: Follow each session from work to review to merge readiness:
<span className="mt-2 flex flex-wrap items-center gap-x-1.5 gap-y-1.5"> <span className="mt-2 flex flex-wrap items-center gap-x-1.5 gap-y-1.5">
{FLOW_LEGEND.map((zone, index) => ( {FLOW_LEGEND.map((zone, index) => (
<Fragment key={zone.label}> <Fragment key={zone.label}>
@ -123,7 +123,7 @@ export function ProjectBoardEmpty({
return ( return (
<div className="flex h-full min-h-0 items-center justify-center overflow-y-auto"> <div className="flex h-full min-h-0 items-center justify-center overflow-y-auto">
<div className="flex w-full max-w-[400px] flex-col items-center pb-[5vh] text-center"> <div className="flex w-full max-w-[400px] flex-col items-center pb-[5vh] text-center">
<h2 className="text-[15px] font-semibold tracking-[-0.01em] text-foreground">No sessions yet</h2> <h2 className="text-[15px] font-semibold tracking-[-0.01em] text-foreground">No worker sessions yet</h2>
<p className="mt-2 text-[12.5px] leading-[1.6] text-muted-foreground"> <p className="mt-2 text-[12.5px] leading-[1.6] text-muted-foreground">
Describe a task and the orchestrator plans it, spawns worker sessions, and tracks them here from work to Describe a task and the orchestrator plans it, spawns worker sessions, and tracks them here from work to
merge. merge.

View File

@ -225,11 +225,16 @@ export function SessionsBoard({ projectId }: SessionsBoardProps) {
return ( return (
<div className="flex h-full min-h-0 flex-col bg-background text-foreground"> <div className="flex h-full min-h-0 flex-col bg-background text-foreground">
{/* The first-launch welcome carries its own orientation; a "Board"
header above it would describe a board that isn't rendered
(review feedback on #2432). */}
{!showWelcome && (
<DashboardSubhead <DashboardSubhead
title="Board" title="Board"
subtitle="Live agent sessions flowing from work → review → merge." subtitle="Live agent sessions flowing from work → review → merge."
actions={actions} actions={actions}
/> />
)}
<div className="min-h-0 flex-1 overflow-hidden p-[18px]"> <div className="min-h-0 flex-1 overflow-hidden p-[18px]">
{projectId && health.state !== "ok" ? ( {projectId && health.state !== "ok" ? (