titan-cli 0.1.5__py3-none-any.whl → 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. titan_cli/core/workflows/project_step_source.py +52 -7
  2. titan_cli/core/workflows/workflow_filter_service.py +6 -4
  3. titan_cli/engine/__init__.py +5 -1
  4. titan_cli/engine/results.py +31 -1
  5. titan_cli/engine/steps/ai_assistant_step.py +18 -18
  6. titan_cli/engine/workflow_executor.py +7 -2
  7. titan_cli/ui/tui/screens/plugin_config_wizard.py +16 -0
  8. titan_cli/ui/tui/screens/workflow_execution.py +22 -24
  9. titan_cli/ui/tui/screens/workflows.py +8 -4
  10. titan_cli/ui/tui/textual_components.py +293 -189
  11. titan_cli/ui/tui/textual_workflow_executor.py +30 -2
  12. titan_cli/ui/tui/theme.py +34 -5
  13. titan_cli/ui/tui/widgets/__init__.py +15 -0
  14. titan_cli/ui/tui/widgets/multiline_input.py +32 -0
  15. titan_cli/ui/tui/widgets/prompt_choice.py +138 -0
  16. titan_cli/ui/tui/widgets/prompt_input.py +74 -0
  17. titan_cli/ui/tui/widgets/prompt_selection_list.py +150 -0
  18. titan_cli/ui/tui/widgets/prompt_textarea.py +87 -0
  19. titan_cli/ui/tui/widgets/styled_option_list.py +107 -0
  20. titan_cli/ui/tui/widgets/text.py +51 -130
  21. {titan_cli-0.1.5.dist-info → titan_cli-0.1.7.dist-info}/METADATA +5 -10
  22. {titan_cli-0.1.5.dist-info → titan_cli-0.1.7.dist-info}/RECORD +54 -41
  23. {titan_cli-0.1.5.dist-info → titan_cli-0.1.7.dist-info}/WHEEL +1 -1
  24. titan_plugin_git/clients/git_client.py +59 -2
  25. titan_plugin_git/plugin.py +10 -0
  26. titan_plugin_git/steps/ai_commit_message_step.py +8 -8
  27. titan_plugin_git/steps/branch_steps.py +6 -6
  28. titan_plugin_git/steps/checkout_step.py +66 -0
  29. titan_plugin_git/steps/commit_step.py +3 -3
  30. titan_plugin_git/steps/create_branch_step.py +131 -0
  31. titan_plugin_git/steps/diff_summary_step.py +11 -13
  32. titan_plugin_git/steps/pull_step.py +70 -0
  33. titan_plugin_git/steps/push_step.py +3 -3
  34. titan_plugin_git/steps/restore_original_branch_step.py +97 -0
  35. titan_plugin_git/steps/save_current_branch_step.py +82 -0
  36. titan_plugin_git/steps/status_step.py +23 -13
  37. titan_plugin_git/workflows/commit-ai.yaml +4 -3
  38. titan_plugin_github/steps/ai_pr_step.py +90 -22
  39. titan_plugin_github/steps/create_pr_step.py +8 -8
  40. titan_plugin_github/steps/github_prompt_steps.py +13 -13
  41. titan_plugin_github/steps/issue_steps.py +14 -15
  42. titan_plugin_github/steps/preview_step.py +8 -8
  43. titan_plugin_github/workflows/create-pr-ai.yaml +1 -11
  44. titan_plugin_jira/messages.py +12 -0
  45. titan_plugin_jira/plugin.py +4 -0
  46. titan_plugin_jira/steps/ai_analyze_issue_step.py +6 -6
  47. titan_plugin_jira/steps/get_issue_step.py +7 -7
  48. titan_plugin_jira/steps/list_versions_step.py +133 -0
  49. titan_plugin_jira/steps/prompt_select_issue_step.py +8 -9
  50. titan_plugin_jira/steps/search_jql_step.py +191 -0
  51. titan_plugin_jira/steps/search_saved_query_step.py +13 -13
  52. titan_plugin_jira/utils/__init__.py +1 -1
  53. {titan_cli-0.1.5.dist-info/licenses → titan_cli-0.1.7.dist-info}/LICENSE +0 -0
  54. {titan_cli-0.1.5.dist-info → titan_cli-0.1.7.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,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"]
@@ -26,12 +26,12 @@ def prompt_select_issue_step(ctx: WorkflowContext) -> WorkflowResult:
26
26
  # Get issues from previous search
27
27
  issues = ctx.get("jira_issues")
28
28
  if not issues:
29
- ctx.textual.text(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE, markup="red")
29
+ ctx.textual.error_text(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
30
30
  ctx.textual.end_step("error")
31
31
  return Error(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
32
32
 
33
33
  if len(issues) == 0:
34
- ctx.textual.text(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE, markup="red")
34
+ ctx.textual.error_text(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
35
35
  ctx.textual.end_step("error")
36
36
  return Error(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
37
37
 
@@ -46,7 +46,7 @@ def prompt_select_issue_step(ctx: WorkflowContext) -> WorkflowResult:
46
46
  )
47
47
 
48
48
  if not response or not response.strip():
49
- ctx.textual.text(msg.Steps.PromptSelectIssue.NO_ISSUE_SELECTED, markup="red")
49
+ ctx.textual.error_text(msg.Steps.PromptSelectIssue.NO_ISSUE_SELECTED)
50
50
  ctx.textual.end_step("error")
51
51
  return Error(msg.Steps.PromptSelectIssue.NO_ISSUE_SELECTED)
52
52
 
@@ -54,13 +54,13 @@ def prompt_select_issue_step(ctx: WorkflowContext) -> WorkflowResult:
54
54
  try:
55
55
  selected_index = int(response.strip())
56
56
  except ValueError:
57
- ctx.textual.text(f"Invalid input: '{response}' is not a number", markup="red")
57
+ ctx.textual.error_text(f"Invalid input: '{response}' is not a number")
58
58
  ctx.textual.end_step("error")
59
59
  return Error(f"Invalid input: '{response}' is not a number")
60
60
 
61
61
  # Validate it's in range
62
62
  if selected_index < 1 or selected_index > len(issues):
63
- ctx.textual.text(f"Invalid selection: must be between 1 and {len(issues)}", markup="red")
63
+ ctx.textual.error_text(f"Invalid selection: must be between 1 and {len(issues)}")
64
64
  ctx.textual.end_step("error")
65
65
  return Error(f"Invalid selection: must be between 1 and {len(issues)}")
66
66
 
@@ -68,12 +68,11 @@ def prompt_select_issue_step(ctx: WorkflowContext) -> WorkflowResult:
68
68
  selected_issue = issues[selected_index - 1]
69
69
 
70
70
  ctx.textual.text("")
71
- ctx.textual.text(
71
+ ctx.textual.success_text(
72
72
  msg.Steps.PromptSelectIssue.ISSUE_SELECTION_CONFIRM.format(
73
73
  key=selected_issue.key,
74
74
  summary=selected_issue.summary
75
- ),
76
- markup="green"
75
+ )
77
76
  )
78
77
 
79
78
  ctx.textual.end_step("success")
@@ -85,7 +84,7 @@ def prompt_select_issue_step(ctx: WorkflowContext) -> WorkflowResult:
85
84
  }
86
85
  )
87
86
  except (KeyboardInterrupt, EOFError):
88
- ctx.textual.text("User cancelled issue selection", markup="red")
87
+ ctx.textual.error_text("User cancelled issue selection")
89
88
  ctx.textual.end_step("error")
90
89
  return Error("User cancelled issue selection")
91
90
 
@@ -0,0 +1,191 @@
1
+ """
2
+ Search JIRA issues using custom JQL query
3
+ """
4
+
5
+ from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
6
+ from titan_cli.ui.tui.widgets import Table
7
+ from ..exceptions import JiraAPIError
8
+ from ..messages import msg
9
+ from ..utils import IssueSorter
10
+
11
+
12
+ def search_jql_step(ctx: WorkflowContext) -> WorkflowResult:
13
+ """
14
+ Search JIRA issues using a custom JQL query.
15
+
16
+ This is a generic search step that accepts raw JQL and allows variable substitution.
17
+ Use this when you need a specific query that isn't covered by saved queries.
18
+
19
+ Inputs (from ctx.data):
20
+ jql (str): JQL query string (supports variable substitution with ${var_name})
21
+ max_results (int, optional): Maximum number of results (default: 100)
22
+
23
+ Outputs (saved to ctx.data):
24
+ jira_issues (list): List of JiraTicket objects
25
+ jira_issue_count (int): Number of issues found
26
+
27
+ Returns:
28
+ Success: Issues found
29
+ Error: Search failed or JQL not provided
30
+
31
+ Variable substitution:
32
+ You can use ${variable_name} in the JQL and it will be replaced with values from ctx.data.
33
+ Example: "project = ${project_key} AND fixVersion = ${fix_version}"
34
+
35
+ Example usage in workflow:
36
+ ```yaml
37
+ - id: search_issues
38
+ plugin: jira
39
+ step: search_jql
40
+ params:
41
+ jql: "project = MYPROJECT AND status = 'In Progress' ORDER BY created DESC"
42
+ max_results: 50
43
+
44
+ # With variable substitution:
45
+ - id: search_release
46
+ plugin: jira
47
+ step: search_jql
48
+ params:
49
+ jql: "project = ${project_key} AND fixVersion = ${fix_version} ORDER BY created DESC"
50
+ ```
51
+ """
52
+ if not ctx.textual:
53
+ return Error("Textual UI context is not available for this step.")
54
+
55
+ # Begin step container
56
+ ctx.textual.begin_step("Search JIRA Issues")
57
+
58
+ if not ctx.jira:
59
+ ctx.textual.error_text(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
60
+ ctx.textual.end_step("error")
61
+ return Error(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
62
+
63
+ # Get JQL query
64
+ jql = ctx.get("jql")
65
+ if not jql:
66
+ error_msg = "JQL query is required but not provided"
67
+ ctx.textual.error_text(error_msg)
68
+ ctx.textual.dim_text("Provide 'jql' parameter in workflow step")
69
+ ctx.textual.end_step("error")
70
+ return Error(error_msg)
71
+
72
+ # Perform variable substitution: replace ${var} with ctx.data values
73
+ # This allows dynamic queries like "project = ${project_key}"
74
+ import re
75
+
76
+ def replace_var(match):
77
+ var_name = match.group(1)
78
+ value = ctx.get(var_name)
79
+ if value is None:
80
+ # Variable not found in context, keep original placeholder
81
+ return match.group(0)
82
+ return str(value)
83
+
84
+ # Replace ${variable} patterns
85
+ jql = re.sub(r'\$\{([^}]+)\}', replace_var, jql)
86
+
87
+ # Show which query is being executed
88
+ ctx.textual.text("")
89
+ ctx.textual.bold_text("Executing JQL Query:")
90
+ ctx.textual.dim_text(f" {jql}")
91
+ ctx.textual.text("")
92
+
93
+ # Get max results
94
+ max_results = ctx.get("max_results", 100)
95
+
96
+ try:
97
+ # Execute search with loading indicator
98
+ # Request ALL fields including custom fields
99
+ with ctx.textual.loading("Searching JIRA issues..."):
100
+ issues = ctx.jira.search_tickets(jql=jql, max_results=max_results, fields=["*all"])
101
+
102
+ if not issues:
103
+ ctx.textual.dim_text("No issues found")
104
+ ctx.textual.end_step("success")
105
+ return Success(
106
+ "No issues found",
107
+ metadata={
108
+ "jira_issues": [],
109
+ "issues": [], # Alias for compatibility
110
+ "jira_issue_count": 0
111
+ }
112
+ )
113
+
114
+ # Show results
115
+ ctx.textual.text("") # spacing
116
+ ctx.textual.success_text(f"Found {len(issues)} issues")
117
+ ctx.textual.text("")
118
+
119
+ # Show detailed table
120
+ ctx.textual.bold_text("Found Issues:")
121
+ ctx.textual.text("")
122
+
123
+ try:
124
+ # Sort issues intelligently
125
+ sorter = IssueSorter()
126
+ sorted_issues = sorter.sort(issues)
127
+
128
+ # Prepare table data with row numbers for selection
129
+ headers = ["#", "Key", "Status", "Summary", "Assignee", "Type", "Priority"]
130
+ rows = []
131
+ for i, issue in enumerate(sorted_issues, 1):
132
+ assignee = issue.assignee or "Unassigned"
133
+ status = issue.status or "Unknown"
134
+ priority = issue.priority or "Unknown"
135
+ issue_type = issue.issue_type or "Unknown"
136
+ summary = (issue.summary or "No summary")[:60]
137
+
138
+ rows.append([
139
+ str(i),
140
+ issue.key,
141
+ status,
142
+ summary,
143
+ assignee,
144
+ issue_type,
145
+ priority
146
+ ])
147
+
148
+ # Render table using textual widget
149
+ ctx.textual.mount(
150
+ Table(
151
+ headers=headers,
152
+ rows=rows,
153
+ title=f"Issues (sorted by {sorter.get_sort_description()})"
154
+ )
155
+ )
156
+
157
+ # Use sorted issues for downstream steps
158
+ issues = sorted_issues
159
+ except Exception as e:
160
+ # If table rendering fails, show error but continue with raw issue list
161
+ ctx.textual.error_text(f"Error rendering table: {e}")
162
+ ctx.textual.primary_text(f"Found {len(issues)} issues (showing raw data)")
163
+ for i, issue in enumerate(issues, 1):
164
+ ctx.textual.text(f"{i}. {issue.key} - {getattr(issue, 'summary', 'N/A')}")
165
+ ctx.textual.text("")
166
+
167
+ ctx.textual.end_step("success")
168
+ return Success(
169
+ f"Found {len(issues)} issues",
170
+ metadata={
171
+ "jira_issues": issues,
172
+ "issues": issues, # Alias for compatibility
173
+ "jira_issue_count": len(issues)
174
+ }
175
+ )
176
+
177
+ except JiraAPIError as e:
178
+ error_msg = f"JIRA search failed: {e}"
179
+ ctx.textual.error_text(error_msg)
180
+ ctx.textual.end_step("error")
181
+ return Error(error_msg)
182
+ except Exception as e:
183
+ import traceback
184
+ error_detail = traceback.format_exc()
185
+ error_msg = f"Unexpected error: {e}\n\nTraceback:\n{error_detail}"
186
+ ctx.textual.error_text(error_msg)
187
+ ctx.textual.end_step("error")
188
+ return Error(error_msg)
189
+
190
+
191
+ __all__ = ["search_jql_step"]
@@ -61,14 +61,14 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
61
61
  ctx.textual.begin_step("Search Open Issues")
62
62
 
63
63
  if not ctx.jira:
64
- ctx.textual.text(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT, markup="red")
64
+ ctx.textual.error_text(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
65
65
  ctx.textual.end_step("error")
66
66
  return Error(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
67
67
 
68
68
  # Get query name
69
69
  query_name = ctx.get("query_name")
70
70
  if not query_name:
71
- ctx.textual.text(msg.Steps.Search.QUERY_NAME_REQUIRED, markup="red")
71
+ ctx.textual.error_text(msg.Steps.Search.QUERY_NAME_REQUIRED)
72
72
  ctx.textual.end_step("error")
73
73
  return Error(msg.Steps.Search.QUERY_NAME_REQUIRED)
74
74
 
@@ -107,7 +107,7 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
107
107
  error_msg += "\n\n" + msg.Steps.Search.ADD_CUSTOM_HINT + "\n"
108
108
  error_msg += msg.Steps.Search.CUSTOM_QUERY_EXAMPLE
109
109
 
110
- ctx.textual.text(error_msg, markup="red")
110
+ ctx.textual.error_text(error_msg)
111
111
  ctx.textual.end_step("error")
112
112
  return Error(error_msg)
113
113
 
@@ -125,7 +125,7 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
125
125
 
126
126
  if not project:
127
127
  error_msg = msg.Steps.Search.PROJECT_REQUIRED.format(query_name=query_name, jql=jql)
128
- ctx.textual.text(error_msg, markup="red")
128
+ ctx.textual.error_text(error_msg)
129
129
  ctx.textual.end_step("error")
130
130
  return Error(error_msg)
131
131
 
@@ -136,8 +136,8 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
136
136
  source_label = "Custom" if is_custom else "Predefined"
137
137
 
138
138
  ctx.textual.text("")
139
- ctx.textual.text(f"Using {source_label} Query: {query_name}", markup="bold")
140
- ctx.textual.text(f" JQL: {jql}", markup="dim")
139
+ ctx.textual.bold_text(f"Using {source_label} Query: {query_name}")
140
+ ctx.textual.dim_text(f" JQL: {jql}")
141
141
  ctx.textual.text("")
142
142
 
143
143
  # Get max results
@@ -149,7 +149,7 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
149
149
  issues = ctx.jira.search_tickets(jql=jql, max_results=max_results)
150
150
 
151
151
  if not issues:
152
- ctx.textual.text(f"No issues found for query: {query_name}", markup="dim")
152
+ ctx.textual.dim_text(f"No issues found for query: {query_name}")
153
153
  ctx.textual.end_step("success")
154
154
  return Success(
155
155
  "No issues found",
@@ -162,11 +162,11 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
162
162
 
163
163
  # Show results
164
164
  ctx.textual.text("") # spacing
165
- ctx.textual.text(f"Found {len(issues)} issues", markup="green")
165
+ ctx.textual.success_text(f"Found {len(issues)} issues")
166
166
  ctx.textual.text("")
167
167
 
168
168
  # Show detailed table
169
- ctx.textual.text("Found Issues:", markup="bold")
169
+ ctx.textual.bold_text("Found Issues:")
170
170
  ctx.textual.text("")
171
171
 
172
172
  try:
@@ -207,8 +207,8 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
207
207
  issues = sorted_issues
208
208
  except Exception as e:
209
209
  # If table rendering fails, show error but continue with raw issue list
210
- ctx.textual.text(f"Error rendering table: {e}", markup="red")
211
- ctx.textual.text(f"Found {len(issues)} issues (showing raw data)", markup="cyan")
210
+ ctx.textual.error_text(f"Error rendering table: {e}")
211
+ ctx.textual.primary_text(f"Found {len(issues)} issues (showing raw data)")
212
212
  for i, issue in enumerate(issues, 1):
213
213
  ctx.textual.text(f"{i}. {issue.key} - {getattr(issue, 'summary', 'N/A')}")
214
214
  ctx.textual.text("")
@@ -225,14 +225,14 @@ def search_saved_query_step(ctx: WorkflowContext) -> WorkflowResult:
225
225
 
226
226
  except JiraAPIError as e:
227
227
  error_msg = f"JIRA search failed: {e}"
228
- ctx.textual.text(error_msg, markup="red")
228
+ ctx.textual.error_text(error_msg)
229
229
  ctx.textual.end_step("error")
230
230
  return Error(error_msg)
231
231
  except Exception as e:
232
232
  import traceback
233
233
  error_detail = traceback.format_exc()
234
234
  error_msg = f"Unexpected error: {e}\n\nTraceback:\n{error_detail}"
235
- ctx.textual.text(error_msg, markup="red")
235
+ ctx.textual.error_text(error_msg)
236
236
  ctx.textual.end_step("error")
237
237
  return Error(error_msg)
238
238
 
@@ -9,5 +9,5 @@ __all__ = [
9
9
  "SavedQueries",
10
10
  "SAVED_QUERIES",
11
11
  "IssueSorter",
12
- "IssueSortConfig"
12
+ "IssueSortConfig",
13
13
  ]