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,405 @@
1
+ """
2
+ Textual Workflow Executor
3
+
4
+ Workflow executor specifically designed for Textual TUI.
5
+ Emits Textual messages instead of using Rich UI components.
6
+ """
7
+ from typing import Any, Dict, Optional
8
+
9
+ from textual.message import Message
10
+
11
+ from titan_cli.core.workflows import ParsedWorkflow
12
+ from titan_cli.core.workflows.workflow_exceptions import WorkflowExecutionError
13
+ from titan_cli.core.workflows.workflow_registry import WorkflowRegistry
14
+ from titan_cli.core.plugins.plugin_registry import PluginRegistry
15
+ from titan_cli.core.workflows.models import WorkflowStepModel
16
+ from titan_cli.engine.context import WorkflowContext
17
+ from titan_cli.engine.results import WorkflowResult, Success, Error, is_error, is_skip
18
+ from titan_cli.engine.steps.command_step import execute_command_step as execute_external_command_step
19
+ from titan_cli.engine.steps.ai_assistant_step import execute_ai_assistant_step
20
+
21
+
22
+ class TextualWorkflowExecutor:
23
+ """
24
+ Workflow executor for Textual TUI.
25
+
26
+ Instead of using ctx.ui (Rich components), this executor emits
27
+ Textual messages that the screen can listen to and display.
28
+ """
29
+
30
+ # Core steps available to all workflows
31
+ CORE_STEPS = {
32
+ "ai_code_assistant": execute_ai_assistant_step,
33
+ }
34
+
35
+ # Message classes for communication with the screen
36
+ class WorkflowStarted(Message):
37
+ """Emitted when workflow execution starts."""
38
+ def __init__(self, workflow_name: str, description: Optional[str], source: Optional[str], total_steps: int, steps: list = None, is_nested: bool = False) -> None:
39
+ super().__init__()
40
+ self.workflow_name = workflow_name
41
+ self.description = description
42
+ self.source = source
43
+ self.total_steps = total_steps
44
+ self.steps = steps or []
45
+ self.is_nested = is_nested
46
+
47
+ class WorkflowCompleted(Message):
48
+ """Emitted when workflow completes successfully."""
49
+ def __init__(self, workflow_name: str, message: str, is_nested: bool = False) -> None:
50
+ super().__init__()
51
+ self.workflow_name = workflow_name
52
+ self.message = message
53
+ self.is_nested = is_nested
54
+
55
+ class WorkflowFailed(Message):
56
+ """Emitted when workflow fails."""
57
+ def __init__(self, workflow_name: str, step_name: str, error_message: str) -> None:
58
+ super().__init__()
59
+ self.workflow_name = workflow_name
60
+ self.step_name = step_name
61
+ self.error_message = error_message
62
+
63
+ class StepStarted(Message):
64
+ """Emitted when a step starts executing."""
65
+ def __init__(self, step_index: int, step_id: str, step_name: str) -> None:
66
+ super().__init__()
67
+ self.step_index = step_index
68
+ self.step_id = step_id
69
+ self.step_name = step_name
70
+
71
+ class StepCompleted(Message):
72
+ """Emitted when a step completes successfully."""
73
+ def __init__(self, step_index: int, step_id: str, step_name: str) -> None:
74
+ super().__init__()
75
+ self.step_index = step_index
76
+ self.step_id = step_id
77
+ self.step_name = step_name
78
+
79
+ class StepFailed(Message):
80
+ """Emitted when a step fails."""
81
+ def __init__(self, step_index: int, step_id: str, step_name: str, error_message: str, on_error: str) -> None:
82
+ super().__init__()
83
+ self.step_index = step_index
84
+ self.step_id = step_id
85
+ self.step_name = step_name
86
+ self.error_message = error_message
87
+ self.on_error = on_error
88
+
89
+ class StepSkipped(Message):
90
+ """Emitted when a step is skipped."""
91
+ def __init__(self, step_index: int, step_id: str, step_name: str) -> None:
92
+ super().__init__()
93
+ self.step_index = step_index
94
+ self.step_id = step_id
95
+ self.step_name = step_name
96
+
97
+ class StepOutput(Message):
98
+ """Emitted when a step produces output."""
99
+ def __init__(self, step_index: int, step_id: str, output: str) -> None:
100
+ super().__init__()
101
+ self.step_index = step_index
102
+ self.step_id = step_id
103
+ self.output = output
104
+
105
+ def __init__(
106
+ self,
107
+ plugin_registry: PluginRegistry,
108
+ workflow_registry: WorkflowRegistry,
109
+ message_target: Any = None
110
+ ):
111
+ """
112
+ Initialize the Textual workflow executor.
113
+
114
+ Args:
115
+ plugin_registry: Plugin registry for resolving plugins
116
+ workflow_registry: Workflow registry for resolving workflows
117
+ message_target: Target to post messages to (typically a Textual Widget/Screen)
118
+ """
119
+ self._plugin_registry = plugin_registry
120
+ self._workflow_registry = workflow_registry
121
+ self._message_target = message_target
122
+
123
+ def _post_message(self, message: Message) -> None:
124
+ """Post a message to the target if available."""
125
+ if self._message_target and hasattr(self._message_target, 'post_message'):
126
+ self._message_target.post_message(message)
127
+
128
+ def _post_message_sync(self, message: Message) -> None:
129
+ """Post a message synchronously (blocks until processed)."""
130
+ if self._message_target and hasattr(self._message_target, 'post_message'):
131
+ def _post():
132
+ self._message_target.post_message(message)
133
+
134
+ # Use call_from_thread to block until message is posted
135
+ if hasattr(self._message_target, 'app'):
136
+ try:
137
+ self._message_target.app.call_from_thread(_post)
138
+ except Exception:
139
+ # App is closing or worker was cancelled, fail silently
140
+ pass
141
+ else:
142
+ # Fallback to async post if no app available
143
+ self._message_target.post_message(message)
144
+
145
+ def execute(
146
+ self,
147
+ workflow: ParsedWorkflow,
148
+ ctx: WorkflowContext,
149
+ params_override: Optional[Dict[str, Any]] = None
150
+ ) -> WorkflowResult:
151
+ """
152
+ Execute the given ParsedWorkflow.
153
+
154
+ Args:
155
+ workflow: The workflow to execute
156
+ ctx: Workflow context
157
+ params_override: Optional parameter overrides
158
+
159
+ Returns:
160
+ WorkflowResult indicating success or failure
161
+ """
162
+ # Inject Textual components into context if message_target is available
163
+ if self._message_target and hasattr(self._message_target, 'app'):
164
+ try:
165
+ from titan_cli.ui.tui.textual_components import TextualComponents
166
+ from titan_cli.ui.tui.screens.workflow_execution import WorkflowExecutionContent
167
+
168
+ app = self._message_target.app
169
+ output_widget = self._message_target.query_one("#execution-content", WorkflowExecutionContent)
170
+
171
+ ctx.textual = TextualComponents(app, output_widget)
172
+ except Exception:
173
+ # If we can't get the components, steps will fall back to ctx.ui
174
+ pass
175
+
176
+ # Merge workflow params into ctx.data with optional overrides
177
+ effective_params = {**workflow.params}
178
+ if params_override:
179
+ effective_params.update(params_override)
180
+
181
+ # Load params into ctx.data so steps can access them
182
+ ctx.data.update(effective_params)
183
+
184
+ # Inject workflow metadata into context
185
+ ctx.workflow_name = workflow.name
186
+ ctx.total_steps = len([s for s in workflow.steps if not s.get("hook")])
187
+
188
+ # Check if this is a nested workflow (called from another workflow)
189
+ is_nested = len(ctx._workflow_stack) > 0
190
+
191
+ # Emit workflow started event
192
+ self._post_message(
193
+ self.WorkflowStarted(
194
+ workflow_name=workflow.name,
195
+ description=workflow.description,
196
+ source=workflow.source,
197
+ total_steps=ctx.total_steps,
198
+ steps=workflow.steps,
199
+ is_nested=is_nested
200
+ )
201
+ )
202
+
203
+ ctx.enter_workflow(workflow.name)
204
+ try:
205
+ step_index = 0
206
+ for step_data in workflow.steps:
207
+ step_config = WorkflowStepModel(**step_data)
208
+
209
+ # Hooks are resolved by the registry, so we just skip the placeholder
210
+ if step_config.hook:
211
+ continue
212
+
213
+ step_index += 1
214
+ ctx.current_step = step_index
215
+
216
+ step_id = step_config.id
217
+ step_name = step_config.name or step_id
218
+
219
+ # Emit step started event
220
+ self._post_message_sync(
221
+ self.StepStarted(
222
+ step_index=step_index,
223
+ step_id=step_id,
224
+ step_name=step_name
225
+ )
226
+ )
227
+
228
+ try:
229
+ if step_config.workflow:
230
+ step_result = self._execute_workflow_step(step_config, ctx)
231
+ elif step_config.plugin and step_config.step:
232
+ step_result = self._execute_plugin_step(step_config, ctx)
233
+ elif step_config.command:
234
+ step_result = self._execute_command_step(step_config, ctx)
235
+ else:
236
+ step_result = Error(f"Invalid step configuration for '{step_id}'.")
237
+ except Exception as e:
238
+ step_result = Error(f"An unexpected error occurred in step '{step_name}': {e}", e)
239
+
240
+ # Handle step result
241
+ if is_error(step_result):
242
+ self._post_message_sync(
243
+ self.StepFailed(
244
+ step_index=step_index,
245
+ step_id=step_id,
246
+ step_name=step_name,
247
+ error_message=step_result.message,
248
+ on_error=step_config.on_error
249
+ )
250
+ )
251
+
252
+ if step_config.on_error == "fail":
253
+ self._post_message_sync(
254
+ self.WorkflowFailed(
255
+ workflow_name=workflow.name,
256
+ step_name=step_name,
257
+ error_message=step_result.message
258
+ )
259
+ )
260
+ return Error(f"Workflow failed at step '{step_name}'", step_result.exception)
261
+ # else: on_error == "continue" - continue to next step
262
+ elif is_skip(step_result):
263
+ self._post_message_sync(
264
+ self.StepSkipped(
265
+ step_index=step_index,
266
+ step_id=step_id,
267
+ step_name=step_name
268
+ )
269
+ )
270
+ if step_result.metadata:
271
+ ctx.data.update(step_result.metadata)
272
+ else: # Success
273
+ self._post_message_sync(
274
+ self.StepCompleted(
275
+ step_index=step_index,
276
+ step_id=step_id,
277
+ step_name=step_name
278
+ )
279
+ )
280
+ if step_result.metadata:
281
+ ctx.data.update(step_result.metadata)
282
+
283
+ finally:
284
+ ctx.exit_workflow(workflow.name)
285
+
286
+ # Check if this is a nested workflow (called from another workflow)
287
+ is_nested = len(ctx._workflow_stack) > 0
288
+
289
+ # DEBUG: Log completion
290
+ # with open("/tmp/titan_debug.log", "a") as f:
291
+ # f.write(f"[{time.time():.3f}] Workflow '{workflow.name}' completed. is_nested={is_nested}, stack={ctx._workflow_stack}\n")
292
+
293
+ # Emit workflow completed event
294
+ self._post_message(
295
+ self.WorkflowCompleted(
296
+ workflow_name=workflow.name,
297
+ message=f"Workflow '{workflow.name}' finished.",
298
+ is_nested=is_nested
299
+ )
300
+ )
301
+
302
+ # with open("/tmp/titan_debug.log", "a") as f:
303
+ # f.write(f"[{time.time():.3f}] WorkflowCompleted message posted\n")
304
+
305
+ return Success(f"Workflow '{workflow.name}' finished.", {})
306
+
307
+ def _execute_workflow_step(self, step_config: WorkflowStepModel, ctx: WorkflowContext) -> WorkflowResult:
308
+ """Execute a nested workflow as a step."""
309
+ workflow_name = step_config.workflow
310
+ if not workflow_name:
311
+ return Error("Workflow step is missing the 'workflow' name.")
312
+
313
+ try:
314
+ sub_workflow = self._workflow_registry.get_workflow(workflow_name)
315
+ if not sub_workflow:
316
+ return Error(f"Nested workflow '{workflow_name}' not found.")
317
+ except Exception as e:
318
+ return Error(f"Failed to load workflow '{workflow_name}': {e}", e)
319
+
320
+ # Recursively execute the nested workflow
321
+ return self.execute(sub_workflow, ctx, params_override=step_config.params)
322
+
323
+ def _execute_plugin_step(self, step_config: WorkflowStepModel, ctx: WorkflowContext) -> WorkflowResult:
324
+ """Execute a plugin step."""
325
+ plugin_name = step_config.plugin
326
+ step_func_name = step_config.step
327
+ step_params = step_config.params
328
+
329
+ # Validate required context variables
330
+ required_vars = step_config.params.get("requires", [])
331
+ for var in required_vars:
332
+ if var not in ctx.data:
333
+ return Error(f"Step '{step_func_name}' is missing required context variable: '{var}'")
334
+
335
+ step_func = None
336
+ if plugin_name == "project":
337
+ # Handle virtual 'project' plugin for project-specific steps
338
+ step_func = self._workflow_registry.get_project_step(step_func_name)
339
+ if not step_func:
340
+ return Error(
341
+ f"Project step '{step_func_name}' not found in '.titan/steps/'.",
342
+ WorkflowExecutionError(f"Project step '{step_func_name}' not found")
343
+ )
344
+ elif plugin_name == "core":
345
+ # Handle virtual 'core' plugin for built-in core steps
346
+ step_func = self.CORE_STEPS.get(step_func_name)
347
+ if not step_func:
348
+ available = ", ".join(self.CORE_STEPS.keys())
349
+ return Error(
350
+ f"Core step '{step_func_name}' not found. Available: {available}",
351
+ WorkflowExecutionError(f"Core step '{step_func_name}' not found")
352
+ )
353
+ else:
354
+ # Handle regular plugins
355
+ plugin_instance = self._plugin_registry.get_plugin(plugin_name)
356
+ if not plugin_instance:
357
+ return Error(
358
+ f"Plugin '{plugin_name}' not found or not initialized.",
359
+ WorkflowExecutionError(f"Plugin '{plugin_name}' not found")
360
+ )
361
+
362
+ step_functions = plugin_instance.get_steps()
363
+ step_func = step_functions.get(step_func_name)
364
+ if not step_func:
365
+ return Error(
366
+ f"Step '{step_func_name}' not found in plugin '{plugin_name}'.",
367
+ WorkflowExecutionError(f"Step '{step_func_name}' not found")
368
+ )
369
+
370
+ # Prepare parameters for the step function
371
+ resolved_params = self._resolve_parameters(step_params, ctx)
372
+
373
+ # Add resolved parameters to context data so step can access them via ctx.get()
374
+ ctx.data.update(resolved_params)
375
+
376
+ # Execute the step function
377
+ try:
378
+ if plugin_name == "core":
379
+ # Core steps receive (step: WorkflowStepModel, ctx: WorkflowContext)
380
+ return step_func(step_config, ctx)
381
+ else:
382
+ # Plugin and project steps receive only ctx (params are in ctx.data)
383
+ return step_func(ctx)
384
+ except Exception as e:
385
+ error_source = f"plugin '{plugin_name}'" if plugin_name not in ("project", "core") else f"{plugin_name} step"
386
+ return Error(f"Error executing step '{step_func_name}' from {error_source}: {e}", e)
387
+
388
+ def _execute_command_step(self, step_config: WorkflowStepModel, ctx: WorkflowContext) -> WorkflowResult:
389
+ """Execute a shell command using the dedicated external function."""
390
+ return execute_external_command_step(step_config, ctx)
391
+
392
+ def _resolve_parameters(self, params: Dict[str, Any], ctx: WorkflowContext) -> Dict[str, Any]:
393
+ """
394
+ Resolve parameter values by substituting placeholders from context data.
395
+ All workflow params are already in ctx.data.
396
+ """
397
+ from titan_cli.engine.steps.command_step import resolve_parameters_in_string
398
+
399
+ resolved = {}
400
+ for key, value in params.items():
401
+ if isinstance(value, str):
402
+ resolved[key] = resolve_parameters_in_string(value, ctx)
403
+ else:
404
+ resolved[key] = value # Keep non-string parameters as is
405
+ return resolved
@@ -0,0 +1,102 @@
1
+ """
2
+ Titan TUI Theme
3
+
4
+ Centralized theme configuration for the Textual UI.
5
+ Defines color variables, CSS utilities, and style constants.
6
+ """
7
+
8
+ # Titan Theme CSS - Dracula Edition (Grises y Púrpuras)
9
+ TITAN_THEME_CSS = """
10
+ /* Color Variables */
11
+ $primary: #bd93f9; /* Purple (Dracula standard) */
12
+ $secondary: #50fa7b; /* Green */
13
+ $accent: #ff79c6; /* Pink */
14
+ $error: #ff5555; /* Red */
15
+ $warning: #f1fa8c; /* Yellow */
16
+ $success: #50fa7b; /* Green */
17
+ $info: #8be9fd; /* Cyan */
18
+
19
+ /* Backgrounds */
20
+ $surface: #282a36;
21
+ $surface-lighten-1: #343746;
22
+ $surface-lighten-2: #44475a;
23
+
24
+ /* Text Colors */
25
+ $text: #f8f8f2; /* Foreground (Almost white) */
26
+ $text-muted: #6272a4; /* Comment */
27
+ $text-disabled: #44475a; /* Disabled */
28
+
29
+ /* Banner gradient colors */
30
+ $banner-start: #6272a4;
31
+ $banner-mid: #bd93f9;
32
+ $banner-end: #ff79c6;
33
+
34
+ /* Base widget styles */
35
+ .title {
36
+ color: $primary;
37
+ text-style: bold;
38
+ }
39
+
40
+ .subtitle {
41
+ color: $secondary;
42
+ text-style: bold;
43
+ }
44
+
45
+ .body {
46
+ color: $text;
47
+ }
48
+
49
+ .muted {
50
+ color: $text-muted;
51
+ text-style: italic;
52
+ }
53
+
54
+ .error-text {
55
+ color: $error;
56
+ text-style: bold;
57
+ }
58
+
59
+ .success-text {
60
+ color: $success;
61
+ text-style: bold;
62
+ }
63
+
64
+ .warning-text {
65
+ color: $warning;
66
+ text-style: bold;
67
+ }
68
+
69
+ .info-text {
70
+ color: $info;
71
+ }
72
+
73
+ /* Global scrollbar styles - applies to all widgets */
74
+ * {
75
+ scrollbar-background: $surface;
76
+ scrollbar-background-hover: $surface-lighten-1;
77
+ scrollbar-background-active: $surface-lighten-2;
78
+ scrollbar-color: $primary;
79
+ scrollbar-color-hover: $accent;
80
+ scrollbar-color-active: $accent;
81
+ scrollbar-corner-color: $surface;
82
+ }
83
+
84
+ /* Global OptionList styles - transparent to inherit parent background */
85
+ OptionList {
86
+ border: none;
87
+ background: transparent;
88
+ }
89
+
90
+ OptionList > .option-list--option {
91
+ background: transparent;
92
+ }
93
+
94
+ OptionList > .option-list--option-highlighted {
95
+ background: $primary;
96
+ }
97
+
98
+ Screen {
99
+ background: $surface;
100
+ color: $text;
101
+ }
102
+ """
@@ -0,0 +1,40 @@
1
+ """
2
+ Titan TUI Widgets
3
+
4
+ Reusable Textual widgets for the Titan TUI.
5
+ """
6
+ from .status_bar import StatusBarWidget
7
+ from .header import HeaderWidget
8
+ from .panel import Panel
9
+ from .table import Table
10
+ from .button import Button
11
+ from .text import (
12
+ Text,
13
+ DimText,
14
+ BoldText,
15
+ PrimaryText,
16
+ BoldPrimaryText,
17
+ SuccessText,
18
+ ErrorText,
19
+ WarningText,
20
+ ItalicText,
21
+ DimItalicText,
22
+ )
23
+
24
+ __all__ = [
25
+ "StatusBarWidget",
26
+ "HeaderWidget",
27
+ "Panel",
28
+ "Table",
29
+ "Button",
30
+ "Text",
31
+ "DimText",
32
+ "BoldText",
33
+ "PrimaryText",
34
+ "BoldPrimaryText",
35
+ "SuccessText",
36
+ "ErrorText",
37
+ "WarningText",
38
+ "ItalicText",
39
+ "DimItalicText",
40
+ ]
@@ -0,0 +1,108 @@
1
+ """
2
+ Button Widget
3
+
4
+ Reusable button widget with consistent styling and variants.
5
+ """
6
+ from textual.widgets import Button as TextualButton
7
+
8
+
9
+ class Button(TextualButton):
10
+ """
11
+ Custom Button widget with consistent styling.
12
+
13
+ Fixes the focus issue (black box around text) and provides consistent variants.
14
+
15
+ Usage:
16
+ Button("Click Me", variant="primary", id="my-button")
17
+ Button("Delete", variant="error")
18
+ Button("Cancel", variant="default")
19
+
20
+ Available variants:
21
+ - primary: Primary action button (blue/accent color)
22
+ - default: Default button (neutral)
23
+ - error: Destructive action (red)
24
+ - success: Success action (green)
25
+ - warning: Warning action (yellow)
26
+ """
27
+
28
+ DEFAULT_CSS = """
29
+ Button {
30
+ min-width: 16;
31
+ height: 3;
32
+ margin: 0 1;
33
+ }
34
+
35
+ Button:focus {
36
+ text-style: none;
37
+ }
38
+
39
+ Button.-primary {
40
+ background: $primary;
41
+ color: $text;
42
+ }
43
+
44
+ Button.-primary:hover {
45
+ background: $primary-lighten-1;
46
+ }
47
+
48
+ Button.-primary:focus {
49
+ background: $primary;
50
+ text-style: none;
51
+ }
52
+
53
+ Button.-default {
54
+ background: $surface-lighten-2;
55
+ color: $text;
56
+ }
57
+
58
+ Button.-default:hover {
59
+ background: $surface-lighten-3;
60
+ }
61
+
62
+ Button.-default:focus {
63
+ background: $surface-lighten-2;
64
+ text-style: none;
65
+ }
66
+
67
+ Button.-error {
68
+ background: $error;
69
+ color: $text;
70
+ }
71
+
72
+ Button.-error:hover {
73
+ background: $error-lighten-1;
74
+ }
75
+
76
+ Button.-error:focus {
77
+ background: $error;
78
+ text-style: none;
79
+ }
80
+
81
+ Button.-success {
82
+ background: $success;
83
+ color: $text;
84
+ }
85
+
86
+ Button.-success:hover {
87
+ background: $success-lighten-1;
88
+ }
89
+
90
+ Button.-success:focus {
91
+ background: $success;
92
+ text-style: none;
93
+ }
94
+
95
+ Button.-warning {
96
+ background: $warning;
97
+ color: $text;
98
+ }
99
+
100
+ Button.-warning:hover {
101
+ background: $warning-lighten-1;
102
+ }
103
+
104
+ Button.-warning:focus {
105
+ background: $warning;
106
+ text-style: none;
107
+ }
108
+ """