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
|
@@ -47,7 +47,7 @@ def ai_suggest_pr_description_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
47
47
|
|
|
48
48
|
# Check if AI is configured
|
|
49
49
|
if not ctx.ai or not ctx.ai.is_available():
|
|
50
|
-
ctx.textual.
|
|
50
|
+
ctx.textual.dim_text(msg.GitHub.AI.AI_NOT_CONFIGURED)
|
|
51
51
|
ctx.textual.end_step("skip")
|
|
52
52
|
return Skip(msg.GitHub.AI.AI_NOT_CONFIGURED)
|
|
53
53
|
|
|
@@ -66,10 +66,10 @@ def ai_suggest_pr_description_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
66
66
|
|
|
67
67
|
try:
|
|
68
68
|
# Show progress
|
|
69
|
-
ctx.textual.
|
|
69
|
+
ctx.textual.dim_text(msg.GitHub.AI.ANALYZING_BRANCH_DIFF.format(
|
|
70
70
|
head_branch=head_branch,
|
|
71
71
|
base_branch=base_branch
|
|
72
|
-
)
|
|
72
|
+
))
|
|
73
73
|
|
|
74
74
|
# Create PRAgent instance
|
|
75
75
|
pr_agent = PRAgent(
|
|
@@ -88,59 +88,127 @@ def ai_suggest_pr_description_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
88
88
|
|
|
89
89
|
# Check if PR content was generated (need commits in branch)
|
|
90
90
|
if not analysis.pr_title or not analysis.pr_body:
|
|
91
|
-
ctx.textual.
|
|
91
|
+
ctx.textual.dim_text("No commits found in branch to generate PR description.")
|
|
92
92
|
ctx.textual.end_step("skip")
|
|
93
93
|
return Skip("No commits found for PR generation")
|
|
94
94
|
|
|
95
95
|
# Show PR size info
|
|
96
96
|
if analysis.pr_size:
|
|
97
|
-
ctx.textual.
|
|
97
|
+
ctx.textual.dim_text(msg.GitHub.AI.PR_SIZE_INFO.format(
|
|
98
98
|
pr_size=analysis.pr_size,
|
|
99
99
|
files_changed=analysis.files_changed,
|
|
100
100
|
diff_lines=analysis.lines_changed,
|
|
101
101
|
max_chars="varies by size"
|
|
102
|
-
)
|
|
102
|
+
))
|
|
103
103
|
|
|
104
104
|
# Show PR preview to user
|
|
105
105
|
ctx.textual.text("") # spacing
|
|
106
|
-
ctx.textual.
|
|
106
|
+
ctx.textual.bold_text(msg.GitHub.AI.AI_GENERATED_PR_TITLE)
|
|
107
107
|
ctx.textual.text("") # spacing
|
|
108
108
|
|
|
109
109
|
# Show title
|
|
110
|
-
ctx.textual.
|
|
111
|
-
ctx.textual.
|
|
110
|
+
ctx.textual.bold_text(msg.GitHub.AI.TITLE_LABEL)
|
|
111
|
+
ctx.textual.primary_text(f" {analysis.pr_title}")
|
|
112
112
|
|
|
113
113
|
# Warn if title is too long
|
|
114
114
|
if len(analysis.pr_title) > 72:
|
|
115
|
-
ctx.textual.
|
|
115
|
+
ctx.textual.warning_text(msg.GitHub.AI.TITLE_TOO_LONG_WARNING.format(
|
|
116
116
|
length=len(analysis.pr_title)
|
|
117
|
-
)
|
|
117
|
+
))
|
|
118
118
|
|
|
119
119
|
ctx.textual.text("") # spacing
|
|
120
120
|
|
|
121
121
|
# Show description
|
|
122
|
-
ctx.textual.
|
|
122
|
+
ctx.textual.bold_text(msg.GitHub.AI.DESCRIPTION_LABEL)
|
|
123
123
|
# Render markdown in a scrollable container
|
|
124
124
|
ctx.textual.markdown(analysis.pr_body)
|
|
125
125
|
|
|
126
|
+
# Scroll to show the choice buttons below
|
|
127
|
+
ctx.textual.scroll_to_end()
|
|
128
|
+
|
|
126
129
|
ctx.textual.text("") # spacing
|
|
127
130
|
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
# Ask user what to do with the AI suggestion
|
|
132
|
+
from titan_cli.ui.tui.widgets import ChoiceOption
|
|
133
|
+
|
|
134
|
+
options = [
|
|
135
|
+
ChoiceOption(value="use", label="Use as-is", variant="primary"),
|
|
136
|
+
ChoiceOption(value="edit", label="Edit", variant="default"),
|
|
137
|
+
ChoiceOption(value="reject", label="Reject", variant="error"),
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
choice = ctx.textual.ask_choice(
|
|
141
|
+
"What would you like to do with this PR description?",
|
|
142
|
+
options
|
|
132
143
|
)
|
|
133
144
|
|
|
134
|
-
|
|
135
|
-
|
|
145
|
+
# Handle user choice
|
|
146
|
+
if choice == "reject":
|
|
147
|
+
ctx.textual.warning_text(msg.GitHub.AI.AI_SUGGESTION_REJECTED)
|
|
136
148
|
ctx.textual.end_step("skip")
|
|
137
149
|
return Skip("User rejected AI-generated PR")
|
|
138
150
|
|
|
151
|
+
# Store initial values
|
|
152
|
+
pr_title = analysis.pr_title
|
|
153
|
+
pr_body = analysis.pr_body
|
|
154
|
+
|
|
155
|
+
if choice == "edit":
|
|
156
|
+
# Edit loop: allow user to edit until they confirm
|
|
157
|
+
while True:
|
|
158
|
+
ctx.textual.text("")
|
|
159
|
+
ctx.textual.dim_text("Edit the PR content below (first line = title, rest = description)")
|
|
160
|
+
|
|
161
|
+
# Combine title and body as markdown for editing
|
|
162
|
+
combined_markdown = f"{pr_title}\n\n{pr_body}"
|
|
163
|
+
|
|
164
|
+
# Ask user to edit
|
|
165
|
+
edited_content = ctx.textual.ask_multiline(
|
|
166
|
+
"Edit PR content:",
|
|
167
|
+
default=combined_markdown
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Scroll to show what comes next after editing
|
|
171
|
+
ctx.textual.scroll_to_end()
|
|
172
|
+
|
|
173
|
+
if not edited_content or not edited_content.strip():
|
|
174
|
+
ctx.textual.warning_text("PR content cannot be empty")
|
|
175
|
+
ctx.textual.end_step("skip")
|
|
176
|
+
return Skip("Empty PR content")
|
|
177
|
+
|
|
178
|
+
# Parse: first line = title, rest = body
|
|
179
|
+
lines = edited_content.strip().split("\n", 1)
|
|
180
|
+
pr_title = lines[0].strip()
|
|
181
|
+
pr_body = lines[1].strip() if len(lines) > 1 else ""
|
|
182
|
+
|
|
183
|
+
# Show final preview
|
|
184
|
+
ctx.textual.text("")
|
|
185
|
+
ctx.textual.bold_text("Final Preview:")
|
|
186
|
+
ctx.textual.text("")
|
|
187
|
+
ctx.textual.bold_text("Title:")
|
|
188
|
+
ctx.textual.primary_text(f" {pr_title}")
|
|
189
|
+
ctx.textual.text("")
|
|
190
|
+
ctx.textual.bold_text("Description:")
|
|
191
|
+
ctx.textual.markdown(pr_body)
|
|
192
|
+
ctx.textual.text("")
|
|
193
|
+
|
|
194
|
+
# Scroll to show the confirm question below
|
|
195
|
+
ctx.textual.scroll_to_end()
|
|
196
|
+
|
|
197
|
+
# Confirm
|
|
198
|
+
confirmed = ctx.textual.ask_confirm(
|
|
199
|
+
"Use this PR content?",
|
|
200
|
+
default=True
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if confirmed:
|
|
204
|
+
break
|
|
205
|
+
# If not confirmed, loop back to edit
|
|
206
|
+
|
|
139
207
|
# Success - save to context
|
|
140
208
|
metadata = {
|
|
141
209
|
"ai_generated": True,
|
|
142
|
-
"pr_title":
|
|
143
|
-
"pr_body":
|
|
210
|
+
"pr_title": pr_title,
|
|
211
|
+
"pr_body": pr_body,
|
|
144
212
|
"pr_size": analysis.pr_size
|
|
145
213
|
}
|
|
146
214
|
|
|
@@ -152,8 +220,8 @@ def ai_suggest_pr_description_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
152
220
|
|
|
153
221
|
except Exception as e:
|
|
154
222
|
# Don't fail the workflow, just skip AI and use manual prompts
|
|
155
|
-
ctx.textual.
|
|
156
|
-
ctx.textual.
|
|
223
|
+
ctx.textual.warning_text(msg.GitHub.AI.AI_GENERATION_FAILED.format(e=e))
|
|
224
|
+
ctx.textual.dim_text(msg.GitHub.AI.FALLBACK_TO_MANUAL)
|
|
157
225
|
|
|
158
226
|
ctx.textual.end_step("skip")
|
|
159
227
|
return Skip(msg.GitHub.AI.AI_GENERATION_FAILED.format(e=e))
|
|
@@ -37,11 +37,11 @@ def create_pr_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
37
37
|
|
|
38
38
|
# 1. Get GitHub client from context
|
|
39
39
|
if not ctx.github:
|
|
40
|
-
ctx.textual.
|
|
40
|
+
ctx.textual.error_text("GitHub client is not available in the workflow context.")
|
|
41
41
|
ctx.textual.end_step("error")
|
|
42
42
|
return Error("GitHub client is not available in the workflow context.")
|
|
43
43
|
if not ctx.git:
|
|
44
|
-
ctx.textual.
|
|
44
|
+
ctx.textual.error_text("Git client is not available in the workflow context.")
|
|
45
45
|
ctx.textual.end_step("error")
|
|
46
46
|
return Error("Git client is not available in the workflow context.")
|
|
47
47
|
|
|
@@ -53,7 +53,7 @@ def create_pr_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
53
53
|
is_draft = ctx.get("pr_is_draft", False) # Default to not a draft
|
|
54
54
|
|
|
55
55
|
if not all([title, base, head]):
|
|
56
|
-
ctx.textual.
|
|
56
|
+
ctx.textual.error_text("Missing required context for creating a pull request: pr_title, pr_head_branch.")
|
|
57
57
|
ctx.textual.end_step("error")
|
|
58
58
|
return Error(
|
|
59
59
|
"Missing required context for creating a pull request: pr_title, pr_head_branch."
|
|
@@ -67,16 +67,16 @@ def create_pr_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
67
67
|
assignees = [current_user]
|
|
68
68
|
except GitHubAPIError as e:
|
|
69
69
|
# Log warning but continue without assignee
|
|
70
|
-
ctx.textual.
|
|
70
|
+
ctx.textual.warning_text(f"Could not get current user for auto-assign: {e}")
|
|
71
71
|
|
|
72
72
|
# 4. Call the client method
|
|
73
73
|
try:
|
|
74
|
-
ctx.textual.
|
|
74
|
+
ctx.textual.dim_text(f"Creating pull request '{title}' from {head} to {base}...")
|
|
75
75
|
pr = ctx.github.create_pull_request(
|
|
76
76
|
title=title, body=body, base=base, head=head, draft=is_draft, assignees=assignees
|
|
77
77
|
)
|
|
78
78
|
ctx.textual.text("") # spacing
|
|
79
|
-
ctx.textual.
|
|
79
|
+
ctx.textual.success_text(msg.GitHub.PR_CREATED.format(number=pr["number"], url=pr["url"]))
|
|
80
80
|
|
|
81
81
|
# 4. Return Success with PR info
|
|
82
82
|
ctx.textual.end_step("success")
|
|
@@ -85,11 +85,11 @@ def create_pr_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
85
85
|
metadata={"pr_number": pr["number"], "pr_url": pr["url"]},
|
|
86
86
|
)
|
|
87
87
|
except GitHubAPIError as e:
|
|
88
|
-
ctx.textual.
|
|
88
|
+
ctx.textual.error_text(f"Failed to create pull request: {e}")
|
|
89
89
|
ctx.textual.end_step("error")
|
|
90
90
|
return Error(f"Failed to create pull request: {e}")
|
|
91
91
|
except Exception as e:
|
|
92
|
-
ctx.textual.
|
|
92
|
+
ctx.textual.error_text(f"An unexpected error occurred while creating the pull request: {e}")
|
|
93
93
|
ctx.textual.end_step("error")
|
|
94
94
|
return Error(
|
|
95
95
|
f"An unexpected error occurred while creating the pull request: {e}"
|
|
@@ -28,7 +28,7 @@ def prompt_for_pr_title_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
28
28
|
|
|
29
29
|
# Skip if title already exists (e.g., from AI generation)
|
|
30
30
|
if ctx.get("pr_title"):
|
|
31
|
-
ctx.textual.
|
|
31
|
+
ctx.textual.dim_text("PR title already provided, skipping manual prompt.")
|
|
32
32
|
ctx.textual.end_step("skip")
|
|
33
33
|
return Skip("PR title already provided, skipping manual prompt.")
|
|
34
34
|
|
|
@@ -71,7 +71,7 @@ def prompt_for_pr_body_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
71
71
|
|
|
72
72
|
# Skip if body already exists (e.g., from AI generation)
|
|
73
73
|
if ctx.get("pr_body"):
|
|
74
|
-
ctx.textual.
|
|
74
|
+
ctx.textual.dim_text("PR body already provided, skipping manual prompt.")
|
|
75
75
|
ctx.textual.end_step("skip")
|
|
76
76
|
return Skip("PR body already provided, skipping manual prompt.")
|
|
77
77
|
|
|
@@ -112,7 +112,7 @@ def prompt_for_issue_body_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
112
112
|
|
|
113
113
|
# Skip if body already exists (e.g., from AI generation)
|
|
114
114
|
if ctx.get("issue_body"):
|
|
115
|
-
ctx.textual.
|
|
115
|
+
ctx.textual.dim_text("Issue body already provided, skipping manual prompt.")
|
|
116
116
|
ctx.textual.end_step("skip")
|
|
117
117
|
return Skip("Issue body already provided, skipping manual prompt.")
|
|
118
118
|
|
|
@@ -140,7 +140,7 @@ def prompt_for_self_assign_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
140
140
|
ctx.textual.begin_step("Assign Issue")
|
|
141
141
|
|
|
142
142
|
if not ctx.github:
|
|
143
|
-
ctx.textual.
|
|
143
|
+
ctx.textual.error_text("GitHub client not available")
|
|
144
144
|
ctx.textual.end_step("error")
|
|
145
145
|
return Error("GitHub client not available")
|
|
146
146
|
|
|
@@ -151,17 +151,17 @@ def prompt_for_self_assign_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
151
151
|
if current_user not in assignees:
|
|
152
152
|
assignees.append(current_user)
|
|
153
153
|
ctx.set("assignees", assignees)
|
|
154
|
-
ctx.textual.
|
|
154
|
+
ctx.textual.success_text(f"Issue will be assigned to {current_user}")
|
|
155
155
|
ctx.textual.end_step("success")
|
|
156
156
|
return Success(f"Issue will be assigned to {current_user}")
|
|
157
|
-
ctx.textual.
|
|
157
|
+
ctx.textual.dim_text("Issue will not be assigned to current user")
|
|
158
158
|
ctx.textual.end_step("success")
|
|
159
159
|
return Success("Issue will not be assigned to current user")
|
|
160
160
|
except (KeyboardInterrupt, EOFError):
|
|
161
161
|
ctx.textual.end_step("error")
|
|
162
162
|
return Error("User cancelled.")
|
|
163
163
|
except Exception as e:
|
|
164
|
-
ctx.textual.
|
|
164
|
+
ctx.textual.error_text(f"Failed to prompt for self-assign: {e}")
|
|
165
165
|
ctx.textual.end_step("error")
|
|
166
166
|
return Error(f"Failed to prompt for self-assign: {e}", exception=e)
|
|
167
167
|
|
|
@@ -177,19 +177,19 @@ def prompt_for_labels_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
177
177
|
ctx.textual.begin_step("Select Labels")
|
|
178
178
|
|
|
179
179
|
if not ctx.github:
|
|
180
|
-
ctx.textual.
|
|
180
|
+
ctx.textual.error_text("GitHub client not available")
|
|
181
181
|
ctx.textual.end_step("error")
|
|
182
182
|
return Error("GitHub client not available")
|
|
183
183
|
|
|
184
184
|
try:
|
|
185
185
|
available_labels = ctx.github.list_labels()
|
|
186
186
|
if not available_labels:
|
|
187
|
-
ctx.textual.
|
|
187
|
+
ctx.textual.dim_text("No labels found in the repository.")
|
|
188
188
|
ctx.textual.end_step("skip")
|
|
189
189
|
return Skip("No labels found in the repository.")
|
|
190
190
|
|
|
191
191
|
# Show available labels
|
|
192
|
-
ctx.textual.
|
|
192
|
+
ctx.textual.dim_text(f"Available labels: {', '.join(available_labels)}")
|
|
193
193
|
|
|
194
194
|
# Get default labels as comma-separated string
|
|
195
195
|
existing_labels = ctx.get("labels", [])
|
|
@@ -209,15 +209,15 @@ def prompt_for_labels_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
209
209
|
|
|
210
210
|
ctx.set("labels", selected_labels)
|
|
211
211
|
if selected_labels:
|
|
212
|
-
ctx.textual.
|
|
212
|
+
ctx.textual.success_text(f"Selected labels: {', '.join(selected_labels)}")
|
|
213
213
|
else:
|
|
214
|
-
ctx.textual.
|
|
214
|
+
ctx.textual.dim_text("No labels selected")
|
|
215
215
|
ctx.textual.end_step("success")
|
|
216
216
|
return Success("Labels selected")
|
|
217
217
|
except (KeyboardInterrupt, EOFError):
|
|
218
218
|
ctx.textual.end_step("error")
|
|
219
219
|
return Error("User cancelled.")
|
|
220
220
|
except Exception as e:
|
|
221
|
-
ctx.textual.
|
|
221
|
+
ctx.textual.error_text(f"Failed to prompt for labels: {e}")
|
|
222
222
|
ctx.textual.end_step("error")
|
|
223
223
|
return Error(f"Failed to prompt for labels: {e}", exception=e)
|
|
@@ -2,6 +2,7 @@ import ast
|
|
|
2
2
|
from titan_cli.engine.context import WorkflowContext
|
|
3
3
|
from titan_cli.engine.results import WorkflowResult, Success, Error, Skip
|
|
4
4
|
from ..agents.issue_generator import IssueGeneratorAgent
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
def ai_suggest_issue_title_and_body_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
7
8
|
"""
|
|
@@ -15,18 +16,17 @@ def ai_suggest_issue_title_and_body_step(ctx: WorkflowContext) -> WorkflowResult
|
|
|
15
16
|
ctx.textual.begin_step("Categorize and Generate Issue")
|
|
16
17
|
|
|
17
18
|
if not ctx.ai:
|
|
18
|
-
ctx.textual.
|
|
19
|
+
ctx.textual.dim_text("AI client not available")
|
|
19
20
|
ctx.textual.end_step("skip")
|
|
20
21
|
return Skip("AI client not available")
|
|
21
22
|
|
|
22
23
|
issue_body_prompt = ctx.get("issue_body")
|
|
23
24
|
if not issue_body_prompt:
|
|
24
|
-
ctx.textual.
|
|
25
|
+
ctx.textual.error_text("issue_body not found in context")
|
|
25
26
|
ctx.textual.end_step("error")
|
|
26
27
|
return Error("issue_body not found in context")
|
|
27
28
|
|
|
28
|
-
ctx.textual.
|
|
29
|
-
|
|
29
|
+
ctx.textual.dim_text("Using AI to categorize and generate issue...")
|
|
30
30
|
try:
|
|
31
31
|
# Get available labels from repository for smart mapping
|
|
32
32
|
available_labels = None
|
|
@@ -40,7 +40,6 @@ def ai_suggest_issue_title_and_body_step(ctx: WorkflowContext) -> WorkflowResult
|
|
|
40
40
|
# Get template directory from repo path
|
|
41
41
|
template_dir = None
|
|
42
42
|
if ctx.git:
|
|
43
|
-
from pathlib import Path
|
|
44
43
|
template_dir = Path(ctx.git.repo_path) / ".github" / "ISSUE_TEMPLATE"
|
|
45
44
|
|
|
46
45
|
issue_generator = IssueGeneratorAgent(ctx.ai, template_dir=template_dir)
|
|
@@ -53,12 +52,12 @@ def ai_suggest_issue_title_and_body_step(ctx: WorkflowContext) -> WorkflowResult
|
|
|
53
52
|
template_used = result.get("template_used", False)
|
|
54
53
|
|
|
55
54
|
ctx.textual.text("") # spacing
|
|
56
|
-
ctx.textual.
|
|
55
|
+
ctx.textual.success_text(f"Category detected: {category}")
|
|
57
56
|
|
|
58
57
|
if template_used:
|
|
59
|
-
ctx.textual.
|
|
58
|
+
ctx.textual.success_text(f"Using template: {category}.md")
|
|
60
59
|
else:
|
|
61
|
-
ctx.textual.
|
|
60
|
+
ctx.textual.warning_text(f"No template found for {category}, using default structure")
|
|
62
61
|
|
|
63
62
|
ctx.set("issue_title", result["title"])
|
|
64
63
|
ctx.set("issue_body", result["body"])
|
|
@@ -68,7 +67,7 @@ def ai_suggest_issue_title_and_body_step(ctx: WorkflowContext) -> WorkflowResult
|
|
|
68
67
|
ctx.textual.end_step("success")
|
|
69
68
|
return Success(f"AI-generated issue ({category}) created successfully")
|
|
70
69
|
except Exception as e:
|
|
71
|
-
ctx.textual.
|
|
70
|
+
ctx.textual.error_text(f"Failed to generate issue: {e}")
|
|
72
71
|
ctx.textual.end_step("error")
|
|
73
72
|
return Error(f"Failed to generate issue: {e}")
|
|
74
73
|
|
|
@@ -84,7 +83,7 @@ def create_issue_steps(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
84
83
|
ctx.textual.begin_step("Create Issue")
|
|
85
84
|
|
|
86
85
|
if not ctx.github:
|
|
87
|
-
ctx.textual.
|
|
86
|
+
ctx.textual.error_text("GitHub client not available")
|
|
88
87
|
ctx.textual.end_step("error")
|
|
89
88
|
return Error("GitHub client not available")
|
|
90
89
|
|
|
@@ -113,12 +112,12 @@ def create_issue_steps(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
113
112
|
labels = []
|
|
114
113
|
|
|
115
114
|
if not issue_title:
|
|
116
|
-
ctx.textual.
|
|
115
|
+
ctx.textual.error_text("issue_title not found in context")
|
|
117
116
|
ctx.textual.end_step("error")
|
|
118
117
|
return Error("issue_title not found in context")
|
|
119
118
|
|
|
120
119
|
if not issue_body:
|
|
121
|
-
ctx.textual.
|
|
120
|
+
ctx.textual.error_text("issue_body not found in context")
|
|
122
121
|
ctx.textual.end_step("error")
|
|
123
122
|
return Error("issue_body not found in context")
|
|
124
123
|
|
|
@@ -137,7 +136,7 @@ def create_issue_steps(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
137
136
|
pass
|
|
138
137
|
|
|
139
138
|
try:
|
|
140
|
-
ctx.textual.
|
|
139
|
+
ctx.textual.dim_text(f"Creating issue: {issue_title}...")
|
|
141
140
|
issue = ctx.github.create_issue(
|
|
142
141
|
title=issue_title,
|
|
143
142
|
body=issue_body,
|
|
@@ -145,13 +144,13 @@ def create_issue_steps(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
145
144
|
labels=labels,
|
|
146
145
|
)
|
|
147
146
|
ctx.textual.text("") # spacing
|
|
148
|
-
ctx.textual.
|
|
147
|
+
ctx.textual.success_text(f"Successfully created issue #{issue.number}")
|
|
149
148
|
ctx.textual.end_step("success")
|
|
150
149
|
return Success(
|
|
151
150
|
f"Successfully created issue #{issue.number}",
|
|
152
151
|
metadata={"issue": issue}
|
|
153
152
|
)
|
|
154
153
|
except Exception as e:
|
|
155
|
-
ctx.textual.
|
|
154
|
+
ctx.textual.error_text(f"Failed to create issue: {e}")
|
|
156
155
|
ctx.textual.end_step("error")
|
|
157
156
|
return Error(f"Failed to create issue: {e}")
|
|
@@ -15,22 +15,22 @@ def preview_and_confirm_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
15
15
|
issue_body = ctx.get("issue_body")
|
|
16
16
|
|
|
17
17
|
if not issue_title or not issue_body:
|
|
18
|
-
ctx.textual.
|
|
18
|
+
ctx.textual.error_text("issue_title or issue_body not found in context")
|
|
19
19
|
ctx.textual.end_step("error")
|
|
20
20
|
return Error("issue_title or issue_body not found in context")
|
|
21
21
|
|
|
22
22
|
# Show preview header
|
|
23
23
|
ctx.textual.text("") # spacing
|
|
24
|
-
ctx.textual.
|
|
24
|
+
ctx.textual.bold_text("AI-Generated Issue Preview")
|
|
25
25
|
ctx.textual.text("") # spacing
|
|
26
26
|
|
|
27
27
|
# Show title
|
|
28
|
-
ctx.textual.
|
|
29
|
-
ctx.textual.
|
|
28
|
+
ctx.textual.bold_text("Title:")
|
|
29
|
+
ctx.textual.primary_text(f" {issue_title}")
|
|
30
30
|
ctx.textual.text("") # spacing
|
|
31
31
|
|
|
32
32
|
# Show description
|
|
33
|
-
ctx.textual.
|
|
33
|
+
ctx.textual.bold_text("Description:")
|
|
34
34
|
# Render markdown in a scrollable container
|
|
35
35
|
ctx.textual.markdown(issue_body)
|
|
36
36
|
|
|
@@ -38,14 +38,14 @@ def preview_and_confirm_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
38
38
|
|
|
39
39
|
try:
|
|
40
40
|
if not ctx.textual.ask_confirm("Use this AI-generated issue?", default=True):
|
|
41
|
-
ctx.textual.
|
|
41
|
+
ctx.textual.warning_text("User rejected AI-generated issue")
|
|
42
42
|
ctx.textual.end_step("error")
|
|
43
43
|
return Error("User rejected AI-generated issue")
|
|
44
44
|
except (KeyboardInterrupt, EOFError):
|
|
45
|
-
ctx.textual.
|
|
45
|
+
ctx.textual.error_text("User cancelled operation")
|
|
46
46
|
ctx.textual.end_step("error")
|
|
47
47
|
return Error("User cancelled operation")
|
|
48
48
|
|
|
49
|
-
ctx.textual.
|
|
49
|
+
ctx.textual.success_text("User confirmed AI-generated issue")
|
|
50
50
|
ctx.textual.end_step("success")
|
|
51
51
|
return Success("User confirmed AI-generated issue")
|
|
@@ -25,6 +25,7 @@ steps:
|
|
|
25
25
|
step: show_branch_diff_summary
|
|
26
26
|
|
|
27
27
|
# AI generates PR title and description from branch commits
|
|
28
|
+
# User can choose to use as-is, edit, or reject
|
|
28
29
|
- id: ai_pr_description
|
|
29
30
|
name: "AI PR Description"
|
|
30
31
|
plugin: github
|
|
@@ -32,17 +33,6 @@ steps:
|
|
|
32
33
|
|
|
33
34
|
- hook: before_push
|
|
34
35
|
|
|
35
|
-
# Fallback to manual prompts if AI was rejected
|
|
36
|
-
- id: prompt_pr_title
|
|
37
|
-
name: "Prompt for PR Title"
|
|
38
|
-
plugin: github
|
|
39
|
-
step: prompt_for_pr_title
|
|
40
|
-
|
|
41
|
-
- id: prompt_pr_body
|
|
42
|
-
name: "Prompt for PR Body"
|
|
43
|
-
plugin: github
|
|
44
|
-
step: prompt_for_pr_body
|
|
45
|
-
|
|
46
36
|
- id: create_pr
|
|
47
37
|
name: "Create Pull Request"
|
|
48
38
|
plugin: github
|
titan_plugin_jira/messages.py
CHANGED
|
@@ -101,6 +101,18 @@ class Messages:
|
|
|
101
101
|
ISSUE_SELECTION_CONFIRM: str = "Selected: {key} - {summary}"
|
|
102
102
|
SELECT_SUCCESS: str = "Selected issue: {key}"
|
|
103
103
|
|
|
104
|
+
class ReleaseNotes:
|
|
105
|
+
"""Release notes generation step messages"""
|
|
106
|
+
AI_NOT_AVAILABLE: str = "AI not available, using original JIRA summaries"
|
|
107
|
+
AI_GENERATION_FAILED: str = "AI generation failed: {error}"
|
|
108
|
+
USING_FALLBACK_SUMMARIES: str = "Using original JIRA summaries as fallback"
|
|
109
|
+
PROCESSING_ISSUES: str = "Processing {count} issues for version {version}"
|
|
110
|
+
GROUPED_BRANDS: str = "Grouped into {count} brands"
|
|
111
|
+
GENERATING_AI_DESCRIPTIONS: str = "Generating AI descriptions..."
|
|
112
|
+
SUCCESS: str = "Release notes generated successfully!"
|
|
113
|
+
COPY_INSTRUCTIONS: str = "Copy the markdown above and paste it into your release notes"
|
|
114
|
+
NO_ISSUES_FOUND: str = "No JIRA issues found to generate release notes"
|
|
115
|
+
|
|
104
116
|
class JIRA:
|
|
105
117
|
"""JIRA-specific messages"""
|
|
106
118
|
AUTHENTICATION_FAILED: str = "JIRA authentication failed. Check your API token."
|
titan_plugin_jira/plugin.py
CHANGED
|
@@ -243,14 +243,18 @@ class JiraPlugin(TitanPlugin):
|
|
|
243
243
|
Returns a dictionary of available workflow steps.
|
|
244
244
|
"""
|
|
245
245
|
from .steps.search_saved_query_step import search_saved_query_step
|
|
246
|
+
from .steps.search_jql_step import search_jql_step
|
|
246
247
|
from .steps.prompt_select_issue_step import prompt_select_issue_step
|
|
247
248
|
from .steps.get_issue_step import get_issue_step
|
|
248
249
|
from .steps.ai_analyze_issue_step import ai_analyze_issue_requirements_step
|
|
250
|
+
from .steps.list_versions_step import list_versions_step
|
|
249
251
|
return {
|
|
250
252
|
"search_saved_query": search_saved_query_step,
|
|
253
|
+
"search_jql": search_jql_step,
|
|
251
254
|
"prompt_select_issue": prompt_select_issue_step,
|
|
252
255
|
"get_issue": get_issue_step,
|
|
253
256
|
"ai_analyze_issue_requirements": ai_analyze_issue_requirements_step,
|
|
257
|
+
"list_versions": list_versions_step,
|
|
254
258
|
}
|
|
255
259
|
|
|
256
260
|
@property
|
|
@@ -36,14 +36,14 @@ def ai_analyze_issue_requirements_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
36
36
|
|
|
37
37
|
# Check if AI is available
|
|
38
38
|
if not ctx.ai or not ctx.ai.is_available():
|
|
39
|
-
ctx.textual.
|
|
39
|
+
ctx.textual.dim_text(msg.Steps.AIIssue.AI_NOT_CONFIGURED_SKIP)
|
|
40
40
|
ctx.textual.end_step("skip")
|
|
41
41
|
return Skip(msg.Steps.AIIssue.AI_NOT_CONFIGURED)
|
|
42
42
|
|
|
43
43
|
# Get issue to analyze
|
|
44
44
|
issue = ctx.get("jira_issue") or ctx.get("selected_issue")
|
|
45
45
|
if not issue:
|
|
46
|
-
ctx.textual.
|
|
46
|
+
ctx.textual.error_text(msg.Steps.AIIssue.NO_ISSUE_FOUND)
|
|
47
47
|
ctx.textual.end_step("error")
|
|
48
48
|
return Error(msg.Steps.AIIssue.NO_ISSUE_FOUND)
|
|
49
49
|
|
|
@@ -65,12 +65,12 @@ def ai_analyze_issue_requirements_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
65
65
|
|
|
66
66
|
# Display analysis
|
|
67
67
|
ctx.textual.text("")
|
|
68
|
-
ctx.textual.
|
|
68
|
+
ctx.textual.bold_primary_text("AI Analysis Results")
|
|
69
69
|
ctx.textual.text("")
|
|
70
70
|
|
|
71
71
|
# Show issue header
|
|
72
|
-
ctx.textual.
|
|
73
|
-
ctx.textual.
|
|
72
|
+
ctx.textual.bold_text(f"{issue.key}: {issue.summary}")
|
|
73
|
+
ctx.textual.dim_text(f"Type: {issue.issue_type} | Status: {issue.status} | Priority: {issue.priority}")
|
|
74
74
|
ctx.textual.text("")
|
|
75
75
|
|
|
76
76
|
# Show AI analysis as markdown
|
|
@@ -78,7 +78,7 @@ def ai_analyze_issue_requirements_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
78
78
|
|
|
79
79
|
# Show token usage
|
|
80
80
|
if analysis.total_tokens_used > 0:
|
|
81
|
-
ctx.textual.
|
|
81
|
+
ctx.textual.dim_text(f"Tokens used: {analysis.total_tokens_used}")
|
|
82
82
|
|
|
83
83
|
# Save structured analysis to context
|
|
84
84
|
ctx.set("ai_analysis_structured", {
|
|
@@ -30,14 +30,14 @@ def get_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
30
30
|
|
|
31
31
|
# Check if JIRA client is available
|
|
32
32
|
if not ctx.jira:
|
|
33
|
-
ctx.textual.
|
|
33
|
+
ctx.textual.error_text(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
|
|
34
34
|
ctx.textual.end_step("error")
|
|
35
35
|
return Error(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
|
|
36
36
|
|
|
37
37
|
# Get issue key
|
|
38
38
|
issue_key = ctx.get("jira_issue_key")
|
|
39
39
|
if not issue_key:
|
|
40
|
-
ctx.textual.
|
|
40
|
+
ctx.textual.error_text("JIRA issue key is required")
|
|
41
41
|
ctx.textual.end_step("error")
|
|
42
42
|
return Error("JIRA issue key is required")
|
|
43
43
|
|
|
@@ -51,10 +51,10 @@ def get_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
51
51
|
|
|
52
52
|
# Show success
|
|
53
53
|
ctx.textual.text("") # spacing
|
|
54
|
-
ctx.textual.
|
|
54
|
+
ctx.textual.success_text(msg.Steps.GetIssue.GET_SUCCESS.format(issue_key=issue_key))
|
|
55
55
|
|
|
56
56
|
# Show issue details
|
|
57
|
-
ctx.textual.
|
|
57
|
+
ctx.textual.primary_text(f" Title: {issue.summary}")
|
|
58
58
|
ctx.textual.text(f" Status: {issue.status}")
|
|
59
59
|
ctx.textual.text(f" Type: {issue.issue_type}")
|
|
60
60
|
ctx.textual.text(f" Assignee: {issue.assignee or 'Unassigned'}")
|
|
@@ -69,16 +69,16 @@ def get_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
69
69
|
except JiraAPIError as e:
|
|
70
70
|
if e.status_code == 404:
|
|
71
71
|
error_msg = msg.Steps.GetIssue.ISSUE_NOT_FOUND.format(issue_key=issue_key)
|
|
72
|
-
ctx.textual.
|
|
72
|
+
ctx.textual.error_text(error_msg)
|
|
73
73
|
ctx.textual.end_step("error")
|
|
74
74
|
return Error(error_msg)
|
|
75
75
|
error_msg = msg.Steps.GetIssue.GET_FAILED.format(e=e)
|
|
76
|
-
ctx.textual.
|
|
76
|
+
ctx.textual.error_text(error_msg)
|
|
77
77
|
ctx.textual.end_step("error")
|
|
78
78
|
return Error(error_msg)
|
|
79
79
|
except Exception as e:
|
|
80
80
|
error_msg = f"Unexpected error getting issue: {e}"
|
|
81
|
-
ctx.textual.
|
|
81
|
+
ctx.textual.error_text(error_msg)
|
|
82
82
|
ctx.textual.end_step("error")
|
|
83
83
|
return Error(error_msg)
|
|
84
84
|
|