titan-cli 0.1.5__py3-none-any.whl → 0.1.6__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.
- titan_cli/core/workflows/project_step_source.py +52 -7
- titan_cli/core/workflows/workflow_filter_service.py +6 -4
- titan_cli/engine/__init__.py +5 -1
- titan_cli/engine/results.py +31 -1
- titan_cli/engine/steps/ai_assistant_step.py +18 -18
- titan_cli/engine/workflow_executor.py +7 -2
- titan_cli/ui/tui/screens/plugin_config_wizard.py +16 -0
- titan_cli/ui/tui/screens/workflow_execution.py +22 -24
- titan_cli/ui/tui/screens/workflows.py +8 -4
- titan_cli/ui/tui/textual_components.py +293 -189
- titan_cli/ui/tui/textual_workflow_executor.py +30 -2
- titan_cli/ui/tui/theme.py +34 -5
- titan_cli/ui/tui/widgets/__init__.py +15 -0
- titan_cli/ui/tui/widgets/multiline_input.py +32 -0
- titan_cli/ui/tui/widgets/prompt_choice.py +138 -0
- titan_cli/ui/tui/widgets/prompt_input.py +74 -0
- titan_cli/ui/tui/widgets/prompt_selection_list.py +150 -0
- titan_cli/ui/tui/widgets/prompt_textarea.py +87 -0
- titan_cli/ui/tui/widgets/styled_option_list.py +107 -0
- titan_cli/ui/tui/widgets/text.py +51 -130
- {titan_cli-0.1.5.dist-info → titan_cli-0.1.6.dist-info}/METADATA +5 -10
- {titan_cli-0.1.5.dist-info → titan_cli-0.1.6.dist-info}/RECORD +54 -41
- {titan_cli-0.1.5.dist-info → titan_cli-0.1.6.dist-info}/WHEEL +1 -1
- titan_plugin_git/clients/git_client.py +59 -2
- titan_plugin_git/plugin.py +10 -0
- titan_plugin_git/steps/ai_commit_message_step.py +8 -8
- titan_plugin_git/steps/branch_steps.py +6 -6
- titan_plugin_git/steps/checkout_step.py +66 -0
- titan_plugin_git/steps/commit_step.py +3 -3
- titan_plugin_git/steps/create_branch_step.py +131 -0
- titan_plugin_git/steps/diff_summary_step.py +11 -13
- titan_plugin_git/steps/pull_step.py +70 -0
- titan_plugin_git/steps/push_step.py +3 -3
- titan_plugin_git/steps/restore_original_branch_step.py +97 -0
- titan_plugin_git/steps/save_current_branch_step.py +82 -0
- titan_plugin_git/steps/status_step.py +23 -13
- titan_plugin_git/workflows/commit-ai.yaml +4 -3
- titan_plugin_github/steps/ai_pr_step.py +90 -22
- titan_plugin_github/steps/create_pr_step.py +8 -8
- titan_plugin_github/steps/github_prompt_steps.py +13 -13
- titan_plugin_github/steps/issue_steps.py +14 -15
- titan_plugin_github/steps/preview_step.py +8 -8
- titan_plugin_github/workflows/create-pr-ai.yaml +1 -11
- titan_plugin_jira/messages.py +12 -0
- titan_plugin_jira/plugin.py +4 -0
- titan_plugin_jira/steps/ai_analyze_issue_step.py +6 -6
- titan_plugin_jira/steps/get_issue_step.py +7 -7
- titan_plugin_jira/steps/list_versions_step.py +133 -0
- titan_plugin_jira/steps/prompt_select_issue_step.py +8 -9
- titan_plugin_jira/steps/search_jql_step.py +191 -0
- titan_plugin_jira/steps/search_saved_query_step.py +13 -13
- titan_plugin_jira/utils/__init__.py +1 -1
- {titan_cli-0.1.5.dist-info/licenses → titan_cli-0.1.6.dist-info}/LICENSE +0 -0
- {titan_cli-0.1.5.dist-info → titan_cli-0.1.6.dist-info}/entry_points.txt +0 -0
|
@@ -7,193 +7,39 @@ Steps can import widgets directly from titan_cli.ui.tui.widgets and mount them u
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import threading
|
|
10
|
-
from typing import Optional,
|
|
10
|
+
from typing import Optional, List, Any
|
|
11
11
|
from contextlib import contextmanager
|
|
12
12
|
from textual.widget import Widget
|
|
13
|
-
from textual.widgets import
|
|
13
|
+
from textual.widgets import LoadingIndicator, Static, Markdown
|
|
14
14
|
from textual.containers import Container
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class PromptInput(Widget):
|
|
19
|
-
"""Widget wrapper for Input that handles submission events."""
|
|
20
|
-
|
|
21
|
-
# Allow this widget and its children to receive focus
|
|
22
|
-
can_focus = True
|
|
23
|
-
can_focus_children = True
|
|
24
|
-
|
|
25
|
-
DEFAULT_CSS = """
|
|
26
|
-
PromptInput {
|
|
27
|
-
width: 100%;
|
|
28
|
-
height: auto;
|
|
29
|
-
padding: 1;
|
|
30
|
-
margin: 1 0;
|
|
31
|
-
background: $surface-lighten-1;
|
|
32
|
-
border: round $accent;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
PromptInput > Static {
|
|
36
|
-
width: 100%;
|
|
37
|
-
height: auto;
|
|
38
|
-
margin-bottom: 1;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
PromptInput > Input {
|
|
42
|
-
width: 100%;
|
|
43
|
-
}
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
def __init__(self, question: str, default: str, placeholder: str, on_submit: Callable[[str], None], **kwargs):
|
|
47
|
-
super().__init__(**kwargs)
|
|
48
|
-
self.question = question
|
|
49
|
-
self.default = default
|
|
50
|
-
self.placeholder = placeholder
|
|
51
|
-
self.on_submit_callback = on_submit
|
|
52
|
-
|
|
53
|
-
def compose(self):
|
|
54
|
-
from textual.widgets import Static
|
|
55
|
-
yield Static(f"[bold cyan]{self.question}[/bold cyan]")
|
|
56
|
-
yield Input(
|
|
57
|
-
value=self.default,
|
|
58
|
-
placeholder=self.placeholder,
|
|
59
|
-
id="prompt-input"
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
def on_mount(self):
|
|
63
|
-
"""Focus input when mounted and scroll into view."""
|
|
64
|
-
# Use call_after_refresh to ensure widget tree is ready
|
|
65
|
-
self.call_after_refresh(self._focus_input)
|
|
66
|
-
|
|
67
|
-
def _focus_input(self):
|
|
68
|
-
"""Focus the input widget and scroll into view."""
|
|
69
|
-
try:
|
|
70
|
-
input_widget = self.query_one(Input)
|
|
71
|
-
# Use app.set_focus() to force focus change from steps-panel
|
|
72
|
-
self.app.set_focus(input_widget)
|
|
73
|
-
# Scroll to make this widget visible
|
|
74
|
-
self.scroll_visible(animate=False)
|
|
75
|
-
except Exception:
|
|
76
|
-
pass
|
|
77
|
-
|
|
78
|
-
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
79
|
-
"""Handle input submission."""
|
|
80
|
-
value = event.value
|
|
81
|
-
self.on_submit_callback(value)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
class MultilineInput(TextArea):
|
|
85
|
-
"""Custom TextArea that handles Enter for submission and Shift+Enter for new lines."""
|
|
86
|
-
|
|
87
|
-
class Submitted(Message):
|
|
88
|
-
"""Message sent when the input is submitted."""
|
|
89
|
-
def __init__(self, sender: Widget, value: str):
|
|
90
|
-
super().__init__()
|
|
91
|
-
self.sender = sender
|
|
92
|
-
self.value = value
|
|
93
|
-
|
|
94
|
-
def _on_key(self, event) -> None:
|
|
95
|
-
"""Intercept key events before TextArea processes them."""
|
|
96
|
-
from textual.events import Key
|
|
97
|
-
|
|
98
|
-
# Check if it's Enter without shift
|
|
99
|
-
if isinstance(event, Key) and event.key == "enter":
|
|
100
|
-
# Submit the input
|
|
101
|
-
self.post_message(self.Submitted(self, self.text))
|
|
102
|
-
event.prevent_default()
|
|
103
|
-
event.stop()
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
# For all other keys, let TextArea handle it
|
|
107
|
-
super()._on_key(event)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
class PromptTextArea(Widget):
|
|
111
|
-
"""Widget wrapper for MultilineInput that handles multiline input submission."""
|
|
112
|
-
|
|
113
|
-
can_focus = True
|
|
114
|
-
can_focus_children = True
|
|
115
|
-
|
|
116
|
-
DEFAULT_CSS = """
|
|
117
|
-
PromptTextArea {
|
|
118
|
-
width: 100%;
|
|
119
|
-
height: auto;
|
|
120
|
-
padding: 1;
|
|
121
|
-
margin: 1 0;
|
|
122
|
-
background: $surface-lighten-1;
|
|
123
|
-
border: round $accent;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
PromptTextArea > Static {
|
|
127
|
-
width: 100%;
|
|
128
|
-
height: auto;
|
|
129
|
-
margin-bottom: 1;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
PromptTextArea > MultilineInput {
|
|
133
|
-
width: 100%;
|
|
134
|
-
height: auto;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
PromptTextArea .hint-text {
|
|
138
|
-
width: 100%;
|
|
139
|
-
height: auto;
|
|
140
|
-
margin-top: 1;
|
|
141
|
-
color: $text-muted;
|
|
142
|
-
}
|
|
143
|
-
"""
|
|
144
|
-
|
|
145
|
-
def __init__(self, question: str, default: str, on_submit: Callable[[str], None], **kwargs):
|
|
146
|
-
super().__init__(**kwargs)
|
|
147
|
-
self.question = question
|
|
148
|
-
self.default = default
|
|
149
|
-
self.on_submit_callback = on_submit
|
|
150
|
-
|
|
151
|
-
def compose(self):
|
|
152
|
-
from textual.widgets import Static
|
|
153
|
-
yield Static(f"[bold cyan]{self.question}[/bold cyan]")
|
|
154
|
-
yield MultilineInput(
|
|
155
|
-
text=self.default,
|
|
156
|
-
id="prompt-textarea",
|
|
157
|
-
soft_wrap=True
|
|
158
|
-
)
|
|
159
|
-
yield Static("[dim]Press Enter to submit, Shift+Enter for new line[/dim]", classes="hint-text")
|
|
160
|
-
|
|
161
|
-
def on_mount(self):
|
|
162
|
-
"""Focus textarea when mounted and scroll into view."""
|
|
163
|
-
self.call_after_refresh(self._focus_textarea)
|
|
164
|
-
|
|
165
|
-
def _focus_textarea(self):
|
|
166
|
-
"""Focus the textarea widget and scroll into view."""
|
|
167
|
-
try:
|
|
168
|
-
textarea = self.query_one(MultilineInput)
|
|
169
|
-
self.app.set_focus(textarea)
|
|
170
|
-
self.scroll_visible(animate=False)
|
|
171
|
-
except Exception:
|
|
172
|
-
pass
|
|
173
|
-
|
|
174
|
-
def on_multiline_input_submitted(self, message: MultilineInput.Submitted):
|
|
175
|
-
"""Handle submission from MultilineInput."""
|
|
176
|
-
self.on_submit_callback(message.value)
|
|
15
|
+
from titan_cli.ui.tui.widgets import Panel, PromptInput, PromptTextArea, PromptSelectionList, SelectionOption, PromptChoice, ChoiceOption
|
|
177
16
|
|
|
178
17
|
|
|
179
18
|
class TextualComponents:
|
|
180
19
|
"""
|
|
181
20
|
Textual UI utilities for workflow steps.
|
|
182
21
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
22
|
+
All text styling uses the theme system for consistent colors across themes.
|
|
23
|
+
|
|
24
|
+
Steps can use these utilities to:
|
|
25
|
+
- Display panels with consistent styling
|
|
26
|
+
- Append text with theme-based styling
|
|
186
27
|
- Request user input interactively
|
|
28
|
+
- Mount custom widgets to the output panel
|
|
187
29
|
|
|
188
30
|
Example:
|
|
189
|
-
from titan_cli.ui.tui.widgets import Panel, DimText
|
|
190
|
-
|
|
191
31
|
def my_step(ctx):
|
|
192
|
-
#
|
|
193
|
-
ctx.textual.
|
|
32
|
+
# Show a panel
|
|
33
|
+
ctx.textual.panel("Warning message", panel_type="warning")
|
|
194
34
|
|
|
195
|
-
# Append
|
|
196
|
-
ctx.textual.
|
|
35
|
+
# Append styled text (uses theme system)
|
|
36
|
+
ctx.textual.dim_text("Fetching data...")
|
|
37
|
+
ctx.textual.success_text("Operation completed!")
|
|
38
|
+
ctx.textual.error_text("Failed to connect")
|
|
39
|
+
ctx.textual.bold_primary_text("AI Analysis Results")
|
|
40
|
+
|
|
41
|
+
# Append plain text
|
|
42
|
+
ctx.textual.text("Processing...")
|
|
197
43
|
|
|
198
44
|
# Ask for input
|
|
199
45
|
response = ctx.textual.ask_confirm("Continue?", default=True)
|
|
@@ -213,7 +59,7 @@ class TextualComponents:
|
|
|
213
59
|
|
|
214
60
|
def begin_step(self, step_name: str) -> None:
|
|
215
61
|
"""
|
|
216
|
-
Begin a new step by creating a StepContainer.
|
|
62
|
+
Begin a new step by creating a StepContainer and auto-scrolling to it.
|
|
217
63
|
|
|
218
64
|
Args:
|
|
219
65
|
step_name: Name of the step
|
|
@@ -224,6 +70,8 @@ class TextualComponents:
|
|
|
224
70
|
container = StepContainer(step_name=step_name)
|
|
225
71
|
self.output_widget.mount(container)
|
|
226
72
|
self._active_step_container = container
|
|
73
|
+
# Auto-scroll to show the new step
|
|
74
|
+
self.output_widget._scroll_to_end()
|
|
227
75
|
|
|
228
76
|
try:
|
|
229
77
|
self.app.call_from_thread(_create_container)
|
|
@@ -273,33 +121,46 @@ class TextualComponents:
|
|
|
273
121
|
# App is closing or worker was cancelled
|
|
274
122
|
pass
|
|
275
123
|
|
|
276
|
-
def
|
|
124
|
+
def scroll_to_end(self) -> None:
|
|
277
125
|
"""
|
|
278
|
-
|
|
126
|
+
Scroll to the end of the output widget.
|
|
127
|
+
|
|
128
|
+
Useful for ensuring user sees newly added content in steps with lots of output.
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
ctx.textual.markdown(large_content)
|
|
132
|
+
ctx.textual.scroll_to_end() # Ensure user sees what comes next
|
|
133
|
+
"""
|
|
134
|
+
def _scroll():
|
|
135
|
+
self.output_widget._scroll_to_end()
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
self.app.call_from_thread(_scroll)
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
def text(self, text: str) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Append plain text without styling.
|
|
145
|
+
|
|
146
|
+
For styled text, use specific methods: dim_text(), success_text(), etc.
|
|
279
147
|
|
|
280
148
|
Args:
|
|
281
149
|
text: Text to append
|
|
282
|
-
markup: Optional Rich markup style (e.g., "cyan", "bold green")
|
|
283
150
|
|
|
284
151
|
Example:
|
|
285
|
-
ctx.textual.text("
|
|
286
|
-
ctx.textual.text("
|
|
152
|
+
ctx.textual.text("Processing...")
|
|
153
|
+
ctx.textual.text("") # Empty line
|
|
287
154
|
"""
|
|
288
155
|
def _append():
|
|
289
156
|
# If there's an active step container, append to it; otherwise to output widget
|
|
290
157
|
if self._active_step_container:
|
|
291
158
|
from textual.widgets import Static
|
|
292
|
-
|
|
293
|
-
widget = Static(f"[{markup}]{text}[/{markup}]")
|
|
294
|
-
else:
|
|
295
|
-
widget = Static(text)
|
|
159
|
+
widget = Static(text)
|
|
296
160
|
widget.styles.height = "auto"
|
|
297
161
|
self._active_step_container.mount(widget)
|
|
298
162
|
else:
|
|
299
|
-
|
|
300
|
-
self.output_widget.append_output(f"[{markup}]{text}[/{markup}]")
|
|
301
|
-
else:
|
|
302
|
-
self.output_widget.append_output(text)
|
|
163
|
+
self.output_widget.append_output(text)
|
|
303
164
|
|
|
304
165
|
# call_from_thread already blocks until the function completes
|
|
305
166
|
try:
|
|
@@ -331,8 +192,7 @@ class TextualComponents:
|
|
|
331
192
|
# Mount to active step container if it exists, otherwise to output widget
|
|
332
193
|
target = self._active_step_container if self._active_step_container else self.output_widget
|
|
333
194
|
target.mount(md_widget)
|
|
334
|
-
#
|
|
335
|
-
self.output_widget._scroll_to_end()
|
|
195
|
+
# Note: Screen handles auto-scroll when step completes, not here
|
|
336
196
|
|
|
337
197
|
# call_from_thread already blocks until the function completes
|
|
338
198
|
try:
|
|
@@ -341,6 +201,126 @@ class TextualComponents:
|
|
|
341
201
|
# App is closing or worker was cancelled
|
|
342
202
|
pass
|
|
343
203
|
|
|
204
|
+
def panel(self, text: str, panel_type: str = "info") -> None:
|
|
205
|
+
"""
|
|
206
|
+
Show a panel with consistent styling.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
text: Text to display in the panel
|
|
210
|
+
panel_type: Type of panel - "info", "success", "warning", or "error"
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
ctx.textual.panel("Operation completed successfully!", panel_type="success")
|
|
214
|
+
ctx.textual.panel("Warning: This action cannot be undone", panel_type="warning")
|
|
215
|
+
"""
|
|
216
|
+
panel_widget = Panel(text=text, panel_type=panel_type)
|
|
217
|
+
self.mount(panel_widget)
|
|
218
|
+
|
|
219
|
+
def dim_text(self, text: str) -> None:
|
|
220
|
+
"""
|
|
221
|
+
Append dim/muted text (uses theme system).
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
text: Text to display
|
|
225
|
+
|
|
226
|
+
Example:
|
|
227
|
+
ctx.textual.dim_text("Fetching versions for project: ECAPP")
|
|
228
|
+
"""
|
|
229
|
+
from titan_cli.ui.tui.widgets import DimText
|
|
230
|
+
widget = DimText(text)
|
|
231
|
+
widget.styles.height = "auto"
|
|
232
|
+
self.mount(widget)
|
|
233
|
+
|
|
234
|
+
def success_text(self, text: str) -> None:
|
|
235
|
+
"""
|
|
236
|
+
Append success text (green, uses theme system).
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
text: Text to display
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
ctx.textual.success_text("Commit created: abc1234")
|
|
243
|
+
"""
|
|
244
|
+
from titan_cli.ui.tui.widgets import SuccessText
|
|
245
|
+
widget = SuccessText(text)
|
|
246
|
+
widget.styles.height = "auto"
|
|
247
|
+
self.mount(widget)
|
|
248
|
+
|
|
249
|
+
def error_text(self, text: str) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Append error text (red, uses theme system).
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
text: Text to display
|
|
255
|
+
|
|
256
|
+
Example:
|
|
257
|
+
ctx.textual.error_text("Failed to connect to API")
|
|
258
|
+
"""
|
|
259
|
+
from titan_cli.ui.tui.widgets import ErrorText
|
|
260
|
+
widget = ErrorText(text)
|
|
261
|
+
widget.styles.height = "auto"
|
|
262
|
+
self.mount(widget)
|
|
263
|
+
|
|
264
|
+
def warning_text(self, text: str) -> None:
|
|
265
|
+
"""
|
|
266
|
+
Append warning text (yellow, uses theme system).
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
text: Text to display
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
ctx.textual.warning_text("This action will overwrite existing files")
|
|
273
|
+
"""
|
|
274
|
+
from titan_cli.ui.tui.widgets import WarningText
|
|
275
|
+
widget = WarningText(text)
|
|
276
|
+
widget.styles.height = "auto"
|
|
277
|
+
self.mount(widget)
|
|
278
|
+
|
|
279
|
+
def primary_text(self, text: str) -> None:
|
|
280
|
+
"""
|
|
281
|
+
Append primary colored text (uses theme system).
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
text: Text to display
|
|
285
|
+
|
|
286
|
+
Example:
|
|
287
|
+
ctx.textual.primary_text("Processing items...")
|
|
288
|
+
"""
|
|
289
|
+
from titan_cli.ui.tui.widgets import PrimaryText
|
|
290
|
+
widget = PrimaryText(text)
|
|
291
|
+
widget.styles.height = "auto"
|
|
292
|
+
self.mount(widget)
|
|
293
|
+
|
|
294
|
+
def bold_text(self, text: str) -> None:
|
|
295
|
+
"""
|
|
296
|
+
Append bold text.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
text: Text to display
|
|
300
|
+
|
|
301
|
+
Example:
|
|
302
|
+
ctx.textual.bold_text("Important: Read carefully")
|
|
303
|
+
"""
|
|
304
|
+
from titan_cli.ui.tui.widgets import BoldText
|
|
305
|
+
widget = BoldText(text)
|
|
306
|
+
widget.styles.height = "auto"
|
|
307
|
+
self.mount(widget)
|
|
308
|
+
|
|
309
|
+
def bold_primary_text(self, text: str) -> None:
|
|
310
|
+
"""
|
|
311
|
+
Append bold text with primary theme color.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
text: Text to display
|
|
315
|
+
|
|
316
|
+
Example:
|
|
317
|
+
ctx.textual.bold_primary_text("AI Analysis Results")
|
|
318
|
+
"""
|
|
319
|
+
from titan_cli.ui.tui.widgets import BoldPrimaryText
|
|
320
|
+
widget = BoldPrimaryText(text)
|
|
321
|
+
widget.styles.height = "auto"
|
|
322
|
+
self.mount(widget)
|
|
323
|
+
|
|
344
324
|
def ask_text(self, question: str, default: str = "") -> Optional[str]:
|
|
345
325
|
"""
|
|
346
326
|
Ask user for text input (blocks until user responds).
|
|
@@ -496,6 +476,130 @@ class TextualComponents:
|
|
|
496
476
|
# Invalid response, use default
|
|
497
477
|
return default
|
|
498
478
|
|
|
479
|
+
def ask_multiselect(
|
|
480
|
+
self,
|
|
481
|
+
question: str,
|
|
482
|
+
options: List[SelectionOption],
|
|
483
|
+
) -> List[Any]:
|
|
484
|
+
"""
|
|
485
|
+
Ask user to select multiple options from a list using checkboxes.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
question: Question to display
|
|
489
|
+
options: List of SelectionOption instances
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
List of selected values (the 'value' field from SelectionOption)
|
|
493
|
+
|
|
494
|
+
Example:
|
|
495
|
+
from titan_cli.ui.tui.widgets import SelectionOption
|
|
496
|
+
|
|
497
|
+
options = [
|
|
498
|
+
SelectionOption(value="option1", label="First Option", selected=True),
|
|
499
|
+
SelectionOption(value="option2", label="Second Option", selected=True),
|
|
500
|
+
SelectionOption(value="option3", label="Third Option", selected=False),
|
|
501
|
+
]
|
|
502
|
+
|
|
503
|
+
selected = ctx.textual.ask_multiselect(
|
|
504
|
+
"Select options to include:",
|
|
505
|
+
options
|
|
506
|
+
)
|
|
507
|
+
# selected might be ["option1", "option2"] if user didn't change anything
|
|
508
|
+
"""
|
|
509
|
+
result_container = {"result": None, "ready": threading.Event()}
|
|
510
|
+
|
|
511
|
+
def on_submit(selected_values: List[Any]):
|
|
512
|
+
result_container["result"] = selected_values
|
|
513
|
+
result_container["ready"].set()
|
|
514
|
+
|
|
515
|
+
# Create and mount the selection widget
|
|
516
|
+
selection_widget = PromptSelectionList(
|
|
517
|
+
question=question,
|
|
518
|
+
options=options,
|
|
519
|
+
on_submit=on_submit
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
self.mount(selection_widget)
|
|
523
|
+
|
|
524
|
+
# Wait for user to submit
|
|
525
|
+
result_container["ready"].wait()
|
|
526
|
+
|
|
527
|
+
# Remove the widget
|
|
528
|
+
def _remove():
|
|
529
|
+
try:
|
|
530
|
+
selection_widget.remove()
|
|
531
|
+
except Exception:
|
|
532
|
+
pass
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
self.app.call_from_thread(_remove)
|
|
536
|
+
except Exception:
|
|
537
|
+
pass
|
|
538
|
+
|
|
539
|
+
return result_container["result"]
|
|
540
|
+
|
|
541
|
+
def ask_choice(
|
|
542
|
+
self,
|
|
543
|
+
question: str,
|
|
544
|
+
options: List[ChoiceOption],
|
|
545
|
+
) -> Any:
|
|
546
|
+
"""
|
|
547
|
+
Ask user to select one option from multiple choices using buttons.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
question: Question to display
|
|
551
|
+
options: List of ChoiceOption instances
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
The selected value (the 'value' field from ChoiceOption)
|
|
555
|
+
|
|
556
|
+
Example:
|
|
557
|
+
from titan_cli.ui.tui.widgets import ChoiceOption
|
|
558
|
+
|
|
559
|
+
options = [
|
|
560
|
+
ChoiceOption(value="use", label="Use as-is", variant="primary"),
|
|
561
|
+
ChoiceOption(value="edit", label="Edit", variant="default"),
|
|
562
|
+
ChoiceOption(value="reject", label="Reject", variant="error"),
|
|
563
|
+
]
|
|
564
|
+
|
|
565
|
+
choice = ctx.textual.ask_choice(
|
|
566
|
+
"What would you like to do with this PR description?",
|
|
567
|
+
options
|
|
568
|
+
)
|
|
569
|
+
# choice might be "use", "edit", or "reject"
|
|
570
|
+
"""
|
|
571
|
+
result_container = {"result": None, "ready": threading.Event()}
|
|
572
|
+
|
|
573
|
+
def on_select(selected_value: Any):
|
|
574
|
+
result_container["result"] = selected_value
|
|
575
|
+
result_container["ready"].set()
|
|
576
|
+
|
|
577
|
+
# Create and mount the choice widget
|
|
578
|
+
choice_widget = PromptChoice(
|
|
579
|
+
question=question,
|
|
580
|
+
options=options,
|
|
581
|
+
on_select=on_select
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
self.mount(choice_widget)
|
|
585
|
+
|
|
586
|
+
# Wait for user to select
|
|
587
|
+
result_container["ready"].wait()
|
|
588
|
+
|
|
589
|
+
# Remove the widget
|
|
590
|
+
def _remove():
|
|
591
|
+
try:
|
|
592
|
+
choice_widget.remove()
|
|
593
|
+
except Exception:
|
|
594
|
+
pass
|
|
595
|
+
|
|
596
|
+
try:
|
|
597
|
+
self.app.call_from_thread(_remove)
|
|
598
|
+
except Exception:
|
|
599
|
+
pass
|
|
600
|
+
|
|
601
|
+
return result_container["result"]
|
|
602
|
+
|
|
499
603
|
@contextmanager
|
|
500
604
|
def loading(self, message: str = "Loading..."):
|
|
501
605
|
"""
|
|
@@ -14,7 +14,7 @@ from titan_cli.core.workflows.workflow_registry import WorkflowRegistry
|
|
|
14
14
|
from titan_cli.core.plugins.plugin_registry import PluginRegistry
|
|
15
15
|
from titan_cli.core.workflows.models import WorkflowStepModel
|
|
16
16
|
from titan_cli.engine.context import WorkflowContext
|
|
17
|
-
from titan_cli.engine.results import WorkflowResult, Success, Error, is_error, is_skip
|
|
17
|
+
from titan_cli.engine.results import WorkflowResult, Success, Error, is_error, is_skip, is_exit
|
|
18
18
|
from titan_cli.engine.steps.command_step import execute_command_step as execute_external_command_step
|
|
19
19
|
from titan_cli.engine.steps.ai_assistant_step import execute_ai_assistant_step
|
|
20
20
|
|
|
@@ -238,7 +238,35 @@ class TextualWorkflowExecutor:
|
|
|
238
238
|
step_result = Error(f"An unexpected error occurred in step '{step_name}': {e}", e)
|
|
239
239
|
|
|
240
240
|
# Handle step result
|
|
241
|
-
if
|
|
241
|
+
if is_exit(step_result):
|
|
242
|
+
# Exit workflow immediately (not an error)
|
|
243
|
+
if step_result.metadata:
|
|
244
|
+
ctx.data.update(step_result.metadata)
|
|
245
|
+
|
|
246
|
+
# Post completion message
|
|
247
|
+
self._post_message_sync(
|
|
248
|
+
self.StepCompleted(
|
|
249
|
+
step_index=step_index,
|
|
250
|
+
step_id=step_id,
|
|
251
|
+
step_name=step_name
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Calculate is_nested before exiting workflow
|
|
256
|
+
# (check if there are parent workflows in the stack)
|
|
257
|
+
is_nested = len(ctx._workflow_stack) > 1 # >1 because current workflow is still in stack
|
|
258
|
+
|
|
259
|
+
# Post workflow completed message and exit
|
|
260
|
+
self._post_message_sync(
|
|
261
|
+
self.WorkflowCompleted(
|
|
262
|
+
workflow_name=workflow.name,
|
|
263
|
+
message=step_result.message,
|
|
264
|
+
is_nested=is_nested
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
return Success(step_result.message, step_result.metadata)
|
|
268
|
+
|
|
269
|
+
elif is_error(step_result):
|
|
242
270
|
self._post_message_sync(
|
|
243
271
|
self.StepFailed(
|
|
244
272
|
step_index=step_index,
|
titan_cli/ui/tui/theme.py
CHANGED
|
@@ -17,8 +17,8 @@ $success: #50fa7b; /* Green */
|
|
|
17
17
|
$info: #8be9fd; /* Cyan */
|
|
18
18
|
|
|
19
19
|
/* Backgrounds */
|
|
20
|
-
$surface: #282a36;
|
|
21
|
-
$surface-lighten-1: #343746;
|
|
20
|
+
$surface: #282a36;
|
|
21
|
+
$surface-lighten-1: #343746;
|
|
22
22
|
$surface-lighten-2: #44475a;
|
|
23
23
|
|
|
24
24
|
/* Text Colors */
|
|
@@ -27,9 +27,9 @@ $text-muted: #6272a4; /* Comment */
|
|
|
27
27
|
$text-disabled: #44475a; /* Disabled */
|
|
28
28
|
|
|
29
29
|
/* Banner gradient colors */
|
|
30
|
-
$banner-start: #6272a4;
|
|
31
|
-
$banner-mid: #bd93f9;
|
|
32
|
-
$banner-end: #ff79c6;
|
|
30
|
+
$banner-start: #6272a4;
|
|
31
|
+
$banner-mid: #bd93f9;
|
|
32
|
+
$banner-end: #ff79c6;
|
|
33
33
|
|
|
34
34
|
/* Base widget styles */
|
|
35
35
|
.title {
|
|
@@ -70,6 +70,35 @@ $banner-end: #ff79c6;
|
|
|
70
70
|
color: $info;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/* Text widget styles (from widgets/text.py) */
|
|
74
|
+
.dim, DimText, DimItalicText {
|
|
75
|
+
color: $text-muted;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.bold, BoldText, BoldPrimaryText {
|
|
79
|
+
text-style: bold;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.italic, ItalicText, DimItalicText {
|
|
83
|
+
text-style: italic;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.primary, PrimaryText, BoldPrimaryText {
|
|
87
|
+
color: $primary;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.success, SuccessText {
|
|
91
|
+
color: $success;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.error, ErrorText {
|
|
95
|
+
color: $error;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.warning, WarningText {
|
|
99
|
+
color: $warning;
|
|
100
|
+
}
|
|
101
|
+
|
|
73
102
|
/* Global scrollbar styles - applies to all widgets */
|
|
74
103
|
* {
|
|
75
104
|
scrollbar-background: $surface;
|
|
@@ -9,6 +9,12 @@ from .panel import Panel
|
|
|
9
9
|
from .table import Table
|
|
10
10
|
from .button import Button
|
|
11
11
|
from .step_container import StepContainer
|
|
12
|
+
from .multiline_input import MultilineInput
|
|
13
|
+
from .prompt_input import PromptInput
|
|
14
|
+
from .prompt_textarea import PromptTextArea
|
|
15
|
+
from .prompt_selection_list import PromptSelectionList, SelectionOption
|
|
16
|
+
from .prompt_choice import PromptChoice, ChoiceOption
|
|
17
|
+
from .styled_option_list import StyledOptionList, StyledOption
|
|
12
18
|
from .text import (
|
|
13
19
|
Text,
|
|
14
20
|
DimText,
|
|
@@ -29,6 +35,15 @@ __all__ = [
|
|
|
29
35
|
"Table",
|
|
30
36
|
"Button",
|
|
31
37
|
"StepContainer",
|
|
38
|
+
"MultilineInput",
|
|
39
|
+
"PromptInput",
|
|
40
|
+
"PromptTextArea",
|
|
41
|
+
"PromptSelectionList",
|
|
42
|
+
"SelectionOption",
|
|
43
|
+
"PromptChoice",
|
|
44
|
+
"ChoiceOption",
|
|
45
|
+
"StyledOptionList",
|
|
46
|
+
"StyledOption",
|
|
32
47
|
"Text",
|
|
33
48
|
"DimText",
|
|
34
49
|
"BoldText",
|