@cat-factory/app 0.55.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, onMounted, reactive, ref } from 'vue'
2
+ import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
3
3
  import type { ContentStorageBackend, ContentStorageConfig } from '~/types/accountSettings'
4
4
 
5
5
  // Deployment integration secrets for an account (admin only): the Slack app OAuth
@@ -11,9 +11,30 @@ import type { ContentStorageBackend, ContentStorageConfig } from '~/types/accoun
11
11
  const props = defineProps<{ accountId: string }>()
12
12
 
13
13
  const store = useAccountSettingsStore()
14
+ const ui = useUiStore()
14
15
  const toast = useToast()
15
16
  const { t } = useI18n()
16
17
 
18
+ // Deep-link anchor: the pipeline-start "configure storage" prompt opens this tab with the
19
+ // ui store's scroll target set to `content-storage`, so we bring the storage section (which
20
+ // sits at the bottom of a long tab) into view once rather than leaving the user to hunt for
21
+ // it. Scrolls after the section actually renders (it is gated on the async settings load).
22
+ const storageSection = ref<HTMLElement | null>(null)
23
+ async function maybeScrollToStorage() {
24
+ if (ui.accountSettingsScrollTarget !== 'content-storage') return
25
+ await nextTick()
26
+ const el = storageSection.value
27
+ if (!el) return
28
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' })
29
+ ui.clearAccountSettingsScrollTarget()
30
+ }
31
+ watch(
32
+ () => ui.accountSettingsScrollTarget,
33
+ () => {
34
+ void maybeScrollToStorage()
35
+ },
36
+ )
37
+
17
38
  const slack = reactive({ clientId: '', clientSecret: '', redirectUrl: '' })
18
39
  const linear = reactive({ clientId: '', clientSecret: '', redirectUrl: '' })
19
40
  const web = reactive({ braveApiKey: '', searxngUrl: '', searxngApiKey: '' })
@@ -71,6 +92,9 @@ onMounted(async () => {
71
92
  try {
72
93
  await store.load(props.accountId)
73
94
  hydrateStorage()
95
+ // The storage section only renders once the settings load resolves, so attempt the
96
+ // deep-link scroll here (the up-front watcher misses the target set before mount).
97
+ void maybeScrollToStorage()
74
98
  } catch (e) {
75
99
  toast.add({
76
100
  title: t('layout.accountDeployment.loadFailed'),
@@ -508,7 +532,12 @@ async function clearWeb() {
508
532
  </section>
509
533
 
510
534
  <!-- Content storage (binary artifacts / screenshots) -->
511
- <section v-if="storageCapability" class="space-y-2 border-t border-slate-800 pt-6">
535
+ <section
536
+ v-if="storageCapability"
537
+ id="content-storage"
538
+ ref="storageSection"
539
+ class="space-y-2 border-t border-slate-800 pt-6"
540
+ >
512
541
  <div class="flex items-center gap-2">
513
542
  <h4 class="text-sm font-semibold text-slate-200">
514
543
  {{ t('layout.accountDeployment.contentStorage.title') }}
@@ -28,10 +28,14 @@ interface ConflictDetails {
28
28
  * fails THIS typecheck until it is mapped here. (The typed-message-keys feature can't see the
29
29
  * `t()` lookup because the key is resolved at runtime via this map, not written as a literal —
30
30
  * so the exhaustiveness of the map, not `t()`, is what makes a missing reason a build error.)
31
- * `providers_unconfigured` is excluded: it has bespoke handling + its own `providersUnconfigured.*`
32
- * key namespace, so it never reaches the generic lookup below.
31
+ * `providers_unconfigured` and `binary_storage_unconfigured` are excluded: each has bespoke
32
+ * handling (a "configure X" action) + its own key namespace, so neither reaches the generic
33
+ * lookup below.
33
34
  */
34
- const CONFLICT_TITLE_KEYS: Record<Exclude<ConflictReason, 'providers_unconfigured'>, string> = {
35
+ const CONFLICT_TITLE_KEYS: Record<
36
+ Exclude<ConflictReason, 'providers_unconfigured' | 'binary_storage_unconfigured'>,
37
+ string
38
+ > = {
35
39
  dependencies_unmet: 'errors.conflict.title.dependencies_unmet',
36
40
  task_limit_reached: 'errors.conflict.title.task_limit_reached',
37
41
  tester_infra_unsupported: 'errors.conflict.title.tester_infra_unsupported',
@@ -97,12 +101,41 @@ export function usePipelineErrorToast() {
97
101
  return
98
102
  }
99
103
 
104
+ // A pipeline step relies on binary-artifact storage (the UI Tester uploads screenshots)
105
+ // but the account has none configured. Explain it and offer the jump to the content-storage
106
+ // settings — the same shape as the providers-unconfigured case above. Prefer the localized
107
+ // body (it carries no runtime interpolation) so non-English users see translated copy; the
108
+ // raw backend prose is only the last-resort fallback when the locale lacks the key.
109
+ if (conflict?.reason === 'binary_storage_unconfigured') {
110
+ toast.add({
111
+ title: t('errors.conflict.binaryStorageUnconfigured.title'),
112
+ description: te('errors.conflict.binaryStorageUnconfigured.body')
113
+ ? t('errors.conflict.binaryStorageUnconfigured.body')
114
+ : (conflict.message ?? t('errors.conflict.fallbackMessage')),
115
+ color: 'error',
116
+ icon: 'i-lucide-image',
117
+ actions: [
118
+ {
119
+ label: t('errors.conflict.binaryStorageUnconfigured.action'),
120
+ icon: 'i-lucide-settings',
121
+ onClick: () => ui.openContentStorageSettings(),
122
+ },
123
+ ],
124
+ })
125
+ return
126
+ }
127
+
100
128
  if (conflict) {
101
129
  // Per-reason title key from the exhaustive map; fall back to the caller's title key when
102
130
  // this reason has no mapped/translated copy (`te` = translation-exists, so a key missing
103
131
  // in the active locale never leaks as raw text). An unknown reason isn't in the map.
104
132
  const reasonKey =
105
- CONFLICT_TITLE_KEYS[conflict.reason as Exclude<ConflictReason, 'providers_unconfigured'>]
133
+ CONFLICT_TITLE_KEYS[
134
+ conflict.reason as Exclude<
135
+ ConflictReason,
136
+ 'providers_unconfigured' | 'binary_storage_unconfigured'
137
+ >
138
+ ]
106
139
  toast.add({
107
140
  title: reasonKey && te(reasonKey) ? t(reasonKey) : t(fallbackTitleKey),
108
141
  description: conflict.message ?? t('errors.conflict.fallbackMessage'),
package/app/stores/ui.ts CHANGED
@@ -121,6 +121,11 @@ export const useUiStore = defineStore('ui', () => {
121
121
  // straight to a tab.
122
122
  const accountSettingsOpen = ref(false)
123
123
  const accountSettingsTab = ref('team')
124
+ // A one-shot deep-link anchor: when a surface opens account settings AND wants to land on a
125
+ // specific section within the (long) tab body, it sets this to that section's id. The owning
126
+ // panel scrolls the matching element into view once, then calls `clearAccountSettingsScrollTarget`
127
+ // so a later plain open doesn't re-scroll. Null when no section was requested.
128
+ const accountSettingsScrollTarget = ref<string | null>(null)
124
129
  // Observability integration: the post-release-health connection panel (Datadog
125
130
  // today, pluggable). NB: distinct from `observabilityInstanceId` below, which is the
126
131
  // LLM per-call observability panel.
@@ -464,8 +469,21 @@ export const useUiStore = defineStore('ui', () => {
464
469
  accountSettingsTab.value = tab
465
470
  accountSettingsOpen.value = true
466
471
  }
472
+ // Deep-link to the content (binary-artifact) storage configuration, which lives near the
473
+ // bottom of the account settings' team tab (`AccountDeploymentSettings`). Used by the
474
+ // pipeline-start error prompt when a storage-reliant agent (the UI Tester) has no storage
475
+ // configured. Sets a scroll anchor so the panel brings the storage section into view rather
476
+ // than dropping the user at the top of the long team tab to hunt for it.
477
+ function openContentStorageSettings() {
478
+ accountSettingsScrollTarget.value = 'content-storage'
479
+ openAccountSettings('team')
480
+ }
481
+ function clearAccountSettingsScrollTarget() {
482
+ accountSettingsScrollTarget.value = null
483
+ }
467
484
  function closeAccountSettings() {
468
485
  accountSettingsOpen.value = false
486
+ accountSettingsScrollTarget.value = null
469
487
  }
470
488
  function setAccountSettingsTab(tab: string) {
471
489
  accountSettingsTab.value = tab
@@ -664,6 +682,7 @@ export const useUiStore = defineStore('ui', () => {
664
682
  workspaceSettingsTab,
665
683
  accountSettingsOpen,
666
684
  accountSettingsTab,
685
+ accountSettingsScrollTarget,
667
686
  observabilityConnectionOpen,
668
687
  infrastructureOpen,
669
688
  infrastructureTab,
@@ -739,6 +758,8 @@ export const useUiStore = defineStore('ui', () => {
739
758
  closeWorkspaceSettings,
740
759
  setWorkspaceSettingsTab,
741
760
  openAccountSettings,
761
+ openContentStorageSettings,
762
+ clearAccountSettingsScrollTarget,
742
763
  closeAccountSettings,
743
764
  setAccountSettingsTab,
744
765
  openObservabilityConnection,
@@ -292,6 +292,11 @@
292
292
  "description": "Keep the named placeholder for the model list intact (a comma-separated list of model ids is injected at runtime)."
293
293
  },
294
294
  "action": "Configure AI"
295
+ },
296
+ "binaryStorageUnconfigured": {
297
+ "title": "No storage for this pipeline",
298
+ "body": "This pipeline includes an agent that needs binary storage (the UI Tester uploads its screenshots), but this account has no content storage configured. Configure content storage to run it.",
299
+ "action": "Configure storage"
295
300
  }
296
301
  }
297
302
  },
@@ -259,6 +259,11 @@
259
259
  "title": "No hay proveedor de IA para este modelo",
260
260
  "body": "No hay ningún proveedor configurado para {models}. Añade una clave de proveedor, conecta una suscripción o activa Cloudflare AI para ejecutarlo.",
261
261
  "action": "Configurar IA"
262
+ },
263
+ "binaryStorageUnconfigured": {
264
+ "title": "No hay almacenamiento para esta canalización",
265
+ "body": "Esta canalización incluye un agente que necesita almacenamiento binario (el probador de UI sube sus capturas de pantalla), pero esta cuenta no tiene almacenamiento de contenido configurado. Configura el almacenamiento de contenido para ejecutarla.",
266
+ "action": "Configurar almacenamiento"
262
267
  }
263
268
  }
264
269
  },
@@ -259,6 +259,11 @@
259
259
  "title": "Aucun fournisseur d’IA pour ce modèle",
260
260
  "body": "Aucun fournisseur n’est configuré pour {models}. Ajoutez une clé de fournisseur, connectez un abonnement ou activez Cloudflare AI pour l’exécuter.",
261
261
  "action": "Configurer l’IA"
262
+ },
263
+ "binaryStorageUnconfigured": {
264
+ "title": "Aucun stockage pour ce pipeline",
265
+ "body": "Ce pipeline inclut un agent qui a besoin d’un stockage binaire (le testeur d’UI téléverse ses captures d’écran), mais aucun stockage de contenu n’est configuré pour ce compte. Configurez le stockage de contenu pour l’exécuter.",
266
+ "action": "Configurer le stockage"
262
267
  }
263
268
  }
264
269
  },
@@ -259,6 +259,11 @@
259
259
  "title": "אין ספק AI למודל זה",
260
260
  "body": "לא מוגדר ספק עבור {models}. הוסף מפתח ספק, חבר מנוי, או הפעל את Cloudflare AI כדי להריץ אותו.",
261
261
  "action": "הגדר AI"
262
+ },
263
+ "binaryStorageUnconfigured": {
264
+ "title": "אין אחסון לצינור הזה",
265
+ "body": "צינור זה כולל סוכן שזקוק לאחסון בינארי (בודק ה-UI מעלה את צילומי המסך שלו), אך לחשבון זה לא מוגדר אחסון תוכן. הגדר אחסון תוכן כדי להריץ אותו.",
266
+ "action": "הגדר אחסון"
262
267
  }
263
268
  }
264
269
  },
@@ -259,6 +259,11 @@
259
259
  "title": "Brak dostawcy AI dla tego modelu",
260
260
  "body": "Dla {models} nie skonfigurowano żadnego dostawcy. Dodaj klucz dostawcy, połącz subskrypcję lub włącz Cloudflare AI, aby go uruchomić.",
261
261
  "action": "Skonfiguruj AI"
262
+ },
263
+ "binaryStorageUnconfigured": {
264
+ "title": "Brak magazynu dla tego potoku",
265
+ "body": "Ten potok zawiera agenta, który wymaga magazynu binarnego (tester UI przesyła swoje zrzuty ekranu), ale to konto nie ma skonfigurowanego magazynu treści. Skonfiguruj magazyn treści, aby go uruchomić.",
266
+ "action": "Skonfiguruj magazyn"
262
267
  }
263
268
  }
264
269
  },
@@ -259,6 +259,11 @@
259
259
  "title": "Немає постачальника ШІ для цієї моделі",
260
260
  "body": "Для {models} не налаштовано жодного постачальника. Додайте ключ постачальника, підключіть підписку або увімкніть Cloudflare AI, щоб запустити її.",
261
261
  "action": "Налаштувати ШІ"
262
+ },
263
+ "binaryStorageUnconfigured": {
264
+ "title": "Немає сховища для цього конвеєра",
265
+ "body": "Цей конвеєр містить агента, якому потрібне бінарне сховище (тестувальник інтерфейсу завантажує свої знімки екрана), але для цього облікового запису не налаштовано сховище вмісту. Налаштуйте сховище вмісту, щоб запустити його.",
266
+ "action": "Налаштувати сховище"
262
267
  }
263
268
  }
264
269
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.55.0",
3
+ "version": "0.56.0",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "valibot": "^1.4.2",
35
35
  "vue": "^3.5.39",
36
36
  "wretch": "^3.0.9",
37
- "@cat-factory/contracts": "0.57.0"
37
+ "@cat-factory/contracts": "0.58.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",