@@ -39,24 +39,26 @@ const ProgressModal: React.FC<ProgressModalProps> = ({
3939 migrationError = false ,
4040 onNavigateHome
4141} ) => {
42- // Calculate progress percentage based on phases
42+ // Calculate progress percentage based on step (stable step-level identifier)
4343 const getProgressPercentage = ( ) => {
4444 if ( migrationError ) return 0 ; // Show 0% progress for errors
45- if ( ! apiData || ! apiData . phase ) return 0 ;
46-
47- const phases = [ 'Analysis' , 'Design' , 'YAML' , 'Documentation' ] ;
48- const currentPhaseIndex = phases . indexOf ( apiData . phase ) ;
49-
50- if ( currentPhaseIndex === - 1 ) return 0 ;
5145 if ( processingCompleted && ! migrationError ) return 100 ;
52-
53- // Each phase represents 25% of the progress
54- const baseProgress = ( currentPhaseIndex / phases . length ) * 100 ;
55-
56- // Add some progress within the current phase based on time elapsed
57- const phaseProgress = Math . min ( 20 , ( currentPhaseIndex + 1 ) * 5 ) ;
58-
59- return Math . min ( 95 , baseProgress + phaseProgress ) ;
46+ if ( ! apiData ) return 0 ;
47+
48+ // Use apiData.step (stable: "analysis", "design", "yaml_conversion", "documentation")
49+ // rather than apiData.phase which changes to sub-phase names like "Platform Enhancement"
50+ const steps = [ 'analysis' , 'design' , 'yaml_conversion' , 'documentation' ] ;
51+ const currentStepIndex = steps . indexOf ( ( apiData . step || '' ) . toLowerCase ( ) ) ;
52+
53+ if ( currentStepIndex === - 1 ) return 0 ;
54+
55+ // Each step represents 25% of the progress
56+ const baseProgress = ( currentStepIndex / steps . length ) * 100 ;
57+
58+ // Add some progress within the current step
59+ const stepProgress = Math . min ( 20 , ( currentStepIndex + 1 ) * 5 ) ;
60+
61+ return Math . min ( 95 , baseProgress + stepProgress ) ;
6062 } ;
6163
6264 const progressPercentage = getProgressPercentage ( ) ;
@@ -180,12 +182,135 @@ const ProgressModal: React.FC<ProgressModalProps> = ({
180182 borderRadius : '6px' ,
181183 fontSize : '14px'
182184 } } >
183- < div style = { { fontWeight : '500' , marginBottom : '4px' } } > Current Activity:</ div >
184- < div style = { { color : '#666' } } >
185- { apiData . agents ?. find ( agent => agent . includes ( 'speaking' ) || agent . includes ( 'thinking' ) ) ||
186- `Working on ${ currentPhase ?. toLowerCase ( ) } phase...` }
185+ < div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : '8px' } } >
186+ < span style = { { fontWeight : '500' } } > Current Activity:</ span >
187+ { ( ( ) => {
188+ // Show step-level elapsed time from step_timings
189+ const stepTimings = apiData . step_timings || { } ;
190+ const currentStep = ( apiData . step || '' ) . toLowerCase ( ) ;
191+ const timing = stepTimings [ currentStep ] ;
192+ if ( timing ?. started_at ) {
193+ try {
194+ const started = new Date ( timing . started_at . replace ( ' UTC' , 'Z' ) ) ;
195+ const diffSec = Math . max ( 0 , Math . floor ( ( Date . now ( ) - started . getTime ( ) ) / 1000 ) ) ;
196+ let elapsed = '' ;
197+ if ( diffSec < 60 ) elapsed = `${ diffSec } s` ;
198+ else if ( diffSec < 3600 ) elapsed = `${ Math . floor ( diffSec / 60 ) } m ${ diffSec % 60 } s` ;
199+ else elapsed = `${ Math . floor ( diffSec / 3600 ) } h ${ Math . floor ( ( diffSec % 3600 ) / 60 ) } m` ;
200+ return (
201+ < span style = { { fontSize : '12px' , color : '#888' } } >
202+ ⏱ { elapsed }
203+ </ span >
204+ ) ;
205+ } catch { /* ignore */ }
206+ }
207+ return null ;
208+ } ) ( ) }
187209 </ div >
188- { apiData . active_agent_count && apiData . total_agents && (
210+ { ( ( ) => {
211+ // Parse all active agents from raw telemetry strings
212+ const activeAgents = ( apiData . agents || [ ] ) . filter ( ( agent : string ) =>
213+ agent . startsWith ( '✓' )
214+ ) ;
215+
216+ if ( activeAgents . length === 0 ) {
217+ return (
218+ < div style = { { color : '#666' } } >
219+ Working on { currentPhase ?. toLowerCase ( ) } phase...
220+ </ div >
221+ ) ;
222+ }
223+
224+ return activeAgents . map ( ( raw : string , idx : number ) => {
225+ // Strip prefix: "✓[🤔🔥] " → ""
226+ const cleaned = raw . replace ( / ^ [ ✓ ✗ ] \[ .* ?\] \s * / , '' ) ;
227+ // Agent name: everything before first ":"
228+ const colonIdx = cleaned . indexOf ( ':' ) ;
229+ const agentName = colonIdx > 0 ? cleaned . substring ( 0 , colonIdx ) . trim ( ) : 'Agent' ;
230+
231+ // Determine action from status keywords
232+ const actionIcons : Record < string , { icon : string ; label : string } > = {
233+ 'speaking' : { icon : '🗣️' , label : 'Speaking' } ,
234+ 'thinking' : { icon : '💭' , label : 'Thinking' } ,
235+ 'using_tool' :{ icon : '🔧' , label : 'Invoking Tool' } ,
236+ 'analyzing' : { icon : '🔍' , label : 'Analyzing' } ,
237+ 'responded' : { icon : '✅' , label : 'Responded' } ,
238+ 'ready' : { icon : '⏳' , label : 'Ready' } ,
239+ } ;
240+ const statusPart = colonIdx > 0 ? cleaned . substring ( colonIdx + 1 ) : cleaned ;
241+ let actionInfo = { icon : '⚡' , label : 'Working' } ;
242+ for ( const [ key , info ] of Object . entries ( actionIcons ) ) {
243+ if ( statusPart . toLowerCase ( ) . includes ( key ) ) {
244+ actionInfo = info ;
245+ break ;
246+ }
247+ }
248+
249+ // Extract tool name(s) from 🔧 segment
250+ const toolMatch = raw . match ( / 🔧 \s * ( [ ^ | ] + ) / ) ;
251+ let toolName = '' ;
252+ if ( toolMatch ) {
253+ // Clean up: take tool names, strip long JSON args
254+ toolName = toolMatch [ 1 ]
255+ . trim ( )
256+ . replace ( / \{ [ ^ } ] * \} \. * / g, '' ) // remove JSON snippets
257+ . replace ( / \( .* ?\) / g, '' ) // remove parenthesized args
258+ . replace ( / , \s * $ / , '' )
259+ . trim ( ) ;
260+ if ( toolName ) {
261+ actionInfo = { icon : '🔧' , label : 'Invoking Tool' } ;
262+ }
263+ }
264+
265+ // Extract action count from 📊 segment
266+ const actionsMatch = raw . match ( / 📊 \s * ( \d + ) \s * a c t i o n s ? / ) ;
267+ const actionCount = actionsMatch ? parseInt ( actionsMatch [ 1 ] ) : 0 ;
268+
269+ // Extract blocking info from 🚧 segment
270+ const blockingMatch = raw . match ( / 🚧 \s * B l o c k i n g \s * ( \d + ) / ) ;
271+ const blockingCount = blockingMatch ? parseInt ( blockingMatch [ 1 ] ) : 0 ;
272+
273+ return (
274+ < div key = { idx } style = { {
275+ marginBottom : idx < activeAgents . length - 1 ? '10px' : 0 ,
276+ padding : '8px 10px' ,
277+ backgroundColor : 'white' ,
278+ borderRadius : '6px' ,
279+ border : '1px solid #e8e8e8' ,
280+ } } >
281+ { /* Agent name + action */ }
282+ < div style = { { display : 'flex' , alignItems : 'center' , gap : '6px' , marginBottom : '4px' } } >
283+ < span style = { { fontSize : '15px' } } > { actionInfo . icon } </ span >
284+ < span style = { { fontWeight : '600' , color : '#333' } } > { agentName } </ span >
285+ < span style = { {
286+ fontSize : '12px' ,
287+ color : '#0078d4' ,
288+ backgroundColor : '#e8f4fd' ,
289+ padding : '1px 8px' ,
290+ borderRadius : '10px' ,
291+ fontWeight : '500'
292+ } } >
293+ { actionInfo . label }
294+ </ span >
295+ </ div >
296+ { /* Tool + metrics row */ }
297+ < div style = { { display : 'flex' , flexWrap : 'wrap' , gap : '8px' , fontSize : '12px' , color : '#666' } } >
298+ { toolName && (
299+ < span style = { { display : 'flex' , alignItems : 'center' , gap : '3px' } } >
300+ < span > 🔧</ span > { toolName }
301+ </ span >
302+ ) }
303+ { actionCount > 0 && (
304+ < span style = { { display : 'flex' , alignItems : 'center' , gap : '3px' } } >
305+ < span > 📊</ span > { actionCount } action{ actionCount !== 1 ? 's' : '' }
306+ </ span >
307+ ) }
308+ </ div >
309+ </ div >
310+ ) ;
311+ } ) ;
312+ } ) ( ) }
313+ { apiData . active_agent_count != null && apiData . total_agents != null && (
189314 < div style = { { marginTop : '8px' , fontSize : '12px' , color : '#888' } } >
190315 { apiData . active_agent_count } /{ apiData . total_agents } agents active
191316 { apiData . health_status ?. includes ( '🟢' ) && ' 🟢' }
0 commit comments