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.
Files changed (146) hide show
  1. titan_cli/__init__.py +3 -0
  2. titan_cli/__main__.py +4 -0
  3. titan_cli/ai/__init__.py +0 -0
  4. titan_cli/ai/agents/__init__.py +15 -0
  5. titan_cli/ai/agents/base.py +152 -0
  6. titan_cli/ai/client.py +170 -0
  7. titan_cli/ai/constants.py +56 -0
  8. titan_cli/ai/exceptions.py +48 -0
  9. titan_cli/ai/models.py +34 -0
  10. titan_cli/ai/oauth_helper.py +120 -0
  11. titan_cli/ai/providers/__init__.py +9 -0
  12. titan_cli/ai/providers/anthropic.py +117 -0
  13. titan_cli/ai/providers/base.py +75 -0
  14. titan_cli/ai/providers/gemini.py +278 -0
  15. titan_cli/cli.py +59 -0
  16. titan_cli/clients/__init__.py +1 -0
  17. titan_cli/clients/gcloud_client.py +52 -0
  18. titan_cli/core/__init__.py +3 -0
  19. titan_cli/core/config.py +274 -0
  20. titan_cli/core/discovery.py +51 -0
  21. titan_cli/core/errors.py +81 -0
  22. titan_cli/core/models.py +52 -0
  23. titan_cli/core/plugins/available.py +36 -0
  24. titan_cli/core/plugins/models.py +67 -0
  25. titan_cli/core/plugins/plugin_base.py +108 -0
  26. titan_cli/core/plugins/plugin_registry.py +163 -0
  27. titan_cli/core/secrets.py +141 -0
  28. titan_cli/core/workflows/__init__.py +22 -0
  29. titan_cli/core/workflows/models.py +88 -0
  30. titan_cli/core/workflows/project_step_source.py +86 -0
  31. titan_cli/core/workflows/workflow_exceptions.py +17 -0
  32. titan_cli/core/workflows/workflow_filter_service.py +137 -0
  33. titan_cli/core/workflows/workflow_registry.py +419 -0
  34. titan_cli/core/workflows/workflow_sources.py +307 -0
  35. titan_cli/engine/__init__.py +39 -0
  36. titan_cli/engine/builder.py +159 -0
  37. titan_cli/engine/context.py +82 -0
  38. titan_cli/engine/mock_context.py +176 -0
  39. titan_cli/engine/results.py +91 -0
  40. titan_cli/engine/steps/ai_assistant_step.py +185 -0
  41. titan_cli/engine/steps/command_step.py +93 -0
  42. titan_cli/engine/utils/__init__.py +3 -0
  43. titan_cli/engine/utils/venv.py +31 -0
  44. titan_cli/engine/workflow_executor.py +187 -0
  45. titan_cli/external_cli/__init__.py +0 -0
  46. titan_cli/external_cli/configs.py +17 -0
  47. titan_cli/external_cli/launcher.py +65 -0
  48. titan_cli/messages.py +121 -0
  49. titan_cli/ui/tui/__init__.py +205 -0
  50. titan_cli/ui/tui/__previews__/statusbar_preview.py +88 -0
  51. titan_cli/ui/tui/app.py +113 -0
  52. titan_cli/ui/tui/icons.py +70 -0
  53. titan_cli/ui/tui/screens/__init__.py +24 -0
  54. titan_cli/ui/tui/screens/ai_config.py +498 -0
  55. titan_cli/ui/tui/screens/ai_config_wizard.py +882 -0
  56. titan_cli/ui/tui/screens/base.py +110 -0
  57. titan_cli/ui/tui/screens/cli_launcher.py +151 -0
  58. titan_cli/ui/tui/screens/global_setup_wizard.py +363 -0
  59. titan_cli/ui/tui/screens/main_menu.py +162 -0
  60. titan_cli/ui/tui/screens/plugin_config_wizard.py +550 -0
  61. titan_cli/ui/tui/screens/plugin_management.py +377 -0
  62. titan_cli/ui/tui/screens/project_setup_wizard.py +686 -0
  63. titan_cli/ui/tui/screens/workflow_execution.py +592 -0
  64. titan_cli/ui/tui/screens/workflows.py +249 -0
  65. titan_cli/ui/tui/textual_components.py +537 -0
  66. titan_cli/ui/tui/textual_workflow_executor.py +405 -0
  67. titan_cli/ui/tui/theme.py +102 -0
  68. titan_cli/ui/tui/widgets/__init__.py +40 -0
  69. titan_cli/ui/tui/widgets/button.py +108 -0
  70. titan_cli/ui/tui/widgets/header.py +116 -0
  71. titan_cli/ui/tui/widgets/panel.py +81 -0
  72. titan_cli/ui/tui/widgets/status_bar.py +115 -0
  73. titan_cli/ui/tui/widgets/table.py +77 -0
  74. titan_cli/ui/tui/widgets/text.py +177 -0
  75. titan_cli/utils/__init__.py +0 -0
  76. titan_cli/utils/autoupdate.py +155 -0
  77. titan_cli-0.1.0.dist-info/METADATA +149 -0
  78. titan_cli-0.1.0.dist-info/RECORD +146 -0
  79. titan_cli-0.1.0.dist-info/WHEEL +4 -0
  80. titan_cli-0.1.0.dist-info/entry_points.txt +9 -0
  81. titan_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
  82. titan_plugin_git/__init__.py +1 -0
  83. titan_plugin_git/clients/__init__.py +8 -0
  84. titan_plugin_git/clients/git_client.py +772 -0
  85. titan_plugin_git/exceptions.py +40 -0
  86. titan_plugin_git/messages.py +112 -0
  87. titan_plugin_git/models.py +39 -0
  88. titan_plugin_git/plugin.py +118 -0
  89. titan_plugin_git/steps/__init__.py +1 -0
  90. titan_plugin_git/steps/ai_commit_message_step.py +171 -0
  91. titan_plugin_git/steps/branch_steps.py +104 -0
  92. titan_plugin_git/steps/commit_step.py +80 -0
  93. titan_plugin_git/steps/push_step.py +63 -0
  94. titan_plugin_git/steps/status_step.py +59 -0
  95. titan_plugin_git/workflows/__previews__/__init__.py +1 -0
  96. titan_plugin_git/workflows/__previews__/commit_ai_preview.py +124 -0
  97. titan_plugin_git/workflows/commit-ai.yaml +28 -0
  98. titan_plugin_github/__init__.py +11 -0
  99. titan_plugin_github/agents/__init__.py +6 -0
  100. titan_plugin_github/agents/config_loader.py +130 -0
  101. titan_plugin_github/agents/issue_generator.py +353 -0
  102. titan_plugin_github/agents/pr_agent.py +528 -0
  103. titan_plugin_github/clients/__init__.py +8 -0
  104. titan_plugin_github/clients/github_client.py +1105 -0
  105. titan_plugin_github/config/__init__.py +0 -0
  106. titan_plugin_github/config/pr_agent.toml +85 -0
  107. titan_plugin_github/exceptions.py +28 -0
  108. titan_plugin_github/messages.py +88 -0
  109. titan_plugin_github/models.py +330 -0
  110. titan_plugin_github/plugin.py +131 -0
  111. titan_plugin_github/steps/__init__.py +12 -0
  112. titan_plugin_github/steps/ai_pr_step.py +172 -0
  113. titan_plugin_github/steps/create_pr_step.py +86 -0
  114. titan_plugin_github/steps/github_prompt_steps.py +171 -0
  115. titan_plugin_github/steps/issue_steps.py +143 -0
  116. titan_plugin_github/steps/preview_step.py +40 -0
  117. titan_plugin_github/utils.py +82 -0
  118. titan_plugin_github/workflows/__previews__/__init__.py +1 -0
  119. titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +140 -0
  120. titan_plugin_github/workflows/create-issue-ai.yaml +32 -0
  121. titan_plugin_github/workflows/create-pr-ai.yaml +49 -0
  122. titan_plugin_jira/__init__.py +8 -0
  123. titan_plugin_jira/agents/__init__.py +6 -0
  124. titan_plugin_jira/agents/config_loader.py +154 -0
  125. titan_plugin_jira/agents/jira_agent.py +553 -0
  126. titan_plugin_jira/agents/prompts.py +364 -0
  127. titan_plugin_jira/agents/response_parser.py +435 -0
  128. titan_plugin_jira/agents/token_tracker.py +223 -0
  129. titan_plugin_jira/agents/validators.py +246 -0
  130. titan_plugin_jira/clients/jira_client.py +745 -0
  131. titan_plugin_jira/config/jira_agent.toml +92 -0
  132. titan_plugin_jira/config/templates/issue_analysis.md.j2 +78 -0
  133. titan_plugin_jira/exceptions.py +37 -0
  134. titan_plugin_jira/formatters/__init__.py +6 -0
  135. titan_plugin_jira/formatters/markdown_formatter.py +245 -0
  136. titan_plugin_jira/messages.py +115 -0
  137. titan_plugin_jira/models.py +89 -0
  138. titan_plugin_jira/plugin.py +264 -0
  139. titan_plugin_jira/steps/ai_analyze_issue_step.py +105 -0
  140. titan_plugin_jira/steps/get_issue_step.py +82 -0
  141. titan_plugin_jira/steps/prompt_select_issue_step.py +80 -0
  142. titan_plugin_jira/steps/search_saved_query_step.py +238 -0
  143. titan_plugin_jira/utils/__init__.py +13 -0
  144. titan_plugin_jira/utils/issue_sorter.py +140 -0
  145. titan_plugin_jira/utils/saved_queries.py +150 -0
  146. 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