diff --git a/substrate-api/src/routes/containers.ts b/substrate-api/src/routes/containers.ts index 27e70a7..75d894d 100644 --- a/substrate-api/src/routes/containers.ts +++ b/substrate-api/src/routes/containers.ts @@ -7,6 +7,7 @@ import { errMessage } from '../services/errors'; import { commandText, inferMode, parseExecCommand } from '../utils/exec-command'; import { rewriteProxyHtml, rewriteProxyLocationHeader } from '../utils/proxy-rewrite'; import { releaseExecSlot, reserveExecSlot } from '../utils/exec-rate-limit'; +import { workspaceWatcher } from '../services/workspace-watcher'; const app = new Hono(); @@ -305,7 +306,16 @@ app.post('/:id/files/write', async (c) => { if (!filePath || content === undefined) { return c.json({ error: 'path and content are required' }, 400); } - await containerManager.writeContainerFile(c.req.param('id'), filePath, content); + const containerId = c.req.param('id'); + await containerManager.writeContainerFile(containerId, filePath, content); + const rawPath = String(filePath); + workspaceWatcher.emit({ + type: 'file', + kind: 'change', + path: rawPath.startsWith('/') ? rawPath : `/workspace/${rawPath.replace(/^\/+/, '')}`, + containerId, + ts: Date.now(), + }); return c.json({ success: true, path: filePath }); } catch (error: any) { const status = error.statusCode || 500; diff --git a/substrate-api/src/routes/events.ts b/substrate-api/src/routes/events.ts index ba84d69..67f623e 100644 --- a/substrate-api/src/routes/events.ts +++ b/substrate-api/src/routes/events.ts @@ -12,7 +12,9 @@ app.get('/workspace/events', async (c) => { return c.json({ error: 'Workspace is disabled' }, 403); } workspaceWatcher.start(); - return streamSSE(c, (stream) => runWorkspaceEventStream(c, stream)); + return streamSSE(c, (stream) => + runWorkspaceEventStream(c, stream, (ev) => ev.type !== 'file' || !ev.containerId), + ); }); // GET /containers/:id/events — container lifecycle + job events. @@ -26,6 +28,7 @@ app.get('/containers/:id/events', async (c) => { return streamSSE(c, (stream) => runWorkspaceEventStream(c, stream, (ev) => { + if (ev.type === 'file') return ev.containerId === containerId; if (ev.type === 'container' && ev.containerId !== containerId) return false; if (ev.type === 'job' && ev.scope !== 'container') return false; return true; diff --git a/substrate-api/src/routes/workspace.ts b/substrate-api/src/routes/workspace.ts index 83aad21..c1ef143 100644 --- a/substrate-api/src/routes/workspace.ts +++ b/substrate-api/src/routes/workspace.ts @@ -6,6 +6,7 @@ import { errMessage } from '../services/errors'; import { resolveExistingPathWithinRoot } from '../utils/path-security'; import { commandText, inferMode, parseExecCommand } from '../utils/exec-command'; import { getMimeType } from '../utils/mime'; +import { workspaceWatcher } from '../services/workspace-watcher'; const app = new Hono(); @@ -123,6 +124,12 @@ app.post('/files/write', async (c) => { return c.json({ error: 'path and content are required' }, 400); } await containerManager.writeWorkspaceFile(filePath, content); + workspaceWatcher.emit({ + type: 'file', + kind: 'change', + path: `/${String(filePath).replace(/^\/+/, '')}`, + ts: Date.now(), + }); return c.json({ success: true, path: filePath }); } catch (error: any) { console.error('[Workspace] Write file error:', errMessage(error)); diff --git a/substrate-api/src/services/workspace-watcher.ts b/substrate-api/src/services/workspace-watcher.ts index 36d2f88..fa5f15e 100644 --- a/substrate-api/src/services/workspace-watcher.ts +++ b/substrate-api/src/services/workspace-watcher.ts @@ -5,7 +5,7 @@ import { CONFIG } from '../config'; import { log } from './logger'; export type WorkspaceEvent = - | { type: 'file'; kind: 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'; path: string; ts: number } + | { type: 'file'; kind: 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'; path: string; ts: number; containerId?: string } | { type: 'git'; ts: number } | { type: 'container'; containerId: string; status: string; ts: number } | { type: 'job'; jobId: string; status: string; scope: 'workspace' | 'container'; ts: number }