@cat-factory/app 0.256.2 → 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/README.md +4 -0
- package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +284 -0
- package/app/components/binaryOutput/BinaryOutputReport.vue +33 -0
- package/app/components/board/nodes/BlockNode.vue +3 -7
- package/app/components/board/nodes/InitiativeCard.vue +1 -1
- package/app/components/focus/BlockFocusView.vue +1 -1
- package/app/components/forkDecision/ForkDecisionWindow.vue +3 -1
- package/app/components/outcome/OutcomeSummaryWindow.vue +1 -2
- package/app/components/panels/AgentStepDetail.vue +42 -20
- package/app/components/panels/InspectorPanel.vue +6 -11
- 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 +48 -10
- package/app/components/settings/KubernetesEngineForm.vue +98 -46
- package/app/components/spec/ServiceSpecWindow.vue +5 -4
- package/app/composables/api/binaryCandidates.ts +36 -0
- package/app/composables/useApi.ts +2 -0
- package/app/docs/architecture.md +1 -1
- package/app/modular/panels/inspector.ts +3 -3
- package/app/modular/result-views.ts +4 -0
- package/app/stores/binaryCandidates.ts +89 -0
- package/app/stores/board/placement.ts +32 -8
- 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/badge.ts +14 -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/catalog.ts +2 -1
- package/app/utils/initiative.ts +1 -4
- 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
|
@@ -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,11 +11,13 @@ 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'
|
|
17
18
|
import PrReviewPhaseBadge from '~/components/prReview/PrReviewPhaseBadge.vue'
|
|
18
19
|
import { useNowTick, stepDurationLabel } from '~/composables/useStepTimer'
|
|
20
|
+
import type { BadgeColor } from '~/utils/badge'
|
|
19
21
|
|
|
20
22
|
const props = defineProps<{ instance: ExecutionInstance }>()
|
|
21
23
|
const emit = defineEmits<{
|
|
@@ -89,6 +91,17 @@ function prReviewAwaiting(step: PipelineStep): boolean {
|
|
|
89
91
|
return step.prReview?.status === 'awaiting_selection'
|
|
90
92
|
}
|
|
91
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
|
+
|
|
92
105
|
/**
|
|
93
106
|
* Whether a `pr-reviewer` step has a LIVE phase to surface (slicing / reviewing / … ). Drives
|
|
94
107
|
* showing the phase badge in place of the generic subtask count header; a terminal (done/skipped)
|
|
@@ -147,15 +160,15 @@ const STATE_META = computed<Record<AgentState, { label: string; color: string; i
|
|
|
147
160
|
)
|
|
148
161
|
|
|
149
162
|
/** Visual language for the pipeline instance as a whole. */
|
|
150
|
-
const STATUS_META = computed<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
163
|
+
const STATUS_META = computed<
|
|
164
|
+
Record<ExecutionInstance['status'], { label: string; chip: BadgeColor }>
|
|
165
|
+
>(() => ({
|
|
166
|
+
running: { label: t('pipeline.progress.status.running'), chip: 'primary' },
|
|
167
|
+
blocked: { label: t('pipeline.progress.status.blocked'), chip: 'warning' },
|
|
168
|
+
paused: { label: t('pipeline.progress.status.paused'), chip: 'neutral' },
|
|
169
|
+
done: { label: t('pipeline.progress.status.done'), chip: 'success' },
|
|
170
|
+
failed: { label: t('pipeline.progress.status.failed'), chip: 'error' },
|
|
171
|
+
}))
|
|
159
172
|
|
|
160
173
|
const steps = computed(() => props.instance.steps)
|
|
161
174
|
const total = computed(() => steps.value.length)
|
|
@@ -254,7 +267,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
254
267
|
<!-- summary -->
|
|
255
268
|
<div class="rounded-xl border border-slate-800 bg-slate-900/60 p-4">
|
|
256
269
|
<div class="flex flex-wrap items-center gap-3">
|
|
257
|
-
<UBadge :color="statusMeta.chip
|
|
270
|
+
<UBadge :color="statusMeta.chip" variant="subtle">{{ statusMeta.label }}</UBadge>
|
|
258
271
|
<span class="text-sm text-slate-300">
|
|
259
272
|
<i18n-t keypath="pipeline.progress.agentsComplete" tag="span" scope="global">
|
|
260
273
|
<template #completed>
|
|
@@ -664,6 +677,31 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
664
677
|
</span>
|
|
665
678
|
</button>
|
|
666
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
|
+
|
|
667
705
|
<!-- reviewer gate folding/re-reviewing in the background: a working indicator,
|
|
668
706
|
NOT a "Review & approve" gate (the human is summoned only if needed) -->
|
|
669
707
|
<div
|