@cat-factory/app 0.61.0 → 0.63.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.
@@ -230,6 +230,9 @@ async function detectFromRepo() {
230
230
  owner: repo.owner,
231
231
  repo: repo.name,
232
232
  ...(ctx.directory ? { directory: ctx.directory } : {}),
233
+ // Prioritize the option matching the currently-selected tab (kubernetes vs
234
+ // docker-compose); the detector falls back to the other when the preferred isn't found.
235
+ prefer: provisionType.value,
233
236
  })
234
237
  detectResult.value = rec
235
238
  // Only prefill when the detector actually inferred something. A `detected: false`
@@ -61,13 +61,14 @@ const name = ref('')
61
61
  const selectedPromptIds = ref<string[]>([])
62
62
  const selectedModelIds = ref<string[]>([])
63
63
  const selectedFixtureIds = ref<string[]>([])
64
- // The judge model. Empty string = the deployment's routing default (resolved server-side);
64
+ // The judge model. 'default' = the deployment's routing default (resolved server-side);
65
65
  // picking one explicitly is the recourse on a deployment that has no default model wired,
66
- // where leaving it on default makes every run fail at create time.
67
- const selectedJudgeModel = ref<string>('')
66
+ // where leaving it on default makes every run fail at create time. ('default' is a non-empty
67
+ // sentinel because reka-ui's SelectItem reserves the empty string to clear a selection.)
68
+ const selectedJudgeModel = ref<string>('default')
68
69
 
69
70
  const judgeModelItems = computed(() => [
70
- { label: t('sandbox.deploymentDefault'), value: '' },
71
+ { label: t('sandbox.deploymentDefault'), value: 'default' },
71
72
  ...store.selectableModels.map((m) => ({ label: m.label, value: m.id })),
72
73
  ])
73
74
 
@@ -112,7 +113,7 @@ async function createAndRun() {
112
113
  const created = await store.createExperiment({
113
114
  name: name.value.trim() || t('sandbox.defaultRunName', { kind: agentKind.value }),
114
115
  agentKind: agentKind.value,
115
- judgeModel: selectedJudgeModel.value || undefined,
116
+ judgeModel: selectedJudgeModel.value === 'default' ? undefined : selectedJudgeModel.value,
116
117
  matrix: {
117
118
  promptVersionIds: selectedPromptIds.value,
118
119
  models: selectedModelIds.value,
@@ -60,7 +60,10 @@ const form = reactive({
60
60
  servicePort: '',
61
61
  gatewayName: '',
62
62
  httpRouteName: '',
63
- urlScheme: '' as '' | 'http' | 'https',
63
+ // 'default' is a non-empty sentinel for "let the apiserver/derivation decide the scheme":
64
+ // reka-ui's SelectItem reserves the empty string to clear a selection, so it can't be an
65
+ // option value. buildUrl() omits `scheme` entirely when this is 'default'.
66
+ urlScheme: 'default' as 'default' | 'http' | 'https',
64
67
  })
65
68
  const apiToken = ref('')
66
69
 
@@ -78,7 +81,7 @@ const urlSourceItems = computed(() => [
78
81
  },
79
82
  ])
80
83
  const schemeItems = computed(() => [
81
- { label: t('settings.infrastructure.kubernetesEngine.schemeDefault'), value: '' },
84
+ { label: t('settings.infrastructure.kubernetesEngine.schemeDefault'), value: 'default' },
82
85
  { label: 'https', value: 'https' },
83
86
  { label: 'http', value: 'http' },
84
87
  ])
@@ -107,7 +110,7 @@ watch(
107
110
  form.servicePort = typeof url?.port === 'number' ? String(url.port) : ''
108
111
  form.gatewayName = typeof url?.gatewayName === 'string' ? url.gatewayName : ''
109
112
  form.httpRouteName = typeof url?.httpRouteName === 'string' ? url.httpRouteName : ''
110
- if (url?.scheme === 'http' || url?.scheme === 'https') form.urlScheme = url.scheme
113
+ form.urlScheme = url?.scheme === 'http' || url?.scheme === 'https' ? url.scheme : 'default'
111
114
  },
112
115
  { immediate: true },
113
116
  )
@@ -145,7 +148,7 @@ function buildUrl(): Record<string, unknown> {
145
148
  } else {
146
149
  if (form.httpRouteName.trim()) url.httpRouteName = form.httpRouteName.trim()
147
150
  }
148
- if (form.urlScheme) url.scheme = form.urlScheme
151
+ if (form.urlScheme !== 'default') url.scheme = form.urlScheme
149
152
  return url
150
153
  }
151
154
 
@@ -43,7 +43,10 @@ const form = reactive({
43
43
  ingressName: '',
44
44
  serviceName: '',
45
45
  servicePort: '',
46
- urlScheme: '' as '' | 'http' | 'https',
46
+ // 'default' is a non-empty sentinel for "let the apiserver/derivation decide the scheme":
47
+ // reka-ui's SelectItem reserves the empty string to clear a selection, so it can't be an
48
+ // option value. The url builder omits `scheme` entirely when this is 'default'.
49
+ urlScheme: 'default' as 'default' | 'http' | 'https',
47
50
  })
48
51
  const apiToken = ref('')
49
52
 
@@ -66,7 +69,7 @@ const urlSourceItems = computed(() => [
66
69
  },
67
70
  ])
68
71
  const schemeItems = computed(() => [
69
- { label: t('settings.providerConnection.kubernetesEnv.schemeDefault'), value: '' },
72
+ { label: t('settings.providerConnection.kubernetesEnv.schemeDefault'), value: 'default' },
70
73
  { label: 'https', value: 'https' },
71
74
  { label: 'http', value: 'http' },
72
75
  ])
@@ -111,7 +114,8 @@ watch(
111
114
  form.serviceName = typeof url.serviceName === 'string' ? url.serviceName : ''
112
115
  form.servicePort = typeof url.port === 'number' ? String(url.port) : ''
113
116
  }
114
- if (url && (url.scheme === 'http' || url.scheme === 'https')) form.urlScheme = url.scheme
117
+ form.urlScheme =
118
+ url && (url.scheme === 'http' || url.scheme === 'https') ? url.scheme : 'default'
115
119
  },
116
120
  { immediate: true },
117
121
  )
@@ -171,7 +175,7 @@ function buildUrl(): Record<string, unknown> {
171
175
  const port = Number(form.servicePort)
172
176
  if (form.servicePort.trim() && Number.isInteger(port)) url.port = port
173
177
  }
174
- if (form.urlScheme) url.scheme = form.urlScheme
178
+ if (form.urlScheme !== 'default') url.scheme = form.urlScheme
175
179
  return url
176
180
  }
177
181
 
@@ -0,0 +1,166 @@
1
+ <script setup lang="ts">
2
+ // Startup advisory for built-in merge presets that drifted from the catalog. Opened once per
3
+ // session from the board page when `useMergePresetHealth` reports any issue. Lists:
4
+ // • new built-in presets the workspace doesn't have yet (ADD them);
5
+ // • built-ins with a newer catalog version available (RESEED to adopt it).
6
+ // Both fixes are the same reseed call (it creates or updates by catalog id). Detection is
7
+ // client-side (see useMergePresetHealth); the actions hit the mergePresets store.
8
+ const { t } = useI18n()
9
+ const ui = useUiStore()
10
+ const presets = useMergePresetsStore()
11
+ const { newPresets, outdated, hasIssues } = useMergePresetHealth()
12
+ const toast = useToast()
13
+
14
+ const open = computed({
15
+ get: () => ui.mergePresetHealthOpen,
16
+ set: (v: boolean) => {
17
+ if (!v) ui.closeMergePresetHealth()
18
+ },
19
+ })
20
+
21
+ // Per-preset in-flight ids, so each row's button shows its own spinner.
22
+ const busy = ref<Set<string>>(new Set())
23
+ const isBusy = (id: string) => busy.value.has(id)
24
+ const anyBusy = computed(() => busy.value.size > 0)
25
+
26
+ async function reseed(id: string) {
27
+ busy.value = new Set(busy.value).add(id)
28
+ try {
29
+ await presets.reseed(id)
30
+ } catch (e) {
31
+ toast.add({
32
+ title: t('mergePreset.health.toast.reseedFailed'),
33
+ description: e instanceof Error ? e.message : String(e),
34
+ icon: 'i-lucide-triangle-alert',
35
+ color: 'error',
36
+ })
37
+ } finally {
38
+ const next = new Set(busy.value)
39
+ next.delete(id)
40
+ busy.value = next
41
+ }
42
+ }
43
+
44
+ /** Reseed every advised preset (new + outdated built-ins) in one go. */
45
+ async function reseedAll() {
46
+ const ids = [...newPresets.value, ...outdated.value].map((i) => i.id)
47
+ for (const id of new Set(ids)) await reseed(id)
48
+ }
49
+
50
+ const reseedableCount = computed(
51
+ () => new Set([...newPresets.value, ...outdated.value].map((i) => i.id)).size,
52
+ )
53
+ </script>
54
+
55
+ <template>
56
+ <UModal v-model:open="open" :title="t('mergePreset.health.title')" :ui="{ content: 'max-w-2xl' }">
57
+ <template #body>
58
+ <div v-if="!hasIssues" class="py-6 text-center text-sm text-slate-400">
59
+ <UIcon name="i-lucide-check-circle-2" class="mx-auto mb-2 h-8 w-8 text-emerald-400" />
60
+ {{ t('mergePreset.health.allValid') }}
61
+ </div>
62
+
63
+ <div v-else class="space-y-5">
64
+ <!-- New built-in presets the workspace can add. -->
65
+ <section v-if="newPresets.length" class="space-y-2">
66
+ <div class="flex items-center gap-2">
67
+ <UIcon name="i-lucide-sparkles" class="h-4 w-4 text-emerald-400" />
68
+ <h3 class="text-sm font-semibold text-slate-200">
69
+ {{ t('mergePreset.health.newHeading') }}
70
+ </h3>
71
+ </div>
72
+ <p class="text-[11px] text-slate-500">{{ t('mergePreset.health.newDescription') }}</p>
73
+ <ul class="space-y-2">
74
+ <li
75
+ v-for="i in newPresets"
76
+ :key="i.id"
77
+ class="flex items-center justify-between gap-3 rounded-lg border border-slate-800 bg-slate-900/40 p-3"
78
+ >
79
+ <div class="min-w-0">
80
+ <span class="truncate text-sm font-medium text-slate-100 capitalize">{{
81
+ i.name
82
+ }}</span>
83
+ </div>
84
+ <UButton
85
+ size="xs"
86
+ color="primary"
87
+ variant="subtle"
88
+ icon="i-lucide-plus"
89
+ :loading="isBusy(i.id)"
90
+ :disabled="anyBusy"
91
+ @click="reseed(i.id)"
92
+ >
93
+ {{ t('mergePreset.health.add') }}
94
+ </UButton>
95
+ </li>
96
+ </ul>
97
+ </section>
98
+
99
+ <!-- Outdated built-ins: a newer catalog version is available. -->
100
+ <section v-if="outdated.length" class="space-y-2">
101
+ <div class="flex items-center gap-2">
102
+ <UIcon name="i-lucide-arrow-up-circle" class="h-4 w-4 text-amber-400" />
103
+ <h3 class="text-sm font-semibold text-slate-200">
104
+ {{ t('mergePreset.health.updatesHeading') }}
105
+ </h3>
106
+ </div>
107
+ <p class="text-[11px] text-slate-500">{{ t('mergePreset.health.updatesDescription') }}</p>
108
+ <ul class="space-y-2">
109
+ <li
110
+ v-for="i in outdated"
111
+ :key="i.id"
112
+ class="flex items-center justify-between gap-3 rounded-lg border border-slate-800 bg-slate-900/40 p-3"
113
+ >
114
+ <div class="min-w-0">
115
+ <span class="truncate text-sm font-medium text-slate-100">{{ i.name }}</span>
116
+ <p class="text-[11px] text-amber-400/80">
117
+ {{
118
+ t('mergePreset.health.versionAvailable', {
119
+ from: i.fromVersion ?? 0,
120
+ to: i.toVersion ?? 0,
121
+ })
122
+ }}
123
+ </p>
124
+ </div>
125
+ <UButton
126
+ size="xs"
127
+ color="primary"
128
+ variant="subtle"
129
+ icon="i-lucide-rotate-ccw"
130
+ :loading="isBusy(i.id)"
131
+ :disabled="anyBusy"
132
+ @click="reseed(i.id)"
133
+ >
134
+ {{ t('mergePreset.health.reseed') }}
135
+ </UButton>
136
+ </li>
137
+ </ul>
138
+ </section>
139
+ </div>
140
+ </template>
141
+
142
+ <template #footer>
143
+ <div class="flex w-full items-center justify-between gap-2">
144
+ <UButton
145
+ v-if="reseedableCount > 1"
146
+ color="primary"
147
+ variant="ghost"
148
+ icon="i-lucide-rotate-ccw"
149
+ :loading="anyBusy"
150
+ @click="reseedAll"
151
+ >
152
+ {{ t('mergePreset.health.reseedAll', { count: reseedableCount }) }}
153
+ </UButton>
154
+ <span v-else />
155
+ <UButton
156
+ color="neutral"
157
+ variant="ghost"
158
+ :disabled="anyBusy"
159
+ @click="ui.closeMergePresetHealth()"
160
+ >
161
+ {{ hasIssues ? t('mergePreset.health.dismiss') : t('mergePreset.health.done') }}
162
+ </UButton>
163
+ </div>
164
+ </template>
165
+ </UModal>
166
+ </template>
@@ -40,6 +40,7 @@ interface Draft {
40
40
  ciMaxAttempts: number
41
41
  maxRequirementIterations: number
42
42
  maxRequirementConcernAllowed: RequirementConcernLevel
43
+ autoMergeEnabled: boolean
43
44
  }
44
45
  const drafts = reactive<Record<string, Draft>>({})
45
46
 
@@ -52,6 +53,7 @@ function toDraft(p: MergeThresholdPreset): Draft {
52
53
  ciMaxAttempts: p.ciMaxAttempts,
53
54
  maxRequirementIterations: p.maxRequirementIterations,
54
55
  maxRequirementConcernAllowed: p.maxRequirementConcernAllowed,
56
+ autoMergeEnabled: p.autoMergeEnabled,
55
57
  }
56
58
  }
57
59
 
@@ -88,6 +90,7 @@ async function save(p: MergeThresholdPreset) {
88
90
  ciMaxAttempts: d.ciMaxAttempts,
89
91
  maxRequirementIterations: d.maxRequirementIterations,
90
92
  maxRequirementConcernAllowed: d.maxRequirementConcernAllowed,
93
+ autoMergeEnabled: d.autoMergeEnabled,
91
94
  })
92
95
  toast.add({
93
96
  title: t('settings.mergeThresholds.toast.saved'),
@@ -133,6 +136,7 @@ const draft = reactive<Draft>({
133
136
  ciMaxAttempts: 10,
134
137
  maxRequirementIterations: 6,
135
138
  maxRequirementConcernAllowed: 'none',
139
+ autoMergeEnabled: true,
136
140
  })
137
141
 
138
142
  async function create() {
@@ -147,8 +151,10 @@ async function create() {
147
151
  ciMaxAttempts: draft.ciMaxAttempts,
148
152
  maxRequirementIterations: draft.maxRequirementIterations,
149
153
  maxRequirementConcernAllowed: draft.maxRequirementConcernAllowed,
154
+ autoMergeEnabled: draft.autoMergeEnabled,
150
155
  })
151
156
  draft.name = ''
157
+ draft.autoMergeEnabled = true
152
158
  toast.add({
153
159
  title: t('settings.mergeThresholds.toast.created'),
154
160
  icon: 'i-lucide-check',
@@ -290,7 +296,17 @@ async function create() {
290
296
  </label>
291
297
  </div>
292
298
 
293
- <div class="mt-3 flex justify-end">
299
+ <div class="mt-3 flex items-center justify-between gap-3">
300
+ <USwitch
301
+ v-model="drafts[p.id]!.autoMergeEnabled"
302
+ size="sm"
303
+ :label="t('settings.mergeThresholds.field.autoMerge')"
304
+ :description="
305
+ drafts[p.id]!.autoMergeEnabled
306
+ ? t('settings.mergeThresholds.autoMergeOnHint')
307
+ : t('settings.mergeThresholds.autoMergeOffHint')
308
+ "
309
+ />
294
310
  <UButton
295
311
  color="primary"
296
312
  variant="soft"
@@ -373,6 +389,11 @@ async function create() {
373
389
  size="sm"
374
390
  />
375
391
  </label>
392
+ <USwitch
393
+ v-model="draft.autoMergeEnabled"
394
+ size="sm"
395
+ :label="t('settings.mergeThresholds.field.autoMerge')"
396
+ />
376
397
  <UButton
377
398
  color="primary"
378
399
  size="sm"
@@ -5,6 +5,7 @@ import {
5
5
  deleteModelPresetContract,
6
6
  listMergePresetsContract,
7
7
  listModelPresetsContract,
8
+ reseedMergePresetContract,
8
9
  updateMergePresetContract,
9
10
  updateModelPresetContract,
10
11
  } from '@cat-factory/contracts'
@@ -38,6 +39,11 @@ export function presetsApi({ send, ws }: ApiContext) {
38
39
  deleteMergePreset: (workspaceId: string, presetId: string) =>
39
40
  send(deleteMergePresetContract, { pathPrefix: ws(workspaceId), pathParams: { presetId } }),
40
41
 
42
+ // Restore a built-in preset to its current catalog definition (adopt an update, repair a
43
+ // drifted one, or materialise a new built-in that appeared). Custom presets reject this.
44
+ reseedMergePreset: (workspaceId: string, presetId: string) =>
45
+ send(reseedMergePresetContract, { pathPrefix: ws(workspaceId), pathParams: { presetId } }),
46
+
41
47
  // ---- model presets (per-task model->agent mapping library) ------------
42
48
  listModelPresets: (workspaceId: string) =>
43
49
  send(listModelPresetsContract, { pathPrefix: ws(workspaceId) }),
@@ -0,0 +1,65 @@
1
+ import { computed } from 'vue'
2
+ import type { MergeThresholdPreset } from '~/types/merge'
3
+ import { useMergePresetsStore } from '~/stores/mergePresets'
4
+
5
+ export type MergePresetIssueType = 'outdated' | 'new'
6
+
7
+ /** A built-in merge preset that the workspace should reseed (an update, or a new one to add). */
8
+ export interface MergePresetIssue {
9
+ type: MergePresetIssueType
10
+ /** The catalog (built-in) id — what the reseed endpoint is keyed by. */
11
+ id: string
12
+ /** The preset name (the stored copy's for `outdated`, the built-in id for a `new` one). */
13
+ name: string
14
+ /** For an `outdated` issue: the persisted copy's version (the display copy renders it via i18n). */
15
+ fromVersion?: number
16
+ /** For an `outdated` issue: the newer catalog version available. */
17
+ toVersion?: number
18
+ }
19
+
20
+ /** A built-in's display name for an issue message (humanise its catalog id as a fallback). */
21
+ function builtinName(id: string, stored: MergeThresholdPreset | undefined): string {
22
+ if (stored) return stored.name
23
+ // `mp_manual_review` -> "Manual review" — only used until the row is reseeded into existence.
24
+ return id.replace(/^mp_/, '').replace(/_/g, ' ')
25
+ }
26
+
27
+ /**
28
+ * Detect built-in merge presets the workspace should reseed for the startup advisory: a stored
29
+ * built-in whose catalog definition moved ahead (offer to adopt it) and a brand-new built-in
30
+ * that appeared in the catalog but isn't in the workspace yet (offer to add it). The catalog
31
+ * versions the snapshot ships ARE the set of built-in ids, so detection is entirely client-side:
32
+ * a stored preset is a built-in iff its id is a catalog key, and a catalog key with no stored
33
+ * preset is a new built-in.
34
+ */
35
+ export function useMergePresetHealth() {
36
+ const store = useMergePresetsStore()
37
+
38
+ const issues = computed<MergePresetIssue[]>(() => {
39
+ const out: MergePresetIssue[] = []
40
+ const byId = new Map(store.presets.map((p) => [p.id, p]))
41
+ for (const [id, catalogVersion] of Object.entries(store.catalogVersions)) {
42
+ const stored = byId.get(id)
43
+ if (!stored) {
44
+ out.push({ type: 'new', id, name: builtinName(id, undefined) })
45
+ continue
46
+ }
47
+ if (catalogVersion > (stored.version ?? 0)) {
48
+ out.push({
49
+ type: 'outdated',
50
+ id,
51
+ name: stored.name,
52
+ fromVersion: stored.version ?? 0,
53
+ toVersion: catalogVersion,
54
+ })
55
+ }
56
+ }
57
+ return out
58
+ })
59
+
60
+ const hasIssues = computed(() => issues.value.length > 0)
61
+ const newPresets = computed(() => issues.value.filter((i) => i.type === 'new'))
62
+ const outdated = computed(() => issues.value.filter((i) => i.type === 'outdated'))
63
+
64
+ return { issues, hasIssues, newPresets, outdated }
65
+ }
@@ -62,6 +62,10 @@ const FragmentLibraryPanel = defineAsyncComponent(
62
62
  const PipelineHealthModal = defineAsyncComponent(
63
63
  () => import('~/components/pipeline/PipelineHealthModal.vue'),
64
64
  )
65
+ // Startup advisory for new / outdated built-in merge presets — same once-per-session pattern.
66
+ const MergePresetHealthModal = defineAsyncComponent(
67
+ () => import('~/components/settings/MergePresetHealthModal.vue'),
68
+ )
65
69
  const IntegrationsHub = defineAsyncComponent(
66
70
  () => import('~/components/layout/IntegrationsHub.vue'),
67
71
  )
@@ -150,6 +154,18 @@ watch(
150
154
  },
151
155
  { immediate: true },
152
156
  )
157
+ // Same advisory for built-in merge presets: surface new / outdated ones once per session. Defers
158
+ // to the pipeline advisory when both fire, so at most one modal auto-opens on a given load.
159
+ const { hasIssues: mergePresetIssues } = useMergePresetHealth()
160
+ watch(
161
+ () => [workspace.ready, mergePresetIssues.value, ui.pipelineHealthOpen],
162
+ () => {
163
+ if (workspace.ready && mergePresetIssues.value && !ui.pipelineHealthOpen) {
164
+ ui.maybeOpenMergePresetHealth()
165
+ }
166
+ },
167
+ { immediate: true },
168
+ )
153
169
 
154
170
  // Auto-open the right AI-onboarding dialog once per session: the no-source prompt takes
155
171
  // precedence over the preset-mismatch prompt. Honour the per-session dismissed flags so a
@@ -303,6 +319,7 @@ watch(
303
319
  <SlackPanel v-if="ui.slackOpen" />
304
320
  <FragmentLibraryPanel v-if="ui.fragmentLibraryOpen" />
305
321
  <PipelineHealthModal v-if="ui.pipelineHealthOpen" />
322
+ <MergePresetHealthModal v-if="ui.mergePresetHealthOpen" />
306
323
  <IntegrationsHub v-if="ui.integrationsOpen" />
307
324
  <PersonalSetupModal v-if="ui.personalSetupOpen" />
308
325
  <WorkspaceSettingsPanel v-if="ui.workspaceSettingsOpen" />
@@ -13,9 +13,18 @@ export const useMergePresetsStore = defineStore('mergePresets', () => {
13
13
  const api = useApi()
14
14
 
15
15
  const presets = ref<MergeThresholdPreset[]>([])
16
+ /**
17
+ * Current built-in catalog versions (`seedMergePresets()`), keyed by preset id, from the
18
+ * workspace snapshot. The keys ARE the set of built-in ids: a stored preset whose id is a
19
+ * key here is a built-in (and is outdated when its `version` is below the catalog value),
20
+ * and a key with no matching stored preset is a NEW built-in the workspace can add. Drives
21
+ * `useMergePresetHealth`.
22
+ */
23
+ const catalogVersions = ref<Record<string, number>>({})
16
24
 
17
- function hydrate(list: MergeThresholdPreset[]) {
25
+ function hydrate(list: MergeThresholdPreset[], versions?: Record<string, number>) {
18
26
  presets.value = [...list].sort((a, b) => a.createdAt - b.createdAt)
27
+ if (versions) catalogVersions.value = versions
19
28
  }
20
29
 
21
30
  /** The workspace default (fallback for a task that picks none). */
@@ -50,5 +59,27 @@ export const useMergePresetsStore = defineStore('mergePresets', () => {
50
59
  await ws.refresh()
51
60
  }
52
61
 
53
- return { presets, defaultPreset, resolve, hydrate, create, update, remove }
62
+ /**
63
+ * Reseed a built-in preset from the backend's current catalog: adopt an updated definition,
64
+ * repair a drifted one, or materialise a NEW built-in that appeared after the workspace was
65
+ * created. The `presetId` is the catalog id (e.g. `mp_balanced`). Refreshes the snapshot.
66
+ */
67
+ async function reseed(presetId: string) {
68
+ const ws = useWorkspaceStore()
69
+ const updated = await api.reseedMergePreset(ws.requireId(), presetId)
70
+ await ws.refresh()
71
+ return updated
72
+ }
73
+
74
+ return {
75
+ presets,
76
+ catalogVersions,
77
+ defaultPreset,
78
+ resolve,
79
+ hydrate,
80
+ create,
81
+ update,
82
+ remove,
83
+ reseed,
84
+ }
54
85
  })
package/app/stores/ui.ts CHANGED
@@ -24,6 +24,11 @@ export const useUiStore = defineStore('ui', () => {
24
24
  // session so it does not re-pop on every snapshot re-hydration.
25
25
  const pipelineHealthOpen = ref(false)
26
26
  const pipelineHealthSeen = ref(false)
27
+ // Merge-preset health startup advisory: lists built-ins with a newer catalog version (reseed)
28
+ // and new built-in presets the workspace can add. `mergePresetHealthSeen` gates auto-open to
29
+ // once per session so it does not re-pop on every snapshot re-hydration (mirrors pipelines).
30
+ const mergePresetHealthOpen = ref(false)
31
+ const mergePresetHealthSeen = ref(false)
27
32
  const decisionContext = ref<{ instanceId: string; decisionId: string } | null>(null)
28
33
 
29
34
  // Document-source integration modals, keyed by source. `documentImport` and
@@ -250,6 +255,22 @@ export const useUiStore = defineStore('ui', () => {
250
255
  pipelineHealthOpen.value = false
251
256
  }
252
257
 
258
+ /** Auto-open the merge-preset health advisory once per session (no-op after it's been shown). */
259
+ function maybeOpenMergePresetHealth() {
260
+ if (mergePresetHealthSeen.value) return
261
+ mergePresetHealthSeen.value = true
262
+ mergePresetHealthOpen.value = true
263
+ }
264
+
265
+ function openMergePresetHealth() {
266
+ mergePresetHealthSeen.value = true
267
+ mergePresetHealthOpen.value = true
268
+ }
269
+
270
+ function closeMergePresetHealth() {
271
+ mergePresetHealthOpen.value = false
272
+ }
273
+
253
274
  function openDecision(instanceId: string, decisionId: string) {
254
275
  decisionContext.value = { instanceId, decisionId }
255
276
  }
@@ -658,6 +679,8 @@ export const useUiStore = defineStore('ui', () => {
658
679
  builderOpen,
659
680
  pipelineHealthOpen,
660
681
  pipelineHealthSeen,
682
+ mergePresetHealthOpen,
683
+ mergePresetHealthSeen,
661
684
  decisionContext,
662
685
  documentConnect,
663
686
  documentImport,
@@ -715,6 +738,9 @@ export const useUiStore = defineStore('ui', () => {
715
738
  maybeOpenPipelineHealth,
716
739
  openPipelineHealth,
717
740
  closePipelineHealth,
741
+ maybeOpenMergePresetHealth,
742
+ openMergePresetHealth,
743
+ closeMergePresetHealth,
718
744
  openDecision,
719
745
  closeDecision,
720
746
  openApprovalDetail,
@@ -88,7 +88,10 @@ export const useWorkspaceStore = defineStore(
88
88
  useAgentRunsStore().hydrate(snapshot.bootstrapJobs ?? [])
89
89
  useAgentRunsStore().hydrateEnvConfigRepair(snapshot.envConfigRepairJobs ?? [])
90
90
  useNotificationsStore().hydrate(snapshot.notifications ?? [])
91
- useMergePresetsStore().hydrate(snapshot.mergePresets ?? [])
91
+ useMergePresetsStore().hydrate(
92
+ snapshot.mergePresets ?? [],
93
+ snapshot.mergePresetCatalogVersions,
94
+ )
92
95
  useWorkspaceSettingsStore().hydrate(snapshot.settings)
93
96
  useAgentConfigStore().hydrate(snapshot.agentConfigCatalog ?? [])
94
97
  useModelPresetsStore().hydrate(snapshot.modelPresets ?? [])
@@ -7,6 +7,8 @@ import type { MergeThresholdPreset } from '~/types/merge'
7
7
  * rendered as whole percents.
8
8
  */
9
9
  export function mergePresetThresholds(p: MergeThresholdPreset): string {
10
+ // Auto-merge disabled: the thresholds don't apply, every PR goes to human review.
11
+ if (!p.autoMergeEnabled) return `manual review only · ${p.ciMaxAttempts} CI fixes`
10
12
  const pct = (n: number) => `${Math.round(n * 100)}%`
11
13
  return `cx ≤${pct(p.maxComplexity)} · risk ≤${pct(p.maxRisk)} · impact ≤${pct(
12
14
  p.maxImpact,
@@ -1643,8 +1643,11 @@
1643
1643
  "maxImpact": "Max impact %",
1644
1644
  "ciMaxAttempts": "CI-fix attempts",
1645
1645
  "maxRequirementIterations": "Requirement iterations",
1646
- "maxRequirementConcernAllowed": "Auto-pass concerns at or below"
1646
+ "maxRequirementConcernAllowed": "Auto-pass concerns at or below",
1647
+ "autoMerge": "Auto-merge"
1647
1648
  },
1649
+ "autoMergeOnHint": "Merge automatically when within thresholds.",
1650
+ "autoMergeOffHint": "Always route the PR to a human review.",
1648
1651
  "newPreset": "New preset",
1649
1652
  "create": {
1650
1653
  "name": "Name",
@@ -2529,6 +2532,25 @@
2529
2532
  }
2530
2533
  }
2531
2534
  },
2535
+ "mergePreset": {
2536
+ "health": {
2537
+ "title": "Merge preset updates",
2538
+ "allValid": "All built-in merge presets are up to date.",
2539
+ "newHeading": "New presets available",
2540
+ "newDescription": "New built-in merge presets have shipped. Add them to this board's library.",
2541
+ "add": "Add",
2542
+ "updatesHeading": "Updates available",
2543
+ "updatesDescription": "A newer version of these built-in merge presets has shipped. Reseed to adopt it (the default and ordering are kept).",
2544
+ "versionAvailable": "Version {from} → {to} available.",
2545
+ "reseed": "Reseed",
2546
+ "reseedAll": "Update all ({count})",
2547
+ "dismiss": "Dismiss",
2548
+ "done": "Done",
2549
+ "toast": {
2550
+ "reseedFailed": "Could not reseed merge preset"
2551
+ }
2552
+ }
2553
+ },
2532
2554
  "palette": {
2533
2555
  "hint": "Click an agent to append it to the pipeline.",
2534
2556
  "customAgents": "Custom agents"
@@ -1499,7 +1499,8 @@
1499
1499
  "maxImpact": "Impacto máx. %",
1500
1500
  "ciMaxAttempts": "Intentos de corrección de CI",
1501
1501
  "maxRequirementIterations": "Iteraciones de requisitos",
1502
- "maxRequirementConcernAllowed": "Aprobación automática de inquietudes hasta"
1502
+ "maxRequirementConcernAllowed": "Aprobación automática de inquietudes hasta",
1503
+ "autoMerge": "Fusión automática"
1503
1504
  },
1504
1505
  "newPreset": "Nuevo ajuste",
1505
1506
  "create": {
@@ -1520,7 +1521,9 @@
1520
1521
  "createFailed": "No se pudo crear el ajuste",
1521
1522
  "defaultFailed": "No se pudo establecer el predeterminado",
1522
1523
  "deleteFailed": "No se pudo eliminar el ajuste"
1523
- }
1524
+ },
1525
+ "autoMergeOnHint": "Fusionar automáticamente cuando esté dentro de los umbrales.",
1526
+ "autoMergeOffHint": "Enviar siempre el PR a revisión humana."
1524
1527
  },
1525
1528
  "observabilityConnection": {
1526
1529
  "title": "Salud posterior al lanzamiento",
@@ -3498,5 +3501,24 @@
3498
3501
  "saveArchFailed": "No se pudo guardar la arquitectura de referencia",
3499
3502
  "deleteFailed": "No se pudo eliminar"
3500
3503
  }
3504
+ },
3505
+ "mergePreset": {
3506
+ "health": {
3507
+ "title": "Actualizaciones de presets de fusión",
3508
+ "allValid": "Todos los presets de fusión integrados están actualizados.",
3509
+ "newHeading": "Nuevos presets disponibles",
3510
+ "newDescription": "Hay nuevos presets de fusión integrados. Añádelos a la biblioteca de este tablero.",
3511
+ "add": "Añadir",
3512
+ "updatesHeading": "Actualizaciones disponibles",
3513
+ "updatesDescription": "Hay una versión más reciente de estos presets de fusión integrados. Regenera para adoptarla (se conservan el predeterminado y el orden).",
3514
+ "versionAvailable": "Versión {from} → {to} disponible.",
3515
+ "reseed": "Regenerar",
3516
+ "reseedAll": "Actualizar todos ({count})",
3517
+ "dismiss": "Descartar",
3518
+ "done": "Hecho",
3519
+ "toast": {
3520
+ "reseedFailed": "No se pudo regenerar el preset de fusión"
3521
+ }
3522
+ }
3501
3523
  }
3502
3524
  }
@@ -1499,7 +1499,8 @@
1499
1499
  "maxImpact": "Impact max %",
1500
1500
  "ciMaxAttempts": "Tentatives de correction CI",
1501
1501
  "maxRequirementIterations": "Itérations d'exigences",
1502
- "maxRequirementConcernAllowed": "Validation auto. des préoccupations jusqu'à"
1502
+ "maxRequirementConcernAllowed": "Validation auto. des préoccupations jusqu'à",
1503
+ "autoMerge": "Fusion automatique"
1503
1504
  },
1504
1505
  "newPreset": "Nouveau préréglage",
1505
1506
  "create": {
@@ -1520,7 +1521,9 @@
1520
1521
  "createFailed": "Impossible de créer le préréglage",
1521
1522
  "defaultFailed": "Impossible de définir par défaut",
1522
1523
  "deleteFailed": "Impossible de supprimer le préréglage"
1523
- }
1524
+ },
1525
+ "autoMergeOnHint": "Fusionner automatiquement si dans les seuils.",
1526
+ "autoMergeOffHint": "Toujours envoyer la PR en revue humaine."
1524
1527
  },
1525
1528
  "observabilityConnection": {
1526
1529
  "title": "Santé post-publication",
@@ -3498,5 +3501,24 @@
3498
3501
  "saveArchFailed": "Impossible d'enregistrer l'architecture de référence",
3499
3502
  "deleteFailed": "Impossible de supprimer"
3500
3503
  }
3504
+ },
3505
+ "mergePreset": {
3506
+ "health": {
3507
+ "title": "Mises à jour des presets de fusion",
3508
+ "allValid": "Tous les presets de fusion intégrés sont à jour.",
3509
+ "newHeading": "Nouveaux presets disponibles",
3510
+ "newDescription": "De nouveaux presets de fusion intégrés sont arrivés. Ajoutez-les à la bibliothèque de ce tableau.",
3511
+ "add": "Ajouter",
3512
+ "updatesHeading": "Mises à jour disponibles",
3513
+ "updatesDescription": "Une version plus récente de ces presets de fusion intégrés est arrivée. Régénérez pour l'adopter (le preset par défaut et l'ordre sont conservés).",
3514
+ "versionAvailable": "Version {from} → {to} disponible.",
3515
+ "reseed": "Régénérer",
3516
+ "reseedAll": "Tout mettre à jour ({count})",
3517
+ "dismiss": "Ignorer",
3518
+ "done": "Terminé",
3519
+ "toast": {
3520
+ "reseedFailed": "Impossible de régénérer le preset de fusion"
3521
+ }
3522
+ }
3501
3523
  }
3502
3524
  }
@@ -1598,7 +1598,8 @@
1598
1598
  "maxImpact": "השפעה מרבית %",
1599
1599
  "ciMaxAttempts": "ניסיונות תיקון CI",
1600
1600
  "maxRequirementIterations": "איטרציות דרישות",
1601
- "maxRequirementConcernAllowed": "מעבר אוטומטי בחששות ברף או מתחתיו"
1601
+ "maxRequirementConcernAllowed": "מעבר אוטומטי בחששות ברף או מתחתיו",
1602
+ "autoMerge": "מיזוג אוטומטי"
1602
1603
  },
1603
1604
  "newPreset": "תצורה חדשה",
1604
1605
  "create": {
@@ -1619,7 +1620,9 @@
1619
1620
  "createFailed": "לא ניתן היה ליצור תצורה",
1620
1621
  "defaultFailed": "לא ניתן היה להגדיר ברירת מחדל",
1621
1622
  "deleteFailed": "לא ניתן היה למחוק את התצורה"
1622
- }
1623
+ },
1624
+ "autoMergeOnHint": "מזג אוטומטית כשבתוך הספים.",
1625
+ "autoMergeOffHint": "נתב תמיד את ה-PR לבדיקה אנושית."
1623
1626
  },
1624
1627
  "observabilityConnection": {
1625
1628
  "title": "בריאות שלאחר שחרור",
@@ -3509,5 +3512,24 @@
3509
3512
  "saveArchFailed": "לא ניתן היה לשמור ארכיטקטורת ייחוס",
3510
3513
  "deleteFailed": "לא ניתן היה למחוק"
3511
3514
  }
3515
+ },
3516
+ "mergePreset": {
3517
+ "health": {
3518
+ "title": "עדכוני קביעות מיזוג",
3519
+ "allValid": "כל קביעות המיזוג המובנות מעודכנות.",
3520
+ "newHeading": "קביעות חדשות זמינות",
3521
+ "newDescription": "הגיעו קביעות מיזוג מובנות חדשות. הוסף אותן לספריית הלוח הזה.",
3522
+ "add": "הוסף",
3523
+ "updatesHeading": "עדכונים זמינים",
3524
+ "updatesDescription": "גרסה חדשה יותר של קביעות המיזוג המובנות האלה הגיעה. זרע מחדש כדי לאמץ אותה (ברירת המחדל והסדר נשמרים).",
3525
+ "versionAvailable": "גרסה {from} → {to} זמינה.",
3526
+ "reseed": "זרע מחדש",
3527
+ "reseedAll": "עדכן הכל ({count})",
3528
+ "dismiss": "התעלם",
3529
+ "done": "בוצע",
3530
+ "toast": {
3531
+ "reseedFailed": "לא ניתן לזרוע מחדש את קביעת המיזוג"
3532
+ }
3533
+ }
3512
3534
  }
3513
3535
  }
@@ -1600,7 +1600,8 @@
1600
1600
  "maxImpact": "最大影響度 %",
1601
1601
  "ciMaxAttempts": "CI 修正の試行回数",
1602
1602
  "maxRequirementIterations": "要件のイテレーション回数",
1603
- "maxRequirementConcernAllowed": "この水準以下の懸念を自動承認"
1603
+ "maxRequirementConcernAllowed": "この水準以下の懸念を自動承認",
1604
+ "autoMerge": "自動マージ"
1604
1605
  },
1605
1606
  "newPreset": "新しいプリセット",
1606
1607
  "create": {
@@ -1621,7 +1622,9 @@
1621
1622
  "createFailed": "プリセットを作成できませんでした",
1622
1623
  "defaultFailed": "デフォルトを設定できませんでした",
1623
1624
  "deleteFailed": "プリセットを削除できませんでした"
1624
- }
1625
+ },
1626
+ "autoMergeOnHint": "しきい値内なら自動的にマージします。",
1627
+ "autoMergeOffHint": "常にPRを人間のレビューに回します。"
1625
1628
  },
1626
1629
  "observabilityConnection": {
1627
1630
  "title": "リリース後のヘルス",
@@ -3511,5 +3514,24 @@
3511
3514
  "saveArchFailed": "リファレンスアーキテクチャを保存できませんでした",
3512
3515
  "deleteFailed": "削除できませんでした"
3513
3516
  }
3517
+ },
3518
+ "mergePreset": {
3519
+ "health": {
3520
+ "title": "マージプリセットの更新",
3521
+ "allValid": "組み込みのマージプリセットはすべて最新です。",
3522
+ "newHeading": "新しいプリセットがあります",
3523
+ "newDescription": "新しい組み込みマージプリセットが追加されました。このボードのライブラリに追加してください。",
3524
+ "add": "追加",
3525
+ "updatesHeading": "更新あり",
3526
+ "updatesDescription": "これらの組み込みマージプリセットの新しいバージョンがあります。再シードして取り込みます(既定と並び順は保持されます)。",
3527
+ "versionAvailable": "バージョン {from} → {to} が利用可能です。",
3528
+ "reseed": "再シード",
3529
+ "reseedAll": "すべて更新 ({count})",
3530
+ "dismiss": "閉じる",
3531
+ "done": "完了",
3532
+ "toast": {
3533
+ "reseedFailed": "マージプリセットを再シードできませんでした"
3534
+ }
3535
+ }
3514
3536
  }
3515
3537
  }
@@ -1499,7 +1499,8 @@
1499
1499
  "maxImpact": "Maks. wpływ %",
1500
1500
  "ciMaxAttempts": "Próby naprawy CI",
1501
1501
  "maxRequirementIterations": "Iteracje wymagań",
1502
- "maxRequirementConcernAllowed": "Automatyczne zaliczenie zastrzeżeń do"
1502
+ "maxRequirementConcernAllowed": "Automatyczne zaliczenie zastrzeżeń do",
1503
+ "autoMerge": "Auto-scalanie"
1503
1504
  },
1504
1505
  "newPreset": "Nowe ustawienie",
1505
1506
  "create": {
@@ -1520,7 +1521,9 @@
1520
1521
  "createFailed": "Nie można utworzyć ustawienia",
1521
1522
  "defaultFailed": "Nie można ustawić jako domyślne",
1522
1523
  "deleteFailed": "Nie można usunąć ustawienia"
1523
- }
1524
+ },
1525
+ "autoMergeOnHint": "Scalaj automatycznie, gdy mieści się w progach.",
1526
+ "autoMergeOffHint": "Zawsze kieruj PR do recenzji człowieka."
1524
1527
  },
1525
1528
  "observabilityConnection": {
1526
1529
  "title": "Kondycja po wydaniu",
@@ -3498,5 +3501,24 @@
3498
3501
  "saveArchFailed": "Nie udało się zapisać architektury referencyjnej",
3499
3502
  "deleteFailed": "Nie udało się usunąć"
3500
3503
  }
3504
+ },
3505
+ "mergePreset": {
3506
+ "health": {
3507
+ "title": "Aktualizacje presetów scalania",
3508
+ "allValid": "Wszystkie wbudowane presety scalania są aktualne.",
3509
+ "newHeading": "Dostępne nowe presety",
3510
+ "newDescription": "Pojawiły się nowe wbudowane presety scalania. Dodaj je do biblioteki tej tablicy.",
3511
+ "add": "Dodaj",
3512
+ "updatesHeading": "Dostępne aktualizacje",
3513
+ "updatesDescription": "Dostępna jest nowsza wersja tych wbudowanych presetów scalania. Zregeneruj, aby ją przyjąć (domyślny i kolejność są zachowane).",
3514
+ "versionAvailable": "Dostępna wersja {from} → {to}.",
3515
+ "reseed": "Zregeneruj",
3516
+ "reseedAll": "Zaktualizuj wszystkie ({count})",
3517
+ "dismiss": "Odrzuć",
3518
+ "done": "Gotowe",
3519
+ "toast": {
3520
+ "reseedFailed": "Nie udało się zregenerować presetu scalania"
3521
+ }
3522
+ }
3501
3523
  }
3502
3524
  }
@@ -1600,7 +1600,8 @@
1600
1600
  "maxImpact": "Maks. etki %",
1601
1601
  "ciMaxAttempts": "CI düzeltme denemeleri",
1602
1602
  "maxRequirementIterations": "Gereksinim yinelemeleri",
1603
- "maxRequirementConcernAllowed": "Şu düzeyde veya altında otomatik geç"
1603
+ "maxRequirementConcernAllowed": "Şu düzeyde veya altında otomatik geç",
1604
+ "autoMerge": "Otomatik birleştirme"
1604
1605
  },
1605
1606
  "newPreset": "Yeni ön ayar",
1606
1607
  "create": {
@@ -1621,7 +1622,9 @@
1621
1622
  "createFailed": "Ön ayar oluşturulamadı",
1622
1623
  "defaultFailed": "Varsayılan ayarlanamadı",
1623
1624
  "deleteFailed": "Ön ayar silinemedi"
1624
- }
1625
+ },
1626
+ "autoMergeOnHint": "Eşiklerin içindeyken otomatik birleştir.",
1627
+ "autoMergeOffHint": "PR'yi her zaman insan incelemesine gönder."
1625
1628
  },
1626
1629
  "observabilityConnection": {
1627
1630
  "title": "Sürüm sonrası sağlık",
@@ -3511,5 +3514,24 @@
3511
3514
  "saveArchFailed": "Referans mimari kaydedilemedi",
3512
3515
  "deleteFailed": "Silinemedi"
3513
3516
  }
3517
+ },
3518
+ "mergePreset": {
3519
+ "health": {
3520
+ "title": "Birleştirme ön ayarı güncellemeleri",
3521
+ "allValid": "Tüm yerleşik birleştirme ön ayarları güncel.",
3522
+ "newHeading": "Yeni ön ayarlar mevcut",
3523
+ "newDescription": "Yeni yerleşik birleştirme ön ayarları geldi. Bu panonun kütüphanesine ekleyin.",
3524
+ "add": "Ekle",
3525
+ "updatesHeading": "Güncellemeler mevcut",
3526
+ "updatesDescription": "Bu yerleşik birleştirme ön ayarlarının daha yeni bir sürümü geldi. Benimsemek için yeniden tohumlayın (varsayılan ve sıralama korunur).",
3527
+ "versionAvailable": "Sürüm {from} → {to} mevcut.",
3528
+ "reseed": "Yeniden tohumla",
3529
+ "reseedAll": "Tümünü güncelle ({count})",
3530
+ "dismiss": "Yoksay",
3531
+ "done": "Tamam",
3532
+ "toast": {
3533
+ "reseedFailed": "Birleştirme ön ayarı yeniden tohumlanamadı"
3534
+ }
3535
+ }
3514
3536
  }
3515
3537
  }
@@ -1499,7 +1499,8 @@
1499
1499
  "maxImpact": "Макс. вплив %",
1500
1500
  "ciMaxAttempts": "Спроби виправлення CI",
1501
1501
  "maxRequirementIterations": "Ітерації вимог",
1502
- "maxRequirementConcernAllowed": "Авто-пропуск зауважень до"
1502
+ "maxRequirementConcernAllowed": "Авто-пропуск зауважень до",
1503
+ "autoMerge": "Автозлиття"
1503
1504
  },
1504
1505
  "newPreset": "Новий набір",
1505
1506
  "create": {
@@ -1520,7 +1521,9 @@
1520
1521
  "createFailed": "Не вдалося створити набір",
1521
1522
  "defaultFailed": "Не вдалося встановити за замовчуванням",
1522
1523
  "deleteFailed": "Не вдалося видалити набір"
1523
- }
1524
+ },
1525
+ "autoMergeOnHint": "Зливати автоматично, коли в межах порогів.",
1526
+ "autoMergeOffHint": "Завжди надсилати PR на перевірку людиною."
1524
1527
  },
1525
1528
  "observabilityConnection": {
1526
1529
  "title": "Стан після випуску",
@@ -3498,5 +3501,24 @@
3498
3501
  "saveArchFailed": "Не вдалося зберегти еталонну архітектуру",
3499
3502
  "deleteFailed": "Не вдалося видалити"
3500
3503
  }
3504
+ },
3505
+ "mergePreset": {
3506
+ "health": {
3507
+ "title": "Оновлення пресетів злиття",
3508
+ "allValid": "Усі вбудовані пресети злиття актуальні.",
3509
+ "newHeading": "Доступні нові пресети",
3510
+ "newDescription": "З'явилися нові вбудовані пресети злиття. Додайте їх до бібліотеки цієї дошки.",
3511
+ "add": "Додати",
3512
+ "updatesHeading": "Доступні оновлення",
3513
+ "updatesDescription": "Доступна новіша версія цих вбудованих пресетів злиття. Перегенеруйте, щоб застосувати її (стандартний пресет і порядок збережено).",
3514
+ "versionAvailable": "Доступна версія {from} → {to}.",
3515
+ "reseed": "Перегенерувати",
3516
+ "reseedAll": "Оновити всі ({count})",
3517
+ "dismiss": "Відхилити",
3518
+ "done": "Готово",
3519
+ "toast": {
3520
+ "reseedFailed": "Не вдалося перегенерувати пресет злиття"
3521
+ }
3522
+ }
3501
3523
  }
3502
3524
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.61.0",
3
+ "version": "0.63.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.68.0"
37
+ "@cat-factory/contracts": "0.70.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",