## Summary The template picker's search now surfaces the right template for how people actually type — abbreviations, typos, multi-word intent, and non-Latin (CJK) titles — and orders results by real popularity instead of a fuzzy-match score that was being thrown away. Search and ranking now behave the same here as they do on the workflow hub. ## Changes **What** - Searching for the way people phrase things now works: `t2v`, `i2v`, `cn` expand to their full modality terms, `img2img`/`v2v` expand to editing (matching how the catalog tags image/video edit templates), `flux upscale` and `sdxl lora` match across title/model/tag fields together, prefixes like `vid` match `video`, and typos like `contorlnet` still find ControlNet. Versioned names tokenize sensibly, so `wan 2.2` and `wan2.2` both hit, while `2.5` never blurs into `3.5`. - CJK titles are searchable. Unspaced Han/Hiragana/Katakana runs are tokenized into character unigrams and bigrams, so a substring a user types (`放大` inside `图像放大`, or the single trailing `大`) lands on a match. Korean and other spaced scripts fall to the normal word tokenizer, unchanged. - Fuzzy matching is tighter: a term now tolerates edits up to 20% of its length (down from a flat threshold), so `contorlnet` still finds ControlNet but `upscale` no longer fuzzy-matches the shorter, unrelated `scale`. Short (≤3-char) and digit-bearing terms stay exact. - Results lead with text relevance. Previously the fuzzy match score was computed and then discarded, and any active sort re-ordered results by usage — so the best textual match rarely landed on top. Now relevance is the authoritative order while a query is active, and when two results match about equally well, the more-used template wins the tie (dampened so one runaway-popular template can't dominate). - The ranking is a stable total order. Scores are bucketed before usage breaks ties, so a cluster of near-equally-relevant results always sorts the same way — a naive per-pair "within X%" comparison is intransitive and makes the order depend on internal input order (it can even shuffle as you type another character). - "Popular" ranks by raw usage, matching what the hub and the search index show. It previously blended in a freshness term that pushed newer, less-used templates above genuinely popular ones. - The sort dropdown works during search again: it defaults to "Relevance" but you can switch to Popular/Newest/etc. to re-order the results, and your browse sort is restored (and never overwritten by a search-time choice) when you clear the query. - Alphabetical sort reads correctly: it sorts by the title shown on the card, trims stray leading whitespace that used to jump templates to the top, and groups number-prefixed titles after the letters instead of ahead of them. - Filter telemetry now reports the sort the user is actually seeing (relevance while searching) rather than the persisted browse sort, so analytics reflect the visible ordering. - Removed the old runtime Fuse-options override path, which is obsolete under the new engine. **Breaking** None. Existing filters (Model / Use Case / Runs On / distribution), pagination, and persisted sort settings are unchanged; the relevance mode is search-only and never persisted. ## Review Focus - The ranking crux is `rankByRelevanceThenUsage` in `templateSearchConfig.ts`: relevance is primary, usage only re-orders results in the same score bucket, and bucketing keeps it a stable total order. That's the one function to review for correctness. - The CJK tokenizer (`cjkGrams` / `tokenize` in `templateSearchConfig.ts`): script-matched so only unspaced scripts are grammed, and a pure-CJK run relies on its grams (no whole-word token). Splitting by code point is safe here (these scripts are BMP-only; emoji are excluded by the run regex). - Deliberately not touched: the "Recommended" sort keeps its curated blend (usage + editorial rank + freshness) so it stays distinct from "Popular"; `vram-low-to-high` remains unimplemented exactly as on main. ## Tradeoffs / notes - Adds `minisearch` (~18 kB gzip). The template selector is where it's used; accepted for the search-quality gain (a later change could lazy-load it if bundle size becomes a concern). - Bucketing means two results just across a bucket boundary don't tie-break on usage even when their scores are close — the accepted cost of a transitive, predictable order (this mirrors how the search index quantizes relevance). - `img2img` expands to editing (not literal "image to image") because the catalog labels those templates "Image Edit" — verified against the real data. - CJK bigrams roughly double the token count for a pure-CJK title; negligible at catalog scale (~550 templates, short titles). ## Testing Behavioral coverage over the real search paths, not the mocks — the ranking and tokenizer run against actual MiniSearch output; only the ranking-store math is mocked. Also verified against the full ~550-template catalog end to end (all query types stable, zero input-order-dependent orderings). ### Behavior matrix (verified on the real catalog) | Input / action | Now | Previously | | --- | --- | --- | | `img2img` | Image-editing templates (Qwen Image Edit, …) | Matched every "image" template — intent lost | | `flux upscale` | Flux upscale templates (matches both terms across fields) | **No results** (single-field fuzzy couldn't span title + tag) | | `sdxl lora` | SDXL templates | **No results** | | `t2v` / `i2v` / `cn` | Expand to text→video / image→video / controlnet | Only partial slug hits, if any | | `vid` (prefix) | Matches `video` templates | Unreliable | | `contorlnet` (typo) | Finds ControlNet | Often dropped by the strict threshold | | `upscale` | Matches upscale titles only | Fuzzy-matched the unrelated substring `scale` | | `放大` / `大` (CJK) | Matches `图像放大` and other titles containing the run | No match — CJK titles were unsearchable by substring | | `wan 2.2` and `wan2.2` | Both match; `2.5` never matches `3.5` | Space vs no-space degraded the match | | Near-tied cluster (e.g. `upscale`) | Stable order every time | Reordered depending on input order (could shuffle as you type) | | Query active, "Popular" selected | Best textual match still leads; usage breaks near-ties | Sort re-ordered by usage, burying the best match | | Change sort while searching | Re-orders the search results; relevance is the default | Sort was locked; dropdown had no effect | | Clear the search | Restores the browse sort you had before | — | | "Popular" sort | Orders by raw usage (matches hub / index) | Freshness blend pushed newer low-usage templates up | | A–Z sort | Letters first (`ACE…`), number-prefixed titles last (`3x3…`, `360…`); leading whitespace ignored | Leading-space titles jumped to the top; numbers sorted before letters | ### Unit tests (81 total, all passing) - `templateSearchConfig.test.ts` (34) — tokenizer identifier/version splits, CJK unigram/bigram gramming (and Korean left as a spaced word), per-term fuzziness (`upscale` ≠ `scale`), abbreviation expansion (incl. `img2img`→edit intent), prefix + typo matching, AND-then-OR, literal-before-expansion ordering, relevance>tag>description ranking, and `rankByRelevanceThenUsage` giving a stable order on an intransitive cluster. - `useTemplateFiltering.test.ts` (35) — the `img2img` / `flux upscale` / `sdxl lora` regressions, relevance-default-on-search, override-sort-while-searching, browse-sort restore on clear, ephemeral mid-search sort, telemetry reporting the visible sort, Runs-On filter, empty-result guard, filters preserving relevance order, and alphabetical trimming + numbers-after-letters. - `templateRankingStore.test.ts` (12) — freshness and default-score (recommended) math. Gate: `pnpm typecheck`, `pnpm lint`, `pnpm knip` clean. ## Screen Recording (if applicable) https://github.com/user-attachments/assets/6748a3f7-e69d-44ac-826c-71990c8dce90
Stores
This directory contains Pinia stores for the ComfyUI frontend application. Stores provide centralized state management for the application.
Table of Contents
Overview
Stores in ComfyUI use Pinia, Vue's official state management library. Each store is responsible for managing a specific domain of the application state, such as user data, workflow information, graph state, and UI configuration.
Stores provide a way to maintain global application state that can be accessed from any component, regardless of where those components are in the component hierarchy. This solves the problem of "prop drilling" (passing data down through multiple levels of components) and allows components that aren't directly related to share and modify the same state.
For example, without global state:
App
│
┌──────────┴──────────┐
│ │
HeaderBar Canvas
│ │
│ │
UserMenu NodeProperties
In this structure, if the UserMenu component needs to update something that affects NodeProperties, the data would need to be passed up to App and then down again, through all intermediate components.
With Pinia stores, components can directly access and update the shared state:
┌─────────────────┐
│ │
│ Pinia Stores │
│ │
└───────┬─────────┘
│
│ Accessed by
▼
┌──────────────────────────┐
│ │
│ Components │
│ │
└──────────────────────────┘
Store Architecture
The store architecture in ComfyUI follows these principles:
- Domain-driven: Each store focuses on a specific domain of the application
- Single source of truth: Stores serve as the definitive source for specific data
- Composition: Stores can interact with other stores when needed
- Actions for logic: Business logic is encapsulated in store actions
- Getters for derived state: Computed values are exposed via getters
The following diagram illustrates the store architecture and data flow:
┌─────────────────────────────────────────────────────────┐
│ Vue Components │
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Component A │ │ Component B │ │
│ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │
└───────────┼────────────────────────────┼────────────────┘
│ │
│ ┌───────────────┐ │
└────►│ Composables │◄─────┘
└───────┬───────┘
│
┌─────────────────────────┼─────────────────────────────┐
│ Pinia Stores │ │
│ │ │
│ ┌───────────────────▼───────────────────────┐ │
│ │ Actions │ │
│ └───────────────────┬───────────────────────┘ │
│ │ │
│ ┌───────────────────▼───────────────────────┐ │
│ │ State │ │
│ └───────────────────┬───────────────────────┘ │
│ │ │
│ ┌───────────────────▼───────────────────────┐ │
│ │ Getters │ │
│ └───────────────────┬───────────────────────┘ │
│ │ │
└─────────────────────────┼─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ External Services │
│ (API, localStorage, WebSocket, etc.) │
└─────────────────────────────────────────────────────────┘
Core Stores
The following table lists ALL 46 store instances in the system as of 2026-01-29:
Main Stores
| File | Store | Description | Category |
|---|---|---|---|
| aboutPanelStore.ts | useAboutPanelStore | Manages the About panel state and badges | UI |
| apiKeyAuthStore.ts | useApiKeyAuthStore | Handles API key authentication | Auth |
| comfyManagerStore.ts | useComfyManagerStore | Manages ComfyUI application state | Core |
| comfyRegistryStore.ts | useComfyRegistryStore | Handles extensions registry | Registry |
| commandStore.ts | useCommandStore | Manages commands and command execution | Core |
| dialogStore.ts | useDialogStore | Controls dialog/modal display and state | UI |
| domWidgetStore.ts | useDomWidgetStore | Manages DOM widget state | Widgets |
| electronDownloadStore.ts | useElectronDownloadStore | Handles Electron-specific download operations | Platform |
| executionStore.ts | useExecutionStore | Tracks workflow execution state | Execution |
| extensionStore.ts | useExtensionStore | Manages extension registration and state | Extensions |
| firebaseAuthStore.ts | useFirebaseAuthStore | Handles Firebase authentication | Auth |
| graphStore.ts | useTitleEditorStore | Manages title editing for nodes and groups | UI |
| graphStore.ts | useCanvasStore | Manages the graph canvas state and interactions | Core |
| helpCenterStore.ts | useHelpCenterStore | Manages help center visibility and state | UI |
| nodeOutputStore.ts | useNodeOutputStore | Manages node outputs and execution results | Media |
| maintenanceTaskStore.ts | useMaintenanceTaskStore | Handles system maintenance tasks | System |
| menuItemStore.ts | useMenuItemStore | Handles menu items and their state | UI |
| modelStore.ts | useModelStore | Manages AI models information | Models |
| modelToNodeStore.ts | useModelToNodeStore | Maps models to compatible nodes | Models |
| nodeBookmarkStore.ts | useNodeBookmarkStore | Manages node bookmarks and favorites | Nodes |
| nodeDefStore.ts | useNodeDefStore | Manages node definitions and schemas | Nodes |
| nodeDefStore.ts | useNodeFrequencyStore | Tracks node usage frequency | Nodes |
| queueStore.ts | useQueueStore | Manages execution queue and task history | Execution |
| queueStore.ts | useQueuePendingTaskCountStore | Tracks pending task counts | Execution |
| queueStore.ts | useQueueSettingsStore | Manages queue execution settings | Execution |
| releaseStore.ts | useReleaseStore | Manages application release information | System |
| serverConfigStore.ts | useServerConfigStore | Handles server configuration | Config |
| settingStore.ts | useSettingStore | Manages application settings | Config |
| subgraphNavigationStore.ts | useSubgraphNavigationStore | Handles subgraph navigation state | Navigation |
| systemStatsStore.ts | useSystemStatsStore | Tracks system performance statistics | System |
| toastStore.ts | useToastStore | Manages toast notifications | UI |
| userFileStore.ts | useUserFileStore | Manages user file operations | Files |
| userStore.ts | useUserStore | Manages user data and preferences | User |
| versionCompatibilityStore.ts | useVersionCompatibilityStore | Manages frontend/backend version compatibility warnings | Core |
| widgetStore.ts | useWidgetStore | Manages widget configurations | Widgets |
| workflowStore.ts | useWorkflowStore | Handles workflow data and operations | Workflows |
| workflowStore.ts | useWorkflowBookmarkStore | Manages workflow bookmarks and favorites | Workflows |
| workflowTemplatesStore.ts | useWorkflowTemplatesStore | Manages workflow templates | Workflows |
| workspaceStore.ts | useWorkspaceStore | Manages overall workspace state | Workspace |
Workspace Stores
Located in stores/workspace/:
| File | Store | Description | Category |
|---|---|---|---|
| bottomPanelStore.ts | useBottomPanelStore | Controls bottom panel visibility and state | UI |
| colorPaletteStore.ts | useColorPaletteStore | Manages color palette configurations | UI |
| nodeHelpStore.ts | useNodeHelpStore | Handles node help and documentation display | UI |
| searchBoxStore.ts | useSearchBoxStore | Manages search box functionality | UI |
| sidebarTabStore.ts | useSidebarTabStore | Controls sidebar tab states and navigation | UI |
Store Development Guidelines
When developing or modifying stores, follow these best practices:
- Define clear purpose: Each store should have a specific responsibility
- Use actions for async operations: Encapsulate asynchronous logic in actions
- Keep stores focused: Each store should manage related state
- Document public API: Add comments for state properties, actions, and getters
- Use getters for derived state: Compute derived values using getters
- Test store functionality: Write unit tests for stores
Store Template
Here's a template for creating a new Pinia store, following the setup style used in ComfyUI:
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useExampleStore = defineStore('example', () => {
// State
const items = ref([])
const isLoading = ref(false)
const error = ref(null)
// Getters
const itemCount = computed(() => items.value.length)
const hasError = computed(() => error.value !== null)
// Actions
function addItem(item) {
items.value.push(item)
}
async function fetchItems() {
isLoading.value = true
error.value = null
try {
const response = await fetch('/api/items')
const data = await response.json()
items.value = data
} catch (err) {
error.value = err.message
} finally {
isLoading.value = false
}
}
// Expose state, getters, and actions
return {
// State
items,
isLoading,
error,
// Getters
itemCount,
hasError,
// Actions
addItem,
fetchItems
}
})
Common Patterns
Stores in ComfyUI frequently use these patterns:
API Integration
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { api } from '@/scripts/api'
export const useDataStore = defineStore('data', () => {
const data = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchData() {
loading.value = true
try {
const result = await api.getExtensions()
data.value = result
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
return {
data,
loading,
error,
fetchData
}
})
Store Composition
import { defineStore, storeToRefs } from 'pinia'
import { computed, ref, watch } from 'vue'
import { useOtherStore } from './otherStore'
export const useComposedStore = defineStore('composed', () => {
const otherStore = useOtherStore()
const { someData } = storeToRefs(otherStore)
// Local state
const localState = ref(0)
// Computed value based on other store
const derivedValue = computed(() => {
return computeFromOtherData(someData.value, localState.value)
})
// Action that uses another store
async function complexAction() {
await otherStore.someAction()
localState.value += 1
}
return {
localState,
derivedValue,
complexAction
}
})
Persistent State
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
export const usePreferencesStore = defineStore('preferences', () => {
// Load from localStorage if available
const theme = ref(localStorage.getItem('theme') || 'light')
const fontSize = ref(parseInt(localStorage.getItem('fontSize') || '14'))
// Save to localStorage when changed
watch(theme, (newTheme) => {
localStorage.setItem('theme', newTheme)
})
watch(fontSize, (newSize) => {
localStorage.setItem('fontSize', newSize.toString())
})
function setTheme(newTheme) {
theme.value = newTheme
}
return {
theme,
fontSize,
setTheme
}
})
Testing Stores
Stores should be tested to ensure they behave as expected. Here's an example of how to test a store:
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { api } from '@/scripts/api'
import { useExampleStore } from '@/stores/exampleStore'
// Mock API dependencies
vi.mock('@/scripts/api', () => ({
api: {
getData: vi.fn()
}
}))
describe('useExampleStore', () => {
let store: ReturnType<typeof useExampleStore>
beforeEach(() => {
// Create a fresh pinia instance and make it active
setActivePinia(createPinia())
store = useExampleStore()
// Clear all mocks
vi.clearAllMocks()
})
it('should initialize with default state', () => {
expect(store.items).toEqual([])
expect(store.isLoading).toBe(false)
expect(store.error).toBeNull()
})
it('should add an item', () => {
store.addItem('test')
expect(store.items).toEqual(['test'])
expect(store.itemCount).toBe(1)
})
it('should fetch items', async () => {
// Setup mock response
vi.mocked(api.getData).mockResolvedValue(['item1', 'item2'])
// Call the action
await store.fetchItems()
// Verify state changes
expect(store.isLoading).toBe(false)
expect(store.items).toEqual(['item1', 'item2'])
expect(store.error).toBeNull()
})
})
For more information on Pinia, refer to the Pinia documentation.