gitflow-analytics 1.0.0__py3-none-any.whl → 1.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. gitflow_analytics/__init__.py +11 -9
  2. gitflow_analytics/_version.py +2 -2
  3. gitflow_analytics/cli.py +691 -243
  4. gitflow_analytics/cli_rich.py +353 -0
  5. gitflow_analytics/config.py +389 -96
  6. gitflow_analytics/core/analyzer.py +175 -78
  7. gitflow_analytics/core/branch_mapper.py +132 -132
  8. gitflow_analytics/core/cache.py +242 -173
  9. gitflow_analytics/core/identity.py +214 -178
  10. gitflow_analytics/extractors/base.py +13 -11
  11. gitflow_analytics/extractors/story_points.py +70 -59
  12. gitflow_analytics/extractors/tickets.py +111 -88
  13. gitflow_analytics/integrations/github_integration.py +91 -77
  14. gitflow_analytics/integrations/jira_integration.py +284 -0
  15. gitflow_analytics/integrations/orchestrator.py +99 -72
  16. gitflow_analytics/metrics/dora.py +183 -179
  17. gitflow_analytics/models/database.py +191 -54
  18. gitflow_analytics/qualitative/__init__.py +30 -0
  19. gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
  20. gitflow_analytics/qualitative/classifiers/change_type.py +468 -0
  21. gitflow_analytics/qualitative/classifiers/domain_classifier.py +399 -0
  22. gitflow_analytics/qualitative/classifiers/intent_analyzer.py +436 -0
  23. gitflow_analytics/qualitative/classifiers/risk_analyzer.py +412 -0
  24. gitflow_analytics/qualitative/core/__init__.py +13 -0
  25. gitflow_analytics/qualitative/core/llm_fallback.py +653 -0
  26. gitflow_analytics/qualitative/core/nlp_engine.py +373 -0
  27. gitflow_analytics/qualitative/core/pattern_cache.py +457 -0
  28. gitflow_analytics/qualitative/core/processor.py +540 -0
  29. gitflow_analytics/qualitative/models/__init__.py +25 -0
  30. gitflow_analytics/qualitative/models/schemas.py +272 -0
  31. gitflow_analytics/qualitative/utils/__init__.py +13 -0
  32. gitflow_analytics/qualitative/utils/batch_processor.py +326 -0
  33. gitflow_analytics/qualitative/utils/cost_tracker.py +343 -0
  34. gitflow_analytics/qualitative/utils/metrics.py +347 -0
  35. gitflow_analytics/qualitative/utils/text_processing.py +243 -0
  36. gitflow_analytics/reports/analytics_writer.py +25 -8
  37. gitflow_analytics/reports/csv_writer.py +60 -32
  38. gitflow_analytics/reports/narrative_writer.py +21 -15
  39. gitflow_analytics/tui/__init__.py +5 -0
  40. gitflow_analytics/tui/app.py +721 -0
  41. gitflow_analytics/tui/screens/__init__.py +8 -0
  42. gitflow_analytics/tui/screens/analysis_progress_screen.py +487 -0
  43. gitflow_analytics/tui/screens/configuration_screen.py +547 -0
  44. gitflow_analytics/tui/screens/loading_screen.py +358 -0
  45. gitflow_analytics/tui/screens/main_screen.py +304 -0
  46. gitflow_analytics/tui/screens/results_screen.py +698 -0
  47. gitflow_analytics/tui/widgets/__init__.py +7 -0
  48. gitflow_analytics/tui/widgets/data_table.py +257 -0
  49. gitflow_analytics/tui/widgets/export_modal.py +301 -0
  50. gitflow_analytics/tui/widgets/progress_widget.py +192 -0
  51. gitflow_analytics-1.0.3.dist-info/METADATA +490 -0
  52. gitflow_analytics-1.0.3.dist-info/RECORD +62 -0
  53. gitflow_analytics-1.0.0.dist-info/METADATA +0 -201
  54. gitflow_analytics-1.0.0.dist-info/RECORD +0 -30
  55. {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/WHEEL +0 -0
  56. {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/entry_points.txt +0 -0
  57. {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/licenses/LICENSE +0 -0
  58. {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,358 @@
1
+ """Loading screen for GitFlow Analytics TUI startup."""
2
+
3
+ import asyncio
4
+ import time
5
+ from typing import Optional, Dict, Any
6
+
7
+ from textual.widgets import Header, Footer, Label, Static, LoadingIndicator
8
+ from textual.containers import Container, Vertical, Horizontal, Center
9
+ from textual.screen import Screen
10
+ from textual.binding import Binding
11
+ from textual.message import Message
12
+ from rich.text import Text
13
+ from rich.align import Align
14
+
15
+ from ..widgets.progress_widget import AnalysisProgressWidget
16
+
17
+
18
+ class LoadingScreen(Screen):
19
+ """
20
+ Loading screen displayed during application startup and heavy initialization.
21
+
22
+ WHY: The TUI application needs to load configurations, spaCy models, and other
23
+ heavy resources during startup. This screen provides user feedback about the
24
+ loading process instead of showing a black screen, improving user experience.
25
+
26
+ DESIGN DECISION: Uses a combination of progress indicators and status messages
27
+ to show both overall progress and specific loading steps. This keeps users
28
+ informed about what's happening and that the application is responsive.
29
+ """
30
+
31
+ BINDINGS = [
32
+ Binding("ctrl+c", "cancel", "Cancel Loading"),
33
+ Binding("escape", "cancel", "Cancel Loading"),
34
+ ]
35
+
36
+ def __init__(
37
+ self,
38
+ loading_message: str = "Initializing GitFlow Analytics...",
39
+ *,
40
+ name: Optional[str] = None,
41
+ id: Optional[str] = None
42
+ ) -> None:
43
+ super().__init__(name=name, id=id)
44
+ self.loading_message = loading_message
45
+ self.loading_task: Optional[asyncio.Task] = None
46
+ self.start_time = time.time()
47
+ self.cancelled = False
48
+
49
+ def compose(self):
50
+ """Compose the loading screen with progress indicators."""
51
+ yield Header()
52
+
53
+ with Container(id="loading-container"):
54
+ # Main loading title
55
+ yield Label("GitFlow Analytics", classes="screen-title")
56
+ yield Label("Developer Productivity Analysis", classes="help-text center")
57
+
58
+ # Loading animation and message
59
+ with Center():
60
+ with Vertical(id="loading-content"):
61
+ yield LoadingIndicator(id="main-spinner")
62
+ yield Label(self.loading_message, classes="center", id="loading-message")
63
+
64
+ # Overall progress bar
65
+ yield AnalysisProgressWidget(
66
+ "Startup Progress",
67
+ total=100.0,
68
+ id="startup-progress"
69
+ )
70
+
71
+ # Status messages
72
+ with Container(classes="status-panel"):
73
+ yield Label("Status", classes="panel-title")
74
+ yield Static("Starting up...", id="status-message")
75
+
76
+ # Loading steps indicators
77
+ with Container(classes="stats-panel"):
78
+ yield Label("Loading Steps", classes="panel-title")
79
+ with Vertical(id="loading-steps"):
80
+ yield Static("⏳ Initializing application...", id="step-init")
81
+ yield Static("⏳ Loading configuration...", id="step-config")
82
+ yield Static("⏳ Preparing analysis engine...", id="step-engine")
83
+ yield Static("⏳ Loading NLP models...", id="step-nlp")
84
+ yield Static("⏳ Finalizing setup...", id="step-finalize")
85
+
86
+ yield Footer()
87
+
88
+ def on_mount(self) -> None:
89
+ """Start loading process when screen mounts."""
90
+ self.loading_task = asyncio.create_task(self._simulate_loading())
91
+
92
+ async def _simulate_loading(self) -> None:
93
+ """
94
+ Simulate the loading process with progress updates.
95
+
96
+ WHY: This provides visual feedback during startup initialization.
97
+ In practice, this would be replaced by actual initialization calls.
98
+ """
99
+ progress_widget = self.query_one("#startup-progress", AnalysisProgressWidget)
100
+ status_message = self.query_one("#status-message", Static)
101
+
102
+ try:
103
+ # Step 1: Initialize application (20%)
104
+ await self._update_step("step-init", "✅ Application initialized", "success")
105
+ status_message.update("Initializing core components...")
106
+ progress_widget.update_progress(20, "Core components ready")
107
+ await asyncio.sleep(0.3)
108
+
109
+ if self.cancelled:
110
+ return
111
+
112
+ # Step 2: Load configuration (40%)
113
+ await self._update_step("step-config", "✅ Configuration loaded", "success")
114
+ status_message.update("Loading configuration files...")
115
+ progress_widget.update_progress(40, "Configuration loaded")
116
+ await asyncio.sleep(0.5)
117
+
118
+ if self.cancelled:
119
+ return
120
+
121
+ # Step 3: Prepare analysis engine (60%)
122
+ await self._update_step("step-engine", "✅ Analysis engine ready", "success")
123
+ status_message.update("Preparing Git analysis engine...")
124
+ progress_widget.update_progress(60, "Analysis engine initialized")
125
+ await asyncio.sleep(0.4)
126
+
127
+ if self.cancelled:
128
+ return
129
+
130
+ # Step 4: Load NLP models (85%) - This is the heavy operation
131
+ await self._update_step("step-nlp", "⏳ Loading spaCy models...", "warning")
132
+ status_message.update("Loading natural language processing models...")
133
+ progress_widget.update_progress(70, "Loading spaCy models...")
134
+ await asyncio.sleep(1.2) # Simulate spaCy model loading time
135
+
136
+ if self.cancelled:
137
+ return
138
+
139
+ await self._update_step("step-nlp", "✅ NLP models loaded", "success")
140
+ progress_widget.update_progress(85, "NLP models ready")
141
+
142
+ # Step 5: Finalize setup (100%)
143
+ await self._update_step("step-finalize", "✅ Setup complete", "success")
144
+ status_message.update("Finalizing application setup...")
145
+ progress_widget.update_progress(100, "GitFlow Analytics ready!")
146
+ await asyncio.sleep(0.3)
147
+
148
+ # Show completion message briefly
149
+ loading_message = self.query_one("#loading-message", Label)
150
+ loading_message.update("Loading complete! Starting application...")
151
+
152
+ elapsed_time = time.time() - self.start_time
153
+ status_message.update(f"Ready! Loaded in {elapsed_time:.1f} seconds")
154
+
155
+ # Wait a moment to show completion
156
+ await asyncio.sleep(0.8)
157
+
158
+ # Signal that loading is complete
159
+ self.app.post_message(self.LoadingComplete())
160
+
161
+ except asyncio.CancelledError:
162
+ progress_widget.update_progress(0, "Loading cancelled")
163
+ status_message.update("Loading cancelled by user")
164
+ except Exception as e:
165
+ progress_widget.update_progress(0, f"Error: {str(e)[:50]}...")
166
+ status_message.update(f"Loading failed: {e}")
167
+ self.app.notify(f"Loading failed: {e}", severity="error")
168
+
169
+ async def _update_step(self, step_id: str, message: str, status: str) -> None:
170
+ """
171
+ Update a loading step with status.
172
+
173
+ @param step_id: ID of the step element to update
174
+ @param message: Status message to display
175
+ @param status: Status type (success, warning, error)
176
+ """
177
+ step_element = self.query_one(f"#{step_id}", Static)
178
+ step_element.update(message)
179
+
180
+ # Apply appropriate styling based on status
181
+ step_element.remove_class("success", "warning", "error")
182
+ step_element.add_class(status)
183
+
184
+ def update_loading_message(self, message: str) -> None:
185
+ """
186
+ Update the main loading message.
187
+
188
+ @param message: New loading message to display
189
+ """
190
+ try:
191
+ loading_message = self.query_one("#loading-message", Label)
192
+ loading_message.update(message)
193
+ except:
194
+ pass # Ignore if element not found
195
+
196
+ def update_progress(self, percentage: float, status: str) -> None:
197
+ """
198
+ Update the overall progress bar.
199
+
200
+ @param percentage: Progress percentage (0-100)
201
+ @param status: Status message to display
202
+ """
203
+ try:
204
+ progress_widget = self.query_one("#startup-progress", AnalysisProgressWidget)
205
+ progress_widget.update_progress(percentage, status)
206
+ except:
207
+ pass # Ignore if element not found
208
+
209
+ def update_status(self, status: str) -> None:
210
+ """
211
+ Update the status message.
212
+
213
+ @param status: Status message to display
214
+ """
215
+ try:
216
+ status_message = self.query_one("#status-message", Static)
217
+ status_message.update(status)
218
+ except:
219
+ pass # Ignore if element not found
220
+
221
+ def action_cancel(self) -> None:
222
+ """Cancel the loading process."""
223
+ self.cancelled = True
224
+ if self.loading_task and not self.loading_task.done():
225
+ self.loading_task.cancel()
226
+ self.app.post_message(self.LoadingCancelled())
227
+
228
+ class LoadingComplete(Message):
229
+ """Message sent when loading is complete."""
230
+ def __init__(self) -> None:
231
+ super().__init__()
232
+
233
+ class LoadingCancelled(Message):
234
+ """Message sent when loading is cancelled."""
235
+ def __init__(self) -> None:
236
+ super().__init__()
237
+
238
+
239
+ class InitializationLoadingScreen(LoadingScreen):
240
+ """
241
+ Specialized loading screen for real application initialization.
242
+
243
+ WHY: This version of the loading screen performs actual initialization
244
+ tasks instead of just simulating them, providing real progress feedback
245
+ during startup.
246
+ """
247
+
248
+ def __init__(
249
+ self,
250
+ config_loader_func=None,
251
+ nlp_init_func=None,
252
+ *args,
253
+ **kwargs
254
+ ) -> None:
255
+ super().__init__(*args, **kwargs)
256
+ self.config_loader_func = config_loader_func
257
+ self.nlp_init_func = nlp_init_func
258
+ self.initialization_data = {}
259
+
260
+ async def _simulate_loading(self) -> None:
261
+ """
262
+ Perform actual initialization tasks with progress updates.
263
+
264
+ WHY: This replaces the simulation with real initialization work,
265
+ allowing the loading screen to show progress of actual operations.
266
+ """
267
+ progress_widget = self.query_one("#startup-progress", AnalysisProgressWidget)
268
+ status_message = self.query_one("#status-message", Static)
269
+
270
+ try:
271
+ # Step 1: Initialize application (20%)
272
+ await self._update_step("step-init", "✅ Application initialized", "success")
273
+ status_message.update("Initializing core components...")
274
+ progress_widget.update_progress(20, "Core components ready")
275
+ await asyncio.sleep(0.1)
276
+
277
+ if self.cancelled:
278
+ return
279
+
280
+ # Step 2: Load configuration (40%)
281
+ status_message.update("Discovering configuration files...")
282
+ await self._update_step("step-config", "⏳ Loading configuration...", "warning")
283
+
284
+ if self.config_loader_func:
285
+ config_result = await asyncio.get_event_loop().run_in_executor(
286
+ None, self.config_loader_func
287
+ )
288
+ self.initialization_data['config'] = config_result
289
+
290
+ await self._update_step("step-config", "✅ Configuration loaded", "success")
291
+ progress_widget.update_progress(40, "Configuration ready")
292
+ await asyncio.sleep(0.1)
293
+
294
+ if self.cancelled:
295
+ return
296
+
297
+ # Step 3: Prepare analysis engine (60%)
298
+ await self._update_step("step-engine", "✅ Analysis engine ready", "success")
299
+ status_message.update("Preparing Git analysis components...")
300
+ progress_widget.update_progress(60, "Analysis engine initialized")
301
+ await asyncio.sleep(0.2)
302
+
303
+ if self.cancelled:
304
+ return
305
+
306
+ # Step 4: Load NLP models (85%) - Heavy operation
307
+ config = self.initialization_data.get('config')
308
+ if config and getattr(config, 'qualitative', None) and config.qualitative.enabled:
309
+ await self._update_step("step-nlp", "⏳ Loading spaCy models...", "warning")
310
+ status_message.update("Loading natural language processing models...")
311
+ progress_widget.update_progress(70, "Loading spaCy models...")
312
+
313
+ if self.nlp_init_func:
314
+ nlp_result = await asyncio.get_event_loop().run_in_executor(
315
+ None, self.nlp_init_func, config
316
+ )
317
+ self.initialization_data['nlp'] = nlp_result
318
+
319
+ await self._update_step("step-nlp", "✅ NLP models loaded", "success")
320
+ progress_widget.update_progress(85, "NLP models ready")
321
+ else:
322
+ await self._update_step("step-nlp", "⏸️ NLP models skipped", "warning")
323
+ progress_widget.update_progress(85, "NLP models skipped (qualitative disabled)")
324
+
325
+ if self.cancelled:
326
+ return
327
+
328
+ # Step 5: Finalize setup (100%)
329
+ await self._update_step("step-finalize", "✅ Setup complete", "success")
330
+ status_message.update("Finalizing application setup...")
331
+ progress_widget.update_progress(100, "GitFlow Analytics ready!")
332
+
333
+ # Show completion message briefly
334
+ loading_message = self.query_one("#loading-message", Label)
335
+ loading_message.update("Initialization complete! Starting application...")
336
+
337
+ elapsed_time = time.time() - self.start_time
338
+ status_message.update(f"Ready! Initialized in {elapsed_time:.1f} seconds")
339
+
340
+ # Wait a moment to show completion
341
+ await asyncio.sleep(0.5)
342
+
343
+ # Signal that loading is complete with initialization data
344
+ self.app.post_message(self.InitializationComplete(self.initialization_data))
345
+
346
+ except asyncio.CancelledError:
347
+ progress_widget.update_progress(0, "Initialization cancelled")
348
+ status_message.update("Initialization cancelled by user")
349
+ except Exception as e:
350
+ progress_widget.update_progress(0, f"Error: {str(e)[:50]}...")
351
+ status_message.update(f"Initialization failed: {e}")
352
+ self.app.notify(f"Initialization failed: {e}", severity="error")
353
+
354
+ class InitializationComplete(Message):
355
+ """Message sent when initialization is complete with data."""
356
+ def __init__(self, data: Dict[str, Any]) -> None:
357
+ super().__init__()
358
+ self.data = data
@@ -0,0 +1,304 @@
1
+ """Main screen for GitFlow Analytics TUI."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from textual.widgets import Header, Footer, Button, Label, Static, Rule
7
+ from textual.containers import Container, Vertical, Horizontal
8
+ from textual.screen import Screen
9
+ from textual.binding import Binding
10
+ from textual.message import Message
11
+
12
+ from ..widgets.export_modal import ExportModal
13
+ from gitflow_analytics.config import Config
14
+
15
+
16
+ class MainScreen(Screen):
17
+ """
18
+ Main dashboard screen showing project information and navigation options.
19
+
20
+ WHY: Serves as the primary entry point for the TUI, providing users with
21
+ an overview of their configuration and clear navigation to all major features.
22
+
23
+ DESIGN DECISION: Uses a dashboard layout rather than a menu-driven approach
24
+ to provide immediate visibility into the current configuration status and
25
+ quick access to common operations.
26
+ """
27
+
28
+ BINDINGS = [
29
+ Binding("ctrl+q", "quit", "Quit"),
30
+ Binding("ctrl+n", "new_analysis", "New Analysis"),
31
+ Binding("ctrl+o", "open_config", "Open Config"),
32
+ Binding("f1", "help", "Help"),
33
+ Binding("c", "cache_status", "Cache Status"),
34
+ Binding("i", "manage_identities", "Identities"),
35
+ ]
36
+
37
+ class NewAnalysisRequested(Message):
38
+ """Message sent when new analysis is requested."""
39
+ def __init__(self) -> None:
40
+ super().__init__()
41
+
42
+ class ConfigurationRequested(Message):
43
+ """Message sent when configuration is requested."""
44
+ def __init__(self) -> None:
45
+ super().__init__()
46
+
47
+ class CacheStatusRequested(Message):
48
+ """Message sent when cache status is requested."""
49
+ def __init__(self) -> None:
50
+ super().__init__()
51
+
52
+ class IdentityManagementRequested(Message):
53
+ """Message sent when identity management is requested."""
54
+ def __init__(self) -> None:
55
+ super().__init__()
56
+
57
+ class HelpRequested(Message):
58
+ """Message sent when help is requested."""
59
+ def __init__(self) -> None:
60
+ super().__init__()
61
+
62
+ def __init__(
63
+ self,
64
+ config: Optional[Config] = None,
65
+ config_path: Optional[Path] = None,
66
+ *,
67
+ name: Optional[str] = None,
68
+ id: Optional[str] = None
69
+ ) -> None:
70
+ super().__init__(name=name, id=id)
71
+ self.config = config
72
+ self.config_path = config_path
73
+
74
+ def compose(self):
75
+ """Compose the main screen."""
76
+ yield Header()
77
+
78
+ with Container(id="main-container"):
79
+ yield Label("GitFlow Analytics", classes="screen-title")
80
+ yield Static("Developer Productivity Analysis Tool", id="subtitle")
81
+
82
+ # Configuration status section
83
+ with Container(classes="status-panel"):
84
+ yield Label("Configuration Status", classes="section-title")
85
+
86
+ if self.config:
87
+ config_status = f"✅ Loaded: {self.config_path}" if self.config_path else "✅ Configuration loaded"
88
+ yield Static(config_status, id="config-status")
89
+
90
+ # Show key configuration details
91
+ details = self._get_config_summary()
92
+ yield Static(details, id="config-details")
93
+ else:
94
+ yield Static("❌ No configuration loaded", id="config-status")
95
+ yield Static("Load or create a configuration to begin analysis", id="config-help")
96
+
97
+ yield Rule()
98
+
99
+ # Main actions section
100
+ with Container(classes="actions-panel"):
101
+ yield Label("Available Actions", classes="section-title")
102
+
103
+ with Vertical(id="action-buttons"):
104
+ if self.config:
105
+ yield Button("🚀 Run Analysis", variant="primary", id="run-analysis")
106
+ yield Button("📊 View Cache Status", id="cache-status")
107
+ yield Button("👥 Manage Developer Identities", id="manage-identities")
108
+ yield Button("⚙️ Edit Configuration", id="edit-config")
109
+ else:
110
+ yield Button("📁 Load Configuration", variant="primary", id="load-config")
111
+ yield Button("➕ Create New Configuration", id="new-config")
112
+
113
+ yield Rule()
114
+ yield Button("❓ Help & Documentation", id="help")
115
+
116
+ # Quick stats section (if config loaded)
117
+ if self.config:
118
+ with Container(classes="stats-panel"):
119
+ yield Label("Quick Information", classes="section-title")
120
+ stats = self._get_quick_stats()
121
+ yield Static(stats, id="quick-stats")
122
+
123
+ yield Footer()
124
+
125
+ def _get_config_summary(self) -> str:
126
+ """
127
+ Generate configuration summary for display.
128
+
129
+ WHY: Provides users with immediate visibility into their current
130
+ configuration without needing to navigate to a separate screen.
131
+ """
132
+ if not self.config:
133
+ return ""
134
+
135
+ lines = []
136
+
137
+ # Repository count
138
+ repo_count = len(self.config.repositories) if self.config.repositories else 0
139
+ if self.config.github.organization and not self.config.repositories:
140
+ lines.append(f"• Organization: {self.config.github.organization} (auto-discovery)")
141
+ else:
142
+ lines.append(f"• Repositories: {repo_count} configured")
143
+
144
+ # GitHub integration
145
+ if self.config.github.token:
146
+ lines.append("✅ GitHub API configured")
147
+ else:
148
+ lines.append("⚠️ GitHub API not configured")
149
+
150
+ # Qualitative analysis
151
+ if hasattr(self.config, 'qualitative') and self.config.qualitative and self.config.qualitative.enabled:
152
+ lines.append("✅ Qualitative analysis enabled")
153
+ else:
154
+ lines.append("⚠️ Qualitative analysis disabled")
155
+
156
+ # JIRA integration
157
+ if hasattr(self.config, 'jira') and self.config.jira and self.config.jira.base_url:
158
+ lines.append("✅ JIRA integration configured")
159
+ else:
160
+ lines.append("⚠️ JIRA integration not configured")
161
+
162
+ # Cache configuration
163
+ lines.append(f"• Cache TTL: {self.config.cache.ttl_hours}h")
164
+
165
+ return "\n".join(lines)
166
+
167
+ def _get_quick_stats(self) -> str:
168
+ """
169
+ Generate quick statistics if cache data is available.
170
+
171
+ WHY: Shows users what data is already available from previous runs,
172
+ helping them understand if they need to run a new analysis.
173
+ """
174
+ try:
175
+ from ....core.cache import GitAnalysisCache
176
+
177
+ cache = GitAnalysisCache(self.config.cache.directory)
178
+ stats = cache.get_cache_stats()
179
+
180
+ lines = []
181
+ lines.append(f"• Cached commits: {stats['cached_commits']:,}")
182
+ lines.append(f"• Cached PRs: {stats['cached_prs']:,}")
183
+ lines.append(f"• Cached issues: {stats['cached_issues']:,}")
184
+
185
+ if stats['stale_commits'] > 0:
186
+ lines.append(f"• Stale entries: {stats['stale_commits']:,}")
187
+
188
+ # Cache size
189
+ import os
190
+ cache_size = 0
191
+ try:
192
+ for root, _dirs, files in os.walk(self.config.cache.directory):
193
+ for f in files:
194
+ cache_size += os.path.getsize(os.path.join(root, f))
195
+ cache_size_mb = cache_size / 1024 / 1024
196
+ lines.append(f"• Cache size: {cache_size_mb:.1f} MB")
197
+ except:
198
+ pass
199
+
200
+ return "\n".join(lines)
201
+
202
+ except Exception:
203
+ return "Cache statistics unavailable"
204
+
205
+ def on_button_pressed(self, event: Button.Pressed) -> None:
206
+ """Handle button press events."""
207
+ button_actions = {
208
+ "run-analysis": self._run_analysis,
209
+ "load-config": self._load_config,
210
+ "new-config": self._new_config,
211
+ "edit-config": self._edit_config,
212
+ "cache-status": self._cache_status,
213
+ "manage-identities": self._manage_identities,
214
+ "help": self._show_help,
215
+ }
216
+
217
+ action = button_actions.get(event.button.id)
218
+ if action:
219
+ action()
220
+
221
+ def _run_analysis(self) -> None:
222
+ """Request new analysis."""
223
+ if not self.config:
224
+ self.notify("Please load or create a configuration first", severity="error")
225
+ return
226
+ self.post_message(self.NewAnalysisRequested())
227
+
228
+ def _load_config(self) -> None:
229
+ """Request configuration loading."""
230
+ self.post_message(self.ConfigurationRequested())
231
+
232
+ def _new_config(self) -> None:
233
+ """Request new configuration creation."""
234
+ self.post_message(self.ConfigurationRequested())
235
+
236
+ def _edit_config(self) -> None:
237
+ """Request configuration editing."""
238
+ self.post_message(self.ConfigurationRequested())
239
+
240
+ def _cache_status(self) -> None:
241
+ """Request cache status display."""
242
+ self.post_message(self.CacheStatusRequested())
243
+
244
+ def _manage_identities(self) -> None:
245
+ """Request identity management."""
246
+ self.post_message(self.IdentityManagementRequested())
247
+
248
+ def _show_help(self) -> None:
249
+ """Request help display."""
250
+ self.post_message(self.HelpRequested())
251
+
252
+ # Action handlers for key bindings
253
+ def action_quit(self) -> None:
254
+ """Quit the application."""
255
+ self.app.exit()
256
+
257
+ def action_new_analysis(self) -> None:
258
+ """Start new analysis via keyboard shortcut."""
259
+ self._run_analysis()
260
+
261
+ def action_open_config(self) -> None:
262
+ """Open configuration via keyboard shortcut."""
263
+ self._load_config()
264
+
265
+ def action_help(self) -> None:
266
+ """Show help via keyboard shortcut."""
267
+ self._show_help()
268
+
269
+ def action_cache_status(self) -> None:
270
+ """Show cache status via keyboard shortcut."""
271
+ self._cache_status()
272
+
273
+ def action_manage_identities(self) -> None:
274
+ """Manage identities via keyboard shortcut."""
275
+ self._manage_identities()
276
+
277
+ def update_config(self, config: Config, config_path: Optional[Path] = None) -> None:
278
+ """
279
+ Update the configuration and refresh the display.
280
+
281
+ WHY: Allows the main screen to be updated when configuration changes
282
+ without requiring a full screen rebuild, maintaining user context.
283
+ """
284
+ self.config = config
285
+ self.config_path = config_path
286
+
287
+ # Update configuration status
288
+ if self.config:
289
+ config_status = f"✅ Loaded: {self.config_path}" if self.config_path else "✅ Configuration loaded"
290
+ self.query_one("#config-status", Static).update(config_status)
291
+
292
+ # Update configuration details
293
+ details = self._get_config_summary()
294
+ self.query_one("#config-details", Static).update(details)
295
+
296
+ # Update quick stats if available
297
+ try:
298
+ stats = self._get_quick_stats()
299
+ self.query_one("#quick-stats", Static).update(stats)
300
+ except:
301
+ pass
302
+
303
+ # Refresh to rebuild buttons with new state
304
+ self.refresh(recompose=True)