@cat-factory/app 0.187.0 → 0.189.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 +26 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +11 -0
- package/app/components/palettes/AgentPalette.vue +12 -1
- package/app/components/palettes/AgentTierSelect.vue +67 -0
- package/app/components/pipeline/PipelineBuilder.vue +166 -92
- package/app/components/pipeline/PipelineHealthModal.vue +68 -12
- package/app/components/settings/ConsensusGroupsSection.vue +498 -0
- package/app/components/settings/ModelConfigurationPanel.vue +34 -2
- package/app/composables/api/presets.ts +22 -0
- package/app/composables/usePipelineErrorToast.ts +17 -0
- package/app/composables/usePipelineHealth.spec.ts +79 -7
- package/app/composables/usePipelineHealth.ts +70 -3
- package/app/modular/agent-kinds.spec.ts +9 -3
- package/app/modular/agent-kinds.ts +4 -0
- package/app/stores/agentTier.spec.ts +24 -0
- package/app/stores/agentTier.ts +42 -0
- package/app/stores/consensusGroups.ts +77 -0
- package/app/stores/pipelines/draftActions.ts +11 -113
- package/app/stores/pipelines/draftStepConfig.ts +157 -0
- package/app/stores/pipelines.ts +21 -3
- package/app/stores/workspace/hydrate.ts +7 -1
- package/app/types/consensus.ts +3 -0
- package/app/types/domain.ts +8 -1
- package/app/utils/agentTier.spec.ts +59 -0
- package/app/utils/agentTier.ts +49 -0
- package/app/utils/catalog.spec.ts +12 -0
- package/app/utils/catalog.ts +58 -2
- package/i18n/locales/de.json +74 -5
- package/i18n/locales/en.json +80 -5
- package/i18n/locales/es.json +74 -5
- package/i18n/locales/fr.json +74 -5
- package/i18n/locales/he.json +74 -5
- package/i18n/locales/it.json +74 -5
- package/i18n/locales/ja.json +74 -5
- package/i18n/locales/pl.json +74 -5
- package/i18n/locales/tr.json +74 -5
- package/i18n/locales/uk.json +74 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ The SPA source lives under `app/` (the Nuxt srcDir).
|
|
|
16
16
|
- [Tech stack](#tech-stack)
|
|
17
17
|
- [Layout](#layout)
|
|
18
18
|
- [Interface modes (basic / advanced)](#interface-modes-basic--advanced)
|
|
19
|
+
- [Agent tiers (basic / intermediate / advanced)](#agent-tiers-basic--intermediate--advanced)
|
|
19
20
|
- [Key UI surfaces](#key-ui-surfaces)
|
|
20
21
|
- [Develop & test](#develop--test)
|
|
21
22
|
|
|
@@ -116,6 +117,31 @@ hoc where it can be avoided:
|
|
|
116
117
|
`false` is a choice, not absence), so basic mode can never conceal a setting a run will
|
|
117
118
|
actually use.
|
|
118
119
|
|
|
120
|
+
## Agent tiers (basic / intermediate / advanced)
|
|
121
|
+
|
|
122
|
+
A separate, narrower axis: how deep into the **agent catalog** a surface reaches. Every agent
|
|
123
|
+
kind carries a `tier` — `basic` (the everyday delivery loop), `intermediate` (reached for
|
|
124
|
+
regularly) or `advanced` (specialist) — and the two surfaces that enumerate the catalog, the
|
|
125
|
+
**pipeline builder's palette** and the **model preset's per-agent override list**, show the
|
|
126
|
+
selected tier and everything below it. They open on `basic`; the `AgentTierSelect` control on
|
|
127
|
+
each widens them, with `advanced` showing the whole catalog. The choice is one shared,
|
|
128
|
+
persisted preference (the `agentTier` store), because picking the agents a pipeline runs and
|
|
129
|
+
picking what each of them runs on are halves of the same job.
|
|
130
|
+
|
|
131
|
+
- The vocabulary, the default and the cumulative predicate live in `@cat-factory/contracts`
|
|
132
|
+
(`AGENT_TIERS` / `DEFAULT_AGENT_TIER` / `agentTierVisibleAt`), beside
|
|
133
|
+
`purposeAllowsAgentCategory` — so a **deployment-registered kind's** declared tier
|
|
134
|
+
(`presentation.tier`, carried in the workspace snapshot) and the SPA's own built-ins are
|
|
135
|
+
read by one rule. A kind that declares no tier is treated as `intermediate`.
|
|
136
|
+
- **This is not the interface mode.** That tier decides which surfaces the whole SPA offers;
|
|
137
|
+
this one decides how much of one surface's catalog is listed. They are independent — an
|
|
138
|
+
advanced-mode user still starts on the basic agent tier — and the tier control is present in
|
|
139
|
+
**both** interface modes, since it is the only route to the kinds it hides.
|
|
140
|
+
- A narrowed catalog states what it is holding back (the "n hidden at this tier" hint), and
|
|
141
|
+
the model preset list **always keeps a kind the edited preset already pins a model for**,
|
|
142
|
+
whatever the tier — the same rule `showOverrideField` states for a single field: a row the
|
|
143
|
+
user can neither read nor clear is worse than a longer list.
|
|
144
|
+
|
|
119
145
|
## Extending the layer (consumer modules)
|
|
120
146
|
|
|
121
147
|
A deployment can contribute its own components — result windows, nav entries, inspector
|
|
@@ -124,6 +124,17 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
|
|
|
124
124
|
@close="close"
|
|
125
125
|
>
|
|
126
126
|
<template v-if="session" #header-extras>
|
|
127
|
+
<!-- Which workspace consensus GROUP the run escalated to. Only a tiered step has one, and
|
|
128
|
+
the name is the session's own copy — so it still reads correctly after the library
|
|
129
|
+
entry was renamed or deleted. Without it, a reader asking "why did five models run on
|
|
130
|
+
this task" has the transcript but not the reason. -->
|
|
131
|
+
<span
|
|
132
|
+
v-if="session.groupName"
|
|
133
|
+
class="rounded-full bg-emerald-900/50 px-2.5 py-1 text-xs font-medium text-emerald-200"
|
|
134
|
+
:title="t('consensus.groupTitle')"
|
|
135
|
+
>
|
|
136
|
+
{{ session.groupName }}
|
|
137
|
+
</span>
|
|
127
138
|
<span
|
|
128
139
|
class="rounded-full px-2.5 py-1 text-xs font-medium"
|
|
129
140
|
:class="STATUS_CLASS[session.status] ?? 'bg-slate-700 text-slate-300'"
|
|
@@ -3,10 +3,13 @@ import { computed } from 'vue'
|
|
|
3
3
|
import { useLocalStorage } from '@vueuse/core'
|
|
4
4
|
import { purposeAllowsAgentCategory } from '@cat-factory/contracts'
|
|
5
5
|
import type { AgentKind, PipelinePurpose } from '~/types/domain'
|
|
6
|
+
import AgentTierSelect from '~/components/palettes/AgentTierSelect.vue'
|
|
7
|
+
import { filterByAgentTier } from '~/utils/agentTier'
|
|
6
8
|
import { AGENT_CATEGORIES, OBSERVABILITY_GATE_ARCHETYPE } from '~/utils/catalog'
|
|
7
9
|
|
|
8
10
|
const { t } = useI18n()
|
|
9
11
|
const agents = useAgentsStore()
|
|
12
|
+
const agentTier = useAgentTierStore()
|
|
10
13
|
const releaseHealth = useReleaseHealthStore()
|
|
11
14
|
defineEmits<{ (e: 'add', kind: AgentKind): void }>()
|
|
12
15
|
// The purpose of the pipeline being built. When set to a non-`build` classifier, the
|
|
@@ -16,7 +19,7 @@ const props = defineProps<{ purpose?: PipelinePurpose | null }>()
|
|
|
16
19
|
|
|
17
20
|
// The post-release-health gate is only meaningful — and only accepted by the backend —
|
|
18
21
|
// with an observability integration connected, so it appears in the palette ONLY then.
|
|
19
|
-
const
|
|
22
|
+
const offered = computed(() => {
|
|
20
23
|
const all = releaseHealth.connection.connected
|
|
21
24
|
? [...agents.archetypes, OBSERVABILITY_GATE_ARCHETYPE]
|
|
22
25
|
: agents.archetypes
|
|
@@ -25,6 +28,13 @@ const palette = computed(() => {
|
|
|
25
28
|
return all.filter((a) => !a.category || purposeAllowsAgentCategory(props.purpose, a.category))
|
|
26
29
|
})
|
|
27
30
|
|
|
31
|
+
// Then narrow to the selected tier. Applied AFTER the purpose gate so the "n hidden" hint
|
|
32
|
+
// counts only what the TIER is holding back — a kind the pipeline's purpose rules out is not
|
|
33
|
+
// something a wider tier would reveal, so counting it would send the user chasing a control
|
|
34
|
+
// that cannot help them.
|
|
35
|
+
const palette = computed(() => filterByAgentTier(offered.value, agentTier.tier))
|
|
36
|
+
const hiddenByTier = computed(() => offered.value.length - palette.value.length)
|
|
37
|
+
|
|
28
38
|
// Group the palette into the ordered catalog categories, plus a trailing "Custom" bucket
|
|
29
39
|
// for runtime-added agents that carry no category. Empty groups are dropped.
|
|
30
40
|
const groups = computed(() => {
|
|
@@ -54,6 +64,7 @@ function toggle(id: string) {
|
|
|
54
64
|
<template>
|
|
55
65
|
<div class="space-y-2">
|
|
56
66
|
<p class="px-1 text-[11px] text-slate-500">{{ t('palette.hint') }}</p>
|
|
67
|
+
<AgentTierSelect :hidden-count="hiddenByTier" />
|
|
57
68
|
<div class="space-y-2">
|
|
58
69
|
<section v-for="g in groups" :key="g.id">
|
|
59
70
|
<button
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The agent-tier control, shared by the pipeline builder's palette and the model preset's
|
|
3
|
+
// per-agent override list (the two surfaces that enumerate the agent catalog).
|
|
4
|
+
//
|
|
5
|
+
// Tiers are CUMULATIVE, so this is a level dial rather than a set of independent toggles:
|
|
6
|
+
// `basic` is the everyday delivery loop, `intermediate` adds the kinds reached for regularly,
|
|
7
|
+
// and `advanced` — the widest level — shows the whole catalog. The trailing hint states how
|
|
8
|
+
// many kinds the current level is holding back, so a narrowed catalog never reads as the
|
|
9
|
+
// complete one.
|
|
10
|
+
import { computed } from 'vue'
|
|
11
|
+
import { AGENT_TIERS, type AgentTier } from '@cat-factory/contracts'
|
|
12
|
+
|
|
13
|
+
const { t } = useI18n()
|
|
14
|
+
const agentTier = useAgentTierStore()
|
|
15
|
+
|
|
16
|
+
const props = defineProps<{
|
|
17
|
+
/** How many kinds the current level hides. Renders the "n hidden" hint when > 0. */
|
|
18
|
+
hiddenCount?: number
|
|
19
|
+
}>()
|
|
20
|
+
|
|
21
|
+
// One STATIC literal `t()` key per tier (not a key assembled from the loop variable), so the
|
|
22
|
+
// typed-message-keys check covers them — the same shape the vendor/enum-keyed sets use.
|
|
23
|
+
const TIER_LABELS = computed<Record<AgentTier, string>>(() => ({
|
|
24
|
+
basic: t('agentTier.basic'),
|
|
25
|
+
intermediate: t('agentTier.intermediate'),
|
|
26
|
+
advanced: t('agentTier.advanced'),
|
|
27
|
+
}))
|
|
28
|
+
const TIER_HINTS = computed<Record<AgentTier, string>>(() => ({
|
|
29
|
+
basic: t('agentTier.basicHint'),
|
|
30
|
+
intermediate: t('agentTier.intermediateHint'),
|
|
31
|
+
advanced: t('agentTier.advancedHint'),
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
const items = computed(() => [
|
|
35
|
+
AGENT_TIERS.map((tier) => ({
|
|
36
|
+
label: TIER_LABELS.value[tier],
|
|
37
|
+
// The hint is what distinguishes "everything below this level" from "only this level",
|
|
38
|
+
// which the bare tier names cannot say.
|
|
39
|
+
description: TIER_HINTS.value[tier],
|
|
40
|
+
icon: tier === agentTier.tier ? 'i-lucide-check' : 'i-lucide-layers',
|
|
41
|
+
onSelect: () => agentTier.setTier(tier),
|
|
42
|
+
})),
|
|
43
|
+
])
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<div class="space-y-1">
|
|
48
|
+
<UDropdownMenu :items="items" :ui="{ content: 'z-[60] max-w-72' }">
|
|
49
|
+
<UButton
|
|
50
|
+
size="xs"
|
|
51
|
+
color="neutral"
|
|
52
|
+
variant="soft"
|
|
53
|
+
icon="i-lucide-layers"
|
|
54
|
+
trailing-icon="i-lucide-chevron-down"
|
|
55
|
+
class="w-full justify-between"
|
|
56
|
+
:title="t('agentTier.tooltip')"
|
|
57
|
+
>
|
|
58
|
+
<span class="truncate">
|
|
59
|
+
{{ t('agentTier.label') }}: {{ TIER_LABELS[agentTier.tier] }}
|
|
60
|
+
</span>
|
|
61
|
+
</UButton>
|
|
62
|
+
</UDropdownMenu>
|
|
63
|
+
<p v-if="props.hiddenCount" class="px-1 text-[10px] text-slate-500">
|
|
64
|
+
{{ t('agentTier.hidden', { count: props.hiddenCount }, props.hiddenCount) }}
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
@@ -54,6 +54,34 @@ function toggleGating(i: number) {
|
|
|
54
54
|
? { ...cfg.gating, enabled: false }
|
|
55
55
|
: { enabled: true, minRisk: 0.6, minImpact: 0.6, onMissingEstimate: 'consensus' }
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
// ---- The workspace consensus-GROUP tier set -------------------------------
|
|
59
|
+
// A step either authors its panel inline (the participants + gating editor below) or names a
|
|
60
|
+
// SET of workspace groups, each with its own estimate bar, and the engine runs the most
|
|
61
|
+
// demanding tier the task clears. The two are mutually exclusive by design — a non-empty tier
|
|
62
|
+
// set takes over — so the builder shows one editor or the other rather than both at once.
|
|
63
|
+
const consensusGroups = useConsensusGroupsStore()
|
|
64
|
+
|
|
65
|
+
/** Whether the draft step at `i` has escalated to the group library. */
|
|
66
|
+
function usesGroups(i: number): boolean {
|
|
67
|
+
return (pipelines.draftConsensus[i]?.groupIds?.length ?? 0) > 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isGroupSelected(i: number, groupId: string): boolean {
|
|
71
|
+
return pipelines.draftConsensus[i]?.groupIds?.includes(groupId) ?? false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The bar a group sets, as display text: its highest named threshold, or the "always" label when
|
|
76
|
+
* it is ungated. What a reader needs from the tier list is where each panel sits relative to the
|
|
77
|
+
* others, which is exactly this one number.
|
|
78
|
+
*/
|
|
79
|
+
function groupBarLabel(groupId: string): string {
|
|
80
|
+
const group = consensusGroups.groups.find((g) => g.id === groupId)
|
|
81
|
+
if (!group) return ''
|
|
82
|
+
const bar = consensusGroups.barFor(group)
|
|
83
|
+
return bar === null ? t('pipeline.builder.consensusGroupAlways') : `≥ ${bar}`
|
|
84
|
+
}
|
|
57
85
|
const agents = useAgentsStore()
|
|
58
86
|
const ui = useUiStore()
|
|
59
87
|
const uiMode = useUiModeStore()
|
|
@@ -760,109 +788,155 @@ async function clone(p: Pipeline) {
|
|
|
760
788
|
v-if="pipelines.draftConsensus[unit.index]?.enabled"
|
|
761
789
|
class="ms-6 space-y-2 rounded-md border border-emerald-800/40 bg-emerald-950/20 p-2 text-xs"
|
|
762
790
|
>
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
791
|
+
<!-- The workspace consensus-GROUP tier set: pick which reusable panels this
|
|
792
|
+
step may escalate to. Each group carries its own estimate bar; the engine
|
|
793
|
+
runs the most demanding one the task clears, and none clearing means the
|
|
794
|
+
standard single agent runs. Selecting any group replaces the inline panel
|
|
795
|
+
editor below, so the step has exactly one source of truth for its panel. -->
|
|
796
|
+
<div
|
|
797
|
+
v-if="consensusGroups.hasGroups"
|
|
798
|
+
class="space-y-1 border-b border-slate-800 pb-2"
|
|
799
|
+
>
|
|
800
|
+
<div class="flex items-center gap-1.5">
|
|
801
|
+
<UIcon name="i-lucide-layers" class="h-3.5 w-3.5 text-emerald-400" />
|
|
802
|
+
<span class="text-slate-300">{{ t('pipeline.builder.consensusGroups') }}</span>
|
|
803
|
+
</div>
|
|
804
|
+
<p class="text-[11px] text-slate-500">
|
|
805
|
+
{{ t('pipeline.builder.consensusGroupsHint') }}
|
|
806
|
+
</p>
|
|
807
|
+
<div class="flex flex-wrap gap-1">
|
|
808
|
+
<button
|
|
809
|
+
v-for="group in consensusGroups.groups"
|
|
810
|
+
:key="group.id"
|
|
811
|
+
type="button"
|
|
812
|
+
class="rounded border px-1.5 py-0.5 text-[11px]"
|
|
813
|
+
:class="
|
|
814
|
+
isGroupSelected(unit.index, group.id)
|
|
815
|
+
? 'border-emerald-600 bg-emerald-900/40 text-emerald-200'
|
|
816
|
+
: 'border-slate-700 bg-slate-900 text-slate-400 hover:text-slate-200'
|
|
817
|
+
"
|
|
818
|
+
:title="group.description"
|
|
819
|
+
@click="pipelines.toggleDraftConsensusGroup(unit.index, group.id)"
|
|
820
|
+
>
|
|
821
|
+
{{ group.name }}
|
|
822
|
+
<span class="ms-1 text-slate-500">{{ groupBarLabel(group.id) }}</span>
|
|
823
|
+
</button>
|
|
824
|
+
</div>
|
|
787
825
|
</div>
|
|
788
826
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
<
|
|
797
|
-
v-model="
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
827
|
+
<div v-if="usesGroups(unit.index)" class="text-[11px] text-slate-500">
|
|
828
|
+
{{ t('pipeline.builder.consensusGroupsActive') }}
|
|
829
|
+
</div>
|
|
830
|
+
|
|
831
|
+
<template v-else>
|
|
832
|
+
<div class="flex items-center gap-2">
|
|
833
|
+
<label class="text-slate-400">{{ t('pipeline.builder.strategy') }}</label>
|
|
834
|
+
<select
|
|
835
|
+
v-model="pipelines.draftConsensus[unit.index]!.strategy"
|
|
836
|
+
class="rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
837
|
+
>
|
|
838
|
+
<option v-for="s in CONSENSUS_STRATEGIES" :key="s.value" :value="s.value">
|
|
839
|
+
{{ s.label }}
|
|
840
|
+
</option>
|
|
841
|
+
</select>
|
|
842
|
+
<label
|
|
843
|
+
v-if="pipelines.draftConsensus[unit.index]!.strategy === 'debate'"
|
|
844
|
+
class="ms-2 text-slate-400"
|
|
845
|
+
>{{ t('pipeline.builder.rounds') }}</label
|
|
846
|
+
>
|
|
801
847
|
<input
|
|
802
|
-
v-
|
|
803
|
-
|
|
804
|
-
|
|
848
|
+
v-if="pipelines.draftConsensus[unit.index]!.strategy === 'debate'"
|
|
849
|
+
v-model.number="pipelines.draftConsensus[unit.index]!.rounds"
|
|
850
|
+
type="number"
|
|
851
|
+
min="1"
|
|
852
|
+
max="5"
|
|
853
|
+
placeholder="2"
|
|
854
|
+
class="w-12 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
805
855
|
/>
|
|
856
|
+
</div>
|
|
857
|
+
|
|
858
|
+
<!-- participants -->
|
|
859
|
+
<div class="space-y-1">
|
|
860
|
+
<div
|
|
861
|
+
v-for="(p, pIdx) in pipelines.draftConsensus[unit.index]!.participants"
|
|
862
|
+
:key="p.id"
|
|
863
|
+
class="flex items-center gap-1.5"
|
|
864
|
+
>
|
|
865
|
+
<input
|
|
866
|
+
v-model="p.role"
|
|
867
|
+
:placeholder="t('pipeline.builder.rolePlaceholder')"
|
|
868
|
+
class="w-28 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
869
|
+
/>
|
|
870
|
+
<input
|
|
871
|
+
v-model="p.modelId"
|
|
872
|
+
:placeholder="t('pipeline.builder.modelIdPlaceholder')"
|
|
873
|
+
class="flex-1 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-300"
|
|
874
|
+
/>
|
|
875
|
+
<UButton
|
|
876
|
+
icon="i-lucide-x"
|
|
877
|
+
color="error"
|
|
878
|
+
variant="ghost"
|
|
879
|
+
size="xs"
|
|
880
|
+
:disabled="pipelines.draftConsensus[unit.index]!.participants.length <= 2"
|
|
881
|
+
:title="t('pipeline.builder.removeParticipant')"
|
|
882
|
+
@click="removeParticipant(unit.index, pIdx)"
|
|
883
|
+
/>
|
|
884
|
+
</div>
|
|
806
885
|
<UButton
|
|
807
|
-
icon="i-lucide-
|
|
808
|
-
color="
|
|
886
|
+
icon="i-lucide-plus"
|
|
887
|
+
color="neutral"
|
|
809
888
|
variant="ghost"
|
|
810
889
|
size="xs"
|
|
811
|
-
:
|
|
812
|
-
|
|
813
|
-
@click="removeParticipant(unit.index, pIdx)"
|
|
890
|
+
:label="t('pipeline.builder.addParticipant')"
|
|
891
|
+
@click="addParticipant(unit.index)"
|
|
814
892
|
/>
|
|
815
893
|
</div>
|
|
816
|
-
<UButton
|
|
817
|
-
icon="i-lucide-plus"
|
|
818
|
-
color="neutral"
|
|
819
|
-
variant="ghost"
|
|
820
|
-
size="xs"
|
|
821
|
-
:label="t('pipeline.builder.addParticipant')"
|
|
822
|
-
@click="addParticipant(unit.index)"
|
|
823
|
-
/>
|
|
824
|
-
</div>
|
|
825
894
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
<label class="text-slate-400">{{ t('pipeline.builder.riskThreshold') }}</label>
|
|
845
|
-
<input
|
|
846
|
-
v-model.number="pipelines.draftConsensus[unit.index]!.gating!.minRisk"
|
|
847
|
-
type="number"
|
|
848
|
-
min="0"
|
|
849
|
-
max="1"
|
|
850
|
-
step="0.1"
|
|
851
|
-
class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
852
|
-
/>
|
|
853
|
-
<label class="text-slate-400">{{
|
|
854
|
-
t('pipeline.builder.impactThreshold')
|
|
855
|
-
}}</label>
|
|
856
|
-
<input
|
|
857
|
-
v-model.number="pipelines.draftConsensus[unit.index]!.gating!.minImpact"
|
|
858
|
-
type="number"
|
|
859
|
-
min="0"
|
|
860
|
-
max="1"
|
|
861
|
-
step="0.1"
|
|
862
|
-
class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
895
|
+
<!-- gating -->
|
|
896
|
+
<div class="flex flex-wrap items-center gap-2 border-t border-slate-800 pt-2">
|
|
897
|
+
<UButton
|
|
898
|
+
:icon="
|
|
899
|
+
pipelines.draftConsensus[unit.index]!.gating?.enabled
|
|
900
|
+
? 'i-lucide-toggle-right'
|
|
901
|
+
: 'i-lucide-toggle-left'
|
|
902
|
+
"
|
|
903
|
+
:color="
|
|
904
|
+
pipelines.draftConsensus[unit.index]!.gating?.enabled
|
|
905
|
+
? 'success'
|
|
906
|
+
: 'neutral'
|
|
907
|
+
"
|
|
908
|
+
variant="ghost"
|
|
909
|
+
size="xs"
|
|
910
|
+
:label="t('pipeline.builder.gateOnEstimate')"
|
|
911
|
+
:title="t('pipeline.builder.consensusGateTooltip')"
|
|
912
|
+
@click="toggleGating(unit.index)"
|
|
863
913
|
/>
|
|
864
|
-
|
|
865
|
-
|
|
914
|
+
<template v-if="pipelines.draftConsensus[unit.index]!.gating?.enabled">
|
|
915
|
+
<label class="text-slate-400">{{
|
|
916
|
+
t('pipeline.builder.riskThreshold')
|
|
917
|
+
}}</label>
|
|
918
|
+
<input
|
|
919
|
+
v-model.number="pipelines.draftConsensus[unit.index]!.gating!.minRisk"
|
|
920
|
+
type="number"
|
|
921
|
+
min="0"
|
|
922
|
+
max="1"
|
|
923
|
+
step="0.1"
|
|
924
|
+
class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
925
|
+
/>
|
|
926
|
+
<label class="text-slate-400">{{
|
|
927
|
+
t('pipeline.builder.impactThreshold')
|
|
928
|
+
}}</label>
|
|
929
|
+
<input
|
|
930
|
+
v-model.number="pipelines.draftConsensus[unit.index]!.gating!.minImpact"
|
|
931
|
+
type="number"
|
|
932
|
+
min="0"
|
|
933
|
+
max="1"
|
|
934
|
+
step="0.1"
|
|
935
|
+
class="w-14 rounded border border-slate-700 bg-slate-900 px-1.5 py-0.5 text-slate-100"
|
|
936
|
+
/>
|
|
937
|
+
</template>
|
|
938
|
+
</div>
|
|
939
|
+
</template>
|
|
866
940
|
</div>
|
|
867
941
|
|
|
868
942
|
<!-- Test quality-control companion config (shown when QC is enabled on a Tester
|
|
@@ -3,15 +3,22 @@
|
|
|
3
3
|
// `usePipelineHealth` reports any issue. Lists:
|
|
4
4
|
// • new built-in pipelines the workspace doesn't have yet (ADD them);
|
|
5
5
|
// • invalid pipelines (unknown agent kind / bad shape) — DELETE a custom one, RESEED a built-in;
|
|
6
|
-
// • outdated built-ins (a newer catalog definition is available) — RESEED to adopt it
|
|
6
|
+
// • outdated built-ins (a newer catalog definition is available) — RESEED to adopt it;
|
|
7
|
+
// • RETIRED built-ins (withdrawn from the catalog) — REMOVE them; there is nothing left to
|
|
8
|
+
// reseed from, which is why they are the one built-in the backend lets a delete through.
|
|
7
9
|
// Adding a new built-in and reseeding an existing one are the same reseed call (it creates or
|
|
8
10
|
// updates by catalog id). Detection is client-side (see usePipelineHealth); the actions hit the
|
|
9
11
|
// pipelines store.
|
|
10
12
|
const { t } = useI18n()
|
|
11
13
|
const ui = useUiStore()
|
|
12
14
|
const pipelines = usePipelinesStore()
|
|
13
|
-
const { invalid, outdated, newPipelines, hasIssues } = usePipelineHealth()
|
|
14
|
-
|
|
15
|
+
const { invalid, outdated, newPipelines, retired, hasIssues } = usePipelineHealth()
|
|
16
|
+
// Failures go through the shared conflict presenter rather than a raw `toast.add`: the refusals
|
|
17
|
+
// this screen actually provokes are 409s (a recurring schedule still points at the pipeline), and
|
|
18
|
+
// those carry a machine-readable `details.reason` the presenter turns into translated remedy copy.
|
|
19
|
+
// Dumping `error.message` instead would put untranslated backend prose in front of every non-English
|
|
20
|
+
// user — on the one screen whose whole purpose is telling them what to do next.
|
|
21
|
+
const { present } = usePipelineErrorToast()
|
|
15
22
|
|
|
16
23
|
const open = computed({
|
|
17
24
|
get: () => ui.pipelineHealthOpen,
|
|
@@ -25,17 +32,14 @@ const busy = ref<Set<string>>(new Set())
|
|
|
25
32
|
const isBusy = (id: string) => busy.value.has(id)
|
|
26
33
|
const anyBusy = computed(() => busy.value.size > 0)
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
/** `failTitleKey` is an i18n KEY (not resolved copy) — `present` uses it only when the failure has
|
|
36
|
+
* no mapped conflict reason of its own. */
|
|
37
|
+
async function run(id: string, action: () => Promise<unknown>, failTitleKey: string) {
|
|
29
38
|
busy.value = new Set(busy.value).add(id)
|
|
30
39
|
try {
|
|
31
40
|
await action()
|
|
32
41
|
} catch (e) {
|
|
33
|
-
|
|
34
|
-
title: failTitle,
|
|
35
|
-
description: e instanceof Error ? e.message : String(e),
|
|
36
|
-
icon: 'i-lucide-triangle-alert',
|
|
37
|
-
color: 'error',
|
|
38
|
-
})
|
|
42
|
+
present(e, failTitleKey)
|
|
39
43
|
} finally {
|
|
40
44
|
const next = new Set(busy.value)
|
|
41
45
|
next.delete(id)
|
|
@@ -44,9 +48,19 @@ async function run(id: string, action: () => Promise<unknown>, failTitle: string
|
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
const reseed = (id: string) =>
|
|
47
|
-
run(id, () => pipelines.reseed(id),
|
|
51
|
+
run(id, () => pipelines.reseed(id), 'pipeline.health.toast.reseedFailed')
|
|
48
52
|
const remove = (id: string) =>
|
|
49
|
-
run(id, () => pipelines.removePipeline(id),
|
|
53
|
+
run(id, () => pipelines.removePipeline(id), 'pipeline.health.toast.deleteFailed')
|
|
54
|
+
// Same call as `remove`, different failure copy: the retired section says "Remove" (the pipeline is
|
|
55
|
+
// gone from the catalog), so a failure toast reading "could not DELETE" would name an action the
|
|
56
|
+
// user was never offered. This is only the FALLBACK title — the likely failure here is a recurring
|
|
57
|
+
// schedule still pointing at the pipeline, which arrives as a 409 the presenter words itself.
|
|
58
|
+
const removeRetired = (id: string) =>
|
|
59
|
+
run(id, () => pipelines.removePipeline(id), 'pipeline.health.toast.removeFailed')
|
|
60
|
+
|
|
61
|
+
// Removals are deliberately per-row with no bulk twin, unlike the reseeds below: a reseed restores
|
|
62
|
+
// what the catalog says, while a delete is the one irreversible action on this screen (a built-in
|
|
63
|
+
// the catalog no longer defines cannot be reseeded back). One click per pipeline is the point.
|
|
50
64
|
|
|
51
65
|
/** Reseed every reseedable pipeline (new + outdated built-ins + invalid built-ins) in one go. */
|
|
52
66
|
async function reseedAll() {
|
|
@@ -179,6 +193,48 @@ const reseedableCount = computed(
|
|
|
179
193
|
</ul>
|
|
180
194
|
</section>
|
|
181
195
|
|
|
196
|
+
<!-- Retired built-ins: withdrawn from the catalog, so the only action is removal. -->
|
|
197
|
+
<section v-if="retired.length" class="space-y-2">
|
|
198
|
+
<div class="flex items-center gap-2">
|
|
199
|
+
<UIcon name="i-lucide-archive-x" class="h-4 w-4 text-slate-400" />
|
|
200
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
201
|
+
{{ t('pipeline.health.retiredHeading') }}
|
|
202
|
+
</h3>
|
|
203
|
+
</div>
|
|
204
|
+
<p class="text-[11px] text-slate-500">{{ t('pipeline.health.retiredDescription') }}</p>
|
|
205
|
+
<ul class="space-y-2">
|
|
206
|
+
<li
|
|
207
|
+
v-for="r in retired"
|
|
208
|
+
:key="r.pipeline.id"
|
|
209
|
+
class="flex items-center justify-between gap-3 rounded-lg border border-slate-800 bg-slate-900/40 p-3"
|
|
210
|
+
>
|
|
211
|
+
<div class="min-w-0">
|
|
212
|
+
<span class="truncate text-sm font-medium text-slate-100">{{
|
|
213
|
+
r.pipeline.name
|
|
214
|
+
}}</span>
|
|
215
|
+
<p class="text-[11px] text-slate-400/80">
|
|
216
|
+
{{
|
|
217
|
+
r.replacement
|
|
218
|
+
? t('pipeline.health.retiredReplacedBy', { name: r.replacement.name })
|
|
219
|
+
: t('pipeline.health.retiredNote')
|
|
220
|
+
}}
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
<UButton
|
|
224
|
+
size="xs"
|
|
225
|
+
color="error"
|
|
226
|
+
variant="subtle"
|
|
227
|
+
icon="i-lucide-trash-2"
|
|
228
|
+
:loading="isBusy(r.pipeline.id)"
|
|
229
|
+
:disabled="anyBusy"
|
|
230
|
+
@click="removeRetired(r.pipeline.id)"
|
|
231
|
+
>
|
|
232
|
+
{{ t('pipeline.health.remove') }}
|
|
233
|
+
</UButton>
|
|
234
|
+
</li>
|
|
235
|
+
</ul>
|
|
236
|
+
</section>
|
|
237
|
+
|
|
182
238
|
<!-- Outdated built-ins: a newer catalog version is available. -->
|
|
183
239
|
<section v-if="outdated.length" class="space-y-2">
|
|
184
240
|
<div class="flex items-center gap-2">
|