@cat-factory/app 0.54.4 → 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.
Files changed (81) hide show
  1. package/app/app.vue +14 -0
  2. package/app/components/auth/UserMenu.vue +1 -1
  3. package/app/components/board/AddTaskModal.vue +5 -4
  4. package/app/components/board/nodes/AgentChip.vue +2 -2
  5. package/app/components/board/nodes/BlockNode.vue +12 -8
  6. package/app/components/board/nodes/DraggableTask.vue +1 -1
  7. package/app/components/board/nodes/EpicNode.vue +1 -1
  8. package/app/components/board/nodes/ModuleFrame.vue +7 -4
  9. package/app/components/board/nodes/TaskCard.vue +2 -2
  10. package/app/components/board/nodes/TaskPipelineMini.vue +3 -3
  11. package/app/components/brainstorm/BrainstormWindow.vue +8 -7
  12. package/app/components/clarity/ClarityReviewWindow.vue +8 -7
  13. package/app/components/consensus/ConsensusSessionWindow.vue +2 -2
  14. package/app/components/documents/ContextDocumentPicker.vue +4 -4
  15. package/app/components/documents/SpawnPreviewModal.vue +4 -4
  16. package/app/components/focus/BlockFocusView.vue +11 -4
  17. package/app/components/fragments/FragmentLibraryManager.vue +5 -5
  18. package/app/components/gates/GateResultView.vue +3 -3
  19. package/app/components/github/GitHubPanel.vue +5 -2
  20. package/app/components/humanTest/HumanTestWindow.vue +1 -1
  21. package/app/components/kaizen/KaizenPanel.vue +5 -5
  22. package/app/components/kaizen/KaizenStepStatus.vue +2 -2
  23. package/app/components/layout/AccountDeploymentSettings.vue +31 -2
  24. package/app/components/layout/BoardSwitcher.vue +4 -4
  25. package/app/components/layout/CommandBar.vue +1 -1
  26. package/app/components/layout/IntegrationBackTitle.vue +2 -1
  27. package/app/components/layout/IntegrationsHub.vue +10 -4
  28. package/app/components/layout/LanguageSwitcher.vue +1 -1
  29. package/app/components/layout/NotificationsInbox.vue +1 -1
  30. package/app/components/layout/PersonalSetupModal.vue +5 -2
  31. package/app/components/layout/SideBar.vue +5 -3
  32. package/app/components/media/ArtifactLightbox.vue +5 -5
  33. package/app/components/media/ImageCompare.vue +6 -1
  34. package/app/components/observability/StepMetricsBar.vue +6 -2
  35. package/app/components/palettes/AgentPalette.vue +4 -4
  36. package/app/components/panels/AgentStepDetail.vue +12 -12
  37. package/app/components/panels/GenericStructuredResultView.vue +1 -1
  38. package/app/components/panels/InspectorPanel.vue +2 -2
  39. package/app/components/panels/ObservabilityPanel.vue +6 -6
  40. package/app/components/panels/StepMetadataCard.vue +1 -1
  41. package/app/components/panels/inspector/ContainerSummary.vue +2 -2
  42. package/app/components/panels/inspector/EpicChildren.vue +3 -3
  43. package/app/components/panels/inspector/RecurringScheduleSettings.vue +1 -1
  44. package/app/components/panels/inspector/ServiceFragments.vue +1 -1
  45. package/app/components/panels/inspector/ServiceTestConfig.vue +1 -1
  46. package/app/components/panels/inspector/TaskDependencies.vue +1 -1
  47. package/app/components/panels/inspector/TaskEstimateBadge.vue +1 -1
  48. package/app/components/panels/inspector/TaskExecution.vue +9 -9
  49. package/app/components/panels/inspector/TaskRunSettings.vue +2 -2
  50. package/app/components/panels/inspector/TaskStructure.vue +1 -1
  51. package/app/components/pipeline/IterationCapPrompt.vue +1 -0
  52. package/app/components/pipeline/PipelineBuilder.vue +8 -8
  53. package/app/components/pipeline/PipelineProgress.vue +4 -4
  54. package/app/components/providers/ApiKeysSection.vue +2 -2
  55. package/app/components/providers/PersonalSubscriptionSection.vue +2 -2
  56. package/app/components/providers/VendorCredentialsModal.vue +2 -2
  57. package/app/components/provisioning/ProvisioningLogsDrawer.vue +1 -1
  58. package/app/components/requirements/RequirementsReviewWindow.vue +9 -8
  59. package/app/components/sandbox/SandboxPanel.vue +17 -17
  60. package/app/components/settings/LocalModelEndpointsPanel.vue +1 -1
  61. package/app/components/settings/ModelConfigurationPanel.vue +4 -3
  62. package/app/components/settings/OpenRouterCatalogPanel.vue +3 -3
  63. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +1 -1
  64. package/app/components/spec/ServiceSpecWindow.vue +3 -3
  65. package/app/components/tasks/ContextIssuePicker.vue +5 -5
  66. package/app/components/tasks/TaskContextIssues.vue +1 -1
  67. package/app/components/tasks/TaskImportModal.vue +1 -1
  68. package/app/components/testing/TestReportWindow.vue +5 -5
  69. package/app/components/visualConfirm/VisualConfirmationWindow.vue +2 -2
  70. package/app/composables/usePipelineErrorToast.ts +37 -4
  71. package/app/pages/index.vue +1 -1
  72. package/app/stores/ui.ts +21 -0
  73. package/i18n/i18n.config.ts +9 -0
  74. package/i18n/locales/en.json +5 -0
  75. package/i18n/locales/es.json +5 -0
  76. package/i18n/locales/fr.json +5 -0
  77. package/i18n/locales/he.json +3390 -0
  78. package/i18n/locales/pl.json +5 -0
  79. package/i18n/locales/uk.json +5 -0
  80. package/nuxt.config.ts +1 -0
  81. package/package.json +2 -2
@@ -203,12 +203,12 @@ onMounted(() => {
203
203
  v-for="row in importedRows"
204
204
  :key="`imp:${row.externalId}`"
205
205
  type="button"
206
- class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs text-slate-300 hover:bg-slate-800/70"
206
+ class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-start text-xs text-slate-300 hover:bg-slate-800/70"
207
207
  @click="pickImported(row)"
208
208
  >
209
209
  <UIcon :name="icon" class="h-3.5 w-3.5 shrink-0 text-indigo-400" />
210
210
  <span class="truncate">{{ row.externalId }} · {{ row.title }}</span>
211
- <UBadge color="neutral" variant="soft" size="xs" class="ml-auto shrink-0">{{
211
+ <UBadge color="neutral" variant="soft" size="xs" class="ms-auto shrink-0">{{
212
212
  t('tasks.picker.imported')
213
213
  }}</UBadge>
214
214
  </button>
@@ -218,12 +218,12 @@ onMounted(() => {
218
218
  v-for="r in searchRows"
219
219
  :key="`hit:${r.externalId}`"
220
220
  type="button"
221
- class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs text-slate-300 hover:bg-slate-800/70"
221
+ class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-start text-xs text-slate-300 hover:bg-slate-800/70"
222
222
  @click="pickSearch(r)"
223
223
  >
224
224
  <UIcon :name="icon" class="h-3.5 w-3.5 shrink-0 text-slate-400" />
225
225
  <span class="truncate">{{ r.externalId }} · {{ r.title }}</span>
226
- <UBadge v-if="r.status" color="neutral" variant="soft" size="xs" class="ml-auto shrink-0">
226
+ <UBadge v-if="r.status" color="neutral" variant="soft" size="xs" class="ms-auto shrink-0">
227
227
  {{ r.status }}
228
228
  </UBadge>
229
229
  </button>
@@ -232,7 +232,7 @@ onMounted(() => {
232
232
  <button
233
233
  v-if="refRow"
234
234
  type="button"
235
- class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs text-slate-300 hover:bg-slate-800/70"
235
+ class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-start text-xs text-slate-300 hover:bg-slate-800/70"
236
236
  @click="pickRef(refRow)"
237
237
  >
238
238
  <UIcon name="i-lucide-link" class="h-3.5 w-3.5 shrink-0 text-slate-400" />
@@ -79,7 +79,7 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
79
79
  class="h-3.5 w-3.5 shrink-0 text-indigo-400"
80
80
  />
81
81
  <span class="truncate">{{ task.externalId }} · {{ task.title }}</span>
82
- <UBadge color="neutral" variant="soft" size="xs" class="ml-auto shrink-0">
82
+ <UBadge color="neutral" variant="soft" size="xs" class="ms-auto shrink-0">
83
83
  {{ task.status }}
84
84
  </UBadge>
85
85
  </a>
@@ -331,7 +331,7 @@ async function doSpawnEpic() {
331
331
  >
332
332
  <button
333
333
  type="button"
334
- class="min-w-0 flex-1 text-left disabled:cursor-not-allowed disabled:opacity-60"
334
+ class="min-w-0 flex-1 text-start disabled:cursor-not-allowed disabled:opacity-60"
335
335
  :disabled="!containerId"
336
336
  :title="
337
337
  containerId
@@ -381,7 +381,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
381
381
  </span>
382
382
  <span
383
383
  v-if="infraSetup.durationMs != null"
384
- class="ml-auto text-[11px] text-slate-500"
384
+ class="ms-auto text-[11px] text-slate-500"
385
385
  >
386
386
  {{
387
387
  t('testing.standup.took', {
@@ -484,7 +484,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
484
484
  : t('testing.fixerTimeline.failed')
485
485
  }}
486
486
  </UBadge>
487
- <span class="ml-auto text-[11px] text-slate-500">{{
487
+ <span class="ms-auto text-[11px] text-slate-500">{{
488
488
  d(new Date(a.at), 'short')
489
489
  }}</span>
490
490
  </div>
@@ -541,7 +541,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
541
541
  class="overflow-hidden rounded-lg border border-slate-800 bg-slate-900/60"
542
542
  >
543
543
  <button
544
- class="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-slate-800/40"
544
+ class="flex w-full items-center gap-2 px-3 py-2 text-start hover:bg-slate-800/40"
545
545
  @click="toggle(g.key)"
546
546
  >
547
547
  <UIcon
@@ -584,7 +584,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
584
584
  </span>
585
585
  </button>
586
586
 
587
- <div v-if="!collapsed.has(g.key)" class="space-y-1 px-3 pb-3 pl-9">
587
+ <div v-if="!collapsed.has(g.key)" class="space-y-1 px-3 pb-3 ps-9">
588
588
  <!-- Outcomes -->
589
589
  <div
590
590
  v-for="(o, oi) in g.outcomes"
@@ -710,7 +710,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
710
710
 
711
711
  <!-- Sidebar: metadata -->
712
712
  <aside
713
- class="hidden w-60 shrink-0 flex-col gap-4 border-l border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
713
+ class="hidden w-60 shrink-0 flex-col gap-4 border-s border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
714
714
  >
715
715
  <div v-if="report">
716
716
  <h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
@@ -315,7 +315,7 @@ const canApprove = computed(
315
315
  type="file"
316
316
  accept="image/png,image/jpeg"
317
317
  :disabled="busy || !uploadView.trim()"
318
- class="text-[12px] text-slate-300 file:mr-2 file:rounded file:border-0 file:bg-slate-800 file:px-2 file:py-1 file:text-slate-200 disabled:opacity-40"
318
+ class="text-[12px] text-slate-300 file:me-2 file:rounded file:border-0 file:bg-slate-800 file:px-2 file:py-1 file:text-slate-200 disabled:opacity-40"
319
319
  @change="onFilePicked"
320
320
  />
321
321
  </div>
@@ -378,7 +378,7 @@ const canApprove = computed(
378
378
  t('visualConfirm.history.fixRequested')
379
379
  }}</span>
380
380
  <span
381
- class="ml-1.5 rounded px-1 text-[10px] uppercase"
381
+ class="ms-1.5 rounded px-1 text-[10px] uppercase"
382
382
  :class="
383
383
  r.outcome === 'completed'
384
384
  ? 'bg-emerald-500/15 text-emerald-300'
@@ -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'),
@@ -243,7 +243,7 @@ watch(
243
243
  <!-- z-30 keeps the trigger above the centered BoardToolbar (z-20), whose
244
244
  max-width can otherwise reach this corner on the narrowest viewports. -->
245
245
  <UButton
246
- class="absolute left-3 top-3 z-30 lg:hidden"
246
+ class="absolute start-3 top-3 z-30 lg:hidden"
247
247
  icon="i-lucide-menu"
248
248
  color="neutral"
249
249
  variant="soft"
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,
@@ -61,6 +61,11 @@ export default defineI18nConfig(() => ({
61
61
  currency: { style: 'currency', currency: 'USD', currencyDisplay: 'narrowSymbol' },
62
62
  percent: { style: 'percent', maximumFractionDigits: 1 },
63
63
  },
64
+ he: {
65
+ decimal: { style: 'decimal' },
66
+ currency: { style: 'currency', currency: 'USD', currencyDisplay: 'narrowSymbol' },
67
+ percent: { style: 'percent', maximumFractionDigits: 1 },
68
+ },
64
69
  },
65
70
  datetimeFormats: {
66
71
  en: {
@@ -83,5 +88,9 @@ export default defineI18nConfig(() => ({
83
88
  short: { dateStyle: 'medium' },
84
89
  long: { dateStyle: 'long', timeStyle: 'short' },
85
90
  },
91
+ he: {
92
+ short: { dateStyle: 'medium' },
93
+ long: { dateStyle: 'long', timeStyle: 'short' },
94
+ },
86
95
  },
87
96
  }))
@@ -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
  },