@cat-factory/app 0.152.0 → 0.153.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/panels/AppOverlayHost.logic.spec.ts +38 -0
- package/app/components/panels/AppOverlayHost.logic.ts +24 -0
- package/app/components/panels/AppOverlayHost.vue +57 -0
- package/app/composables/useAppOverlays.ts +26 -0
- package/app/docs/consumer-extensions.md +29 -0
- package/app/modular/nav-contributions.spec.ts +1 -0
- package/app/modular/registry.spec.ts +23 -0
- package/app/modular/registry.ts +1 -0
- package/app/modular/slots.ts +21 -0
- package/app/pages/index.vue +4 -0
- package/app/plugins/modular.client.ts +6 -1
- package/app/stores/environmentWizard/context.ts +68 -0
- package/app/stores/environmentWizard/flow.ts +143 -0
- package/app/stores/environmentWizard/recipe.ts +63 -0
- package/app/stores/environmentWizard/save.ts +132 -0
- package/app/stores/environmentWizard.ts +46 -278
- package/app/stores/pipelines/context.ts +103 -0
- package/app/stores/pipelines/draftActions.ts +307 -0
- package/app/stores/pipelines/persistence.ts +136 -0
- package/app/stores/pipelines.ts +37 -458
- package/app/stores/ui/modals.ts +29 -0
- package/app/stores/ui/overlays.spec.ts +43 -0
- package/package.json +1 -1
package/app/stores/pipelines.ts
CHANGED
|
@@ -1,81 +1,11 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
5
|
-
import type { PipelinePurpose, StepOptions, TesterQualityConfig } from '@cat-factory/contracts'
|
|
6
|
-
import { companionForProducer, uid } from '~/utils/catalog'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { Pipeline } from '~/types/domain'
|
|
4
|
+
import type { PipelinePurpose } from '@cat-factory/contracts'
|
|
7
5
|
import { useUpsertList } from '~/composables/useUpsertList'
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function defaultConsensusConfig(): ConsensusStepConfig {
|
|
12
|
-
return {
|
|
13
|
-
enabled: true,
|
|
14
|
-
strategy: 'specialist-panel',
|
|
15
|
-
participants: [
|
|
16
|
-
{ id: uid('cp'), role: 'Pragmatist', systemFraming: 'Favour the simplest viable approach.' },
|
|
17
|
-
{
|
|
18
|
-
id: uid('cp'),
|
|
19
|
-
role: 'Skeptic',
|
|
20
|
-
systemFraming: 'Probe risks, edge cases and failure modes.',
|
|
21
|
-
},
|
|
22
|
-
],
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* The per-step, index-aligned draft arrays. Every one is kept the same length as `draft` and
|
|
28
|
-
* spliced/reordered in lockstep (see `insertAt` / `removeFromDraft` / `moveUnit`), so grouping
|
|
29
|
-
* their construction keeps that alignment contract — and its documentation — in one place.
|
|
30
|
-
*/
|
|
31
|
-
function createDraftStepState() {
|
|
32
|
-
/** The chain currently being assembled in the builder. */
|
|
33
|
-
const draft = ref<AgentKind[]>([])
|
|
34
|
-
/** Per-step approval gates, kept index-aligned with `draft`. */
|
|
35
|
-
const draftGates = ref<boolean[]>([])
|
|
36
|
-
/** Per-step enable flags, kept index-aligned with `draft` (false ⇒ skipped at run). */
|
|
37
|
-
const draftEnabled = ref<boolean[]>([])
|
|
38
|
-
/**
|
|
39
|
-
* Per-step companion thresholds, kept index-aligned with `draft`. Not editable in the
|
|
40
|
-
* builder UI today, but carried through edits so an existing pipeline's thresholds
|
|
41
|
-
* survive a save.
|
|
42
|
-
*/
|
|
43
|
-
const draftThresholds = ref<(number | null)[]>([])
|
|
44
|
-
/** Per-step consensus configs, kept index-aligned with `draft` (null ⇒ standard agent). */
|
|
45
|
-
const draftConsensus = ref<(ConsensusStepConfig | null)[]>([])
|
|
46
|
-
/** Per-step estimate gating, kept index-aligned with `draft` (null ⇒ always run). */
|
|
47
|
-
const draftGating = ref<(StepGating | null)[]>([])
|
|
48
|
-
/**
|
|
49
|
-
* Per-step Follow-up companion toggle, kept index-aligned with `draft`. Only meaningful on
|
|
50
|
-
* a `coder` step; `false` disables the companion there (default/true ⇒ enabled).
|
|
51
|
-
*/
|
|
52
|
-
const draftFollowUps = ref<(boolean | null)[]>([])
|
|
53
|
-
/**
|
|
54
|
-
* Per-step test quality-control companion config, kept index-aligned with `draft`. Only
|
|
55
|
-
* meaningful on a Tester step (`tester-api`/`tester-ui`); `null`/absent means "enabled, no
|
|
56
|
-
* gating" (the QC companion is on by default), `{ enabled: false }` disables it, and an
|
|
57
|
-
* entry with `gating` makes it conditional on the task estimate.
|
|
58
|
-
*/
|
|
59
|
-
const draftTesterQuality = ref<(TesterQualityConfig | null)[]>([])
|
|
60
|
-
/**
|
|
61
|
-
* Per-step options bag, kept index-aligned with `draft`: the extensible home for new per-step
|
|
62
|
-
* parameters (see `StepOptions`). `null`/absent per step ⇒ that step's defaults. Today the only
|
|
63
|
-
* field is `autoRecommend` (requirements-review); by convention we store ONLY deviations from a
|
|
64
|
-
* default, so an entry exists only when a step opts out of something.
|
|
65
|
-
*/
|
|
66
|
-
const draftStepOptions = ref<(StepOptions | null)[]>([])
|
|
67
|
-
return {
|
|
68
|
-
draft,
|
|
69
|
-
draftGates,
|
|
70
|
-
draftEnabled,
|
|
71
|
-
draftThresholds,
|
|
72
|
-
draftConsensus,
|
|
73
|
-
draftGating,
|
|
74
|
-
draftFollowUps,
|
|
75
|
-
draftTesterQuality,
|
|
76
|
-
draftStepOptions,
|
|
77
|
-
}
|
|
78
|
-
}
|
|
6
|
+
import { createDraftStepState, type PipelinesContext } from '~/stores/pipelines/context'
|
|
7
|
+
import { createPipelineDraftActions } from '~/stores/pipelines/draftActions'
|
|
8
|
+
import { createPipelinePersistence } from '~/stores/pipelines/persistence'
|
|
79
9
|
|
|
80
10
|
/**
|
|
81
11
|
* Saved, reusable pipelines (the pipeline palette) plus the in-progress draft
|
|
@@ -83,6 +13,11 @@ function createDraftStepState() {
|
|
|
83
13
|
* the draft is transient client state. The draft doubles as the EDIT surface: a
|
|
84
14
|
* custom pipeline can be loaded into it (`loadForEdit`) and saved back in place,
|
|
85
15
|
* while a built-in is cloned first (`clonePipeline`) into an editable copy.
|
|
16
|
+
*
|
|
17
|
+
* The draft manipulation + persistence operations live in cohesive factories
|
|
18
|
+
* ({@link createPipelineDraftActions} / {@link createPipelinePersistence}, under
|
|
19
|
+
* `stores/pipelines/`) that close over the shared state assembled here — a size-only
|
|
20
|
+
* split mirroring `stores/board/`, not a new seam.
|
|
86
21
|
*/
|
|
87
22
|
export const usePipelinesStore = defineStore('pipelines', () => {
|
|
88
23
|
const api = useApi()
|
|
@@ -135,360 +70,31 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
135
70
|
return pipelines.value.find((p) => p.id === id)
|
|
136
71
|
}
|
|
137
72
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
draftConsensus.value.splice(index, 1)
|
|
161
|
-
draftGating.value.splice(index, 1)
|
|
162
|
-
draftFollowUps.value.splice(index, 1)
|
|
163
|
-
draftTesterQuality.value.splice(index, 1)
|
|
164
|
-
draftStepOptions.value.splice(index, 1)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function moveInDraft(from: number, to: number) {
|
|
168
|
-
if (to < 0 || to >= draft.value.length) return
|
|
169
|
-
const [item] = draft.value.splice(from, 1)
|
|
170
|
-
if (item) draft.value.splice(to, 0, item)
|
|
171
|
-
const [gate] = draftGates.value.splice(from, 1)
|
|
172
|
-
draftGates.value.splice(to, 0, gate ?? false)
|
|
173
|
-
const [on] = draftEnabled.value.splice(from, 1)
|
|
174
|
-
draftEnabled.value.splice(to, 0, on ?? true)
|
|
175
|
-
const [th] = draftThresholds.value.splice(from, 1)
|
|
176
|
-
draftThresholds.value.splice(to, 0, th ?? null)
|
|
177
|
-
const [cons] = draftConsensus.value.splice(from, 1)
|
|
178
|
-
draftConsensus.value.splice(to, 0, cons ?? null)
|
|
179
|
-
const [gat] = draftGating.value.splice(from, 1)
|
|
180
|
-
draftGating.value.splice(to, 0, gat ?? null)
|
|
181
|
-
const [fu] = draftFollowUps.value.splice(from, 1)
|
|
182
|
-
draftFollowUps.value.splice(to, 0, fu ?? null)
|
|
183
|
-
const [tq] = draftTesterQuality.value.splice(from, 1)
|
|
184
|
-
draftTesterQuality.value.splice(to, 0, tq ?? null)
|
|
185
|
-
const [so] = draftStepOptions.value.splice(from, 1)
|
|
186
|
-
draftStepOptions.value.splice(to, 0, so ?? null)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Whether the producer step at `index` currently has its companion attached after it. */
|
|
190
|
-
function hasCompanion(index: number): boolean {
|
|
191
|
-
const companion = companionForProducer(draft.value[index] ?? '')
|
|
192
|
-
return companion !== undefined && draft.value[index + 1] === companion
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Toggle the dependent companion on the producer step at `index`: insert it immediately
|
|
197
|
-
* after (turn on) or remove it (turn off). A no-op for a kind that has no companion.
|
|
198
|
-
*/
|
|
199
|
-
function toggleCompanion(index: number) {
|
|
200
|
-
const companion = companionForProducer(draft.value[index] ?? '')
|
|
201
|
-
if (!companion) return
|
|
202
|
-
if (draft.value[index + 1] === companion) removeFromDraft(index + 1)
|
|
203
|
-
else insertAt(index + 1, companion)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Toggle estimate gating on/off for the (companion) step at `index`. */
|
|
207
|
-
function toggleDraftGating(index: number) {
|
|
208
|
-
draftGating.value[index] = draftGating.value[index]?.enabled
|
|
209
|
-
? null
|
|
210
|
-
: { enabled: true, minRisk: 0.5, minImpact: 0.5, onMissingEstimate: 'run' }
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* The draft as a list of "units" for rendering: each step is one unit, EXCEPT a companion
|
|
215
|
-
* that sits immediately after its producer — that companion is folded into the producer's
|
|
216
|
-
* unit (`companionIndex`) and surfaced as a toggle on it, not a standalone row. The backend
|
|
217
|
-
* now REJECTS a companion that is not immediately after its producer (strict adjacency in
|
|
218
|
-
* `validatePipelineShape`), so a saved pipeline never has one — but a stray companion that
|
|
219
|
-
* still shows up in the draft (e.g. a pre-existing pipeline saved before adjacency was
|
|
220
|
-
* enforced) is emitted as its own standalone unit so it stays visible and removable/
|
|
221
|
-
* reorderable into a valid shape rather than being silently dropped — and, crucially, so
|
|
222
|
-
* every `draft` index belongs to exactly one unit, which is what lets {@link moveUnit}
|
|
223
|
-
* reorder by unit boundaries without ever dropping a step.
|
|
224
|
-
* `index`/`companionIndex` are positions in the raw `draft` arrays.
|
|
225
|
-
*/
|
|
226
|
-
const units = computed(() => {
|
|
227
|
-
const out: { index: number; kind: AgentKind; companionIndex: number | null }[] = []
|
|
228
|
-
let folded = -1 // draft index already consumed as the previous unit's adjacent companion
|
|
229
|
-
for (let i = 0; i < draft.value.length; i++) {
|
|
230
|
-
const kind = draft.value[i]
|
|
231
|
-
if (kind === undefined || i === folded) continue
|
|
232
|
-
const companion = companionForProducer(kind)
|
|
233
|
-
const companionIndex = companion && draft.value[i + 1] === companion ? i + 1 : null
|
|
234
|
-
if (companionIndex !== null) folded = companionIndex
|
|
235
|
-
out.push({ index: i, kind, companionIndex })
|
|
236
|
-
}
|
|
237
|
-
return out
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Move the unit at visible position `from` to `to`, carrying its attached companion. Rebuilds
|
|
242
|
-
* every parallel array by the SAME unit boundaries so they stay index-aligned.
|
|
243
|
-
*/
|
|
244
|
-
function moveUnit(from: number, to: number) {
|
|
245
|
-
const u = units.value
|
|
246
|
-
if (to < 0 || to >= u.length || from === to) return
|
|
247
|
-
const reorder = <T>(arr: T[]): T[] => {
|
|
248
|
-
const chunks = u.map((unit) =>
|
|
249
|
-
arr.slice(unit.index, unit.index + (unit.companionIndex !== null ? 2 : 1)),
|
|
250
|
-
)
|
|
251
|
-
const [moved] = chunks.splice(from, 1)
|
|
252
|
-
if (moved) chunks.splice(to, 0, moved)
|
|
253
|
-
return chunks.flat()
|
|
254
|
-
}
|
|
255
|
-
draft.value = reorder(draft.value)
|
|
256
|
-
draftGates.value = reorder(draftGates.value)
|
|
257
|
-
draftEnabled.value = reorder(draftEnabled.value)
|
|
258
|
-
draftThresholds.value = reorder(draftThresholds.value)
|
|
259
|
-
draftConsensus.value = reorder(draftConsensus.value)
|
|
260
|
-
draftGating.value = reorder(draftGating.value)
|
|
261
|
-
draftFollowUps.value = reorder(draftFollowUps.value)
|
|
262
|
-
draftTesterQuality.value = reorder(draftTesterQuality.value)
|
|
263
|
-
draftStepOptions.value = reorder(draftStepOptions.value)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/** Toggle the consensus mechanism on the draft step at `index` (default config / off). */
|
|
267
|
-
function toggleDraftConsensus(index: number) {
|
|
268
|
-
draftConsensus.value[index] = draftConsensus.value[index] ? null : defaultConsensusConfig()
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/** Replace the consensus config of the draft step at `index` (builder editor edits). */
|
|
272
|
-
function setDraftConsensus(index: number, config: ConsensusStepConfig | null) {
|
|
273
|
-
draftConsensus.value[index] = config
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/** Toggle the approval gate on the draft step at `index`. */
|
|
277
|
-
function toggleDraftGate(index: number) {
|
|
278
|
-
draftGates.value[index] = !draftGates.value[index]
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/** Toggle the Follow-up companion on the draft (coder) step at `index` (default on → off). */
|
|
282
|
-
function toggleDraftFollowUps(index: number) {
|
|
283
|
-
// Default (null/true) is enabled, so the first toggle disables it (false); toggle back to null.
|
|
284
|
-
draftFollowUps.value[index] = draftFollowUps.value[index] === false ? null : false
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Toggle the test quality-control companion on the draft (Tester) step at `index`. The
|
|
289
|
-
* companion is enabled by default (a `null` entry), so the first toggle disables it
|
|
290
|
-
* (`{ enabled: false }`, dropping any gating) and the next restores the default.
|
|
291
|
-
*/
|
|
292
|
-
function toggleDraftTesterQuality(index: number) {
|
|
293
|
-
draftTesterQuality.value[index] =
|
|
294
|
-
draftTesterQuality.value[index]?.enabled === false ? null : { enabled: false }
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Toggle estimate gating on/off for the QC companion on the draft (Tester) step at `index`.
|
|
299
|
-
* A no-op while the companion is disabled (nothing to gate). Enabling gating pins the config
|
|
300
|
-
* to `{ enabled: true, gating }` so the thresholds are editable; disabling drops back to the
|
|
301
|
-
* default `null` (enabled, ungated).
|
|
302
|
-
*/
|
|
303
|
-
function toggleDraftTesterQualityGating(index: number) {
|
|
304
|
-
const cur = draftTesterQuality.value[index]
|
|
305
|
-
if (cur?.enabled === false) return
|
|
306
|
-
draftTesterQuality.value[index] = cur?.gating?.enabled
|
|
307
|
-
? null
|
|
308
|
-
: {
|
|
309
|
-
enabled: true,
|
|
310
|
-
gating: { enabled: true, minRisk: 0.5, minImpact: 0.5, onMissingEstimate: 'run' },
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** Enable/disable the draft step at `index` without removing it. */
|
|
315
|
-
function toggleDraftEnabled(index: number) {
|
|
316
|
-
draftEnabled.value[index] = draftEnabled.value[index] === false
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** Whether auto-recommendation is on for the draft (requirements-review) step at `index`. */
|
|
320
|
-
function draftAutoRecommendEnabled(index: number): boolean {
|
|
321
|
-
return draftStepOptions.value[index]?.autoRecommend !== false
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Toggle the requirements-review auto-recommendation on the draft step at `index`. It is on by
|
|
326
|
-
* default, so we store ONLY the opt-out (`{ autoRecommend: false }`); toggling back drops the
|
|
327
|
-
* flag. Merges with any other future StepOptions fields rather than clobbering the whole bag.
|
|
328
|
-
*/
|
|
329
|
-
function toggleDraftAutoRecommend(index: number) {
|
|
330
|
-
const next: StepOptions = { ...draftStepOptions.value[index] }
|
|
331
|
-
if (draftAutoRecommendEnabled(index)) next.autoRecommend = false
|
|
332
|
-
else delete next.autoRecommend
|
|
333
|
-
draftStepOptions.value[index] = Object.keys(next).length ? next : null
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/** The skill picked for the draft `skill` step at `index` (its `stepOptions.skillId`). */
|
|
337
|
-
function draftSkillId(index: number): string | undefined {
|
|
338
|
-
return draftStepOptions.value[index]?.skillId
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Set (or clear) the picked skill on the draft `skill` step at `index`. Merges into the
|
|
343
|
-
* step's `StepOptions` bag rather than clobbering it; clearing drops the field and, if the
|
|
344
|
-
* bag empties, the whole entry (so it normalizes away like the other options).
|
|
345
|
-
*/
|
|
346
|
-
function setDraftSkillId(index: number, skillId: string | undefined) {
|
|
347
|
-
const next: StepOptions = { ...draftStepOptions.value[index] }
|
|
348
|
-
if (skillId) next.skillId = skillId
|
|
349
|
-
else delete next.skillId
|
|
350
|
-
draftStepOptions.value[index] = Object.keys(next).length ? next : null
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function clearDraft() {
|
|
354
|
-
draft.value = []
|
|
355
|
-
draftGates.value = []
|
|
356
|
-
draftEnabled.value = []
|
|
357
|
-
draftThresholds.value = []
|
|
358
|
-
draftConsensus.value = []
|
|
359
|
-
draftGating.value = []
|
|
360
|
-
draftFollowUps.value = []
|
|
361
|
-
draftTesterQuality.value = []
|
|
362
|
-
draftStepOptions.value = []
|
|
363
|
-
draftLabels.value = []
|
|
364
|
-
draftPurpose.value = null
|
|
365
|
-
draftName.value = 'New pipeline'
|
|
366
|
-
draftDescription.value = ''
|
|
367
|
-
editingId.value = null
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** Load an existing (custom) pipeline into the draft so it can be edited in place. */
|
|
371
|
-
function loadForEdit(pipeline: Pipeline) {
|
|
372
|
-
draft.value = [...pipeline.agentKinds]
|
|
373
|
-
draftGates.value = pipeline.agentKinds.map((_, i) => pipeline.gates?.[i] ?? false)
|
|
374
|
-
draftEnabled.value = pipeline.agentKinds.map((_, i) => pipeline.enabled?.[i] ?? true)
|
|
375
|
-
draftThresholds.value = pipeline.agentKinds.map((_, i) => pipeline.thresholds?.[i] ?? null)
|
|
376
|
-
draftConsensus.value = pipeline.agentKinds.map((_, i) => pipeline.consensus?.[i] ?? null)
|
|
377
|
-
draftGating.value = pipeline.agentKinds.map((_, i) => pipeline.gating?.[i] ?? null)
|
|
378
|
-
draftFollowUps.value = pipeline.agentKinds.map((_, i) => pipeline.followUps?.[i] ?? null)
|
|
379
|
-
draftTesterQuality.value = pipeline.agentKinds.map(
|
|
380
|
-
(_, i) => pipeline.testerQuality?.[i] ?? null,
|
|
381
|
-
)
|
|
382
|
-
draftStepOptions.value = pipeline.agentKinds.map((_, i) => pipeline.stepOptions?.[i] ?? null)
|
|
383
|
-
draftLabels.value = [...(pipeline.labels ?? [])]
|
|
384
|
-
draftPurpose.value = pipeline.purpose ?? null
|
|
385
|
-
draftName.value = pipeline.name
|
|
386
|
-
draftDescription.value = pipeline.description ?? ''
|
|
387
|
-
editingId.value = pipeline.id
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/** The optional arrays to send, omitting the ones that are at their defaults. */
|
|
391
|
-
function draftPayload() {
|
|
392
|
-
return {
|
|
393
|
-
name: draftName.value.trim() || 'Untitled pipeline',
|
|
394
|
-
// ALWAYS send description (like stepOptions) so an update can CLEAR it — an omitted field
|
|
395
|
-
// reads as "keep existing", so toggling the last of the text away must send the empty string.
|
|
396
|
-
// The backend trims + drops a blank one, so this is a no-op on create / an empty description.
|
|
397
|
-
description: draftDescription.value.trim(),
|
|
398
|
-
agentKinds: [...draft.value],
|
|
399
|
-
// Only send gates when at least one step is gated.
|
|
400
|
-
...(draftGates.value.some(Boolean) ? { gates: [...draftGates.value] } : {}),
|
|
401
|
-
// Only send enabled when at least one step is disabled (default is all-on).
|
|
402
|
-
...(draftEnabled.value.some((e) => e === false) ? { enabled: [...draftEnabled.value] } : {}),
|
|
403
|
-
// Only send thresholds when at least one step pins an explicit value.
|
|
404
|
-
...(draftThresholds.value.some((t) => t != null)
|
|
405
|
-
? { thresholds: [...draftThresholds.value] }
|
|
406
|
-
: {}),
|
|
407
|
-
// Only send consensus when at least one step is consensus-enabled.
|
|
408
|
-
...(draftConsensus.value.some((c) => c?.enabled)
|
|
409
|
-
? { consensus: [...draftConsensus.value] }
|
|
410
|
-
: {}),
|
|
411
|
-
// Only send gating when at least one step has gating enabled.
|
|
412
|
-
...(draftGating.value.some((g) => g?.enabled) ? { gating: [...draftGating.value] } : {}),
|
|
413
|
-
// Only send followUps when at least one step disables it (default is on, so only the
|
|
414
|
-
// explicit `false` opt-outs are worth persisting).
|
|
415
|
-
...(draftFollowUps.value.some((f) => f === false)
|
|
416
|
-
? { followUps: [...draftFollowUps.value] }
|
|
417
|
-
: {}),
|
|
418
|
-
// Only send testerQuality when at least one Tester step deviates from the default
|
|
419
|
-
// (companion disabled, or an estimate gate configured) — the default (null/enabled,
|
|
420
|
-
// ungated) is not worth persisting.
|
|
421
|
-
...(draftTesterQuality.value.some((q) => q?.enabled === false || q?.gating?.enabled)
|
|
422
|
-
? { testerQuality: [...draftTesterQuality.value] }
|
|
423
|
-
: {}),
|
|
424
|
-
// ALWAYS send stepOptions, unlike the legacy per-step arrays above. Those omit-when-default,
|
|
425
|
-
// which means an update can never CLEAR them (an omitted field reads as "keep existing"), so
|
|
426
|
-
// toggling the last opt-out back to its default on a saved pipeline would silently not
|
|
427
|
-
// persist. Sending the aligned array always lets `update` overwrite; the backend normalizes
|
|
428
|
-
// an all-default array away (stores nothing), so this is a no-op on create / all-default.
|
|
429
|
-
stepOptions: draftStepOptions.value.map((o) => o ?? null),
|
|
430
|
-
// Only send labels when there are any.
|
|
431
|
-
...(draftLabels.value.length ? { labels: [...draftLabels.value] } : {}),
|
|
432
|
-
// Only send purpose when the pipeline is classified (null ⇒ leave unclassified). Like the
|
|
433
|
-
// legacy per-step arrays, an omitted `purpose` on update reads as "keep existing"; clearing
|
|
434
|
-
// a classification back to unclassified is not a supported edit (every built-in ships one).
|
|
435
|
-
...(draftPurpose.value ? { purpose: draftPurpose.value } : {}),
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/** Persist the draft: update the pipeline being edited, else create a new one. */
|
|
440
|
-
async function saveDraft(): Promise<Pipeline | null> {
|
|
441
|
-
if (draft.value.length === 0) return null
|
|
442
|
-
const wsId = useWorkspaceStore().requireId()
|
|
443
|
-
const payload = draftPayload()
|
|
444
|
-
if (editingId.value) {
|
|
445
|
-
const updated = await api.updatePipeline(wsId, editingId.value, payload)
|
|
446
|
-
upsertPipeline(updated)
|
|
447
|
-
clearDraft()
|
|
448
|
-
return updated
|
|
449
|
-
}
|
|
450
|
-
const pipeline = await api.createPipeline(wsId, payload)
|
|
451
|
-
upsertPipeline(pipeline)
|
|
452
|
-
clearDraft()
|
|
453
|
-
return pipeline
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/** Clone any pipeline (built-in or custom) into an editable copy, ready to edit. */
|
|
457
|
-
async function clonePipeline(id: string): Promise<Pipeline> {
|
|
458
|
-
const clone = await api.clonePipeline(useWorkspaceStore().requireId(), id)
|
|
459
|
-
upsertPipeline(clone)
|
|
460
|
-
loadForEdit(clone)
|
|
461
|
-
return clone
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
async function removePipeline(id: string) {
|
|
465
|
-
await api.removePipeline(useWorkspaceStore().requireId(), id)
|
|
466
|
-
dropPipeline(id)
|
|
467
|
-
if (editingId.value === id) clearDraft()
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Reseed a built-in pipeline from the backend's current catalog definition: restores its
|
|
472
|
-
* canonical structure + version (adopting an update, or repairing a drifted/invalid copy)
|
|
473
|
-
* while preserving its labels/archive state. Replaces the pipeline in the cache.
|
|
474
|
-
*/
|
|
475
|
-
async function reseed(id: string): Promise<Pipeline> {
|
|
476
|
-
const updated = await api.reseedPipeline(useWorkspaceStore().requireId(), id)
|
|
477
|
-
upsertPipeline(updated)
|
|
478
|
-
if (editingId.value === id) clearDraft()
|
|
479
|
-
return updated
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/** Set a pipeline's organizational metadata (labels / archive). Works on built-ins too. */
|
|
483
|
-
async function organize(id: string, body: { labels?: string[]; archived?: boolean }) {
|
|
484
|
-
const updated = await api.organizePipeline(useWorkspaceStore().requireId(), id, body)
|
|
485
|
-
upsertPipeline(updated)
|
|
486
|
-
return updated
|
|
73
|
+
// The draft manipulation + persistence operations, split into cohesive factories sharing the
|
|
74
|
+
// state above (a size-only extraction — behaviour is identical to the former in-closure
|
|
75
|
+
// functions). Persistence drives the draft-lifecycle helpers (`clearDraft`/`loadForEdit`).
|
|
76
|
+
const context: PipelinesContext = {
|
|
77
|
+
api,
|
|
78
|
+
pipelines,
|
|
79
|
+
upsertPipeline,
|
|
80
|
+
dropPipeline,
|
|
81
|
+
draft,
|
|
82
|
+
draftGates,
|
|
83
|
+
draftEnabled,
|
|
84
|
+
draftThresholds,
|
|
85
|
+
draftConsensus,
|
|
86
|
+
draftGating,
|
|
87
|
+
draftFollowUps,
|
|
88
|
+
draftTesterQuality,
|
|
89
|
+
draftStepOptions,
|
|
90
|
+
draftLabels,
|
|
91
|
+
draftPurpose,
|
|
92
|
+
draftName,
|
|
93
|
+
draftDescription,
|
|
94
|
+
editingId,
|
|
487
95
|
}
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
const unarchive = (id: string) => organize(id, { archived: false })
|
|
491
|
-
const setLabels = (id: string, labels: string[]) => organize(id, { labels })
|
|
96
|
+
const draftActions = createPipelineDraftActions(context)
|
|
97
|
+
const persistence = createPipelinePersistence(context, draftActions)
|
|
492
98
|
|
|
493
99
|
return {
|
|
494
100
|
pipelines,
|
|
@@ -507,36 +113,9 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
507
113
|
draftName,
|
|
508
114
|
draftDescription,
|
|
509
115
|
editingId,
|
|
510
|
-
units,
|
|
511
116
|
hydrate,
|
|
512
117
|
getPipeline,
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
moveInDraft,
|
|
516
|
-
moveUnit,
|
|
517
|
-
hasCompanion,
|
|
518
|
-
toggleCompanion,
|
|
519
|
-
toggleDraftGating,
|
|
520
|
-
toggleDraftGate,
|
|
521
|
-
toggleDraftFollowUps,
|
|
522
|
-
toggleDraftTesterQuality,
|
|
523
|
-
toggleDraftTesterQualityGating,
|
|
524
|
-
draftAutoRecommendEnabled,
|
|
525
|
-
toggleDraftAutoRecommend,
|
|
526
|
-
draftSkillId,
|
|
527
|
-
setDraftSkillId,
|
|
528
|
-
toggleDraftEnabled,
|
|
529
|
-
toggleDraftConsensus,
|
|
530
|
-
setDraftConsensus,
|
|
531
|
-
clearDraft,
|
|
532
|
-
loadForEdit,
|
|
533
|
-
saveDraft,
|
|
534
|
-
clonePipeline,
|
|
535
|
-
removePipeline,
|
|
536
|
-
reseed,
|
|
537
|
-
organize,
|
|
538
|
-
archive,
|
|
539
|
-
unarchive,
|
|
540
|
-
setLabels,
|
|
118
|
+
...draftActions,
|
|
119
|
+
...persistence,
|
|
541
120
|
}
|
|
542
121
|
})
|
package/app/stores/ui/modals.ts
CHANGED
|
@@ -839,6 +839,33 @@ function createAiOnboardingModals() {
|
|
|
839
839
|
}
|
|
840
840
|
}
|
|
841
841
|
|
|
842
|
+
/**
|
|
843
|
+
* The generic CONSUMER-overlay host (extension slice D — the frontend-extension-mechanism
|
|
844
|
+
* initiative). Unlike every sub-slice above, this holds NO per-modal boolean: it is a single
|
|
845
|
+
* pick-one pointer to whichever consumer-contributed overlay (the `appOverlays` slot) is
|
|
846
|
+
* currently open, so a consumer nav item's `run` closure finally has something to open. A
|
|
847
|
+
* deployment registers `{ id: '<ns>:<name>', component }` in the `appOverlays` slot and calls
|
|
848
|
+
* `ui.openOverlay(id, subject?)` (usually via the auto-imported `useAppOverlays()` composable);
|
|
849
|
+
* the single `<AppOverlayHost>` in `pages/index.vue` resolves the slot and mounts the matching
|
|
850
|
+
* component, handing it the optional `subject`. First-party modals stay hand-mounted in
|
|
851
|
+
* `index.vue` — this seam is deliberately scoped to consumer extensions (strangler discipline).
|
|
852
|
+
*/
|
|
853
|
+
function createConsumerOverlayHost() {
|
|
854
|
+
// The active consumer overlay: its slot id + an optional opaque subject the overlay renders
|
|
855
|
+
// against (e.g. a block id). Null when no consumer overlay is open. Only ONE at a time —
|
|
856
|
+
// opening another replaces it (a pick-one host, like the result-view seam).
|
|
857
|
+
const activeOverlay = ref<{ id: string; subject?: unknown } | null>(null)
|
|
858
|
+
|
|
859
|
+
function openOverlay(id: string, subject?: unknown) {
|
|
860
|
+
activeOverlay.value = { id, subject }
|
|
861
|
+
}
|
|
862
|
+
function closeOverlay() {
|
|
863
|
+
activeOverlay.value = null
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return { activeOverlay, openOverlay, closeOverlay }
|
|
867
|
+
}
|
|
868
|
+
|
|
842
869
|
/**
|
|
843
870
|
* The modal / panel slice of the UI store: every open-close flag for the dozens of modals,
|
|
844
871
|
* panels and hubs (document + task import, bootstrap, integrations, workspace/account settings,
|
|
@@ -904,6 +931,7 @@ export function createUiModals() {
|
|
|
904
931
|
const misc = createMiscModals()
|
|
905
932
|
const documentsTasks = createDocumentTaskModals(resetHubReturn)
|
|
906
933
|
const overlays = createOverlayModals()
|
|
934
|
+
const consumerOverlays = createConsumerOverlayHost()
|
|
907
935
|
const panels = createIntegrationPanelModals(resetHubReturn)
|
|
908
936
|
const settings = createSettingsModals(resetHubReturn)
|
|
909
937
|
const infra = createInfraModals(resetHubReturn)
|
|
@@ -924,6 +952,7 @@ export function createUiModals() {
|
|
|
924
952
|
...misc,
|
|
925
953
|
...documentsTasks,
|
|
926
954
|
...overlays,
|
|
955
|
+
...consumerOverlays,
|
|
927
956
|
...panels,
|
|
928
957
|
...settings,
|
|
929
958
|
...infra,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { createUiModals } from '~/stores/ui/modals'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The consumer-overlay host slice (extension slice D — the frontend-extension-mechanism
|
|
6
|
+
* initiative). It is composed into `createUiModals`, so these tests drive it there directly
|
|
7
|
+
* (plain refs/functions, no Pinia). The pointer is pick-one: opening a second overlay replaces
|
|
8
|
+
* the first, and closing clears it — this is what `<AppOverlayHost>` reads to mount exactly one
|
|
9
|
+
* consumer overlay.
|
|
10
|
+
*/
|
|
11
|
+
describe('ui overlay host (slice D)', () => {
|
|
12
|
+
it('starts with no active overlay', () => {
|
|
13
|
+
const ui = createUiModals()
|
|
14
|
+
expect(ui.activeOverlay.value).toBeNull()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('openOverlay sets the active id + subject; closeOverlay clears it', () => {
|
|
18
|
+
const ui = createUiModals()
|
|
19
|
+
ui.openOverlay('acme:security-dashboard-overlay', { blockId: 'blk_1' })
|
|
20
|
+
expect(ui.activeOverlay.value).toEqual({
|
|
21
|
+
id: 'acme:security-dashboard-overlay',
|
|
22
|
+
subject: { blockId: 'blk_1' },
|
|
23
|
+
})
|
|
24
|
+
ui.closeOverlay()
|
|
25
|
+
expect(ui.activeOverlay.value).toBeNull()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('openOverlay without a subject leaves subject undefined', () => {
|
|
29
|
+
const ui = createUiModals()
|
|
30
|
+
ui.openOverlay('acme:security-dashboard-overlay')
|
|
31
|
+
expect(ui.activeOverlay.value).toEqual({
|
|
32
|
+
id: 'acme:security-dashboard-overlay',
|
|
33
|
+
subject: undefined,
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('is pick-one — opening a second overlay replaces the first', () => {
|
|
38
|
+
const ui = createUiModals()
|
|
39
|
+
ui.openOverlay('acme:one')
|
|
40
|
+
ui.openOverlay('acme:two', 42)
|
|
41
|
+
expect(ui.activeOverlay.value).toEqual({ id: 'acme:two', subject: 42 })
|
|
42
|
+
})
|
|
43
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.153.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",
|