titan-cli 0.1.4__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 (61) hide show
  1. titan_cli/core/config.py +3 -1
  2. titan_cli/core/workflows/__init__.py +2 -1
  3. titan_cli/core/workflows/project_step_source.py +95 -32
  4. titan_cli/core/workflows/workflow_filter_service.py +16 -8
  5. titan_cli/core/workflows/workflow_registry.py +12 -1
  6. titan_cli/core/workflows/workflow_sources.py +1 -1
  7. titan_cli/engine/__init__.py +5 -1
  8. titan_cli/engine/results.py +31 -1
  9. titan_cli/engine/steps/ai_assistant_step.py +47 -12
  10. titan_cli/engine/workflow_executor.py +13 -3
  11. titan_cli/ui/tui/screens/plugin_config_wizard.py +16 -0
  12. titan_cli/ui/tui/screens/workflow_execution.py +28 -50
  13. titan_cli/ui/tui/screens/workflows.py +8 -4
  14. titan_cli/ui/tui/textual_components.py +342 -185
  15. titan_cli/ui/tui/textual_workflow_executor.py +39 -3
  16. titan_cli/ui/tui/theme.py +34 -5
  17. titan_cli/ui/tui/widgets/__init__.py +17 -0
  18. titan_cli/ui/tui/widgets/multiline_input.py +32 -0
  19. titan_cli/ui/tui/widgets/prompt_choice.py +138 -0
  20. titan_cli/ui/tui/widgets/prompt_input.py +74 -0
  21. titan_cli/ui/tui/widgets/prompt_selection_list.py +150 -0
  22. titan_cli/ui/tui/widgets/prompt_textarea.py +87 -0
  23. titan_cli/ui/tui/widgets/step_container.py +70 -0
  24. titan_cli/ui/tui/widgets/styled_option_list.py +107 -0
  25. titan_cli/ui/tui/widgets/text.py +51 -130
  26. {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/METADATA +3 -5
  27. {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/RECORD +61 -46
  28. titan_plugin_git/clients/git_client.py +140 -5
  29. titan_plugin_git/plugin.py +13 -0
  30. titan_plugin_git/steps/ai_commit_message_step.py +39 -34
  31. titan_plugin_git/steps/branch_steps.py +18 -37
  32. titan_plugin_git/steps/checkout_step.py +66 -0
  33. titan_plugin_git/steps/commit_step.py +18 -22
  34. titan_plugin_git/steps/create_branch_step.py +131 -0
  35. titan_plugin_git/steps/diff_summary_step.py +180 -0
  36. titan_plugin_git/steps/pull_step.py +70 -0
  37. titan_plugin_git/steps/push_step.py +27 -11
  38. titan_plugin_git/steps/restore_original_branch_step.py +97 -0
  39. titan_plugin_git/steps/save_current_branch_step.py +82 -0
  40. titan_plugin_git/steps/status_step.py +32 -25
  41. titan_plugin_git/workflows/commit-ai.yaml +9 -3
  42. titan_plugin_github/agents/pr_agent.py +15 -2
  43. titan_plugin_github/steps/ai_pr_step.py +99 -40
  44. titan_plugin_github/steps/create_pr_step.py +18 -8
  45. titan_plugin_github/steps/github_prompt_steps.py +53 -1
  46. titan_plugin_github/steps/issue_steps.py +31 -18
  47. titan_plugin_github/steps/preview_step.py +15 -4
  48. titan_plugin_github/utils.py +5 -4
  49. titan_plugin_github/workflows/create-pr-ai.yaml +6 -11
  50. titan_plugin_jira/messages.py +12 -0
  51. titan_plugin_jira/plugin.py +4 -0
  52. titan_plugin_jira/steps/ai_analyze_issue_step.py +12 -7
  53. titan_plugin_jira/steps/get_issue_step.py +17 -13
  54. titan_plugin_jira/steps/list_versions_step.py +133 -0
  55. titan_plugin_jira/steps/prompt_select_issue_step.py +20 -8
  56. titan_plugin_jira/steps/search_jql_step.py +191 -0
  57. titan_plugin_jira/steps/search_saved_query_step.py +26 -24
  58. titan_plugin_jira/utils/__init__.py +1 -1
  59. {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/LICENSE +0 -0
  60. {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/WHEEL +0 -0
  61. {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/entry_points.txt +0 -0
@@ -1,8 +1,8 @@
1
1
  import ast
2
2
  from titan_cli.engine.context import WorkflowContext
3
3
  from titan_cli.engine.results import WorkflowResult, Success, Error, Skip
4
- from titan_cli.ui.tui.widgets import Panel
5
4
  from ..agents.issue_generator import IssueGeneratorAgent
5
+ from pathlib import Path
6
6
 
7
7
  def ai_suggest_issue_title_and_body_step(ctx: WorkflowContext) -> WorkflowResult:
8
8
  """
@@ -12,15 +12,21 @@ def ai_suggest_issue_title_and_body_step(ctx: WorkflowContext) -> WorkflowResult
12
12
  if not ctx.textual:
13
13
  return Error("Textual UI context is not available for this step.")
14
14
 
15
+ # Begin step container
16
+ ctx.textual.begin_step("Categorize and Generate Issue")
17
+
15
18
  if not ctx.ai:
19
+ ctx.textual.dim_text("AI client not available")
20
+ ctx.textual.end_step("skip")
16
21
  return Skip("AI client not available")
17
22
 
18
23
  issue_body_prompt = ctx.get("issue_body")
19
24
  if not issue_body_prompt:
25
+ ctx.textual.error_text("issue_body not found in context")
26
+ ctx.textual.end_step("error")
20
27
  return Error("issue_body not found in context")
21
28
 
22
- ctx.textual.text("Using AI to categorize and generate issue...", markup="cyan")
23
-
29
+ ctx.textual.dim_text("Using AI to categorize and generate issue...")
24
30
  try:
25
31
  # Get available labels from repository for smart mapping
26
32
  available_labels = None
@@ -34,7 +40,6 @@ def ai_suggest_issue_title_and_body_step(ctx: WorkflowContext) -> WorkflowResult
34
40
  # Get template directory from repo path
35
41
  template_dir = None
36
42
  if ctx.git:
37
- from pathlib import Path
38
43
  template_dir = Path(ctx.git.repo_path) / ".github" / "ISSUE_TEMPLATE"
39
44
 
40
45
  issue_generator = IssueGeneratorAgent(ctx.ai, template_dir=template_dir)
@@ -46,25 +51,24 @@ def ai_suggest_issue_title_and_body_step(ctx: WorkflowContext) -> WorkflowResult
46
51
  category = result["category"]
47
52
  template_used = result.get("template_used", False)
48
53
 
49
- ctx.textual.mount(
50
- Panel(
51
- text=f"Category detected: {category}",
52
- panel_type="success"
53
- )
54
- )
54
+ ctx.textual.text("") # spacing
55
+ ctx.textual.success_text(f"Category detected: {category}")
55
56
 
56
57
  if template_used:
57
- ctx.textual.text(f"Using template: {category}.md", markup="cyan")
58
+ ctx.textual.success_text(f"Using template: {category}.md")
58
59
  else:
59
- 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")
60
61
 
61
62
  ctx.set("issue_title", result["title"])
62
63
  ctx.set("issue_body", result["body"])
63
64
  ctx.set("issue_category", category)
64
65
  ctx.set("labels", result["labels"])
65
66
 
67
+ ctx.textual.end_step("success")
66
68
  return Success(f"AI-generated issue ({category}) created successfully")
67
69
  except Exception as e:
70
+ ctx.textual.error_text(f"Failed to generate issue: {e}")
71
+ ctx.textual.end_step("error")
68
72
  return Error(f"Failed to generate issue: {e}")
69
73
 
70
74
 
@@ -75,7 +79,12 @@ def create_issue_steps(ctx: WorkflowContext) -> WorkflowResult:
75
79
  if not ctx.textual:
76
80
  return Error("Textual UI context is not available for this step.")
77
81
 
82
+ # Begin step container
83
+ ctx.textual.begin_step("Create Issue")
84
+
78
85
  if not ctx.github:
86
+ ctx.textual.error_text("GitHub client not available")
87
+ ctx.textual.end_step("error")
79
88
  return Error("GitHub client not available")
80
89
 
81
90
  issue_title = ctx.get("issue_title")
@@ -103,9 +112,13 @@ def create_issue_steps(ctx: WorkflowContext) -> WorkflowResult:
103
112
  labels = []
104
113
 
105
114
  if not issue_title:
115
+ ctx.textual.error_text("issue_title not found in context")
116
+ ctx.textual.end_step("error")
106
117
  return Error("issue_title not found in context")
107
118
 
108
119
  if not issue_body:
120
+ ctx.textual.error_text("issue_body not found in context")
121
+ ctx.textual.end_step("error")
109
122
  return Error("issue_body not found in context")
110
123
 
111
124
  # Filter labels to only those that exist in the repository
@@ -123,21 +136,21 @@ def create_issue_steps(ctx: WorkflowContext) -> WorkflowResult:
123
136
  pass
124
137
 
125
138
  try:
139
+ ctx.textual.dim_text(f"Creating issue: {issue_title}...")
126
140
  issue = ctx.github.create_issue(
127
141
  title=issue_title,
128
142
  body=issue_body,
129
143
  assignees=assignees,
130
144
  labels=labels,
131
145
  )
132
- ctx.textual.mount(
133
- Panel(
134
- text=f"Successfully created issue #{issue.number}",
135
- panel_type="success"
136
- )
137
- )
146
+ ctx.textual.text("") # spacing
147
+ ctx.textual.success_text(f"Successfully created issue #{issue.number}")
148
+ ctx.textual.end_step("success")
138
149
  return Success(
139
150
  f"Successfully created issue #{issue.number}",
140
151
  metadata={"issue": issue}
141
152
  )
142
153
  except Exception as e:
154
+ ctx.textual.error_text(f"Failed to create issue: {e}")
155
+ ctx.textual.end_step("error")
143
156
  return Error(f"Failed to create issue: {e}")
@@ -8,24 +8,29 @@ def preview_and_confirm_issue_step(ctx: WorkflowContext) -> WorkflowResult:
8
8
  if not ctx.textual:
9
9
  return Error("Textual UI context is not available for this step.")
10
10
 
11
+ # Begin step container
12
+ ctx.textual.begin_step("Preview and Confirm Issue")
13
+
11
14
  issue_title = ctx.get("issue_title")
12
15
  issue_body = ctx.get("issue_body")
13
16
 
14
17
  if not issue_title or not issue_body:
18
+ ctx.textual.error_text("issue_title or issue_body not found in context")
19
+ ctx.textual.end_step("error")
15
20
  return Error("issue_title or issue_body not found in context")
16
21
 
17
22
  # Show preview header
18
23
  ctx.textual.text("") # spacing
19
- ctx.textual.text("AI-Generated Issue Preview", markup="bold")
24
+ ctx.textual.bold_text("AI-Generated Issue Preview")
20
25
  ctx.textual.text("") # spacing
21
26
 
22
27
  # Show title
23
- ctx.textual.text("Title:", markup="bold")
24
- ctx.textual.text(f" {issue_title}", markup="cyan")
28
+ ctx.textual.bold_text("Title:")
29
+ ctx.textual.primary_text(f" {issue_title}")
25
30
  ctx.textual.text("") # spacing
26
31
 
27
32
  # Show description
28
- ctx.textual.text("Description:", markup="bold")
33
+ ctx.textual.bold_text("Description:")
29
34
  # Render markdown in a scrollable container
30
35
  ctx.textual.markdown(issue_body)
31
36
 
@@ -33,8 +38,14 @@ def preview_and_confirm_issue_step(ctx: WorkflowContext) -> WorkflowResult:
33
38
 
34
39
  try:
35
40
  if not ctx.textual.ask_confirm("Use this AI-generated issue?", default=True):
41
+ ctx.textual.warning_text("User rejected AI-generated issue")
42
+ ctx.textual.end_step("error")
36
43
  return Error("User rejected AI-generated issue")
37
44
  except (KeyboardInterrupt, EOFError):
45
+ ctx.textual.error_text("User cancelled operation")
46
+ ctx.textual.end_step("error")
38
47
  return Error("User cancelled operation")
39
48
 
49
+ ctx.textual.success_text("User confirmed AI-generated issue")
50
+ ctx.textual.end_step("success")
40
51
  return Success("User confirmed AI-generated issue")
@@ -54,21 +54,22 @@ def calculate_pr_size(diff: str) -> PRSizeEstimation:
54
54
  files_changed = len(re.findall(file_pattern, diff, re.MULTILINE))
55
55
 
56
56
  # Dynamic character limit based on PR size
57
+ # Increased limits to accommodate PR templates with images/GIFs
57
58
  if files_changed <= 3 and diff_lines < 100:
58
59
  # Small PR: bug fix, doc update, small feature
59
- max_chars = 800
60
+ max_chars = 1500
60
61
  pr_size = "small"
61
62
  elif files_changed <= 10 and diff_lines < 500:
62
63
  # Medium PR: feature, moderate refactor
63
- max_chars = 1800
64
+ max_chars = 2500
64
65
  pr_size = "medium"
65
66
  elif files_changed <= 30 and diff_lines < 2000:
66
67
  # Large PR: architectural changes, new modules
67
- max_chars = 3000
68
+ max_chars = 4000
68
69
  pr_size = "large"
69
70
  else:
70
71
  # Very large PR: major refactor, breaking changes
71
- max_chars = 4500
72
+ max_chars = 6000
72
73
  pr_size = "very large"
73
74
 
74
75
  return PRSizeEstimation(
@@ -19,7 +19,13 @@ steps:
19
19
  plugin: git
20
20
  step: get_current_branch
21
21
 
22
+ - id: branch_diff_summary
23
+ name: "Show Branch Changes Summary"
24
+ plugin: git
25
+ step: show_branch_diff_summary
26
+
22
27
  # AI generates PR title and description from branch commits
28
+ # User can choose to use as-is, edit, or reject
23
29
  - id: ai_pr_description
24
30
  name: "AI PR Description"
25
31
  plugin: github
@@ -27,17 +33,6 @@ steps:
27
33
 
28
34
  - hook: before_push
29
35
 
30
- # Fallback to manual prompts if AI was rejected
31
- - id: prompt_pr_title
32
- name: "Prompt for PR Title"
33
- plugin: github
34
- step: prompt_for_pr_title
35
-
36
- - id: prompt_pr_body
37
- name: "Prompt for PR Body"
38
- plugin: github
39
- step: prompt_for_pr_body
40
-
41
36
  - id: create_pr
42
37
  name: "Create Pull Request"
43
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
@@ -3,7 +3,6 @@ AI-powered JIRA issue analysis step
3
3
  """
4
4
 
5
5
  from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error, Skip
6
- from titan_cli.ui.tui.widgets import Panel
7
6
  from ..messages import msg
8
7
  from ..agents import JiraAgent
9
8
  from ..formatters import IssueAnalysisMarkdownFormatter
@@ -32,15 +31,20 @@ def ai_analyze_issue_requirements_step(ctx: WorkflowContext) -> WorkflowResult:
32
31
  if not ctx.textual:
33
32
  return Error("Textual UI context is not available for this step.")
34
33
 
34
+ # Begin step container
35
+ ctx.textual.begin_step("AI Analyze Issue")
36
+
35
37
  # Check if AI is available
36
38
  if not ctx.ai or not ctx.ai.is_available():
37
- ctx.textual.mount(Panel(msg.Steps.AIIssue.AI_NOT_CONFIGURED_SKIP, panel_type="info"))
39
+ ctx.textual.dim_text(msg.Steps.AIIssue.AI_NOT_CONFIGURED_SKIP)
40
+ ctx.textual.end_step("skip")
38
41
  return Skip(msg.Steps.AIIssue.AI_NOT_CONFIGURED)
39
42
 
40
43
  # Get issue to analyze
41
44
  issue = ctx.get("jira_issue") or ctx.get("selected_issue")
42
45
  if not issue:
43
- ctx.textual.mount(Panel(msg.Steps.AIIssue.NO_ISSUE_FOUND, panel_type="error"))
46
+ ctx.textual.error_text(msg.Steps.AIIssue.NO_ISSUE_FOUND)
47
+ ctx.textual.end_step("error")
44
48
  return Error(msg.Steps.AIIssue.NO_ISSUE_FOUND)
45
49
 
46
50
  # Create JiraAgent instance and analyze issue with loading indicator
@@ -61,12 +65,12 @@ def ai_analyze_issue_requirements_step(ctx: WorkflowContext) -> WorkflowResult:
61
65
 
62
66
  # Display analysis
63
67
  ctx.textual.text("")
64
- ctx.textual.text("AI Analysis Results", markup="bold cyan")
68
+ ctx.textual.bold_primary_text("AI Analysis Results")
65
69
  ctx.textual.text("")
66
70
 
67
71
  # Show issue header
68
- ctx.textual.text(f"{issue.key}: {issue.summary}", markup="bold")
69
- 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}")
70
74
  ctx.textual.text("")
71
75
 
72
76
  # Show AI analysis as markdown
@@ -74,7 +78,7 @@ def ai_analyze_issue_requirements_step(ctx: WorkflowContext) -> WorkflowResult:
74
78
 
75
79
  # Show token usage
76
80
  if analysis.total_tokens_used > 0:
77
- ctx.textual.text(f"Tokens used: {analysis.total_tokens_used}", markup="dim")
81
+ ctx.textual.dim_text(f"Tokens used: {analysis.total_tokens_used}")
78
82
 
79
83
  # Save structured analysis to context
80
84
  ctx.set("ai_analysis_structured", {
@@ -90,6 +94,7 @@ def ai_analyze_issue_requirements_step(ctx: WorkflowContext) -> WorkflowResult:
90
94
  "estimated_effort": analysis.estimated_effort
91
95
  })
92
96
 
97
+ ctx.textual.end_step("success")
93
98
  return Success(
94
99
  "AI analysis completed",
95
100
  metadata={
@@ -3,7 +3,6 @@ Get JIRA issue details step
3
3
  """
4
4
 
5
5
  from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
6
- from titan_cli.ui.tui.widgets import Panel
7
6
  from ..exceptions import JiraAPIError
8
7
  from ..messages import msg
9
8
 
@@ -26,15 +25,20 @@ def get_issue_step(ctx: WorkflowContext) -> WorkflowResult:
26
25
  if not ctx.textual:
27
26
  return Error("Textual UI context is not available for this step.")
28
27
 
28
+ # Begin step container
29
+ ctx.textual.begin_step("Get Full Issue Details")
30
+
29
31
  # Check if JIRA client is available
30
32
  if not ctx.jira:
31
- ctx.textual.mount(Panel(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT, panel_type="error"))
33
+ ctx.textual.error_text(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
34
+ ctx.textual.end_step("error")
32
35
  return Error(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
33
36
 
34
37
  # Get issue key
35
38
  issue_key = ctx.get("jira_issue_key")
36
39
  if not issue_key:
37
- ctx.textual.mount(Panel("JIRA issue key is required", panel_type="error"))
40
+ ctx.textual.error_text("JIRA issue key is required")
41
+ ctx.textual.end_step("error")
38
42
  return Error("JIRA issue key is required")
39
43
 
40
44
  # Get optional expand fields
@@ -46,20 +50,17 @@ def get_issue_step(ctx: WorkflowContext) -> WorkflowResult:
46
50
  issue = ctx.jira.get_ticket(ticket_key=issue_key, expand=expand)
47
51
 
48
52
  # Show success
49
- ctx.textual.mount(
50
- Panel(
51
- text=msg.Steps.GetIssue.GET_SUCCESS.format(issue_key=issue_key),
52
- panel_type="success"
53
- )
54
- )
53
+ ctx.textual.text("") # spacing
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'}")
61
61
  ctx.textual.text("")
62
62
 
63
+ ctx.textual.end_step("success")
63
64
  return Success(
64
65
  msg.Steps.GetIssue.GET_SUCCESS.format(issue_key=issue_key),
65
66
  metadata={"jira_issue": issue}
@@ -68,14 +69,17 @@ def get_issue_step(ctx: WorkflowContext) -> WorkflowResult:
68
69
  except JiraAPIError as e:
69
70
  if e.status_code == 404:
70
71
  error_msg = msg.Steps.GetIssue.ISSUE_NOT_FOUND.format(issue_key=issue_key)
71
- ctx.textual.mount(Panel(error_msg, panel_type="error"))
72
+ ctx.textual.error_text(error_msg)
73
+ ctx.textual.end_step("error")
72
74
  return Error(error_msg)
73
75
  error_msg = msg.Steps.GetIssue.GET_FAILED.format(e=e)
74
- ctx.textual.mount(Panel(error_msg, panel_type="error"))
76
+ ctx.textual.error_text(error_msg)
77
+ ctx.textual.end_step("error")
75
78
  return Error(error_msg)
76
79
  except Exception as e:
77
80
  error_msg = f"Unexpected error getting issue: {e}"
78
- ctx.textual.mount(Panel(error_msg, panel_type="error"))
81
+ ctx.textual.error_text(error_msg)
82
+ ctx.textual.end_step("error")
79
83
  return Error(error_msg)
80
84
 
81
85
 
@@ -0,0 +1,133 @@
1
+ """
2
+ List available versions (fixVersions) for a JIRA project
3
+ """
4
+
5
+ from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
6
+ from ..exceptions import JiraAPIError
7
+
8
+
9
+ def list_versions_step(ctx: WorkflowContext) -> WorkflowResult:
10
+ """
11
+ List unreleased versions for a JIRA project.
12
+
13
+ Filters and returns only versions that are not yet released,
14
+ sorted by name in descending order (most recent first).
15
+
16
+ Inputs (from ctx.data):
17
+ project_key (str, optional): Project key. If not provided, uses default_project from JIRA plugin config.
18
+
19
+ Outputs (saved to ctx.data):
20
+ versions (list): List of unreleased version names
21
+ versions_full (list): List of full unreleased version objects
22
+
23
+ Returns:
24
+ Success: Unreleased versions listed
25
+ Error: Failed to fetch versions or project_key not configured
26
+
27
+ Example usage in workflow:
28
+ ```yaml
29
+ - id: list_versions
30
+ plugin: jira
31
+ step: list_versions
32
+ params:
33
+ project_key: "MYPROJECT"
34
+ ```
35
+ """
36
+ if not ctx.textual:
37
+ return Error("Textual UI context is not available for this step.")
38
+
39
+ if not ctx.jira:
40
+ return Error("JIRA client not available in context")
41
+
42
+ # Begin step container
43
+ ctx.textual.begin_step("List Project Versions")
44
+
45
+ # Get project key from context or fall back to default_project from JIRA client
46
+ project_key = ctx.get("project_key")
47
+ if not project_key:
48
+ # Try to use default project from JIRA client config
49
+ if hasattr(ctx.jira, 'project_key') and ctx.jira.project_key:
50
+ project_key = ctx.jira.project_key
51
+ ctx.textual.dim_text(f"Using default project from JIRA config: {project_key}")
52
+ else:
53
+ ctx.textual.error_text("project_key is required but not provided")
54
+ ctx.textual.dim_text("Set project_key in workflow params or configure default_project in JIRA plugin")
55
+ ctx.textual.end_step("error")
56
+ return Error("project_key is required. Provide it in workflow params or configure default_project in JIRA plugin.")
57
+
58
+ # Show fetching message
59
+ ctx.textual.dim_text(f"Fetching versions for project: {project_key}")
60
+ ctx.textual.text("")
61
+
62
+ try:
63
+ # Get project details which includes versions
64
+ project = ctx.jira.get_project(project_key)
65
+ versions = project.get("versions", [])
66
+
67
+ if not versions:
68
+ ctx.textual.panel(
69
+ f"No versions found for project {project_key}", panel_type="info"
70
+ )
71
+ ctx.textual.end_step("skip")
72
+ return Success(
73
+ "No versions found", metadata={"versions": [], "versions_full": []}
74
+ )
75
+
76
+ # Filter only unreleased versions for release notes workflow
77
+ unreleased_versions = [v for v in versions if not v.get("released", False)]
78
+
79
+ # Sort unreleased by name descending (most recent first)
80
+ unreleased_versions.sort(key=lambda v: v.get("name", ""), reverse=True)
81
+
82
+ # Use only unreleased versions
83
+ sorted_versions = unreleased_versions
84
+
85
+ # Extract version names
86
+ version_names = [v.get("name", "") for v in sorted_versions]
87
+
88
+ # Show success panel
89
+ ctx.textual.text("")
90
+ ctx.textual.panel(
91
+ f"Found {len(sorted_versions)} unreleased versions",
92
+ panel_type="success",
93
+ )
94
+
95
+ # Show versions list
96
+ ctx.textual.text("")
97
+ ctx.textual.bold_primary_text("Unreleased Versions:")
98
+ ctx.textual.text("")
99
+
100
+ for v in sorted_versions[:20]: # Show first 20
101
+ name = v.get("name", "")
102
+ description = v.get("description", "")
103
+ desc_text = f" - {description[:50]}" if description else ""
104
+ ctx.textual.primary_text(f" • {name}{desc_text}")
105
+
106
+ if len(sorted_versions) > 20:
107
+ ctx.textual.dim_text(
108
+ f" ... and {len(sorted_versions) - 20} more"
109
+ )
110
+ ctx.textual.text("")
111
+
112
+ ctx.textual.end_step("success")
113
+ return Success(
114
+ f"Found {len(sorted_versions)} unreleased versions",
115
+ metadata={"versions": version_names, "versions_full": sorted_versions},
116
+ )
117
+
118
+ except JiraAPIError as e:
119
+ error_msg = f"Failed to fetch versions: {e}"
120
+ ctx.textual.panel(error_msg, panel_type="error")
121
+ ctx.textual.end_step("error")
122
+ return Error(error_msg)
123
+ except Exception as e:
124
+ import traceback
125
+
126
+ error_detail = traceback.format_exc()
127
+ error_msg = f"Unexpected error: {e}\n\nTraceback:\n{error_detail}"
128
+ ctx.textual.panel(error_msg, panel_type="error")
129
+ ctx.textual.end_step("error")
130
+ return Error(error_msg)
131
+
132
+
133
+ __all__ = ["list_versions_step"]
@@ -3,7 +3,6 @@ Prompt user to select an issue from search results
3
3
  """
4
4
 
5
5
  from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
6
- from titan_cli.ui.tui.widgets import Panel
7
6
  from ..messages import msg
8
7
 
9
8
 
@@ -21,12 +20,19 @@ def prompt_select_issue_step(ctx: WorkflowContext) -> WorkflowResult:
21
20
  if not ctx.textual:
22
21
  return Error("Textual UI context is not available for this step.")
23
22
 
23
+ # Begin step container
24
+ ctx.textual.begin_step("Select Issue to Analyze")
25
+
24
26
  # Get issues from previous search
25
27
  issues = ctx.get("jira_issues")
26
28
  if not issues:
29
+ ctx.textual.error_text(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
30
+ ctx.textual.end_step("error")
27
31
  return Error(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
28
32
 
29
33
  if len(issues) == 0:
34
+ ctx.textual.error_text(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
35
+ ctx.textual.end_step("error")
30
36
  return Error(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
31
37
 
32
38
  # Prompt user to select issue (issues already displayed in table from previous step)
@@ -40,32 +46,36 @@ def prompt_select_issue_step(ctx: WorkflowContext) -> WorkflowResult:
40
46
  )
41
47
 
42
48
  if not response or not response.strip():
49
+ ctx.textual.error_text(msg.Steps.PromptSelectIssue.NO_ISSUE_SELECTED)
50
+ ctx.textual.end_step("error")
43
51
  return Error(msg.Steps.PromptSelectIssue.NO_ISSUE_SELECTED)
44
52
 
45
53
  # Validate it's a number
46
54
  try:
47
55
  selected_index = int(response.strip())
48
56
  except ValueError:
57
+ ctx.textual.error_text(f"Invalid input: '{response}' is not a number")
58
+ ctx.textual.end_step("error")
49
59
  return Error(f"Invalid input: '{response}' is not a number")
50
60
 
51
61
  # Validate it's in range
52
62
  if selected_index < 1 or selected_index > len(issues):
63
+ ctx.textual.error_text(f"Invalid selection: must be between 1 and {len(issues)}")
64
+ ctx.textual.end_step("error")
53
65
  return Error(f"Invalid selection: must be between 1 and {len(issues)}")
54
66
 
55
67
  # Convert to 0-based index
56
68
  selected_issue = issues[selected_index - 1]
57
69
 
58
70
  ctx.textual.text("")
59
- ctx.textual.mount(
60
- Panel(
61
- text=msg.Steps.PromptSelectIssue.ISSUE_SELECTION_CONFIRM.format(
62
- key=selected_issue.key,
63
- summary=selected_issue.summary
64
- ),
65
- panel_type="success"
71
+ ctx.textual.success_text(
72
+ msg.Steps.PromptSelectIssue.ISSUE_SELECTION_CONFIRM.format(
73
+ key=selected_issue.key,
74
+ summary=selected_issue.summary
66
75
  )
67
76
  )
68
77
 
78
+ ctx.textual.end_step("success")
69
79
  return Success(
70
80
  msg.Steps.PromptSelectIssue.SELECT_SUCCESS.format(key=selected_issue.key),
71
81
  metadata={
@@ -74,6 +84,8 @@ def prompt_select_issue_step(ctx: WorkflowContext) -> WorkflowResult:
74
84
  }
75
85
  )
76
86
  except (KeyboardInterrupt, EOFError):
87
+ ctx.textual.error_text("User cancelled issue selection")
88
+ ctx.textual.end_step("error")
77
89
  return Error("User cancelled issue selection")
78
90
 
79
91