@@ -12,6 +12,7 @@ import { consola } from 'consola'
1212import { KEEPS_PROCESS_ALIVE } from '../../utils/errors'
1313import { debug , isEmittingCliLog , setLoggerImpl } from '../../utils/logger'
1414import { getPkgVersion } from '../../utils/pkg'
15+ import { READY_MESSAGE } from '../../utils/progress-snapshot'
1516import { startupElapsedMs } from '../../utils/startup-clock'
1617import { resolveBackground } from '../../utils/terminal-theme'
1718import { currentRequest , isServingRequest } from '../serving-state'
@@ -86,6 +87,12 @@ export interface DevUISession {
8687 stopStartupTicker : ( ) => void
8788 /** Narrate the current startup phase while the server is loading. */
8889 reportProgress : ( snapshot : ProgressSnapshot ) => void
90+ /**
91+ * Repaint through the controller instead of {@link render} whenever progress
92+ * changes what is on the panel, so the controller can re-arm the animation it
93+ * owns: a render in flight is work, and a still panel reads as a hung one.
94+ */
95+ onProgressChange : ( listener : ( ) => void ) => void
8996 /**
9097 * Show the bound address the moment the socket answers, spinning until the
9198 * resolved config confirms it. The full URL block replaces it on ready.
@@ -176,22 +183,36 @@ export function beginDevUI(options: DevUISupportOptions & { version?: string, cw
176183 surface . render ( renderPanel ( state , process . stdout . columns || 80 , process . stdout . rows || 24 ) )
177184 }
178185
186+ let progressListener : ( ( ) => void ) | undefined
187+
188+ /** Repaint through the controller where one is attached, so it sees the change. */
189+ function repaint ( ) : void {
190+ if ( progressListener ) {
191+ progressListener ( )
192+ return
193+ }
194+ render ( )
195+ }
196+
179197 function reportProgress ( snapshot : ProgressSnapshot ) : void {
180198 if ( snapshot . status === 'ready' ) {
181199 // Between the server accepting requests and answering one there is
182200 // nothing to watch but a badge, so it says which of the two has happened.
183- // A render in flight is the only thing worth waiting for at this point,
184- // and until the first one lands nothing else is being reported at all.
185- const rendering = ! ! snapshot . pending && ! snapshot . serving
186- state . awaitingFirstRender = rendering
187- state . note = snapshot . pending ? `rendering ${ snapshot . pending . label } ` : undefined
188- state . progress = rendering ? snapshot . progress : undefined
201+ // Something already waiting for a page is a state of the load; the request
202+ // being rendered is not, and is held separately so that a load reporting
203+ // itself ready again cannot take it off the panel.
204+ const waiting = ! snapshot . serving && snapshot . message !== READY_MESSAGE
205+ state . awaitingFirstRender = ! snapshot . serving
206+ state . note = waiting ? snapshot . message : undefined
207+ state . progress = waiting ? snapshot . progress : undefined
208+ state . rendering = snapshot . pending && { label : snapshot . pending . label , startedAt : snapshot . pending . startedAt }
209+ state . renderingMs = snapshot . pending && Date . now ( ) - snapshot . pending . startedAt
189210 state . phaseStartedAt = undefined
190211 state . phaseElapsedMs = undefined
191212 if ( state . status === 'ready' || state . status === 'warming' ) {
192- state . status = rendering ? 'warming' : 'ready'
193- render ( )
213+ state . status = waiting ? 'warming' : 'ready'
194214 }
215+ repaint ( )
195216 return
196217 }
197218 if ( snapshot . status !== 'loading' ) {
@@ -413,6 +434,9 @@ export function beginDevUI(options: DevUISupportOptions & { version?: string, cw
413434 expectRender,
414435 stopStartupTicker,
415436 reportProgress,
437+ onProgressChange : ( listener ) => {
438+ progressListener = listener
439+ } ,
416440 reportListening,
417441 teardown,
418442 onTeardown : task => void teardownTasks . push ( task ) ,
0 commit comments