titan-cli 0.1.3__py3-none-any.whl → 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. titan_cli/core/config.py +3 -1
  2. titan_cli/core/plugins/models.py +35 -7
  3. titan_cli/core/plugins/plugin_registry.py +11 -2
  4. titan_cli/core/workflows/__init__.py +2 -1
  5. titan_cli/core/workflows/project_step_source.py +48 -30
  6. titan_cli/core/workflows/workflow_filter_service.py +14 -8
  7. titan_cli/core/workflows/workflow_registry.py +12 -1
  8. titan_cli/core/workflows/workflow_sources.py +1 -1
  9. titan_cli/engine/steps/ai_assistant_step.py +42 -7
  10. titan_cli/engine/workflow_executor.py +6 -1
  11. titan_cli/ui/tui/screens/plugin_config_wizard.py +40 -9
  12. titan_cli/ui/tui/screens/workflow_execution.py +8 -28
  13. titan_cli/ui/tui/textual_components.py +59 -6
  14. titan_cli/ui/tui/textual_workflow_executor.py +9 -1
  15. titan_cli/ui/tui/widgets/__init__.py +2 -0
  16. titan_cli/ui/tui/widgets/step_container.py +70 -0
  17. {titan_cli-0.1.3.dist-info → titan_cli-0.1.5.dist-info}/METADATA +6 -3
  18. {titan_cli-0.1.3.dist-info → titan_cli-0.1.5.dist-info}/RECORD +42 -40
  19. {titan_cli-0.1.3.dist-info → titan_cli-0.1.5.dist-info}/WHEEL +1 -1
  20. titan_plugin_git/clients/git_client.py +82 -4
  21. titan_plugin_git/plugin.py +3 -0
  22. titan_plugin_git/steps/ai_commit_message_step.py +33 -28
  23. titan_plugin_git/steps/branch_steps.py +18 -37
  24. titan_plugin_git/steps/commit_step.py +18 -22
  25. titan_plugin_git/steps/diff_summary_step.py +182 -0
  26. titan_plugin_git/steps/push_step.py +27 -11
  27. titan_plugin_git/steps/status_step.py +15 -18
  28. titan_plugin_git/workflows/commit-ai.yaml +5 -0
  29. titan_plugin_github/agents/pr_agent.py +15 -2
  30. titan_plugin_github/steps/ai_pr_step.py +12 -21
  31. titan_plugin_github/steps/create_pr_step.py +17 -7
  32. titan_plugin_github/steps/github_prompt_steps.py +52 -0
  33. titan_plugin_github/steps/issue_steps.py +28 -14
  34. titan_plugin_github/steps/preview_step.py +11 -0
  35. titan_plugin_github/utils.py +5 -4
  36. titan_plugin_github/workflows/create-pr-ai.yaml +5 -0
  37. titan_plugin_jira/steps/ai_analyze_issue_step.py +8 -3
  38. titan_plugin_jira/steps/get_issue_step.py +16 -12
  39. titan_plugin_jira/steps/prompt_select_issue_step.py +22 -9
  40. titan_plugin_jira/steps/search_saved_query_step.py +21 -19
  41. {titan_cli-0.1.3.dist-info → titan_cli-0.1.5.dist-info}/entry_points.txt +0 -0
  42. {titan_cli-0.1.3.dist-info → titan_cli-0.1.5.dist-info/licenses}/LICENSE +0 -0
@@ -1,7 +1,6 @@
1
1
  # plugins/titan-plugin-git/titan_plugin_git/steps/ai_commit_message_step.py
2
2
  from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error, Skip
3
3
  from titan_plugin_git.messages import msg
4
- from titan_cli.ui.tui.widgets import Panel
5
4
 
6
5
 
7
6
  def ai_generate_commit_message(ctx: WorkflowContext) -> WorkflowResult:
@@ -29,29 +28,25 @@ def ai_generate_commit_message(ctx: WorkflowContext) -> WorkflowResult:
29
28
  if not ctx.textual:
30
29
  return Error("Textual UI context is not available for this step.")
31
30
 
31
+ # Begin step container
32
+ ctx.textual.begin_step("AI Commit Message")
33
+
32
34
  # Check if AI is configured
33
35
  if not ctx.ai or not ctx.ai.is_available():
34
- ctx.textual.mount(
35
- Panel(
36
- text=msg.Steps.AICommitMessage.AI_NOT_CONFIGURED,
37
- panel_type="info"
38
- )
39
- )
36
+ ctx.textual.text(msg.Steps.AICommitMessage.AI_NOT_CONFIGURED, markup="dim")
37
+ ctx.textual.end_step("skip")
40
38
  return Skip(msg.Steps.AICommitMessage.AI_NOT_CONFIGURED)
41
39
 
42
40
  # Get git client
43
41
  if not ctx.git:
42
+ ctx.textual.end_step("error")
44
43
  return Error(msg.Steps.AICommitMessage.GIT_CLIENT_NOT_AVAILABLE)
45
44
 
46
45
  # Get git status
47
46
  git_status = ctx.get('git_status')
48
47
  if not git_status or git_status.is_clean:
49
- ctx.textual.mount(
50
- Panel(
51
- text=msg.Steps.AICommitMessage.NO_CHANGES_TO_COMMIT,
52
- panel_type="info"
53
- )
54
- )
48
+ ctx.textual.text(msg.Steps.AICommitMessage.NO_CHANGES_TO_COMMIT, markup="dim")
49
+ ctx.textual.end_step("skip")
55
50
  return Skip(msg.Steps.AICommitMessage.NO_CHANGES_TO_COMMIT)
56
51
 
57
52
  try:
@@ -62,6 +57,7 @@ def ai_generate_commit_message(ctx: WorkflowContext) -> WorkflowResult:
62
57
  diff_text = ctx.git.get_uncommitted_diff()
63
58
 
64
59
  if not diff_text or diff_text.strip() == "":
60
+ ctx.textual.end_step("skip")
65
61
  return Skip(msg.Steps.AICommitMessage.NO_UNCOMMITTED_CHANGES)
66
62
 
67
63
  # Build AI prompt
@@ -86,17 +82,17 @@ def ai_generate_commit_message(ctx: WorkflowContext) -> WorkflowResult:
86
82
 
87
83
  ## CRITICAL Instructions
88
84
  Generate ONE single-line conventional commit message following this EXACT format:
89
- - type(scope): description
85
+ - type(scope): Description
90
86
  - Types: feat, fix, refactor, docs, test, chore, style, perf
91
87
  - Scope: area affected (e.g., auth, api, ui)
92
- - Description: clear summary in imperative mood (be descriptive, concise, and at least 5 words long)
88
+ - Description: clear summary in imperative mood, starting with CAPITAL letter (be descriptive, concise, and at least 5 words long)
93
89
  - NO line breaks, NO body, NO additional explanation
94
90
 
95
- Examples (notice they are all one line):
96
- - feat(auth): add OAuth2 integration with Google provider
97
- - fix(api): resolve race condition in cache invalidation
98
- - refactor(ui): simplify menu component and remove unused props
99
- - refactor(workflows): add support for nested workflow execution
91
+ Examples (notice they start with capital letter and are all one line):
92
+ - feat(auth): Add OAuth2 integration with Google provider
93
+ - fix(api): Resolve race condition in cache invalidation
94
+ - refactor(ui): Simplify menu component and remove unused props
95
+ - refactor(workflows): Add support for nested workflow execution
100
96
 
101
97
  Return ONLY the single-line commit message, absolutely nothing else."""
102
98
 
@@ -115,6 +111,18 @@ Return ONLY the single-line commit message, absolutely nothing else."""
115
111
  # Take only the first line if AI returned multiple lines
116
112
  commit_message = commit_message.split('\n')[0].strip()
117
113
 
114
+ # Ensure subject starts with capital letter (conventional commits requirement)
115
+ # Format: type(scope): Description
116
+ if ':' in commit_message:
117
+ parts = commit_message.split(':', 1)
118
+ if len(parts) == 2:
119
+ prefix = parts[0] # type(scope)
120
+ subject = parts[1].strip() # description
121
+ # Capitalize first letter of subject
122
+ if subject and subject[0].islower():
123
+ subject = subject[0].upper() + subject[1:]
124
+ commit_message = f"{prefix}: {subject}"
125
+
118
126
  # Show preview to user
119
127
  ctx.textual.text("") # spacing
120
128
  ctx.textual.text(msg.Steps.AICommitMessage.GENERATED_MESSAGE_TITLE, markup="bold")
@@ -136,25 +144,21 @@ Return ONLY the single-line commit message, absolutely nothing else."""
136
144
  try:
137
145
  manual_message = ctx.textual.ask_text(msg.Prompts.ENTER_COMMIT_MESSAGE)
138
146
  if not manual_message:
147
+ ctx.textual.end_step("error")
139
148
  return Error(msg.Steps.Commit.COMMIT_MESSAGE_REQUIRED)
140
149
 
141
150
  # Overwrite the metadata to ensure the manual message is used
151
+ ctx.textual.end_step("success")
142
152
  return Success(
143
153
  message=msg.Steps.Prompt.COMMIT_MESSAGE_CAPTURED,
144
154
  metadata={"commit_message": manual_message}
145
155
  )
146
156
  except (KeyboardInterrupt, EOFError):
157
+ ctx.textual.end_step("error")
147
158
  return Error(msg.Steps.Prompt.USER_CANCELLED)
148
159
 
149
- # Show success panel
150
- ctx.textual.mount(
151
- Panel(
152
- text="AI commit message generated successfully",
153
- panel_type="success"
154
- )
155
- )
156
-
157
160
  # Success - save to context
161
+ ctx.textual.end_step("success")
158
162
  return Success(
159
163
  msg.Steps.AICommitMessage.SUCCESS_MESSAGE,
160
164
  metadata={"commit_message": commit_message}
@@ -164,6 +168,7 @@ Return ONLY the single-line commit message, absolutely nothing else."""
164
168
  ctx.textual.text(msg.Steps.AICommitMessage.GENERATION_FAILED.format(e=e), markup="yellow")
165
169
  ctx.textual.text(msg.Steps.AICommitMessage.FALLBACK_TO_MANUAL, markup="dim")
166
170
 
171
+ ctx.textual.end_step("skip")
167
172
  return Skip(msg.Steps.AICommitMessage.GENERATION_FAILED.format(e=e))
168
173
 
169
174
 
@@ -1,6 +1,5 @@
1
1
  # plugins/titan-plugin-git/titan_plugin_git/steps/branch_steps.py
2
2
  from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
3
- from titan_cli.ui.tui.widgets import Panel
4
3
  from titan_plugin_git.messages import msg
5
4
 
6
5
  def get_current_branch_step(ctx: WorkflowContext) -> WorkflowResult:
@@ -20,37 +19,28 @@ def get_current_branch_step(ctx: WorkflowContext) -> WorkflowResult:
20
19
  if not ctx.textual:
21
20
  return Error("Textual UI context is not available for this step.")
22
21
 
22
+ # Begin step container
23
+ ctx.textual.begin_step("Get Head Branch")
24
+
23
25
  if not ctx.git:
24
26
  error_msg = msg.Steps.Status.GIT_CLIENT_NOT_AVAILABLE
25
- ctx.textual.mount(
26
- Panel(
27
- text=error_msg,
28
- panel_type="error"
29
- )
30
- )
27
+ ctx.textual.text(error_msg, markup="red")
28
+ ctx.textual.end_step("error")
31
29
  return Error(error_msg)
32
30
 
33
31
  try:
34
32
  current_branch = ctx.git.get_current_branch()
35
33
  success_msg = msg.Steps.Branch.GET_CURRENT_BRANCH_SUCCESS.format(branch=current_branch)
36
- ctx.textual.mount(
37
- Panel(
38
- text=success_msg,
39
- panel_type="success"
40
- )
41
- )
34
+ ctx.textual.text(success_msg, markup="green")
35
+ ctx.textual.end_step("success")
42
36
  return Success(
43
37
  success_msg,
44
38
  metadata={"pr_head_branch": current_branch}
45
39
  )
46
40
  except Exception as e:
47
41
  error_msg = msg.Steps.Branch.GET_CURRENT_BRANCH_FAILED.format(e=e)
48
- ctx.textual.mount(
49
- Panel(
50
- text=error_msg,
51
- panel_type="error"
52
- )
53
- )
42
+ ctx.textual.text(error_msg, markup="red")
43
+ ctx.textual.end_step("error")
54
44
  return Error(error_msg, exception=e)
55
45
 
56
46
  def get_base_branch_step(ctx: WorkflowContext) -> WorkflowResult:
@@ -70,35 +60,26 @@ def get_base_branch_step(ctx: WorkflowContext) -> WorkflowResult:
70
60
  if not ctx.textual:
71
61
  return Error("Textual UI context is not available for this step.")
72
62
 
63
+ # Begin step container
64
+ ctx.textual.begin_step("Get Base Branch")
65
+
73
66
  if not ctx.git:
74
67
  error_msg = msg.Steps.Status.GIT_CLIENT_NOT_AVAILABLE
75
- ctx.textual.mount(
76
- Panel(
77
- text=error_msg,
78
- panel_type="error"
79
- )
80
- )
68
+ ctx.textual.text(error_msg, markup="red")
69
+ ctx.textual.end_step("error")
81
70
  return Error(error_msg)
82
71
 
83
72
  try:
84
73
  base_branch = ctx.git.main_branch
85
74
  success_msg = msg.Steps.Branch.GET_BASE_BRANCH_SUCCESS.format(branch=base_branch)
86
- ctx.textual.mount(
87
- Panel(
88
- text=success_msg,
89
- panel_type="success"
90
- )
91
- )
75
+ ctx.textual.text(success_msg, markup="green")
76
+ ctx.textual.end_step("success")
92
77
  return Success(
93
78
  success_msg,
94
79
  metadata={"pr_base_branch": base_branch}
95
80
  )
96
81
  except Exception as e:
97
82
  error_msg = msg.Steps.Branch.GET_BASE_BRANCH_FAILED.format(e=e)
98
- ctx.textual.mount(
99
- Panel(
100
- text=error_msg,
101
- panel_type="error"
102
- )
103
- )
83
+ ctx.textual.text(error_msg, markup="red")
84
+ ctx.textual.end_step("error")
104
85
  return Error(error_msg, exception=e)
@@ -3,7 +3,6 @@ from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
3
3
  from titan_cli.engine.results import Skip
4
4
  from titan_plugin_git.exceptions import GitClientError, GitCommandError
5
5
  from titan_plugin_git.messages import msg
6
- from titan_cli.ui.tui.widgets import Panel
7
6
 
8
7
 
9
8
  def create_git_commit_step(ctx: WorkflowContext) -> WorkflowResult:
@@ -18,6 +17,7 @@ def create_git_commit_step(ctx: WorkflowContext) -> WorkflowResult:
18
17
  git_status (GitStatus): The git status object, used to check if the working directory is clean.
19
18
  commit_message (str): The message for the commit.
20
19
  all_files (bool, optional): Whether to commit all modified and new files. Defaults to True.
20
+ no_verify (bool, optional): Skip pre-commit and commit-msg hooks. Defaults to False.
21
21
  commit_hash (str, optional): If present, indicates a commit was already created.
22
22
 
23
23
  Outputs (saved to ctx.data):
@@ -31,50 +31,46 @@ def create_git_commit_step(ctx: WorkflowContext) -> WorkflowResult:
31
31
  if not ctx.textual:
32
32
  return Error("Textual UI context is not available for this step.")
33
33
 
34
+ # Begin step container
35
+ ctx.textual.begin_step("Create Commit")
36
+
34
37
  # Skip if there's nothing to commit
35
38
  git_status = ctx.data.get("git_status")
36
39
  if git_status and git_status.is_clean:
37
- ctx.textual.mount(
38
- Panel(
39
- text=msg.Steps.Commit.WORKING_DIRECTORY_CLEAN,
40
- panel_type="info"
41
- )
42
- )
40
+ ctx.textual.text(msg.Steps.Commit.WORKING_DIRECTORY_CLEAN, markup="dim")
41
+ ctx.textual.end_step("skip")
43
42
  return Skip(msg.Steps.Commit.WORKING_DIRECTORY_CLEAN)
44
43
 
45
44
  if not ctx.git:
45
+ ctx.textual.end_step("error")
46
46
  return Error(msg.Steps.Commit.GIT_CLIENT_NOT_AVAILABLE)
47
47
 
48
48
  commit_message = ctx.get('commit_message')
49
49
  if not commit_message:
50
- ctx.textual.mount(
51
- Panel(
52
- text=msg.Steps.Commit.NO_COMMIT_MESSAGE,
53
- panel_type="info"
54
- )
55
- )
50
+ ctx.textual.text(msg.Steps.Commit.NO_COMMIT_MESSAGE, markup="dim")
51
+ ctx.textual.end_step("skip")
56
52
  return Skip(msg.Steps.Commit.NO_COMMIT_MESSAGE)
57
-
53
+
58
54
  all_files = ctx.get('all_files', True)
55
+ no_verify = ctx.get('no_verify', False)
59
56
 
60
57
  try:
61
- commit_hash = ctx.git.commit(message=commit_message, all=all_files)
58
+ commit_hash = ctx.git.commit(message=commit_message, all=all_files, no_verify=no_verify)
62
59
 
63
- # Show success panel
64
- ctx.textual.mount(
65
- Panel(
66
- text=f"Commit created: {commit_hash[:7]}",
67
- panel_type="success"
68
- )
69
- )
60
+ # Show success message
61
+ ctx.textual.text(f"Commit created: {commit_hash[:7]}", markup="green")
70
62
 
63
+ ctx.textual.end_step("success")
71
64
  return Success(
72
65
  message=msg.Steps.Commit.COMMIT_SUCCESS.format(commit_hash=commit_hash),
73
66
  metadata={"commit_hash": commit_hash}
74
67
  )
75
68
  except GitClientError as e:
69
+ ctx.textual.end_step("error")
76
70
  return Error(msg.Steps.Commit.CLIENT_ERROR_DURING_COMMIT.format(e=e))
77
71
  except GitCommandError as e:
72
+ ctx.textual.end_step("error")
78
73
  return Error(msg.Steps.Commit.COMMAND_FAILED_DURING_COMMIT.format(e=e))
79
74
  except Exception as e:
75
+ ctx.textual.end_step("error")
80
76
  return Error(msg.Steps.Commit.UNEXPECTED_ERROR_DURING_COMMIT.format(e=e))
@@ -0,0 +1,182 @@
1
+ # plugins/titan-plugin-git/titan_plugin_git/steps/diff_summary_step.py
2
+ from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error, Skip
3
+ from titan_plugin_git.messages import msg
4
+
5
+
6
+ def show_uncommitted_diff_summary(ctx: WorkflowContext) -> WorkflowResult:
7
+ """
8
+ Show summary of uncommitted changes (git diff --stat).
9
+
10
+ Provides a visual overview of files changed and lines modified
11
+ before generating commit messages.
12
+
13
+ Returns:
14
+ Success: Always (even if no changes, for workflow continuity)
15
+ """
16
+ if not ctx.textual:
17
+ return Error("Textual UI context is not available for this step.")
18
+
19
+ if not ctx.git:
20
+ return Error(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE)
21
+
22
+ # Begin step container
23
+ ctx.textual.begin_step("Show Changes Summary")
24
+
25
+ try:
26
+ # Get diff stat for uncommitted changes
27
+ stat_output = ctx.git._run_command(["git", "diff", "--stat", "HEAD"])
28
+
29
+ if not stat_output or not stat_output.strip():
30
+ ctx.textual.text("No uncommitted changes to show", markup="dim")
31
+ ctx.textual.end_step("success")
32
+ return Success("No changes")
33
+
34
+ # Show the stat summary with colors
35
+ ctx.textual.text("") # spacing
36
+ ctx.textual.text("Changes summary:", markup="bold")
37
+ ctx.textual.text("") # spacing
38
+
39
+ # Parse lines to find max filename length for alignment
40
+ file_lines = []
41
+ summary_lines = []
42
+ max_filename_len = 0
43
+
44
+ for line in stat_output.split('\n'):
45
+ if not line.strip():
46
+ continue
47
+
48
+ if '|' in line:
49
+ parts = line.split('|')
50
+ filename = parts[0].strip()
51
+ stats = '|'.join(parts[1:]) if len(parts) > 1 else ''
52
+ file_lines.append((filename, stats))
53
+ max_filename_len = max(max_filename_len, len(filename))
54
+ else:
55
+ summary_lines.append(line)
56
+
57
+ # Display aligned file changes
58
+ for filename, stats in file_lines:
59
+ # Pad filename to align pipes
60
+ padded_filename = filename.ljust(max_filename_len)
61
+
62
+ # Replace + with green and - with red
63
+ stats = stats.replace('+', '[green]+[/green]')
64
+ stats = stats.replace('-', '[red]-[/red]')
65
+
66
+ ctx.textual.text(f" {padded_filename} | {stats}")
67
+
68
+ # Display summary lines
69
+ for line in summary_lines:
70
+ colored_line = line.replace('(+)', '[green](+)[/green]')
71
+ colored_line = colored_line.replace('(-)', '[red](-)[/red]')
72
+ ctx.textual.text(f" {colored_line}", markup="dim")
73
+
74
+ ctx.textual.text("") # spacing
75
+
76
+ # End step container with success
77
+ ctx.textual.end_step("success")
78
+
79
+ return Success("Diff summary displayed")
80
+
81
+ except Exception as e:
82
+ # Don't fail the workflow, just skip
83
+ ctx.textual.end_step("skip")
84
+ return Skip(f"Could not show diff summary: {e}")
85
+
86
+
87
+ def show_branch_diff_summary(ctx: WorkflowContext) -> WorkflowResult:
88
+ """
89
+ Show summary of branch changes (git diff base...head --stat).
90
+
91
+ Provides a visual overview of files changed between branches
92
+ before generating PR descriptions.
93
+
94
+ Inputs (from ctx.data):
95
+ pr_head_branch (str): Head branch name
96
+
97
+ Returns:
98
+ Success: Always (even if no changes, for workflow continuity)
99
+ """
100
+ if not ctx.textual:
101
+ return Error("Textual UI context is not available for this step.")
102
+
103
+ # Begin step container
104
+ ctx.textual.begin_step("Show Branch Changes Summary")
105
+
106
+ if not ctx.git:
107
+ ctx.textual.text(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE, markup="red")
108
+ ctx.textual.end_step("error")
109
+ return Error(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE)
110
+
111
+ head_branch = ctx.get("pr_head_branch")
112
+ if not head_branch:
113
+ ctx.textual.text("No head branch specified", markup="dim")
114
+ ctx.textual.end_step("skip")
115
+ return Skip("No head branch specified")
116
+
117
+ base_branch = ctx.git.main_branch
118
+
119
+ try:
120
+ # Get diff stat between branches
121
+ stat_output = ctx.git._run_command([
122
+ "git", "diff", "--stat", f"{base_branch}...{head_branch}"
123
+ ])
124
+
125
+ if not stat_output or not stat_output.strip():
126
+ ctx.textual.text(f"No changes between {base_branch} and {head_branch}", markup="dim")
127
+ ctx.textual.end_step("success")
128
+ return Success("No changes")
129
+
130
+ # Show the stat summary with colors
131
+ ctx.textual.text("") # spacing
132
+ ctx.textual.text(f"Changes in {head_branch} vs {base_branch}:", markup="bold")
133
+ ctx.textual.text("") # spacing
134
+
135
+ # Parse lines to find max filename length for alignment
136
+ file_lines = []
137
+ summary_lines = []
138
+ max_filename_len = 0
139
+
140
+ for line in stat_output.split('\n'):
141
+ if not line.strip():
142
+ continue
143
+
144
+ if '|' in line:
145
+ parts = line.split('|')
146
+ filename = parts[0].strip()
147
+ stats = '|'.join(parts[1:]) if len(parts) > 1 else ''
148
+ file_lines.append((filename, stats))
149
+ max_filename_len = max(max_filename_len, len(filename))
150
+ else:
151
+ summary_lines.append(line)
152
+
153
+ # Display aligned file changes
154
+ for filename, stats in file_lines:
155
+ # Pad filename to align pipes
156
+ padded_filename = filename.ljust(max_filename_len)
157
+
158
+ # Replace + with green and - with red
159
+ stats = stats.replace('+', '[green]+[/green]')
160
+ stats = stats.replace('-', '[red]-[/red]')
161
+
162
+ ctx.textual.text(f" {padded_filename} | {stats}")
163
+
164
+ # Display summary lines
165
+ for line in summary_lines:
166
+ colored_line = line.replace('(+)', '[green](+)[/green]')
167
+ colored_line = colored_line.replace('(-)', '[red](-)[/red]')
168
+ ctx.textual.text(f" {colored_line}", markup="dim")
169
+
170
+ ctx.textual.text("") # spacing
171
+
172
+ ctx.textual.end_step("success")
173
+ return Success("Branch diff summary displayed")
174
+
175
+ except Exception as e:
176
+ # Don't fail the workflow, just skip
177
+ ctx.textual.text(f"Could not show branch diff summary: {e}", markup="yellow")
178
+ ctx.textual.end_step("skip")
179
+ return Skip(f"Could not show branch diff summary: {e}")
180
+
181
+
182
+ __all__ = ["show_uncommitted_diff_summary", "show_branch_diff_summary"]
@@ -2,7 +2,6 @@
2
2
  from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
3
3
  from titan_plugin_git.exceptions import GitCommandError
4
4
  from titan_plugin_git.messages import msg
5
- from titan_cli.ui.tui.widgets import Panel
6
5
 
7
6
  def create_git_push_step(ctx: WorkflowContext) -> WorkflowResult:
8
7
  """
@@ -12,6 +11,7 @@ def create_git_push_step(ctx: WorkflowContext) -> WorkflowResult:
12
11
  remote (str, optional): The name of the remote to push to. Defaults to the client's default remote.
13
12
  branch (str, optional): The name of the branch to push. Defaults to the current branch.
14
13
  set_upstream (bool, optional): Whether to set the upstream tracking branch. Defaults to False.
14
+ push_tags (bool, optional): Whether to push tags along with the branch. Defaults to False.
15
15
 
16
16
  Requires:
17
17
  ctx.git: An initialized GitClient.
@@ -29,10 +29,14 @@ def create_git_push_step(ctx: WorkflowContext) -> WorkflowResult:
29
29
  if not ctx.git:
30
30
  return Error(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE)
31
31
 
32
+ # Begin step container
33
+ ctx.textual.begin_step("Push changes to remote")
34
+
32
35
  # Get params from context
33
36
  remote = ctx.get('remote')
34
37
  branch = ctx.get('branch')
35
38
  set_upstream = ctx.get('set_upstream', False)
39
+ push_tags = ctx.get('push_tags', False)
36
40
 
37
41
  # Use defaults from the GitClient if not provided in the context
38
42
  remote_to_use = remote or ctx.git.default_remote
@@ -43,21 +47,33 @@ def create_git_push_step(ctx: WorkflowContext) -> WorkflowResult:
43
47
  if not ctx.git.branch_exists_on_remote(branch=branch_to_use, remote=remote_to_use):
44
48
  set_upstream = True
45
49
 
46
- ctx.git.push(remote=remote_to_use, branch=branch_to_use, set_upstream=set_upstream)
47
-
48
- # Show success panel
49
- ctx.textual.mount(
50
- Panel(
51
- text=f"Pushed to {remote_to_use}/{branch_to_use}",
52
- panel_type="success"
53
- )
50
+ # Push branch (and tags if requested)
51
+ ctx.git.push(
52
+ remote=remote_to_use,
53
+ branch=branch_to_use,
54
+ set_upstream=set_upstream,
55
+ tags=push_tags
54
56
  )
55
57
 
58
+ # Show success message
59
+ success_msg = f"Pushed to {remote_to_use}/{branch_to_use}"
60
+ if push_tags:
61
+ success_msg += " (with tags)"
62
+
63
+ ctx.textual.text(success_msg, markup="green")
64
+
65
+ ctx.textual.end_step("success")
56
66
  return Success(
57
67
  message=msg.Git.PUSH_SUCCESS.format(remote=remote_to_use, branch=branch_to_use),
58
68
  metadata={"pr_head_branch": branch_to_use}
59
69
  )
60
70
  except GitCommandError as e:
61
- return Error(msg.Steps.Push.PUSH_FAILED.format(e=e))
71
+ error_msg = msg.Steps.Push.PUSH_FAILED.format(e=e)
72
+ ctx.textual.text(error_msg, markup="red")
73
+ ctx.textual.end_step("error")
74
+ return Error(error_msg)
62
75
  except Exception as e:
63
- return Error(msg.Git.UNEXPECTED_ERROR.format(e=e))
76
+ error_msg = msg.Git.UNEXPECTED_ERROR.format(e=e)
77
+ ctx.textual.text(error_msg, markup="red")
78
+ ctx.textual.end_step("error")
79
+ return Error(error_msg)
@@ -7,7 +7,6 @@ from titan_cli.engine import (
7
7
  )
8
8
  from titan_cli.messages import msg as global_msg
9
9
  from ..messages import msg
10
- from titan_cli.ui.tui.widgets import Panel
11
10
 
12
11
  def get_git_status_step(ctx: WorkflowContext) -> WorkflowResult:
13
12
  """
@@ -23,37 +22,35 @@ def get_git_status_step(ctx: WorkflowContext) -> WorkflowResult:
23
22
  Success: If the status was retrieved successfully.
24
23
  Error: If the GitClient is not available or the git command fails.
25
24
  """
25
+ if not ctx.textual:
26
+ return Error("Textual UI context is not available for this step.")
27
+
26
28
  if not ctx.git:
27
29
  return Error(msg.Steps.Status.GIT_CLIENT_NOT_AVAILABLE)
28
30
 
31
+ # Begin step container
32
+ ctx.textual.begin_step("Check Git Status")
33
+
29
34
  try:
30
35
  status = ctx.git.get_status()
31
36
 
32
- if not ctx.textual:
33
- return Error("Textual UI context is not available for this step.")
34
-
35
- # If there are uncommitted changes, show warning panel
37
+ # If there are uncommitted changes, show text (border will be green on success)
36
38
  if not status.is_clean:
37
- ctx.textual.mount(
38
- Panel(
39
- text=global_msg.Workflow.UNCOMMITTED_CHANGES_WARNING,
40
- panel_type="warning"
41
- )
42
- )
39
+ ctx.textual.text(global_msg.Workflow.UNCOMMITTED_CHANGES_WARNING, markup="yellow")
43
40
  message = msg.Steps.Status.STATUS_RETRIEVED_WITH_UNCOMMITTED
44
41
  else:
45
- # Show success panel for clean working directory
46
- ctx.textual.mount(
47
- Panel(
48
- text=msg.Steps.Status.WORKING_DIRECTORY_IS_CLEAN,
49
- panel_type="success"
50
- )
51
- )
42
+ # Show text for clean working directory (border will be green)
43
+ ctx.textual.text(msg.Steps.Status.WORKING_DIRECTORY_IS_CLEAN, markup="green")
52
44
  message = msg.Steps.Status.WORKING_DIRECTORY_IS_CLEAN
53
45
 
46
+ # End step container with success
47
+ ctx.textual.end_step("success")
48
+
54
49
  return Success(
55
50
  message=message,
56
51
  metadata={"git_status": status}
57
52
  )
58
53
  except Exception as e:
54
+ # End step container with error
55
+ ctx.textual.end_step("error")
59
56
  return Error(msg.Steps.Status.FAILED_TO_GET_STATUS.format(e=e))
@@ -12,6 +12,11 @@ steps:
12
12
  plugin: git
13
13
  step: get_status
14
14
 
15
+ - id: diff_summary
16
+ name: "Show Changes Summary"
17
+ plugin: git
18
+ step: show_uncommitted_diff_summary
19
+
15
20
  - id: ai_commit_message
16
21
  name: "AI Commit Message"
17
22
  plugin: git
@@ -420,8 +420,9 @@ COMMIT_MESSAGE: <conventional commit message>"""
420
420
  ```
421
421
 
422
422
  ## CRITICAL Instructions
423
- 1. **Title**: Follow conventional commits (type(scope): description), be clear and descriptive
424
- - Examples: "feat(auth): add OAuth2 integration with Google provider", "fix(api): resolve race condition in cache invalidation"
423
+ 1. **Title**: Follow conventional commits (type(scope): Description), be clear and descriptive
424
+ - Start description with CAPITAL letter (imperative mood)
425
+ - Examples: "feat(auth): Add OAuth2 integration with Google provider", "fix(api): Resolve race condition in cache invalidation"
425
426
 
426
427
  2. **Description**: MUST follow the template structure above but keep it under {max_chars} characters total
427
428
  - Fill in the template sections (Summary, Type of Change, Changes Made, etc.)
@@ -460,6 +461,18 @@ DESCRIPTION:
460
461
  # Clean up title
461
462
  title = title.strip('"').strip("'")
462
463
 
464
+ # Ensure title subject starts with capital letter (conventional commits requirement)
465
+ # Format: type(scope): Description
466
+ if ':' in title:
467
+ parts = title.split(':', 1)
468
+ if len(parts) == 2:
469
+ prefix = parts[0] # type(scope)
470
+ subject = parts[1].strip() # description
471
+ # Capitalize first letter of subject
472
+ if subject and subject[0].islower():
473
+ subject = subject[0].upper() + subject[1:]
474
+ title = f"{prefix}: {subject}"
475
+
463
476
  # Truncate description if needed (but not title)
464
477
  if len(description) > max_chars:
465
478
  description = description[:max_chars - 3] + "..."