@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
@@ -237,40 +237,43 @@ async function remove() {
237
237
  </p>
238
238
  </div>
239
239
 
240
- <details class="rounded-md border border-muted/70 p-2">
241
- <summary class="cursor-pointer text-2xs text-muted">
242
- {{ t('settings.infrastructure.cloudflare.advanced') }}
243
- </summary>
244
- <div class="mt-2 space-y-2">
245
- <div class="space-y-1">
246
- <label class="text-2xs text-muted">{{
247
- t('settings.infrastructure.cloudflare.workerTemplate')
248
- }}</label>
240
+ <UCollapsible class="rounded-md border border-muted/70 p-2">
241
+ <template #default="{ open }">
242
+ <UButton
243
+ variant="link"
244
+ color="neutral"
245
+ size="xs"
246
+ :icon="open ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
247
+ :label="t('settings.infrastructure.cloudflare.advanced')"
248
+ :ui="{ base: 'w-full justify-start gap-1 p-0 text-2xs text-muted' }"
249
+ />
250
+ </template>
251
+ <template #content>
252
+ <div class="mt-2 space-y-2">
249
253
  <!-- The placeholder is a FORMAT EXAMPLE containing vue-i18n metacharacters, so it
250
254
  stays inline rather than becoming a catalog key. -->
251
- <UInput
252
- v-model="workerNameTemplate"
255
+ <UFormField size="xs" :label="t('settings.infrastructure.cloudflare.workerTemplate')">
256
+ <UInput
257
+ v-model="workerNameTemplate"
258
+ size="xs"
259
+ placeholder="cat-factory-pr-{{pullNumber}}"
260
+ />
261
+ </UFormField>
262
+ <UFormField
253
263
  size="xs"
254
- placeholder="cat-factory-pr-{{pullNumber}}"
255
- />
264
+ :label="t('settings.infrastructure.cloudflare.environmentTemplate')"
265
+ >
266
+ <UInput v-model="environmentNameTemplate" size="xs" placeholder="pr-{{pullNumber}}" />
267
+ </UFormField>
268
+ <p class="text-2xs text-dimmed">
269
+ {{ t('settings.infrastructure.cloudflare.templateHint') }}
270
+ </p>
271
+ <UFormField size="xs" :label="t('settings.infrastructure.cloudflare.apiBaseUrl')">
272
+ <UInput v-model="apiBaseUrl" size="xs" placeholder="https://api.github.com" />
273
+ </UFormField>
256
274
  </div>
257
- <div class="space-y-1">
258
- <label class="text-2xs text-muted">{{
259
- t('settings.infrastructure.cloudflare.environmentTemplate')
260
- }}</label>
261
- <UInput v-model="environmentNameTemplate" size="xs" placeholder="pr-{{pullNumber}}" />
262
- </div>
263
- <p class="text-2xs text-dimmed">
264
- {{ t('settings.infrastructure.cloudflare.templateHint') }}
265
- </p>
266
- <div class="space-y-1">
267
- <label class="text-2xs text-muted">{{
268
- t('settings.infrastructure.cloudflare.apiBaseUrl')
269
- }}</label>
270
- <UInput v-model="apiBaseUrl" size="xs" placeholder="https://api.github.com" />
271
- </div>
272
- </div>
273
- </details>
275
+ </template>
276
+ </UCollapsible>
274
277
 
275
278
  <div class="flex items-center gap-2">
276
279
  <UButton size="xs" :disabled="!canSave || busy" @click="save">
@@ -14,6 +14,7 @@ import type { ConsensusGroup, ConsensusStrategy } from '~/types/consensus'
14
14
  import { isSelectable } from '~/stores/models'
15
15
  import { uid } from '~/utils/catalog'
16
16
  import SectionLabel from '~/components/common/SectionLabel.vue'
17
+ import IconButton from '~/components/common/IconButton.vue'
17
18
 
18
19
  const { t } = useI18n()
19
20
  const groups = useConsensusGroupsStore()
@@ -116,6 +117,17 @@ const selectableModelIds = computed(() => {
116
117
  .map((m) => ({ id: m.id, label: m.label }))
117
118
  })
118
119
 
120
+ // "Inherit the step's model" is a NAMED sentinel in the picker, never an empty string and never
121
+ // `undefined`: a Select reserves both for "nothing selected", so an item carrying one either
122
+ // throws (empty string) or renders the placeholder over its own label (undefined). One item list
123
+ // serves both pickers; each binding below maps the sentinel to what its field holds on the wire,
124
+ // which is `undefined` for a participant and an empty string for the synthesizer.
125
+ const STEP_MODEL = '__step__'
126
+ const modelItems = computed(() => [
127
+ { label: t('settings.consensusGroups.editor.stepModel'), value: STEP_MODEL },
128
+ ...selectableModelIds.value.map((m) => ({ label: m.label, value: m.id })),
129
+ ])
130
+
119
131
  /**
120
132
  * A threshold field's value as the contract wants it, or undefined when the author left it blank.
121
133
  *
@@ -246,21 +258,21 @@ async function remove(group: ConsensusGroup) {
246
258
  <span class="truncate text-sm font-semibold text-app-100">{{ g.name }}</span>
247
259
  <UBadge color="neutral" variant="subtle" size="xs">{{ barLabel(g) }}</UBadge>
248
260
  <div class="ms-auto flex items-center gap-1">
249
- <UButton
261
+ <IconButton
250
262
  size="xs"
251
263
  variant="ghost"
252
264
  color="neutral"
253
265
  icon="i-lucide-pencil"
254
- :title="t('settings.consensusGroups.list.editTitle')"
266
+ :label="t('settings.consensusGroups.list.editTitle')"
255
267
  @click="startEdit(g)"
256
268
  />
257
- <UButton
269
+ <IconButton
258
270
  size="xs"
259
271
  variant="ghost"
260
272
  color="error"
261
273
  icon="i-lucide-trash-2"
262
274
  :loading="busy"
263
- :title="t('settings.consensusGroups.list.deleteTitle')"
275
+ :label="t('settings.consensusGroups.list.deleteTitle')"
264
276
  @click="remove(g)"
265
277
  />
266
278
  </div>
@@ -302,12 +314,7 @@ async function remove(group: ConsensusGroup) {
302
314
  <SectionLabel as="label" class="mb-1 block">
303
315
  {{ t('settings.consensusGroups.editor.strategyLabel') }}
304
316
  </SectionLabel>
305
- <select
306
- v-model="editor.strategy"
307
- class="w-full rounded-sm border border-muted bg-default px-2 py-1.5 text-sm text-app-100"
308
- >
309
- <option v-for="s in STRATEGIES" :key="s.value" :value="s.value">{{ s.label }}</option>
310
- </select>
317
+ <USelect v-model="editor.strategy" :items="STRATEGIES" size="sm" class="w-full" />
311
318
  </div>
312
319
  </div>
313
320
 
@@ -358,20 +365,20 @@ async function remove(group: ConsensusGroup) {
358
365
  class="min-w-40 flex-1"
359
366
  :placeholder="t('settings.consensusGroups.editor.framingPlaceholder')"
360
367
  />
361
- <select
362
- v-model="p.modelId"
363
- class="w-44 rounded-sm border border-muted bg-default px-1.5 py-1 text-xs text-toned"
364
- >
365
- <option :value="undefined">{{ t('settings.consensusGroups.editor.stepModel') }}</option>
366
- <option v-for="m in selectableModelIds" :key="m.id" :value="m.id">{{ m.label }}</option>
367
- </select>
368
- <UButton
368
+ <USelect
369
+ :model-value="p.modelId || STEP_MODEL"
370
+ :items="modelItems"
371
+ size="xs"
372
+ class="w-44"
373
+ @update:model-value="p.modelId = $event === STEP_MODEL ? undefined : String($event)"
374
+ />
375
+ <IconButton
369
376
  icon="i-lucide-x"
370
377
  color="error"
371
378
  variant="ghost"
372
379
  size="xs"
373
380
  :disabled="editor.participants.length <= 2"
374
- :title="t('pipeline.builder.removeParticipant')"
381
+ :label="t('pipeline.builder.removeParticipant')"
375
382
  @click="removeParticipant(index)"
376
383
  />
377
384
  </div>
@@ -382,13 +389,15 @@ async function remove(group: ConsensusGroup) {
382
389
  <SectionLabel as="label" class="mb-1 block">
383
390
  {{ t('settings.consensusGroups.editor.synthesizerLabel') }}
384
391
  </SectionLabel>
385
- <select
386
- v-model="editor.synthesizerModelId"
387
- class="w-full rounded-sm border border-muted bg-default px-2 py-1.5 text-sm text-app-100"
388
- >
389
- <option value="">{{ t('settings.consensusGroups.editor.stepModel') }}</option>
390
- <option v-for="m in selectableModelIds" :key="m.id" :value="m.id">{{ m.label }}</option>
391
- </select>
392
+ <USelect
393
+ :model-value="editor.synthesizerModelId || STEP_MODEL"
394
+ :items="modelItems"
395
+ size="sm"
396
+ class="w-full"
397
+ @update:model-value="
398
+ editor.synthesizerModelId = $event === STEP_MODEL ? '' : String($event)
399
+ "
400
+ />
392
401
  </div>
393
402
  <div v-if="editor.strategy === 'debate'">
394
403
  <SectionLabel as="label" class="mb-1 block">
@@ -400,10 +409,11 @@ async function remove(group: ConsensusGroup) {
400
409
 
401
410
  <!-- the estimate bar -->
402
411
  <div class="space-y-2 rounded-lg border border-default bg-app-950/40 p-3">
403
- <label class="flex items-center gap-2 text-xs text-toned">
404
- <input v-model="editor.gated" type="checkbox" class="accent-app-success-500" />
405
- {{ t('settings.consensusGroups.editor.gatedLabel') }}
406
- </label>
412
+ <UCheckbox
413
+ v-model="editor.gated"
414
+ size="xs"
415
+ :label="t('settings.consensusGroups.editor.gatedLabel')"
416
+ />
407
417
  <p class="text-2xs text-dimmed">
408
418
  {{ t('settings.consensusGroups.editor.gatedHint') }}
409
419
  </p>
@@ -514,21 +514,24 @@ function toastRemoved() {
514
514
 
515
515
  <!-- Local mode: a personal override for THIS machine, layered over the workspace handler. -->
516
516
  <div v-if="userOverridesOn" class="border-t border-default pt-2">
517
- <SectionLabel
518
- as="button"
519
- type="button"
520
- class="flex w-full items-center gap-1.5 text-start hover:text-default"
517
+ <UButton
518
+ color="neutral"
519
+ variant="link"
520
+ class="group w-full p-0"
521
+ :aria-expanded="showKubeOverride"
521
522
  @click="showKubeOverride = !showKubeOverride"
522
523
  >
523
- <UIcon
524
- :name="showKubeOverride ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
525
- class="h-3.5 w-3.5"
526
- />
527
- {{ t('settings.infrastructure.handler.personalOverride') }}
528
- <UBadge v-if="kubeUserHandler" color="primary" variant="subtle" size="sm">
529
- {{ t('settings.infrastructure.handler.overrideActive') }}
530
- </UBadge>
531
- </SectionLabel>
524
+ <SectionLabel as="span" class="flex w-full items-center gap-1.5 group-hover:text-default">
525
+ <UIcon
526
+ :name="showKubeOverride ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
527
+ class="h-3.5 w-3.5"
528
+ />
529
+ {{ t('settings.infrastructure.handler.personalOverride') }}
530
+ <UBadge v-if="kubeUserHandler" color="primary" variant="subtle" size="sm">
531
+ {{ t('settings.infrastructure.handler.overrideActive') }}
532
+ </UBadge>
533
+ </SectionLabel>
534
+ </UButton>
532
535
  <div v-if="showKubeOverride" class="mt-2 space-y-2">
533
536
  <p class="text-2xs text-dimmed">
534
537
  {{ t('settings.infrastructure.handler.personalOverrideHint') }}
@@ -199,6 +199,16 @@ const items = computed<PickerItem[]>(() => {
199
199
  return out
200
200
  })
201
201
 
202
+ // The same list in the shape URadioGroup reads. `desc` is optional on a PickerItem and an empty
203
+ // string renders an empty description line, so it is dropped rather than passed through blank.
204
+ const backendItems = computed(() =>
205
+ items.value.map((item) => ({
206
+ value: item.id,
207
+ label: item.label,
208
+ ...(item.desc ? { description: item.desc } : {}),
209
+ })),
210
+ )
211
+
202
212
  // The active-line label: prefer the registered connection's concrete kind over the generic
203
213
  // "delegated" label so "Active: Kubernetes cluster" reads truthfully.
204
214
  const activeLabel = computed(() => {
@@ -275,23 +285,13 @@ const labelKey = computed(() =>
275
285
  {{ t('settings.infrastructure.active', { backend: activeLabel }) }}
276
286
  </p>
277
287
 
278
- <div class="space-y-1.5" :data-testid="`${axis}-backend-options`">
279
- <label v-for="item in items" :key="item.id" class="flex cursor-pointer items-start gap-2">
280
- <input
281
- type="radio"
282
- class="mt-1"
283
- :value="item.id"
284
- :checked="item.id === selected"
285
- :disabled="saving"
286
- :data-testid="`${axis}-backend-${item.id}`"
287
- @change="select(item.id)"
288
- />
289
- <span class="min-w-0">
290
- <span class="text-sm text-default">{{ item.label }}</span>
291
- <span v-if="item.desc" class="block text-2xs text-muted">{{ item.desc }}</span>
292
- </span>
293
- </label>
294
- </div>
288
+ <URadioGroup
289
+ :model-value="selected"
290
+ :items="backendItems"
291
+ :disabled="saving"
292
+ :data-testid="`${axis}-backend-options`"
293
+ @update:model-value="select(String($event))"
294
+ />
295
295
 
296
296
  <p v-if="showRegisterHint" class="text-2xs text-app-warning-300/80">
297
297
  {{ t('settings.infrastructure.registerHint') }}
@@ -272,18 +272,28 @@ async function checkSetup(source: TaskSourceKind) {
272
272
  <p v-if="trackerKind === 'github' && !githubAvailable" class="text-2xs text-app-warning-400">
273
273
  <i18n-t keypath="settings.issueTracker.filing.githubHint" tag="span" scope="global">
274
274
  <template #link>
275
- <button class="underline" @click="ui.openGitHub()">
275
+ <UButton
276
+ color="neutral"
277
+ variant="link"
278
+ class="p-0 text-2xs underline"
279
+ @click="ui.openGitHub()"
280
+ >
276
281
  {{ t('settings.issueTracker.filing.githubHintLink') }}
277
- </button>
282
+ </UButton>
278
283
  </template>
279
284
  </i18n-t>
280
285
  </p>
281
286
  <p v-else-if="trackerKind === 'jira' && !jiraConnected" class="text-2xs text-app-warning-400">
282
287
  <i18n-t keypath="settings.issueTracker.filing.jiraHint" tag="span" scope="global">
283
288
  <template #link>
284
- <button class="underline" @click="ui.openTaskConnect('jira')">
289
+ <UButton
290
+ color="neutral"
291
+ variant="link"
292
+ class="p-0 text-2xs underline"
293
+ @click="ui.openTaskConnect('jira')"
294
+ >
285
295
  {{ t('settings.issueTracker.filing.connectLink') }}
286
- </button>
296
+ </UButton>
287
297
  </template>
288
298
  </i18n-t>
289
299
  </p>
@@ -293,9 +303,14 @@ async function checkSetup(source: TaskSourceKind) {
293
303
  >
294
304
  <i18n-t keypath="settings.issueTracker.filing.linearHint" tag="span" scope="global">
295
305
  <template #link>
296
- <button class="underline" @click="ui.openTaskConnect('linear')">
306
+ <UButton
307
+ color="neutral"
308
+ variant="link"
309
+ class="p-0 text-2xs underline"
310
+ @click="ui.openTaskConnect('linear')"
311
+ >
297
312
  {{ t('settings.issueTracker.filing.connectLink') }}
298
- </button>
313
+ </UButton>
299
314
  </template>
300
315
  </i18n-t>
301
316
  </p>
@@ -18,6 +18,7 @@ import {
18
18
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
19
19
  import SecretInput from '~/components/common/SecretInput.vue'
20
20
  import SectionLabel from '~/components/common/SectionLabel.vue'
21
+ import IconButton from '~/components/common/IconButton.vue'
21
22
 
22
23
  const { t } = useI18n()
23
24
  const ui = useUiStore()
@@ -331,13 +332,13 @@ async function remove(p: LocalRunner) {
331
332
  </div>
332
333
  </div>
333
334
  <div class="flex items-center gap-1">
334
- <UButton
335
+ <IconButton
335
336
  icon="i-lucide-pencil"
336
337
  color="neutral"
337
338
  variant="ghost"
338
339
  size="xs"
339
340
  :disabled="busy"
340
- :title="t('settings.localModelEndpoints.edit')"
341
+ :label="t('settings.localModelEndpoints.edit')"
341
342
  @click="selectRunner(e.provider)"
342
343
  />
343
344
  <UButton
@@ -23,6 +23,7 @@ import ConsensusGroupsSection from '~/components/settings/ConsensusGroupsSection
23
23
  import ProviderPreferenceEditor from '~/components/settings/ProviderPreferenceEditor.vue'
24
24
  import { showOverrideField } from '~/utils/uiMode'
25
25
  import SectionLabel from '~/components/common/SectionLabel.vue'
26
+ import IconButton from '~/components/common/IconButton.vue'
26
27
 
27
28
  const { t } = useI18n()
28
29
  const ui = useUiStore()
@@ -331,13 +332,13 @@ async function save() {
331
332
  >
332
333
  {{ t('settings.modelConfiguration.back') }}
333
334
  </UButton>
334
- <UButton
335
+ <IconButton
335
336
  icon="i-lucide-x"
336
337
  color="neutral"
337
338
  variant="ghost"
338
339
  size="sm"
339
340
  :class="editor ? '' : 'ms-auto'"
340
- :title="t('settings.modelConfiguration.closeEsc')"
341
+ :label="t('settings.modelConfiguration.closeEsc')"
341
342
  @click="
342
343
  () => {
343
344
  open = false
@@ -391,32 +392,32 @@ async function save() {
391
392
  {{ t('settings.modelConfiguration.list.default') }}
392
393
  </UBadge>
393
394
  <div class="ms-auto flex items-center gap-1">
394
- <UButton
395
+ <IconButton
395
396
  v-if="!p.isDefault"
396
397
  size="xs"
397
398
  variant="ghost"
398
399
  color="neutral"
399
400
  icon="i-lucide-star"
400
401
  :loading="busy"
401
- :title="t('settings.modelConfiguration.list.setDefaultTitle')"
402
+ :label="t('settings.modelConfiguration.list.setDefaultTitle')"
402
403
  @click="setDefault(p)"
403
404
  />
404
- <UButton
405
+ <IconButton
405
406
  size="xs"
406
407
  variant="ghost"
407
408
  color="neutral"
408
409
  icon="i-lucide-pencil"
409
- :title="t('settings.modelConfiguration.list.editTitle')"
410
+ :label="t('settings.modelConfiguration.list.editTitle')"
410
411
  @click="startEdit(p)"
411
412
  />
412
- <UButton
413
+ <IconButton
413
414
  size="xs"
414
415
  variant="ghost"
415
416
  color="error"
416
417
  icon="i-lucide-trash-2"
417
418
  :disabled="p.isDefault"
418
419
  :loading="busy"
419
- :title="
420
+ :label="
420
421
  p.isDefault
421
422
  ? t('settings.modelConfiguration.list.deleteDisabledTitle')
422
423
  : t('settings.modelConfiguration.list.deleteTitle')
@@ -247,12 +247,13 @@ function manageKeys() {
247
247
  <li>
248
248
  <i18n-t keypath="settings.openRouterCatalog.step1" tag="span" scope="global">
249
249
  <template #link>
250
- <a
251
- href="https://openrouter.ai/keys"
250
+ <ULink
251
+ raw
252
+ to="https://openrouter.ai/keys"
252
253
  target="_blank"
253
254
  rel="noopener noreferrer"
254
255
  class="text-primary underline"
255
- >{{ t('settings.openRouterCatalog.step1Link') }}</a
256
+ >{{ t('settings.openRouterCatalog.step1Link') }}</ULink
256
257
  >
257
258
  </template>
258
259
  </i18n-t>
@@ -27,6 +27,7 @@ import {
27
27
  subscriptionOverridesOrder,
28
28
  } from '~/components/settings/ProviderPreferenceEditor.logic'
29
29
  import SectionLabel from '~/components/common/SectionLabel.vue'
30
+ import IconButton from '~/components/common/IconButton.vue'
30
31
 
31
32
  const props = defineProps<{
32
33
  /** The preset's stored order; empty/absent ⇒ the deployment's default order. */
@@ -154,24 +155,22 @@ function reset() {
154
155
  <p class="truncate text-2xs text-dimmed">{{ t(ROUTE_HINTS[flavor]) }}</p>
155
156
  </div>
156
157
  <div class="flex shrink-0 items-center gap-1">
157
- <UButton
158
+ <IconButton
158
159
  size="xs"
159
160
  variant="ghost"
160
161
  color="neutral"
161
162
  icon="i-lucide-chevron-up"
162
163
  :disabled="index === 0"
163
- :title="t('settings.modelConfiguration.routeOrder.moveUp')"
164
- :aria-label="t('settings.modelConfiguration.routeOrder.moveUp')"
164
+ :label="t('settings.modelConfiguration.routeOrder.moveUp')"
165
165
  @click="move(index, -1)"
166
166
  />
167
- <UButton
167
+ <IconButton
168
168
  size="xs"
169
169
  variant="ghost"
170
170
  color="neutral"
171
171
  icon="i-lucide-chevron-down"
172
172
  :disabled="index === order.length - 1"
173
- :title="t('settings.modelConfiguration.routeOrder.moveDown')"
174
- :aria-label="t('settings.modelConfiguration.routeOrder.moveDown')"
173
+ :label="t('settings.modelConfiguration.routeOrder.moveDown')"
175
174
  @click="move(index, 1)"
176
175
  />
177
176
  </div>
@@ -24,6 +24,7 @@ import { RISK_POLICY_AXES, RISK_POLICY_CEILING_FIELD } from '~/utils/riskPolicy'
24
24
  import MergeClassRulesEditor from '~/components/settings/MergeClassRulesEditor.vue'
25
25
  import MergeRolePolicyEditor from '~/components/settings/MergeRolePolicyEditor.vue'
26
26
  import SectionLabel from '~/components/common/SectionLabel.vue'
27
+ import IconButton from '~/components/common/IconButton.vue'
27
28
 
28
29
  const props = defineProps<{
29
30
  policy: RiskPolicyLibraryEntry
@@ -144,13 +145,13 @@ const deleteBlocked = computed(
144
145
  {{ t('settings.riskPolicy.makeDefault') }}
145
146
  </UButton>
146
147
  </template>
147
- <UButton
148
+ <IconButton
148
149
  color="error"
149
150
  variant="ghost"
150
151
  size="xs"
151
152
  icon="i-lucide-trash-2"
152
153
  :disabled="deleteBlocked || busy?.startsWith(policy.id)"
153
- :title="deleteBlockedReason"
154
+ :label="deleteBlockedReason"
154
155
  @click="emit('remove')"
155
156
  />
156
157
  </div>
@@ -99,20 +99,22 @@ function remove(id: string) {
99
99
  <span class="text-dimmed">
100
100
  {{ t('settings.serviceFragmentDefaults.footer.question') }}
101
101
  </span>
102
- <button
103
- type="button"
104
- class="font-medium text-primary hover:underline"
102
+ <UButton
103
+ color="primary"
104
+ variant="link"
105
+ class="p-0 text-2xs font-medium text-primary hover:underline"
105
106
  @click="ui.openFragmentLibrary()"
106
107
  >
107
108
  {{ t('settings.serviceFragmentDefaults.footer.manageBoard') }}
108
- </button>
109
- <button
110
- type="button"
111
- class="font-medium text-primary hover:underline"
109
+ </UButton>
110
+ <UButton
111
+ color="primary"
112
+ variant="link"
113
+ class="p-0 text-2xs font-medium text-primary hover:underline"
112
114
  @click="ui.openAccountSettings('fragments')"
113
115
  >
114
116
  {{ t('settings.serviceFragmentDefaults.footer.manageAccount') }}
115
- </button>
117
+ </UButton>
116
118
  </div>
117
119
  </div>
118
120
  </template>
@@ -84,10 +84,11 @@ function toggle(id: string) {
84
84
  >
85
85
  <div class="min-h-0 flex-1 overflow-y-auto p-1">
86
86
  <template v-if="offered.length">
87
- <button
87
+ <UButton
88
+ color="neutral"
89
+ variant="ghost"
88
90
  v-for="s in offered"
89
91
  :key="s.id"
90
- type="button"
91
92
  class="flex w-full items-start gap-2 rounded-sm px-2 py-1.5 text-start text-sm hover:bg-elevated/60 disabled:cursor-not-allowed disabled:opacity-40"
92
93
  :class="selectedSet.has(s.id) ? 'text-app-100' : 'text-toned'"
93
94
  :disabled="atCap && !selectedSet.has(s.id)"
@@ -106,7 +107,7 @@ function toggle(id: string) {
106
107
  {{ s.description }}
107
108
  </span>
108
109
  </span>
109
- </button>
110
+ </UButton>
110
111
  </template>
111
112
  <p v-else class="px-2 py-3 text-xs text-dimmed">
112
113
  {{ t('skills.reviewQueue.pickerEmpty') }}
@@ -12,6 +12,7 @@ import { useSkillLibrary } from '~/stores/skillLibrary'
12
12
  import { SKILL_GROUP_LABEL_KEYS } from '~/utils/skills'
13
13
  import GitHubRepoSearchSelect from '~/components/github/GitHubRepoSearchSelect.vue'
14
14
  import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
15
+ import IconButton from '~/components/common/IconButton.vue'
15
16
 
16
17
  const props = defineProps<{ accountId: string }>()
17
18
 
@@ -234,29 +235,29 @@ async function unlinkSource(id: string) {
234
235
  {{ t('skills.sources.changes') }}
235
236
  </UBadge>
236
237
  <div class="ms-auto flex gap-1">
237
- <UButton
238
+ <IconButton
238
239
  icon="i-lucide-search-check"
239
240
  size="xs"
240
241
  variant="ghost"
241
242
  :loading="rowBusy(`check:${s.id}`)"
242
- :title="t('skills.sources.check')"
243
+ :label="t('skills.sources.check')"
243
244
  @click="checkSource(s.id)"
244
245
  />
245
- <UButton
246
+ <IconButton
246
247
  icon="i-lucide-refresh-cw"
247
248
  size="xs"
248
249
  variant="ghost"
249
250
  :loading="rowBusy(`sync:${s.id}`)"
250
- :title="t('skills.sources.sync')"
251
+ :label="t('skills.sources.sync')"
251
252
  @click="syncSource(s.id)"
252
253
  />
253
- <UButton
254
+ <IconButton
254
255
  icon="i-lucide-unplug"
255
256
  size="xs"
256
257
  color="error"
257
258
  variant="ghost"
258
259
  :loading="rowBusy(`unlink:${s.id}`)"
259
- :title="t('skills.sources.unlink')"
260
+ :label="t('skills.sources.unlink')"
260
261
  @click="unlinkSource(s.id)"
261
262
  />
262
263
  </div>