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.
- titan_cli/core/config.py +3 -1
- titan_cli/core/workflows/__init__.py +2 -1
- titan_cli/core/workflows/project_step_source.py +48 -30
- titan_cli/core/workflows/workflow_filter_service.py +14 -8
- titan_cli/core/workflows/workflow_registry.py +12 -1
- titan_cli/core/workflows/workflow_sources.py +1 -1
- titan_cli/engine/steps/ai_assistant_step.py +42 -7
- titan_cli/engine/workflow_executor.py +6 -1
- titan_cli/ui/tui/screens/workflow_execution.py +8 -28
- titan_cli/ui/tui/textual_components.py +59 -6
- titan_cli/ui/tui/textual_workflow_executor.py +9 -1
- titan_cli/ui/tui/widgets/__init__.py +2 -0
- titan_cli/ui/tui/widgets/step_container.py +70 -0
- {titan_cli-0.1.4.dist-info → titan_cli-0.1.5.dist-info}/METADATA +6 -3
- {titan_cli-0.1.4.dist-info → titan_cli-0.1.5.dist-info}/RECORD +39 -37
- {titan_cli-0.1.4.dist-info → titan_cli-0.1.5.dist-info}/WHEEL +1 -1
- titan_plugin_git/clients/git_client.py +82 -4
- titan_plugin_git/plugin.py +3 -0
- titan_plugin_git/steps/ai_commit_message_step.py +33 -28
- titan_plugin_git/steps/branch_steps.py +18 -37
- titan_plugin_git/steps/commit_step.py +18 -22
- titan_plugin_git/steps/diff_summary_step.py +182 -0
- titan_plugin_git/steps/push_step.py +27 -11
- titan_plugin_git/steps/status_step.py +15 -18
- titan_plugin_git/workflows/commit-ai.yaml +5 -0
- titan_plugin_github/agents/pr_agent.py +15 -2
- titan_plugin_github/steps/ai_pr_step.py +12 -21
- titan_plugin_github/steps/create_pr_step.py +17 -7
- titan_plugin_github/steps/github_prompt_steps.py +52 -0
- titan_plugin_github/steps/issue_steps.py +28 -14
- titan_plugin_github/steps/preview_step.py +11 -0
- titan_plugin_github/utils.py +5 -4
- titan_plugin_github/workflows/create-pr-ai.yaml +5 -0
- titan_plugin_jira/steps/ai_analyze_issue_step.py +8 -3
- titan_plugin_jira/steps/get_issue_step.py +16 -12
- titan_plugin_jira/steps/prompt_select_issue_step.py +22 -9
- titan_plugin_jira/steps/search_saved_query_step.py +21 -19
- {titan_cli-0.1.4.dist-info → titan_cli-0.1.5.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
46
|
-
ctx.textual.
|
|
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))
|
|
@@ -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):
|
|
424
|
-
-
|
|
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.
|
|
49
|
-
|
|
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.
|
|
92
|
-
|
|
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.
|
|
70
|
-
|
|
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)
|