@siftd/connect-agent 0.2.29 → 0.2.31

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.
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Asset System - Fast-path preview architecture
3
+ *
4
+ * Assets are files produced by workers with metadata for instant preview.
5
+ * The preview-worker generates thumbnails/metadata WITHOUT using the LLM.
6
+ */
7
+ import { createHash } from 'crypto';
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'fs';
9
+ import { basename, extname, join } from 'path';
10
+ // ============================================================================
11
+ // UTILITY FUNCTIONS
12
+ // ============================================================================
13
+ /**
14
+ * Generate SHA256 hash of file contents
15
+ */
16
+ export function hashFile(filePath) {
17
+ const content = readFileSync(filePath);
18
+ return createHash('sha256').update(content).digest('hex');
19
+ }
20
+ /**
21
+ * Get MIME type from file extension
22
+ */
23
+ export function getMimeType(filePath) {
24
+ const ext = extname(filePath).toLowerCase().slice(1);
25
+ const mimeMap = {
26
+ // Images
27
+ png: 'image/png',
28
+ jpg: 'image/jpeg',
29
+ jpeg: 'image/jpeg',
30
+ gif: 'image/gif',
31
+ svg: 'image/svg+xml',
32
+ webp: 'image/webp',
33
+ ico: 'image/x-icon',
34
+ // Documents
35
+ pdf: 'application/pdf',
36
+ doc: 'application/msword',
37
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
38
+ // Code/Text
39
+ js: 'text/javascript',
40
+ ts: 'text/typescript',
41
+ jsx: 'text/javascript',
42
+ tsx: 'text/typescript',
43
+ json: 'application/json',
44
+ html: 'text/html',
45
+ css: 'text/css',
46
+ md: 'text/markdown',
47
+ txt: 'text/plain',
48
+ csv: 'text/csv',
49
+ xml: 'text/xml',
50
+ yaml: 'text/yaml',
51
+ yml: 'text/yaml',
52
+ sh: 'text/x-shellscript',
53
+ py: 'text/x-python',
54
+ // Data
55
+ sqlite: 'application/x-sqlite3',
56
+ db: 'application/x-sqlite3',
57
+ // Video/Audio
58
+ mp4: 'video/mp4',
59
+ webm: 'video/webm',
60
+ mp3: 'audio/mpeg',
61
+ wav: 'audio/wav',
62
+ };
63
+ return mimeMap[ext] || 'application/octet-stream';
64
+ }
65
+ /**
66
+ * Generate unique asset ID
67
+ */
68
+ export function generateAssetId() {
69
+ const timestamp = Date.now().toString(36);
70
+ const random = Math.random().toString(36).substring(2, 8);
71
+ return `asset_${timestamp}_${random}`;
72
+ }
73
+ /**
74
+ * Create asset manifest for a file
75
+ */
76
+ export function createAssetManifest(filePath, options = {}) {
77
+ const stats = statSync(filePath);
78
+ return {
79
+ id: generateAssetId(),
80
+ path: filePath,
81
+ name: basename(filePath),
82
+ mime: getMimeType(filePath),
83
+ sha256: hashFile(filePath),
84
+ size: stats.size,
85
+ createdAt: Date.now(),
86
+ groupId: options.groupId,
87
+ label: options.label,
88
+ kind: 'file',
89
+ };
90
+ }
91
+ /**
92
+ * Write asset manifest alongside the file
93
+ */
94
+ export function writeAssetManifest(manifest) {
95
+ const manifestPath = manifest.path + '.asset.json';
96
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
97
+ }
98
+ /**
99
+ * Read asset manifest for a file
100
+ */
101
+ export function readAssetManifest(filePath) {
102
+ const manifestPath = filePath + '.asset.json';
103
+ if (!existsSync(manifestPath))
104
+ return null;
105
+ try {
106
+ return JSON.parse(readFileSync(manifestPath, 'utf-8'));
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ /**
113
+ * Get preview cache directory
114
+ */
115
+ export function getPreviewCacheDir() {
116
+ const home = process.env.HOME || '/tmp';
117
+ const cacheDir = join(home, '.connect-hub', 'preview-cache');
118
+ if (!existsSync(cacheDir)) {
119
+ mkdirSync(cacheDir, { recursive: true });
120
+ }
121
+ return cacheDir;
122
+ }
123
+ /**
124
+ * Generate cache key for preview
125
+ */
126
+ export function getPreviewCacheKey(sha256, params) {
127
+ const paramsStr = params ? JSON.stringify(params) : '';
128
+ return createHash('sha256').update(sha256 + paramsStr).digest('hex').substring(0, 16);
129
+ }
130
+ /**
131
+ * Check if preview exists in cache
132
+ */
133
+ export function getPreviewFromCache(cacheKey) {
134
+ const cachePath = join(getPreviewCacheDir(), `${cacheKey}.json`);
135
+ if (!existsSync(cachePath))
136
+ return null;
137
+ try {
138
+ return JSON.parse(readFileSync(cachePath, 'utf-8'));
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ }
144
+ /**
145
+ * Save preview to cache
146
+ */
147
+ export function savePreviewToCache(preview) {
148
+ const cachePath = join(getPreviewCacheDir(), `${preview.cacheKey}.json`);
149
+ writeFileSync(cachePath, JSON.stringify(preview, null, 2));
150
+ }
151
+ /**
152
+ * Create a ViewSpec for comparing two assets
153
+ */
154
+ export function createCompareView(beforeAssetId, afterAssetId, title = 'Comparison') {
155
+ return {
156
+ id: `view_${Date.now().toString(36)}`,
157
+ title,
158
+ layout: 'compare',
159
+ before: { type: 'asset', assetId: beforeAssetId, label: 'Before' },
160
+ after: { type: 'asset', assetId: afterAssetId, label: 'After' },
161
+ createdAt: Date.now(),
162
+ };
163
+ }
164
+ /**
165
+ * Create a ViewSpec for a grid of assets
166
+ */
167
+ export function createGridView(assetIds, title = 'Gallery') {
168
+ return {
169
+ id: `view_${Date.now().toString(36)}`,
170
+ title,
171
+ layout: 'grid',
172
+ items: assetIds.map(id => ({ type: 'asset', assetId: id })),
173
+ createdAt: Date.now(),
174
+ };
175
+ }
176
+ /**
177
+ * Create a ViewSpec for a report with markdown and assets
178
+ */
179
+ export function createReportView(title, sections) {
180
+ return {
181
+ id: `view_${Date.now().toString(36)}`,
182
+ title,
183
+ layout: 'stack',
184
+ items: sections.map(section => {
185
+ if (section.markdown) {
186
+ return { type: 'markdown', content: section.markdown, label: section.label };
187
+ }
188
+ return { type: 'asset', assetId: section.assetId, label: section.label };
189
+ }),
190
+ createdAt: Date.now(),
191
+ };
192
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Preview Worker - Fast-path local preview generation
3
+ *
4
+ * Watches ~/Lia-Hub/shared/outputs/ and generates:
5
+ * - Thumbnails (for images)
6
+ * - Metadata extraction (dimensions, page count, text snippets)
7
+ * - Preview JSON for instant gallery display
8
+ *
9
+ * NO LLM involvement - pure local, deterministic processing.
10
+ * Targets sub-second response for most assets.
11
+ */
12
+ import { AssetManifest, AssetPreview } from './assets.js';
13
+ export interface PreviewWorkerOptions {
14
+ /** Directories to watch */
15
+ watchDirs?: string[];
16
+ /** Max concurrent preview jobs */
17
+ concurrency?: number;
18
+ /** Callback when asset is discovered */
19
+ onAsset?: (manifest: AssetManifest) => void;
20
+ /** Callback when preview is ready */
21
+ onPreview?: (preview: AssetPreview) => void;
22
+ /** Enable verbose logging */
23
+ verbose?: boolean;
24
+ }
25
+ export declare class PreviewWorker {
26
+ private watchers;
27
+ private queue;
28
+ private processing;
29
+ private concurrency;
30
+ private options;
31
+ private running;
32
+ constructor(options?: PreviewWorkerOptions);
33
+ /**
34
+ * Start watching directories
35
+ */
36
+ start(): void;
37
+ /**
38
+ * Stop watching
39
+ */
40
+ stop(): void;
41
+ /**
42
+ * Scan directory for existing files
43
+ */
44
+ private scanDirectory;
45
+ /**
46
+ * Add file to processing queue
47
+ */
48
+ private enqueue;
49
+ /**
50
+ * Process queue with concurrency limit
51
+ */
52
+ private processQueue;
53
+ /**
54
+ * Process a single file - generate manifest and preview
55
+ */
56
+ private processFile;
57
+ /**
58
+ * Generate preview for an asset
59
+ */
60
+ private generatePreview;
61
+ /**
62
+ * Get image dimensions using sips (macOS) or file command
63
+ */
64
+ private getImageInfo;
65
+ /**
66
+ * Generate thumbnail as base64 data URL
67
+ * Uses sips on macOS for fast resizing
68
+ */
69
+ private generateImageThumbnail;
70
+ /**
71
+ * Get PDF info using pdfinfo or mdls
72
+ */
73
+ private getPdfInfo;
74
+ /**
75
+ * Get text snippet from file
76
+ */
77
+ private getTextSnippet;
78
+ /**
79
+ * Extract group ID from file path
80
+ * e.g., ~/Lia-Hub/shared/outputs/run_abc123/file.png -> run_abc123
81
+ */
82
+ private extractGroupId;
83
+ private log;
84
+ }
85
+ export interface AssetRegistry {
86
+ assets: Map<string, AssetManifest>;
87
+ previews: Map<string, AssetPreview>;
88
+ byGroup: Map<string, Set<string>>;
89
+ }
90
+ export declare function getAssetRegistry(): AssetRegistry;
91
+ export declare function registerAsset(manifest: AssetManifest): void;
92
+ export declare function registerPreview(preview: AssetPreview): void;
93
+ export declare function getAsset(assetId: string): AssetManifest | undefined;
94
+ export declare function getPreview(assetId: string): AssetPreview | undefined;
95
+ export declare function getAssetsByGroup(groupId: string): AssetManifest[];
96
+ export declare function getAllAssets(): AssetManifest[];
97
+ export declare function startPreviewWorker(options?: PreviewWorkerOptions): PreviewWorker;
98
+ export declare function stopPreviewWorker(): void;
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Preview Worker - Fast-path local preview generation
3
+ *
4
+ * Watches ~/Lia-Hub/shared/outputs/ and generates:
5
+ * - Thumbnails (for images)
6
+ * - Metadata extraction (dimensions, page count, text snippets)
7
+ * - Preview JSON for instant gallery display
8
+ *
9
+ * NO LLM involvement - pure local, deterministic processing.
10
+ * Targets sub-second response for most assets.
11
+ */
12
+ import { watch } from 'fs';
13
+ import { readFileSync, existsSync, readdirSync, statSync, mkdirSync } from 'fs';
14
+ import { join, basename } from 'path';
15
+ import { execSync } from 'child_process';
16
+ import { createAssetManifest, writeAssetManifest, readAssetManifest, getPreviewCacheDir, getPreviewCacheKey, getPreviewFromCache, savePreviewToCache, } from './assets.js';
17
+ import { getSharedOutputPath } from './hub.js';
18
+ export class PreviewWorker {
19
+ watchers = [];
20
+ queue = [];
21
+ processing = new Set();
22
+ concurrency;
23
+ options;
24
+ running = false;
25
+ constructor(options = {}) {
26
+ this.options = options;
27
+ this.concurrency = options.concurrency || 4;
28
+ }
29
+ /**
30
+ * Start watching directories
31
+ */
32
+ start() {
33
+ if (this.running)
34
+ return;
35
+ this.running = true;
36
+ const dirs = this.options.watchDirs || [getSharedOutputPath()];
37
+ for (const dir of dirs) {
38
+ if (!existsSync(dir)) {
39
+ mkdirSync(dir, { recursive: true });
40
+ }
41
+ this.log(`Watching: ${dir}`);
42
+ // Initial scan
43
+ this.scanDirectory(dir);
44
+ // Watch for changes
45
+ const watcher = watch(dir, { recursive: true }, (event, filename) => {
46
+ if (!filename)
47
+ return;
48
+ const filePath = join(dir, filename);
49
+ // Skip manifest files and hidden files
50
+ if (filename.endsWith('.asset.json') || filename.startsWith('.'))
51
+ return;
52
+ // Skip directories
53
+ try {
54
+ if (statSync(filePath).isDirectory())
55
+ return;
56
+ }
57
+ catch {
58
+ return; // File may have been deleted
59
+ }
60
+ this.enqueue(filePath);
61
+ });
62
+ this.watchers.push(watcher);
63
+ }
64
+ // Start processing queue
65
+ this.processQueue();
66
+ }
67
+ /**
68
+ * Stop watching
69
+ */
70
+ stop() {
71
+ this.running = false;
72
+ for (const watcher of this.watchers) {
73
+ watcher.close();
74
+ }
75
+ this.watchers = [];
76
+ }
77
+ /**
78
+ * Scan directory for existing files
79
+ */
80
+ scanDirectory(dir) {
81
+ try {
82
+ const entries = readdirSync(dir, { withFileTypes: true });
83
+ for (const entry of entries) {
84
+ const fullPath = join(dir, entry.name);
85
+ if (entry.isDirectory()) {
86
+ this.scanDirectory(fullPath);
87
+ }
88
+ else if (!entry.name.endsWith('.asset.json') && !entry.name.startsWith('.')) {
89
+ this.enqueue(fullPath);
90
+ }
91
+ }
92
+ }
93
+ catch (error) {
94
+ this.log(`Scan error: ${error}`);
95
+ }
96
+ }
97
+ /**
98
+ * Add file to processing queue
99
+ */
100
+ enqueue(filePath) {
101
+ if (this.queue.includes(filePath) || this.processing.has(filePath)) {
102
+ return;
103
+ }
104
+ this.queue.push(filePath);
105
+ this.processQueue();
106
+ }
107
+ /**
108
+ * Process queue with concurrency limit
109
+ */
110
+ async processQueue() {
111
+ while (this.running && this.queue.length > 0 && this.processing.size < this.concurrency) {
112
+ const filePath = this.queue.shift();
113
+ if (!filePath)
114
+ continue;
115
+ this.processing.add(filePath);
116
+ this.processFile(filePath)
117
+ .catch(err => this.log(`Error processing ${filePath}: ${err}`))
118
+ .finally(() => {
119
+ this.processing.delete(filePath);
120
+ this.processQueue();
121
+ });
122
+ }
123
+ }
124
+ /**
125
+ * Process a single file - generate manifest and preview
126
+ */
127
+ async processFile(filePath) {
128
+ // Skip if file no longer exists
129
+ if (!existsSync(filePath))
130
+ return;
131
+ const startTime = Date.now();
132
+ // Check for existing manifest
133
+ let manifest = readAssetManifest(filePath);
134
+ if (!manifest) {
135
+ // Create new manifest
136
+ manifest = createAssetManifest(filePath, {
137
+ groupId: this.extractGroupId(filePath),
138
+ });
139
+ writeAssetManifest(manifest);
140
+ this.log(`Created manifest: ${manifest.name} (${manifest.id})`);
141
+ this.options.onAsset?.(manifest);
142
+ }
143
+ // Check preview cache
144
+ const cacheKey = getPreviewCacheKey(manifest.sha256);
145
+ let preview = getPreviewFromCache(cacheKey);
146
+ if (!preview) {
147
+ // Generate preview
148
+ preview = await this.generatePreview(manifest, cacheKey);
149
+ savePreviewToCache(preview);
150
+ const elapsed = Date.now() - startTime;
151
+ this.log(`Generated preview: ${manifest.name} (${elapsed}ms)`);
152
+ }
153
+ this.options.onPreview?.(preview);
154
+ }
155
+ /**
156
+ * Generate preview for an asset
157
+ */
158
+ async generatePreview(manifest, cacheKey) {
159
+ const preview = {
160
+ assetId: manifest.id,
161
+ generatedAt: Date.now(),
162
+ cacheKey,
163
+ };
164
+ const mime = manifest.mime;
165
+ // Image preview
166
+ if (mime.startsWith('image/')) {
167
+ const imageInfo = this.getImageInfo(manifest.path);
168
+ if (imageInfo) {
169
+ preview.dimensions = imageInfo.dimensions;
170
+ preview.thumbnailData = this.generateImageThumbnail(manifest.path);
171
+ }
172
+ }
173
+ // PDF preview
174
+ else if (mime === 'application/pdf') {
175
+ const pdfInfo = this.getPdfInfo(manifest.path);
176
+ if (pdfInfo) {
177
+ preview.pageCount = pdfInfo.pageCount;
178
+ preview.textSnippet = pdfInfo.textSnippet;
179
+ }
180
+ }
181
+ // Text/code preview
182
+ else if (mime.startsWith('text/') || mime === 'application/json') {
183
+ preview.textSnippet = this.getTextSnippet(manifest.path);
184
+ }
185
+ return preview;
186
+ }
187
+ /**
188
+ * Get image dimensions using sips (macOS) or file command
189
+ */
190
+ getImageInfo(filePath) {
191
+ try {
192
+ // Try sips on macOS
193
+ if (process.platform === 'darwin') {
194
+ const output = execSync(`sips -g pixelWidth -g pixelHeight "${filePath}" 2>/dev/null`, { encoding: 'utf-8', timeout: 5000 });
195
+ const widthMatch = output.match(/pixelWidth:\s*(\d+)/);
196
+ const heightMatch = output.match(/pixelHeight:\s*(\d+)/);
197
+ if (widthMatch && heightMatch) {
198
+ return {
199
+ dimensions: {
200
+ width: parseInt(widthMatch[1], 10),
201
+ height: parseInt(heightMatch[1], 10),
202
+ },
203
+ };
204
+ }
205
+ }
206
+ // Try file command (cross-platform)
207
+ const output = execSync(`file "${filePath}"`, { encoding: 'utf-8', timeout: 5000 });
208
+ const match = output.match(/(\d+)\s*x\s*(\d+)/);
209
+ if (match) {
210
+ return {
211
+ dimensions: {
212
+ width: parseInt(match[1], 10),
213
+ height: parseInt(match[2], 10),
214
+ },
215
+ };
216
+ }
217
+ }
218
+ catch {
219
+ // Ignore errors
220
+ }
221
+ return null;
222
+ }
223
+ /**
224
+ * Generate thumbnail as base64 data URL
225
+ * Uses sips on macOS for fast resizing
226
+ */
227
+ generateImageThumbnail(filePath, maxSize = 200) {
228
+ try {
229
+ if (process.platform !== 'darwin')
230
+ return undefined;
231
+ const cacheDir = getPreviewCacheDir();
232
+ const thumbName = `thumb_${basename(filePath)}.jpg`;
233
+ const thumbPath = join(cacheDir, thumbName);
234
+ // Use sips to resize
235
+ execSync(`sips -Z ${maxSize} -s format jpeg "${filePath}" --out "${thumbPath}" 2>/dev/null`, { timeout: 10000 });
236
+ // Read as base64
237
+ const data = readFileSync(thumbPath);
238
+ return `data:image/jpeg;base64,${data.toString('base64')}`;
239
+ }
240
+ catch {
241
+ return undefined;
242
+ }
243
+ }
244
+ /**
245
+ * Get PDF info using pdfinfo or mdls
246
+ */
247
+ getPdfInfo(filePath) {
248
+ try {
249
+ let pageCount;
250
+ // Try pdfinfo
251
+ try {
252
+ const output = execSync(`pdfinfo "${filePath}" 2>/dev/null`, {
253
+ encoding: 'utf-8',
254
+ timeout: 5000,
255
+ });
256
+ const match = output.match(/Pages:\s*(\d+)/);
257
+ if (match) {
258
+ pageCount = parseInt(match[1], 10);
259
+ }
260
+ }
261
+ catch {
262
+ // Try mdls on macOS
263
+ if (process.platform === 'darwin') {
264
+ const output = execSync(`mdls -name kMDItemNumberOfPages "${filePath}" 2>/dev/null`, {
265
+ encoding: 'utf-8',
266
+ timeout: 5000,
267
+ });
268
+ const match = output.match(/kMDItemNumberOfPages\s*=\s*(\d+)/);
269
+ if (match) {
270
+ pageCount = parseInt(match[1], 10);
271
+ }
272
+ }
273
+ }
274
+ // Try to extract text snippet
275
+ let textSnippet;
276
+ try {
277
+ const output = execSync(`pdftotext -l 1 -q "${filePath}" - 2>/dev/null | head -c 500`, {
278
+ encoding: 'utf-8',
279
+ timeout: 10000,
280
+ });
281
+ if (output.trim()) {
282
+ textSnippet = output.trim().substring(0, 500);
283
+ }
284
+ }
285
+ catch {
286
+ // pdftotext not available
287
+ }
288
+ return { pageCount, textSnippet };
289
+ }
290
+ catch {
291
+ return null;
292
+ }
293
+ }
294
+ /**
295
+ * Get text snippet from file
296
+ */
297
+ getTextSnippet(filePath, maxChars = 500) {
298
+ try {
299
+ const content = readFileSync(filePath, 'utf-8');
300
+ return content.substring(0, maxChars);
301
+ }
302
+ catch {
303
+ return undefined;
304
+ }
305
+ }
306
+ /**
307
+ * Extract group ID from file path
308
+ * e.g., ~/Lia-Hub/shared/outputs/run_abc123/file.png -> run_abc123
309
+ */
310
+ extractGroupId(filePath) {
311
+ const outputPath = getSharedOutputPath();
312
+ if (filePath.startsWith(outputPath)) {
313
+ const relative = filePath.substring(outputPath.length + 1);
314
+ const parts = relative.split('/');
315
+ if (parts.length > 1) {
316
+ return parts[0]; // First directory is the group
317
+ }
318
+ }
319
+ return undefined;
320
+ }
321
+ log(message) {
322
+ if (this.options.verbose) {
323
+ console.log(`[PREVIEW] ${message}`);
324
+ }
325
+ }
326
+ }
327
+ let registry = null;
328
+ export function getAssetRegistry() {
329
+ if (!registry) {
330
+ registry = {
331
+ assets: new Map(),
332
+ previews: new Map(),
333
+ byGroup: new Map(),
334
+ };
335
+ }
336
+ return registry;
337
+ }
338
+ export function registerAsset(manifest) {
339
+ const reg = getAssetRegistry();
340
+ reg.assets.set(manifest.id, manifest);
341
+ if (manifest.groupId) {
342
+ if (!reg.byGroup.has(manifest.groupId)) {
343
+ reg.byGroup.set(manifest.groupId, new Set());
344
+ }
345
+ reg.byGroup.get(manifest.groupId).add(manifest.id);
346
+ }
347
+ }
348
+ export function registerPreview(preview) {
349
+ const reg = getAssetRegistry();
350
+ reg.previews.set(preview.assetId, preview);
351
+ }
352
+ export function getAsset(assetId) {
353
+ return getAssetRegistry().assets.get(assetId);
354
+ }
355
+ export function getPreview(assetId) {
356
+ return getAssetRegistry().previews.get(assetId);
357
+ }
358
+ export function getAssetsByGroup(groupId) {
359
+ const reg = getAssetRegistry();
360
+ const ids = reg.byGroup.get(groupId);
361
+ if (!ids)
362
+ return [];
363
+ return Array.from(ids).map(id => reg.assets.get(id)).filter(Boolean);
364
+ }
365
+ export function getAllAssets() {
366
+ return Array.from(getAssetRegistry().assets.values());
367
+ }
368
+ // ============================================================================
369
+ // SINGLETON WORKER
370
+ // ============================================================================
371
+ let previewWorker = null;
372
+ export function startPreviewWorker(options = {}) {
373
+ if (previewWorker) {
374
+ return previewWorker;
375
+ }
376
+ previewWorker = new PreviewWorker({
377
+ ...options,
378
+ onAsset: (manifest) => {
379
+ registerAsset(manifest);
380
+ options.onAsset?.(manifest);
381
+ },
382
+ onPreview: (preview) => {
383
+ registerPreview(preview);
384
+ options.onPreview?.(preview);
385
+ },
386
+ });
387
+ previewWorker.start();
388
+ return previewWorker;
389
+ }
390
+ export function stopPreviewWorker() {
391
+ if (previewWorker) {
392
+ previewWorker.stop();
393
+ previewWorker = null;
394
+ }
395
+ }