@siftd/connect-agent 0.2.22 → 0.2.23
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.
- package/dist/agent.js +8 -0
- package/dist/core/file-tracker.d.ts +36 -0
- package/dist/core/file-tracker.js +253 -0
- package/dist/orchestrator.d.ts +21 -0
- package/dist/orchestrator.js +63 -1
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -257,6 +257,14 @@ export async function runAgent(pollInterval = 2000) {
|
|
|
257
257
|
console.log(`[WORKERS] ${running.length} running`);
|
|
258
258
|
}
|
|
259
259
|
});
|
|
260
|
+
// Gallery updates - send worker assets for UI gallery view
|
|
261
|
+
orchestrator.setGalleryCallback((galleryWorkers) => {
|
|
262
|
+
if (wsClient?.connected()) {
|
|
263
|
+
wsClient.sendGalleryWorkers(galleryWorkers);
|
|
264
|
+
const totalAssets = galleryWorkers.reduce((sum, w) => sum + w.assets.length, 0);
|
|
265
|
+
console.log(`[GALLERY] ${galleryWorkers.length} workers, ${totalAssets} assets`);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
260
268
|
// Worker results - send to user when workers complete
|
|
261
269
|
orchestrator.setWorkerResultCallback((workerId, result) => {
|
|
262
270
|
console.log(`[WORKER DONE] ${workerId}: ${result.slice(0, 100)}...`);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Tracker - Detects new/modified files in a directory
|
|
3
|
+
*
|
|
4
|
+
* Used to track what workers create or modify for the gallery view.
|
|
5
|
+
*/
|
|
6
|
+
export interface TrackedFile {
|
|
7
|
+
path: string;
|
|
8
|
+
name: string;
|
|
9
|
+
mtime: number;
|
|
10
|
+
size: number;
|
|
11
|
+
hash?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface FileSnapshot {
|
|
14
|
+
workingDir: string;
|
|
15
|
+
files: Map<string, TrackedFile>;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
}
|
|
18
|
+
export interface WorkerAsset {
|
|
19
|
+
path: string;
|
|
20
|
+
name: string;
|
|
21
|
+
type: 'new' | 'modified' | 'unchanged';
|
|
22
|
+
fileType: 'code' | 'image' | 'pdf' | 'text' | 'other';
|
|
23
|
+
preview?: string;
|
|
24
|
+
diff?: Array<{
|
|
25
|
+
type: 'context' | 'add' | 'remove';
|
|
26
|
+
content: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Take a snapshot of files in a directory
|
|
31
|
+
*/
|
|
32
|
+
export declare function takeSnapshot(workingDir: string): FileSnapshot;
|
|
33
|
+
/**
|
|
34
|
+
* Compare two snapshots to find new/modified files
|
|
35
|
+
*/
|
|
36
|
+
export declare function compareSnapshots(before: FileSnapshot, after: FileSnapshot): WorkerAsset[];
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Tracker - Detects new/modified files in a directory
|
|
3
|
+
*
|
|
4
|
+
* Used to track what workers create or modify for the gallery view.
|
|
5
|
+
*/
|
|
6
|
+
import { readdirSync, statSync, readFileSync, existsSync } from 'fs';
|
|
7
|
+
import { join, relative, extname } from 'path';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
// File extensions to track
|
|
10
|
+
const CODE_EXTENSIONS = new Set([
|
|
11
|
+
'.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.go', '.rs',
|
|
12
|
+
'.java', '.c', '.cpp', '.h', '.cs', '.php', '.swift', '.kt',
|
|
13
|
+
'.json', '.yaml', '.yml', '.toml', '.xml', '.html', '.css',
|
|
14
|
+
'.scss', '.less', '.vue', '.svelte', '.sh', '.bash', '.zsh',
|
|
15
|
+
'.sql', '.graphql', '.md', '.mdx'
|
|
16
|
+
]);
|
|
17
|
+
const IMAGE_EXTENSIONS = new Set([
|
|
18
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.bmp'
|
|
19
|
+
]);
|
|
20
|
+
const TEXT_EXTENSIONS = new Set([
|
|
21
|
+
'.txt', '.log', '.env', '.gitignore', '.dockerignore', '.editorconfig'
|
|
22
|
+
]);
|
|
23
|
+
// Directories to skip
|
|
24
|
+
const SKIP_DIRS = new Set([
|
|
25
|
+
'node_modules', '.git', '.next', 'dist', 'build', '__pycache__',
|
|
26
|
+
'.cache', 'coverage', '.nyc_output', 'vendor', 'target'
|
|
27
|
+
]);
|
|
28
|
+
// Max files to track (performance limit)
|
|
29
|
+
const MAX_FILES = 500;
|
|
30
|
+
// Max depth to traverse
|
|
31
|
+
const MAX_DEPTH = 5;
|
|
32
|
+
/**
|
|
33
|
+
* Take a snapshot of files in a directory
|
|
34
|
+
*/
|
|
35
|
+
export function takeSnapshot(workingDir) {
|
|
36
|
+
const files = new Map();
|
|
37
|
+
function walk(dir, depth) {
|
|
38
|
+
if (depth > MAX_DEPTH || files.size >= MAX_FILES)
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (files.size >= MAX_FILES)
|
|
44
|
+
break;
|
|
45
|
+
const fullPath = join(dir, entry.name);
|
|
46
|
+
const relativePath = relative(workingDir, fullPath);
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
// Skip ignored directories
|
|
49
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.'))
|
|
50
|
+
continue;
|
|
51
|
+
walk(fullPath, depth + 1);
|
|
52
|
+
}
|
|
53
|
+
else if (entry.isFile()) {
|
|
54
|
+
// Only track interesting files
|
|
55
|
+
const ext = extname(entry.name).toLowerCase();
|
|
56
|
+
if (!isInterestingFile(entry.name, ext))
|
|
57
|
+
continue;
|
|
58
|
+
try {
|
|
59
|
+
const stats = statSync(fullPath);
|
|
60
|
+
files.set(relativePath, {
|
|
61
|
+
path: relativePath,
|
|
62
|
+
name: entry.name,
|
|
63
|
+
mtime: stats.mtimeMs,
|
|
64
|
+
size: stats.size
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Skip files we can't stat
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Skip directories we can't read
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
walk(workingDir, 0);
|
|
78
|
+
return {
|
|
79
|
+
workingDir,
|
|
80
|
+
files,
|
|
81
|
+
timestamp: Date.now()
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Compare two snapshots to find new/modified files
|
|
86
|
+
*/
|
|
87
|
+
export function compareSnapshots(before, after) {
|
|
88
|
+
const assets = [];
|
|
89
|
+
// Find new and modified files
|
|
90
|
+
for (const [path, afterFile] of after.files) {
|
|
91
|
+
const beforeFile = before.files.get(path);
|
|
92
|
+
if (!beforeFile) {
|
|
93
|
+
// New file
|
|
94
|
+
assets.push(createAsset(after.workingDir, afterFile, 'new'));
|
|
95
|
+
}
|
|
96
|
+
else if (afterFile.mtime > beforeFile.mtime || afterFile.size !== beforeFile.size) {
|
|
97
|
+
// Modified file
|
|
98
|
+
const asset = createAsset(after.workingDir, afterFile, 'modified');
|
|
99
|
+
// Generate diff for code files
|
|
100
|
+
if (asset.fileType === 'code') {
|
|
101
|
+
asset.diff = generateSimpleDiff(after.workingDir, path);
|
|
102
|
+
}
|
|
103
|
+
assets.push(asset);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Sort: new files first, then by path
|
|
107
|
+
assets.sort((a, b) => {
|
|
108
|
+
if (a.type !== b.type)
|
|
109
|
+
return a.type === 'new' ? -1 : 1;
|
|
110
|
+
return a.path.localeCompare(b.path);
|
|
111
|
+
});
|
|
112
|
+
// Limit to most relevant files
|
|
113
|
+
return assets.slice(0, 20);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Create a WorkerAsset from a tracked file
|
|
117
|
+
*/
|
|
118
|
+
function createAsset(workingDir, file, type) {
|
|
119
|
+
const ext = extname(file.name).toLowerCase();
|
|
120
|
+
const fileType = getFileType(ext);
|
|
121
|
+
const fullPath = join(workingDir, file.path);
|
|
122
|
+
let preview;
|
|
123
|
+
// Generate preview for small files
|
|
124
|
+
if (file.size < 10000) {
|
|
125
|
+
try {
|
|
126
|
+
if (fileType === 'code' || fileType === 'text') {
|
|
127
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
128
|
+
preview = content.slice(0, 1000);
|
|
129
|
+
if (content.length > 1000)
|
|
130
|
+
preview += '\n... (truncated)';
|
|
131
|
+
}
|
|
132
|
+
else if (fileType === 'image') {
|
|
133
|
+
// For images, we could generate a base64 thumbnail
|
|
134
|
+
// For now, just indicate it's an image
|
|
135
|
+
preview = `[Image: ${file.name}]`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Skip preview on error
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
path: file.path,
|
|
144
|
+
name: file.name,
|
|
145
|
+
type,
|
|
146
|
+
fileType,
|
|
147
|
+
preview
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get file type category from extension
|
|
152
|
+
*/
|
|
153
|
+
function getFileType(ext) {
|
|
154
|
+
if (CODE_EXTENSIONS.has(ext))
|
|
155
|
+
return 'code';
|
|
156
|
+
if (IMAGE_EXTENSIONS.has(ext))
|
|
157
|
+
return 'image';
|
|
158
|
+
if (TEXT_EXTENSIONS.has(ext))
|
|
159
|
+
return 'text';
|
|
160
|
+
if (ext === '.pdf')
|
|
161
|
+
return 'pdf';
|
|
162
|
+
return 'other';
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Check if a file is worth tracking
|
|
166
|
+
*/
|
|
167
|
+
function isInterestingFile(name, ext) {
|
|
168
|
+
// Skip hidden files (except some config files)
|
|
169
|
+
if (name.startsWith('.') && !TEXT_EXTENSIONS.has(ext))
|
|
170
|
+
return false;
|
|
171
|
+
// Skip lock files and generated files
|
|
172
|
+
if (name.includes('.lock') || name.includes('-lock.'))
|
|
173
|
+
return false;
|
|
174
|
+
if (name === 'package-lock.json' || name === 'yarn.lock')
|
|
175
|
+
return false;
|
|
176
|
+
// Track code, images, text, pdf
|
|
177
|
+
return (CODE_EXTENSIONS.has(ext) ||
|
|
178
|
+
IMAGE_EXTENSIONS.has(ext) ||
|
|
179
|
+
TEXT_EXTENSIONS.has(ext) ||
|
|
180
|
+
ext === '.pdf');
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Generate a simple diff for a file using git
|
|
184
|
+
*/
|
|
185
|
+
function generateSimpleDiff(workingDir, relativePath) {
|
|
186
|
+
try {
|
|
187
|
+
// Check if we're in a git repo
|
|
188
|
+
const isGit = existsSync(join(workingDir, '.git'));
|
|
189
|
+
if (!isGit)
|
|
190
|
+
return undefined;
|
|
191
|
+
// Get git diff
|
|
192
|
+
const diff = execSync(`git diff --no-color -- "${relativePath}"`, {
|
|
193
|
+
cwd: workingDir,
|
|
194
|
+
encoding: 'utf8',
|
|
195
|
+
maxBuffer: 100 * 1024 // 100KB max
|
|
196
|
+
});
|
|
197
|
+
if (!diff.trim()) {
|
|
198
|
+
// No staged changes, try unstaged
|
|
199
|
+
const unstagedDiff = execSync(`git diff HEAD --no-color -- "${relativePath}"`, {
|
|
200
|
+
cwd: workingDir,
|
|
201
|
+
encoding: 'utf8',
|
|
202
|
+
maxBuffer: 100 * 1024
|
|
203
|
+
});
|
|
204
|
+
if (!unstagedDiff.trim())
|
|
205
|
+
return undefined;
|
|
206
|
+
return parseDiff(unstagedDiff);
|
|
207
|
+
}
|
|
208
|
+
return parseDiff(diff);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Parse git diff output into structured format
|
|
216
|
+
*/
|
|
217
|
+
function parseDiff(diffOutput) {
|
|
218
|
+
const lines = diffOutput.split('\n');
|
|
219
|
+
const result = [];
|
|
220
|
+
let inHunk = false;
|
|
221
|
+
for (const line of lines) {
|
|
222
|
+
// Skip diff header lines
|
|
223
|
+
if (line.startsWith('diff --git') ||
|
|
224
|
+
line.startsWith('index ') ||
|
|
225
|
+
line.startsWith('---') ||
|
|
226
|
+
line.startsWith('+++')) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
// Detect hunk header
|
|
230
|
+
if (line.startsWith('@@')) {
|
|
231
|
+
inHunk = true;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (!inHunk)
|
|
235
|
+
continue;
|
|
236
|
+
// Parse diff lines
|
|
237
|
+
if (line.startsWith('+')) {
|
|
238
|
+
result.push({ type: 'add', content: line.slice(1) });
|
|
239
|
+
}
|
|
240
|
+
else if (line.startsWith('-')) {
|
|
241
|
+
result.push({ type: 'remove', content: line.slice(1) });
|
|
242
|
+
}
|
|
243
|
+
else if (line.startsWith(' ')) {
|
|
244
|
+
result.push({ type: 'context', content: line.slice(1) });
|
|
245
|
+
}
|
|
246
|
+
// Limit diff size
|
|
247
|
+
if (result.length >= 100) {
|
|
248
|
+
result.push({ type: 'context', content: '... (diff truncated)' });
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
253
|
+
}
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* It does NOT do the work itself. Claude Code CLI workers do the work.
|
|
6
6
|
*/
|
|
7
7
|
import type { MessageParam } from '@anthropic-ai/sdk/resources/messages';
|
|
8
|
+
import { WorkerAsset } from './core/file-tracker.js';
|
|
8
9
|
export type MessageSender = (message: string) => Promise<void>;
|
|
9
10
|
export interface WorkerStatus {
|
|
10
11
|
id: string;
|
|
@@ -15,6 +16,13 @@ export interface WorkerStatus {
|
|
|
15
16
|
estimated: number;
|
|
16
17
|
}
|
|
17
18
|
export type WorkerStatusCallback = (workers: WorkerStatus[]) => void;
|
|
19
|
+
export interface GalleryWorker {
|
|
20
|
+
id: string;
|
|
21
|
+
task: string;
|
|
22
|
+
status: 'running' | 'completed' | 'failed';
|
|
23
|
+
assets: WorkerAsset[];
|
|
24
|
+
}
|
|
25
|
+
export type GalleryCallback = (workers: GalleryWorker[]) => void;
|
|
18
26
|
export declare class MasterOrchestrator {
|
|
19
27
|
private client;
|
|
20
28
|
private model;
|
|
@@ -26,6 +34,7 @@ export declare class MasterOrchestrator {
|
|
|
26
34
|
private jobCounter;
|
|
27
35
|
private workerStatusCallback;
|
|
28
36
|
private workerStatusInterval;
|
|
37
|
+
private galleryCallback;
|
|
29
38
|
private userId;
|
|
30
39
|
private workspaceDir;
|
|
31
40
|
private claudePath;
|
|
@@ -51,6 +60,18 @@ export declare class MasterOrchestrator {
|
|
|
51
60
|
* Set callback for worker status updates (for UI progress bars)
|
|
52
61
|
*/
|
|
53
62
|
setWorkerStatusCallback(callback: WorkerStatusCallback | null): void;
|
|
63
|
+
/**
|
|
64
|
+
* Set callback for gallery updates (worker assets for UI gallery view)
|
|
65
|
+
*/
|
|
66
|
+
setGalleryCallback(callback: GalleryCallback | null): void;
|
|
67
|
+
/**
|
|
68
|
+
* Get gallery workers with their assets
|
|
69
|
+
*/
|
|
70
|
+
getGalleryWorkers(): GalleryWorker[];
|
|
71
|
+
/**
|
|
72
|
+
* Broadcast gallery update to callback
|
|
73
|
+
*/
|
|
74
|
+
private broadcastGalleryUpdate;
|
|
54
75
|
/**
|
|
55
76
|
* Get current status of all workers (from both delegateToWorker and spawn_worker)
|
|
56
77
|
*/
|
package/dist/orchestrator.js
CHANGED
|
@@ -17,6 +17,7 @@ import { WorkerTools } from './tools/worker.js';
|
|
|
17
17
|
import { SharedState } from './workers/shared-state.js';
|
|
18
18
|
import { getKnowledgeForPrompt } from './genesis/index.js';
|
|
19
19
|
import { loadHubContext, formatHubContext, logAction } from './core/hub.js';
|
|
20
|
+
import { takeSnapshot, compareSnapshots } from './core/file-tracker.js';
|
|
20
21
|
const SYSTEM_PROMPT = `You are a MASTER ORCHESTRATOR - NOT a worker. You delegate ALL file/code work to Claude Code CLI workers.
|
|
21
22
|
|
|
22
23
|
CRITICAL IDENTITY:
|
|
@@ -85,6 +86,7 @@ export class MasterOrchestrator {
|
|
|
85
86
|
jobCounter = 0;
|
|
86
87
|
workerStatusCallback = null;
|
|
87
88
|
workerStatusInterval = null;
|
|
89
|
+
galleryCallback = null;
|
|
88
90
|
userId;
|
|
89
91
|
workspaceDir;
|
|
90
92
|
claudePath;
|
|
@@ -167,6 +169,41 @@ export class MasterOrchestrator {
|
|
|
167
169
|
}
|
|
168
170
|
}
|
|
169
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Set callback for gallery updates (worker assets for UI gallery view)
|
|
174
|
+
*/
|
|
175
|
+
setGalleryCallback(callback) {
|
|
176
|
+
this.galleryCallback = callback;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get gallery workers with their assets
|
|
180
|
+
*/
|
|
181
|
+
getGalleryWorkers() {
|
|
182
|
+
const workers = [];
|
|
183
|
+
for (const [id, job] of this.jobs) {
|
|
184
|
+
// Only include jobs with assets or that are running
|
|
185
|
+
if (job.assets || job.status === 'running') {
|
|
186
|
+
workers.push({
|
|
187
|
+
id,
|
|
188
|
+
task: job.task,
|
|
189
|
+
status: job.status === 'timeout' ? 'failed' : job.status,
|
|
190
|
+
assets: job.assets || []
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return workers;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Broadcast gallery update to callback
|
|
198
|
+
*/
|
|
199
|
+
broadcastGalleryUpdate() {
|
|
200
|
+
if (!this.galleryCallback)
|
|
201
|
+
return;
|
|
202
|
+
const workers = this.getGalleryWorkers();
|
|
203
|
+
if (workers.length > 0) {
|
|
204
|
+
this.galleryCallback(workers);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
170
207
|
/**
|
|
171
208
|
* Get current status of all workers (from both delegateToWorker and spawn_worker)
|
|
172
209
|
*/
|
|
@@ -866,13 +903,24 @@ Be specific about what you want done.`,
|
|
|
866
903
|
console.log(`[ORCHESTRATOR] Worker ${id} starting: ${task.slice(0, 80)}...`);
|
|
867
904
|
// Estimate task duration
|
|
868
905
|
const estimatedTime = this.estimateTaskDuration(task);
|
|
906
|
+
// Take snapshot of files before worker starts (for asset tracking)
|
|
907
|
+
let beforeSnapshot;
|
|
908
|
+
try {
|
|
909
|
+
beforeSnapshot = takeSnapshot(cwd);
|
|
910
|
+
console.log(`[ORCHESTRATOR] Snapshot: ${beforeSnapshot.files.size} files in ${cwd}`);
|
|
911
|
+
}
|
|
912
|
+
catch (err) {
|
|
913
|
+
console.log(`[ORCHESTRATOR] Could not take snapshot: ${err}`);
|
|
914
|
+
}
|
|
869
915
|
const job = {
|
|
870
916
|
id,
|
|
871
917
|
task: task.slice(0, 200),
|
|
872
918
|
status: 'running',
|
|
873
919
|
startTime: Date.now(),
|
|
874
920
|
output: '',
|
|
875
|
-
estimatedTime
|
|
921
|
+
estimatedTime,
|
|
922
|
+
workingDir: cwd,
|
|
923
|
+
beforeSnapshot
|
|
876
924
|
};
|
|
877
925
|
// Escape single quotes in prompt for shell safety
|
|
878
926
|
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
@@ -914,6 +962,20 @@ Be specific about what you want done.`,
|
|
|
914
962
|
job.endTime = Date.now();
|
|
915
963
|
const duration = Math.round((job.endTime - job.startTime) / 1000);
|
|
916
964
|
console.log(`[ORCHESTRATOR] Worker ${id} done in ${duration}s`);
|
|
965
|
+
// Track file changes (for gallery)
|
|
966
|
+
if (job.beforeSnapshot) {
|
|
967
|
+
try {
|
|
968
|
+
const afterSnapshot = takeSnapshot(job.workingDir);
|
|
969
|
+
const assets = compareSnapshots(job.beforeSnapshot, afterSnapshot);
|
|
970
|
+
job.assets = assets;
|
|
971
|
+
console.log(`[ORCHESTRATOR] Worker ${id} assets: ${assets.length} files (${assets.filter(a => a.type === 'new').length} new, ${assets.filter(a => a.type === 'modified').length} modified)`);
|
|
972
|
+
// Broadcast gallery update
|
|
973
|
+
this.broadcastGalleryUpdate();
|
|
974
|
+
}
|
|
975
|
+
catch (err) {
|
|
976
|
+
console.log(`[ORCHESTRATOR] Could not track assets: ${err}`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
917
979
|
const result = job.output.trim() || '(No output)';
|
|
918
980
|
// Notify via callback (sends to user via WebSocket)
|
|
919
981
|
if (this.workerResultCallback) {
|