titan-cli 0.1.5__py3-none-any.whl → 0.1.7__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.7.dist-info}/METADATA +5 -10
- {titan_cli-0.1.5.dist-info → titan_cli-0.1.7.dist-info}/RECORD +54 -41
- {titan_cli-0.1.5.dist-info → titan_cli-0.1.7.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.7.dist-info}/LICENSE +0 -0
- {titan_cli-0.1.5.dist-info → titan_cli-0.1.7.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MultilineInput Widget
|
|
3
|
+
|
|
4
|
+
Custom TextArea that handles Enter for submission and Shift+Enter for new lines.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from textual.widget import Widget
|
|
8
|
+
from textual.widgets import TextArea
|
|
9
|
+
from textual.message import Message
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MultilineInput(TextArea):
|
|
13
|
+
"""Custom TextArea that handles Ctrl+Enter for submission and Enter for new lines."""
|
|
14
|
+
|
|
15
|
+
class Submitted(Message):
|
|
16
|
+
"""Message sent when the input is submitted."""
|
|
17
|
+
def __init__(self, sender: Widget, value: str):
|
|
18
|
+
super().__init__()
|
|
19
|
+
self.sender = sender
|
|
20
|
+
self.value = value
|
|
21
|
+
|
|
22
|
+
def _on_key(self, event) -> None:
|
|
23
|
+
"""Intercept key events at low level before TextArea processes them."""
|
|
24
|
+
# Use Ctrl+D for submit (standard in many CLI tools)
|
|
25
|
+
if event.key == "ctrl+d":
|
|
26
|
+
self.post_message(self.Submitted(self, self.text))
|
|
27
|
+
event.prevent_default()
|
|
28
|
+
event.stop()
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
# Let TextArea handle everything else (Enter creates new lines)
|
|
32
|
+
super()._on_key(event)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PromptChoice Widget
|
|
3
|
+
|
|
4
|
+
Widget for selecting a single option from multiple choices using buttons.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Callable, List, Any
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from textual.widget import Widget
|
|
10
|
+
from textual.containers import Horizontal
|
|
11
|
+
from .text import BoldText, DimText
|
|
12
|
+
from .button import Button
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ChoiceOption:
|
|
17
|
+
"""
|
|
18
|
+
Option for choice selection.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
value: The value to return when selected (can be any type)
|
|
22
|
+
label: The text to display on the button
|
|
23
|
+
variant: Button variant (default, primary, success, warning, error)
|
|
24
|
+
"""
|
|
25
|
+
value: Any
|
|
26
|
+
label: str
|
|
27
|
+
variant: str = "default"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PromptChoice(Widget):
|
|
31
|
+
"""
|
|
32
|
+
Widget for selecting a single option from multiple choices.
|
|
33
|
+
|
|
34
|
+
Displays a question and horizontal buttons for each option.
|
|
35
|
+
User clicks a button to select that option.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
options = [
|
|
39
|
+
ChoiceOption(value="use", label="Use as-is", variant="primary"),
|
|
40
|
+
ChoiceOption(value="edit", label="Edit", variant="default"),
|
|
41
|
+
ChoiceOption(value="reject", label="Reject", variant="error"),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
choice_widget = PromptChoice(
|
|
45
|
+
question="What would you like to do?",
|
|
46
|
+
options=options,
|
|
47
|
+
on_select=lambda value: print(f"Selected: {value}")
|
|
48
|
+
)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# Allow this widget to receive focus
|
|
52
|
+
can_focus = True
|
|
53
|
+
can_focus_children = True
|
|
54
|
+
|
|
55
|
+
DEFAULT_CSS = """
|
|
56
|
+
PromptChoice {
|
|
57
|
+
width: 100%;
|
|
58
|
+
height: auto;
|
|
59
|
+
padding: 1;
|
|
60
|
+
margin: 1 0;
|
|
61
|
+
background: $surface-lighten-1;
|
|
62
|
+
border: round $accent;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
PromptChoice > BoldText,
|
|
66
|
+
PromptChoice > DimText {
|
|
67
|
+
width: 100%;
|
|
68
|
+
height: auto;
|
|
69
|
+
margin-bottom: 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
PromptChoice Horizontal {
|
|
73
|
+
width: 100%;
|
|
74
|
+
height: auto;
|
|
75
|
+
align: center middle;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
PromptChoice Button {
|
|
79
|
+
margin: 0 1;
|
|
80
|
+
}
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
question: str,
|
|
86
|
+
options: List[ChoiceOption],
|
|
87
|
+
on_select: Callable[[Any], None],
|
|
88
|
+
**kwargs
|
|
89
|
+
):
|
|
90
|
+
"""
|
|
91
|
+
Initialize PromptChoice.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
question: Question to display above the buttons
|
|
95
|
+
options: List of ChoiceOption instances
|
|
96
|
+
on_select: Callback that receives the selected value
|
|
97
|
+
"""
|
|
98
|
+
super().__init__(**kwargs)
|
|
99
|
+
self.question = question
|
|
100
|
+
self.options = options
|
|
101
|
+
self.on_select_callback = on_select
|
|
102
|
+
|
|
103
|
+
def compose(self):
|
|
104
|
+
# Question text
|
|
105
|
+
yield BoldText(self.question)
|
|
106
|
+
|
|
107
|
+
# Instructions
|
|
108
|
+
yield DimText("Select an option:")
|
|
109
|
+
|
|
110
|
+
# Buttons in horizontal layout
|
|
111
|
+
with Horizontal():
|
|
112
|
+
for i, option in enumerate(self.options):
|
|
113
|
+
button = Button(
|
|
114
|
+
option.label,
|
|
115
|
+
variant=option.variant,
|
|
116
|
+
id=f"choice-btn-{i}"
|
|
117
|
+
)
|
|
118
|
+
button.choice_value = option.value # Store value on button
|
|
119
|
+
yield button
|
|
120
|
+
|
|
121
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
122
|
+
"""Handle button press - call callback with selected value."""
|
|
123
|
+
if hasattr(event.button, 'choice_value'):
|
|
124
|
+
self.on_select_callback(event.button.choice_value)
|
|
125
|
+
event.stop()
|
|
126
|
+
|
|
127
|
+
def on_mount(self):
|
|
128
|
+
"""Focus first button when mounted."""
|
|
129
|
+
self.call_after_refresh(self._focus_first_button)
|
|
130
|
+
|
|
131
|
+
def _focus_first_button(self):
|
|
132
|
+
"""Focus the first button."""
|
|
133
|
+
try:
|
|
134
|
+
first_button = self.query_one("#choice-btn-0", Button)
|
|
135
|
+
self.app.set_focus(first_button)
|
|
136
|
+
self.scroll_visible(animate=False)
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PromptInput Widget
|
|
3
|
+
|
|
4
|
+
Widget wrapper for Input that handles submission events.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Callable
|
|
8
|
+
from textual.widget import Widget
|
|
9
|
+
from textual.widgets import Input, Static
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PromptInput(Widget):
|
|
13
|
+
"""Widget wrapper for Input that handles submission events."""
|
|
14
|
+
|
|
15
|
+
# Allow this widget and its children to receive focus
|
|
16
|
+
can_focus = True
|
|
17
|
+
can_focus_children = True
|
|
18
|
+
|
|
19
|
+
DEFAULT_CSS = """
|
|
20
|
+
PromptInput {
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: auto;
|
|
23
|
+
padding: 1;
|
|
24
|
+
margin: 1 0;
|
|
25
|
+
background: $surface-lighten-1;
|
|
26
|
+
border: round $accent;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
PromptInput > Static {
|
|
30
|
+
width: 100%;
|
|
31
|
+
height: auto;
|
|
32
|
+
margin-bottom: 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
PromptInput > Input {
|
|
36
|
+
width: 100%;
|
|
37
|
+
}
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, question: str, default: str, placeholder: str, on_submit: Callable[[str], None], **kwargs):
|
|
41
|
+
super().__init__(**kwargs)
|
|
42
|
+
self.question = question
|
|
43
|
+
self.default = default
|
|
44
|
+
self.placeholder = placeholder
|
|
45
|
+
self.on_submit_callback = on_submit
|
|
46
|
+
|
|
47
|
+
def compose(self):
|
|
48
|
+
yield Static(f"[bold cyan]{self.question}[/bold cyan]")
|
|
49
|
+
yield Input(
|
|
50
|
+
value=self.default,
|
|
51
|
+
placeholder=self.placeholder,
|
|
52
|
+
id="prompt-input"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def on_mount(self):
|
|
56
|
+
"""Focus input when mounted and scroll into view."""
|
|
57
|
+
# Use call_after_refresh to ensure widget tree is ready
|
|
58
|
+
self.call_after_refresh(self._focus_input)
|
|
59
|
+
|
|
60
|
+
def _focus_input(self):
|
|
61
|
+
"""Focus the input widget and scroll into view."""
|
|
62
|
+
try:
|
|
63
|
+
input_widget = self.query_one(Input)
|
|
64
|
+
# Use app.set_focus() to force focus change from steps-panel
|
|
65
|
+
self.app.set_focus(input_widget)
|
|
66
|
+
# Scroll to make this widget visible
|
|
67
|
+
self.scroll_visible(animate=False)
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
72
|
+
"""Handle input submission."""
|
|
73
|
+
value = event.value
|
|
74
|
+
self.on_submit_callback(value)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PromptSelectionList Widget
|
|
3
|
+
|
|
4
|
+
Widget wrapper for SelectionList that handles multi-selection with checkboxes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Callable, List, Any
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from textual.widget import Widget
|
|
10
|
+
from textual.widgets import SelectionList
|
|
11
|
+
from textual.widgets.selection_list import Selection
|
|
12
|
+
from .text import BoldText, DimText
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SelectionOption:
|
|
17
|
+
"""
|
|
18
|
+
Option for selection list.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
value: The value to return when selected (can be any type)
|
|
22
|
+
label: The text to display in the list
|
|
23
|
+
selected: Whether the option is initially selected
|
|
24
|
+
"""
|
|
25
|
+
value: Any
|
|
26
|
+
label: str
|
|
27
|
+
selected: bool = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PromptSelectionList(Widget):
|
|
31
|
+
"""
|
|
32
|
+
Widget wrapper for SelectionList that handles multi-selection events.
|
|
33
|
+
|
|
34
|
+
Displays a question and a list of options with checkboxes that the user
|
|
35
|
+
can toggle with Space and confirm with Enter.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# Allow this widget and its children to receive focus
|
|
39
|
+
can_focus = True
|
|
40
|
+
can_focus_children = True
|
|
41
|
+
|
|
42
|
+
DEFAULT_CSS = """
|
|
43
|
+
PromptSelectionList {
|
|
44
|
+
width: 100%;
|
|
45
|
+
height: auto;
|
|
46
|
+
padding: 1;
|
|
47
|
+
margin: 1 0;
|
|
48
|
+
background: $surface-lighten-1;
|
|
49
|
+
border: round $accent;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
PromptSelectionList > BoldText,
|
|
53
|
+
PromptSelectionList > DimText {
|
|
54
|
+
width: 100%;
|
|
55
|
+
height: auto;
|
|
56
|
+
margin-bottom: 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
PromptSelectionList > SelectionList {
|
|
60
|
+
width: 100%;
|
|
61
|
+
height: auto;
|
|
62
|
+
max-height: 20;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
PromptSelectionList .selection-list--option {
|
|
66
|
+
padding: 0 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
PromptSelectionList .selection-list--option-highlighted {
|
|
70
|
+
background: $accent 20%;
|
|
71
|
+
}
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
question: str,
|
|
77
|
+
options: List[SelectionOption],
|
|
78
|
+
on_submit: Callable[[List[Any]], None],
|
|
79
|
+
**kwargs
|
|
80
|
+
):
|
|
81
|
+
"""
|
|
82
|
+
Initialize PromptSelectionList.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
question: Question to display above the selection list
|
|
86
|
+
options: List of SelectionOption instances
|
|
87
|
+
on_submit: Callback that receives list of selected values
|
|
88
|
+
"""
|
|
89
|
+
super().__init__(**kwargs)
|
|
90
|
+
self.question = question
|
|
91
|
+
self.options = options
|
|
92
|
+
self.on_submit_callback = on_submit
|
|
93
|
+
|
|
94
|
+
def compose(self):
|
|
95
|
+
# Question text
|
|
96
|
+
yield BoldText(self.question)
|
|
97
|
+
|
|
98
|
+
# Instructions
|
|
99
|
+
yield DimText("↑/↓: Navegar │ Space: Seleccionar/Deseleccionar │ Enter: Continuar")
|
|
100
|
+
|
|
101
|
+
# Create Selection objects for SelectionList
|
|
102
|
+
selections = [
|
|
103
|
+
Selection(
|
|
104
|
+
value=str(i), # Use index as internal value
|
|
105
|
+
prompt=option.label,
|
|
106
|
+
initial_state=option.selected
|
|
107
|
+
)
|
|
108
|
+
for i, option in enumerate(self.options)
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
# SelectionList widget
|
|
112
|
+
yield SelectionList(*selections, id="selection-list")
|
|
113
|
+
|
|
114
|
+
def on_mount(self):
|
|
115
|
+
"""Focus selection list when mounted and scroll into view."""
|
|
116
|
+
self.call_after_refresh(self._focus_list)
|
|
117
|
+
|
|
118
|
+
def _focus_list(self):
|
|
119
|
+
"""Focus the selection list widget and scroll into view."""
|
|
120
|
+
try:
|
|
121
|
+
selection_list = self.query_one(SelectionList)
|
|
122
|
+
# Use app.set_focus() to force focus change
|
|
123
|
+
self.app.set_focus(selection_list)
|
|
124
|
+
# Scroll to make this widget visible
|
|
125
|
+
self.scroll_visible(animate=False)
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
def on_selection_list_selected_changed(self, event: SelectionList.SelectedChanged) -> None:
|
|
130
|
+
"""Handle when user presses Enter to confirm selection."""
|
|
131
|
+
# Don't handle automatically - wait for explicit submission
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
async def on_key(self, event) -> None:
|
|
135
|
+
"""Intercept Enter key before it reaches SelectionList."""
|
|
136
|
+
if event.key == "enter":
|
|
137
|
+
# Stop propagation FIRST to prevent SelectionList from handling it
|
|
138
|
+
event.stop()
|
|
139
|
+
event.prevent_default()
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
selection_list = self.query_one(SelectionList)
|
|
143
|
+
# Get indices of selected items
|
|
144
|
+
selected_indices = [int(sel) for sel in selection_list.selected]
|
|
145
|
+
# Map indices back to option values
|
|
146
|
+
selected_values = [self.options[i].value for i in selected_indices]
|
|
147
|
+
# Call the callback with selected values
|
|
148
|
+
self.on_submit_callback(selected_values)
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PromptTextArea Widget
|
|
3
|
+
|
|
4
|
+
Widget wrapper for MultilineInput that handles multiline input submission.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Callable
|
|
8
|
+
from textual.widget import Widget
|
|
9
|
+
from .multiline_input import MultilineInput
|
|
10
|
+
from .text import BoldText, DimText
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PromptTextArea(Widget):
|
|
14
|
+
"""Widget wrapper for MultilineInput that handles multiline input submission."""
|
|
15
|
+
|
|
16
|
+
can_focus = True
|
|
17
|
+
can_focus_children = True
|
|
18
|
+
|
|
19
|
+
DEFAULT_CSS = """
|
|
20
|
+
PromptTextArea {
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: auto;
|
|
23
|
+
padding: 1;
|
|
24
|
+
margin: 1 0;
|
|
25
|
+
background: $surface-lighten-1;
|
|
26
|
+
border: round $accent;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
PromptTextArea > BoldText,
|
|
30
|
+
PromptTextArea > DimText {
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: auto;
|
|
33
|
+
margin-bottom: 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
PromptTextArea > MultilineInput {
|
|
37
|
+
width: 100%;
|
|
38
|
+
height: auto;
|
|
39
|
+
}
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, question: str, default: str, on_submit: Callable[[str], None], **kwargs):
|
|
43
|
+
super().__init__(**kwargs)
|
|
44
|
+
self.question = question
|
|
45
|
+
self.default = default
|
|
46
|
+
self.on_submit_callback = on_submit
|
|
47
|
+
|
|
48
|
+
def compose(self):
|
|
49
|
+
yield BoldText(self.question)
|
|
50
|
+
yield MultilineInput(
|
|
51
|
+
id="prompt-textarea",
|
|
52
|
+
soft_wrap=True
|
|
53
|
+
)
|
|
54
|
+
yield DimText("Press Ctrl+D to submit, Enter for new line")
|
|
55
|
+
|
|
56
|
+
def on_mount(self):
|
|
57
|
+
"""Focus textarea when mounted and set default text."""
|
|
58
|
+
self.call_after_refresh(self._setup_textarea)
|
|
59
|
+
|
|
60
|
+
def _setup_textarea(self):
|
|
61
|
+
"""Set default text, focus the textarea widget and scroll into view."""
|
|
62
|
+
try:
|
|
63
|
+
textarea = self.query_one(MultilineInput)
|
|
64
|
+
# Set default text AFTER mounting (TextArea doesn't accept text in constructor)
|
|
65
|
+
if self.default:
|
|
66
|
+
textarea.text = self.default
|
|
67
|
+
# TextArea needs a refresh to recalculate content after text change
|
|
68
|
+
# Call focus/scroll after that refresh
|
|
69
|
+
self.call_after_refresh(self._focus_and_scroll)
|
|
70
|
+
else:
|
|
71
|
+
# No default text, focus immediately
|
|
72
|
+
self._focus_and_scroll()
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
def _focus_and_scroll(self):
|
|
77
|
+
"""Focus and scroll the textarea after it has processed the text."""
|
|
78
|
+
try:
|
|
79
|
+
textarea = self.query_one(MultilineInput)
|
|
80
|
+
self.app.set_focus(textarea)
|
|
81
|
+
self.scroll_visible(animate=False)
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
def on_multiline_input_submitted(self, message: MultilineInput.Submitted):
|
|
86
|
+
"""Handle submission from MultilineInput."""
|
|
87
|
+
self.on_submit_callback(message.value)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
StyledOptionList Widget
|
|
3
|
+
|
|
4
|
+
Custom OptionList that renders options with bold titles and dim descriptions.
|
|
5
|
+
Uses the same styling as BoldText and DimText components.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from textual.widgets import OptionList
|
|
11
|
+
from textual.widgets.option_list import Option
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class StyledOption:
|
|
16
|
+
"""
|
|
17
|
+
Option with styled title and description.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
id: Unique identifier for the option
|
|
21
|
+
title: Title text (rendered with bold styling)
|
|
22
|
+
description: Description text (rendered with dim styling)
|
|
23
|
+
disabled: Whether the option is disabled
|
|
24
|
+
"""
|
|
25
|
+
id: str
|
|
26
|
+
title: str
|
|
27
|
+
description: str = ""
|
|
28
|
+
disabled: bool = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class StyledOptionList(OptionList):
|
|
32
|
+
"""
|
|
33
|
+
OptionList that renders options with bold titles and dim descriptions.
|
|
34
|
+
|
|
35
|
+
This widget provides a consistent way to display lists of options across
|
|
36
|
+
the application, using BoldText and DimText styling patterns.
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
from titan_cli.ui.tui.widgets import StyledOptionList, StyledOption
|
|
40
|
+
|
|
41
|
+
options = [
|
|
42
|
+
StyledOption(
|
|
43
|
+
id="workflow1",
|
|
44
|
+
title="Release Notes",
|
|
45
|
+
description="Generate multi-brand weekly release notes"
|
|
46
|
+
),
|
|
47
|
+
StyledOption(
|
|
48
|
+
id="workflow2",
|
|
49
|
+
title="Create PR",
|
|
50
|
+
description="Create a pull request with AI-generated description"
|
|
51
|
+
),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
option_list = StyledOptionList(*options)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, *options: StyledOption, **kwargs):
|
|
58
|
+
"""
|
|
59
|
+
Initialize StyledOptionList with styled options.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
*options: StyledOption instances to display
|
|
63
|
+
**kwargs: Additional arguments passed to OptionList
|
|
64
|
+
"""
|
|
65
|
+
# Convert StyledOptions to Option objects with markup
|
|
66
|
+
option_objects = []
|
|
67
|
+
for opt in options:
|
|
68
|
+
if opt.description:
|
|
69
|
+
# Title in bold, description in dim, separated by newline
|
|
70
|
+
prompt = f"[bold]{opt.title}[/bold]\n[dim]{opt.description}[/dim]"
|
|
71
|
+
else:
|
|
72
|
+
# Just title in bold
|
|
73
|
+
prompt = f"[bold]{opt.title}[/bold]"
|
|
74
|
+
|
|
75
|
+
option_objects.append(
|
|
76
|
+
Option(prompt, id=opt.id, disabled=opt.disabled)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
super().__init__(*option_objects, **kwargs)
|
|
80
|
+
|
|
81
|
+
def add_styled_option(self, option: StyledOption) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Add a new styled option to the list.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
option: StyledOption to add
|
|
87
|
+
"""
|
|
88
|
+
if option.description:
|
|
89
|
+
prompt = f"[bold]{option.title}[/bold]\n[dim]{option.description}[/dim]"
|
|
90
|
+
else:
|
|
91
|
+
prompt = f"[bold]{option.title}[/bold]"
|
|
92
|
+
|
|
93
|
+
self.add_option(Option(prompt, id=option.id, disabled=option.disabled))
|
|
94
|
+
|
|
95
|
+
def set_styled_options(self, options: List[StyledOption]) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Replace all options with new styled options.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
options: List of StyledOption instances
|
|
101
|
+
"""
|
|
102
|
+
# Clear existing options
|
|
103
|
+
self.clear_options()
|
|
104
|
+
|
|
105
|
+
# Add new options
|
|
106
|
+
for opt in options:
|
|
107
|
+
self.add_styled_option(opt)
|