Coverage for src / moai_adk / statusline / enhanced_output_style_detector.py: 0.00%
172 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1# type: ignore
2#!/usr/bin/env python3
3"""
4Enhanced Output Style Detector for Claude Code Statusline
6This module provides real-time detection of Claude Code's current output style
7by analyzing session context, environment variables, and behavioral indicators.
9Key improvements:
101. Real-time session context analysis
112. Multiple detection methods with priority ordering
123. Behavioral pattern recognition
134. Robust error handling and graceful degradation
14"""
16import json
17import os
18import sys
19import time
20from pathlib import Path
21from typing import Any, Dict, Optional
24class OutputStyleDetector:
25 """
26 Enhanced output style detector with multiple detection strategies.
28 Detection Priority:
29 1. Session context (most reliable)
30 2. Environment variables
31 3. Behavioral analysis
32 4. Settings file (fallback)
33 5. Heuristics (last resort)
34 """
36 # Style mapping for consistent display format
37 STYLE_MAPPING = {
38 # Internal style names
39 "streaming": "R2-D2",
40 "explanatory": "Explanatory",
41 "concise": "Concise",
42 "detailed": "Detailed",
43 "yoda": "🧙 Yoda Master",
44 "yoda-master": "🧙 Yoda Master",
45 "tutorial": "🧙 Yoda Master",
47 # Display values with emojis
48 "🤖 R2-D2": "R2-D2",
49 "R2-D2": "R2-D2",
50 "🧙 Explanatory": "Explanatory",
51 "Explanatory": "Explanatory",
52 "🧙 Concise": "Concise",
53 "Concise": "Concise",
54 "🧙 Detailed": "Detailed",
55 "Detailed": "Detailed",
56 "🧙 Yoda Master": "🧙 Yoda Master",
57 "Yoda Master": "🧙 Yoda Master",
58 }
60 def __init__(self):
61 self.cache = {}
62 self.cache_ttl = 5 # Cache for 5 seconds to balance performance and accuracy
64 def detect_from_session_context(self, session_data: Dict[str, Any]) -> Optional[str]:
65 """
66 Detect output style from Claude Code session context.
68 This is the most reliable method as it uses real-time session data.
69 """
70 try:
71 # Method 1: Check explicit outputStyle in session
72 if "outputStyle" in session_data:
73 style = session_data["outputStyle"]
74 if style:
75 return self._normalize_style(style)
77 # Method 2: Check model configuration for style indicators
78 model_info = session_data.get("model", {})
79 if isinstance(model_info, dict):
80 model_name = model_info.get("name", "").lower()
81 display_name = model_info.get("display_name", "").lower()
83 # Check for style indicators in model names
84 for text in [model_name, display_name]:
85 if "explanatory" in text:
86 return "Explanatory"
87 elif "yoda" in text or "tutorial" in text:
88 return "🧙 Yoda Master"
89 elif "concise" in text:
90 return "Concise"
91 elif "detailed" in text:
92 return "Detailed"
93 elif "streaming" in text or "r2d2" in text:
94 return "R2-D2"
96 # Method 3: Check conversation patterns
97 messages = session_data.get("messages", [])
98 if messages:
99 # Analyze recent message patterns for style indicators
100 recent_messages = messages[-3:] # Last 3 messages
101 return self._analyze_message_patterns(recent_messages)
103 except Exception as e:
104 # Log error but don't fail - continue to next detection method
105 print(f"Session context detection error: {e}", file=sys.stderr)
107 return None
109 def detect_from_environment(self) -> Optional[str]:
110 """
111 Detect output style from environment variables.
112 """
113 try:
114 # Check for explicit environment variable
115 if "CLAUDE_OUTPUT_STYLE" in os.environ:
116 env_style = os.environ["CLAUDE_OUTPUT_STYLE"]
117 if env_style:
118 return self._normalize_style(env_style)
120 # Check for Claude Code session indicators
121 if "CLAUDE_SESSION_ID" in os.environ:
122 # Could integrate with Claude Code session management
123 pass
125 # Check for process name or command line indicators
126 try:
127 # This could be enhanced with process inspection if needed
128 pass
129 except Exception:
130 pass
132 except Exception as e:
133 print(f"Environment detection error: {e}", file=sys.stderr)
135 return None
137 def detect_from_behavioral_analysis(self) -> Optional[str]:
138 """
139 Analyze behavioral patterns to infer current output style.
141 This method uses heuristics based on file system state and recent activity.
142 """
143 try:
144 cwd = Path.cwd()
146 # Check for active Yoda session indicators
147 moai_dir = cwd / ".moai"
148 if moai_dir.exists():
149 # Look for recent Yoda-related activity
150 yoda_files = list(moai_dir.rglob("*yoda*"))
151 if yoda_files:
152 # Check if any Yoda files are recently modified
153 recent_yoda = any(
154 f.stat().st_mtime > (time.time() - 300) # Last 5 minutes
155 for f in yoda_files
156 )
157 if recent_yoda:
158 return "🧙 Yoda Master"
160 # Check for extensive documentation (might indicate Explanatory mode)
161 docs_dir = cwd / "docs"
162 if docs_dir.exists():
163 md_files = list(docs_dir.rglob("*.md"))
164 if len(md_files) > 10: # Heuristic threshold
165 return "Explanatory"
167 # Check for TODO/task tracking patterns
168 todo_file = cwd / ".moai" / "current_session_todo.txt"
169 if todo_file.exists():
170 content = todo_file.read_text()
171 if "plan" in content.lower() or "phase" in content.lower():
172 return "Explanatory"
174 except Exception as e:
175 print(f"Behavioral analysis error: {e}", file=sys.stderr)
177 return None
179 def detect_from_settings(self) -> Optional[str]:
180 """
181 Detect output style from settings.json file.
183 This is the least reliable method as it may not reflect current session.
184 """
185 try:
186 settings_path = Path.cwd() / ".claude" / "settings.json"
187 if settings_path.exists():
188 with open(settings_path, "r", encoding="utf-8") as f:
189 settings = json.load(f)
190 output_style = settings.get("outputStyle", "")
192 if output_style:
193 return self._normalize_style(output_style)
195 except Exception as e:
196 print(f"Settings file detection error: {e}", file=sys.stderr)
198 return None
200 def _normalize_style(self, style: str) -> str:
201 """
202 Normalize style name to consistent display format.
203 """
204 if not style:
205 return "Unknown"
207 # Direct mapping lookup
208 if style in self.STYLE_MAPPING:
209 return self.STYLE_MAPPING[style]
211 # Case-insensitive lookup
212 style_lower = style.lower()
213 for key, value in self.STYLE_MAPPING.items():
214 if key.lower() == style_lower:
215 return value
217 # Pattern-based normalization
218 if "r2d2" in style_lower or "streaming" in style_lower:
219 return "R2-D2"
220 elif "yoda" in style_lower or "master" in style_lower:
221 return "🧙 Yoda Master"
222 elif "explanatory" in style_lower:
223 return "Explanatory"
224 elif "concise" in style_lower:
225 return "Concise"
226 elif "detailed" in style_lower:
227 return "Detailed"
229 # Extract from emoji-prefixed values
230 if "🤖" in style:
231 return "R2-D2"
232 elif "🧙" in style:
233 # Extract the part after emoji
234 parts = style.split("🧙", 1)
235 if len(parts) > 1:
236 extracted = parts[1].strip()
237 return self._normalize_style(extracted)
239 # Fallback: capitalize first letter
240 return style.title() if style else "Unknown"
242 def _analyze_message_patterns(self, messages: list) -> Optional[str]:
243 """
244 Analyze recent message patterns for style indicators.
245 """
246 try:
247 if not messages:
248 return None
250 # Look for style indicators in recent responses
251 full_text = " ".join(
252 msg.get("content", "")
253 for msg in messages[-3:]
254 if msg.get("role") == "assistant"
255 )
257 if not full_text:
258 return None
260 # Style heuristics based on response patterns
261 text_lower = full_text.lower()
263 # Yoda Master indicators
264 yoda_indicators = ["young padawan", "the force", "master", "wisdom", "patience"]
265 yoda_count = sum(1 for indicator in yoda_indicators if indicator in text_lower)
267 if yoda_count >= 2:
268 return "🧙 Yoda Master"
270 # Explanatory indicators
271 if len(full_text) > 2000: # Long responses
272 explanatory_count = sum(1 for phrase in
273 ["let me explain", "here's how", "the reason is", "to understand"]
274 if phrase in text_lower
275 )
276 if explanatory_count >= 2:
277 return "Explanatory"
279 # Concise indicators
280 if len(full_text) < 500:
281 return "Concise"
283 # Default fallback
284 return "R2-D2"
286 except Exception as e:
287 print(f"Message pattern analysis error: {e}", file=sys.stderr)
288 return None
290 def get_output_style(self, session_context: Dict[str, Any] = None) -> str:
291 """
292 Get the current output style using all available detection methods.
294 Args:
295 session_context: Optional session context from Claude Code
297 Returns:
298 Normalized output style string
299 """
300 # Use cache if available and fresh
301 cache_key = f"{id(session_context)}_{hash(str(session_context))}"
302 current_time = time.time()
304 if cache_key in self.cache:
305 cached_style, cached_time = self.cache[cache_key]
306 if current_time - cached_time < self.cache_ttl:
307 return cached_style
309 # Detection methods in priority order
310 detection_methods = [
311 ("Session Context", lambda: self.detect_from_session_context(session_context or {})),
312 ("Environment", self.detect_from_environment),
313 ("Behavioral Analysis", self.detect_from_behavioral_analysis),
314 ("Settings File", self.detect_from_settings),
315 ]
317 for method_name, method_func in detection_methods:
318 try:
319 style = method_func()
320 if style and style != "Unknown":
321 # Cache the result
322 self.cache[cache_key] = (style, current_time)
323 return style
324 except Exception as e:
325 print(f"{method_name} detection failed: {e}", file=sys.stderr)
326 continue
328 # Default fallback
329 return "R2-D2"
332def safe_collect_output_style() -> str:
333 """
334 Legacy compatibility function that maintains the original interface.
336 This function provides backward compatibility with the existing statusline
337 system while using the enhanced detection capabilities.
339 Returns:
340 Detected output style string
341 """
342 try:
343 # Read session context from stdin if available
344 session_context = {}
345 try:
346 input_data = sys.stdin.read()
347 if input_data.strip():
348 session_context = json.loads(input_data)
349 except (json.JSONDecodeError, EOFError):
350 pass
352 # Use the enhanced detector
353 detector = OutputStyleDetector()
354 return detector.get_output_style(session_context)
356 except Exception as e:
357 print(f"Output style detection failed: {e}", file=sys.stderr)
358 return "R2-D2"
361# For backward compatibility
362if __name__ == "__main__":
363 # If run as a script, output the detected style
364 print(safe_collect_output_style())