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.
Files changed (54) hide show
  1. titan_cli/core/workflows/project_step_source.py +52 -7
  2. titan_cli/core/workflows/workflow_filter_service.py +6 -4
  3. titan_cli/engine/__init__.py +5 -1
  4. titan_cli/engine/results.py +31 -1
  5. titan_cli/engine/steps/ai_assistant_step.py +18 -18
  6. titan_cli/engine/workflow_executor.py +7 -2
  7. titan_cli/ui/tui/screens/plugin_config_wizard.py +16 -0
  8. titan_cli/ui/tui/screens/workflow_execution.py +22 -24
  9. titan_cli/ui/tui/screens/workflows.py +8 -4
  10. titan_cli/ui/tui/textual_components.py +293 -189
  11. titan_cli/ui/tui/textual_workflow_executor.py +30 -2
  12. titan_cli/ui/tui/theme.py +34 -5
  13. titan_cli/ui/tui/widgets/__init__.py +15 -0
  14. titan_cli/ui/tui/widgets/multiline_input.py +32 -0
  15. titan_cli/ui/tui/widgets/prompt_choice.py +138 -0
  16. titan_cli/ui/tui/widgets/prompt_input.py +74 -0
  17. titan_cli/ui/tui/widgets/prompt_selection_list.py +150 -0
  18. titan_cli/ui/tui/widgets/prompt_textarea.py +87 -0
  19. titan_cli/ui/tui/widgets/styled_option_list.py +107 -0
  20. titan_cli/ui/tui/widgets/text.py +51 -130
  21. {titan_cli-0.1.5.dist-info → titan_cli-0.1.7.dist-info}/METADATA +5 -10
  22. {titan_cli-0.1.5.dist-info → titan_cli-0.1.7.dist-info}/RECORD +54 -41
  23. {titan_cli-0.1.5.dist-info → titan_cli-0.1.7.dist-info}/WHEEL +1 -1
  24. titan_plugin_git/clients/git_client.py +59 -2
  25. titan_plugin_git/plugin.py +10 -0
  26. titan_plugin_git/steps/ai_commit_message_step.py +8 -8
  27. titan_plugin_git/steps/branch_steps.py +6 -6
  28. titan_plugin_git/steps/checkout_step.py +66 -0
  29. titan_plugin_git/steps/commit_step.py +3 -3
  30. titan_plugin_git/steps/create_branch_step.py +131 -0
  31. titan_plugin_git/steps/diff_summary_step.py +11 -13
  32. titan_plugin_git/steps/pull_step.py +70 -0
  33. titan_plugin_git/steps/push_step.py +3 -3
  34. titan_plugin_git/steps/restore_original_branch_step.py +97 -0
  35. titan_plugin_git/steps/save_current_branch_step.py +82 -0
  36. titan_plugin_git/steps/status_step.py +23 -13
  37. titan_plugin_git/workflows/commit-ai.yaml +4 -3
  38. titan_plugin_github/steps/ai_pr_step.py +90 -22
  39. titan_plugin_github/steps/create_pr_step.py +8 -8
  40. titan_plugin_github/steps/github_prompt_steps.py +13 -13
  41. titan_plugin_github/steps/issue_steps.py +14 -15
  42. titan_plugin_github/steps/preview_step.py +8 -8
  43. titan_plugin_github/workflows/create-pr-ai.yaml +1 -11
  44. titan_plugin_jira/messages.py +12 -0
  45. titan_plugin_jira/plugin.py +4 -0
  46. titan_plugin_jira/steps/ai_analyze_issue_step.py +6 -6
  47. titan_plugin_jira/steps/get_issue_step.py +7 -7
  48. titan_plugin_jira/steps/list_versions_step.py +133 -0
  49. titan_plugin_jira/steps/prompt_select_issue_step.py +8 -9
  50. titan_plugin_jira/steps/search_jql_step.py +191 -0
  51. titan_plugin_jira/steps/search_saved_query_step.py +13 -13
  52. titan_plugin_jira/utils/__init__.py +1 -1
  53. {titan_cli-0.1.5.dist-info/licenses → titan_cli-0.1.7.dist-info}/LICENSE +0 -0
  54. {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)