@cat-factory/app 0.256.3 → 0.258.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/binaryCandidates/BinaryCandidatesWindow.vue +284 -0
- package/app/components/binaryOutput/BinaryOutputReport.vue +75 -0
- package/app/components/board/AddTaskModal.vue +38 -12
- package/app/components/board/RecurringPipelineModal.vue +24 -12
- package/app/components/board/nodes/TaskCard.vue +34 -7
- package/app/components/forkDecision/ForkDecisionWindow.vue +3 -1
- package/app/components/panels/AgentStepDetail.vue +42 -20
- package/app/components/panels/InspectorPanel.vue +39 -0
- package/app/components/panels/ResultWindowShell.logic.spec.ts +4 -0
- package/app/components/panels/inspector/TaskExecution.vue +34 -34
- package/app/components/pipeline/BinaryOutputStepPicker.logic.spec.ts +119 -1
- package/app/components/pipeline/BinaryOutputStepPicker.logic.ts +115 -1
- package/app/components/pipeline/BinaryOutputStepPicker.vue +463 -1
- package/app/components/pipeline/PipelineBuilder.vue +44 -0
- package/app/components/pipeline/PipelinePreview.vue +20 -1
- package/app/components/pipeline/PipelineProgress.vue +50 -0
- package/app/composables/api/binaryCandidates.ts +36 -0
- package/app/composables/api/execution.ts +19 -0
- package/app/composables/useApi.ts +2 -0
- package/app/composables/usePipelineHealth.spec.ts +42 -5
- package/app/composables/usePipelineHealth.ts +109 -48
- package/app/modular/agent-kinds.ts +5 -0
- package/app/modular/result-views.ts +4 -0
- package/app/stores/binaryCandidates.ts +89 -0
- package/app/stores/environmentWizard/context.ts +0 -2
- package/app/stores/environmentWizard/flow.ts +11 -6
- package/app/stores/environmentWizard.ts +12 -11
- package/app/stores/execution/commands.ts +26 -1
- package/app/stores/pipelines/draftActions.ts +2 -0
- package/app/stores/pipelines/draftStepConfig.ts +4 -161
- package/app/stores/pipelines/draftStepOptions.ts +204 -0
- package/app/stores/ui/resultViews.ts +8 -6
- package/app/stores/ui/runStepOpeners.ts +23 -1
- package/app/types/domain.ts +6 -0
- package/app/types/execution.ts +5 -0
- package/app/utils/agentPalette.spec.ts +26 -0
- package/app/utils/agentPalette.ts +9 -4
- package/app/utils/binaryCandidates.spec.ts +110 -0
- package/app/utils/binaryCandidates.ts +126 -0
- package/app/utils/binaryOutput.spec.ts +122 -1
- package/app/utils/binaryOutput.ts +149 -2
- package/app/utils/catalog.spec.ts +24 -0
- package/app/utils/catalog.ts +21 -4
- package/app/utils/pipeline.spec.ts +35 -3
- package/app/utils/pipeline.ts +54 -3
- package/app/utils/pipelineRender.spec.ts +122 -1
- package/app/utils/pipelineRender.ts +113 -2
- package/i18n/locales/de.json +107 -3
- package/i18n/locales/en.json +107 -3
- package/i18n/locales/es.json +107 -3
- package/i18n/locales/fr.json +107 -3
- package/i18n/locales/he.json +107 -3
- package/i18n/locales/it.json +107 -3
- package/i18n/locales/ja.json +107 -3
- package/i18n/locales/pl.json +107 -3
- package/i18n/locales/tr.json +107 -3
- package/i18n/locales/uk.json +107 -3
- package/i18n/plural-forms.spec.ts +15 -0
- package/package.json +2 -2
|
@@ -23,12 +23,24 @@ import { computed, ref, watch } from 'vue'
|
|
|
23
23
|
import {
|
|
24
24
|
ASSET_STORAGE_CAPABILITY,
|
|
25
25
|
GENERATION_CONTEXT_CAPABILITY,
|
|
26
|
+
isBinaryGeneratorCapability,
|
|
26
27
|
isBinaryModality,
|
|
28
|
+
type BinaryGenerationOptions,
|
|
29
|
+
type BinaryGeneratorCapability,
|
|
27
30
|
type BinaryModality,
|
|
28
31
|
type BinaryOutputConfig,
|
|
32
|
+
type ConflictingOutputSizeOption,
|
|
29
33
|
} from '@cat-factory/contracts'
|
|
30
34
|
import { binaryOutputPickIssues, type BinaryOutputPickIssue } from '~/utils/binaryOutput'
|
|
31
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
formatExtent,
|
|
37
|
+
formatReferenceImages,
|
|
38
|
+
generationControlOffer,
|
|
39
|
+
parseExtent,
|
|
40
|
+
parseMediaTypeRequirement,
|
|
41
|
+
parseReferenceImages,
|
|
42
|
+
sameFormats,
|
|
43
|
+
} from './BinaryOutputStepPicker.logic'
|
|
32
44
|
|
|
33
45
|
const props = defineProps<{ index: number }>()
|
|
34
46
|
|
|
@@ -241,6 +253,181 @@ const overlapSummary = computed(() =>
|
|
|
241
253
|
.join('; '),
|
|
242
254
|
)
|
|
243
255
|
|
|
256
|
+
/**
|
|
257
|
+
* The capability vocabulary, as STATIC literal `t()` keys: one per member, never assembled at
|
|
258
|
+
* runtime, so the typed-message-key check covers them (the standing rule for an enum-keyed set).
|
|
259
|
+
*/
|
|
260
|
+
const CAPABILITY_LABELS: Record<BinaryGeneratorCapability, () => string> = {
|
|
261
|
+
'reference-image': () => t('pipeline.builder.binaryCapability.reference-image'),
|
|
262
|
+
'multi-reference': () => t('pipeline.builder.binaryCapability.multi-reference'),
|
|
263
|
+
'instruction-edit': () => t('pipeline.builder.binaryCapability.instruction-edit'),
|
|
264
|
+
'mask-edit': () => t('pipeline.builder.binaryCapability.mask-edit'),
|
|
265
|
+
'negative-prompt': () => t('pipeline.builder.binaryCapability.negative-prompt'),
|
|
266
|
+
seed: () => t('pipeline.builder.binaryCapability.seed'),
|
|
267
|
+
'aspect-ratio': () => t('pipeline.builder.binaryCapability.aspect-ratio'),
|
|
268
|
+
'exact-size': () => t('pipeline.builder.binaryCapability.exact-size'),
|
|
269
|
+
'candidate-batch': () => t('pipeline.builder.binaryCapability.candidate-batch'),
|
|
270
|
+
upscale: () => t('pipeline.builder.binaryCapability.upscale'),
|
|
271
|
+
'transparent-background': () => t('pipeline.builder.binaryCapability.transparent-background'),
|
|
272
|
+
tileable: () => t('pipeline.builder.binaryCapability.tileable'),
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The options that restate a delivered size, named by the FIELD LABEL each one carries in this
|
|
277
|
+
* form, so the refusal points at a control the reader can see. Static literal keys, the
|
|
278
|
+
* enum-keyed-set rule again; the union is closed here (not a persisted vocabulary a stale step
|
|
279
|
+
* could carry a retired member of), so the lookup needs no membership guard.
|
|
280
|
+
*/
|
|
281
|
+
const SIZE_CONFLICT_LABELS: Record<ConflictingOutputSizeOption, () => string> = {
|
|
282
|
+
aspectRatio: () => t('pipeline.builder.binaryAspectRatio'),
|
|
283
|
+
upscale: () => t('pipeline.builder.binaryUpscale'),
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* A capability in the reader's language, INCLUDING one this build does not define.
|
|
288
|
+
*
|
|
289
|
+
* The `Record` is exhaustive over the union and the lookup is still not total: the integrations
|
|
290
|
+
* ride the workspace snapshot, and on a mothership-mode deployment that snapshot is served by a
|
|
291
|
+
* process which may be a build AHEAD of this one. A bare lookup on such a value is a `TypeError`
|
|
292
|
+
* on the surface where the selection is made. Same guard, same reason, as `modalityLabel`.
|
|
293
|
+
*/
|
|
294
|
+
function capabilityLabel(capability: BinaryGeneratorCapability): string {
|
|
295
|
+
return isBinaryGeneratorCapability(capability)
|
|
296
|
+
? CAPABILITY_LABELS[capability]()
|
|
297
|
+
: t('pipeline.builder.binaryCapabilityUnknown', { capability: String(capability) })
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const generation = computed<BinaryGenerationOptions>(() => config.value?.generation ?? {})
|
|
301
|
+
|
|
302
|
+
/** The registered integrations this step has selected, in the order the step names them. */
|
|
303
|
+
const selectedGenerators = computed(() => {
|
|
304
|
+
const byId = new Map(agents.binaryGenerators.map((generator) => [generator.id, generator]))
|
|
305
|
+
return (config.value?.generatorIds ?? []).flatMap((id) => byId.get(id) ?? [])
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Whether to OFFER the control an option belongs to. The rule itself is pure and lives in the
|
|
310
|
+
* logic sibling, where its three cases (undeclared, declared, already-set) are testable without
|
|
311
|
+
* mounting this component.
|
|
312
|
+
*/
|
|
313
|
+
const offers = computed(() => generationControlOffer(selectedGenerators.value, generation.value))
|
|
314
|
+
|
|
315
|
+
/** The edit modes, as static literal keys: the enum-keyed-set rule again. */
|
|
316
|
+
const editModeItems = computed(() => [
|
|
317
|
+
{ label: t('pipeline.builder.binaryEditModeInstruction'), value: 'instruction' },
|
|
318
|
+
{ label: t('pipeline.builder.binaryEditModeMask'), value: 'mask' },
|
|
319
|
+
])
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Patch ONE generation option, dropping it when it is cleared.
|
|
323
|
+
*
|
|
324
|
+
* A cleared option is REMOVED rather than stored as an empty value, because the two are different
|
|
325
|
+
* requirements: an absent `seed` means the step does not pin one, while `seed: 0` is a pinned
|
|
326
|
+
* seed of zero, and admission judges the option's PRESENCE.
|
|
327
|
+
*/
|
|
328
|
+
function setGeneration(fields: Partial<BinaryGenerationOptions>) {
|
|
329
|
+
const next: Record<string, unknown> = { ...generation.value, ...fields }
|
|
330
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
331
|
+
if (value === undefined || value === '' || (Array.isArray(value) && value.length === 0)) {
|
|
332
|
+
delete next[key]
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
patch({
|
|
336
|
+
generation: Object.keys(next).length > 0 ? (next as BinaryGenerationOptions) : undefined,
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The size fields' own text, held locally like {@link referenceText} and for the same reason: a
|
|
342
|
+
* control has to render what is being typed, and the step must not store what is being typed.
|
|
343
|
+
*
|
|
344
|
+
* The pair is ONE requirement, so it reaches the step only once BOTH halves read as a pixel
|
|
345
|
+
* extent. Writing the unset half as 0 while the first number is typed would store a value the
|
|
346
|
+
* schema refuses (the minimum is 1), which makes the step unsaveable behind an opaque validation
|
|
347
|
+
* error and renders "0" back into a field nobody touched. A half-entered pair is instead simply
|
|
348
|
+
* not stored, and {@link outputSizeIncomplete} says so where it is being typed.
|
|
349
|
+
*/
|
|
350
|
+
const outputSizeText = ref({
|
|
351
|
+
width: formatExtent(config.value?.generation?.outputSize?.width),
|
|
352
|
+
height: formatExtent(config.value?.generation?.outputSize?.height),
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
watch(
|
|
356
|
+
() => config.value?.generation?.outputSize,
|
|
357
|
+
(size) => {
|
|
358
|
+
outputSizeText.value = { width: formatExtent(size?.width), height: formatExtent(size?.height) }
|
|
359
|
+
},
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
/** One half is filled and the other is not, so nothing is stored and the composer is told why. */
|
|
363
|
+
const outputSizeIncomplete = computed(() => {
|
|
364
|
+
const filled = [outputSizeText.value.width, outputSizeText.value.height].filter(
|
|
365
|
+
(half) => half.trim() !== '',
|
|
366
|
+
)
|
|
367
|
+
return filled.length === 1
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
/** Patch ONE half of the exact output size, committing the pair only once both halves read. */
|
|
371
|
+
function setOutputSize(axis: 'width' | 'height', raw: string) {
|
|
372
|
+
outputSizeText.value = { ...outputSizeText.value, [axis]: raw }
|
|
373
|
+
const width = parseExtent(outputSizeText.value.width)
|
|
374
|
+
const height = parseExtent(outputSizeText.value.height)
|
|
375
|
+
setGeneration({ outputSize: width !== null && height !== null ? { width, height } : undefined })
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** The reference-image field's text, and the lines it refused (see `parseReferenceImages`). */
|
|
379
|
+
const referenceText = ref(formatReferenceImages(config.value?.generation?.referenceImages))
|
|
380
|
+
const unusableReferences = ref<string[]>([])
|
|
381
|
+
|
|
382
|
+
watch(
|
|
383
|
+
() => config.value?.generation?.referenceImages,
|
|
384
|
+
(references) => {
|
|
385
|
+
referenceText.value = formatReferenceImages(references)
|
|
386
|
+
},
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
function setReferences(text: string) {
|
|
390
|
+
const { usable, unusable } = parseReferenceImages(text)
|
|
391
|
+
unusableReferences.value = unusable
|
|
392
|
+
referenceText.value = formatReferenceImages(usable)
|
|
393
|
+
setGeneration({ referenceImages: usable })
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Turn the candidate comparison on or off. Off DROPS the whole bag rather than storing a disabled
|
|
398
|
+
* one: the presence of `comparison` is what the engine reads, so a saved "off" object would be a
|
|
399
|
+
* configuration describing a behaviour the run does not have.
|
|
400
|
+
*/
|
|
401
|
+
function setComparison(on: boolean) {
|
|
402
|
+
patch({ comparison: on ? (config.value?.comparison ?? {}) : undefined })
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function setPerGenerator(value: number) {
|
|
406
|
+
const current = config.value?.comparison
|
|
407
|
+
if (!current) return
|
|
408
|
+
patch({ comparison: { ...current, perGenerator: value } })
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function setMultiSelect(on: boolean) {
|
|
412
|
+
const current = config.value?.comparison
|
|
413
|
+
if (!current) return
|
|
414
|
+
patch({
|
|
415
|
+
comparison: on ? { ...current, multiSelect: true } : { perGenerator: current.perGenerator },
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Whether the step could actually produce a comparison: the SAME rule
|
|
421
|
+
* `assertComparableCandidates` refuses at save, stated here where it is fixable. Without it a
|
|
422
|
+
* step saves clean, starts, and keeps its only candidate without ever asking anyone.
|
|
423
|
+
*/
|
|
424
|
+
const comparisonUnreachable = computed(() => {
|
|
425
|
+
const comparison = config.value?.comparison
|
|
426
|
+
if (!comparison) return false
|
|
427
|
+
if ((comparison.perGenerator ?? 1) > 1) return false
|
|
428
|
+
return (config.value?.generatorIds?.length ?? 0) < 2
|
|
429
|
+
})
|
|
430
|
+
|
|
244
431
|
/** What the SELECTED integrations say they emit — the discoverable half of the free-text field. */
|
|
245
432
|
const declaredFormats = computed(() => {
|
|
246
433
|
const byId = new Map(agents.binaryGenerators.map((generator) => [generator.id, generator]))
|
|
@@ -347,6 +534,241 @@ const declaredFormats = computed(() => {
|
|
|
347
534
|
}}
|
|
348
535
|
</p>
|
|
349
536
|
|
|
537
|
+
<!-- GENERATION OPTIONS. Each control is gated on a capability the selected integrations
|
|
538
|
+
declare, so a step cannot ask for a reference image from an endpoint that takes no image
|
|
539
|
+
input. While nothing has DECLARED its capabilities every control stays offered and the
|
|
540
|
+
advisory line below says the support is unconfirmed: hiding one would be a claim about a
|
|
541
|
+
vendor's API that nobody established. Both tiers, like the rest of this picker: an
|
|
542
|
+
option nobody stated is an option the run does not apply. -->
|
|
543
|
+
<template v-if="config?.storageServiceId">
|
|
544
|
+
<div v-if="offers('reference-image')" class="flex items-start gap-2">
|
|
545
|
+
<span class="mt-1 text-[10px] text-slate-500">{{
|
|
546
|
+
t('pipeline.builder.binaryReferenceImages')
|
|
547
|
+
}}</span>
|
|
548
|
+
<UTextarea
|
|
549
|
+
class="w-56"
|
|
550
|
+
:rows="2"
|
|
551
|
+
size="xs"
|
|
552
|
+
:model-value="referenceText"
|
|
553
|
+
:placeholder="t('pipeline.builder.binaryReferenceImagesPlaceholder')"
|
|
554
|
+
data-testid="binary-output-reference-input"
|
|
555
|
+
@update:model-value="referenceText = String($event)"
|
|
556
|
+
@change="setReferences(referenceText)"
|
|
557
|
+
/>
|
|
558
|
+
</div>
|
|
559
|
+
<p
|
|
560
|
+
v-if="unusableReferences.length"
|
|
561
|
+
class="ms-1 text-[10px] text-amber-400"
|
|
562
|
+
data-testid="binary-output-reference-unusable"
|
|
563
|
+
>
|
|
564
|
+
{{
|
|
565
|
+
t('pipeline.builder.binaryReferenceImagesUnusable', {
|
|
566
|
+
entries: unusableReferences.join('; '),
|
|
567
|
+
})
|
|
568
|
+
}}
|
|
569
|
+
</p>
|
|
570
|
+
|
|
571
|
+
<div v-if="offers('instruction-edit') || offers('mask-edit')" class="flex items-center gap-2">
|
|
572
|
+
<span class="text-[10px] text-slate-500">{{ t('pipeline.builder.binaryEditMode') }}</span>
|
|
573
|
+
<USelect
|
|
574
|
+
class="w-56"
|
|
575
|
+
size="xs"
|
|
576
|
+
:model-value="generation.edit?.mode ?? ''"
|
|
577
|
+
:items="editModeItems"
|
|
578
|
+
value-key="value"
|
|
579
|
+
:placeholder="t('pipeline.builder.binaryEditModeNone')"
|
|
580
|
+
data-testid="binary-output-edit-mode"
|
|
581
|
+
@update:model-value="
|
|
582
|
+
setGeneration({
|
|
583
|
+
edit: $event
|
|
584
|
+
? { ...generation.edit, mode: $event as 'instruction' | 'mask' }
|
|
585
|
+
: undefined,
|
|
586
|
+
})
|
|
587
|
+
"
|
|
588
|
+
/>
|
|
589
|
+
</div>
|
|
590
|
+
<div v-if="generation.edit" class="flex items-center gap-2">
|
|
591
|
+
<span class="text-[10px] text-slate-500">{{
|
|
592
|
+
t('pipeline.builder.binaryEditInstruction')
|
|
593
|
+
}}</span>
|
|
594
|
+
<UInput
|
|
595
|
+
class="w-56"
|
|
596
|
+
size="xs"
|
|
597
|
+
:model-value="generation.edit.instruction ?? ''"
|
|
598
|
+
:placeholder="t('pipeline.builder.binaryEditInstructionPlaceholder')"
|
|
599
|
+
data-testid="binary-output-edit-instruction"
|
|
600
|
+
@change="
|
|
601
|
+
setGeneration({
|
|
602
|
+
edit: { ...generation.edit!, instruction: ($event.target as HTMLInputElement).value },
|
|
603
|
+
})
|
|
604
|
+
"
|
|
605
|
+
/>
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
<div v-if="offers('negative-prompt')" class="flex items-center gap-2">
|
|
609
|
+
<span class="text-[10px] text-slate-500">{{
|
|
610
|
+
t('pipeline.builder.binaryNegativePrompt')
|
|
611
|
+
}}</span>
|
|
612
|
+
<UInput
|
|
613
|
+
class="w-56"
|
|
614
|
+
size="xs"
|
|
615
|
+
:model-value="generation.negativePrompt ?? ''"
|
|
616
|
+
:placeholder="t('pipeline.builder.binaryNegativePromptPlaceholder')"
|
|
617
|
+
data-testid="binary-output-negative-prompt"
|
|
618
|
+
@change="
|
|
619
|
+
setGeneration({ negativePrompt: ($event.target as HTMLInputElement).value.trim() })
|
|
620
|
+
"
|
|
621
|
+
/>
|
|
622
|
+
</div>
|
|
623
|
+
|
|
624
|
+
<div v-if="offers('aspect-ratio')" class="flex items-center gap-2">
|
|
625
|
+
<span class="text-[10px] text-slate-500">{{
|
|
626
|
+
t('pipeline.builder.binaryAspectRatio')
|
|
627
|
+
}}</span>
|
|
628
|
+
<UInput
|
|
629
|
+
class="w-56"
|
|
630
|
+
size="xs"
|
|
631
|
+
:model-value="generation.aspectRatio ?? ''"
|
|
632
|
+
:placeholder="t('pipeline.builder.binaryAspectRatioPlaceholder')"
|
|
633
|
+
data-testid="binary-output-aspect-ratio"
|
|
634
|
+
@change="setGeneration({ aspectRatio: ($event.target as HTMLInputElement).value.trim() })"
|
|
635
|
+
/>
|
|
636
|
+
</div>
|
|
637
|
+
|
|
638
|
+
<!-- Two numbers rather than one free-text "WxH": the requirement is a pair of integers and
|
|
639
|
+
a parser over a string is a second place for it to be wrong. Offered only where an
|
|
640
|
+
integration declares `exact-size`, because a bucketed endpoint cannot be asked for
|
|
641
|
+
dimensions at all and a control implying otherwise is the misconception this axis
|
|
642
|
+
exists to remove. -->
|
|
643
|
+
<div v-if="offers('exact-size')" class="flex flex-col gap-1">
|
|
644
|
+
<div class="flex items-center gap-2">
|
|
645
|
+
<span class="text-[10px] text-slate-500">{{
|
|
646
|
+
t('pipeline.builder.binaryOutputSize')
|
|
647
|
+
}}</span>
|
|
648
|
+
<UInput
|
|
649
|
+
class="w-24"
|
|
650
|
+
type="number"
|
|
651
|
+
size="xs"
|
|
652
|
+
:model-value="outputSizeText.width"
|
|
653
|
+
:placeholder="t('pipeline.builder.binaryOutputSizeWidth')"
|
|
654
|
+
data-testid="binary-output-size-width"
|
|
655
|
+
@change="setOutputSize('width', ($event.target as HTMLInputElement).value)"
|
|
656
|
+
/>
|
|
657
|
+
<span class="text-[10px] text-slate-500">×</span>
|
|
658
|
+
<UInput
|
|
659
|
+
class="w-24"
|
|
660
|
+
type="number"
|
|
661
|
+
size="xs"
|
|
662
|
+
:model-value="outputSizeText.height"
|
|
663
|
+
:placeholder="t('pipeline.builder.binaryOutputSizeHeight')"
|
|
664
|
+
data-testid="binary-output-size-height"
|
|
665
|
+
@change="setOutputSize('height', ($event.target as HTMLInputElement).value)"
|
|
666
|
+
/>
|
|
667
|
+
</div>
|
|
668
|
+
<!-- Nothing is stored until both halves read, so the half-entered state is stated where
|
|
669
|
+
it is being typed. Silence there would be a field that looks filled in and a step
|
|
670
|
+
that does not carry the requirement. -->
|
|
671
|
+
<p
|
|
672
|
+
v-if="outputSizeIncomplete"
|
|
673
|
+
class="text-[10px] text-amber-400"
|
|
674
|
+
data-testid="binary-output-size-incomplete"
|
|
675
|
+
>
|
|
676
|
+
{{ t('pipeline.builder.binaryOutputSizeIncomplete') }}
|
|
677
|
+
</p>
|
|
678
|
+
</div>
|
|
679
|
+
|
|
680
|
+
<div v-if="offers('seed')" class="flex items-center gap-2">
|
|
681
|
+
<span class="text-[10px] text-slate-500">{{ t('pipeline.builder.binarySeed') }}</span>
|
|
682
|
+
<UInput
|
|
683
|
+
class="w-56"
|
|
684
|
+
type="number"
|
|
685
|
+
size="xs"
|
|
686
|
+
:model-value="generation.seed === undefined ? '' : String(generation.seed)"
|
|
687
|
+
:placeholder="t('pipeline.builder.binarySeedPlaceholder')"
|
|
688
|
+
data-testid="binary-output-seed"
|
|
689
|
+
@change="
|
|
690
|
+
setGeneration({
|
|
691
|
+
seed: ($event.target as HTMLInputElement).value
|
|
692
|
+
? Number(($event.target as HTMLInputElement).value)
|
|
693
|
+
: undefined,
|
|
694
|
+
})
|
|
695
|
+
"
|
|
696
|
+
/>
|
|
697
|
+
</div>
|
|
698
|
+
|
|
699
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
700
|
+
<label v-if="offers('transparent-background')" class="flex items-center gap-1.5">
|
|
701
|
+
<UCheckbox
|
|
702
|
+
:model-value="generation.transparentBackground === true"
|
|
703
|
+
data-testid="binary-output-transparent"
|
|
704
|
+
@update:model-value="
|
|
705
|
+
setGeneration({ transparentBackground: $event ? true : undefined })
|
|
706
|
+
"
|
|
707
|
+
/>
|
|
708
|
+
<span class="text-[10px] text-slate-500">{{
|
|
709
|
+
t('pipeline.builder.binaryTransparent')
|
|
710
|
+
}}</span>
|
|
711
|
+
</label>
|
|
712
|
+
<label v-if="offers('tileable')" class="flex items-center gap-1.5">
|
|
713
|
+
<UCheckbox
|
|
714
|
+
:model-value="generation.tileable === true"
|
|
715
|
+
data-testid="binary-output-tileable"
|
|
716
|
+
@update:model-value="setGeneration({ tileable: $event ? true : undefined })"
|
|
717
|
+
/>
|
|
718
|
+
<span class="text-[10px] text-slate-500">{{ t('pipeline.builder.binaryTileable') }}</span>
|
|
719
|
+
</label>
|
|
720
|
+
<label v-if="offers('upscale')" class="flex items-center gap-1.5">
|
|
721
|
+
<UCheckbox
|
|
722
|
+
:model-value="generation.upscale !== undefined"
|
|
723
|
+
data-testid="binary-output-upscale"
|
|
724
|
+
@update:model-value="setGeneration({ upscale: $event ? 2 : undefined })"
|
|
725
|
+
/>
|
|
726
|
+
<span class="text-[10px] text-slate-500">{{ t('pipeline.builder.binaryUpscale') }}</span>
|
|
727
|
+
</label>
|
|
728
|
+
</div>
|
|
729
|
+
|
|
730
|
+
<!-- CANDIDATE COMPARISON: generate several and let a person choose, rather than letting the
|
|
731
|
+
agent commit to one producer unobserved. -->
|
|
732
|
+
<label class="flex items-center gap-1.5">
|
|
733
|
+
<UCheckbox
|
|
734
|
+
:model-value="config.comparison !== undefined"
|
|
735
|
+
data-testid="binary-output-comparison"
|
|
736
|
+
@update:model-value="setComparison($event === true)"
|
|
737
|
+
/>
|
|
738
|
+
<span class="text-[10px] text-slate-500">{{ t('pipeline.builder.binaryComparison') }}</span>
|
|
739
|
+
</label>
|
|
740
|
+
<div v-if="config.comparison" class="ms-6 flex flex-wrap items-center gap-3">
|
|
741
|
+
<span class="text-[10px] text-slate-500">{{
|
|
742
|
+
t('pipeline.builder.binaryPerGenerator')
|
|
743
|
+
}}</span>
|
|
744
|
+
<USelect
|
|
745
|
+
class="w-20"
|
|
746
|
+
size="xs"
|
|
747
|
+
:model-value="config.comparison.perGenerator ?? 1"
|
|
748
|
+
:items="[1, 2, 3, 4]"
|
|
749
|
+
data-testid="binary-output-per-generator"
|
|
750
|
+
@update:model-value="setPerGenerator(Number($event))"
|
|
751
|
+
/>
|
|
752
|
+
<label class="flex items-center gap-1.5">
|
|
753
|
+
<UCheckbox
|
|
754
|
+
:model-value="config.comparison.multiSelect === true"
|
|
755
|
+
data-testid="binary-output-multi-select"
|
|
756
|
+
@update:model-value="setMultiSelect($event === true)"
|
|
757
|
+
/>
|
|
758
|
+
<span class="text-[10px] text-slate-500">{{
|
|
759
|
+
t('pipeline.builder.binaryMultiSelect')
|
|
760
|
+
}}</span>
|
|
761
|
+
</label>
|
|
762
|
+
</div>
|
|
763
|
+
<p
|
|
764
|
+
v-if="comparisonUnreachable"
|
|
765
|
+
class="text-[10px] text-amber-400"
|
|
766
|
+
data-testid="binary-output-comparison-unreachable"
|
|
767
|
+
>
|
|
768
|
+
{{ t('pipeline.builder.binaryComparisonUnreachable') }}
|
|
769
|
+
</p>
|
|
770
|
+
</template>
|
|
771
|
+
|
|
350
772
|
<!-- Every refusal this step would hit, named where it is fixable. Each is its own line
|
|
351
773
|
with its own remedy: an unreachable catalog is not an empty one, a lost service is not
|
|
352
774
|
an untagged one, and a lost CONTEXT service is not a lost storage target. -->
|
|
@@ -448,6 +870,46 @@ const declaredFormats = computed(() => {
|
|
|
448
870
|
>
|
|
449
871
|
{{ t('pipeline.builder.binaryOutputGeneratorOverlap', { overlaps: overlapSummary }) }}
|
|
450
872
|
</p>
|
|
873
|
+
<p
|
|
874
|
+
v-if="has('capability_unsupported')"
|
|
875
|
+
class="text-[10px] text-amber-400"
|
|
876
|
+
data-testid="binary-output-capability-unsupported"
|
|
877
|
+
>
|
|
878
|
+
{{
|
|
879
|
+
t('pipeline.builder.binaryCapabilityUnsupported', {
|
|
880
|
+
capabilities: pick.unsupportedCapabilities.map(capabilityLabel).join(', '),
|
|
881
|
+
})
|
|
882
|
+
}}
|
|
883
|
+
</p>
|
|
884
|
+
<!-- A refusal the SAVE makes on the step's own fields, so it is stated here rather than
|
|
885
|
+
waited for: all three controls are offered together, and the remedy is deleting one of
|
|
886
|
+
two values on this form. -->
|
|
887
|
+
<p
|
|
888
|
+
v-if="has('output_size_ambiguous')"
|
|
889
|
+
class="text-[10px] text-amber-400"
|
|
890
|
+
data-testid="binary-output-size-ambiguous"
|
|
891
|
+
>
|
|
892
|
+
{{
|
|
893
|
+
t('pipeline.builder.binaryOutputSizeAmbiguous', {
|
|
894
|
+
options: pick.conflictingSizeOptions.map((o) => SIZE_CONFLICT_LABELS[o]()).join(', '),
|
|
895
|
+
})
|
|
896
|
+
}}
|
|
897
|
+
</p>
|
|
898
|
+
<!-- ADVISORY, grouped with the other two: an integration that declared no capabilities has
|
|
899
|
+
said only that they are unknown, and every integration registered before this axis
|
|
900
|
+
existed is in exactly that state. Styling it as a refusal would flag most working
|
|
901
|
+
selections in the product. -->
|
|
902
|
+
<p
|
|
903
|
+
v-if="has('capability_unverifiable')"
|
|
904
|
+
class="text-[10px] text-slate-500"
|
|
905
|
+
data-testid="binary-output-capability-unverifiable"
|
|
906
|
+
>
|
|
907
|
+
{{
|
|
908
|
+
t('pipeline.builder.binaryCapabilityUnverifiable', {
|
|
909
|
+
capabilities: pick.unverifiableCapabilities.map(capabilityLabel).join(', '),
|
|
910
|
+
})
|
|
911
|
+
}}
|
|
912
|
+
</p>
|
|
451
913
|
<p
|
|
452
914
|
v-if="unusableMediaTypes.length"
|
|
453
915
|
class="text-[10px] text-amber-400"
|
|
@@ -12,11 +12,20 @@ import BinaryOutputStepPicker from '~/components/pipeline/BinaryOutputStepPicker
|
|
|
12
12
|
import { ESTIMATE_AXES, ESTIMATE_AXIS_FIELD, type EstimateAxis } from '~/utils/estimateGating'
|
|
13
13
|
import { showOverrideField } from '~/utils/uiMode'
|
|
14
14
|
import { narrowPipelineLibrary } from '~/utils/pipelineLibrary'
|
|
15
|
+
import { CONDITION_MARKERS, stepConditionsAt } from '~/utils/pipeline'
|
|
16
|
+
|
|
17
|
+
/** The cycle button's icon per state — the two condition markers, plus the unconditional one. */
|
|
18
|
+
const CONDITION_ICONS = {
|
|
19
|
+
always: 'i-lucide-infinity',
|
|
20
|
+
frontend: CONDITION_MARKERS.frontend.icon,
|
|
21
|
+
backend: CONDITION_MARKERS.backend.icon,
|
|
22
|
+
} as const
|
|
15
23
|
import {
|
|
16
24
|
agentKindMeta,
|
|
17
25
|
companionForProducer,
|
|
18
26
|
isConsensusEligibleKind,
|
|
19
27
|
isTesterKind,
|
|
28
|
+
mayCarrySkipAxis,
|
|
20
29
|
} from '~/utils/catalog'
|
|
21
30
|
import type { ConsensusStrategy } from '~/types/consensus'
|
|
22
31
|
|
|
@@ -611,6 +620,31 @@ async function clone(p: Pipeline) {
|
|
|
611
620
|
"
|
|
612
621
|
@click="toggleEnabled(unit)"
|
|
613
622
|
/>
|
|
623
|
+
<!-- Run condition: restrict this step to tasks that change a frontend
|
|
624
|
+
service, or to tasks that change anything else. Cycles through the three
|
|
625
|
+
states (see `cycleDraftStepCondition`). Shown in BOTH interface tiers, and
|
|
626
|
+
not behind `showOverrideField`: cloning a built-in carries the tester pair's
|
|
627
|
+
conditions in, so a control that hid them by default would leave a basic-mode
|
|
628
|
+
editor saving a step whose "when does this run" they were never shown.
|
|
629
|
+
|
|
630
|
+
Offered only where the step MAY be skipped at all. A condition is a skip axis,
|
|
631
|
+
so the engine holds it to the same gatability rule as an estimate gate
|
|
632
|
+
(`assertValidRunConditions`) — without this the builder invited a condition on
|
|
633
|
+
`merger` and answered the save with a 422. -->
|
|
634
|
+
<UButton
|
|
635
|
+
v-if="mayCarrySkipAxis(unit.kind)"
|
|
636
|
+
:icon="CONDITION_ICONS[pipelines.draftStepCondition(unit.index) ?? 'always']"
|
|
637
|
+
:color="pipelines.draftStepCondition(unit.index) ? 'info' : 'neutral'"
|
|
638
|
+
variant="ghost"
|
|
639
|
+
size="xs"
|
|
640
|
+
:title="
|
|
641
|
+
t(
|
|
642
|
+
`pipeline.builder.condition.${pipelines.draftStepCondition(unit.index) ?? 'always'}`,
|
|
643
|
+
)
|
|
644
|
+
"
|
|
645
|
+
data-testid="pipeline-step-condition"
|
|
646
|
+
@click="pipelines.cycleDraftStepCondition(unit.index)"
|
|
647
|
+
/>
|
|
614
648
|
<!-- Approval gate: pause after this step so a human reviews (and
|
|
615
649
|
can edit) its proposal before the next step runs. -->
|
|
616
650
|
<UButton
|
|
@@ -1308,6 +1342,16 @@ async function clone(p: Pipeline) {
|
|
|
1308
1342
|
i + 1
|
|
1309
1343
|
}}</span>
|
|
1310
1344
|
<AgentKindIcon :kind="k" show-label />
|
|
1345
|
+
<!-- A step that does not run on every task says so HERE, in the library, because
|
|
1346
|
+
this list is what a reader compares two pipelines by: a preset whose testers
|
|
1347
|
+
are conditional and one whose testers always run look identical otherwise. -->
|
|
1348
|
+
<UIcon
|
|
1349
|
+
v-for="c in stepConditionsAt(p, i)"
|
|
1350
|
+
:key="c"
|
|
1351
|
+
:name="CONDITION_MARKERS[c].icon"
|
|
1352
|
+
class="h-3 w-3 shrink-0 text-sky-400"
|
|
1353
|
+
:title="t(CONDITION_MARKERS[c].key)"
|
|
1354
|
+
/>
|
|
1311
1355
|
</li>
|
|
1312
1356
|
</ol>
|
|
1313
1357
|
</li>
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
import { computed } from 'vue'
|
|
12
12
|
import type { Pipeline } from '~/types/domain'
|
|
13
13
|
import { agentKindMeta } from '~/utils/catalog'
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
CONDITION_MARKERS,
|
|
16
|
+
pipelineConditionalCount,
|
|
17
|
+
pipelineDisplaySteps,
|
|
18
|
+
pipelineGateCount,
|
|
19
|
+
} from '~/utils/pipeline'
|
|
15
20
|
import AgentKindIcon from '~/components/pipeline/AgentKindIcon.vue'
|
|
16
21
|
|
|
17
22
|
const props = defineProps<{ pipeline: Pipeline }>()
|
|
@@ -19,6 +24,7 @@ const { t } = useI18n()
|
|
|
19
24
|
|
|
20
25
|
const steps = computed(() => pipelineDisplaySteps(props.pipeline))
|
|
21
26
|
const gateCount = computed(() => pipelineGateCount(props.pipeline))
|
|
27
|
+
const conditionalCount = computed(() => pipelineConditionalCount(props.pipeline))
|
|
22
28
|
|
|
23
29
|
/** What the agent at this step does — the same catalog prose the palette and step tooltips use. */
|
|
24
30
|
function stepDescription(kind: string): string {
|
|
@@ -50,6 +56,12 @@ function stepDescription(kind: string): string {
|
|
|
50
56
|
<UIcon name="i-lucide-shield-check" class="h-3 w-3" />
|
|
51
57
|
{{ t('pipeline.preview.gateCount', { count: gateCount }, gateCount) }}
|
|
52
58
|
</span>
|
|
59
|
+
<!-- Conditional steps change what a run of this pipeline actually does from task to task,
|
|
60
|
+
which is exactly what a preview read BEFORE picking has to say out loud. -->
|
|
61
|
+
<span v-if="conditionalCount" class="inline-flex items-center gap-1 text-sky-500">
|
|
62
|
+
<UIcon name="i-lucide-git-branch" class="h-3 w-3" />
|
|
63
|
+
{{ t('pipeline.preview.conditionalCount', { count: conditionalCount }, conditionalCount) }}
|
|
64
|
+
</span>
|
|
53
65
|
</div>
|
|
54
66
|
|
|
55
67
|
<!-- The ordered steps. The number column doubles as the flow connector (a rule drawn between
|
|
@@ -79,6 +91,13 @@ function stepDescription(kind: string): string {
|
|
|
79
91
|
class="h-3 w-3 shrink-0 text-amber-400"
|
|
80
92
|
:title="t('pipeline.preview.gated')"
|
|
81
93
|
/>
|
|
94
|
+
<UIcon
|
|
95
|
+
v-for="c in s.conditions"
|
|
96
|
+
:key="c"
|
|
97
|
+
:name="CONDITION_MARKERS[c].icon"
|
|
98
|
+
class="h-3 w-3 shrink-0 text-sky-400"
|
|
99
|
+
:title="t(CONDITION_MARKERS[c].key)"
|
|
100
|
+
/>
|
|
82
101
|
</div>
|
|
83
102
|
<!-- Clamped: the catalog prose runs long for some kinds, and <AgentKindIcon> already
|
|
84
103
|
carries the full text in its hover tooltip. -->
|