@cat-factory/app 0.47.8 → 0.47.10

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.
@@ -7,6 +7,7 @@
7
7
  // the user returns to the board, summoned back (a notification) only if the re-run raises new
8
8
  // options or hits the iteration cap. The converged direction — not the original description — is
9
9
  // what the downstream stage (the requirements review / the architect) consumes.
10
+ import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
10
11
  import { parseOutputOutline } from '~/utils/agentOutput'
11
12
  import type {
12
13
  BrainstormItem,
@@ -22,6 +23,7 @@ const board = useBoardStore()
22
23
  const brainstorm = useBrainstormStore()
23
24
  const ui = useUiStore()
24
25
  const toast = useToast()
26
+ const { t } = useI18n()
25
27
 
26
28
  const drafts = ref<Record<string, string>>({})
27
29
  const redoComment = ref('')
@@ -41,9 +43,15 @@ const { open, blockId, stage, close } = useResultView('brainstorm', {
41
43
  })
42
44
  const activeStage = computed<BrainstormStage>(() => stage.value ?? 'requirements')
43
45
  const isArchitecture = computed(() => activeStage.value === 'architecture')
44
- const subjectNoun = computed(() => (isArchitecture.value ? 'approach' : 'requirements'))
46
+ const subjectNoun = computed(() =>
47
+ isArchitecture.value
48
+ ? t('brainstorm.subjectNoun.architecture')
49
+ : t('brainstorm.subjectNoun.requirements'),
50
+ )
45
51
  const docNoun = computed(() =>
46
- isArchitecture.value ? 'technical approach' : 'requirements direction',
52
+ isArchitecture.value
53
+ ? t('brainstorm.docNoun.architecture')
54
+ : t('brainstorm.docNoun.requirements'),
47
55
  )
48
56
 
49
57
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
@@ -116,6 +124,28 @@ const STATUS_COLOR = {
116
124
  recommend_requested: 'primary',
117
125
  } as const satisfies Record<ReviewItemStatus, string>
118
126
 
127
+ // Exhaustive enum→label maps of literal keys (keeps the typed-key drift guard live vs a
128
+ // runtime-built `brainstorm.severity.${value}`).
129
+ const SEVERITY_LABELS: Record<ReviewItemSeverity, string> = {
130
+ low: 'brainstorm.severity.low',
131
+ medium: 'brainstorm.severity.medium',
132
+ high: 'brainstorm.severity.high',
133
+ }
134
+ const CATEGORY_LABELS: Record<ReviewItemCategory, string> = {
135
+ gap: 'brainstorm.category.gap',
136
+ clarification: 'brainstorm.category.clarification',
137
+ assumption: 'brainstorm.category.assumption',
138
+ risk: 'brainstorm.category.risk',
139
+ question: 'brainstorm.category.question',
140
+ }
141
+ const STATUS_LABELS: Record<ReviewItemStatus, string> = {
142
+ open: 'brainstorm.itemStatus.open',
143
+ answered: 'brainstorm.itemStatus.answered',
144
+ resolved: 'brainstorm.itemStatus.resolved',
145
+ dismissed: 'brainstorm.itemStatus.dismissed',
146
+ recommend_requested: 'brainstorm.itemStatus.recommend_requested',
147
+ }
148
+
119
149
  function notifyError(title: string, e: unknown) {
120
150
  toast.add({
121
151
  title,
@@ -133,7 +163,7 @@ async function submitReply(item: BrainstormItem) {
133
163
  await brainstorm.reply(session.value, item.id, text)
134
164
  drafts.value = { ...drafts.value, [item.id]: '' }
135
165
  } catch (e) {
136
- notifyError('Could not save the choice', e)
166
+ notifyError(t('brainstorm.toast.saveChoiceError'), e)
137
167
  }
138
168
  }
139
169
 
@@ -142,7 +172,7 @@ async function setStatus(item: BrainstormItem, itemStatus: BrainstormItemStatus)
142
172
  try {
143
173
  await brainstorm.setItemStatus(session.value, item.id, itemStatus)
144
174
  } catch (e) {
145
- notifyError('Could not update the option', e)
175
+ notifyError(t('brainstorm.toast.updateOptionError'), e)
146
176
  }
147
177
  }
148
178
 
@@ -151,14 +181,14 @@ async function incorporate(feedback?: string) {
151
181
  try {
152
182
  await brainstorm.incorporate(session.value, feedback)
153
183
  } catch (e) {
154
- notifyError('Could not incorporate the choices', e)
184
+ notifyError(t('brainstorm.toast.incorporateError'), e)
155
185
  return
156
186
  }
157
187
  redoComment.value = ''
158
188
  showRedo.value = false
159
189
  toast.add({
160
- title: `Drafting the ${docNoun.value} in the background`,
161
- description: "You're back on the board — we'll notify you only if more input is needed.",
190
+ title: t('brainstorm.toast.draftingTitle', { doc: docNoun.value }),
191
+ description: t('brainstorm.toast.draftingDescription'),
162
192
  icon: 'i-lucide-wand-sparkles',
163
193
  })
164
194
  close()
@@ -168,17 +198,18 @@ async function reReview() {
168
198
  if (!blockId.value) return
169
199
  try {
170
200
  const updated = await brainstorm.reReview(blockId.value, activeStage.value)
201
+ const newCount = brainstorm.openCount(updated)
171
202
  toast.add({
172
203
  title:
173
204
  updated.status === 'incorporated'
174
- ? 'Direction settled — continuing the pipeline'
205
+ ? t('brainstorm.toast.reReviewSettled')
175
206
  : updated.status === 'exceeded'
176
- ? 'Iteration limit reached — choose how to proceed'
177
- : `${brainstorm.openCount(updated)} new option(s) to react to`,
207
+ ? t('brainstorm.toast.reReviewExceeded')
208
+ : t('brainstorm.toast.reReviewNewOptions', { count: newCount }, newCount),
178
209
  icon: 'i-lucide-sparkles',
179
210
  })
180
211
  } catch (e) {
181
- notifyError('Could not re-run the brainstorm', e)
212
+ notifyError(t('brainstorm.toast.reReviewError'), e)
182
213
  }
183
214
  }
184
215
 
@@ -187,9 +218,9 @@ async function proceed() {
187
218
  acting.value = true
188
219
  try {
189
220
  await brainstorm.proceed(blockId.value, activeStage.value)
190
- toast.add({ title: 'Proceeding to the next phase', icon: 'i-lucide-arrow-right' })
221
+ toast.add({ title: t('brainstorm.toast.proceeding'), icon: 'i-lucide-arrow-right' })
191
222
  } catch (e) {
192
- notifyError('Could not proceed', e)
223
+ notifyError(t('brainstorm.toast.proceedError'), e)
193
224
  } finally {
194
225
  acting.value = false
195
226
  }
@@ -201,15 +232,15 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
201
232
  try {
202
233
  await brainstorm.resolveExceeded(blockId.value, activeStage.value, choice)
203
234
  if (choice === 'stop-reset') {
204
- toast.add({ title: 'Task reset — edit it and resubmit', icon: 'i-lucide-undo' })
235
+ toast.add({ title: t('brainstorm.toast.taskReset'), icon: 'i-lucide-undo' })
205
236
  close()
206
237
  } else if (choice === 'proceed') {
207
- toast.add({ title: 'Proceeding to the next phase', icon: 'i-lucide-arrow-right' })
238
+ toast.add({ title: t('brainstorm.toast.proceeding'), icon: 'i-lucide-arrow-right' })
208
239
  } else {
209
- toast.add({ title: 'One more brainstorm round granted', icon: 'i-lucide-rotate-cw' })
240
+ toast.add({ title: t('brainstorm.toast.extraRoundGranted'), icon: 'i-lucide-rotate-cw' })
210
241
  }
211
242
  } catch (e) {
212
- notifyError('Could not resolve the brainstorm', e)
243
+ notifyError(t('brainstorm.toast.resolveError'), e)
213
244
  } finally {
214
245
  acting.value = false
215
246
  }
@@ -236,13 +267,17 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
236
267
  </div>
237
268
  <div class="min-w-0">
238
269
  <h1 class="truncate text-base font-semibold text-white">
239
- {{ isArchitecture ? 'Architecture brainstorm' : 'Requirements brainstorm' }}
270
+ {{
271
+ isArchitecture
272
+ ? t('brainstorm.title.architecture')
273
+ : t('brainstorm.title.requirements')
274
+ }}
240
275
  </h1>
241
276
  <p v-if="block" class="truncate text-xs text-slate-500">{{ block.title }}</p>
242
277
  </div>
243
278
  <div class="ml-auto flex items-center gap-1.5">
244
279
  <UBadge v-if="session" color="neutral" variant="subtle" size="sm">
245
- Iteration {{ iteration }} / {{ maxIterations }}
280
+ {{ t('brainstorm.iteration', { current: iteration, max: maxIterations }) }}
246
281
  </UBadge>
247
282
  <UButton icon="i-lucide-x" color="neutral" variant="ghost" size="sm" @click="close" />
248
283
  </div>
@@ -252,10 +287,16 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
252
287
  <!-- main column -->
253
288
  <div class="min-w-0 flex-1 overflow-y-auto px-6 py-5">
254
289
  <p class="mb-4 text-sm text-slate-400">
255
- An AI partner proposed the {{ subjectNoun }} options below, each with its trade-offs.
256
- <span class="text-slate-300">Choose</span> the ones you want (and steer them) and
257
- <span class="text-slate-300">dismiss</span> the rest, then incorporate; it re-runs
258
- until you converge on a {{ docNoun }}.
290
+ <i18n-t keypath="brainstorm.intro" tag="span" scope="global">
291
+ <template #subject>{{ subjectNoun }}</template>
292
+ <template #doc>{{ docNoun }}</template>
293
+ <template #choose>
294
+ <span class="text-slate-300">{{ t('brainstorm.introChoose') }}</span>
295
+ </template>
296
+ <template #dismiss>
297
+ <span class="text-slate-300">{{ t('brainstorm.introDismiss') }}</span>
298
+ </template>
299
+ </i18n-t>
259
300
  </p>
260
301
 
261
302
  <!-- empty state -->
@@ -263,8 +304,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
263
304
  v-if="!session && !busy && !loading"
264
305
  class="rounded-lg border border-dashed border-slate-700 p-8 text-center text-sm text-slate-500"
265
306
  >
266
- No brainstorm yet. It runs automatically when this task's pipeline reaches the
267
- brainstorm step.
307
+ {{ t('brainstorm.empty') }}
268
308
  </div>
269
309
 
270
310
  <!-- working state (initial fetch on open, or an agent pass running) -->
@@ -273,7 +313,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
273
313
  class="flex items-center justify-center gap-2 p-8 text-sm text-slate-400"
274
314
  >
275
315
  <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
276
- {{ loading && !busy ? 'Loading the brainstorm' : 'Generating options…' }}
316
+ {{ loading && !busy ? t('brainstorm.loading') : t('brainstorm.generating') }}
277
317
  </div>
278
318
 
279
319
  <template v-else-if="session">
@@ -283,15 +323,15 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
283
323
  class="mb-4 flex items-center gap-2 rounded-lg border border-emerald-900/60 bg-emerald-950/30 p-4 text-sm text-emerald-300"
284
324
  >
285
325
  <UIcon name="i-lucide-circle-check" class="h-5 w-5 shrink-0" />
286
- The {{ docNoun }} is settled. The document below is what the next stage uses.
326
+ {{ t('brainstorm.settledBanner', { doc: docNoun }) }}
287
327
  </div>
288
328
 
289
329
  <!-- iteration cap hit -->
290
330
  <IterationCapPrompt
291
331
  v-else-if="exceeded"
292
332
  class="mb-4"
293
- :heading="`Reached the ${maxIterations}-iteration limit with options still open.`"
294
- detail="Do one more brainstorm round, proceed to the next phase with the last direction anyway, or stop and reset the task so you can edit it and resubmit."
333
+ :heading="t('brainstorm.exceeded.heading', { max: maxIterations })"
334
+ :detail="t('brainstorm.exceeded.detail')"
295
335
  :loading="acting"
296
336
  @resolve="resolveExceeded"
297
337
  />
@@ -303,12 +343,10 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
303
343
  >
304
344
  <UIcon name="i-lucide-loader-circle" class="h-5 w-5 shrink-0 animate-spin" />
305
345
  <span v-if="incorporating">
306
- Folding your choices into a {{ docNoun }}… You can close this — we’ll notify you
307
- only if more input is needed.
346
+ {{ t('brainstorm.working.incorporating', { doc: docNoun }) }}
308
347
  </span>
309
348
  <span v-else>
310
- Re-running the brainstorm on the updated direction… You can close this — we’ll
311
- notify you only if more input is needed.
349
+ {{ t('brainstorm.working.reReviewing') }}
312
350
  </span>
313
351
  </div>
314
352
 
@@ -329,10 +367,10 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
329
367
  <div class="flex flex-wrap items-center gap-1.5">
330
368
  <span class="text-sm font-medium text-white">{{ item.title }}</span>
331
369
  <UBadge size="xs" variant="subtle" :color="SEVERITY_COLOR[item.severity]">
332
- {{ item.severity }}
370
+ {{ t(SEVERITY_LABELS[item.severity]) }}
333
371
  </UBadge>
334
372
  <UBadge size="xs" variant="outline" color="neutral">
335
- {{ item.category }}
373
+ {{ t(CATEGORY_LABELS[item.category]) }}
336
374
  </UBadge>
337
375
  <UBadge
338
376
  size="xs"
@@ -340,7 +378,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
340
378
  :color="STATUS_COLOR[item.status]"
341
379
  class="ml-auto"
342
380
  >
343
- {{ item.status }}
381
+ {{ t(STATUS_LABELS[item.status]) }}
344
382
  </UBadge>
345
383
  </div>
346
384
  <p class="mt-1 whitespace-pre-line text-sm text-slate-400">
@@ -353,7 +391,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
353
391
  class="mt-2 rounded-md border-l-2 border-slate-700 bg-slate-950/40 px-3 py-1.5 text-sm text-slate-300"
354
392
  >
355
393
  <span class="text-[10px] uppercase tracking-wide text-slate-500">
356
- Your choice
394
+ {{ t('brainstorm.yourChoice') }}
357
395
  </span>
358
396
  <p class="whitespace-pre-line">{{ item.reply }}</p>
359
397
  </div>
@@ -367,7 +405,9 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
367
405
  size="sm"
368
406
  class="mt-2 w-full"
369
407
  :placeholder="
370
- item.reply ? 'Refine your choice…' : 'Choose / steer this option…'
408
+ item.reply
409
+ ? t('brainstorm.replyPlaceholder.refine')
410
+ : t('brainstorm.replyPlaceholder.choose')
371
411
  "
372
412
  :disabled="frozen"
373
413
  />
@@ -380,7 +420,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
380
420
  :disabled="!(drafts[item.id] ?? '').trim() || frozen"
381
421
  @click="submitReply(item)"
382
422
  >
383
- Save choice
423
+ {{ t('brainstorm.saveChoice') }}
384
424
  </UButton>
385
425
  <UButton
386
426
  color="neutral"
@@ -390,7 +430,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
390
430
  :disabled="frozen"
391
431
  @click="setStatus(item, 'dismissed')"
392
432
  >
393
- Dismiss
433
+ {{ t('brainstorm.dismiss') }}
394
434
  </UButton>
395
435
  </div>
396
436
  </template>
@@ -405,7 +445,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
405
445
  :disabled="frozen"
406
446
  @click="setStatus(item, 'open')"
407
447
  >
408
- Reopen
448
+ {{ t('brainstorm.reopen') }}
409
449
  </UButton>
410
450
  </div>
411
451
  </div>
@@ -418,7 +458,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
418
458
  <div class="mb-3 flex items-center gap-1.5 text-[11px] text-emerald-400">
419
459
  <UIcon name="i-lucide-file-check-2" class="h-3.5 w-3.5" />
420
460
  <span class="font-semibold uppercase tracking-wide">
421
- {{ incorporated ? docNoun : `${docNoun} (draft)` }}
461
+ {{ incorporated ? docNoun : t('brainstorm.docDraft', { doc: docNoun }) }}
422
462
  </span>
423
463
  </div>
424
464
  <div v-for="s in outline.sections" :key="s.id" class="mb-2">
@@ -453,19 +493,19 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
453
493
  <div class="flex flex-col gap-4 px-4 py-5">
454
494
  <div v-if="session" class="space-y-2 text-xs text-slate-400">
455
495
  <div class="flex items-center justify-between">
456
- <span>Options</span>
496
+ <span>{{ t('brainstorm.rail.options') }}</span>
457
497
  <span class="text-slate-300">{{ session.items.length }}</span>
458
498
  </div>
459
499
  <div class="flex items-center justify-between">
460
- <span>Open</span>
500
+ <span>{{ t('brainstorm.rail.open') }}</span>
461
501
  <span class="text-slate-300">{{ openCount }}</span>
462
502
  </div>
463
503
  <div class="flex items-center justify-between">
464
- <span>Chosen</span>
504
+ <span>{{ t('brainstorm.rail.chosen') }}</span>
465
505
  <span class="text-slate-300">{{ answeredCount }}</span>
466
506
  </div>
467
507
  <div v-if="session.model" class="flex items-center justify-between">
468
- <span>Model</span>
508
+ <span>{{ t('brainstorm.rail.model') }}</span>
469
509
  <span class="truncate pl-2 text-slate-500">{{ session.model }}</span>
470
510
  </div>
471
511
  </div>
@@ -484,7 +524,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
484
524
  :loading="acting"
485
525
  @click="proceed"
486
526
  >
487
- Proceed (nothing to incorporate)
527
+ {{ t('brainstorm.proceedNothing') }}
488
528
  </UButton>
489
529
  <UButton
490
530
  v-else
@@ -496,17 +536,16 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
496
536
  :disabled="!canIncorporate"
497
537
  @click="incorporate()"
498
538
  >
499
- Incorporate choices
539
+ {{ t('brainstorm.incorporateChoices') }}
500
540
  </UButton>
501
541
  <p class="text-[11px] leading-relaxed text-slate-500">
502
542
  <template v-if="canProceed">
503
- Every option is dismissed — proceed to the next phase without a direction.
543
+ {{ t('brainstorm.hint.allDismissed') }}
504
544
  </template>
505
545
  <template v-else-if="canIncorporate">
506
- Folds your choices into one {{ docNoun }}, then re-runs the brainstorm
507
- automatically.
546
+ {{ t('brainstorm.hint.incorporate', { doc: docNoun }) }}
508
547
  </template>
509
- <template v-else> Choose or dismiss every option to continue. </template>
548
+ <template v-else> {{ t('brainstorm.hint.chooseAll') }} </template>
510
549
  </p>
511
550
  </div>
512
551
 
@@ -520,7 +559,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
520
559
  :loading="busy"
521
560
  @click="reReview"
522
561
  >
523
- {{ busy ? 'Re-running…' : 'Looks good — re-run' }}
562
+ {{ busy ? t('brainstorm.reRunning') : t('brainstorm.reRun') }}
524
563
  </UButton>
525
564
  <UButton
526
565
  color="neutral"
@@ -530,7 +569,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
530
569
  icon="i-lucide-pencil"
531
570
  @click="showRedo = !showRedo"
532
571
  >
533
- Redo incorporation
572
+ {{ t('brainstorm.redoIncorporation') }}
534
573
  </UButton>
535
574
  <div v-if="showRedo" class="space-y-2">
536
575
  <UTextarea
@@ -539,7 +578,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
539
578
  autoresize
540
579
  size="sm"
541
580
  class="w-full"
542
- placeholder="What should the direction do differently?"
581
+ :placeholder="t('brainstorm.redoPlaceholder')"
543
582
  />
544
583
  <UButton
545
584
  color="primary"
@@ -551,12 +590,11 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
551
590
  :disabled="!redoComment.trim()"
552
591
  @click="incorporate(redoComment.trim())"
553
592
  >
554
- Redo with this direction
593
+ {{ t('brainstorm.redoWithDirection') }}
555
594
  </UButton>
556
595
  </div>
557
596
  <p class="text-[11px] leading-relaxed text-slate-500">
558
- Re-run runs the agent against this direction. If you’re unhappy with how it was
559
- merged, redo it with a comment instead.
597
+ {{ t('brainstorm.mergedHint') }}
560
598
  </p>
561
599
  </div>
562
600
 
@@ -564,7 +602,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
564
602
  v-if="session && incorporated"
565
603
  class="border-t border-slate-800 pt-4 text-[11px] leading-relaxed text-slate-500"
566
604
  >
567
- Direction settled — the pipeline is continuing with the document on the left.
605
+ {{ t('brainstorm.incorporatedFooter') }}
568
606
  </div>
569
607
  </div>
570
608
  </aside>