@cat-factory/app 0.309.0 → 0.310.0

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 +1 -1
@@ -94,8 +94,9 @@ function choose(id: string) {
94
94
  <!-- left: selectable options -->
95
95
  <ul class="w-1/2 shrink-0 overflow-y-auto border-e border-default p-1">
96
96
  <li v-if="noneLabel !== undefined">
97
- <button
98
- type="button"
97
+ <UButton
98
+ color="neutral"
99
+ variant="ghost"
99
100
  class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-start text-sm hover:bg-elevated/60"
100
101
  :class="modelValue ? 'text-toned' : 'text-app-100'"
101
102
  data-testid="pipeline-option-none"
@@ -109,11 +110,12 @@ function choose(id: string) {
109
110
  name="i-lucide-check"
110
111
  class="h-4 w-4 shrink-0 text-primary"
111
112
  />
112
- </button>
113
+ </UButton>
113
114
  </li>
114
115
  <li v-for="p in options" :key="p.id">
115
- <button
116
- type="button"
116
+ <UButton
117
+ color="neutral"
118
+ variant="ghost"
117
119
  class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-start text-sm hover:bg-elevated/60"
118
120
  :class="modelValue === p.id ? 'text-app-100' : 'text-toned'"
119
121
  :data-testid="`pipeline-option-${p.id}`"
@@ -127,7 +129,7 @@ function choose(id: string) {
127
129
  name="i-lucide-check"
128
130
  class="h-4 w-4 shrink-0 text-primary"
129
131
  />
130
- </button>
132
+ </UButton>
131
133
  </li>
132
134
  </ul>
133
135
 
@@ -21,6 +21,7 @@ import PrReviewPhaseBadge from '~/components/prReview/PrReviewPhaseBadge.vue'
21
21
  import { useNowTick, stepDurationLabel } from '~/composables/useStepTimer'
22
22
  import type { BadgeColor } from '~/utils/badge'
23
23
  import SectionLabel from '~/components/common/SectionLabel.vue'
24
+ import IconButton from '~/components/common/IconButton.vue'
24
25
 
25
26
  const props = defineProps<{ instance: ExecutionInstance }>()
26
27
  const emit = defineEmits<{
@@ -397,14 +398,14 @@ const ITEM_ICON: Record<string, string> = {
397
398
  (resetting later steps is destructive). Stops propagation so it
398
399
  doesn't also open the step-detail overlay. -->
399
400
  <template v-if="canRestart(s)">
400
- <UButton
401
+ <IconButton
401
402
  v-if="restartArmed !== i"
402
403
  icon="i-lucide-rotate-ccw"
403
404
  color="neutral"
404
405
  variant="ghost"
405
406
  size="xs"
406
- class="shrink-0 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100"
407
- :title="t('pipeline.progress.restartTooltip')"
407
+ class="shrink-0 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 focus-within:opacity-100"
408
+ :label="t('pipeline.progress.restartTooltip')"
408
409
  @click.stop="
409
410
  () => {
410
411
  restartArmed = i
@@ -607,14 +608,15 @@ const ITEM_ICON: Record<string, string> = {
607
608
  <!-- Follow-up companion (future-looking Coder): a blinking chip that lights up the
608
609
  moment the Coder streams an item; click to triage. Blinks while any item is
609
610
  undecided (the gate holds the pipeline until they're all decided). -->
610
- <button
611
+ <UButton
612
+ color="neutral"
613
+ variant="ghost"
611
614
  v-if="s.followUps?.enabled"
612
- type="button"
613
615
  class="mt-3 flex w-full items-center gap-2 rounded-lg border border-dashed px-2.5 py-1.5 text-start transition hover:border-app-hue-pink/60"
614
616
  :class="
615
617
  followUpPending(s) > 0
616
- ? 'border-app-hue-pink/50 bg-app-hue-pink/10 followup-blink'
617
- : 'border-muted/70 bg-default/40'
618
+ ? 'border-app-hue-pink/50 bg-app-hue-pink/10 hover:bg-app-hue-pink/10 focus-visible:bg-app-hue-pink/10 disabled:bg-app-hue-pink/10 followup-blink'
619
+ : 'border-muted/70 bg-default/40 hover:bg-default/40 focus-visible:bg-default/40 disabled:bg-default/40'
618
620
  "
619
621
  @click="ui.openFollowUps(instance.id, i)"
620
622
  >
@@ -633,20 +635,21 @@ const ITEM_ICON: Record<string, string> = {
633
635
  >
634
636
  {{ followUpLabel(s) }}
635
637
  </span>
636
- </button>
638
+ </UButton>
637
639
 
638
640
  <!-- Implementation-fork decision phase (Coder step): a spinner while the proposer
639
641
  surfaces approaches, then a clickable chip to choose one. -->
640
- <button
642
+ <UButton
643
+ color="neutral"
644
+ variant="ghost"
641
645
  v-if="forkPhase(s)"
642
- type="button"
643
646
  data-testid="fork-decision-open"
644
647
  :data-fork-phase="forkPhase(s)"
645
648
  class="mt-3 flex w-full items-center gap-2 rounded-lg border border-dashed px-2.5 py-1.5 text-start transition hover:border-app-secondary-400/60"
646
649
  :class="
647
650
  forkPhase(s) === 'awaiting_choice'
648
- ? 'border-app-secondary-500/50 bg-app-secondary-500/10 followup-blink'
649
- : 'border-muted/70 bg-default/40'
651
+ ? 'border-app-secondary-500/50 bg-app-secondary-500/10 hover:bg-app-secondary-500/10 focus-visible:bg-app-secondary-500/10 disabled:bg-app-secondary-500/10 followup-blink'
652
+ : 'border-muted/70 bg-default/40 hover:bg-default/40 focus-visible:bg-default/40 disabled:bg-default/40'
650
653
  "
651
654
  :disabled="forkPhase(s) === 'proposing'"
652
655
  @click="ui.openForkDecision(instance.id, i)"
@@ -669,16 +672,17 @@ const ITEM_ICON: Record<string, string> = {
669
672
  : t('pipeline.progress.forkDecision.choose')
670
673
  }}
671
674
  </span>
672
- </button>
675
+ </UButton>
673
676
 
674
677
  <!-- PR deep-review parked for a human to select which findings matter: a
675
678
  purpose-built chip opening the findings-selection window, ahead of the
676
679
  generic approval gate (mirrors the fork-decision chip above). -->
677
- <button
680
+ <UButton
681
+ color="neutral"
682
+ variant="ghost"
678
683
  v-if="prReviewAwaiting(s)"
679
- type="button"
680
684
  data-testid="pr-review-open"
681
- class="mt-3 flex w-full items-center gap-2 rounded-lg border border-dashed border-primary/50 bg-primary/10 px-2.5 py-1.5 text-start transition followup-blink hover:border-primary/60"
685
+ class="mt-3 flex w-full items-center gap-2 rounded-lg border border-dashed border-primary/50 bg-primary/10 hover:bg-primary/10 focus-visible:bg-primary/10 disabled:bg-primary/10 px-2.5 py-1.5 text-start transition followup-blink hover:border-primary/60"
682
686
  @click="ui.openPrReview(instance.id, i)"
683
687
  >
684
688
  <span
@@ -689,18 +693,19 @@ const ITEM_ICON: Record<string, string> = {
689
693
  <span class="min-w-0 flex-1 truncate text-xs text-toned">
690
694
  {{ t('pipeline.progress.prReview.review') }}
691
695
  </span>
692
- </button>
696
+ </UButton>
693
697
 
694
698
  <!-- A generating step parked between its candidate pass and its delivering pass: a
695
699
  purpose-built chip opening the comparison window, ahead of the generic approval
696
700
  gate (mirrors the fork-decision and pr-review chips above). Without it the step
697
701
  shows no action at all, because the generic gate below is suppressed for every
698
702
  park a dedicated window owns. -->
699
- <button
703
+ <UButton
704
+ color="neutral"
705
+ variant="ghost"
700
706
  v-if="candidatesAwaiting(s)"
701
- type="button"
702
707
  data-testid="binary-candidates-open"
703
- class="mt-3 flex w-full items-center gap-2 rounded-lg border border-dashed border-app-hue-cyan/50 bg-app-hue-cyan/10 px-2.5 py-1.5 text-start transition followup-blink hover:border-app-hue-cyan/60"
708
+ class="mt-3 flex w-full items-center gap-2 rounded-lg border border-dashed border-app-hue-cyan/50 bg-app-hue-cyan/10 hover:bg-app-hue-cyan/10 focus-visible:bg-app-hue-cyan/10 disabled:bg-app-hue-cyan/10 px-2.5 py-1.5 text-start transition followup-blink hover:border-app-hue-cyan/60"
704
709
  @click="ui.openBinaryCandidates(instance.id, i)"
705
710
  >
706
711
  <span
@@ -714,7 +719,7 @@ const ITEM_ICON: Record<string, string> = {
714
719
  <span class="min-w-0 flex-1 truncate text-xs text-toned">
715
720
  {{ t('pipeline.progress.binaryCandidates.choose') }}
716
721
  </span>
717
- </button>
722
+ </UButton>
718
723
 
719
724
  <!-- reviewer gate folding/re-reviewing in the background: a working indicator,
720
725
  NOT a "Review & approve" gate (the human is summoned only if needed) -->
@@ -292,14 +292,15 @@ const { requestClose } = useUnsavedGuard({
292
292
  @close="requestClose"
293
293
  >
294
294
  <template v-if="state?.prUrl" #header-extras>
295
- <a
296
- :href="state.prUrl"
295
+ <ULink
296
+ raw
297
+ :to="state.prUrl"
297
298
  target="_blank"
298
299
  rel="noopener"
299
300
  class="rounded-md px-2 py-1 text-2xs text-primary hover:bg-elevated"
300
301
  >
301
302
  {{ t('prReview.openPr') }}
302
- </a>
303
+ </ULink>
303
304
  </template>
304
305
 
305
306
  <div class="flex min-h-0 flex-1">
@@ -586,12 +587,22 @@ const { requestClose } = useUnsavedGuard({
586
587
  <span data-testid="pr-review-selected-count">
587
588
  {{ t('prReview.selectedCount', { count: activeSelectedIds.length }) }}
588
589
  </span>
589
- <button class="text-primary hover:underline" @click="selectAll">
590
+ <UButton
591
+ color="primary"
592
+ variant="link"
593
+ class="p-0 text-2xs text-primary hover:underline"
594
+ @click="selectAll"
595
+ >
590
596
  {{ t('prReview.selectAll') }}
591
- </button>
592
- <button class="text-primary hover:underline" @click="clearAll">
597
+ </UButton>
598
+ <UButton
599
+ color="primary"
600
+ variant="link"
601
+ class="p-0 text-2xs text-primary hover:underline"
602
+ @click="clearAll"
603
+ >
593
604
  {{ t('prReview.clear') }}
594
- </button>
605
+ </UButton>
595
606
  </div>
596
607
 
597
608
  <!-- Findings grouped by slice -->
@@ -615,14 +626,14 @@ const { requestClose } = useUnsavedGuard({
615
626
  ]"
616
627
  >
617
628
  <div class="flex items-start gap-2">
618
- <input
629
+ <UCheckbox
619
630
  v-if="awaiting || challenging"
620
- type="checkbox"
621
- class="mt-1 accent-primary"
631
+ size="xs"
632
+ class="mt-1"
622
633
  data-testid="pr-review-finding-toggle"
623
- :checked="selected.has(f.id) && !isRetracted(f)"
634
+ :model-value="selected.has(f.id) && !isRetracted(f)"
624
635
  :disabled="!awaiting || isRetracted(f)"
625
- @change="toggle(f.id)"
636
+ @update:model-value="toggle(f.id)"
626
637
  />
627
638
  <div class="min-w-0 flex-1">
628
639
  <div class="flex flex-wrap items-center gap-1.5">
@@ -748,10 +759,12 @@ const { requestClose } = useUnsavedGuard({
748
759
  v-if="awaiting && !isInvestigating(f)"
749
760
  class="mt-1.5 flex items-center gap-3 text-2xs"
750
761
  >
751
- <button
762
+ <UButton
763
+ color="primary"
764
+ variant="link"
752
765
  v-if="!isRetracted(f)"
753
766
  data-testid="pr-review-finding-challenge"
754
- class="flex items-center gap-1 text-primary hover:underline disabled:opacity-50"
767
+ class="p-0 text-2xs flex items-center gap-1 text-primary hover:underline disabled:opacity-50"
755
768
  :disabled="!canResolve || !access.canExecuteRuns.value"
756
769
  @click="openChallenge(f.id)"
757
770
  >
@@ -761,16 +774,18 @@ const { requestClose } = useUnsavedGuard({
761
774
  ? t('prReview.challenge.reChallenge')
762
775
  : t('prReview.challenge.action')
763
776
  }}
764
- </button>
765
- <button
777
+ </UButton>
778
+ <UButton
779
+ color="neutral"
780
+ variant="link"
766
781
  data-testid="pr-review-finding-dismiss"
767
- class="flex items-center gap-1 text-muted hover:text-app-error-300 hover:underline disabled:opacity-50"
782
+ class="p-0 text-xs flex items-center gap-1 text-muted hover:text-app-error-300 hover:underline disabled:opacity-50"
768
783
  :disabled="!canResolve || !access.canExecuteRuns.value"
769
784
  @click="onDismiss(f.id)"
770
785
  >
771
786
  <UIcon name="i-lucide-trash-2" class="h-3.5 w-3.5" />
772
787
  {{ t('prReview.challenge.dismiss') }}
773
- </button>
788
+ </UButton>
774
789
  </div>
775
790
 
776
791
  <!-- The inline challenge box: an OPTIONAL specific concern for the investigator. -->
@@ -779,31 +794,37 @@ const { requestClose } = useUnsavedGuard({
779
794
  data-testid="pr-review-challenge-box"
780
795
  class="mt-2 rounded-md border border-primary/40 bg-default/80 p-2"
781
796
  >
782
- <textarea
797
+ <UTextarea
783
798
  v-model="challengeText"
784
799
  data-testid="pr-review-challenge-input"
785
- rows="2"
800
+ :rows="2"
786
801
  :placeholder="t('prReview.challenge.placeholder')"
787
- class="w-full resize-y rounded-sm border border-muted bg-app-950/60 px-2 py-1 text-xs text-default outline-none focus:border-primary"
802
+ size="xs"
803
+ class="w-full"
804
+ :ui="{ base: 'resize-y' }"
788
805
  />
789
806
  <p class="mt-1 text-3xs text-dimmed">
790
807
  {{ t('prReview.challenge.hint') }}
791
808
  </p>
792
809
  <div class="mt-1.5 flex justify-end gap-2">
793
- <button
810
+ <UButton
811
+ color="neutral"
812
+ variant="ghost"
794
813
  class="rounded-sm px-2 py-1 text-2xs text-muted hover:text-default"
795
814
  @click="cancelChallenge"
796
815
  >
797
816
  {{ t('common.cancel') }}
798
- </button>
799
- <button
817
+ </UButton>
818
+ <UButton
819
+ color="neutral"
820
+ variant="ghost"
800
821
  data-testid="pr-review-challenge-submit"
801
822
  class="rounded-sm bg-primary/80 px-2 py-1 text-2xs font-medium text-inverted hover:bg-primary/90 disabled:opacity-50"
802
823
  :disabled="!canResolve"
803
824
  @click="submitChallenge(f.id)"
804
825
  >
805
826
  {{ t('prReview.challenge.submit') }}
806
- </button>
827
+ </UButton>
807
828
  </div>
808
829
  </div>
809
830
  </div>
@@ -329,14 +329,15 @@ async function remove(k: ApiKey) {
329
329
  >
330
330
  <li v-for="(step, i) in selected.steps" :key="i">{{ step }}</li>
331
331
  <li>
332
- <a
333
- :href="selected.url"
332
+ <ULink
333
+ raw
334
+ :to="selected.url"
334
335
  target="_blank"
335
336
  rel="noopener noreferrer"
336
337
  class="text-primary underline"
337
338
  >
338
339
  {{ t('providers.apiKeys.openKeys', { provider: selected.label }) }}
339
- </a>
340
+ </ULink>
340
341
  </li>
341
342
  </ol>
342
343
 
@@ -169,16 +169,17 @@ const STATUS_META = computed<
169
169
  </div>
170
170
  </template>
171
171
 
172
- <a
172
+ <ULink
173
+ raw
173
174
  v-if="prUrl"
174
- :href="prUrl"
175
+ :to="prUrl"
175
176
  target="_blank"
176
177
  rel="noopener"
177
178
  class="mt-3 inline-flex items-center gap-1 text-xs text-app-info-300 hover:text-app-info-200 hover:underline"
178
179
  >
179
180
  {{ t('ralph.viewPr') }}
180
181
  <UIcon name="i-lucide-external-link" class="h-3 w-3" />
181
- </a>
182
+ </ULink>
182
183
 
183
184
  <!-- Why a loop that stopped short of its budget stopped: without this the count in
184
185
  the sidebar (e.g. "3 of 20") reads as an unexplained abandonment. -->
@@ -1116,8 +1116,10 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
1116
1116
  collapses as a unit (a long doc otherwise pushes the findings/recommendations
1117
1117
  off-screen); the per-heading toggles below still work when it's expanded. -->
1118
1118
  <section v-if="outline" class="mt-6 border-t border-default pt-5">
1119
- <button
1120
- class="mb-3 flex w-full items-center gap-1.5 text-2xs text-app-success-400"
1119
+ <UButton
1120
+ color="neutral"
1121
+ variant="ghost"
1122
+ class="mb-3 flex w-full items-center gap-1.5 p-0 text-2xs text-app-success-400 hover:bg-transparent"
1121
1123
  @click="toggleDoc"
1122
1124
  >
1123
1125
  <UIcon
@@ -1133,16 +1135,18 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
1133
1135
  : t('requirements.incorporatedDraft')
1134
1136
  }}
1135
1137
  </span>
1136
- </button>
1138
+ </UButton>
1137
1139
  <!-- The same reading measure the findings' own prose takes above (see the shell's
1138
1140
  `width` prop): the window is `full`-width now, and this is continuous prose that
1139
1141
  would otherwise run to 200-character lines. Left-aligned rather than centred, so
1140
1142
  it starts where every finding above it starts. -->
1141
1143
  <div v-show="!docCollapsed" class="max-w-3xl">
1142
1144
  <div v-for="s in outline.sections" :key="s.id" class="mb-2">
1143
- <button
1145
+ <UButton
1146
+ color="neutral"
1147
+ variant="ghost"
1144
1148
  v-if="s.title"
1145
- class="group flex w-full items-center gap-2 text-start"
1149
+ class="group flex w-full items-center gap-2 p-0 text-start hover:bg-transparent"
1146
1150
  @click="toggle(s.id)"
1147
1151
  >
1148
1152
  <UIcon
@@ -1155,7 +1159,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
1155
1159
  :class="s.depth <= 1 ? 'text-base' : s.depth === 2 ? 'text-sm' : 'text-xs'"
1156
1160
  v-html="s.titleHtml"
1157
1161
  />
1158
- </button>
1162
+ </UButton>
1159
1163
  <div
1160
1164
  v-show="!s.title || !collapsed[s.id]"
1161
1165
  class="reader-prose mt-1 ps-5.5 text-sm leading-relaxed text-toned"
@@ -140,8 +140,9 @@ function choose(id: string) {
140
140
  <!-- left: selectable options, NAME ONLY -->
141
141
  <ul class="w-1/2 shrink-0 overflow-y-auto border-e border-default p-1">
142
142
  <li>
143
- <button
144
- type="button"
143
+ <UButton
144
+ color="neutral"
145
+ variant="ghost"
145
146
  class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-start text-sm"
146
147
  :class="[
147
148
  refused.has('') ? 'cursor-not-allowed opacity-50' : 'hover:bg-elevated/60',
@@ -164,11 +165,12 @@ function choose(id: string) {
164
165
  name="i-lucide-check"
165
166
  class="h-4 w-4 shrink-0 text-primary"
166
167
  />
167
- </button>
168
+ </UButton>
168
169
  </li>
169
170
  <li v-for="p in options" :key="p.id">
170
- <button
171
- type="button"
171
+ <UButton
172
+ color="neutral"
173
+ variant="ghost"
172
174
  class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-start text-sm"
173
175
  :class="[
174
176
  refused.has(p.id) ? 'cursor-not-allowed opacity-50' : 'hover:bg-elevated/60',
@@ -191,7 +193,7 @@ function choose(id: string) {
191
193
  name="i-lucide-check"
192
194
  class="h-4 w-4 shrink-0 text-primary"
193
195
  />
194
- </button>
196
+ </UButton>
195
197
  </li>
196
198
  </ul>
197
199
 
@@ -6,6 +6,7 @@
6
6
  // and Fixtures (the graded inputs each run is scored against). Loaded on demand when the
7
7
  // window opens; 503 (the deployment hasn't provisioned the Sandbox DB) shows a notice.
8
8
  import { computed, ref, watch } from 'vue'
9
+ import type { TableColumn } from '@nuxt/ui'
9
10
  import type {
10
11
  SandboxExperimentStatus,
11
12
  SandboxFixtureKind,
@@ -16,6 +17,7 @@ import type {
16
17
  SandboxUnsupportedReason,
17
18
  } from '~/types/sandbox'
18
19
  import SectionLabel from '~/components/common/SectionLabel.vue'
20
+ import IconButton from '~/components/common/IconButton.vue'
19
21
 
20
22
  const ui = useUiStore()
21
23
  const store = useSandboxStore()
@@ -222,6 +224,16 @@ const detailRows = computed(() =>
222
224
  )
223
225
  const selectedRun = ref<SandboxRun | null>(null)
224
226
 
227
+ // Column definitions rather than hand-written `<th>`s: the header typography then comes from the
228
+ // table theme instead of a per-file recipe.
229
+ const detailColumns = computed<TableColumn<(typeof detailRows.value)[number]>[]>(() => [
230
+ { id: 'prompt', header: t('sandbox.results.col.prompt') },
231
+ { id: 'model', header: t('sandbox.results.col.model') },
232
+ { id: 'fixture', header: t('sandbox.results.col.fixture') },
233
+ { id: 'score', header: t('sandbox.results.col.score') },
234
+ { id: 'objective', header: t('sandbox.results.col.objective') },
235
+ ])
236
+
225
237
  function scoreColor(score: number): string {
226
238
  if (score >= 4) return 'text-app-success-400'
227
239
  if (score >= 3) return 'text-app-warning-400'
@@ -469,55 +481,60 @@ async function archive(prompt: SandboxPromptVersion) {
469
481
  }}</UBadge>
470
482
  </div>
471
483
  <div class="overflow-auto">
472
- <table class="w-full text-start text-xs">
473
- <thead class="text-dimmed">
474
- <tr>
475
- <th class="py-1 pe-2 font-medium">{{ t('sandbox.results.col.prompt') }}</th>
476
- <th class="py-1 pe-2 font-medium">{{ t('sandbox.results.col.model') }}</th>
477
- <th class="py-1 pe-2 font-medium">{{ t('sandbox.results.col.fixture') }}</th>
478
- <th class="py-1 pe-2 font-medium">{{ t('sandbox.results.col.score') }}</th>
479
- <th class="py-1 font-medium">{{ t('sandbox.results.col.objective') }}</th>
480
- </tr>
481
- </thead>
482
- <tbody>
483
- <tr
484
- v-for="{ run, grade, fixtureName } in detailRows"
485
- :key="run.id"
486
- class="cursor-pointer border-t border-default hover:bg-elevated/40"
487
- @click="selectedRun = run"
484
+ <UTable
485
+ :data="detailRows"
486
+ :columns="detailColumns"
487
+ :ui="{
488
+ base: 'text-xs',
489
+ th: 'ps-0 pe-2 py-1',
490
+ td: 'ps-0 pe-2 py-1 text-xs whitespace-normal',
491
+ tbody: '[&>tr]:cursor-pointer',
492
+ empty: 'py-0',
493
+ }"
494
+ @select="(_event, row) => (selectedRun = row.original.run)"
495
+ >
496
+ <template #prompt-cell="{ row }">
497
+ <span class="text-toned">{{ row.original.run.promptLabel }}</span>
498
+ </template>
499
+ <template #model-cell="{ row }">
500
+ <span class="font-mono text-2xs text-muted">{{ row.original.run.model }}</span>
501
+ </template>
502
+ <template #fixture-cell="{ row }">
503
+ <span class="text-muted">{{ row.original.fixtureName }}</span>
504
+ </template>
505
+ <template #score-cell="{ row }">
506
+ <span
507
+ v-if="row.original.grade"
508
+ :class="scoreColor(row.original.grade.weightedTotal)"
509
+ class="font-semibold"
488
510
  >
489
- <td class="py-1 pe-2 text-toned">{{ run.promptLabel }}</td>
490
- <td class="py-1 pe-2 font-mono text-2xs text-muted">
491
- {{ run.model }}
492
- </td>
493
- <td class="py-1 pe-2 text-muted">{{ fixtureName }}</td>
494
- <td class="py-1 pe-2">
495
- <span
496
- v-if="grade"
497
- :class="scoreColor(grade.weightedTotal)"
498
- class="font-semibold"
499
- >
500
- {{ grade.weightedTotal.toFixed(2) }}
501
- </span>
502
- <span v-else-if="run.status === 'failed'" class="text-app-error-400">{{
503
- t('sandbox.results.failed')
504
- }}</span>
505
- <span v-else class="text-app-600">—</span>
506
- </td>
507
- <td class="py-1">
508
- <span
509
- v-if="grade?.objective"
510
- :class="
511
- grade.objective.pass ? 'text-app-success-400' : 'text-app-warning-400'
512
- "
513
- >
514
- {{ grade.objective.caught }}/{{ grade.objective.total }}
515
- </span>
516
- <span v-else class="text-app-600">—</span>
517
- </td>
518
- </tr>
519
- </tbody>
520
- </table>
511
+ {{ row.original.grade.weightedTotal.toFixed(2) }}
512
+ </span>
513
+ <span
514
+ v-else-if="row.original.run.status === 'failed'"
515
+ class="text-app-error-400"
516
+ >{{ t('sandbox.results.failed') }}</span
517
+ >
518
+ <span v-else class="text-app-600">&mdash;</span>
519
+ </template>
520
+ <template #objective-cell="{ row }">
521
+ <span
522
+ v-if="row.original.grade?.objective"
523
+ :class="
524
+ row.original.grade.objective.pass
525
+ ? 'text-app-success-400'
526
+ : 'text-app-warning-400'
527
+ "
528
+ >
529
+ {{ row.original.grade.objective.caught }}/{{
530
+ row.original.grade.objective.total
531
+ }}
532
+ </span>
533
+ <span v-else class="text-app-600">&mdash;</span>
534
+ </template>
535
+ <!-- See InitiativeTrackerWindow: the fallback copy is untranslated. -->
536
+ <template #empty />
537
+ </UTable>
521
538
  </div>
522
539
 
523
540
  <!-- selected cell output -->
@@ -550,7 +567,9 @@ async function archive(prompt: SandboxPromptVersion) {
550
567
  {{ t('sandbox.results.past') }}
551
568
  </SectionLabel>
552
569
  <div class="max-h-56 space-y-1 overflow-auto">
553
- <button
570
+ <UButton
571
+ color="neutral"
572
+ variant="ghost"
554
573
  v-for="x in store.experiments"
555
574
  :key="x.id"
556
575
  class="flex w-full items-center justify-between rounded-md border border-default bg-default/40 px-2 py-1.5 text-start text-sm hover:bg-elevated/50"
@@ -558,7 +577,7 @@ async function archive(prompt: SandboxPromptVersion) {
558
577
  >
559
578
  <span class="truncate text-toned">{{ x.name }}</span>
560
579
  <UBadge variant="soft" size="xs">{{ EXPERIMENT_STATUS_LABEL[x.status] }}</UBadge>
561
- </button>
580
+ </UButton>
562
581
  <p v-if="!store.experiments.length" class="text-xs text-dimmed">
563
582
  {{ t('sandbox.results.empty') }}
564
583
  </p>
@@ -595,12 +614,12 @@ async function archive(prompt: SandboxPromptVersion) {
595
614
  <span class="text-2xs text-dimmed">{{ p.agentKind }}</span>
596
615
  </div>
597
616
  <div class="flex items-center gap-1">
598
- <UButton
617
+ <IconButton
599
618
  icon="i-lucide-pencil"
600
619
  color="neutral"
601
620
  variant="ghost"
602
621
  size="xs"
603
- :title="
622
+ :label="
604
623
  p.origin === 'baseline'
605
624
  ? t('sandbox.prompts.forkTitle')
606
625
  : t('sandbox.prompts.editTitle')
@@ -610,14 +629,14 @@ async function archive(prompt: SandboxPromptVersion) {
610
629
  <!-- Deploy: make this version the workspace's live prompt for its agent kind.
611
630
  Offered on a graded candidate and on an older workspace revision (rolling
612
631
  back), but not on the one already live, where it would be a no-op. -->
613
- <UButton
632
+ <IconButton
614
633
  v-if="canPromote(p)"
615
634
  icon="i-lucide-rocket"
616
635
  color="primary"
617
636
  variant="ghost"
618
637
  size="xs"
619
638
  :loading="promoting === p.id"
620
- :title="t('sandbox.prompts.promoteTitle')"
639
+ :label="t('sandbox.prompts.promoteTitle')"
621
640
  @click="promote(p)"
622
641
  />
623
642
  <UButton