Docs/Search Matching API

Search Matching API

API documentation for the CoreBox search matching system, including pinyin matching, fuzzy search, and highlighting

Universal Developer

Search Matching API

Overview

The CoreBox search system provides powerful matching capabilities:

  • Pinyin Matching: Search fanyi to match translate
  • Initial Abbreviations: Search fy to match translate
  • Fuzzy Matching: Typo-tolerant search, e.g., helol matches hello
  • Highlighting: Highlight matched portions in search results

Introduction

Search matching combines Feature metadata, keywords, and clipboard inputs to drive ranking and highlights in CoreBox.

SemanticAliasSDK

Recognition marker: App Semantic Alias Catalog / 应用语义别名目录. Starting with sdkapi: 260626, developers can import SemanticAliasSDK helpers from @talex-touch/utils/plugin/sdk or @talex-touch/utils/search to normalize short aliases, category terms, Chinese/English synonyms, and common abbreviations before merging them into existing keywords, searchTokens, or provider write paths.

EXAMPLE.TYPESCRIPT
import { buildSemanticAliasPayload } from '@talex-touch/utils/plugin/sdk'

const semantic = buildSemanticAliasPayload({
  existingKeywords: ['todo'],
  aliases: ['td'],
  categories: ['office', '办公'],
  synonyms: ['待办', '任务'],
})

export const feature = {
  id: 'todo.new',
  name: 'Create Todo',
  desc: 'Create a local todo from input text',
  keywords: semantic.keywords,
  searchTokens: semantic.searchTokens,
}

This SDK is a pure declaration/normalization helper. It does not auto-register providers, scan local applications, open external links, or change the SQLite schema. The built-in CoreApp app catalog uses the same alias token source, so matchAlias, highlighting, and ranking explanations stay aligned.

Indexed Source Types

@talex-touch/utils/search also exports shared types for local indexed search sources. They describe the lifecycle of App, File, Everything, Browser Data, Quicklinks, and similar sources; they do not replace CoreBox Feature matching.

EXAMPLE.TYPESCRIPT
import type {
  IndexedSource,
  IndexedSourceDescriptor,
  IndexedSourceEvidence,
  IndexedSourceHealth,
  IndexedSourceRecord,
  SearchProviderDescriptor,
  SearchProviderUserConfig,
} from '@talex-touch/utils/search'

The first contract covers:

  • source descriptor: platform, priority, storage mode, privacy level, and capabilities.
  • source health: ready/degraded/unsupported, permission, itemCount, watch state, and reconcile state.
  • source evidence: platform sub-source, root count, itemCount, last check time, and failure reason.
  • records: stable ID, title, path or URI, keywords, tags, and metadata.
  • lifecycle: scan, watch event, reconcile, search, open, and clearIndex.

Plugins or CoreApp sources that need long-lived indexing should reuse these types before adding custom scan/watch/rebuild state.

IndexedSourceDescriptor.admission defines source admission: owner (core / official-plugin / third-party-plugin), permissionScopes, defaultState, requiresUserConsent, clearable, and rebuildable. getIndexedSourceAdmissionIssues() and isIndexedSourceAdmissionReady() are pure SDK validators. They currently catch high-privacy sources that silently default-enable, Browser Data sources without high privacy or browser-data scope, external-fast sources without external-tool scope, third-party external-fast sources, non-clearable sqlite-index sources, and watch sources that lack reconcile support. resolveIndexedSourceTaskEligibility() further combines descriptor, health, and task: "scan" | "watch" | "reconcile" into an { eligible, reason } result for pre-scheduling source maintenance or watch handling. resolveIndexedSourceMaintenanceActions() derives scan / reconcile / reset action enablement and blocked reasons from diagnostics so Settings, no-result recovery, and official plugin settings can use the same button policy. resolveIndexedSourceRecoveryRecommendation() turns admission/lifecycle issues, permission or disabled states, failed/stalled progress, recent failed/skipped tasks, and degraded/error health into a read-only recovery recommendation such as wait, grant-permission, enable-provider, scan, reconcile, reset, or inspect-*; it is diagnostic guidance only, does not execute maintenance, and does not replace a durable job scheduler.

getIndexedSourceLifecycleIssues() / isIndexedSourceLifecycleReady() validate whether source descriptor capabilities actually have matching handlers: scan must exist; declaring watch / reconcile / search / reset / clear / open requires handleWatchEvent / reconcile / search / resetIndex / clearIndex / open; providing a handler without declaring the corresponding capability emits handler-provided-without-capability. CoreApp IndexingRuntime.registerSource() currently logs these lifecycle contract issues as warnings and does not block legacy sources; IndexedSourceDiagnostics.lifecycleIssues and Settings Contract chips expose the same issue list. New official plugin sources should clear these issues before registration so Settings does not expose a capability that has no runtime entry point.

IndexedSourceDiagnostics.admissionIssues exposes the SDK admission validator result from getIndexedSourceAdmissionIssues() next to runtime health. CoreApp IndexingRuntime.registerSource() logs admission contract issues as warnings, and Settings shows them as Admission chips before lifecycle contract chips. A source that is skipped by runtime task eligibility because of high privacy, Browser Data, external-fast, persistence, or watch/reconcile policy can therefore explain the policy failure without waiting for a scan/watch/reconcile task to run.

Use getIndexedSourceContractIssues() / isIndexedSourceContractReady() when a tool needs one source contract check. It returns { admission, lifecycle, ready } so plugin tooling, Settings, and runtime code do not need to stitch admission and lifecycle rules independently.

When reading source health fails, use getIndexedSourceErrorMessage() / buildIndexedSourceErrorHealth() to produce one shared error-health shape. The default output uses status: "error", permissionState: "not-required", watchState: "unavailable", and reconcileState: "failed", and maps thrown errors or non-Error values into lastError; source adapters can pass permission, watch/reconcile, or reason context when needed. CoreApp diagnostics now reuse this helper, so runtimes, official plugin sources, and developer tooling should not hand-roll their own error-health fallback.

Diagnostics summary aggregation should use summarizeIndexedSourceHealth() / buildIndexedSourceDiagnosticsSummary(). These helpers standardize total, byStatus, ready, degraded, and unavailable; unavailable always includes disabled, unsupported, permission-required, and error. CoreApp SourceDiagnosticsService now reuses this helper, so future plugin runtimes, official source settings, or developer tooling should not redefine summary semantics independently.

Source diagnostics snapshot caching should use IndexedSourceSnapshotCacheService. It provides short TTL, concurrent load de-duplication, failed-load non-caching, and explicit clear semantics. It is meant for sharing local scan output across health/roots/evidence during one diagnostics refresh so Browser Data / profile sources do not reread the same local files repeatedly. Maintenance paths such as scan, reconcile, watch, and reset should still clear the cache and read a fresh snapshot.

Profile/browser-like sources should use IndexedSourceProfileDiagnosticsService to turn per-profile, per-browser, or similar sub-source diagnostics into IndexedSourceEvidence and IndexedSourceRoot. The helper standardizes rootCount, roots, itemCount, ready/degraded/error/unsupported status, reason, metadata, and granted watch roots. Browser Bookmarks now reuses this helper; future Browser History, VSCode profile, or Obsidian vault sources should not duplicate the same evidence/root mapping.

Watch root routing should also use SDK helpers: normalizeIndexedSourcePathForMatch() / isIndexedSourcePathInsideRoot() centralize path normalization and platform case-sensitivity, while resolveIndexedSourceRootSkipReason() / resolveIndexedSourceWatchRootRoute() map denied or promptable roots to root-permission:* skipped reasons. CoreApp's WatchEventRouter now reuses these helpers, so plugin sources should not copy root matching or permission-skip logic. Path-based source watch path/root policy should also use normalizeIndexedWatchPath() / getIndexedWatchDepthForPath(), resolveIndexedWatchRootSet(), isIndexedWatchPathOwned(), and filterIndexedWatchPendingPermissionPaths() for watch path normalization, default depth, root de-duplication, root/child ownership, and pending-permission root filtering; real watcher registration and permission reads still live at the CoreApp watcher boundary.

The SDK also provides source descriptor templates: createQuicklinksIndexedSourceDescriptor(), createBrowserBookmarksIndexedSourceDescriptor(), createBrowserHistoryIndexedSourceDescriptor(), createSystemSettingsIndexedSourceDescriptor(), and createIndexedSourceDescriptorTemplate(). The Quicklinks template is official-plugin owned, low privacy, sqlite-index backed, fast, clearable, and rebuildable. The Browser Bookmarks and Browser History templates are official-plugin owned, high privacy, sqlite-index backed, deferred, disabled by default, explicit-consent gated, browser-data + file-system scoped, clearable, and rebuildable. The System Settings template is core owned, low privacy, ephemeral, fast, uses the system-index scope, is not clearable, and is rebuildable. These helpers standardize descriptor/admission defaults for later source implementations. Quicklinks now has a low-privacy runtime skeleton and links official quicklink push providers to source enablement through indexedSourceId: "quicklinks", but the real plugin-backed persistent feed, user-facing clear/rebuild, and Settings evidence are still incomplete. Browser Data and System Settings also cannot be claimed complete from templates alone.

Plugins may also declare indexed source lifecycle intent through manifest.indexedSources. This field is parsed and validated as manifest metadata only: it does not auto-register a runtime source and does not read real files. The SDK helper resolveIndexedSourceManifestDescriptors() normalizes declarations into IndexedSourceDescriptor values and reuses admission policy plus permission mapping: browser-data requires fs.read, and file-system requires fs.index; Browser Data sources must be official-plugin owned, high privacy, disabled/ask by default, and explicitly consent-gated. CoreApp's loader exposes valid declarations on plugin state and records INDEXED_SOURCE_* issues in plugin diagnostics so Settings and developer tools can explain why a source has not entered runtime yet.

Search Providers also have a public SDK boundary: SearchProviderDescriptor describes providers that can enter CoreBox root results, SearchProviderUserConfig stores user-facing enabled/order, and normalizeSearchProviderUserConfigs() merges descriptors with saved user config for Settings runtime display. Plugin provider registration should use SearchProviderRegistrationPolicy to express owner, mode, permission scopes, default state, and consent: third-party push providers must declare the root-results scope, defaultState: "ask", and requiresUserConsent: true; third-party indexed providers require explicit user consent, and high-privacy providers cannot silently default-enable. Official plugin providers may declare indexedSourceId to link themselves to a runtime indexed source; Browser Data source ids such as browser-bookmarks / browser-history can only be claimed by official-plugin providers, preventing third-party providers from bypassing the high-privacy boundary. The SDK helper getSearchProviderIdsForIndexedSource() resolves source-to-provider links from provider descriptors; isSearchProviderEnabledByConfig() is the strict provider-level enablement guard and only returns true after descriptor defaults plus saved user config resolve to enabled === true; isIndexedSourceEnabledByProviderConfig() maps a source id plus linked provider ids to explicit user enablement and only treats enabled === true as enabled, so ask/default states cannot be mistaken for consent. The SDK helper resolveSearchProviderPermissionIds() maps root-results to the plugin manifest permission search.root-results; getSearchProviderManifestCoverage() audits whether push features have explicit provider declarations, deriveSearchProvidersFromPushFeatures() exposes the legacy push: true compatibility-provider derivation, and resolveSearchProviderManifestDescriptors() combines manifest provider normalization, policy decisions, missing-permission detection, and compatibility derivation in one SDK resolver. CoreApp's loader now reuses that resolver so plugin SDKs, runtime behavior, and validation tooling do not maintain separate rules. CoreApp also checks the permission on plugin.feature.pushItems() / boxItems.push() root-result write paths, so undeclared or ungranted plugins cannot push content into CoreBox root results.

Plugins should explicitly declare searchProviders in manifest.json so CoreApp can validate policy and permissions during loading. A plugin may declare multiple push providers and connect each provider to a concrete feature with featureId; when pushed items carry meta.featureId, CoreApp automatically writes the matching meta.searchProviderId so Settings can enable or disable a single provider precisely. Legacy plugins that still use push: true features without explicit searchProviders get one derived compatibility provider descriptor per push feature and a SEARCH_PROVIDER_DERIVED_FROM_PUSH_FEATURE warning. That path is for migration only, not the recommended shape for new plugins. Providers missing search.root-results or blocked by registration policy are recorded as issues and are not exposed to the later provider registry.

pnpm plugins:validate emits migration warnings for plugins that still use push: true without manifest.searchProviders, and prints explicit-provider coverage for push plugins. This check is visibility-only for now and is not a hard gate; new plugins and official plugin migrations should clear these warnings first. Repository push plugins now have explicit provider coverage and no longer rely on the legacy derived-provider path.

EXAMPLE.JSON
{
  "permissions": {
    "required": ["search.root-results"]
  },
  "searchProviders": [
    {
      "id": "touch-translation.results",
      "displayName": "Translation Results",
      "mode": "push",
      "permissionScopes": ["root-results"],
      "defaultState": "ask",
      "requiresUserConsent": true,
      "pushesToRootResults": true
    }
  ]
}

Official Browser Data plugins can link an immediate push provider with runtime source diagnostics by declaring both indexedSources lifecycle intent and provider indexedSourceId:

EXAMPLE.JSON
{
  "permissions": {
    "required": ["fs.read", "fs.index", "search.root-results"]
  },
  "indexedSources": [
    {
      "id": "browser-bookmarks",
      "template": "browser-bookmarks",
      "displayName": "Browser Bookmarks",
      "admission": {
        "owner": "official-plugin"
      }
    }
  ],
  "searchProviders": [
    {
      "id": "touch-browser-data.browser-bookmarks",
      "displayName": "Browser Bookmarks",
      "kind": "browser-bookmark",
      "owner": "official-plugin",
      "mode": "push",
      "permissionScopes": ["root-results", "browser-data"],
      "defaultState": "ask",
      "requiresUserConsent": true,
      "pushesToRootResults": true,
      "indexedSourceId": "browser-bookmarks"
    }
  ]
}

The Provider Registry combines Core indexed sources with loaded plugin searchProviders so Settings can show one enablement and ordering list. Push-mode plugin providers with defaultState: "ask" or requiresUserConsent: true are not allowed to write root results until their provider config resolves to enabled === true; the manifest permission gate is only the minimum runtime permission, not user consent. Disabling a push-mode plugin provider blocks future boxItems.push() / boxItems.pushItems() / boxItems.update() writes from that provider into CoreBox root results; other providers from the same plugin can still push when enabled. boxItems.update() first reads the existing item's meta.searchProviderId for the provider-state guard, so updates do not need to repeat the provider id in every payload. remove / clear remains allowed so plugins can clean stale items. Plugin-pushed items carry meta.searchProviderId, and the CoreBox root-result store only keeps provider-tagged items visible when their provider config resolves to enabled === true; legacy untagged items remain visible for migration compatibility. Existing push results are filtered and ordered by provider config during sync and batch upserts. Saving provider config in Settings refreshes the root-result sync.

SearchProviderConfigResponse.issues exposes provider registry diagnostics for SDK consumers and Settings. It includes plugin loader SEARCH_PROVIDER_* issues, such as missing search.root-results permission, policy-blocked providers, migration-only derived providers, invalid declarations, plus registry-level SEARCH_PROVIDER_ID_COLLISION entries when a plugin tries to reuse an existing provider id. Blocked providers are still excluded from availableProviders, but the issue list explains why they did not enter root results.

Browser Bookmarks / History Browser Data sources should be official plugin providers rather than long-lived CoreApp built-ins. touch-browser-data now explicitly declares the touch-browser-data.browser-bookmarks push provider and enters Settings provider management with root-results / browser-data scopes. CoreApp's current Browser Bookmarks source remains a runtime skeleton and migration diagnostics sample. It does not scan real browser files by default, but it dynamically reads the unified provider config and only enters the scanner-backed enabled path when the user explicitly enables browser-bookmarks or the official touch-browser-data.browser-bookmarks provider. The real indexed source, persistent rebuild, watch-root registration, and user-facing clear flows still need to move to the official plugin side under user consent.

IndexedSourceReconcileReasons defines SDK-level standard reconcile reason codes: scheduled, manual-repair, watch-gap, watch-recovery, file-watch-root-recovered, health-repair, schema-migration, external-refresh, and reconcile-not-supported. IndexedSourceReconcileRequest.reason should prefer these standard values; the type still accepts source-specific strings so CoreApp providers and official plugins can preserve detailed diagnostics during migration.

IndexedSourceScanReasons defines SDK-level standard scan reason codes: startup, manual-rebuild, scheduled, watch-recovery, schema-migration, and health-repair. IndexedSourceScanRequest.reason should prefer these standard values so App/File/Everything/Browser Data sources do not keep spreading hard-coded reason strings during the runtime migration.

IndexedSourceResetReasons defines SDK-level standard runtime reset reason codes: manual-rebuild, schema-migration, integrity-repair, health-repair, and user-clear. IndexedSourceResetRequest.reason should prefer these standard values so runtime lastReset diagnostics and FileProvider reset helpers share one stable reason set.

IndexedSource.resetIndex() is the runtime reset entry, described by IndexedSourceResetRequest and IndexedSourceResetResult. It clears source runtime index state, such as scan_progress, so later scan/reconcile can repair the source. It is not the same as user-facing clearIndex(). CoreApp's IndexingRuntime.resetSourceRuntimeState() calls this entry and records diagnostics lastReset, including the reset reason, jobId, queuedAt, whether the search index was cleared, whether scan_progress was cleared, and related row counts. clearSearchIndex is handled at the runtime store boundary: the runtime first calls IndexStoreAdapter.clearSource(sourceId) for the shared SearchIndex, then passes clearSearchIndex: false to source-local resetIndex() so sources do not each depend on SearchIndexService. Reset also reuses the IndexedSourceTaskRunGate owned by the runtime; when the same source reset is already running, a second request returns an IndexedSourceResetResult with error: "reset-already-running" and records diagnostics instead of clearing SearchIndex or invoking source-local reset again.

Renderer settings should use the settings.indexedSource typed SDK for long-lived indexed source maintenance. This SDK maps AppEvents.indexedSource.diagnostics/reset/reconcile/scan and accepts sourceId, standard scan/reconcile/reset reasons, and clear flags, reusing the same runtime diagnostics and task state instead of adding private IPC for File, App, Browser Bookmarks, or future Quicklinks sources. Maintenance button availability should come from resolveIndexedSourceMaintenanceActions(); CoreApp runtime also reruns resolveIndexedSourceTaskEligibility() for single-source scan/reconcile entries, so disabled, permission-required, unsupported, or admission-invalid sources cannot bypass the scheduling guard through Settings. It also exposes providerConfigGet/providerConfigUpdate so Settings can read and save indexed provider enablement and ordering; SearchProviderConfigResponse.sourceLinks returns { sourceId, providerIds[] }[] so Settings can understand runtime source to provider links as structured transport data. SearchEngineCore's default provider pool reads this config, skips disabled providers, orders enabled providers by order, and includes the provider config signature in the search cache key. Active provider mode keeps its existing semantics and is not interrupted by global settings; this config is a user preference and does not replace runtime permission guards.

CoreApp's Windows App scanner now returns grouped scan results by sub-source: Start Menu, UWP/Get-StartApps, Uninstall Registry, App Paths Registry, and Steam manifests. The legacy app search path still flattens and deduplicates those grouped results; AppProvider Windows evidence prefers those grouped results and uses the public IndexedSourceGroupedEvidenceService to surface each sub-source ready/degraded state, empty / error reason, sourceLabel, and scanner metadata. App watch-roots evidence reuses IndexedSourceRootEvidenceService for rootCount, roots, and empty reason. AppProvider only keeps platform key/label, the manual synthetic row, watch-root path reading, and DB metadata fallback. The grouping mainly serves diagnostics, source evidence, and the future runtime store migration.

The App source scan() path now returns IndexedSourceRecordBatch: CoreApp maps ScannedAppInfo into IndexedSourceRecord, reuses IndexedWriteRuntimeEmitterService.buildBatch() for the same runtime batch mapper used by emitted batches, then lets the runtime ScanScheduler and SearchIndexStoreAdapter write into the existing search index. Plugins or official sources should follow the same record batch contract instead of creating provider-private, unobservable index write paths.

The File source scan() path now also returns IndexedSourceRecordBatch: FileProvider maps file rows inserted during full scan and reconciliation into kind: "file" IndexedSourceRecord values, then FileIndexedSource.scan() yields them to the runtime store boundary. FileProvider's internal worker writes remain in place for now and will keep migrating toward the shared store boundary in later slices.

App and File source watch deltas now return increments that the runtime store can consume directly: add/change attempts to return IndexedSourceDelta.record, while delete returns stableKey/path, and these watch deltas share the same IndexedWriteRuntimeEmitterService build helpers as App scan batches. Global FILE_ADDED / FILE_CHANGED / FILE_UNLINKED events, plus macOS App directory add/delete events, are now bridged by SearchEngineCore into IndexingRuntime.routeWatchEventWithResult(). Watch delta accept gating, normalized-key coalescing, delete dominance, prepare-flush gating, and serialized flush scheduling now live in the public @talex-touch/utils/search IndexingWatchDeltaQueueService; FileProviderIncrementalQueueService only adapts File semantics and manual metadata merging; incremental delete path normalization, existing-row lookup, DB delete delegation, SearchIndex removal, and success logging now live in the generic IndexedWriteDeleteExecutorService, and reconciliation deletes now reuse the same resolved-record delete entry; reconciliation worker reconcile and main-thread fallback diff now live in FileProviderReconciliationDiffService, producing one added/updated/deleted calculation result; existing-root reconciliation DB row reads, directory scans, diff orchestration, delete/update/insert delegation, progress, stats, and completed-path reporting now live in FileProviderReconciliationRunService; reconciliation delete/update source-level delta emission and changed/deleted result reporting now live in FileProviderReconciliationDeleteService / FileProviderReconciliationUpdateService, while FileProvider only injects the existing delete/update executors and record mapper; stale watch-root cleanup now lives in FileProviderCleanupDeleteService, reusing the same delete-executor shape while injecting files-table deletes, embedding cleanup, scan_progress cleanup, SearchIndex removal, and cleanup progress; full-scan root scanning, scan progress, event-loop yield, file-row payload mapping, insert delegation, and completed-path reporting now live in FileProviderFullScanRunService; full-scan insert/upsert AIMD batching, idle pacing, side-effect dispatch, record batch emission, progress updates, and added-count reporting now live in FileProviderFullScanInsertService; reconciliation-add chunked upsert, side-effect dispatch, record batch emission, delta emission, and progress updates now live in FileProviderReconciliationInsertService, with upsertSearchIndexFiles and source delta semantics injected by FileProvider; incremental add/change record building, existing-row lookup, insert/update execution, and manual-summary orchestration now live in FileProviderIncrementalWriteService; insert/update/unchanged/manual-summary planning lives in the generic IndexedWritePlanService, while FileProviderIncrementalWritePlannerService only adapts File rows; incremental insert/upsert persist callbacks, inserted-row side-effect dispatch, and success logging now live in the generic IndexedWriteInsertExecutorService; chunked update idle/capacity waits, per-record updates, refreshed-row reads, side-effect dispatch, and progress logging now live in the generic IndexedWriteUpdateExecutorService; post-write keyword/icon extension processing and content-index scheduling now live in the generic IndexedWriteSideEffectService, while FileProviderWriteSideEffectService only adapts File naming; index-worker context gating, chunk dispatch, deferred dispatch, and failure isolation now live in the generic IndexedWorkerSchedulerService, while FileProviderIndexSchedulerService only adapts file-row to worker-payload mapping and large-file background-content policy; index-worker result progress/fileUpdate/indexItem mapping into persist payloads now lives in the public @talex-touch/utils/search IndexedWorkerPersistEntryMapperService, while FileProviderIndexPersistEntryMapperService only adapts File worker results; worker status summary, short-TTL caching, concurrent status load dedupe, and failed-load cache skipping now live in the public IndexedWorkerStatusSnapshotService, while FileProviderWorkerStatusService only adapts File worker status loading; index-worker flush backlog delay, sqlite-busy retry, exponential backoff, jitter, and failure retry reason decisions now live in the public @talex-touch/utils/search IndexedWriteFlushRetryService, while FileProviderIndexFlushRetryService only keeps SQLite busy classification and FileProvider reason mapping; flush execution, worker readiness gating, DB backpressure, persistAndIndex, commit/rollback, and duration recording now live in the generic IndexedWriteFlushExecutorService, which now returns source-agnostic reason / error / metadata; flush timers, in-progress guards, idle snapshots, failure retry scheduling, and successful drain-remaining scheduling now live in the generic IndexedWriteFlushRuntimeService; FileProviderIndexRuntimeService only adapts FileProvider-specific result metadata and logging; pending/inflight enqueue, take, commit, rollback, and size accounting now live in the public @talex-touch/utils/search IndexedWriteBufferService, while FileProviderIndexFlushBufferService only adapts file worker results by fileId; the latest flush snapshot now lives in IndexedWriteFlushSnapshotService, and file-provider:index-flush evidence exposes flushed / worker-not-ready / failed state, pending/inflight counts, retry reason, error, and duration; actual files-table persistence, flush trace wiring, and FTS write semantics still live at the FileProvider / SearchIndex worker boundary for the next migration slice.

File Index progress remaining time is estimated by the public @talex-touch/utils/search IndexingProgressEstimatorService, while FileProvider only adapts FileIndexStage idle/completed terminal states through FileProviderProgressEstimatorService: it prefers stage-local smoothed throughput for ETA; when there are not enough speed samples yet but the stage already has stable elapsed progress, it can emit a conservative elapsed-progress fallback with a safety multiplier so early indexing does not stay without a remaining-time estimate for too long. ETA is still suppressed during stage switches, backward progress, cold starts, or very low progress, so Settings does not show large remaining-time jumps caused by scan/index/reconcile transitions or short-term speed spikes. FileIndexProgress / FileIndexStatus also expose optional estimateStatus, speedSampleCount, and estimateBasis, so UI can distinguish unknown, stabilizing, estimated, stalled, and complete states, as well as whether ETA came from stage-speed, elapsed-progress, stalled, or complete. Progress stream throttling is handled by the public IndexingProgressStream SDK helpers, while FileProvider only adapts the FileIndexProgress payload; first payloads, terminal stages, max silence, min interval, and progress/current/total changes use one shared rule set.

WatchEventRouter isolates single-source handler failures and store delta write failures, and returns route result stats: matched sources, handled/failed sources, applied/failed deltas, and error summaries. The runtime records lastWatch.jobId/queuedAt for applied deltas, handler/store failures, and skipped sources, so watcher routes share the same task identity model as scan/reconcile/reset. IndexedSource.shouldHandleWatchEvent() lets a source decide whether a watch/recovery path belongs to it; when it returns false, the runtime records a source-watch-filtered skipped reason instead of calling the source handler. The existing routeWatchEvent() compatibility entry still returns deltas.

Batch scans also have failure isolation in ScanScheduler: scanSourcesWithResult() returns successful sources, failed sources, batches, records, and error summaries, so one source scan failure does not fail the whole batch. Single-source scanSource() now also passes through the runtime eligibility guard; blocked sources record skipped diagnostics instead of invoking their scanner. IndexingRuntime records scan job ids and queuedAt for single-source scans, batch failures, and skipped sources, and writes them into diagnostics lastScan so Settings/CoreBox trace can distinguish individual scan executions. IndexingRuntime owns one shared IndexedSourceTaskRunGate and injects it into ScanScheduler and ReconcileScheduler; reset uses the same gate as well. The current behavior still rejects same-source same-kind tasks while one is running, while leaving one decision boundary for later retry, debounce, and durable job history.

ReconcileScheduler is the minimal reconcile task entry between IndexingRuntime and ReconcileEngine. It currently records job ids, queuedAt, reason, and rootCount without changing source reconcile algorithms. Batch reconciliation still has failure isolation in ReconcileEngine: reconcileSourcesWithResult() returns successful sources, failed sources, added/changed/deleted/skipped/error totals, and failure summaries, so one source reconcile failure does not fail the whole batch. Single-source reconcileSource() now also passes through the runtime eligibility guard; blocked sources record skipped diagnostics instead of invoking their reconcile handler. A source can now return add/change/delete repairs in IndexedSourceReconcileResult.deltas; the runtime applies them through the same store adapter and writes appliedDeltas, failedDeltas, and deltaErrors back into the result and diagnostics. IndexedSourceReconcileRequest.reason records the trigger reason; runtime lastReconcile keeps reason, rootCount, jobId, and queuedAt so Settings/CoreBox can distinguish scheduled, manual repair, and watch-root recovery reconciles.

IndexedSourceDiagnostics can carry the latest runtime task state: lastScan, lastWatch, lastReconcile, and lastReset. All four task states include runtime jobId / queuedAt. It can also carry bounded in-memory recentTasks, ordered newest first, with scan/watch/reconcile/reset kind, status, jobId, queuedAt, error, and summary fields as a transition layer for Settings / trace source-level task history. The SDK exposes appendIndexedSourceTaskHistory(), updateIndexedSourceTaskState(), and DEFAULT_INDEXED_SOURCE_TASK_HISTORY_LIMIT, so CoreApp runtime and future official plugin indexed sources should reuse the same newest-first / bounded trimming and last* task-state update rules instead of reassembling lastScan/lastWatch/lastReconcile/lastReset plus history per source. Diagnostics can also carry source-level progress, covering stage, current/total, percent, estimatedRemainingMs, estimatedCompletionAt, averageItemsPerSecond, speedSampleCount, and estimateBasis; the File indexed source now adapts FileProvider indexing status into this field. These fields come from runtime memory, are not a persistent source of truth, are not written to JSON sync payloads, and do not replace source health.

The Settings File Index diagnostics area renders recentTasks as recent task chips for scan/watch/reconcile/reset and maps succeeded / failed / skipped to one shared tone model. The chips explain summary fields for scan records/batches, watch delta/action, reconcile add/change/delete/skipped, and reset clear flags. Future trace panels or official plugin settings surfaces should reuse this display contract instead of reinterpreting task history independently.

The same area also renders IndexedSourceDiagnostics.progress as a source progress chip. It maps unknown / idle / running / stabilizing / estimated / stalled / complete / failed to the shared tone model and shows stage, percent, current/total, remaining time, ETA, speed, sample count, estimateBasis, and reason. The File source currently adapts this field from FileProvider indexing status; future Browser Bookmarks, Quicklinks, Obsidian, or VSCode sources only need to implement IndexedSource.getProgress() for Settings to reuse the same UI and ETA semantics instead of adding provider-specific progress panels.

The same diagnostics area also renders resolveIndexedSourceRecoveryRecommendation() as a source recovery chip. The chip only explains whether the next step is permission, provider enablement, waiting, scan, reconcile, reset, or contract/source inspection. It does not execute actions; actual scan/reconcile/reset buttons still use resolveIndexedSourceMaintenanceActions() and the runtime eligibility guard.

The same diagnostics area also renders source evidence as prioritized evidence chips: degraded / permission-required / error evidence first, then ready evidence. File source scan-progress, integrity, and index-flush evidence can therefore surface flush backlog, retry reason, worker-not-ready, duration, and related stall details through unified Settings diagnostics without adding FileProvider-specific UI. The renderer helper consumes existing metadata by evidence id and formats scan completed/failed/pending-permission, flush pending/inflight/entries/duration, and integrity FTS/files/rebuild/orphan keyword fields into stable chip summaries instead of exposing raw metadata in Settings.

@talex-touch/utils/search exposes IndexingProgressEstimatorService and Indexing Progress Stream helpers as shared indexing progress primitives. The former estimates remaining time from smoothed throughput within the current source stage and hides unreliable ETA on terminal stages, stage switches, backward progress, cold starts, and low-progress samples; when a stage has enough elapsed progress but not enough speed samples, it reports a conservative elapsed-progress fallback. It also reports estimate status, speed sample count, and estimateBasis, avoiding a precise-looking ETA from one transient speed sample or stale ETA when progress stalls. The stream helpers control progress payload frequency. CoreApp keeps a compatibility re-export for legacy imports, while FileProvider now imports the SDK primitives directly, adapts FileIndexStage idle/completed terminal states plus the FileIndexProgress payload, and exposes the result through IndexedSource.getProgress() into unified diagnostics. Future Browser Bookmarks, Quicklinks, Obsidian, or VSCode source progress UI should reuse the same SDK rule set.

@talex-touch/utils/search also exposes IndexingWatchDeltaQueueService as a shared watcher delta queue primitive. It coalesces add/change/delete updates by normalized key, lets delete dominate later change events, keeps pending entries when a source is not ready to flush, and serializes flush execution; source-specific metadata is merged through an injectable coalesce hook. CoreApp keeps a compatibility re-export for legacy imports, while FileProvider now imports the SDK queue directly and only adapts the manual flag. normalizeIndexedWatchPath() and getIndexedWatchDepthForPath() are also exposed as watch path policy primitives. resolveIndexedWatchRootSet(), isIndexedWatchPathOwned(), and filterIndexedWatchPendingPermissionPaths() handle base/extra root normalization and de-duplication, root-or-child ownership, shared-prefix false-positive protection, and exact-root pending-permission filtering. FileProvider now only adapts platform typing, the real watcher, settings persistence, and pending path reads. Future official plugin sources should reuse these SDK primitives instead of copying provider-private watcher queues, default depth policy, or root ownership rules.

@talex-touch/utils/search also exposes resolveIndexedScanEligibility() / toIndexedScanTimestamp() as shared auto-scan eligibility primitives. They calculate never-scanned roots, stale roots, and the latest scan timestamp from watch roots, completed scan rows, the auto-scan interval, and the current time. FileProvider still owns reading the real scan_progress table and user auto-scan settings; the SDK does not know about Drizzle, SQLite, or FileProvider table shape. Future Obsidian, VSCode, Browser Bookmarks, and similar sources should reuse this policy when they need the same new-root / stale-root / lastScannedAt decision.

resolveIndexedScanStrategy() is the matching scan strategy primitive. It splits watch roots and a completed path set into first-time full-scan roots and existing-root reconciliation roots. FileProvider still owns completed-path loading, event-loop yielding, and timing/logging; the SDK only expresses the source-agnostic routing rule. Path-based runtime sources should reuse this strategy instead of reimplementing the “completed roots reconcile, incomplete roots full scan” split.

resolveIndexedAutoScanPreflight() centralizes auto-scan preflight skip reason priority, including disabled, initializing, missing-context, no-paths, app-busy, search-active, and interval. FileProvider runs early preflight before reading scan_progress, so initialization or missing context does not trigger DB reads; only after early gates pass does it read scan eligibility and ask the SDK for the interval decision. The real appTaskGate, search activity, and deviceIdleService idle/battery checks remain at the CoreApp boundary and do not move into the SDK.

@talex-touch/utils/search also exposes IndexedWriteFlushExecutorService, IndexedWriteFlushRuntimeService, buildIndexedWriteFlushFailureSnapshot(), and IndexedWriteFlushEvidenceService as shared write flush primitives. The executor owns buffer take/rollback/commit, readiness gating, capacity waits, persist delegation, duration recording, and source-agnostic result metadata. mapIndexedWriteFlushExecutorResult() maps executor results into adapter-owned statuses and numeric metadata fields, such as mapping not-ready to FileProvider's worker-not-ready and extracting withContent. The runtime owns flush timers, unavailable/no-pending/flush-in-progress idle snapshots, in-progress deferral, failure retry scheduling, and successful drain-remaining scheduling. The failure snapshot helper combines an error, attached flushResult, pending/inflight sizes, and retry metadata into a failed snapshot. The evidence service maps the latest flush snapshot into IndexedSourceEvidence ready/degraded status, itemCount, and metadata without knowing the backing DB or worker. IndexedWriteBufferService handles explicit-key pending/inflight buffers, while IndexedEntryKeyedWriteBufferService lets worker payloads enter the same enqueue/take/commit/rollback semantics through a key selector. This fits File's fileId today and future Browser/Quicklinks url/id payload keys. CoreApp only keeps compatibility re-exports for legacy imports, while FileProvider injects SQLite busy metadata, worker readiness, persistAndIndex, File status mapping, retry metadata, and the file-provider:index-flush id/label from its adapter layer; FileProviderIndexFlushBufferService now only keeps the file worker result fileId key selector.

@talex-touch/utils/search also exposes IndexedWritePlanService as a path-record write planning primitive. It splits incoming path records into insert/update/unchanged sets, applies timestamp tolerance, computes normalized manual summaries, and keeps update-record shaping injectable. CoreApp keeps a compatibility re-export, while FileProvider now imports the SDK planner directly and only adapts File row update fields. This primitive is intended for path-based indexed sources such as File, Obsidian, and VSCode; non-path sources such as Browser Bookmarks should keep their own record diff shape.

@talex-touch/utils/search also exposes IndexedWriteInsertExecutorService as a source-agnostic insert executor. It skips empty batches, delegates persistence, dispatches inserted rows, and logs inserted counts through injected callbacks without depending on SQLite, SearchIndex, or File row types. CoreApp keeps a compatibility re-export, while FileProvider now imports the SDK executor directly and only injects File persistence plus side-effect dispatch.

@talex-touch/utils/search also exposes IndexedWriteDeleteExecutorService as a path-record delete executor. It normalizes and deduplicates raw paths, resolves existing records, delegates record deletion, calls an injected removeIndexedArtifacts cleanup hook, and returns deleted ids/paths. The SDK surface avoids SearchIndex naming; CoreApp keeps a compatibility wrapper for legacy removeSearchIndexItems deps, while FileProvider and cleanup/reconciliation delete flows keep their existing File-specific cleanup wiring.

@talex-touch/utils/search also exposes IndexedWriteUpdateExecutorService as an injected-queue update executor. It chunks update records, waits before each chunk, delegates per-record updates, refreshes updated rows, dispatches side effects, and logs chunk duration through injected clock/formatter callbacks. The SDK only defines the runQueue(chunks, handler, options) protocol; CoreApp still injects its adaptive queue and File-specific backpressure policy.

@talex-touch/utils/search also exposes IndexedWriteRuntimeEmitterService as a runtime-output primitive. It maps persisted records into IndexedSourceRecordBatch, add/change IndexedSourceDelta values, delete delta paths, and progress snapshots, and it can build record batches or add/change/delete deltas directly for scan/watch handlers that do not have an emit sink. It does not depend on SQLite, SearchIndex, File rows, or worker types. FileProvider full scan insert, reconciliation insert/update/delete, stale cleanup delete, App scan/watch, Browser Bookmarks scan/reconcile/watch refresh, and the Quicklinks scan/reconcile/watch skeleton now reuse this helper and only inject File/App/Bookmark/Quicklink row mappers, existing upsert/update/delete callbacks, runtime callbacks, and source-specific reasons. Future Obsidian or VSCode reconciliation writes should reuse this primitive instead of duplicating record batch / delta / progress output plumbing.

@talex-touch/utils/search also exposes IndexedWorkerPersistEntryMapperService as a shared worker-result to persist-payload mapping primitive. It centralizes progress null-normalization, fileUpdate content-hash defaults, embedding model/vector projection, and generic indexItem passthrough without depending on CoreApp's SearchIndex worker types. CoreApp keeps a compatibility re-export, while FileProvider now imports the SDK mapper directly and only passes its index worker results into that mapper.

@talex-touch/utils/search also exposes IndexedWriteSideEffectService as a shared post-write side-effect dispatcher. It runs extension processing asynchronously and schedules indexing immediately; extension processing failures are logged without blocking later index worker dispatch, and source-specific failure wording is injectable. CoreApp keeps a compatibility re-export, while FileProvider now imports the SDK dispatcher directly and only keeps File extension processing, index scheduling, and File-specific log wording.

@talex-touch/utils/search also exposes IndexedWorkerSchedulerService as a shared worker scheduling primitive. It owns worker context gating, chunk dispatch, deferred dispatch, and failure isolation without depending on CoreApp worker types. CoreApp keeps a compatibility re-export, while FileProvider now imports the SDK scheduler directly and only keeps file-row to worker-payload mapping, large-file background-content policy, and File-specific log wording.

Runtime batch scan/reconcile and watch route now call resolveIndexedSourceTaskEligibility() before scheduling work or handling events. Sources are skipped when admission issues exist, when the matching capability is absent, when health is disabled / unsupported / permission-required / error, or when permissionState is denied / promptable. Root-based watch routing also checks the matched IndexedSourceRoot.permissionState; denied / promptable roots only return root-permission:* skipped evidence and do not enter the source handler. Batch/route results include skipped source counts and reasons, and diagnostics lastScan / lastWatch / lastReconcile records skipped:* so Settings and trace can explain why a source was not maintained or did not respond. High-privacy Browser Data sources therefore rely on one runtime guard instead of adapter-local empty scans.

File source roots now map FileSystemWatcher pending paths owned by FileProvider watch roots to permissionState: "promptable" with the file-index-watch-root-pending-permission reason. When any such root exists, File source health becomes permission-required and watchState becomes pending-permission. This prevents unauthorized directories from looking like active watch roots and lets the runtime root guard skip them with the same root-permission:* semantics.

When FileSystemWatcher sees a pending path become accessible again, it emits FILE_WATCH_ROOT_RECOVERED. SearchEngineCore first applies the File source shouldHandleWatchEvent() ownership filter, then triggers IndexingRuntime.reconcileSource("file-provider", { reason: "file-watch-root-recovered", roots: [recoveredRoot] }) only for FileProvider watch roots. Permission recovery therefore becomes runtime lastReconcile.reason/rootCount diagnostics instead of remaining internal watcher state.

The App source reconcile() path now returns real IndexedSourceReconcileResult stats. added, changed, deleted, skipped, and errors come from full sync diffing and macOS mdls repair instead of fixed zero counters. New sources should report reconciliation results with the same semantics.

The File source reconcile() path now also reports real stats: new-root full scans count added, existing-root reconciliation counts added, changed, deleted, and skipped, and stale watch-root cleanup counts deleted. Existing-root reconciliation also maps added, changed, and deleted file rows into IndexedSourceDelta values for the runtime store adapter, so reconcile can repair the shared search index instead of only updating FileProvider's internal table or reporting counters. File workers and internal write queues still live inside FileProvider and will migrate toward the runtime store boundary in later slices.

File source evidence also exposes file-provider:scan-progress, file-provider:integrity, and file-provider:index-flush. The public IndexedSourceProgressEvidenceService owns the shared progress evidence policy for ready / warming / degraded / permission-required status and reason selection, and the public IndexedSourceProgressStoreService owns completed-root summary, empty delete/upsert skips, upsert readiness gating, and upsert result reporting. FileProviderScanProgressService keeps only File-specific scan_progress table select/delete wiring, worker upsertScanProgress injection, and metadata mapping. FileProviderScanStrategyService owns completed-root reads, new full-scan path selection, reconciliation path selection, and strategy logging; the evidence summarizes watch roots, pending roots, pending permission roots, completed / failed / skipped file index progress, and embedding counts. The public IndexedSourceIntegrityService owns the source-row versus indexed-row ratio policy, runtime reset decision, SearchIndex clear flag, orphan cleanup call, duration, and snapshot mapping; FileProviderIntegrityService keeps only FTS/files row-count queries, runtime reset injection, orphan keyword_mappings cleanup, and File-named integrity evidence mapping. The integrity evidence records the latest FTS rows, files rows, whether a full re-scan was scheduled, whether stale FTS rows or scan_progress were cleared, and how many orphan keywords were removed. Index-flush evidence records the latest content index worker flush status, entries, pending/inflight counts, retry reason, error, and duration; the latest flush snapshot is stored by the generic IndexedWriteFlushSnapshotService, then converted into source evidence by IndexedWriteFlushEvidenceService, while FileProvider only keeps the file-provider:index-flush id/label and File status adaptation. Settings/CoreBox diagnostics can therefore explain why the source is warming, waiting for directory permission, why integrity repair triggered a re-scan, or why worker flush has not written content into the search index without depending on FileProvider-only logs.

FileProvider reset actions are also being consolidated. Manual rebuild, schema migration, and integrity mismatch now go through FileProviderRuntimeResetService when clearing scan_progress or the provider search index. The shared IndexedSourceResetExecutorService owns reset step orchestration, search-index/source-progress cleanup decisions, timestamps, and the SDK-standard IndexedSourceResetResult; FileProvider only injects provider search-index cleanup plus File-specific scan_progress row count/delete wiring. This keeps reset semantics reusable for Browser Bookmarks / Quicklinks without moving FileProvider's SQLite details into the SDK.

FileIndexedSource now exposes resetIndex(), so CoreApp can call IndexingRuntime.resetSourceRuntimeState("file-provider", ...) and reach the same FileProvider reset helper. This path is for runtime maintenance; it does not replace Settings manual rebuild and still relies on later scan/reconcile to repair the index.

After SearchEngineCore initializes the indexed runtime, it injects a reset delegate into FileProvider. FileProvider manual rebuild, schema migration, and integrity mismatch repair prefer that delegate and enter IndexingRuntime.resetSourceRuntimeState("file-provider", { reason }); FileIndexedSource then calls back into the FileProvider reset helper. Without a delegate they fall back to the internal helper. This avoids a direct FileProvider dependency on the runtime singleton while making those reset actions visible in lastReset diagnostics.

Browser Bookmarks now has a runtime skeleton: the browser-bookmarks descriptor uses privacy: "high", owner: "official-plugin", defaultState: "disabled", and permissionScopes: ["browser-data", "file-system"]. CoreApp now has a pure Chromium Bookmarks scanner for Chrome / Edge / Brave / Arc profile discovery, Bookmarks JSON parsing, non-http(s) URL filtering, URL dedupe, and read-failed/not-found/unsupported diagnostics. In explicit-enabled paths it maps bookmarks into kind: "browser-bookmark" IndexedSourceRecordBatch values, exposes scanner-backed health/evidence/roots, maps profile/browser diagnostics into roots/evidence through IndexedSourceProfileDiagnosticsService, and reuses IndexedSourceSnapshotCacheService so one diagnostics refresh does not reread Bookmarks files for health, roots, and evidence separately; scan batches, small-full-refresh reconcile deltas, and Bookmarks watch refresh deltas now reuse IndexedWriteRuntimeEmitterService, and resetIndex() exposes source-level user-clear / health-repair diagnostics while shared SearchIndex cleanup stays in the runtime store boundary. The default registered source still reports disabled/pending migration, but enablement now lives in a lightweight config resolver: it dynamically reads the unified provider config for browser-bookmarks / touch-browser-data.browser-bookmarks, and does not read real browser files while those providers are not explicitly enabled. Persistent rebuild, watch roots, and user-facing clear are not complete yet.

Quicklinks now has a low-privacy runtime skeleton too: the quicklinks descriptor uses owner: "official-plugin", privacy: "low", storage: "sqlite-index", and defaultState: "enabled". The registered CoreApp skeleton supports injected quicklink snapshots for scan batches, reconcile/watch change deltas, source-level health/evidence, and reset/open/clear lifecycle contracts; scan/reconcile/watch output already reuses IndexedWriteRuntimeEmitterService. The default empty source reports degraded quicklinks-empty diagnostics without reading plugin storage or pretending content exists. touch-browser-bookmarks.quicklinks and touch-dev-toolbox.dev-toolbox now declare indexedSourceId: "quicklinks", and CoreApp maps those linked provider configs to source enablement; Quicklinks stays enabled by default, reports quicklinks-provider-disabled when the core quicklinks provider is explicitly disabled, and returns to enabled when any linked official provider is explicitly enabled. The real official-plugin persistent feed, user-facing clear/rebuild, and Settings evidence still need to be connected.

Search Tokens

Plugin Features automatically generate search tokens upon registration, including:

  • Original name (lowercase)
  • Full pinyin
  • Pinyin initials
  • Keywords
  • Command values

Custom Keywords

Add keywords to Features in manifest.json to enhance search matching:

EXAMPLE.JSON
{
  "features": [
    {
      "id": "translate",
      "name": "translate",
      "desc": "Translate selected text",
      "keywords": ["translate", "translation", "fanyi", "fy"]
    }
  ]
}

Match Types & Priority

The search engine matches in the following priority order:

PriorityMatch TypeScore RangeDescription
1Exact1000Title exactly matches query
2Prefix800-900Title starts with query
3Token600-950Pinyin/initials/keyword match
4Contains600-700Title contains query
5Description400Query found in description
6Fuzzy0-500Typo-tolerant match

Highlighting

Search results include a matchResult field for UI highlighting:

EXAMPLE.TYPESCRIPT
interface MatchRange {
  start: number  // Start position
  end: number    // End position (exclusive)
}

// In TuffItem.meta.extension
interface FeatureExtension {
  matchResult?: MatchRange[]
  searchTokens?: string[]
}

Using Highlighting in Renderer

The BoxItem component automatically handles matchResult highlighting:

EXAMPLE.VUE
<h5
  class="text-sm font-semibold truncate"
  v-html="getHighlightedHTML(
    render.basic?.title || '',
    props.item.meta?.extension?.matchResult
  )"
/>

The getHighlightedHTML function wraps matched regions in <span> tags:

EXAMPLE.TYPESCRIPT
function getHighlightedHTML(
  text: string,
  matchedIndices?: MatchRange[],
  opts?: {
    className?: string   // Highlight CSS class
    base?: 0 | 1        // Index base
    inclusiveEnd?: boolean
  }
): string

Using Search Matching in Plugins

Using matchFeature Function

The matchFeature function from @talex-touch/utils/search enables custom search:

EXAMPLE.TYPESCRIPT
import { matchFeature } from '@talex-touch/utils/search'

const result = matchFeature({
  title: 'translate',
  desc: 'Translate selected text',
  searchTokens: ['translate', 'fanyi', 'fy', 'translate'],
  query: 'fanyi',
  enableFuzzy: true
})

if (result.matched) {
  console.log('Match type:', result.matchType)
  console.log('Match score:', result.score)
  console.log('Highlight ranges:', result.matchRanges)
}

FeatureMatchResult Interface

EXAMPLE.TYPESCRIPT
interface FeatureMatchResult {
  /** Whether matched */
  matched: boolean
  /** Match score (0-1000) */
  score: number
  /** Match type */
  matchType: 'exact' | 'token' | 'prefix' | 'contains' | 'fuzzy' | 'none'
  /** Highlight ranges */
  matchRanges: MatchRange[]
  /** Matched token (for debugging) */
  matchedToken?: string
}

Fuzzy Matching API

fuzzyMatch Function

For typo-tolerant search:

EXAMPLE.TYPESCRIPT
import { fuzzyMatch, indicesToRanges } from '@talex-touch/utils/search'

const result = fuzzyMatch('hello', 'helol', 2)

if (result.matched) {
  console.log('Score:', result.score)
  console.log('Matched indices:', result.matchedIndices)

  // Convert to highlight ranges
  const ranges = indicesToRanges(result.matchedIndices)
}

FuzzyMatchResult Interface

EXAMPLE.TYPESCRIPT
interface FuzzyMatchResult {
  /** Whether matched */
  matched: boolean
  /** Match score (0-1) */
  score: number
  /** Array of matched character indices */
  matchedIndices: number[]
}

Command Matching

The Feature commands field is used for precise trigger matching:

EXAMPLE.JSON
{
  "features": [
    {
      "id": "search-web",
      "name": "Search Web",
      "commands": [
        { "type": "over" },
        { "type": "match", "value": ["g ", "google "] },
        { "type": "contain", "value": "search" },
        { "type": "regex", "value": "^s\\s+" }
      ]
    }
  ]
}

Command types:

TypeDescriptionExample
overAlways matchShows in empty search results
matchPrefix matchg hello matches g
containContains matchI want to search matches search
regexRegex matchs hello matches ^s\\s+

Clipboard State Sync

Search queries include current clipboard state:

EXAMPLE.TYPESCRIPT
interface TuffQuery {
  text: string
  inputs?: TuffQueryInput[]
}

interface TuffQueryInput {
  type: TuffInputType  // 'text' | 'image' | 'files' | 'html'
  content: string
  thumbnail?: string
  rawContent?: string
  metadata?: Record<string, unknown>
}

Declaring Accepted Input Types

Declare acceptedInputTypes in Features to receive clipboard content:

EXAMPLE.JSON
{
  "features": [
    {
      "id": "image-ocr",
      "name": "Image OCR",
      "acceptedInputTypes": ["image"]
    }
  ]
}

Supported input types:

  • text - Plain text
  • image - Image (Base64)
  • files - File path list
  • html - HTML rich text

Best Practices

  1. Provide multilingual keywords: Include both English and Chinese in keywords
  2. Use meaningful Feature names: Names automatically generate pinyin tokens
  3. Declare acceptedInputTypes: Explicitly state what input types your Feature can handle
  4. Use appropriate command types: over for general features, match for specific prefix triggers

Technical Notes

  • Search tokens are generated at Feature registration and feed the scoring pipeline.
  • matchFeature and fuzzyMatch return match types, scores, and highlight ranges for rendering.