ace-git-copilot 0.3.3__tar.gz → 0.3.4__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.4}/PKG-INFO +7 -1
  2. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/README.md +6 -0
  3. ace_git_copilot-0.3.4/ace/__init__.py +1 -0
  4. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/cli.py +189 -0
  5. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/pyproject.toml +1 -1
  6. ace_git_copilot-0.3.4/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.4}/.agents/AGENTS.md +0 -0
  9. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/BRIEFING.md +0 -0
  10. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/ORIGINAL_REQUEST.md +0 -0
  11. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/e2e_testing_track/BRIEFING.md +0 -0
  12. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/e2e_testing_track/ORIGINAL_REQUEST.md +0 -0
  13. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/e2e_testing_track/SCOPE.md +0 -0
  14. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/e2e_testing_track/progress.md +0 -0
  15. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/BRIEFING.md +0 -0
  16. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/ORIGINAL_REQUEST.md +0 -0
  17. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/emojis_list.txt +0 -0
  18. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/find_unused_modules.py +0 -0
  19. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/handoff.md +0 -0
  20. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/measure_lazy_startup.py +0 -0
  21. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/measure_startup.py +0 -0
  22. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/profile_imports.py +0 -0
  23. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/progress.md +0 -0
  24. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/run_importtime.py +0 -0
  25. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/search_banner.py +0 -0
  26. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/search_emojis.py +0 -0
  27. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/search_git_usages.py +0 -0
  28. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/search_usages.py +0 -0
  29. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/test_import_profiler.py +0 -0
  30. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/explorer_init/test_mocked_sys.py +0 -0
  31. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/handoff.md +0 -0
  32. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/implementation_track/BRIEFING.md +0 -0
  33. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/implementation_track/ORIGINAL_REQUEST.md +0 -0
  34. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/implementation_track/explorer_initial_report.md +0 -0
  35. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/implementation_track/progress.md +0 -0
  36. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/orchestrator/.gitkeep +0 -0
  37. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/orchestrator/BRIEFING.md +0 -0
  38. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/orchestrator/ORIGINAL_REQUEST.md +0 -0
  39. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/orchestrator/PROJECT.md +0 -0
  40. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/orchestrator/progress.md +0 -0
  41. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/teamwork_preview_explorer_e2e_explore/BRIEFING.md +0 -0
  42. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/teamwork_preview_explorer_e2e_explore/ORIGINAL_REQUEST.md +0 -0
  43. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/teamwork_preview_explorer_e2e_explore/handoff.md +0 -0
  44. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/teamwork_preview_explorer_e2e_explore/progress.md +0 -0
  45. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/worker_e2e_testing/BRIEFING.md +0 -0
  46. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/worker_e2e_testing/ORIGINAL_REQUEST.md +0 -0
  47. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/worker_e2e_testing/progress.md +0 -0
  48. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/worker_m1_startup/BRIEFING.md +0 -0
  49. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/worker_m1_startup/ORIGINAL_REQUEST.md +0 -0
  50. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.agents/worker_m1_startup/progress.md +0 -0
  51. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.env.example +0 -0
  52. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.github/workflows/tests.yml +0 -0
  53. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/.gitignore +0 -0
  54. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/CODE_OF_CONDUCT.md +0 -0
  55. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/CONTRIBUTING.md +0 -0
  56. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/LICENSE +0 -0
  57. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/PROJECT.md +0 -0
  58. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/SECURITY.md +0 -0
  59. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/SUPPORT.md +0 -0
  60. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/TEST_INFRA.md +0 -0
  61. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/TEST_READY.md +0 -0
  62. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/__main__.py +0 -0
  63. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/changelog_generator.py +0 -0
  64. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/code_reviewer.py +0 -0
  65. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/commit_generator.py +0 -0
  66. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/conflict_resolver.py +0 -0
  67. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/gitignore_generator.py +0 -0
  68. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/history_analyzer.py +0 -0
  69. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/intent_parser.py +0 -0
  70. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/llm_factory.py +0 -0
  71. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/pr_drafter.py +0 -0
  72. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/prompts/changelog.py +0 -0
  73. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/prompts/commit.py +0 -0
  74. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/prompts/conflict.py +0 -0
  75. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/prompts/doctor.py +0 -0
  76. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/prompts/explain.py +0 -0
  77. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/prompts/ignore.py +0 -0
  78. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/prompts/intent.py +0 -0
  79. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/prompts/pr.py +0 -0
  80. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/prompts/rebase.py +0 -0
  81. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/prompts/review.py +0 -0
  82. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/prompts/search.py +0 -0
  83. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/prompts/undo.py +0 -0
  84. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ai/rebase_helper.py +0 -0
  85. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/core/config.py +0 -0
  86. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/core/context.py +0 -0
  87. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/core/diagnostics.py +0 -0
  88. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/core/git_ops.py +0 -0
  89. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/core/hooks.py +0 -0
  90. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/core/safety.py +0 -0
  91. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ui/banner.py +0 -0
  92. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ui/dashboard.py +0 -0
  93. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ui/display.py +0 -0
  94. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ui/prompts.py +0 -0
  95. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/ui/themes.py +0 -0
  96. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/utils/conflict_parser.py +0 -0
  97. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/utils/diff_parser.py +0 -0
  98. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/ace/utils/json_utils.py +0 -0
  99. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/conftest.py +0 -0
  100. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/e2e/conftest.py +0 -0
  101. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/e2e/test_tier1_features.py +0 -0
  102. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/e2e/test_tier2_boundaries.py +0 -0
  103. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/e2e/test_tier3_combinations.py +0 -0
  104. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/e2e/test_tier4_workloads.py +0 -0
  105. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_changelog_generator.py +0 -0
  106. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_code_reviewer.py +0 -0
  107. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_config_atomic_write.py +0 -0
  108. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_conflict_resolver.py +0 -0
  109. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_conflict_resolver_backup.py +0 -0
  110. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_context_gitdir_detection.py +0 -0
  111. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_diagnostics.py +0 -0
  112. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_diff_trimmer.py +0 -0
  113. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_git_ops.py +0 -0
  114. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_git_ops_execute.py +0 -0
  115. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_help.py +0 -0
  116. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_history_analyzer.py +0 -0
  117. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_hooks.py +0 -0
  118. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_ignore.py +0 -0
  119. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_intent_parser.py +0 -0
  120. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_llm_factory.py +0 -0
  121. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_llm_factory_ollama.py +0 -0
  122. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_pr_drafter.py +0 -0
  123. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_rebase_helper.py +0 -0
  124. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_safety.py +0 -0
  125. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/tests/test_search.py +0 -0
  126. {ace_git_copilot-0.3.3 → ace_git_copilot-0.3.4}/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.4
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,11 @@ Distributed under the MIT License. See [LICENSE](LICENSE) for more details.
198
199
 
199
200
  ## Changelog
200
201
 
202
+ ### v0.3.4 — Workspace Management (2026-07-11)
203
+ * Added new `workspace` command (alias `ws`) to scan, monitor, and navigate multiple repositories.
204
+ * Shows active branch, staged/unstaged/untracked file counts, and local upstream sync offsets in a Rich Table.
205
+ * Supports interactive actions to open the TUI dashboard, spawn a nested shell, or run copy-paste commands inside any chosen repository.
206
+
201
207
  ### v0.3.3 — Stability & Resilience (2026-07-04)
202
208
  * Implemented streaming progress and download percentages for Ollama model pulls to prevent thread hangs.
203
209
  * 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,11 @@ Distributed under the MIT License. See [LICENSE](LICENSE) for more details.
165
166
 
166
167
  ## Changelog
167
168
 
169
+ ### v0.3.4 — Workspace Management (2026-07-11)
170
+ * Added new `workspace` command (alias `ws`) to scan, monitor, and navigate multiple repositories.
171
+ * Shows active branch, staged/unstaged/untracked file counts, and local upstream sync offsets in a Rich Table.
172
+ * Supports interactive actions to open the TUI dashboard, spawn a nested shell, or run copy-paste commands inside any chosen repository.
173
+
168
174
  ### v0.3.3 — Stability & Resilience (2026-07-04)
169
175
  * Implemented streaming progress and download percentages for Ollama model pulls to prevent thread hangs.
170
176
  * 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.4"
@@ -1686,6 +1686,195 @@ def hook_cmd(
1686
1686
  show_error_panel("Invalid action. Use 'install' or 'uninstall'.", "Usage Error")
1687
1687
  raise typer.Exit(code=1)
1688
1688
 
1689
+ @app.command(name="workspace", help="Monitor status of multiple repositories and navigate between them.")
1690
+ def workspace_cmd(
1691
+ path: Optional[str] = typer.Argument(
1692
+ None, help="Custom directory to scan (defaults to sibling projects scan)"
1693
+ ),
1694
+ ):
1695
+ from pathlib import Path
1696
+ import os
1697
+ import sys
1698
+ import subprocess
1699
+ from rich.table import Table
1700
+ from ace.core.git_ops import GitOps, NotAGitRepositoryError
1701
+ from ace.ui.display import console, print_info, print_success, print_warning, print_error
1702
+ from ace.ui.prompts import prompt_select, prompt_action
1703
+
1704
+ # 1. Determine target directory to scan
1705
+ if path:
1706
+ scan_dir = Path(path).resolve()
1707
+ else:
1708
+ # Smart default
1709
+ cwd = Path(os.getcwd()).resolve()
1710
+ if (cwd / ".git").exists():
1711
+ scan_dir = cwd.parent
1712
+ else:
1713
+ scan_dir = cwd
1714
+
1715
+ if not scan_dir.exists() or not scan_dir.is_dir():
1716
+ print_error(f"Directory not found: {scan_dir}")
1717
+ raise typer.Exit(code=1)
1718
+
1719
+ print_info(f"Scanning workspace directory: [bold]{scan_dir}[/bold]")
1720
+
1721
+ # 2. Find git repositories
1722
+ repos_found = []
1723
+ try:
1724
+ for child in sorted(scan_dir.iterdir()):
1725
+ if not child.is_dir():
1726
+ continue
1727
+ # Basic validation that it's a git repo
1728
+ if (child / ".git").exists() or (child / "gitdir").exists() or child.name.endswith(".git"):
1729
+ try:
1730
+ git_ops = GitOps(str(child))
1731
+ repos_found.append((child, git_ops))
1732
+ except Exception:
1733
+ pass
1734
+ except Exception as e:
1735
+ print_error(f"Failed to scan directory: {e}")
1736
+ raise typer.Exit(code=1)
1737
+
1738
+ if not repos_found:
1739
+ print_warning(f"No active Git repositories found in: {scan_dir}")
1740
+ return
1741
+
1742
+ # 3. Build Status Table
1743
+ table = Table(
1744
+ title=f"[bold white]Workspace Summary: {scan_dir.name}[/bold white]",
1745
+ title_style="bold",
1746
+ border_style="#FF6D00",
1747
+ expand=True,
1748
+ )
1749
+ table.add_column("[bold #00D5FF]#[/bold #00D5FF]", justify="right", width=4)
1750
+ table.add_column("Repository", style="bold white")
1751
+ table.add_column("Branch", style="bold cyan")
1752
+ table.add_column("Status", justify="center")
1753
+ table.add_column("Sync", justify="center")
1754
+
1755
+ display_options = []
1756
+ for idx, (repo_path, git_ops) in enumerate(repos_found, 1):
1757
+ repo_name = repo_path.name
1758
+
1759
+ # Branch
1760
+ branch_name = git_ops.get_current_branch() or "Detached HEAD"
1761
+
1762
+ # Status counts
1763
+ status = git_ops.get_status()
1764
+ staged = len(status.get("staged", []))
1765
+ unstaged = len(status.get("unstaged", []))
1766
+ untracked = len(status.get("untracked", []))
1767
+
1768
+ if staged == 0 and unstaged == 0 and untracked == 0:
1769
+ status_desc = "[success]Clean[/success]"
1770
+ else:
1771
+ parts = []
1772
+ if staged > 0:
1773
+ parts.append(f"[bold #00E676]+{staged}[/bold #00E676]")
1774
+ if unstaged > 0:
1775
+ parts.append(f"[bold #FFD600]~{unstaged}[/bold #FFD600]")
1776
+ if untracked > 0:
1777
+ parts.append(f"[bold #FF1744]?{untracked}[/bold #FF1744]")
1778
+ status_desc = " ".join(parts)
1779
+
1780
+ # Sync offsets
1781
+ ab = git_ops.get_ahead_behind()
1782
+ ahead = ab.get("ahead", 0)
1783
+ behind = ab.get("behind", 0)
1784
+ tracking = git_ops.get_upstream_tracking()
1785
+
1786
+ if not tracking:
1787
+ sync_desc = "[#666666]No Upstream[/#666666]"
1788
+ elif ahead == 0 and behind == 0:
1789
+ sync_desc = "[success]Up-to-date[/success]"
1790
+ elif ahead > 0 and behind == 0:
1791
+ sync_desc = f"[bold #00D5FF]Ahead {ahead}[/bold #00D5FF]"
1792
+ elif behind > 0 and ahead == 0:
1793
+ sync_desc = f"[bold #FFD600]Behind {behind}[/bold #FFD600]"
1794
+ else:
1795
+ sync_desc = f"[bold #FF1744]Diverged (A{ahead}, B{behind})[/bold #FF1744]"
1796
+
1797
+ table.add_row(
1798
+ str(idx),
1799
+ repo_name,
1800
+ branch_name,
1801
+ status_desc,
1802
+ sync_desc
1803
+ )
1804
+ display_options.append(repo_name)
1805
+
1806
+ console.print()
1807
+ console.print(table)
1808
+ console.print()
1809
+
1810
+ # 4. Prompt selection
1811
+ selected_idx = prompt_select(
1812
+ display_options,
1813
+ prompt_text=" Select repository number to manage (or 'q' to quit)",
1814
+ default="q"
1815
+ )
1816
+
1817
+ if selected_idx < 0:
1818
+ print_info("Exit workspace monitor.")
1819
+ return
1820
+
1821
+ selected_path, _ = repos_found[selected_idx]
1822
+
1823
+ # 5. Prompt action choice
1824
+ console.print(f"\n[bold white] Choose action for [bold cyan]{selected_path.name}[/bold cyan]:[/bold white]")
1825
+ action_options = {
1826
+ "d": ("Dashboard", "Open interactive TUI dashboard"),
1827
+ "s": ("Shell", "Spawn an interactive shell in project directory"),
1828
+ "c": ("Command", "Execute an ace natural-language command"),
1829
+ "q": ("Quit", "Cancel action and exit")
1830
+ }
1831
+
1832
+ action = prompt_action(action_options, default_key="d")
1833
+
1834
+ if action == "d":
1835
+ print_success(f"Launching dashboard in {selected_path.name}...")
1836
+ console.print()
1837
+ try:
1838
+ subprocess.run([sys.executable, "-m", "ace", "dash"], cwd=str(selected_path))
1839
+ except Exception as e:
1840
+ print_error(f"Failed to launch dashboard: {e}")
1841
+
1842
+ elif action == "s":
1843
+ shell = os.environ.get("SHELL")
1844
+ if not shell:
1845
+ if sys.platform == "win32":
1846
+ shell = "powershell.exe"
1847
+ else:
1848
+ shell = "/bin/bash"
1849
+
1850
+ print_success(f"Opening interactive shell in {selected_path.name}...")
1851
+ print_info("Type 'exit' to return to Ace workspace monitor.")
1852
+ console.print()
1853
+
1854
+ try:
1855
+ subprocess.run([shell], cwd=str(selected_path))
1856
+ print_success("Returned to Ace workspace.")
1857
+ except Exception as e:
1858
+ print_error(f"Failed to open shell: {e}")
1859
+
1860
+ elif action == "c":
1861
+ query = typer.prompt(" Enter command for this repo")
1862
+ if query.strip():
1863
+ print_success(f"Running command: {query}")
1864
+ console.print()
1865
+ try:
1866
+ subprocess.run([sys.executable, "-m", "ace", query], cwd=str(selected_path))
1867
+ except Exception as e:
1868
+ print_error(f"Failed to run command: {e}")
1869
+
1870
+ @app.command(name="ws", help="Alias for 'workspace' command.")
1871
+ def ws_cmd(
1872
+ path: Optional[str] = typer.Argument(
1873
+ None, help="Custom directory to scan"
1874
+ ),
1875
+ ):
1876
+ workspace_cmd(path)
1877
+
1689
1878
  if __name__ == "__main__":
1690
1879
  app()
1691
1880
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ace-git-copilot"
3
- version = "0.3.3"
3
+ version = "0.3.4"
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