@@ -19,6 +19,10 @@ const logger = winston.createLogger({
1919const ASSUME_BUSY_SINCE_OUTPUT_MS = 8000 ; // 8s
2020const ASSUME_BUSY_SINCE_OUTPUT_AGENT_MS = 15000 ; // 15s
2121const ASSUME_BUSY_SINCE_OUTPUT_CLAUDE_MS = 30000 ; // 30s
22+ const ASSUME_BUSY_SINCE_OUTPUT_CODEX_MS = 10000 ; // 10s
23+ const ASSUME_BUSY_SINCE_OUTPUT_GEMINI_MS = 6000 ; // 6s
24+ const ASSUME_BUSY_SINCE_OUTPUT_OPENCODE_MS = 5000 ; // 5s
25+ const ASSUME_BUSY_SINCE_OUTPUT_AIDER_MS = 10000 ; // 10s
2226
2327class StatusDetector {
2428 constructor ( ) {
@@ -28,7 +32,10 @@ class StatusDetector {
2832 / C o s t : \$ [ \d . ] + / , // Cost line - MOST RELIABLE done indicator
2933 / T o t a l c o s t : \$ [ \d . ] + / ,
3034 / S e s s i o n c o s t : \$ [ \d . ] + / ,
35+ / T o t a l d u r a t i o n \( w a l l \) : / , // Session summary line
36+ / T o t a l c o d e c h a n g e s : / , // Session summary line
3137 / t o k e n s u s e d / i, // Token usage line
38+ / \d + i n p u t , \d + o u t p u t .* c a c h e / , // Per-model token usage line
3239 ] ;
3340
3441 // Patterns indicating active tool usage (Claude is busy)
@@ -43,11 +50,26 @@ class StatusDetector {
4350 / G r e p \( .* \) / , // Grep tool
4451 / G l o b \( .* \) / , // Glob tool
4552 / T a s k \( .* \) / , // Task tool
53+ / A g e n t \( .* \) / , // Sub-agent tool
54+ / W e b F e t c h \( .* \) / , // Web fetch tool
55+ / W e b S e a r c h \( .* \) / , // Web search tool
56+ / N o t e b o o k E d i t \( .* \) / , // Notebook edit tool
57+ / N o t e b o o k R e a d \( .* \) / , // Notebook read tool
58+ / S k i l l \( .* \) / , // Skill execution tool
59+ / A s k U s e r Q u e s t i o n \( .* \) / , // User question tool
60+ / T o o l S e a r c h \( .* \) / , // Tool search tool
61+ / T o d o W r i t e \( .* \) / , // Todo write tool
62+ / T a s k O u t p u t \( .* \) / , // Task output tool
63+ / T a s k S t o p \( .* \) / , // Task stop tool
4664 ] ;
4765
48- // Patterns that suggest Claude is typing (busy) - more conservative
66+ // Patterns that suggest Claude is typing/processing (busy)
4967 this . typingPatterns = [
5068 / ∴ T h i n k i n g … / , // Thinking indicator
69+ / W a i t i n g f o r p e r m i s s i o n / , // Permission prompt pending
70+ / W a i t i n g f o r t a s k / , // Sub-agent task pending
71+ / R u n n i n g c o m m a n d / , // Bash tool executing
72+ / c o m p a c t i n g c o n v e r s a t i o n / i, // Context compaction in progress
5173 ] ;
5274
5375 // Per-session state (StatusDetector is shared across sessions).
@@ -91,9 +113,163 @@ class StatusDetector {
91113 return out ;
92114 }
93115
116+ normalizeAgent ( agent ) {
117+ const normalized = String ( agent || '' ) . trim ( ) . toLowerCase ( ) ;
118+ if ( ! normalized ) return null ;
119+ if ( normalized === 'gemini-cli' ) return 'gemini' ;
120+ if ( normalized === 'open-code' ) return 'opencode' ;
121+ return normalized ;
122+ }
123+
124+ getAssumeBusyWindowMs ( { agent, isAgentTerminal, claudeLikely } ) {
125+ const normalizedAgent = this . normalizeAgent ( agent ) ;
126+ if ( normalizedAgent === 'codex' ) return ASSUME_BUSY_SINCE_OUTPUT_CODEX_MS ;
127+ if ( normalizedAgent === 'gemini' ) return ASSUME_BUSY_SINCE_OUTPUT_GEMINI_MS ;
128+ if ( normalizedAgent === 'opencode' ) return ASSUME_BUSY_SINCE_OUTPUT_OPENCODE_MS ;
129+ if ( normalizedAgent === 'aider' ) return ASSUME_BUSY_SINCE_OUTPUT_AIDER_MS ;
130+ if ( claudeLikely ) return ASSUME_BUSY_SINCE_OUTPUT_CLAUDE_MS ;
131+ if ( isAgentTerminal ) return ASSUME_BUSY_SINCE_OUTPUT_AGENT_MS ;
132+ return ASSUME_BUSY_SINCE_OUTPUT_MS ;
133+ }
134+
135+ detectProviderStatus ( agent , context = { } ) {
136+ const normalizedAgent = this . normalizeAgent ( agent ) ;
137+ if ( ! normalizedAgent || normalizedAgent === 'claude' ) return null ;
138+
139+ const {
140+ recentOutput = '' ,
141+ recentAll = '' ,
142+ trimmedLastNonEmptyLine = '' ,
143+ hasRecentOutput = false ,
144+ } = context ;
145+
146+ if ( normalizedAgent === 'codex' ) {
147+ // Codex waiting: user-facing prompts and approval dialogs
148+ if (
149+ trimmedLastNonEmptyLine === '>' ||
150+ / ^ c o d e x > \s * $ / i. test ( trimmedLastNonEmptyLine ) ||
151+ ( / O p e n A I C o d e x / i. test ( recentOutput ) && / \? f o r s h o r t c u t s / i. test ( recentOutput ) ) ||
152+ / C h o o s e h o w y o u ' d l i k e C o d e x t o p r o c e e d \. / i. test ( recentOutput ) ||
153+ / C h o o s e a n a c t i o n / i. test ( recentOutput ) ||
154+ / S e l e c t p r o v i d e r \? / i. test ( recentOutput ) ||
155+ / U s e .* p r e s s e n t e r t o c o n f i r m / i. test ( recentOutput )
156+ ) {
157+ return 'waiting' ;
158+ }
159+ // Codex busy: active processing indicators
160+ if (
161+ hasRecentOutput && (
162+ / e s c t o i n t e r r u p t / i. test ( recentOutput ) ||
163+ / t a b t o a d d n o t e s / i. test ( recentOutput )
164+ )
165+ ) {
166+ return 'busy' ;
167+ }
168+ return null ;
169+ }
170+
171+ if ( normalizedAgent === 'gemini' ) {
172+ const hasGeminiPrompt = / T y p e y o u r m e s s a g e o r @ p a t h \/ t o \/ f i l e / i. test ( recentOutput ) ;
173+ const hasGeminiChrome = (
174+ / \? f o r s h o r t c u t s / i. test ( recentOutput )
175+ || / S h i f t \+ T a b t o a c c e p t e d i t s / i. test ( recentOutput )
176+ || / w o r k s p a c e \( / i. test ( recentOutput )
177+ ) ;
178+
179+ // Gemini waiting: input prompts, auth dialogs, trust dialogs, tool confirmations
180+ if (
181+ / W a i t i n g f o r a u t h e n t i c a t i o n \. \. \. / i. test ( recentOutput ) ||
182+ / W a i t i n g f o r v e r i f i c a t i o n \. \. \. / i. test ( recentOutput ) ||
183+ / D o y o u t r u s t t h e f i l e s i n t h i s f o l d e r \? / i. test ( recentOutput ) ||
184+ / N e e d a p p r o v a l \? / i. test ( recentOutput ) ||
185+ / A p p l y t h i s c h a n g e \? / i. test ( recentOutput ) ||
186+ / A l l o w e x e c u t i o n o f / i. test ( recentOutput ) ||
187+ / D o y o u w a n t t o p r o c e e d \? / i. test ( recentOutput ) ||
188+ / R e a d y t o s t a r t i m p l e m e n t a t i o n \? / i. test ( recentOutput ) ||
189+ / M o d i f y T r u s t L e v e l / i. test ( recentOutput ) ||
190+ / U s e a r r o w k e y s t o n a v i g a t e , E n t e r t o c o n f i r m , E s c t o c a n c e l \. / i. test ( recentOutput ) ||
191+ ( / P r e s s C t r l \+ [ C D ] a g a i n t o e x i t \. / i. test ( recentOutput ) && hasGeminiPrompt ) ||
192+ ( hasGeminiPrompt && hasGeminiChrome )
193+ ) {
194+ return 'waiting' ;
195+ }
196+
197+ // Gemini busy: thinking, processing, initializing
198+ if (
199+ hasRecentOutput && (
200+ / T h i n k i n g \. \. \. / i. test ( recentOutput ) ||
201+ / \( e s c t o c a n c e l , \s * [ \d : s m h ] + \) / i. test ( recentOutput ) ||
202+ / \( p r e s s t a b t o f o c u s \) / i. test ( recentOutput ) ||
203+ / W a i t i n g f o r M C P s e r v e r s t o i n i t i a l i z e / i. test ( recentAll )
204+ )
205+ ) {
206+ return 'busy' ;
207+ }
208+
209+ return null ;
210+ }
211+
212+ if ( normalizedAgent === 'opencode' ) {
213+ const hasOpenCodePrompt = / A s k a n y t h i n g \. \. \. / i. test ( recentOutput ) ;
214+ const hasOpenCodeChrome = (
215+ / c t r l \+ t \s + v a r i a n t s / i. test ( recentOutput )
216+ || / c t r l \+ p \s + c o m m a n d s / i. test ( recentOutput )
217+ || / \b t a b \s + a g e n t s \b / i. test ( recentOutput )
218+ ) ;
219+
220+ // OpenCode waiting: input prompt with chrome, or idle help text
221+ if (
222+ ( hasOpenCodePrompt && hasOpenCodeChrome ) ||
223+ / p r e s s e n t e r t o s e n d t h e m e s s a g e / i. test ( recentOutput )
224+ ) {
225+ return 'waiting' ;
226+ }
227+
228+ // OpenCode busy: thinking, generating, tool calls, working
229+ if (
230+ hasRecentOutput && (
231+ / T h i n k i n g \. \. \. / i. test ( recentOutput ) ||
232+ / G e n e r a t i n g \. \. \. / i. test ( recentOutput ) ||
233+ / W o r k i n g \. \. \. / i. test ( recentOutput ) ||
234+ / W a i t i n g f o r t o o l r e s p o n s e \. \. \. / i. test ( recentOutput ) ||
235+ / B u i l d i n g t o o l c a l l \. \. \. / i. test ( recentOutput ) ||
236+ / p r e s s e s c t o e x i t c a n c e l / i. test ( recentOutput )
237+ )
238+ ) {
239+ return 'busy' ;
240+ }
241+
242+ return null ;
243+ }
244+
245+ if ( normalizedAgent === 'aider' ) {
246+ // Aider waiting: standard prompt or multiline prompt
247+ if (
248+ trimmedLastNonEmptyLine === '>' ||
249+ / ^ m u l t i > \s * $ / i. test ( trimmedLastNonEmptyLine ) ||
250+ / ^ a i d e r > \s * $ / i. test ( trimmedLastNonEmptyLine )
251+ ) {
252+ return 'waiting' ;
253+ }
254+ // Aider busy: waiting for LLM, thinking
255+ if (
256+ hasRecentOutput && (
257+ / W a i t i n g f o r .* L L M / i. test ( recentOutput ) ||
258+ / W a i t i n g f o r .* m o d e l / i. test ( recentOutput ) ||
259+ / t h i n k t o k e n s / i. test ( recentOutput )
260+ )
261+ ) {
262+ return 'busy' ;
263+ }
264+ return null ;
265+ }
266+
267+ return null ;
268+ }
269+
94270 detectStatus ( sessionId , buffer , options = { } ) {
95271 const state = this . getState ( sessionId ) ;
96- const agent = String ( options ?. agent || '' ) . trim ( ) || null ;
272+ const agent = this . normalizeAgent ( options ?. agent ) ;
97273 const isNonClaudeAgent = ! ! ( agent && agent !== 'claude' ) ;
98274 if ( agent ) {
99275 state . agent = agent ;
@@ -109,9 +285,11 @@ class StatusDetector {
109285 }
110286 const timeSinceOutput = now - state . lastOutputTime ;
111287 const isAgentTerminal = / - ( c l a u d e | c o d e x ) $ / . test ( String ( sessionId || '' ) ) ;
112- const assumeBusyWindowMs = ( state . claudeLikely || isAgentTerminal )
113- ? ( state . claudeLikely ? ASSUME_BUSY_SINCE_OUTPUT_CLAUDE_MS : ASSUME_BUSY_SINCE_OUTPUT_AGENT_MS )
114- : ASSUME_BUSY_SINCE_OUTPUT_MS ;
288+ const assumeBusyWindowMs = this . getAssumeBusyWindowMs ( {
289+ agent,
290+ isAgentTerminal,
291+ claudeLikely : state . claudeLikely
292+ } ) ;
115293 const hasRecentOutput = timeSinceOutput < assumeBusyWindowMs ;
116294
117295 // Get recent output for analysis
@@ -121,23 +299,37 @@ class StatusDetector {
121299 const lastNonEmptyLine = this . getLastNonEmptyLine ( lines ) ;
122300 const trimmedLastNonEmptyLine = lastNonEmptyLine . trim ( ) ;
123301 const lastNonEmptyLines = this . getLastNonEmptyLines ( lines , 6 ) ;
302+ const recentAll = lastNonEmptyLines . join ( '\n' ) ;
124303
125304 // If a different agent (e.g. Codex) is running in this terminal, avoid reusing Claude UI heuristics.
126305 if ( isNonClaudeAgent ) {
127306 state . claudeLikely = false ;
128307 }
129308
130- // Best-effort Codex interactive prompt detection.
131- // This prevents the ">" prompt from being misclassified as idle (grey) and reduces dot flicker.
132- if ( agent === 'codex' ) {
133- if ( trimmedLastNonEmptyLine === '>' || / ^ c o d e x > \s * $ / . test ( trimmedLastNonEmptyLine ) ) {
134- return 'waiting' ;
309+ const providerStatus = this . detectProviderStatus ( agent , {
310+ recentOutput,
311+ recentAll,
312+ trimmedLastNonEmptyLine,
313+ hasRecentOutput,
314+ } ) ;
315+ if ( providerStatus ) {
316+ return providerStatus ;
317+ }
318+
319+ if ( isNonClaudeAgent ) {
320+ if ( this . hasExplicitShellIndicator ( recentAll , trimmedLastNonEmptyLine ) ) {
321+ return 'idle' ;
135322 }
323+
324+ if ( timeSinceOutput < assumeBusyWindowMs && buffer . length > 100 ) {
325+ return 'busy' ;
326+ }
327+
328+ return 'idle' ;
136329 }
137330
138331 // Heuristic: determine whether Claude Code UI is likely active in this session.
139332 // This is used to avoid misclassifying shell-like prompts that can appear inside output while Claude is working.
140- const recentAll = lastNonEmptyLines . join ( '\n' ) ;
141333 if ( ! isNonClaudeAgent ) {
142334 if ( / W e l c o m e t o C l a u d e C o d e ! / . test ( recentAll ) || / \? f o r s h o r t c u t s / . test ( recentAll ) ) {
143335 state . claudeLikely = true ;
0 commit comments