titan-cli 0.1.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 (146) hide show
  1. titan_cli/__init__.py +3 -0
  2. titan_cli/__main__.py +4 -0
  3. titan_cli/ai/__init__.py +0 -0
  4. titan_cli/ai/agents/__init__.py +15 -0
  5. titan_cli/ai/agents/base.py +152 -0
  6. titan_cli/ai/client.py +170 -0
  7. titan_cli/ai/constants.py +56 -0
  8. titan_cli/ai/exceptions.py +48 -0
  9. titan_cli/ai/models.py +34 -0
  10. titan_cli/ai/oauth_helper.py +120 -0
  11. titan_cli/ai/providers/__init__.py +9 -0
  12. titan_cli/ai/providers/anthropic.py +117 -0
  13. titan_cli/ai/providers/base.py +75 -0
  14. titan_cli/ai/providers/gemini.py +278 -0
  15. titan_cli/cli.py +59 -0
  16. titan_cli/clients/__init__.py +1 -0
  17. titan_cli/clients/gcloud_client.py +52 -0
  18. titan_cli/core/__init__.py +3 -0
  19. titan_cli/core/config.py +274 -0
  20. titan_cli/core/discovery.py +51 -0
  21. titan_cli/core/errors.py +81 -0
  22. titan_cli/core/models.py +52 -0
  23. titan_cli/core/plugins/available.py +36 -0
  24. titan_cli/core/plugins/models.py +67 -0
  25. titan_cli/core/plugins/plugin_base.py +108 -0
  26. titan_cli/core/plugins/plugin_registry.py +163 -0
  27. titan_cli/core/secrets.py +141 -0
  28. titan_cli/core/workflows/__init__.py +22 -0
  29. titan_cli/core/workflows/models.py +88 -0
  30. titan_cli/core/workflows/project_step_source.py +86 -0
  31. titan_cli/core/workflows/workflow_exceptions.py +17 -0
  32. titan_cli/core/workflows/workflow_filter_service.py +137 -0
  33. titan_cli/core/workflows/workflow_registry.py +419 -0
  34. titan_cli/core/workflows/workflow_sources.py +307 -0
  35. titan_cli/engine/__init__.py +39 -0
  36. titan_cli/engine/builder.py +159 -0
  37. titan_cli/engine/context.py +82 -0
  38. titan_cli/engine/mock_context.py +176 -0
  39. titan_cli/engine/results.py +91 -0
  40. titan_cli/engine/steps/ai_assistant_step.py +185 -0
  41. titan_cli/engine/steps/command_step.py +93 -0
  42. titan_cli/engine/utils/__init__.py +3 -0
  43. titan_cli/engine/utils/venv.py +31 -0
  44. titan_cli/engine/workflow_executor.py +187 -0
  45. titan_cli/external_cli/__init__.py +0 -0
  46. titan_cli/external_cli/configs.py +17 -0
  47. titan_cli/external_cli/launcher.py +65 -0
  48. titan_cli/messages.py +121 -0
  49. titan_cli/ui/tui/__init__.py +205 -0
  50. titan_cli/ui/tui/__previews__/statusbar_preview.py +88 -0
  51. titan_cli/ui/tui/app.py +113 -0
  52. titan_cli/ui/tui/icons.py +70 -0
  53. titan_cli/ui/tui/screens/__init__.py +24 -0
  54. titan_cli/ui/tui/screens/ai_config.py +498 -0
  55. titan_cli/ui/tui/screens/ai_config_wizard.py +882 -0
  56. titan_cli/ui/tui/screens/base.py +110 -0
  57. titan_cli/ui/tui/screens/cli_launcher.py +151 -0
  58. titan_cli/ui/tui/screens/global_setup_wizard.py +363 -0
  59. titan_cli/ui/tui/screens/main_menu.py +162 -0
  60. titan_cli/ui/tui/screens/plugin_config_wizard.py +550 -0
  61. titan_cli/ui/tui/screens/plugin_management.py +377 -0
  62. titan_cli/ui/tui/screens/project_setup_wizard.py +686 -0
  63. titan_cli/ui/tui/screens/workflow_execution.py +592 -0
  64. titan_cli/ui/tui/screens/workflows.py +249 -0
  65. titan_cli/ui/tui/textual_components.py +537 -0
  66. titan_cli/ui/tui/textual_workflow_executor.py +405 -0
  67. titan_cli/ui/tui/theme.py +102 -0
  68. titan_cli/ui/tui/widgets/__init__.py +40 -0
  69. titan_cli/ui/tui/widgets/button.py +108 -0
  70. titan_cli/ui/tui/widgets/header.py +116 -0
  71. titan_cli/ui/tui/widgets/panel.py +81 -0
  72. titan_cli/ui/tui/widgets/status_bar.py +115 -0
  73. titan_cli/ui/tui/widgets/table.py +77 -0
  74. titan_cli/ui/tui/widgets/text.py +177 -0
  75. titan_cli/utils/__init__.py +0 -0
  76. titan_cli/utils/autoupdate.py +155 -0
  77. titan_cli-0.1.0.dist-info/METADATA +149 -0
  78. titan_cli-0.1.0.dist-info/RECORD +146 -0
  79. titan_cli-0.1.0.dist-info/WHEEL +4 -0
  80. titan_cli-0.1.0.dist-info/entry_points.txt +9 -0
  81. titan_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
  82. titan_plugin_git/__init__.py +1 -0
  83. titan_plugin_git/clients/__init__.py +8 -0
  84. titan_plugin_git/clients/git_client.py +772 -0
  85. titan_plugin_git/exceptions.py +40 -0
  86. titan_plugin_git/messages.py +112 -0
  87. titan_plugin_git/models.py +39 -0
  88. titan_plugin_git/plugin.py +118 -0
  89. titan_plugin_git/steps/__init__.py +1 -0
  90. titan_plugin_git/steps/ai_commit_message_step.py +171 -0
  91. titan_plugin_git/steps/branch_steps.py +104 -0
  92. titan_plugin_git/steps/commit_step.py +80 -0
  93. titan_plugin_git/steps/push_step.py +63 -0
  94. titan_plugin_git/steps/status_step.py +59 -0
  95. titan_plugin_git/workflows/__previews__/__init__.py +1 -0
  96. titan_plugin_git/workflows/__previews__/commit_ai_preview.py +124 -0
  97. titan_plugin_git/workflows/commit-ai.yaml +28 -0
  98. titan_plugin_github/__init__.py +11 -0
  99. titan_plugin_github/agents/__init__.py +6 -0
  100. titan_plugin_github/agents/config_loader.py +130 -0
  101. titan_plugin_github/agents/issue_generator.py +353 -0
  102. titan_plugin_github/agents/pr_agent.py +528 -0
  103. titan_plugin_github/clients/__init__.py +8 -0
  104. titan_plugin_github/clients/github_client.py +1105 -0
  105. titan_plugin_github/config/__init__.py +0 -0
  106. titan_plugin_github/config/pr_agent.toml +85 -0
  107. titan_plugin_github/exceptions.py +28 -0
  108. titan_plugin_github/messages.py +88 -0
  109. titan_plugin_github/models.py +330 -0
  110. titan_plugin_github/plugin.py +131 -0
  111. titan_plugin_github/steps/__init__.py +12 -0
  112. titan_plugin_github/steps/ai_pr_step.py +172 -0
  113. titan_plugin_github/steps/create_pr_step.py +86 -0
  114. titan_plugin_github/steps/github_prompt_steps.py +171 -0
  115. titan_plugin_github/steps/issue_steps.py +143 -0
  116. titan_plugin_github/steps/preview_step.py +40 -0
  117. titan_plugin_github/utils.py +82 -0
  118. titan_plugin_github/workflows/__previews__/__init__.py +1 -0
  119. titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +140 -0
  120. titan_plugin_github/workflows/create-issue-ai.yaml +32 -0
  121. titan_plugin_github/workflows/create-pr-ai.yaml +49 -0
  122. titan_plugin_jira/__init__.py +8 -0
  123. titan_plugin_jira/agents/__init__.py +6 -0
  124. titan_plugin_jira/agents/config_loader.py +154 -0
  125. titan_plugin_jira/agents/jira_agent.py +553 -0
  126. titan_plugin_jira/agents/prompts.py +364 -0
  127. titan_plugin_jira/agents/response_parser.py +435 -0
  128. titan_plugin_jira/agents/token_tracker.py +223 -0
  129. titan_plugin_jira/agents/validators.py +246 -0
  130. titan_plugin_jira/clients/jira_client.py +745 -0
  131. titan_plugin_jira/config/jira_agent.toml +92 -0
  132. titan_plugin_jira/config/templates/issue_analysis.md.j2 +78 -0
  133. titan_plugin_jira/exceptions.py +37 -0
  134. titan_plugin_jira/formatters/__init__.py +6 -0
  135. titan_plugin_jira/formatters/markdown_formatter.py +245 -0
  136. titan_plugin_jira/messages.py +115 -0
  137. titan_plugin_jira/models.py +89 -0
  138. titan_plugin_jira/plugin.py +264 -0
  139. titan_plugin_jira/steps/ai_analyze_issue_step.py +105 -0
  140. titan_plugin_jira/steps/get_issue_step.py +82 -0
  141. titan_plugin_jira/steps/prompt_select_issue_step.py +80 -0
  142. titan_plugin_jira/steps/search_saved_query_step.py +238 -0
  143. titan_plugin_jira/utils/__init__.py +13 -0
  144. titan_plugin_jira/utils/issue_sorter.py +140 -0
  145. titan_plugin_jira/utils/saved_queries.py +150 -0
  146. titan_plugin_jira/workflows/analyze-jira-issues.yaml +34 -0
@@ -0,0 +1,592 @@
1
+ """
2
+ Workflow Execution Screen
3
+
4
+ Screen for executing workflows and displaying progress in real-time.
5
+ """
6
+ import os
7
+ from typing import Any, List, Optional, Dict
8
+
9
+ from textual.app import ComposeResult
10
+ from textual.widget import Widget
11
+ from textual.widgets import Static
12
+ from textual.containers import Container, VerticalScroll
13
+ from textual.worker import Worker, WorkerState
14
+
15
+ from titan_cli.ui.tui.widgets import HeaderWidget
16
+ from titan_cli.ui.tui.icons import Icons
17
+
18
+ from titan_cli.core.secrets import SecretManager
19
+ from titan_cli.core.workflows import ParsedWorkflow
20
+ from titan_cli.engine.builder import WorkflowContextBuilder
21
+ from titan_cli.core.workflows.workflow_exceptions import (
22
+ WorkflowNotFoundError,
23
+ WorkflowExecutionError,
24
+ )
25
+ from titan_cli.ui.tui.textual_workflow_executor import TextualWorkflowExecutor
26
+ from titan_cli.ui.tui.widgets.text import DimText
27
+ from .base import BaseScreen
28
+
29
+ from textual.containers import Horizontal
30
+
31
+ class WorkflowExecutionScreen(BaseScreen):
32
+ """
33
+ Screen for executing a workflow with real-time progress display.
34
+
35
+ The internal structure (progress tracking, output handling, etc.)
36
+ will be implemented separately.
37
+ """
38
+
39
+ BINDINGS = [
40
+ ("escape", "cancel_execution", "Cancel"),
41
+ ("q", "cancel_execution", "Cancel"),
42
+ ]
43
+
44
+ CSS = """
45
+ WorkflowExecutionScreen {
46
+ align: center middle;
47
+ }
48
+
49
+ #workflow-description {
50
+ width: 100%;
51
+ text-align: center;
52
+ padding-bottom: 1;
53
+ }
54
+
55
+ #execution-container {
56
+ width: 100%;
57
+ height: 1fr;
58
+ background: $surface-lighten-1;
59
+ padding: 0 2 1 2;
60
+ }
61
+
62
+ #steps-panel {
63
+ width: 20%;
64
+ height: 100%;
65
+ border: round $primary;
66
+ border-title-align: center;
67
+ background: $surface-lighten-1;
68
+ padding: 0;
69
+ }
70
+
71
+ #steps-content {
72
+ padding: 1;
73
+ }
74
+
75
+ #workflow-execution-panel {
76
+ width: 80%;
77
+ height: 100%;
78
+ border: round $primary;
79
+ border-title-align: center;
80
+ background: $surface-lighten-1;
81
+ padding: 0;
82
+ }
83
+
84
+ #execution-content {
85
+ padding: 1;
86
+ }
87
+ """
88
+
89
+ def __init__(self, config, workflow_name: str, **kwargs):
90
+ super().__init__(
91
+ config,
92
+ title=f"{Icons.WORKFLOW} Executing: {workflow_name}",
93
+ show_back=True,
94
+ **kwargs
95
+ )
96
+ self.workflow_name = workflow_name
97
+ self.workflow: Optional[ParsedWorkflow] = None
98
+ self._worker: Optional[Worker] = None
99
+ self._original_cwd = os.getcwd()
100
+ self._should_auto_back = False # Flag to trigger auto-back when worker finishes
101
+
102
+ def compose_content(self) -> ComposeResult:
103
+ """Compose the workflow execution screen."""
104
+ with Container(id="execution-container"):
105
+ yield DimText(id="workflow-description")
106
+ with Horizontal():
107
+ left_panel = VerticalScroll(id="steps-panel")
108
+ left_panel.border_title = "Steps"
109
+ with left_panel:
110
+ yield StepsContent(id="steps-content")
111
+
112
+ right_panel = VerticalScroll(id="workflow-execution-panel")
113
+ right_panel.border_title = "Workflow Execution"
114
+ with right_panel:
115
+ yield WorkflowExecutionContent(id="execution-content")
116
+
117
+ def on_mount(self) -> None:
118
+ """Start workflow execution when screen is mounted."""
119
+ self._load_and_execute_workflow()
120
+
121
+ def _load_and_execute_workflow(self) -> None:
122
+ """Load and execute the workflow."""
123
+ try:
124
+ # Load workflow
125
+ self.workflow = self.config.workflows.get_workflow(self.workflow_name)
126
+ # if not self.workflow:
127
+ # TODO Create empty error screen
128
+ # self._update_workflow_info(
129
+ # f"[red]Error: Workflow '{self.workflow_name}' not found[/red]"
130
+ # )
131
+ # return
132
+
133
+ self._update_header_title(f"{Icons.WORKFLOW} {self.workflow.name}")
134
+ self._update_description(self.workflow.description or "")
135
+
136
+ # Create step widgets for non-hook steps
137
+ steps_widget = self.query_one("#steps-content", StepsContent)
138
+ steps_widget.set_steps(self.workflow.steps)
139
+
140
+ self._output("Preparing to execute workflow...")
141
+
142
+ # Execute workflow in background thread (not async worker)
143
+ self._worker = self.run_worker(
144
+ self._execute_workflow,
145
+ name="workflow_executor",
146
+ thread=True
147
+ )
148
+
149
+ except (WorkflowNotFoundError, WorkflowExecutionError):
150
+ pass
151
+ # TODO Create empty error screen
152
+ # self._update_workflow_info(f"[red]Error: {e}[/red]")
153
+ except Exception:
154
+ pass
155
+ # TODO Create empty error screen
156
+ # self._update_workflow_info(
157
+ # f"[red]Unexpected error: {type(e).__name__} - {e}[/red]"
158
+ # )
159
+
160
+ def _execute_workflow(self) -> None:
161
+ """Execute the workflow in a background thread."""
162
+ try:
163
+ # We're already in the project directory (current working directory)
164
+ # No need to change directory
165
+
166
+ # Create secret manager for current project
167
+ from pathlib import Path
168
+ secrets = SecretManager(project_path=Path.cwd())
169
+
170
+ # Build workflow context (without UI - executor handles messaging)
171
+ ctx_builder = WorkflowContextBuilder(
172
+ plugin_registry=self.config.registry,
173
+ secrets=secrets,
174
+ ai_config=self.config.config.ai,
175
+ )
176
+
177
+ # Add AI if configured
178
+ ctx_builder.with_ai()
179
+
180
+ # Add registered plugins to context
181
+ for plugin_name in self.config.registry.list_installed():
182
+ plugin = self.config.registry.get_plugin(plugin_name)
183
+ if plugin and hasattr(ctx_builder, f"with_{plugin_name}"):
184
+ try:
185
+ client = plugin.get_client()
186
+ getattr(ctx_builder, f"with_{plugin_name}")(client)
187
+ except Exception:
188
+ # Plugin client initialization failed - workflow steps
189
+ # using this plugin will fail gracefully
190
+ pass
191
+
192
+ # Build context and create executor
193
+ execution_context = ctx_builder.build()
194
+ executor = TextualWorkflowExecutor(
195
+ plugin_registry=self.config.registry,
196
+ workflow_registry=self.config.workflows,
197
+ message_target=self # Pass self to receive messages
198
+ )
199
+
200
+ # Execute workflow (this is synchronous and may take time)
201
+ executor.execute(self.workflow, execution_context)
202
+
203
+ except (WorkflowNotFoundError, WorkflowExecutionError) as e:
204
+ self._output(f"\n[red]{Icons.ERROR} Workflow failed: {e}[/red]")
205
+ self._output("[dim]Press ESC or Q to return[/dim]")
206
+ except Exception as e:
207
+ self._output(f"\n[red]{Icons.ERROR} Unexpected error: {type(e).__name__}: {e}[/red]")
208
+ self._output("[dim]Press ESC or Q to return[/dim]")
209
+ finally:
210
+ # Restore original working directory
211
+ os.chdir(self._original_cwd)
212
+
213
+ def _update_header_title(self, title: str) -> None:
214
+ """Update the header title."""
215
+ try:
216
+ header = self.query_one(HeaderWidget)
217
+ header.title = title
218
+ except Exception:
219
+ pass
220
+
221
+ def _update_description(self, description: str) -> None:
222
+ """Update the workflow description."""
223
+ try:
224
+ header = self.query_one("#workflow-description", DimText)
225
+ header.update(description)
226
+ except Exception:
227
+ pass
228
+
229
+ def _output(self, text: str) -> None:
230
+ """Helper to append output to execution widget."""
231
+ try:
232
+ execution_widget = self.query_one("#execution-content", WorkflowExecutionContent)
233
+ execution_widget.append_output(text)
234
+ except Exception:
235
+ pass
236
+
237
+ # Generic message handler for TextualWorkflowExecutor events
238
+ def on_textual_workflow_executor_workflow_started(
239
+ self, message: TextualWorkflowExecutor.WorkflowStarted
240
+ ) -> None:
241
+ """Handle workflow started event."""
242
+ self._handle_workflow_event(message)
243
+
244
+ def on_textual_workflow_executor_step_started(
245
+ self, message: TextualWorkflowExecutor.StepStarted
246
+ ) -> None:
247
+ """Handle step started event."""
248
+ self._handle_workflow_event(message)
249
+
250
+ def on_textual_workflow_executor_step_completed(
251
+ self, message: TextualWorkflowExecutor.StepCompleted
252
+ ) -> None:
253
+ """Handle step completed event."""
254
+ self._handle_workflow_event(message)
255
+
256
+ def on_textual_workflow_executor_step_failed(
257
+ self, message: TextualWorkflowExecutor.StepFailed
258
+ ) -> None:
259
+ """Handle step failed event."""
260
+ self._handle_workflow_event(message)
261
+
262
+ def on_textual_workflow_executor_step_skipped(
263
+ self, message: TextualWorkflowExecutor.StepSkipped
264
+ ) -> None:
265
+ """Handle step skipped event."""
266
+ self._handle_workflow_event(message)
267
+
268
+ def on_textual_workflow_executor_workflow_completed(
269
+ self, message: TextualWorkflowExecutor.WorkflowCompleted
270
+ ) -> None:
271
+ """Handle workflow completed event."""
272
+ self._handle_workflow_event(message)
273
+
274
+ def on_textual_workflow_executor_workflow_failed(
275
+ self, message: TextualWorkflowExecutor.WorkflowFailed
276
+ ) -> None:
277
+ """Handle workflow failed event."""
278
+ self._handle_workflow_event(message)
279
+
280
+ def _handle_workflow_event(self, message) -> None:
281
+ """Generic handler that delegates to widgets."""
282
+ try:
283
+ from titan_cli.ui.tui.textual_workflow_executor import TextualWorkflowExecutor
284
+
285
+ # Update steps widget for step-related and workflow events
286
+ if hasattr(message, 'step_id') or isinstance(message, (TextualWorkflowExecutor.WorkflowStarted, TextualWorkflowExecutor.WorkflowCompleted)):
287
+ steps_widget = self.query_one("#steps-content", StepsContent)
288
+ steps_widget.handle_event(message)
289
+
290
+ # Update execution widget for output
291
+ execution_widget = self.query_one("#execution-content", WorkflowExecutionContent)
292
+ execution_widget.handle_event(message)
293
+ except Exception:
294
+ pass
295
+
296
+ def _schedule_auto_back(self) -> None:
297
+ """Schedule auto-back - will poll worker state until it finishes."""
298
+ # import time
299
+ # with open("/tmp/titan_debug.log", "a") as f:
300
+ # f.write(f"[{time.time():.3f}] SCREEN: Auto-back scheduled, starting polling\n")
301
+ self._should_auto_back = True
302
+ # Start polling worker state
303
+ self._poll_worker_and_pop()
304
+
305
+ def _poll_worker_and_pop(self) -> None:
306
+ """Poll worker state and pop screen when finished."""
307
+ # Check if worker is still running
308
+ if self._worker and self._worker.state == WorkerState.RUNNING:
309
+ # with open("/tmp/titan_debug.log", "a") as f:
310
+ # f.write(f"[{time.time():.3f}] SCREEN: Worker still running, will check again in 0.1s\n")
311
+ # Worker still running, check again in 100ms
312
+ self.set_timer(0.1, self._poll_worker_and_pop)
313
+ else:
314
+ # Worker finished, safe to pop
315
+ # with open("/tmp/titan_debug.log", "a") as f:
316
+ # f.write(f"[{time.time():.3f}] SCREEN: Worker finished, popping screen now\n")
317
+ self.app.pop_screen()
318
+
319
+ def action_cancel_execution(self) -> None:
320
+ """Cancel workflow execution and go back."""
321
+ # Cancel worker if running
322
+ if self._worker and self._worker.state == WorkerState.RUNNING:
323
+ # Try to cancel, but don't wait for it to finish
324
+ # The worker thread may be blocked, so we just move on
325
+ try:
326
+ self._worker.cancel()
327
+ except Exception:
328
+ pass
329
+
330
+ # Restore working directory
331
+ try:
332
+ os.chdir(self._original_cwd)
333
+ except Exception:
334
+ pass
335
+
336
+ # Pop screen immediately without waiting for worker
337
+ self.app.pop_screen()
338
+
339
+ class StepsContent(Widget):
340
+ """Widget to display workflow steps and their statuses."""
341
+
342
+ DEFAULT_CSS = """
343
+ StepsContent {
344
+ width: 100%;
345
+ height: auto;
346
+ layout: vertical;
347
+ }
348
+
349
+ .step-widget {
350
+ width: 100%;
351
+ height: auto;
352
+ padding: 0 1 1 1;
353
+ }
354
+ """
355
+
356
+ def __init__(self, **kwargs):
357
+ super().__init__(**kwargs)
358
+ self.steps: List[Dict[str, Any]] = []
359
+ self._step_widgets: Dict[str, Static] = {}
360
+ self._workflow_stack: List[Dict[str, Any]] = [] # Stack to track nested workflows
361
+
362
+ def set_steps(self, steps: List[Dict[str, Any]]) -> None:
363
+ """Set the steps to display."""
364
+ self.steps = steps
365
+
366
+ for idx, step_data in enumerate(steps):
367
+ if step_data.get("hook"):
368
+ continue
369
+
370
+ step_id = step_data.get("id") or f"step_{idx}"
371
+ step_name = step_data.get("name") or step_id
372
+
373
+ step_widget = Static(f"{Icons.PENDING} {step_name}", classes="step-widget")
374
+ self._step_widgets[step_id] = step_widget
375
+ self.mount(step_widget)
376
+
377
+ def update_step(self, step_id: str, text: str) -> None:
378
+ """Update a specific step's display."""
379
+ if step_id in self._step_widgets:
380
+ self._step_widgets[step_id].update(text)
381
+
382
+ def set_step_running(self, step_id: str, step_name: str) -> None:
383
+ """Mark a step as running."""
384
+ self.update_step(step_id, f"{Icons.RUNNING} [cyan]{step_name}[/cyan]")
385
+
386
+ def set_step_success(self, step_id: str, step_name: str) -> None:
387
+ """Mark a step as successful."""
388
+ self.update_step(step_id, f"{Icons.SUCCESS} [green]{step_name}[/green]")
389
+
390
+ def set_step_failed(self, step_id: str, step_name: str) -> None:
391
+ """Mark a step as failed."""
392
+ self.update_step(step_id, f"{Icons.ERROR} [red]{step_name}[/red]")
393
+
394
+ def set_step_skipped(self, step_id: str, step_name: str) -> None:
395
+ """Mark a step as skipped."""
396
+ self.update_step(step_id, f"{Icons.SKIPPED} [yellow]{step_name}[/yellow]")
397
+
398
+ def handle_event(self, message) -> None:
399
+ """Handle workflow events generically."""
400
+ from titan_cli.ui.tui.textual_workflow_executor import TextualWorkflowExecutor
401
+
402
+ if isinstance(message, TextualWorkflowExecutor.WorkflowStarted):
403
+ # If it's a nested workflow, save current state and show nested steps
404
+ if message.is_nested:
405
+ # Save current state
406
+ self._workflow_stack.append({
407
+ 'steps': self.steps,
408
+ 'widgets': self._step_widgets.copy()
409
+ })
410
+ # Clear current widgets
411
+ for widget in self._step_widgets.values():
412
+ widget.remove()
413
+ self._step_widgets.clear()
414
+ # Set nested workflow steps
415
+ self.set_steps(message.steps)
416
+
417
+ elif isinstance(message, TextualWorkflowExecutor.WorkflowCompleted):
418
+ # If nested workflow completed, restore parent workflow steps
419
+ if message.is_nested and self._workflow_stack:
420
+ # Clear nested widgets
421
+ for widget in self._step_widgets.values():
422
+ widget.remove()
423
+ self._step_widgets.clear()
424
+
425
+ # Restore parent workflow state
426
+ parent_state = self._workflow_stack.pop()
427
+ self.steps = parent_state['steps']
428
+ self._step_widgets = parent_state['widgets']
429
+
430
+ # Re-mount parent widgets
431
+ for widget in self._step_widgets.values():
432
+ self.mount(widget)
433
+
434
+ elif isinstance(message, TextualWorkflowExecutor.StepStarted):
435
+ self.update_step(message.step_id, f"{Icons.RUNNING} [cyan]{message.step_name}[/cyan]")
436
+ elif isinstance(message, TextualWorkflowExecutor.StepCompleted):
437
+ self.update_step(message.step_id, f"{Icons.SUCCESS} [green]{message.step_name}[/green]")
438
+ elif isinstance(message, TextualWorkflowExecutor.StepFailed):
439
+ self.update_step(message.step_id, f"{Icons.ERROR} [red]{message.step_name}[/red]")
440
+ elif isinstance(message, TextualWorkflowExecutor.StepSkipped):
441
+ self.update_step(message.step_id, f"{Icons.SKIPPED} [yellow]{message.step_name}[/yellow]")
442
+
443
+
444
+ class WorkflowExecutionContent(Widget):
445
+ """Widget to display workflow execution output."""
446
+
447
+ # Allow children to receive focus (for input widgets)
448
+ can_focus_children = True
449
+
450
+ DEFAULT_CSS = """
451
+ WorkflowExecutionContent {
452
+ width: 100%;
453
+ height: auto;
454
+ layout: vertical;
455
+ }
456
+
457
+ WorkflowExecutionContent > Static {
458
+ width: 100%;
459
+ height: auto;
460
+ }
461
+ """
462
+
463
+ def __init__(self, **kwargs):
464
+ super().__init__(**kwargs)
465
+ self._workflow_depth = 0 # Track nested workflow depth
466
+
467
+ def compose(self) -> ComposeResult:
468
+ """Compose the execution content."""
469
+ # Don't yield anything - content will be mounted dynamically
470
+ return
471
+ yield # Make this a generator
472
+
473
+ def append_output(self, text: str) -> None:
474
+ """Append text to the output."""
475
+ # Mount each line as a separate Static widget to preserve order
476
+ try:
477
+ text_widget = Static(text)
478
+ self.mount(text_widget)
479
+ # Auto-scroll to show new content
480
+ self._scroll_to_end()
481
+ except Exception:
482
+ pass
483
+
484
+ def _scroll_to_end(self) -> None:
485
+ """Scroll the parent container to show the end."""
486
+ try:
487
+ # Get the parent VerticalScroll container
488
+ parent = self.parent
489
+ if parent and hasattr(parent, 'scroll_end'):
490
+ parent.scroll_end(animate=False)
491
+ except Exception:
492
+ pass
493
+
494
+ def on_descendant_mount(self, event) -> None:
495
+ """Auto-scroll when any widget is mounted as a descendant."""
496
+ # Don't auto-scroll if we're mounting a PromptInput (it will handle its own scroll)
497
+ from titan_cli.ui.tui.textual_components import PromptInput
498
+
499
+ # Skip scroll only if the widget itself is a PromptInput
500
+ # (not if it's a child of PromptInput, to avoid blocking scroll after PromptInput is removed)
501
+ if not isinstance(event.widget, PromptInput):
502
+ self._scroll_to_end()
503
+
504
+ def handle_event(self, message) -> None:
505
+ """Handle workflow events generically."""
506
+ from titan_cli.ui.tui.textual_workflow_executor import TextualWorkflowExecutor
507
+ from titan_cli.ui.tui.widgets import Panel
508
+
509
+ if isinstance(message, TextualWorkflowExecutor.WorkflowStarted):
510
+ # Track nested workflow depth
511
+ if message.is_nested:
512
+ self._workflow_depth += 1
513
+ self.append_output(f"\n[bold cyan]🚀 Starting workflow: {message.workflow_name}[/bold cyan]")
514
+
515
+ elif isinstance(message, TextualWorkflowExecutor.StepStarted):
516
+ # Format differently for nested workflows
517
+ if self._workflow_depth > 0:
518
+ # Nested workflow: show with indentation, no step number
519
+ indent = " " * self._workflow_depth
520
+ self.append_output(f"[cyan]{indent}→ Step {message.step_index}: {message.step_name}[/cyan]")
521
+ else:
522
+ # Top-level workflow: show with step number
523
+ self.append_output(f"[cyan]→ Step {message.step_index}: {message.step_name}[/cyan]")
524
+
525
+ elif isinstance(message, TextualWorkflowExecutor.StepCompleted):
526
+ # Apply indentation for nested workflows
527
+ if self._workflow_depth > 0:
528
+ indent = " " * self._workflow_depth
529
+ self.append_output(f"[green]{indent}{Icons.SUCCESS} Completed: {message.step_name}[/green]\n")
530
+ else:
531
+ self.append_output(f"[green]{Icons.SUCCESS} Completed: {message.step_name}[/green]\n")
532
+
533
+ elif isinstance(message, TextualWorkflowExecutor.StepFailed):
534
+ # Mount error panel
535
+ try:
536
+ self.mount(Panel(f"Failed: {message.step_name} - {message.error_message}", panel_type="error"))
537
+ self._scroll_to_end()
538
+ except Exception:
539
+ pass
540
+
541
+ if message.on_error == "continue":
542
+ indent = " " * self._workflow_depth if self._workflow_depth > 0 else ""
543
+ self.append_output(f"[yellow]{indent} {Icons.WARNING} Continuing despite error[/yellow]\n")
544
+ else:
545
+ self.append_output("")
546
+
547
+ elif isinstance(message, TextualWorkflowExecutor.StepSkipped):
548
+ # Mount warning panel for skipped steps
549
+ try:
550
+ self.mount(Panel(f"Skipped: {message.step_name}", panel_type="warning"))
551
+ self._scroll_to_end()
552
+ except Exception:
553
+ pass
554
+
555
+ elif isinstance(message, TextualWorkflowExecutor.WorkflowCompleted):
556
+ # Track nested workflow depth
557
+ if message.is_nested and self._workflow_depth > 0:
558
+ self._workflow_depth -= 1
559
+
560
+ # DEBUG: Log receipt
561
+ # with open("/tmp/titan_debug.log", "a") as f:
562
+ # f.write(f"[{time.time():.3f}] SCREEN: Received WorkflowCompleted, is_nested={message.is_nested}\n")
563
+
564
+ # Show success toast instead of inline message
565
+ try:
566
+ # with open("/tmp/titan_debug.log", "a") as f:
567
+ # f.write(f"[{time.time():.3f}] SCREEN: About to call notify\n")
568
+ self.app.notify(f"✨ Workflow completed: {message.workflow_name}", severity="information", timeout=5)
569
+ # with open("/tmp/titan_debug.log", "a") as f:
570
+ # f.write(f"[{time.time():.3f}] SCREEN: notify called successfully\n")
571
+ except Exception:
572
+ # with open("/tmp/titan_debug.log", "a") as f:
573
+ # f.write(f"[{time.time():.3f}] SCREEN: notify failed: {e}\n")
574
+ # Fallback if notify fails
575
+ self.append_output(f"\n[bold green]✨ Workflow completed: {message.workflow_name}[/bold green]")
576
+
577
+ # Schedule auto-back after a short delay (only if not nested)
578
+ if not message.is_nested:
579
+ # with open("/tmp/titan_debug.log", "a") as f:
580
+ # f.write(f"[{time.time():.3f}] SCREEN: Setting timer for auto-back flag\n")
581
+ # Don't pop immediately - wait for worker to finish, then pop
582
+ self.set_timer(3.0, self._schedule_auto_back)
583
+ else:
584
+ # with open("/tmp/titan_debug.log", "a") as f:
585
+ # f.write(f"[{time.time():.3f}] SCREEN: Workflow is nested, skipping auto-back\n")
586
+ pass
587
+
588
+ elif isinstance(message, TextualWorkflowExecutor.WorkflowFailed):
589
+ # Show error toast for workflow failure
590
+ self.app.notify(f"❌ Workflow failed at step: {message.step_name}", severity="error", timeout=10)
591
+ self.append_output(f"[red]{message.error_message}[/red]")
592
+