@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
@@ -8,6 +8,7 @@ import AccountModelPolicySettings from '~/components/layout/AccountModelPolicySe
8
8
  import AccountPlatformAlertSettings from '~/components/layout/AccountPlatformAlertSettings.vue'
9
9
  import AccountRunCredentialSettings from '~/components/layout/AccountRunCredentialSettings.vue'
10
10
  import SecretInput from '~/components/common/SecretInput.vue'
11
+ import IconButton from '~/components/common/IconButton.vue'
11
12
 
12
13
  // Team settings for an org account: the member roster (with combinable admin /
13
14
  // developer / product roles), pending email invitations, and the per-account
@@ -207,7 +208,7 @@ async function disconnectEmail() {
207
208
  <p class="mb-3 text-muted">
208
209
  {{ t('layout.accountTeam.org.ctaBody') }}
209
210
  </p>
210
- <form class="flex gap-2" @submit.prevent="createOrganization">
211
+ <UForm class="flex gap-2" @submit="createOrganization">
211
212
  <UInput
212
213
  v-model="newOrgName"
213
214
  :placeholder="t('layout.accountTeam.org.namePlaceholder')"
@@ -216,7 +217,7 @@ async function disconnectEmail() {
216
217
  <UButton type="submit" color="primary" :loading="busy" icon="i-lucide-plus">
217
218
  {{ t('layout.accountTeam.org.create') }}
218
219
  </UButton>
219
- </form>
220
+ </UForm>
220
221
  </section>
221
222
 
222
223
  <!-- members -->
@@ -249,14 +250,13 @@ async function disconnectEmail() {
249
250
  <!-- Offboarding: end every session this member holds, leaving their membership and
250
251
  roles alone. Confirmed, because it is not undoable from here (the person simply
251
252
  signs in again) and because it is the sort of thing a mis-click should not do. -->
252
- <UButton
253
+ <IconButton
253
254
  v-if="isAdmin"
254
255
  size="xs"
255
256
  color="neutral"
256
257
  variant="ghost"
257
258
  icon="i-lucide-log-out"
258
- :title="t('layout.accountTeam.members.revokeSessions')"
259
- :aria-label="t('layout.accountTeam.members.revokeSessions')"
259
+ :label="t('layout.accountTeam.members.revokeSessions')"
260
260
  data-testid="revoke-member-sessions"
261
261
  @click="revokeSessions(m.userId, m.name || m.email || m.userId)"
262
262
  />
@@ -272,7 +272,7 @@ async function disconnectEmail() {
272
272
  <h3 class="mb-2 font-semibold text-highlighted">
273
273
  {{ t('layout.accountTeam.invite.title') }}
274
274
  </h3>
275
- <form class="flex gap-2" @submit.prevent="sendInvite">
275
+ <UForm class="flex gap-2" @submit="sendInvite">
276
276
  <UInput
277
277
  v-model="inviteEmail"
278
278
  type="email"
@@ -283,7 +283,7 @@ async function disconnectEmail() {
283
283
  <UButton type="submit" color="primary" :loading="busy" icon="i-lucide-send">
284
284
  {{ t('layout.accountTeam.invite.submit') }}
285
285
  </UButton>
286
- </form>
286
+ </UForm>
287
287
 
288
288
  <ul v-if="accounts.invitations.length" class="mt-3 space-y-1">
289
289
  <li
@@ -332,7 +332,7 @@ async function disconnectEmail() {
332
332
  {{ t('layout.accountTeam.email.disconnect') }}
333
333
  </UButton>
334
334
  </div>
335
- <form v-else class="space-y-2" @submit.prevent="connectEmail">
335
+ <UForm v-else class="space-y-2" @submit="connectEmail">
336
336
  <USelect v-model="emailProvider" :items="EMAIL_PROVIDER_ITEMS" class="w-full" />
337
337
  <UInput
338
338
  v-model="emailFrom"
@@ -348,7 +348,7 @@ async function disconnectEmail() {
348
348
  <UButton type="submit" color="primary" :loading="busy">
349
349
  {{ t('layout.accountTeam.email.connect') }}
350
350
  </UButton>
351
- </form>
351
+ </UForm>
352
352
  </template>
353
353
  </section>
354
354
 
@@ -86,8 +86,9 @@ const items = computed<DropdownMenuItem[][]>(() => [
86
86
 
87
87
  <template>
88
88
  <UDropdownMenu :items="items" :content="{ side: 'top', align: 'start' }">
89
- <button
90
- type="button"
89
+ <UButton
90
+ color="neutral"
91
+ variant="ghost"
91
92
  data-testid="appearance-switcher"
92
93
  :aria-label="t('appearance.switcher')"
93
94
  :title="collapsed ? `${t('appearance.switcher')}: ${summary}` : undefined"
@@ -102,7 +103,7 @@ const items = computed<DropdownMenuItem[][]>(() => [
102
103
  <div class="truncate text-xs font-medium text-highlighted">{{ summary }}</div>
103
104
  </div>
104
105
  <UIcon v-if="!collapsed" name="i-lucide-chevron-up" class="h-4 w-4 shrink-0 text-dimmed" />
105
- </button>
106
+ </UButton>
106
107
  </UDropdownMenu>
107
108
  <ThemeImportModal v-model:open="importOpen" />
108
109
  </template>
@@ -257,8 +257,9 @@ async function submitPrompt() {
257
257
  :content="{ align: 'start' }"
258
258
  class="w-full"
259
259
  >
260
- <button
261
- type="button"
260
+ <UButton
261
+ color="neutral"
262
+ variant="ghost"
262
263
  class="flex w-full items-center gap-2 rounded-md px-1.5 py-1 text-start transition hover:bg-elevated/60"
263
264
  :disabled="busy"
264
265
  >
@@ -270,13 +271,14 @@ async function submitPrompt() {
270
271
  {{ accounts.activeAccount?.name ?? t('layout.boardSwitcher.accountFallback') }}
271
272
  </SectionLabel>
272
273
  <UIcon name="i-lucide-chevrons-up-down" class="ms-auto h-3.5 w-3.5 shrink-0 text-app-600" />
273
- </button>
274
+ </UButton>
274
275
  </UDropdownMenu>
275
276
 
276
277
  <!-- board selector -->
277
278
  <UDropdownMenu :items="boardItems" :content="{ align: 'start' }" class="w-full">
278
- <button
279
- type="button"
279
+ <UButton
280
+ color="neutral"
281
+ variant="ghost"
280
282
  :title="
281
283
  collapsed
282
284
  ? (workspace.activeWorkspace?.name ?? t('layout.boardSwitcher.boardFallback'))
@@ -297,13 +299,13 @@ async function submitPrompt() {
297
299
  name="i-lucide-chevron-down"
298
300
  class="ms-auto h-4 w-4 shrink-0 text-dimmed"
299
301
  />
300
- </button>
302
+ </UButton>
301
303
  </UDropdownMenu>
302
304
 
303
305
  <!-- create / rename prompt -->
304
306
  <UModal v-model:open="promptOpen" :title="prompt ? promptMeta[prompt].title : ''">
305
307
  <template #body>
306
- <form class="space-y-3" @submit.prevent="submitPrompt">
308
+ <UForm class="space-y-3" @submit="submitPrompt">
307
309
  <UFormField :label="t('layout.boardSwitcher.prompt.nameLabel')">
308
310
  <UInput
309
311
  v-model="promptValue"
@@ -341,7 +343,7 @@ async function submitPrompt() {
341
343
  {{ prompt ? promptMeta[prompt].cta : '' }}
342
344
  </UButton>
343
345
  </div>
344
- </form>
346
+ </UForm>
345
347
  </template>
346
348
  </UModal>
347
349
  </div>
@@ -151,9 +151,10 @@ const decisionItems = computed(() =>
151
151
  />
152
152
  <!-- Click the readout to snap back to 100%. Always visible (only the LOD sub-label
153
153
  drops on narrow viewports) so the zoom level is never a mystery. -->
154
- <button
155
- type="button"
156
- class="w-16 rounded-sm text-center text-xs tabular-nums text-toned hover:bg-elevated focus-visible:ring-2 focus-visible:ring-app-400/60 sm:w-20"
154
+ <UButton
155
+ color="neutral"
156
+ variant="ghost"
157
+ class="flex w-16 flex-col items-center gap-0 rounded-sm p-0 text-center text-xs tabular-nums text-toned hover:bg-elevated sm:w-20"
157
158
  :title="t('board.toolbar.resetZoom')"
158
159
  :aria-label="t('board.toolbar.resetZoom')"
159
160
  data-testid="board-zoom-reset"
@@ -167,7 +168,7 @@ const decisionItems = computed(() =>
167
168
  <span class="hidden text-3xs uppercase tracking-wide text-dimmed sm:block">{{
168
169
  lodLabel
169
170
  }}</span>
170
- </button>
171
+ </UButton>
171
172
  <IconButton
172
173
  :label="t('board.toolbar.zoomIn')"
173
174
  icon="i-lucide-zoom-in"
@@ -281,10 +281,11 @@ function indexOf(cmd: Command) {
281
281
  <SectionLabel as="p" class="px-2 pb-1 pt-2">
282
282
  {{ group.name }}
283
283
  </SectionLabel>
284
- <button
284
+ <UButton
285
+ color="neutral"
286
+ variant="ghost"
285
287
  v-for="cmd in group.items"
286
288
  :key="cmd.id"
287
- type="button"
288
289
  :data-testid="`command-${cmd.id}`"
289
290
  class="flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-start text-sm transition"
290
291
  :class="
@@ -297,7 +298,7 @@ function indexOf(cmd: Command) {
297
298
  >
298
299
  <UIcon :name="cmd.icon" class="h-4 w-4 shrink-0 text-muted" />
299
300
  <span class="truncate">{{ cmd.label }}</span>
300
- </button>
301
+ </UButton>
301
302
  </div>
302
303
  </div>
303
304
  </div>
@@ -90,14 +90,15 @@ function openConfig(event: MouseEvent) {
90
90
  {{ t('layout.defaultTestEnvBanner.action') }}
91
91
  </UButton>
92
92
  <!-- The URL itself, shown so it can be read and copied, not just clicked. -->
93
- <a
94
- :href="configUrl"
93
+ <ULink
94
+ raw
95
+ :to="configUrl"
95
96
  class="min-w-0 truncate font-mono text-2xs text-app-info-300/80 underline decoration-dotted underline-offset-2 hover:text-app-info-200"
96
97
  data-testid="default-test-env-url"
97
98
  @click="openConfig"
98
99
  >
99
100
  {{ configUrl }}
100
- </a>
101
+ </ULink>
101
102
  </div>
102
103
  </div>
103
104
  </div>
@@ -427,10 +427,11 @@ const filteredGroups = computed<IntegrationGroup[]>(() => {
427
427
  {{ group.title }}
428
428
  </SectionLabel>
429
429
  <div class="space-y-1.5">
430
- <button
430
+ <UButton
431
+ color="neutral"
432
+ variant="ghost"
431
433
  v-for="item in group.items"
432
434
  :key="item.key"
433
- type="button"
434
435
  class="flex w-full items-center gap-3 rounded-lg border border-default bg-default/50 px-3 py-2.5 text-start transition hover:border-muted hover:bg-default"
435
436
  @click="item.onClick()"
436
437
  >
@@ -462,13 +463,14 @@ const filteredGroups = computed<IntegrationGroup[]>(() => {
462
463
  name="i-lucide-chevron-right"
463
464
  class="h-4 w-4 shrink-0 text-dimmed rtl:-scale-x-100"
464
465
  />
465
- </button>
466
+ </UButton>
466
467
  </div>
467
468
 
468
469
  <!-- De-emphasised workspace-config link (e.g. issue tracker settings). -->
469
- <button
470
+ <UButton
471
+ color="neutral"
472
+ variant="ghost"
470
473
  v-if="group.footerLink"
471
- type="button"
472
474
  class="mt-1.5 flex w-full items-center gap-2 rounded-md px-3 py-1.5 text-start text-xs text-muted transition hover:bg-default/60 hover:text-default"
473
475
  @click="group.footerLink.onClick()"
474
476
  >
@@ -481,7 +483,7 @@ const filteredGroups = computed<IntegrationGroup[]>(() => {
481
483
  name="i-lucide-chevron-right"
482
484
  class="h-3.5 w-3.5 shrink-0 text-app-600 rtl:-scale-x-100"
483
485
  />
484
- </button>
486
+ </UButton>
485
487
  </section>
486
488
  </div>
487
489
  </template>
@@ -40,8 +40,9 @@ const items = computed<DropdownMenuItem[][]>(() => [
40
40
 
41
41
  <template>
42
42
  <UDropdownMenu :items="items" :content="{ side: 'top', align: 'start' }">
43
- <button
44
- type="button"
43
+ <UButton
44
+ color="neutral"
45
+ variant="ghost"
45
46
  data-testid="language-switcher"
46
47
  :aria-label="t('language.switcher')"
47
48
  :title="collapsed ? `${t('language.switcher')}: ${current}` : undefined"
@@ -56,6 +57,6 @@ const items = computed<DropdownMenuItem[][]>(() => [
56
57
  <div class="truncate text-xs font-medium text-highlighted">{{ current }}</div>
57
58
  </div>
58
59
  <UIcon v-if="!collapsed" name="i-lucide-chevron-up" class="h-4 w-4 shrink-0 text-dimmed" />
59
- </button>
60
+ </UButton>
60
61
  </UDropdownMenu>
61
62
  </template>
@@ -206,10 +206,11 @@ const filteredGroups = computed<ProviderGroup[]>(() => {
206
206
  {{ group.title }}
207
207
  </SectionLabel>
208
208
  <div class="space-y-1.5">
209
- <button
209
+ <UButton
210
+ color="neutral"
211
+ variant="ghost"
210
212
  v-for="item in group.items"
211
213
  :key="item.key"
212
- type="button"
213
214
  class="flex w-full items-center gap-3 rounded-lg border border-default bg-default/50 px-3 py-2.5 text-start transition hover:border-muted hover:bg-default"
214
215
  :data-testid="`model-provider-${item.key}`"
215
216
  @click="item.onClick()"
@@ -239,7 +240,7 @@ const filteredGroups = computed<ProviderGroup[]>(() => {
239
240
  name="i-lucide-chevron-right"
240
241
  class="h-4 w-4 shrink-0 text-dimmed rtl:-scale-x-100"
241
242
  />
242
- </button>
243
+ </UButton>
243
244
  </div>
244
245
  <p v-if="group.note" class="mt-1.5 px-1 text-2xs text-dimmed">{{ group.note }}</p>
245
246
  </section>
@@ -429,13 +429,15 @@ function revealDecision(n: Notification) {
429
429
  />
430
430
  <div class="min-w-0 flex-1">
431
431
  <div class="flex items-center gap-1.5">
432
- <button
433
- class="block min-w-0 flex-1 truncate text-start text-sm font-medium text-default hover:underline"
432
+ <UButton
433
+ color="neutral"
434
+ variant="link"
435
+ class="block min-w-0 flex-1 truncate p-0 text-start text-sm font-medium text-default hover:underline"
434
436
  :title="n.title"
435
437
  @click="reveal(n)"
436
438
  >
437
439
  {{ n.title }}
438
- </button>
440
+ </UButton>
439
441
  <span
440
442
  v-if="isUrgent(n)"
441
443
  class="shrink-0 rounded-sm bg-app-error-500/20 px-1.5 py-0.5 text-3xs font-semibold uppercase tracking-wide text-app-error-400"
@@ -444,16 +446,17 @@ function revealDecision(n: Notification) {
444
446
  </span>
445
447
  </div>
446
448
  <p class="mt-0.5 text-2xs leading-snug text-muted">{{ n.body }}</p>
447
- <a
449
+ <ULink
450
+ raw
448
451
  v-if="n.payload?.prUrl"
449
- :href="n.payload.prUrl"
452
+ :to="n.payload.prUrl"
450
453
  target="_blank"
451
454
  rel="noopener"
452
455
  class="mt-1 inline-flex items-center gap-1 text-2xs text-app-info-400 hover:underline"
453
456
  >
454
457
  <UIcon name="i-lucide-external-link" class="h-3 w-3" />
455
458
  {{ t('layout.notifications.openPr') }}
456
- </a>
459
+ </ULink>
457
460
  <!--
458
461
  A platform-health card deep-links to the runs it aggregated, so the operator
459
462
  lands on the evidence rather than only on the dashboard.
@@ -463,30 +466,37 @@ function revealDecision(n: Notification) {
463
466
  class="mt-1.5 flex flex-col gap-0.5"
464
467
  data-testid="notification-failing-runs"
465
468
  >
466
- <component
467
- :is="canOpenFailingRun(run) ? 'button' : 'span'"
468
- v-for="run in failingRuns(n)"
469
- :key="run.executionId"
470
- :type="canOpenFailingRun(run) ? 'button' : undefined"
471
- class="flex items-center gap-1 text-start text-2xs"
472
- :class="
473
- canOpenFailingRun(run)
474
- ? 'text-app-info-400 hover:underline'
475
- : 'cursor-default text-dimmed'
476
- "
477
- :title="
478
- canOpenFailingRun(run) ? undefined : t('layout.notifications.failingRunGone')
479
- "
480
- @click="canOpenFailingRun(run) && revealFailingRun(run)"
481
- >
482
- <UIcon name="i-lucide-circle-alert" class="h-3 w-3 shrink-0" />
483
- <span class="truncate">{{
484
- t('layout.notifications.failingRun', {
485
- kind: run.failureKind,
486
- at: d(new Date(run.createdAt), 'short'),
487
- })
488
- }}</span>
489
- </component>
469
+ <template v-for="run in failingRuns(n)" :key="run.executionId">
470
+ <UButton
471
+ v-if="canOpenFailingRun(run)"
472
+ color="neutral"
473
+ variant="link"
474
+ size="xs"
475
+ class="gap-1 p-0 text-start text-2xs text-app-info-400 hover:text-app-info-400 hover:underline"
476
+ @click="revealFailingRun(run)"
477
+ >
478
+ <UIcon name="i-lucide-circle-alert" class="h-3 w-3 shrink-0" />
479
+ <span class="truncate">{{
480
+ t('layout.notifications.failingRun', {
481
+ kind: run.failureKind,
482
+ at: d(new Date(run.createdAt), 'short'),
483
+ })
484
+ }}</span>
485
+ </UButton>
486
+ <span
487
+ v-else
488
+ class="flex cursor-default items-center gap-1 text-start text-2xs text-dimmed"
489
+ :title="t('layout.notifications.failingRunGone')"
490
+ >
491
+ <UIcon name="i-lucide-circle-alert" class="h-3 w-3 shrink-0" />
492
+ <span class="truncate">{{
493
+ t('layout.notifications.failingRun', {
494
+ kind: run.failureKind,
495
+ at: d(new Date(run.createdAt), 'short'),
496
+ })
497
+ }}</span>
498
+ </span>
499
+ </template>
490
500
  <span v-if="failingRunsOmitted(n) > 0" class="text-2xs text-dimmed">
491
501
  {{ t('layout.notifications.failingRunsMore', { count: failingRunsOmitted(n) }) }}
492
502
  </span>
@@ -123,10 +123,11 @@ const groups = computed<PersonalGroup[]>(() => {
123
123
  {{ group.title }}
124
124
  </SectionLabel>
125
125
  <div class="space-y-1.5">
126
- <button
126
+ <UButton
127
+ color="neutral"
128
+ variant="ghost"
127
129
  v-for="item in group.items"
128
130
  :key="item.key"
129
- type="button"
130
131
  class="flex w-full items-center gap-3 rounded-lg border border-default bg-default/50 px-3 py-2.5 text-start transition hover:border-muted hover:bg-default"
131
132
  @click="item.onClick()"
132
133
  >
@@ -147,7 +148,7 @@ const groups = computed<PersonalGroup[]>(() => {
147
148
  name="i-lucide-chevron-right"
148
149
  class="h-4 w-4 shrink-0 text-dimmed rtl:-scale-x-100"
149
150
  />
150
- </button>
151
+ </UButton>
151
152
  </div>
152
153
  </section>
153
154
  </div>
@@ -31,10 +31,11 @@ function pick(role: UiRole) {
31
31
  <div class="space-y-4" data-testid="role-prompt">
32
32
  <p class="text-sm text-toned">{{ t('uiRole.prompt.intro') }}</p>
33
33
  <div class="space-y-2">
34
- <button
34
+ <UButton
35
+ color="neutral"
36
+ variant="ghost"
35
37
  v-for="role in UI_ROLES"
36
38
  :key="role"
37
- type="button"
38
39
  :data-testid="`role-option-${role}`"
39
40
  :aria-pressed="role === uiRole.role && uiRole.chosen"
40
41
  class="flex w-full items-center gap-3 rounded-lg border p-3 text-start transition"
@@ -52,7 +53,7 @@ function pick(role: UiRole) {
52
53
  </div>
53
54
  <p class="text-xs text-muted">{{ t(ROLE_PRESENTATION[role].hintKey) }}</p>
54
55
  </div>
55
- </button>
56
+ </UButton>
56
57
  </div>
57
58
  <!-- The choice is not a commitment, and saying so is what makes the narrowed role
58
59
  pickable: it is one dropdown at the top of the sidebar to leave again. -->
@@ -226,8 +226,9 @@ watch(
226
226
  <div class="contents" @click="onNavAction">
227
227
  <!-- Command bar launcher (⌘K) — the primary way to create blocks / pipelines
228
228
  and reach every action below. -->
229
- <button
230
- type="button"
229
+ <UButton
230
+ color="neutral"
231
+ variant="ghost"
231
232
  class="flex items-center gap-2 rounded-lg border border-muted bg-elevated/60 py-2 text-start text-sm text-muted transition hover:border-app-500 hover:bg-elevated"
232
233
  :class="railed ? 'justify-center px-0' : 'px-2.5'"
233
234
  :aria-label="t('nav.commandBar')"
@@ -238,7 +239,7 @@ watch(
238
239
  <UIcon name="i-lucide-search" class="h-4 w-4 shrink-0" />
239
240
  <span v-if="!railed" class="flex-1 truncate">{{ t('nav.commandBar') }}</span>
240
241
  <UKbd v-if="!railed" value="⌘K" />
241
- </button>
242
+ </UButton>
242
243
 
243
244
  <!-- Sections + items come from the shared nav manifest, already gated by the
244
245
  reactive slotFilter (backend/docs/adr/0049-modular-vue-adoption.md, slice 1) — which
@@ -39,15 +39,16 @@ function dismiss() {
39
39
  <span class="mx-1.5 text-app-warning-400/60">·</span>
40
40
  <i18n-t keypath="language.warning.body" tag="span" scope="global">
41
41
  <template #repoLink>
42
- <a
43
- :href="REPO_URL"
42
+ <ULink
43
+ raw
44
+ :to="REPO_URL"
44
45
  target="_blank"
45
46
  rel="noopener noreferrer"
46
47
  class="inline-flex items-center gap-1 font-medium text-app-info-300 hover:underline"
47
48
  >
48
49
  {{ t('language.warning.repoLinkLabel') }}
49
50
  <UIcon name="i-lucide-external-link" class="h-3 w-3" />
50
- </a>
51
+ </ULink>
51
52
  </template>
52
53
  </i18n-t>
53
54
  </p>
@@ -67,9 +67,10 @@ const pinnedTitle = computed(
67
67
  <!-- Rail: one button, flips the tier. The label rides under the glyph so the rail still says
68
68
  which tier is on — the audience that needs to find the advanced half is exactly the one
69
69
  sitting in basic mode's collapsed default. -->
70
- <button
70
+ <UButton
71
+ color="neutral"
72
+ variant="ghost"
71
73
  v-else-if="collapsed"
72
- type="button"
73
74
  data-testid="ui-mode-toggle"
74
75
  :aria-label="t('uiMode.switchTo', { mode: t(MODE_LABELS[otherMode]) })"
75
76
  :title="t('uiMode.switchTo', { mode: t(MODE_LABELS[otherMode]) })"
@@ -80,7 +81,7 @@ const pinnedTitle = computed(
80
81
  <span class="w-full truncate text-center text-3xs font-medium uppercase text-toned">
81
82
  {{ currentLabel }}
82
83
  </span>
83
- </button>
84
+ </UButton>
84
85
 
85
86
  <div v-else data-testid="ui-mode-switcher" class="w-full">
86
87
  <SectionLabel class="mb-1 px-1">
@@ -91,10 +92,11 @@ const pinnedTitle = computed(
91
92
  :aria-label="t('uiMode.switcher')"
92
93
  class="flex w-full gap-1 rounded-lg border border-muted bg-default/60 p-1"
93
94
  >
94
- <button
95
+ <UButton
96
+ color="neutral"
97
+ variant="ghost"
95
98
  v-for="mode in UI_MODES"
96
99
  :key="mode"
97
- type="button"
98
100
  :data-testid="`ui-mode-option-${mode}`"
99
101
  :aria-pressed="mode === uiMode.mode"
100
102
  class="flex-1 truncate rounded-md px-2 py-1 text-xs font-medium transition"
@@ -106,7 +108,7 @@ const pinnedTitle = computed(
106
108
  @click="uiMode.setMode(mode)"
107
109
  >
108
110
  {{ t(MODE_LABELS[mode]) }}
109
- </button>
111
+ </UButton>
110
112
  </div>
111
113
  </div>
112
114
 
@@ -38,9 +38,10 @@ const items = computed(() =>
38
38
  <template>
39
39
  <UDropdownMenu :items="items" :ui="{ content: 'min-w-48' }">
40
40
  <!-- Rail: glyph over the role name, matching the tier button beside it. -->
41
- <button
41
+ <UButton
42
+ color="neutral"
43
+ variant="ghost"
42
44
  v-if="collapsed"
43
- type="button"
44
45
  data-testid="ui-role-toggle"
45
46
  :aria-label="`${t('uiRole.switcher')}: ${currentLabel}`"
46
47
  :title="`${t('uiRole.switcher')}: ${currentLabel}`"
@@ -50,11 +51,12 @@ const items = computed(() =>
50
51
  <span class="w-full truncate text-center text-3xs font-medium uppercase text-toned">
51
52
  {{ currentLabel }}
52
53
  </span>
53
- </button>
54
+ </UButton>
54
55
 
55
- <button
56
+ <UButton
57
+ color="neutral"
58
+ variant="ghost"
56
59
  v-else
57
- type="button"
58
60
  data-testid="ui-role-switcher"
59
61
  :aria-label="t('uiRole.switcher')"
60
62
  :title="t(current.hintKey)"
@@ -68,6 +70,6 @@ const items = computed(() =>
68
70
  <div class="truncate text-xs font-medium text-default">{{ currentLabel }}</div>
69
71
  </div>
70
72
  <UIcon name="i-lucide-chevron-down" class="h-3.5 w-3.5 shrink-0 text-dimmed" />
71
- </button>
73
+ </UButton>
72
74
  </UDropdownMenu>
73
75
  </template>
@@ -221,7 +221,7 @@ function memberLabel(userId: string, name?: string | null, email?: string | null
221
221
  {{ t('layout.workspaceMembers.add.noAccount') }}
222
222
  </p>
223
223
  <template v-else>
224
- <form class="flex gap-2" @submit.prevent="addMember">
224
+ <UForm class="flex gap-2" @submit="addMember">
225
225
  <USelect
226
226
  v-model="addUserId"
227
227
  :items="candidates"
@@ -242,7 +242,7 @@ function memberLabel(userId: string, name?: string | null, email?: string | null
242
242
  >
243
243
  {{ t('layout.workspaceMembers.add.submit') }}
244
244
  </UButton>
245
- </form>
245
+ </UForm>
246
246
  <p v-if="candidates.length === 0" class="mt-2 text-dimmed">
247
247
  {{ t('layout.workspaceMembers.add.allAdded') }}
248
248
  </p>