diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 6bfe081df2..7e01508f55 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -2136,6 +2136,10 @@
"sortZA": "Z-A",
"sortRecent": "Recent",
"sortPopular": "Popular",
+ "ownership": "Ownership",
+ "ownershipAll": "All",
+ "ownershipMyModels": "My models",
+ "ownershipPublicModels": "Public models",
"selectFrameworks": "Select Frameworks",
"selectProjects": "Select Projects",
"sortingType": "Sorting Type",
diff --git a/src/platform/assets/components/AssetBrowserModal.vue b/src/platform/assets/components/AssetBrowserModal.vue
index ef85ce5f9a..e593861774 100644
--- a/src/platform/assets/components/AssetBrowserModal.vue
+++ b/src/platform/assets/components/AssetBrowserModal.vue
@@ -48,6 +48,7 @@
diff --git a/src/platform/assets/components/AssetFilterBar.vue b/src/platform/assets/components/AssetFilterBar.vue
index a86a7a1a84..0a4fe0aa6a 100644
--- a/src/platform/assets/components/AssetFilterBar.vue
+++ b/src/platform/assets/components/AssetFilterBar.vue
@@ -26,6 +26,16 @@
data-component-id="asset-filter-base-models"
@update:model-value="handleFilterChange"
/>
+
+
@@ -46,7 +56,7 @@
diff --git a/src/platform/assets/composables/useAssetBrowser.ts b/src/platform/assets/composables/useAssetBrowser.ts
index e5913e94a9..083cf0e6e4 100644
--- a/src/platform/assets/composables/useAssetBrowser.ts
+++ b/src/platform/assets/composables/useAssetBrowser.ts
@@ -35,6 +35,15 @@ function filterByBaseModels(models: string[]) {
}
}
+function filterByOwnership(ownership: string) {
+ return (asset: AssetItem) => {
+ if (ownership === 'all') return true
+ if (ownership === 'my-models') return asset.is_immutable === false
+ if (ownership === 'public-models') return asset.is_immutable === true
+ return true
+ }
+}
+
type AssetBadge = {
label: string
type: 'type' | 'base' | 'size'
@@ -65,7 +74,8 @@ export function useAssetBrowser(
const filters = ref({
sortBy: 'recent',
fileFormats: [],
- baseModels: []
+ baseModels: [],
+ ownership: 'all'
})
// Transform API asset to display asset
@@ -176,6 +186,7 @@ export function useAssetBrowser(
const filtered = searchFiltered.value
.filter(filterByFileFormats(filters.value.fileFormats))
.filter(filterByBaseModels(filters.value.baseModels))
+ .filter(filterByOwnership(filters.value.ownership))
const sortedAssets = [...filtered]
sortedAssets.sort((a, b) => {
diff --git a/src/platform/assets/fixtures/ui-mock-assets.ts b/src/platform/assets/fixtures/ui-mock-assets.ts
index 482a48af93..a31c7b859c 100644
--- a/src/platform/assets/fixtures/ui-mock-assets.ts
+++ b/src/platform/assets/fixtures/ui-mock-assets.ts
@@ -146,9 +146,15 @@ export function createAssetWithoutUserMetadata() {
return asset
}
-export function createAssetWithSpecificExtension(extension: string) {
+export function createAssetWithSpecificExtension(
+ extension: string,
+ isImmutable?: boolean
+) {
const asset = createMockAssets(1)[0]
asset.name = `test-model.${extension}`
+ if (isImmutable !== undefined) {
+ asset.is_immutable = isImmutable
+ }
return asset
}
diff --git a/tests-ui/platform/assets/components/AssetFilterBar.test.ts b/tests-ui/platform/assets/components/AssetFilterBar.test.ts
index d9762e9782..1f412d8096 100644
--- a/tests-ui/platform/assets/components/AssetFilterBar.test.ts
+++ b/tests-ui/platform/assets/components/AssetFilterBar.test.ts
@@ -105,8 +105,11 @@ describe('AssetFilterBar', () => {
await nextTick()
// Update sort
- const sortSelect = wrapper.findComponent({ name: 'SingleSelect' })
- await sortSelect.vm.$emit('update:modelValue', 'popular')
+ const sortSelect = wrapper
+ .findAllComponents({ name: 'SingleSelect' })
+ .find((component) => component.props('label') === 'assetBrowser.sortBy')
+ expect(sortSelect).toBeTruthy()
+ await sortSelect!.vm.$emit('update:modelValue', 'popular')
await nextTick()
@@ -247,5 +250,95 @@ describe('AssetFilterBar', () => {
const multiSelects = wrapper.findAllComponents({ name: 'MultiSelect' })
expect(multiSelects).toHaveLength(0)
})
+
+ it('hides ownership filter when no mutable assets', () => {
+ const assets = [
+ createAssetWithSpecificExtension('safetensors', true) // immutable
+ ]
+ const wrapper = mountAssetFilterBar({ assets })
+
+ const ownershipSelects = wrapper
+ .findAllComponents({ name: 'SingleSelect' })
+ .filter(
+ (component) => component.props('label') === 'assetBrowser.ownership'
+ )
+
+ expect(ownershipSelects).toHaveLength(0)
+ })
+
+ it('shows ownership filter when mutable assets exist', () => {
+ const assets = [
+ createAssetWithSpecificExtension('safetensors', false) // mutable
+ ]
+ const wrapper = mountAssetFilterBar({ assets, allAssets: assets })
+
+ const ownershipSelects = wrapper
+ .findAllComponents({ name: 'SingleSelect' })
+ .filter(
+ (component) => component.props('label') === 'assetBrowser.ownership'
+ )
+
+ expect(ownershipSelects).toHaveLength(1)
+ })
+
+ it('shows ownership filter when mixed assets exist', () => {
+ const assets = [
+ createAssetWithSpecificExtension('safetensors', true), // immutable
+ createAssetWithSpecificExtension('ckpt', false) // mutable
+ ]
+ const wrapper = mountAssetFilterBar({ assets, allAssets: assets })
+
+ const ownershipSelects = wrapper
+ .findAllComponents({ name: 'SingleSelect' })
+ .filter(
+ (component) => component.props('label') === 'assetBrowser.ownership'
+ )
+
+ expect(ownershipSelects).toHaveLength(1)
+ })
+ })
+
+ describe('Ownership Filter', () => {
+ it('emits ownership filter changes', async () => {
+ const assets = [
+ createAssetWithSpecificExtension('safetensors', false) // mutable
+ ]
+ const wrapper = mountAssetFilterBar({ assets, allAssets: assets })
+
+ const ownershipSelect = wrapper
+ .findAllComponents({ name: 'SingleSelect' })
+ .find(
+ (component) => component.props('label') === 'assetBrowser.ownership'
+ )
+
+ expect(ownershipSelect).toBeTruthy()
+ await ownershipSelect!.vm.$emit('update:modelValue', 'my-models')
+ await nextTick()
+
+ const emitted = wrapper.emitted('filterChange')
+ expect(emitted).toHaveLength(1)
+
+ const filterState = emitted![0][0] as FilterState
+ expect(filterState.ownership).toBe('my-models')
+ })
+
+ it('ownership filter defaults to "all"', async () => {
+ const assets = [
+ createAssetWithSpecificExtension('safetensors', false) // mutable
+ ]
+ const wrapper = mountAssetFilterBar({ assets })
+
+ const sortSelect = wrapper
+ .findAllComponents({ name: 'SingleSelect' })
+ .find((component) => component.props('label') === 'assetBrowser.sortBy')
+
+ expect(sortSelect).toBeTruthy()
+ await sortSelect!.vm.$emit('update:modelValue', 'recent')
+ await nextTick()
+
+ const emitted = wrapper.emitted('filterChange')
+ const filterState = emitted![0][0] as FilterState
+ expect(filterState.ownership).toBe('all')
+ })
})
})
diff --git a/tests-ui/platform/assets/composables/useAssetBrowser.test.ts b/tests-ui/platform/assets/composables/useAssetBrowser.test.ts
index 92ea78c33a..9c724f32b7 100644
--- a/tests-ui/platform/assets/composables/useAssetBrowser.test.ts
+++ b/tests-ui/platform/assets/composables/useAssetBrowser.test.ts
@@ -249,7 +249,8 @@ describe('useAssetBrowser', () => {
updateFilters({
sortBy: 'name-asc',
fileFormats: ['safetensors'],
- baseModels: []
+ baseModels: [],
+ ownership: 'all'
})
await nextTick()
@@ -284,7 +285,8 @@ describe('useAssetBrowser', () => {
updateFilters({
sortBy: 'name-asc',
fileFormats: [],
- baseModels: ['SDXL']
+ baseModels: ['SDXL'],
+ ownership: 'all'
})
await nextTick()
@@ -335,7 +337,12 @@ describe('useAssetBrowser', () => {
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
- updateFilters({ sortBy: 'name', fileFormats: [], baseModels: [] })
+ updateFilters({
+ sortBy: 'name',
+ fileFormats: [],
+ baseModels: [],
+ ownership: 'all'
+ })
await nextTick()
const names = filteredAssets.value.map((asset) => asset.name)
@@ -355,7 +362,12 @@ describe('useAssetBrowser', () => {
const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
- updateFilters({ sortBy: 'recent', fileFormats: [], baseModels: [] })
+ updateFilters({
+ sortBy: 'recent',
+ fileFormats: [],
+ baseModels: [],
+ ownership: 'all'
+ })
await nextTick()
const dates = filteredAssets.value.map((asset) => asset.created_at)
@@ -367,6 +379,92 @@ describe('useAssetBrowser', () => {
})
})
+ describe('Ownership filtering', () => {
+ it('filters by ownership - all', async () => {
+ const assets = [
+ createApiAsset({ name: 'my-model.safetensors', is_immutable: false }),
+ createApiAsset({
+ name: 'public-model.safetensors',
+ is_immutable: true
+ }),
+ createApiAsset({
+ name: 'another-my-model.safetensors',
+ is_immutable: false
+ })
+ ]
+
+ const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
+
+ updateFilters({
+ sortBy: 'name-asc',
+ fileFormats: [],
+ baseModels: [],
+ ownership: 'all'
+ })
+ await nextTick()
+
+ expect(filteredAssets.value).toHaveLength(3)
+ })
+
+ it('filters by ownership - my models only', async () => {
+ const assets = [
+ createApiAsset({ name: 'my-model.safetensors', is_immutable: false }),
+ createApiAsset({
+ name: 'public-model.safetensors',
+ is_immutable: true
+ }),
+ createApiAsset({
+ name: 'another-my-model.safetensors',
+ is_immutable: false
+ })
+ ]
+
+ const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
+
+ updateFilters({
+ sortBy: 'name-asc',
+ fileFormats: [],
+ baseModels: [],
+ ownership: 'my-models'
+ })
+ await nextTick()
+
+ expect(filteredAssets.value).toHaveLength(2)
+ expect(filteredAssets.value.every((asset) => !asset.is_immutable)).toBe(
+ true
+ )
+ })
+
+ it('filters by ownership - public models only', async () => {
+ const assets = [
+ createApiAsset({ name: 'my-model.safetensors', is_immutable: false }),
+ createApiAsset({
+ name: 'public-model.safetensors',
+ is_immutable: true
+ }),
+ createApiAsset({
+ name: 'another-public-model.safetensors',
+ is_immutable: true
+ })
+ ]
+
+ const { updateFilters, filteredAssets } = useAssetBrowser(ref(assets))
+
+ updateFilters({
+ sortBy: 'name-asc',
+ fileFormats: [],
+ baseModels: [],
+ ownership: 'public-models'
+ })
+ await nextTick()
+
+ expect(filteredAssets.value).toHaveLength(2)
+ expect(filteredAssets.value.every((asset) => asset.is_immutable)).toBe(
+ true
+ )
+ })
+ })
+
describe('Dynamic Category Extraction', () => {
it('extracts categories from asset tags', () => {
const assets = [