← vibesdk
import { TypedEmitter } from './emitter';
import { isRecord } from './utils';
import type { AgentState, FileOutputType } from './protocol';
import type { AgentWsServerMessage } from './types';
export type WorkspaceFile = {
path: string;
content: string;
};
export type WorkspaceChange =
| { type: 'reset'; files: number }
| { type: 'upsert'; path: string }
| { type: 'delete'; path: string };
type WorkspaceEvents = {
change: WorkspaceChange;
};
function isFileOutputType(value: unknown): value is FileOutputType {
if (!isRecord(value)) return false;
return typeof value.filePath === 'string' && typeof value.fileContents === 'string';
}
function extractGeneratedFilesFromState(state: AgentState): WorkspaceFile[] {
const out: WorkspaceFile[] = [];
for (const file of Object.values(state.generatedFilesMap ?? {})) {
if (!isFileOutputType(file)) continue;
out.push({ path: file.filePath, content: file.fileContents });
}
return out;
}
function extractGeneratedFileFromMessageFile(file: unknown): WorkspaceFile | null {
if (!isFileOutputType(file)) return null;
return { path: file.filePath, content: file.fileContents };
}
export class WorkspaceStore {
private files = new Map<string, string>();
private emitter = new TypedEmitter<WorkspaceEvents>();
paths(): string[] {
return Array.from(this.files.keys()).sort();
}
read(path: string): string | null {
return this.files.get(path) ?? null;
}
snapshot(): Record<string, string> {
const out: Record<string, string> = {};
for (const [path, content] of this.files.entries()) out[path] = content;
return out;
}
onChange(cb: (change: WorkspaceChange) => void): () => void {
return this.emitter.on('change', cb);
}
/** Apply authoritative snapshot from an AgentState. */
applyStateSnapshot(state: AgentState): void {
this.files.clear();
for (const f of extractGeneratedFilesFromState(state)) {
this.files.set(f.path, f.content);
}
this.emitter.emit('change', { type: 'reset', files: this.files.size });
}
/** Apply a single file upsert from WS file events. */
applyFileUpsert(file: unknown): void {
const f = extractGeneratedFileFromMessageFile(file);
if (!f) return;
this.files.set(f.path, f.content);
this.emitter.emit('change', { type: 'upsert', path: f.path });
}
applyWsMessage(msg: AgentWsServerMessage): void {
switch (msg.type) {
case 'agent_connected':
this.applyStateSnapshot(msg.state);
break;
case 'cf_agent_state':
this.applyStateSnapshot(msg.state);
break;
case 'file_generated':
this.applyFileUpsert(msg.file);
break;
case 'file_regenerated':
this.applyFileUpsert(msg.file);
break;
default:
break;
}
}
clear(): void {
this.files.clear();
this.emitter.clear();
}
}