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
@@ -3,16 +3,20 @@ from titan_cli.engine import (
3
3
  WorkflowContext,
4
4
  WorkflowResult,
5
5
  Success,
6
- Error
6
+ Error,
7
+ Exit
7
8
  )
8
9
  from titan_cli.messages import msg as global_msg
9
10
  from ..messages import msg
10
- from titan_cli.ui.tui.widgets import Panel
11
11
 
12
12
  def get_git_status_step(ctx: WorkflowContext) -> WorkflowResult:
13
13
  """
14
14
  Retrieves the current git status and saves it to the context.
15
15
 
16
+ Behavior:
17
+ - If there are uncommitted changes: Returns Success and continues workflow
18
+ - If working directory is clean: Returns Exit (stops workflow - nothing to commit)
19
+
16
20
  Requires:
17
21
  ctx.git: An initialized GitClient.
18
22
 
@@ -20,40 +24,43 @@ def get_git_status_step(ctx: WorkflowContext) -> WorkflowResult:
20
24
  git_status (GitStatus): The full git status object, which includes the `is_clean` flag.
21
25
 
22
26
  Returns:
23
- Success: If the status was retrieved successfully.
27
+ Success: If there are changes to commit (workflow continues)
28
+ Exit: If working directory is clean (workflow stops - nothing to commit)
24
29
  Error: If the GitClient is not available or the git command fails.
25
30
  """
31
+ if not ctx.textual:
32
+ return Error("Textual UI context is not available for this step.")
33
+
26
34
  if not ctx.git:
27
35
  return Error(msg.Steps.Status.GIT_CLIENT_NOT_AVAILABLE)
28
36
 
37
+ # Begin step container
38
+ ctx.textual.begin_step("Check Git Status")
39
+
29
40
  try:
30
41
  status = ctx.git.get_status()
31
42
 
32
- if not ctx.textual:
33
- return Error("Textual UI context is not available for this step.")
34
-
35
- # If there are uncommitted changes, show warning panel
43
+ # If there are uncommitted changes, continue with workflow
36
44
  if not status.is_clean:
37
- ctx.textual.mount(
38
- Panel(
39
- text=global_msg.Workflow.UNCOMMITTED_CHANGES_WARNING,
40
- panel_type="warning"
41
- )
42
- )
45
+ ctx.textual.warning_text(global_msg.Workflow.UNCOMMITTED_CHANGES_WARNING)
43
46
  message = msg.Steps.Status.STATUS_RETRIEVED_WITH_UNCOMMITTED
44
- else:
45
- # Show success panel for clean working directory
46
- ctx.textual.mount(
47
- Panel(
48
- text=msg.Steps.Status.WORKING_DIRECTORY_IS_CLEAN,
49
- panel_type="success"
50
- )
47
+ ctx.textual.end_step("success")
48
+
49
+ return Success(
50
+ message=message,
51
+ metadata={"git_status": status}
51
52
  )
52
- message = msg.Steps.Status.WORKING_DIRECTORY_IS_CLEAN
53
+ else:
54
+ # Working directory is clean - exit workflow (nothing to commit)
55
+ ctx.textual.success_text(msg.Steps.Status.WORKING_DIRECTORY_IS_CLEAN)
56
+ ctx.textual.text("")
57
+ ctx.textual.dim_text("Nothing to commit. Skipping workflow.")
58
+ ctx.textual.end_step("success")
59
+
60
+ # Exit workflow early (not an error)
61
+ return Exit("No changes to commit", metadata={"git_status": status})
53
62
 
54
- return Success(
55
- message=message,
56
- metadata={"git_status": status}
57
- )
58
63
  except Exception as e:
64
+ # End step container with error
65
+ ctx.textual.end_step("error")
59
66
  return Error(msg.Steps.Status.FAILED_TO_GET_STATUS.format(e=e))
@@ -5,13 +5,19 @@ hooks:
5
5
  - before_commit # Hook for linting/testing
6
6
 
7
7
  steps:
8
- - hook: before_commit # This is where injected steps will run
9
-
8
+ # Check for changes FIRST - exits workflow if nothing to commit
10
9
  - id: git_status
11
10
  name: "Check Git Status"
12
11
  plugin: git
13
12
  step: get_status
14
13
 
14
+ - hook: before_commit # This is where injected steps will run
15
+
16
+ - id: diff_summary
17
+ name: "Show Changes Summary"
18
+ plugin: git
19
+ step: show_uncommitted_diff_summary
20
+
15
21
  - id: ai_commit_message
16
22
  name: "AI Commit Message"
17
23
  plugin: git
@@ -21,7 +27,7 @@ steps:
21
27
  name: "Create Commit"
22
28
  plugin: git
23
29
  step: create_commit
24
-
30
+
25
31
  - id: push
26
32
  name: "Push changes to remote"
27
33
  plugin: git
@@ -420,8 +420,9 @@ COMMIT_MESSAGE: <conventional commit message>"""
420
420
  ```
421
421
 
422
422
  ## CRITICAL Instructions
423
- 1. **Title**: Follow conventional commits (type(scope): description), be clear and descriptive
424
- - Examples: "feat(auth): add OAuth2 integration with Google provider", "fix(api): resolve race condition in cache invalidation"
423
+ 1. **Title**: Follow conventional commits (type(scope): Description), be clear and descriptive
424
+ - Start description with CAPITAL letter (imperative mood)
425
+ - Examples: "feat(auth): Add OAuth2 integration with Google provider", "fix(api): Resolve race condition in cache invalidation"
425
426
 
426
427
  2. **Description**: MUST follow the template structure above but keep it under {max_chars} characters total
427
428
  - Fill in the template sections (Summary, Type of Change, Changes Made, etc.)
@@ -460,6 +461,18 @@ DESCRIPTION:
460
461
  # Clean up title
461
462
  title = title.strip('"').strip("'")
462
463
 
464
+ # Ensure title subject starts with capital letter (conventional commits requirement)
465
+ # Format: type(scope): Description
466
+ if ':' in title:
467
+ parts = title.split(':', 1)
468
+ if len(parts) == 2:
469
+ prefix = parts[0] # type(scope)
470
+ subject = parts[1].strip() # description
471
+ # Capitalize first letter of subject
472
+ if subject and subject[0].islower():
473
+ subject = subject[0].upper() + subject[1:]
474
+ title = f"{prefix}: {subject}"
475
+
463
476
  # Truncate description if needed (but not title)
464
477
  if len(description) > max_chars:
465
478
  description = description[:max_chars - 3] + "..."
@@ -6,7 +6,6 @@ Uses PRAgent to analyze branch context and generate PR content.
6
6
  """
7
7
 
8
8
  from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error, Skip
9
- from titan_cli.ui.tui.widgets import Panel
10
9
 
11
10
  from ..agents import PRAgent
12
11
  from ..messages import msg
@@ -43,33 +42,34 @@ def ai_suggest_pr_description_step(ctx: WorkflowContext) -> WorkflowResult:
43
42
  if not ctx.textual:
44
43
  return Error("Textual UI context is not available for this step.")
45
44
 
45
+ # Begin step container
46
+ ctx.textual.begin_step("AI PR Description")
47
+
46
48
  # Check if AI is configured
47
49
  if not ctx.ai or not ctx.ai.is_available():
48
- ctx.textual.mount(
49
- Panel(
50
- text=msg.GitHub.AI.AI_NOT_CONFIGURED,
51
- panel_type="info"
52
- )
53
- )
50
+ ctx.textual.dim_text(msg.GitHub.AI.AI_NOT_CONFIGURED)
51
+ ctx.textual.end_step("skip")
54
52
  return Skip(msg.GitHub.AI.AI_NOT_CONFIGURED)
55
53
 
56
54
  # Get Git client
57
55
  if not ctx.git:
56
+ ctx.textual.end_step("error")
58
57
  return Error(msg.GitHub.AI.GIT_CLIENT_NOT_AVAILABLE)
59
58
 
60
59
  # Get branch info
61
60
  head_branch = ctx.get("pr_head_branch")
62
61
  if not head_branch:
62
+ ctx.textual.end_step("error")
63
63
  return Error(msg.GitHub.AI.MISSING_PR_HEAD_BRANCH)
64
64
 
65
65
  base_branch = ctx.git.main_branch
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,73 +88,131 @@ 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.mount(
92
- Panel(
93
- text="No commits found in branch to generate PR description.",
94
- panel_type="info"
95
- )
96
- )
91
+ ctx.textual.dim_text("No commits found in branch to generate PR description.")
92
+ ctx.textual.end_step("skip")
97
93
  return Skip("No commits found for PR generation")
98
94
 
99
95
  # Show PR size info
100
96
  if analysis.pr_size:
101
- ctx.textual.text(msg.GitHub.AI.PR_SIZE_INFO.format(
97
+ ctx.textual.dim_text(msg.GitHub.AI.PR_SIZE_INFO.format(
102
98
  pr_size=analysis.pr_size,
103
99
  files_changed=analysis.files_changed,
104
100
  diff_lines=analysis.lines_changed,
105
101
  max_chars="varies by size"
106
- ), markup="dim")
102
+ ))
107
103
 
108
104
  # Show PR preview to user
109
105
  ctx.textual.text("") # spacing
110
- ctx.textual.text(msg.GitHub.AI.AI_GENERATED_PR_TITLE, markup="bold")
106
+ ctx.textual.bold_text(msg.GitHub.AI.AI_GENERATED_PR_TITLE)
111
107
  ctx.textual.text("") # spacing
112
108
 
113
109
  # Show title
114
- ctx.textual.text(msg.GitHub.AI.TITLE_LABEL, markup="bold")
115
- 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}")
116
112
 
117
113
  # Warn if title is too long
118
114
  if len(analysis.pr_title) > 72:
119
- ctx.textual.text(msg.GitHub.AI.TITLE_TOO_LONG_WARNING.format(
115
+ ctx.textual.warning_text(msg.GitHub.AI.TITLE_TOO_LONG_WARNING.format(
120
116
  length=len(analysis.pr_title)
121
- ), markup="yellow")
117
+ ))
122
118
 
123
119
  ctx.textual.text("") # spacing
124
120
 
125
121
  # Show description
126
- ctx.textual.text(msg.GitHub.AI.DESCRIPTION_LABEL, markup="bold")
122
+ ctx.textual.bold_text(msg.GitHub.AI.DESCRIPTION_LABEL)
127
123
  # Render markdown in a scrollable container
128
124
  ctx.textual.markdown(analysis.pr_body)
129
125
 
126
+ # Scroll to show the choice buttons below
127
+ ctx.textual.scroll_to_end()
128
+
130
129
  ctx.textual.text("") # spacing
131
130
 
132
- # Single confirmation for both title and description
133
- use_ai_pr = ctx.textual.ask_confirm(
134
- msg.GitHub.AI.CONFIRM_USE_AI_PR,
135
- 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
136
143
  )
137
144
 
138
- if not use_ai_pr:
139
- 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)
148
+ ctx.textual.end_step("skip")
140
149
  return Skip("User rejected AI-generated PR")
141
150
 
142
- # Show success panel
143
- ctx.textual.mount(
144
- Panel(
145
- text="AI generated PR description successfully",
146
- panel_type="success"
147
- )
148
- )
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
149
206
 
150
207
  # Success - save to context
151
208
  metadata = {
152
209
  "ai_generated": True,
153
- "pr_title": analysis.pr_title,
154
- "pr_body": analysis.pr_body,
210
+ "pr_title": pr_title,
211
+ "pr_body": pr_body,
155
212
  "pr_size": analysis.pr_size
156
213
  }
157
214
 
215
+ ctx.textual.end_step("success")
158
216
  return Success(
159
217
  msg.GitHub.AI.AI_GENERATED_PR_DESCRIPTION_SUCCESS,
160
218
  metadata=metadata
@@ -162,9 +220,10 @@ def ai_suggest_pr_description_step(ctx: WorkflowContext) -> WorkflowResult:
162
220
 
163
221
  except Exception as e:
164
222
  # Don't fail the workflow, just skip AI and use manual prompts
165
- ctx.textual.text(msg.GitHub.AI.AI_GENERATION_FAILED.format(e=e), markup="yellow")
166
- 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)
167
225
 
226
+ ctx.textual.end_step("skip")
168
227
  return Skip(msg.GitHub.AI.AI_GENERATION_FAILED.format(e=e))
169
228
 
170
229
 
@@ -1,6 +1,5 @@
1
1
  # plugins/titan-plugin-github/titan_plugin_github/steps/create_pr_step.py
2
2
  from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
3
- from titan_cli.ui.tui.widgets import Panel
4
3
  from ..exceptions import GitHubAPIError
5
4
  from ..messages import msg
6
5
 
@@ -33,10 +32,17 @@ def create_pr_step(ctx: WorkflowContext) -> WorkflowResult:
33
32
  if not ctx.textual:
34
33
  return Error("Textual UI context is not available for this step.")
35
34
 
35
+ # Begin step container
36
+ ctx.textual.begin_step("Create Pull Request")
37
+
36
38
  # 1. Get GitHub client from context
37
39
  if not ctx.github:
40
+ ctx.textual.error_text("GitHub client is not available in the workflow context.")
41
+ ctx.textual.end_step("error")
38
42
  return Error("GitHub client is not available in the workflow context.")
39
43
  if not ctx.git:
44
+ ctx.textual.error_text("Git client is not available in the workflow context.")
45
+ ctx.textual.end_step("error")
40
46
  return Error("Git client is not available in the workflow context.")
41
47
 
42
48
  # 2. Get required data from context and client config
@@ -47,6 +53,8 @@ def create_pr_step(ctx: WorkflowContext) -> WorkflowResult:
47
53
  is_draft = ctx.get("pr_is_draft", False) # Default to not a draft
48
54
 
49
55
  if not all([title, base, head]):
56
+ ctx.textual.error_text("Missing required context for creating a pull request: pr_title, pr_head_branch.")
57
+ ctx.textual.end_step("error")
50
58
  return Error(
51
59
  "Missing required context for creating a pull request: pr_title, pr_head_branch."
52
60
  )
@@ -59,28 +67,30 @@ def create_pr_step(ctx: WorkflowContext) -> WorkflowResult:
59
67
  assignees = [current_user]
60
68
  except GitHubAPIError as e:
61
69
  # Log warning but continue without assignee
62
- 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}")
63
71
 
64
72
  # 4. Call the client method
65
73
  try:
74
+ ctx.textual.dim_text(f"Creating pull request '{title}' from {head} to {base}...")
66
75
  pr = ctx.github.create_pull_request(
67
76
  title=title, body=body, base=base, head=head, draft=is_draft, assignees=assignees
68
77
  )
69
- ctx.textual.mount(
70
- Panel(
71
- text=msg.GitHub.PR_CREATED.format(number=pr["number"], url=pr["url"]),
72
- panel_type="success"
73
- )
74
- )
78
+ ctx.textual.text("") # spacing
79
+ ctx.textual.success_text(msg.GitHub.PR_CREATED.format(number=pr["number"], url=pr["url"]))
75
80
 
76
81
  # 4. Return Success with PR info
82
+ ctx.textual.end_step("success")
77
83
  return Success(
78
84
  "Pull request created successfully.",
79
85
  metadata={"pr_number": pr["number"], "pr_url": pr["url"]},
80
86
  )
81
87
  except GitHubAPIError as e:
88
+ ctx.textual.error_text(f"Failed to create pull request: {e}")
89
+ ctx.textual.end_step("error")
82
90
  return Error(f"Failed to create pull request: {e}")
83
91
  except Exception as e:
92
+ ctx.textual.error_text(f"An unexpected error occurred while creating the pull request: {e}")
93
+ ctx.textual.end_step("error")
84
94
  return Error(
85
95
  f"An unexpected error occurred while creating the pull request: {e}"
86
96
  )
@@ -23,18 +23,27 @@ def prompt_for_pr_title_step(ctx: WorkflowContext) -> WorkflowResult:
23
23
  if not ctx.textual:
24
24
  return Error("Textual UI context is not available for this step.")
25
25
 
26
+ # Begin step container
27
+ ctx.textual.begin_step("Prompt for PR Title")
28
+
26
29
  # Skip if title already exists (e.g., from AI generation)
27
30
  if ctx.get("pr_title"):
31
+ ctx.textual.dim_text("PR title already provided, skipping manual prompt.")
32
+ ctx.textual.end_step("skip")
28
33
  return Skip("PR title already provided, skipping manual prompt.")
29
34
 
30
35
  try:
31
36
  title = ctx.textual.ask_text(msg.Prompts.ENTER_PR_TITLE)
32
37
  if not title:
38
+ ctx.textual.end_step("error")
33
39
  return Error("PR title cannot be empty.")
40
+ ctx.textual.end_step("success")
34
41
  return Success("PR title captured", metadata={"pr_title": title})
35
42
  except (KeyboardInterrupt, EOFError):
43
+ ctx.textual.end_step("error")
36
44
  return Error("User cancelled.")
37
45
  except Exception as e:
46
+ ctx.textual.end_step("error")
38
47
  return Error(f"Failed to prompt for PR title: {e}", exception=e)
39
48
 
40
49
 
@@ -57,17 +66,25 @@ def prompt_for_pr_body_step(ctx: WorkflowContext) -> WorkflowResult:
57
66
  if not ctx.textual:
58
67
  return Error("Textual UI context is not available for this step.")
59
68
 
69
+ # Begin step container
70
+ ctx.textual.begin_step("Prompt for PR Body")
71
+
60
72
  # Skip if body already exists (e.g., from AI generation)
61
73
  if ctx.get("pr_body"):
74
+ ctx.textual.dim_text("PR body already provided, skipping manual prompt.")
75
+ ctx.textual.end_step("skip")
62
76
  return Skip("PR body already provided, skipping manual prompt.")
63
77
 
64
78
  try:
65
79
  body = ctx.textual.ask_multiline(msg.Prompts.ENTER_PR_BODY, default="")
66
80
  # Body can be empty
81
+ ctx.textual.end_step("success")
67
82
  return Success("PR body captured", metadata={"pr_body": body})
68
83
  except (KeyboardInterrupt, EOFError):
84
+ ctx.textual.end_step("error")
69
85
  return Error("User cancelled.")
70
86
  except Exception as e:
87
+ ctx.textual.end_step("error")
71
88
  return Error(f"Failed to prompt for PR body: {e}", exception=e)
72
89
 
73
90
 
@@ -90,17 +107,25 @@ def prompt_for_issue_body_step(ctx: WorkflowContext) -> WorkflowResult:
90
107
  if not ctx.textual:
91
108
  return Error("Textual UI context is not available for this step.")
92
109
 
110
+ # Begin step container
111
+ ctx.textual.begin_step("Prompt for Issue Body")
112
+
93
113
  # Skip if body already exists (e.g., from AI generation)
94
114
  if ctx.get("issue_body"):
115
+ ctx.textual.dim_text("Issue body already provided, skipping manual prompt.")
116
+ ctx.textual.end_step("skip")
95
117
  return Skip("Issue body already provided, skipping manual prompt.")
96
118
 
97
119
  try:
98
120
  body = ctx.textual.ask_multiline(msg.Prompts.ENTER_ISSUE_BODY, default="")
99
121
  # Body can be empty
122
+ ctx.textual.end_step("success")
100
123
  return Success("Issue body captured", metadata={"issue_body": body})
101
124
  except (KeyboardInterrupt, EOFError):
125
+ ctx.textual.end_step("error")
102
126
  return Error("User cancelled.")
103
127
  except Exception as e:
128
+ ctx.textual.end_step("error")
104
129
  return Error(f"Failed to prompt for issue body: {e}", exception=e)
105
130
 
106
131
 
@@ -111,7 +136,12 @@ def prompt_for_self_assign_step(ctx: WorkflowContext) -> WorkflowResult:
111
136
  if not ctx.textual:
112
137
  return Error("Textual UI context is not available for this step.")
113
138
 
139
+ # Begin step container
140
+ ctx.textual.begin_step("Assign Issue")
141
+
114
142
  if not ctx.github:
143
+ ctx.textual.error_text("GitHub client not available")
144
+ ctx.textual.end_step("error")
115
145
  return Error("GitHub client not available")
116
146
 
117
147
  try:
@@ -121,11 +151,18 @@ def prompt_for_self_assign_step(ctx: WorkflowContext) -> WorkflowResult:
121
151
  if current_user not in assignees:
122
152
  assignees.append(current_user)
123
153
  ctx.set("assignees", assignees)
154
+ ctx.textual.success_text(f"Issue will be assigned to {current_user}")
155
+ ctx.textual.end_step("success")
124
156
  return Success(f"Issue will be assigned to {current_user}")
157
+ ctx.textual.dim_text("Issue will not be assigned to current user")
158
+ ctx.textual.end_step("success")
125
159
  return Success("Issue will not be assigned to current user")
126
160
  except (KeyboardInterrupt, EOFError):
161
+ ctx.textual.end_step("error")
127
162
  return Error("User cancelled.")
128
163
  except Exception as e:
164
+ ctx.textual.error_text(f"Failed to prompt for self-assign: {e}")
165
+ ctx.textual.end_step("error")
129
166
  return Error(f"Failed to prompt for self-assign: {e}", exception=e)
130
167
 
131
168
 
@@ -136,16 +173,23 @@ def prompt_for_labels_step(ctx: WorkflowContext) -> WorkflowResult:
136
173
  if not ctx.textual:
137
174
  return Error("Textual UI context is not available for this step.")
138
175
 
176
+ # Begin step container
177
+ ctx.textual.begin_step("Select Labels")
178
+
139
179
  if not ctx.github:
180
+ ctx.textual.error_text("GitHub client not available")
181
+ ctx.textual.end_step("error")
140
182
  return Error("GitHub client not available")
141
183
 
142
184
  try:
143
185
  available_labels = ctx.github.list_labels()
144
186
  if not available_labels:
187
+ ctx.textual.dim_text("No labels found in the repository.")
188
+ ctx.textual.end_step("skip")
145
189
  return Skip("No labels found in the repository.")
146
190
 
147
191
  # Show available labels
148
- ctx.textual.text(f"Available labels: {', '.join(available_labels)}", markup="dim")
192
+ ctx.textual.dim_text(f"Available labels: {', '.join(available_labels)}")
149
193
 
150
194
  # Get default labels as comma-separated string
151
195
  existing_labels = ctx.get("labels", [])
@@ -164,8 +208,16 @@ def prompt_for_labels_step(ctx: WorkflowContext) -> WorkflowResult:
164
208
  selected_labels = []
165
209
 
166
210
  ctx.set("labels", selected_labels)
211
+ if selected_labels:
212
+ ctx.textual.success_text(f"Selected labels: {', '.join(selected_labels)}")
213
+ else:
214
+ ctx.textual.dim_text("No labels selected")
215
+ ctx.textual.end_step("success")
167
216
  return Success("Labels selected")
168
217
  except (KeyboardInterrupt, EOFError):
218
+ ctx.textual.end_step("error")
169
219
  return Error("User cancelled.")
170
220
  except Exception as e:
221
+ ctx.textual.error_text(f"Failed to prompt for labels: {e}")
222
+ ctx.textual.end_step("error")
171
223
  return Error(f"Failed to prompt for labels: {e}", exception=e)