@cat-factory/app 0.309.0 → 0.310.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.
Files changed (133) hide show
  1. package/README.md +11 -2
  2. package/app/components/assistant/AssistantModal.vue +5 -4
  3. package/app/components/auth/BackendMisconfiguredScreen.vue +4 -3
  4. package/app/components/auth/LoginScreen.vue +40 -26
  5. package/app/components/auth/ResetPasswordScreen.vue +9 -4
  6. package/app/components/auth/UserMenu.vue +4 -3
  7. package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +34 -23
  8. package/app/components/binaryOutput/StoredAssetView.vue +8 -6
  9. package/app/components/board/AddTaskModal.vue +16 -15
  10. package/app/components/board/AgentFailureCard.vue +12 -9
  11. package/app/components/board/AgentFailureHistory.vue +18 -9
  12. package/app/components/board/CreateInitiativeModal.vue +5 -4
  13. package/app/components/board/FailureDetail.vue +22 -12
  14. package/app/components/board/ReviewFrictionDialog.vue +4 -3
  15. package/app/components/board/nodes/BlockNode.vue +17 -15
  16. package/app/components/board/nodes/DecisionBadge.vue +4 -3
  17. package/app/components/board/nodes/FrameSwimlanes.vue +4 -3
  18. package/app/components/board/nodes/TaskCard.vue +12 -7
  19. package/app/components/board/nodes/TaskPipelineMini.vue +9 -7
  20. package/app/components/bootstrap/AdoptionReviewModal.vue +30 -21
  21. package/app/components/bootstrap/BootstrapModal.vue +3 -3
  22. package/app/components/brainstorm/BrainstormWindow.vue +5 -3
  23. package/app/components/bugFishing/BugFishingWindow.vue +49 -27
  24. package/app/components/clarity/ClarityReviewWindow.vue +5 -3
  25. package/app/components/common/CopyButton.vue +12 -11
  26. package/app/components/common/IconButton.vue +34 -9
  27. package/app/components/context/ContextAttachmentFields.vue +23 -12
  28. package/app/components/documents/ContextDocumentPicker.vue +12 -9
  29. package/app/components/documents/DocumentImportModal.vue +4 -3
  30. package/app/components/documents/DocumentOriginLink.vue +12 -9
  31. package/app/components/documents/DocumentSyncState.vue +3 -3
  32. package/app/components/documents/RepoContextDocPicker.vue +4 -3
  33. package/app/components/environments/EnvironmentStatusPanel.vue +4 -3
  34. package/app/components/followUp/FollowUpWindow.vue +9 -6
  35. package/app/components/forkDecision/ForkDecisionWindow.vue +71 -80
  36. package/app/components/foundational/FoundationalServiceCatalogList.vue +3 -2
  37. package/app/components/foundational/FoundationalServiceRegistry.vue +7 -6
  38. package/app/components/foundational/FoundationalServiceSources.vue +7 -6
  39. package/app/components/fragments/FragmentLibraryManager.vue +5 -4
  40. package/app/components/fragments/FragmentSelector.vue +12 -9
  41. package/app/components/gates/GateFailingCheckList.vue +4 -3
  42. package/app/components/gates/GateResultView.vue +17 -12
  43. package/app/components/github/AddServiceFromRepoModal.vue +11 -7
  44. package/app/components/github/GitHubOnboarding.vue +7 -2
  45. package/app/components/github/GitHubPanel.vue +9 -6
  46. package/app/components/github/RepoTreeBrowser.vue +15 -12
  47. package/app/components/humanTest/HumanTestWindow.vue +13 -9
  48. package/app/components/initiative/InitiativePlanReview.vue +28 -18
  49. package/app/components/initiative/InitiativeTrackerWindow.vue +171 -124
  50. package/app/components/judge/JudgeResultView.vue +5 -3
  51. package/app/components/kaizen/KaizenPanel.vue +67 -53
  52. package/app/components/layout/AccountTeamSettings.vue +9 -9
  53. package/app/components/layout/AppearanceSwitcher.vue +4 -3
  54. package/app/components/layout/BoardSwitcher.vue +10 -8
  55. package/app/components/layout/BoardToolbar.vue +5 -4
  56. package/app/components/layout/CommandBar.vue +4 -3
  57. package/app/components/layout/DefaultTestEnvBanner.vue +4 -3
  58. package/app/components/layout/IntegrationsHub.vue +8 -6
  59. package/app/components/layout/LanguageSwitcher.vue +4 -3
  60. package/app/components/layout/ModelProvidersHub.vue +4 -3
  61. package/app/components/layout/NotificationsInbox.vue +40 -30
  62. package/app/components/layout/PersonalSetupModal.vue +4 -3
  63. package/app/components/layout/RolePrompt.vue +4 -3
  64. package/app/components/layout/SideBar.vue +4 -3
  65. package/app/components/layout/TranslationWarningBanner.vue +4 -3
  66. package/app/components/layout/UiModeSwitcher.vue +8 -6
  67. package/app/components/layout/UiRoleSwitcher.vue +8 -6
  68. package/app/components/layout/WorkspaceMembersSettings.vue +2 -2
  69. package/app/components/media/ArtifactLightbox.vue +69 -39
  70. package/app/components/media/ImageCompare.vue +69 -74
  71. package/app/components/observability/OutcomeFilterChips.vue +5 -4
  72. package/app/components/observability/RunFailureSummary.vue +8 -6
  73. package/app/components/observability/StepModelActivity.vue +5 -3
  74. package/app/components/observability/ToolCallList.vue +4 -2
  75. package/app/components/outcome/OutcomeSummaryWindow.vue +5 -4
  76. package/app/components/palettes/AgentPalette.vue +18 -14
  77. package/app/components/panels/AgentStepDetail.vue +30 -19
  78. package/app/components/panels/ObservabilityPanel.vue +149 -114
  79. package/app/components/panels/OperatorDashboardPanel.vue +99 -91
  80. package/app/components/panels/ReportsPanel.vue +64 -61
  81. package/app/components/panels/ResultWindowShell.vue +148 -134
  82. package/app/components/panels/StepContainerStatus.vue +9 -9
  83. package/app/components/panels/StepDelegatedStatus.vue +11 -9
  84. package/app/components/panels/StepRestartControl.vue +3 -2
  85. package/app/components/panels/inspector/EpicChildren.vue +4 -3
  86. package/app/components/panels/inspector/FrontendConfig.vue +3 -2
  87. package/app/components/panels/inspector/InspectorSection.vue +22 -17
  88. package/app/components/panels/inspector/ServiceConnections.vue +3 -2
  89. package/app/components/panels/inspector/ServiceTestConfig.vue +5 -4
  90. package/app/components/panels/inspector/TaskExecution.vue +5 -4
  91. package/app/components/panels/inspector/TaskRunSettings.vue +10 -6
  92. package/app/components/pipeline/EstimateThresholdFields.vue +13 -10
  93. package/app/components/pipeline/GateConfigFields.vue +14 -18
  94. package/app/components/pipeline/PipelineBuilder.vue +90 -77
  95. package/app/components/pipeline/PipelinePicker.vue +8 -6
  96. package/app/components/pipeline/PipelineProgress.vue +26 -21
  97. package/app/components/prReview/PrReviewWindow.vue +46 -25
  98. package/app/components/providers/ApiKeysSection.vue +4 -3
  99. package/app/components/ralph/RalphLoopResultView.vue +4 -3
  100. package/app/components/requirements/RequirementsReviewWindow.vue +10 -6
  101. package/app/components/riskPolicy/RiskPolicyPicker.vue +8 -6
  102. package/app/components/sandbox/SandboxPanel.vue +73 -54
  103. package/app/components/settings/CloudflareHandlerSection.vue +33 -30
  104. package/app/components/settings/ConsensusGroupsSection.vue +40 -30
  105. package/app/components/settings/InfraHandlersConfigurator.vue +16 -13
  106. package/app/components/settings/InfrastructureBackendPicker.vue +17 -17
  107. package/app/components/settings/IssueTrackerPanel.vue +21 -6
  108. package/app/components/settings/LocalModelEndpointsPanel.vue +3 -2
  109. package/app/components/settings/ModelConfigurationPanel.vue +9 -8
  110. package/app/components/settings/OpenRouterCatalogPanel.vue +4 -3
  111. package/app/components/settings/ProviderPreferenceEditor.vue +5 -6
  112. package/app/components/settings/RiskPolicyEditorRow.vue +3 -2
  113. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +10 -8
  114. package/app/components/skills/ReviewSkillQueue.vue +4 -3
  115. package/app/components/skills/SkillLibraryManager.vue +7 -6
  116. package/app/components/slack/SlackPanel.vue +9 -5
  117. package/app/components/spec/ServiceSpecWindow.vue +4 -3
  118. package/app/components/tasks/ContextIssuePicker.vue +12 -9
  119. package/app/components/tasks/TaskContextIssues.vue +4 -3
  120. package/app/components/testing/TestReportWindow.vue +14 -8
  121. package/app/components/theme/ThemeImportModal.vue +2 -2
  122. package/app/components/visualConfirm/VisualConfirmationWindow.vue +62 -35
  123. package/i18n/locales/de.json +7 -3
  124. package/i18n/locales/en.json +7 -3
  125. package/i18n/locales/es.json +7 -3
  126. package/i18n/locales/fr.json +7 -3
  127. package/i18n/locales/he.json +7 -3
  128. package/i18n/locales/it.json +7 -3
  129. package/i18n/locales/ja.json +7 -3
  130. package/i18n/locales/pl.json +7 -3
  131. package/i18n/locales/tr.json +7 -3
  132. package/i18n/locales/uk.json +7 -3
  133. package/package.json +2 -2
@@ -13,6 +13,7 @@
13
13
  // request-changes rail has to live here or the gate has no resolving surface at all —
14
14
  // which is exactly how an approved-only-over-REST plan gate shipped.
15
15
  import { computed, reactive, ref } from 'vue'
16
+ import type { TableColumn } from '@nuxt/ui'
16
17
  import type { InitiativeFollowUp, InitiativeItem } from '~/types/domain'
17
18
  import { useInitiativePlanning } from '~/composables/useInitiativePlanning'
18
19
  import {
@@ -87,6 +88,16 @@ function itemsOf(phaseId: string): InitiativeItem[] {
87
88
  return (initiative.value?.items ?? []).filter((i) => i.phaseId === phaseId)
88
89
  }
89
90
 
91
+ // One column set for every phase's table, so the header typography is the table theme's rather
92
+ // than a per-file recipe.
93
+ const phaseItems = computed(() => phases.value.map((p) => ({ label: p.title, value: p.id })))
94
+
95
+ const itemColumns = computed<TableColumn<InitiativeItem>[]>(() => [
96
+ { id: 'item', header: t('initiative.tracker.colItem') },
97
+ { id: 'status', header: t('initiative.tracker.colStatus') },
98
+ { id: 'pr', header: t('initiative.tracker.colPr') },
99
+ ])
100
+
90
101
  const progress = computed(() => initiativeProgress(initiative.value?.items))
91
102
  const progressPct = computed(() =>
92
103
  progress.value && progress.value.total > 0
@@ -393,22 +404,26 @@ const { requestClose } = useUnsavedGuard({
393
404
  {{ t('initiative.checkpoint.pausedBody', { phase: checkpointPhase!.title }) }}
394
405
  </p>
395
406
  <div class="mt-2.5 flex flex-wrap gap-2">
396
- <button
407
+ <UButton
408
+ color="neutral"
409
+ variant="ghost"
397
410
  class="rounded-sm bg-primary/90 px-2.5 py-1 text-2xs font-medium text-inverted hover:bg-primary disabled:opacity-50"
398
411
  :disabled="initiatives.controlling"
399
412
  data-testid="initiative-checkpoint-resume"
400
413
  @click="checkpointControl('resume')"
401
414
  >
402
415
  {{ t('initiative.inspector.resume') }}
403
- </button>
404
- <button
416
+ </UButton>
417
+ <UButton
418
+ color="neutral"
419
+ variant="ghost"
405
420
  class="rounded-sm border border-app-error-500/50 px-2.5 py-1 text-2xs font-medium text-app-error-300 hover:bg-app-error-500/10 disabled:opacity-50"
406
421
  :disabled="initiatives.controlling"
407
422
  data-testid="initiative-checkpoint-cancel"
408
423
  @click="checkpointControl('cancel')"
409
424
  >
410
425
  {{ t('initiative.inspector.cancel') }}
411
- </button>
426
+ </UButton>
412
427
  </div>
413
428
  </div>
414
429
  </div>
@@ -490,84 +505,91 @@ const { requestClose } = useUnsavedGuard({
490
505
  </UBadge>
491
506
  </h3>
492
507
  <p v-if="phase.goal" class="mb-2 text-xs text-muted">{{ phase.goal }}</p>
493
- <div class="overflow-x-auto rounded-lg border border-default">
494
- <table class="w-full text-xs">
495
- <thead>
496
- <tr class="border-b border-default text-left text-dimmed">
497
- <th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colItem') }}</th>
498
- <th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colStatus') }}</th>
499
- <th class="px-3 py-2 font-medium">{{ t('initiative.tracker.colPr') }}</th>
500
- </tr>
501
- </thead>
502
- <tbody>
503
- <tr
504
- v-for="item in itemsOf(phase.id)"
505
- :key="item.id"
506
- class="border-b border-default/60 last:border-0"
507
- >
508
- <td class="px-3 py-2 align-top">
509
- <div class="font-medium text-default">{{ item.title }}</div>
510
- <div v-if="item.dependsOn?.length" class="mt-0.5 text-3xs text-dimmed">
511
- {{
512
- t('initiative.tracker.dependsOn', {
513
- items: item.dependsOn.join(', '),
514
- })
515
- }}
516
- </div>
517
- <div
518
- v-if="item.note"
519
- class="mt-0.5 max-w-3xl text-3xs text-app-warning-300/80"
520
- >
521
- {{ item.note }}
522
- </div>
523
- <div
524
- v-if="editable && (item.status === 'blocked' || item.status === 'pending')"
525
- class="mt-1 flex gap-1.5"
526
- >
527
- <button
528
- v-if="item.status === 'blocked'"
529
- class="rounded-sm border border-muted px-1.5 py-0.5 text-3xs text-toned hover:bg-elevated disabled:opacity-50"
530
- :disabled="initiatives.curating"
531
- :data-testid="`initiative-item-retry-${item.id}`"
532
- @click="itemAction(item, 'retry')"
533
- >
534
- {{ t('initiative.curation.retry') }}
535
- </button>
536
- <button
537
- class="rounded-sm border border-muted px-1.5 py-0.5 text-3xs text-toned hover:bg-elevated disabled:opacity-50"
538
- :disabled="initiatives.curating"
539
- :data-testid="`initiative-item-skip-${item.id}`"
540
- @click="itemAction(item, 'skip')"
541
- >
542
- {{ t('initiative.curation.skip') }}
543
- </button>
544
- </div>
545
- </td>
546
- <td class="px-3 py-2 align-top">
547
- <UBadge
548
- :color="INITIATIVE_ITEM_STATUS_CHIPS[item.status]"
549
- variant="subtle"
550
- size="sm"
551
- >
552
- {{ t(INITIATIVE_ITEM_STATUS_LABEL_KEYS[item.status]) }}
553
- </UBadge>
554
- </td>
555
- <td class="px-3 py-2 align-top">
556
- <a
557
- v-if="item.pr"
558
- :href="item.pr.url"
559
- target="_blank"
560
- rel="noopener"
561
- class="text-app-info-400 hover:underline"
562
- >
563
- {{ item.pr.number ? `#${item.pr.number}` : t('initiative.tracker.prLink') }}
564
- </a>
565
- <span v-else class="text-app-600">—</span>
566
- </td>
567
- </tr>
568
- </tbody>
569
- </table>
570
- </div>
508
+ <UTable
509
+ :data="itemsOf(phase.id)"
510
+ :columns="itemColumns"
511
+ :ui="{
512
+ root: 'overflow-x-auto rounded-lg border border-default',
513
+ base: 'text-xs',
514
+ th: 'px-3 py-2',
515
+ td: 'px-3 py-2 text-xs whitespace-normal',
516
+ empty: 'py-0',
517
+ tr: 'align-top',
518
+ }"
519
+ >
520
+ <template #item-cell="{ row }">
521
+ <div class="font-medium text-default">{{ row.original.title }}</div>
522
+ <div v-if="row.original.dependsOn?.length" class="mt-0.5 text-3xs text-dimmed">
523
+ {{
524
+ t('initiative.tracker.dependsOn', {
525
+ items: row.original.dependsOn.join(', '),
526
+ })
527
+ }}
528
+ </div>
529
+ <div
530
+ v-if="row.original.note"
531
+ class="mt-0.5 max-w-3xl text-3xs text-app-warning-300/80"
532
+ >
533
+ {{ row.original.note }}
534
+ </div>
535
+ <div
536
+ v-if="
537
+ editable &&
538
+ (row.original.status === 'blocked' || row.original.status === 'pending')
539
+ "
540
+ class="mt-1 flex gap-1.5"
541
+ >
542
+ <UButton
543
+ v-if="row.original.status === 'blocked'"
544
+ color="neutral"
545
+ variant="outline"
546
+ size="xs"
547
+ :label="t('initiative.curation.retry')"
548
+ :disabled="initiatives.curating"
549
+ :data-testid="`initiative-item-retry-${row.original.id}`"
550
+ @click="itemAction(row.original, 'retry')"
551
+ />
552
+ <UButton
553
+ color="neutral"
554
+ variant="outline"
555
+ size="xs"
556
+ :label="t('initiative.curation.skip')"
557
+ :disabled="initiatives.curating"
558
+ :data-testid="`initiative-item-skip-${row.original.id}`"
559
+ @click="itemAction(row.original, 'skip')"
560
+ />
561
+ </div>
562
+ </template>
563
+ <template #status-cell="{ row }">
564
+ <UBadge
565
+ :color="INITIATIVE_ITEM_STATUS_CHIPS[row.original.status]"
566
+ variant="subtle"
567
+ size="sm"
568
+ >
569
+ {{ t(INITIATIVE_ITEM_STATUS_LABEL_KEYS[row.original.status]) }}
570
+ </UBadge>
571
+ </template>
572
+ <template #pr-cell="{ row }">
573
+ <ULink
574
+ raw
575
+ v-if="row.original.pr"
576
+ :to="row.original.pr.url"
577
+ target="_blank"
578
+ rel="noopener"
579
+ class="text-app-info-400 hover:underline"
580
+ >
581
+ {{
582
+ row.original.pr.number
583
+ ? `#${row.original.pr.number}`
584
+ : t('initiative.tracker.prLink')
585
+ }}
586
+ </ULink>
587
+ <span v-else class="text-app-600">&mdash;</span>
588
+ </template>
589
+ <!-- The hand-built table rendered its header and no row; Nuxt UI's fallback is
590
+ an untranslated "No data.", so the slot states the same nothing. -->
591
+ <template #empty />
592
+ </UTable>
571
593
  </section>
572
594
 
573
595
  <!-- Execution policy -->
@@ -576,14 +598,16 @@ const { requestClose } = useUnsavedGuard({
576
598
  <SectionLabel as="h3">
577
599
  {{ t('initiative.tracker.policy') }}
578
600
  </SectionLabel>
579
- <button
601
+ <UButton
602
+ color="neutral"
603
+ variant="ghost"
580
604
  v-if="editable && !editingPolicy"
581
605
  class="rounded-sm border border-muted px-1.5 py-0.5 text-3xs text-toned hover:bg-elevated"
582
606
  data-testid="initiative-policy-edit"
583
607
  @click="startEditPolicy"
584
608
  >
585
609
  {{ t('initiative.curation.edit') }}
586
- </button>
610
+ </UButton>
587
611
  </div>
588
612
  <ul v-if="!editingPolicy" class="text-xs text-toned">
589
613
  <li>
@@ -604,41 +628,56 @@ const { requestClose } = useUnsavedGuard({
604
628
  </ul>
605
629
  <!-- Edit form: the two scalar knobs; planner-authored rules are preserved. -->
606
630
  <div v-else class="flex flex-col gap-2 rounded-lg border border-default p-3">
607
- <label class="flex items-center gap-2 text-xs text-toned">
608
- <span class="w-40">{{ t('initiative.curation.maxConcurrentField') }}</span>
609
- <input
610
- v-model.number="policyForm.maxConcurrent"
611
- type="number"
612
- min="1"
613
- max="20"
614
- class="w-20 rounded-sm border border-muted bg-app-950 px-2 py-1 text-default"
631
+ <UFormField
632
+ size="xs"
633
+ :label="t('initiative.curation.maxConcurrentField')"
634
+ :ui="{ root: 'flex items-center gap-2', label: 'w-40', container: 'mt-0' }"
635
+ >
636
+ <UInputNumber
637
+ v-model.optional="policyForm.maxConcurrent"
638
+ :min="1"
639
+ :max="20"
640
+ size="xs"
641
+ class="w-28"
615
642
  data-testid="initiative-policy-max-concurrent"
616
643
  />
617
- </label>
618
- <label class="flex items-center gap-2 text-xs text-toned">
619
- <span class="w-40">{{ t('initiative.curation.defaultPipelineField') }}</span>
620
- <input
644
+ </UFormField>
645
+ <UFormField
646
+ size="xs"
647
+ :label="t('initiative.curation.defaultPipelineField')"
648
+ :ui="{
649
+ root: 'flex items-center gap-2',
650
+ label: 'w-40',
651
+ container: 'flex-1 mt-0',
652
+ }"
653
+ >
654
+ <UInput
621
655
  v-model="policyForm.defaultPipelineId"
622
- type="text"
623
- class="flex-1 rounded-sm border border-muted bg-app-950 px-2 py-1 font-mono text-2xs text-default"
656
+ size="xs"
657
+ class="w-full"
658
+ :ui="{ base: 'font-mono' }"
624
659
  data-testid="initiative-policy-default-pipeline"
625
660
  />
626
- </label>
661
+ </UFormField>
627
662
  <div class="flex gap-2">
628
- <button
663
+ <UButton
664
+ color="neutral"
665
+ variant="ghost"
629
666
  class="rounded-sm bg-primary/90 px-2 py-1 text-2xs text-inverted hover:bg-primary disabled:opacity-50"
630
667
  :disabled="initiatives.curating"
631
668
  data-testid="initiative-policy-save"
632
669
  @click="savePolicy"
633
670
  >
634
671
  {{ t('initiative.curation.save') }}
635
- </button>
636
- <button
672
+ </UButton>
673
+ <UButton
674
+ color="neutral"
675
+ variant="ghost"
637
676
  class="rounded-sm border border-muted px-2 py-1 text-2xs text-toned hover:bg-elevated"
638
677
  @click="editingPolicy = false"
639
678
  >
640
679
  {{ t('initiative.curation.cancel') }}
641
- </button>
680
+ </UButton>
642
681
  </div>
643
682
  </div>
644
683
  </section>
@@ -694,58 +733,66 @@ const { requestClose } = useUnsavedGuard({
694
733
  <!-- Triage actions for an open follow-up (only while executing) -->
695
734
  <div v-if="editable && f.status === 'open'" class="mt-2">
696
735
  <div v-if="promotingId === f.id" class="flex flex-col gap-2">
697
- <label class="flex items-center gap-2 text-xs">
698
- <span class="text-muted">{{ t('initiative.curation.phaseField') }}</span>
699
- <select
736
+ <UFormField
737
+ size="xs"
738
+ :label="t('initiative.curation.phaseField')"
739
+ :ui="{ root: 'flex items-center gap-2', container: 'flex-1 mt-0' }"
740
+ >
741
+ <USelect
700
742
  v-model="promoteForm.phaseId"
701
- class="flex-1 rounded-sm border border-muted bg-app-950 px-2 py-1 text-default"
743
+ :items="phaseItems"
744
+ size="xs"
745
+ class="w-full"
702
746
  data-testid="initiative-promote-phase"
703
- >
704
- <option v-for="p in phases" :key="p.id" :value="p.id">
705
- {{ p.title }}
706
- </option>
707
- </select>
708
- </label>
709
- <input
747
+ />
748
+ </UFormField>
749
+ <UInput
710
750
  v-model="promoteForm.title"
711
- type="text"
712
- class="rounded-sm border border-muted bg-app-950 px-2 py-1 text-xs text-default"
751
+ size="xs"
713
752
  :placeholder="t('initiative.curation.itemTitlePlaceholder')"
714
753
  data-testid="initiative-promote-title"
715
754
  />
716
755
  <div class="flex gap-2">
717
- <button
756
+ <UButton
757
+ color="neutral"
758
+ variant="ghost"
718
759
  class="rounded-sm bg-primary/90 px-2 py-1 text-2xs text-inverted hover:bg-primary disabled:opacity-50"
719
760
  :disabled="initiatives.curating || !promoteForm.phaseId"
720
761
  data-testid="initiative-promote-submit"
721
762
  @click="submitPromote(f)"
722
763
  >
723
764
  {{ t('initiative.curation.promoteConfirm') }}
724
- </button>
725
- <button
765
+ </UButton>
766
+ <UButton
767
+ color="neutral"
768
+ variant="ghost"
726
769
  class="rounded-sm border border-muted px-2 py-1 text-2xs text-toned hover:bg-elevated"
727
770
  @click="promotingId = null"
728
771
  >
729
772
  {{ t('initiative.curation.cancel') }}
730
- </button>
773
+ </UButton>
731
774
  </div>
732
775
  </div>
733
776
  <div v-else class="flex gap-1.5">
734
- <button
777
+ <UButton
778
+ color="neutral"
779
+ variant="ghost"
735
780
  class="rounded-sm border border-muted px-1.5 py-0.5 text-3xs text-toned hover:bg-elevated"
736
781
  data-testid="initiative-followup-promote"
737
782
  @click="startPromote(f)"
738
783
  >
739
784
  {{ t('initiative.curation.promote') }}
740
- </button>
741
- <button
785
+ </UButton>
786
+ <UButton
787
+ color="neutral"
788
+ variant="ghost"
742
789
  class="rounded-sm border border-muted px-1.5 py-0.5 text-3xs text-toned hover:bg-elevated disabled:opacity-50"
743
790
  :disabled="initiatives.curating"
744
791
  data-testid="initiative-followup-dismiss"
745
792
  @click="dismissFollowUp(f)"
746
793
  >
747
794
  {{ t('initiative.curation.dismiss') }}
748
- </button>
795
+ </UButton>
749
796
  </div>
750
797
  </div>
751
798
  </li>
@@ -253,12 +253,14 @@ async function act(choice: 'proceed' | 'bounce' | 'stop') {
253
253
  <p class="mb-2 text-2xs leading-relaxed text-dimmed">
254
254
  {{ t('judge.decisionDescription') }}
255
255
  </p>
256
- <textarea
256
+ <UTextarea
257
257
  v-model="feedback"
258
- rows="3"
258
+ :rows="3"
259
259
  :disabled="busy"
260
260
  :placeholder="t('judge.feedbackPlaceholder')"
261
- class="w-full resize-y rounded-md border border-default bg-app-950/60 px-3 py-2 text-sm text-default placeholder:text-app-600 focus:border-app-warning-500/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-app-warning-500/60"
261
+ size="sm"
262
+ class="w-full"
263
+ :ui="{ base: 'resize-y' }"
262
264
  />
263
265
  <p v-if="judgeStore.error" class="mt-2 text-xs text-app-error-300">
264
266
  {{ judgeStore.error }}
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, watch } from 'vue'
3
3
  import { onKeyStroke } from '@vueuse/core'
4
+ import type { TableColumn } from '@nuxt/ui'
4
5
  import type { KaizenGrading } from '~/types/domain'
5
6
  import { agentKindMeta } from '~/utils/catalog'
6
7
 
@@ -48,6 +49,16 @@ function statusLabel(g: KaizenGrading): string {
48
49
  if (g.status === 'failed') return t('kaizen.status.failed')
49
50
  return g.grade != null ? t('kaizen.gradeValue', { grade: g.grade }) : t('kaizen.status.graded')
50
51
  }
52
+
53
+ // Column definitions rather than hand-written `<th>`s, so the header typography comes from the
54
+ // table theme and is the same one every other table in the SPA renders.
55
+ const historyColumns = computed<TableColumn<KaizenGrading>[]>(() => [
56
+ { id: 'when', header: t('kaizen.history.col.when') },
57
+ { id: 'agent', header: t('kaizen.history.col.agent') },
58
+ { id: 'model', header: t('kaizen.history.col.model') },
59
+ { id: 'grade', header: t('kaizen.history.col.grade') },
60
+ { id: 'recommendations', header: t('kaizen.history.col.recommendations') },
61
+ ])
51
62
  </script>
52
63
 
53
64
  <template>
@@ -155,59 +166,62 @@ function statusLabel(g: KaizenGrading): string {
155
166
  <UIcon name="i-lucide-history" class="h-4 w-4 text-app-hue-teal" />
156
167
  {{ t('kaizen.history.title') }}
157
168
  </h2>
158
- <div class="overflow-hidden rounded-lg border border-default">
159
- <table class="w-full text-start text-xs">
160
- <thead class="bg-default/60 text-2xs uppercase tracking-wide text-dimmed">
161
- <tr>
162
- <th class="px-3 py-2 font-medium">{{ t('kaizen.history.col.when') }}</th>
163
- <th class="px-3 py-2 font-medium">{{ t('kaizen.history.col.agent') }}</th>
164
- <th class="px-3 py-2 font-medium">{{ t('kaizen.history.col.model') }}</th>
165
- <th class="px-3 py-2 font-medium">{{ t('kaizen.history.col.grade') }}</th>
166
- <th class="px-3 py-2 font-medium">
167
- {{ t('kaizen.history.col.recommendations') }}
168
- </th>
169
- </tr>
170
- </thead>
171
- <tbody class="divide-y divide-default/70">
172
- <tr v-for="g in kaizen.history" :key="g.id" class="align-top">
173
- <td class="whitespace-nowrap px-3 py-2 text-dimmed">
174
- {{ when(g.createdAt) }}
175
- </td>
176
- <td class="px-3 py-2">
177
- <span class="flex items-center gap-1.5">
178
- <UIcon
179
- :name="meta(g.agentKind).icon"
180
- class="h-3.5 w-3.5"
181
- :style="{ color: meta(g.agentKind).color }"
182
- />
183
- <span class="text-default">{{ meta(g.agentKind).label }}</span>
184
- <span class="text-app-600">{{
185
- t('kaizen.promptVersion', { version: g.promptVersion })
186
- }}</span>
187
- </span>
188
- </td>
189
- <td class="max-w-[12rem] truncate px-3 py-2 text-muted" :title="g.model">
190
- {{ g.model }}
191
- </td>
192
- <td class="whitespace-nowrap px-3 py-2 font-semibold" :class="gradeTone(g)">
193
- {{ statusLabel(g) }}
194
- </td>
195
- <td class="px-3 py-2 text-muted">
196
- <ul v-if="g.recommendations.length" class="list-disc space-y-0.5 ps-4">
197
- <li v-for="(r, i) in g.recommendations" :key="i">{{ r }}</li>
198
- </ul>
199
- <span v-else-if="g.status === 'complete'" class="text-app-600">—</span>
200
- <span v-else-if="g.error" class="text-app-error-400/80">{{ g.error }}</span>
201
- </td>
202
- </tr>
203
- <tr v-if="kaizen.history.length === 0">
204
- <td colspan="5" class="px-3 py-6 text-center text-app-600">
205
- {{ t('kaizen.history.empty') }}
206
- </td>
207
- </tr>
208
- </tbody>
209
- </table>
210
- </div>
169
+ <UTable
170
+ :data="kaizen.history"
171
+ :columns="historyColumns"
172
+ :ui="{
173
+ root: 'overflow-hidden rounded-lg border border-default',
174
+ base: 'text-xs',
175
+ th: 'px-3 py-2',
176
+ td: 'px-3 py-2 text-xs whitespace-normal',
177
+ tr: 'align-top',
178
+ }"
179
+ >
180
+ <template #when-cell="{ row }">
181
+ <span class="whitespace-nowrap text-dimmed">{{
182
+ when(row.original.createdAt)
183
+ }}</span>
184
+ </template>
185
+ <template #agent-cell="{ row }">
186
+ <span class="flex items-center gap-1.5">
187
+ <UIcon
188
+ :name="meta(row.original.agentKind).icon"
189
+ class="h-3.5 w-3.5"
190
+ :style="{ color: meta(row.original.agentKind).color }"
191
+ />
192
+ <span class="text-default">{{ meta(row.original.agentKind).label }}</span>
193
+ <span class="text-app-600">{{
194
+ t('kaizen.promptVersion', { version: row.original.promptVersion })
195
+ }}</span>
196
+ </span>
197
+ </template>
198
+ <template #model-cell="{ row }">
199
+ <span class="block max-w-[12rem] truncate text-muted" :title="row.original.model">
200
+ {{ row.original.model }}
201
+ </span>
202
+ </template>
203
+ <template #grade-cell="{ row }">
204
+ <span class="whitespace-nowrap font-semibold" :class="gradeTone(row.original)">
205
+ {{ statusLabel(row.original) }}
206
+ </span>
207
+ </template>
208
+ <template #recommendations-cell="{ row }">
209
+ <div class="text-muted">
210
+ <ul v-if="row.original.recommendations.length" class="list-disc space-y-0.5 ps-4">
211
+ <li v-for="(r, i) in row.original.recommendations" :key="i">{{ r }}</li>
212
+ </ul>
213
+ <span v-else-if="row.original.status === 'complete'" class="text-app-600">
214
+ &mdash;
215
+ </span>
216
+ <span v-else-if="row.original.error" class="text-app-error-400/80">
217
+ {{ row.original.error }}
218
+ </span>
219
+ </div>
220
+ </template>
221
+ <template #empty>
222
+ <span class="text-app-600">{{ t('kaizen.history.empty') }}</span>
223
+ </template>
224
+ </UTable>
211
225
  </section>
212
226
  </div>
213
227
  </div>