Skip to content

Commit b00d543

Browse files
committed
feat(app): tap an idea on the welcome screen
Four crew templates show as cards under the first-run checks; tapping one opens the new-crew dialog already on that template.
1 parent 688149b commit b00d543

6 files changed

Lines changed: 88 additions & 8 deletions

File tree

‎app/src/features/crews/CrewDialog.tsx‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,30 @@ import { WorkFolderField } from "./WorkFolderField";
1616

1717
const MODELS: BotModel[] = ["default", "fable", "opus", "sonnet", "haiku"];
1818

19-
/** Creates a crew with its chief, or renames `crew`. */
20-
export function CrewDialog({ crew, onClose }: { crew?: Crew; onClose(): void }) {
19+
/**
20+
* Creates a crew with its chief, or renames `crew`. `start` opens a new crew
21+
* already on that template, as when the owner taps an idea on the welcome
22+
* screen.
23+
*/
24+
export function CrewDialog({
25+
crew,
26+
start,
27+
onClose,
28+
}: {
29+
crew?: Crew;
30+
start?: CrewTemplate | undefined;
31+
onClose(): void;
32+
}) {
2133
const t = useT();
2234
const api = useApi();
2335
const putCrew = useApp((state) => state.putCrew);
2436
const selectCrew = useApp((state) => state.selectCrew);
25-
const [name, setName] = useState(crew?.name ?? "");
37+
const [name, setName] = useState(
38+
crew?.name ?? (start ? t.crewTemplates.items[start.id].name : ""),
39+
);
2640
const [color, setColor] = useState<string | null>(crew?.color ?? null);
2741
const [folder, setFolder] = useState<string | null>(null);
28-
const [goal, setGoal] = useState("");
42+
const [goal, setGoal] = useState(start ? t.crewTemplates.items[start.id].goal : "");
2943
const [chiefModel, setChiefModel] = useState<BotModel>("default");
3044
const selectBot = useApp((state) => state.selectBot);
3145
const putBot = useApp((state) => state.putBot);
@@ -34,7 +48,7 @@ export function CrewDialog({ crew, onClose }: { crew?: Crew; onClose(): void })
3448
// A new crew starts from a template (spec 29.1) when the catalog loaded:
3549
// `undefined` is the list of templates, `null` an empty crew.
3650
const { roles, failed: catalogFailed } = useCatalog(!crew);
37-
const [template, setTemplate] = useState<CrewTemplate | null | undefined>(undefined);
51+
const [template, setTemplate] = useState<CrewTemplate | null | undefined>(start);
3852
const loading = !crew && roles === null && !catalogFailed;
3953
const choosing = !crew && template === undefined && roles !== null;
4054

‎app/src/features/onboarding/Welcome.tsx‎

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@ import { Button } from "../../ui/Button";
88
import { useDaemonSettings } from "../account/useDaemonSettings";
99
import { BotAvatar } from "../bots/BotAvatar";
1010
import { CrewDialog } from "../crews/CrewDialog";
11+
import { CREW_TEMPLATES, type CrewTemplate } from "../crews/crewTemplates";
1112
import { ClaudeCodeHelp } from "./ClaudeCodeHelp";
1213
import { SignInButton } from "./SignIn";
1314

15+
/** Templates shown as ideas on the first screen. */
16+
const IDEAS = 4;
17+
1418
/** First run: what is ready, what is not, and the first crew. */
1519
export function Welcome() {
16-
const w = useT().onboarding.welcome;
20+
const t = useT();
21+
const w = t.onboarding.welcome;
22+
const ideas = t.crewTemplates.items;
1723
const system = useApp((state) => state.system);
1824
const [creating, setCreating] = useState(false);
25+
const [idea, setIdea] = useState<CrewTemplate | undefined>(undefined);
1926
const whenClosed = usePref(prefs.whenClosed);
2027
const { settings } = useDaemonSettings();
2128
const running =
@@ -82,11 +89,42 @@ export function Welcome() {
8289
<p className="font-semibold">{w.askFirstTitle}</p>
8390
<p className="mt-1 text-ink-soft">{w.askFirstBody}</p>
8491
</div>
85-
<Button className="mt-6" variant="primary" icon={Plus} onClick={() => setCreating(true)}>
92+
<section aria-label={w.ideasTitle} className="mt-6">
93+
<h2 className="font-semibold text-sm">{w.ideasTitle}</h2>
94+
<ul className="m-0 mt-2 grid list-none grid-cols-1 gap-2 p-0 sm:grid-cols-2">
95+
{CREW_TEMPLATES.slice(0, IDEAS).map((template) => {
96+
const item = ideas[template.id];
97+
return (
98+
<li key={template.id} className="flex">
99+
<button
100+
type="button"
101+
onClick={() => {
102+
setIdea(template);
103+
setCreating(true);
104+
}}
105+
className="flex w-full flex-col gap-0.5 rounded-xl border border-line bg-panel p-3 text-left transition-colors hover:border-line-strong hover:bg-sunken"
106+
>
107+
<span className="font-semibold text-sm">{item.name}</span>
108+
<span className="line-clamp-2 text-ink-soft text-sm">{item.summary}</span>
109+
</button>
110+
</li>
111+
);
112+
})}
113+
</ul>
114+
</section>
115+
<Button
116+
className="mt-4"
117+
variant="primary"
118+
icon={Plus}
119+
onClick={() => {
120+
setIdea(undefined);
121+
setCreating(true);
122+
}}
123+
>
86124
{w.createCrew}
87125
</Button>
88126
</div>
89-
{creating && <CrewDialog onClose={() => setCreating(false)} />}
127+
{creating && <CrewDialog start={idea} onClose={() => setCreating(false)} />}
90128
</main>
91129
);
92130
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { cleanup, fireEvent, screen, within } from "@testing-library/react";
2+
import { afterEach, describe, expect, test } from "vitest";
3+
import { FakeBotloft } from "../../lib/fake";
4+
import { renderApp } from "../../test/app";
5+
6+
afterEach(cleanup);
7+
8+
describe("ideas on the welcome screen", () => {
9+
test("tapping an idea opens a new crew already set up from that template", async () => {
10+
renderApp(new FakeBotloft());
11+
const ideas = await screen.findByRole("region", { name: "Or tap an idea" });
12+
expect(within(ideas).getAllByRole("listitem")).toHaveLength(4);
13+
14+
fireEvent.click(within(ideas).getByRole("button", { name: /Software team/ }));
15+
const dialog = await screen.findByRole("dialog", { name: "New crew" });
16+
expect((within(dialog).getByLabelText("Name") as HTMLInputElement).value).toBe("Software team");
17+
expect(within(dialog).getByText(/This team adds/)).toBeDefined();
18+
});
19+
20+
test("the plain button still starts at the list of templates", async () => {
21+
renderApp(new FakeBotloft());
22+
fireEvent.click(await screen.findByRole("button", { name: "Create your first crew" }));
23+
expect(await screen.findByRole("dialog", { name: "Start from a template" })).toBeDefined();
24+
});
25+
});

‎app/src/i18n/en/onboarding.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export const onboarding = {
6565
askFirstTitle: "Bots ask before they change things",
6666
askFirstBody:
6767
"When a bot wants to run a command or edit a file, it asks in its chat and waits for you to allow or deny it.",
68+
ideasTitle: "Or tap an idea",
6869
createCrew: "Create your first crew",
6970
},
7071
claudeCode: {

‎app/src/i18n/es/onboarding.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export const onboarding: Messages["onboarding"] = {
6565
askFirstTitle: "Los bots preguntan antes de cambiar cosas",
6666
askFirstBody:
6767
"Cuando un bot quiere ejecutar un comando o editar un archivo, lo pregunta en su chat y espera a que lo permitas o lo deniegues.",
68+
ideasTitle: "O toca una idea",
6869
createCrew: "Crear tu primer equipo",
6970
},
7071
claudeCode: {

‎app/src/i18n/pt-BR/onboarding.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export const onboarding: Messages["onboarding"] = {
6464
askFirstTitle: "Os bots perguntam antes de mudar as coisas",
6565
askFirstBody:
6666
"Quando um bot quer rodar um comando ou editar um arquivo, ele pergunta no chat e espera você permitir ou negar.",
67+
ideasTitle: "Ou toque em uma ideia",
6768
createCrew: "Criar sua primeira equipe",
6869
},
6970
claudeCode: {

0 commit comments

Comments
 (0)