@cat-factory/app 0.256.3 → 0.257.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 +33 -0
- package/app/components/forkDecision/ForkDecisionWindow.vue +3 -1
- package/app/components/panels/AgentStepDetail.vue +42 -20
- 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 +90 -1
- package/app/components/pipeline/BinaryOutputStepPicker.logic.ts +92 -1
- package/app/components/pipeline/BinaryOutputStepPicker.vue +354 -1
- package/app/components/pipeline/PipelineProgress.vue +37 -0
- package/app/composables/api/binaryCandidates.ts +36 -0
- package/app/composables/useApi.ts +2 -0
- package/app/modular/result-views.ts +4 -0
- package/app/stores/binaryCandidates.ts +89 -0
- package/app/stores/ui/resultViews.ts +8 -6
- package/app/stores/ui/runStepOpeners.ts +23 -1
- package/app/types/execution.ts +5 -0
- package/app/utils/binaryCandidates.spec.ts +110 -0
- package/app/utils/binaryCandidates.ts +126 -0
- package/app/utils/binaryOutput.ts +48 -2
- package/app/utils/pipelineRender.spec.ts +46 -1
- package/app/utils/pipelineRender.ts +70 -2
- package/i18n/locales/de.json +78 -2
- package/i18n/locales/en.json +78 -2
- package/i18n/locales/es.json +78 -2
- package/i18n/locales/fr.json +78 -2
- package/i18n/locales/he.json +78 -2
- package/i18n/locales/it.json +78 -2
- package/i18n/locales/ja.json +78 -2
- package/i18n/locales/pl.json +78 -2
- package/i18n/locales/tr.json +78 -2
- package/i18n/locales/uk.json +78 -2
- package/i18n/plural-forms.spec.ts +15 -0
- package/package.json +2 -2
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
binaryReferenceImageSchema,
|
|
3
|
+
mediaTypeSchema,
|
|
4
|
+
normalizeMediaType,
|
|
5
|
+
requiredBinaryCapabilities,
|
|
6
|
+
type BinaryGenerationOptions,
|
|
7
|
+
type BinaryGeneratorCapability,
|
|
8
|
+
type BinaryReferenceImage,
|
|
9
|
+
} from '@cat-factory/contracts'
|
|
2
10
|
import * as v from 'valibot'
|
|
3
11
|
|
|
4
12
|
// The pure half of BinaryOutputStepPicker: reading a free-text FORMAT requirement, and telling
|
|
@@ -53,3 +61,86 @@ export function sameFormats(
|
|
|
53
61
|
): boolean {
|
|
54
62
|
return (a ?? []).join(',') === (b ?? []).join(',')
|
|
55
63
|
}
|
|
64
|
+
|
|
65
|
+
/** A parsed reference-image list: what the step will carry, and what was refused on the way in. */
|
|
66
|
+
export interface ParsedReferenceImages {
|
|
67
|
+
/** Well-formed entries, in the order typed: exactly what gets stored. */
|
|
68
|
+
usable: BinaryReferenceImage[]
|
|
69
|
+
/** Lines that are not `role|location[|service]`, kept VERBATIM so the warning can quote them. */
|
|
70
|
+
unusable: string[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Read a reference-image list from the one-per-line `role|location[|service]` text the builder
|
|
75
|
+
* accepts.
|
|
76
|
+
*
|
|
77
|
+
* Free TEXT rather than a picker, for the reason the format requirement beside it is: what a
|
|
78
|
+
* reference points at is an object in the org's own storage or a URL, and neither is a set the
|
|
79
|
+
* platform can enumerate. The three fields are positional because the shape is small and a
|
|
80
|
+
* three-input row per reference would dominate a step row that is already dense; the ROLE comes
|
|
81
|
+
* first because it is the constrained field, so a typo lands on the half the parser can name.
|
|
82
|
+
*
|
|
83
|
+
* A refused line is REPORTED, never quietly dropped, exactly as a refused format is: a reference
|
|
84
|
+
* someone typed and the step does not carry is a generation that silently ignores it.
|
|
85
|
+
*/
|
|
86
|
+
export function parseReferenceImages(text: string): ParsedReferenceImages {
|
|
87
|
+
const usable: BinaryReferenceImage[] = []
|
|
88
|
+
const unusable: string[] = []
|
|
89
|
+
for (const line of text
|
|
90
|
+
.split('\n')
|
|
91
|
+
.map((part) => part.trim())
|
|
92
|
+
.filter(Boolean)) {
|
|
93
|
+
const [role, location, service] = line.split('|').map((part) => part.trim())
|
|
94
|
+
const parsed = v.safeParse(binaryReferenceImageSchema, {
|
|
95
|
+
role,
|
|
96
|
+
location,
|
|
97
|
+
...(service ? { service } : {}),
|
|
98
|
+
})
|
|
99
|
+
if (parsed.success) usable.push(parsed.output)
|
|
100
|
+
else unusable.push(line)
|
|
101
|
+
}
|
|
102
|
+
return { usable, unusable }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Render a stored reference list back into the text the field shows. */
|
|
106
|
+
export function formatReferenceImages(
|
|
107
|
+
references: readonly BinaryReferenceImage[] | undefined,
|
|
108
|
+
): string {
|
|
109
|
+
return (references ?? [])
|
|
110
|
+
.map((ref) => [ref.role, ref.location, ref.service].filter(Boolean).join('|'))
|
|
111
|
+
.join('\n')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build the predicate that decides whether the control for a generation option is OFFERED, given
|
|
116
|
+
* what the step's selected integrations declare and what its stored options already require.
|
|
117
|
+
*
|
|
118
|
+
* Three rules, and the third is the one that is easy to miss:
|
|
119
|
+
*
|
|
120
|
+
* - An integration that declares NOTHING pins nothing down, so while one is selected (or nothing
|
|
121
|
+
* is) every control stays offered and the advisory line says the support is unconfirmed.
|
|
122
|
+
* Hiding one would be a claim about a vendor's API that nobody established.
|
|
123
|
+
* - Otherwise a control is offered exactly when something selected declares its capability.
|
|
124
|
+
* - And a control whose option is ALREADY SET stays offered whatever the selection says. Changing
|
|
125
|
+
* the selection does not clear options authored against the old one, so hiding the control on
|
|
126
|
+
* the way past strands a stored requirement that refuses the run at admission, under an error
|
|
127
|
+
* telling the reader to remove an option whose only control has just disappeared. The platform
|
|
128
|
+
* will not silently drop an authored requirement, so the person who stated it needs the control
|
|
129
|
+
* that withdraws it. This is the SPA's standing rule that a hidden field must leave behind
|
|
130
|
+
* exactly the default it would have shown.
|
|
131
|
+
*
|
|
132
|
+
* Returned as a closure over ONE pass across the selection rather than recomputed per capability:
|
|
133
|
+
* the template asks it once per control, and the sets are the same for all of them.
|
|
134
|
+
*/
|
|
135
|
+
export function generationControlOffer(
|
|
136
|
+
selected: readonly { capabilities?: readonly BinaryGeneratorCapability[] }[],
|
|
137
|
+
options: BinaryGenerationOptions | undefined,
|
|
138
|
+
): (capability: BinaryGeneratorCapability) => boolean {
|
|
139
|
+
const declared = new Set(selected.flatMap((generator) => generator.capabilities ?? []))
|
|
140
|
+
const undeclared =
|
|
141
|
+
selected.length === 0 || selected.some((g) => (g.capabilities ?? []).length === 0)
|
|
142
|
+
// The same derivation admission judges the step by, imported rather than re-implemented, so the
|
|
143
|
+
// control a person is offered and the requirement the run is refused for cannot disagree.
|
|
144
|
+
const required = new Set(requiredBinaryCapabilities(options))
|
|
145
|
+
return (capability) => undeclared || declared.has(capability) || required.has(capability)
|
|
146
|
+
}
|
|
@@ -23,12 +23,21 @@ 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,
|
|
29
32
|
} from '@cat-factory/contracts'
|
|
30
33
|
import { binaryOutputPickIssues, type BinaryOutputPickIssue } from '~/utils/binaryOutput'
|
|
31
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
formatReferenceImages,
|
|
36
|
+
generationControlOffer,
|
|
37
|
+
parseMediaTypeRequirement,
|
|
38
|
+
parseReferenceImages,
|
|
39
|
+
sameFormats,
|
|
40
|
+
} from './BinaryOutputStepPicker.logic'
|
|
32
41
|
|
|
33
42
|
const props = defineProps<{ index: number }>()
|
|
34
43
|
|
|
@@ -241,6 +250,131 @@ const overlapSummary = computed(() =>
|
|
|
241
250
|
.join('; '),
|
|
242
251
|
)
|
|
243
252
|
|
|
253
|
+
/**
|
|
254
|
+
* The capability vocabulary, as STATIC literal `t()` keys: one per member, never assembled at
|
|
255
|
+
* runtime, so the typed-message-key check covers them (the standing rule for an enum-keyed set).
|
|
256
|
+
*/
|
|
257
|
+
const CAPABILITY_LABELS: Record<BinaryGeneratorCapability, () => string> = {
|
|
258
|
+
'reference-image': () => t('pipeline.builder.binaryCapability.reference-image'),
|
|
259
|
+
'multi-reference': () => t('pipeline.builder.binaryCapability.multi-reference'),
|
|
260
|
+
'instruction-edit': () => t('pipeline.builder.binaryCapability.instruction-edit'),
|
|
261
|
+
'mask-edit': () => t('pipeline.builder.binaryCapability.mask-edit'),
|
|
262
|
+
'negative-prompt': () => t('pipeline.builder.binaryCapability.negative-prompt'),
|
|
263
|
+
seed: () => t('pipeline.builder.binaryCapability.seed'),
|
|
264
|
+
'aspect-ratio': () => t('pipeline.builder.binaryCapability.aspect-ratio'),
|
|
265
|
+
'candidate-batch': () => t('pipeline.builder.binaryCapability.candidate-batch'),
|
|
266
|
+
upscale: () => t('pipeline.builder.binaryCapability.upscale'),
|
|
267
|
+
'transparent-background': () => t('pipeline.builder.binaryCapability.transparent-background'),
|
|
268
|
+
tileable: () => t('pipeline.builder.binaryCapability.tileable'),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* A capability in the reader's language, INCLUDING one this build does not define.
|
|
273
|
+
*
|
|
274
|
+
* The `Record` is exhaustive over the union and the lookup is still not total: the integrations
|
|
275
|
+
* ride the workspace snapshot, and on a mothership-mode deployment that snapshot is served by a
|
|
276
|
+
* process which may be a build AHEAD of this one. A bare lookup on such a value is a `TypeError`
|
|
277
|
+
* on the surface where the selection is made. Same guard, same reason, as `modalityLabel`.
|
|
278
|
+
*/
|
|
279
|
+
function capabilityLabel(capability: BinaryGeneratorCapability): string {
|
|
280
|
+
return isBinaryGeneratorCapability(capability)
|
|
281
|
+
? CAPABILITY_LABELS[capability]()
|
|
282
|
+
: t('pipeline.builder.binaryCapabilityUnknown', { capability: String(capability) })
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const generation = computed<BinaryGenerationOptions>(() => config.value?.generation ?? {})
|
|
286
|
+
|
|
287
|
+
/** The registered integrations this step has selected, in the order the step names them. */
|
|
288
|
+
const selectedGenerators = computed(() => {
|
|
289
|
+
const byId = new Map(agents.binaryGenerators.map((generator) => [generator.id, generator]))
|
|
290
|
+
return (config.value?.generatorIds ?? []).flatMap((id) => byId.get(id) ?? [])
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Whether to OFFER the control an option belongs to. The rule itself is pure and lives in the
|
|
295
|
+
* logic sibling, where its three cases (undeclared, declared, already-set) are testable without
|
|
296
|
+
* mounting this component.
|
|
297
|
+
*/
|
|
298
|
+
const offers = computed(() => generationControlOffer(selectedGenerators.value, generation.value))
|
|
299
|
+
|
|
300
|
+
/** The edit modes, as static literal keys: the enum-keyed-set rule again. */
|
|
301
|
+
const editModeItems = computed(() => [
|
|
302
|
+
{ label: t('pipeline.builder.binaryEditModeInstruction'), value: 'instruction' },
|
|
303
|
+
{ label: t('pipeline.builder.binaryEditModeMask'), value: 'mask' },
|
|
304
|
+
])
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Patch ONE generation option, dropping it when it is cleared.
|
|
308
|
+
*
|
|
309
|
+
* A cleared option is REMOVED rather than stored as an empty value, because the two are different
|
|
310
|
+
* requirements: an absent `seed` means the step does not pin one, while `seed: 0` is a pinned
|
|
311
|
+
* seed of zero, and admission judges the option's PRESENCE.
|
|
312
|
+
*/
|
|
313
|
+
function setGeneration(fields: Partial<BinaryGenerationOptions>) {
|
|
314
|
+
const next: Record<string, unknown> = { ...generation.value, ...fields }
|
|
315
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
316
|
+
if (value === undefined || value === '' || (Array.isArray(value) && value.length === 0)) {
|
|
317
|
+
delete next[key]
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
patch({
|
|
321
|
+
generation: Object.keys(next).length > 0 ? (next as BinaryGenerationOptions) : undefined,
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** The reference-image field's text, and the lines it refused (see `parseReferenceImages`). */
|
|
326
|
+
const referenceText = ref(formatReferenceImages(config.value?.generation?.referenceImages))
|
|
327
|
+
const unusableReferences = ref<string[]>([])
|
|
328
|
+
|
|
329
|
+
watch(
|
|
330
|
+
() => config.value?.generation?.referenceImages,
|
|
331
|
+
(references) => {
|
|
332
|
+
referenceText.value = formatReferenceImages(references)
|
|
333
|
+
},
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
function setReferences(text: string) {
|
|
337
|
+
const { usable, unusable } = parseReferenceImages(text)
|
|
338
|
+
unusableReferences.value = unusable
|
|
339
|
+
referenceText.value = formatReferenceImages(usable)
|
|
340
|
+
setGeneration({ referenceImages: usable })
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Turn the candidate comparison on or off. Off DROPS the whole bag rather than storing a disabled
|
|
345
|
+
* one: the presence of `comparison` is what the engine reads, so a saved "off" object would be a
|
|
346
|
+
* configuration describing a behaviour the run does not have.
|
|
347
|
+
*/
|
|
348
|
+
function setComparison(on: boolean) {
|
|
349
|
+
patch({ comparison: on ? (config.value?.comparison ?? {}) : undefined })
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function setPerGenerator(value: number) {
|
|
353
|
+
const current = config.value?.comparison
|
|
354
|
+
if (!current) return
|
|
355
|
+
patch({ comparison: { ...current, perGenerator: value } })
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function setMultiSelect(on: boolean) {
|
|
359
|
+
const current = config.value?.comparison
|
|
360
|
+
if (!current) return
|
|
361
|
+
patch({
|
|
362
|
+
comparison: on ? { ...current, multiSelect: true } : { perGenerator: current.perGenerator },
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Whether the step could actually produce a comparison: the SAME rule
|
|
368
|
+
* `assertComparableCandidates` refuses at save, stated here where it is fixable. Without it a
|
|
369
|
+
* step saves clean, starts, and keeps its only candidate without ever asking anyone.
|
|
370
|
+
*/
|
|
371
|
+
const comparisonUnreachable = computed(() => {
|
|
372
|
+
const comparison = config.value?.comparison
|
|
373
|
+
if (!comparison) return false
|
|
374
|
+
if ((comparison.perGenerator ?? 1) > 1) return false
|
|
375
|
+
return (config.value?.generatorIds?.length ?? 0) < 2
|
|
376
|
+
})
|
|
377
|
+
|
|
244
378
|
/** What the SELECTED integrations say they emit — the discoverable half of the free-text field. */
|
|
245
379
|
const declaredFormats = computed(() => {
|
|
246
380
|
const byId = new Map(agents.binaryGenerators.map((generator) => [generator.id, generator]))
|
|
@@ -347,6 +481,199 @@ const declaredFormats = computed(() => {
|
|
|
347
481
|
}}
|
|
348
482
|
</p>
|
|
349
483
|
|
|
484
|
+
<!-- GENERATION OPTIONS. Each control is gated on a capability the selected integrations
|
|
485
|
+
declare, so a step cannot ask for a reference image from an endpoint that takes no image
|
|
486
|
+
input. While nothing has DECLARED its capabilities every control stays offered and the
|
|
487
|
+
advisory line below says the support is unconfirmed: hiding one would be a claim about a
|
|
488
|
+
vendor's API that nobody established. Both tiers, like the rest of this picker: an
|
|
489
|
+
option nobody stated is an option the run does not apply. -->
|
|
490
|
+
<template v-if="config?.storageServiceId">
|
|
491
|
+
<div v-if="offers('reference-image')" class="flex items-start gap-2">
|
|
492
|
+
<span class="mt-1 text-[10px] text-slate-500">{{
|
|
493
|
+
t('pipeline.builder.binaryReferenceImages')
|
|
494
|
+
}}</span>
|
|
495
|
+
<UTextarea
|
|
496
|
+
class="w-56"
|
|
497
|
+
:rows="2"
|
|
498
|
+
size="xs"
|
|
499
|
+
:model-value="referenceText"
|
|
500
|
+
:placeholder="t('pipeline.builder.binaryReferenceImagesPlaceholder')"
|
|
501
|
+
data-testid="binary-output-reference-input"
|
|
502
|
+
@update:model-value="referenceText = String($event)"
|
|
503
|
+
@change="setReferences(referenceText)"
|
|
504
|
+
/>
|
|
505
|
+
</div>
|
|
506
|
+
<p
|
|
507
|
+
v-if="unusableReferences.length"
|
|
508
|
+
class="ms-1 text-[10px] text-amber-400"
|
|
509
|
+
data-testid="binary-output-reference-unusable"
|
|
510
|
+
>
|
|
511
|
+
{{
|
|
512
|
+
t('pipeline.builder.binaryReferenceImagesUnusable', {
|
|
513
|
+
entries: unusableReferences.join('; '),
|
|
514
|
+
})
|
|
515
|
+
}}
|
|
516
|
+
</p>
|
|
517
|
+
|
|
518
|
+
<div v-if="offers('instruction-edit') || offers('mask-edit')" class="flex items-center gap-2">
|
|
519
|
+
<span class="text-[10px] text-slate-500">{{ t('pipeline.builder.binaryEditMode') }}</span>
|
|
520
|
+
<USelect
|
|
521
|
+
class="w-56"
|
|
522
|
+
size="xs"
|
|
523
|
+
:model-value="generation.edit?.mode ?? ''"
|
|
524
|
+
:items="editModeItems"
|
|
525
|
+
value-key="value"
|
|
526
|
+
:placeholder="t('pipeline.builder.binaryEditModeNone')"
|
|
527
|
+
data-testid="binary-output-edit-mode"
|
|
528
|
+
@update:model-value="
|
|
529
|
+
setGeneration({
|
|
530
|
+
edit: $event
|
|
531
|
+
? { ...generation.edit, mode: $event as 'instruction' | 'mask' }
|
|
532
|
+
: undefined,
|
|
533
|
+
})
|
|
534
|
+
"
|
|
535
|
+
/>
|
|
536
|
+
</div>
|
|
537
|
+
<div v-if="generation.edit" class="flex items-center gap-2">
|
|
538
|
+
<span class="text-[10px] text-slate-500">{{
|
|
539
|
+
t('pipeline.builder.binaryEditInstruction')
|
|
540
|
+
}}</span>
|
|
541
|
+
<UInput
|
|
542
|
+
class="w-56"
|
|
543
|
+
size="xs"
|
|
544
|
+
:model-value="generation.edit.instruction ?? ''"
|
|
545
|
+
:placeholder="t('pipeline.builder.binaryEditInstructionPlaceholder')"
|
|
546
|
+
data-testid="binary-output-edit-instruction"
|
|
547
|
+
@change="
|
|
548
|
+
setGeneration({
|
|
549
|
+
edit: { ...generation.edit!, instruction: ($event.target as HTMLInputElement).value },
|
|
550
|
+
})
|
|
551
|
+
"
|
|
552
|
+
/>
|
|
553
|
+
</div>
|
|
554
|
+
|
|
555
|
+
<div v-if="offers('negative-prompt')" class="flex items-center gap-2">
|
|
556
|
+
<span class="text-[10px] text-slate-500">{{
|
|
557
|
+
t('pipeline.builder.binaryNegativePrompt')
|
|
558
|
+
}}</span>
|
|
559
|
+
<UInput
|
|
560
|
+
class="w-56"
|
|
561
|
+
size="xs"
|
|
562
|
+
:model-value="generation.negativePrompt ?? ''"
|
|
563
|
+
:placeholder="t('pipeline.builder.binaryNegativePromptPlaceholder')"
|
|
564
|
+
data-testid="binary-output-negative-prompt"
|
|
565
|
+
@change="
|
|
566
|
+
setGeneration({ negativePrompt: ($event.target as HTMLInputElement).value.trim() })
|
|
567
|
+
"
|
|
568
|
+
/>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
<div v-if="offers('aspect-ratio')" class="flex items-center gap-2">
|
|
572
|
+
<span class="text-[10px] text-slate-500">{{
|
|
573
|
+
t('pipeline.builder.binaryAspectRatio')
|
|
574
|
+
}}</span>
|
|
575
|
+
<UInput
|
|
576
|
+
class="w-56"
|
|
577
|
+
size="xs"
|
|
578
|
+
:model-value="generation.aspectRatio ?? ''"
|
|
579
|
+
:placeholder="t('pipeline.builder.binaryAspectRatioPlaceholder')"
|
|
580
|
+
data-testid="binary-output-aspect-ratio"
|
|
581
|
+
@change="setGeneration({ aspectRatio: ($event.target as HTMLInputElement).value.trim() })"
|
|
582
|
+
/>
|
|
583
|
+
</div>
|
|
584
|
+
|
|
585
|
+
<div v-if="offers('seed')" class="flex items-center gap-2">
|
|
586
|
+
<span class="text-[10px] text-slate-500">{{ t('pipeline.builder.binarySeed') }}</span>
|
|
587
|
+
<UInput
|
|
588
|
+
class="w-56"
|
|
589
|
+
type="number"
|
|
590
|
+
size="xs"
|
|
591
|
+
:model-value="generation.seed === undefined ? '' : String(generation.seed)"
|
|
592
|
+
:placeholder="t('pipeline.builder.binarySeedPlaceholder')"
|
|
593
|
+
data-testid="binary-output-seed"
|
|
594
|
+
@change="
|
|
595
|
+
setGeneration({
|
|
596
|
+
seed: ($event.target as HTMLInputElement).value
|
|
597
|
+
? Number(($event.target as HTMLInputElement).value)
|
|
598
|
+
: undefined,
|
|
599
|
+
})
|
|
600
|
+
"
|
|
601
|
+
/>
|
|
602
|
+
</div>
|
|
603
|
+
|
|
604
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
605
|
+
<label v-if="offers('transparent-background')" class="flex items-center gap-1.5">
|
|
606
|
+
<UCheckbox
|
|
607
|
+
:model-value="generation.transparentBackground === true"
|
|
608
|
+
data-testid="binary-output-transparent"
|
|
609
|
+
@update:model-value="
|
|
610
|
+
setGeneration({ transparentBackground: $event ? true : undefined })
|
|
611
|
+
"
|
|
612
|
+
/>
|
|
613
|
+
<span class="text-[10px] text-slate-500">{{
|
|
614
|
+
t('pipeline.builder.binaryTransparent')
|
|
615
|
+
}}</span>
|
|
616
|
+
</label>
|
|
617
|
+
<label v-if="offers('tileable')" class="flex items-center gap-1.5">
|
|
618
|
+
<UCheckbox
|
|
619
|
+
:model-value="generation.tileable === true"
|
|
620
|
+
data-testid="binary-output-tileable"
|
|
621
|
+
@update:model-value="setGeneration({ tileable: $event ? true : undefined })"
|
|
622
|
+
/>
|
|
623
|
+
<span class="text-[10px] text-slate-500">{{ t('pipeline.builder.binaryTileable') }}</span>
|
|
624
|
+
</label>
|
|
625
|
+
<label v-if="offers('upscale')" class="flex items-center gap-1.5">
|
|
626
|
+
<UCheckbox
|
|
627
|
+
:model-value="generation.upscale !== undefined"
|
|
628
|
+
data-testid="binary-output-upscale"
|
|
629
|
+
@update:model-value="setGeneration({ upscale: $event ? 2 : undefined })"
|
|
630
|
+
/>
|
|
631
|
+
<span class="text-[10px] text-slate-500">{{ t('pipeline.builder.binaryUpscale') }}</span>
|
|
632
|
+
</label>
|
|
633
|
+
</div>
|
|
634
|
+
|
|
635
|
+
<!-- CANDIDATE COMPARISON: generate several and let a person choose, rather than letting the
|
|
636
|
+
agent commit to one producer unobserved. -->
|
|
637
|
+
<label class="flex items-center gap-1.5">
|
|
638
|
+
<UCheckbox
|
|
639
|
+
:model-value="config.comparison !== undefined"
|
|
640
|
+
data-testid="binary-output-comparison"
|
|
641
|
+
@update:model-value="setComparison($event === true)"
|
|
642
|
+
/>
|
|
643
|
+
<span class="text-[10px] text-slate-500">{{ t('pipeline.builder.binaryComparison') }}</span>
|
|
644
|
+
</label>
|
|
645
|
+
<div v-if="config.comparison" class="ms-6 flex flex-wrap items-center gap-3">
|
|
646
|
+
<span class="text-[10px] text-slate-500">{{
|
|
647
|
+
t('pipeline.builder.binaryPerGenerator')
|
|
648
|
+
}}</span>
|
|
649
|
+
<USelect
|
|
650
|
+
class="w-20"
|
|
651
|
+
size="xs"
|
|
652
|
+
:model-value="config.comparison.perGenerator ?? 1"
|
|
653
|
+
:items="[1, 2, 3, 4]"
|
|
654
|
+
data-testid="binary-output-per-generator"
|
|
655
|
+
@update:model-value="setPerGenerator(Number($event))"
|
|
656
|
+
/>
|
|
657
|
+
<label class="flex items-center gap-1.5">
|
|
658
|
+
<UCheckbox
|
|
659
|
+
:model-value="config.comparison.multiSelect === true"
|
|
660
|
+
data-testid="binary-output-multi-select"
|
|
661
|
+
@update:model-value="setMultiSelect($event === true)"
|
|
662
|
+
/>
|
|
663
|
+
<span class="text-[10px] text-slate-500">{{
|
|
664
|
+
t('pipeline.builder.binaryMultiSelect')
|
|
665
|
+
}}</span>
|
|
666
|
+
</label>
|
|
667
|
+
</div>
|
|
668
|
+
<p
|
|
669
|
+
v-if="comparisonUnreachable"
|
|
670
|
+
class="text-[10px] text-amber-400"
|
|
671
|
+
data-testid="binary-output-comparison-unreachable"
|
|
672
|
+
>
|
|
673
|
+
{{ t('pipeline.builder.binaryComparisonUnreachable') }}
|
|
674
|
+
</p>
|
|
675
|
+
</template>
|
|
676
|
+
|
|
350
677
|
<!-- Every refusal this step would hit, named where it is fixable. Each is its own line
|
|
351
678
|
with its own remedy: an unreachable catalog is not an empty one, a lost service is not
|
|
352
679
|
an untagged one, and a lost CONTEXT service is not a lost storage target. -->
|
|
@@ -448,6 +775,32 @@ const declaredFormats = computed(() => {
|
|
|
448
775
|
>
|
|
449
776
|
{{ t('pipeline.builder.binaryOutputGeneratorOverlap', { overlaps: overlapSummary }) }}
|
|
450
777
|
</p>
|
|
778
|
+
<p
|
|
779
|
+
v-if="has('capability_unsupported')"
|
|
780
|
+
class="text-[10px] text-amber-400"
|
|
781
|
+
data-testid="binary-output-capability-unsupported"
|
|
782
|
+
>
|
|
783
|
+
{{
|
|
784
|
+
t('pipeline.builder.binaryCapabilityUnsupported', {
|
|
785
|
+
capabilities: pick.unsupportedCapabilities.map(capabilityLabel).join(', '),
|
|
786
|
+
})
|
|
787
|
+
}}
|
|
788
|
+
</p>
|
|
789
|
+
<!-- ADVISORY, grouped with the other two: an integration that declared no capabilities has
|
|
790
|
+
said only that they are unknown, and every integration registered before this axis
|
|
791
|
+
existed is in exactly that state. Styling it as a refusal would flag most working
|
|
792
|
+
selections in the product. -->
|
|
793
|
+
<p
|
|
794
|
+
v-if="has('capability_unverifiable')"
|
|
795
|
+
class="text-[10px] text-slate-500"
|
|
796
|
+
data-testid="binary-output-capability-unverifiable"
|
|
797
|
+
>
|
|
798
|
+
{{
|
|
799
|
+
t('pipeline.builder.binaryCapabilityUnverifiable', {
|
|
800
|
+
capabilities: pick.unverifiableCapabilities.map(capabilityLabel).join(', '),
|
|
801
|
+
})
|
|
802
|
+
}}
|
|
803
|
+
</p>
|
|
451
804
|
<p
|
|
452
805
|
v-if="unusableMediaTypes.length"
|
|
453
806
|
class="text-[10px] text-amber-400"
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
FAILED_STEP_META,
|
|
12
12
|
containerPhaseLabel,
|
|
13
13
|
dedicatedParkView,
|
|
14
|
+
REDIRECT_PARK_PRESENTATION,
|
|
14
15
|
} from '~/utils/pipelineRender'
|
|
15
16
|
import { prReviewPhase } from '~/utils/prReviewProgress'
|
|
16
17
|
import StepMetricsBar from '~/components/observability/StepMetricsBar.vue'
|
|
@@ -90,6 +91,17 @@ function prReviewAwaiting(step: PipelineStep): boolean {
|
|
|
90
91
|
return step.prReview?.status === 'awaiting_selection'
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Whether a binary-output step is parked awaiting a human candidate choice. Asked of the shared
|
|
96
|
+
* park recognizer rather than re-derived from `step.binaryCandidates`, so this chip and the
|
|
97
|
+
* generic approval gate below (which suppresses itself for exactly the parks that recognizer
|
|
98
|
+
* names) can never disagree about who owns the park. That disagreement is what leaves a parked
|
|
99
|
+
* run showing no action at all.
|
|
100
|
+
*/
|
|
101
|
+
function candidatesAwaiting(step: PipelineStep): boolean {
|
|
102
|
+
return dedicatedParkView(step, props.instance) === 'binary-candidates'
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
/**
|
|
94
106
|
* Whether a `pr-reviewer` step has a LIVE phase to surface (slicing / reviewing / … ). Drives
|
|
95
107
|
* showing the phase badge in place of the generic subtask count header; a terminal (done/skipped)
|
|
@@ -665,6 +677,31 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
665
677
|
</span>
|
|
666
678
|
</button>
|
|
667
679
|
|
|
680
|
+
<!-- A generating step parked between its candidate pass and its delivering pass: a
|
|
681
|
+
purpose-built chip opening the comparison window, ahead of the generic approval
|
|
682
|
+
gate (mirrors the fork-decision and pr-review chips above). Without it the step
|
|
683
|
+
shows no action at all, because the generic gate below is suppressed for every
|
|
684
|
+
park a dedicated window owns. -->
|
|
685
|
+
<button
|
|
686
|
+
v-if="candidatesAwaiting(s)"
|
|
687
|
+
type="button"
|
|
688
|
+
data-testid="binary-candidates-open"
|
|
689
|
+
class="mt-3 flex w-full items-center gap-2 rounded-lg border border-dashed border-cyan-500/50 bg-cyan-500/10 px-2.5 py-1.5 text-start transition followup-blink hover:border-cyan-400/60"
|
|
690
|
+
@click="ui.openBinaryCandidates(instance.id, i)"
|
|
691
|
+
>
|
|
692
|
+
<span
|
|
693
|
+
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border border-cyan-500/40 bg-cyan-500/15"
|
|
694
|
+
>
|
|
695
|
+
<UIcon
|
|
696
|
+
:name="REDIRECT_PARK_PRESENTATION['binary-candidates'].icon"
|
|
697
|
+
class="h-3 w-3 text-cyan-300"
|
|
698
|
+
/>
|
|
699
|
+
</span>
|
|
700
|
+
<span class="min-w-0 flex-1 truncate text-[12px] text-slate-300">
|
|
701
|
+
{{ t('pipeline.progress.binaryCandidates.choose') }}
|
|
702
|
+
</span>
|
|
703
|
+
</button>
|
|
704
|
+
|
|
668
705
|
<!-- reviewer gate folding/re-reviewing in the background: a working indicator,
|
|
669
706
|
NOT a "Review & approve" gate (the human is summoned only if needed) -->
|
|
670
707
|
<div
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getBinaryCandidatesContract,
|
|
3
|
+
keepBinaryCandidatesContract,
|
|
4
|
+
type KeepBinaryCandidatesInput,
|
|
5
|
+
} from '@cat-factory/contracts'
|
|
6
|
+
import type { ApiContext } from './context'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generated-candidate comparison. A binary-output step configured to COMPARE generates a
|
|
10
|
+
* candidate from each of its selected integrations, stages them through the step's storage
|
|
11
|
+
* service, and parks. These endpoints read the staged candidates and record which of them
|
|
12
|
+
* survive (and under which alternate ids); keeping re-runs the step to deliver exactly those.
|
|
13
|
+
* The read returns null when no step carries candidate state.
|
|
14
|
+
*/
|
|
15
|
+
export function binaryCandidatesApi({ send, ws }: ApiContext) {
|
|
16
|
+
return {
|
|
17
|
+
// The live candidate state for a run (null when no step carries one).
|
|
18
|
+
getBinaryCandidates: (workspaceId: string, executionId: string) =>
|
|
19
|
+
send(getBinaryCandidatesContract, {
|
|
20
|
+
pathPrefix: ws(workspaceId),
|
|
21
|
+
pathParams: { executionId },
|
|
22
|
+
}),
|
|
23
|
+
|
|
24
|
+
// Keep the chosen candidates and discard the rest.
|
|
25
|
+
keepBinaryCandidates: (
|
|
26
|
+
workspaceId: string,
|
|
27
|
+
executionId: string,
|
|
28
|
+
body: KeepBinaryCandidatesInput,
|
|
29
|
+
) =>
|
|
30
|
+
send(keepBinaryCandidatesContract, {
|
|
31
|
+
pathPrefix: ws(workspaceId),
|
|
32
|
+
pathParams: { executionId },
|
|
33
|
+
body,
|
|
34
|
+
}),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -13,6 +13,7 @@ import { boardApi } from './api/board'
|
|
|
13
13
|
import { documentsApi } from './api/documents'
|
|
14
14
|
import { executionApi } from './api/execution'
|
|
15
15
|
import { followUpsApi } from './api/followUps'
|
|
16
|
+
import { binaryCandidatesApi } from './api/binaryCandidates'
|
|
16
17
|
import { forkDecisionApi } from './api/forkDecision'
|
|
17
18
|
import { inputGateApi } from './api/inputGate'
|
|
18
19
|
import { judgeApi } from './api/judge'
|
|
@@ -131,6 +132,7 @@ export function useApi() {
|
|
|
131
132
|
...bugHuntApi(ctx),
|
|
132
133
|
...reviewsApi(ctx),
|
|
133
134
|
...followUpsApi(ctx),
|
|
135
|
+
...binaryCandidatesApi(ctx),
|
|
134
136
|
...forkDecisionApi(ctx),
|
|
135
137
|
...inputGateApi(ctx),
|
|
136
138
|
...judgeApi(ctx),
|
|
@@ -13,6 +13,7 @@ import ConsensusSessionWindow from '~/components/consensus/ConsensusSessionWindo
|
|
|
13
13
|
import GenericStructuredResultView from '~/components/panels/GenericStructuredResultView.vue'
|
|
14
14
|
import ServiceSpecWindow from '~/components/spec/ServiceSpecWindow.vue'
|
|
15
15
|
import FollowUpWindow from '~/components/followUp/FollowUpWindow.vue'
|
|
16
|
+
import BinaryCandidatesWindow from '~/components/binaryCandidates/BinaryCandidatesWindow.vue'
|
|
16
17
|
import ForkDecisionWindow from '~/components/forkDecision/ForkDecisionWindow.vue'
|
|
17
18
|
import PrReviewWindow from '~/components/prReview/PrReviewWindow.vue'
|
|
18
19
|
import MergerResultView from '~/components/panels/MergerResultView.vue'
|
|
@@ -77,6 +78,9 @@ const BUILT_IN_RESULT_VIEWS: Record<ResultViewId, Component> = {
|
|
|
77
78
|
'follow-ups': FollowUpWindow,
|
|
78
79
|
// The implementation-fork decision: the proposer's approaches + the human's pick / custom.
|
|
79
80
|
'fork-decision': ForkDecisionWindow,
|
|
81
|
+
// The generated-candidate comparison: the candidates a generating step staged, side by side,
|
|
82
|
+
// and the human's keep/discard decision (with the alternate ids they assigned).
|
|
83
|
+
'binary-candidates': BinaryCandidatesWindow,
|
|
80
84
|
// The PR deep-review: the reviewer's sliced, prioritized findings + the human's multi-select.
|
|
81
85
|
'pr-review': PrReviewWindow,
|
|
82
86
|
// The merger's verdict: PR complexity/risk/impact scores + the engine's decision (and why).
|