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