@cat-factory/app 0.261.3 → 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 +1 -1
package/README.md CHANGED
@@ -195,6 +195,46 @@ The failure is silent, which is why this is a rule rather than a preference. An
195
195
 
196
196
  `scripts/check-component-imports.mjs` enforces it (CI's `repo-guards` job). If a panel section is missing and the data looks right, check the import first.
197
197
 
198
+ ### Every failure toast goes through ONE funnel
199
+
200
+ **A failed call is reported with `usePipelineErrorToast().present(error, titleKey)`.** Never build
201
+ `toast.add({ title, description: e instanceof Error ? e.message : String(e) })`, and never wrap that
202
+ shape in a per-component `notifyError(title, e)` helper (29 components had a copy of the same six
203
+ lines, plus four more spellings of it).
204
+
205
+ The funnel is not a formatting convenience. Four properties are what it exists for, and a hand-built
206
+ toast has none of them:
207
+
208
+ - **Translated copy.** The description is resolved from the envelope's status class or its
209
+ `details.reason`; the backend's untranslated prose is DETAIL, never the headline (see the i18n
210
+ section). A hand-built toast shows English to every locale.
211
+ - **It does not auto-dismiss.** An error is the one toast a reader has to finish, quote, or act on,
212
+ and a ~5s dismissal took it away mid-sentence. It keeps its close button, so leaving is a choice.
213
+ - **One-click copy of the whole thing**, through `useCopyToClipboard` (so the copy's own
214
+ success/failure is reported rather than silently no-op'ing in an insecure context). Selecting text
215
+ in a toast is fiddly and impossible once it is gone.
216
+ - **The `requestId` travels with it.** `mountRequestLogging` puts that id on every error envelope,
217
+ and it is the ONLY join between what the user saw and the one server log line that explains it. A
218
+ report that arrives without it costs whoever reads it the entire diagnosis.
219
+
220
+ `present` takes a KEY, not a resolved title (plus optional interpolation params), so it can resolve
221
+ its own copy. A store passes it through its context alongside `api`/`toast` (see
222
+ `stores/board/context.ts`) rather than calling the composable per write. A site with BESPOKE copy for
223
+ a recognised refusal keeps that branch and drains only its fallback into the funnel
224
+ (`stores/board/placement.ts`, `components/board/AddTaskModal.vue`).
225
+
226
+ **A FAILED CALL, though, not every refusal.** The funnel's whole job is to classify what the backend
227
+ answered, so a local check that never left the browser must not be dressed up as one: a synthesized
228
+ `new Error(t('...'))` has no envelope and no status, which is precisely the input `describeGenericFailure`
229
+ reads as a network fault. A blank required field then renders as "The server could not be reached",
230
+ with the real sentence hidden behind a disclosure. Client-side validation stays a plain
231
+ `toast.add` with translated title and description (`components/settings/ModelConfigurationPanel.vue`).
232
+
233
+ The still-open remainder is the INLINE family: `error.value = e.message` rendered in a panel, and
234
+ `testResult = { ok: false, message }` rendered by `ConnectionTestVerdict`. Those need a render
235
+ surface rather than a toast, and are tracked as G4 in
236
+ [`error-message-coverage.md`](https://github.com/kibertoad/cat-factory/blob/main/docs/initiatives/error-message-coverage.md).
237
+
198
238
  ### Type a chip map with `BadgeColor`, never `string`
199
239
 
200
240
  A status → chip map feeding a `<UBadge :color="…">` types its values as `BadgeColor` (`utils/badge.ts`), which is derived from `UBadge`'s own prop type rather than restated as a literal union. Typed `string`, the binding does not compile and the reflex is `as any` at each call site: seven of them had accumulated. That cast also accepts a colour Nuxt UI does not define, which renders as an unstyled badge with nothing failing.
@@ -59,6 +59,7 @@ const pipelines = usePipelinesStore()
59
59
  const agentConfig = useAgentConfigStore()
60
60
  const fragments = useFragmentsStore()
61
61
  const toast = useToast()
62
+ const { present } = usePipelineErrorToast()
62
63
  const { t } = useI18n()
63
64
 
64
65
  const { resolvePending, linkPending, presentLinkFailures } = useContextLinking()
@@ -730,12 +731,15 @@ async function submitCreate(acknowledgeReviewDebt: boolean) {
730
731
  openReviewFrictionDialog(conflict)
731
732
  return
732
733
  }
733
- toast.add({
734
- title: t('board.addTask.addFailedTitle'),
735
- description: createRefusalMessage(e) ?? (e instanceof Error ? e.message : String(e)),
736
- icon: 'i-lucide-triangle-alert',
737
- color: 'error',
738
- })
734
+ const refusal = createRefusalMessage(e)
735
+ if (refusal) {
736
+ toast.add({
737
+ title: t('board.addTask.addFailedTitle'),
738
+ description: refusal,
739
+ icon: 'i-lucide-triangle-alert',
740
+ color: 'error',
741
+ })
742
+ } else present(e, 'board.addTask.addFailedTitle')
739
743
  } finally {
740
744
  saving.value = false
741
745
  }
@@ -23,6 +23,7 @@ const { t } = useI18n()
23
23
  const agentRuns = useAgentRunsStore()
24
24
  const access = useWorkspaceAccess()
25
25
  const toast = useToast()
26
+ const { present } = usePipelineErrorToast()
26
27
  const { confirm } = useConfirm()
27
28
  const stopping = ref(false)
28
29
 
@@ -50,11 +51,7 @@ async function stop() {
50
51
  color: 'warning',
51
52
  })
52
53
  } catch (e) {
53
- toast.add({
54
- title: t('board.stop.stopFailed'),
55
- description: e instanceof Error ? e.message : String(e),
56
- color: 'error',
57
- })
54
+ present(e, 'board.stop.stopFailed')
58
55
  } finally {
59
56
  stopping.value = false
60
57
  }
@@ -30,7 +30,7 @@ import type { PendingContext } from '~/composables/useContextLinking'
30
30
  const ui = useUiStore()
31
31
  const board = useBoardStore()
32
32
  const initiatives = useInitiativesStore()
33
- const toast = useToast()
33
+ const { present } = usePipelineErrorToast()
34
34
  const { t } = useI18n()
35
35
  const { resolvePending, linkPending, presentLinkFailures } = useContextLinking()
36
36
 
@@ -170,12 +170,7 @@ async function create() {
170
170
  // Select the fresh block so the inspector offers "Run planning" right away.
171
171
  ui.select(block.id)
172
172
  } catch (e) {
173
- toast.add({
174
- title: t('initiative.create.failedTitle'),
175
- description: e instanceof Error ? e.message : String(e),
176
- icon: 'i-lucide-triangle-alert',
177
- color: 'error',
178
- })
173
+ present(e, 'initiative.create.failedTitle')
179
174
  } finally {
180
175
  submitting.value = false
181
176
  }
@@ -20,6 +20,7 @@ const recurring = useRecurringPipelinesStore()
20
20
  const tracker = useTrackerStore()
21
21
  const tasks = useTasksStore()
22
22
  const toast = useToast()
23
+ const { present } = usePipelineErrorToast()
23
24
  const access = useWorkspaceAccess()
24
25
  const { t, te } = useI18n()
25
26
 
@@ -375,12 +376,15 @@ async function add() {
375
376
  })
376
377
  ui.closeAddRecurring()
377
378
  } catch (e) {
378
- toast.add({
379
- title: t('board.recurring.addFailedTitle'),
380
- description: intakeRefusalCopy(e) ?? (e instanceof Error ? e.message : String(e)),
381
- icon: 'i-lucide-triangle-alert',
382
- color: 'error',
383
- })
379
+ const refusal = intakeRefusalCopy(e)
380
+ if (refusal) {
381
+ toast.add({
382
+ title: t('board.recurring.addFailedTitle'),
383
+ description: refusal,
384
+ icon: 'i-lucide-triangle-alert',
385
+ color: 'error',
386
+ })
387
+ } else present(e, 'board.recurring.addFailedTitle')
384
388
  } finally {
385
389
  saving.value = false
386
390
  }
@@ -14,6 +14,7 @@ const agentRuns = useAgentRunsStore()
14
14
  const github = useGitHubStore()
15
15
  const board = useBoardStore()
16
16
  const toast = useToast()
17
+ const { present } = usePipelineErrorToast()
17
18
  const { freeFramePosition, focusFrame } = useFramePlacement()
18
19
  const { t } = useI18n()
19
20
  const { confirmAction, toastDone } = useConfirmAction()
@@ -205,12 +206,7 @@ async function openCreateRepo() {
205
206
  color: 'success',
206
207
  })
207
208
  } catch (e) {
208
- toast.add({
209
- title: t('bootstrap.toast.repoCreateFailed'),
210
- description: e instanceof Error ? e.message : String(e),
211
- icon: 'i-lucide-triangle-alert',
212
- color: 'error',
213
- })
209
+ present(e, 'bootstrap.toast.repoCreateFailed')
214
210
  } finally {
215
211
  creatingRepo.value = false
216
212
  }
@@ -291,12 +287,7 @@ async function launch() {
291
287
  ui.closeBootstrap()
292
288
  }
293
289
  } catch (e) {
294
- toast.add({
295
- title: t('bootstrap.toast.bootstrapFailed'),
296
- description: e instanceof Error ? e.message : String(e),
297
- icon: 'i-lucide-triangle-alert',
298
- color: 'error',
299
- })
290
+ present(e, 'bootstrap.toast.bootstrapFailed')
300
291
  } finally {
301
292
  launching.value = false
302
293
  }
@@ -382,12 +373,7 @@ async function saveArch() {
382
373
  archForm.value = blankForm()
383
374
  archRepoSlug.value = undefined
384
375
  } catch (e) {
385
- toast.add({
386
- title: t('bootstrap.toast.saveArchFailed'),
387
- description: e instanceof Error ? e.message : String(e),
388
- icon: 'i-lucide-triangle-alert',
389
- color: 'error',
390
- })
376
+ present(e, 'bootstrap.toast.saveArchFailed')
391
377
  } finally {
392
378
  savingArch.value = false
393
379
  }
@@ -400,12 +386,7 @@ async function removeArch(a: ReferenceArchitecture) {
400
386
  if (selectedArchId.value === a.id) selectedArchId.value = undefined
401
387
  toastDone('remove', a.name)
402
388
  } catch (e) {
403
- toast.add({
404
- title: t('bootstrap.toast.deleteFailed'),
405
- description: e instanceof Error ? e.message : String(e),
406
- icon: 'i-lucide-triangle-alert',
407
- color: 'error',
408
- })
389
+ present(e, 'bootstrap.toast.deleteFailed')
409
390
  }
410
391
  }
411
392
 
@@ -23,6 +23,7 @@ import type {
23
23
  const board = useBoardStore()
24
24
  const brainstorm = useBrainstormStore()
25
25
  const toast = useToast()
26
+ const { present } = usePipelineErrorToast()
26
27
  const { t } = useI18n()
27
28
  const access = useWorkspaceAccess()
28
29
 
@@ -143,15 +144,6 @@ const STATUS_LABELS: Record<ReviewItemStatus, string> = {
143
144
  recommend_requested: 'brainstorm.itemStatus.recommend_requested',
144
145
  }
145
146
 
146
- function notifyError(title: string, e: unknown) {
147
- toast.add({
148
- title,
149
- description: e instanceof Error ? e.message : String(e),
150
- icon: 'i-lucide-triangle-alert',
151
- color: 'error',
152
- })
153
- }
154
-
155
147
  async function submitReply(item: BrainstormItem) {
156
148
  if (!session.value) return
157
149
  const text = (drafts.value[item.id] ?? '').trim()
@@ -160,7 +152,7 @@ async function submitReply(item: BrainstormItem) {
160
152
  await brainstorm.reply(session.value, item.id, text)
161
153
  drafts.value = { ...drafts.value, [item.id]: '' }
162
154
  } catch (e) {
163
- notifyError(t('brainstorm.toast.saveChoiceError'), e)
155
+ present(e, 'brainstorm.toast.saveChoiceError')
164
156
  }
165
157
  }
166
158
 
@@ -169,7 +161,7 @@ async function setStatus(item: BrainstormItem, itemStatus: BrainstormItemStatus)
169
161
  try {
170
162
  await brainstorm.setItemStatus(session.value, item.id, itemStatus)
171
163
  } catch (e) {
172
- notifyError(t('brainstorm.toast.updateOptionError'), e)
164
+ present(e, 'brainstorm.toast.updateOptionError')
173
165
  }
174
166
  }
175
167
 
@@ -178,7 +170,7 @@ async function incorporate(feedback?: string) {
178
170
  try {
179
171
  await brainstorm.incorporate(session.value, feedback)
180
172
  } catch (e) {
181
- notifyError(t('brainstorm.toast.incorporateError'), e)
173
+ present(e, 'brainstorm.toast.incorporateError')
182
174
  return
183
175
  }
184
176
  redoComment.value = ''
@@ -206,7 +198,7 @@ async function reReview() {
206
198
  icon: 'i-lucide-sparkles',
207
199
  })
208
200
  } catch (e) {
209
- notifyError(t('brainstorm.toast.reReviewError'), e)
201
+ present(e, 'brainstorm.toast.reReviewError')
210
202
  }
211
203
  }
212
204
 
@@ -217,7 +209,7 @@ async function proceed() {
217
209
  await brainstorm.proceed(blockId.value, activeStage.value)
218
210
  toast.add({ title: t('brainstorm.toast.proceeding'), icon: 'i-lucide-arrow-right' })
219
211
  } catch (e) {
220
- notifyError(t('brainstorm.toast.proceedError'), e)
212
+ present(e, 'brainstorm.toast.proceedError')
221
213
  } finally {
222
214
  acting.value = false
223
215
  }
@@ -237,7 +229,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
237
229
  toast.add({ title: t('brainstorm.toast.extraRoundGranted'), icon: 'i-lucide-rotate-cw' })
238
230
  }
239
231
  } catch (e) {
240
- notifyError(t('brainstorm.toast.resolveError'), e)
232
+ present(e, 'brainstorm.toast.resolveError')
241
233
  } finally {
242
234
  acting.value = false
243
235
  }
@@ -24,6 +24,7 @@ const board = useBoardStore()
24
24
  const clarity = useClarityStore()
25
25
  const models = useModelsStore()
26
26
  const toast = useToast()
27
+ const { present } = usePipelineErrorToast()
27
28
  const { t } = useI18n()
28
29
  const access = useWorkspaceAccess()
29
30
 
@@ -145,15 +146,6 @@ const STATUS_LABELS: Record<ReviewItemStatus, string> = {
145
146
  recommend_requested: 'clarity.itemStatus.recommend_requested',
146
147
  }
147
148
 
148
- function notifyError(title: string, e: unknown) {
149
- toast.add({
150
- title,
151
- description: e instanceof Error ? e.message : String(e),
152
- icon: 'i-lucide-triangle-alert',
153
- color: 'error',
154
- })
155
- }
156
-
157
149
  // Answers auto-save on blur — no explicit "save" button (matching the requirements window, so
158
150
  // muscle memory carries across the two, UX-34). The textarea is pre-seeded with the recorded
159
151
  // reply (see the watch below); persist only when the trimmed draft actually differs from what's
@@ -165,7 +157,7 @@ async function persistDraft(item: ClarityReviewItem, r: ClarityReview | null = r
165
157
  try {
166
158
  await clarity.reply(r, item.id, text)
167
159
  } catch (e) {
168
- notifyError(t('clarity.error.saveAnswer'), e)
160
+ present(e, 'clarity.error.saveAnswer')
169
161
  }
170
162
  }
171
163
 
@@ -223,7 +215,7 @@ async function setStatus(item: ClarityReviewItem, itemStatus: ClarityItemStatus)
223
215
  try {
224
216
  await clarity.setItemStatus(review.value, item.id, itemStatus)
225
217
  } catch (e) {
226
- notifyError(t('clarity.error.updateFinding'), e)
218
+ present(e, 'clarity.error.updateFinding')
227
219
  }
228
220
  }
229
221
 
@@ -233,7 +225,7 @@ async function incorporate(feedback?: string) {
233
225
  await flushDrafts()
234
226
  await clarity.incorporate(review.value, feedback)
235
227
  } catch (e) {
236
- notifyError(t('clarity.error.incorporate'), e)
228
+ present(e, 'clarity.error.incorporate')
237
229
  return
238
230
  }
239
231
  redoComment.value = ''
@@ -263,7 +255,7 @@ async function reReview() {
263
255
  icon: 'i-lucide-sparkles',
264
256
  })
265
257
  } catch (e) {
266
- notifyError(t('clarity.error.reReview'), e)
258
+ present(e, 'clarity.error.reReview')
267
259
  }
268
260
  }
269
261
 
@@ -275,7 +267,7 @@ async function proceed() {
275
267
  await clarity.proceed(blockId.value)
276
268
  toast.add({ title: t('clarity.toast.proceeding'), icon: 'i-lucide-arrow-right' })
277
269
  } catch (e) {
278
- notifyError(t('clarity.error.proceed'), e)
270
+ present(e, 'clarity.error.proceed')
279
271
  } finally {
280
272
  acting.value = false
281
273
  }
@@ -295,7 +287,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
295
287
  toast.add({ title: t('clarity.toast.extraRoundGranted'), icon: 'i-lucide-rotate-cw' })
296
288
  }
297
289
  } catch (e) {
298
- notifyError(t('clarity.error.resolve'), e)
290
+ present(e, 'clarity.error.resolve')
299
291
  } finally {
300
292
  acting.value = false
301
293
  }
@@ -11,6 +11,7 @@ const { t } = useI18n()
11
11
  const ui = useUiStore()
12
12
  const documents = useDocumentsStore()
13
13
  const toast = useToast()
14
+ const { present } = usePipelineErrorToast()
14
15
 
15
16
  const open = computed({
16
17
  get: () => ui.documentImport !== null,
@@ -61,12 +62,7 @@ async function doImport() {
61
62
  icon: 'i-lucide-file-down',
62
63
  })
63
64
  } catch (e) {
64
- toast.add({
65
- title: t('documents.import.importFailed'),
66
- description: e instanceof Error ? e.message : String(e),
67
- icon: 'i-lucide-triangle-alert',
68
- color: 'error',
69
- })
65
+ present(e, 'documents.import.importFailed')
70
66
  } finally {
71
67
  importing.value = false
72
68
  }
@@ -18,6 +18,7 @@ const { t } = useI18n()
18
18
  const ui = useUiStore()
19
19
  const documents = useDocumentsStore()
20
20
  const toast = useToast()
21
+ const { present } = usePipelineErrorToast()
21
22
  const { confirmAction } = useConfirmAction()
22
23
 
23
24
  const source = computed(() => ui.documentConnect?.source ?? null)
@@ -57,12 +58,7 @@ async function connectWithOAuth() {
57
58
  // path (an app un-registered between the probe and the click).
58
59
  await documents.beginOAuthConnect(source.value)
59
60
  } catch (e) {
60
- toast.add({
61
- title: t('documents.connect.oauth.failed', { source: descriptor.value?.label ?? '' }),
62
- description: e instanceof Error ? e.message : String(e),
63
- icon: 'i-lucide-triangle-alert',
64
- color: 'error',
65
- })
61
+ present(e, 'documents.connect.oauth.failed', { source: descriptor.value?.label ?? '' })
66
62
  } finally {
67
63
  startingOAuth.value = false
68
64
  }
@@ -93,12 +89,7 @@ async function submit() {
93
89
  })
94
90
  ui.closeDocumentConnect()
95
91
  } catch (e) {
96
- toast.add({
97
- title: t('documents.connect.connectFailed'),
98
- description: e instanceof Error ? e.message : String(e),
99
- icon: 'i-lucide-triangle-alert',
100
- color: 'error',
101
- })
92
+ present(e, 'documents.connect.connectFailed')
102
93
  } finally {
103
94
  saving.value = false
104
95
  }
@@ -39,7 +39,7 @@ const props = defineProps<{ doc: SourceDocument }>()
39
39
 
40
40
  const { t, d } = useI18n()
41
41
  const documents = useDocumentsStore()
42
- const toast = useToast()
42
+ const { present } = usePipelineErrorToast()
43
43
 
44
44
  /**
45
45
  * The source to ask, or null for an origin with nobody to ask. An `upload` was handed to the
@@ -159,12 +159,7 @@ async function refresh() {
159
159
  try {
160
160
  await documents.refresh(source, props.doc.externalId)
161
161
  } catch (e) {
162
- toast.add({
163
- title: t('documents.freshness.refreshFailed'),
164
- description: e instanceof Error ? e.message : String(e),
165
- icon: 'i-lucide-triangle-alert',
166
- color: 'error',
167
- })
162
+ present(e, 'documents.freshness.refreshFailed')
168
163
  }
169
164
  }
170
165
  </script>
@@ -11,7 +11,7 @@ import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
11
11
  const { t } = useI18n()
12
12
  const ui = useUiStore()
13
13
  const documents = useDocumentsStore()
14
- const toast = useToast()
14
+ const { present } = usePipelineErrorToast()
15
15
 
16
16
  const open = computed({
17
17
  get: () => ui.documentTemplates,
@@ -34,12 +34,7 @@ watch(
34
34
  // Surface a load failure instead of silently rendering an empty panel (which would invite
35
35
  // re-linking over links that still exist server-side).
36
36
  Promise.all([documents.loadDocuments(), documents.loadRoleLinks()]).catch((e) => {
37
- toast.add({
38
- title: t('documents.templates.loadFailed'),
39
- description: e instanceof Error ? e.message : String(e),
40
- icon: 'i-lucide-triangle-alert',
41
- color: 'error',
42
- })
37
+ present(e, 'documents.templates.loadFailed')
43
38
  })
44
39
  }
45
40
  },
@@ -73,12 +68,7 @@ async function link(role: DocumentLinkRole) {
73
68
  await documents.linkForKind(doc.source, doc.externalId, role, kind.value)
74
69
  pick.value = undefined
75
70
  } catch (e) {
76
- toast.add({
77
- title: t('documents.templates.linkFailed'),
78
- description: e instanceof Error ? e.message : String(e),
79
- icon: 'i-lucide-triangle-alert',
80
- color: 'error',
81
- })
71
+ present(e, 'documents.templates.linkFailed')
82
72
  } finally {
83
73
  busy.value = false
84
74
  }
@@ -89,12 +79,7 @@ async function unlink(doc: SourceDocument) {
89
79
  try {
90
80
  await documents.unlinkForKind(doc.source, doc.externalId)
91
81
  } catch (e) {
92
- toast.add({
93
- title: t('documents.templates.linkFailed'),
94
- description: e instanceof Error ? e.message : String(e),
95
- icon: 'i-lucide-triangle-alert',
96
- color: 'error',
97
- })
82
+ present(e, 'documents.templates.linkFailed')
98
83
  } finally {
99
84
  busy.value = false
100
85
  }
@@ -17,6 +17,7 @@ const ui = useUiStore()
17
17
  const board = useBoardStore()
18
18
  const documents = useDocumentsStore()
19
19
  const toast = useToast()
20
+ const { present } = usePipelineErrorToast()
20
21
 
21
22
  /** Design documents are planned into an existing service; prose documents at the board root. */
22
23
  const needsTarget = computed(() => !!ui.spawnPreview && isDesignSource(ui.spawnPreview.source))
@@ -80,12 +81,7 @@ watch(
80
81
  // A superseded request's failure is not this selection's failure: toasting it would report an
81
82
  // outage against a target whose own plan may be loading fine.
82
83
  if (request !== planRequest) return
83
- toast.add({
84
- title: t('documents.spawn.planFailed'),
85
- description: e instanceof Error ? e.message : String(e),
86
- icon: 'i-lucide-triangle-alert',
87
- color: 'error',
88
- })
84
+ present(e, 'documents.spawn.planFailed')
89
85
  } finally {
90
86
  // Only the current request may clear the spinner; an older one settling first would report
91
87
  // "loaded" over a plan still on its way.
@@ -120,12 +116,7 @@ async function spawn() {
120
116
  ui.closeSpawnPreview()
121
117
  ui.closeDocumentImport()
122
118
  } catch (e) {
123
- toast.add({
124
- title: t('documents.spawn.spawnFailed'),
125
- description: e instanceof Error ? e.message : String(e),
126
- icon: 'i-lucide-triangle-alert',
127
- color: 'error',
128
- })
119
+ present(e, 'documents.spawn.spawnFailed')
129
120
  } finally {
130
121
  spawning.value = false
131
122
  }
@@ -21,6 +21,7 @@ import FoundationalContractSummary from '~/components/foundational/FoundationalC
21
21
 
22
22
  const catalog = useFoundationalServicesStore()
23
23
  const toast = useToast()
24
+ const { present } = usePipelineErrorToast()
24
25
  const { t } = useI18n()
25
26
 
26
27
  // Exhaustive maps of literal `t(...)` keys, so a new tier/format fails the typed-key guard.
@@ -41,15 +42,6 @@ const tierColor = {
41
42
  workspace: 'primary',
42
43
  } as const satisfies Record<FoundationalServiceTier, string>
43
44
 
44
- function notifyError(title: string, e: unknown) {
45
- toast.add({
46
- title,
47
- description: e instanceof Error ? e.message : String(e),
48
- icon: 'i-lucide-triangle-alert',
49
- color: 'error',
50
- })
51
- }
52
-
53
45
  const busyRows = reactive(new Set<string>())
54
46
  const rowBusy = (key: string) => busyRows.has(key)
55
47
  async function withRow(key: string, fn: () => Promise<void>) {
@@ -75,7 +67,7 @@ async function toggleContracts(serviceId: string) {
75
67
  await catalog.contractsFor(serviceId)
76
68
  expanded.value = [...expanded.value, serviceId]
77
69
  } catch (e) {
78
- notifyError(t('foundational.toast.contractsFailed'), e)
70
+ present(e, 'foundational.toast.contractsFailed')
79
71
  }
80
72
  })
81
73
  }
@@ -86,7 +78,7 @@ async function suppress(serviceId: string) {
86
78
  await catalog.suppress(serviceId)
87
79
  toast.add({ title: t('foundational.toast.suppressed'), icon: 'i-lucide-eye-off' })
88
80
  } catch (e) {
89
- notifyError(t('foundational.toast.suppressFailed'), e)
81
+ present(e, 'foundational.toast.suppressFailed')
90
82
  }
91
83
  })
92
84
  }
@@ -28,6 +28,7 @@ const catalog =
28
28
  ? useFoundationalServicesStore()
29
29
  : useFoundationalServices(props.kind, props.ownerId)
30
30
  const toast = useToast()
31
+ const { present } = usePipelineErrorToast()
31
32
  const { t } = useI18n()
32
33
  const { confirm } = useConfirm()
33
34
 
@@ -44,15 +45,6 @@ const formatItems = computed(() =>
44
45
  })),
45
46
  )
46
47
 
47
- function notifyError(title: string, e: unknown) {
48
- toast.add({
49
- title,
50
- description: e instanceof Error ? e.message : String(e),
51
- icon: 'i-lucide-triangle-alert',
52
- color: 'error',
53
- })
54
- }
55
-
56
48
  // ---- the editor ------------------------------------------------------------
57
49
  /** null = closed; '' = creating; a service id = editing that row. */
58
50
  const editing = ref<string | null>(null)
@@ -148,7 +140,7 @@ async function save() {
148
140
  }
149
141
  editing.value = null
150
142
  } catch (e) {
151
- notifyError(t('foundational.toast.saveFailed'), e)
143
+ present(e, 'foundational.toast.saveFailed')
152
144
  } finally {
153
145
  saving.value = false
154
146
  }
@@ -177,7 +169,7 @@ async function remove(service: FoundationalService) {
177
169
  await catalog.remove(service.id)
178
170
  toast.add({ title: t('foundational.toast.deleted'), icon: 'i-lucide-trash-2' })
179
171
  } catch (e) {
180
- notifyError(t('foundational.toast.deleteFailed'), e)
172
+ present(e, 'foundational.toast.deleteFailed')
181
173
  }
182
174
  }
183
175
  </script>