@cat-factory/app 0.143.1 → 0.144.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.
@@ -21,12 +21,11 @@ import type {
21
21
  TaskTypeFields,
22
22
  } from '~/types/domain'
23
23
  import { DOC_KINDS, DOC_KIND_FIELDS } from '~/types/domain'
24
- import type { DropdownMenuItem } from '@nuxt/ui'
25
24
  import ContextDocumentPicker from '~/components/documents/ContextDocumentPicker.vue'
26
25
  import ContextIssuePicker from '~/components/tasks/ContextIssuePicker.vue'
26
+ import FragmentSelector from '~/components/fragments/FragmentSelector.vue'
27
27
  import { riskPolicyOptionLabel, riskPolicySummary } from '~/utils/riskPolicy'
28
28
  import { pipelineAllowedForManualStart } from '~/utils/pipeline'
29
- import { buildFragmentPickerGroups } from '~/utils/fragmentPicker'
30
29
 
31
30
  const ui = useUiStore()
32
31
  const board = useBoardStore()
@@ -37,7 +36,6 @@ const modelPresets = useModelPresetsStore()
37
36
  const pipelines = usePipelinesStore()
38
37
  const agentConfig = useAgentConfigStore()
39
38
  const fragments = useFragmentsStore()
40
- const accounts = useAccountsStore()
41
39
  const toast = useToast()
42
40
  const { t } = useI18n()
43
41
 
@@ -314,50 +312,10 @@ const selectedModelPresetLabel = computed(() => {
314
312
  })
315
313
 
316
314
  // ---- best-practice prompt fragments (pinned at creation) -------------------
317
- // The fragments already chosen, resolved against the catalog. An id the catalog no longer
318
- // resolves still renders (labelled by its raw id) so it stays visible and removable mirrors
319
- // the inspector's TaskStructure picker.
320
- const selectedFragments = computed(() =>
321
- fragmentIds.value.map((id) => fragments.getFragment(id) ?? { id, title: id, summary: '' }),
322
- )
323
- // A trailing group linking out to the fragment library (board tier always; account tier when
324
- // enabled) — managing fragments is open to every member, exactly like the inspector picker.
325
- const fragmentManageItems = computed<DropdownMenuItem[]>(() => {
326
- const items: DropdownMenuItem[] = [
327
- {
328
- label: t('inspector.fragments.manageBoard'),
329
- icon: 'i-lucide-book-marked',
330
- onSelect: () => ui.openFragmentLibrary(),
331
- },
332
- ]
333
- if (accounts.enabled) {
334
- items.push({
335
- label: t('inspector.fragments.manageAccount'),
336
- icon: 'i-lucide-users',
337
- onSelect: () => ui.openAccountSettings('fragments'),
338
- })
339
- }
340
- return items
341
- })
342
- // Picker menu: fragments appropriate to the enclosing frame's block type (the "scope"), not
343
- // already selected, grouped by category, with the management links appended as the final group.
344
- // Falls back to `service` before a frame resolves so the catalog is still browsable.
345
- const fragmentMenu = computed<DropdownMenuItem[][]>(() => {
346
- const selected = new Set(fragmentIds.value)
347
- return [
348
- ...buildFragmentPickerGroups(
349
- fragments.forBlockType(frame.value?.type ?? 'service'),
350
- (id) => selected.has(id),
351
- (id) => {
352
- if (!fragmentIds.value.includes(id)) fragmentIds.value = [...fragmentIds.value, id]
353
- },
354
- ),
355
- fragmentManageItems.value,
356
- ]
357
- })
358
- function removeFragment(id: string) {
359
- fragmentIds.value = fragmentIds.value.filter((x) => x !== id)
360
- }
315
+ // The pool the shared <FragmentSelector> offers: fragments appropriate to the enclosing frame's
316
+ // block type (the "scope"). Falls back to `service` before a frame resolves so the catalog is
317
+ // still browsable.
318
+ const fragmentPool = computed(() => fragments.forBlockType(frame.value?.type ?? 'service'))
361
319
 
362
320
  // Hide UI-testing pipelines (`tester-ui` / `visual-confirmation`) when the target frame has no
363
321
  // UI to exercise — they'd be refused server-side (see utils/pipeline + the backend gate). Also
@@ -544,7 +502,11 @@ watch(open, (isOpen) => {
544
502
  docOutlineHints.value = ''
545
503
  reviewPrRef.value = ''
546
504
  reviewFocus.value = ''
547
- fragmentIds.value = []
505
+ // Pre-seed the best-practice fragments from the enclosing service's standards, so a new task
506
+ // ships with its service's fragments already selected (and freely add/removable here). The task
507
+ // OWNS this selection from creation — the engine folds exactly these, without re-unioning the
508
+ // service's set, so removing one here actually drops it for this task.
509
+ fragmentIds.value = [...(frame.value?.serviceFragmentIds ?? [])]
548
510
  for (const key of Object.keys(docKindFieldValues) as DocKindFieldKey[])
549
511
  delete docKindFieldValues[key]
550
512
  riskPolicyId.value = ''
@@ -664,7 +626,10 @@ async function add() {
664
626
  ...(Object.keys(agentConfigValues.value).length
665
627
  ? { agentConfig: agentConfigValues.value }
666
628
  : {}),
667
- ...(fragmentIds.value.length ? { fragmentIds: [...fragmentIds.value] } : {}),
629
+ // Always send the (service-seeded, then user-edited) selection including an empty list,
630
+ // which means "the user cleared the inherited picks" and must be honoured rather than
631
+ // re-seeded from the service. The task owns its fragments from here.
632
+ fragmentIds: [...fragmentIds.value],
668
633
  ...(technical.value ? { technical: true } : {}),
669
634
  })
670
635
  if (block) {
@@ -1058,41 +1023,15 @@ async function add() {
1058
1023
  </div>
1059
1024
  </div>
1060
1025
 
1061
- <!-- Best-practice fragments pinned on the task at creation, scoped to the frame's type. -->
1026
+ <!-- Best-practice fragments pinned on the task at creation, scoped to the frame's type.
1027
+ Pre-seeded from the enclosing service's standards; the task owns them from here. -->
1062
1028
  <div class="space-y-2">
1063
- <div class="flex items-center justify-between">
1064
- <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
1065
- {{ t('board.addTask.bestPractices') }}
1066
- </span>
1067
- <UDropdownMenu :items="fragmentMenu">
1068
- <UButton
1069
- color="neutral"
1070
- variant="soft"
1071
- size="xs"
1072
- icon="i-lucide-plus"
1073
- trailing-icon="i-lucide-chevron-down"
1074
- >
1075
- {{ t('board.addTask.attach') }}
1076
- </UButton>
1077
- </UDropdownMenu>
1078
- </div>
1079
- <div v-if="selectedFragments.length" class="flex flex-wrap gap-1">
1080
- <UBadge
1081
- v-for="f in selectedFragments"
1082
- :key="f.id"
1083
- color="primary"
1084
- variant="subtle"
1085
- size="sm"
1086
- class="cursor-pointer"
1087
- :title="f.summary"
1088
- @click="removeFragment(f.id)"
1089
- >
1090
- {{ f.title }}<UIcon name="i-lucide-x" class="ms-0.5 h-3 w-3" />
1091
- </UBadge>
1092
- </div>
1093
- <p v-else class="text-[11px] text-slate-500">
1094
- {{ t('board.addTask.bestPracticesHint') }}
1095
- </p>
1029
+ <FragmentSelector
1030
+ v-model="fragmentIds"
1031
+ :pool="fragmentPool"
1032
+ :label="t('board.addTask.bestPractices')"
1033
+ :empty-text="t('board.addTask.bestPracticesHint')"
1034
+ />
1096
1035
  </div>
1097
1036
 
1098
1037
  <!-- Context documents (ungated; Attach disabled until a source is connected). -->
@@ -0,0 +1,121 @@
1
+ <script setup lang="ts">
2
+ // Shared best-practice prompt-fragment picker: a category-grouped "add" dropdown (with links out
3
+ // to the fragment library) plus the currently-selected fragments as removable badges.
4
+ // Presentational + `v-model`-driven — the caller owns WHERE the selection lives (a task's
5
+ // `fragmentIds`, a service's `serviceFragmentIds`, or a not-yet-created task's local draft) and
6
+ // binds the id list. Authored once and reused by the create-task form and the task/service
7
+ // inspectors so the three pickers can't drift. Ids the catalog no longer resolves still render
8
+ // (labelled by their raw id) so they stay visible and removable.
9
+ import type { DropdownMenuItem } from '@nuxt/ui'
10
+ import type { PromptFragment } from '~/types/domain'
11
+ import { buildFragmentPickerGroups } from '~/utils/fragmentPicker'
12
+
13
+ const props = withDefaults(
14
+ defineProps<{
15
+ /** The selected fragment ids (`v-model`). */
16
+ modelValue: string[]
17
+ /** Candidate pool to offer — e.g. `fragments.forBlockType(type)` or the whole catalog. */
18
+ pool: PromptFragment[]
19
+ /** Optional label rendered to the left of the add button (omit when a section supplies it). */
20
+ label?: string
21
+ /** Optional text shown when nothing is selected. */
22
+ emptyText?: string
23
+ }>(),
24
+ { label: '', emptyText: '' },
25
+ )
26
+ const emit = defineEmits<{ 'update:modelValue': [string[]] }>()
27
+
28
+ const fragments = useFragmentsStore()
29
+ const ui = useUiStore()
30
+ const accounts = useAccountsStore()
31
+ const { t } = useI18n()
32
+
33
+ // The catalog is per-board and invalidated on a workspace switch; (re)load it lazily — a no-op
34
+ // while current.
35
+ onMounted(() => fragments.ensureLoaded())
36
+
37
+ const selectedFragments = computed(() =>
38
+ props.modelValue.map((id) => fragments.getFragment(id) ?? { id, title: id, summary: '' }),
39
+ )
40
+
41
+ // A trailing group that jumps from "attach a fragment" to authoring/editing the library itself
42
+ // (board tier always; account tier when accounts are enabled). Open to every member.
43
+ const manageItems = computed<DropdownMenuItem[]>(() => {
44
+ const items: DropdownMenuItem[] = [
45
+ {
46
+ label: t('inspector.fragments.manageBoard'),
47
+ icon: 'i-lucide-book-marked',
48
+ onSelect: () => ui.openFragmentLibrary(),
49
+ },
50
+ ]
51
+ if (accounts.enabled) {
52
+ items.push({
53
+ label: t('inspector.fragments.manageAccount'),
54
+ icon: 'i-lucide-users',
55
+ onSelect: () => ui.openAccountSettings('fragments'),
56
+ })
57
+ }
58
+ return items
59
+ })
60
+
61
+ // Picker menu: pool fragments not already selected, grouped into labelled per-category sections,
62
+ // with the management links appended as the final group.
63
+ const fragmentMenu = computed<DropdownMenuItem[][]>(() => {
64
+ const selected = new Set(props.modelValue)
65
+ return [
66
+ ...buildFragmentPickerGroups(props.pool, (id) => selected.has(id), addFragment),
67
+ manageItems.value,
68
+ ]
69
+ })
70
+
71
+ function addFragment(id: string) {
72
+ if (props.modelValue.includes(id)) return
73
+ emit('update:modelValue', [...props.modelValue, id])
74
+ }
75
+
76
+ function removeFragment(id: string) {
77
+ emit(
78
+ 'update:modelValue',
79
+ props.modelValue.filter((x) => x !== id),
80
+ )
81
+ }
82
+ </script>
83
+
84
+ <template>
85
+ <div>
86
+ <div class="mb-1 flex items-center justify-between gap-2">
87
+ <span v-if="label" class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
88
+ {{ label }}
89
+ </span>
90
+ <span v-else />
91
+ <UDropdownMenu :items="fragmentMenu">
92
+ <UButton
93
+ size="xs"
94
+ variant="ghost"
95
+ color="neutral"
96
+ icon="i-lucide-plus"
97
+ trailing-icon="i-lucide-chevron-down"
98
+ data-testid="fragment-add"
99
+ />
100
+ </UDropdownMenu>
101
+ </div>
102
+ <div v-if="selectedFragments.length" class="flex flex-wrap gap-1">
103
+ <UBadge
104
+ v-for="f in selectedFragments"
105
+ :key="f.id"
106
+ color="primary"
107
+ variant="subtle"
108
+ size="sm"
109
+ class="cursor-pointer"
110
+ :title="f.summary"
111
+ data-testid="fragment-badge"
112
+ @click="removeFragment(f.id)"
113
+ >
114
+ {{ f.title }}<UIcon name="i-lucide-x" class="ms-0.5 h-3 w-3" />
115
+ </UBadge>
116
+ </div>
117
+ <div v-else-if="emptyText" class="text-[11px] text-slate-500">
118
+ {{ emptyText }}
119
+ </div>
120
+ </div>
121
+ </template>
@@ -1,76 +1,22 @@
1
1
  <script setup lang="ts">
2
- import type { DropdownMenuItem } from '@nuxt/ui'
3
2
  import type { Block } from '~/types/domain'
4
3
  import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
5
- import { buildFragmentPickerGroups } from '~/utils/fragmentPicker'
4
+ import FragmentSelector from '~/components/fragments/FragmentSelector.vue'
6
5
 
7
6
  // Service-level best-practice fragments (frame blocks). These are the programming
8
- // standards/guidelines for the whole service; at run time their bodies are folded
9
- // into the prompt of every `code-aware` agent on tasks under this service. Drawn from
10
- // the board's merged fragment catalog (built-in ∪ registered ∪ account ∪ workspace,
11
- // via the fragments store; static pool when the library is off), grouped by category.
12
- // `defaultOpen` expands the section on surfaces that embed this as the primary content
13
- // (the add-service modal); the inspector leaves it collapsed.
7
+ // standards/guidelines for the whole service: they SEED a new task's own selection at creation,
8
+ // and at run time their bodies fold into the frame's own `code-aware` runs. Drawn from the
9
+ // board's merged fragment catalog (built-in ∪ registered ∪ account ∪ workspace). `defaultOpen`
10
+ // expands the section on surfaces that embed this as the primary content (the add-service
11
+ // modal); the inspector leaves it collapsed. The shared <FragmentSelector> renders the picker.
14
12
  const props = defineProps<{ block: Block; defaultOpen?: boolean }>()
15
13
 
16
14
  const board = useBoardStore()
17
15
  const fragments = useFragmentsStore()
18
- const ui = useUiStore()
19
- const accounts = useAccountsStore()
20
16
  const { t } = useI18n()
21
17
 
22
- onMounted(() => fragments.ensureLoaded())
23
-
24
- // An id the catalog no longer resolves (removed/suppressed after selection) still
25
- // renders — labelled by its raw id — so it stays visible and removable.
26
- const selectedFragments = computed(() =>
27
- (props.block.serviceFragmentIds ?? []).map(
28
- (id) => fragments.getFragment(id) ?? { id, title: id, summary: '' },
29
- ),
30
- )
31
-
32
- // A trailing group that jumps from "attach a fragment" to authoring/editing the
33
- // library itself (board tier always; account tier when accounts are enabled).
34
- // Open to every member — managing fragments is not an admin-only action.
35
- const manageItems = computed<DropdownMenuItem[]>(() => {
36
- const items: DropdownMenuItem[] = [
37
- {
38
- label: t('inspector.fragments.manageBoard'),
39
- icon: 'i-lucide-book-marked',
40
- onSelect: () => ui.openFragmentLibrary(),
41
- },
42
- ]
43
- if (accounts.enabled) {
44
- items.push({
45
- label: t('inspector.fragments.manageAccount'),
46
- icon: 'i-lucide-users',
47
- onSelect: () => ui.openAccountSettings('fragments'),
48
- })
49
- }
50
- return items
51
- })
52
-
53
- // Picker menu: every pool fragment not already selected, grouped into labelled
54
- // per-category sections, with the management links appended as the final group.
55
- const fragmentMenu = computed<DropdownMenuItem[][]>(() => {
56
- const selected = new Set(props.block.serviceFragmentIds ?? [])
57
- return [
58
- ...buildFragmentPickerGroups(fragments.fragments, (id) => selected.has(id), addFragment),
59
- manageItems.value,
60
- ]
61
- })
62
-
63
- function addFragment(id: string) {
64
- const list = props.block.serviceFragmentIds ? [...props.block.serviceFragmentIds] : []
65
- if (!list.includes(id)) list.push(id)
66
- board.updateBlock(props.block.id, { serviceFragmentIds: list })
67
- }
68
-
69
- function removeFragment(id: string) {
70
- if (!props.block.serviceFragmentIds) return
71
- board.updateBlock(props.block.id, {
72
- serviceFragmentIds: props.block.serviceFragmentIds.filter((x) => x !== id),
73
- })
18
+ function setFragments(ids: string[]) {
19
+ board.updateBlock(props.block.id, { serviceFragmentIds: ids })
74
20
  }
75
21
  </script>
76
22
 
@@ -78,36 +24,14 @@ function removeFragment(id: string) {
78
24
  <InspectorSection
79
25
  :title="t('inspector.fragments.serviceTitle')"
80
26
  :hint="t('inspector.fragments.serviceHint')"
81
- :count="selectedFragments.length"
27
+ :count="(block.serviceFragmentIds ?? []).length"
82
28
  :default-open="props.defaultOpen"
83
29
  >
84
- <template #actions>
85
- <UDropdownMenu :items="fragmentMenu">
86
- <UButton
87
- size="xs"
88
- variant="ghost"
89
- color="neutral"
90
- icon="i-lucide-plus"
91
- trailing-icon="i-lucide-chevron-down"
92
- />
93
- </UDropdownMenu>
94
- </template>
95
- <div v-if="selectedFragments.length" class="flex flex-wrap gap-1">
96
- <UBadge
97
- v-for="f in selectedFragments"
98
- :key="f.id"
99
- color="primary"
100
- variant="subtle"
101
- size="sm"
102
- class="cursor-pointer"
103
- :title="f.summary"
104
- @click="removeFragment(f.id)"
105
- >
106
- {{ f.title }}<UIcon name="i-lucide-x" class="ms-0.5 h-3 w-3" />
107
- </UBadge>
108
- </div>
109
- <div v-else class="text-[11px] text-slate-500">
110
- {{ t('inspector.fragments.serviceEmpty') }}
111
- </div>
30
+ <FragmentSelector
31
+ :model-value="block.serviceFragmentIds ?? []"
32
+ :pool="fragments.fragments"
33
+ :empty-text="t('inspector.fragments.serviceEmpty')"
34
+ @update:model-value="setFragments"
35
+ />
112
36
  </InspectorSection>
113
37
  </template>
@@ -1,78 +1,20 @@
1
1
  <script setup lang="ts">
2
- import type { DropdownMenuItem } from '@nuxt/ui'
3
2
  import type { Block } from '~/types/domain'
4
3
  import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
5
- import { buildFragmentPickerGroups } from '~/utils/fragmentPicker'
4
+ import FragmentSelector from '~/components/fragments/FragmentSelector.vue'
6
5
 
7
6
  const props = defineProps<{ block: Block }>()
8
7
 
9
8
  const board = useBoardStore()
10
9
  const fragments = useFragmentsStore()
11
- const ui = useUiStore()
12
- const accounts = useAccountsStore()
13
10
  const { t } = useI18n()
14
11
 
15
- // The catalog is per-board and invalidated on a workspace switch, so (re)load it when the
16
- // task inspector mounts — mirrors ServiceFragments; ensureLoaded is a no-op while current.
17
- onMounted(() => fragments.ensureLoaded())
18
-
19
12
  // ---- best-practice prompt fragments ----------------------------------------
20
- // Selected fragments, resolved against the catalog. An id the catalog no longer
21
- // resolves (removed/suppressed after selection) still renders labelled by its
22
- // raw id so it stays visible and removable.
23
- const selectedFragments = computed(() =>
24
- (props.block.fragmentIds ?? []).map(
25
- (id) => fragments.getFragment(id) ?? { id, title: id, summary: '' },
26
- ),
27
- )
28
-
29
- // A trailing group that jumps from "attach a fragment" to authoring/editing the
30
- // library itself (board tier always; account tier when accounts are enabled).
31
- // Open to every member — managing fragments is not an admin-only action.
32
- const manageItems = computed<DropdownMenuItem[]>(() => {
33
- const items: DropdownMenuItem[] = [
34
- {
35
- label: t('inspector.fragments.manageBoard'),
36
- icon: 'i-lucide-book-marked',
37
- onSelect: () => ui.openFragmentLibrary(),
38
- },
39
- ]
40
- if (accounts.enabled) {
41
- items.push({
42
- label: t('inspector.fragments.manageAccount'),
43
- icon: 'i-lucide-users',
44
- onSelect: () => ui.openAccountSettings('fragments'),
45
- })
46
- }
47
- return items
48
- })
49
-
50
- // Picker menu: fragments suitable for this block's type, not already selected,
51
- // grouped into labelled per-category sections so the dropdown reads like the catalog,
52
- // with the management links appended as the final group.
53
- const fragmentMenu = computed<DropdownMenuItem[][]>(() => {
54
- const selected = new Set(props.block.fragmentIds ?? [])
55
- return [
56
- ...buildFragmentPickerGroups(
57
- fragments.forBlockType(props.block.type),
58
- (id) => selected.has(id),
59
- addFragment,
60
- ),
61
- manageItems.value,
62
- ]
63
- })
64
-
65
- function addFragment(id: string) {
66
- const list = props.block.fragmentIds ? [...props.block.fragmentIds] : []
67
- if (!list.includes(id)) list.push(id)
68
- board.updateBlock(props.block.id, { fragmentIds: list })
69
- }
70
-
71
- function removeFragment(id: string) {
72
- if (!props.block.fragmentIds) return
73
- board.updateBlock(props.block.id, {
74
- fragmentIds: props.block.fragmentIds.filter((x) => x !== id),
75
- })
13
+ // The task's OWN selection (seeded from its service at creation, then editable per task). The
14
+ // shared <FragmentSelector> renders the picker; a change persists via updateBlock.
15
+ const fragmentPool = computed(() => fragments.forBlockType(props.block.type))
16
+ function setFragments(ids: string[]) {
17
+ board.updateBlock(props.block.id, { fragmentIds: ids })
76
18
  }
77
19
  </script>
78
20
 
@@ -97,37 +39,13 @@ function removeFragment(id: string) {
97
39
 
98
40
  <!-- best practices (prompt fragments) -->
99
41
  <div>
100
- <div class="mb-1 flex items-center justify-between">
101
- <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
102
- {{ t('inspector.structure.bestPractices') }}
103
- </span>
104
- <UDropdownMenu :items="fragmentMenu">
105
- <UButton
106
- size="xs"
107
- variant="ghost"
108
- color="neutral"
109
- icon="i-lucide-plus"
110
- trailing-icon="i-lucide-chevron-down"
111
- />
112
- </UDropdownMenu>
113
- </div>
114
- <div v-if="selectedFragments.length" class="mb-1 flex flex-wrap gap-1">
115
- <UBadge
116
- v-for="f in selectedFragments"
117
- :key="f.id"
118
- color="primary"
119
- variant="subtle"
120
- size="sm"
121
- class="cursor-pointer"
122
- :title="f.summary"
123
- @click="removeFragment(f.id)"
124
- >
125
- {{ f.title }}<UIcon name="i-lucide-x" class="ms-0.5 h-3 w-3" />
126
- </UBadge>
127
- </div>
128
- <div v-else class="text-[11px] text-slate-500">
129
- {{ t('inspector.structure.bestPracticesEmpty') }}
130
- </div>
42
+ <FragmentSelector
43
+ :model-value="block.fragmentIds ?? []"
44
+ :pool="fragmentPool"
45
+ :label="t('inspector.structure.bestPractices')"
46
+ :empty-text="t('inspector.structure.bestPracticesEmpty')"
47
+ @update:model-value="setFragments"
48
+ />
131
49
  <p class="mt-1 text-[11px] leading-snug text-slate-500">
132
50
  {{ t('inspector.structure.bestPracticesHint') }}
133
51
  </p>
@@ -241,7 +241,10 @@ export const useBoardStore = defineStore('board', () => {
241
241
  ...(options?.modelPresetId ? { modelPresetId: options.modelPresetId } : {}),
242
242
  ...(options?.pipelineId ? { pipelineId: options.pipelineId } : {}),
243
243
  ...(options?.agentConfig ? { agentConfig: options.agentConfig } : {}),
244
- ...(options?.fragmentIds?.length ? { fragmentIds: options.fragmentIds } : {}),
244
+ // Forward the selection when the caller provides one (the create form always does, even
245
+ // when empty — an explicit clear the backend must honour rather than re-seed); omit only
246
+ // when a caller doesn't manage fragments at all (then the backend seeds from the service).
247
+ ...(options?.fragmentIds !== undefined ? { fragmentIds: options.fragmentIds } : {}),
245
248
  ...(options?.technical ? { technical: true } : {}),
246
249
  })
247
250
  upsert(block)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.143.1",
3
+ "version": "0.144.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",
@@ -40,7 +40,7 @@
40
40
  "valibot": "^1.4.2",
41
41
  "vue": "3.5.40",
42
42
  "wretch": "^3.0.9",
43
- "@cat-factory/contracts": "0.152.0"
43
+ "@cat-factory/contracts": "0.152.1"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@toad-contracts/testing": "0.3.2",