@cat-factory/app 0.311.2 → 0.312.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/README.md +92 -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/board/nodes/TaskCard.vue +45 -212
  17. package/app/components/bootstrap/AdoptionReviewModal.vue +2 -5
  18. package/app/components/bootstrap/BootstrapModal.vue +6 -15
  19. package/app/components/bootstrap/BootstrapRunSteps.vue +3 -1
  20. package/app/components/brainstorm/BrainstormWindow.vue +32 -26
  21. package/app/components/bugFishing/BugFishingWindow.vue +15 -13
  22. package/app/components/clarity/ClarityReviewWindow.vue +31 -26
  23. package/app/components/common/ClarificationItem.vue +3 -1
  24. package/app/components/common/EmptyState.vue +41 -19
  25. package/app/components/common/InterviewGateNotice.vue +3 -1
  26. package/app/components/common/Spinner.vue +28 -0
  27. package/app/components/consensus/ConsensusSessionWindow.vue +5 -5
  28. package/app/components/docs/DocInterviewWindow.vue +6 -6
  29. package/app/components/documents/ContextDocumentPicker.vue +2 -2
  30. package/app/components/documents/DocumentImportModal.vue +9 -8
  31. package/app/components/documents/DocumentSourceConnectModal.vue +10 -10
  32. package/app/components/documents/DocumentTemplatesModal.vue +13 -7
  33. package/app/components/documents/RepoContextDocPicker.vue +2 -2
  34. package/app/components/documents/SpawnPreviewModal.vue +4 -6
  35. package/app/components/documents/StartFromDesignModal.vue +2 -1
  36. package/app/components/documents/TaskContextDocs.vue +4 -5
  37. package/app/components/environments/EnvironmentStatusPanel.vue +11 -8
  38. package/app/components/environments/steps/EnvPickStep.vue +7 -3
  39. package/app/components/environments/steps/EnvPreflightStep.vue +6 -5
  40. package/app/components/focus/BlockFocusView.vue +6 -5
  41. package/app/components/followUp/FollowUpWindow.vue +7 -9
  42. package/app/components/forkDecision/ForkDecisionWindow.vue +9 -8
  43. package/app/components/foundational/FoundationalServiceCatalogList.vue +9 -5
  44. package/app/components/foundational/FoundationalServiceRegistry.vue +11 -7
  45. package/app/components/foundational/FoundationalServiceSources.vue +20 -22
  46. package/app/components/foundational/FoundationalSuppressions.vue +2 -2
  47. package/app/components/foundational/ServiceCatalogConnection.vue +15 -15
  48. package/app/components/fragments/FragmentLibraryManager.vue +36 -34
  49. package/app/components/fragments/FragmentSelector.vue +7 -3
  50. package/app/components/gates/GateResultView.vue +6 -9
  51. package/app/components/github/AddServiceFromRepoModal.vue +6 -10
  52. package/app/components/github/GitHubConnect.vue +10 -17
  53. package/app/components/github/GitHubPanel.vue +44 -47
  54. package/app/components/github/RepoTreeBrowser.vue +9 -5
  55. package/app/components/guidedReview/GuidedReviewModal.vue +16 -6
  56. package/app/components/guidedReview/GuidedReviewOverview.vue +2 -1
  57. package/app/components/guidedReview/GuidedReviewPicker.vue +10 -7
  58. package/app/components/guidedReview/GuidedReviewThread.vue +9 -4
  59. package/app/components/humanTest/HumanTestWindow.vue +2 -1
  60. package/app/components/initiative/InitiativePlanningWindow.vue +6 -6
  61. package/app/components/initiative/InitiativeTrackerWindow.vue +11 -11
  62. package/app/components/judge/JudgeResultView.vue +2 -7
  63. package/app/components/kaizen/KaizenPanel.vue +20 -5
  64. package/app/components/kaizen/KaizenStepStatus.vue +2 -1
  65. package/app/components/layout/AccountAuditLog.vue +11 -3
  66. package/app/components/layout/AccountDeploymentSettings.vue +17 -62
  67. package/app/components/layout/AccountFailureKindRules.vue +6 -5
  68. package/app/components/layout/AccountModelPolicySettings.vue +2 -6
  69. package/app/components/layout/AccountPlatformAlertSettings.vue +5 -13
  70. package/app/components/layout/AccountRiskPolicySettings.vue +13 -14
  71. package/app/components/layout/AccountRunCredentialSettings.vue +2 -2
  72. package/app/components/layout/AccountTeamSettings.vue +12 -9
  73. package/app/components/layout/BoardSwitcher.vue +2 -2
  74. package/app/components/layout/BoardToolbar.vue +27 -37
  75. package/app/components/layout/BoardTopOverlays.vue +8 -2
  76. package/app/components/layout/CommandBar.vue +7 -3
  77. package/app/components/layout/ConnectionStatusBanner.vue +2 -1
  78. package/app/components/layout/IntegrationsHub.vue +7 -3
  79. package/app/components/layout/ModelProvidersHub.vue +7 -3
  80. package/app/components/layout/NotificationsInbox.vue +3 -11
  81. package/app/components/layout/SideBar.vue +5 -0
  82. package/app/components/layout/WorkViewSwitcher.vue +64 -0
  83. package/app/components/layout/WorkspaceMembersSettings.vue +12 -5
  84. package/app/components/media/ArtifactLightbox.vue +4 -3
  85. package/app/components/notifications/NotificationSettingsPanel.vue +4 -11
  86. package/app/components/observability/ToolCallList.vue +12 -17
  87. package/app/components/panels/AgentStepDetail.vue +5 -5
  88. package/app/components/panels/GenericStructuredResultView.vue +6 -9
  89. package/app/components/panels/InspectorPanel.vue +2 -1
  90. package/app/components/panels/MergerResultView.vue +5 -6
  91. package/app/components/panels/ObservabilityPanel.vue +26 -41
  92. package/app/components/panels/OperatorDashboardPanel.vue +29 -17
  93. package/app/components/panels/ReportsPanel.vue +17 -11
  94. package/app/components/panels/ReportsSpendBreakdown.vue +7 -3
  95. package/app/components/panels/RunDetailLoadState.vue +4 -3
  96. package/app/components/panels/StepContainerStatus.vue +4 -3
  97. package/app/components/panels/StepDelegatedStatus.vue +3 -2
  98. package/app/components/panels/StepMetadataCard.vue +6 -8
  99. package/app/components/panels/StepRunMeta.vue +2 -5
  100. package/app/components/panels/inspector/ContainerSummary.vue +7 -1
  101. package/app/components/panels/inspector/EpicChildren.vue +7 -3
  102. package/app/components/panels/inspector/FrontendConfig.vue +7 -3
  103. package/app/components/panels/inspector/InitiativeInspector.vue +5 -2
  104. package/app/components/panels/inspector/ServiceConnections.vue +7 -3
  105. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +2 -6
  106. package/app/components/panels/inspector/ServiceTestSecrets.vue +2 -6
  107. package/app/components/panels/inspector/ServiceTestingContext.vue +2 -6
  108. package/app/components/panels/inspector/ServiceValidationConfig.vue +38 -39
  109. package/app/components/panels/inspector/TaskExecution.vue +5 -6
  110. package/app/components/panels/inspector/TaskRunSettings.vue +7 -3
  111. package/app/components/panels/inspector/TaskStructure.vue +0 -1
  112. package/app/components/pipeline/AgentPromptEditor.vue +25 -18
  113. package/app/components/pipeline/PipelineBuilder.vue +14 -24
  114. package/app/components/pipeline/PipelineProgress.vue +16 -17
  115. package/app/components/prReview/PrReviewPhaseBadge.vue +3 -2
  116. package/app/components/prReview/PrReviewWindow.vue +16 -20
  117. package/app/components/providers/ApiKeysSection.vue +2 -6
  118. package/app/components/providers/PersonalCredentialModal.vue +2 -6
  119. package/app/components/providers/PersonalSubscriptionSection.vue +3 -7
  120. package/app/components/providers/VendorCredentialsModal.vue +2 -6
  121. package/app/components/provisioning/ProvisioningLogsDrawer.vue +10 -6
  122. package/app/components/queue/QueueCard.vue +269 -0
  123. package/app/components/queue/QueueView.vue +192 -0
  124. package/app/components/ralph/RalphLoopResultView.vue +2 -7
  125. package/app/components/requirements/RequirementsReviewWindow.vue +44 -39
  126. package/app/components/sandbox/SandboxPanel.vue +37 -25
  127. package/app/components/settings/AccountSettingsPanel.vue +6 -3
  128. package/app/components/settings/ApiTokensPanel.vue +2 -6
  129. package/app/components/settings/BudgetSettings.vue +2 -2
  130. package/app/components/settings/CapabilityCredentialsPanel.vue +2 -6
  131. package/app/components/settings/CloudflareHandlerSection.vue +3 -7
  132. package/app/components/settings/ConsensusGroupsSection.vue +7 -3
  133. package/app/components/settings/CustomManifestTypeEditor.vue +7 -3
  134. package/app/components/settings/DefaultProvisionTypeSection.vue +2 -2
  135. package/app/components/settings/InfraHandlersConfigurator.vue +8 -11
  136. package/app/components/settings/IssueTrackerPanel.vue +9 -9
  137. package/app/components/settings/LocalContainerPoolSettings.vue +2 -6
  138. package/app/components/settings/LocalModelEndpointsPanel.vue +5 -7
  139. package/app/components/settings/McpAuthorizeScreen.vue +8 -3
  140. package/app/components/settings/McpOAuthCallbackScreen.vue +2 -1
  141. package/app/components/settings/ModelConfigurationPanel.vue +45 -17
  142. package/app/components/settings/ObservabilityConnectionPanel.vue +4 -15
  143. package/app/components/settings/OpenRouterCatalogPanel.vue +8 -24
  144. package/app/components/settings/PackageRegistriesPanel.vue +2 -6
  145. package/app/components/settings/ProviderConnectionTab.vue +3 -7
  146. package/app/components/settings/RiskPolicyPanel.vue +4 -16
  147. package/app/components/settings/SharedStacksPanel.vue +8 -18
  148. package/app/components/settings/TaskTypeSuppressionsPanel.vue +10 -6
  149. package/app/components/settings/UsageSettings.vue +11 -2
  150. package/app/components/settings/UserSecretsSection.vue +5 -7
  151. package/app/components/settings/WorkspaceMetadataSettings.vue +9 -9
  152. package/app/components/settings/WorkspaceSettingsPanel.vue +3 -11
  153. package/app/components/skills/ReviewSkillQueue.vue +7 -3
  154. package/app/components/skills/SkillLibraryManager.vue +20 -18
  155. package/app/components/slack/SlackPanel.vue +5 -8
  156. package/app/components/spec/ServiceSpecWindow.vue +32 -29
  157. package/app/components/tasks/BugHuntModal.vue +23 -19
  158. package/app/components/tasks/ContextIssuePicker.vue +2 -2
  159. package/app/components/tasks/TaskContextIssues.vue +4 -5
  160. package/app/components/tasks/TaskImportModal.vue +10 -8
  161. package/app/components/tasks/TaskSourceConnectModal.vue +11 -11
  162. package/app/components/testing/TestReportWindow.vue +6 -12
  163. package/app/components/tutorial/TutorialCatalogue.vue +7 -3
  164. package/app/components/tutorial/TutorialOverlay.vue +2 -1
  165. package/app/components/tutorial/TutorialPrompt.vue +7 -3
  166. package/app/components/vcs/GitLabConnect.vue +2 -6
  167. package/app/components/visualConfirm/VisualConfirmationWindow.vue +9 -4
  168. package/app/composables/toastTitleKeys.spec.ts +143 -0
  169. package/app/composables/useActionToast.spec.ts +71 -0
  170. package/app/composables/useActionToast.ts +101 -0
  171. package/app/composables/useBlockDeletion.ts +3 -6
  172. package/app/composables/useConfirmAction.ts +5 -3
  173. package/app/composables/useContextLinking.spec.ts +3 -0
  174. package/app/composables/useContextLinking.ts +11 -16
  175. package/app/composables/useCopyToClipboard.ts +4 -3
  176. package/app/composables/useFrameLanes.ts +6 -82
  177. package/app/composables/useNavContributions.ts +6 -5
  178. package/app/composables/usePipelineErrorToast.spec.ts +24 -0
  179. package/app/composables/usePipelineErrorToast.ts +26 -2
  180. package/app/composables/usePipelineLibraryActions.ts +8 -9
  181. package/app/composables/useTaskActions.ts +254 -0
  182. package/app/composables/useTaskLaneClassifier.ts +88 -0
  183. package/app/modular/nav-contributions.spec.ts +4 -0
  184. package/app/modular/nav-contributions.ts +31 -0
  185. package/app/modular/tutorial-tours.ts +10 -0
  186. package/app/pages/index.vue +42 -8
  187. package/app/stores/board/context.ts +4 -3
  188. package/app/stores/board/placement.ts +8 -20
  189. package/app/stores/board/removal.ts +4 -13
  190. package/app/stores/board.spec.ts +10 -1
  191. package/app/stores/board.ts +3 -2
  192. package/app/stores/workView.ts +51 -0
  193. package/app/stores/workspaceQueue.ts +148 -0
  194. package/app/utils/bootstrapSteps.ts +11 -2
  195. package/app/utils/pipelineRender.ts +21 -8
  196. package/app/utils/queueSections.spec.ts +111 -0
  197. package/app/utils/queueSections.ts +117 -0
  198. package/app/utils/workView.spec.ts +16 -0
  199. package/app/utils/workView.ts +25 -0
  200. package/i18n/locales/de.json +73 -32
  201. package/i18n/locales/en.json +73 -32
  202. package/i18n/locales/es.json +73 -32
  203. package/i18n/locales/fr.json +73 -32
  204. package/i18n/locales/he.json +73 -32
  205. package/i18n/locales/it.json +73 -32
  206. package/i18n/locales/ja.json +73 -32
  207. package/i18n/locales/pl.json +73 -32
  208. package/i18n/locales/tr.json +73 -32
  209. package/i18n/locales/uk.json +73 -32
  210. package/package.json +1 -1
@@ -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
@@ -13,12 +13,13 @@ 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
15
  import IconButton from '~/components/common/IconButton.vue'
16
+ import EmptyState from '~/components/common/EmptyState.vue'
16
17
 
17
18
  const props = defineProps<{ accountId: string }>()
18
19
 
19
20
  const library = useSkillLibrary(props.accountId)
20
21
  const github = useGitHubStore()
21
- const toast = useToast()
22
+ const actionToast = useActionToast()
22
23
  const { present } = usePipelineErrorToast()
23
24
  const { t, d } = useI18n()
24
25
  const { confirm } = useConfirm()
@@ -85,7 +86,7 @@ async function linkSource() {
85
86
  gitRef: sourceRef.value.trim() || undefined,
86
87
  })
87
88
  resetSourceDraft()
88
- toast.add({ title: t('skills.toast.sourceLinked'), icon: 'i-lucide-git-branch' })
89
+ actionToast.success('skills.toast.sourceLinked')
89
90
  } catch (e) {
90
91
  present(e, 'skills.toast.linkSourceFailed')
91
92
  } finally {
@@ -97,13 +98,11 @@ async function syncSource(id: string) {
97
98
  await withRow(`sync:${id}`, async () => {
98
99
  try {
99
100
  const result = await library.syncSource(id)
100
- toast.add({
101
- title: t('skills.toast.synced', {
101
+ actionToast.success('skills.toast.synced', {
102
+ params: {
102
103
  updated: result.upserted,
103
104
  removed: result.tombstoned,
104
- }),
105
- icon: 'i-lucide-refresh-cw',
106
- color: 'info',
105
+ },
107
106
  })
108
107
  } catch (e) {
109
108
  present(e, 'skills.toast.syncFailed')
@@ -115,10 +114,7 @@ async function checkSource(id: string) {
115
114
  await withRow(`check:${id}`, async () => {
116
115
  try {
117
116
  const status = await library.checkSource(id)
118
- toast.add({
119
- title: status.changed ? t('skills.toast.changesAvailable') : t('skills.toast.upToDate'),
120
- icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
121
- })
117
+ actionToast.info(status.changed ? 'skills.toast.changesAvailable' : 'skills.toast.upToDate')
122
118
  } catch (e) {
123
119
  present(e, 'skills.toast.checkSourceFailed')
124
120
  }
@@ -139,7 +135,7 @@ async function unlinkSource(id: string) {
139
135
  await withRow(`unlink:${id}`, async () => {
140
136
  try {
141
137
  await library.unlinkSource(id)
142
- toast.add({ title: t('skills.toast.sourceUnlinked'), icon: 'i-lucide-unplug' })
138
+ actionToast.success('skills.toast.sourceUnlinked')
143
139
  } catch (e) {
144
140
  present(e, 'skills.toast.unlinkSourceFailed')
145
141
  }
@@ -195,9 +191,12 @@ async function unlinkSource(id: string) {
195
191
  </p>
196
192
  </div>
197
193
  </div>
198
- <p v-if="!library.catalog.length" class="text-sm text-dimmed">
199
- {{ t('skills.catalog.empty') }}
200
- </p>
194
+ <EmptyState
195
+ v-if="!library.catalog.length"
196
+ compact
197
+ icon="i-lucide-book-open-check"
198
+ :title="t('skills.catalog.empty')"
199
+ />
201
200
  </div>
202
201
 
203
202
  <!-- Repo sources -->
@@ -264,9 +263,12 @@ async function unlinkSource(id: string) {
264
263
  />
265
264
  </div>
266
265
  </div>
267
- <p v-if="!library.sources.length" class="text-sm text-dimmed">
268
- {{ t('skills.sources.empty') }}
269
- </p>
266
+ <EmptyState
267
+ v-if="!library.sources.length"
268
+ compact
269
+ icon="i-lucide-git-branch"
270
+ :title="t('skills.sources.empty')"
271
+ />
270
272
 
271
273
  <!-- Link a new source. Needs the GitHub integration; hide the form when it's off. -->
272
274
  <div
@@ -20,7 +20,7 @@ import SectionLabel from '~/components/common/SectionLabel.vue'
20
20
 
21
21
  const ui = useUiStore()
22
22
  const slack = useSlackStore()
23
- const toast = useToast()
23
+ const actionToast = useActionToast()
24
24
  const { present } = usePipelineErrorToast()
25
25
  const { t } = useI18n()
26
26
  const { confirm } = useConfirm()
@@ -134,7 +134,7 @@ async function connectWithToken() {
134
134
  try {
135
135
  await slack.connectWithToken(tokenInput.value.trim())
136
136
  tokenInput.value = ''
137
- toast.add({ title: t('slack.toast.connected'), icon: 'i-lucide-check', color: 'success' })
137
+ actionToast.success('slack.toast.connected')
138
138
  } catch (e) {
139
139
  present(e, 'slack.error.connect')
140
140
  }
@@ -163,7 +163,7 @@ async function saveRouting() {
163
163
  routes: { ...routes },
164
164
  mentionsEnabled: mentionsEnabled.value,
165
165
  })
166
- toast.add({ title: t('slack.toast.routingSaved'), icon: 'i-lucide-check', color: 'success' })
166
+ actionToast.success('slack.toast.routingSaved')
167
167
  } catch (e) {
168
168
  present(e, 'slack.error.saveRouting')
169
169
  } finally {
@@ -182,11 +182,8 @@ async function saveMapping() {
182
182
  // dropped on save (UX-23) — block instead so the user doesn't lose the entry. A
183
183
  // fully-empty row is just an unused slot and is ignored.
184
184
  if (hasHalfFilledRow(mapping.value)) {
185
- toast.add({
186
- title: t('slack.members.incompleteTitle'),
185
+ actionToast.error('slack.members.incompleteTitle', {
187
186
  description: t('slack.members.incompleteBody'),
188
- icon: 'i-lucide-triangle-alert',
189
- color: 'warning',
190
187
  })
191
188
  return
192
189
  }
@@ -195,7 +192,7 @@ async function saveMapping() {
195
192
  const entries = toMemberEntries(mapping.value)
196
193
  await slack.updateMemberMapping(entries)
197
194
  mapping.value = slack.memberMapping.map((e) => toMemberRow(e, nextUid()))
198
- toast.add({ title: t('slack.toast.mapSaved'), icon: 'i-lucide-check', color: 'success' })
195
+ actionToast.success('slack.toast.mapSaved')
199
196
  } catch (e) {
200
197
  present(e, 'slack.error.saveMap')
201
198
  } finally {