titan-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- titan_cli/__init__.py +3 -0
- titan_cli/__main__.py +4 -0
- titan_cli/ai/__init__.py +0 -0
- titan_cli/ai/agents/__init__.py +15 -0
- titan_cli/ai/agents/base.py +152 -0
- titan_cli/ai/client.py +170 -0
- titan_cli/ai/constants.py +56 -0
- titan_cli/ai/exceptions.py +48 -0
- titan_cli/ai/models.py +34 -0
- titan_cli/ai/oauth_helper.py +120 -0
- titan_cli/ai/providers/__init__.py +9 -0
- titan_cli/ai/providers/anthropic.py +117 -0
- titan_cli/ai/providers/base.py +75 -0
- titan_cli/ai/providers/gemini.py +278 -0
- titan_cli/cli.py +59 -0
- titan_cli/clients/__init__.py +1 -0
- titan_cli/clients/gcloud_client.py +52 -0
- titan_cli/core/__init__.py +3 -0
- titan_cli/core/config.py +274 -0
- titan_cli/core/discovery.py +51 -0
- titan_cli/core/errors.py +81 -0
- titan_cli/core/models.py +52 -0
- titan_cli/core/plugins/available.py +36 -0
- titan_cli/core/plugins/models.py +67 -0
- titan_cli/core/plugins/plugin_base.py +108 -0
- titan_cli/core/plugins/plugin_registry.py +163 -0
- titan_cli/core/secrets.py +141 -0
- titan_cli/core/workflows/__init__.py +22 -0
- titan_cli/core/workflows/models.py +88 -0
- titan_cli/core/workflows/project_step_source.py +86 -0
- titan_cli/core/workflows/workflow_exceptions.py +17 -0
- titan_cli/core/workflows/workflow_filter_service.py +137 -0
- titan_cli/core/workflows/workflow_registry.py +419 -0
- titan_cli/core/workflows/workflow_sources.py +307 -0
- titan_cli/engine/__init__.py +39 -0
- titan_cli/engine/builder.py +159 -0
- titan_cli/engine/context.py +82 -0
- titan_cli/engine/mock_context.py +176 -0
- titan_cli/engine/results.py +91 -0
- titan_cli/engine/steps/ai_assistant_step.py +185 -0
- titan_cli/engine/steps/command_step.py +93 -0
- titan_cli/engine/utils/__init__.py +3 -0
- titan_cli/engine/utils/venv.py +31 -0
- titan_cli/engine/workflow_executor.py +187 -0
- titan_cli/external_cli/__init__.py +0 -0
- titan_cli/external_cli/configs.py +17 -0
- titan_cli/external_cli/launcher.py +65 -0
- titan_cli/messages.py +121 -0
- titan_cli/ui/tui/__init__.py +205 -0
- titan_cli/ui/tui/__previews__/statusbar_preview.py +88 -0
- titan_cli/ui/tui/app.py +113 -0
- titan_cli/ui/tui/icons.py +70 -0
- titan_cli/ui/tui/screens/__init__.py +24 -0
- titan_cli/ui/tui/screens/ai_config.py +498 -0
- titan_cli/ui/tui/screens/ai_config_wizard.py +882 -0
- titan_cli/ui/tui/screens/base.py +110 -0
- titan_cli/ui/tui/screens/cli_launcher.py +151 -0
- titan_cli/ui/tui/screens/global_setup_wizard.py +363 -0
- titan_cli/ui/tui/screens/main_menu.py +162 -0
- titan_cli/ui/tui/screens/plugin_config_wizard.py +550 -0
- titan_cli/ui/tui/screens/plugin_management.py +377 -0
- titan_cli/ui/tui/screens/project_setup_wizard.py +686 -0
- titan_cli/ui/tui/screens/workflow_execution.py +592 -0
- titan_cli/ui/tui/screens/workflows.py +249 -0
- titan_cli/ui/tui/textual_components.py +537 -0
- titan_cli/ui/tui/textual_workflow_executor.py +405 -0
- titan_cli/ui/tui/theme.py +102 -0
- titan_cli/ui/tui/widgets/__init__.py +40 -0
- titan_cli/ui/tui/widgets/button.py +108 -0
- titan_cli/ui/tui/widgets/header.py +116 -0
- titan_cli/ui/tui/widgets/panel.py +81 -0
- titan_cli/ui/tui/widgets/status_bar.py +115 -0
- titan_cli/ui/tui/widgets/table.py +77 -0
- titan_cli/ui/tui/widgets/text.py +177 -0
- titan_cli/utils/__init__.py +0 -0
- titan_cli/utils/autoupdate.py +155 -0
- titan_cli-0.1.0.dist-info/METADATA +149 -0
- titan_cli-0.1.0.dist-info/RECORD +146 -0
- titan_cli-0.1.0.dist-info/WHEEL +4 -0
- titan_cli-0.1.0.dist-info/entry_points.txt +9 -0
- titan_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- titan_plugin_git/__init__.py +1 -0
- titan_plugin_git/clients/__init__.py +8 -0
- titan_plugin_git/clients/git_client.py +772 -0
- titan_plugin_git/exceptions.py +40 -0
- titan_plugin_git/messages.py +112 -0
- titan_plugin_git/models.py +39 -0
- titan_plugin_git/plugin.py +118 -0
- titan_plugin_git/steps/__init__.py +1 -0
- titan_plugin_git/steps/ai_commit_message_step.py +171 -0
- titan_plugin_git/steps/branch_steps.py +104 -0
- titan_plugin_git/steps/commit_step.py +80 -0
- titan_plugin_git/steps/push_step.py +63 -0
- titan_plugin_git/steps/status_step.py +59 -0
- titan_plugin_git/workflows/__previews__/__init__.py +1 -0
- titan_plugin_git/workflows/__previews__/commit_ai_preview.py +124 -0
- titan_plugin_git/workflows/commit-ai.yaml +28 -0
- titan_plugin_github/__init__.py +11 -0
- titan_plugin_github/agents/__init__.py +6 -0
- titan_plugin_github/agents/config_loader.py +130 -0
- titan_plugin_github/agents/issue_generator.py +353 -0
- titan_plugin_github/agents/pr_agent.py +528 -0
- titan_plugin_github/clients/__init__.py +8 -0
- titan_plugin_github/clients/github_client.py +1105 -0
- titan_plugin_github/config/__init__.py +0 -0
- titan_plugin_github/config/pr_agent.toml +85 -0
- titan_plugin_github/exceptions.py +28 -0
- titan_plugin_github/messages.py +88 -0
- titan_plugin_github/models.py +330 -0
- titan_plugin_github/plugin.py +131 -0
- titan_plugin_github/steps/__init__.py +12 -0
- titan_plugin_github/steps/ai_pr_step.py +172 -0
- titan_plugin_github/steps/create_pr_step.py +86 -0
- titan_plugin_github/steps/github_prompt_steps.py +171 -0
- titan_plugin_github/steps/issue_steps.py +143 -0
- titan_plugin_github/steps/preview_step.py +40 -0
- titan_plugin_github/utils.py +82 -0
- titan_plugin_github/workflows/__previews__/__init__.py +1 -0
- titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +140 -0
- titan_plugin_github/workflows/create-issue-ai.yaml +32 -0
- titan_plugin_github/workflows/create-pr-ai.yaml +49 -0
- titan_plugin_jira/__init__.py +8 -0
- titan_plugin_jira/agents/__init__.py +6 -0
- titan_plugin_jira/agents/config_loader.py +154 -0
- titan_plugin_jira/agents/jira_agent.py +553 -0
- titan_plugin_jira/agents/prompts.py +364 -0
- titan_plugin_jira/agents/response_parser.py +435 -0
- titan_plugin_jira/agents/token_tracker.py +223 -0
- titan_plugin_jira/agents/validators.py +246 -0
- titan_plugin_jira/clients/jira_client.py +745 -0
- titan_plugin_jira/config/jira_agent.toml +92 -0
- titan_plugin_jira/config/templates/issue_analysis.md.j2 +78 -0
- titan_plugin_jira/exceptions.py +37 -0
- titan_plugin_jira/formatters/__init__.py +6 -0
- titan_plugin_jira/formatters/markdown_formatter.py +245 -0
- titan_plugin_jira/messages.py +115 -0
- titan_plugin_jira/models.py +89 -0
- titan_plugin_jira/plugin.py +264 -0
- titan_plugin_jira/steps/ai_analyze_issue_step.py +105 -0
- titan_plugin_jira/steps/get_issue_step.py +82 -0
- titan_plugin_jira/steps/prompt_select_issue_step.py +80 -0
- titan_plugin_jira/steps/search_saved_query_step.py +238 -0
- titan_plugin_jira/utils/__init__.py +13 -0
- titan_plugin_jira/utils/issue_sorter.py +140 -0
- titan_plugin_jira/utils/saved_queries.py +150 -0
- titan_plugin_jira/workflows/analyze-jira-issues.yaml +34 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# plugins/titan-plugin-github/titan_plugin_github/steps/ai_pr_step.py
|
|
2
|
+
"""
|
|
3
|
+
AI-powered PR description generation step.
|
|
4
|
+
|
|
5
|
+
Uses PRAgent to analyze branch context and generate PR content.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error, Skip
|
|
9
|
+
from titan_cli.ui.tui.widgets import Panel
|
|
10
|
+
|
|
11
|
+
from ..agents import PRAgent
|
|
12
|
+
from ..messages import msg
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ai_suggest_pr_description_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
16
|
+
"""
|
|
17
|
+
Generate PR title and description using PRAgent.
|
|
18
|
+
|
|
19
|
+
Uses PRAgent to analyze the complete branch context and generate:
|
|
20
|
+
- PR title following conventional commits
|
|
21
|
+
- PR description following template (if exists)
|
|
22
|
+
- Appropriate detail level based on PR size
|
|
23
|
+
|
|
24
|
+
Requires:
|
|
25
|
+
ctx.ai: An initialized AIClient
|
|
26
|
+
ctx.git: An initialized GitClient
|
|
27
|
+
ctx.github: An initialized GitHubClient
|
|
28
|
+
|
|
29
|
+
Inputs (from ctx.data):
|
|
30
|
+
pr_head_branch (str): The head branch for the PR
|
|
31
|
+
|
|
32
|
+
Outputs (saved to ctx.data):
|
|
33
|
+
pr_title (str): AI-generated PR title
|
|
34
|
+
pr_body (str): AI-generated PR description
|
|
35
|
+
pr_size (str): Size classification (small/medium/large/very large)
|
|
36
|
+
ai_generated (bool): True if AI generated the content
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Success: PR description generated
|
|
40
|
+
Skip: AI not configured or user declined
|
|
41
|
+
Error: Failed to generate PR description
|
|
42
|
+
"""
|
|
43
|
+
if not ctx.textual:
|
|
44
|
+
return Error("Textual UI context is not available for this step.")
|
|
45
|
+
|
|
46
|
+
# Check if AI is configured
|
|
47
|
+
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
|
+
)
|
|
54
|
+
return Skip(msg.GitHub.AI.AI_NOT_CONFIGURED)
|
|
55
|
+
|
|
56
|
+
# Get Git client
|
|
57
|
+
if not ctx.git:
|
|
58
|
+
return Error(msg.GitHub.AI.GIT_CLIENT_NOT_AVAILABLE)
|
|
59
|
+
|
|
60
|
+
# Get branch info
|
|
61
|
+
head_branch = ctx.get("pr_head_branch")
|
|
62
|
+
if not head_branch:
|
|
63
|
+
return Error(msg.GitHub.AI.MISSING_PR_HEAD_BRANCH)
|
|
64
|
+
|
|
65
|
+
base_branch = ctx.git.main_branch
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
# Show progress
|
|
69
|
+
ctx.textual.text(msg.GitHub.AI.ANALYZING_BRANCH_DIFF.format(
|
|
70
|
+
head_branch=head_branch,
|
|
71
|
+
base_branch=base_branch
|
|
72
|
+
), markup="dim")
|
|
73
|
+
|
|
74
|
+
# Create PRAgent instance
|
|
75
|
+
pr_agent = PRAgent(
|
|
76
|
+
ai_client=ctx.ai,
|
|
77
|
+
git_client=ctx.git,
|
|
78
|
+
github_client=ctx.github
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Use PRAgent to analyze and generate PR content with loading indicator
|
|
82
|
+
with ctx.textual.loading(msg.GitHub.AI.GENERATING_PR_DESCRIPTION):
|
|
83
|
+
analysis = pr_agent.analyze_and_plan(
|
|
84
|
+
head_branch=head_branch,
|
|
85
|
+
base_branch=base_branch,
|
|
86
|
+
auto_stage=False # Only analyze branch commits, not uncommitted changes
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Check if PR content was generated (need commits in branch)
|
|
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
|
+
)
|
|
97
|
+
return Skip("No commits found for PR generation")
|
|
98
|
+
|
|
99
|
+
# Show PR size info
|
|
100
|
+
if analysis.pr_size:
|
|
101
|
+
ctx.textual.text(msg.GitHub.AI.PR_SIZE_INFO.format(
|
|
102
|
+
pr_size=analysis.pr_size,
|
|
103
|
+
files_changed=analysis.files_changed,
|
|
104
|
+
diff_lines=analysis.lines_changed,
|
|
105
|
+
max_chars="varies by size"
|
|
106
|
+
), markup="dim")
|
|
107
|
+
|
|
108
|
+
# Show PR preview to user
|
|
109
|
+
ctx.textual.text("") # spacing
|
|
110
|
+
ctx.textual.text(msg.GitHub.AI.AI_GENERATED_PR_TITLE, markup="bold")
|
|
111
|
+
ctx.textual.text("") # spacing
|
|
112
|
+
|
|
113
|
+
# Show title
|
|
114
|
+
ctx.textual.text(msg.GitHub.AI.TITLE_LABEL, markup="bold")
|
|
115
|
+
ctx.textual.text(f" {analysis.pr_title}", markup="cyan")
|
|
116
|
+
|
|
117
|
+
# Warn if title is too long
|
|
118
|
+
if len(analysis.pr_title) > 72:
|
|
119
|
+
ctx.textual.text(msg.GitHub.AI.TITLE_TOO_LONG_WARNING.format(
|
|
120
|
+
length=len(analysis.pr_title)
|
|
121
|
+
), markup="yellow")
|
|
122
|
+
|
|
123
|
+
ctx.textual.text("") # spacing
|
|
124
|
+
|
|
125
|
+
# Show description
|
|
126
|
+
ctx.textual.text(msg.GitHub.AI.DESCRIPTION_LABEL, markup="bold")
|
|
127
|
+
# Render markdown in a scrollable container
|
|
128
|
+
ctx.textual.markdown(analysis.pr_body)
|
|
129
|
+
|
|
130
|
+
ctx.textual.text("") # spacing
|
|
131
|
+
|
|
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
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if not use_ai_pr:
|
|
139
|
+
ctx.textual.text(msg.GitHub.AI.AI_SUGGESTION_REJECTED, markup="yellow")
|
|
140
|
+
return Skip("User rejected AI-generated PR")
|
|
141
|
+
|
|
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
|
+
# Success - save to context
|
|
151
|
+
metadata = {
|
|
152
|
+
"ai_generated": True,
|
|
153
|
+
"pr_title": analysis.pr_title,
|
|
154
|
+
"pr_body": analysis.pr_body,
|
|
155
|
+
"pr_size": analysis.pr_size
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return Success(
|
|
159
|
+
msg.GitHub.AI.AI_GENERATED_PR_DESCRIPTION_SUCCESS,
|
|
160
|
+
metadata=metadata
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
# 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")
|
|
167
|
+
|
|
168
|
+
return Skip(msg.GitHub.AI.AI_GENERATION_FAILED.format(e=e))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# Export for plugin registration
|
|
172
|
+
__all__ = ["ai_suggest_pr_description_step"]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# plugins/titan-plugin-github/titan_plugin_github/steps/create_pr_step.py
|
|
2
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
3
|
+
from titan_cli.ui.tui.widgets import Panel
|
|
4
|
+
from ..exceptions import GitHubAPIError
|
|
5
|
+
from ..messages import msg
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_pr_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
9
|
+
"""
|
|
10
|
+
Creates a GitHub pull request using data from the workflow context.
|
|
11
|
+
|
|
12
|
+
Requires:
|
|
13
|
+
ctx.github: An initialized GitHubClient.
|
|
14
|
+
ctx.git: An initialized GitClient.
|
|
15
|
+
|
|
16
|
+
Inputs (from ctx.data):
|
|
17
|
+
pr_title (str): The title of the pull request.
|
|
18
|
+
pr_body (str, optional): The body/description of the pull request.
|
|
19
|
+
pr_head_branch (str): The branch with the new changes.
|
|
20
|
+
pr_is_draft (bool, optional): Whether to create the PR as a draft. Defaults to False.
|
|
21
|
+
|
|
22
|
+
Configuration (from ctx.github.config):
|
|
23
|
+
auto_assign_prs (bool): If True, automatically assigns the PR to the current GitHub user.
|
|
24
|
+
|
|
25
|
+
Outputs (saved to ctx.data):
|
|
26
|
+
pr_number (int): The number of the created pull request.
|
|
27
|
+
pr_url (str): The URL of the created pull request.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Success: If the PR is created successfully.
|
|
31
|
+
Error: If any required context arguments are missing or if the API call fails.
|
|
32
|
+
"""
|
|
33
|
+
if not ctx.textual:
|
|
34
|
+
return Error("Textual UI context is not available for this step.")
|
|
35
|
+
|
|
36
|
+
# 1. Get GitHub client from context
|
|
37
|
+
if not ctx.github:
|
|
38
|
+
return Error("GitHub client is not available in the workflow context.")
|
|
39
|
+
if not ctx.git:
|
|
40
|
+
return Error("Git client is not available in the workflow context.")
|
|
41
|
+
|
|
42
|
+
# 2. Get required data from context and client config
|
|
43
|
+
title = ctx.get("pr_title")
|
|
44
|
+
body = ctx.get("pr_body")
|
|
45
|
+
base = ctx.git.main_branch # Get base branch from git client config
|
|
46
|
+
head = ctx.get("pr_head_branch")
|
|
47
|
+
is_draft = ctx.get("pr_is_draft", False) # Default to not a draft
|
|
48
|
+
|
|
49
|
+
if not all([title, base, head]):
|
|
50
|
+
return Error(
|
|
51
|
+
"Missing required context for creating a pull request: pr_title, pr_head_branch."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# 3. Determine assignees if auto-assign is enabled
|
|
55
|
+
assignees = None
|
|
56
|
+
if ctx.github.config.auto_assign_prs:
|
|
57
|
+
try:
|
|
58
|
+
current_user = ctx.github.get_current_user()
|
|
59
|
+
assignees = [current_user]
|
|
60
|
+
except GitHubAPIError as e:
|
|
61
|
+
# Log warning but continue without assignee
|
|
62
|
+
ctx.textual.text(f"Could not get current user for auto-assign: {e}", markup="yellow")
|
|
63
|
+
|
|
64
|
+
# 4. Call the client method
|
|
65
|
+
try:
|
|
66
|
+
pr = ctx.github.create_pull_request(
|
|
67
|
+
title=title, body=body, base=base, head=head, draft=is_draft, assignees=assignees
|
|
68
|
+
)
|
|
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
|
+
)
|
|
75
|
+
|
|
76
|
+
# 4. Return Success with PR info
|
|
77
|
+
return Success(
|
|
78
|
+
"Pull request created successfully.",
|
|
79
|
+
metadata={"pr_number": pr["number"], "pr_url": pr["url"]},
|
|
80
|
+
)
|
|
81
|
+
except GitHubAPIError as e:
|
|
82
|
+
return Error(f"Failed to create pull request: {e}")
|
|
83
|
+
except Exception as e:
|
|
84
|
+
return Error(
|
|
85
|
+
f"An unexpected error occurred while creating the pull request: {e}"
|
|
86
|
+
)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# plugins/titan-plugin-github/titan_plugin_github/steps/prompt_steps.py
|
|
2
|
+
from titan_cli.engine.context import WorkflowContext
|
|
3
|
+
from titan_cli.engine.results import WorkflowResult, Success, Error, Skip
|
|
4
|
+
from ..messages import msg
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def prompt_for_pr_title_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
8
|
+
"""
|
|
9
|
+
Interactively prompts the user for a Pull Request title.
|
|
10
|
+
Skips if pr_title already exists.
|
|
11
|
+
|
|
12
|
+
Requires:
|
|
13
|
+
ctx.textual: Textual UI components.
|
|
14
|
+
|
|
15
|
+
Outputs (saved to ctx.data):
|
|
16
|
+
pr_title (str): The title entered by the user.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Success: If the title was captured successfully.
|
|
20
|
+
Error: If the user cancels or the title is empty.
|
|
21
|
+
Skip: If pr_title already exists.
|
|
22
|
+
"""
|
|
23
|
+
if not ctx.textual:
|
|
24
|
+
return Error("Textual UI context is not available for this step.")
|
|
25
|
+
|
|
26
|
+
# Skip if title already exists (e.g., from AI generation)
|
|
27
|
+
if ctx.get("pr_title"):
|
|
28
|
+
return Skip("PR title already provided, skipping manual prompt.")
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
title = ctx.textual.ask_text(msg.Prompts.ENTER_PR_TITLE)
|
|
32
|
+
if not title:
|
|
33
|
+
return Error("PR title cannot be empty.")
|
|
34
|
+
return Success("PR title captured", metadata={"pr_title": title})
|
|
35
|
+
except (KeyboardInterrupt, EOFError):
|
|
36
|
+
return Error("User cancelled.")
|
|
37
|
+
except Exception as e:
|
|
38
|
+
return Error(f"Failed to prompt for PR title: {e}", exception=e)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def prompt_for_pr_body_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
42
|
+
"""
|
|
43
|
+
Interactively prompts the user for a Pull Request body.
|
|
44
|
+
Skips if pr_body already exists.
|
|
45
|
+
|
|
46
|
+
Requires:
|
|
47
|
+
ctx.textual: Textual UI components.
|
|
48
|
+
|
|
49
|
+
Outputs (saved to ctx.data):
|
|
50
|
+
pr_body (str): The body/description entered by the user.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Success: If the body was captured successfully.
|
|
54
|
+
Error: If the user cancels.
|
|
55
|
+
Skip: If pr_body already exists.
|
|
56
|
+
"""
|
|
57
|
+
if not ctx.textual:
|
|
58
|
+
return Error("Textual UI context is not available for this step.")
|
|
59
|
+
|
|
60
|
+
# Skip if body already exists (e.g., from AI generation)
|
|
61
|
+
if ctx.get("pr_body"):
|
|
62
|
+
return Skip("PR body already provided, skipping manual prompt.")
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
body = ctx.textual.ask_multiline(msg.Prompts.ENTER_PR_BODY, default="")
|
|
66
|
+
# Body can be empty
|
|
67
|
+
return Success("PR body captured", metadata={"pr_body": body})
|
|
68
|
+
except (KeyboardInterrupt, EOFError):
|
|
69
|
+
return Error("User cancelled.")
|
|
70
|
+
except Exception as e:
|
|
71
|
+
return Error(f"Failed to prompt for PR body: {e}", exception=e)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def prompt_for_issue_body_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
75
|
+
"""
|
|
76
|
+
Interactively prompts the user for a GitHub issue body.
|
|
77
|
+
Skips if issue_body already exists.
|
|
78
|
+
|
|
79
|
+
Requires:
|
|
80
|
+
ctx.textual: Textual UI components.
|
|
81
|
+
|
|
82
|
+
Outputs (saved to ctx.data):
|
|
83
|
+
issue_body (str): The body/description entered by the user.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Success: If the body was captured successfully.
|
|
87
|
+
Error: If the user cancels.
|
|
88
|
+
Skip: If issue_body already exists.
|
|
89
|
+
"""
|
|
90
|
+
if not ctx.textual:
|
|
91
|
+
return Error("Textual UI context is not available for this step.")
|
|
92
|
+
|
|
93
|
+
# Skip if body already exists (e.g., from AI generation)
|
|
94
|
+
if ctx.get("issue_body"):
|
|
95
|
+
return Skip("Issue body already provided, skipping manual prompt.")
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
body = ctx.textual.ask_multiline(msg.Prompts.ENTER_ISSUE_BODY, default="")
|
|
99
|
+
# Body can be empty
|
|
100
|
+
return Success("Issue body captured", metadata={"issue_body": body})
|
|
101
|
+
except (KeyboardInterrupt, EOFError):
|
|
102
|
+
return Error("User cancelled.")
|
|
103
|
+
except Exception as e:
|
|
104
|
+
return Error(f"Failed to prompt for issue body: {e}", exception=e)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def prompt_for_self_assign_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
108
|
+
"""
|
|
109
|
+
Asks the user if they want to assign the issue to themselves.
|
|
110
|
+
"""
|
|
111
|
+
if not ctx.textual:
|
|
112
|
+
return Error("Textual UI context is not available for this step.")
|
|
113
|
+
|
|
114
|
+
if not ctx.github:
|
|
115
|
+
return Error("GitHub client not available")
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
if ctx.textual.ask_confirm(msg.Prompts.ASSIGN_TO_SELF, default=True):
|
|
119
|
+
current_user = ctx.github.get_current_user()
|
|
120
|
+
assignees = ctx.get("assignees", [])
|
|
121
|
+
if current_user not in assignees:
|
|
122
|
+
assignees.append(current_user)
|
|
123
|
+
ctx.set("assignees", assignees)
|
|
124
|
+
return Success(f"Issue will be assigned to {current_user}")
|
|
125
|
+
return Success("Issue will not be assigned to current user")
|
|
126
|
+
except (KeyboardInterrupt, EOFError):
|
|
127
|
+
return Error("User cancelled.")
|
|
128
|
+
except Exception as e:
|
|
129
|
+
return Error(f"Failed to prompt for self-assign: {e}", exception=e)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def prompt_for_labels_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
133
|
+
"""
|
|
134
|
+
Prompts the user to select labels for the issue.
|
|
135
|
+
"""
|
|
136
|
+
if not ctx.textual:
|
|
137
|
+
return Error("Textual UI context is not available for this step.")
|
|
138
|
+
|
|
139
|
+
if not ctx.github:
|
|
140
|
+
return Error("GitHub client not available")
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
available_labels = ctx.github.list_labels()
|
|
144
|
+
if not available_labels:
|
|
145
|
+
return Skip("No labels found in the repository.")
|
|
146
|
+
|
|
147
|
+
# Show available labels
|
|
148
|
+
ctx.textual.text(f"Available labels: {', '.join(available_labels)}", markup="dim")
|
|
149
|
+
|
|
150
|
+
# Get default labels as comma-separated string
|
|
151
|
+
existing_labels = ctx.get("labels", [])
|
|
152
|
+
default_value = ",".join(existing_labels) if existing_labels else ""
|
|
153
|
+
|
|
154
|
+
# TODO: Implement multi-select in Textual - for now use comma-separated input
|
|
155
|
+
labels_input = ctx.textual.ask_text(
|
|
156
|
+
f"{msg.Prompts.SELECT_LABELS} (comma-separated)",
|
|
157
|
+
default=default_value
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Parse comma-separated labels
|
|
161
|
+
if labels_input:
|
|
162
|
+
selected_labels = [label.strip() for label in labels_input.split(",") if label.strip()]
|
|
163
|
+
else:
|
|
164
|
+
selected_labels = []
|
|
165
|
+
|
|
166
|
+
ctx.set("labels", selected_labels)
|
|
167
|
+
return Success("Labels selected")
|
|
168
|
+
except (KeyboardInterrupt, EOFError):
|
|
169
|
+
return Error("User cancelled.")
|
|
170
|
+
except Exception as e:
|
|
171
|
+
return Error(f"Failed to prompt for labels: {e}", exception=e)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from titan_cli.engine.context import WorkflowContext
|
|
3
|
+
from titan_cli.engine.results import WorkflowResult, Success, Error, Skip
|
|
4
|
+
from titan_cli.ui.tui.widgets import Panel
|
|
5
|
+
from ..agents.issue_generator import IssueGeneratorAgent
|
|
6
|
+
|
|
7
|
+
def ai_suggest_issue_title_and_body_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
8
|
+
"""
|
|
9
|
+
Use AI to suggest a title and description for a GitHub issue.
|
|
10
|
+
Auto-categorizes and selects the appropriate template.
|
|
11
|
+
"""
|
|
12
|
+
if not ctx.textual:
|
|
13
|
+
return Error("Textual UI context is not available for this step.")
|
|
14
|
+
|
|
15
|
+
if not ctx.ai:
|
|
16
|
+
return Skip("AI client not available")
|
|
17
|
+
|
|
18
|
+
issue_body_prompt = ctx.get("issue_body")
|
|
19
|
+
if not issue_body_prompt:
|
|
20
|
+
return Error("issue_body not found in context")
|
|
21
|
+
|
|
22
|
+
ctx.textual.text("Using AI to categorize and generate issue...", markup="cyan")
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
# Get available labels from repository for smart mapping
|
|
26
|
+
available_labels = None
|
|
27
|
+
if ctx.github:
|
|
28
|
+
try:
|
|
29
|
+
available_labels = ctx.github.list_labels()
|
|
30
|
+
except Exception:
|
|
31
|
+
# If we can't get labels, continue without filtering
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
# Get template directory from repo path
|
|
35
|
+
template_dir = None
|
|
36
|
+
if ctx.git:
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
template_dir = Path(ctx.git.repo_path) / ".github" / "ISSUE_TEMPLATE"
|
|
39
|
+
|
|
40
|
+
issue_generator = IssueGeneratorAgent(ctx.ai, template_dir=template_dir)
|
|
41
|
+
|
|
42
|
+
with ctx.textual.loading("Generating issue with AI..."):
|
|
43
|
+
result = issue_generator.generate_issue(issue_body_prompt, available_labels=available_labels)
|
|
44
|
+
|
|
45
|
+
# Show category detected
|
|
46
|
+
category = result["category"]
|
|
47
|
+
template_used = result.get("template_used", False)
|
|
48
|
+
|
|
49
|
+
ctx.textual.mount(
|
|
50
|
+
Panel(
|
|
51
|
+
text=f"Category detected: {category}",
|
|
52
|
+
panel_type="success"
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if template_used:
|
|
57
|
+
ctx.textual.text(f"Using template: {category}.md", markup="cyan")
|
|
58
|
+
else:
|
|
59
|
+
ctx.textual.text(f"No template found for {category}, using default structure", markup="yellow")
|
|
60
|
+
|
|
61
|
+
ctx.set("issue_title", result["title"])
|
|
62
|
+
ctx.set("issue_body", result["body"])
|
|
63
|
+
ctx.set("issue_category", category)
|
|
64
|
+
ctx.set("labels", result["labels"])
|
|
65
|
+
|
|
66
|
+
return Success(f"AI-generated issue ({category}) created successfully")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return Error(f"Failed to generate issue: {e}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def create_issue_steps(ctx: WorkflowContext) -> WorkflowResult:
|
|
72
|
+
"""
|
|
73
|
+
Create a new GitHub issue.
|
|
74
|
+
"""
|
|
75
|
+
if not ctx.textual:
|
|
76
|
+
return Error("Textual UI context is not available for this step.")
|
|
77
|
+
|
|
78
|
+
if not ctx.github:
|
|
79
|
+
return Error("GitHub client not available")
|
|
80
|
+
|
|
81
|
+
issue_title = ctx.get("issue_title")
|
|
82
|
+
issue_body = ctx.get("issue_body")
|
|
83
|
+
assignees = ctx.get("assignees", [])
|
|
84
|
+
labels = ctx.get("labels", [])
|
|
85
|
+
|
|
86
|
+
# Safely parse string representations to lists
|
|
87
|
+
if isinstance(assignees, str):
|
|
88
|
+
try:
|
|
89
|
+
assignees = ast.literal_eval(assignees)
|
|
90
|
+
except (ValueError, SyntaxError):
|
|
91
|
+
assignees = []
|
|
92
|
+
|
|
93
|
+
if isinstance(labels, str):
|
|
94
|
+
try:
|
|
95
|
+
labels = ast.literal_eval(labels)
|
|
96
|
+
except (ValueError, SyntaxError):
|
|
97
|
+
labels = []
|
|
98
|
+
|
|
99
|
+
# Ensure they are lists
|
|
100
|
+
if not isinstance(assignees, list):
|
|
101
|
+
assignees = []
|
|
102
|
+
if not isinstance(labels, list):
|
|
103
|
+
labels = []
|
|
104
|
+
|
|
105
|
+
if not issue_title:
|
|
106
|
+
return Error("issue_title not found in context")
|
|
107
|
+
|
|
108
|
+
if not issue_body:
|
|
109
|
+
return Error("issue_body not found in context")
|
|
110
|
+
|
|
111
|
+
# Filter labels to only those that exist in the repository
|
|
112
|
+
if labels and ctx.github:
|
|
113
|
+
try:
|
|
114
|
+
available_labels = ctx.github.list_labels()
|
|
115
|
+
# Filter labels to only include those that exist (case-insensitive)
|
|
116
|
+
filtered_labels = [
|
|
117
|
+
label for label in labels
|
|
118
|
+
if label.lower() in [av_label.lower() for av_label in available_labels]
|
|
119
|
+
]
|
|
120
|
+
labels = filtered_labels
|
|
121
|
+
except Exception:
|
|
122
|
+
# If we can't validate labels, continue with all labels anyway
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
issue = ctx.github.create_issue(
|
|
127
|
+
title=issue_title,
|
|
128
|
+
body=issue_body,
|
|
129
|
+
assignees=assignees,
|
|
130
|
+
labels=labels,
|
|
131
|
+
)
|
|
132
|
+
ctx.textual.mount(
|
|
133
|
+
Panel(
|
|
134
|
+
text=f"Successfully created issue #{issue.number}",
|
|
135
|
+
panel_type="success"
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
return Success(
|
|
139
|
+
f"Successfully created issue #{issue.number}",
|
|
140
|
+
metadata={"issue": issue}
|
|
141
|
+
)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
return Error(f"Failed to create issue: {e}")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from titan_cli.engine.context import WorkflowContext
|
|
2
|
+
from titan_cli.engine.results import WorkflowResult, Success, Error
|
|
3
|
+
|
|
4
|
+
def preview_and_confirm_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
5
|
+
"""
|
|
6
|
+
Show a preview of the AI-generated issue and ask for confirmation.
|
|
7
|
+
"""
|
|
8
|
+
if not ctx.textual:
|
|
9
|
+
return Error("Textual UI context is not available for this step.")
|
|
10
|
+
|
|
11
|
+
issue_title = ctx.get("issue_title")
|
|
12
|
+
issue_body = ctx.get("issue_body")
|
|
13
|
+
|
|
14
|
+
if not issue_title or not issue_body:
|
|
15
|
+
return Error("issue_title or issue_body not found in context")
|
|
16
|
+
|
|
17
|
+
# Show preview header
|
|
18
|
+
ctx.textual.text("") # spacing
|
|
19
|
+
ctx.textual.text("AI-Generated Issue Preview", markup="bold")
|
|
20
|
+
ctx.textual.text("") # spacing
|
|
21
|
+
|
|
22
|
+
# Show title
|
|
23
|
+
ctx.textual.text("Title:", markup="bold")
|
|
24
|
+
ctx.textual.text(f" {issue_title}", markup="cyan")
|
|
25
|
+
ctx.textual.text("") # spacing
|
|
26
|
+
|
|
27
|
+
# Show description
|
|
28
|
+
ctx.textual.text("Description:", markup="bold")
|
|
29
|
+
# Render markdown in a scrollable container
|
|
30
|
+
ctx.textual.markdown(issue_body)
|
|
31
|
+
|
|
32
|
+
ctx.textual.text("") # spacing
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
if not ctx.textual.ask_confirm("Use this AI-generated issue?", default=True):
|
|
36
|
+
return Error("User rejected AI-generated issue")
|
|
37
|
+
except (KeyboardInterrupt, EOFError):
|
|
38
|
+
return Error("User cancelled operation")
|
|
39
|
+
|
|
40
|
+
return Success("User confirmed AI-generated issue")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# plugins/titan-plugin-github/titan_plugin_github/utils.py
|
|
2
|
+
"""Utility functions for GitHub plugin."""
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Default limits for diff processing
|
|
9
|
+
DEFAULT_MAX_DIFF_SIZE = 8000 # Characters
|
|
10
|
+
DEFAULT_MAX_FILES_IN_DIFF = 50
|
|
11
|
+
DEFAULT_MAX_COMMITS_TO_ANALYZE = 15
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class PRSizeEstimation:
|
|
16
|
+
"""
|
|
17
|
+
PR size estimation with character limits.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
pr_size: Size category (small, medium, large, very large)
|
|
21
|
+
max_chars: Maximum characters for PR description
|
|
22
|
+
files_changed: Number of files changed
|
|
23
|
+
diff_lines: Number of lines in diff
|
|
24
|
+
"""
|
|
25
|
+
pr_size: str
|
|
26
|
+
max_chars: int
|
|
27
|
+
files_changed: int
|
|
28
|
+
diff_lines: int
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def calculate_pr_size(diff: str) -> PRSizeEstimation:
|
|
32
|
+
"""
|
|
33
|
+
Analyzes a git diff to estimate PR size and suggest character limits.
|
|
34
|
+
|
|
35
|
+
This is the single source of truth for PR size calculation.
|
|
36
|
+
Both PRAgent and workflow steps use this function.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
diff: The full text of the git diff
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
PRSizeEstimation with size category and character limits
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
>>> diff = "diff --git a/file.py b/file.py\\n..."
|
|
46
|
+
>>> estimation = calculate_pr_size(diff)
|
|
47
|
+
>>> print(estimation.pr_size)
|
|
48
|
+
'small'
|
|
49
|
+
"""
|
|
50
|
+
diff_lines = len(diff.split('\n'))
|
|
51
|
+
|
|
52
|
+
# Count files changed (count file headers in diff)
|
|
53
|
+
file_pattern = r'^diff --git'
|
|
54
|
+
files_changed = len(re.findall(file_pattern, diff, re.MULTILINE))
|
|
55
|
+
|
|
56
|
+
# Dynamic character limit based on PR size
|
|
57
|
+
if files_changed <= 3 and diff_lines < 100:
|
|
58
|
+
# Small PR: bug fix, doc update, small feature
|
|
59
|
+
max_chars = 800
|
|
60
|
+
pr_size = "small"
|
|
61
|
+
elif files_changed <= 10 and diff_lines < 500:
|
|
62
|
+
# Medium PR: feature, moderate refactor
|
|
63
|
+
max_chars = 1800
|
|
64
|
+
pr_size = "medium"
|
|
65
|
+
elif files_changed <= 30 and diff_lines < 2000:
|
|
66
|
+
# Large PR: architectural changes, new modules
|
|
67
|
+
max_chars = 3000
|
|
68
|
+
pr_size = "large"
|
|
69
|
+
else:
|
|
70
|
+
# Very large PR: major refactor, breaking changes
|
|
71
|
+
max_chars = 4500
|
|
72
|
+
pr_size = "very large"
|
|
73
|
+
|
|
74
|
+
return PRSizeEstimation(
|
|
75
|
+
pr_size=pr_size,
|
|
76
|
+
max_chars=max_chars,
|
|
77
|
+
files_changed=files_changed,
|
|
78
|
+
diff_lines=diff_lines
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = ["calculate_pr_size", "PRSizeEstimation"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Workflow previews for GitHub plugin
|