@cat-factory/app 0.261.2 → 0.261.4

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 (90) hide show
  1. package/README.md +40 -0
  2. package/app/components/board/AddTaskModal.vue +10 -6
  3. package/app/components/board/AgentStopButton.vue +2 -5
  4. package/app/components/board/CreateInitiativeModal.vue +2 -7
  5. package/app/components/board/RecurringPipelineModal.vue +10 -6
  6. package/app/components/bootstrap/BootstrapModal.vue +5 -24
  7. package/app/components/brainstorm/BrainstormWindow.vue +7 -15
  8. package/app/components/clarity/ClarityReviewWindow.vue +7 -15
  9. package/app/components/documents/DocumentImportModal.vue +2 -6
  10. package/app/components/documents/DocumentSourceConnectModal.vue +3 -12
  11. package/app/components/documents/DocumentSyncState.vue +2 -7
  12. package/app/components/documents/DocumentTemplatesModal.vue +4 -19
  13. package/app/components/documents/SpawnPreviewModal.vue +3 -12
  14. package/app/components/foundational/FoundationalServiceCatalogList.vue +3 -11
  15. package/app/components/foundational/FoundationalServiceRegistry.vue +3 -11
  16. package/app/components/foundational/FoundationalServiceSources.vue +5 -13
  17. package/app/components/foundational/FoundationalSuppressions.vue +2 -6
  18. package/app/components/fragments/FragmentLibraryManager.vue +12 -20
  19. package/app/components/github/AddServiceFromRepoModal.vue +3 -12
  20. package/app/components/github/GitHubConnect.vue +4 -12
  21. package/app/components/github/GitHubPanel.vue +9 -17
  22. package/app/components/github/RepoTreeBrowser.vue +2 -7
  23. package/app/components/humanTest/HumanTestWindow.vue +2 -7
  24. package/app/components/initiative/InitiativeTrackerWindow.vue +2 -3
  25. package/app/components/layout/AccountAuditLog.vue +2 -7
  26. package/app/components/layout/AccountDeploymentSettings.vue +11 -51
  27. package/app/components/layout/AccountModelPolicySettings.vue +2 -5
  28. package/app/components/layout/AccountPlatformAlertSettings.vue +2 -5
  29. package/app/components/layout/AccountRunCredentialSettings.vue +2 -5
  30. package/app/components/layout/AccountTeamSettings.vue +9 -18
  31. package/app/components/layout/BoardSwitcher.vue +6 -14
  32. package/app/components/layout/BoardToolbar.vue +3 -10
  33. package/app/components/layout/NotificationsInbox.vue +3 -13
  34. package/app/components/layout/WorkspaceMembersSettings.vue +6 -15
  35. package/app/components/notifications/NotificationSettingsPanel.vue +3 -11
  36. package/app/components/panels/DecisionModal.vue +2 -7
  37. package/app/components/panels/inspector/RecurringScheduleSettings.vue +4 -20
  38. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +3 -11
  39. package/app/components/panels/inspector/ServiceTestSecrets.vue +3 -11
  40. package/app/components/panels/inspector/ServiceValidationConfig.vue +4 -12
  41. package/app/components/pipeline/PipelineBuilder.vue +4 -7
  42. package/app/components/providers/ApiKeysSection.vue +4 -15
  43. package/app/components/providers/PersonalCredentialModal.vue +2 -5
  44. package/app/components/providers/PersonalSubscriptionSection.vue +3 -10
  45. package/app/components/providers/VendorCredentialsModal.vue +5 -20
  46. package/app/components/requirements/RequirementsReviewWindow.vue +11 -19
  47. package/app/components/sandbox/SandboxPanel.vue +4 -18
  48. package/app/components/settings/ApiTokensPanel.vue +4 -12
  49. package/app/components/settings/BudgetSettings.vue +2 -5
  50. package/app/components/settings/CloudflareHandlerSection.vue +3 -12
  51. package/app/components/settings/ConsensusGroupsSection.vue +3 -12
  52. package/app/components/settings/CustomManifestTypeEditor.vue +3 -13
  53. package/app/components/settings/DefaultProvisionTypeSection.vue +2 -5
  54. package/app/components/settings/InfraHandlersConfigurator.vue +7 -14
  55. package/app/components/settings/InfrastructureBackendPicker.vue +2 -7
  56. package/app/components/settings/IssueTrackerPanel.vue +4 -18
  57. package/app/components/settings/LocalContainerPoolSettings.vue +2 -6
  58. package/app/components/settings/LocalModelEndpointsPanel.vue +3 -11
  59. package/app/components/settings/ModelConfigurationPanel.vue +13 -16
  60. package/app/components/settings/ModelPresetHealthModal.vue +3 -13
  61. package/app/components/settings/ObservabilityConnectionPanel.vue +6 -14
  62. package/app/components/settings/OpenRouterCatalogPanel.vue +3 -12
  63. package/app/components/settings/PackageRegistriesPanel.vue +4 -12
  64. package/app/components/settings/ProviderConnectionTab.vue +5 -13
  65. package/app/components/settings/RiskPolicyHealthModal.vue +2 -7
  66. package/app/components/settings/RiskPolicyPanel.vue +5 -13
  67. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +2 -7
  68. package/app/components/settings/SharedStacksPanel.vue +10 -20
  69. package/app/components/settings/TaskTypeSuppressionsPanel.vue +3 -12
  70. package/app/components/settings/UserSecretsSection.vue +3 -11
  71. package/app/components/settings/WorkspaceMetadataSettings.vue +2 -6
  72. package/app/components/settings/WorkspaceSettingsPanel.vue +2 -6
  73. package/app/components/skills/SkillLibraryManager.vue +5 -13
  74. package/app/components/slack/SlackPanel.vue +7 -15
  75. package/app/components/tasks/BugHuntModal.vue +2 -6
  76. package/app/components/tasks/TaskImportModal.vue +2 -6
  77. package/app/components/tasks/TaskSourceConnectModal.vue +4 -18
  78. package/app/composables/useBlockDeletion.ts +2 -6
  79. package/app/composables/useCopyToClipboard.ts +7 -1
  80. package/app/composables/usePipelineErrorToast.spec.ts +76 -16
  81. package/app/composables/usePipelineErrorToast.ts +109 -50
  82. package/app/stores/board/context.ts +9 -2
  83. package/app/stores/board/dependencies.ts +3 -13
  84. package/app/stores/board/mutations.ts +2 -7
  85. package/app/stores/board/placement.ts +12 -25
  86. package/app/stores/board/removal.ts +2 -7
  87. package/app/stores/board.spec.ts +15 -8
  88. package/app/stores/board.ts +11 -1
  89. package/app/stores/recurringPipelines.ts +5 -11
  90. package/package.json +2 -2
@@ -1,6 +1,5 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, onMounted, ref, watch } from 'vue'
3
- import { apiErrorEnvelope } from '~/composables/api/errors'
4
3
  import type { AccountRole } from '~/types/domain'
5
4
  import type { InvitationStatus } from '@cat-factory/contracts'
6
5
  import AccountAuditLog from '~/components/layout/AccountAuditLog.vue'
@@ -20,6 +19,7 @@ const accounts = useAccountsStore()
20
19
  const uiMode = useUiModeStore()
21
20
  const auth = useAuthStore()
22
21
  const toast = useToast()
22
+ const { present } = usePipelineErrorToast()
23
23
  const { t, te } = useI18n()
24
24
  const { confirmAction, toastDone } = useConfirmAction()
25
25
  const busy = ref(false)
@@ -64,7 +64,7 @@ async function updateMemberRoles(userId: string, roles: AccountRole[]) {
64
64
  try {
65
65
  await accounts.setMemberRoles(props.accountId, userId, roles.length ? roles : ['developer'])
66
66
  } catch (e) {
67
- notifyError(t('layout.accountTeam.errors.updateRoles'), e)
67
+ present(e, 'layout.accountTeam.errors.updateRoles')
68
68
  }
69
69
  }
70
70
 
@@ -78,19 +78,10 @@ async function revokeSessions(userId: string, label: string) {
78
78
  await accounts.revokeMemberSessions(props.accountId, userId)
79
79
  toast.add({ title: t('layout.accountTeam.members.sessionsRevoked'), icon: 'i-lucide-check' })
80
80
  } catch (e) {
81
- notifyError(t('layout.accountTeam.errors.revokeSessions'), e)
81
+ present(e, 'layout.accountTeam.errors.revokeSessions')
82
82
  }
83
83
  }
84
84
 
85
- function notifyError(title: string, e: unknown) {
86
- toast.add({
87
- title,
88
- description: apiErrorEnvelope(e)?.message ?? (e instanceof Error ? e.message : String(e)),
89
- icon: 'i-lucide-triangle-alert',
90
- color: 'error',
91
- })
92
- }
93
-
94
85
  async function loadAll(accountId: string) {
95
86
  try {
96
87
  const jobs: Promise<unknown>[] = [accounts.loadEmailConnection(accountId)]
@@ -98,7 +89,7 @@ async function loadAll(accountId: string) {
98
89
  if (isOrg.value) jobs.push(accounts.loadRoster(accountId))
99
90
  await Promise.all(jobs)
100
91
  } catch (e) {
101
- notifyError(t('layout.accountTeam.errors.loadSettings'), e)
92
+ present(e, 'layout.accountTeam.errors.loadSettings')
102
93
  }
103
94
  }
104
95
 
@@ -124,7 +115,7 @@ async function createOrganization() {
124
115
  newOrgName.value = ''
125
116
  toast.add({ title: t('layout.accountTeam.org.created'), icon: 'i-lucide-check' })
126
117
  } catch (e) {
127
- notifyError(t('layout.accountTeam.errors.createOrg'), e)
118
+ present(e, 'layout.accountTeam.errors.createOrg')
128
119
  } finally {
129
120
  busy.value = false
130
121
  }
@@ -152,7 +143,7 @@ async function sendInvite() {
152
143
  icon: 'i-lucide-mail-check',
153
144
  })
154
145
  } catch (e) {
155
- notifyError(t('layout.accountTeam.errors.sendInvite'), e)
146
+ present(e, 'layout.accountTeam.errors.sendInvite')
156
147
  } finally {
157
148
  busy.value = false
158
149
  }
@@ -164,7 +155,7 @@ async function revoke(id: string, email: string) {
164
155
  await accounts.revokeInvite(props.accountId, id)
165
156
  toastDone('revoke', email)
166
157
  } catch (e) {
167
- notifyError(t('layout.accountTeam.errors.revokeInvite'), e)
158
+ present(e, 'layout.accountTeam.errors.revokeInvite')
168
159
  }
169
160
  }
170
161
 
@@ -185,7 +176,7 @@ async function connectEmail() {
185
176
  emailApiKey.value = ''
186
177
  toast.add({ title: t('layout.accountTeam.email.connected'), icon: 'i-lucide-check' })
187
178
  } catch (e) {
188
- notifyError(t('layout.accountTeam.errors.connectEmail'), e)
179
+ present(e, 'layout.accountTeam.errors.connectEmail')
189
180
  } finally {
190
181
  busy.value = false
191
182
  }
@@ -199,7 +190,7 @@ async function disconnectEmail() {
199
190
  await accounts.disconnectEmail(props.accountId)
200
191
  toastDone('disconnect', noun)
201
192
  } catch (e) {
202
- notifyError(t('layout.accountTeam.errors.disconnectEmail'), e)
193
+ present(e, 'layout.accountTeam.errors.disconnectEmail')
203
194
  } finally {
204
195
  busy.value = false
205
196
  }
@@ -19,20 +19,12 @@ const accounts = useAccountsStore()
19
19
  const workspace = useWorkspaceStore()
20
20
  const ui = useUiStore()
21
21
  const toast = useToast()
22
+ const { present } = usePipelineErrorToast()
22
23
  const access = useWorkspaceAccess()
23
24
  const { confirm } = useConfirm()
24
25
 
25
26
  const busy = ref(false)
26
27
 
27
- function notifyError(title: string, e: unknown) {
28
- toast.add({
29
- title,
30
- description: e instanceof Error ? e.message : String(e),
31
- icon: 'i-lucide-triangle-alert',
32
- color: 'error',
33
- })
34
- }
35
-
36
28
  // The cloud provider new services in the active account default to (a service may
37
29
  // override it per-frame). `docker` is the local Docker/Podman backend. The brand
38
30
  // names (Cloudflare/AWS/GCP/Azure) are proper nouns kept verbatim; only the
@@ -52,7 +44,7 @@ async function setDefaultProvider(provider: CloudProvider) {
52
44
  try {
53
45
  await accounts.setDefaultCloudProvider(id, provider)
54
46
  } catch (e) {
55
- notifyError(t('layout.boardSwitcher.toast.updateProviderFailed'), e)
47
+ present(e, 'layout.boardSwitcher.toast.updateProviderFailed')
56
48
  }
57
49
  }
58
50
 
@@ -146,7 +138,7 @@ async function selectAccount(id: string) {
146
138
  try {
147
139
  await workspace.selectAccount(id)
148
140
  } catch (e) {
149
- notifyError(t('layout.boardSwitcher.toast.switchAccountFailed'), e)
141
+ present(e, 'layout.boardSwitcher.toast.switchAccountFailed')
150
142
  } finally {
151
143
  busy.value = false
152
144
  }
@@ -157,7 +149,7 @@ async function switchBoard(id: string) {
157
149
  try {
158
150
  await workspace.switchTo(id)
159
151
  } catch (e) {
160
- notifyError(t('layout.boardSwitcher.toast.openBoardFailed'), e)
152
+ present(e, 'layout.boardSwitcher.toast.openBoardFailed')
161
153
  } finally {
162
154
  busy.value = false
163
155
  }
@@ -181,7 +173,7 @@ async function removeBoard() {
181
173
  await workspace.remove(id)
182
174
  toast.add({ title: t('layout.boardSwitcher.toast.boardDeleted'), icon: 'i-lucide-check' })
183
175
  } catch (e) {
184
- notifyError(t('layout.boardSwitcher.toast.deleteBoardFailed'), e)
176
+ present(e, 'layout.boardSwitcher.toast.deleteBoardFailed')
185
177
  } finally {
186
178
  busy.value = false
187
179
  }
@@ -248,7 +240,7 @@ async function submitPrompt() {
248
240
  }
249
241
  prompt.value = null
250
242
  } catch (e) {
251
- notifyError(t('common.actionFailed'), e)
243
+ present(e, 'common.actionFailed')
252
244
  } finally {
253
245
  busy.value = false
254
246
  }
@@ -15,6 +15,7 @@ const workspace = useWorkspaceStore()
15
15
  const workspaceSettings = useWorkspaceSettingsStore()
16
16
  const services = useServicesStore()
17
17
  const toast = useToast()
18
+ const { present } = usePipelineErrorToast()
18
19
  const access = useWorkspaceAccess()
19
20
  const { t, n } = useI18n()
20
21
  const { fitView, zoomIn, zoomOut, resetZoom } = useBoardFlow()
@@ -28,11 +29,7 @@ async function mountService(serviceId: string, title: string) {
28
29
  color: 'success',
29
30
  })
30
31
  } catch (e) {
31
- toast.add({
32
- title: t('board.toolbar.serviceAddFailed'),
33
- description: e instanceof Error ? e.message : String(e),
34
- color: 'error',
35
- })
32
+ present(e, 'board.toolbar.serviceAddFailed')
36
33
  }
37
34
  }
38
35
 
@@ -59,11 +56,7 @@ async function restoreService(id: string, title: string) {
59
56
  color: 'neutral',
60
57
  })
61
58
  } catch (e) {
62
- toast.add({
63
- title: t('board.toast.restoreFailed'),
64
- description: e instanceof Error ? e.message : String(e),
65
- color: 'error',
66
- })
59
+ present(e, 'board.toast.restoreFailed')
67
60
  }
68
61
  }
69
62
 
@@ -16,20 +16,10 @@ const access = useWorkspaceAccess()
16
16
  const execution = useExecutionStore()
17
17
  const trackRecords = useMergeTrackRecordsStore()
18
18
  const toast = useToast()
19
+ const { present } = usePipelineErrorToast()
19
20
 
20
21
  const busy = ref<string | null>(null)
21
22
 
22
- /** Toast a failed act/dismiss — the store throws, so without this a failure was silent and the
23
- * item just stayed in the inbox with no explanation. */
24
- function notifyError(title: string, e: unknown) {
25
- toast.add({
26
- title,
27
- description: e instanceof Error ? e.message : String(e),
28
- icon: 'i-lucide-triangle-alert',
29
- color: 'error',
30
- })
31
- }
32
-
33
23
  /** Per-type display metadata (icon, colour). The primary-action label is resolved
34
24
  * separately through the i18n catalog (`ACTION_KEYS`). */
35
25
  type Accent = 'warning' | 'primary' | 'error'
@@ -203,7 +193,7 @@ async function act(n: Notification) {
203
193
  icon: 'i-lucide-check',
204
194
  })
205
195
  } catch (e) {
206
- notifyError(t('layout.notifications.toast.actFailed'), e)
196
+ present(e, 'layout.notifications.toast.actFailed')
207
197
  } finally {
208
198
  busy.value = null
209
199
  }
@@ -219,7 +209,7 @@ async function dismiss(n: Notification) {
219
209
  icon: 'i-lucide-check',
220
210
  })
221
211
  } catch (e) {
222
- notifyError(t('layout.notifications.toast.dismissFailed'), e)
212
+ present(e, 'layout.notifications.toast.dismissFailed')
223
213
  } finally {
224
214
  busy.value = null
225
215
  }
@@ -1,6 +1,5 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, onMounted, ref, watch } from 'vue'
3
- import { apiErrorEnvelope } from '~/composables/api/errors'
4
3
  import type { WorkspaceAccessMode, WorkspaceRole } from '~/types/domain'
5
4
 
6
5
  // Workspace-membership management (workspace-rbac initiative, slice 9). The tier BELOW
@@ -18,6 +17,7 @@ const members = useWorkspaceMembersStore()
18
17
  const accounts = useAccountsStore()
19
18
  const auth = useAuthStore()
20
19
  const toast = useToast()
20
+ const { present } = usePipelineErrorToast()
21
21
  const { t } = useI18n()
22
22
  const { confirmAction, toastDone } = useConfirmAction()
23
23
 
@@ -57,15 +57,6 @@ const candidates = computed(() => {
57
57
  const addUserId = ref<string | undefined>(undefined)
58
58
  const addRole = ref<WorkspaceRole>('member')
59
59
 
60
- function notifyError(title: string, e: unknown) {
61
- toast.add({
62
- title,
63
- description: apiErrorEnvelope(e)?.message ?? (e instanceof Error ? e.message : String(e)),
64
- icon: 'i-lucide-triangle-alert',
65
- color: 'error',
66
- })
67
- }
68
-
69
60
  async function loadAll(workspaceId: string) {
70
61
  try {
71
62
  const jobs: Promise<unknown>[] = [members.load(workspaceId)]
@@ -74,7 +65,7 @@ async function loadAll(workspaceId: string) {
74
65
  if (accountId.value) jobs.push(accounts.loadRoster(accountId.value))
75
66
  await Promise.all(jobs)
76
67
  } catch (e) {
77
- notifyError(t('layout.workspaceMembers.errors.load'), e)
68
+ present(e, 'layout.workspaceMembers.errors.load')
78
69
  }
79
70
  }
80
71
 
@@ -92,7 +83,7 @@ async function setAccessMode(next: boolean) {
92
83
  try {
93
84
  await members.setAccessMode(props.workspaceId, mode)
94
85
  } catch (e) {
95
- notifyError(t('layout.workspaceMembers.errors.accessMode'), e)
86
+ present(e, 'layout.workspaceMembers.errors.accessMode')
96
87
  } finally {
97
88
  busy.value = false
98
89
  }
@@ -103,7 +94,7 @@ async function updateRole(userId: string, role: WorkspaceRole) {
103
94
  try {
104
95
  await members.setRole(props.workspaceId, userId, role)
105
96
  } catch (e) {
106
- notifyError(t('layout.workspaceMembers.errors.setRole'), e)
97
+ present(e, 'layout.workspaceMembers.errors.setRole')
107
98
  } finally {
108
99
  busy.value = false
109
100
  }
@@ -119,7 +110,7 @@ async function addMember() {
119
110
  addRole.value = 'member'
120
111
  toast.add({ title: t('layout.workspaceMembers.add.added'), icon: 'i-lucide-user-plus' })
121
112
  } catch (e) {
122
- notifyError(t('layout.workspaceMembers.errors.add'), e)
113
+ present(e, 'layout.workspaceMembers.errors.add')
123
114
  } finally {
124
115
  busy.value = false
125
116
  }
@@ -132,7 +123,7 @@ async function removeMember(userId: string, name: string) {
132
123
  await members.remove(props.workspaceId, userId)
133
124
  toastDone('remove', name)
134
125
  } catch (e) {
135
- notifyError(t('layout.workspaceMembers.errors.remove'), e)
126
+ present(e, 'layout.workspaceMembers.errors.remove')
136
127
  } finally {
137
128
  busy.value = false
138
129
  }
@@ -30,6 +30,7 @@ const ui = useUiStore()
30
30
  const notifications = useNotificationsStore()
31
31
  const slack = useSlackStore()
32
32
  const toast = useToast()
33
+ const { present } = usePipelineErrorToast()
33
34
  const { t } = useI18n()
34
35
 
35
36
  const open = computed({
@@ -70,15 +71,6 @@ function applyMatrix(matrix: NotificationRoutingMatrix | null | undefined) {
70
71
  }
71
72
  }
72
73
 
73
- function notifyError(title: string, e: unknown) {
74
- toast.add({
75
- title,
76
- description: e instanceof Error ? e.message : String(e),
77
- icon: 'i-lucide-triangle-alert',
78
- color: 'error',
79
- })
80
- }
81
-
82
74
  /**
83
75
  * The grid is editable only once the board's OWN matrix is in hand.
84
76
  *
@@ -95,7 +87,7 @@ async function load() {
95
87
  await notifications.loadSettings()
96
88
  applyMatrix(notifications.settings?.matrix)
97
89
  } catch (e) {
98
- notifyError(t('notificationSettings.error.load'), e)
90
+ present(e, 'notificationSettings.error.load')
99
91
  }
100
92
  }
101
93
 
@@ -132,7 +124,7 @@ async function save() {
132
124
  color: 'success',
133
125
  })
134
126
  } catch (e) {
135
- notifyError(t('notificationSettings.error.save'), e)
127
+ present(e, 'notificationSettings.error.save')
136
128
  } finally {
137
129
  busy.value = false
138
130
  }
@@ -4,7 +4,7 @@ import { agentKindMeta } from '~/utils/catalog'
4
4
  const execution = useExecutionStore()
5
5
  const board = useBoardStore()
6
6
  const ui = useUiStore()
7
- const toast = useToast()
7
+ const { present } = usePipelineErrorToast()
8
8
  const { t } = useI18n()
9
9
 
10
10
  const ctx = computed(() => ui.decisionContext)
@@ -44,12 +44,7 @@ async function choose(option: string) {
44
44
  )
45
45
  if (resolved) ui.closeDecision()
46
46
  } catch (e) {
47
- toast.add({
48
- title: t('panels.decision.resolveFailed'),
49
- description: e instanceof Error ? e.message : String(e),
50
- icon: 'i-lucide-triangle-alert',
51
- color: 'error',
52
- })
47
+ present(e, 'panels.decision.resolveFailed')
53
48
  } finally {
54
49
  resolvingOption.value = null
55
50
  }
@@ -8,7 +8,7 @@ import type { Recurrence } from '~/types/recurring'
8
8
  const props = defineProps<{ block: Block }>()
9
9
  const recurring = useRecurringPipelinesStore()
10
10
  const pipelines = usePipelinesStore()
11
- const toast = useToast()
11
+ const { present } = usePipelineErrorToast()
12
12
  const { t, d } = useI18n()
13
13
 
14
14
  const schedule = computed(() => recurring.byBlock(props.block.id))
@@ -72,11 +72,7 @@ async function saveEdit() {
72
72
  await recurring.update(schedule.value.id, { recurrence: draft.value })
73
73
  editing.value = false
74
74
  } catch (e) {
75
- toast.add({
76
- title: t('inspector.recurring.updateFailed'),
77
- description: errMsg(e),
78
- color: 'error',
79
- })
75
+ present(e, 'inspector.recurring.updateFailed')
80
76
  } finally {
81
77
  busy.value = false
82
78
  }
@@ -88,11 +84,7 @@ async function toggleEnabled() {
88
84
  try {
89
85
  await recurring.update(schedule.value.id, { enabled: !schedule.value.enabled })
90
86
  } catch (e) {
91
- toast.add({
92
- title: t('inspector.recurring.updateFailed'),
93
- description: errMsg(e),
94
- color: 'error',
95
- })
87
+ present(e, 'inspector.recurring.updateFailed')
96
88
  } finally {
97
89
  busy.value = false
98
90
  }
@@ -104,20 +96,12 @@ async function runNow() {
104
96
  try {
105
97
  await recurring.runNow(schedule.value.id)
106
98
  } catch (e) {
107
- toast.add({
108
- title: t('inspector.recurring.runNowFailed'),
109
- description: errMsg(e),
110
- color: 'error',
111
- })
99
+ present(e, 'inspector.recurring.runNowFailed')
112
100
  } finally {
113
101
  busy.value = false
114
102
  }
115
103
  }
116
104
 
117
- function errMsg(e: unknown) {
118
- return e instanceof Error ? e.message : String(e)
119
- }
120
-
121
105
  const RUN_COLOR: Record<string, string> = {
122
106
  running: 'text-amber-400',
123
107
  done: 'text-emerald-400',
@@ -14,6 +14,7 @@ const store = useReleaseHealthStore()
14
14
  const ui = useUiStore()
15
15
  const uiMode = useUiModeStore()
16
16
  const toast = useToast()
17
+ const { present } = usePipelineErrorToast()
17
18
  const { t } = useI18n()
18
19
  const { confirmAction, toastDone } = useConfirmAction()
19
20
 
@@ -52,15 +53,6 @@ watch(
52
53
  { immediate: true },
53
54
  )
54
55
 
55
- function notifyError(title: string, e: unknown) {
56
- toast.add({
57
- title,
58
- description: e instanceof Error ? e.message : String(e),
59
- icon: 'i-lucide-triangle-alert',
60
- color: 'error',
61
- })
62
- }
63
-
64
56
  async function save() {
65
57
  busy.value = true
66
58
  try {
@@ -75,7 +67,7 @@ async function save() {
75
67
  color: 'success',
76
68
  })
77
69
  } catch (e) {
78
- notifyError(t('inspector.releaseHealth.saveFailed'), e)
70
+ present(e, 'inspector.releaseHealth.saveFailed')
79
71
  } finally {
80
72
  busy.value = false
81
73
  }
@@ -92,7 +84,7 @@ async function clear() {
92
84
  draft.envTag = ''
93
85
  toastDone('clear', noun)
94
86
  } catch (e) {
95
- notifyError(t('inspector.releaseHealth.clearFailed'), e)
87
+ present(e, 'inspector.releaseHealth.clearFailed')
96
88
  } finally {
97
89
  busy.value = false
98
90
  }
@@ -18,6 +18,7 @@ const props = defineProps<{ block: Block }>()
18
18
 
19
19
  const store = useTestSecretsStore()
20
20
  const toast = useToast()
21
+ const { present } = usePipelineErrorToast()
21
22
  const { t } = useI18n()
22
23
  const { confirmAction, toastDone } = useConfirmAction()
23
24
 
@@ -90,15 +91,6 @@ function removeRow(index: number) {
90
91
  draft.rows.splice(index, 1)
91
92
  }
92
93
 
93
- function notifyError(title: string, e: unknown) {
94
- toast.add({
95
- title,
96
- description: e instanceof Error ? e.message : String(e),
97
- icon: 'i-lucide-triangle-alert',
98
- color: 'error',
99
- })
100
- }
101
-
102
94
  async function save() {
103
95
  busy.value = true
104
96
  try {
@@ -115,7 +107,7 @@ async function save() {
115
107
  color: 'success',
116
108
  })
117
109
  } catch (e) {
118
- notifyError(t('inspector.testSecrets.saveFailed'), e)
110
+ present(e, 'inspector.testSecrets.saveFailed')
119
111
  } finally {
120
112
  busy.value = false
121
113
  }
@@ -129,7 +121,7 @@ async function clearAll() {
129
121
  await store.clear(props.block.id)
130
122
  toastDone('clear', noun)
131
123
  } catch (e) {
132
- notifyError(t('inspector.testSecrets.clearFailed'), e)
124
+ present(e, 'inspector.testSecrets.clearFailed')
133
125
  } finally {
134
126
  busy.value = false
135
127
  }
@@ -20,6 +20,7 @@ const props = defineProps<{ block: Block }>()
20
20
 
21
21
  const store = useValidationChecksStore()
22
22
  const toast = useToast()
23
+ const { present } = usePipelineErrorToast()
23
24
  const { t, te } = useI18n()
24
25
  const { confirmAction, toastDone } = useConfirmAction()
25
26
 
@@ -65,15 +66,6 @@ function removeRow(index: number) {
65
66
  rows.value.splice(index, 1)
66
67
  }
67
68
 
68
- function notifyError(title: string, e: unknown) {
69
- toast.add({
70
- title,
71
- description: e instanceof Error ? e.message : String(e),
72
- icon: 'i-lucide-triangle-alert',
73
- color: 'error',
74
- })
75
- }
76
-
77
69
  // Ecosystem label KEYS, exhaustive over the contracts `ValidationEcosystem` union: a new
78
70
  // backend detector fails THIS typecheck until it is mapped (the key is assembled at runtime,
79
71
  // so the typed-message-keys check cannot see the `t()` lookup — the map's exhaustiveness is
@@ -162,7 +154,7 @@ async function detect() {
162
154
  color: 'success',
163
155
  })
164
156
  } catch (e) {
165
- notifyError(t('inspector.validationChecks.detect.failed'), e)
157
+ present(e, 'inspector.validationChecks.detect.failed')
166
158
  } finally {
167
159
  detecting.value = false
168
160
  }
@@ -183,7 +175,7 @@ async function save() {
183
175
  color: 'success',
184
176
  })
185
177
  } catch (e) {
186
- notifyError(t('inspector.validationChecks.saveFailed'), e)
178
+ present(e, 'inspector.validationChecks.saveFailed')
187
179
  } finally {
188
180
  busy.value = false
189
181
  }
@@ -199,7 +191,7 @@ async function clear() {
199
191
  dependencyInstall.value = ''
200
192
  toastDone('clear', noun)
201
193
  } catch (e) {
202
- notifyError(t('inspector.validationChecks.clearFailed'), e)
194
+ present(e, 'inspector.validationChecks.clearFailed')
203
195
  } finally {
204
196
  busy.value = false
205
197
  }
@@ -279,6 +279,7 @@ function toggleSaved(id: string) {
279
279
  }
280
280
 
281
281
  const toast = useToast()
282
+ const { present } = usePipelineErrorToast()
282
283
 
283
284
  // ---- "Add agent" mini-form -------------------------------------------------
284
285
  const addAgentOpen = ref(false)
@@ -329,13 +330,9 @@ async function save() {
329
330
  toast.add({ title: t('pipeline.builder.toast.addOneFirst'), color: 'warning' })
330
331
  }
331
332
  } catch (e) {
332
- // Surface the backend reason (e.g. post-release-health rejected without an
333
- // observability integration) rather than a generic failure.
334
- toast.add({
335
- title: t('pipeline.builder.toast.saveFailed'),
336
- description: e instanceof Error ? e.message : undefined,
337
- color: 'error',
338
- })
333
+ // Through the funnel, so the backend reason (e.g. post-release-health rejected without an
334
+ // observability integration) is reachable as copyable detail rather than raw prose up front.
335
+ present(e, 'pipeline.builder.toast.saveFailed')
339
336
  }
340
337
  }
341
338
 
@@ -28,6 +28,7 @@ const keys = useApiKeysStore()
28
28
  const models = useModelsStore()
29
29
  const auth = useAuthStore()
30
30
  const toast = useToast()
31
+ const { present } = usePipelineErrorToast()
31
32
  const { confirmAction, toastDone } = useConfirmAction()
32
33
 
33
34
  /** Account-wide mode (single account scope) vs the default workspace/user toggle. */
@@ -202,11 +203,7 @@ async function add() {
202
203
  color: 'success',
203
204
  })
204
205
  } catch (e) {
205
- toast.add({
206
- title: t('providers.apiKeys.toast.connectFailed'),
207
- description: e instanceof Error ? e.message : String(e),
208
- color: 'error',
209
- })
206
+ present(e, 'providers.apiKeys.toast.connectFailed')
210
207
  } finally {
211
208
  busy.value = false
212
209
  }
@@ -221,11 +218,7 @@ async function updateKey(k: ApiKey, patch: { enabled?: boolean; isDefault?: bool
221
218
  // Enabling/disabling changes provider selectability in the picker — refresh it.
222
219
  if (workspace.workspaceId) await models.refresh(workspace.workspaceId)
223
220
  } catch (e) {
224
- toast.add({
225
- title: t('providers.apiKeys.toast.updateFailed'),
226
- description: e instanceof Error ? e.message : String(e),
227
- color: 'error',
228
- })
221
+ present(e, 'providers.apiKeys.toast.updateFailed')
229
222
  }
230
223
  }
231
224
 
@@ -239,11 +232,7 @@ async function remove(k: ApiKey) {
239
232
  if (workspace.workspaceId) await models.refresh(workspace.workspaceId)
240
233
  toastDone('remove', noun)
241
234
  } catch (e) {
242
- toast.add({
243
- title: t('providers.apiKeys.toast.removeFailed'),
244
- description: e instanceof Error ? e.message : String(e),
245
- color: 'error',
246
- })
235
+ present(e, 'providers.apiKeys.toast.removeFailed')
247
236
  }
248
237
  }
249
238
  </script>
@@ -11,6 +11,7 @@ const { t } = useI18n()
11
11
  const personal = usePersonalSubscriptionsStore()
12
12
  const ui = useUiStore()
13
13
  const toast = useToast()
14
+ const { present } = usePipelineErrorToast()
14
15
 
15
16
  const password = ref('')
16
17
  const busy = ref(false)
@@ -73,11 +74,7 @@ async function submit() {
73
74
  })
74
75
  } catch (e) {
75
76
  // A fresh 428 (e.g. still-wrong password) re-arms `pending`, keeping the modal open.
76
- toast.add({
77
- title: t('providers.personalCredential.toast.startFailed'),
78
- description: e instanceof Error ? e.message : String(e),
79
- color: 'error',
80
- })
77
+ present(e, 'providers.personalCredential.toast.startFailed')
81
78
  } finally {
82
79
  busy.value = false
83
80
  }