titan-cli 0.1.4__py3-none-any.whl → 0.1.5__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 (39) 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 +48 -30
  4. titan_cli/core/workflows/workflow_filter_service.py +14 -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/steps/ai_assistant_step.py +42 -7
  8. titan_cli/engine/workflow_executor.py +6 -1
  9. titan_cli/ui/tui/screens/workflow_execution.py +8 -28
  10. titan_cli/ui/tui/textual_components.py +59 -6
  11. titan_cli/ui/tui/textual_workflow_executor.py +9 -1
  12. titan_cli/ui/tui/widgets/__init__.py +2 -0
  13. titan_cli/ui/tui/widgets/step_container.py +70 -0
  14. {titan_cli-0.1.4.dist-info → titan_cli-0.1.5.dist-info}/METADATA +6 -3
  15. {titan_cli-0.1.4.dist-info → titan_cli-0.1.5.dist-info}/RECORD +39 -37
  16. {titan_cli-0.1.4.dist-info → titan_cli-0.1.5.dist-info}/WHEEL +1 -1
  17. titan_plugin_git/clients/git_client.py +82 -4
  18. titan_plugin_git/plugin.py +3 -0
  19. titan_plugin_git/steps/ai_commit_message_step.py +33 -28
  20. titan_plugin_git/steps/branch_steps.py +18 -37
  21. titan_plugin_git/steps/commit_step.py +18 -22
  22. titan_plugin_git/steps/diff_summary_step.py +182 -0
  23. titan_plugin_git/steps/push_step.py +27 -11
  24. titan_plugin_git/steps/status_step.py +15 -18
  25. titan_plugin_git/workflows/commit-ai.yaml +5 -0
  26. titan_plugin_github/agents/pr_agent.py +15 -2
  27. titan_plugin_github/steps/ai_pr_step.py +12 -21
  28. titan_plugin_github/steps/create_pr_step.py +17 -7
  29. titan_plugin_github/steps/github_prompt_steps.py +52 -0
  30. titan_plugin_github/steps/issue_steps.py +28 -14
  31. titan_plugin_github/steps/preview_step.py +11 -0
  32. titan_plugin_github/utils.py +5 -4
  33. titan_plugin_github/workflows/create-pr-ai.yaml +5 -0
  34. titan_plugin_jira/steps/ai_analyze_issue_step.py +8 -3
  35. titan_plugin_jira/steps/get_issue_step.py +16 -12
  36. titan_plugin_jira/steps/prompt_select_issue_step.py +22 -9
  37. titan_plugin_jira/steps/search_saved_query_step.py +21 -19
  38. {titan_cli-0.1.4.dist-info → titan_cli-0.1.5.dist-info}/entry_points.txt +0 -0
  39. {titan_cli-0.1.4.dist-info → titan_cli-0.1.5.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,182 @@
1
+ # plugins/titan-plugin-git/titan_plugin_git/steps/diff_summary_step.py
2
+ from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error, Skip
3
+ from titan_plugin_git.messages import msg
4
+
5
+
6
+ def show_uncommitted_diff_summary(ctx: WorkflowContext) -> WorkflowResult:
7
+ """
8
+ Show summary of uncommitted changes (git diff --stat).
9
+
10
+ Provides a visual overview of files changed and lines modified
11
+ before generating commit messages.
12
+
13
+ Returns:
14
+ Success: Always (even if no changes, for workflow continuity)
15
+ """
16
+ if not ctx.textual:
17
+ return Error("Textual UI context is not available for this step.")
18
+
19
+ if not ctx.git:
20
+ return Error(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE)
21
+
22
+ # Begin step container
23
+ ctx.textual.begin_step("Show Changes Summary")
24
+
25
+ try:
26
+ # Get diff stat for uncommitted changes
27
+ stat_output = ctx.git._run_command(["git", "diff", "--stat", "HEAD"])
28
+
29
+ if not stat_output or not stat_output.strip():
30
+ ctx.textual.text("No uncommitted changes to show", markup="dim")
31
+ ctx.textual.end_step("success")
32
+ return Success("No changes")
33
+
34
+ # Show the stat summary with colors
35
+ ctx.textual.text("") # spacing
36
+ ctx.textual.text("Changes summary:", markup="bold")
37
+ ctx.textual.text("") # spacing
38
+
39
+ # Parse lines to find max filename length for alignment
40
+ file_lines = []
41
+ summary_lines = []
42
+ max_filename_len = 0
43
+
44
+ for line in stat_output.split('\n'):
45
+ if not line.strip():
46
+ continue
47
+
48
+ if '|' in line:
49
+ parts = line.split('|')
50
+ filename = parts[0].strip()
51
+ stats = '|'.join(parts[1:]) if len(parts) > 1 else ''
52
+ file_lines.append((filename, stats))
53
+ max_filename_len = max(max_filename_len, len(filename))
54
+ else:
55
+ summary_lines.append(line)
56
+
57
+ # Display aligned file changes
58
+ for filename, stats in file_lines:
59
+ # Pad filename to align pipes
60
+ padded_filename = filename.ljust(max_filename_len)
61
+
62
+ # Replace + with green and - with red
63
+ stats = stats.replace('+', '[green]+[/green]')
64
+ stats = stats.replace('-', '[red]-[/red]')
65
+
66
+ ctx.textual.text(f" {padded_filename} | {stats}")
67
+
68
+ # Display summary lines
69
+ for line in summary_lines:
70
+ colored_line = line.replace('(+)', '[green](+)[/green]')
71
+ colored_line = colored_line.replace('(-)', '[red](-)[/red]')
72
+ ctx.textual.text(f" {colored_line}", markup="dim")
73
+
74
+ ctx.textual.text("") # spacing
75
+
76
+ # End step container with success
77
+ ctx.textual.end_step("success")
78
+
79
+ return Success("Diff summary displayed")
80
+
81
+ except Exception as e:
82
+ # Don't fail the workflow, just skip
83
+ ctx.textual.end_step("skip")
84
+ return Skip(f"Could not show diff summary: {e}")
85
+
86
+
87
+ def show_branch_diff_summary(ctx: WorkflowContext) -> WorkflowResult:
88
+ """
89
+ Show summary of branch changes (git diff base...head --stat).
90
+
91
+ Provides a visual overview of files changed between branches
92
+ before generating PR descriptions.
93
+
94
+ Inputs (from ctx.data):
95
+ pr_head_branch (str): Head branch name
96
+
97
+ Returns:
98
+ Success: Always (even if no changes, for workflow continuity)
99
+ """
100
+ if not ctx.textual:
101
+ return Error("Textual UI context is not available for this step.")
102
+
103
+ # Begin step container
104
+ ctx.textual.begin_step("Show Branch Changes Summary")
105
+
106
+ if not ctx.git:
107
+ ctx.textual.text(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE, markup="red")
108
+ ctx.textual.end_step("error")
109
+ return Error(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE)
110
+
111
+ head_branch = ctx.get("pr_head_branch")
112
+ if not head_branch:
113
+ ctx.textual.text("No head branch specified", markup="dim")
114
+ ctx.textual.end_step("skip")
115
+ return Skip("No head branch specified")
116
+
117
+ base_branch = ctx.git.main_branch
118
+
119
+ try:
120
+ # Get diff stat between branches
121
+ stat_output = ctx.git._run_command([
122
+ "git", "diff", "--stat", f"{base_branch}...{head_branch}"
123
+ ])
124
+
125
+ if not stat_output or not stat_output.strip():
126
+ ctx.textual.text(f"No changes between {base_branch} and {head_branch}", markup="dim")
127
+ ctx.textual.end_step("success")
128
+ return Success("No changes")
129
+
130
+ # Show the stat summary with colors
131
+ ctx.textual.text("") # spacing
132
+ ctx.textual.text(f"Changes in {head_branch} vs {base_branch}:", markup="bold")
133
+ ctx.textual.text("") # spacing
134
+
135
+ # Parse lines to find max filename length for alignment
136
+ file_lines = []
137
+ summary_lines = []
138
+ max_filename_len = 0
139
+
140
+ for line in stat_output.split('\n'):
141
+ if not line.strip():
142
+ continue
143
+
144
+ if '|' in line:
145
+ parts = line.split('|')
146
+ filename = parts[0].strip()
147
+ stats = '|'.join(parts[1:]) if len(parts) > 1 else ''
148
+ file_lines.append((filename, stats))
149
+ max_filename_len = max(max_filename_len, len(filename))
150
+ else:
151
+ summary_lines.append(line)
152
+
153
+ # Display aligned file changes
154
+ for filename, stats in file_lines:
155
+ # Pad filename to align pipes
156
+ padded_filename = filename.ljust(max_filename_len)
157
+
158
+ # Replace + with green and - with red
159
+ stats = stats.replace('+', '[green]+[/green]')
160
+ stats = stats.replace('-', '[red]-[/red]')
161
+
162
+ ctx.textual.text(f" {padded_filename} | {stats}")
163
+
164
+ # Display summary lines
165
+ for line in summary_lines:
166
+ colored_line = line.replace('(+)', '[green](+)[/green]')
167
+ colored_line = colored_line.replace('(-)', '[red](-)[/red]')
168
+ ctx.textual.text(f" {colored_line}", markup="dim")
169
+
170
+ ctx.textual.text("") # spacing
171
+
172
+ ctx.textual.end_step("success")
173
+ return Success("Branch diff summary displayed")
174
+
175
+ except Exception as e:
176
+ # Don't fail the workflow, just skip
177
+ ctx.textual.text(f"Could not show branch diff summary: {e}", markup="yellow")
178
+ ctx.textual.end_step("skip")
179
+ return Skip(f"Could not show branch diff summary: {e}")
180
+
181
+
182
+ __all__ = ["show_uncommitted_diff_summary", "show_branch_diff_summary"]
@@ -2,7 +2,6 @@
2
2
  from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
3
3
  from titan_plugin_git.exceptions import GitCommandError
4
4
  from titan_plugin_git.messages import msg
5
- from titan_cli.ui.tui.widgets import Panel
6
5
 
7
6
  def create_git_push_step(ctx: WorkflowContext) -> WorkflowResult:
8
7
  """
@@ -12,6 +11,7 @@ def create_git_push_step(ctx: WorkflowContext) -> WorkflowResult:
12
11
  remote (str, optional): The name of the remote to push to. Defaults to the client's default remote.
13
12
  branch (str, optional): The name of the branch to push. Defaults to the current branch.
14
13
  set_upstream (bool, optional): Whether to set the upstream tracking branch. Defaults to False.
14
+ push_tags (bool, optional): Whether to push tags along with the branch. Defaults to False.
15
15
 
16
16
  Requires:
17
17
  ctx.git: An initialized GitClient.
@@ -29,10 +29,14 @@ def create_git_push_step(ctx: WorkflowContext) -> WorkflowResult:
29
29
  if not ctx.git:
30
30
  return Error(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE)
31
31
 
32
+ # Begin step container
33
+ ctx.textual.begin_step("Push changes to remote")
34
+
32
35
  # Get params from context
33
36
  remote = ctx.get('remote')
34
37
  branch = ctx.get('branch')
35
38
  set_upstream = ctx.get('set_upstream', False)
39
+ push_tags = ctx.get('push_tags', False)
36
40
 
37
41
  # Use defaults from the GitClient if not provided in the context
38
42
  remote_to_use = remote or ctx.git.default_remote
@@ -43,21 +47,33 @@ def create_git_push_step(ctx: WorkflowContext) -> WorkflowResult:
43
47
  if not ctx.git.branch_exists_on_remote(branch=branch_to_use, remote=remote_to_use):
44
48
  set_upstream = True
45
49
 
46
- ctx.git.push(remote=remote_to_use, branch=branch_to_use, set_upstream=set_upstream)
47
-
48
- # Show success panel
49
- ctx.textual.mount(
50
- Panel(
51
- text=f"Pushed to {remote_to_use}/{branch_to_use}",
52
- panel_type="success"
53
- )
50
+ # Push branch (and tags if requested)
51
+ ctx.git.push(
52
+ remote=remote_to_use,
53
+ branch=branch_to_use,
54
+ set_upstream=set_upstream,
55
+ tags=push_tags
54
56
  )
55
57
 
58
+ # Show success message
59
+ success_msg = f"Pushed to {remote_to_use}/{branch_to_use}"
60
+ if push_tags:
61
+ success_msg += " (with tags)"
62
+
63
+ ctx.textual.text(success_msg, markup="green")
64
+
65
+ ctx.textual.end_step("success")
56
66
  return Success(
57
67
  message=msg.Git.PUSH_SUCCESS.format(remote=remote_to_use, branch=branch_to_use),
58
68
  metadata={"pr_head_branch": branch_to_use}
59
69
  )
60
70
  except GitCommandError as e:
61
- return Error(msg.Steps.Push.PUSH_FAILED.format(e=e))
71
+ error_msg = msg.Steps.Push.PUSH_FAILED.format(e=e)
72
+ ctx.textual.text(error_msg, markup="red")
73
+ ctx.textual.end_step("error")
74
+ return Error(error_msg)
62
75
  except Exception as e:
63
- return Error(msg.Git.UNEXPECTED_ERROR.format(e=e))
76
+ error_msg = msg.Git.UNEXPECTED_ERROR.format(e=e)
77
+ ctx.textual.text(error_msg, markup="red")
78
+ ctx.textual.end_step("error")
79
+ return Error(error_msg)
@@ -7,7 +7,6 @@ from titan_cli.engine import (
7
7
  )
8
8
  from titan_cli.messages import msg as global_msg
9
9
  from ..messages import msg
10
- from titan_cli.ui.tui.widgets import Panel
11
10
 
12
11
  def get_git_status_step(ctx: WorkflowContext) -> WorkflowResult:
13
12
  """
@@ -23,37 +22,35 @@ def get_git_status_step(ctx: WorkflowContext) -> WorkflowResult:
23
22
  Success: If the status was retrieved successfully.
24
23
  Error: If the GitClient is not available or the git command fails.
25
24
  """
25
+ if not ctx.textual:
26
+ return Error("Textual UI context is not available for this step.")
27
+
26
28
  if not ctx.git:
27
29
  return Error(msg.Steps.Status.GIT_CLIENT_NOT_AVAILABLE)
28
30
 
31
+ # Begin step container
32
+ ctx.textual.begin_step("Check Git Status")
33
+
29
34
  try:
30
35
  status = ctx.git.get_status()
31
36
 
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
37
+ # If there are uncommitted changes, show text (border will be green on success)
36
38
  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
- )
39
+ ctx.textual.text(global_msg.Workflow.UNCOMMITTED_CHANGES_WARNING, markup="yellow")
43
40
  message = msg.Steps.Status.STATUS_RETRIEVED_WITH_UNCOMMITTED
44
41
  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
- )
51
- )
42
+ # Show text for clean working directory (border will be green)
43
+ ctx.textual.text(msg.Steps.Status.WORKING_DIRECTORY_IS_CLEAN, markup="green")
52
44
  message = msg.Steps.Status.WORKING_DIRECTORY_IS_CLEAN
53
45
 
46
+ # End step container with success
47
+ ctx.textual.end_step("success")
48
+
54
49
  return Success(
55
50
  message=message,
56
51
  metadata={"git_status": status}
57
52
  )
58
53
  except Exception as e:
54
+ # End step container with error
55
+ ctx.textual.end_step("error")
59
56
  return Error(msg.Steps.Status.FAILED_TO_GET_STATUS.format(e=e))
@@ -12,6 +12,11 @@ steps:
12
12
  plugin: git
13
13
  step: get_status
14
14
 
15
+ - id: diff_summary
16
+ name: "Show Changes Summary"
17
+ plugin: git
18
+ step: show_uncommitted_diff_summary
19
+
15
20
  - id: ai_commit_message
16
21
  name: "AI Commit Message"
17
22
  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,23 +42,24 @@ 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.text(msg.GitHub.AI.AI_NOT_CONFIGURED, markup="dim")
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
@@ -88,12 +88,8 @@ 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.text("No commits found in branch to generate PR description.", markup="dim")
92
+ ctx.textual.end_step("skip")
97
93
  return Skip("No commits found for PR generation")
98
94
 
99
95
  # Show PR size info
@@ -137,16 +133,9 @@ def ai_suggest_pr_description_step(ctx: WorkflowContext) -> WorkflowResult:
137
133
 
138
134
  if not use_ai_pr:
139
135
  ctx.textual.text(msg.GitHub.AI.AI_SUGGESTION_REJECTED, markup="yellow")
136
+ ctx.textual.end_step("skip")
140
137
  return Skip("User rejected AI-generated PR")
141
138
 
142
- # Show success panel
143
- ctx.textual.mount(
144
- Panel(
145
- text="AI generated PR description successfully",
146
- panel_type="success"
147
- )
148
- )
149
-
150
139
  # Success - save to context
151
140
  metadata = {
152
141
  "ai_generated": True,
@@ -155,6 +144,7 @@ def ai_suggest_pr_description_step(ctx: WorkflowContext) -> WorkflowResult:
155
144
  "pr_size": analysis.pr_size
156
145
  }
157
146
 
147
+ ctx.textual.end_step("success")
158
148
  return Success(
159
149
  msg.GitHub.AI.AI_GENERATED_PR_DESCRIPTION_SUCCESS,
160
150
  metadata=metadata
@@ -165,6 +155,7 @@ def ai_suggest_pr_description_step(ctx: WorkflowContext) -> WorkflowResult:
165
155
  ctx.textual.text(msg.GitHub.AI.AI_GENERATION_FAILED.format(e=e), markup="yellow")
166
156
  ctx.textual.text(msg.GitHub.AI.FALLBACK_TO_MANUAL, markup="dim")
167
157
 
158
+ ctx.textual.end_step("skip")
168
159
  return Skip(msg.GitHub.AI.AI_GENERATION_FAILED.format(e=e))
169
160
 
170
161
 
@@ -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.text("GitHub client is not available in the workflow context.", markup="red")
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.text("Git client is not available in the workflow context.", markup="red")
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.text("Missing required context for creating a pull request: pr_title, pr_head_branch.", markup="red")
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
  )
@@ -63,24 +71,26 @@ def create_pr_step(ctx: WorkflowContext) -> WorkflowResult:
63
71
 
64
72
  # 4. Call the client method
65
73
  try:
74
+ ctx.textual.text(f"Creating pull request '{title}' from {head} to {base}...", markup="dim")
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.text(msg.GitHub.PR_CREATED.format(number=pr["number"], url=pr["url"]), markup="green")
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.text(f"Failed to create pull request: {e}", markup="red")
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.text(f"An unexpected error occurred while creating the pull request: {e}", markup="red")
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.text("PR title already provided, skipping manual prompt.", markup="dim")
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.text("PR body already provided, skipping manual prompt.", markup="dim")
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.text("Issue body already provided, skipping manual prompt.", markup="dim")
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.text("GitHub client not available", markup="red")
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.text(f"Issue will be assigned to {current_user}", markup="green")
155
+ ctx.textual.end_step("success")
124
156
  return Success(f"Issue will be assigned to {current_user}")
157
+ ctx.textual.text("Issue will not be assigned to current user", markup="dim")
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.text(f"Failed to prompt for self-assign: {e}", markup="red")
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,12 +173,19 @@ 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.text("GitHub client not available", markup="red")
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.text("No labels found in the repository.", markup="dim")
188
+ ctx.textual.end_step("skip")
145
189
  return Skip("No labels found in the repository.")
146
190
 
147
191
  # Show available 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.text(f"Selected labels: {', '.join(selected_labels)}", markup="green")
213
+ else:
214
+ ctx.textual.text("No labels selected", markup="dim")
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.text(f"Failed to prompt for labels: {e}", markup="red")
222
+ ctx.textual.end_step("error")
171
223
  return Error(f"Failed to prompt for labels: {e}", exception=e)