@siftd/connect-agent 0.2.22 → 0.2.24

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 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
+ }
@@ -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
  */
@@ -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,61 @@ 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
+ // Also set on workerTools for spawn_worker jobs
178
+ this.workerTools.setGalleryCallback(callback ? (workers) => {
179
+ // Merge with delegate_to_worker jobs and broadcast
180
+ this.broadcastGalleryUpdate();
181
+ } : null);
182
+ }
183
+ /**
184
+ * Get gallery workers with their assets
185
+ */
186
+ getGalleryWorkers() {
187
+ const workers = [];
188
+ const seenIds = new Set();
189
+ // Include jobs from delegateToWorker (this.jobs)
190
+ for (const [id, job] of this.jobs) {
191
+ // Only include jobs with assets or that are running
192
+ if (job.assets || job.status === 'running') {
193
+ workers.push({
194
+ id,
195
+ task: job.task,
196
+ status: job.status === 'timeout' ? 'failed' : job.status,
197
+ assets: job.assets || []
198
+ });
199
+ seenIds.add(id);
200
+ }
201
+ }
202
+ // Include jobs from workerTools (spawn_worker)
203
+ try {
204
+ const spawnedWorkers = this.workerTools.getGalleryWorkers();
205
+ for (const worker of spawnedWorkers) {
206
+ if (!seenIds.has(worker.id)) {
207
+ workers.push(worker);
208
+ }
209
+ }
210
+ }
211
+ catch {
212
+ // WorkerTools not available
213
+ }
214
+ return workers;
215
+ }
216
+ /**
217
+ * Broadcast gallery update to callback
218
+ */
219
+ broadcastGalleryUpdate() {
220
+ if (!this.galleryCallback)
221
+ return;
222
+ const workers = this.getGalleryWorkers();
223
+ if (workers.length > 0) {
224
+ this.galleryCallback(workers);
225
+ }
226
+ }
170
227
  /**
171
228
  * Get current status of all workers (from both delegateToWorker and spawn_worker)
172
229
  */
@@ -866,13 +923,24 @@ Be specific about what you want done.`,
866
923
  console.log(`[ORCHESTRATOR] Worker ${id} starting: ${task.slice(0, 80)}...`);
867
924
  // Estimate task duration
868
925
  const estimatedTime = this.estimateTaskDuration(task);
926
+ // Take snapshot of files before worker starts (for asset tracking)
927
+ let beforeSnapshot;
928
+ try {
929
+ beforeSnapshot = takeSnapshot(cwd);
930
+ console.log(`[ORCHESTRATOR] Snapshot: ${beforeSnapshot.files.size} files in ${cwd}`);
931
+ }
932
+ catch (err) {
933
+ console.log(`[ORCHESTRATOR] Could not take snapshot: ${err}`);
934
+ }
869
935
  const job = {
870
936
  id,
871
937
  task: task.slice(0, 200),
872
938
  status: 'running',
873
939
  startTime: Date.now(),
874
940
  output: '',
875
- estimatedTime
941
+ estimatedTime,
942
+ workingDir: cwd,
943
+ beforeSnapshot
876
944
  };
877
945
  // Escape single quotes in prompt for shell safety
878
946
  const escapedPrompt = prompt.replace(/'/g, "'\\''");
@@ -914,6 +982,20 @@ Be specific about what you want done.`,
914
982
  job.endTime = Date.now();
915
983
  const duration = Math.round((job.endTime - job.startTime) / 1000);
916
984
  console.log(`[ORCHESTRATOR] Worker ${id} done in ${duration}s`);
985
+ // Track file changes (for gallery)
986
+ if (job.beforeSnapshot) {
987
+ try {
988
+ const afterSnapshot = takeSnapshot(job.workingDir);
989
+ const assets = compareSnapshots(job.beforeSnapshot, afterSnapshot);
990
+ job.assets = assets;
991
+ 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)`);
992
+ // Broadcast gallery update
993
+ this.broadcastGalleryUpdate();
994
+ }
995
+ catch (err) {
996
+ console.log(`[ORCHESTRATOR] Could not track assets: ${err}`);
997
+ }
998
+ }
917
999
  const result = job.output.trim() || '(No output)';
918
1000
  // Notify via callback (sends to user via WebSocket)
919
1001
  if (this.workerResultCallback) {
@@ -2,10 +2,20 @@
2
2
  * Worker Tools
3
3
  * Tools for spawning and managing Claude Code workers
4
4
  */
5
+ import { GalleryCallback, GalleryWorker } from '../workers/manager.js';
5
6
  import type { ToolResult } from './bash.js';
7
+ export { GalleryCallback, GalleryWorker };
6
8
  export declare class WorkerTools {
7
9
  private manager;
8
10
  constructor(workspaceDir: string);
11
+ /**
12
+ * Set callback for gallery updates (worker assets for UI)
13
+ */
14
+ setGalleryCallback(callback: GalleryCallback | null): void;
15
+ /**
16
+ * Get gallery workers with assets
17
+ */
18
+ getGalleryWorkers(): GalleryWorker[];
9
19
  /**
10
20
  * Spawn a new Claude Code worker
11
21
  */
@@ -8,6 +8,18 @@ export class WorkerTools {
8
8
  constructor(workspaceDir) {
9
9
  this.manager = new WorkerManager(workspaceDir);
10
10
  }
11
+ /**
12
+ * Set callback for gallery updates (worker assets for UI)
13
+ */
14
+ setGalleryCallback(callback) {
15
+ this.manager.setGalleryCallback(callback);
16
+ }
17
+ /**
18
+ * Get gallery workers with assets
19
+ */
20
+ getGalleryWorkers() {
21
+ return this.manager.getGalleryWorkers();
22
+ }
11
23
  /**
12
24
  * Spawn a new Claude Code worker
13
25
  */
@@ -2,11 +2,32 @@
2
2
  * Claude Code Worker Manager
3
3
  * Spawns and manages Claude Code CLI instances for parallel task execution
4
4
  */
5
- import { WorkerJob, SpawnOptions, WorkerConfig } from './types.js';
5
+ import { WorkerJob, SpawnOptions, WorkerConfig, WorkerAsset } from './types.js';
6
+ export interface GalleryWorker {
7
+ id: string;
8
+ task: string;
9
+ status: 'running' | 'completed' | 'failed';
10
+ assets: WorkerAsset[];
11
+ }
12
+ export type GalleryCallback = (workers: GalleryWorker[]) => void;
6
13
  export declare class WorkerManager {
7
14
  private config;
8
15
  private activeWorkers;
16
+ private fileSnapshots;
17
+ private galleryCallback;
9
18
  constructor(workspaceDir: string, configOverrides?: Partial<WorkerConfig>);
19
+ /**
20
+ * Set callback for gallery updates (worker assets for UI)
21
+ */
22
+ setGalleryCallback(callback: GalleryCallback | null): void;
23
+ /**
24
+ * Get gallery workers with assets for UI
25
+ */
26
+ getGalleryWorkers(): GalleryWorker[];
27
+ /**
28
+ * Broadcast gallery update
29
+ */
30
+ private broadcastGalleryUpdate;
10
31
  /**
11
32
  * Generate a unique job ID
12
33
  */
@@ -6,9 +6,12 @@ import { spawn } from 'child_process';
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import { DEFAULT_WORKER_CONFIG } from './types.js';
9
+ import { takeSnapshot, compareSnapshots } from '../core/file-tracker.js';
9
10
  export class WorkerManager {
10
11
  config;
11
12
  activeWorkers = new Map();
13
+ fileSnapshots = new Map(); // Before-snapshots per job
14
+ galleryCallback = null;
12
15
  constructor(workspaceDir, configOverrides) {
13
16
  this.config = {
14
17
  ...DEFAULT_WORKER_CONFIG,
@@ -20,6 +23,39 @@ export class WorkerManager {
20
23
  fs.mkdirSync(this.config.jobsDir, { recursive: true });
21
24
  }
22
25
  }
26
+ /**
27
+ * Set callback for gallery updates (worker assets for UI)
28
+ */
29
+ setGalleryCallback(callback) {
30
+ this.galleryCallback = callback;
31
+ }
32
+ /**
33
+ * Get gallery workers with assets for UI
34
+ */
35
+ getGalleryWorkers() {
36
+ const jobs = this.list();
37
+ return jobs
38
+ .filter(job => job.assets && job.assets.length > 0 || job.status === 'running')
39
+ .slice(0, 20) // Limit for UI
40
+ .map(job => ({
41
+ id: job.id,
42
+ task: job.task.slice(0, 60),
43
+ status: (job.status === 'timeout' || job.status === 'cancelled') ? 'failed' :
44
+ (job.status === 'pending' ? 'running' : job.status),
45
+ assets: job.assets || []
46
+ }));
47
+ }
48
+ /**
49
+ * Broadcast gallery update
50
+ */
51
+ broadcastGalleryUpdate() {
52
+ if (!this.galleryCallback)
53
+ return;
54
+ const workers = this.getGalleryWorkers();
55
+ if (workers.length > 0) {
56
+ this.galleryCallback(workers);
57
+ }
58
+ }
23
59
  /**
24
60
  * Generate a unique job ID
25
61
  */
@@ -58,6 +94,15 @@ export class WorkerManager {
58
94
  // Validate timeout
59
95
  const effectiveTimeout = Math.min(timeout, this.config.maxTimeout);
60
96
  const jobId = this.generateJobId();
97
+ // Take snapshot before spawning (for asset tracking)
98
+ try {
99
+ const beforeSnapshot = takeSnapshot(workspace);
100
+ this.fileSnapshots.set(jobId, beforeSnapshot);
101
+ console.log(`[WORKER] Snapshot for ${jobId}: ${beforeSnapshot.files.size} files`);
102
+ }
103
+ catch (err) {
104
+ console.log(`[WORKER] Could not take snapshot for ${jobId}: ${err}`);
105
+ }
61
106
  const job = {
62
107
  id: jobId,
63
108
  task,
@@ -155,6 +200,25 @@ This ensures nothing is lost even if your output gets truncated.`;
155
200
  if (stderr && code !== 0) {
156
201
  currentJob.error = stderr.trim();
157
202
  }
203
+ // Track file changes (for gallery)
204
+ const beforeSnapshot = this.fileSnapshots.get(jobId);
205
+ if (beforeSnapshot) {
206
+ try {
207
+ const afterSnapshot = takeSnapshot(currentJob.workspace);
208
+ const assets = compareSnapshots(beforeSnapshot, afterSnapshot);
209
+ currentJob.assets = assets;
210
+ const newCount = assets.filter(a => a.type === 'new').length;
211
+ const modCount = assets.filter(a => a.type === 'modified').length;
212
+ console.log(`[WORKER] ${jobId} assets: ${assets.length} files (${newCount} new, ${modCount} modified)`);
213
+ // Clean up snapshot
214
+ this.fileSnapshots.delete(jobId);
215
+ // Broadcast gallery update
216
+ this.broadcastGalleryUpdate();
217
+ }
218
+ catch (err) {
219
+ console.log(`[WORKER] Could not track assets for ${jobId}: ${err}`);
220
+ }
221
+ }
158
222
  this.saveJob(currentJob);
159
223
  }
160
224
  });
@@ -1,6 +1,17 @@
1
1
  /**
2
2
  * Worker System Types
3
3
  */
4
+ export interface WorkerAsset {
5
+ path: string;
6
+ name: string;
7
+ type: 'new' | 'modified' | 'unchanged';
8
+ fileType: 'code' | 'image' | 'pdf' | 'text' | 'other';
9
+ preview?: string;
10
+ diff?: Array<{
11
+ type: 'context' | 'add' | 'remove';
12
+ content: string;
13
+ }>;
14
+ }
4
15
  export interface WorkerJob {
5
16
  id: string;
6
17
  task: string;
@@ -15,6 +26,7 @@ export interface WorkerJob {
15
26
  pid?: number;
16
27
  timeout: number;
17
28
  exitCode?: number;
29
+ assets?: WorkerAsset[];
18
30
  }
19
31
  export interface SpawnOptions {
20
32
  workspace?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.22",
3
+ "version": "0.2.24",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",