@talex-touch/utils 1.0.39 → 1.0.42

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.
@@ -14,6 +14,7 @@ class StorageSubscriptionManager {
14
14
  private subscribers = new Map<string, Set<StorageSubscriptionCallback>>()
15
15
  private channelListenerRegistered = false
16
16
  private pendingUpdates = new Map<string, NodeJS.Timeout>()
17
+ private configVersions = new Map<string, number>()
17
18
 
18
19
  /**
19
20
  * Initialize the subscription manager with a channel
@@ -24,8 +25,15 @@ class StorageSubscriptionManager {
24
25
  if (!this.channelListenerRegistered) {
25
26
  // Listen to storage:update events from main process
26
27
  this.channel.regChannel('storage:update', ({ data }) => {
27
- const { name } = data as { name: string }
28
- this.handleStorageUpdate(name)
28
+ const { name, version } = data as { name: string, version?: number }
29
+ // Only handle update if version is newer or unknown
30
+ const currentVersion = this.configVersions.get(name) ?? 0
31
+ if (version === undefined || version > currentVersion) {
32
+ if (version !== undefined) {
33
+ this.configVersions.set(name, version)
34
+ }
35
+ this.handleStorageUpdate(name)
36
+ }
29
37
  })
30
38
  this.channelListenerRegistered = true
31
39
  }
@@ -36,13 +44,13 @@ class StorageSubscriptionManager {
36
44
  * @param configName - The configuration file name (e.g., 'app-setting.ini')
37
45
  * @param callback - Callback function to receive updates
38
46
  * @returns Unsubscribe function
39
- *
47
+ *
40
48
  * @example
41
49
  * ```typescript
42
50
  * const unsubscribe = subscribeStorage('app-setting.ini', (data) => {
43
51
  * console.log('Config updated:', data)
44
52
  * })
45
- *
53
+ *
46
54
  * // Later:
47
55
  * unsubscribe()
48
56
  * ```
@@ -155,7 +163,7 @@ const subscriptionManager = new StorageSubscriptionManager()
155
163
  /**
156
164
  * Initialize storage subscription system with channel
157
165
  * Must be called before using subscribeStorage
158
- *
166
+ *
159
167
  * @param channel - The storage channel
160
168
  */
161
169
  export function initStorageSubscription(channel: IStorageChannel): void {
@@ -164,19 +172,19 @@ export function initStorageSubscription(channel: IStorageChannel): void {
164
172
 
165
173
  /**
166
174
  * Subscribe to storage configuration changes
167
- *
175
+ *
168
176
  * @param configName - Configuration file name (e.g., 'app-setting.ini')
169
177
  * @param callback - Callback function that receives updated data
170
178
  * @returns Unsubscribe function
171
- *
179
+ *
172
180
  * @example
173
181
  * ```typescript
174
182
  * import { subscribeStorage } from '@talex-touch/utils/renderer/storage/storage-subscription'
175
- *
183
+ *
176
184
  * const unsubscribe = subscribeStorage('app-setting.ini', (data) => {
177
185
  * console.log('Settings updated:', data)
178
186
  * })
179
- *
187
+ *
180
188
  * // Clean up when no longer needed
181
189
  * unsubscribe()
182
190
  * ```
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Fuzzy matching utilities for typo-tolerant search
3
+ * Supports matching queries like "helol" to "hello"
4
+ */
5
+
6
+ export interface FuzzyMatchResult {
7
+ /** Whether the match was successful */
8
+ matched: boolean
9
+ /** Match score (0-1, higher is better) */
10
+ score: number
11
+ /** Indices of matched characters in the target string */
12
+ matchedIndices: number[]
13
+ }
14
+
15
+ /**
16
+ * Performs fuzzy matching with typo tolerance
17
+ * Uses a combination of subsequence matching and edit distance
18
+ *
19
+ * @param target - The string to match against (e.g., "hello")
20
+ * @param query - The search query (e.g., "helol")
21
+ * @param maxErrors - Maximum allowed errors (default: 2)
22
+ * @returns FuzzyMatchResult with match info and indices
23
+ */
24
+ export function fuzzyMatch(
25
+ target: string,
26
+ query: string,
27
+ maxErrors = 2
28
+ ): FuzzyMatchResult {
29
+ if (!query || !target) {
30
+ return { matched: false, score: 0, matchedIndices: [] }
31
+ }
32
+
33
+ const targetLower = target.toLowerCase()
34
+ const queryLower = query.toLowerCase()
35
+
36
+ // Exact match - highest score
37
+ if (targetLower === queryLower) {
38
+ return {
39
+ matched: true,
40
+ score: 1,
41
+ matchedIndices: Array.from({ length: target.length }, (_, i) => i)
42
+ }
43
+ }
44
+
45
+ // Substring match
46
+ const substringIndex = targetLower.indexOf(queryLower)
47
+ if (substringIndex !== -1) {
48
+ return {
49
+ matched: true,
50
+ score: 0.95,
51
+ matchedIndices: Array.from({ length: query.length }, (_, i) => substringIndex + i)
52
+ }
53
+ }
54
+
55
+ // Try subsequence matching first (for cases like "vsc" -> "Visual Studio Code")
56
+ const subsequenceResult = subsequenceMatch(targetLower, queryLower)
57
+ if (subsequenceResult.matched && subsequenceResult.matchedIndices.length === queryLower.length) {
58
+ return {
59
+ matched: true,
60
+ score: 0.8 + (subsequenceResult.matchedIndices.length / target.length) * 0.1,
61
+ matchedIndices: subsequenceResult.matchedIndices
62
+ }
63
+ }
64
+
65
+ // Fuzzy match with edit distance for typo tolerance
66
+ const fuzzyResult = fuzzyMatchWithErrors(targetLower, queryLower, maxErrors)
67
+ if (fuzzyResult.matched) {
68
+ return fuzzyResult
69
+ }
70
+
71
+ return { matched: false, score: 0, matchedIndices: [] }
72
+ }
73
+
74
+ /**
75
+ * Subsequence matching - matches characters in order but not necessarily consecutive
76
+ * e.g., "vsc" matches "Visual Studio Code" at indices [0, 7, 14]
77
+ */
78
+ function subsequenceMatch(
79
+ target: string,
80
+ query: string
81
+ ): { matched: boolean; matchedIndices: number[] } {
82
+ const matchedIndices: number[] = []
83
+ let queryIdx = 0
84
+
85
+ for (let i = 0; i < target.length && queryIdx < query.length; i++) {
86
+ if (target[i] === query[queryIdx]) {
87
+ matchedIndices.push(i)
88
+ queryIdx++
89
+ }
90
+ }
91
+
92
+ return {
93
+ matched: queryIdx === query.length,
94
+ matchedIndices
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Fuzzy matching with error tolerance using dynamic programming
100
+ * Finds the best matching substring allowing for insertions, deletions, and substitutions
101
+ */
102
+ function fuzzyMatchWithErrors(
103
+ target: string,
104
+ query: string,
105
+ maxErrors: number
106
+ ): FuzzyMatchResult {
107
+ const m = query.length
108
+ const n = target.length
109
+
110
+ if (m === 0) return { matched: true, score: 1, matchedIndices: [] }
111
+ if (n === 0) return { matched: false, score: 0, matchedIndices: [] }
112
+
113
+ // Allow more errors for longer queries
114
+ const allowedErrors = Math.min(maxErrors, Math.floor(m / 3) + 1)
115
+
116
+ // Find best matching window using sliding window with edit distance
117
+ let bestScore = 0
118
+ let bestStart = -1
119
+ let bestMatchedIndices: number[] = []
120
+
121
+ // Try different window sizes around query length
122
+ const minWindowSize = Math.max(1, m - allowedErrors)
123
+ const maxWindowSize = Math.min(n, m + allowedErrors)
124
+
125
+ for (let windowSize = minWindowSize; windowSize <= maxWindowSize; windowSize++) {
126
+ for (let start = 0; start <= n - windowSize; start++) {
127
+ const window = target.substring(start, start + windowSize)
128
+ const { distance, matchedIndices } = editDistanceWithPath(window, query)
129
+
130
+ if (distance <= allowedErrors) {
131
+ // Calculate score based on edit distance and position
132
+ const score = calculateFuzzyScore(distance, m, start, n)
133
+
134
+ if (score > bestScore) {
135
+ bestScore = score
136
+ bestStart = start
137
+ // Adjust indices to be relative to the full target string
138
+ bestMatchedIndices = matchedIndices.map((i) => start + i)
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ if (bestStart !== -1) {
145
+ return {
146
+ matched: true,
147
+ score: bestScore,
148
+ matchedIndices: bestMatchedIndices
149
+ }
150
+ }
151
+
152
+ return { matched: false, score: 0, matchedIndices: [] }
153
+ }
154
+
155
+ /**
156
+ * Computes edit distance and tracks which characters matched
157
+ */
158
+ function editDistanceWithPath(
159
+ s1: string,
160
+ s2: string
161
+ ): { distance: number; matchedIndices: number[] } {
162
+ const m = s1.length
163
+ const n = s2.length
164
+
165
+ // DP table
166
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))
167
+
168
+ // Initialize
169
+ for (let i = 0; i <= m; i++) dp[i][0] = i
170
+ for (let j = 0; j <= n; j++) dp[0][j] = j
171
+
172
+ // Fill DP table
173
+ for (let i = 1; i <= m; i++) {
174
+ for (let j = 1; j <= n; j++) {
175
+ if (s1[i - 1] === s2[j - 1]) {
176
+ dp[i][j] = dp[i - 1][j - 1]
177
+ } else {
178
+ dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
179
+ }
180
+ }
181
+ }
182
+
183
+ // Backtrack to find matched indices
184
+ const matchedIndices: number[] = []
185
+ let i = m
186
+ let j = n
187
+
188
+ while (i > 0 && j > 0) {
189
+ if (s1[i - 1] === s2[j - 1]) {
190
+ matchedIndices.unshift(i - 1)
191
+ i--
192
+ j--
193
+ } else if (dp[i - 1][j - 1] <= dp[i - 1][j] && dp[i - 1][j - 1] <= dp[i][j - 1]) {
194
+ // Substitution
195
+ i--
196
+ j--
197
+ } else if (dp[i - 1][j] <= dp[i][j - 1]) {
198
+ // Deletion from s1
199
+ i--
200
+ } else {
201
+ // Insertion into s1
202
+ j--
203
+ }
204
+ }
205
+
206
+ return { distance: dp[m][n], matchedIndices }
207
+ }
208
+
209
+ /**
210
+ * Calculate fuzzy match score based on various factors
211
+ */
212
+ function calculateFuzzyScore(
213
+ editDistance: number,
214
+ queryLength: number,
215
+ matchStart: number,
216
+ targetLength: number
217
+ ): number {
218
+ // Base score from edit distance (0.5 - 0.7 range for fuzzy matches)
219
+ const distanceScore = Math.max(0, 1 - editDistance / queryLength) * 0.3 + 0.4
220
+
221
+ // Bonus for matching at the start
222
+ const positionBonus = matchStart === 0 ? 0.15 : 0
223
+
224
+ // Bonus for shorter targets (more specific matches)
225
+ const lengthBonus = Math.min(0.1, queryLength / targetLength * 0.1)
226
+
227
+ return Math.min(0.75, distanceScore + positionBonus + lengthBonus)
228
+ }
229
+
230
+ /**
231
+ * Convert matched indices to Range array for highlighting
232
+ */
233
+ export function indicesToRanges(indices: number[]): Array<{ start: number; end: number }> {
234
+ if (!indices.length) return []
235
+
236
+ const sorted = Array.from(new Set(indices)).sort((a, b) => a - b)
237
+ const ranges: Array<{ start: number; end: number }> = []
238
+
239
+ let start = sorted[0]
240
+ let end = sorted[0] + 1
241
+
242
+ for (let i = 1; i < sorted.length; i++) {
243
+ if (sorted[i] === end) {
244
+ end++
245
+ } else {
246
+ ranges.push({ start, end })
247
+ start = sorted[i]
248
+ end = sorted[i] + 1
249
+ }
250
+ }
251
+ ranges.push({ start, end })
252
+
253
+ return ranges
254
+ }
@@ -93,6 +93,23 @@ export interface HeaderConfig {
93
93
  */
94
94
  export type DivisionBoxSize = 'compact' | 'medium' | 'expanded'
95
95
 
96
+ /**
97
+ * UI configuration for DivisionBox CoreBox header
98
+ */
99
+ export interface DivisionBoxUIConfig {
100
+ /** Show the search input in header */
101
+ showInput?: boolean
102
+
103
+ /** Placeholder text for search input */
104
+ inputPlaceholder?: string
105
+
106
+ /** Show result list area */
107
+ showResults?: boolean
108
+
109
+ /** Initial input value */
110
+ initialInput?: string
111
+ }
112
+
96
113
  /**
97
114
  * Configuration for creating a DivisionBox instance
98
115
  */
@@ -118,6 +135,9 @@ export interface DivisionBoxConfig {
118
135
  /** Header configuration */
119
136
  header?: HeaderConfig
120
137
 
138
+ /** UI configuration for CoreBox header display */
139
+ ui?: DivisionBoxUIConfig
140
+
121
141
  /** WebContentsView preferences (main process only) */
122
142
  webPreferences?: any
123
143
  }
package/types/flow.ts ADDED
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Flow Transfer Type Definitions
3
+ *
4
+ * Core types for the plugin-to-plugin data flow transfer system.
5
+ * Enables plugins to share structured data with other plugins' features.
6
+ */
7
+
8
+ import type { TuffQuery } from '../core-box/tuff/tuff-dsl'
9
+
10
+ /**
11
+ * Flow payload types
12
+ */
13
+ export type FlowPayloadType = 'text' | 'image' | 'files' | 'json' | 'html' | 'custom'
14
+
15
+ /**
16
+ * Flow session states
17
+ */
18
+ export type FlowSessionState =
19
+ | 'INIT'
20
+ | 'TARGET_SELECTING'
21
+ | 'TARGET_SELECTED'
22
+ | 'DELIVERING'
23
+ | 'DELIVERED'
24
+ | 'PROCESSING'
25
+ | 'ACKED'
26
+ | 'FAILED'
27
+ | 'CANCELLED'
28
+
29
+ /**
30
+ * Flow error codes
31
+ */
32
+ export enum FlowErrorCode {
33
+ SENDER_NOT_ALLOWED = 'SENDER_NOT_ALLOWED',
34
+ TARGET_NOT_FOUND = 'TARGET_NOT_FOUND',
35
+ TARGET_OFFLINE = 'TARGET_OFFLINE',
36
+ PAYLOAD_INVALID = 'PAYLOAD_INVALID',
37
+ PAYLOAD_TOO_LARGE = 'PAYLOAD_TOO_LARGE',
38
+ TYPE_NOT_SUPPORTED = 'TYPE_NOT_SUPPORTED',
39
+ PERMISSION_DENIED = 'PERMISSION_DENIED',
40
+ TIMEOUT = 'TIMEOUT',
41
+ CANCELLED = 'CANCELLED',
42
+ INTERNAL_ERROR = 'INTERNAL_ERROR'
43
+ }
44
+
45
+ /**
46
+ * Flow error
47
+ */
48
+ export interface FlowError {
49
+ code: FlowErrorCode
50
+ message: string
51
+ details?: Record<string, any>
52
+ }
53
+
54
+ /**
55
+ * Flow payload context
56
+ */
57
+ export interface FlowPayloadContext {
58
+ /** Source plugin ID */
59
+ sourcePluginId: string
60
+
61
+ /** Source feature ID (optional) */
62
+ sourceFeatureId?: string
63
+
64
+ /** Original query (if triggered from CoreBox) */
65
+ originalQuery?: TuffQuery
66
+
67
+ /** Custom metadata */
68
+ metadata?: Record<string, any>
69
+ }
70
+
71
+ /**
72
+ * Flow payload - data being transferred
73
+ */
74
+ export interface FlowPayload {
75
+ /** Payload type */
76
+ type: FlowPayloadType
77
+
78
+ /** Main data content */
79
+ data: string | object
80
+
81
+ /** MIME type (optional) */
82
+ mimeType?: string
83
+
84
+ /** Context information */
85
+ context?: FlowPayloadContext
86
+ }
87
+
88
+ /**
89
+ * Flow target capabilities
90
+ */
91
+ export interface FlowTargetCapabilities {
92
+ /** Whether authentication is required */
93
+ requiresAuth?: boolean
94
+
95
+ /** Allowed sender plugin IDs (whitelist) */
96
+ allowedSenders?: string[]
97
+
98
+ /** Maximum payload size in bytes */
99
+ maxPayloadSize?: number
100
+ }
101
+
102
+ /**
103
+ * Flow target - declared in plugin manifest
104
+ */
105
+ export interface FlowTarget {
106
+ /** Target unique ID (unique within plugin) */
107
+ id: string
108
+
109
+ /** Display name */
110
+ name: string
111
+
112
+ /** Description */
113
+ description?: string
114
+
115
+ /** Supported payload types */
116
+ supportedTypes: FlowPayloadType[]
117
+
118
+ /** Icon (iconify format) */
119
+ icon?: string
120
+
121
+ /** Associated feature ID (optional) */
122
+ featureId?: string
123
+
124
+ /** Associated action ID (optional) */
125
+ actionId?: string
126
+
127
+ /** Whether user confirmation is required */
128
+ requireConfirm?: boolean
129
+
130
+ /** Capability requirements */
131
+ capabilities?: FlowTargetCapabilities
132
+ }
133
+
134
+ /**
135
+ * Flow target info - runtime information
136
+ */
137
+ export interface FlowTargetInfo extends FlowTarget {
138
+ /** Full target ID (pluginId.targetId) */
139
+ fullId: string
140
+
141
+ /** Plugin ID */
142
+ pluginId: string
143
+
144
+ /** Plugin name */
145
+ pluginName?: string
146
+
147
+ /** Plugin icon */
148
+ pluginIcon?: string
149
+
150
+ /** Whether the plugin is currently enabled */
151
+ isEnabled: boolean
152
+
153
+ /** Recent usage count */
154
+ usageCount?: number
155
+
156
+ /** Last used timestamp */
157
+ lastUsed?: number
158
+ }
159
+
160
+ /**
161
+ * Flow session - a complete flow operation
162
+ */
163
+ export interface FlowSession {
164
+ /** Session unique ID */
165
+ sessionId: string
166
+
167
+ /** Session state */
168
+ state: FlowSessionState
169
+
170
+ /** Sender plugin ID */
171
+ senderId: string
172
+
173
+ /** Target plugin ID */
174
+ targetPluginId: string
175
+
176
+ /** Target endpoint ID */
177
+ targetId: string
178
+
179
+ /** Full target ID (pluginId.targetId) */
180
+ fullTargetId: string
181
+
182
+ /** Payload data */
183
+ payload: FlowPayload
184
+
185
+ /** Creation timestamp */
186
+ createdAt: number
187
+
188
+ /** Update timestamp */
189
+ updatedAt: number
190
+
191
+ /** Acknowledgment payload (if any) */
192
+ ackPayload?: any
193
+
194
+ /** Error information (if failed) */
195
+ error?: FlowError
196
+ }
197
+
198
+ /**
199
+ * Flow dispatch options
200
+ */
201
+ export interface FlowDispatchOptions {
202
+ /** Display title (for selector panel) */
203
+ title?: string
204
+
205
+ /** Display description */
206
+ description?: string
207
+
208
+ /** Preferred target (bundleId.targetId or tuffItemId) */
209
+ preferredTarget?: string
210
+
211
+ /** Skip selector panel (requires preferredTarget) */
212
+ skipSelector?: boolean
213
+
214
+ /** Timeout in milliseconds (default: 30000) */
215
+ timeout?: number
216
+
217
+ /** Fallback action on failure */
218
+ fallbackAction?: 'copy' | 'none'
219
+
220
+ /** Whether acknowledgment is required */
221
+ requireAck?: boolean
222
+ }
223
+
224
+ /**
225
+ * Flow dispatch result
226
+ */
227
+ export interface FlowDispatchResult {
228
+ /** Session ID */
229
+ sessionId: string
230
+
231
+ /** Final state */
232
+ state: FlowSessionState
233
+
234
+ /** Acknowledgment payload */
235
+ ackPayload?: any
236
+
237
+ /** Error (if failed) */
238
+ error?: FlowError
239
+ }
240
+
241
+ /**
242
+ * Flow session update event
243
+ */
244
+ export interface FlowSessionUpdate {
245
+ /** Session ID */
246
+ sessionId: string
247
+
248
+ /** Previous state */
249
+ previousState: FlowSessionState
250
+
251
+ /** Current state */
252
+ currentState: FlowSessionState
253
+
254
+ /** Timestamp */
255
+ timestamp: number
256
+
257
+ /** Additional data */
258
+ data?: any
259
+ }
260
+
261
+ /**
262
+ * IPC channels for Flow operations
263
+ */
264
+ export enum FlowIPCChannel {
265
+ DISPATCH = 'flow:dispatch',
266
+ GET_TARGETS = 'flow:get-targets',
267
+ CANCEL = 'flow:cancel',
268
+ ACKNOWLEDGE = 'flow:acknowledge',
269
+ REPORT_ERROR = 'flow:report-error',
270
+ SESSION_UPDATE = 'flow:session-update',
271
+ DELIVER = 'flow:deliver'
272
+ }
273
+
274
+ /**
275
+ * Flow manifest configuration (in plugin manifest.json)
276
+ */
277
+ export interface FlowManifestConfig {
278
+ /** Whether this plugin can send flows */
279
+ flowSender?: boolean
280
+
281
+ /** Flow targets this plugin can receive */
282
+ flowTargets?: FlowTarget[]
283
+ }
package/types/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './division-box'
2
+ export * from './flow'
2
3
  export * from './intelligence'
3
4
  export * from './modules'
4
5
  export * from './storage'