@@ -19,6 +19,7 @@ export interface TmuxPane {
1919 status : PaneStatus
2020 choices : TmuxChoice [ ]
2121 prompt : string
22+ activityLine : string
2223}
2324
2425// Strip ANSI escape sequences from captured pane content.
@@ -515,26 +516,49 @@ const CODEX_IDLE_INDICATORS = [
515516 / \b s e n d \b .* \b n e w l i n e \b .* \b q u i t \b / // legacy format (backward compat)
516517]
517518
519+ // Extract activity indicator line from pane content (e.g. "✻ Imagining… (17s · ↑ 107 tokens)")
520+ // These lines appear when Claude is actively processing.
521+ function parseActivityLine ( content : string ) : string {
522+ const lines = content . split ( '\n' )
523+ for ( let i = lines . length - 1 ; i >= Math . max ( 0 , lines . length - 20 ) ; i -- ) {
524+ const line = lines [ i ] . trim ( )
525+ if ( ! line ) continue
526+ // Match lines starting with activity indicator characters (✻, ⏺, braille spinners)
527+ // followed by text containing ellipsis (…)
528+ if ( / ^ [ ✻ ⏺ \u2800 - \u28FF ] / . test ( line ) && / … / . test ( line ) ) {
529+ return line
530+ }
531+ }
532+ return ''
533+ }
534+
518535function detectStatusClaude (
519536 title : string ,
520537 content : string
521- ) : { status : PaneStatus ; choices : TmuxChoice [ ] ; prompt : string } {
538+ ) : { status : PaneStatus ; choices : TmuxChoice [ ] ; prompt : string ; activityLine : string } {
522539 // Always check for choices first — permission prompts (e.g. "Do you want to proceed?
523540 // ❯ 1. Yes 2. No") can appear while the title still shows ⠂ (busy).
524541 const choices = parseChoices ( content )
525542 if ( choices . length > 0 ) {
526- return { status : 'waiting' , choices, prompt : parsePrompt ( content ) }
543+ return { status : 'waiting' , choices, prompt : parsePrompt ( content ) , activityLine : '' }
527544 }
528545
529- if ( ! title . includes ( '✳' ) ) return { status : 'busy' , choices : [ ] , prompt : '' }
546+ if ( ! title . includes ( '✳' ) ) {
547+ return {
548+ status : 'busy' ,
549+ choices : [ ] ,
550+ prompt : '' ,
551+ activityLine : parseActivityLine ( content )
552+ }
553+ }
530554
531555 const lines = content . split ( '\n' ) . slice ( - 10 )
532556 for ( const pattern of WAITING_PATTERNS ) {
533557 if ( lines . some ( ( line ) => pattern . test ( line ) ) ) {
534- return { status : 'waiting' , choices : [ ] , prompt : parsePrompt ( content ) }
558+ return { status : 'waiting' , choices : [ ] , prompt : parsePrompt ( content ) , activityLine : '' }
535559 }
536560 }
537- return { status : 'idle' , choices : [ ] , prompt : '' }
561+ return { status : 'idle' , choices : [ ] , prompt : '' , activityLine : '' }
538562}
539563
540564// Codex presents options as indented "- " list items
@@ -546,14 +570,15 @@ function detectStatusCodex(content: string): {
546570 status : PaneStatus
547571 choices : TmuxChoice [ ]
548572 prompt : string
573+ activityLine : string
549574} {
550575 const lines = content . split ( '\n' ) . slice ( - 15 )
551576 const tail = lines . join ( '\n' )
552577
553578 // 1. Check for explicit busy signals (-ing words or "esc to interrupt")
554579 const isBusy = CODEX_BUSY_PATTERNS . some ( ( p ) => p . test ( tail ) )
555580 if ( isBusy ) {
556- return { status : 'busy' , choices : [ ] , prompt : '' }
581+ return { status : 'busy' , choices : [ ] , prompt : '' , activityLine : parseActivityLine ( content ) }
557582 }
558583
559584 // 2. Check for explicit idle signals (footer hints)
@@ -562,22 +587,22 @@ function detectStatusCodex(content: string): {
562587 const hasQuestion = lines . some ( ( line ) => CODEX_QUESTION_PATTERN . test ( line ) )
563588 const optionCount = lines . filter ( ( line ) => CODEX_OPTION_PATTERN . test ( line ) ) . length
564589 if ( hasQuestion || optionCount >= 2 ) {
565- return { status : 'waiting' , choices : [ ] , prompt : '' }
590+ return { status : 'waiting' , choices : [ ] , prompt : '' , activityLine : '' }
566591 }
567- return { status : 'idle' , choices : [ ] , prompt : '' }
592+ return { status : 'idle' , choices : [ ] , prompt : '' , activityLine : '' }
568593 }
569594
570595 // 3. No busy signal detected → default to idle (not busy).
571596 // If Codex is NOT showing "Working"/"Thinking"/etc,
572597 // it is most likely at the input prompt waiting for user input.
573- return { status : 'idle' , choices : [ ] , prompt : '' }
598+ return { status : 'idle' , choices : [ ] , prompt : '' , activityLine : '' }
574599}
575600
576601function detectStatus (
577602 title : string ,
578603 content : string ,
579604 command : string
580- ) : { status : PaneStatus ; choices : TmuxChoice [ ] ; prompt : string } {
605+ ) : { status : PaneStatus ; choices : TmuxChoice [ ] ; prompt : string ; activityLine : string } {
581606 if ( command === 'codex' ) return detectStatusCodex ( content )
582607 return detectStatusClaude ( title , content )
583608}
@@ -600,7 +625,8 @@ export async function listPanes(): Promise<TmuxPane[]> {
600625 title,
601626 status : 'busy' as PaneStatus ,
602627 choices : [ ] as TmuxChoice [ ] ,
603- prompt : ''
628+ prompt : '' ,
629+ activityLine : ''
604630 }
605631 } )
606632 // Support popular wrappers like `ai` in addition to `claude` and `codex`.
@@ -639,6 +665,7 @@ export async function listPanes(): Promise<TmuxPane[]> {
639665 pane . status = result . status
640666 pane . choices = result . choices
641667 pane . prompt = result . prompt
668+ pane . activityLine = result . activityLine
642669 } )
643670 )
644671
0 commit comments