@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
@@ -386,9 +386,10 @@ async function mergePr() {
386
386
  <div class="flex items-center gap-2">
387
387
  <!-- Every agent is clickable: it opens the step-detail overlay (timing,
388
388
  model, subtasks + the prose output when there is one). -->
389
- <button
390
- type="button"
391
- class="flex min-w-0 cursor-pointer items-center gap-2 text-start transition hover:text-highlighted"
389
+ <UButton
390
+ color="neutral"
391
+ variant="ghost"
392
+ class="flex min-w-0 cursor-pointer items-center gap-2 p-0 text-start transition hover:bg-transparent hover:text-highlighted"
392
393
  data-testid="run-step-open"
393
394
  :title="
394
395
  stepHasOutput(s)
@@ -416,7 +417,7 @@ async function mergePr() {
416
417
  :name="stepHasOutput(s) ? 'i-lucide-book-open-text' : 'i-lucide-info'"
417
418
  class="h-3.5 w-3.5 shrink-0 text-dimmed"
418
419
  />
419
- </button>
420
+ </UButton>
420
421
  <span
421
422
  v-if="s.subtasks && s.subtasks.total > 0"
422
423
  class="ms-auto font-mono text-3xs tabular-nums text-toned"
@@ -434,18 +434,22 @@ const technicalLabel = computed(() => {
434
434
  </i18n-t>
435
435
  </p>
436
436
  <div class="mt-1.5 flex flex-wrap gap-2">
437
- <button
438
- class="font-medium text-app-warning-100 underline-offset-2 hover:underline"
437
+ <UButton
438
+ color="neutral"
439
+ variant="link"
440
+ class="p-0 text-xs font-medium text-app-warning-100 underline-offset-2 hover:underline"
439
441
  @click="ui.openModelConfig()"
440
442
  >
441
443
  {{ t('inspector.runSettings.editPresets') }}
442
- </button>
443
- <button
444
- class="font-medium text-app-warning-100 underline-offset-2 hover:underline"
444
+ </UButton>
445
+ <UButton
446
+ color="neutral"
447
+ variant="link"
448
+ class="p-0 text-xs font-medium text-app-warning-100 underline-offset-2 hover:underline"
445
449
  @click="ui.openVendorCredentials()"
446
450
  >
447
451
  {{ t('inspector.runSettings.configureVendors') }}
448
- </button>
452
+ </UButton>
449
453
  </div>
450
454
  </div>
451
455
  </div>
@@ -47,8 +47,10 @@ const fields = computed(() =>
47
47
  })),
48
48
  )
49
49
 
50
- function commit(axis: EstimateAxis, raw: string) {
51
- emit('update', axis, parseAxisThreshold(raw))
50
+ // The control hands back a number (or nothing when cleared); the shared parser still owns the
51
+ // clamping, so the value that leaves here is the same one the text input used to produce.
52
+ function commit(axis: EstimateAxis, raw: number | undefined) {
53
+ emit('update', axis, raw == null ? undefined : parseAxisThreshold(String(raw)))
52
54
  }
53
55
  </script>
54
56
 
@@ -59,15 +61,16 @@ function commit(axis: EstimateAxis, raw: string) {
59
61
  </span>
60
62
  <template v-for="f in fields" :key="f.axis">
61
63
  <label class="text-muted" :title="f.hint">{{ f.label }}</label>
62
- <input
63
- :value="f.value"
64
+ <UInputNumber
65
+ :model-value="f.value"
64
66
  :title="f.hint"
65
- type="number"
66
- min="0"
67
- max="1"
68
- step="0.1"
69
- class="w-14 rounded-sm border border-muted bg-default px-1.5 py-0.5 text-app-100"
70
- @change="commit(f.axis, ($event.target as HTMLInputElement).value)"
67
+ :min="0"
68
+ :max="1"
69
+ :step="0.1"
70
+ :step-snapping="false"
71
+ size="xs"
72
+ class="w-28"
73
+ @update:model-value="commit(f.axis, $event ?? undefined)"
71
74
  />
72
75
  </template>
73
76
  </div>
@@ -121,19 +121,15 @@ const gateFieldValues = computed<DescriptorFieldValues>({
121
121
  >
122
122
  <div class="flex flex-wrap items-center gap-2 text-3xs">
123
123
  <span class="text-dimmed">{{ t('pipeline.gateConfig.approversLabel') }}</span>
124
- <label
124
+ <UCheckbox
125
125
  v-for="role in APPROVER_ROLES"
126
126
  :key="role"
127
- class="flex items-center gap-1 text-muted"
128
- >
129
- <input
130
- type="checkbox"
131
- :checked="config.approvers?.roles?.includes(role) ?? false"
132
- :data-testid="`gate-approver-role-${role}`"
133
- @change="toggleRole(role, ($event.target as HTMLInputElement).checked)"
134
- />
135
- {{ t(ROLE_LABEL_KEYS[role as 'admin' | 'member']) }}
136
- </label>
127
+ size="xs"
128
+ :model-value="config.approvers?.roles?.includes(role) ?? false"
129
+ :label="t(ROLE_LABEL_KEYS[role as 'admin' | 'member'])"
130
+ :data-testid="`gate-approver-role-${role}`"
131
+ @update:model-value="toggleRole(role, $event === true)"
132
+ />
137
133
  </div>
138
134
 
139
135
  <div class="flex flex-wrap items-center gap-2 text-3xs">
@@ -155,15 +151,15 @@ const gateFieldValues = computed<DescriptorFieldValues>({
155
151
  <label class="text-dimmed" :title="t('pipeline.gateConfig.requiredApprovalsHint')">
156
152
  {{ t('pipeline.gateConfig.requiredApprovalsLabel') }}
157
153
  </label>
158
- <input
159
- :value="requiredApprovals"
160
- type="number"
161
- min="1"
154
+ <UInputNumber
155
+ :model-value="Number(requiredApprovals)"
156
+ :min="1"
162
157
  :max="MAX_GATE_APPROVALS"
163
- step="1"
164
- class="w-14 rounded-sm border border-muted bg-default px-1.5 py-0.5 text-app-100"
158
+ :step="1"
159
+ size="xs"
160
+ class="w-24"
165
161
  data-testid="gate-required-approvals"
166
- @change="setRequiredApprovals(($event.target as HTMLInputElement).value)"
162
+ @update:model-value="setRequiredApprovals(String($event ?? ''))"
167
163
  />
168
164
  <span class="text-dimmed">{{ t('pipeline.gateConfig.requiredApprovalsHint') }}</span>
169
165
  </div>
@@ -29,6 +29,7 @@ import {
29
29
  } from '~/utils/catalog'
30
30
  import type { ConsensusStrategy } from '~/types/consensus'
31
31
  import SectionLabel from '~/components/common/SectionLabel.vue'
32
+ import IconButton from '~/components/common/IconButton.vue'
32
33
 
33
34
  type DraftUnit = { index: number; kind: AgentKind; companionIndex: number | null }
34
35
 
@@ -491,14 +492,24 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
491
492
  class="gap-1"
492
493
  >
493
494
  {{ l }}
494
- <button type="button" class="hover:text-app-error-400" @click="removeLabel(l)">
495
- <UIcon name="i-lucide-x" class="h-3 w-3" />
496
- </button>
495
+ <IconButton
496
+ color="neutral"
497
+ variant="ghost"
498
+ size="xs"
499
+ icon="i-lucide-x"
500
+ :label="t('pipeline.builder.removeLabel', { label: l })"
501
+ :ui="{
502
+ base: 'p-0 hover:bg-transparent hover:text-app-error-400',
503
+ leadingIcon: 'h-3 w-3',
504
+ }"
505
+ @click="removeLabel(l)"
506
+ />
497
507
  </UBadge>
498
- <input
508
+ <UInput
499
509
  v-model="newLabel"
510
+ size="xs"
511
+ class="w-20 focus-within:w-28"
500
512
  :placeholder="t('pipeline.builder.labelPlaceholder')"
501
- class="w-20 rounded-sm border border-muted bg-default px-1.5 py-0.5 text-2xs text-default focus:w-28"
502
513
  @keydown.enter.prevent="addLabel"
503
514
  @blur="addLabel"
504
515
  />
@@ -545,7 +556,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
545
556
  </span>
546
557
  <div class="flex shrink-0 items-center">
547
558
  <!-- Companion toggle: attach/detach the dependent reviewer for this producer. -->
548
- <UButton
559
+ <IconButton
549
560
  v-if="companionForProducer(unit.kind)"
550
561
  :icon="
551
562
  unit.companionIndex !== null ? 'i-lucide-scan-eye' : 'i-lucide-scan-search'
@@ -553,7 +564,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
553
564
  :color="unit.companionIndex !== null ? 'secondary' : 'neutral'"
554
565
  variant="ghost"
555
566
  size="xs"
556
- :title="
567
+ :label="
557
568
  unit.companionIndex !== null
558
569
  ? t('pipeline.builder.companionRemove', {
559
570
  companion: companionLabel(unit.kind),
@@ -565,7 +576,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
565
576
  @click="pipelines.toggleCompanion(unit.index)"
566
577
  />
567
578
  <!-- Enable/disable: keep the step in the pipeline but skip it at run. -->
568
- <UButton
579
+ <IconButton
569
580
  :icon="
570
581
  pipelines.draftEnabled[unit.index] === false
571
582
  ? 'i-lucide-eye-off'
@@ -574,7 +585,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
574
585
  :color="pipelines.draftEnabled[unit.index] === false ? 'neutral' : 'primary'"
575
586
  variant="ghost"
576
587
  size="xs"
577
- :title="
588
+ :label="
578
589
  pipelines.draftEnabled[unit.index] === false
579
590
  ? t('pipeline.builder.enableTooltip')
580
591
  : t('pipeline.builder.disableTooltip')
@@ -592,13 +603,13 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
592
603
  so the engine holds it to the same gatability rule as an estimate gate
593
604
  (`assertValidRunConditions`) — without this the builder invited a condition on
594
605
  `merger` and answered the save with a 422. -->
595
- <UButton
606
+ <IconButton
596
607
  v-if="mayCarrySkipAxis(unit.kind)"
597
608
  :icon="CONDITION_ICONS[pipelines.draftStepCondition(unit.index) ?? 'always']"
598
609
  :color="pipelines.draftStepCondition(unit.index) ? 'info' : 'neutral'"
599
610
  variant="ghost"
600
611
  size="xs"
601
- :title="
612
+ :label="
602
613
  t(
603
614
  `pipeline.builder.condition.${pipelines.draftStepCondition(unit.index) ?? 'always'}`,
604
615
  )
@@ -608,14 +619,14 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
608
619
  />
609
620
  <!-- Approval gate: pause after this step so a human reviews (and
610
621
  can edit) its proposal before the next step runs. -->
611
- <UButton
622
+ <IconButton
612
623
  :icon="
613
624
  pipelines.draftGates[unit.index] ? 'i-lucide-shield-check' : 'i-lucide-shield'
614
625
  "
615
626
  :color="pipelines.draftGates[unit.index] ? 'warning' : 'neutral'"
616
627
  variant="ghost"
617
628
  size="xs"
618
- :title="
629
+ :label="
619
630
  pipelines.draftGates[unit.index]
620
631
  ? t('pipeline.builder.approvalRemoveTooltip')
621
632
  : t('pipeline.builder.approvalAddTooltip')
@@ -625,7 +636,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
625
636
  />
626
637
  <!-- Consensus: run this step through the multi-model mechanism (eligible
627
638
  kinds only — architect/analysis/task-estimator). -->
628
- <UButton
639
+ <IconButton
629
640
  v-if="isConsensusEligibleKind(unit.kind)"
630
641
  :icon="
631
642
  pipelines.draftConsensus[unit.index]?.enabled
@@ -635,7 +646,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
635
646
  :color="pipelines.draftConsensus[unit.index]?.enabled ? 'success' : 'neutral'"
636
647
  variant="ghost"
637
648
  size="xs"
638
- :title="
649
+ :label="
639
650
  pipelines.draftConsensus[unit.index]?.enabled
640
651
  ? t('pipeline.builder.consensusRevertTooltip')
641
652
  : t('pipeline.builder.consensusEnableTooltip')
@@ -644,7 +655,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
644
655
  />
645
656
  <!-- Follow-up companion: the future-looking Coder surfaces loose ends /
646
657
  side-tasks / questions mid-run (coder steps only). Enabled by default. -->
647
- <UButton
658
+ <IconButton
648
659
  v-if="unit.kind === 'coder'"
649
660
  :icon="
650
661
  pipelines.draftFollowUps[unit.index] === false
@@ -656,7 +667,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
656
667
  "
657
668
  variant="ghost"
658
669
  size="xs"
659
- :title="
670
+ :label="
660
671
  pipelines.draftFollowUps[unit.index] === false
661
672
  ? t('pipeline.builder.followUpEnableTooltip')
662
673
  : t('pipeline.builder.followUpDisableTooltip')
@@ -666,7 +677,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
666
677
  <!-- Test quality-control companion: audits the Tester's report for coverage
667
678
  before the greenlight/fixer decision and loops the Tester on gaps (Tester
668
679
  steps only). Enabled by default. -->
669
- <UButton
680
+ <IconButton
670
681
  v-if="isTesterKind(unit.kind)"
671
682
  :icon="
672
683
  pipelines.draftTesterQuality[unit.index]?.enabled === false
@@ -680,7 +691,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
680
691
  "
681
692
  variant="ghost"
682
693
  size="xs"
683
- :title="
694
+ :label="
684
695
  pipelines.draftTesterQuality[unit.index]?.enabled === false
685
696
  ? t('pipeline.builder.testerQualityEnableTooltip')
686
697
  : t('pipeline.builder.testerQualityDisableTooltip')
@@ -690,7 +701,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
690
701
  <!-- Auto-recommendation: the requirements reviewer pre-answers findings it
691
702
  judges answerable from universal practice / provided context, offering them
692
703
  as editable default answers (requirements-review steps only). On by default. -->
693
- <UButton
704
+ <IconButton
694
705
  v-if="unit.kind === 'requirements-review'"
695
706
  :icon="
696
707
  pipelines.draftAutoRecommendEnabled(unit.index)
@@ -702,7 +713,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
702
713
  "
703
714
  variant="ghost"
704
715
  size="xs"
705
- :title="
716
+ :label="
706
717
  pipelines.draftAutoRecommendEnabled(unit.index)
707
718
  ? t('pipeline.builder.autoRecommendDisableTooltip')
708
719
  : t('pipeline.builder.autoRecommendEnableTooltip')
@@ -713,7 +724,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
713
724
  preview a reviewer pokes at after the PR is open. Off by default, and the
714
725
  save boundary refuses a Deployer that neither reclaims nor declares this,
715
726
  so the tick is how an unreclaimed environment says it is deliberate. -->
716
- <UButton
727
+ <IconButton
717
728
  v-if="showRetainEnvironmentToggle(unit.kind, unit.index)"
718
729
  :icon="
719
730
  pipelines.draftRetainEnvironment(unit.index)
@@ -723,7 +734,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
723
734
  :color="pipelines.draftRetainEnvironment(unit.index) ? 'warning' : 'neutral'"
724
735
  variant="ghost"
725
736
  size="xs"
726
- :title="
737
+ :label="
727
738
  pipelines.draftRetainEnvironment(unit.index)
728
739
  ? t('pipeline.builder.retainEnvironmentClearTooltip')
729
740
  : t('pipeline.builder.retainEnvironmentSetTooltip')
@@ -732,43 +743,43 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
732
743
  />
733
744
  <!-- System prompt: replace what this agent kind ships with, for every run in
734
745
  this workspace, with the full revision history to switch back through. -->
735
- <UButton
746
+ <IconButton
736
747
  v-if="showPromptEditor(unit.kind)"
737
748
  icon="i-lucide-file-pen-line"
738
749
  :color="agentPrompts.isCustomized(unit.kind) ? 'warning' : 'neutral'"
739
750
  variant="ghost"
740
751
  size="xs"
741
- :title="
752
+ :label="
742
753
  agentPrompts.isCustomized(unit.kind)
743
754
  ? t('pipeline.builder.promptEditedTooltip')
744
755
  : t('pipeline.builder.promptEditTooltip')
745
756
  "
746
757
  @click="promptEditorKind = unit.kind"
747
758
  />
748
- <UButton
759
+ <IconButton
749
760
  icon="i-lucide-chevron-up"
750
761
  color="neutral"
751
762
  variant="ghost"
752
763
  size="xs"
753
- :title="t('pipeline.builder.moveUp')"
764
+ :label="t('pipeline.builder.moveUp')"
754
765
  :disabled="vi === 0"
755
766
  @click="pipelines.moveUnit(vi, vi - 1)"
756
767
  />
757
- <UButton
768
+ <IconButton
758
769
  icon="i-lucide-chevron-down"
759
770
  color="neutral"
760
771
  variant="ghost"
761
772
  size="xs"
762
- :title="t('pipeline.builder.moveDown')"
773
+ :label="t('pipeline.builder.moveDown')"
763
774
  :disabled="vi === pipelines.units.length - 1"
764
775
  @click="pipelines.moveUnit(vi, vi + 1)"
765
776
  />
766
- <UButton
777
+ <IconButton
767
778
  icon="i-lucide-x"
768
779
  color="error"
769
780
  variant="ghost"
770
781
  size="xs"
771
- :title="t('pipeline.builder.removeStep')"
782
+ :label="t('pipeline.builder.removeStep')"
772
783
  @click="removeUnit(unit)"
773
784
  />
774
785
  </div>
@@ -873,7 +884,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
873
884
  </span>
874
885
  <!-- A companion is an agent kind with a prompt of its own, and this row is its
875
886
  only route to it — so the affordance belongs here too, not only on producers. -->
876
- <UButton
887
+ <IconButton
877
888
  v-if="showPromptEditor(pipelines.draft[unit.companionIndex]!)"
878
889
  icon="i-lucide-file-pen-line"
879
890
  :color="
@@ -883,7 +894,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
883
894
  "
884
895
  variant="ghost"
885
896
  size="xs"
886
- :title="
897
+ :label="
887
898
  agentPrompts.isCustomized(pipelines.draft[unit.companionIndex]!)
888
899
  ? t('pipeline.builder.promptEditedTooltip')
889
900
  : t('pipeline.builder.promptEditTooltip')
@@ -939,22 +950,23 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
939
950
  {{ t('pipeline.builder.consensusGroupsHint') }}
940
951
  </p>
941
952
  <div class="flex flex-wrap gap-1">
942
- <button
953
+ <UButton
954
+ color="neutral"
955
+ variant="ghost"
943
956
  v-for="group in consensusGroups.groups"
944
957
  :key="group.id"
945
- type="button"
946
958
  class="rounded-sm border px-1.5 py-0.5 text-2xs"
947
959
  :class="
948
960
  isGroupSelected(unit.index, group.id)
949
- ? 'border-app-success-600 bg-app-success-900/40 text-app-success-200'
950
- : 'border-muted bg-default text-muted hover:text-default'
961
+ ? 'border-app-success-600 bg-app-success-900/40 hover:bg-app-success-900/40 focus-visible:bg-app-success-900/40 disabled:bg-app-success-900/40 text-app-success-200'
962
+ : 'border-muted bg-default hover:bg-default focus-visible:bg-default disabled:bg-default text-muted hover:text-default'
951
963
  "
952
964
  :title="group.description"
953
965
  @click="pipelines.toggleDraftConsensusGroup(unit.index, group.id)"
954
966
  >
955
967
  {{ group.name }}
956
968
  <span class="ms-1 text-dimmed">{{ groupBarLabel(group.id) }}</span>
957
- </button>
969
+ </UButton>
958
970
  </div>
959
971
  </div>
960
972
 
@@ -965,27 +977,24 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
965
977
  <template v-else>
966
978
  <div class="flex items-center gap-2">
967
979
  <label class="text-muted">{{ t('pipeline.builder.strategy') }}</label>
968
- <select
980
+ <USelect
969
981
  v-model="pipelines.draftConsensus[unit.index]!.strategy"
970
- class="rounded-sm border border-muted bg-default px-1.5 py-0.5 text-app-100"
971
- >
972
- <option v-for="s in CONSENSUS_STRATEGIES" :key="s.value" :value="s.value">
973
- {{ s.label }}
974
- </option>
975
- </select>
982
+ :items="CONSENSUS_STRATEGIES"
983
+ size="xs"
984
+ />
976
985
  <label
977
986
  v-if="pipelines.draftConsensus[unit.index]!.strategy === 'debate'"
978
987
  class="ms-2 text-muted"
979
988
  >{{ t('pipeline.builder.rounds') }}</label
980
989
  >
981
- <input
990
+ <UInputNumber
982
991
  v-if="pipelines.draftConsensus[unit.index]!.strategy === 'debate'"
983
- v-model.number="pipelines.draftConsensus[unit.index]!.rounds"
984
- type="number"
985
- min="1"
986
- max="5"
992
+ v-model.optional="pipelines.draftConsensus[unit.index]!.rounds"
993
+ :min="1"
994
+ :max="5"
987
995
  placeholder="2"
988
- class="w-12 rounded-sm border border-muted bg-default px-1.5 py-0.5 text-app-100"
996
+ size="xs"
997
+ class="w-24"
989
998
  />
990
999
  </div>
991
1000
 
@@ -996,23 +1005,25 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
996
1005
  :key="p.id"
997
1006
  class="flex items-center gap-1.5"
998
1007
  >
999
- <input
1008
+ <UInput
1000
1009
  v-model="p.role"
1010
+ size="xs"
1011
+ class="w-28"
1001
1012
  :placeholder="t('pipeline.builder.rolePlaceholder')"
1002
- class="w-28 rounded-sm border border-muted bg-default px-1.5 py-0.5 text-app-100"
1003
1013
  />
1004
- <input
1014
+ <UInput
1005
1015
  v-model="p.modelId"
1016
+ size="xs"
1017
+ class="flex-1"
1006
1018
  :placeholder="t('pipeline.builder.modelIdPlaceholder')"
1007
- class="flex-1 rounded-sm border border-muted bg-default px-1.5 py-0.5 text-toned"
1008
1019
  />
1009
- <UButton
1020
+ <IconButton
1010
1021
  icon="i-lucide-x"
1011
1022
  color="error"
1012
1023
  variant="ghost"
1013
1024
  size="xs"
1014
1025
  :disabled="pipelines.draftConsensus[unit.index]!.participants.length <= 2"
1015
- :title="t('pipeline.builder.removeParticipant')"
1026
+ :label="t('pipeline.builder.removeParticipant')"
1016
1027
  @click="removeParticipant(unit.index, pIdx)"
1017
1028
  />
1018
1029
  </div>
@@ -1171,9 +1182,10 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
1171
1182
  )
1172
1183
  }}
1173
1184
  </span>
1174
- <button
1175
- type="button"
1176
- class="underline underline-offset-2 hover:text-toned"
1185
+ <UButton
1186
+ color="neutral"
1187
+ variant="link"
1188
+ class="p-0 text-3xs underline underline-offset-2 hover:text-toned"
1177
1189
  data-testid="pipeline-library-purpose-toggle"
1178
1190
  @click="browseEveryPurpose = !browseEveryPurpose"
1179
1191
  >
@@ -1182,7 +1194,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
1182
1194
  ? t('pipeline.builder.narrowToDraftPurpose')
1183
1195
  : t('pipeline.builder.listEveryPurpose')
1184
1196
  }}
1185
- </button>
1197
+ </UButton>
1186
1198
  </div>
1187
1199
 
1188
1200
  <ul class="flex-1 space-y-1.5 pe-1 lg:min-h-0 lg:overflow-y-auto">
@@ -1193,9 +1205,10 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
1193
1205
  :class="{ 'opacity-60': p.archived }"
1194
1206
  >
1195
1207
  <div class="flex items-center gap-2 px-2 py-1.5">
1196
- <button
1197
- type="button"
1198
- class="flex min-w-0 flex-1 items-center gap-2 text-start"
1208
+ <UButton
1209
+ color="neutral"
1210
+ variant="ghost"
1211
+ class="flex min-w-0 flex-1 items-center gap-2 p-0 text-start hover:bg-transparent"
1199
1212
  @click="toggleSaved(p.id)"
1200
1213
  >
1201
1214
  <UIcon
@@ -1259,7 +1272,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
1259
1272
  )
1260
1273
  }}
1261
1274
  </span>
1262
- </button>
1275
+ </UButton>
1263
1276
  <div
1264
1277
  class="flex shrink-0 items-center opacity-0 transition group-hover:opacity-100"
1265
1278
  >
@@ -1270,24 +1283,24 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
1270
1283
  ARCHIVING a row that still holds one: a hidden row never holds a default,
1271
1284
  so there is never one here to release. -->
1272
1285
  <template v-if="uiMode.isAdvanced && !p.archived && !p.internal">
1273
- <UButton
1286
+ <IconButton
1274
1287
  :icon="p.isDefault ? 'i-lucide-star' : 'i-lucide-star-off'"
1275
1288
  :color="p.isDefault ? 'primary' : 'neutral'"
1276
1289
  variant="ghost"
1277
1290
  size="xs"
1278
- :title="
1291
+ :label="
1279
1292
  p.isDefault
1280
1293
  ? t('pipeline.builder.scopeDefault.releaseInteractive')
1281
1294
  : t('pipeline.builder.scopeDefault.claimInteractive')
1282
1295
  "
1283
1296
  @click="toggleDefault(p, 'interactive')"
1284
1297
  />
1285
- <UButton
1298
+ <IconButton
1286
1299
  :icon="p.isUnattendedDefault ? 'i-lucide-bot' : 'i-lucide-bot-off'"
1287
1300
  :color="p.isUnattendedDefault ? 'info' : 'neutral'"
1288
1301
  variant="ghost"
1289
1302
  size="xs"
1290
- :title="
1303
+ :label="
1291
1304
  p.isUnattendedDefault
1292
1305
  ? t('pipeline.builder.scopeDefault.releaseUnattended')
1293
1306
  : t('pipeline.builder.scopeDefault.claimUnattended')
@@ -1297,47 +1310,47 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
1297
1310
  </template>
1298
1311
  <!-- Archive/unarchive: organize the library without deleting. Works on
1299
1312
  built-ins too (view metadata, not structure). -->
1300
- <UButton
1313
+ <IconButton
1301
1314
  :icon="p.archived ? 'i-lucide-archive-restore' : 'i-lucide-archive'"
1302
1315
  color="neutral"
1303
1316
  variant="ghost"
1304
1317
  size="xs"
1305
- :title="
1318
+ :label="
1306
1319
  p.archived ? t('pipeline.builder.unarchive') : t('pipeline.builder.archive')
1307
1320
  "
1308
1321
  @click="toggleArchive(p)"
1309
1322
  />
1310
1323
  <!-- Clone is available on every pipeline — it's how a read-only
1311
1324
  built-in template becomes an editable copy. -->
1312
- <UButton
1325
+ <IconButton
1313
1326
  icon="i-lucide-copy"
1314
1327
  color="neutral"
1315
1328
  variant="ghost"
1316
1329
  size="xs"
1317
- :title="
1330
+ :label="
1318
1331
  p.builtin ? t('pipeline.builder.cloneDefault') : t('pipeline.builder.clone')
1319
1332
  "
1320
1333
  @click="clone(p)"
1321
1334
  />
1322
1335
  <!-- Built-in templates are read-only; only custom pipelines edit in place. -->
1323
- <UButton
1336
+ <IconButton
1324
1337
  v-if="!p.builtin"
1325
1338
  icon="i-lucide-pencil"
1326
1339
  color="neutral"
1327
1340
  variant="ghost"
1328
1341
  size="xs"
1329
- :title="t('pipeline.builder.edit')"
1342
+ :label="t('pipeline.builder.edit')"
1330
1343
  @click="edit(p)"
1331
1344
  />
1332
1345
  <!-- Built-in templates are read-only — they can be cloned but not
1333
1346
  deleted (the backend rejects it too); only custom ones delete. -->
1334
- <UButton
1347
+ <IconButton
1335
1348
  v-if="!p.builtin"
1336
1349
  icon="i-lucide-trash-2"
1337
1350
  color="neutral"
1338
1351
  variant="ghost"
1339
1352
  size="xs"
1340
- :title="t('pipeline.builder.delete')"
1353
+ :label="t('pipeline.builder.delete')"
1341
1354
  @click="removePipeline(p)"
1342
1355
  />
1343
1356
  </div>