@@ -32,6 +32,10 @@ import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
3232import { Container , Spacer , Text , truncateToWidth , visibleWidth } from "@earendil-works/pi-tui" ;
3333import type { TUI } from "@earendil-works/pi-tui" ;
3434import type { AgenticodingState } from "../state.js" ;
35+ import {
36+ __setSingletons ,
37+ getSingletons ,
38+ } from "../runtime-singletons.js" ;
3539import {
3640 getLastAssistantText ,
3741 type SpawnOutcome ,
@@ -249,7 +253,7 @@ interface SpawnFrameTarget {
249253 * streaming events (50-100+/sec) do not trigger an equal number of heavy
250254 * component mutations.
251255 */
252- class SpawnFrameScheduler {
256+ export class SpawnFrameScheduler {
253257 private readonly frameMs : number ;
254258 private dirtyComponents = new Set < SpawnFrameTarget > ( ) ;
255259 private frameTimer : ReturnType < typeof setTimeout > | null = null ;
@@ -316,8 +320,20 @@ class SpawnFrameScheduler {
316320 }
317321}
318322
319- /** Module-level singleton shared by all NestedAgentSessionComponent instances. */
323+ /**
324+ * Module-level singleton shared by all NestedAgentSessionComponent instances.
325+ *
326+ * Registered into the RuntimeSingletons container at module evaluation time.
327+ * Test harnesses overwrite this with a fresh SpawnFrameScheduler via
328+ * createTestHarness(). ESM guarantees all static imports resolve before any
329+ * module body runs, so the harness always wins.
330+ *
331+ * IMPORTANT: never use dynamic import() to load this module *after* a
332+ * createTestHarness() call, or the production scheduler will overwrite the
333+ * test one.
334+ */
320335const spawnFrameScheduler = new SpawnFrameScheduler ( ) ;
336+ __setSingletons ( { ...getSingletons ( ) , frameScheduler : spawnFrameScheduler } ) ;
321337
322338// ── NestedAgentSessionComponent ───────────────────────────────────────
323339
@@ -396,7 +412,7 @@ class NestedAgentSessionComponent extends Container implements SpawnFrameTarget
396412 this . renderQueued = false ;
397413 this . queuedRenderToken = undefined ;
398414 this . renderScheduleToken ++ ;
399- spawnFrameScheduler . cancelDirty ( this ) ;
415+ getSingletons ( ) . frameScheduler . cancelDirty ( this ) ;
400416 }
401417
402418 /**
@@ -409,7 +425,7 @@ class NestedAgentSessionComponent extends Container implements SpawnFrameTarget
409425 if ( this . renderQueued ) return ;
410426 this . renderQueued = true ;
411427 this . queuedRenderToken = ++ this . renderScheduleToken ;
412- spawnFrameScheduler . markDirty ( this ) ;
428+ getSingletons ( ) . frameScheduler . markDirty ( this ) ;
413429 }
414430
415431 /**
@@ -555,7 +571,7 @@ class NestedAgentSessionComponent extends Container implements SpawnFrameTarget
555571 dispose ( ) : void {
556572 this . unsubscribe ?.( ) ;
557573 this . unsubscribe = undefined ;
558- spawnFrameScheduler . cancelDirty ( this ) ;
574+ getSingletons ( ) . frameScheduler . cancelDirty ( this ) ;
559575 this . clearPendingState ( ) ;
560576 // Snapshot fields before clearing: if session.abort() triggers re-entrant
561577 // dispose, the nulled-out fields prevent double-abort.
@@ -731,7 +747,7 @@ class NestedAgentSessionComponent extends Container implements SpawnFrameTarget
731747 if ( ! this . session ) return ;
732748
733749 // Flush any pending state first so accumulated updates don't double-apply
734- spawnFrameScheduler . cancelDirty ( this ) ;
750+ getSingletons ( ) . frameScheduler . cancelDirty ( this ) ;
735751 this . clearPendingState ( ) ;
736752
737753 this . clear ( ) ;
@@ -1209,16 +1225,20 @@ export { NestedAgentSessionComponent, renderSpawnCall, renderSpawnResult };
12091225 * Synchronously flush all pending spawn frame work.
12101226 * Exported for tests. Not needed in production — the frame timer handles
12111227 * everything automatically.
1228+ *
1229+ * Delegate through getSingletons() so that test harness swaps are respected.
12121230 */
12131231export function flushSpawnFrameScheduler ( ) : void {
1214- spawnFrameScheduler . flushNow ( ) ;
1232+ getSingletons ( ) . frameScheduler . flushNow ( ) ;
12151233}
12161234
12171235/**
12181236 * Reset the frame scheduler, discarding any pending dirty markers.
12191237 * Exported for tests. In production the scheduler lifecycle is tied to
12201238 * component dispose(), so this is never needed.
1239+ *
1240+ * Delegate through getSingletons() so that test harness swaps are respected.
12211241 */
12221242export function resetSpawnFrameScheduler ( ) : void {
1223- spawnFrameScheduler . clear ( ) ;
1243+ getSingletons ( ) . frameScheduler . clear ( ) ;
12241244}
0 commit comments