titan-cli 0.1.3__tar.gz → 0.1.5__tar.gz

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 (147) hide show
  1. {titan_cli-0.1.3 → titan_cli-0.1.5}/PKG-INFO +6 -3
  2. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/clients/git_client.py +82 -4
  3. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/plugin.py +3 -0
  4. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/steps/ai_commit_message_step.py +33 -28
  5. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/steps/branch_steps.py +18 -37
  6. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/steps/commit_step.py +18 -22
  7. titan_cli-0.1.5/plugins/titan-plugin-git/titan_plugin_git/steps/diff_summary_step.py +182 -0
  8. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/steps/push_step.py +27 -11
  9. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/steps/status_step.py +15 -18
  10. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/workflows/commit-ai.yaml +5 -0
  11. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/agents/pr_agent.py +15 -2
  12. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/steps/ai_pr_step.py +12 -21
  13. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/steps/create_pr_step.py +17 -7
  14. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/steps/github_prompt_steps.py +52 -0
  15. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/steps/issue_steps.py +28 -14
  16. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/steps/preview_step.py +11 -0
  17. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/utils.py +5 -4
  18. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/workflows/create-pr-ai.yaml +5 -0
  19. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/steps/ai_analyze_issue_step.py +8 -3
  20. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/steps/get_issue_step.py +16 -12
  21. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/steps/prompt_select_issue_step.py +22 -9
  22. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/steps/search_saved_query_step.py +21 -19
  23. {titan_cli-0.1.3 → titan_cli-0.1.5}/pyproject.toml +1 -1
  24. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/config.py +3 -1
  25. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/plugins/models.py +35 -7
  26. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/plugins/plugin_registry.py +11 -2
  27. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/workflows/__init__.py +2 -1
  28. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/workflows/project_step_source.py +48 -30
  29. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/workflows/workflow_filter_service.py +14 -8
  30. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/workflows/workflow_registry.py +12 -1
  31. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/workflows/workflow_sources.py +1 -1
  32. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/engine/steps/ai_assistant_step.py +42 -7
  33. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/engine/workflow_executor.py +6 -1
  34. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/screens/plugin_config_wizard.py +40 -9
  35. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/screens/workflow_execution.py +8 -28
  36. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/textual_components.py +59 -6
  37. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/textual_workflow_executor.py +9 -1
  38. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/widgets/__init__.py +2 -0
  39. titan_cli-0.1.5/titan_cli/ui/tui/widgets/step_container.py +70 -0
  40. {titan_cli-0.1.3 → titan_cli-0.1.5}/LICENSE +0 -0
  41. {titan_cli-0.1.3 → titan_cli-0.1.5}/README.md +0 -0
  42. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/__init__.py +0 -0
  43. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/clients/__init__.py +0 -0
  44. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/exceptions.py +0 -0
  45. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/messages.py +0 -0
  46. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/models.py +0 -0
  47. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/steps/__init__.py +0 -0
  48. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/workflows/__previews__/__init__.py +0 -0
  49. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-git/titan_plugin_git/workflows/__previews__/commit_ai_preview.py +0 -0
  50. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/__init__.py +0 -0
  51. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/agents/__init__.py +0 -0
  52. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/agents/config_loader.py +0 -0
  53. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/agents/issue_generator.py +0 -0
  54. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/clients/__init__.py +0 -0
  55. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/clients/github_client.py +0 -0
  56. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/config/__init__.py +0 -0
  57. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/config/pr_agent.toml +0 -0
  58. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/exceptions.py +0 -0
  59. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/messages.py +0 -0
  60. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/models.py +0 -0
  61. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/plugin.py +0 -0
  62. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/steps/__init__.py +0 -0
  63. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/workflows/__previews__/__init__.py +0 -0
  64. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +0 -0
  65. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-github/titan_plugin_github/workflows/create-issue-ai.yaml +0 -0
  66. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/__init__.py +0 -0
  67. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/agents/__init__.py +0 -0
  68. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/agents/config_loader.py +0 -0
  69. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/agents/jira_agent.py +0 -0
  70. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/agents/prompts.py +0 -0
  71. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/agents/response_parser.py +0 -0
  72. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/agents/token_tracker.py +0 -0
  73. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/agents/validators.py +0 -0
  74. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/clients/jira_client.py +0 -0
  75. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/config/jira_agent.toml +0 -0
  76. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/config/templates/issue_analysis.md.j2 +0 -0
  77. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/exceptions.py +0 -0
  78. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/formatters/__init__.py +0 -0
  79. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/formatters/markdown_formatter.py +0 -0
  80. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/messages.py +0 -0
  81. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/models.py +0 -0
  82. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/plugin.py +0 -0
  83. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/utils/__init__.py +0 -0
  84. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/utils/issue_sorter.py +0 -0
  85. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/utils/saved_queries.py +0 -0
  86. {titan_cli-0.1.3 → titan_cli-0.1.5}/plugins/titan-plugin-jira/titan_plugin_jira/workflows/analyze-jira-issues.yaml +0 -0
  87. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/__init__.py +0 -0
  88. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/__main__.py +0 -0
  89. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ai/__init__.py +0 -0
  90. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ai/agents/__init__.py +0 -0
  91. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ai/agents/base.py +0 -0
  92. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ai/client.py +0 -0
  93. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ai/constants.py +0 -0
  94. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ai/exceptions.py +0 -0
  95. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ai/models.py +0 -0
  96. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ai/oauth_helper.py +0 -0
  97. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ai/providers/__init__.py +0 -0
  98. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ai/providers/anthropic.py +0 -0
  99. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ai/providers/base.py +0 -0
  100. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ai/providers/gemini.py +0 -0
  101. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/cli.py +0 -0
  102. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/clients/__init__.py +0 -0
  103. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/clients/gcloud_client.py +0 -0
  104. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/__init__.py +0 -0
  105. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/discovery.py +0 -0
  106. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/errors.py +0 -0
  107. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/models.py +0 -0
  108. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/plugins/available.py +0 -0
  109. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/plugins/plugin_base.py +0 -0
  110. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/secrets.py +0 -0
  111. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/workflows/models.py +0 -0
  112. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/core/workflows/workflow_exceptions.py +0 -0
  113. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/engine/__init__.py +0 -0
  114. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/engine/builder.py +0 -0
  115. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/engine/context.py +0 -0
  116. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/engine/mock_context.py +0 -0
  117. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/engine/results.py +0 -0
  118. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/engine/steps/command_step.py +0 -0
  119. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/engine/utils/__init__.py +0 -0
  120. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/engine/utils/venv.py +0 -0
  121. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/external_cli/__init__.py +0 -0
  122. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/external_cli/configs.py +0 -0
  123. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/external_cli/launcher.py +0 -0
  124. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/messages.py +0 -0
  125. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/__init__.py +0 -0
  126. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/__previews__/statusbar_preview.py +0 -0
  127. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/app.py +0 -0
  128. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/icons.py +0 -0
  129. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/screens/__init__.py +0 -0
  130. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/screens/ai_config.py +0 -0
  131. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/screens/ai_config_wizard.py +0 -0
  132. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/screens/base.py +0 -0
  133. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/screens/cli_launcher.py +0 -0
  134. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/screens/global_setup_wizard.py +0 -0
  135. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/screens/main_menu.py +0 -0
  136. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/screens/plugin_management.py +0 -0
  137. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/screens/project_setup_wizard.py +0 -0
  138. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/screens/workflows.py +0 -0
  139. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/theme.py +0 -0
  140. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/widgets/button.py +0 -0
  141. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/widgets/header.py +0 -0
  142. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/widgets/panel.py +0 -0
  143. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/widgets/status_bar.py +0 -0
  144. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/widgets/table.py +0 -0
  145. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/ui/tui/widgets/text.py +0 -0
  146. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/utils/__init__.py +0 -0
  147. {titan_cli-0.1.3 → titan_cli-0.1.5}/titan_cli/utils/autoupdate.py +0 -0
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: titan-cli
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Modular development tools orchestrator - Streamline your workflows with AI integration and intuitive terminal UI
5
- Home-page: https://github.com/masmovil/titan-cli
6
5
  License: MIT
6
+ License-File: LICENSE
7
7
  Keywords: cli,workflow,orchestrator,automation,devtools,ai
8
8
  Author: finxo
9
9
  Author-email: finxeto@gmail.com
@@ -19,6 +19,8 @@ Classifier: Programming Language :: Python :: 3
19
19
  Classifier: Programming Language :: Python :: 3.10
20
20
  Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
22
24
  Requires-Dist: anthropic (>=0.75.0,<0.76.0)
23
25
  Requires-Dist: google-auth (>=2.43.0,<3.0.0)
24
26
  Requires-Dist: google-genai (>=1.58.0,<2.0.0)
@@ -34,6 +36,7 @@ Requires-Dist: tomli (>=2.0.0,<3.0.0)
34
36
  Requires-Dist: tomli-w (>=1.0.0,<2.0.0)
35
37
  Requires-Dist: typer (>=0.20.0,<1.0.0)
36
38
  Project-URL: Documentation, https://github.com/masmovil/titan-cli
39
+ Project-URL: Homepage, https://github.com/masmovil/titan-cli
37
40
  Project-URL: Repository, https://github.com/masmovil/titan-cli
38
41
  Description-Content-Type: text/markdown
39
42
 
@@ -282,13 +282,14 @@ class GitClient:
282
282
  """
283
283
  self._run_command(["git", "branch", branch_name, start_point])
284
284
 
285
- def commit(self, message: str, all: bool = False) -> str:
285
+ def commit(self, message: str, all: bool = False, no_verify: bool = False) -> str:
286
286
  """
287
287
  Create a commit
288
288
 
289
289
  Args:
290
290
  message: Commit message
291
291
  all: Stage all modified and new files (`git add --all`)
292
+ no_verify: Skip pre-commit and commit-msg hooks
292
293
 
293
294
  Returns:
294
295
  Commit hash
@@ -298,6 +299,8 @@ class GitClient:
298
299
  self._run_command(["git", "add", "--all"])
299
300
 
300
301
  args = ["git", "commit", "-m", message]
302
+ if no_verify:
303
+ args.append("--no-verify")
301
304
  self._run_command(args)
302
305
 
303
306
  return self._run_command(["git", "rev-parse", "HEAD"])
@@ -347,7 +350,7 @@ class GitClient:
347
350
  self._run_command(["git", "branch", delete_arg, branch])
348
351
  return True
349
352
 
350
- def push(self, remote: str = "origin", branch: Optional[str] = None, set_upstream: bool = False) -> None:
353
+ def push(self, remote: str = "origin", branch: Optional[str] = None, set_upstream: bool = False, tags: bool = False) -> None:
351
354
  """
352
355
  Push to remote
353
356
 
@@ -355,12 +358,16 @@ class GitClient:
355
358
  remote: Remote name
356
359
  branch: Branch to push (default: current)
357
360
  set_upstream: Set upstream tracking
361
+ tags: Push tags to remote (use --tags flag)
358
362
  """
359
363
  args = ["git", "push"]
360
364
 
361
365
  if set_upstream:
362
366
  args.append("-u")
363
-
367
+
368
+ if tags:
369
+ args.append("--tags")
370
+
364
371
  args.append(remote)
365
372
 
366
373
  if branch:
@@ -769,4 +776,75 @@ class GitClient:
769
776
  except GitCommandError:
770
777
  # Command failed, likely no remote 'origin' or not a git repo
771
778
  pass
772
- return None, None
779
+ return None, None
780
+
781
+ def create_tag(self, tag_name: str, message: str, ref: str = "HEAD") -> None:
782
+ """
783
+ Create an annotated tag
784
+
785
+ Args:
786
+ tag_name: Name of the tag
787
+ message: Tag annotation message
788
+ ref: Reference to tag (default: HEAD)
789
+
790
+ Raises:
791
+ GitCommandError: If tag creation fails
792
+ """
793
+ self._run_command(["git", "tag", "-a", tag_name, "-m", message, ref])
794
+
795
+ def delete_tag(self, tag_name: str) -> None:
796
+ """
797
+ Delete a local tag
798
+
799
+ Args:
800
+ tag_name: Name of the tag to delete
801
+
802
+ Raises:
803
+ GitCommandError: If tag deletion fails
804
+ """
805
+ self._run_command(["git", "tag", "-d", tag_name])
806
+
807
+ def tag_exists(self, tag_name: str) -> bool:
808
+ """
809
+ Check if a tag exists locally
810
+
811
+ Args:
812
+ tag_name: Name of the tag to check
813
+
814
+ Returns:
815
+ True if tag exists, False otherwise
816
+ """
817
+ try:
818
+ self._run_command(["git", "tag", "-l", tag_name], check=False)
819
+ tags = self._run_command(["git", "tag", "-l", tag_name]).strip()
820
+ return tags == tag_name
821
+ except GitCommandError:
822
+ return False
823
+
824
+ def list_tags(self) -> List[str]:
825
+ """
826
+ List all tags in the repository
827
+
828
+ Returns:
829
+ List of tag names
830
+ """
831
+ try:
832
+ output = self._run_command(["git", "tag", "-l"])
833
+ if output.strip():
834
+ return [tag.strip() for tag in output.split('\n') if tag.strip()]
835
+ return []
836
+ except GitCommandError:
837
+ return []
838
+
839
+ def is_protected_branch(self, branch: str) -> bool:
840
+ """
841
+ Check if a branch is protected (main, master, develop, etc.)
842
+
843
+ Args:
844
+ branch: Branch name to check
845
+
846
+ Returns:
847
+ True if branch is protected
848
+ """
849
+ protected_branches = ["main", "master", "develop", "production", "staging"]
850
+ return branch.lower() in protected_branches
@@ -100,6 +100,7 @@ class GitPlugin(TitanPlugin):
100
100
  """
101
101
  from .steps.branch_steps import get_current_branch_step, get_base_branch_step
102
102
  from .steps.ai_commit_message_step import ai_generate_commit_message
103
+ from .steps.diff_summary_step import show_uncommitted_diff_summary, show_branch_diff_summary
103
104
 
104
105
  return {
105
106
  "get_status": get_git_status_step,
@@ -108,6 +109,8 @@ class GitPlugin(TitanPlugin):
108
109
  "get_current_branch": get_current_branch_step,
109
110
  "get_base_branch": get_base_branch_step,
110
111
  "ai_generate_commit_message": ai_generate_commit_message,
112
+ "show_uncommitted_diff_summary": show_uncommitted_diff_summary,
113
+ "show_branch_diff_summary": show_branch_diff_summary,
111
114
  }
112
115
 
113
116
  @property
@@ -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"]