ace-git-copilot 0.2.9__tar.gz → 0.3.0__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 (125) hide show
  1. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/PKG-INFO +1 -1
  2. ace_git_copilot-0.3.0/ace/ui/dashboard.py +378 -0
  3. ace_git_copilot-0.3.0/ace/ui/display.py +256 -0
  4. ace_git_copilot-0.3.0/ace/ui/prompts.py +103 -0
  5. ace_git_copilot-0.3.0/ace/ui/themes.py +33 -0
  6. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/pyproject.toml +7 -1
  7. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/e2e/conftest.py +0 -1
  8. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/e2e/test_tier1_features.py +0 -3
  9. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/e2e/test_tier2_boundaries.py +0 -1
  10. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/e2e/test_tier3_combinations.py +0 -3
  11. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/e2e/test_tier4_workloads.py +0 -3
  12. ace_git_copilot-0.2.9/ace/ui/dashboard.py +0 -294
  13. ace_git_copilot-0.2.9/ace/ui/display.py +0 -180
  14. ace_git_copilot-0.2.9/ace/ui/prompts.py +0 -89
  15. ace_git_copilot-0.2.9/ace/ui/themes.py +0 -24
  16. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/AGENTS.md +0 -0
  17. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/BRIEFING.md +0 -0
  18. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/ORIGINAL_REQUEST.md +0 -0
  19. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/e2e_testing_track/BRIEFING.md +0 -0
  20. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/e2e_testing_track/ORIGINAL_REQUEST.md +0 -0
  21. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/e2e_testing_track/SCOPE.md +0 -0
  22. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/e2e_testing_track/progress.md +0 -0
  23. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/BRIEFING.md +0 -0
  24. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/ORIGINAL_REQUEST.md +0 -0
  25. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/emojis_list.txt +0 -0
  26. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/find_unused_modules.py +0 -0
  27. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/handoff.md +0 -0
  28. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/measure_lazy_startup.py +0 -0
  29. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/measure_startup.py +0 -0
  30. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/profile_imports.py +0 -0
  31. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/progress.md +0 -0
  32. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/run_importtime.py +0 -0
  33. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/search_banner.py +0 -0
  34. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/search_emojis.py +0 -0
  35. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/search_git_usages.py +0 -0
  36. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/search_usages.py +0 -0
  37. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/test_import_profiler.py +0 -0
  38. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/test_mocked_sys.py +0 -0
  39. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/handoff.md +0 -0
  40. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/implementation_track/BRIEFING.md +0 -0
  41. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/implementation_track/ORIGINAL_REQUEST.md +0 -0
  42. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/implementation_track/explorer_initial_report.md +0 -0
  43. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/implementation_track/progress.md +0 -0
  44. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/orchestrator/.gitkeep +0 -0
  45. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/orchestrator/BRIEFING.md +0 -0
  46. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/orchestrator/ORIGINAL_REQUEST.md +0 -0
  47. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/orchestrator/PROJECT.md +0 -0
  48. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/orchestrator/progress.md +0 -0
  49. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/teamwork_preview_explorer_e2e_explore/BRIEFING.md +0 -0
  50. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/teamwork_preview_explorer_e2e_explore/ORIGINAL_REQUEST.md +0 -0
  51. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/teamwork_preview_explorer_e2e_explore/handoff.md +0 -0
  52. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/teamwork_preview_explorer_e2e_explore/progress.md +0 -0
  53. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/worker_e2e_testing/BRIEFING.md +0 -0
  54. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/worker_e2e_testing/ORIGINAL_REQUEST.md +0 -0
  55. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/worker_e2e_testing/progress.md +0 -0
  56. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/worker_m1_startup/BRIEFING.md +0 -0
  57. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/worker_m1_startup/ORIGINAL_REQUEST.md +0 -0
  58. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/worker_m1_startup/progress.md +0 -0
  59. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.env.example +0 -0
  60. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.github/workflows/tests.yml +0 -0
  61. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.gitignore +0 -0
  62. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  63. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/CONTRIBUTING.md +0 -0
  64. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/LICENSE +0 -0
  65. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/PROJECT.md +0 -0
  66. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/README.md +0 -0
  67. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/SECURITY.md +0 -0
  68. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/SUPPORT.md +0 -0
  69. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/TEST_INFRA.md +0 -0
  70. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/TEST_READY.md +0 -0
  71. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/__init__.py +0 -0
  72. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/__main__.py +0 -0
  73. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/changelog_generator.py +0 -0
  74. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/code_reviewer.py +0 -0
  75. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/commit_generator.py +0 -0
  76. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/conflict_resolver.py +0 -0
  77. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/gitignore_generator.py +0 -0
  78. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/history_analyzer.py +0 -0
  79. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/intent_parser.py +0 -0
  80. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/llm_factory.py +0 -0
  81. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/pr_drafter.py +0 -0
  82. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/changelog.py +0 -0
  83. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/commit.py +0 -0
  84. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/conflict.py +0 -0
  85. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/doctor.py +0 -0
  86. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/explain.py +0 -0
  87. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/ignore.py +0 -0
  88. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/intent.py +0 -0
  89. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/pr.py +0 -0
  90. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/rebase.py +0 -0
  91. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/review.py +0 -0
  92. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/search.py +0 -0
  93. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/undo.py +0 -0
  94. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/rebase_helper.py +0 -0
  95. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/cli.py +0 -0
  96. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/core/config.py +0 -0
  97. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/core/context.py +0 -0
  98. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/core/diagnostics.py +0 -0
  99. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/core/git_ops.py +0 -0
  100. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/core/hooks.py +0 -0
  101. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/core/safety.py +0 -0
  102. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ui/banner.py +0 -0
  103. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/utils/conflict_parser.py +0 -0
  104. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/utils/diff_parser.py +0 -0
  105. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/utils/json_utils.py +0 -0
  106. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/importtime.txt +0 -0
  107. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/importtime_optimized.txt +0 -0
  108. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/conftest.py +0 -0
  109. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_changelog_generator.py +0 -0
  110. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_code_reviewer.py +0 -0
  111. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_conflict_resolver.py +0 -0
  112. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_diagnostics.py +0 -0
  113. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_diff_trimmer.py +0 -0
  114. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_git_ops.py +0 -0
  115. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_help.py +0 -0
  116. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_history_analyzer.py +0 -0
  117. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_hooks.py +0 -0
  118. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_ignore.py +0 -0
  119. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_intent_parser.py +0 -0
  120. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_llm_factory.py +0 -0
  121. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_pr_drafter.py +0 -0
  122. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_rebase_helper.py +0 -0
  123. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_safety.py +0 -0
  124. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_search.py +0 -0
  125. {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/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.2.9
3
+ Version: 0.3.0
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
@@ -0,0 +1,378 @@
1
+ import click
2
+ import typer
3
+ from pathlib import Path
4
+ from rich.panel import Panel
5
+ from rich.table import Table
6
+ from rich.columns import Columns
7
+ from rich.text import Text
8
+ from rich import box
9
+ from ace.ui.display import console, spinner, show_warning_panel, print_success, print_warning
10
+ from ace.core.git_ops import GitOps
11
+
12
+
13
+ # ─── Helpers ─────────────────────────────────────────────────────────────────
14
+
15
+ def _branch_label(branch: str) -> Text:
16
+ """Render the current branch name with a small indicator."""
17
+ return Text.assemble((" ", ""), (branch, "bold #00E676"))
18
+
19
+
20
+ def _sync_label(ahead: int, behind: int) -> Text:
21
+ if not ahead and not behind:
22
+ return Text("Up to date", style="#00E676")
23
+ parts: list = []
24
+ if ahead:
25
+ parts.append((f"+{ahead} ahead", "bold #00E676"))
26
+ if ahead and behind:
27
+ parts.append((" / ", "dim #555555"))
28
+ if behind:
29
+ parts.append((f"-{behind} behind", "bold #FF1744"))
30
+ return Text.assemble(*parts)
31
+
32
+
33
+ def _file_count_label(n: int, colour: str) -> Text:
34
+ if n == 0:
35
+ return Text("none", style="dim #666666")
36
+ return Text(str(n), style=f"bold {colour}")
37
+
38
+
39
+ # ─── Dashboard entry point ───────────────────────────────────────────────────
40
+
41
+ def show_dashboard(git_ops: GitOps, offline: bool = False):
42
+ """
43
+ Renders an interactive terminal dashboard displaying repository state,
44
+ workspace changes, and a menu to quickly run Ace operations.
45
+ """
46
+ from ace.ui.banner import animate_fire_banner, get_fire_banner_static
47
+
48
+ click.clear()
49
+ try:
50
+ animate_fire_banner(duration_seconds=1.2)
51
+ except Exception:
52
+ pass
53
+
54
+ while True:
55
+ click.clear()
56
+
57
+ # ── Header ──────────────────────────────────────────────────────────
58
+ console.print(get_fire_banner_static())
59
+ console.print(
60
+ Text.assemble(
61
+ (" Ace", "bold #FF6D00"),
62
+ (" AI Git Copilot", "bold white"),
63
+ (" · Interactive Dashboard", "dim #9E9E9E"),
64
+ )
65
+ )
66
+ console.print()
67
+
68
+ # ── Fetch repo state ────────────────────────────────────────────────
69
+ try:
70
+ current_branch = git_ops.get_current_branch() or "Detached HEAD"
71
+ tracking = git_ops.get_upstream_tracking() or "No remote tracking"
72
+ ab = git_ops.get_ahead_behind()
73
+ status = git_ops.get_status()
74
+ commits = git_ops.get_log(n=5)
75
+ except Exception as e:
76
+ console.print(f"[error]Failed to read repository: {e}[/error]")
77
+ break
78
+
79
+ # ── Status panel ────────────────────────────────────────────────────
80
+ st = Table.grid(padding=(0, 2))
81
+ st.add_column(style="label", justify="right", min_width=12)
82
+ st.add_column()
83
+ st.add_row("Branch", _branch_label(current_branch))
84
+ st.add_row("Remote", Text(tracking, style="#9E9E9E"))
85
+ st.add_row("Sync", _sync_label(ab["ahead"], ab["behind"]))
86
+ status_panel = Panel(
87
+ st,
88
+ title="[bold white]Repository[/bold white]",
89
+ border_style="#00D5FF",
90
+ box=box.ROUNDED,
91
+ expand=False,
92
+ padding=(0, 1),
93
+ )
94
+
95
+ # ── Changes panel ───────────────────────────────────────────────────
96
+ ct = Table.grid(padding=(0, 2))
97
+ ct.add_column(style="label", justify="right", min_width=12)
98
+ ct.add_column()
99
+ ct.add_row("Staged", _file_count_label(len(status["staged"]), "#00E676"))
100
+ ct.add_row("Unstaged", _file_count_label(len(status["unstaged"]), "#FFD600"))
101
+ ct.add_row("Untracked", _file_count_label(len(status["untracked"]), "#9E9E9E"))
102
+ changes_panel = Panel(
103
+ ct,
104
+ title="[bold white]Workspace[/bold white]",
105
+ border_style="#FFD600",
106
+ box=box.ROUNDED,
107
+ expand=False,
108
+ padding=(0, 1),
109
+ )
110
+
111
+ # ── Sibling repos panel ─────────────────────────────────────────────
112
+ parent_dir = Path(git_ops.working_dir).parent
113
+ sibling_repos: list[str] = []
114
+ try:
115
+ for p in parent_dir.iterdir():
116
+ if p.is_dir() and p != Path(git_ops.working_dir) and (p / ".git").exists():
117
+ sibling_repos.append(p.name)
118
+ except Exception:
119
+ pass
120
+
121
+ sibling_panel = None
122
+ if sibling_repos:
123
+ rt = Table.grid(padding=(0, 2))
124
+ rt.add_column(style="bold #B388FF", min_width=14)
125
+ rt.add_column(style="dim #9E9E9E")
126
+ for r_name in sibling_repos[:5]:
127
+ sib_branch = "?"
128
+ try:
129
+ import git as _git
130
+ sib_branch = _git.Repo(parent_dir / r_name).active_branch.name
131
+ except Exception:
132
+ pass
133
+ rt.add_row(r_name, sib_branch)
134
+ if len(sibling_repos) > 5:
135
+ rt.add_row(f" +{len(sibling_repos) - 5} more", "")
136
+ sibling_panel = Panel(
137
+ rt,
138
+ title="[bold white]Workspace Repos[/bold white]",
139
+ border_style="#B388FF",
140
+ box=box.ROUNDED,
141
+ expand=False,
142
+ padding=(0, 1),
143
+ )
144
+
145
+ panels = [status_panel, changes_panel]
146
+ if sibling_panel:
147
+ panels.append(sibling_panel)
148
+ console.print(Columns(panels))
149
+ console.print()
150
+
151
+ # ── Staged / Unstaged file lists ────────────────────────────────────
152
+ if status["staged"]:
153
+ t = Table(show_header=False, box=None, padding=(0, 2))
154
+ t.add_column()
155
+ for f in status["staged"]:
156
+ t.add_row(Text.assemble(("+ ", "bold #00E676"), (f, "#BDBDBD")))
157
+ console.print(Panel(t, title="[bold #00E676]Staged[/bold #00E676]",
158
+ border_style="#00E676", box=box.SIMPLE, expand=False))
159
+ console.print()
160
+
161
+ if status["unstaged"]:
162
+ t = Table(show_header=False, box=None, padding=(0, 2))
163
+ t.add_column()
164
+ for f in status["unstaged"]:
165
+ t.add_row(Text.assemble(("~ ", "bold #FFD600"), (f, "#BDBDBD")))
166
+ console.print(Panel(t, title="[bold #FFD600]Unstaged[/bold #FFD600]",
167
+ border_style="#FFD600", box=box.SIMPLE, expand=False))
168
+ console.print()
169
+
170
+ if status["untracked"]:
171
+ t = Table(show_header=False, box=None, padding=(0, 2))
172
+ t.add_column()
173
+ for f in status["untracked"]:
174
+ t.add_row(Text.assemble(("? ", "dim #9E9E9E"), (f, "dim #9E9E9E")))
175
+ console.print(Panel(t, title="[dim]Untracked[/dim]",
176
+ border_style="#555555", box=box.SIMPLE, expand=False))
177
+ console.print()
178
+
179
+ # ── Recent commits ──────────────────────────────────────────────────
180
+ if commits:
181
+ commit_table = Table(
182
+ show_header=True,
183
+ header_style="bold #9E9E9E",
184
+ box=box.SIMPLE_HEAD,
185
+ show_edge=False,
186
+ padding=(0, 2),
187
+ )
188
+ commit_table.add_column("Hash", style="#666666", width=8, no_wrap=True)
189
+ commit_table.add_column("Message", style="white", ratio=4)
190
+ commit_table.add_column("Author", style="#B388FF", ratio=2)
191
+ for c in commits:
192
+ commit_table.add_row(c["hexsha"][:7], c["summary"], c["author"])
193
+ console.print(
194
+ Panel(commit_table, title="[bold white]Recent Commits[/bold white]",
195
+ border_style="#444444", box=box.ROUNDED, expand=False)
196
+ )
197
+ else:
198
+ console.print("[dim] No commit history yet.[/dim]")
199
+ console.print()
200
+
201
+ # ── Action menu ─────────────────────────────────────────────────────
202
+ menu = Table(show_header=False, box=None, padding=(0, 3), expand=False)
203
+ menu.add_column(style="bold #00D5FF", justify="right", width=4)
204
+ menu.add_column(style="#BDBDBD", width=20)
205
+ menu.add_column(style="bold #00D5FF", justify="right", width=4)
206
+ menu.add_column(style="#BDBDBD")
207
+
208
+ menu.add_row("[c]", "AI Commit", "[r]", "AI Code Review")
209
+ menu.add_row("[u]", "Smart Undo", "[p]", "Plan Command (AI)")
210
+ menu.add_row("[s]", "Repo Stats", "[w]", "Switch Repo")
211
+ menu.add_row("[q]", "Quit", "", "")
212
+
213
+ console.print(
214
+ Panel(menu, title="[bold white]Actions[/bold white]",
215
+ border_style="#FF6D00", box=box.ROUNDED, expand=False)
216
+ )
217
+ console.print()
218
+ console.print("[bold #FF6D00] Press a key ...[/bold #FF6D00] ", end="")
219
+
220
+ # ── Key input ───────────────────────────────────────────────────────
221
+ while True:
222
+ choice = click.getchar().lower().strip()
223
+ if choice in ("\r", "\n", ""):
224
+ choice = "q"
225
+ break
226
+ if choice in ("c", "r", "u", "p", "s", "w", "q"):
227
+ break
228
+
229
+ console.print(f"[dim]{choice}[/dim]")
230
+ console.print()
231
+
232
+ # ── Handle choice ───────────────────────────────────────────────────
233
+ if choice == "q":
234
+ console.print("[dim] Exiting dashboard.[/dim]")
235
+ break
236
+
237
+ elif choice == "c":
238
+ from ace.cli import commit_cmd
239
+ try:
240
+ commit_cmd(offline=offline)
241
+ except Exception as e:
242
+ console.print(f"[error] Error: {e}[/error]")
243
+
244
+ elif choice == "r":
245
+ from ace.cli import review_cmd
246
+ try:
247
+ review_cmd(all_changes=True, offline=offline)
248
+ except Exception as e:
249
+ console.print(f"[error] Error: {e}[/error]")
250
+
251
+ elif choice == "u":
252
+ from ace.cli import undo_cmd
253
+ try:
254
+ undo_cmd(offline=offline)
255
+ except Exception as e:
256
+ console.print(f"[error] Error: {e}[/error]")
257
+
258
+ elif choice == "s":
259
+ from ace.cli import stats_cmd
260
+ try:
261
+ stats_cmd()
262
+ except Exception as e:
263
+ console.print(f"[error] Error: {e}[/error]")
264
+
265
+ elif choice == "w":
266
+ _handle_switch_repo(git_ops, parent_dir)
267
+
268
+ elif choice == "p":
269
+ _handle_plan_command(git_ops, offline)
270
+
271
+ console.print()
272
+ console.print("[dim] Press any key to return ...[/dim] ", end="")
273
+ click.getchar()
274
+
275
+
276
+ # ─── Action handlers ──────────────────────────────────────────────────────────
277
+
278
+ def _handle_switch_repo(git_ops: GitOps, parent_dir: Path) -> None:
279
+ """Interactive repository switcher."""
280
+ from ace.ui.prompts import prompt_select
281
+
282
+ all_repos: list[str] = []
283
+ try:
284
+ all_repos = sorted(
285
+ p.name for p in parent_dir.iterdir()
286
+ if p.is_dir() and (p / ".git").exists()
287
+ )
288
+ except Exception:
289
+ pass
290
+
291
+ if not all_repos:
292
+ print_warning("No other repositories found in the parent directory.")
293
+ return
294
+
295
+ current_name = Path(git_ops.working_dir).name
296
+ display_options = [
297
+ f"{name} [bold #00E676](current)[/bold #00E676]" if name == current_name else name
298
+ for name in all_repos
299
+ ]
300
+
301
+ console.print("[bold white] Repositories in workspace:[/bold white]")
302
+ sel_idx = prompt_select(display_options, prompt_text=" Repository number", default="s")
303
+ if sel_idx < 0:
304
+ console.print("[dim] Switch cancelled.[/dim]")
305
+ return
306
+
307
+ selected = all_repos[sel_idx]
308
+ new_path = parent_dir / selected
309
+ try:
310
+ from ace.core.git_ops import GitOps as _GitOps
311
+ git_ops.__dict__.update(_GitOps(str(new_path)).__dict__)
312
+ print_success(f"Switched to {selected}")
313
+ except Exception as e:
314
+ console.print(f"[error] Failed to switch: {e}[/error]")
315
+
316
+
317
+ def _handle_plan_command(git_ops: GitOps, offline: bool) -> None:
318
+ """AI natural-language command planner."""
319
+ from ace.ai.intent_parser import IntentParser
320
+ from ace.core.safety import SafetyChecker
321
+ from ace.ui.display import show_plan
322
+ from ace.ui.prompts import confirm as ui_confirm
323
+
324
+ query = typer.prompt(" What do you want to do with Git?")
325
+ if not query.strip():
326
+ return
327
+
328
+ parser = IntentParser(git_ops)
329
+ try:
330
+ with spinner("Planning commands..."):
331
+ parsed = parser.parse_intent(query, offline=offline)
332
+
333
+ commands = parsed.get("commands", [])
334
+ explanation = parsed.get("explanation", "")
335
+
336
+ if not commands:
337
+ console.print(f"[dim] No commands planned.[/dim] {explanation}")
338
+ return
339
+
340
+ show_plan(commands, [explanation] + [""] * (len(commands) - 1))
341
+
342
+ # Safety classification
343
+ highest_risk = "safe"
344
+ risk_details: list[str] = []
345
+ for cmd in commands:
346
+ r_level, r_desc, _ = SafetyChecker.analyze_command(cmd)
347
+ if r_level == "destructive":
348
+ highest_risk = "destructive"
349
+ risk_details.append(f"[bold]{cmd}[/bold]\n{r_desc}")
350
+ elif r_level == "moderate" and highest_risk != "destructive":
351
+ highest_risk = "moderate"
352
+
353
+ execute = True
354
+ if highest_risk == "destructive":
355
+ show_warning_panel("\n\n".join(risk_details), "Destructive Operation Detected")
356
+ execute = ui_confirm("Execute these destructive commands?", default=False)
357
+ elif highest_risk == "moderate":
358
+ execute = ui_confirm("Execute this plan?", default=True)
359
+
360
+ if execute:
361
+ for cmd in commands:
362
+ console.print(
363
+ Text.assemble(
364
+ (" › ", "bold #00D5FF"),
365
+ ("Running ", "#9E9E9E"),
366
+ (cmd, "bold white"),
367
+ )
368
+ )
369
+ git_args = cmd[4:] if cmd.startswith("git ") else cmd
370
+ result = git_ops.execute(git_args)
371
+ if result.strip():
372
+ console.print(f"[dim]{result}[/dim]")
373
+ print_success("Plan executed successfully.")
374
+ else:
375
+ console.print("[dim] Plan aborted.[/dim]")
376
+
377
+ except Exception as e:
378
+ console.print(f"[error] Error: {e}[/error]")
@@ -0,0 +1,256 @@
1
+ import sys
2
+ from contextlib import contextmanager
3
+ from typing import List, Dict, Any
4
+ from rich.console import Console
5
+ from rich.text import Text
6
+ from ace.ui.themes import get_rich_theme
7
+
8
+ # Initialize global Rich console (force_terminal ensures colour/Unicode on Windows)
9
+ console = Console(theme=get_rich_theme(), force_terminal=True)
10
+ err_console = Console(theme=get_rich_theme(), force_terminal=True, stderr=True)
11
+
12
+ # Status symbols — safe subset that renders in all modern Windows terminals
13
+ _SYM_INFO = ">>" # informational
14
+ _SYM_SUCCESS = "**" # success
15
+ _SYM_WARNING = "!!" # warning
16
+ _SYM_ERROR = "EE" # error
17
+
18
+
19
+ # ─── Inline status printers ──────────────────────────────────────────────────
20
+
21
+ def print_info(message: str) -> None:
22
+ """Print an informational message."""
23
+ console.print(f" [info]{_SYM_INFO}[/info] {message}")
24
+
25
+ def print_success(message: str) -> None:
26
+ """Print a success message."""
27
+ console.print(f" [success]{_SYM_SUCCESS}[/success] {message}")
28
+
29
+ def print_warning(message: str) -> None:
30
+ """Print a warning message."""
31
+ console.print(f" [warning]{_SYM_WARNING}[/warning] [warning]{message}[/warning]")
32
+
33
+ def print_error(message: str) -> None:
34
+ """Print an error message."""
35
+ err_console.print(f" [error]{_SYM_ERROR}[/error] [error]{message}[/error]")
36
+
37
+
38
+ # ─── Panels ──────────────────────────────────────────────────────────────────
39
+
40
+ def show_warning_panel(message: str, title: str = "Warning") -> None:
41
+ """Show a styled amber warning panel."""
42
+ from rich.panel import Panel
43
+ from rich import box
44
+ panel = Panel(
45
+ Text.from_markup(message),
46
+ title=f"[bold #FFD600] ! {title}[/bold #FFD600]",
47
+ border_style="#FFD600",
48
+ box=box.ROUNDED,
49
+ expand=False,
50
+ padding=(0, 2),
51
+ )
52
+ console.print()
53
+ console.print(panel)
54
+ console.print()
55
+
56
+ def show_error_panel(message: str, title: str = "Error") -> None:
57
+ """Show a styled red error panel."""
58
+ from rich.panel import Panel
59
+ from rich import box
60
+ panel = Panel(
61
+ Text.from_markup(message),
62
+ title=f"[bold #FF1744] {_SYM_ERROR} {title}[/bold #FF1744]",
63
+ border_style="#FF1744",
64
+ box=box.ROUNDED,
65
+ expand=False,
66
+ padding=(0, 2),
67
+ )
68
+ console.print()
69
+ console.print(panel)
70
+ console.print()
71
+
72
+
73
+ # ─── Spinner ─────────────────────────────────────────────────────────────────
74
+
75
+ @contextmanager
76
+ def spinner(message: str = "Thinking..."):
77
+ """Context manager to display a loading spinner."""
78
+ with console.status(f"[ai]{message}[/ai]", spinner="dots") as status:
79
+ yield status
80
+
81
+
82
+ # ─── Execution plan ──────────────────────────────────────────────────────────
83
+
84
+ def show_plan(commands: List[str], explanations: List[str]) -> None:
85
+ """Display the AI execution plan as a clean numbered table."""
86
+ from rich.table import Table
87
+ from rich import box
88
+
89
+ table = Table(
90
+ show_header=True,
91
+ header_style="bold #9E9E9E",
92
+ box=box.SIMPLE_HEAD,
93
+ border_style="#FF6D00",
94
+ title="[bold white]Proposed Execution Plan[/bold white]",
95
+ title_justify="left",
96
+ expand=False,
97
+ padding=(0, 2),
98
+ show_edge=False,
99
+ )
100
+ table.add_column("#", justify="right", style="bold #666666", width=4)
101
+ table.add_column("Command", style="bold white", width=32)
102
+ table.add_column("What it does", style="#BDBDBD")
103
+
104
+ for i, (cmd, exp) in enumerate(zip(commands, explanations), 1):
105
+ # Colour-highlight the "git" or "ace" prefix
106
+ if cmd.startswith("git "):
107
+ cmd_text = Text("git ", style="bold #00D5FF") + Text(cmd[4:], style="bold white")
108
+ elif cmd.startswith("ace "):
109
+ cmd_text = Text("ace ", style="bold #FF6D00") + Text(cmd[4:], style="bold white")
110
+ else:
111
+ cmd_text = Text(cmd, style="bold white")
112
+
113
+ table.add_row(str(i), cmd_text, exp)
114
+
115
+ console.print()
116
+ console.print(table)
117
+ console.print()
118
+
119
+
120
+ # ─── Commit message ──────────────────────────────────────────────────────────
121
+
122
+ def show_commit_message(message: str) -> None:
123
+ """Display a suggested commit message in a styled panel."""
124
+ import re
125
+ from rich.panel import Panel
126
+ from rich import box
127
+
128
+ lines = message.splitlines()
129
+ subject = lines[0] if lines else ""
130
+ body = "\n".join(lines[1:]) if len(lines) > 1 else ""
131
+
132
+ text = Text()
133
+ conv_match = re.match(r"^(\w+)(?:\(([^)]+)\))?(!?):(.*)$", subject)
134
+ if conv_match:
135
+ c_type, c_scope, c_breaking, c_desc = conv_match.groups()
136
+ text.append(c_type, style="bold #00D5FF")
137
+ if c_scope:
138
+ text.append(f"({c_scope})", style="bold #B388FF")
139
+ if c_breaking:
140
+ text.append("!", style="bold #FF1744")
141
+ text.append(f":{c_desc}", style="bold #00E676")
142
+ else:
143
+ text.append(subject, style="bold #00E676")
144
+
145
+ if body:
146
+ text.append("\n" + body, style="#BDBDBD")
147
+
148
+ # Character-count indicator in subtitle
149
+ sub_len = len(subject)
150
+ if sub_len <= 50:
151
+ count_color = "#00E676"
152
+ elif sub_len <= 72:
153
+ count_color = "#FFD600"
154
+ else:
155
+ count_color = "#FF1744"
156
+
157
+ subtitle = (
158
+ f"[#666666]chars:[/#666666] "
159
+ f"[bold {count_color}]{sub_len}[/bold {count_color}]"
160
+ f"[#666666]/72[/#666666]"
161
+ )
162
+
163
+ panel = Panel(
164
+ text,
165
+ title="[bold white]Suggested Commit[/bold white]",
166
+ subtitle=subtitle,
167
+ subtitle_align="right",
168
+ border_style="#FF6D00",
169
+ box=box.ROUNDED,
170
+ expand=False,
171
+ padding=(1, 2),
172
+ )
173
+ console.print()
174
+ console.print(panel)
175
+ console.print()
176
+
177
+
178
+ # ─── Diff renderer ───────────────────────────────────────────────────────────
179
+
180
+ def show_diff(diff_text: str) -> None:
181
+ """Render a syntax-highlighted git diff."""
182
+ from rich.syntax import Syntax
183
+ if not diff_text.strip():
184
+ console.print("[muted] No changes to display.[/muted]")
185
+ return
186
+ syntax = Syntax(diff_text, "diff", theme="ansi_dark", word_wrap=True)
187
+ console.print(syntax)
188
+
189
+
190
+ # ─── Code review results ─────────────────────────────────────────────────────
191
+
192
+ def show_review(findings: List[Dict[str, Any]], score: float) -> None:
193
+ """Display aggregated AI code review findings."""
194
+ from rich.syntax import Syntax
195
+ from rich.panel import Panel
196
+ from rich import box
197
+
198
+ # Score badge
199
+ if score >= 8:
200
+ score_style = "bold #00E676"
201
+ elif score >= 5:
202
+ score_style = "bold #FFD600"
203
+ else:
204
+ score_style = "bold #FF1744"
205
+
206
+ console.print()
207
+ console.print(
208
+ Text.assemble(
209
+ (" AI Code Review ", "bold white on #1A237E"),
210
+ (" Score: ", "bold #9E9E9E"),
211
+ (f"{score}/10", score_style),
212
+ )
213
+ )
214
+ console.print()
215
+
216
+ if not findings:
217
+ print_success("No issues found — clean code!")
218
+ return
219
+
220
+ sev_sym = {
221
+ "critical": ("EE", "bold #FF1744"),
222
+ "warning": ("!!", "bold #FFD600"),
223
+ "info": (">>", "bold #00D5FF"),
224
+ }
225
+
226
+ for item in findings:
227
+ sev = item.get("severity", "info").lower()
228
+ sym, sym_style = sev_sym.get(sev, (">>", "bold #00D5FF"))
229
+
230
+ loc = f"{item.get('file', '?')}:{item.get('line', '?')}"
231
+ cat = item.get("category", "issue").upper()
232
+ desc = item.get("description", "")
233
+ fix = item.get("fix", "")
234
+
235
+ # Header row
236
+ console.print(
237
+ Text.assemble(
238
+ (f" {sym} ", sym_style),
239
+ (f"{cat} ", "bold white"),
240
+ (loc, "underline #00D5FF"),
241
+ )
242
+ )
243
+ console.print(f" [#BDBDBD]{desc}[/#BDBDBD]")
244
+
245
+ if fix:
246
+ console.print(" [#666666]Suggested fix:[/#666666]")
247
+ syntax = Syntax(
248
+ fix, "python",
249
+ theme="ansi_dark",
250
+ indent_guides=False,
251
+ word_wrap=True,
252
+ )
253
+ console.print(
254
+ Panel(syntax, border_style="#444444", box=box.SIMPLE, expand=False, padding=(0, 1))
255
+ )
256
+ console.print()