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.
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/PKG-INFO +1 -1
- ace_git_copilot-0.3.0/ace/ui/dashboard.py +378 -0
- ace_git_copilot-0.3.0/ace/ui/display.py +256 -0
- ace_git_copilot-0.3.0/ace/ui/prompts.py +103 -0
- ace_git_copilot-0.3.0/ace/ui/themes.py +33 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/pyproject.toml +7 -1
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/e2e/conftest.py +0 -1
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/e2e/test_tier1_features.py +0 -3
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/e2e/test_tier2_boundaries.py +0 -1
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/e2e/test_tier3_combinations.py +0 -3
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/e2e/test_tier4_workloads.py +0 -3
- ace_git_copilot-0.2.9/ace/ui/dashboard.py +0 -294
- ace_git_copilot-0.2.9/ace/ui/display.py +0 -180
- ace_git_copilot-0.2.9/ace/ui/prompts.py +0 -89
- ace_git_copilot-0.2.9/ace/ui/themes.py +0 -24
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/AGENTS.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/BRIEFING.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/e2e_testing_track/BRIEFING.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/e2e_testing_track/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/e2e_testing_track/SCOPE.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/e2e_testing_track/progress.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/BRIEFING.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/emojis_list.txt +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/find_unused_modules.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/handoff.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/measure_lazy_startup.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/measure_startup.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/profile_imports.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/progress.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/run_importtime.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/search_banner.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/search_emojis.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/search_git_usages.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/search_usages.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/test_import_profiler.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/explorer_init/test_mocked_sys.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/handoff.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/implementation_track/BRIEFING.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/implementation_track/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/implementation_track/explorer_initial_report.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/implementation_track/progress.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/orchestrator/.gitkeep +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/orchestrator/BRIEFING.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/orchestrator/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/orchestrator/PROJECT.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/orchestrator/progress.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/teamwork_preview_explorer_e2e_explore/BRIEFING.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/teamwork_preview_explorer_e2e_explore/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/teamwork_preview_explorer_e2e_explore/handoff.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/teamwork_preview_explorer_e2e_explore/progress.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/worker_e2e_testing/BRIEFING.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/worker_e2e_testing/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/worker_e2e_testing/progress.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/worker_m1_startup/BRIEFING.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/worker_m1_startup/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.agents/worker_m1_startup/progress.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.env.example +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.github/workflows/tests.yml +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/.gitignore +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/CONTRIBUTING.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/LICENSE +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/PROJECT.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/README.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/SECURITY.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/SUPPORT.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/TEST_INFRA.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/TEST_READY.md +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/__init__.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/__main__.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/changelog_generator.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/code_reviewer.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/commit_generator.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/conflict_resolver.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/gitignore_generator.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/history_analyzer.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/intent_parser.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/llm_factory.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/pr_drafter.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/changelog.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/commit.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/conflict.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/doctor.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/explain.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/ignore.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/intent.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/pr.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/rebase.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/review.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/search.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/prompts/undo.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ai/rebase_helper.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/cli.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/core/config.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/core/context.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/core/diagnostics.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/core/git_ops.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/core/hooks.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/core/safety.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/ui/banner.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/utils/conflict_parser.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/utils/diff_parser.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/ace/utils/json_utils.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/importtime.txt +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/importtime_optimized.txt +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/conftest.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_changelog_generator.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_code_reviewer.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_conflict_resolver.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_diagnostics.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_diff_trimmer.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_git_ops.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_help.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_history_analyzer.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_hooks.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_ignore.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_intent_parser.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_llm_factory.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_pr_drafter.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_rebase_helper.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_safety.py +0 -0
- {ace_git_copilot-0.2.9 → ace_git_copilot-0.3.0}/tests/test_search.py +0 -0
- {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.
|
|
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()
|