@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
@@ -65,6 +65,9 @@ const form = reactive({
65
65
  imageTemplate: '',
66
66
  urlSource: 'ingressTemplate' as UrlSource,
67
67
  hostTemplate: '',
68
+ // The ingress-template port, separate from `servicePort`: each url field belongs to exactly ONE
69
+ // variant, so a value entered for one source can never populate a config built for another.
70
+ ingressPort: '',
68
71
  ingressName: '',
69
72
  serviceName: '',
70
73
  servicePort: '',
@@ -121,28 +124,34 @@ watch(
121
124
  form.insecureSkipTlsVerify = k.insecureSkipTlsVerify === true
122
125
  form.namespaceTemplate = k.namespaceTemplate ?? ''
123
126
  form.imageTemplate = k.imageTemplate ?? ''
124
- // Each url field is read off the ONE variant that carries it, so a field belonging to a
125
- // different `source` cannot silently populate the form.
126
- //
127
- // Typed as present with an on-union `source`, read as neither: both were true when the
128
- // connect form admitted this config, and the value has been through storage since — which is
129
- // exactly why the backend re-parses a stored `providerConfig` rather than asserting it, and
130
- // this form is where an operator REPAIRS one that drifted. An unrecognised source falls back
131
- // to the form's default, because `buildUrl` has no branch to build a config out of one.
132
- const url: KubeUrlSource | undefined = k.url
133
- const source = url?.source
134
- form.urlSource = isKubernetesUrlSource(source) ? source : 'ingressTemplate'
135
- form.hostTemplate = url?.source === 'ingressTemplate' ? url.hostTemplate : ''
136
- form.ingressName = url?.source === 'ingressStatus' ? (url.ingressName ?? '') : ''
137
- form.serviceName = url?.source === 'serviceStatus' ? url.serviceName : ''
138
- form.servicePort = url?.source === 'serviceStatus' && url.port != null ? String(url.port) : ''
139
- form.gatewayName = url?.source === 'gatewayStatus' ? (url.gatewayName ?? '') : ''
140
- form.httpRouteName = url?.source === 'httpRouteStatus' ? (url.httpRouteName ?? '') : ''
141
- form.urlScheme = url?.scheme ?? 'default'
127
+ applyStoredUrlSource(k.url)
142
128
  },
143
129
  { immediate: true },
144
130
  )
145
131
 
132
+ /**
133
+ * Prefill the URL-derivation fields from a stored config. Each field is read off the ONE variant
134
+ * that carries it, so a field belonging to a different `source` cannot silently populate the form.
135
+ *
136
+ * Typed as present with an on-union `source`, read as neither: both were true when the connect form
137
+ * admitted this config, and the value has been through storage since, which is exactly why the
138
+ * backend re-parses a stored `providerConfig` rather than asserting it, and this form is where an
139
+ * operator REPAIRS one that drifted. An unrecognised source falls back to the form's default,
140
+ * because `buildUrl` has no branch to build a config out of one.
141
+ */
142
+ function applyStoredUrlSource(url: KubeUrlSource | undefined): void {
143
+ const source = url?.source
144
+ form.urlSource = isKubernetesUrlSource(source) ? source : 'ingressTemplate'
145
+ form.hostTemplate = url?.source === 'ingressTemplate' ? url.hostTemplate : ''
146
+ form.ingressPort = url?.source === 'ingressTemplate' && url.port != null ? String(url.port) : ''
147
+ form.ingressName = url?.source === 'ingressStatus' ? (url.ingressName ?? '') : ''
148
+ form.serviceName = url?.source === 'serviceStatus' ? url.serviceName : ''
149
+ form.servicePort = url?.source === 'serviceStatus' && url.port != null ? String(url.port) : ''
150
+ form.gatewayName = url?.source === 'gatewayStatus' ? (url.gatewayName ?? '') : ''
151
+ form.httpRouteName = url?.source === 'httpRouteStatus' ? (url.httpRouteName ?? '') : ''
152
+ form.urlScheme = url?.scheme ?? 'default'
153
+ }
154
+
146
155
  // The local-cluster apiserver address every loopback distro (k3s / k3d / kind / minikube)
147
156
  // exposes by default — see `seedForEngine`.
148
157
  const LOCAL_K3S_API_SERVER = 'https://127.0.0.1:6443'
@@ -185,22 +194,33 @@ watch(
185
194
  if (prefill.insecureSkipTlsVerify !== undefined)
186
195
  form.insecureSkipTlsVerify = prefill.insecureSkipTlsVerify
187
196
  if (prefill.namespaceTemplate.trim()) form.namespaceTemplate = prefill.namespaceTemplate.trim()
197
+ // An EMPTY host template is meaningful, not a gap the link forgot to fill: the CLI withholds
198
+ // it when it could not establish that the cluster serves an ingress-derived URL, and leaving
199
+ // the required field blank is what stops a URL nothing answers being saved.
188
200
  if (prefill.hostTemplate.trim()) {
189
201
  form.urlSource = 'ingressTemplate'
190
202
  form.hostTemplate = prefill.hostTemplate.trim()
203
+ // Carried alongside the template, never inside it: a local cluster publishing its controller
204
+ // on 18080 needs the port in the URL and NOT in the Ingress host the manifests declare.
205
+ form.ingressPort = prefill.ingressPort.trim()
191
206
  }
207
+ if (prefill.urlScheme) form.urlScheme = prefill.urlScheme
192
208
  },
193
209
  { immediate: true },
194
210
  )
195
211
 
196
- const servicePortValid = computed(() => {
197
- const raw = form.servicePort.trim()
198
- if (!raw) return true
199
- const port = Number(raw)
212
+ /** A blank port is valid (the scheme's default); anything typed has to be a real port number. */
213
+ function portValid(raw: string): boolean {
214
+ const trimmed = raw.trim()
215
+ if (!trimmed) return true
216
+ const port = Number(trimmed)
200
217
  return Number.isInteger(port) && port >= 1 && port <= 65535
201
- })
218
+ }
219
+ const servicePortValid = computed(() => portValid(form.servicePort))
220
+ const ingressPortValid = computed(() => portValid(form.ingressPort))
202
221
  const urlValid = computed(() => {
203
- if (form.urlSource === 'ingressTemplate') return !!form.hostTemplate.trim()
222
+ if (form.urlSource === 'ingressTemplate')
223
+ return !!form.hostTemplate.trim() && ingressPortValid.value
204
224
  if (form.urlSource === 'serviceStatus') return !!form.serviceName.trim() && servicePortValid.value
205
225
  return true // ingressStatus / gatewayStatus / httpRouteStatus have no required field
206
226
  })
@@ -257,8 +277,15 @@ const connectBlockedReason = computed(() => {
257
277
  function buildUrl(): KubeUrlSource {
258
278
  const scheme = form.urlScheme === 'default' ? {} : { scheme: form.urlScheme }
259
279
  switch (form.urlSource) {
260
- case 'ingressTemplate':
261
- return { source: 'ingressTemplate', hostTemplate: form.hostTemplate.trim(), ...scheme }
280
+ case 'ingressTemplate': {
281
+ const port = Number(form.ingressPort)
282
+ return {
283
+ source: 'ingressTemplate',
284
+ hostTemplate: form.hostTemplate.trim(),
285
+ ...(form.ingressPort.trim() && Number.isInteger(port) ? { port } : {}),
286
+ ...scheme,
287
+ }
288
+ }
262
289
  case 'ingressStatus': {
263
290
  const ingressName = form.ingressName.trim()
264
291
  return { source: 'ingressStatus', ...(ingressName ? { ingressName } : {}), ...scheme }
@@ -456,6 +483,24 @@ async function copyAutoSetupCommand() {
456
483
  />
457
484
  </UFormField>
458
485
 
486
+ <!-- The host port the controller answers on, when it is not the scheme's default. Separate from
487
+ the template on purpose: the rendered template is also the Ingress `host` the manifests
488
+ declare, and Kubernetes rejects a `host` with a port in it. -->
489
+ <UFormField
490
+ v-if="form.urlSource === 'ingressTemplate'"
491
+ :label="optional(t('settings.infrastructure.kubernetesEngine.port'))"
492
+ :help="t('settings.infrastructure.kubernetesEngine.ingressPortHelp')"
493
+ >
494
+ <UInput
495
+ v-model="form.ingressPort"
496
+ type="number"
497
+ :min="1"
498
+ :max="65535"
499
+ class="font-mono"
500
+ placeholder="80"
501
+ />
502
+ </UFormField>
503
+
459
504
  <UFormField
460
505
  v-if="form.urlSource === 'ingressStatus'"
461
506
  :label="optional(t('settings.infrastructure.kubernetesEngine.ingressName'))"
@@ -46,6 +46,9 @@ const form = reactive({
46
46
  // url derivation
47
47
  urlSource: 'ingressTemplate' as 'ingressTemplate' | 'ingressStatus' | 'serviceStatus',
48
48
  hostTemplate: '',
49
+ // The ingress-template port, its own field because the rendered template is also the Ingress
50
+ // `host` a service's manifests declare, and Kubernetes rejects a `host` carrying a port.
51
+ ingressPort: '',
49
52
  ingressName: '',
50
53
  serviceName: '',
51
54
  servicePort: '',
@@ -111,6 +114,7 @@ function applyUrl(k: Record<string, unknown>): void {
111
114
  if (url?.source === 'ingressTemplate') {
112
115
  form.urlSource = 'ingressTemplate'
113
116
  form.hostTemplate = readString(url.hostTemplate)
117
+ form.ingressPort = typeof url.port === 'number' ? String(url.port) : ''
114
118
  } else if (url?.source === 'ingressStatus') {
115
119
  form.urlSource = 'ingressStatus'
116
120
  form.ingressName = readString(url.ingressName)
@@ -154,16 +158,20 @@ const manifestSourceValid = computed(() =>
154
158
  ? repoShapeValid.value && !!form.manifestPath.trim()
155
159
  : !!form.manifestPath.trim(),
156
160
  )
157
- // serviceStatus.port is an optional integer 1..65535 (kubernetesUrlSourceSchema). Validate
158
- // it here so a decimal isn't silently dropped and an out-of-range value isn't sent then 422'd.
159
- const servicePortValid = computed(() => {
160
- const raw = form.servicePort.trim()
161
- if (!raw) return true
162
- const port = Number(raw)
161
+ // Both `ingressTemplate.port` and `serviceStatus.port` are optional integers 1..65535
162
+ // (kubernetesUrlSourceSchema). Validate here so a decimal isn't silently dropped and an
163
+ // out-of-range value isn't sent then 422'd.
164
+ function portValid(raw: string): boolean {
165
+ const trimmed = raw.trim()
166
+ if (!trimmed) return true
167
+ const port = Number(trimmed)
163
168
  return Number.isInteger(port) && port >= 1 && port <= 65535
164
- })
169
+ }
170
+ const servicePortValid = computed(() => portValid(form.servicePort))
171
+ const ingressPortValid = computed(() => portValid(form.ingressPort))
165
172
  const urlValid = computed(() => {
166
- if (form.urlSource === 'ingressTemplate') return !!form.hostTemplate.trim()
173
+ if (form.urlSource === 'ingressTemplate')
174
+ return !!form.hostTemplate.trim() && ingressPortValid.value
167
175
  if (form.urlSource === 'serviceStatus') return !!form.serviceName.trim() && servicePortValid.value
168
176
  return true // ingressStatus has no required field
169
177
  })
@@ -219,6 +227,8 @@ function buildUrl(): Record<string, unknown> {
219
227
  const url: Record<string, unknown> = { source: form.urlSource }
220
228
  if (form.urlSource === 'ingressTemplate') {
221
229
  url.hostTemplate = form.hostTemplate.trim()
230
+ const port = Number(form.ingressPort)
231
+ if (form.ingressPort.trim() && Number.isInteger(port)) url.port = port
222
232
  } else if (form.urlSource === 'ingressStatus') {
223
233
  if (form.ingressName.trim()) url.ingressName = form.ingressName.trim()
224
234
  } else {
@@ -338,6 +348,23 @@ function optional(label: string): string {
338
348
  />
339
349
  </UFormField>
340
350
 
351
+ <!-- The host port the controller answers on, when it is not the scheme's default. Kept out of
352
+ the template because that value is also the Ingress `host` the manifests declare. -->
353
+ <UFormField
354
+ v-if="form.urlSource === 'ingressTemplate'"
355
+ :label="optional(t('settings.providerConnection.kubernetesEnv.port'))"
356
+ :help="t('settings.providerConnection.kubernetesEnv.ingressPortHelp')"
357
+ >
358
+ <UInput
359
+ v-model="form.ingressPort"
360
+ type="number"
361
+ :min="1"
362
+ :max="65535"
363
+ class="font-mono"
364
+ placeholder="80"
365
+ />
366
+ </UFormField>
367
+
341
368
  <UFormField
342
369
  v-if="form.urlSource === 'ingressStatus'"
343
370
  :label="optional(t('settings.providerConnection.kubernetesEnv.ingressName'))"
@@ -14,6 +14,7 @@ import { reactive, ref, watch } from 'vue'
14
14
  const { t } = useI18n()
15
15
  const store = useLocalSettingsStore()
16
16
  const toast = useToast()
17
+ const { present } = usePipelineErrorToast()
17
18
 
18
19
  const saving = ref(false)
19
20
 
@@ -65,12 +66,7 @@ async function save() {
65
66
  color: 'success',
66
67
  })
67
68
  } catch (e) {
68
- toast.add({
69
- title: t('settings.localMode.toast.saveFailed'),
70
- description: e instanceof Error ? e.message : String(e),
71
- icon: 'i-lucide-triangle-alert',
72
- color: 'error',
73
- })
69
+ present(e, 'settings.localMode.toast.saveFailed')
74
70
  } finally {
75
71
  saving.value = false
76
72
  }
@@ -19,6 +19,7 @@ const { t } = useI18n()
19
19
  const ui = useUiStore()
20
20
  const store = useLocalModelsStore()
21
21
  const toast = useToast()
22
+ const { present } = usePipelineErrorToast()
22
23
  const { confirm } = useConfirm()
23
24
 
24
25
  const open = computed({
@@ -96,15 +97,6 @@ watch(provider, (p) => {
96
97
  tested.value = false
97
98
  })
98
99
 
99
- function notifyError(title: string, e: unknown) {
100
- toast.add({
101
- title,
102
- description: e instanceof Error ? e.message : String(e),
103
- icon: 'i-lucide-triangle-alert',
104
- color: 'error',
105
- })
106
- }
107
-
108
100
  async function test() {
109
101
  if (!baseUrl.value.trim()) return
110
102
  testing.value = true
@@ -166,7 +158,7 @@ async function save() {
166
158
  color: 'success',
167
159
  })
168
160
  } catch (e) {
169
- notifyError(t('settings.localModelEndpoints.toast.saveFailed'), e)
161
+ present(e, 'settings.localModelEndpoints.toast.saveFailed')
170
162
  } finally {
171
163
  busy.value = false
172
164
  }
@@ -195,7 +187,7 @@ async function remove(p: LocalRunner) {
195
187
  }
196
188
  toast.add({ title: t('settings.localModelEndpoints.toast.removed'), icon: 'i-lucide-check' })
197
189
  } catch (e) {
198
- notifyError(t('settings.localModelEndpoints.toast.removeFailed'), e)
190
+ present(e, 'settings.localModelEndpoints.toast.removeFailed')
199
191
  } finally {
200
192
  busy.value = false
201
193
  }
@@ -32,6 +32,7 @@ const agents = useAgentsStore()
32
32
  const agentTier = useAgentTierStore()
33
33
  const creds = useVendorCredentialsStore()
34
34
  const workspace = useWorkspaceStore()
35
+ const { present } = usePipelineErrorToast()
35
36
  const toast = useToast()
36
37
  const { confirm } = useConfirm()
37
38
 
@@ -180,7 +181,7 @@ async function setDefault(p: ModelPreset) {
180
181
  try {
181
182
  await presets.update(p.id, { isDefault: true })
182
183
  } catch (e) {
183
- fail(t('settings.modelConfiguration.toast.setDefaultFailed'), e)
184
+ present(e, 'settings.modelConfiguration.toast.setDefaultFailed')
184
185
  } finally {
185
186
  busy.value = false
186
187
  }
@@ -199,7 +200,7 @@ async function remove(p: ModelPreset) {
199
200
  try {
200
201
  await presets.remove(p.id)
201
202
  } catch (e) {
202
- fail(t('settings.modelConfiguration.toast.deleteFailed'), e)
203
+ present(e, 'settings.modelConfiguration.toast.deleteFailed')
203
204
  } finally {
204
205
  busy.value = false
205
206
  }
@@ -251,10 +252,15 @@ async function save() {
251
252
  const e = editor.value
252
253
  if (!e) return
253
254
  if (!e.name.trim()) {
254
- fail(
255
- t('settings.modelConfiguration.toast.nameRequiredTitle'),
256
- new Error(t('settings.modelConfiguration.toast.nameRequiredBody')),
257
- )
255
+ // NOT through `present`: that funnel classifies a BACKEND failure, and a synthesized local
256
+ // `Error` carries no envelope and no status, so it lands on the network-fault description and
257
+ // tells the user the server could not be reached about a check that never left the browser.
258
+ toast.add({
259
+ title: t('settings.modelConfiguration.toast.nameRequiredTitle'),
260
+ description: t('settings.modelConfiguration.toast.nameRequiredBody'),
261
+ color: 'warning',
262
+ icon: 'i-lucide-triangle-alert',
263
+ })
258
264
  return
259
265
  }
260
266
  busy.value = true
@@ -280,20 +286,11 @@ async function save() {
280
286
  }
281
287
  editor.value = null
282
288
  } catch (err) {
283
- fail(t('settings.modelConfiguration.toast.saveFailed'), err)
289
+ present(err, 'settings.modelConfiguration.toast.saveFailed')
284
290
  } finally {
285
291
  busy.value = false
286
292
  }
287
293
  }
288
-
289
- function fail(title: string, e: unknown) {
290
- toast.add({
291
- title,
292
- description: e instanceof Error ? e.message : String(e),
293
- icon: 'i-lucide-triangle-alert',
294
- color: 'error',
295
- })
296
- }
297
294
  </script>
298
295
 
299
296
  <template>
@@ -9,7 +9,7 @@ const { t } = useI18n()
9
9
  const ui = useUiStore()
10
10
  const presets = useModelPresetsStore()
11
11
  const { newPresets, outdated, hasIssues } = useModelPresetHealth()
12
- const toast = useToast()
12
+ const { present } = usePipelineErrorToast()
13
13
 
14
14
  const open = computed({
15
15
  get: () => ui.modelPresetHealthOpen,
@@ -28,12 +28,7 @@ async function reseed(id: string) {
28
28
  try {
29
29
  await presets.reseed(id)
30
30
  } catch (e) {
31
- toast.add({
32
- title: t('modelPreset.health.toast.reseedFailed'),
33
- description: e instanceof Error ? e.message : String(e),
34
- icon: 'i-lucide-triangle-alert',
35
- color: 'error',
36
- })
31
+ present(e, 'modelPreset.health.toast.reseedFailed')
37
32
  } finally {
38
33
  const next = new Set(busy.value)
39
34
  next.delete(id)
@@ -48,12 +43,7 @@ async function reseedAll() {
48
43
  try {
49
44
  await presets.reseedMany(ids)
50
45
  } catch (e) {
51
- toast.add({
52
- title: t('modelPreset.health.toast.reseedFailed'),
53
- description: e instanceof Error ? e.message : String(e),
54
- icon: 'i-lucide-triangle-alert',
55
- color: 'error',
56
- })
46
+ present(e, 'modelPreset.health.toast.reseedFailed')
57
47
  } finally {
58
48
  const next = new Set(busy.value)
59
49
  for (const id of ids) next.delete(id)
@@ -13,6 +13,7 @@ const { t } = useI18n()
13
13
  const ui = useUiStore()
14
14
  const store = useReleaseHealthStore()
15
15
  const toast = useToast()
16
+ const { present } = usePipelineErrorToast()
16
17
  const { confirmAction, toastDone } = useConfirmAction()
17
18
 
18
19
  const open = computed({
@@ -36,15 +37,6 @@ const pagerDuty = reactive({ apiToken: '', fromEmail: '' })
36
37
  const incidentIo = reactive({ apiKey: '' })
37
38
  const incidentBusy = ref(false)
38
39
 
39
- function notifyError(title: string, e: unknown) {
40
- toast.add({
41
- title,
42
- description: e instanceof Error ? e.message : String(e),
43
- icon: 'i-lucide-triangle-alert',
44
- color: 'error',
45
- })
46
- }
47
-
48
40
  watch(
49
41
  open,
50
42
  async (isOpen) => {
@@ -56,7 +48,7 @@ watch(
56
48
  if (site) datadog.site = site
57
49
  await store.loadIncident()
58
50
  } catch (e) {
59
- notifyError(t('settings.observabilityConnection.toast.loadFailed'), e)
51
+ present(e, 'settings.observabilityConnection.toast.loadFailed')
60
52
  }
61
53
  },
62
54
  { immediate: true },
@@ -90,7 +82,7 @@ async function saveIncident() {
90
82
  color: 'success',
91
83
  })
92
84
  } catch (e) {
93
- notifyError(t('settings.observabilityConnection.toast.incidentSaveFailed'), e)
85
+ present(e, 'settings.observabilityConnection.toast.incidentSaveFailed')
94
86
  } finally {
95
87
  incidentBusy.value = false
96
88
  }
@@ -104,7 +96,7 @@ async function disconnectIncident() {
104
96
  await store.removeIncident()
105
97
  toastDone('disconnect', noun)
106
98
  } catch (e) {
107
- notifyError(t('settings.observabilityConnection.toast.incidentDisconnectFailed'), e)
99
+ present(e, 'settings.observabilityConnection.toast.incidentDisconnectFailed')
108
100
  } finally {
109
101
  incidentBusy.value = false
110
102
  }
@@ -125,7 +117,7 @@ async function saveConnection() {
125
117
  color: 'success',
126
118
  })
127
119
  } catch (e) {
128
- notifyError(t('settings.observabilityConnection.toast.connectFailed'), e)
120
+ present(e, 'settings.observabilityConnection.toast.connectFailed')
129
121
  } finally {
130
122
  busy.value = false
131
123
  }
@@ -139,7 +131,7 @@ async function disconnect() {
139
131
  await store.removeConnection()
140
132
  toastDone('disconnect', noun)
141
133
  } catch (e) {
142
- notifyError(t('settings.observabilityConnection.toast.disconnectFailed'), e)
134
+ present(e, 'settings.observabilityConnection.toast.disconnectFailed')
143
135
  } finally {
144
136
  busy.value = false
145
137
  }
@@ -19,6 +19,7 @@ const store = useOpenRouterStore()
19
19
  const apiKeys = useApiKeysStore()
20
20
  const models = useModelsStore()
21
21
  const toast = useToast()
22
+ const { present } = usePipelineErrorToast()
22
23
 
23
24
  const open = computed({
24
25
  get: () => ui.openRouterOpen,
@@ -158,12 +159,7 @@ async function connectKey() {
158
159
  color: 'success',
159
160
  })
160
161
  } catch (e) {
161
- toast.add({
162
- title: t('settings.openRouterCatalog.toast.connectFailed'),
163
- description: e instanceof Error ? e.message : String(e),
164
- icon: 'i-lucide-triangle-alert',
165
- color: 'error',
166
- })
162
+ present(e, 'settings.openRouterCatalog.toast.connectFailed')
167
163
  } finally {
168
164
  connectingKey.value = false
169
165
  }
@@ -200,12 +196,7 @@ async function save() {
200
196
  color: 'success',
201
197
  })
202
198
  } catch (e) {
203
- toast.add({
204
- title: t('settings.openRouterCatalog.toast.saveFailed'),
205
- description: e instanceof Error ? e.message : String(e),
206
- icon: 'i-lucide-triangle-alert',
207
- color: 'error',
208
- })
199
+ present(e, 'settings.openRouterCatalog.toast.saveFailed')
209
200
  } finally {
210
201
  busy.value = false
211
202
  }
@@ -13,6 +13,7 @@ import SecretInput from '~/components/common/SecretInput.vue'
13
13
  const { t } = useI18n()
14
14
  const store = usePackageRegistriesStore()
15
15
  const toast = useToast()
16
+ const { present } = usePipelineErrorToast()
16
17
  const { confirmAction, toastDone } = useConfirmAction()
17
18
 
18
19
  // The registry vendors a workspace can connect. Fixed set — the host derives from the
@@ -44,15 +45,6 @@ const parsedScopes = computed(() =>
44
45
  .map((s) => (s.startsWith('@') ? s : `@${s}`)),
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 tab this renders in only exists once the window's probe resolved `available === true`, so
57
49
  // `ensureLoaded()` here would early-return every time and this error branch would be dead code.
58
50
  // Read the list outright instead: the window owns the PROBE (a failure there means no tab), the
@@ -63,7 +55,7 @@ onMounted(async () => {
63
55
  try {
64
56
  await store.load()
65
57
  } catch (e) {
66
- notifyError(t('settings.packageRegistries.toast.loadFailed'), e)
58
+ present(e, 'settings.packageRegistries.toast.loadFailed')
67
59
  }
68
60
  })
69
61
 
@@ -84,7 +76,7 @@ async function addEntry() {
84
76
  color: 'success',
85
77
  })
86
78
  } catch (e) {
87
- notifyError(t('settings.packageRegistries.toast.addFailed'), e)
79
+ present(e, 'settings.packageRegistries.toast.addFailed')
88
80
  } finally {
89
81
  busy.value = false
90
82
  }
@@ -98,7 +90,7 @@ async function removeEntry(entryId: string) {
98
90
  await store.remove(entryId)
99
91
  toastDone('remove', noun)
100
92
  } catch (e) {
101
- notifyError(t('settings.packageRegistries.toast.removeFailed'), e)
93
+ present(e, 'settings.packageRegistries.toast.removeFailed')
102
94
  } finally {
103
95
  busy.value = false
104
96
  }
@@ -36,6 +36,7 @@ const emit = defineEmits<{ connected: [] }>()
36
36
  const { t } = useI18n()
37
37
  const store = useProviderConnectionsStore()
38
38
  const toast = useToast()
39
+ const { present } = usePipelineErrorToast()
39
40
  const { confirmAction } = useConfirmAction()
40
41
 
41
42
  const descriptor = computed(() => store.descriptorFor(props.kind))
@@ -205,15 +206,6 @@ function buildFlatPayload() {
205
206
  return isNativeConfig.value ? buildConfigPayload() : buildManifestPayload()
206
207
  }
207
208
 
208
- function notifyError(title: string, e: unknown) {
209
- toast.add({
210
- title,
211
- description: e instanceof Error ? e.message : String(e),
212
- icon: 'i-lucide-triangle-alert',
213
- color: 'error',
214
- })
215
- }
216
-
217
209
  function toastSaved() {
218
210
  toast.add({
219
211
  title: t('settings.providerConnection.toast.saved', { title: title.value }),
@@ -246,7 +238,7 @@ async function saveNative() {
246
238
  resetDraft()
247
239
  toastSaved()
248
240
  } catch (e) {
249
- notifyError(t('settings.providerConnection.toast.saveFailed'), e)
241
+ present(e, 'settings.providerConnection.toast.saveFailed')
250
242
  } finally {
251
243
  busy.value = false
252
244
  }
@@ -280,7 +272,7 @@ async function saveManifest(payload: {
280
272
  emit('connected')
281
273
  toastSaved()
282
274
  } catch (e) {
283
- notifyError(t('settings.providerConnection.toast.saveFailed'), e)
275
+ present(e, 'settings.providerConnection.toast.saveFailed')
284
276
  } finally {
285
277
  busy.value = false
286
278
  }
@@ -325,7 +317,7 @@ async function saveConfig(payload: {
325
317
  emit('connected')
326
318
  toastSaved()
327
319
  } catch (e) {
328
- notifyError(t('settings.providerConnection.toast.saveFailed'), e)
320
+ present(e, 'settings.providerConnection.toast.saveFailed')
329
321
  } finally {
330
322
  busy.value = false
331
323
  }
@@ -339,7 +331,7 @@ async function remove() {
339
331
  resetDraft()
340
332
  toast.add({ title: t('settings.providerConnection.toast.removed'), icon: 'i-lucide-check' })
341
333
  } catch (e) {
342
- notifyError(t('settings.providerConnection.toast.removeFailed'), e)
334
+ present(e, 'settings.providerConnection.toast.removeFailed')
343
335
  } finally {
344
336
  busy.value = false
345
337
  }
@@ -9,7 +9,7 @@ const { t } = useI18n()
9
9
  const ui = useUiStore()
10
10
  const presets = useRiskPoliciesStore()
11
11
  const { newPresets, outdated, hasIssues } = useRiskPolicyHealth()
12
- const toast = useToast()
12
+ const { present } = usePipelineErrorToast()
13
13
 
14
14
  const open = computed({
15
15
  get: () => ui.riskPolicyHealthOpen,
@@ -28,12 +28,7 @@ async function reseed(id: string) {
28
28
  try {
29
29
  await presets.reseed(id)
30
30
  } catch (e) {
31
- toast.add({
32
- title: t('riskPolicy.health.toast.reseedFailed'),
33
- description: e instanceof Error ? e.message : String(e),
34
- icon: 'i-lucide-triangle-alert',
35
- color: 'error',
36
- })
31
+ present(e, 'riskPolicy.health.toast.reseedFailed')
37
32
  } finally {
38
33
  const next = new Set(busy.value)
39
34
  next.delete(id)