@cat-factory/app 0.106.0 → 0.107.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,10 +1,24 @@
1
1
  <script setup lang="ts">
2
- // Create a new INITIATIVE under a service frame — the longer-running counterpart
3
- // to a task. The user names it and writes the rough goal statement; the server
4
- // materialises the initiative-level board block + its empty tracker entity in one
5
- // call. Nothing is planned here: the user then runs the Initiative Planning
6
- // pipeline (pl_initiative) on the block, which analyses the codebase, drafts the
7
- // multi-phase plan for approval, and commits the in-repo tracker.
2
+ // Create a new INITIATIVE under a service frame — the longer-running counterpart to a task. The
3
+ // user picks a PRESET (the built-in "Custom initiative" plus any a deployment registered), fills
4
+ // the preset's descriptor-driven form, and names the goal; the server materialises the
5
+ // initiative-level board block + its empty tracker entity in one call, freezing the (validated,
6
+ // sanitized) preset inputs on the entity. Nothing is planned here: the user then runs the preset's
7
+ // planning pipeline on the block from the inspector.
8
+ //
9
+ // The preset form is rendered GENERICALLY from `descriptor.fields` (InitiativePresetFields) — zero
10
+ // per-preset frontend code. A preset with a repo-detection probe prefills its form from the frame's
11
+ // repo on selection (best-effort; failures fall back to descriptor defaults and never block create).
12
+ import { computed, ref, watch } from 'vue'
13
+ import {
14
+ sanitizeInitiativePresetInputs,
15
+ validateInitiativePresetInputs,
16
+ } from '@cat-factory/contracts'
17
+ import type { InitiativePresetInputs, InitiativePresetInputValue } from '~/types/domain'
18
+ import { defaultPresetInputs } from '~/utils/initiative'
19
+ import { GENERIC_PRESET_ID } from '~/stores/initiative'
20
+ import InitiativePresetFields from '~/components/board/InitiativePresetFields.vue'
21
+
8
22
  const ui = useUiStore()
9
23
  const board = useBoardStore()
10
24
  const initiatives = useInitiativesStore()
@@ -22,23 +36,92 @@ const frame = computed(() =>
22
36
  ui.createInitiativeFrameId ? board.getBlock(ui.createInitiativeFrameId) : undefined,
23
37
  )
24
38
 
39
+ const presets = computed(() => initiatives.presets)
40
+ const selectedPresetId = ref(GENERIC_PRESET_ID)
41
+ // The resolved descriptor (defaulting to the generic preset). Null only when presets haven't
42
+ // hydrated yet — the create call still sends `preset_generic`, which the server always resolves.
43
+ const selectedPreset = computed(() => initiatives.presetById(selectedPresetId.value))
44
+
25
45
  const title = ref('')
26
46
  const description = ref('')
47
+ const inputs = ref<InitiativePresetInputs>({})
27
48
 
28
- watch(open, (o) => {
29
- if (o) {
30
- title.value = ''
31
- description.value = ''
49
+ // Monotonic token so a slow probe response from a since-changed preset/frame is discarded.
50
+ let probeSeq = 0
51
+
52
+ /** Seed the form to the selected preset's descriptor defaults, then fire its detection probe. */
53
+ function applyPreset(): void {
54
+ const descriptor = selectedPreset.value
55
+ inputs.value = descriptor ? defaultPresetInputs(descriptor) : {}
56
+ void runProbe()
57
+ }
58
+
59
+ /** Whether two preset values are equal (shallow — arrays compared element-wise). */
60
+ function sameValue(
61
+ a: InitiativePresetInputValue | undefined,
62
+ b: InitiativePresetInputValue | undefined,
63
+ ): boolean {
64
+ if (Array.isArray(a) && Array.isArray(b))
65
+ return a.length === b.length && a.every((x, i) => x === b[i])
66
+ return a === b
67
+ }
68
+
69
+ /** Best-effort repo-detection prefill: merge detected values (known fields only) over the defaults. */
70
+ async function runProbe(): Promise<void> {
71
+ const descriptor = selectedPreset.value
72
+ const frameId = ui.createInitiativeFrameId
73
+ if (!descriptor?.probe || !frameId) return
74
+ const seq = ++probeSeq
75
+ // The just-seeded descriptor defaults; a detected value overrides these but NOT a user edit.
76
+ const baseline = inputs.value
77
+ const detected = await initiatives.probePreset(descriptor.id, frameId)
78
+ // Discard a stale response (the user re-picked a preset / closed the modal meanwhile).
79
+ if (seq !== probeSeq || selectedPreset.value?.id !== descriptor.id) return
80
+ const known = new Set(descriptor.fields.map((f) => f.key))
81
+ const merged: InitiativePresetInputs = { ...inputs.value }
82
+ for (const [key, value] of Object.entries(detected)) {
83
+ // Prefill only known fields the user hasn't edited since the probe fired (still at the default),
84
+ // so a slow probe can't clobber a value the user typed while it was in flight.
85
+ if (known.has(key) && sameValue(merged[key], baseline[key])) merged[key] = value
32
86
  }
87
+ inputs.value = merged
88
+ }
89
+
90
+ function selectPreset(id: string): void {
91
+ if (id === selectedPresetId.value) return
92
+ selectedPresetId.value = id
93
+ applyPreset()
94
+ }
95
+
96
+ watch(open, (o) => {
97
+ if (!o) return
98
+ title.value = ''
99
+ description.value = ''
100
+ selectedPresetId.value = GENERIC_PRESET_ID
101
+ applyPreset()
33
102
  })
34
103
 
104
+ // Client-side mirror of the server's create validation (the SAME shared function), so the submit
105
+ // button reflects an invalid form; the per-field path error is shown inline by the renderer.
106
+ const presetProblems = computed(() =>
107
+ selectedPreset.value ? validateInitiativePresetInputs(selectedPreset.value, inputs.value) : [],
108
+ )
109
+ const canSubmit = computed(
110
+ () => title.value.trim().length > 0 && presetProblems.value.length === 0 && !initiatives.creating,
111
+ )
112
+
35
113
  async function create() {
36
114
  const frameId = ui.createInitiativeFrameId
37
- if (!frameId || !title.value.trim() || initiatives.creating) return
115
+ if (!frameId || !canSubmit.value) return
116
+ const descriptor = selectedPreset.value
38
117
  try {
39
118
  const { block } = await initiatives.create(frameId, {
40
119
  title: title.value.trim(),
41
120
  description: description.value.trim() || undefined,
121
+ presetId: descriptor?.id ?? GENERIC_PRESET_ID,
122
+ presetInputs: descriptor
123
+ ? sanitizeInitiativePresetInputs(descriptor, inputs.value)
124
+ : undefined,
42
125
  })
43
126
  ui.closeCreateInitiative()
44
127
  // Select the fresh block so the inspector offers "Run planning" right away.
@@ -66,6 +149,44 @@ async function create() {
66
149
  </i18n-t>
67
150
  </p>
68
151
 
152
+ <!-- Preset picker: only when a deployment registered presets beyond the built-in generic
153
+ one, so a single-preset install keeps today's plain form. -->
154
+ <div v-if="presets.length > 1" class="space-y-1.5">
155
+ <span class="text-xs font-medium text-slate-300">{{
156
+ t('initiative.create.preset')
157
+ }}</span>
158
+ <div class="grid gap-2" data-testid="initiative-preset-picker">
159
+ <button
160
+ v-for="p in presets"
161
+ :key="p.id"
162
+ type="button"
163
+ :data-testid="`initiative-preset-option-${p.id}`"
164
+ :aria-pressed="p.id === selectedPresetId"
165
+ class="flex items-start gap-3 rounded-md border px-3 py-2 text-left transition"
166
+ :class="
167
+ p.id === selectedPresetId
168
+ ? 'border-primary-500 bg-primary-950/30'
169
+ : 'border-slate-700 hover:border-slate-600'
170
+ "
171
+ @click="selectPreset(p.id)"
172
+ >
173
+ <UIcon
174
+ :name="p.presentation.icon"
175
+ class="mt-0.5 size-5 shrink-0"
176
+ :style="{ color: p.presentation.color }"
177
+ />
178
+ <span class="min-w-0">
179
+ <span class="block text-sm font-medium text-slate-200">
180
+ {{ p.presentation.label }}
181
+ </span>
182
+ <span class="block text-[11px] text-slate-400">
183
+ {{ p.presentation.description }}
184
+ </span>
185
+ </span>
186
+ </button>
187
+ </div>
188
+ </div>
189
+
69
190
  <UFormField :label="t('initiative.create.titleField')" required>
70
191
  <UInput
71
192
  v-model="title"
@@ -88,6 +209,13 @@ async function create() {
88
209
  />
89
210
  </UFormField>
90
211
 
212
+ <!-- The preset's descriptor-driven form (renders nothing for the fieldless generic preset). -->
213
+ <InitiativePresetFields
214
+ v-if="selectedPreset"
215
+ v-model="inputs"
216
+ :descriptor="selectedPreset"
217
+ />
218
+
91
219
  <p class="text-[11px] text-slate-500">
92
220
  {{ t('initiative.create.hint') }}
93
221
  </p>
@@ -110,7 +238,7 @@ async function create() {
110
238
  data-testid="create-initiative-submit"
111
239
  color="primary"
112
240
  :loading="initiatives.creating"
113
- :disabled="!title.trim()"
241
+ :disabled="!canSubmit"
114
242
  @click="create"
115
243
  >
116
244
  {{ t('initiative.create.submit') }}
@@ -0,0 +1,155 @@
1
+ <script setup lang="ts">
2
+ // Generic, descriptor-driven renderer for an initiative preset's create-time FORM. Extends the
3
+ // `ProviderConnectionTab.vue` flat-field pattern with the three shapes a preset form adds:
4
+ // `checkbox-group` (multi-select → `string[]`), `path` (a repo-relative dir with inline
5
+ // safety validation), and single-condition `showWhen` visibility. Every preset renders through
6
+ // THIS component with zero per-preset frontend code — the backend descriptor supplies the fields
7
+ // (labels/help/options are backend-supplied English, per the `describeConfig` convention). The
8
+ // model is the typed `InitiativePresetInputs` map (scalars stay strings, `number` a number,
9
+ // `checkbox` a boolean, `checkbox-group` a `string[]`) so it round-trips the wire contract and the
10
+ // shared `validateInitiativePresetInputs` unchanged.
11
+ import { computed } from 'vue'
12
+ import { isPresetFieldVisible, isSafeRepoDirPath } from '@cat-factory/contracts'
13
+ import type {
14
+ InitiativePresetDescriptor,
15
+ InitiativePresetField,
16
+ InitiativePresetInputs,
17
+ InitiativePresetInputValue,
18
+ } from '~/types/domain'
19
+
20
+ const props = defineProps<{ descriptor: InitiativePresetDescriptor }>()
21
+ const model = defineModel<InitiativePresetInputs>({ required: true })
22
+ const { t } = useI18n()
23
+
24
+ // Only fields whose `showWhen` holds against the current values are shown; a hidden field's stale
25
+ // value is kept in the model (so re-showing restores it) but the server + client both drop it at
26
+ // sanitize/validate time, so it can never freeze an unvalidated value.
27
+ const visibleFields = computed(() =>
28
+ props.descriptor.fields.filter((f) => isPresetFieldVisible(f, model.value)),
29
+ )
30
+
31
+ /**
32
+ * An "empty" value that must stay ABSENT from the model rather than freeze on the entity: an
33
+ * unchecked (`false`) checkbox, a blank string, or an empty multi-select. A numeric `0` is a real
34
+ * value and is kept (strict `=== false`/`=== ''` never match it).
35
+ */
36
+ function isEmptyValue(value: InitiativePresetInputValue): boolean {
37
+ return value === false || value === '' || (Array.isArray(value) && value.length === 0)
38
+ }
39
+
40
+ /**
41
+ * Immutably set one field's value on the model, DROPPING empty values so a cleared field never
42
+ * freezes an empty `''`/`[]`/`false` (mirrors `ProviderConnectionTab`'s delete-when-blank and what
43
+ * the shared `validate`/`sanitize` treat as unset — an unchecked box / blank field stays absent).
44
+ */
45
+ function set(key: string, value: InitiativePresetInputValue | undefined): void {
46
+ const next = { ...model.value }
47
+ if (value === undefined || isEmptyValue(value)) delete next[key]
48
+ else next[key] = value
49
+ model.value = next
50
+ }
51
+
52
+ function stringValue(key: string): string {
53
+ const v = model.value[key]
54
+ return typeof v === 'string' ? v : ''
55
+ }
56
+ function boolValue(key: string): boolean {
57
+ return model.value[key] === true
58
+ }
59
+ function numberStr(key: string): string {
60
+ const v = model.value[key]
61
+ return typeof v === 'number' ? String(v) : ''
62
+ }
63
+ function groupValue(key: string): string[] {
64
+ const v = model.value[key]
65
+ return Array.isArray(v) ? v : []
66
+ }
67
+
68
+ function toggleGroup(key: string, option: string, checked: boolean): void {
69
+ const current = groupValue(key)
70
+ set(key, checked ? [...new Set([...current, option])] : current.filter((o) => o !== option))
71
+ }
72
+
73
+ /** A `path` field is flagged only when non-empty AND unsafe (empty is handled by `required`). */
74
+ function pathInvalid(field: InitiativePresetField): boolean {
75
+ if (field.type !== 'path') return false
76
+ const value = stringValue(field.key)
77
+ return value.trim().length > 0 && !isSafeRepoDirPath(value)
78
+ }
79
+
80
+ function selectItems(field: InitiativePresetField) {
81
+ return (field.options ?? []).map((o) => ({ label: o.label, value: o.value }))
82
+ }
83
+ </script>
84
+
85
+ <template>
86
+ <div v-if="visibleFields.length" class="space-y-4">
87
+ <UFormField
88
+ v-for="field in visibleFields"
89
+ :key="field.key"
90
+ :label="field.label"
91
+ :help="field.help"
92
+ :required="field.required"
93
+ :error="pathInvalid(field) ? t('initiative.create.pathInvalid') : undefined"
94
+ :data-testid="`initiative-preset-field-${field.key}`"
95
+ >
96
+ <!-- checkbox-group: a vertical list of toggles whose value is the checked option set. -->
97
+ <div v-if="field.type === 'checkbox-group'" class="space-y-1.5">
98
+ <UCheckbox
99
+ v-for="opt in field.options ?? []"
100
+ :key="opt.value"
101
+ :model-value="groupValue(field.key).includes(opt.value)"
102
+ :label="opt.label"
103
+ @update:model-value="
104
+ (v: boolean | 'indeterminate') => toggleGroup(field.key, opt.value, v === true)
105
+ "
106
+ />
107
+ </div>
108
+
109
+ <USelect
110
+ v-else-if="field.type === 'select'"
111
+ :model-value="stringValue(field.key)"
112
+ :items="selectItems(field)"
113
+ class="w-full"
114
+ :placeholder="field.placeholder"
115
+ @update:model-value="(v: string) => set(field.key, v)"
116
+ />
117
+
118
+ <USwitch
119
+ v-else-if="field.type === 'checkbox'"
120
+ :model-value="boolValue(field.key)"
121
+ @update:model-value="(v: boolean) => set(field.key, v)"
122
+ />
123
+
124
+ <UTextarea
125
+ v-else-if="field.type === 'textarea'"
126
+ :model-value="stringValue(field.key)"
127
+ :rows="3"
128
+ autoresize
129
+ class="w-full"
130
+ :placeholder="field.placeholder"
131
+ @update:model-value="(v: string) => set(field.key, v)"
132
+ />
133
+
134
+ <UInput
135
+ v-else-if="field.type === 'number'"
136
+ :model-value="numberStr(field.key)"
137
+ type="number"
138
+ class="w-full font-mono"
139
+ :placeholder="field.placeholder"
140
+ @update:model-value="(v: string) => set(field.key, v === '' ? undefined : Number(v))"
141
+ />
142
+
143
+ <!-- path + text/password (the untyped default): a single-line input. `path`s stay mono. -->
144
+ <UInput
145
+ v-else
146
+ :model-value="stringValue(field.key)"
147
+ :type="field.type === 'password' ? 'password' : 'text'"
148
+ class="w-full"
149
+ :class="{ 'font-mono': field.type === 'path' }"
150
+ :placeholder="field.placeholder"
151
+ @update:model-value="(v: string) => set(field.key, v)"
152
+ />
153
+ </UFormField>
154
+ </div>
155
+ </template>
@@ -8,6 +8,7 @@ import {
8
8
  getInitiativeContract,
9
9
  listInitiativesContract,
10
10
  pauseInitiativeContract,
11
+ probeInitiativePresetContract,
11
12
  proceedInitiativePlanningContract,
12
13
  promoteInitiativeFollowUpContract,
13
14
  resumeInitiativeContract,
@@ -16,6 +17,7 @@ import {
16
17
  } from '@cat-factory/contracts'
17
18
  import type {
18
19
  InitiativeExecutionPolicy,
20
+ InitiativePresetInputs,
19
21
  PromoteInitiativeFollowUpInput,
20
22
  UpdateInitiativeItemInput,
21
23
  } from '@cat-factory/contracts'
@@ -24,12 +26,30 @@ import type { ApiContext } from './context'
24
26
  /** Initiatives: the long-running multi-task work containers (create + tracker reads). */
25
27
  export function initiativeApi({ send, ws }: ApiContext) {
26
28
  return {
27
- // Create the initiative-level board block AND its empty entity in one call.
29
+ // Create the initiative-level board block AND its empty entity in one call. The optional
30
+ // `presetId` + `presetInputs` carry the create-form picker's selection (validated against the
31
+ // resolved descriptor and frozen on the entity by the server); absent ⇒ the generic behaviour.
28
32
  createInitiative: (
29
33
  workspaceId: string,
30
- body: { frameId: string; title: string; description?: string },
34
+ body: {
35
+ frameId: string
36
+ title: string
37
+ description?: string
38
+ presetId?: string
39
+ presetInputs?: InitiativePresetInputs
40
+ },
31
41
  ) => send(createInitiativeContract, { pathPrefix: ws(workspaceId), body }),
32
42
 
43
+ // Run a preset's repo-detection PREFILL probe against a frame's repo (seeds the create form).
44
+ // Best-effort by contract: `{}` (descriptor defaults) when GitHub is unwired / the frame has no
45
+ // linked repo / the preset has no `detect` hook — so the form never blocks on it.
46
+ probeInitiativePreset: (workspaceId: string, presetId: string, frameId: string) =>
47
+ send(probeInitiativePresetContract, {
48
+ pathPrefix: ws(workspaceId),
49
+ pathParams: { presetId },
50
+ body: { frameId },
51
+ }),
52
+
33
53
  listInitiatives: (workspaceId: string) =>
34
54
  send(listInitiativesContract, { pathPrefix: ws(workspaceId) }),
35
55
 
@@ -4,15 +4,17 @@ import type {
4
4
  Initiative,
5
5
  InitiativeExecutionPolicy,
6
6
  InitiativePresetDescriptor,
7
+ InitiativePresetInputs,
7
8
  PromoteInitiativeFollowUpInput,
8
9
  UpdateInitiativeItemInput,
9
10
  } from '~/types/domain'
10
11
 
11
- /** The built-in generic preset id (mirrors kernel's `GENERIC_INITIATIVE_PRESET_ID`). */
12
- const GENERIC_PRESET_ID = 'preset_generic'
13
12
  import { useWorkspaceStore } from '~/stores/workspace'
14
13
  import { useBoardStore } from '~/stores/board'
15
14
 
15
+ /** The built-in generic preset id (mirrors kernel's `GENERIC_INITIATIVE_PRESET_ID`). */
16
+ export const GENERIC_PRESET_ID = 'preset_generic'
17
+
16
18
  /**
17
19
  * Initiative state — the long-running multi-task work containers, keyed by their
18
20
  * anchor BLOCK id (the id everything on the board navigates by). Hydrated from the
@@ -99,8 +101,21 @@ export const useInitiativesStore = defineStore('initiatives', () => {
99
101
  byBlock.value = { ...byBlock.value, [initiative.blockId]: initiative }
100
102
  }
101
103
 
102
- /** Create an initiative under a service frame (block + entity in one call). */
103
- async function create(frameId: string, input: { title: string; description?: string }) {
104
+ /**
105
+ * Create an initiative under a service frame (block + entity in one call). `presetId` +
106
+ * `presetInputs` carry the create-form picker's selection; the server validates the inputs
107
+ * against the resolved descriptor and freezes their sanitized (known, visible) subset. Empty
108
+ * `presetInputs` is dropped so a preset with no form (the generic one) sends just its id.
109
+ */
110
+ async function create(
111
+ frameId: string,
112
+ input: {
113
+ title: string
114
+ description?: string
115
+ presetId?: string
116
+ presetInputs?: InitiativePresetInputs
117
+ },
118
+ ) {
104
119
  if (!workspace.workspaceId) throw new Error('No active workspace')
105
120
  creating.value = true
106
121
  try {
@@ -108,6 +123,10 @@ export const useInitiativesStore = defineStore('initiatives', () => {
108
123
  frameId,
109
124
  title: input.title,
110
125
  ...(input.description ? { description: input.description } : {}),
126
+ ...(input.presetId ? { presetId: input.presetId } : {}),
127
+ ...(input.presetInputs && Object.keys(input.presetInputs).length
128
+ ? { presetInputs: input.presetInputs }
129
+ : {}),
111
130
  })
112
131
  useBoardStore().upsert(created.block)
113
132
  upsert(created.initiative)
@@ -117,6 +136,21 @@ export const useInitiativesStore = defineStore('initiatives', () => {
117
136
  }
118
137
  }
119
138
 
139
+ /**
140
+ * Run a preset's repo-detection PREFILL probe for a frame, returning the detected form values to
141
+ * seed the create form. Best-effort: an unwired GitHub / no linked repo / no `detect` hook yields
142
+ * `{}` (descriptor defaults) by contract, and any transport error degrades to `{}` — the probe
143
+ * never blocks create.
144
+ */
145
+ async function probePreset(presetId: string, frameId: string): Promise<InitiativePresetInputs> {
146
+ if (!workspace.workspaceId) return {}
147
+ try {
148
+ return await api.probeInitiativePreset(workspace.workspaceId, presetId, frameId)
149
+ } catch {
150
+ return {}
151
+ }
152
+ }
153
+
120
154
  /** Re-fetch one block's initiative (the tracker window's load path). */
121
155
  async function load(blockId: string) {
122
156
  if (!workspace.workspaceId) return
@@ -286,6 +320,7 @@ export const useInitiativesStore = defineStore('initiatives', () => {
286
320
  hydratePresets,
287
321
  upsert,
288
322
  create,
323
+ probePreset,
289
324
  load,
290
325
  answerQuestion,
291
326
  continuePlanning,
@@ -14,6 +14,9 @@ export type {
14
14
  InitiativePhase,
15
15
  InitiativePipelineRule,
16
16
  InitiativePresetDescriptor,
17
+ InitiativePresetField,
18
+ InitiativePresetInputs,
19
+ InitiativePresetInputValue,
17
20
  InitiativeQa,
18
21
  InitiativeStatus,
19
22
  PromoteInitiativeFollowUpInput,
@@ -2,6 +2,8 @@ import type {
2
2
  InitiativeFollowUp,
3
3
  InitiativeItem,
4
4
  InitiativeItemStatus,
5
+ InitiativePresetDescriptor,
6
+ InitiativePresetInputs,
5
7
  InitiativeStatus,
6
8
  } from '~/types/domain'
7
9
 
@@ -68,6 +70,34 @@ export const INITIATIVE_FOLLOWUP_STATUS_CHIPS: Record<InitiativeFollowUp['status
68
70
  dismissed: 'neutral',
69
71
  }
70
72
 
73
+ /**
74
+ * The initial, typed create-form values a preset descriptor implies — its field DEFAULTS folded
75
+ * into the `InitiativePresetInputs` shape the renderer + wire contract expect (`checkbox-group` →
76
+ * `string[]`, `checkbox` → boolean, `number` → number, everything else a string). Only fields with
77
+ * a meaningful default are seeded, so unfilled optional fields stay absent (equivalent to unset for
78
+ * validation) and never freeze an empty value. The probe prefill and the user's edits layer on top.
79
+ */
80
+ export function defaultPresetInputs(
81
+ descriptor: InitiativePresetDescriptor,
82
+ ): InitiativePresetInputs {
83
+ const inputs: InitiativePresetInputs = {}
84
+ for (const field of descriptor.fields) {
85
+ if (field.type === 'checkbox-group') {
86
+ if (field.defaultValues?.length) inputs[field.key] = [...field.defaultValues]
87
+ } else if (field.type === 'checkbox') {
88
+ if (field.default === 'true') inputs[field.key] = true
89
+ } else if (field.type === 'number') {
90
+ const parsed = Number(field.default)
91
+ if (field.default !== undefined && field.default !== '' && Number.isFinite(parsed)) {
92
+ inputs[field.key] = parsed
93
+ }
94
+ } else if (field.default) {
95
+ inputs[field.key] = field.default
96
+ }
97
+ }
98
+ return inputs
99
+ }
100
+
71
101
  /** Item statuses that count as settled — mirrors the backend terminal-status set. */
72
102
  const SETTLED: ReadonlySet<InitiativeItemStatus> = new Set(['done', 'skipped'])
73
103
 
@@ -3451,10 +3451,12 @@
3451
3451
  "create": {
3452
3452
  "title": "Initiative erstellen",
3453
3453
  "inFrame": "Neue Initiative in {frame}",
3454
+ "preset": "Typ",
3454
3455
  "titleField": "Titel",
3455
3456
  "titlePlaceholder": "z. B. Die API auf das neue Auth-Modell migrieren",
3456
3457
  "goalField": "Ziel",
3457
3458
  "goalPlaceholder": "Beschreiben Sie das Ziel, die Einschränkungen und den groben Umfang. Der Planer verfeinert dies zu einem mehrphasigen Plan.",
3459
+ "pathInvalid": "Geben Sie einen Pfad innerhalb des Repositorys an (kein \"..\", keine absoluten Pfade und keine Backslashes).",
3458
3460
  "hint": "Es läuft noch nichts: Führen Sie nach dem Erstellen die Initiative-Planning-Pipeline auf dem Block aus. Sie analysiert die Codebasis und entwirft den mehrphasigen Plan zu Ihrer Freigabe.",
3459
3461
  "submit": "Initiative erstellen",
3460
3462
  "failedTitle": "Die Initiative konnte nicht erstellt werden"
@@ -4397,10 +4397,12 @@
4397
4397
  "create": {
4398
4398
  "title": "Create initiative",
4399
4399
  "inFrame": "New initiative in {frame}",
4400
+ "preset": "Type",
4400
4401
  "titleField": "Title",
4401
4402
  "titlePlaceholder": "e.g. Migrate the API to the new auth model",
4402
4403
  "goalField": "Goal",
4403
4404
  "goalPlaceholder": "Describe the goal, constraints and rough scope. The planner refines this into a multi-phase plan.",
4405
+ "pathInvalid": "Enter a path inside the repository (no \"..\", absolute paths, or backslashes).",
4404
4406
  "hint": "Nothing runs yet: after creating, run the Initiative Planning pipeline on the block. It analyses the codebase and drafts the multi-phase plan for your approval.",
4405
4407
  "submit": "Create initiative",
4406
4408
  "failedTitle": "Could not create the initiative"
@@ -4268,10 +4268,12 @@
4268
4268
  "create": {
4269
4269
  "title": "Crear iniciativa",
4270
4270
  "inFrame": "Nueva iniciativa en {frame}",
4271
+ "preset": "Tipo",
4271
4272
  "titleField": "Titulo",
4272
4273
  "titlePlaceholder": "p. ej. Migrar la API al nuevo modelo de autenticacion",
4273
4274
  "goalField": "Objetivo",
4274
4275
  "goalPlaceholder": "Describe el objetivo, las restricciones y el alcance aproximado. El planificador lo refina en un plan multifase.",
4276
+ "pathInvalid": "Introduce una ruta dentro del repositorio (sin \"..\", rutas absolutas ni barras invertidas).",
4275
4277
  "hint": "Todavia no se ejecuta nada: tras crearla, ejecuta el pipeline de planificacion de iniciativas sobre el bloque. Analiza el codigo y redacta el plan multifase para tu aprobacion.",
4276
4278
  "submit": "Crear iniciativa",
4277
4279
  "failedTitle": "No se pudo crear la iniciativa"
@@ -4268,10 +4268,12 @@
4268
4268
  "create": {
4269
4269
  "title": "Creer une initiative",
4270
4270
  "inFrame": "Nouvelle initiative dans {frame}",
4271
+ "preset": "Type",
4271
4272
  "titleField": "Titre",
4272
4273
  "titlePlaceholder": "p. ex. Migrer l'API vers le nouveau modele d'authentification",
4273
4274
  "goalField": "Objectif",
4274
4275
  "goalPlaceholder": "Decrivez l'objectif, les contraintes et le perimetre approximatif. Le planificateur l'affine en un plan multiphase.",
4276
+ "pathInvalid": "Saisissez un chemin a l'interieur du depot (pas de \"..\", de chemins absolus ni d'antislashs).",
4275
4277
  "hint": "Rien ne s'execute encore : apres la creation, lancez le pipeline de planification d'initiative sur le bloc. Il analyse le code et redige le plan multiphase pour votre approbation.",
4276
4278
  "submit": "Creer l'initiative",
4277
4279
  "failedTitle": "Impossible de creer l'initiative"
@@ -4279,10 +4279,12 @@
4279
4279
  "create": {
4280
4280
  "title": "יצירת יוזמה",
4281
4281
  "inFrame": "יוזמה חדשה ב-{frame}",
4282
+ "preset": "סוג",
4282
4283
  "titleField": "כותרת",
4283
4284
  "titlePlaceholder": "לדוגמה: העברת ה-API למודל האימות החדש",
4284
4285
  "goalField": "מטרה",
4285
4286
  "goalPlaceholder": "תארו את המטרה, המגבלות והיקף משוער. המתכנן מזקק זאת לתוכנית רב-שלבית.",
4287
+ "pathInvalid": "הזינו נתיב בתוך המאגר (ללא \"..\", נתיבים מוחלטים או קו נטוי הפוך).",
4286
4288
  "hint": "שום דבר לא רץ עדיין: לאחר היצירה, הריצו את צינור תכנון היוזמה על הבלוק. הוא מנתח את הקוד ומנסח את התוכנית הרב-שלבית לאישורכם.",
4287
4289
  "submit": "יצירת יוזמה",
4288
4290
  "failedTitle": "לא ניתן היה ליצור את היוזמה"
@@ -3451,10 +3451,12 @@
3451
3451
  "create": {
3452
3452
  "title": "Crea iniziativa",
3453
3453
  "inFrame": "Nuova iniziativa in {frame}",
3454
+ "preset": "Tipo",
3454
3455
  "titleField": "Titolo",
3455
3456
  "titlePlaceholder": "es. Migrare l'API al nuovo modello di autenticazione",
3456
3457
  "goalField": "Obiettivo",
3457
3458
  "goalPlaceholder": "Descrivi l'obiettivo, i vincoli e l'ambito approssimativo. Il pianificatore lo affina in un piano multi-fase.",
3459
+ "pathInvalid": "Inserisci un percorso all'interno del repository (senza \"..\", percorsi assoluti o barre rovesciate).",
3458
3460
  "hint": "Non viene eseguito ancora nulla: dopo la creazione, esegui la pipeline di Pianificazione dell'iniziativa sul blocco. Analizza il codebase e redige il piano multi-fase per la tua approvazione.",
3459
3461
  "submit": "Crea iniziativa",
3460
3462
  "failedTitle": "Impossibile creare l'iniziativa"
@@ -4280,10 +4280,12 @@
4280
4280
  "create": {
4281
4281
  "title": "イニシアチブを作成",
4282
4282
  "inFrame": "{frame} の新しいイニシアチブ",
4283
+ "preset": "種類",
4283
4284
  "titleField": "タイトル",
4284
4285
  "titlePlaceholder": "例: API を新しい認証モデルへ移行する",
4285
4286
  "goalField": "ゴール",
4286
4287
  "goalPlaceholder": "ゴール、制約、おおまかなスコープを記述してください。プランナーが複数フェーズの計画に練り上げます。",
4288
+ "pathInvalid": "リポジトリ内のパスを入力してください(\"..\"、絶対パス、バックスラッシュは使用できません)。",
4287
4289
  "hint": "この時点では何も実行されません。作成後、このブロックでイニシアチブ計画パイプラインを実行してください。コードベースを分析し、承認用の複数フェーズ計画を起草します。",
4288
4290
  "submit": "イニシアチブを作成",
4289
4291
  "failedTitle": "イニシアチブを作成できませんでした"
@@ -4268,10 +4268,12 @@
4268
4268
  "create": {
4269
4269
  "title": "Utworz inicjatywe",
4270
4270
  "inFrame": "Nowa inicjatywa w {frame}",
4271
+ "preset": "Typ",
4271
4272
  "titleField": "Tytul",
4272
4273
  "titlePlaceholder": "np. Migracja API do nowego modelu uwierzytelniania",
4273
4274
  "goalField": "Cel",
4274
4275
  "goalPlaceholder": "Opisz cel, ograniczenia i przyblizony zakres. Planista doprecyzuje to w wielofazowy plan.",
4276
+ "pathInvalid": "Podaj sciezke wewnatrz repozytorium (bez \"..\", sciezek bezwzglednych ani ukosnikow wstecznych).",
4275
4277
  "hint": "Nic jeszcze nie jest uruchamiane: po utworzeniu uruchom na bloku pipeline planowania inicjatywy. Analizuje on kod i przygotowuje wielofazowy plan do zatwierdzenia.",
4276
4278
  "submit": "Utworz inicjatywe",
4277
4279
  "failedTitle": "Nie udalo sie utworzyc inicjatywy"
@@ -4280,10 +4280,12 @@
4280
4280
  "create": {
4281
4281
  "title": "Girisim olustur",
4282
4282
  "inFrame": "{frame} icinde yeni girisim",
4283
+ "preset": "Tur",
4283
4284
  "titleField": "Baslik",
4284
4285
  "titlePlaceholder": "orn. API'yi yeni kimlik dogrulama modeline tasi",
4285
4286
  "goalField": "Hedef",
4286
4287
  "goalPlaceholder": "Hedefi, kisitlari ve yaklasik kapsami aciklayin. Planlayici bunu cok asamali bir plana donusturur.",
4288
+ "pathInvalid": "Depo icinde bir yol girin (\"..\", mutlak yollar veya ters egik cizgi olmadan).",
4287
4289
  "hint": "Henuz hicbir sey calismiyor: olusturduktan sonra blok uzerinde Girisim Planlama hattini calistirin. Kod tabanini analiz eder ve onayiniz icin cok asamali plani taslak olarak hazirlar.",
4288
4290
  "submit": "Girisim olustur",
4289
4291
  "failedTitle": "Girisim olusturulamadi"
@@ -4268,10 +4268,12 @@
4268
4268
  "create": {
4269
4269
  "title": "Створити ініціативу",
4270
4270
  "inFrame": "Нова ініціатива в {frame}",
4271
+ "preset": "Тип",
4271
4272
  "titleField": "Назва",
4272
4273
  "titlePlaceholder": "напр. Перенести API на нову модель автентифікації",
4273
4274
  "goalField": "Мета",
4274
4275
  "goalPlaceholder": "Опишіть мету, обмеження та приблизний обсяг. Планувальник перетворить це на багатофазний план.",
4276
+ "pathInvalid": "Введіть шлях усередині репозиторію (без \"..\", абсолютних шляхів або зворотних скісних рисок).",
4275
4277
  "hint": "Поки що нічого не запускається: після створення запустіть на блоці пайплайн планування ініціативи. Він аналізує кодову базу та готує багатофазний план на ваше затвердження.",
4276
4278
  "submit": "Створити ініціативу",
4277
4279
  "failedTitle": "Не вдалося створити ініціативу"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.106.0",
3
+ "version": "0.107.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.116.0"
37
+ "@cat-factory/contracts": "0.116.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",