@@ -19,6 +19,7 @@ import {
1919import { loadChangeGraph } from './graph' ;
2020import { getTaskRef , toQualifiedTaskId } from './task-identity' ;
2121import { formatTitleFromSlug } from './commands/scaffold' ;
22+ import { readExecutionRootsState , type ExecutionRootsState } from './execution-roots' ;
2223import { resolveProjectIdentity } from './project-identity' ;
2324import { resolveSuperplanRoot , resolveWorkspaceRoot } from './workspace-root' ;
2425
@@ -59,6 +60,20 @@ interface SetOverlayVisibilityOptions {
5960 workspacePath ?: string ;
6061}
6162
63+ interface OverlayRuntimeTaskEntry {
64+ status ?: string ;
65+ }
66+
67+ interface OverlayRuntimeChangeEntry {
68+ active_task_ref ?: string | null ;
69+ tasks ?: Record < string , OverlayRuntimeTaskEntry > ;
70+ }
71+
72+ interface OverlayRuntimeStateFile {
73+ changes ?: Record < string , OverlayRuntimeChangeEntry > ;
74+ tasks ?: Record < string , OverlayRuntimeTaskEntry > ;
75+ }
76+
6277async function pathExists ( targetPath : string ) : Promise < boolean > {
6378 try {
6479 await fs . access ( targetPath ) ;
@@ -81,6 +96,13 @@ async function readOverlaySnapshot(paths: OverlayRuntimePaths): Promise<OverlayS
8196 }
8297}
8398
99+ async function writeOverlaySnapshot ( paths : OverlayRuntimePaths , snapshot : OverlaySnapshot ) : Promise < void > {
100+ await fs . mkdir ( paths . runtime_dir , { recursive : true } ) ;
101+ const tempSnapshotPath = `${ paths . snapshot_path } .${ Date . now ( ) } .${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } .tmp` ;
102+ await fs . writeFile ( tempSnapshotPath , JSON . stringify ( snapshot , null , 2 ) , 'utf-8' ) ;
103+ await fs . rename ( tempSnapshotPath , paths . snapshot_path ) ;
104+ }
105+
84106function getPriorityRank ( priority : TaskPriority ) : number {
85107 if ( priority === 'high' ) {
86108 return 0 ;
@@ -202,6 +224,101 @@ function createAlertEvents(previousEvents: OverlayEvent[], alertKinds: OverlayEv
202224 return [ ...previousEvents , ...newEvents ] . slice ( - MAX_OVERLAY_EVENTS ) ;
203225}
204226
227+ function getOverlaySnapshotWorkspacePaths ( workspacePath : string ) : string [ ] {
228+ const workspaceRoot = resolveWorkspaceRoot ( workspacePath ) ;
229+ const projectRoot = resolveProjectIdentity ( workspacePath ) . project_root ;
230+ return [ ...new Set ( [ workspaceRoot , projectRoot ] ) ] ;
231+ }
232+
233+ async function readOverlayRuntimeTaskRefs ( workspacePath : string ) : Promise < string [ ] > {
234+ const tasksPath = path . join ( resolveSuperplanRoot ( workspacePath ) , 'runtime' , 'tasks.json' ) ;
235+ try {
236+ const parsed = JSON . parse ( await fs . readFile ( tasksPath , 'utf-8' ) ) as OverlayRuntimeStateFile ;
237+ if ( parsed && typeof parsed === 'object' && parsed . changes && typeof parsed . changes === 'object' ) {
238+ const refs = new Set < string > ( ) ;
239+ for ( const [ changeId , changeState ] of Object . entries ( parsed . changes ) ) {
240+ const activeTaskRef = typeof changeState ?. active_task_ref === 'string'
241+ ? changeState . active_task_ref . trim ( )
242+ : '' ;
243+ if ( activeTaskRef ) {
244+ refs . add ( activeTaskRef ) ;
245+ continue ;
246+ }
247+
248+ if ( ! changeState ?. tasks || typeof changeState . tasks !== 'object' ) {
249+ continue ;
250+ }
251+
252+ for ( const [ taskId , taskState ] of Object . entries ( changeState . tasks ) ) {
253+ if ( taskState ?. status === 'in_progress' ) {
254+ refs . add ( toQualifiedTaskId ( changeId , taskId ) ) ;
255+ }
256+ }
257+ }
258+
259+ return [ ...refs ] . sort ( ( left , right ) => left . localeCompare ( right ) ) ;
260+ }
261+
262+ if ( parsed && typeof parsed === 'object' && parsed . tasks && typeof parsed . tasks === 'object' ) {
263+ return Object . entries ( parsed . tasks )
264+ . filter ( ( [ taskRef , taskState ] ) => taskRef . includes ( '/' ) && taskState ?. status === 'in_progress' )
265+ . map ( ( [ taskRef ] ) => taskRef )
266+ . sort ( ( left , right ) => left . localeCompare ( right ) ) ;
267+ }
268+ } catch { }
269+
270+ return [ ] ;
271+ }
272+
273+ function getAttachedChangeIdForWorkspace (
274+ executionRootsState : ExecutionRootsState ,
275+ workspacePath : string ,
276+ ) : string | null {
277+ const workspaceRoot = resolveWorkspaceRoot ( workspacePath ) ;
278+ return Object . values ( executionRootsState . roots )
279+ . find ( record => record . path === workspaceRoot )
280+ ?. attached_change_id ?? null ;
281+ }
282+
283+ function getActiveOverlayTaskForWorkspace (
284+ board : OverlaySnapshot [ 'board' ] ,
285+ options : {
286+ activeTaskRefs : string [ ] ;
287+ attachedChangeId : string | null ;
288+ } ,
289+ ) : OverlayTaskSummary | null {
290+ const taskByRef = new Map < string , OverlayTaskSummary > ( ) ;
291+ for ( const task of board . in_progress ) {
292+ if ( task . task_ref ) {
293+ taskByRef . set ( task . task_ref , task ) ;
294+ }
295+ }
296+
297+ if ( options . attachedChangeId ) {
298+ const attachedActiveTaskRef = options . activeTaskRefs . find ( taskRef => taskRef . startsWith ( `${ options . attachedChangeId } /` ) ) ;
299+ if ( attachedActiveTaskRef ) {
300+ const attachedActiveTask = taskByRef . get ( attachedActiveTaskRef ) ;
301+ if ( attachedActiveTask ) {
302+ return attachedActiveTask ;
303+ }
304+ }
305+
306+ const attachedTask = board . in_progress . find ( task => task . change_id === options . attachedChangeId ) ;
307+ if ( attachedTask ) {
308+ return attachedTask ;
309+ }
310+ }
311+
312+ for ( const taskRef of options . activeTaskRefs ) {
313+ const activeTask = taskByRef . get ( taskRef ) ;
314+ if ( activeTask ) {
315+ return activeTask ;
316+ }
317+ }
318+
319+ return board . in_progress [ 0 ] ?? null ;
320+ }
321+
205322async function getTrackedChangeTaskIds ( changeDir : string ) : Promise < string [ ] > {
206323 const tasksDir = path . join ( changeDir , 'tasks' ) ;
207324 let taskEntries : Array < { isFile ( ) : boolean ; name : string } > = [ ] ;
@@ -371,11 +488,9 @@ export async function refreshOverlaySnapshot(
371488 tasks : OverlayTaskSource [ ] ,
372489 options : RefreshOverlaySnapshotOptions = { } ,
373490) : Promise < { paths : OverlayRuntimePaths ; snapshot : OverlaySnapshot } > {
374- const workspacePath = options . workspacePath ?? resolveWorkspaceRoot ( ) ;
491+ const workspacePath = resolveWorkspaceRoot ( options . workspacePath ?? resolveWorkspaceRoot ( ) ) ;
375492 const projectIdentity = resolveProjectIdentity ( workspacePath ) ;
376- const paths = getOverlayRuntimePaths ( workspacePath ) ;
377493 const timestamp = new Date ( ) . toISOString ( ) ;
378- const previousSnapshot = await readOverlaySnapshot ( paths ) ;
379494 const sortedTasks = [ ...tasks ] . sort ( sortTasks ) ;
380495 const trackedChanges = await collectTrackedChanges ( workspacePath , sortedTasks ) ;
381496 const focusedChange = toFocusedChange ( trackedChanges [ 0 ] ) ;
@@ -389,31 +504,40 @@ export async function refreshOverlaySnapshot(
389504 blocked : sortedTasks . filter ( task => task . status === 'blocked' ) . map ( toOverlayTaskSummary ) ,
390505 needs_feedback : sortedTasks . filter ( task => task . status === 'needs_feedback' ) . map ( toOverlayTaskSummary ) ,
391506 } ;
507+ const [ executionRootsState , activeTaskRefs ] = await Promise . all ( [
508+ readExecutionRootsState ( workspacePath ) ,
509+ readOverlayRuntimeTaskRefs ( workspacePath ) ,
510+ ] ) ;
511+ const targetWorkspacePaths = getOverlaySnapshotWorkspacePaths ( workspacePath ) ;
512+ const snapshots = await Promise . all ( targetWorkspacePaths . map ( async targetWorkspacePath => {
513+ const paths = getOverlayRuntimePaths ( targetWorkspacePath ) ;
514+ const previousSnapshot = await readOverlaySnapshot ( paths ) ;
515+ const snapshot = createOverlaySnapshot ( {
516+ project_id : projectIdentity . project_id ,
517+ project_name : path . basename ( projectIdentity . project_root ) || path . basename ( targetWorkspacePath ) || 'root' ,
518+ project_path : projectIdentity . project_root ,
519+ workspace_path : targetWorkspacePath ,
520+ session_id : `workspace:${ targetWorkspacePath } ` ,
521+ updated_at : timestamp ,
522+ tracked_changes : trackedChanges . map ( toOverlayTrackedChange ) ,
523+ focused_change : focusedChange ,
524+ active_task : getActiveOverlayTaskForWorkspace ( board , {
525+ activeTaskRefs,
526+ attachedChangeId : getAttachedChangeIdForWorkspace ( executionRootsState , targetWorkspacePath ) ,
527+ } ) ,
528+ board,
529+ attention_state : attentionState ,
530+ events : createAlertEvents ( previousSnapshot ?. events ?? [ ] , alertKinds , timestamp ) ,
531+ } ) ;
392532
393- const snapshot = createOverlaySnapshot ( {
394- project_id : projectIdentity . project_id ,
395- project_name : path . basename ( projectIdentity . project_root ) || path . basename ( workspacePath ) || 'root' ,
396- project_path : projectIdentity . project_root ,
397- workspace_path : workspacePath ,
398- session_id : `workspace:${ workspacePath } ` ,
399- updated_at : timestamp ,
400- tracked_changes : trackedChanges . map ( toOverlayTrackedChange ) ,
401- focused_change : focusedChange ,
402- active_task : board . in_progress [ 0 ] ?? null ,
403- board,
404- attention_state : attentionState ,
405- events : createAlertEvents ( previousSnapshot ?. events ?? [ ] , alertKinds , timestamp ) ,
406- } ) ;
407-
408- await fs . mkdir ( paths . runtime_dir , { recursive : true } ) ;
409- const tempSnapshotPath = `${ paths . snapshot_path } .${ Date . now ( ) } .${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } .tmp` ;
410- await fs . writeFile ( tempSnapshotPath , JSON . stringify ( snapshot , null , 2 ) , 'utf-8' ) ;
411- await fs . rename ( tempSnapshotPath , paths . snapshot_path ) ;
533+ await writeOverlaySnapshot ( paths , snapshot ) ;
534+ return {
535+ paths,
536+ snapshot,
537+ } ;
538+ } ) ) ;
412539
413- return {
414- paths,
415- snapshot,
416- } ;
540+ return snapshots . find ( item => item . snapshot . workspace_path === workspacePath ) ?? snapshots [ 0 ] ;
417541}
418542
419543export async function setOverlayVisibilityRequest (
0 commit comments