@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.
@@ -8,6 +8,7 @@
8
8
  // findings or hits the iteration cap. The clarified bug report — not the original
9
9
  // description — is what every downstream agent step (the bug investigator, the coder)
10
10
  // consumes.
11
+ import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
11
12
  import { parseOutputOutline } from '~/utils/agentOutput'
12
13
  import type {
13
14
  ClarityItemStatus,
@@ -21,6 +22,7 @@ import type {
21
22
  const board = useBoardStore()
22
23
  const clarity = useClarityStore()
23
24
  const toast = useToast()
25
+ const { t } = useI18n()
24
26
 
25
27
  // Draft replies, keyed by item id, so editing one item doesn't disturb others.
26
28
  const drafts = ref<Record<string, string>>({})
@@ -111,6 +113,27 @@ const STATUS_COLOR = {
111
113
  recommend_requested: 'primary',
112
114
  } as const satisfies Record<ReviewItemStatus, string>
113
115
 
116
+ // Exhaustive enum→label maps (literal keys keep the typed-key drift guard live).
117
+ const SEVERITY_LABELS: Record<ReviewItemSeverity, string> = {
118
+ low: 'clarity.severity.low',
119
+ medium: 'clarity.severity.medium',
120
+ high: 'clarity.severity.high',
121
+ }
122
+ const CATEGORY_LABELS: Record<ReviewItemCategory, string> = {
123
+ gap: 'clarity.category.gap',
124
+ clarification: 'clarity.category.clarification',
125
+ assumption: 'clarity.category.assumption',
126
+ risk: 'clarity.category.risk',
127
+ question: 'clarity.category.question',
128
+ }
129
+ const STATUS_LABELS: Record<ReviewItemStatus, string> = {
130
+ open: 'clarity.itemStatus.open',
131
+ answered: 'clarity.itemStatus.answered',
132
+ resolved: 'clarity.itemStatus.resolved',
133
+ dismissed: 'clarity.itemStatus.dismissed',
134
+ recommend_requested: 'clarity.itemStatus.recommend_requested',
135
+ }
136
+
114
137
  function notifyError(title: string, e: unknown) {
115
138
  toast.add({
116
139
  title,
@@ -128,7 +151,7 @@ async function submitReply(item: ClarityReviewItem) {
128
151
  await clarity.reply(review.value, item.id, text)
129
152
  drafts.value = { ...drafts.value, [item.id]: '' }
130
153
  } catch (e) {
131
- notifyError('Could not save the answer', e)
154
+ notifyError(t('clarity.error.saveAnswer'), e)
132
155
  }
133
156
  }
134
157
 
@@ -137,7 +160,7 @@ async function setStatus(item: ClarityReviewItem, itemStatus: ClarityItemStatus)
137
160
  try {
138
161
  await clarity.setItemStatus(review.value, item.id, itemStatus)
139
162
  } catch (e) {
140
- notifyError('Could not update the finding', e)
163
+ notifyError(t('clarity.error.updateFinding'), e)
141
164
  }
142
165
  }
143
166
 
@@ -146,7 +169,7 @@ async function incorporate(feedback?: string) {
146
169
  try {
147
170
  await clarity.incorporate(review.value, feedback)
148
171
  } catch (e) {
149
- notifyError('Could not incorporate the answers', e)
172
+ notifyError(t('clarity.error.incorporate'), e)
150
173
  return
151
174
  }
152
175
  redoComment.value = ''
@@ -154,8 +177,8 @@ async function incorporate(feedback?: string) {
154
177
  // The fold + re-review now run in the durable driver. Hand the user back to the board;
155
178
  // a notification calls them back only if the re-review needs more input.
156
179
  toast.add({
157
- title: 'Clarifying the bug report in the background',
158
- description: "You're back on the board — we'll notify you only if more input is needed.",
180
+ title: t('clarity.toast.clarifyingTitle'),
181
+ description: t('clarity.toast.clarifyingDescription'),
159
182
  icon: 'i-lucide-wand-sparkles',
160
183
  })
161
184
  close()
@@ -165,17 +188,18 @@ async function reReview() {
165
188
  if (!blockId.value) return
166
189
  try {
167
190
  const updated = await clarity.reReview(blockId.value)
191
+ const newFindings = clarity.openCount(updated)
168
192
  toast.add({
169
193
  title:
170
194
  updated.status === 'incorporated'
171
- ? 'Reviewer is satisfied — continuing the pipeline'
195
+ ? t('clarity.toast.reReviewSatisfied')
172
196
  : updated.status === 'exceeded'
173
- ? 'Iteration limit reached — choose how to proceed'
174
- : `${clarity.openCount(updated)} new finding(s) to react to`,
197
+ ? t('clarity.toast.reReviewExceeded')
198
+ : t('clarity.toast.reReviewNewFindings', { count: newFindings }, newFindings),
175
199
  icon: 'i-lucide-sparkles',
176
200
  })
177
201
  } catch (e) {
178
- notifyError('Could not re-review the bug report', e)
202
+ notifyError(t('clarity.error.reReview'), e)
179
203
  }
180
204
  }
181
205
 
@@ -184,9 +208,9 @@ async function proceed() {
184
208
  acting.value = true
185
209
  try {
186
210
  await clarity.proceed(blockId.value)
187
- toast.add({ title: 'Proceeding to the next phase', icon: 'i-lucide-arrow-right' })
211
+ toast.add({ title: t('clarity.toast.proceeding'), icon: 'i-lucide-arrow-right' })
188
212
  } catch (e) {
189
- notifyError('Could not proceed', e)
213
+ notifyError(t('clarity.error.proceed'), e)
190
214
  } finally {
191
215
  acting.value = false
192
216
  }
@@ -198,15 +222,15 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
198
222
  try {
199
223
  await clarity.resolveExceeded(blockId.value, choice)
200
224
  if (choice === 'stop-reset') {
201
- toast.add({ title: 'Task reset — edit the bug report and resubmit', icon: 'i-lucide-undo' })
225
+ toast.add({ title: t('clarity.toast.taskReset'), icon: 'i-lucide-undo' })
202
226
  close()
203
227
  } else if (choice === 'proceed') {
204
- toast.add({ title: 'Proceeding to the next phase', icon: 'i-lucide-arrow-right' })
228
+ toast.add({ title: t('clarity.toast.proceeding'), icon: 'i-lucide-arrow-right' })
205
229
  } else {
206
- toast.add({ title: 'One more review round granted', icon: 'i-lucide-rotate-cw' })
230
+ toast.add({ title: t('clarity.toast.extraRoundGranted'), icon: 'i-lucide-rotate-cw' })
207
231
  }
208
232
  } catch (e) {
209
- notifyError('Could not resolve the review', e)
233
+ notifyError(t('clarity.error.resolve'), e)
210
234
  } finally {
211
235
  acting.value = false
212
236
  }
@@ -231,12 +255,12 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
231
255
  <UIcon name="i-lucide-bug" class="h-5 w-5 text-indigo-300" />
232
256
  </div>
233
257
  <div class="min-w-0">
234
- <h1 class="truncate text-base font-semibold text-white">Bug-report triage</h1>
258
+ <h1 class="truncate text-base font-semibold text-white">{{ t('clarity.title') }}</h1>
235
259
  <p v-if="block" class="truncate text-xs text-slate-500">{{ block.title }}</p>
236
260
  </div>
237
261
  <div class="ml-auto flex items-center gap-1.5">
238
262
  <UBadge v-if="review" color="neutral" variant="subtle" size="sm">
239
- Iteration {{ iteration }} / {{ maxIterations }}
263
+ {{ t('clarity.iteration', { current: iteration, max: maxIterations }) }}
240
264
  </UBadge>
241
265
  <UButton icon="i-lucide-x" color="neutral" variant="ghost" size="sm" @click="close" />
242
266
  </div>
@@ -246,11 +270,15 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
246
270
  <!-- main column -->
247
271
  <div class="min-w-0 flex-1 overflow-y-auto px-6 py-5">
248
272
  <p class="mb-4 text-sm text-slate-400">
249
- An AI reviewer triaged this {{ block?.level ?? 'item' }}’s bug report for fixability —
250
- its description plus any linked context — and raised the questions below.
251
- <span class="text-slate-300">Answer</span> the relevant ones and
252
- <span class="text-slate-300">dismiss</span> the irrelevant, then incorporate them; the
253
- reviewer re-reviews until the bug report is clear enough to fix.
273
+ <i18n-t keypath="clarity.intro" tag="span" scope="global">
274
+ <template #level>{{ block?.level ?? t('clarity.itemFallback') }}</template>
275
+ <template #answer
276
+ ><span class="text-slate-300">{{ t('clarity.introAnswer') }}</span></template
277
+ >
278
+ <template #dismiss
279
+ ><span class="text-slate-300">{{ t('clarity.introDismiss') }}</span></template
280
+ >
281
+ </i18n-t>
254
282
  </p>
255
283
 
256
284
  <!-- empty state — the reviewer runs automatically as the first pipeline
@@ -259,8 +287,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
259
287
  v-if="!review && !busy && !loading"
260
288
  class="rounded-lg border border-dashed border-slate-700 p-8 text-center text-sm text-slate-500"
261
289
  >
262
- No review yet. The reviewer runs automatically as the first step when this task's
263
- pipeline starts.
290
+ {{ t('clarity.empty') }}
264
291
  </div>
265
292
 
266
293
  <!-- working state (initial fetch on open, or a reviewer pass running) -->
@@ -269,7 +296,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
269
296
  class="flex items-center justify-center gap-2 p-8 text-sm text-slate-400"
270
297
  >
271
298
  <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
272
- {{ loading && !busy ? 'Loading the review…' : 'Triaging the bug report…' }}
299
+ {{ loading && !busy ? t('clarity.loadingReview') : t('clarity.triaging') }}
273
300
  </div>
274
301
 
275
302
  <template v-else-if="review">
@@ -279,16 +306,15 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
279
306
  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"
280
307
  >
281
308
  <UIcon name="i-lucide-circle-check" class="h-5 w-5 shrink-0" />
282
- The bug report is clarified. The report below is what every downstream agent step
283
- uses.
309
+ {{ t('clarity.converged') }}
284
310
  </div>
285
311
 
286
312
  <!-- iteration cap hit -->
287
313
  <IterationCapPrompt
288
314
  v-else-if="exceeded"
289
315
  class="mb-4"
290
- :heading="`Reached the ${maxIterations}-iteration limit with findings still open.`"
291
- detail="Do one more review round, proceed to the next phase with the last clarified bug report anyway, or stop and reset the task so you can rework the bug report and resubmit."
316
+ :heading="t('clarity.capHeading', { max: maxIterations })"
317
+ :detail="t('clarity.capDetail')"
292
318
  :loading="acting"
293
319
  @resolve="resolveExceeded"
294
320
  />
@@ -301,12 +327,10 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
301
327
  >
302
328
  <UIcon name="i-lucide-loader-circle" class="h-5 w-5 shrink-0 animate-spin" />
303
329
  <span v-if="incorporating">
304
- Incorporating your answers into a clarified bug report… You can close this — we’ll
305
- notify you only if more input is needed.
330
+ {{ t('clarity.incorporatingStage') }}
306
331
  </span>
307
332
  <span v-else>
308
- Re-reviewing the updated bug report… You can close this — we’ll notify you only if
309
- more input is needed.
333
+ {{ t('clarity.reReviewingStage') }}
310
334
  </span>
311
335
  </div>
312
336
 
@@ -327,10 +351,10 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
327
351
  <div class="flex flex-wrap items-center gap-1.5">
328
352
  <span class="text-sm font-medium text-white">{{ item.title }}</span>
329
353
  <UBadge size="xs" variant="subtle" :color="SEVERITY_COLOR[item.severity]">
330
- {{ item.severity }}
354
+ {{ t(SEVERITY_LABELS[item.severity]) }}
331
355
  </UBadge>
332
356
  <UBadge size="xs" variant="outline" color="neutral">
333
- {{ item.category }}
357
+ {{ t(CATEGORY_LABELS[item.category]) }}
334
358
  </UBadge>
335
359
  <UBadge
336
360
  size="xs"
@@ -338,7 +362,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
338
362
  :color="STATUS_COLOR[item.status]"
339
363
  class="ml-auto"
340
364
  >
341
- {{ item.status }}
365
+ {{ t(STATUS_LABELS[item.status]) }}
342
366
  </UBadge>
343
367
  </div>
344
368
  <p class="mt-1 whitespace-pre-line text-sm text-slate-400">
@@ -351,7 +375,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
351
375
  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"
352
376
  >
353
377
  <span class="text-[10px] uppercase tracking-wide text-slate-500">
354
- Answer
378
+ {{ t('clarity.answerLabel') }}
355
379
  </span>
356
380
  <p class="whitespace-pre-line">{{ item.reply }}</p>
357
381
  </div>
@@ -365,7 +389,11 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
365
389
  autoresize
366
390
  size="sm"
367
391
  class="mt-2 w-full"
368
- :placeholder="item.reply ? 'Refine your answer…' : 'Answer this finding…'"
392
+ :placeholder="
393
+ item.reply
394
+ ? t('clarity.refineAnswerPlaceholder')
395
+ : t('clarity.answerPlaceholder')
396
+ "
369
397
  :disabled="frozen"
370
398
  />
371
399
  <div class="mt-2 flex flex-wrap items-center gap-2">
@@ -377,7 +405,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
377
405
  :disabled="!(drafts[item.id] ?? '').trim() || frozen"
378
406
  @click="submitReply(item)"
379
407
  >
380
- Save answer
408
+ {{ t('clarity.saveAnswer') }}
381
409
  </UButton>
382
410
  <UButton
383
411
  color="neutral"
@@ -387,7 +415,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
387
415
  :disabled="frozen"
388
416
  @click="setStatus(item, 'dismissed')"
389
417
  >
390
- Dismiss as irrelevant
418
+ {{ t('clarity.dismissIrrelevant') }}
391
419
  </UButton>
392
420
  </div>
393
421
  </template>
@@ -402,7 +430,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
402
430
  :disabled="frozen"
403
431
  @click="setStatus(item, 'open')"
404
432
  >
405
- Reopen
433
+ {{ t('clarity.reopen') }}
406
434
  </UButton>
407
435
  </div>
408
436
  </div>
@@ -415,7 +443,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
415
443
  <div class="mb-3 flex items-center gap-1.5 text-[11px] text-emerald-400">
416
444
  <UIcon name="i-lucide-file-check-2" class="h-3.5 w-3.5" />
417
445
  <span class="font-semibold uppercase tracking-wide">
418
- {{ incorporated ? 'Clarified bug report' : 'Clarified bug report (draft)' }}
446
+ {{ incorporated ? t('clarity.docHeading') : t('clarity.docHeadingDraft') }}
419
447
  </span>
420
448
  </div>
421
449
  <div v-for="s in outline.sections" :key="s.id" class="mb-2">
@@ -450,19 +478,19 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
450
478
  <div class="flex flex-col gap-4 px-4 py-5">
451
479
  <div v-if="review" class="space-y-2 text-xs text-slate-400">
452
480
  <div class="flex items-center justify-between">
453
- <span>Findings</span>
481
+ <span>{{ t('clarity.rail.findings') }}</span>
454
482
  <span class="text-slate-300">{{ review.items.length }}</span>
455
483
  </div>
456
484
  <div class="flex items-center justify-between">
457
- <span>Open</span>
485
+ <span>{{ t('clarity.rail.open') }}</span>
458
486
  <span class="text-slate-300">{{ openCount }}</span>
459
487
  </div>
460
488
  <div class="flex items-center justify-between">
461
- <span>Answered</span>
489
+ <span>{{ t('clarity.rail.answered') }}</span>
462
490
  <span class="text-slate-300">{{ answeredCount }}</span>
463
491
  </div>
464
492
  <div v-if="review.model" class="flex items-center justify-between">
465
- <span>Model</span>
493
+ <span>{{ t('clarity.rail.model') }}</span>
466
494
  <span class="truncate pl-2 text-slate-500">{{ review.model }}</span>
467
495
  </div>
468
496
  </div>
@@ -481,7 +509,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
481
509
  :loading="acting"
482
510
  @click="proceed"
483
511
  >
484
- Proceed (nothing to incorporate)
512
+ {{ t('clarity.proceedNothing') }}
485
513
  </UButton>
486
514
  <UButton
487
515
  v-else
@@ -493,17 +521,16 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
493
521
  :disabled="!canIncorporate"
494
522
  @click="incorporate()"
495
523
  >
496
- Incorporate answers
524
+ {{ t('clarity.incorporateAnswers') }}
497
525
  </UButton>
498
526
  <p class="text-[11px] leading-relaxed text-slate-500">
499
527
  <template v-if="canProceed">
500
- Every finding is dismissed — proceed to the next phase without reworking.
528
+ {{ t('clarity.hint.proceed') }}
501
529
  </template>
502
530
  <template v-else-if="canIncorporate">
503
- Folds your answers into one clarified bug report, then re-reviews it
504
- automatically.
531
+ {{ t('clarity.hint.incorporate') }}
505
532
  </template>
506
- <template v-else> Answer or dismiss every finding to continue. </template>
533
+ <template v-else> {{ t('clarity.hint.answerAll') }} </template>
507
534
  </p>
508
535
  </div>
509
536
 
@@ -517,7 +544,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
517
544
  :loading="busy"
518
545
  @click="reReview"
519
546
  >
520
- {{ busy ? 'Re-reviewing…' : 'Looks good — re-review' }}
547
+ {{ busy ? t('clarity.reReviewing') : t('clarity.looksGoodReReview') }}
521
548
  </UButton>
522
549
  <UButton
523
550
  color="neutral"
@@ -527,7 +554,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
527
554
  icon="i-lucide-pencil"
528
555
  @click="showRedo = !showRedo"
529
556
  >
530
- Redo incorporation
557
+ {{ t('clarity.redoIncorporation') }}
531
558
  </UButton>
532
559
  <div v-if="showRedo" class="space-y-2">
533
560
  <UTextarea
@@ -536,7 +563,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
536
563
  autoresize
537
564
  size="sm"
538
565
  class="w-full"
539
- placeholder="What should the merge do differently?"
566
+ :placeholder="t('clarity.redoPlaceholder')"
540
567
  />
541
568
  <UButton
542
569
  color="primary"
@@ -548,12 +575,11 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
548
575
  :disabled="!redoComment.trim()"
549
576
  @click="incorporate(redoComment.trim())"
550
577
  >
551
- Redo with this direction
578
+ {{ t('clarity.redoWithDirection') }}
552
579
  </UButton>
553
580
  </div>
554
581
  <p class="text-[11px] leading-relaxed text-slate-500">
555
- Re-review runs the reviewer against this report. If you’re unhappy with how it was
556
- merged, redo it with a comment instead.
582
+ {{ t('clarity.redoHint') }}
557
583
  </p>
558
584
  </div>
559
585
 
@@ -561,7 +587,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
561
587
  v-if="review && incorporated"
562
588
  class="border-t border-slate-800 pt-4 text-[11px] leading-relaxed text-slate-500"
563
589
  >
564
- Bug report clarified — the pipeline is continuing with the report on the left.
590
+ {{ t('clarity.incorporatedFooter') }}
565
591
  </div>
566
592
  </div>
567
593
  </aside>
@@ -9,6 +9,8 @@
9
9
  import { computed } from 'vue'
10
10
  import type { ConsensusContribution, ConsensusSession } from '~/types/consensus'
11
11
 
12
+ const { t, n } = useI18n()
13
+
12
14
  const board = useBoardStore()
13
15
  const consensus = useConsensusStore()
14
16
 
@@ -24,36 +26,60 @@ const session = computed<ConsensusSession | null>(() =>
24
26
  )
25
27
  const loading = computed(() => (blockId.value ? consensus.isLoading(blockId.value) : false))
26
28
 
27
- const STRATEGY_LABEL: Record<string, string> = {
28
- 'specialist-panel': 'Specialist panel',
29
- debate: 'Debate',
30
- 'ranked-voting': 'Ranked voting',
29
+ // Exhaustive enum→key maps (literal key strings keep the typed-key drift guard live,
30
+ // vs a runtime-built `consensus.strategy.${value}`).
31
+ const STRATEGY_LABEL_KEYS: Record<string, string> = {
32
+ 'specialist-panel': 'consensus.strategy.specialistPanel',
33
+ debate: 'consensus.strategy.debate',
34
+ 'ranked-voting': 'consensus.strategy.rankedVoting',
35
+ }
36
+ const ROUND_LABEL_KEYS: Record<string, string> = {
37
+ draft: 'consensus.round.draft',
38
+ critique: 'consensus.round.critique',
39
+ score: 'consensus.round.score',
40
+ }
41
+ const STATUS_LABEL_KEYS: Record<string, string> = {
42
+ running: 'consensus.status.running',
43
+ synthesizing: 'consensus.status.synthesizing',
44
+ done: 'consensus.status.done',
45
+ failed: 'consensus.status.failed',
31
46
  }
32
- const ROUND_LABEL: Record<string, string> = {
33
- draft: 'Independent drafts',
34
- critique: 'Critique & revision',
35
- score: 'Scoring',
47
+ const STATUS_CLASS: Record<string, string> = {
48
+ running: 'bg-sky-500/15 text-sky-300',
49
+ synthesizing: 'bg-indigo-500/15 text-indigo-300',
50
+ done: 'bg-emerald-500/15 text-emerald-300',
51
+ failed: 'bg-rose-500/15 text-rose-300',
36
52
  }
37
53
 
38
- const STATUS_META: Record<string, { label: string; class: string }> = {
39
- running: { label: 'Running', class: 'bg-sky-500/15 text-sky-300' },
40
- synthesizing: { label: 'Synthesizing', class: 'bg-indigo-500/15 text-indigo-300' },
41
- done: { label: 'Done', class: 'bg-emerald-500/15 text-emerald-300' },
42
- failed: { label: 'Failed', class: 'bg-rose-500/15 text-rose-300' },
54
+ function strategyLabel(strategy: string): string {
55
+ const key = STRATEGY_LABEL_KEYS[strategy]
56
+ return key ? t(key) : strategy
57
+ }
58
+ function roundLabel(kind: string | null | undefined): string {
59
+ if (!kind) return t('consensus.round.contributions')
60
+ const key = ROUND_LABEL_KEYS[kind]
61
+ return key ? t(key) : kind
62
+ }
63
+ function statusLabel(status: string): string {
64
+ const key = STATUS_LABEL_KEYS[status]
65
+ return key ? t(key) : status
43
66
  }
44
67
 
45
68
  /** Anonymous label (Expert A/B/…) for a participant, matching the backend's ordering. */
46
69
  function anonLabel(participantId: string): string {
47
70
  const idx = session.value?.participants.findIndex((p) => p.id === participantId) ?? -1
48
- return `Expert ${String.fromCharCode(65 + (idx < 0 ? 0 : idx % 26))}`
71
+ return t('consensus.expert', { letter: String.fromCharCode(65 + (idx < 0 ? 0 : idx % 26)) })
49
72
  }
50
73
 
51
74
  function roleFor(participantId: string): string {
52
- return session.value?.participants.find((p) => p.id === participantId)?.role ?? 'Participant'
75
+ return (
76
+ session.value?.participants.find((p) => p.id === participantId)?.role ??
77
+ t('consensus.participant')
78
+ )
53
79
  }
54
80
 
55
- function pct(n: number | null | undefined): string {
56
- return n == null ? '—' : `${Math.round(n * 100)}%`
81
+ function pct(value: number | null | undefined): string {
82
+ return value == null ? '—' : n(value, 'percent')
57
83
  }
58
84
 
59
85
  function topScore(c: ConsensusContribution): { label: string; value: number } | null {
@@ -80,20 +106,27 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
80
106
  </div>
81
107
  <div class="min-w-0 flex-1">
82
108
  <h2 class="truncate text-sm font-semibold text-slate-100">
83
- Consensus ·
84
- {{ session ? (STRATEGY_LABEL[session.strategy] ?? session.strategy) : '' }}
109
+ {{ t('consensus.titlePrefix') }} ·
110
+ {{ session ? strategyLabel(session.strategy) : '' }}
85
111
  <span v-if="block" class="font-normal text-slate-400">— {{ block.title }}</span>
86
112
  </h2>
87
113
  <p v-if="session" class="text-xs text-slate-500">
88
- {{ session.agentKind }} · {{ session.participants.length }} participants
114
+ {{ session.agentKind }} ·
115
+ {{
116
+ t(
117
+ 'consensus.participantCount',
118
+ { count: session.participants.length },
119
+ session.participants.length,
120
+ )
121
+ }}
89
122
  </p>
90
123
  </div>
91
124
  <span
92
125
  v-if="session"
93
126
  class="rounded-full px-2.5 py-1 text-xs font-medium"
94
- :class="STATUS_META[session.status]?.class ?? 'bg-slate-700 text-slate-300'"
127
+ :class="STATUS_CLASS[session.status] ?? 'bg-slate-700 text-slate-300'"
95
128
  >
96
- {{ STATUS_META[session.status]?.label ?? session.status }}
129
+ {{ statusLabel(session.status) }}
97
130
  </span>
98
131
  <button
99
132
  class="rounded-lg p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
@@ -105,10 +138,10 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
105
138
 
106
139
  <div class="flex-1 overflow-y-auto px-6 py-5">
107
140
  <div v-if="loading && !session" class="py-16 text-center text-sm text-slate-500">
108
- Loading consensus session…
141
+ {{ t('consensus.loading') }}
109
142
  </div>
110
143
  <div v-else-if="!session" class="py-16 text-center text-sm text-slate-500">
111
- No consensus session has run for this step yet.
144
+ {{ t('consensus.empty') }}
112
145
  </div>
113
146
  <template v-else>
114
147
  <!-- failure -->
@@ -116,19 +149,19 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
116
149
  v-if="session.status === 'failed'"
117
150
  class="mb-5 rounded-lg border border-rose-800/60 bg-rose-950/40 px-4 py-3 text-sm text-rose-200"
118
151
  >
119
- Consensus failed: {{ session.error ?? 'unknown error' }}
152
+ {{ t('consensus.failed', { error: session.error ?? t('consensus.unknownError') }) }}
120
153
  </div>
121
154
 
122
155
  <!-- synthesized result -->
123
156
  <section v-if="session.synthesis" class="mb-6">
124
157
  <div class="mb-2 flex items-center gap-2">
125
158
  <h3 class="text-xs font-semibold uppercase tracking-wide text-slate-400">
126
- Synthesized result
159
+ {{ t('consensus.synthesizedResult') }}
127
160
  </h3>
128
161
  <span
129
162
  v-if="session.confidence != null"
130
163
  class="rounded bg-emerald-500/15 px-1.5 py-0.5 text-xs text-emerald-300"
131
- >confidence {{ pct(session.confidence) }}</span
164
+ >{{ t('consensus.confidence', { pct: pct(session.confidence) }) }}</span
132
165
  >
133
166
  </div>
134
167
  <pre
@@ -150,7 +183,7 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
150
183
  <!-- participants -->
151
184
  <section class="mb-6">
152
185
  <h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
153
- Panel
186
+ {{ t('consensus.panel') }}
154
187
  </h3>
155
188
  <div class="flex flex-wrap gap-2">
156
189
  <div
@@ -158,9 +191,9 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
158
191
  :key="p.id"
159
192
  class="rounded-lg border border-slate-800 bg-slate-950/40 px-3 py-1.5 text-xs"
160
193
  >
161
- <span class="font-medium text-slate-200"
162
- >Expert {{ String.fromCharCode(65 + i) }}</span
163
- >
194
+ <span class="font-medium text-slate-200">{{
195
+ t('consensus.expert', { letter: String.fromCharCode(65 + i) })
196
+ }}</span>
164
197
  <span class="text-slate-400"> · {{ p.role }}</span>
165
198
  <span v-if="p.modelId" class="ml-1 text-slate-500">({{ p.modelId }})</span>
166
199
  </div>
@@ -170,8 +203,8 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
170
203
  <!-- rounds -->
171
204
  <section v-for="round in session.rounds" :key="round.index" class="mb-5">
172
205
  <h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
173
- Round {{ round.index + 1 }} ·
174
- {{ round.kind ? (ROUND_LABEL[round.kind] ?? round.kind) : 'Contributions' }}
206
+ {{ t('consensus.round.heading', { n: round.index + 1 }) }} ·
207
+ {{ roundLabel(round.kind) }}
175
208
  </h3>
176
209
  <div class="space-y-3">
177
210
  <div
@@ -187,7 +220,12 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
187
220
  <span
188
221
  v-if="topScore(c)"
189
222
  class="ml-auto rounded bg-slate-800 px-1.5 py-0.5 text-xs text-slate-300"
190
- >top {{ topScore(c)!.label }} {{ pct(topScore(c)!.value) }}</span
223
+ >{{
224
+ t('consensus.topScore', {
225
+ label: topScore(c)!.label,
226
+ pct: pct(topScore(c)!.value),
227
+ })
228
+ }}</span
191
229
  >
192
230
  </div>
193
231
  <pre class="whitespace-pre-wrap text-sm text-slate-300">{{ c.text }}</pre>