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.
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.6.dist-info}/METADATA +5 -10
  22. {titan_cli-0.1.5.dist-info → titan_cli-0.1.6.dist-info}/RECORD +54 -41
  23. {titan_cli-0.1.5.dist-info → titan_cli-0.1.6.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.6.dist-info}/LICENSE +0 -0
  54. {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.text(msg.GitHub.AI.AI_NOT_CONFIGURED, markup="dim")
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.text(msg.GitHub.AI.ANALYZING_BRANCH_DIFF.format(
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
- ), markup="dim")
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.text("No commits found in branch to generate PR description.", markup="dim")
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.text(msg.GitHub.AI.PR_SIZE_INFO.format(
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
- ), markup="dim")
102
+ ))
103
103
 
104
104
  # Show PR preview to user
105
105
  ctx.textual.text("") # spacing
106
- ctx.textual.text(msg.GitHub.AI.AI_GENERATED_PR_TITLE, markup="bold")
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.text(msg.GitHub.AI.TITLE_LABEL, markup="bold")
111
- ctx.textual.text(f" {analysis.pr_title}", markup="cyan")
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.text(msg.GitHub.AI.TITLE_TOO_LONG_WARNING.format(
115
+ ctx.textual.warning_text(msg.GitHub.AI.TITLE_TOO_LONG_WARNING.format(
116
116
  length=len(analysis.pr_title)
117
- ), markup="yellow")
117
+ ))
118
118
 
119
119
  ctx.textual.text("") # spacing
120
120
 
121
121
  # Show description
122
- ctx.textual.text(msg.GitHub.AI.DESCRIPTION_LABEL, markup="bold")
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
- # Single confirmation for both title and description
129
- use_ai_pr = ctx.textual.ask_confirm(
130
- msg.GitHub.AI.CONFIRM_USE_AI_PR,
131
- default=True
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
- if not use_ai_pr:
135
- ctx.textual.text(msg.GitHub.AI.AI_SUGGESTION_REJECTED, markup="yellow")
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": analysis.pr_title,
143
- "pr_body": analysis.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.text(msg.GitHub.AI.AI_GENERATION_FAILED.format(e=e), markup="yellow")
156
- ctx.textual.text(msg.GitHub.AI.FALLBACK_TO_MANUAL, markup="dim")
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.text("GitHub client is not available in the workflow context.", markup="red")
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.text("Git client is not available in the workflow context.", markup="red")
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.text("Missing required context for creating a pull request: pr_title, pr_head_branch.", markup="red")
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.text(f"Could not get current user for auto-assign: {e}", markup="yellow")
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.text(f"Creating pull request '{title}' from {head} to {base}...", markup="dim")
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.text(msg.GitHub.PR_CREATED.format(number=pr["number"], url=pr["url"]), markup="green")
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.text(f"Failed to create pull request: {e}", markup="red")
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.text(f"An unexpected error occurred while creating the pull request: {e}", markup="red")
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.text("PR title already provided, skipping manual prompt.", markup="dim")
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.text("PR body already provided, skipping manual prompt.", markup="dim")
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.text("Issue body already provided, skipping manual prompt.", markup="dim")
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.text("GitHub client not available", markup="red")
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.text(f"Issue will be assigned to {current_user}", markup="green")
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.text("Issue will not be assigned to current user", markup="dim")
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.text(f"Failed to prompt for self-assign: {e}", markup="red")
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.text("GitHub client not available", markup="red")
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.text("No labels found in the repository.", markup="dim")
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.text(f"Available labels: {', '.join(available_labels)}", markup="dim")
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.text(f"Selected labels: {', '.join(selected_labels)}", markup="green")
212
+ ctx.textual.success_text(f"Selected labels: {', '.join(selected_labels)}")
213
213
  else:
214
- ctx.textual.text("No labels selected", markup="dim")
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.text(f"Failed to prompt for labels: {e}", markup="red")
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.text("AI client not available", markup="dim")
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.text("issue_body not found in context", markup="red")
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.text("Using AI to categorize and generate issue...", markup="dim")
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.text(f"Category detected: {category}", markup="green")
55
+ ctx.textual.success_text(f"Category detected: {category}")
57
56
 
58
57
  if template_used:
59
- ctx.textual.text(f"Using template: {category}.md", markup="cyan")
58
+ ctx.textual.success_text(f"Using template: {category}.md")
60
59
  else:
61
- ctx.textual.text(f"No template found for {category}, using default structure", markup="yellow")
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.text(f"Failed to generate issue: {e}", markup="red")
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.text("GitHub client not available", markup="red")
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.text("issue_title not found in context", markup="red")
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.text("issue_body not found in context", markup="red")
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.text(f"Creating issue: {issue_title}...", markup="dim")
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.text(f"Successfully created issue #{issue.number}", markup="green")
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.text(f"Failed to create issue: {e}", markup="red")
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.text("issue_title or issue_body not found in context", markup="red")
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.text("AI-Generated Issue Preview", markup="bold")
24
+ ctx.textual.bold_text("AI-Generated Issue Preview")
25
25
  ctx.textual.text("") # spacing
26
26
 
27
27
  # Show title
28
- ctx.textual.text("Title:", markup="bold")
29
- ctx.textual.text(f" {issue_title}", markup="cyan")
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.text("Description:", markup="bold")
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.text("User rejected AI-generated issue", markup="yellow")
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.text("User cancelled operation", markup="red")
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.text("User confirmed AI-generated issue", markup="green")
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
@@ -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."
@@ -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.text(msg.Steps.AIIssue.AI_NOT_CONFIGURED_SKIP, markup="dim")
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.text(msg.Steps.AIIssue.NO_ISSUE_FOUND, markup="red")
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.text("AI Analysis Results", markup="bold cyan")
68
+ ctx.textual.bold_primary_text("AI Analysis Results")
69
69
  ctx.textual.text("")
70
70
 
71
71
  # Show issue header
72
- ctx.textual.text(f"{issue.key}: {issue.summary}", markup="bold")
73
- ctx.textual.text(f"Type: {issue.issue_type} | Status: {issue.status} | Priority: {issue.priority}", markup="dim")
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.text(f"Tokens used: {analysis.total_tokens_used}", markup="dim")
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.text(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT, markup="red")
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.text("JIRA issue key is required", markup="red")
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.text(msg.Steps.GetIssue.GET_SUCCESS.format(issue_key=issue_key), markup="green")
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.text(f" Title: {issue.summary}", markup="cyan")
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.text(error_msg, markup="red")
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.text(error_msg, markup="red")
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.text(error_msg, markup="red")
81
+ ctx.textual.error_text(error_msg)
82
82
  ctx.textual.end_step("error")
83
83
  return Error(error_msg)
84
84