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

1# type: ignore 

2#!/usr/bin/env python3 

3""" 

4Enhanced Output Style Detector for Claude Code Statusline 

5 

6This module provides real-time detection of Claude Code's current output style 

7by analyzing session context, environment variables, and behavioral indicators. 

8 

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""" 

15 

16import json 

17import os 

18import sys 

19import time 

20from pathlib import Path 

21from typing import Any, Dict, Optional 

22 

23 

24class OutputStyleDetector: 

25 """ 

26 Enhanced output style detector with multiple detection strategies. 

27 

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 """ 

35 

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", 

46 

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 } 

59 

60 def __init__(self): 

61 self.cache = {} 

62 self.cache_ttl = 5 # Cache for 5 seconds to balance performance and accuracy 

63 

64 def detect_from_session_context(self, session_data: Dict[str, Any]) -> Optional[str]: 

65 """ 

66 Detect output style from Claude Code session context. 

67 

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) 

76 

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() 

82 

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" 

95 

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) 

102 

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) 

106 

107 return None 

108 

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) 

119 

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 

124 

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 

131 

132 except Exception as e: 

133 print(f"Environment detection error: {e}", file=sys.stderr) 

134 

135 return None 

136 

137 def detect_from_behavioral_analysis(self) -> Optional[str]: 

138 """ 

139 Analyze behavioral patterns to infer current output style. 

140 

141 This method uses heuristics based on file system state and recent activity. 

142 """ 

143 try: 

144 cwd = Path.cwd() 

145 

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" 

159 

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" 

166 

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" 

173 

174 except Exception as e: 

175 print(f"Behavioral analysis error: {e}", file=sys.stderr) 

176 

177 return None 

178 

179 def detect_from_settings(self) -> Optional[str]: 

180 """ 

181 Detect output style from settings.json file. 

182 

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", "") 

191 

192 if output_style: 

193 return self._normalize_style(output_style) 

194 

195 except Exception as e: 

196 print(f"Settings file detection error: {e}", file=sys.stderr) 

197 

198 return None 

199 

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" 

206 

207 # Direct mapping lookup 

208 if style in self.STYLE_MAPPING: 

209 return self.STYLE_MAPPING[style] 

210 

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 

216 

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" 

228 

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) 

238 

239 # Fallback: capitalize first letter 

240 return style.title() if style else "Unknown" 

241 

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 

249 

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 ) 

256 

257 if not full_text: 

258 return None 

259 

260 # Style heuristics based on response patterns 

261 text_lower = full_text.lower() 

262 

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) 

266 

267 if yoda_count >= 2: 

268 return "🧙 Yoda Master" 

269 

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" 

278 

279 # Concise indicators 

280 if len(full_text) < 500: 

281 return "Concise" 

282 

283 # Default fallback 

284 return "R2-D2" 

285 

286 except Exception as e: 

287 print(f"Message pattern analysis error: {e}", file=sys.stderr) 

288 return None 

289 

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. 

293 

294 Args: 

295 session_context: Optional session context from Claude Code 

296 

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() 

303 

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 

308 

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 ] 

316 

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 

327 

328 # Default fallback 

329 return "R2-D2" 

330 

331 

332def safe_collect_output_style() -> str: 

333 """ 

334 Legacy compatibility function that maintains the original interface. 

335 

336 This function provides backward compatibility with the existing statusline 

337 system while using the enhanced detection capabilities. 

338 

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 

351 

352 # Use the enhanced detector 

353 detector = OutputStyleDetector() 

354 return detector.get_output_style(session_context) 

355 

356 except Exception as e: 

357 print(f"Output style detection failed: {e}", file=sys.stderr) 

358 return "R2-D2" 

359 

360 

361# For backward compatibility 

362if __name__ == "__main__": 

363 # If run as a script, output the detected style 

364 print(safe_collect_output_style())