@cat-factory/app 0.261.3 → 0.261.5

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 (104) 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/KubernetesEngineForm.vue +71 -26
  58. package/app/components/settings/KubernetesEnvironmentForm.vue +35 -8
  59. package/app/components/settings/LocalContainerPoolSettings.vue +2 -6
  60. package/app/components/settings/LocalModelEndpointsPanel.vue +3 -11
  61. package/app/components/settings/ModelConfigurationPanel.vue +13 -16
  62. package/app/components/settings/ModelPresetHealthModal.vue +3 -13
  63. package/app/components/settings/ObservabilityConnectionPanel.vue +6 -14
  64. package/app/components/settings/OpenRouterCatalogPanel.vue +3 -12
  65. package/app/components/settings/PackageRegistriesPanel.vue +4 -12
  66. package/app/components/settings/ProviderConnectionTab.vue +5 -13
  67. package/app/components/settings/RiskPolicyHealthModal.vue +2 -7
  68. package/app/components/settings/RiskPolicyPanel.vue +5 -13
  69. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +2 -7
  70. package/app/components/settings/SharedStacksPanel.vue +10 -20
  71. package/app/components/settings/TaskTypeSuppressionsPanel.vue +3 -12
  72. package/app/components/settings/UserSecretsSection.vue +3 -11
  73. package/app/components/settings/WorkspaceMetadataSettings.vue +2 -6
  74. package/app/components/settings/WorkspaceSettingsPanel.vue +2 -6
  75. package/app/components/skills/SkillLibraryManager.vue +5 -13
  76. package/app/components/slack/SlackPanel.vue +7 -15
  77. package/app/components/tasks/BugHuntModal.vue +2 -6
  78. package/app/components/tasks/TaskImportModal.vue +2 -6
  79. package/app/components/tasks/TaskSourceConnectModal.vue +4 -18
  80. package/app/composables/useBlockDeletion.ts +2 -6
  81. package/app/composables/useCopyToClipboard.ts +7 -1
  82. package/app/composables/usePipelineErrorToast.spec.ts +76 -16
  83. package/app/composables/usePipelineErrorToast.ts +109 -50
  84. package/app/stores/board/context.ts +9 -2
  85. package/app/stores/board/dependencies.ts +3 -13
  86. package/app/stores/board/mutations.ts +2 -7
  87. package/app/stores/board/placement.ts +12 -25
  88. package/app/stores/board/removal.ts +2 -7
  89. package/app/stores/board.spec.ts +15 -8
  90. package/app/stores/board.ts +11 -1
  91. package/app/stores/recurringPipelines.ts +5 -11
  92. package/app/stores/ui/k3sDeepLink.spec.ts +43 -1
  93. package/app/stores/ui/modals.ts +24 -0
  94. package/i18n/locales/de.json +2 -0
  95. package/i18n/locales/en.json +2 -0
  96. package/i18n/locales/es.json +2 -0
  97. package/i18n/locales/fr.json +2 -0
  98. package/i18n/locales/he.json +2 -0
  99. package/i18n/locales/it.json +2 -0
  100. package/i18n/locales/ja.json +2 -0
  101. package/i18n/locales/pl.json +2 -0
  102. package/i18n/locales/tr.json +2 -0
  103. package/i18n/locales/uk.json +2 -0
  104. package/package.json +2 -2
@@ -70,6 +70,7 @@ const CONCERN_LEVELS = computed<{ value: RequirementConcernLevel; label: string
70
70
 
71
71
  const store = useRiskPoliciesStore()
72
72
  const toast = useToast()
73
+ const { present } = usePipelineErrorToast()
73
74
  const { confirm } = useConfirm()
74
75
 
75
76
  // Local editable copy per preset, kept in sync with the store. Percentages are
@@ -153,15 +154,6 @@ watch(
153
154
 
154
155
  const busy = ref<string | null>(null)
155
156
 
156
- function notifyError(title: string, e: unknown) {
157
- toast.add({
158
- title,
159
- description: e instanceof Error ? e.message : String(e),
160
- icon: 'i-lucide-triangle-alert',
161
- color: 'error',
162
- })
163
- }
164
-
165
157
  async function save(p: RiskPolicy) {
166
158
  const d = drafts[p.id]
167
159
  if (!d) return
@@ -188,7 +180,7 @@ async function save(p: RiskPolicy) {
188
180
  color: 'success',
189
181
  })
190
182
  } catch (e) {
191
- notifyError(t('settings.riskPolicy.toast.saveFailed'), e)
183
+ present(e, 'settings.riskPolicy.toast.saveFailed')
192
184
  } finally {
193
185
  busy.value = null
194
186
  }
@@ -199,7 +191,7 @@ async function makeDefault(p: RiskPolicy) {
199
191
  try {
200
192
  await store.update(p.id, { isDefault: true })
201
193
  } catch (e) {
202
- notifyError(t('settings.riskPolicy.toast.defaultFailed'), e)
194
+ present(e, 'settings.riskPolicy.toast.defaultFailed')
203
195
  } finally {
204
196
  busy.value = null
205
197
  }
@@ -218,7 +210,7 @@ async function remove(p: RiskPolicy) {
218
210
  try {
219
211
  await store.remove(p.id)
220
212
  } catch (e) {
221
- notifyError(t('settings.riskPolicy.toast.deleteFailed'), e)
213
+ present(e, 'settings.riskPolicy.toast.deleteFailed')
222
214
  } finally {
223
215
  busy.value = null
224
216
  }
@@ -274,7 +266,7 @@ async function create() {
274
266
  color: 'success',
275
267
  })
276
268
  } catch (e) {
277
- notifyError(t('settings.riskPolicy.toast.createFailed'), e)
269
+ present(e, 'settings.riskPolicy.toast.createFailed')
278
270
  } finally {
279
271
  creating.value = false
280
272
  }
@@ -12,7 +12,7 @@ const { t } = useI18n()
12
12
  const fragments = useFragmentsStore()
13
13
  const defaults = useServiceFragmentDefaultsStore()
14
14
  const ui = useUiStore()
15
- const toast = useToast()
15
+ const { present } = usePipelineErrorToast()
16
16
 
17
17
  const busy = ref(false)
18
18
 
@@ -36,12 +36,7 @@ async function save(ids: string[]) {
36
36
  try {
37
37
  await defaults.set(ids)
38
38
  } catch (e) {
39
- toast.add({
40
- title: t('settings.serviceFragmentDefaults.saveFailed'),
41
- description: e instanceof Error ? e.message : String(e),
42
- icon: 'i-lucide-triangle-alert',
43
- color: 'error',
44
- })
39
+ present(e, 'settings.serviceFragmentDefaults.saveFailed')
45
40
  } finally {
46
41
  busy.value = false
47
42
  }
@@ -16,6 +16,7 @@ import type {
16
16
  const { t } = useI18n()
17
17
  const store = useSharedStacksStore()
18
18
  const toast = useToast()
19
+ const { present } = usePipelineErrorToast()
19
20
  const { confirmAction, toastDone } = useConfirmAction()
20
21
 
21
22
  const stacks = computed(() => store.stacks)
@@ -169,21 +170,12 @@ async function autodetect() {
169
170
  color: 'success',
170
171
  })
171
172
  } catch (e) {
172
- notifyError(t('settings.sharedStacks.detect.failed'), e)
173
+ present(e, 'settings.sharedStacks.detect.failed')
173
174
  } finally {
174
175
  detecting.value = false
175
176
  }
176
177
  }
177
178
 
178
- function notifyError(title: string, e: unknown) {
179
- toast.add({
180
- title,
181
- description: e instanceof Error ? e.message : String(e),
182
- icon: 'i-lucide-triangle-alert',
183
- color: 'error',
184
- })
185
- }
186
-
187
179
  /** Create a new stack, or save edits to the one being edited (same form, mode toggled by `editingId`). */
188
180
  async function saveStack() {
189
181
  saving.value = true
@@ -223,13 +215,11 @@ async function saveStack() {
223
215
  color: 'success',
224
216
  })
225
217
  } catch (e) {
226
- notifyError(
227
- t(
228
- editing
229
- ? 'settings.sharedStacks.toast.updateFailed'
230
- : 'settings.sharedStacks.toast.createFailed',
231
- ),
218
+ present(
232
219
  e,
220
+ editing
221
+ ? 'settings.sharedStacks.toast.updateFailed'
222
+ : 'settings.sharedStacks.toast.createFailed',
233
223
  )
234
224
  } finally {
235
225
  saving.value = false
@@ -243,10 +233,10 @@ async function start(stack: SharedStack) {
243
233
  // surface that as an error toast too — not only a thrown transport/unavailable error.
244
234
  const updated = await store.ensureUp(stack.id)
245
235
  if (updated.status === 'failed') {
246
- notifyError(t('settings.sharedStacks.toast.startFailed'), updated.lastError ?? '')
236
+ present(updated.lastError ?? '', 'settings.sharedStacks.toast.startFailed')
247
237
  }
248
238
  } catch (e) {
249
- notifyError(t('settings.sharedStacks.toast.startFailed'), e)
239
+ present(e, 'settings.sharedStacks.toast.startFailed')
250
240
  } finally {
251
241
  busyId.value = null
252
242
  }
@@ -257,7 +247,7 @@ async function stop(stack: SharedStack) {
257
247
  try {
258
248
  await store.teardown(stack.id)
259
249
  } catch (e) {
260
- notifyError(t('settings.sharedStacks.toast.stopFailed'), e)
250
+ present(e, 'settings.sharedStacks.toast.stopFailed')
261
251
  } finally {
262
252
  busyId.value = null
263
253
  }
@@ -271,7 +261,7 @@ async function remove(stack: SharedStack) {
271
261
  await store.remove(stack.id)
272
262
  toastDone('remove', noun)
273
263
  } catch (e) {
274
- notifyError(t('settings.sharedStacks.toast.removeFailed'), e)
264
+ present(e, 'settings.sharedStacks.toast.removeFailed')
275
265
  } finally {
276
266
  busyId.value = null
277
267
  }
@@ -16,7 +16,7 @@ import type { TaskTypeSuppression } from '~/types/domain'
16
16
  const { t } = useI18n()
17
17
  const api = useApi()
18
18
  const workspace = useWorkspaceStore()
19
- const toast = useToast()
19
+ const { present } = usePipelineErrorToast()
20
20
 
21
21
  const rows = ref<TaskTypeSuppression[]>([])
22
22
  const loading = ref(false)
@@ -31,7 +31,7 @@ async function load() {
31
31
  try {
32
32
  rows.value = (await api.listTaskTypeSuppressions(workspace.requireId())).taskTypes
33
33
  } catch (e) {
34
- fail(e)
34
+ present(e, 'settings.taskTypeSuppressions.saveFailed')
35
35
  } finally {
36
36
  loading.value = false
37
37
  }
@@ -52,20 +52,11 @@ async function toggle(row: TaskTypeSuppression, offered: boolean) {
52
52
  rows.value = result.taskTypes
53
53
  await workspace.refresh()
54
54
  } catch (e) {
55
- fail(e)
55
+ present(e, 'settings.taskTypeSuppressions.saveFailed')
56
56
  } finally {
57
57
  busyId.value = null
58
58
  }
59
59
  }
60
-
61
- function fail(e: unknown) {
62
- toast.add({
63
- title: t('settings.taskTypeSuppressions.saveFailed'),
64
- description: e instanceof Error ? e.message : String(e),
65
- icon: 'i-lucide-triangle-alert',
66
- color: 'error',
67
- })
68
- }
69
60
  </script>
70
61
 
71
62
  <template>
@@ -15,6 +15,7 @@ const { t } = useI18n()
15
15
  const ui = useUiStore()
16
16
  const store = useUserSecretsStore()
17
17
  const toast = useToast()
18
+ const { present } = usePipelineErrorToast()
18
19
  const { confirm } = useConfirm()
19
20
 
20
21
  const open = computed({
@@ -75,15 +76,6 @@ function buildPayload(): { secret: string; metadata?: Record<string, string> } |
75
76
  return { secret, ...(Object.keys(metadata).length ? { metadata } : {}) }
76
77
  }
77
78
 
78
- function notifyError(title: string, e: unknown) {
79
- toast.add({
80
- title,
81
- description: e instanceof Error ? e.message : String(e),
82
- icon: 'i-lucide-triangle-alert',
83
- color: 'error',
84
- })
85
- }
86
-
87
79
  async function test() {
88
80
  const payload = buildPayload()
89
81
  if (!payload) return
@@ -120,7 +112,7 @@ async function save() {
120
112
  color: 'success',
121
113
  })
122
114
  } catch (e) {
123
- notifyError(t('settings.userSecrets.toast.saveFailed'), e)
115
+ present(e, 'settings.userSecrets.toast.saveFailed')
124
116
  } finally {
125
117
  busy.value = false
126
118
  }
@@ -143,7 +135,7 @@ async function remove() {
143
135
  resetDraft()
144
136
  toast.add({ title: t('settings.userSecrets.toast.removed'), icon: 'i-lucide-check' })
145
137
  } catch (e) {
146
- notifyError(t('settings.userSecrets.toast.removeFailed'), e)
138
+ present(e, 'settings.userSecrets.toast.removeFailed')
147
139
  } finally {
148
140
  busy.value = false
149
141
  }
@@ -22,6 +22,7 @@ const { t } = useI18n()
22
22
  const slots = useReactiveSlots<AppSlots>()
23
23
  const store = useWorkspaceSettingsStore()
24
24
  const toast = useToast()
25
+ const { present } = usePipelineErrorToast()
25
26
 
26
27
  /**
27
28
  * The fields to render. A malformed key is dropped (the store would refuse every save) and
@@ -71,12 +72,7 @@ async function save() {
71
72
  color: 'success',
72
73
  })
73
74
  } catch (e) {
74
- toast.add({
75
- title: t('settings.workspaceSettings.toast.saveFailed'),
76
- description: e instanceof Error ? e.message : String(e),
77
- icon: 'i-lucide-triangle-alert',
78
- color: 'error',
79
- })
75
+ present(e, 'settings.workspaceSettings.toast.saveFailed')
80
76
  } finally {
81
77
  saving.value = false
82
78
  }
@@ -29,6 +29,7 @@ const store = useWorkspaceSettingsStore()
29
29
  const workspace = useWorkspaceStore()
30
30
  const access = useWorkspaceAccess()
31
31
  const toast = useToast()
32
+ const { present } = usePipelineErrorToast()
32
33
  const slots = useReactiveSlots<AppSlots>()
33
34
  // Whether the deployment registers any reusable operation at all, hidden or not, so the
34
35
  // Operations tab exists only where there is something for it to manage. Not the OFFERED catalog:
@@ -289,12 +290,7 @@ async function save() {
289
290
  color: 'success',
290
291
  })
291
292
  } catch (e) {
292
- toast.add({
293
- title: t('settings.workspaceSettings.toast.saveFailed'),
294
- description: e instanceof Error ? e.message : String(e),
295
- icon: 'i-lucide-triangle-alert',
296
- color: 'error',
297
- })
293
+ present(e, 'settings.workspaceSettings.toast.saveFailed')
298
294
  } finally {
299
295
  saving.value = false
300
296
  }
@@ -16,6 +16,7 @@ const props = defineProps<{ accountId: string }>()
16
16
  const library = useSkillLibrary(props.accountId)
17
17
  const github = useGitHubStore()
18
18
  const toast = useToast()
19
+ const { present } = usePipelineErrorToast()
19
20
  const { t, d } = useI18n()
20
21
  const { confirm } = useConfirm()
21
22
 
@@ -33,15 +34,6 @@ watch(
33
34
  // the integration is off) the form falls back to manual text entry.
34
35
  const githubReady = computed(() => github.available === true && github.connected)
35
36
 
36
- function notifyError(title: string, e: unknown) {
37
- toast.add({
38
- title,
39
- description: e instanceof Error ? e.message : String(e),
40
- icon: 'i-lucide-triangle-alert',
41
- color: 'error',
42
- })
43
- }
44
-
45
37
  // Per-row in-flight tracking so only the control that triggered an action spins.
46
38
  const busyRows = reactive(new Set<string>())
47
39
  const rowBusy = (key: string) => busyRows.has(key)
@@ -102,7 +94,7 @@ async function linkSource() {
102
94
  resetSourceDraft()
103
95
  toast.add({ title: t('skills.toast.sourceLinked'), icon: 'i-lucide-git-branch' })
104
96
  } catch (e) {
105
- notifyError(t('skills.toast.linkSourceFailed'), e)
97
+ present(e, 'skills.toast.linkSourceFailed')
106
98
  } finally {
107
99
  linkingSource.value = false
108
100
  }
@@ -121,7 +113,7 @@ async function syncSource(id: string) {
121
113
  color: 'info',
122
114
  })
123
115
  } catch (e) {
124
- notifyError(t('skills.toast.syncFailed'), e)
116
+ present(e, 'skills.toast.syncFailed')
125
117
  }
126
118
  })
127
119
  }
@@ -135,7 +127,7 @@ async function checkSource(id: string) {
135
127
  icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
136
128
  })
137
129
  } catch (e) {
138
- notifyError(t('skills.toast.checkSourceFailed'), e)
130
+ present(e, 'skills.toast.checkSourceFailed')
139
131
  }
140
132
  })
141
133
  }
@@ -156,7 +148,7 @@ async function unlinkSource(id: string) {
156
148
  await library.unlinkSource(id)
157
149
  toast.add({ title: t('skills.toast.sourceUnlinked'), icon: 'i-lucide-unplug' })
158
150
  } catch (e) {
159
- notifyError(t('skills.toast.unlinkSourceFailed'), e)
151
+ present(e, 'skills.toast.unlinkSourceFailed')
160
152
  }
161
153
  })
162
154
  }
@@ -20,6 +20,7 @@ import SecretInput from '~/components/common/SecretInput.vue'
20
20
  const ui = useUiStore()
21
21
  const slack = useSlackStore()
22
22
  const toast = useToast()
23
+ const { present } = usePipelineErrorToast()
23
24
  const { t } = useI18n()
24
25
  const { confirm } = useConfirm()
25
26
 
@@ -90,15 +91,6 @@ const tokenInput = ref('')
90
91
  const busy = ref(false)
91
92
  const connectingOAuth = ref(false)
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
  // Load everything the panel needs whenever it opens and Slack is connected.
103
95
  watch(
104
96
  () => open.value,
@@ -112,7 +104,7 @@ watch(
112
104
  mentionsEnabled.value = slack.settings?.mentionsEnabled ?? false
113
105
  mapping.value = slack.memberMapping.map((e) => toMemberRow(e, nextUid()))
114
106
  } catch (e) {
115
- notifyError(t('slack.error.loadSettings'), e)
107
+ present(e, 'slack.error.loadSettings')
116
108
  }
117
109
  },
118
110
  // Lazy v-if mount: the panel mounts with `open` already true, so load immediately.
@@ -127,7 +119,7 @@ async function connectViaOAuth() {
127
119
  window.location.href = await slack.installUrl()
128
120
  } catch (e) {
129
121
  connectingOAuth.value = false
130
- notifyError(t('slack.error.startOAuth'), e)
122
+ present(e, 'slack.error.startOAuth')
131
123
  }
132
124
  }
133
125
 
@@ -138,7 +130,7 @@ async function connectWithToken() {
138
130
  tokenInput.value = ''
139
131
  toast.add({ title: t('slack.toast.connected'), icon: 'i-lucide-check', color: 'success' })
140
132
  } catch (e) {
141
- notifyError(t('slack.error.connect'), e)
133
+ present(e, 'slack.error.connect')
142
134
  }
143
135
  }
144
136
 
@@ -154,7 +146,7 @@ async function disconnect() {
154
146
  try {
155
147
  await slack.disconnect()
156
148
  } catch (e) {
157
- notifyError(t('slack.error.disconnect'), e)
149
+ present(e, 'slack.error.disconnect')
158
150
  }
159
151
  }
160
152
 
@@ -167,7 +159,7 @@ async function saveRouting() {
167
159
  })
168
160
  toast.add({ title: t('slack.toast.routingSaved'), icon: 'i-lucide-check', color: 'success' })
169
161
  } catch (e) {
170
- notifyError(t('slack.error.saveRouting'), e)
162
+ present(e, 'slack.error.saveRouting')
171
163
  } finally {
172
164
  busy.value = false
173
165
  }
@@ -199,7 +191,7 @@ async function saveMapping() {
199
191
  mapping.value = slack.memberMapping.map((e) => toMemberRow(e, nextUid()))
200
192
  toast.add({ title: t('slack.toast.mapSaved'), icon: 'i-lucide-check', color: 'success' })
201
193
  } catch (e) {
202
- notifyError(t('slack.error.saveMap'), e)
194
+ present(e, 'slack.error.saveMap')
203
195
  } finally {
204
196
  busy.value = false
205
197
  }
@@ -43,6 +43,7 @@ const ui = useUiStore()
43
43
  const tasks = useTasksStore()
44
44
  const hunt = useBugHuntStore()
45
45
  const toast = useToast()
46
+ const { present } = usePipelineErrorToast()
46
47
 
47
48
  const open = computed({
48
49
  get: () => ui.bugHunt !== null,
@@ -212,12 +213,7 @@ async function adopt(candidate: BugHuntCandidate) {
212
213
  icon: 'i-lucide-bug-play',
213
214
  })
214
215
  } catch (e) {
215
- toast.add({
216
- title: t('bugHunt.adoptFailed'),
217
- description: e instanceof Error ? e.message : String(e),
218
- icon: 'i-lucide-triangle-alert',
219
- color: 'error',
220
- })
216
+ present(e, 'bugHunt.adoptFailed')
221
217
  }
222
218
  }
223
219
 
@@ -22,6 +22,7 @@ const { t } = useI18n()
22
22
  const ui = useUiStore()
23
23
  const tasks = useTasksStore()
24
24
  const toast = useToast()
25
+ const { present } = usePipelineErrorToast()
25
26
 
26
27
  const open = computed({
27
28
  get: () => ui.taskImport !== null,
@@ -112,12 +113,7 @@ async function doSpawnEpic() {
112
113
  icon: 'i-lucide-layers',
113
114
  })
114
115
  } catch (e) {
115
- toast.add({
116
- title: t('tasks.import.epicFailed'),
117
- description: e instanceof Error ? e.message : String(e),
118
- icon: 'i-lucide-triangle-alert',
119
- color: 'error',
120
- })
116
+ present(e, 'tasks.import.epicFailed')
121
117
  } finally {
122
118
  importing.value = false
123
119
  }
@@ -17,6 +17,7 @@ const { t } = useI18n()
17
17
  const ui = useUiStore()
18
18
  const tasks = useTasksStore()
19
19
  const toast = useToast()
20
+ const { present } = usePipelineErrorToast()
20
21
  const { confirmAction } = useConfirmAction()
21
22
 
22
23
  const source = computed(() => ui.taskConnect?.source ?? null)
@@ -70,12 +71,7 @@ async function submit() {
70
71
  // Re-probe so `available`/`enabled` reflect the new connection.
71
72
  await tasks.probe()
72
73
  } catch (e) {
73
- toast.add({
74
- title: t('tasks.connect.connectFailed'),
75
- description: e instanceof Error ? e.message : String(e),
76
- icon: 'i-lucide-triangle-alert',
77
- color: 'error',
78
- })
74
+ present(e, 'tasks.connect.connectFailed')
79
75
  } finally {
80
76
  saving.value = false
81
77
  }
@@ -88,12 +84,7 @@ async function startOAuth() {
88
84
  // Only Linear wires an OAuth flow today; the browser navigates away on success.
89
85
  if (source.value === 'linear') await tasks.startLinearOAuth()
90
86
  } catch (e) {
91
- toast.add({
92
- title: t('tasks.connect.connectFailed'),
93
- description: e instanceof Error ? e.message : String(e),
94
- icon: 'i-lucide-triangle-alert',
95
- color: 'error',
96
- })
87
+ present(e, 'tasks.connect.connectFailed')
97
88
  oauthStarting.value = false
98
89
  }
99
90
  }
@@ -117,12 +108,7 @@ async function toggleEnabled(enabled: boolean) {
117
108
  try {
118
109
  await tasks.setEnabled(source.value, enabled)
119
110
  } catch (e) {
120
- toast.add({
121
- title: t('tasks.connect.updateFailed'),
122
- description: e instanceof Error ? e.message : String(e),
123
- icon: 'i-lucide-triangle-alert',
124
- color: 'error',
125
- })
111
+ present(e, 'tasks.connect.updateFailed')
126
112
  } finally {
127
113
  togglingEnabled.value = false
128
114
  }
@@ -16,6 +16,7 @@ export function useBlockDeletion() {
16
16
  const ui = useUiStore()
17
17
  const recurring = useRecurringPipelinesStore()
18
18
  const toast = useToast()
19
+ const { present } = usePipelineErrorToast()
19
20
  const { confirm } = useConfirm()
20
21
  const { t } = useI18n()
21
22
  const access = useWorkspaceAccess()
@@ -57,12 +58,7 @@ export function useBlockDeletion() {
57
58
  color: 'neutral',
58
59
  })
59
60
  } catch (e) {
60
- toast.add({
61
- title: t('board.toast.archiveFailed'),
62
- description: e instanceof Error ? e.message : String(e),
63
- icon: 'i-lucide-triangle-alert',
64
- color: 'error',
65
- })
61
+ present(e, 'board.toast.archiveFailed')
66
62
  }
67
63
  return true
68
64
  }
@@ -9,7 +9,13 @@
9
9
  import { useClipboard } from '@vueuse/core'
10
10
 
11
11
  export function useCopyToClipboard() {
12
- const { t } = useI18n()
12
+ // Resolved through the Nuxt app's global i18n instance rather than `useI18n()`, which requires an
13
+ // active component instance. This composable is reached from the failure-toast funnel, which is
14
+ // itself instantiated in STORE setup (`stores/execution.ts`, `stores/board.ts`), and a Pinia
15
+ // setup store runs its body on the first `useStore()` anywhere, which is not necessarily a
16
+ // component. `useI18n()` there throws `MUST_BE_CALL_SETUP_TOP`, and a throw in a store body takes
17
+ // the whole app to Nuxt's error page. Same reason, and the same fix, as `usePipelineErrorToast`.
18
+ const { t } = useNuxtApp().$i18n as ReturnType<typeof useI18n>
13
19
  const toast = useToast()
14
20
  const { copy: writeClipboard, isSupported } = useClipboard()
15
21