ace-git-copilot 0.3.3__tar.gz → 0.3.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 (126) hide show
  1. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/PKG-INFO +10 -1
  2. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/README.md +9 -0
  3. ace_git_copilot-0.3.5/ace/__init__.py +1 -0
  4. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/cli.py +205 -4
  5. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/pyproject.toml +1 -1
  6. ace_git_copilot-0.3.5/tests/test_workspace.py +89 -0
  7. ace_git_copilot-0.3.3/ace/__init__.py +0 -1
  8. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/AGENTS.md +0 -0
  9. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/BRIEFING.md +0 -0
  10. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/ORIGINAL_REQUEST.md +0 -0
  11. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/e2e_testing_track/BRIEFING.md +0 -0
  12. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/e2e_testing_track/ORIGINAL_REQUEST.md +0 -0
  13. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/e2e_testing_track/SCOPE.md +0 -0
  14. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/e2e_testing_track/progress.md +0 -0
  15. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/BRIEFING.md +0 -0
  16. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/ORIGINAL_REQUEST.md +0 -0
  17. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/emojis_list.txt +0 -0
  18. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/find_unused_modules.py +0 -0
  19. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/handoff.md +0 -0
  20. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/measure_lazy_startup.py +0 -0
  21. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/measure_startup.py +0 -0
  22. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/profile_imports.py +0 -0
  23. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/progress.md +0 -0
  24. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/run_importtime.py +0 -0
  25. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/search_banner.py +0 -0
  26. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/search_emojis.py +0 -0
  27. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/search_git_usages.py +0 -0
  28. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/search_usages.py +0 -0
  29. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/test_import_profiler.py +0 -0
  30. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/explorer_init/test_mocked_sys.py +0 -0
  31. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/handoff.md +0 -0
  32. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/implementation_track/BRIEFING.md +0 -0
  33. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/implementation_track/ORIGINAL_REQUEST.md +0 -0
  34. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/implementation_track/explorer_initial_report.md +0 -0
  35. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/implementation_track/progress.md +0 -0
  36. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/orchestrator/.gitkeep +0 -0
  37. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/orchestrator/BRIEFING.md +0 -0
  38. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/orchestrator/ORIGINAL_REQUEST.md +0 -0
  39. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/orchestrator/PROJECT.md +0 -0
  40. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/orchestrator/progress.md +0 -0
  41. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/teamwork_preview_explorer_e2e_explore/BRIEFING.md +0 -0
  42. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/teamwork_preview_explorer_e2e_explore/ORIGINAL_REQUEST.md +0 -0
  43. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/teamwork_preview_explorer_e2e_explore/handoff.md +0 -0
  44. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/teamwork_preview_explorer_e2e_explore/progress.md +0 -0
  45. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/worker_e2e_testing/BRIEFING.md +0 -0
  46. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/worker_e2e_testing/ORIGINAL_REQUEST.md +0 -0
  47. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/worker_e2e_testing/progress.md +0 -0
  48. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/worker_m1_startup/BRIEFING.md +0 -0
  49. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/worker_m1_startup/ORIGINAL_REQUEST.md +0 -0
  50. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.agents/worker_m1_startup/progress.md +0 -0
  51. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.env.example +0 -0
  52. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.github/workflows/tests.yml +0 -0
  53. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/.gitignore +0 -0
  54. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/CODE_OF_CONDUCT.md +0 -0
  55. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/CONTRIBUTING.md +0 -0
  56. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/LICENSE +0 -0
  57. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/PROJECT.md +0 -0
  58. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/SECURITY.md +0 -0
  59. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/SUPPORT.md +0 -0
  60. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/TEST_INFRA.md +0 -0
  61. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/TEST_READY.md +0 -0
  62. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/__main__.py +0 -0
  63. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/changelog_generator.py +0 -0
  64. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/code_reviewer.py +0 -0
  65. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/commit_generator.py +0 -0
  66. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/conflict_resolver.py +0 -0
  67. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/gitignore_generator.py +0 -0
  68. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/history_analyzer.py +0 -0
  69. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/intent_parser.py +0 -0
  70. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/llm_factory.py +0 -0
  71. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/pr_drafter.py +0 -0
  72. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/prompts/changelog.py +0 -0
  73. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/prompts/commit.py +0 -0
  74. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/prompts/conflict.py +0 -0
  75. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/prompts/doctor.py +0 -0
  76. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/prompts/explain.py +0 -0
  77. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/prompts/ignore.py +0 -0
  78. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/prompts/intent.py +0 -0
  79. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/prompts/pr.py +0 -0
  80. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/prompts/rebase.py +0 -0
  81. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/prompts/review.py +0 -0
  82. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/prompts/search.py +0 -0
  83. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/prompts/undo.py +0 -0
  84. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ai/rebase_helper.py +0 -0
  85. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/core/config.py +0 -0
  86. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/core/context.py +0 -0
  87. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/core/diagnostics.py +0 -0
  88. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/core/git_ops.py +0 -0
  89. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/core/hooks.py +0 -0
  90. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/core/safety.py +0 -0
  91. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ui/banner.py +0 -0
  92. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ui/dashboard.py +0 -0
  93. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ui/display.py +0 -0
  94. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ui/prompts.py +0 -0
  95. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/ui/themes.py +0 -0
  96. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/utils/conflict_parser.py +0 -0
  97. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/utils/diff_parser.py +0 -0
  98. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/ace/utils/json_utils.py +0 -0
  99. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/conftest.py +0 -0
  100. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/e2e/conftest.py +0 -0
  101. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/e2e/test_tier1_features.py +0 -0
  102. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/e2e/test_tier2_boundaries.py +0 -0
  103. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/e2e/test_tier3_combinations.py +0 -0
  104. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/e2e/test_tier4_workloads.py +0 -0
  105. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_changelog_generator.py +0 -0
  106. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_code_reviewer.py +0 -0
  107. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_config_atomic_write.py +0 -0
  108. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_conflict_resolver.py +0 -0
  109. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_conflict_resolver_backup.py +0 -0
  110. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_context_gitdir_detection.py +0 -0
  111. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_diagnostics.py +0 -0
  112. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_diff_trimmer.py +0 -0
  113. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_git_ops.py +0 -0
  114. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_git_ops_execute.py +0 -0
  115. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_help.py +0 -0
  116. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_history_analyzer.py +0 -0
  117. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_hooks.py +0 -0
  118. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_ignore.py +0 -0
  119. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_intent_parser.py +0 -0
  120. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_llm_factory.py +0 -0
  121. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_llm_factory_ollama.py +0 -0
  122. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_pr_drafter.py +0 -0
  123. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_rebase_helper.py +0 -0
  124. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_safety.py +0 -0
  125. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_search.py +0 -0
  126. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.5}/tests/test_undo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ace-git-copilot
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary: AI-powered Git copilot — talk to Git in plain English
5
5
  Project-URL: Homepage, https://github.com/jachinsamuel/Ace
6
6
  Project-URL: Documentation, https://github.com/jachinsamuel/Ace#readme
@@ -170,6 +170,7 @@ Ace has dedicated subcommands for specific tasks. Here is the complete command l
170
170
  | `ace squash` | — | Automatically analyze commit history and run AI-guided interactive rebases. |
171
171
  | `ace ignore <rule>` | — | Generate and append standard templates to `.gitignore`. |
172
172
  | `ace undo` | — | Safely revert the last action after checking repository status. |
173
+ | `ace workspace` | `ace ws` | Monitor status of multiple repositories and navigate between them. |
173
174
  | `ace dash` | — | Launch the interactive repository management console (TUI). |
174
175
  | `ace config` | — | Print out active configuration settings. |
175
176
  | `ace help` | — | Show user guide and help information on how to use Ace. |
@@ -198,6 +199,14 @@ Distributed under the MIT License. See [LICENSE](LICENSE) for more details.
198
199
 
199
200
  ## Changelog
200
201
 
202
+ ### v0.3.5 — Empty Diff Handling (2026-07-11)
203
+ * Improved warning panel message and title when committing empty files or mode changes to prevent confusing users when files are already staged.
204
+
205
+ ### v0.3.4 — Workspace Management (2026-07-11)
206
+ * Added new `workspace` command (alias `ws`) to scan, monitor, and navigate multiple repositories.
207
+ * Shows active branch, staged/unstaged/untracked file counts, and local upstream sync offsets in a Rich Table.
208
+ * Supports interactive actions to open the TUI dashboard, spawn a nested shell, or run copy-paste commands inside any chosen repository.
209
+
201
210
  ### v0.3.3 — Stability & Resilience (2026-07-04)
202
211
  * Implemented streaming progress and download percentages for Ollama model pulls to prevent thread hangs.
203
212
  * Added atomic file saves and rollback backups for merge conflict resolutions and config modifications to prevent data corruption.
@@ -137,6 +137,7 @@ Ace has dedicated subcommands for specific tasks. Here is the complete command l
137
137
  | `ace squash` | — | Automatically analyze commit history and run AI-guided interactive rebases. |
138
138
  | `ace ignore <rule>` | — | Generate and append standard templates to `.gitignore`. |
139
139
  | `ace undo` | — | Safely revert the last action after checking repository status. |
140
+ | `ace workspace` | `ace ws` | Monitor status of multiple repositories and navigate between them. |
140
141
  | `ace dash` | — | Launch the interactive repository management console (TUI). |
141
142
  | `ace config` | — | Print out active configuration settings. |
142
143
  | `ace help` | — | Show user guide and help information on how to use Ace. |
@@ -165,6 +166,14 @@ Distributed under the MIT License. See [LICENSE](LICENSE) for more details.
165
166
 
166
167
  ## Changelog
167
168
 
169
+ ### v0.3.5 — Empty Diff Handling (2026-07-11)
170
+ * Improved warning panel message and title when committing empty files or mode changes to prevent confusing users when files are already staged.
171
+
172
+ ### v0.3.4 — Workspace Management (2026-07-11)
173
+ * Added new `workspace` command (alias `ws`) to scan, monitor, and navigate multiple repositories.
174
+ * Shows active branch, staged/unstaged/untracked file counts, and local upstream sync offsets in a Rich Table.
175
+ * Supports interactive actions to open the TUI dashboard, spawn a nested shell, or run copy-paste commands inside any chosen repository.
176
+
168
177
  ### v0.3.3 — Stability & Resilience (2026-07-04)
169
178
  * Implemented streaming progress and download percentages for Ollama model pulls to prevent thread hangs.
170
179
  * Added atomic file saves and rollback backups for merge conflict resolutions and config modifications to prevent data corruption.
@@ -0,0 +1 @@
1
+ __version__ = "0.3.5"
@@ -299,10 +299,22 @@ def commit_cmd(
299
299
  with spinner("Analyzing changes and generating commit message..."):
300
300
  msg = generator.generate_message(format_type=format_type, offline=offline)
301
301
  except NoStagedChangesError as e:
302
- show_warning_panel(
303
- f"{str(e)}\n\n[bold]Tip:[/bold] Stage your files first: [command]git add <files>[/command]",
304
- "No Staged Changes"
305
- )
302
+ staged_files = []
303
+ try:
304
+ staged_files = git_ops.get_status().get("staged", [])
305
+ except Exception:
306
+ pass
307
+
308
+ if staged_files:
309
+ show_warning_panel(
310
+ f"{str(e)}\n\n[bold]Tip:[/bold] Use [command]git commit -m \"message\"[/command] to commit these changes manually, or add content to the files.",
311
+ "Empty Staged Diff"
312
+ )
313
+ else:
314
+ show_warning_panel(
315
+ f"{str(e)}\n\n[bold]Tip:[/bold] Stage your files first: [command]git add <files>[/command]",
316
+ "No Staged Changes"
317
+ )
306
318
  raise typer.Exit(code=0)
307
319
  except LLMConfigurationError as e:
308
320
  show_error_panel(f"{str(e)}\n\nRun [bold]ace setup[/bold] to configure your AI credentials.", "Configuration Error")
@@ -1686,6 +1698,195 @@ def hook_cmd(
1686
1698
  show_error_panel("Invalid action. Use 'install' or 'uninstall'.", "Usage Error")
1687
1699
  raise typer.Exit(code=1)
1688
1700
 
1701
+ @app.command(name="workspace", help="Monitor status of multiple repositories and navigate between them.")
1702
+ def workspace_cmd(
1703
+ path: Optional[str] = typer.Argument(
1704
+ None, help="Custom directory to scan (defaults to sibling projects scan)"
1705
+ ),
1706
+ ):
1707
+ from pathlib import Path
1708
+ import os
1709
+ import sys
1710
+ import subprocess
1711
+ from rich.table import Table
1712
+ from ace.core.git_ops import GitOps, NotAGitRepositoryError
1713
+ from ace.ui.display import console, print_info, print_success, print_warning, print_error
1714
+ from ace.ui.prompts import prompt_select, prompt_action
1715
+
1716
+ # 1. Determine target directory to scan
1717
+ if path:
1718
+ scan_dir = Path(path).resolve()
1719
+ else:
1720
+ # Smart default
1721
+ cwd = Path(os.getcwd()).resolve()
1722
+ if (cwd / ".git").exists():
1723
+ scan_dir = cwd.parent
1724
+ else:
1725
+ scan_dir = cwd
1726
+
1727
+ if not scan_dir.exists() or not scan_dir.is_dir():
1728
+ print_error(f"Directory not found: {scan_dir}")
1729
+ raise typer.Exit(code=1)
1730
+
1731
+ print_info(f"Scanning workspace directory: [bold]{scan_dir}[/bold]")
1732
+
1733
+ # 2. Find git repositories
1734
+ repos_found = []
1735
+ try:
1736
+ for child in sorted(scan_dir.iterdir()):
1737
+ if not child.is_dir():
1738
+ continue
1739
+ # Basic validation that it's a git repo
1740
+ if (child / ".git").exists() or (child / "gitdir").exists() or child.name.endswith(".git"):
1741
+ try:
1742
+ git_ops = GitOps(str(child))
1743
+ repos_found.append((child, git_ops))
1744
+ except Exception:
1745
+ pass
1746
+ except Exception as e:
1747
+ print_error(f"Failed to scan directory: {e}")
1748
+ raise typer.Exit(code=1)
1749
+
1750
+ if not repos_found:
1751
+ print_warning(f"No active Git repositories found in: {scan_dir}")
1752
+ return
1753
+
1754
+ # 3. Build Status Table
1755
+ table = Table(
1756
+ title=f"[bold white]Workspace Summary: {scan_dir.name}[/bold white]",
1757
+ title_style="bold",
1758
+ border_style="#FF6D00",
1759
+ expand=True,
1760
+ )
1761
+ table.add_column("[bold #00D5FF]#[/bold #00D5FF]", justify="right", width=4)
1762
+ table.add_column("Repository", style="bold white")
1763
+ table.add_column("Branch", style="bold cyan")
1764
+ table.add_column("Status", justify="center")
1765
+ table.add_column("Sync", justify="center")
1766
+
1767
+ display_options = []
1768
+ for idx, (repo_path, git_ops) in enumerate(repos_found, 1):
1769
+ repo_name = repo_path.name
1770
+
1771
+ # Branch
1772
+ branch_name = git_ops.get_current_branch() or "Detached HEAD"
1773
+
1774
+ # Status counts
1775
+ status = git_ops.get_status()
1776
+ staged = len(status.get("staged", []))
1777
+ unstaged = len(status.get("unstaged", []))
1778
+ untracked = len(status.get("untracked", []))
1779
+
1780
+ if staged == 0 and unstaged == 0 and untracked == 0:
1781
+ status_desc = "[success]Clean[/success]"
1782
+ else:
1783
+ parts = []
1784
+ if staged > 0:
1785
+ parts.append(f"[bold #00E676]+{staged}[/bold #00E676]")
1786
+ if unstaged > 0:
1787
+ parts.append(f"[bold #FFD600]~{unstaged}[/bold #FFD600]")
1788
+ if untracked > 0:
1789
+ parts.append(f"[bold #FF1744]?{untracked}[/bold #FF1744]")
1790
+ status_desc = " ".join(parts)
1791
+
1792
+ # Sync offsets
1793
+ ab = git_ops.get_ahead_behind()
1794
+ ahead = ab.get("ahead", 0)
1795
+ behind = ab.get("behind", 0)
1796
+ tracking = git_ops.get_upstream_tracking()
1797
+
1798
+ if not tracking:
1799
+ sync_desc = "[#666666]No Upstream[/#666666]"
1800
+ elif ahead == 0 and behind == 0:
1801
+ sync_desc = "[success]Up-to-date[/success]"
1802
+ elif ahead > 0 and behind == 0:
1803
+ sync_desc = f"[bold #00D5FF]Ahead {ahead}[/bold #00D5FF]"
1804
+ elif behind > 0 and ahead == 0:
1805
+ sync_desc = f"[bold #FFD600]Behind {behind}[/bold #FFD600]"
1806
+ else:
1807
+ sync_desc = f"[bold #FF1744]Diverged (A{ahead}, B{behind})[/bold #FF1744]"
1808
+
1809
+ table.add_row(
1810
+ str(idx),
1811
+ repo_name,
1812
+ branch_name,
1813
+ status_desc,
1814
+ sync_desc
1815
+ )
1816
+ display_options.append(repo_name)
1817
+
1818
+ console.print()
1819
+ console.print(table)
1820
+ console.print()
1821
+
1822
+ # 4. Prompt selection
1823
+ selected_idx = prompt_select(
1824
+ display_options,
1825
+ prompt_text=" Select repository number to manage (or 'q' to quit)",
1826
+ default="q"
1827
+ )
1828
+
1829
+ if selected_idx < 0:
1830
+ print_info("Exit workspace monitor.")
1831
+ return
1832
+
1833
+ selected_path, _ = repos_found[selected_idx]
1834
+
1835
+ # 5. Prompt action choice
1836
+ console.print(f"\n[bold white] Choose action for [bold cyan]{selected_path.name}[/bold cyan]:[/bold white]")
1837
+ action_options = {
1838
+ "d": ("Dashboard", "Open interactive TUI dashboard"),
1839
+ "s": ("Shell", "Spawn an interactive shell in project directory"),
1840
+ "c": ("Command", "Execute an ace natural-language command"),
1841
+ "q": ("Quit", "Cancel action and exit")
1842
+ }
1843
+
1844
+ action = prompt_action(action_options, default_key="d")
1845
+
1846
+ if action == "d":
1847
+ print_success(f"Launching dashboard in {selected_path.name}...")
1848
+ console.print()
1849
+ try:
1850
+ subprocess.run([sys.executable, "-m", "ace", "dash"], cwd=str(selected_path))
1851
+ except Exception as e:
1852
+ print_error(f"Failed to launch dashboard: {e}")
1853
+
1854
+ elif action == "s":
1855
+ shell = os.environ.get("SHELL")
1856
+ if not shell:
1857
+ if sys.platform == "win32":
1858
+ shell = "powershell.exe"
1859
+ else:
1860
+ shell = "/bin/bash"
1861
+
1862
+ print_success(f"Opening interactive shell in {selected_path.name}...")
1863
+ print_info("Type 'exit' to return to Ace workspace monitor.")
1864
+ console.print()
1865
+
1866
+ try:
1867
+ subprocess.run([shell], cwd=str(selected_path))
1868
+ print_success("Returned to Ace workspace.")
1869
+ except Exception as e:
1870
+ print_error(f"Failed to open shell: {e}")
1871
+
1872
+ elif action == "c":
1873
+ query = typer.prompt(" Enter command for this repo")
1874
+ if query.strip():
1875
+ print_success(f"Running command: {query}")
1876
+ console.print()
1877
+ try:
1878
+ subprocess.run([sys.executable, "-m", "ace", query], cwd=str(selected_path))
1879
+ except Exception as e:
1880
+ print_error(f"Failed to run command: {e}")
1881
+
1882
+ @app.command(name="ws", help="Alias for 'workspace' command.")
1883
+ def ws_cmd(
1884
+ path: Optional[str] = typer.Argument(
1885
+ None, help="Custom directory to scan"
1886
+ ),
1887
+ ):
1888
+ workspace_cmd(path)
1889
+
1689
1890
  if __name__ == "__main__":
1690
1891
  app()
1691
1892
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ace-git-copilot"
3
- version = "0.3.3"
3
+ version = "0.3.5"
4
4
  description = "AI-powered Git copilot — talk to Git in plain English"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -0,0 +1,89 @@
1
+ import pytest
2
+ from unittest.mock import patch, MagicMock
3
+ from pathlib import Path
4
+ from ace.cli import workspace_cmd
5
+
6
+ @patch("ace.core.git_ops.GitOps")
7
+ @patch("pathlib.Path.iterdir")
8
+ @patch("pathlib.Path.is_dir")
9
+ @patch("pathlib.Path.exists")
10
+ @patch("ace.ui.prompts.prompt_select")
11
+ @patch("ace.ui.prompts.prompt_action")
12
+ @patch("subprocess.run")
13
+ def test_workspace_cmd_select_dashboard(
14
+ mock_sub_run,
15
+ mock_action,
16
+ mock_select,
17
+ mock_exists,
18
+ mock_is_dir,
19
+ mock_iterdir,
20
+ mock_gitops
21
+ ):
22
+ # Setup directories
23
+ repo_path = MagicMock(spec=Path)
24
+ repo_path.name = "my-test-project"
25
+ repo_path.__str__.return_value = "/mock/workspace/my-test-project"
26
+
27
+ # Mock checks
28
+ mock_is_dir.return_value = True
29
+ # mock exists to simulate that (child / ".git") exists
30
+ mock_exists.side_effect = lambda: True
31
+ mock_iterdir.return_value = [repo_path]
32
+
33
+ # Mock GitOps response
34
+ mock_git = MagicMock()
35
+ mock_git.get_current_branch.return_value = "main"
36
+ mock_git.get_status.return_value = {"staged": ["file1.txt"], "unstaged": [], "untracked": []}
37
+ mock_git.get_ahead_behind.return_value = {"ahead": 2, "behind": 0}
38
+ mock_git.get_upstream_tracking.return_value = "origin/main"
39
+ mock_gitops.return_value = mock_git
40
+
41
+ # Selection: idx 0 (my-test-project)
42
+ mock_select.return_value = 0
43
+ # Action: 'd' (Dashboard)
44
+ mock_action.return_value = "d"
45
+
46
+ # Run
47
+ workspace_cmd("/mock/workspace")
48
+
49
+ # Assert dashboard was spawned in the subdirectory via subprocess
50
+ mock_sub_run.assert_called_once()
51
+ args, kwargs = mock_sub_run.call_args
52
+ assert "dash" in args[0]
53
+ assert kwargs["cwd"] == "/mock/workspace/my-test-project"
54
+
55
+ @patch("ace.core.git_ops.GitOps")
56
+ @patch("pathlib.Path.iterdir")
57
+ @patch("pathlib.Path.is_dir")
58
+ @patch("pathlib.Path.exists")
59
+ @patch("ace.ui.prompts.prompt_select")
60
+ def test_workspace_cmd_quit_early(
61
+ mock_select,
62
+ mock_exists,
63
+ mock_is_dir,
64
+ mock_iterdir,
65
+ mock_gitops
66
+ ):
67
+ repo_path = MagicMock(spec=Path)
68
+ repo_path.name = "my-test-project"
69
+ repo_path.__str__.return_value = "/mock/workspace/my-test-project"
70
+
71
+ mock_is_dir.return_value = True
72
+ mock_exists.side_effect = lambda: True
73
+ mock_iterdir.return_value = [repo_path]
74
+
75
+ mock_git = MagicMock()
76
+ mock_git.get_current_branch.return_value = "main"
77
+ mock_git.get_status.return_value = {"staged": [], "unstaged": [], "untracked": []}
78
+ mock_git.get_ahead_behind.return_value = {"ahead": 0, "behind": 0}
79
+ mock_git.get_upstream_tracking.return_value = "origin/main"
80
+ mock_gitops.return_value = mock_git
81
+
82
+ # Selection: -1 (Quit/Skip)
83
+ mock_select.return_value = -1
84
+
85
+ # Run
86
+ workspace_cmd("/mock/workspace")
87
+
88
+ # Verify no action prompt or subprocess was triggered
89
+ mock_select.assert_called_once()
@@ -1 +0,0 @@
1
- __version__ = "0.3.3"
File without changes