Skip to content

Commit f8b14cd

Browse files
committed
feat: add phase timeline change subscription and event handling
1 parent dbcf51f commit f8b14cd

File tree

7 files changed

+310
-4
lines changed

7 files changed

+310
-4
lines changed

sdk/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,24 @@ if (session.phases.allCompleted()) {
250250

251251
// Get phase by ID
252252
const phase = session.phases.get('phase-0');
253+
254+
// Subscribe to phase changes
255+
const unsubscribe = session.phases.onChange((event) => {
256+
console.log(`Phase ${event.type}:`, event.phase.name);
257+
console.log(`Status: ${event.phase.status}`);
258+
console.log(`Total phases: ${event.allPhases.length}`);
259+
});
260+
// Later: unsubscribe();
261+
```
262+
263+
The `onChange` callback receives a `PhaseTimelineEvent`:
264+
265+
```ts
266+
type PhaseTimelineEvent = {
267+
type: 'added' | 'updated'; // New phase vs status/file change
268+
phase: PhaseInfo; // The affected phase
269+
allPhases: PhaseInfo[]; // All phases after this change
270+
};
253271
```
254272

255273
Each phase contains:
@@ -403,6 +421,8 @@ import type {
403421
PhaseStatus,
404422
PhaseFileStatus,
405423
PhaseEventType,
424+
PhaseTimelineEvent,
425+
PhaseTimelineChangeType,
406426

407427
// API
408428
ApiResponse,

sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cf-vibesdk/sdk",
3-
"version": "0.0.8",
3+
"version": "0.0.9",
44
"type": "module",
55
"exports": {
66
".": {

sdk/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export type {
3838
PhaseFileStatus,
3939
PhaseInfo,
4040
PhaseStatus,
41+
PhaseTimelineChangeType,
42+
PhaseTimelineEvent,
4143
ProjectType,
4244
PublicAppsQuery,
4345
SessionDeployable,

sdk/src/session.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
ImageAttachment,
1010
PhaseEventType,
1111
PhaseInfo,
12+
PhaseTimelineEvent,
1213
ProjectType,
1314
SessionDeployable,
1415
SessionFiles,
@@ -129,6 +130,14 @@ export class BuildSession {
129130
allCompleted: (): boolean =>
130131
this.state.get().phases.length > 0 &&
131132
this.state.get().phases.every((p) => p.status === 'completed'),
133+
134+
/**
135+
* Subscribe to phase timeline changes.
136+
* Fires when a phase is added or when a phase's status/files change.
137+
* @returns Unsubscribe function.
138+
*/
139+
onChange: (cb: (event: PhaseTimelineEvent) => void): (() => void) =>
140+
this.state.onPhaseChange(cb),
132141
};
133142

134143
readonly wait = {

sdk/src/state.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { TypedEmitter } from './emitter';
2-
import type { AgentWsServerMessage, WsMessageOf, PhaseInfo, PhaseFile, BehaviorType, ProjectType } from './types';
2+
import type { AgentWsServerMessage, WsMessageOf, PhaseInfo, PhaseFile, BehaviorType, ProjectType, PhaseTimelineEvent, PhaseTimelineChangeType } from './types';
33
import type { AgentState } from './protocol';
44

55
export type ConnectionState = 'disconnected' | 'connecting' | 'connected';
@@ -78,6 +78,7 @@ export type SessionState = {
7878

7979
type SessionStateEvents = {
8080
change: { prev: SessionState; next: SessionState };
81+
phaseChange: PhaseTimelineEvent;
8182
};
8283

8384
const INITIAL_STATE: SessionState = {
@@ -169,6 +170,14 @@ export class SessionStateStore {
169170
return this.emitter.on('change', ({ prev, next }) => cb(next, prev));
170171
}
171172

173+
/**
174+
* Subscribe to phase timeline changes.
175+
* Fires when a phase is added or when a phase's status/files change.
176+
*/
177+
onPhaseChange(cb: (event: PhaseTimelineEvent) => void): () => void {
178+
return this.emitter.on('phaseChange', cb);
179+
}
180+
172181
setConnection(state: ConnectionState): void {
173182
this.setState({ connection: state });
174183
}
@@ -451,7 +460,7 @@ export class SessionStateStore {
451460
const files: PhaseFile[] = (phaseFiles ?? []).map((f) => ({
452461
path: f.path,
453462
purpose: f.purpose,
454-
status: status === 'completed' ? 'completed' : 'generating',
463+
status: status === 'completed' ? 'completed' : 'pending',
455464
}));
456465

457466
if (existingIndex >= 0) {
@@ -481,6 +490,48 @@ export class SessionStateStore {
481490
const next: SessionState = { ...prev, ...patch };
482491
this.state = next;
483492
this.emitter.emit('change', { prev, next });
493+
494+
// Emit phase change events if phases array changed
495+
if (patch.phases && patch.phases !== prev.phases) {
496+
this.emitPhaseChanges(prev.phases, patch.phases);
497+
}
498+
}
499+
500+
/**
501+
* Compare old and new phases arrays and emit change events.
502+
*/
503+
private emitPhaseChanges(prevPhases: PhaseInfo[], nextPhases: PhaseInfo[]): void {
504+
// Check for new phases (added)
505+
for (const phase of nextPhases) {
506+
const prevPhase = prevPhases.find((p) => p.id === phase.id);
507+
if (!prevPhase) {
508+
// New phase added
509+
this.emitter.emit('phaseChange', {
510+
type: 'added',
511+
phase,
512+
allPhases: nextPhases,
513+
});
514+
} else if (this.hasPhaseChanged(prevPhase, phase)) {
515+
// Existing phase updated
516+
this.emitter.emit('phaseChange', {
517+
type: 'updated',
518+
phase,
519+
allPhases: nextPhases,
520+
});
521+
}
522+
}
523+
}
524+
525+
/**
526+
* Check if a phase has meaningfully changed (status or file statuses).
527+
*/
528+
private hasPhaseChanged(prev: PhaseInfo, next: PhaseInfo): boolean {
529+
if (prev.status !== next.status) return true;
530+
if (prev.files.length !== next.files.length) return true;
531+
for (let i = 0; i < prev.files.length; i++) {
532+
if (prev.files[i]!.status !== next.files[i]!.status) return true;
533+
}
534+
return false;
484535
}
485536

486537
clear(): void {

sdk/src/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@ export type AgentEventMap = {
186186
| 'cloudflare_deployment_error'
187187
>;
188188
error: { error: string };
189+
190+
/** Emitted when the phase timeline changes (phase added or updated). */
191+
phases: PhaseTimelineEvent;
189192
};
190193

191194
// ============================================================================
@@ -306,6 +309,20 @@ export type SessionPhases = {
306309
allCompleted: () => boolean;
307310
};
308311

312+
/**
313+
* Event emitted when the phase timeline changes.
314+
*/
315+
export type PhaseTimelineChangeType = 'added' | 'updated';
316+
317+
export type PhaseTimelineEvent = {
318+
/** Type of change: 'added' for new phase, 'updated' for status/file changes. */
319+
type: PhaseTimelineChangeType;
320+
/** The phase that was added or updated. */
321+
phase: PhaseInfo;
322+
/** All phases in the timeline after this change. */
323+
allPhases: PhaseInfo[];
324+
};
325+
309326
export type SessionDeployable = {
310327
files: number;
311328
reason: 'generation_complete' | 'phase_validated';

0 commit comments

Comments
 (0)