termi-kids 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,6 @@ import { appendAudit } from '../safety/audit.js';
16
16
  import { ensureGuardFetch, guardProgressBar } from '../safety/guarddownload.js';
17
17
  import { guardModelReady } from '../safety/modelstore.js';
18
18
  import { nameIsOkay } from '../safety/prefilter.js';
19
- import { scaffoldById } from '../projects/scaffolds/index.js';
20
19
  import { renderBanner } from '../ui/banner.js';
21
20
  import { mascot } from '../ui/mascot.js';
22
21
  import { style } from '../ui/theme.js';
@@ -262,14 +261,7 @@ async function createPinStep() {
262
261
  }
263
262
  }
264
263
  async function consentStep(settings) {
265
- const ageBand = ensure(await p.select({
266
- message: 'How old is your kid?',
267
- options: [
268
- { value: 'under13', label: 'Under 13' },
269
- { value: 'teen', label: '13 or older' },
270
- ],
271
- initialValue: 'under13',
272
- }));
264
+ // One safety bar for every age. No under-13 / over-13 split.
273
265
  const agreed = ensure(await p.confirm({ message: `${T.wizard.consentIntro} Do you agree?`, initialValue: true }));
274
266
  if (!agreed) {
275
267
  bail();
@@ -280,13 +272,13 @@ async function consentStep(settings) {
280
272
  ts: attestedAt,
281
273
  layer: 'system',
282
274
  event: 'consent',
283
- excerpt: `parent consent, age band ${ageBand}`,
275
+ excerpt: 'parent consent, one safety bar for all ages',
284
276
  });
285
277
  }
286
278
  catch {
287
279
  // Consent still counts; the audit line is best effort.
288
280
  }
289
- return { ...settings, ageBand, consentAttestedAt: attestedAt };
281
+ return { ...settings, ageBand: 'under13', consentAttestedAt: attestedAt };
290
282
  }
291
283
  async function providerLoop(settings) {
292
284
  let current = settings;
@@ -318,41 +310,27 @@ async function providerLoop(settings) {
318
310
  return { ...current, activeProvider: null };
319
311
  }
320
312
  /**
321
- * Offers the on-device safety checker download. Default is yes: the
322
- * classifier setting ships on, and this step starts fetching its model file
323
- * in the background. The parent hears, plainly, that basic safety (the
324
- * local filter plus the online checks) is already on and that the checker
325
- * strengthens it when the download lands, then chooses to start building
326
- * now or wait and watch the bar. Either way the pipeline hot-attaches the
327
- * checker the moment the verified file is in place. Declining turns the
328
- * setting off; a failed or interrupted download resumes on the next start.
313
+ * Installs the on-device safety checker as part of setup. Always on: the
314
+ * model download starts here (and resumes on later boots). Parent can wait
315
+ * for the bar or keep going; declining is not offered.
329
316
  */
330
317
  async function localGuardStep(settings) {
331
318
  if (guardModelReady()) {
332
- // Model already present: keep whatever the parent chose before. A
333
- // wizard re-run must not silently flip a deliberate off back to on.
334
- return settings;
335
- }
336
- const wants = ensure(await p.confirm({ message: T.wizard.guardOffer, initialValue: true }));
337
- if (!wants) {
338
- p.log.info(T.wizard.guardDeclined);
339
- audit('settings_change', 'local classifier off (declined in setup)');
340
- return { ...settings, localClassifier: false };
319
+ return { ...settings, localClassifier: true };
341
320
  }
321
+ p.log.info(T.wizard.guardOffer);
342
322
  const fetchDone = ensureGuardFetch();
343
323
  p.log.info(T.wizard.guardBackground);
324
+ audit('settings_change', 'local classifier install started in setup');
344
325
  const wait = ensure(await p.select({
345
326
  message: T.wizard.guardWaitPick,
346
327
  options: [
347
- { value: 'now', label: T.wizard.guardStartNow, hint: T.wizard.guardStartNowHint },
348
328
  { value: 'wait', label: T.wizard.guardWaitHere, hint: T.wizard.guardWaitHereHint },
329
+ { value: 'now', label: T.wizard.guardStartNow, hint: T.wizard.guardStartNowHint },
349
330
  ],
350
- initialValue: 'now',
331
+ initialValue: 'wait',
351
332
  }));
352
333
  if (wait === 'wait') {
353
- // The wait shows a live bar with periodic escape hatches (after one
354
- // minute, then every ten): a slow connection must not trap the parent
355
- // in setup when the download finishes fine in the background anyway.
356
334
  const escapeAfterMs = [60_000, 600_000];
357
335
  let escapeIndex = 0;
358
336
  for (;;) {
@@ -406,64 +384,12 @@ async function firstGameStep(settings) {
406
384
  if (p.isCancel(wants) || !wants) {
407
385
  return;
408
386
  }
409
- const games = scaffoldById('games');
410
- if (games === undefined) {
411
- return;
412
- }
413
- const themeId = await p.select({
414
- message: 'Pick a style for your game.',
415
- options: games.themes.map((t) => ({ value: t.id, label: `${t.emoji} ${t.label}` })),
416
- });
417
- if (p.isCancel(themeId)) {
418
- return;
419
- }
420
- const theme = games.themes.find((t) => t.id === themeId);
421
- if (theme === undefined) {
422
- return;
423
- }
424
- const CUSTOM = '__custom__';
425
- const suggestions = suggestProjectNames(theme.label);
426
- const namePick = await p.select({
427
- message: 'Pick a name for it.',
428
- options: [
429
- ...suggestions.map((n) => ({ value: n, label: n })),
430
- { value: CUSTOM, label: 'Type my own name' },
431
- ],
432
- });
433
- if (p.isCancel(namePick)) {
434
- return;
435
- }
436
- let prettyName = namePick;
437
- if (namePick === CUSTOM) {
438
- const typed = await p.text({
439
- message: 'What is its name?',
440
- validate: (value) => {
441
- const trimmed = (value ?? '').trim();
442
- if (trimmed.length === 0)
443
- return 'It needs a name.';
444
- if (!nameIsOkay(trimmed))
445
- return 'That name will not work. Try another one.';
446
- return undefined;
447
- },
448
- });
449
- if (p.isCancel(typed)) {
450
- return;
451
- }
452
- prettyName = typed.trim();
453
- }
454
387
  try {
455
- const create = await import('../projects/create.js');
456
- const home = await import('../surfaces/home.js');
457
- const made = create.createProject('games', theme.id, prettyName);
458
- await home.awardBadge('first-project');
459
- const starters = made.starterPrompts.slice(0, 2);
460
- if (starters.length > 0) {
461
- p.note(starters.map((line) => `- ${line}`).join('\n'), 'Try saying');
462
- }
463
- await home.openChatLoop(made.project, settings);
388
+ const build = await import('../surfaces/buildGame.js');
389
+ await build.runBuildGame(settings);
464
390
  }
465
391
  catch {
466
- p.log.warn('I could not start the game yet. Try "termi new" next time.');
392
+ p.log.warn('I could not start the game yet. Try Build a game next time.');
467
393
  }
468
394
  }
469
395
  /** Runs the full setup wizard, parent flow then kid flow. */
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Build a game: pick an idea, prompt loop, live preview, done/improve,
3
+ * final polish. Projects save in the local library under TERMI_PROJECTS_DIR.
4
+ */
5
+ import * as p from '@clack/prompts';
6
+ import { getSecret } from '../auth/keychain.js';
7
+ import { hasTokens } from '../auth/tokens.js';
8
+ import { startPreview } from '../preview/server.js';
9
+ import { createProviderClient, pickClassifierBackend, } from '../providers/index.js';
10
+ import { classifyProviderError } from '../providers/errors.js';
11
+ import { appendAudit } from '../safety/audit.js';
12
+ import { createSafetyPipeline } from '../safety/classifier.js';
13
+ import { ensureGuardFetch } from '../safety/guarddownload.js';
14
+ import { lazyGuardAccessor } from '../safety/guardrunner.js';
15
+ import { guardModelReady } from '../safety/modelstore.js';
16
+ import { createSessionState } from '../safety/session.js';
17
+ import { nameIsOkay } from '../safety/prefilter.js';
18
+ import { createBlankGameProject } from '../projects/blankGame.js';
19
+ import { GAME_IDEAS, gameIdeaById, gameIdeaMenuOptions, isOwnIdea, } from '../projects/gameIdeas.js';
20
+ import { providerLabel } from '../setup/wizard.js';
21
+ import { renderProviderError } from '../ui/errors.js';
22
+ import { style } from '../ui/theme.js';
23
+ import { T } from '../ui/text.js';
24
+ import { completenessHint, defaultNameForIdea, helpQuestions, parseDoneChoice, polishPrompt, seedPromptForIdea, suggestPromptFromAnswers, summarizeProjectFiles, } from './buildLoop.js';
25
+ import { awardBadge } from './home.js';
26
+ function audit(event) {
27
+ try {
28
+ appendAudit(event);
29
+ }
30
+ catch {
31
+ // Never break the kid flow for the log.
32
+ }
33
+ }
34
+ function providerAvailability(settings) {
35
+ return {
36
+ 'openai-chatgpt': hasTokens(),
37
+ 'openai-api': (getSecret('api-key-openai-api') ?? '').length > 0,
38
+ anthropic: (getSecret('api-key-anthropic') ?? '').length > 0,
39
+ xai: (getSecret('api-key-xai') ?? '').length > 0 && settings.xaiParentAck,
40
+ };
41
+ }
42
+ async function pickIdea() {
43
+ const pick = await p.select({
44
+ message: 'Build a game. What do you want to make?',
45
+ options: gameIdeaMenuOptions(),
46
+ initialValue: GAME_IDEAS[0].id,
47
+ });
48
+ if (p.isCancel(pick)) {
49
+ return null;
50
+ }
51
+ return gameIdeaById(pick) ?? null;
52
+ }
53
+ async function pickProjectName(idea) {
54
+ const suggested = defaultNameForIdea(idea);
55
+ const typed = await p.text({
56
+ message: 'Name your game.',
57
+ placeholder: suggested,
58
+ defaultValue: suggested,
59
+ validate: (value) => {
60
+ const trimmed = (value ?? '').trim() || suggested;
61
+ if (trimmed.length === 0)
62
+ return 'It needs a name.';
63
+ if (!nameIsOkay(trimmed))
64
+ return 'That name will not work. Try another one.';
65
+ return undefined;
66
+ },
67
+ });
68
+ if (p.isCancel(typed)) {
69
+ return null;
70
+ }
71
+ const name = typed.trim() || suggested;
72
+ return name;
73
+ }
74
+ async function gatherHelpAnswers(idea) {
75
+ const questions = helpQuestions(idea);
76
+ const answers = [];
77
+ for (const question of questions) {
78
+ const ans = await p.text({
79
+ message: question,
80
+ validate: (value) => (value ?? '').trim().length > 0 ? undefined : 'Say a little so I can help.',
81
+ });
82
+ if (p.isCancel(ans)) {
83
+ return null;
84
+ }
85
+ answers.push(ans.trim());
86
+ }
87
+ return answers;
88
+ }
89
+ async function obtainPrompt(idea) {
90
+ const path = await p.select({
91
+ message: 'How do you want to tell Termi what to build?',
92
+ options: [
93
+ { value: 'write', label: 'I will write my own prompt' },
94
+ { value: 'help', label: 'Help me with a prompt for my idea' },
95
+ ...(seedPromptForIdea(idea).length > 0
96
+ ? [{ value: 'seed', label: 'Use the ready idea prompt' }]
97
+ : []),
98
+ ],
99
+ });
100
+ if (p.isCancel(path)) {
101
+ return null;
102
+ }
103
+ let draft = '';
104
+ if (path === 'seed') {
105
+ draft = seedPromptForIdea(idea);
106
+ }
107
+ else if (path === 'help') {
108
+ const answers = await gatherHelpAnswers(idea);
109
+ if (answers === null) {
110
+ return null;
111
+ }
112
+ draft = suggestPromptFromAnswers(idea, answers);
113
+ p.note(draft, 'Suggested prompt');
114
+ }
115
+ const text = await p.text({
116
+ message: path === 'write' ? 'Type your prompt.' : 'Edit the prompt, or press Enter to run it.',
117
+ defaultValue: draft,
118
+ placeholder: draft || 'Make a fun browser game where...',
119
+ validate: (value) => (value ?? '').trim().length > 0 ? undefined : 'Need a prompt to build.',
120
+ });
121
+ if (p.isCancel(text)) {
122
+ return null;
123
+ }
124
+ return text.trim();
125
+ }
126
+ function listKidFileSummaries(project) {
127
+ const out = [];
128
+ for (const file of project.listKidFiles()) {
129
+ const content = project.readFile(file.relPath);
130
+ if (content !== null) {
131
+ out.push({ relPath: file.relPath, content });
132
+ }
133
+ }
134
+ return out;
135
+ }
136
+ async function runOneTurn(kidMessage, deps) {
137
+ const loop = await import('../agent/loop.js');
138
+ const spin = p.spinner();
139
+ spin.start(T.chat.thinking);
140
+ let result;
141
+ try {
142
+ result = await loop.runTurn(kidMessage, {
143
+ provider: deps.provider,
144
+ modelAlias: deps.settings.modelAlias,
145
+ safety: deps.safety,
146
+ session: deps.session,
147
+ project: deps.project,
148
+ snapshots: deps.snapshots,
149
+ preview: deps.preview,
150
+ audit,
151
+ ui: {
152
+ onActivity(line) {
153
+ spin.message(line);
154
+ },
155
+ },
156
+ });
157
+ }
158
+ catch (err) {
159
+ spin.stop();
160
+ const label = deps.settings.activeProvider !== null
161
+ ? providerLabel(deps.settings.activeProvider)
162
+ : 'AI helper';
163
+ console.log(renderProviderError(classifyProviderError(err), label));
164
+ return 'error';
165
+ }
166
+ spin.stop(T.chat.working);
167
+ if (result.status === 'ok') {
168
+ if (result.replyText) {
169
+ console.log(result.replyText);
170
+ }
171
+ try {
172
+ deps.preview.notifyChange();
173
+ }
174
+ catch {
175
+ // Preview may already be closed.
176
+ }
177
+ return 'ok';
178
+ }
179
+ if (result.status === 'blocked') {
180
+ if (result.screen !== null) {
181
+ console.log(result.screen.body);
182
+ }
183
+ else {
184
+ console.log(T.blocks.generic);
185
+ }
186
+ return 'blocked';
187
+ }
188
+ if (result.screen !== null) {
189
+ console.log(result.screen.body);
190
+ }
191
+ else {
192
+ console.log(T.errors.oops);
193
+ }
194
+ return 'error';
195
+ }
196
+ /**
197
+ * Full Build a game session for one project from idea pick to library save.
198
+ * Returns the project when something was saved, null on cancel at the start.
199
+ */
200
+ export async function runBuildGame(settings) {
201
+ const idea = await pickIdea();
202
+ if (idea === null) {
203
+ return null;
204
+ }
205
+ const prettyName = await pickProjectName(idea);
206
+ if (prettyName === null) {
207
+ return null;
208
+ }
209
+ let project;
210
+ try {
211
+ project = createBlankGameProject(prettyName, idea.label);
212
+ }
213
+ catch (err) {
214
+ p.log.error(err instanceof Error ? err.message : 'Could not create the game folder.');
215
+ return null;
216
+ }
217
+ await awardBadge('first-project');
218
+ let provider = null;
219
+ if (settings.activeProvider !== null) {
220
+ try {
221
+ provider = createProviderClient(settings.activeProvider);
222
+ }
223
+ catch {
224
+ provider = null;
225
+ }
226
+ }
227
+ if (provider === null) {
228
+ p.log.warn(`${T.offline.noProvider} ${T.offline.stillWorks}`);
229
+ p.log.info(`Your game folder is ready: ${project.meta.prettyName}`);
230
+ return project;
231
+ }
232
+ if (settings.localClassifier && !guardModelReady()) {
233
+ void ensureGuardFetch();
234
+ }
235
+ const backend = pickClassifierBackend(settings, providerAvailability(settings));
236
+ const safety = createSafetyPipeline({
237
+ classifierModel: () => backend.classifierClient !== null
238
+ ? backend.classifierClient.languageModel('classifier', settings.modelAlias)
239
+ : null,
240
+ moderationKey: () => backend.moderationKey,
241
+ localGuard: lazyGuardAccessor(settings.localClassifier),
242
+ audit,
243
+ });
244
+ const session = createSessionState();
245
+ let snapshots = {
246
+ beginTurn() { },
247
+ undo() {
248
+ return false;
249
+ },
250
+ redo() {
251
+ return false;
252
+ },
253
+ };
254
+ try {
255
+ const snapMod = await import('../projects/snapshots.js');
256
+ snapshots = snapMod.createSnapshotStore(project);
257
+ }
258
+ catch {
259
+ // undo optional
260
+ }
261
+ const preview = await startPreview(project.dir, { openBrowser: true });
262
+ console.log(`${T.chat.previewOpened} ${style.dim(preview.url)}`);
263
+ console.log(style.dim('The browser will update after each build.'));
264
+ const turnDeps = { provider, settings, project, preview, safety, session, snapshots };
265
+ // First build
266
+ for (;;) {
267
+ const prompt = await obtainPrompt(idea);
268
+ if (prompt === null) {
269
+ break;
270
+ }
271
+ const status = await runOneTurn(prompt, turnDeps);
272
+ if (status === 'ok') {
273
+ try {
274
+ preview.notifyChange();
275
+ }
276
+ catch {
277
+ //
278
+ }
279
+ }
280
+ const next = await p.select({
281
+ message: 'Do you think you are done, or want to improve the idea?',
282
+ options: [
283
+ { value: 'improve', label: 'Improve' },
284
+ { value: 'done', label: 'I am done' },
285
+ ],
286
+ initialValue: 'improve',
287
+ });
288
+ if (p.isCancel(next)) {
289
+ break;
290
+ }
291
+ const choice = parseDoneChoice(next);
292
+ if (choice === 'improve') {
293
+ continue;
294
+ }
295
+ if (choice === 'done') {
296
+ console.log(style.title('Testing your game and making final fixes...'));
297
+ const summary = summarizeProjectFiles(listKidFileSummaries(project));
298
+ const hint = completenessHint(summary);
299
+ console.log(style.dim(`Suggestion: ${hint}`));
300
+ const approve = await p.confirm({
301
+ message: 'Apply this final improvement?',
302
+ initialValue: true,
303
+ });
304
+ if (!p.isCancel(approve) && approve) {
305
+ const finalPrompt = polishPrompt(`${hint}. Project: ${summary}`);
306
+ await runOneTurn(finalPrompt, turnDeps);
307
+ try {
308
+ preview.notifyChange();
309
+ }
310
+ catch {
311
+ //
312
+ }
313
+ console.log(style.good('Final fixes applied. Check the browser!'));
314
+ }
315
+ else {
316
+ console.log('Okay. Your game is saved as is.');
317
+ }
318
+ break;
319
+ }
320
+ }
321
+ p.log.success(`Saved in your game library: ${project.meta.prettyName}`);
322
+ // Keep preview open so the kid can keep playing; they Ctrl+C later or return home.
323
+ return project;
324
+ }
325
+ /** Exported for tests that assert the catalog is wired. */
326
+ export function buildGameIdeaCount() {
327
+ return GAME_IDEAS.length;
328
+ }
329
+ export function buildGameFirstLabel() {
330
+ return GAME_IDEAS[0]?.label ?? '';
331
+ }
332
+ export function buildGameIsOwnFirst() {
333
+ const first = GAME_IDEAS[0];
334
+ return first !== undefined && isOwnIdea(first);
335
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Pure helpers for the Build a game prompt loop.
3
+ * UI and network live in buildGame.ts; tests cover this module fully.
4
+ */
5
+ import { isOwnIdea } from '../projects/gameIdeas.js';
6
+ /** Clarifying questions when the kid picks Help me with a prompt. */
7
+ export function helpQuestions(idea) {
8
+ if (isOwnIdea(idea)) {
9
+ return [
10
+ 'What is your game about in one short line?',
11
+ 'How does the player win or finish?',
12
+ 'What keys or clicks should control it?',
13
+ ];
14
+ }
15
+ return [
16
+ `What should feel special about "${idea.label}"?`,
17
+ 'Do you want easy mode, normal, or hard?',
18
+ ];
19
+ }
20
+ /** Builds a starter prompt from a catalog idea (not own-idea). */
21
+ export function seedPromptForIdea(idea) {
22
+ if (isOwnIdea(idea) || idea.seedPrompt.trim().length === 0) {
23
+ return '';
24
+ }
25
+ return idea.seedPrompt.trim();
26
+ }
27
+ /**
28
+ * Turns help-question answers into a suggested build prompt the kid can
29
+ * edit. Works offline without a model call.
30
+ */
31
+ export function suggestPromptFromAnswers(idea, answers) {
32
+ const cleaned = answers.map((a) => a.trim()).filter((a) => a.length > 0);
33
+ if (isOwnIdea(idea)) {
34
+ const about = cleaned[0] ?? 'a fun browser game';
35
+ const win = cleaned[1] ?? 'reach a high score';
36
+ const controls = cleaned[2] ?? 'arrow keys or click';
37
+ return (`Make a complete local browser game about ${about}. ` +
38
+ `The player wins when they ${win}. ` +
39
+ `Controls: ${controls}. ` +
40
+ `Use only HTML, CSS, and JavaScript in index.html, style.css, and game.js. ` +
41
+ `No images from the internet. Keep it kid friendly and playable after one load.`);
42
+ }
43
+ const special = cleaned[0] ?? idea.blurb;
44
+ const difficulty = cleaned[1] ?? 'normal';
45
+ const base = seedPromptForIdea(idea) || `Make a ${idea.label} browser game.`;
46
+ return (`${base} ` +
47
+ `Make it feel special by: ${special}. ` +
48
+ `Difficulty: ${difficulty}. ` +
49
+ `Use only HTML, CSS, and JavaScript files. No outside images.`);
50
+ }
51
+ /** Final polish instruction after the kid says they are done. */
52
+ export function polishPrompt(projectSummary) {
53
+ const summary = projectSummary.trim().length > 0
54
+ ? projectSummary.trim().slice(0, 800)
55
+ : 'the current game files';
56
+ return (`You are doing final testing and fixes for this kid game. ` +
57
+ `Read the project files. Based on ${summary}, make ONE clear improvement ` +
58
+ `so the game feels more complete (for example a start screen, score, ` +
59
+ `restart key, win or lose message, or clearer controls). ` +
60
+ `Apply the change in the files. Keep it simple and kid friendly. ` +
61
+ `No internet images. Then briefly say what you fixed.`);
62
+ }
63
+ /** Summarize kid files for the polish prompt (pure). */
64
+ export function summarizeProjectFiles(files) {
65
+ if (files.length === 0) {
66
+ return 'no files yet';
67
+ }
68
+ return files
69
+ .map((f) => {
70
+ const lines = f.content.split('\n').length;
71
+ const hasCanvas = /canvas/i.test(f.content);
72
+ const hasScore = /score/i.test(f.content);
73
+ const flags = [
74
+ hasCanvas ? 'canvas' : null,
75
+ hasScore ? 'score' : null,
76
+ lines < 20 ? 'short' : null,
77
+ ]
78
+ .filter(Boolean)
79
+ .join(',');
80
+ return `${f.relPath}(${lines} lines${flags ? `; ${flags}` : ''})`;
81
+ })
82
+ .join('; ');
83
+ }
84
+ /** Maps select values to done/improve. */
85
+ export function parseDoneChoice(value) {
86
+ if (value === 'done' || value === 'improve') {
87
+ return value;
88
+ }
89
+ return null;
90
+ }
91
+ /** Default project name from an idea label. */
92
+ export function defaultNameForIdea(idea) {
93
+ if (isOwnIdea(idea)) {
94
+ return 'My Game';
95
+ }
96
+ return idea.label;
97
+ }
98
+ /**
99
+ * One completeness suggestion line for the terminal (before the kid
100
+ * approves a polish apply). Pure heuristic from file summary text.
101
+ */
102
+ export function completenessHint(summary) {
103
+ const s = summary.toLowerCase();
104
+ if (!s.includes('score')) {
105
+ return 'Add a clear score the player can see.';
106
+ }
107
+ if (!s.includes('canvas') && !s.includes('button')) {
108
+ return 'Add a start screen with a Play button.';
109
+ }
110
+ if (s.includes('short')) {
111
+ return 'Add a win or lose message and a restart key.';
112
+ }
113
+ return 'Add clearer control hints on the start screen.';
114
+ }
@@ -110,10 +110,10 @@ export function helpText() {
110
110
  ['/preview', 'watch your project run'],
111
111
  ['/undo', 'take back the last change'],
112
112
  ['/redo', 'bring a change back'],
113
- ['/new', 'start a fresh project'],
114
- ['/ideas', 'get fun ideas'],
113
+ ['/new', 'build a new game'],
114
+ ['/ideas', 'get game ideas'],
115
115
  ['/badges', 'see your badges'],
116
- ['/learn', 'play short AI lessons'],
116
+ ['/learn', 'Learn AI lessons'],
117
117
  ['/quest', 'build with me, step by step'],
118
118
  ['/help', 'show this list'],
119
119
  ['/done', 'finish and celebrate'],