@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.
- package/app/components/board/CreateInitiativeModal.vue +140 -12
- package/app/components/board/InitiativePresetFields.vue +155 -0
- package/app/composables/api/initiative.ts +22 -2
- package/app/stores/initiative.ts +39 -4
- package/app/types/initiative.ts +3 -0
- package/app/utils/initiative.ts +30 -0
- package/i18n/locales/de.json +2 -0
- package/i18n/locales/en.json +2 -0
- package/i18n/locales/es.json +2 -0
- package/i18n/locales/fr.json +2 -0
- package/i18n/locales/he.json +2 -0
- package/i18n/locales/it.json +2 -0
- package/i18n/locales/ja.json +2 -0
- package/i18n/locales/pl.json +2 -0
- package/i18n/locales/tr.json +2 -0
- package/i18n/locales/uk.json +2 -0
- package/package.json +2 -2
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// Create a new INITIATIVE under a service frame — the longer-running counterpart
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 || !
|
|
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="!
|
|
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: {
|
|
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
|
|
package/app/stores/initiative.ts
CHANGED
|
@@ -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
|
-
/**
|
|
103
|
-
|
|
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,
|
package/app/types/initiative.ts
CHANGED
package/app/utils/initiative.ts
CHANGED
|
@@ -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
|
|
package/i18n/locales/de.json
CHANGED
|
@@ -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"
|
package/i18n/locales/en.json
CHANGED
|
@@ -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"
|
package/i18n/locales/es.json
CHANGED
|
@@ -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"
|
package/i18n/locales/fr.json
CHANGED
|
@@ -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"
|
package/i18n/locales/he.json
CHANGED
|
@@ -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": "לא ניתן היה ליצור את היוזמה"
|
package/i18n/locales/it.json
CHANGED
|
@@ -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"
|
package/i18n/locales/ja.json
CHANGED
|
@@ -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": "イニシアチブを作成できませんでした"
|
package/i18n/locales/pl.json
CHANGED
|
@@ -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"
|
package/i18n/locales/tr.json
CHANGED
|
@@ -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"
|
package/i18n/locales/uk.json
CHANGED
|
@@ -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.
|
|
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.
|
|
37
|
+
"@cat-factory/contracts": "0.116.1"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@toad-contracts/testing": "0.3.2",
|