@cat-factory/app 0.311.2 → 0.311.3

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 (192) hide show
  1. package/README.md +65 -4
  2. package/app/app.config.ts +7 -0
  3. package/app/components/assistant/AssistantModal.vue +2 -5
  4. package/app/components/auth/AuthGate.vue +2 -1
  5. package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +14 -15
  6. package/app/components/binaryOutput/StoredAssetView.vue +1 -3
  7. package/app/components/board/AddTaskModal.vue +3 -10
  8. package/app/components/board/AgentFailureCard.vue +4 -5
  9. package/app/components/board/AgentStopButton.vue +5 -7
  10. package/app/components/board/BoardCanvas.vue +31 -30
  11. package/app/components/board/CreateInitiativeModal.vue +2 -4
  12. package/app/components/board/RecurringPipelineModal.vue +2 -7
  13. package/app/components/board/nodes/AgentChip.vue +3 -2
  14. package/app/components/board/nodes/BlockNode.vue +14 -11
  15. package/app/components/board/nodes/InitiativeCard.vue +6 -3
  16. package/app/components/bootstrap/AdoptionReviewModal.vue +2 -5
  17. package/app/components/bootstrap/BootstrapModal.vue +6 -15
  18. package/app/components/bootstrap/BootstrapRunSteps.vue +3 -1
  19. package/app/components/brainstorm/BrainstormWindow.vue +32 -26
  20. package/app/components/bugFishing/BugFishingWindow.vue +15 -13
  21. package/app/components/clarity/ClarityReviewWindow.vue +31 -26
  22. package/app/components/common/ClarificationItem.vue +3 -1
  23. package/app/components/common/EmptyState.vue +41 -19
  24. package/app/components/common/InterviewGateNotice.vue +3 -1
  25. package/app/components/common/Spinner.vue +28 -0
  26. package/app/components/consensus/ConsensusSessionWindow.vue +5 -5
  27. package/app/components/docs/DocInterviewWindow.vue +6 -6
  28. package/app/components/documents/ContextDocumentPicker.vue +2 -2
  29. package/app/components/documents/DocumentImportModal.vue +9 -8
  30. package/app/components/documents/DocumentSourceConnectModal.vue +10 -10
  31. package/app/components/documents/DocumentTemplatesModal.vue +13 -7
  32. package/app/components/documents/RepoContextDocPicker.vue +2 -2
  33. package/app/components/documents/SpawnPreviewModal.vue +4 -6
  34. package/app/components/documents/StartFromDesignModal.vue +2 -1
  35. package/app/components/documents/TaskContextDocs.vue +4 -5
  36. package/app/components/environments/EnvironmentStatusPanel.vue +11 -8
  37. package/app/components/environments/steps/EnvPickStep.vue +7 -3
  38. package/app/components/environments/steps/EnvPreflightStep.vue +6 -5
  39. package/app/components/focus/BlockFocusView.vue +6 -5
  40. package/app/components/followUp/FollowUpWindow.vue +7 -9
  41. package/app/components/forkDecision/ForkDecisionWindow.vue +9 -8
  42. package/app/components/foundational/FoundationalServiceCatalogList.vue +9 -5
  43. package/app/components/foundational/FoundationalServiceRegistry.vue +11 -7
  44. package/app/components/foundational/FoundationalServiceSources.vue +20 -22
  45. package/app/components/foundational/FoundationalSuppressions.vue +2 -2
  46. package/app/components/foundational/ServiceCatalogConnection.vue +15 -15
  47. package/app/components/fragments/FragmentLibraryManager.vue +36 -34
  48. package/app/components/fragments/FragmentSelector.vue +7 -3
  49. package/app/components/gates/GateResultView.vue +6 -9
  50. package/app/components/github/AddServiceFromRepoModal.vue +6 -10
  51. package/app/components/github/GitHubConnect.vue +10 -17
  52. package/app/components/github/GitHubPanel.vue +44 -47
  53. package/app/components/github/RepoTreeBrowser.vue +9 -5
  54. package/app/components/guidedReview/GuidedReviewModal.vue +16 -6
  55. package/app/components/guidedReview/GuidedReviewOverview.vue +2 -1
  56. package/app/components/guidedReview/GuidedReviewPicker.vue +10 -7
  57. package/app/components/guidedReview/GuidedReviewThread.vue +9 -4
  58. package/app/components/humanTest/HumanTestWindow.vue +2 -1
  59. package/app/components/initiative/InitiativePlanningWindow.vue +6 -6
  60. package/app/components/initiative/InitiativeTrackerWindow.vue +11 -11
  61. package/app/components/judge/JudgeResultView.vue +2 -7
  62. package/app/components/kaizen/KaizenPanel.vue +20 -5
  63. package/app/components/kaizen/KaizenStepStatus.vue +2 -1
  64. package/app/components/layout/AccountAuditLog.vue +11 -3
  65. package/app/components/layout/AccountDeploymentSettings.vue +17 -62
  66. package/app/components/layout/AccountFailureKindRules.vue +6 -5
  67. package/app/components/layout/AccountModelPolicySettings.vue +2 -6
  68. package/app/components/layout/AccountPlatformAlertSettings.vue +5 -13
  69. package/app/components/layout/AccountRiskPolicySettings.vue +13 -14
  70. package/app/components/layout/AccountRunCredentialSettings.vue +2 -2
  71. package/app/components/layout/AccountTeamSettings.vue +12 -9
  72. package/app/components/layout/BoardSwitcher.vue +2 -2
  73. package/app/components/layout/BoardToolbar.vue +3 -11
  74. package/app/components/layout/CommandBar.vue +7 -3
  75. package/app/components/layout/ConnectionStatusBanner.vue +2 -1
  76. package/app/components/layout/IntegrationsHub.vue +7 -3
  77. package/app/components/layout/ModelProvidersHub.vue +7 -3
  78. package/app/components/layout/NotificationsInbox.vue +3 -11
  79. package/app/components/layout/WorkspaceMembersSettings.vue +12 -5
  80. package/app/components/media/ArtifactLightbox.vue +4 -3
  81. package/app/components/notifications/NotificationSettingsPanel.vue +4 -11
  82. package/app/components/observability/ToolCallList.vue +12 -17
  83. package/app/components/panels/AgentStepDetail.vue +5 -5
  84. package/app/components/panels/GenericStructuredResultView.vue +6 -9
  85. package/app/components/panels/InspectorPanel.vue +2 -1
  86. package/app/components/panels/MergerResultView.vue +5 -6
  87. package/app/components/panels/ObservabilityPanel.vue +26 -41
  88. package/app/components/panels/OperatorDashboardPanel.vue +29 -17
  89. package/app/components/panels/ReportsPanel.vue +17 -11
  90. package/app/components/panels/ReportsSpendBreakdown.vue +7 -3
  91. package/app/components/panels/RunDetailLoadState.vue +4 -3
  92. package/app/components/panels/StepContainerStatus.vue +4 -3
  93. package/app/components/panels/StepDelegatedStatus.vue +3 -2
  94. package/app/components/panels/StepMetadataCard.vue +6 -8
  95. package/app/components/panels/StepRunMeta.vue +2 -5
  96. package/app/components/panels/inspector/ContainerSummary.vue +7 -1
  97. package/app/components/panels/inspector/EpicChildren.vue +7 -3
  98. package/app/components/panels/inspector/FrontendConfig.vue +7 -3
  99. package/app/components/panels/inspector/InitiativeInspector.vue +5 -2
  100. package/app/components/panels/inspector/ServiceConnections.vue +7 -3
  101. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +2 -6
  102. package/app/components/panels/inspector/ServiceTestSecrets.vue +2 -6
  103. package/app/components/panels/inspector/ServiceTestingContext.vue +2 -6
  104. package/app/components/panels/inspector/ServiceValidationConfig.vue +38 -39
  105. package/app/components/panels/inspector/TaskExecution.vue +5 -6
  106. package/app/components/panels/inspector/TaskRunSettings.vue +7 -3
  107. package/app/components/panels/inspector/TaskStructure.vue +0 -1
  108. package/app/components/pipeline/AgentPromptEditor.vue +25 -18
  109. package/app/components/pipeline/PipelineBuilder.vue +14 -24
  110. package/app/components/pipeline/PipelineProgress.vue +16 -17
  111. package/app/components/prReview/PrReviewPhaseBadge.vue +3 -2
  112. package/app/components/prReview/PrReviewWindow.vue +16 -20
  113. package/app/components/providers/ApiKeysSection.vue +2 -6
  114. package/app/components/providers/PersonalCredentialModal.vue +2 -6
  115. package/app/components/providers/PersonalSubscriptionSection.vue +3 -7
  116. package/app/components/providers/VendorCredentialsModal.vue +2 -6
  117. package/app/components/provisioning/ProvisioningLogsDrawer.vue +10 -6
  118. package/app/components/ralph/RalphLoopResultView.vue +2 -7
  119. package/app/components/requirements/RequirementsReviewWindow.vue +44 -39
  120. package/app/components/sandbox/SandboxPanel.vue +37 -25
  121. package/app/components/settings/AccountSettingsPanel.vue +6 -3
  122. package/app/components/settings/ApiTokensPanel.vue +2 -6
  123. package/app/components/settings/BudgetSettings.vue +2 -2
  124. package/app/components/settings/CapabilityCredentialsPanel.vue +2 -6
  125. package/app/components/settings/CloudflareHandlerSection.vue +3 -7
  126. package/app/components/settings/ConsensusGroupsSection.vue +7 -3
  127. package/app/components/settings/CustomManifestTypeEditor.vue +7 -3
  128. package/app/components/settings/DefaultProvisionTypeSection.vue +2 -2
  129. package/app/components/settings/InfraHandlersConfigurator.vue +8 -11
  130. package/app/components/settings/IssueTrackerPanel.vue +9 -9
  131. package/app/components/settings/LocalContainerPoolSettings.vue +2 -6
  132. package/app/components/settings/LocalModelEndpointsPanel.vue +5 -7
  133. package/app/components/settings/McpAuthorizeScreen.vue +8 -3
  134. package/app/components/settings/McpOAuthCallbackScreen.vue +2 -1
  135. package/app/components/settings/ModelConfigurationPanel.vue +45 -17
  136. package/app/components/settings/ObservabilityConnectionPanel.vue +4 -15
  137. package/app/components/settings/OpenRouterCatalogPanel.vue +8 -24
  138. package/app/components/settings/PackageRegistriesPanel.vue +2 -6
  139. package/app/components/settings/ProviderConnectionTab.vue +3 -7
  140. package/app/components/settings/RiskPolicyPanel.vue +4 -16
  141. package/app/components/settings/SharedStacksPanel.vue +8 -18
  142. package/app/components/settings/TaskTypeSuppressionsPanel.vue +10 -6
  143. package/app/components/settings/UsageSettings.vue +11 -2
  144. package/app/components/settings/UserSecretsSection.vue +5 -7
  145. package/app/components/settings/WorkspaceMetadataSettings.vue +9 -9
  146. package/app/components/settings/WorkspaceSettingsPanel.vue +3 -11
  147. package/app/components/skills/ReviewSkillQueue.vue +7 -3
  148. package/app/components/skills/SkillLibraryManager.vue +20 -18
  149. package/app/components/slack/SlackPanel.vue +5 -8
  150. package/app/components/spec/ServiceSpecWindow.vue +32 -29
  151. package/app/components/tasks/BugHuntModal.vue +23 -19
  152. package/app/components/tasks/ContextIssuePicker.vue +2 -2
  153. package/app/components/tasks/TaskContextIssues.vue +4 -5
  154. package/app/components/tasks/TaskImportModal.vue +10 -8
  155. package/app/components/tasks/TaskSourceConnectModal.vue +11 -11
  156. package/app/components/testing/TestReportWindow.vue +6 -12
  157. package/app/components/tutorial/TutorialCatalogue.vue +7 -3
  158. package/app/components/tutorial/TutorialOverlay.vue +2 -1
  159. package/app/components/tutorial/TutorialPrompt.vue +7 -3
  160. package/app/components/vcs/GitLabConnect.vue +2 -6
  161. package/app/components/visualConfirm/VisualConfirmationWindow.vue +9 -4
  162. package/app/composables/toastTitleKeys.spec.ts +143 -0
  163. package/app/composables/useActionToast.spec.ts +71 -0
  164. package/app/composables/useActionToast.ts +101 -0
  165. package/app/composables/useBlockDeletion.ts +3 -6
  166. package/app/composables/useConfirmAction.ts +5 -3
  167. package/app/composables/useContextLinking.spec.ts +3 -0
  168. package/app/composables/useContextLinking.ts +11 -16
  169. package/app/composables/useCopyToClipboard.ts +4 -3
  170. package/app/composables/useNavContributions.ts +4 -5
  171. package/app/composables/usePipelineErrorToast.spec.ts +24 -0
  172. package/app/composables/usePipelineErrorToast.ts +26 -2
  173. package/app/composables/usePipelineLibraryActions.ts +8 -9
  174. package/app/pages/index.vue +3 -2
  175. package/app/stores/board/context.ts +4 -3
  176. package/app/stores/board/placement.ts +8 -20
  177. package/app/stores/board/removal.ts +4 -13
  178. package/app/stores/board.spec.ts +10 -1
  179. package/app/stores/board.ts +3 -2
  180. package/app/utils/bootstrapSteps.ts +11 -2
  181. package/app/utils/pipelineRender.ts +21 -8
  182. package/i18n/locales/de.json +12 -30
  183. package/i18n/locales/en.json +12 -30
  184. package/i18n/locales/es.json +12 -30
  185. package/i18n/locales/fr.json +12 -30
  186. package/i18n/locales/he.json +12 -30
  187. package/i18n/locales/it.json +12 -30
  188. package/i18n/locales/ja.json +12 -30
  189. package/i18n/locales/pl.json +12 -30
  190. package/i18n/locales/tr.json +12 -30
  191. package/i18n/locales/uk.json +12 -30
  192. package/package.json +1 -1
@@ -24,6 +24,7 @@ import ProviderPreferenceEditor from '~/components/settings/ProviderPreferenceEd
24
24
  import { showOverrideField } from '~/utils/uiMode'
25
25
  import SectionLabel from '~/components/common/SectionLabel.vue'
26
26
  import IconButton from '~/components/common/IconButton.vue'
27
+ import EmptyState from '~/components/common/EmptyState.vue'
27
28
  import { FULL_BLEED_TAKEOVER_CLASS } from '~/utils/safeArea'
28
29
 
29
30
  const { t } = useI18n()
@@ -36,7 +37,7 @@ const agentTier = useAgentTierStore()
36
37
  const creds = useVendorCredentialsStore()
37
38
  const workspace = useWorkspaceStore()
38
39
  const { present } = usePipelineErrorToast()
39
- const toast = useToast()
40
+ const actionToast = useActionToast()
40
41
  const { confirm } = useConfirm()
41
42
 
42
43
  const open = computed({
@@ -93,13 +94,25 @@ const hiddenByTier = computed(() =>
93
94
  filter.value.trim() ? 0 : configurableKinds.value.length - tieredKinds.value.length,
94
95
  )
95
96
 
97
+ // Set when the catalog read rejects, so the preset list states the failure instead of holding a
98
+ // skeleton that never resolves (`models.loaded` stays false after a failed read).
99
+ const catalogFailed = ref(false)
100
+
101
+ function loadCatalog() {
102
+ catalogFailed.value = false
103
+ models.ensureLoaded(workspace.workspaceId ?? undefined).catch((error) => {
104
+ catalogFailed.value = true
105
+ present(error, 'settings.modelConfiguration.toast.catalogFailed')
106
+ })
107
+ }
108
+
96
109
  watch(
97
110
  open,
98
111
  (isOpen) => {
99
112
  if (isOpen) {
100
113
  editor.value = null
101
114
  filter.value = ''
102
- void models.ensureLoaded(workspace.workspaceId ?? undefined)
115
+ loadCatalog()
103
116
  if (workspace.workspaceId) void creds.load(workspace.workspaceId)
104
117
  }
105
118
  },
@@ -258,11 +271,8 @@ async function save() {
258
271
  // NOT through `present`: that funnel classifies a BACKEND failure, and a synthesized local
259
272
  // `Error` carries no envelope and no status, so it lands on the network-fault description and
260
273
  // tells the user the server could not be reached about a check that never left the browser.
261
- toast.add({
262
- title: t('settings.modelConfiguration.toast.nameRequiredTitle'),
274
+ actionToast.error('settings.modelConfiguration.toast.nameRequiredTitle', {
263
275
  description: t('settings.modelConfiguration.toast.nameRequiredBody'),
264
- color: 'warning',
265
- icon: 'i-lucide-triangle-alert',
266
276
  })
267
277
  return
268
278
  }
@@ -372,9 +382,25 @@ async function save() {
372
382
  </UButton>
373
383
  </div>
374
384
 
375
- <p v-if="models.models.length === 0" class="py-4 text-center text-sm text-dimmed">
376
- {{ t('settings.modelConfiguration.list.loadingCatalog') }}
377
- </p>
385
+ <UAlert
386
+ v-if="catalogFailed"
387
+ color="error"
388
+ variant="subtle"
389
+ icon="i-lucide-triangle-alert"
390
+ :title="t('settings.modelConfiguration.list.catalogFailed')"
391
+ :actions="[
392
+ {
393
+ label: t('common.retry'),
394
+ color: 'neutral',
395
+ variant: 'outline',
396
+ onClick: loadCatalog,
397
+ },
398
+ ]"
399
+ />
400
+
401
+ <div v-else-if="!models.loaded" class="space-y-3">
402
+ <USkeleton v-for="n in 3" :key="n" class="h-20 w-full rounded-lg" />
403
+ </div>
378
404
 
379
405
  <div v-else class="space-y-3">
380
406
  <div
@@ -443,9 +469,11 @@ async function save() {
443
469
  </span>
444
470
  </div>
445
471
  </div>
446
- <p v-if="sortedPresets.length === 0" class="py-6 text-center text-sm text-dimmed">
447
- {{ t('settings.modelConfiguration.list.empty') }}
448
- </p>
472
+ <EmptyState
473
+ v-if="sortedPresets.length === 0"
474
+ icon="i-lucide-sliders-horizontal"
475
+ :title="t('settings.modelConfiguration.list.empty')"
476
+ />
449
477
  </div>
450
478
 
451
479
  <!-- The consensus-GROUP library: which model PANELS review the workspace's heavier
@@ -549,12 +577,12 @@ async function save() {
549
577
  </UButton>
550
578
  </UDropdownMenu>
551
579
  </div>
552
- <p
580
+ <EmptyState
553
581
  v-if="filteredKinds.length === 0"
554
- class="px-4 py-6 text-center text-sm text-dimmed"
555
- >
556
- {{ t('settings.modelConfiguration.editor.noAgentsMatch', { filter }) }}
557
- </p>
582
+ compact
583
+ icon="i-lucide-search-x"
584
+ :title="t('settings.modelConfiguration.editor.noAgentsMatch', { filter })"
585
+ />
558
586
  </div>
559
587
  </div>
560
588
 
@@ -12,7 +12,7 @@ import SecretInput from '~/components/common/SecretInput.vue'
12
12
  const { t } = useI18n()
13
13
  const ui = useUiStore()
14
14
  const store = useReleaseHealthStore()
15
- const toast = useToast()
15
+ const actionToast = useActionToast()
16
16
  const { present } = usePipelineErrorToast()
17
17
  const { confirmAction, toastDone } = useConfirmAction()
18
18
 
@@ -66,21 +66,14 @@ async function saveIncident() {
66
66
  }
67
67
  if (incidentIo.apiKey.trim()) input.incidentIo = { apiKey: incidentIo.apiKey.trim() }
68
68
  if (!input.pagerDuty && !input.incidentIo) {
69
- toast.add({
70
- title: t('settings.observabilityConnection.toast.incidentCredsRequired'),
71
- color: 'error',
72
- })
69
+ actionToast.error('settings.observabilityConnection.toast.incidentCredsRequired')
73
70
  return
74
71
  }
75
72
  await store.saveIncident(input)
76
73
  pagerDuty.apiToken = ''
77
74
  pagerDuty.fromEmail = ''
78
75
  incidentIo.apiKey = ''
79
- toast.add({
80
- title: t('settings.observabilityConnection.toast.incidentSaved'),
81
- icon: 'i-lucide-check',
82
- color: 'success',
83
- })
76
+ actionToast.success('settings.observabilityConnection.toast.incidentSaved')
84
77
  } catch (e) {
85
78
  present(e, 'settings.observabilityConnection.toast.incidentSaveFailed')
86
79
  } finally {
@@ -111,11 +104,7 @@ async function saveConnection() {
111
104
  })
112
105
  datadog.apiKey = ''
113
106
  datadog.appKey = ''
114
- toast.add({
115
- title: t('settings.observabilityConnection.toast.connected'),
116
- icon: 'i-lucide-check',
117
- color: 'success',
118
- })
107
+ actionToast.success('settings.observabilityConnection.toast.connected')
119
108
  } catch (e) {
120
109
  present(e, 'settings.observabilityConnection.toast.connectFailed')
121
110
  } finally {
@@ -19,8 +19,8 @@ const workspace = useWorkspaceStore()
19
19
  const store = useOpenRouterStore()
20
20
  const apiKeys = useApiKeysStore()
21
21
  const models = useModelsStore()
22
- const toast = useToast()
23
- const { present } = usePipelineErrorToast()
22
+ const actionToast = useActionToast()
23
+ const { present, presentReported } = usePipelineErrorToast()
24
24
 
25
25
  const open = computed({
26
26
  get: () => ui.openRouterOpen,
@@ -155,21 +155,14 @@ async function connectKey() {
155
155
  if (scope === 'workspace') await apiKeys.removeWorkspaceKey(created.id).catch(() => {})
156
156
  else await apiKeys.removeUserKey(created.id).catch(() => {})
157
157
  }
158
- toast.add({
159
- title: t('settings.openRouterCatalog.toast.connectFailed'),
160
- description: store.refreshError ?? t('settings.openRouterCatalog.toast.rejected'),
161
- icon: 'i-lucide-triangle-alert',
162
- color: 'error',
163
- })
158
+ // `!reachable` covers an outage or a network fault as well as a refused key, and the probe
159
+ // does not say which, so the description stays the generic "it reported a failure" line.
160
+ presentReported(store.refreshError, 'settings.openRouterCatalog.toast.connectFailed')
164
161
  return
165
162
  }
166
163
  keyValue.value = ''
167
164
  keyLabel.value = ''
168
- toast.add({
169
- title: t('settings.openRouterCatalog.toast.connected'),
170
- icon: 'i-lucide-check',
171
- color: 'success',
172
- })
165
+ actionToast.success('settings.openRouterCatalog.toast.connected')
173
166
  } catch (e) {
174
167
  present(e, 'settings.openRouterCatalog.toast.connectFailed')
175
168
  } finally {
@@ -181,12 +174,7 @@ async function refresh() {
181
174
  if (!workspace.workspaceId) return
182
175
  const result = await store.refresh(workspace.workspaceId)
183
176
  if (!result.reachable) {
184
- toast.add({
185
- title: t('settings.openRouterCatalog.toast.unreachable'),
186
- description: store.refreshError ?? t('settings.openRouterCatalog.toast.connectFirst'),
187
- icon: 'i-lucide-triangle-alert',
188
- color: 'error',
189
- })
177
+ presentReported(store.refreshError, 'settings.openRouterCatalog.toast.unreachable')
190
178
  }
191
179
  }
192
180
 
@@ -202,11 +190,7 @@ async function save() {
202
190
  await store.save(workspace.workspaceId, models2)
203
191
  // Reflect newly-enabled models in the picker immediately.
204
192
  await models.refresh(workspace.workspaceId)
205
- toast.add({
206
- title: t('settings.openRouterCatalog.toast.saved'),
207
- icon: 'i-lucide-check',
208
- color: 'success',
209
- })
193
+ actionToast.success('settings.openRouterCatalog.toast.saved')
210
194
  } catch (e) {
211
195
  present(e, 'settings.openRouterCatalog.toast.saveFailed')
212
196
  } finally {
@@ -12,7 +12,7 @@ import SecretInput from '~/components/common/SecretInput.vue'
12
12
 
13
13
  const { t } = useI18n()
14
14
  const store = usePackageRegistriesStore()
15
- const toast = useToast()
15
+ const actionToast = useActionToast()
16
16
  const { present } = usePipelineErrorToast()
17
17
  const { confirmAction, toastDone } = useConfirmAction()
18
18
 
@@ -70,11 +70,7 @@ async function addEntry() {
70
70
  })
71
71
  form.scopes = ''
72
72
  form.token = ''
73
- toast.add({
74
- title: t('settings.packageRegistries.toast.added'),
75
- icon: 'i-lucide-check',
76
- color: 'success',
77
- })
73
+ actionToast.success('settings.packageRegistries.toast.added')
78
74
  } catch (e) {
79
75
  present(e, 'settings.packageRegistries.toast.addFailed')
80
76
  } finally {
@@ -36,7 +36,7 @@ const emit = defineEmits<{ connected: [] }>()
36
36
 
37
37
  const { t } = useI18n()
38
38
  const store = useProviderConnectionsStore()
39
- const toast = useToast()
39
+ const actionToast = useActionToast()
40
40
  const { present } = usePipelineErrorToast()
41
41
  const { confirmAction } = useConfirmAction()
42
42
 
@@ -208,11 +208,7 @@ function buildFlatPayload() {
208
208
  }
209
209
 
210
210
  function toastSaved() {
211
- toast.add({
212
- title: t('settings.providerConnection.toast.saved', { title: title.value }),
213
- icon: 'i-lucide-check',
214
- color: 'success',
215
- })
211
+ actionToast.success('settings.providerConnection.toast.saved', { params: { title: title.value } })
216
212
  }
217
213
 
218
214
  // --- Native flat-form actions (both manifest-overlay and config-overlay flavours) ---
@@ -330,7 +326,7 @@ async function remove() {
330
326
  try {
331
327
  await store.remove(props.kind)
332
328
  resetDraft()
333
- toast.add({ title: t('settings.providerConnection.toast.removed'), icon: 'i-lucide-check' })
329
+ actionToast.success('settings.providerConnection.toast.removed')
334
330
  } catch (e) {
335
331
  present(e, 'settings.providerConnection.toast.removeFailed')
336
332
  } finally {
@@ -18,7 +18,7 @@ import SectionLabel from '~/components/common/SectionLabel.vue'
18
18
  const { t } = useI18n()
19
19
 
20
20
  const store = useRiskPoliciesStore()
21
- const toast = useToast()
21
+ const actionToast = useActionToast()
22
22
  const { present } = usePipelineErrorToast()
23
23
  const { confirm } = useConfirm()
24
24
 
@@ -69,11 +69,7 @@ async function run(key: string, titleKey: string, action: () => Promise<unknown>
69
69
  async function save(policy: RiskPolicyLibraryEntry, patch: UpdateRiskPolicyInput) {
70
70
  await run(policy.id, 'settings.riskPolicy.toast.saveFailed', async () => {
71
71
  await store.update(policy.id, patch)
72
- toast.add({
73
- title: t('settings.riskPolicy.toast.saved'),
74
- icon: 'i-lucide-check',
75
- color: 'success',
76
- })
72
+ actionToast.success('settings.riskPolicy.toast.saved')
77
73
  })
78
74
  }
79
75
 
@@ -112,11 +108,7 @@ async function create(input: Parameters<typeof store.create>[0]) {
112
108
  creating.value = true
113
109
  try {
114
110
  await store.create(input)
115
- toast.add({
116
- title: t('settings.riskPolicy.toast.created'),
117
- icon: 'i-lucide-check',
118
- color: 'success',
119
- })
111
+ actionToast.success('settings.riskPolicy.toast.created')
120
112
  } catch (e) {
121
113
  present(e, 'settings.riskPolicy.toast.createFailed')
122
114
  } finally {
@@ -136,11 +128,7 @@ function clone(policy: RiskPolicyLibraryEntry) {
136
128
  t('settings.riskPolicy.inherited.copyName', { name: source }),
137
129
  )
138
130
  await store.clone(policy.id, name)
139
- toast.add({
140
- title: t('settings.riskPolicy.toast.cloned'),
141
- icon: 'i-lucide-check',
142
- color: 'success',
143
- })
131
+ actionToast.success('settings.riskPolicy.toast.cloned')
144
132
  })
145
133
  }
146
134
 
@@ -15,8 +15,8 @@ import type {
15
15
 
16
16
  const { t } = useI18n()
17
17
  const store = useSharedStacksStore()
18
- const toast = useToast()
19
- const { present } = usePipelineErrorToast()
18
+ const actionToast = useActionToast()
19
+ const { present, presentReported } = usePipelineErrorToast()
20
20
  const { confirmAction, toastDone } = useConfirmAction()
21
21
 
22
22
  const stacks = computed(() => store.stacks)
@@ -150,11 +150,8 @@ async function autodetect() {
150
150
  ...(form.directory.trim() ? { directory: form.directory.trim() } : {}),
151
151
  })
152
152
  if (!rec.detected) {
153
- toast.add({
154
- title: t('settings.sharedStacks.detect.nothing'),
153
+ actionToast.warning('settings.sharedStacks.detect.nothing', {
155
154
  description: rec.notes[0]?.message ?? '',
156
- icon: 'i-lucide-info',
157
- color: 'warning',
158
155
  })
159
156
  return
160
157
  }
@@ -163,11 +160,8 @@ async function autodetect() {
163
160
  form.composeProfiles = rec.composeProfiles.join(', ')
164
161
  form.managedNetworks = rec.managedNetworks.join(', ')
165
162
  detectedEnvFiles.value = rec.envFiles
166
- toast.add({
167
- title: t('settings.sharedStacks.detect.detected'),
163
+ actionToast.success('settings.sharedStacks.detect.detected', {
168
164
  description: t('settings.sharedStacks.detect.detectedBody'),
169
- icon: 'i-lucide-wand-sparkles',
170
- color: 'success',
171
165
  })
172
166
  } catch (e) {
173
167
  present(e, 'settings.sharedStacks.detect.failed')
@@ -207,13 +201,9 @@ async function saveStack() {
207
201
  await store.create(payload)
208
202
  }
209
203
  resetForm()
210
- toast.add({
211
- title: t(
212
- editing ? 'settings.sharedStacks.toast.updated' : 'settings.sharedStacks.toast.created',
213
- ),
214
- icon: 'i-lucide-check',
215
- color: 'success',
216
- })
204
+ actionToast.success(
205
+ editing ? 'settings.sharedStacks.toast.updated' : 'settings.sharedStacks.toast.created',
206
+ )
217
207
  } catch (e) {
218
208
  present(
219
209
  e,
@@ -233,7 +223,7 @@ async function start(stack: SharedStack) {
233
223
  // surface that as an error toast too — not only a thrown transport/unavailable error.
234
224
  const updated = await store.ensureUp(stack.id)
235
225
  if (updated.status === 'failed') {
236
- present(updated.lastError ?? '', 'settings.sharedStacks.toast.startFailed')
226
+ presentReported(updated.lastError, 'settings.sharedStacks.toast.startFailed')
237
227
  }
238
228
  } catch (e) {
239
229
  present(e, 'settings.sharedStacks.toast.startFailed')
@@ -12,6 +12,7 @@
12
12
  // convention); only the chrome around them is i18n.
13
13
  import { onMounted, ref } from 'vue'
14
14
  import type { TaskTypeSuppression } from '~/types/domain'
15
+ import EmptyState from '~/components/common/EmptyState.vue'
15
16
 
16
17
  const { t } = useI18n()
17
18
  const api = useApi()
@@ -65,12 +66,15 @@ async function toggle(row: TaskTypeSuppression, offered: boolean) {
65
66
  {{ t('settings.taskTypeSuppressions.intro') }}
66
67
  </p>
67
68
 
68
- <p v-if="loading" class="text-2xs text-dimmed">
69
- {{ t('settings.taskTypeSuppressions.loading') }}
70
- </p>
71
- <p v-else-if="!rows.length" class="text-2xs text-dimmed">
72
- {{ t('settings.taskTypeSuppressions.empty') }}
73
- </p>
69
+ <div v-if="loading" class="space-y-2">
70
+ <USkeleton v-for="n in 3" :key="n" class="h-12 w-full" />
71
+ </div>
72
+ <EmptyState
73
+ v-else-if="!rows.length"
74
+ compact
75
+ icon="i-lucide-list-checks"
76
+ :title="t('settings.taskTypeSuppressions.empty')"
77
+ />
74
78
  <ul v-else class="space-y-2" data-testid="task-type-suppressions">
75
79
  <li
76
80
  v-for="row in rows"
@@ -5,6 +5,7 @@
5
5
  // only the metered rows. See docs/initiatives/usage-and-quota-tracking.md.
6
6
  import { computed, watch } from 'vue'
7
7
  import type { UsageBreakdownRow } from '@cat-factory/contracts'
8
+ import EmptyState from '~/components/common/EmptyState.vue'
8
9
 
9
10
  const { t, n, d } = useI18n()
10
11
  const usage = useUsageStore()
@@ -46,9 +47,17 @@ const hasAny = computed(() => usage.rows.length > 0)
46
47
  {{ t('settings.usage.period', { date: d(new Date(usage.report.periodStart), 'short') }) }}
47
48
  </p>
48
49
 
49
- <p v-if="usage.loading" class="text-2xs text-dimmed">{{ t('common.loading') }}</p>
50
+ <div v-if="usage.loading" class="space-y-3">
51
+ <USkeleton class="h-5 w-1/3" />
52
+ <USkeleton v-for="n in 3" :key="n" class="h-8 w-full" />
53
+ </div>
50
54
  <p v-else-if="usage.error" class="text-2xs text-app-error-400">{{ usage.error }}</p>
51
- <p v-else-if="!hasAny" class="text-2xs text-dimmed">{{ t('settings.usage.empty') }}</p>
55
+ <EmptyState
56
+ v-else-if="!hasAny"
57
+ compact
58
+ icon="i-lucide-chart-bar"
59
+ :title="t('settings.usage.empty')"
60
+ />
52
61
 
53
62
  <template v-else>
54
63
  <!-- Subscriptions (flat-rate quota harnesses) -->
@@ -15,7 +15,7 @@ import SectionLabel from '~/components/common/SectionLabel.vue'
15
15
  const { t } = useI18n()
16
16
  const ui = useUiStore()
17
17
  const store = useUserSecretsStore()
18
- const toast = useToast()
18
+ const actionToast = useActionToast()
19
19
  const { present } = usePipelineErrorToast()
20
20
  const { confirm } = useConfirm()
21
21
 
@@ -105,12 +105,10 @@ async function save() {
105
105
  // (backend/docs/security-model.md). Best-effort: the save already succeeded, so a probe
106
106
  // failure must not read as one.
107
107
  testResult.value = await store.test(kind.value, payload).catch(() => null)
108
- toast.add({
109
- title: t('settings.userSecrets.toast.saved', {
108
+ actionToast.success('settings.userSecrets.toast.saved', {
109
+ params: {
110
110
  label: descriptor.value?.label ?? t('settings.userSecrets.secretFallback'),
111
- }),
112
- icon: 'i-lucide-check',
113
- color: 'success',
111
+ },
114
112
  })
115
113
  } catch (e) {
116
114
  present(e, 'settings.userSecrets.toast.saveFailed')
@@ -134,7 +132,7 @@ async function remove() {
134
132
  try {
135
133
  await store.remove(kind.value)
136
134
  resetDraft()
137
- toast.add({ title: t('settings.userSecrets.toast.removed'), icon: 'i-lucide-check' })
135
+ actionToast.success('settings.userSecrets.toast.removed')
138
136
  } catch (e) {
139
137
  present(e, 'settings.userSecrets.toast.removeFailed')
140
138
  } finally {
@@ -18,11 +18,12 @@ import {
18
18
  import type { WorkspaceMetadataFieldDefinition } from '~/modular/workspace-metadata'
19
19
  import type { AppSlots } from '~/modular/slots'
20
20
  import SectionLabel from '~/components/common/SectionLabel.vue'
21
+ import EmptyState from '~/components/common/EmptyState.vue'
21
22
 
22
23
  const { t } = useI18n()
23
24
  const slots = useReactiveSlots<AppSlots>()
24
25
  const store = useWorkspaceSettingsStore()
25
- const toast = useToast()
26
+ const actionToast = useActionToast()
26
27
  const { present } = usePipelineErrorToast()
27
28
 
28
29
  /**
@@ -67,11 +68,7 @@ async function save() {
67
68
  await store.update({
68
69
  metadata: metadataPatchFrom(fields.value, draft, store.settings.metadata),
69
70
  })
70
- toast.add({
71
- title: t('settings.workspaceSettings.toast.saved'),
72
- icon: 'i-lucide-check',
73
- color: 'success',
74
- })
71
+ actionToast.success('settings.workspaceSettings.toast.saved')
75
72
  } catch (e) {
76
73
  present(e, 'settings.workspaceSettings.toast.saveFailed')
77
74
  } finally {
@@ -99,9 +96,12 @@ function selectItems(field: WorkspaceMetadataFieldDefinition) {
99
96
  </p>
100
97
  </section>
101
98
 
102
- <p v-if="fields.length === 0" class="text-2xs text-dimmed">
103
- {{ t('settings.workspaceSettings.metadata.empty') }}
104
- </p>
99
+ <EmptyState
100
+ v-if="fields.length === 0"
101
+ compact
102
+ icon="i-lucide-tags"
103
+ :title="t('settings.workspaceSettings.metadata.empty')"
104
+ />
105
105
 
106
106
  <template v-else>
107
107
  <div class="space-y-4">
@@ -32,7 +32,7 @@ const store = useWorkspaceSettingsStore()
32
32
  const workspace = useWorkspaceStore()
33
33
  const access = useWorkspaceAccess()
34
34
  const pipelines = usePipelinesStore()
35
- const toast = useToast()
35
+ const actionToast = useActionToast()
36
36
  const { present } = usePipelineErrorToast()
37
37
  const slots = useReactiveSlots<AppSlots>()
38
38
  // Whether the deployment registers any reusable operation at all, hidden or not, so the
@@ -283,11 +283,7 @@ async function save() {
283
283
  // Mirror the backend's enforce-mode validation client-side so the user gets an immediate,
284
284
  // localized message instead of the raw 422 (enforce needs at least one hard trigger).
285
285
  if (draft.reviewFrictionMode === 'enforce' && blockCount == null && blockStuckMinutes == null) {
286
- toast.add({
287
- title: t('settings.workspaceSettings.reviewFriction.needsTrigger'),
288
- icon: 'i-lucide-triangle-alert',
289
- color: 'warning',
290
- })
286
+ actionToast.error('settings.workspaceSettings.reviewFriction.needsTrigger')
291
287
  return
292
288
  }
293
289
  saving.value = true
@@ -322,11 +318,7 @@ async function save() {
322
318
  reviewFrictionBlockCount: blockCount,
323
319
  reviewFrictionBlockStuckMinutes: blockStuckMinutes,
324
320
  })
325
- toast.add({
326
- title: t('settings.workspaceSettings.toast.saved'),
327
- icon: 'i-lucide-check',
328
- color: 'success',
329
- })
321
+ actionToast.success('settings.workspaceSettings.toast.saved')
330
322
  } catch (e) {
331
323
  present(e, 'settings.workspaceSettings.toast.saveFailed')
332
324
  } finally {
@@ -18,6 +18,7 @@ import { computed, ref } from 'vue'
18
18
  import { MAX_REVIEW_SKILLS } from '@cat-factory/contracts'
19
19
  import { useSkillsStore } from '~/stores/skills'
20
20
  import SectionLabel from '~/components/common/SectionLabel.vue'
21
+ import EmptyState from '~/components/common/EmptyState.vue'
21
22
 
22
23
  const props = defineProps<{
23
24
  /** The queued skill ids, in the order the reviewer applies them (`v-model`). */
@@ -109,9 +110,12 @@ function toggle(id: string) {
109
110
  </span>
110
111
  </UButton>
111
112
  </template>
112
- <p v-else class="px-2 py-3 text-xs text-dimmed">
113
- {{ t('skills.reviewQueue.pickerEmpty') }}
114
- </p>
113
+ <EmptyState
114
+ v-else
115
+ compact
116
+ icon="i-lucide-book-open-check"
117
+ :title="t('skills.reviewQueue.pickerEmpty')"
118
+ />
115
119
  </div>
116
120
 
117
121
  <p