titan-cli 0.1.0__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 (146) hide show
  1. titan_cli/__init__.py +3 -0
  2. titan_cli/__main__.py +4 -0
  3. titan_cli/ai/__init__.py +0 -0
  4. titan_cli/ai/agents/__init__.py +15 -0
  5. titan_cli/ai/agents/base.py +152 -0
  6. titan_cli/ai/client.py +170 -0
  7. titan_cli/ai/constants.py +56 -0
  8. titan_cli/ai/exceptions.py +48 -0
  9. titan_cli/ai/models.py +34 -0
  10. titan_cli/ai/oauth_helper.py +120 -0
  11. titan_cli/ai/providers/__init__.py +9 -0
  12. titan_cli/ai/providers/anthropic.py +117 -0
  13. titan_cli/ai/providers/base.py +75 -0
  14. titan_cli/ai/providers/gemini.py +278 -0
  15. titan_cli/cli.py +59 -0
  16. titan_cli/clients/__init__.py +1 -0
  17. titan_cli/clients/gcloud_client.py +52 -0
  18. titan_cli/core/__init__.py +3 -0
  19. titan_cli/core/config.py +274 -0
  20. titan_cli/core/discovery.py +51 -0
  21. titan_cli/core/errors.py +81 -0
  22. titan_cli/core/models.py +52 -0
  23. titan_cli/core/plugins/available.py +36 -0
  24. titan_cli/core/plugins/models.py +67 -0
  25. titan_cli/core/plugins/plugin_base.py +108 -0
  26. titan_cli/core/plugins/plugin_registry.py +163 -0
  27. titan_cli/core/secrets.py +141 -0
  28. titan_cli/core/workflows/__init__.py +22 -0
  29. titan_cli/core/workflows/models.py +88 -0
  30. titan_cli/core/workflows/project_step_source.py +86 -0
  31. titan_cli/core/workflows/workflow_exceptions.py +17 -0
  32. titan_cli/core/workflows/workflow_filter_service.py +137 -0
  33. titan_cli/core/workflows/workflow_registry.py +419 -0
  34. titan_cli/core/workflows/workflow_sources.py +307 -0
  35. titan_cli/engine/__init__.py +39 -0
  36. titan_cli/engine/builder.py +159 -0
  37. titan_cli/engine/context.py +82 -0
  38. titan_cli/engine/mock_context.py +176 -0
  39. titan_cli/engine/results.py +91 -0
  40. titan_cli/engine/steps/ai_assistant_step.py +185 -0
  41. titan_cli/engine/steps/command_step.py +93 -0
  42. titan_cli/engine/utils/__init__.py +3 -0
  43. titan_cli/engine/utils/venv.py +31 -0
  44. titan_cli/engine/workflow_executor.py +187 -0
  45. titan_cli/external_cli/__init__.py +0 -0
  46. titan_cli/external_cli/configs.py +17 -0
  47. titan_cli/external_cli/launcher.py +65 -0
  48. titan_cli/messages.py +121 -0
  49. titan_cli/ui/tui/__init__.py +205 -0
  50. titan_cli/ui/tui/__previews__/statusbar_preview.py +88 -0
  51. titan_cli/ui/tui/app.py +113 -0
  52. titan_cli/ui/tui/icons.py +70 -0
  53. titan_cli/ui/tui/screens/__init__.py +24 -0
  54. titan_cli/ui/tui/screens/ai_config.py +498 -0
  55. titan_cli/ui/tui/screens/ai_config_wizard.py +882 -0
  56. titan_cli/ui/tui/screens/base.py +110 -0
  57. titan_cli/ui/tui/screens/cli_launcher.py +151 -0
  58. titan_cli/ui/tui/screens/global_setup_wizard.py +363 -0
  59. titan_cli/ui/tui/screens/main_menu.py +162 -0
  60. titan_cli/ui/tui/screens/plugin_config_wizard.py +550 -0
  61. titan_cli/ui/tui/screens/plugin_management.py +377 -0
  62. titan_cli/ui/tui/screens/project_setup_wizard.py +686 -0
  63. titan_cli/ui/tui/screens/workflow_execution.py +592 -0
  64. titan_cli/ui/tui/screens/workflows.py +249 -0
  65. titan_cli/ui/tui/textual_components.py +537 -0
  66. titan_cli/ui/tui/textual_workflow_executor.py +405 -0
  67. titan_cli/ui/tui/theme.py +102 -0
  68. titan_cli/ui/tui/widgets/__init__.py +40 -0
  69. titan_cli/ui/tui/widgets/button.py +108 -0
  70. titan_cli/ui/tui/widgets/header.py +116 -0
  71. titan_cli/ui/tui/widgets/panel.py +81 -0
  72. titan_cli/ui/tui/widgets/status_bar.py +115 -0
  73. titan_cli/ui/tui/widgets/table.py +77 -0
  74. titan_cli/ui/tui/widgets/text.py +177 -0
  75. titan_cli/utils/__init__.py +0 -0
  76. titan_cli/utils/autoupdate.py +155 -0
  77. titan_cli-0.1.0.dist-info/METADATA +149 -0
  78. titan_cli-0.1.0.dist-info/RECORD +146 -0
  79. titan_cli-0.1.0.dist-info/WHEEL +4 -0
  80. titan_cli-0.1.0.dist-info/entry_points.txt +9 -0
  81. titan_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
  82. titan_plugin_git/__init__.py +1 -0
  83. titan_plugin_git/clients/__init__.py +8 -0
  84. titan_plugin_git/clients/git_client.py +772 -0
  85. titan_plugin_git/exceptions.py +40 -0
  86. titan_plugin_git/messages.py +112 -0
  87. titan_plugin_git/models.py +39 -0
  88. titan_plugin_git/plugin.py +118 -0
  89. titan_plugin_git/steps/__init__.py +1 -0
  90. titan_plugin_git/steps/ai_commit_message_step.py +171 -0
  91. titan_plugin_git/steps/branch_steps.py +104 -0
  92. titan_plugin_git/steps/commit_step.py +80 -0
  93. titan_plugin_git/steps/push_step.py +63 -0
  94. titan_plugin_git/steps/status_step.py +59 -0
  95. titan_plugin_git/workflows/__previews__/__init__.py +1 -0
  96. titan_plugin_git/workflows/__previews__/commit_ai_preview.py +124 -0
  97. titan_plugin_git/workflows/commit-ai.yaml +28 -0
  98. titan_plugin_github/__init__.py +11 -0
  99. titan_plugin_github/agents/__init__.py +6 -0
  100. titan_plugin_github/agents/config_loader.py +130 -0
  101. titan_plugin_github/agents/issue_generator.py +353 -0
  102. titan_plugin_github/agents/pr_agent.py +528 -0
  103. titan_plugin_github/clients/__init__.py +8 -0
  104. titan_plugin_github/clients/github_client.py +1105 -0
  105. titan_plugin_github/config/__init__.py +0 -0
  106. titan_plugin_github/config/pr_agent.toml +85 -0
  107. titan_plugin_github/exceptions.py +28 -0
  108. titan_plugin_github/messages.py +88 -0
  109. titan_plugin_github/models.py +330 -0
  110. titan_plugin_github/plugin.py +131 -0
  111. titan_plugin_github/steps/__init__.py +12 -0
  112. titan_plugin_github/steps/ai_pr_step.py +172 -0
  113. titan_plugin_github/steps/create_pr_step.py +86 -0
  114. titan_plugin_github/steps/github_prompt_steps.py +171 -0
  115. titan_plugin_github/steps/issue_steps.py +143 -0
  116. titan_plugin_github/steps/preview_step.py +40 -0
  117. titan_plugin_github/utils.py +82 -0
  118. titan_plugin_github/workflows/__previews__/__init__.py +1 -0
  119. titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +140 -0
  120. titan_plugin_github/workflows/create-issue-ai.yaml +32 -0
  121. titan_plugin_github/workflows/create-pr-ai.yaml +49 -0
  122. titan_plugin_jira/__init__.py +8 -0
  123. titan_plugin_jira/agents/__init__.py +6 -0
  124. titan_plugin_jira/agents/config_loader.py +154 -0
  125. titan_plugin_jira/agents/jira_agent.py +553 -0
  126. titan_plugin_jira/agents/prompts.py +364 -0
  127. titan_plugin_jira/agents/response_parser.py +435 -0
  128. titan_plugin_jira/agents/token_tracker.py +223 -0
  129. titan_plugin_jira/agents/validators.py +246 -0
  130. titan_plugin_jira/clients/jira_client.py +745 -0
  131. titan_plugin_jira/config/jira_agent.toml +92 -0
  132. titan_plugin_jira/config/templates/issue_analysis.md.j2 +78 -0
  133. titan_plugin_jira/exceptions.py +37 -0
  134. titan_plugin_jira/formatters/__init__.py +6 -0
  135. titan_plugin_jira/formatters/markdown_formatter.py +245 -0
  136. titan_plugin_jira/messages.py +115 -0
  137. titan_plugin_jira/models.py +89 -0
  138. titan_plugin_jira/plugin.py +264 -0
  139. titan_plugin_jira/steps/ai_analyze_issue_step.py +105 -0
  140. titan_plugin_jira/steps/get_issue_step.py +82 -0
  141. titan_plugin_jira/steps/prompt_select_issue_step.py +80 -0
  142. titan_plugin_jira/steps/search_saved_query_step.py +238 -0
  143. titan_plugin_jira/utils/__init__.py +13 -0
  144. titan_plugin_jira/utils/issue_sorter.py +140 -0
  145. titan_plugin_jira/utils/saved_queries.py +150 -0
  146. titan_plugin_jira/workflows/analyze-jira-issues.yaml +34 -0
@@ -0,0 +1,264 @@
1
+ from pathlib import Path
2
+ from typing import Optional, TypedDict, List
3
+ from pydantic import ValidationError
4
+ from titan_cli.core.plugins.models import JiraPluginConfig
5
+ from titan_cli.core.plugins.plugin_base import TitanPlugin
6
+ from titan_cli.core.config import TitanConfig
7
+ from titan_cli.core.secrets import SecretManager
8
+ from .clients.jira_client import JiraClient
9
+ from .exceptions import JiraConfigurationError, JiraClientError
10
+ from .messages import msg
11
+
12
+
13
+ class TokenValidationResult(TypedDict):
14
+ """Result of token validation."""
15
+ valid: bool
16
+ error: Optional[str]
17
+ user: Optional[str]
18
+ email: Optional[str]
19
+ token_source: dict
20
+ warnings: List[str]
21
+
22
+
23
+ class JiraPlugin(TitanPlugin):
24
+ """
25
+ Titan CLI Plugin for JIRA operations.
26
+ Provides a JiraClient for interacting with JIRA REST API.
27
+ """
28
+
29
+ @property
30
+ def name(self) -> str:
31
+ return "jira"
32
+
33
+ @property
34
+ def description(self) -> str:
35
+ return "Provides JIRA API integration with AI-powered issue management."
36
+
37
+ @property
38
+ def dependencies(self) -> list[str]:
39
+ return []
40
+
41
+ def initialize(self, config: TitanConfig, secrets: SecretManager) -> None:
42
+ """
43
+ Initialize with configuration.
44
+
45
+ Configuration cascade (project overrides global):
46
+ 1. Global credentials (~/.titan/config.toml): base_url, email
47
+ 2. Project settings (.titan/config.toml): default_project (optional)
48
+
49
+ Note: TitanConfig automatically merges global and project configs,
50
+ so _get_plugin_config() returns the already-merged configuration.
51
+
52
+ Reads API token from secrets:
53
+ JIRA_API_TOKEN or {email}_jira_api_token
54
+ """
55
+ # Get plugin-specific configuration data (already merged by TitanConfig)
56
+ plugin_config_data = self._get_plugin_config(config)
57
+
58
+ # Validate configuration using Pydantic model
59
+ # Pydantic validators will check base_url and email during construction
60
+ try:
61
+ validated_config = JiraPluginConfig(**plugin_config_data)
62
+ except ValidationError as e:
63
+ raise JiraConfigurationError(str(e)) from e
64
+
65
+ # Get API token from secrets
66
+ # Try multiple secret keys for backwards compatibility
67
+ # Priority: project-specific → plugin-specific → env var → email-specific
68
+ project_name = config.get_project_name()
69
+ project_key = f"{project_name}_jira_api_token" if project_name else None
70
+
71
+ api_token = (
72
+ (secrets.get(project_key) if project_key else None) or # Project-specific keychain
73
+ secrets.get("jira_api_token") or # Standard: plugin_fieldname
74
+ secrets.get("JIRA_API_TOKEN") or # Environment variable
75
+ secrets.get(f"{validated_config.email}_jira_api_token") # Email-specific
76
+ )
77
+
78
+ if not api_token:
79
+ raise JiraConfigurationError(
80
+ "JIRA API token not found in secrets. "
81
+ "Please configure the JIRA plugin to set the API token."
82
+ )
83
+
84
+ # Initialize client with validated configuration
85
+ self._client = JiraClient(
86
+ base_url=validated_config.base_url,
87
+ email=validated_config.email,
88
+ api_token=api_token,
89
+ project_key=validated_config.default_project,
90
+ timeout=validated_config.timeout,
91
+ enable_cache=validated_config.enable_cache,
92
+ cache_ttl=validated_config.cache_ttl
93
+ )
94
+
95
+ # Store token source info for diagnostics (without exposing token value)
96
+ self._token_source = self._identify_token_source(
97
+ secrets, project_name, validated_config.email, api_token
98
+ )
99
+
100
+ def _identify_token_source(
101
+ self, secrets: SecretManager, project_name: Optional[str],
102
+ email: str, token: str
103
+ ) -> dict:
104
+ """
105
+ Identify which source the token came from for diagnostics.
106
+
107
+ Returns:
108
+ Dict with source info (name, type, details)
109
+ """
110
+ project_key = f"{project_name}_jira_api_token" if project_name else None
111
+
112
+ if project_key and secrets.get(project_key) == token:
113
+ return {
114
+ "name": project_key,
115
+ "type": "project-specific",
116
+ "details": f"Token for project '{project_name}'"
117
+ }
118
+ elif secrets.get("jira_api_token") == token:
119
+ return {
120
+ "name": "jira_api_token",
121
+ "type": "global",
122
+ "details": "Global JIRA token (recommended)"
123
+ }
124
+ elif secrets.get("JIRA_API_TOKEN") == token:
125
+ return {
126
+ "name": "JIRA_API_TOKEN",
127
+ "type": "environment",
128
+ "details": "Environment variable"
129
+ }
130
+ elif secrets.get(f"{email}_jira_api_token") == token:
131
+ return {
132
+ "name": f"{email}_jira_api_token",
133
+ "type": "email-specific",
134
+ "details": f"Token for email '{email}'"
135
+ }
136
+ else:
137
+ return {
138
+ "name": "unknown",
139
+ "type": "unknown",
140
+ "details": "Token source could not be identified"
141
+ }
142
+
143
+ @property
144
+ def has_default_project(self) -> bool:
145
+ """Check if a default project is configured."""
146
+ return hasattr(self, '_client') and self._client.project_key is not None
147
+
148
+ def validate_token(self) -> TokenValidationResult:
149
+ """
150
+ Validate that the current token works by making a test API call.
151
+
152
+ Also checks configuration completeness and returns warnings.
153
+
154
+ Returns:
155
+ TokenValidationResult with validation results
156
+ """
157
+ warnings = []
158
+
159
+ # Check if default project is configured
160
+ if not self.has_default_project:
161
+ warnings.append(
162
+ "No default_project configured. "
163
+ "Some operations (like create_subtask) will fail without a project."
164
+ )
165
+
166
+ if not self.is_available():
167
+ return {
168
+ "valid": False,
169
+ "error": "JIRA client not initialized",
170
+ "user": None,
171
+ "email": None,
172
+ "token_source": getattr(self, '_token_source', {}),
173
+ "warnings": warnings
174
+ }
175
+
176
+ try:
177
+ # Test token with /rest/api/2/myself endpoint
178
+ myself = self._client.get_current_user()
179
+ return {
180
+ "valid": True,
181
+ "error": None,
182
+ "user": myself.get("displayName", "Unknown"),
183
+ "email": myself.get("emailAddress", "Unknown"),
184
+ "token_source": getattr(self, '_token_source', {}),
185
+ "warnings": warnings
186
+ }
187
+ except Exception as e:
188
+ return {
189
+ "valid": False,
190
+ "error": str(e),
191
+ "user": None,
192
+ "email": None,
193
+ "token_source": getattr(self, '_token_source', {}),
194
+ "warnings": warnings
195
+ }
196
+
197
+ def _get_plugin_config(self, config: TitanConfig) -> dict:
198
+ """
199
+ Extract plugin-specific configuration.
200
+
201
+ Args:
202
+ config: TitanConfig instance
203
+
204
+ Returns:
205
+ Plugin config dict (empty if not configured)
206
+ """
207
+ if "jira" not in config.config.plugins:
208
+ return {}
209
+
210
+ plugin_entry = config.config.plugins["jira"]
211
+ return plugin_entry.config if hasattr(plugin_entry, 'config') else {}
212
+
213
+ def get_config_schema(self) -> dict:
214
+ """
215
+ Return JSON schema for plugin configuration.
216
+
217
+ Returns:
218
+ JSON schema dict with api_token marked as required (even though it's stored in secrets)
219
+ """
220
+ schema = JiraPluginConfig.model_json_schema()
221
+ # Ensure api_token is in required list for interactive configuration
222
+ # (even though it's Optional in the model since it's stored in secrets)
223
+ if "api_token" not in schema.get("required", []):
224
+ schema.setdefault("required", []).append("api_token")
225
+ return schema
226
+
227
+ def is_available(self) -> bool:
228
+ """
229
+ Checks if the JIRA client is initialized and ready.
230
+ """
231
+ return hasattr(self, '_client') and self._client is not None
232
+
233
+ def get_client(self) -> JiraClient:
234
+ """
235
+ Returns the initialized JiraClient instance.
236
+ """
237
+ if not hasattr(self, '_client') or self._client is None:
238
+ raise JiraClientError(msg.Plugin.JIRA_CLIENT_NOT_AVAILABLE)
239
+ return self._client
240
+
241
+ def get_steps(self) -> dict:
242
+ """
243
+ Returns a dictionary of available workflow steps.
244
+ """
245
+ from .steps.search_saved_query_step import search_saved_query_step
246
+ from .steps.prompt_select_issue_step import prompt_select_issue_step
247
+ from .steps.get_issue_step import get_issue_step
248
+ from .steps.ai_analyze_issue_step import ai_analyze_issue_requirements_step
249
+ return {
250
+ "search_saved_query": search_saved_query_step,
251
+ "prompt_select_issue": prompt_select_issue_step,
252
+ "get_issue": get_issue_step,
253
+ "ai_analyze_issue_requirements": ai_analyze_issue_requirements_step,
254
+ }
255
+
256
+ @property
257
+ def workflows_path(self) -> Optional[Path]:
258
+ """
259
+ Returns the path to the workflows directory.
260
+
261
+ Returns:
262
+ Path to workflows directory containing YAML workflow definitions
263
+ """
264
+ return Path(__file__).parent / "workflows"
@@ -0,0 +1,105 @@
1
+ """
2
+ AI-powered JIRA issue analysis step
3
+ """
4
+
5
+ from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error, Skip
6
+ from titan_cli.ui.tui.widgets import Panel
7
+ from ..messages import msg
8
+ from ..agents import JiraAgent
9
+ from ..formatters import IssueAnalysisMarkdownFormatter
10
+
11
+
12
+ def ai_analyze_issue_requirements_step(ctx: WorkflowContext) -> WorkflowResult:
13
+ """
14
+ Analyze JIRA issue requirements using AI.
15
+
16
+ Provides intelligent analysis of:
17
+ - Summary and description breakdown
18
+ - Acceptance criteria extraction
19
+ - Technical requirements identification
20
+ - Potential challenges and risks
21
+ - Implementation suggestions
22
+ - Missing information detection
23
+
24
+ Inputs (from ctx.data):
25
+ jira_issue (JiraTicket): JIRA issue object to analyze
26
+ selected_issue (JiraTicket, optional): Alternative source
27
+
28
+ Outputs (saved to ctx.data):
29
+ ai_analysis (str): AI-generated analysis
30
+ analysis_sections (dict): Structured analysis breakdown
31
+ """
32
+ if not ctx.textual:
33
+ return Error("Textual UI context is not available for this step.")
34
+
35
+ # Check if AI is available
36
+ 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"))
38
+ return Skip(msg.Steps.AIIssue.AI_NOT_CONFIGURED)
39
+
40
+ # Get issue to analyze
41
+ issue = ctx.get("jira_issue") or ctx.get("selected_issue")
42
+ if not issue:
43
+ ctx.textual.mount(Panel(msg.Steps.AIIssue.NO_ISSUE_FOUND, panel_type="error"))
44
+ return Error(msg.Steps.AIIssue.NO_ISSUE_FOUND)
45
+
46
+ # Create JiraAgent instance and analyze issue with loading indicator
47
+ with ctx.textual.loading(msg.Steps.AIIssue.ANALYZING):
48
+ jira_agent = JiraAgent(ctx.ai, ctx.jira)
49
+ analysis = jira_agent.analyze_issue(
50
+ issue_key=issue.key,
51
+ include_subtasks=True,
52
+ include_comments=False,
53
+ include_linked_issues=False
54
+ )
55
+
56
+ # Build formatted analysis for display
57
+ # Use template from config if specified, otherwise use built-in formatter
58
+ template_name = jira_agent.config.template if jira_agent.config.template else None
59
+ formatter = IssueAnalysisMarkdownFormatter(template_path=template_name)
60
+ ai_analysis = formatter.format(analysis)
61
+
62
+ # Display analysis
63
+ ctx.textual.text("")
64
+ ctx.textual.text("AI Analysis Results", markup="bold cyan")
65
+ ctx.textual.text("")
66
+
67
+ # 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")
70
+ ctx.textual.text("")
71
+
72
+ # Show AI analysis as markdown
73
+ ctx.textual.markdown(ai_analysis)
74
+
75
+ # Show token usage
76
+ if analysis.total_tokens_used > 0:
77
+ ctx.textual.text(f"Tokens used: {analysis.total_tokens_used}", markup="dim")
78
+
79
+ # Save structured analysis to context
80
+ ctx.set("ai_analysis_structured", {
81
+ "functional_requirements": analysis.functional_requirements,
82
+ "non_functional_requirements": analysis.non_functional_requirements,
83
+ "acceptance_criteria": analysis.acceptance_criteria,
84
+ "technical_approach": analysis.technical_approach,
85
+ "dependencies": analysis.dependencies,
86
+ "risks": analysis.risks,
87
+ "edge_cases": analysis.edge_cases,
88
+ "suggested_subtasks": analysis.suggested_subtasks,
89
+ "complexity_score": analysis.complexity_score,
90
+ "estimated_effort": analysis.estimated_effort
91
+ })
92
+
93
+ return Success(
94
+ "AI analysis completed",
95
+ metadata={
96
+ "ai_analysis": ai_analysis,
97
+ "analyzed_issue_key": issue.key,
98
+ "tokens_used": analysis.total_tokens_used,
99
+ "complexity": analysis.complexity_score,
100
+ "effort": analysis.estimated_effort
101
+ }
102
+ )
103
+
104
+
105
+ __all__ = ["ai_analyze_issue_requirements_step"]
@@ -0,0 +1,82 @@
1
+ """
2
+ Get JIRA issue details step
3
+ """
4
+
5
+ from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
6
+ from titan_cli.ui.tui.widgets import Panel
7
+ from ..exceptions import JiraAPIError
8
+ from ..messages import msg
9
+
10
+
11
+ def get_issue_step(ctx: WorkflowContext) -> WorkflowResult:
12
+ """
13
+ Get JIRA issue details by key.
14
+
15
+ Inputs (from ctx.data):
16
+ jira_issue_key (str): JIRA issue key (e.g., "PROJ-123")
17
+ expand (list[str], optional): Additional fields to expand
18
+
19
+ Outputs (saved to ctx.data):
20
+ jira_issue (JiraTicket): Issue details
21
+
22
+ Returns:
23
+ Success: Issue retrieved
24
+ Error: Failed to get issue
25
+ """
26
+ if not ctx.textual:
27
+ return Error("Textual UI context is not available for this step.")
28
+
29
+ # Check if JIRA client is available
30
+ if not ctx.jira:
31
+ ctx.textual.mount(Panel(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT, panel_type="error"))
32
+ return Error(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
33
+
34
+ # Get issue key
35
+ issue_key = ctx.get("jira_issue_key")
36
+ if not issue_key:
37
+ ctx.textual.mount(Panel("JIRA issue key is required", panel_type="error"))
38
+ return Error("JIRA issue key is required")
39
+
40
+ # Get optional expand fields
41
+ expand = ctx.get("expand")
42
+
43
+ try:
44
+ # Get issue with loading indicator
45
+ with ctx.textual.loading(msg.Steps.GetIssue.GETTING_ISSUE.format(issue_key=issue_key)):
46
+ issue = ctx.jira.get_ticket(ticket_key=issue_key, expand=expand)
47
+
48
+ # 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
+ )
55
+
56
+ # Show issue details
57
+ ctx.textual.text(f" Title: {issue.summary}", markup="cyan")
58
+ ctx.textual.text(f" Status: {issue.status}")
59
+ ctx.textual.text(f" Type: {issue.issue_type}")
60
+ ctx.textual.text(f" Assignee: {issue.assignee or 'Unassigned'}")
61
+ ctx.textual.text("")
62
+
63
+ return Success(
64
+ msg.Steps.GetIssue.GET_SUCCESS.format(issue_key=issue_key),
65
+ metadata={"jira_issue": issue}
66
+ )
67
+
68
+ except JiraAPIError as e:
69
+ if e.status_code == 404:
70
+ error_msg = msg.Steps.GetIssue.ISSUE_NOT_FOUND.format(issue_key=issue_key)
71
+ ctx.textual.mount(Panel(error_msg, panel_type="error"))
72
+ return Error(error_msg)
73
+ error_msg = msg.Steps.GetIssue.GET_FAILED.format(e=e)
74
+ ctx.textual.mount(Panel(error_msg, panel_type="error"))
75
+ return Error(error_msg)
76
+ except Exception as e:
77
+ error_msg = f"Unexpected error getting issue: {e}"
78
+ ctx.textual.mount(Panel(error_msg, panel_type="error"))
79
+ return Error(error_msg)
80
+
81
+
82
+ __all__ = ["get_issue_step"]
@@ -0,0 +1,80 @@
1
+ """
2
+ Prompt user to select an issue from search results
3
+ """
4
+
5
+ from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
6
+ from titan_cli.ui.tui.widgets import Panel
7
+ from ..messages import msg
8
+
9
+
10
+ def prompt_select_issue_step(ctx: WorkflowContext) -> WorkflowResult:
11
+ """
12
+ Prompt user to select a JIRA issue from search results.
13
+
14
+ Inputs (from ctx.data):
15
+ jira_issues (List[JiraTicket]): List of issues from search
16
+
17
+ Outputs (saved to ctx.data):
18
+ jira_issue_key (str): Selected issue key
19
+ selected_issue (JiraTicket): Selected issue object
20
+ """
21
+ if not ctx.textual:
22
+ return Error("Textual UI context is not available for this step.")
23
+
24
+ # Get issues from previous search
25
+ issues = ctx.get("jira_issues")
26
+ if not issues:
27
+ return Error(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
28
+
29
+ if len(issues) == 0:
30
+ return Error(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
31
+
32
+ # Prompt user to select issue (issues already displayed in table from previous step)
33
+ ctx.textual.text("")
34
+
35
+ try:
36
+ # Ask for selection using text input and validate
37
+ response = ctx.textual.ask_text(
38
+ msg.Steps.PromptSelectIssue.ASK_ISSUE_NUMBER,
39
+ default=""
40
+ )
41
+
42
+ if not response or not response.strip():
43
+ return Error(msg.Steps.PromptSelectIssue.NO_ISSUE_SELECTED)
44
+
45
+ # Validate it's a number
46
+ try:
47
+ selected_index = int(response.strip())
48
+ except ValueError:
49
+ return Error(f"Invalid input: '{response}' is not a number")
50
+
51
+ # Validate it's in range
52
+ if selected_index < 1 or selected_index > len(issues):
53
+ return Error(f"Invalid selection: must be between 1 and {len(issues)}")
54
+
55
+ # Convert to 0-based index
56
+ selected_issue = issues[selected_index - 1]
57
+
58
+ 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"
66
+ )
67
+ )
68
+
69
+ return Success(
70
+ msg.Steps.PromptSelectIssue.SELECT_SUCCESS.format(key=selected_issue.key),
71
+ metadata={
72
+ "jira_issue_key": selected_issue.key,
73
+ "selected_issue": selected_issue
74
+ }
75
+ )
76
+ except (KeyboardInterrupt, EOFError):
77
+ return Error("User cancelled issue selection")
78
+
79
+
80
+ __all__ = ["prompt_select_issue_step"]