ezshell 0.2.0__py3-none-any.whl
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.
- ezshell-0.2.0.dist-info/METADATA +174 -0
- ezshell-0.2.0.dist-info/RECORD +37 -0
- ezshell-0.2.0.dist-info/WHEEL +5 -0
- ezshell-0.2.0.dist-info/entry_points.txt +3 -0
- ezshell-0.2.0.dist-info/licenses/LICENSE +21 -0
- ezshell-0.2.0.dist-info/top_level.txt +1 -0
- src/__init__.py +3 -0
- src/app.py +286 -0
- src/core/__init__.py +0 -0
- src/core/assistant.py +525 -0
- src/core/capture.py +213 -0
- src/core/chat_bridge.py +107 -0
- src/core/config.py +238 -0
- src/core/context.py +229 -0
- src/core/groq_client.py +376 -0
- src/core/llm_client.py +111 -0
- src/core/prompts.py +262 -0
- src/core/provider.py +233 -0
- src/core/proxy_client.py +166 -0
- src/core/pty_shell.py +273 -0
- src/core/redact.py +95 -0
- src/core/risk.py +455 -0
- src/core/safety.py +268 -0
- src/core/sanitize.py +39 -0
- src/core/schema.py +65 -0
- src/core/shell_hook.py +388 -0
- src/core/suggest.py +94 -0
- src/core/updater.py +428 -0
- src/i18n.py +422 -0
- src/main.py +346 -0
- src/ui/chat_tui.py +634 -0
- src/ui/gui_window.py +1018 -0
- src/ui/inline.py +330 -0
- src/ui/logo.py +194 -0
- src/ui/panel.py +163 -0
- src/ui/prompt.py +184 -0
- src/ui/render.py +269 -0
src/ui/gui_window.py
ADDED
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import threading
|
|
6
|
+
import warnings
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
# Suppress spurious CustomTkinter HighDPI warning when using native tk.PhotoImage
|
|
11
|
+
warnings.filterwarnings("ignore", message=".*Given image is not CTkImage.*")
|
|
12
|
+
|
|
13
|
+
import customtkinter as ctk
|
|
14
|
+
from PIL import Image, ImageTk
|
|
15
|
+
|
|
16
|
+
from src.core import chat_bridge
|
|
17
|
+
from src.core.assistant import ChatMessageResult
|
|
18
|
+
from src.core.config import EzShellConfig
|
|
19
|
+
from src.core.context import SystemContext, get_last_exit_code, get_last_shell_command
|
|
20
|
+
from src.core.provider import get_provider
|
|
21
|
+
from src.core.pty_shell import PTYExecutor
|
|
22
|
+
from src.core.safety import RiskLevel
|
|
23
|
+
from src.i18n import t
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def clean_markdown_explanation(text: str) -> str:
|
|
27
|
+
"""Cleans raw markdown formatting from AI text for a clean, human-readable desktop UI."""
|
|
28
|
+
if not text:
|
|
29
|
+
return ""
|
|
30
|
+
# Strip full fenced code blocks (```bash ... ```)
|
|
31
|
+
cleaned = re.sub(r"```[a-zA-Z]*\n[\s\S]*?\n```", "", text)
|
|
32
|
+
# Strip inline backticks `command` -> command
|
|
33
|
+
cleaned = re.sub(r"`(.*?)`", r"\1", cleaned)
|
|
34
|
+
# Strip bold asterisks **word** -> word
|
|
35
|
+
cleaned = re.sub(r"\*\*(.*?)\*\*", r"\1", cleaned)
|
|
36
|
+
# Strip repetitive section headers
|
|
37
|
+
cleaned = re.sub(r"(?im)^\s*Command\s*:?\s*$", "", cleaned)
|
|
38
|
+
cleaned = re.sub(r"(?im)^\s*What it does\s*:?\s*$", "", cleaned)
|
|
39
|
+
cleaned = re.sub(r"(?im)^\s*Explication\s*:?\s*$", "", cleaned)
|
|
40
|
+
cleaned = re.sub(r"(?im)^\s*Solution\s*:?\s*$", "", cleaned)
|
|
41
|
+
# Format bullet points nicely
|
|
42
|
+
lines = []
|
|
43
|
+
for line in cleaned.splitlines():
|
|
44
|
+
s = line.strip()
|
|
45
|
+
if not s:
|
|
46
|
+
continue
|
|
47
|
+
if s.startswith("- ") or s.startswith("* "):
|
|
48
|
+
lines.append(" • " + s[2:].strip())
|
|
49
|
+
else:
|
|
50
|
+
lines.append(s)
|
|
51
|
+
return "\n".join(lines).strip()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class EzShellDesktopApp(ctk.CTk):
|
|
55
|
+
"""Modern, clean desktop GUI popup window for ezShell AI Assistant.
|
|
56
|
+
|
|
57
|
+
Theme: Refined developer palette (Royal Blue accent, deep slate background).
|
|
58
|
+
NO cyberpunk styling, NO electric cyan.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# Typography & Font Families
|
|
62
|
+
FONT_FAMILY_UI = "Inter"
|
|
63
|
+
FONT_FAMILY_TITLE = "Inter Display"
|
|
64
|
+
FONT_FAMILY_CODE = "JetBrains Mono"
|
|
65
|
+
|
|
66
|
+
DEFAULT_COLUMNS = 45
|
|
67
|
+
DEFAULT_ROWS = 15
|
|
68
|
+
|
|
69
|
+
FONT_SIZE_APP_TITLE = 18
|
|
70
|
+
FONT_SIZE_QUERY_CHEVRON = 16
|
|
71
|
+
FONT_SIZE_QUERY_TEXT = 14
|
|
72
|
+
FONT_SIZE_SECTION_HEADER = 11
|
|
73
|
+
FONT_SIZE_CAUSE_BODY = 14
|
|
74
|
+
FONT_SIZE_FIX_COMMAND = 14
|
|
75
|
+
FONT_SIZE_RISK_BADGE = 12
|
|
76
|
+
FONT_SIZE_INPUT_ENTRY = 13
|
|
77
|
+
FONT_SIZE_PILL_KEY = 11
|
|
78
|
+
FONT_SIZE_PILL_LABEL = 11
|
|
79
|
+
FONT_SIZE_EXEC_OUTPUT = 12
|
|
80
|
+
|
|
81
|
+
# Quiet charcoal palette with one restrained mint accent.
|
|
82
|
+
COLOR_PRIMARY_BLUE = "#2DD4BF"
|
|
83
|
+
COLOR_BLUE_HOVER = "#14B8A6"
|
|
84
|
+
COLOR_LIGHT_BLUE = "#99F6E4"
|
|
85
|
+
COLOR_BG_WINDOW = "#111315"
|
|
86
|
+
COLOR_BG_CARD = "#191C1F"
|
|
87
|
+
COLOR_BG_CARD_HOVER = "#23282B"
|
|
88
|
+
COLOR_BG_CODE = "#0C0E0F"
|
|
89
|
+
COLOR_BG_INPUT = "#171A1C"
|
|
90
|
+
COLOR_BORDER = "#2A3033"
|
|
91
|
+
COLOR_BORDER_FOCUS = "#2DD4BF"
|
|
92
|
+
COLOR_TEXT_PRIMARY = "#F4F1EA"
|
|
93
|
+
COLOR_TEXT_MUTED = "#A6AFB0"
|
|
94
|
+
COLOR_TEXT_SUBTLE = "#748082"
|
|
95
|
+
COLOR_SUCCESS = "#34D399"
|
|
96
|
+
COLOR_WARNING = "#FBBF24"
|
|
97
|
+
COLOR_DANGER = "#FB7185"
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
config: Optional[EzShellConfig] = None,
|
|
102
|
+
initial_query: Optional[str] = None,
|
|
103
|
+
error_context: Optional[str] = None,
|
|
104
|
+
):
|
|
105
|
+
ctk.set_appearance_mode("dark")
|
|
106
|
+
ctk.set_default_color_theme("blue")
|
|
107
|
+
|
|
108
|
+
super().__init__()
|
|
109
|
+
|
|
110
|
+
self.config = config or EzShellConfig.load()
|
|
111
|
+
self.context = SystemContext.collect()
|
|
112
|
+
self.provider = get_provider(self.config)
|
|
113
|
+
|
|
114
|
+
# Window properties
|
|
115
|
+
self.title("ezShell")
|
|
116
|
+
self.configure(fg_color=self.COLOR_BG_WINDOW)
|
|
117
|
+
|
|
118
|
+
# Taille par défaut en cellules de terminal (45 colonnes x 15 lignes), comme un émulateur
|
|
119
|
+
win_w, win_h = self._cell_size_px(self.DEFAULT_COLUMNS, self.DEFAULT_ROWS)
|
|
120
|
+
try:
|
|
121
|
+
sw = self.winfo_screenwidth()
|
|
122
|
+
sh = self.winfo_screenheight()
|
|
123
|
+
x = max(0, (sw - win_w) // 2)
|
|
124
|
+
y = max(0, (sh - win_h) // 2)
|
|
125
|
+
self.geometry(f"{win_w}x{win_h}+{x}+{y}")
|
|
126
|
+
except Exception:
|
|
127
|
+
self.geometry(f"{win_w}x{win_h}")
|
|
128
|
+
|
|
129
|
+
self.minsize(win_w, win_h)
|
|
130
|
+
|
|
131
|
+
# Keyboard shortcuts
|
|
132
|
+
self.bind("<Escape>", lambda e: self.destroy())
|
|
133
|
+
self.bind("<KeyPress>", self._handle_global_key)
|
|
134
|
+
|
|
135
|
+
# Drag tracking
|
|
136
|
+
self._drag_x = 0
|
|
137
|
+
self._drag_y = 0
|
|
138
|
+
|
|
139
|
+
# State
|
|
140
|
+
self.current_query = initial_query or ""
|
|
141
|
+
self.current_cause = ""
|
|
142
|
+
self.current_cmd = ""
|
|
143
|
+
self.current_risk_level = RiskLevel.SAFE
|
|
144
|
+
self.current_risk_reason = ""
|
|
145
|
+
self.last_detected_cmd = get_last_shell_command()
|
|
146
|
+
self.last_exit_code = get_last_exit_code()
|
|
147
|
+
|
|
148
|
+
# Load the transparent symbol logo at the compact size used by the header.
|
|
149
|
+
self.logo_image = None
|
|
150
|
+
logo_path = Path(__file__).resolve().parent.parent.parent / "assets" / "logos" / "ezshell-symbol-transparent.png"
|
|
151
|
+
if logo_path.is_file():
|
|
152
|
+
try:
|
|
153
|
+
logo = Image.open(logo_path).convert("RGBA").resize((28, 28), Image.Resampling.LANCZOS)
|
|
154
|
+
self.logo_image = ImageTk.PhotoImage(logo)
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
self._build_ui()
|
|
159
|
+
|
|
160
|
+
# If a real query was passed, analyze it immediately
|
|
161
|
+
if initial_query:
|
|
162
|
+
self._query_groq(initial_query)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _cell_size_px(self, columns: int, rows: int) -> tuple:
|
|
166
|
+
"""Taille en pixels de `columns` x `rows` cellules de la police mono de l'application."""
|
|
167
|
+
import tkinter.font as tkfont
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
font = tkfont.Font(root=self, family=self.FONT_FAMILY_CODE, size=self.FONT_SIZE_FIX_COMMAND)
|
|
171
|
+
cell_w, cell_h = font.measure("0"), font.metrics("linespace")
|
|
172
|
+
except Exception:
|
|
173
|
+
cell_w, cell_h = 8, 17
|
|
174
|
+
return columns * cell_w, rows * cell_h
|
|
175
|
+
|
|
176
|
+
def _autowrap(self, label: ctk.CTkLabel, margin: int = 0) -> None:
|
|
177
|
+
"""Renvoie le texte du label à la ligne selon la largeur réelle de son conteneur."""
|
|
178
|
+
container = label.master
|
|
179
|
+
|
|
180
|
+
def _on_resize(event) -> None:
|
|
181
|
+
scaling = ctk.ScalingTracker.get_widget_scaling(label)
|
|
182
|
+
width = max(60, int((event.width - margin) / scaling))
|
|
183
|
+
try:
|
|
184
|
+
label.configure(wraplength=width)
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
container.bind("<Configure>", _on_resize, add="+")
|
|
189
|
+
def _build_ui(self) -> None:
|
|
190
|
+
"""Constructs the application layout."""
|
|
191
|
+
self.main_container = ctk.CTkFrame(self, fg_color="transparent")
|
|
192
|
+
self.main_container.pack(fill="both", expand=True, padx=16, pady=(12, 8))
|
|
193
|
+
|
|
194
|
+
# 1. Header Bar: [Icon] ez (blue) Shell (white) ............ [Esc] to close
|
|
195
|
+
self.header_frame = ctk.CTkFrame(self.main_container, fg_color="transparent", height=32)
|
|
196
|
+
self.header_frame.pack(fill="x", pady=(0, 8))
|
|
197
|
+
|
|
198
|
+
# Drag support on header
|
|
199
|
+
self.header_frame.bind("<ButtonPress-1>", self._start_drag)
|
|
200
|
+
self.header_frame.bind("<B1-Motion>", self._on_drag)
|
|
201
|
+
|
|
202
|
+
# Left: Logo icon + 'ez' in Blue + 'Shell' in White
|
|
203
|
+
header_left = ctk.CTkFrame(self.header_frame, fg_color="transparent")
|
|
204
|
+
header_left.pack(side="left")
|
|
205
|
+
header_left.bind("<ButtonPress-1>", self._start_drag)
|
|
206
|
+
header_left.bind("<B1-Motion>", self._on_drag)
|
|
207
|
+
|
|
208
|
+
if self.logo_image:
|
|
209
|
+
logo_lbl = ctk.CTkLabel(header_left, text="", image=self.logo_image)
|
|
210
|
+
logo_lbl.pack(side="left", padx=(0, 8))
|
|
211
|
+
logo_lbl.bind("<ButtonPress-1>", self._start_drag)
|
|
212
|
+
logo_lbl.bind("<B1-Motion>", self._on_drag)
|
|
213
|
+
|
|
214
|
+
app_title_ez = ctk.CTkLabel(
|
|
215
|
+
header_left,
|
|
216
|
+
text="ez",
|
|
217
|
+
text_color=self.COLOR_PRIMARY_BLUE,
|
|
218
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_TITLE, size=self.FONT_SIZE_APP_TITLE, weight="bold"),
|
|
219
|
+
)
|
|
220
|
+
app_title_ez.pack(side="left")
|
|
221
|
+
app_title_ez.bind("<ButtonPress-1>", self._start_drag)
|
|
222
|
+
app_title_ez.bind("<B1-Motion>", self._on_drag)
|
|
223
|
+
|
|
224
|
+
app_title_shell = ctk.CTkLabel(
|
|
225
|
+
header_left,
|
|
226
|
+
text="Shell",
|
|
227
|
+
text_color=self.COLOR_TEXT_PRIMARY,
|
|
228
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_TITLE, size=self.FONT_SIZE_APP_TITLE, weight="bold"),
|
|
229
|
+
)
|
|
230
|
+
app_title_shell.pack(side="left")
|
|
231
|
+
app_title_shell.bind("<ButtonPress-1>", self._start_drag)
|
|
232
|
+
app_title_shell.bind("<B1-Motion>", self._on_drag)
|
|
233
|
+
|
|
234
|
+
# Right: [Esc] to close pill
|
|
235
|
+
close_pill = ctk.CTkFrame(
|
|
236
|
+
self.header_frame,
|
|
237
|
+
fg_color=self.COLOR_BG_CARD,
|
|
238
|
+
border_color=self.COLOR_BORDER,
|
|
239
|
+
border_width=1,
|
|
240
|
+
corner_radius=6,
|
|
241
|
+
height=26,
|
|
242
|
+
)
|
|
243
|
+
close_pill.pack(side="right")
|
|
244
|
+
|
|
245
|
+
esc_badge = ctk.CTkLabel(
|
|
246
|
+
close_pill,
|
|
247
|
+
text="Esc",
|
|
248
|
+
text_color="#CBD5E1",
|
|
249
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=11, weight="bold"),
|
|
250
|
+
fg_color="#1E293B",
|
|
251
|
+
corner_radius=4,
|
|
252
|
+
width=32,
|
|
253
|
+
height=20,
|
|
254
|
+
)
|
|
255
|
+
esc_badge.pack(side="left", padx=(3, 5), pady=3)
|
|
256
|
+
|
|
257
|
+
close_text = ctk.CTkLabel(
|
|
258
|
+
close_pill,
|
|
259
|
+
text=t("esc_to_close"),
|
|
260
|
+
text_color=self.COLOR_TEXT_SUBTLE,
|
|
261
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=11),
|
|
262
|
+
)
|
|
263
|
+
close_text.pack(side="left", padx=(0, 7), pady=3)
|
|
264
|
+
|
|
265
|
+
close_pill.bind("<Button-1>", lambda e: self.destroy())
|
|
266
|
+
esc_badge.bind("<Button-1>", lambda e: self.destroy())
|
|
267
|
+
close_text.bind("<Button-1>", lambda e: self.destroy())
|
|
268
|
+
|
|
269
|
+
# Top divider line
|
|
270
|
+
ctk.CTkFrame(self.main_container, fg_color=self.COLOR_BORDER, height=1).pack(fill="x", pady=(0, 10))
|
|
271
|
+
|
|
272
|
+
# Footer réservé en premier (side="bottom") : le corps extensible ne peut plus le rogner
|
|
273
|
+
self.footer_bar = ctk.CTkFrame(self.main_container, fg_color="transparent", height=24)
|
|
274
|
+
self.footer_bar.pack(side="bottom", fill="x")
|
|
275
|
+
self._build_footer_status()
|
|
276
|
+
ctk.CTkFrame(self.main_container, fg_color=self.COLOR_BORDER, height=1).pack(
|
|
277
|
+
side="bottom", fill="x", pady=(8, 6)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Dynamic body container
|
|
281
|
+
self.body_container = ctk.CTkFrame(self.main_container, fg_color="transparent")
|
|
282
|
+
self.body_container.pack(fill="both", expand=True)
|
|
283
|
+
|
|
284
|
+
if self.current_query:
|
|
285
|
+
self._render_result_view()
|
|
286
|
+
else:
|
|
287
|
+
self._render_launcher_view()
|
|
288
|
+
|
|
289
|
+
def _build_footer_status(self) -> None:
|
|
290
|
+
"""Renders the refined bottom status bar with shortcuts and model info."""
|
|
291
|
+
for w in self.footer_bar.winfo_children():
|
|
292
|
+
w.destroy()
|
|
293
|
+
|
|
294
|
+
# Left: Keyboard shortcuts hint
|
|
295
|
+
ctk.CTkLabel(
|
|
296
|
+
self.footer_bar,
|
|
297
|
+
text=t("status_footer_hint"),
|
|
298
|
+
text_color=self.COLOR_TEXT_SUBTLE,
|
|
299
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=10),
|
|
300
|
+
anchor="w",
|
|
301
|
+
).pack(side="left")
|
|
302
|
+
|
|
303
|
+
# Right: Active Provider & Model pill
|
|
304
|
+
provider_label = chat_bridge.provider_label(self.provider, self.config)
|
|
305
|
+
|
|
306
|
+
provider_frame = ctk.CTkFrame(self.footer_bar, fg_color="transparent")
|
|
307
|
+
provider_frame.pack(side="right")
|
|
308
|
+
|
|
309
|
+
# Green online dot
|
|
310
|
+
ctk.CTkLabel(
|
|
311
|
+
provider_frame,
|
|
312
|
+
text="●",
|
|
313
|
+
text_color=self.COLOR_SUCCESS,
|
|
314
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=9),
|
|
315
|
+
).pack(side="left", padx=(0, 4))
|
|
316
|
+
|
|
317
|
+
ctk.CTkLabel(
|
|
318
|
+
provider_frame,
|
|
319
|
+
text=provider_label,
|
|
320
|
+
text_color=self.COLOR_TEXT_SUBTLE,
|
|
321
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_CODE, size=10),
|
|
322
|
+
anchor="e",
|
|
323
|
+
).pack(side="left")
|
|
324
|
+
|
|
325
|
+
def _render_launcher_view(self) -> None:
|
|
326
|
+
"""Renders clean, focused launcher mode with high-end developer dashboard layout."""
|
|
327
|
+
for w in self.body_container.winfo_children():
|
|
328
|
+
w.destroy()
|
|
329
|
+
|
|
330
|
+
# 1. Primary Search / Query Entry Card
|
|
331
|
+
search_card = ctk.CTkFrame(
|
|
332
|
+
self.body_container,
|
|
333
|
+
fg_color=self.COLOR_BG_INPUT,
|
|
334
|
+
border_color=self.COLOR_BORDER,
|
|
335
|
+
border_width=1,
|
|
336
|
+
corner_radius=10,
|
|
337
|
+
height=48,
|
|
338
|
+
)
|
|
339
|
+
search_card.pack(fill="x", pady=(2, 14))
|
|
340
|
+
search_card.pack_propagate(False)
|
|
341
|
+
|
|
342
|
+
prompt_icon = ctk.CTkLabel(
|
|
343
|
+
search_card,
|
|
344
|
+
text="›",
|
|
345
|
+
text_color=self.COLOR_PRIMARY_BLUE,
|
|
346
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_CODE, size=18, weight="bold"),
|
|
347
|
+
)
|
|
348
|
+
prompt_icon.pack(side="left", padx=(12, 6))
|
|
349
|
+
|
|
350
|
+
self.input_entry = ctk.CTkEntry(
|
|
351
|
+
search_card,
|
|
352
|
+
placeholder_text=t("input_placeholder"),
|
|
353
|
+
placeholder_text_color="#475569",
|
|
354
|
+
fg_color="transparent",
|
|
355
|
+
border_width=0,
|
|
356
|
+
text_color=self.COLOR_TEXT_PRIMARY,
|
|
357
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_CODE, size=self.FONT_SIZE_INPUT_ENTRY),
|
|
358
|
+
)
|
|
359
|
+
self.input_entry.pack(side="left", fill="both", expand=True, padx=(0, 8))
|
|
360
|
+
self.input_entry.bind("<Return>", self._on_input_return)
|
|
361
|
+
self.input_entry.focus()
|
|
362
|
+
|
|
363
|
+
# Focus ring animation
|
|
364
|
+
def _on_entry_focus(e):
|
|
365
|
+
search_card.configure(border_color=self.COLOR_PRIMARY_BLUE)
|
|
366
|
+
|
|
367
|
+
def _on_entry_blur(e):
|
|
368
|
+
search_card.configure(border_color=self.COLOR_BORDER)
|
|
369
|
+
|
|
370
|
+
self.input_entry.bind("<FocusIn>", _on_entry_focus)
|
|
371
|
+
self.input_entry.bind("<FocusOut>", _on_entry_blur)
|
|
372
|
+
|
|
373
|
+
# Sleek Send / Enter badge button
|
|
374
|
+
ask_btn = ctk.CTkButton(
|
|
375
|
+
search_card,
|
|
376
|
+
text=f"↵ {t('btn_send')}",
|
|
377
|
+
fg_color="#1E293B",
|
|
378
|
+
hover_color=self.COLOR_PRIMARY_BLUE,
|
|
379
|
+
text_color="#CBD5E1",
|
|
380
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=11, weight="bold"),
|
|
381
|
+
width=84,
|
|
382
|
+
height=30,
|
|
383
|
+
corner_radius=6,
|
|
384
|
+
command=self._on_submit_input,
|
|
385
|
+
)
|
|
386
|
+
ask_btn.pack(side="right", padx=(0, 8), pady=8)
|
|
387
|
+
|
|
388
|
+
# 2. Context Card: Detected Last Shell Command OR Clean System Status
|
|
389
|
+
if self.last_detected_cmd:
|
|
390
|
+
last_cmd_card = ctk.CTkFrame(
|
|
391
|
+
self.body_container,
|
|
392
|
+
fg_color=self.COLOR_BG_CARD,
|
|
393
|
+
border_color=self.COLOR_BORDER,
|
|
394
|
+
border_width=1,
|
|
395
|
+
corner_radius=8,
|
|
396
|
+
)
|
|
397
|
+
last_cmd_card.pack(fill="x", pady=(0, 14))
|
|
398
|
+
|
|
399
|
+
# Header row inside card
|
|
400
|
+
row_top = ctk.CTkFrame(last_cmd_card, fg_color="transparent")
|
|
401
|
+
row_top.pack(fill="x", padx=12, pady=(10, 6))
|
|
402
|
+
|
|
403
|
+
ctk.CTkLabel(
|
|
404
|
+
row_top,
|
|
405
|
+
text=f"› {t('last_cmd_detected')}",
|
|
406
|
+
text_color=self.COLOR_TEXT_MUTED,
|
|
407
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=11, weight="bold"),
|
|
408
|
+
anchor="w",
|
|
409
|
+
).pack(side="left")
|
|
410
|
+
|
|
411
|
+
# Exit code pill
|
|
412
|
+
if self.last_exit_code is not None:
|
|
413
|
+
is_err = self.last_exit_code != 0
|
|
414
|
+
badge_bg = "#2B1216" if is_err else "#0F291E"
|
|
415
|
+
badge_fg = self.COLOR_DANGER if is_err else self.COLOR_SUCCESS
|
|
416
|
+
badge_text = f"✘ Code {self.last_exit_code}" if is_err else "✓ Code 0"
|
|
417
|
+
|
|
418
|
+
ctk.CTkLabel(
|
|
419
|
+
row_top,
|
|
420
|
+
text=badge_text,
|
|
421
|
+
text_color=badge_fg,
|
|
422
|
+
fg_color=badge_bg,
|
|
423
|
+
corner_radius=4,
|
|
424
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_CODE, size=10, weight="bold"),
|
|
425
|
+
padx=6,
|
|
426
|
+
pady=2,
|
|
427
|
+
).pack(side="right")
|
|
428
|
+
|
|
429
|
+
# Monospace command container
|
|
430
|
+
cmd_box = ctk.CTkFrame(
|
|
431
|
+
last_cmd_card,
|
|
432
|
+
fg_color=self.COLOR_BG_CODE,
|
|
433
|
+
border_color=self.COLOR_BORDER,
|
|
434
|
+
border_width=1,
|
|
435
|
+
corner_radius=6,
|
|
436
|
+
height=34,
|
|
437
|
+
)
|
|
438
|
+
cmd_box.pack(fill="x", padx=12, pady=(0, 10))
|
|
439
|
+
|
|
440
|
+
cmd_mono = ctk.CTkLabel(
|
|
441
|
+
cmd_box,
|
|
442
|
+
text=f" $ {self.last_detected_cmd}",
|
|
443
|
+
text_color=self.COLOR_LIGHT_BLUE,
|
|
444
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_CODE, size=12, weight="bold"),
|
|
445
|
+
anchor="w",
|
|
446
|
+
)
|
|
447
|
+
cmd_mono.pack(fill="both", expand=True, padx=6, pady=6)
|
|
448
|
+
self._autowrap(cmd_mono, 24)
|
|
449
|
+
|
|
450
|
+
# Action buttons row
|
|
451
|
+
btn_row = ctk.CTkFrame(last_cmd_card, fg_color="transparent")
|
|
452
|
+
btn_row.pack(fill="x", padx=12, pady=(0, 10))
|
|
453
|
+
|
|
454
|
+
diag_btn = ctk.CTkButton(
|
|
455
|
+
btn_row,
|
|
456
|
+
text=f"⚡ {t('btn_diagnose_error')}",
|
|
457
|
+
fg_color=self.COLOR_PRIMARY_BLUE,
|
|
458
|
+
hover_color=self.COLOR_BLUE_HOVER,
|
|
459
|
+
text_color="#FFFFFF",
|
|
460
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=11, weight="bold"),
|
|
461
|
+
height=28,
|
|
462
|
+
corner_radius=5,
|
|
463
|
+
command=lambda: self._query_groq(
|
|
464
|
+
f"diagnose failed command: {self.last_detected_cmd}", mode="explain_error"
|
|
465
|
+
),
|
|
466
|
+
)
|
|
467
|
+
diag_btn.pack(side="left", padx=(0, 8))
|
|
468
|
+
|
|
469
|
+
explain_btn = ctk.CTkButton(
|
|
470
|
+
btn_row,
|
|
471
|
+
text=f"ℹ {t('btn_explain_command')}",
|
|
472
|
+
fg_color="#1E293B",
|
|
473
|
+
hover_color="#334155",
|
|
474
|
+
border_color="#334155",
|
|
475
|
+
border_width=1,
|
|
476
|
+
text_color=self.COLOR_TEXT_MUTED,
|
|
477
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=11),
|
|
478
|
+
height=28,
|
|
479
|
+
corner_radius=5,
|
|
480
|
+
command=lambda: self._query_groq(
|
|
481
|
+
f"explain what this command does: {self.last_detected_cmd}",
|
|
482
|
+
mode="explain_command",
|
|
483
|
+
),
|
|
484
|
+
)
|
|
485
|
+
explain_btn.pack(side="left")
|
|
486
|
+
else:
|
|
487
|
+
# Clean System Context Card when no prior failure is in history
|
|
488
|
+
status_card = ctk.CTkFrame(
|
|
489
|
+
self.body_container,
|
|
490
|
+
fg_color=self.COLOR_BG_CARD,
|
|
491
|
+
border_color=self.COLOR_BORDER,
|
|
492
|
+
border_width=1,
|
|
493
|
+
corner_radius=8,
|
|
494
|
+
)
|
|
495
|
+
status_card.pack(fill="x", pady=(0, 14))
|
|
496
|
+
|
|
497
|
+
top_row = ctk.CTkFrame(status_card, fg_color="transparent")
|
|
498
|
+
top_row.pack(fill="x", padx=12, pady=(8, 4))
|
|
499
|
+
|
|
500
|
+
ctk.CTkLabel(
|
|
501
|
+
top_row,
|
|
502
|
+
text=f"› {t('status_ready')}",
|
|
503
|
+
text_color=self.COLOR_TEXT_PRIMARY,
|
|
504
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=11, weight="bold"),
|
|
505
|
+
anchor="w",
|
|
506
|
+
).pack(side="left")
|
|
507
|
+
|
|
508
|
+
ctk.CTkLabel(
|
|
509
|
+
top_row,
|
|
510
|
+
text="● Online",
|
|
511
|
+
text_color=self.COLOR_SUCCESS,
|
|
512
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=10, weight="bold"),
|
|
513
|
+
).pack(side="right")
|
|
514
|
+
|
|
515
|
+
# System info pills row
|
|
516
|
+
chips_row = ctk.CTkFrame(status_card, fg_color="transparent")
|
|
517
|
+
chips_row.pack(fill="x", padx=12, pady=(0, 8))
|
|
518
|
+
|
|
519
|
+
cur_dir_name = Path(self.context.current_directory).name or "/"
|
|
520
|
+
chips = [
|
|
521
|
+
f"OS: {self.context.os_name} ({self.context.architecture})",
|
|
522
|
+
f"Shell: {self.context.shell_name}",
|
|
523
|
+
f"Cwd: {cur_dir_name}",
|
|
524
|
+
]
|
|
525
|
+
if self.context.git_branch:
|
|
526
|
+
chips.append(f"Git: {self.context.git_branch}")
|
|
527
|
+
|
|
528
|
+
# Deux puces par ligne : elles tiennent dans la fenêtre compacte (45 colonnes)
|
|
529
|
+
row = None
|
|
530
|
+
for i, c in enumerate(chips):
|
|
531
|
+
if i % 2 == 0:
|
|
532
|
+
row = ctk.CTkFrame(chips_row, fg_color="transparent")
|
|
533
|
+
row.pack(fill="x", pady=(0, 4))
|
|
534
|
+
chip_lbl = ctk.CTkLabel(
|
|
535
|
+
row,
|
|
536
|
+
text=c,
|
|
537
|
+
text_color=self.COLOR_TEXT_MUTED,
|
|
538
|
+
fg_color="#0F172A",
|
|
539
|
+
corner_radius=4,
|
|
540
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_CODE, size=10),
|
|
541
|
+
padx=6,
|
|
542
|
+
pady=2,
|
|
543
|
+
)
|
|
544
|
+
chip_lbl.pack(side="left", padx=(0, 6))
|
|
545
|
+
|
|
546
|
+
# Quick Actions désactivées pour garder le launcher centré sur la question.
|
|
547
|
+
# Elles restent documentées ici pour une réactivation ultérieure :
|
|
548
|
+
# - ports : lister les ports ouverts et leurs PID
|
|
549
|
+
# - disque : afficher l'espace utilisé dans le dossier courant
|
|
550
|
+
# - Docker : nettoyer les conteneurs, volumes et images inutilisés
|
|
551
|
+
# - Git : annuler le dernier commit en conservant les changements locaux
|
|
552
|
+
|
|
553
|
+
def _render_result_view(self) -> None:
|
|
554
|
+
"""Renders the clean diagnostic and result view with command prioritized at the top."""
|
|
555
|
+
for w in self.body_container.winfo_children():
|
|
556
|
+
w.destroy()
|
|
557
|
+
|
|
558
|
+
content_scroll = ctk.CTkScrollableFrame(
|
|
559
|
+
self.body_container,
|
|
560
|
+
fg_color="transparent",
|
|
561
|
+
scrollbar_button_color="#1E293B",
|
|
562
|
+
scrollbar_button_hover_color=self.COLOR_PRIMARY_BLUE,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
# 1. User Query Pill Banner: > {query}
|
|
566
|
+
query_card = ctk.CTkFrame(
|
|
567
|
+
content_scroll,
|
|
568
|
+
fg_color=self.COLOR_BG_CARD,
|
|
569
|
+
border_color=self.COLOR_BORDER,
|
|
570
|
+
border_width=1,
|
|
571
|
+
corner_radius=8,
|
|
572
|
+
height=38,
|
|
573
|
+
)
|
|
574
|
+
query_card.pack(fill="x", pady=(0, 12))
|
|
575
|
+
|
|
576
|
+
q_inner = ctk.CTkFrame(query_card, fg_color="transparent")
|
|
577
|
+
q_inner.pack(fill="both", expand=True, padx=12, pady=6)
|
|
578
|
+
|
|
579
|
+
ctk.CTkLabel(
|
|
580
|
+
q_inner,
|
|
581
|
+
text="›",
|
|
582
|
+
text_color=self.COLOR_PRIMARY_BLUE,
|
|
583
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_CODE, size=self.FONT_SIZE_QUERY_CHEVRON, weight="bold"),
|
|
584
|
+
).pack(side="left", padx=(0, 8))
|
|
585
|
+
|
|
586
|
+
self.query_label = ctk.CTkLabel(
|
|
587
|
+
q_inner,
|
|
588
|
+
text=self.current_query,
|
|
589
|
+
text_color=self.COLOR_TEXT_PRIMARY,
|
|
590
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_CODE, size=self.FONT_SIZE_QUERY_TEXT),
|
|
591
|
+
anchor="w",
|
|
592
|
+
)
|
|
593
|
+
self.query_label.pack(side="left", fill="x", expand=True)
|
|
594
|
+
self.query_label.configure(justify="left")
|
|
595
|
+
self._autowrap(self.query_label, 8)
|
|
596
|
+
|
|
597
|
+
# 2. Section: FIX / COMMANDE RECOMMANDEE (Prominently placed at top so it's instantly visible!)
|
|
598
|
+
if self.current_cmd:
|
|
599
|
+
ctk.CTkLabel(
|
|
600
|
+
content_scroll,
|
|
601
|
+
text=t("section_fix"),
|
|
602
|
+
text_color=self.COLOR_TEXT_MUTED,
|
|
603
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=self.FONT_SIZE_SECTION_HEADER, weight="bold"),
|
|
604
|
+
anchor="w",
|
|
605
|
+
).pack(fill="x", pady=(0, 4))
|
|
606
|
+
|
|
607
|
+
self.cmd_box = ctk.CTkFrame(
|
|
608
|
+
content_scroll,
|
|
609
|
+
fg_color=self.COLOR_BG_CODE,
|
|
610
|
+
border_color=self.COLOR_BORDER,
|
|
611
|
+
border_width=1,
|
|
612
|
+
corner_radius=8,
|
|
613
|
+
height=46,
|
|
614
|
+
)
|
|
615
|
+
self.cmd_box.pack(fill="x", pady=(0, 8))
|
|
616
|
+
|
|
617
|
+
cmd_inner = ctk.CTkFrame(self.cmd_box, fg_color="transparent")
|
|
618
|
+
cmd_inner.pack(fill="both", expand=True, padx=12, pady=6)
|
|
619
|
+
|
|
620
|
+
ctk.CTkLabel(
|
|
621
|
+
cmd_inner,
|
|
622
|
+
text="$",
|
|
623
|
+
text_color=self.COLOR_TEXT_SUBTLE,
|
|
624
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_CODE, size=self.FONT_SIZE_FIX_COMMAND),
|
|
625
|
+
).pack(side="left", padx=(0, 8))
|
|
626
|
+
|
|
627
|
+
self.cmd_text_lbl = ctk.CTkLabel(
|
|
628
|
+
cmd_inner,
|
|
629
|
+
text=self.current_cmd,
|
|
630
|
+
text_color="#60A5FA",
|
|
631
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_CODE, size=self.FONT_SIZE_FIX_COMMAND, weight="bold"),
|
|
632
|
+
anchor="w",
|
|
633
|
+
)
|
|
634
|
+
self.cmd_text_lbl.pack(side="left", fill="x", expand=True)
|
|
635
|
+
self.cmd_text_lbl.configure(justify="left")
|
|
636
|
+
self._autowrap(self.cmd_text_lbl, 8)
|
|
637
|
+
|
|
638
|
+
# Quick Copy Button inside the code box
|
|
639
|
+
self.quick_copy_btn = ctk.CTkButton(
|
|
640
|
+
cmd_inner,
|
|
641
|
+
text="Copier",
|
|
642
|
+
fg_color="#1E293B",
|
|
643
|
+
hover_color=self.COLOR_PRIMARY_BLUE,
|
|
644
|
+
text_color="#CBD5E1",
|
|
645
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=11),
|
|
646
|
+
width=60,
|
|
647
|
+
height=26,
|
|
648
|
+
corner_radius=4,
|
|
649
|
+
command=self.copy_current_command,
|
|
650
|
+
)
|
|
651
|
+
self.quick_copy_btn.pack(side="right")
|
|
652
|
+
|
|
653
|
+
# Action Pills Bar: [Enter run] [c copy] [e edit]
|
|
654
|
+
actions_bar = ctk.CTkFrame(content_scroll, fg_color="transparent")
|
|
655
|
+
actions_bar.pack(fill="x", pady=(0, 10))
|
|
656
|
+
|
|
657
|
+
self.btn_run = self._create_action_pill(
|
|
658
|
+
actions_bar,
|
|
659
|
+
key_text="Enter",
|
|
660
|
+
label_text=t("action_run"),
|
|
661
|
+
command=self.run_current_command,
|
|
662
|
+
accent=True,
|
|
663
|
+
)
|
|
664
|
+
self.btn_run.pack(side="left", padx=(0, 8))
|
|
665
|
+
|
|
666
|
+
self.btn_copy = self._create_action_pill(
|
|
667
|
+
actions_bar,
|
|
668
|
+
key_text="c",
|
|
669
|
+
label_text=t("action_copy"),
|
|
670
|
+
command=self.copy_current_command,
|
|
671
|
+
)
|
|
672
|
+
self.btn_copy.pack(side="left", padx=(0, 8))
|
|
673
|
+
|
|
674
|
+
self.btn_edit = self._create_action_pill(
|
|
675
|
+
actions_bar,
|
|
676
|
+
key_text="e",
|
|
677
|
+
label_text=t("action_edit"),
|
|
678
|
+
command=self.edit_current_command,
|
|
679
|
+
)
|
|
680
|
+
self.btn_edit.pack(side="left", padx=(0, 8))
|
|
681
|
+
|
|
682
|
+
# Risk Badge Row
|
|
683
|
+
risk_row = ctk.CTkFrame(content_scroll, fg_color="transparent")
|
|
684
|
+
risk_row.pack(fill="x", pady=(0, 12))
|
|
685
|
+
|
|
686
|
+
risk_color = (
|
|
687
|
+
self.COLOR_DANGER
|
|
688
|
+
if self.current_risk_level == RiskLevel.DANGEROUS
|
|
689
|
+
else (self.COLOR_WARNING if self.current_risk_level == RiskLevel.WARNING else self.COLOR_SUCCESS)
|
|
690
|
+
)
|
|
691
|
+
risk_label = (
|
|
692
|
+
t("risk_high")
|
|
693
|
+
if self.current_risk_level == RiskLevel.DANGEROUS
|
|
694
|
+
else (t("risk_medium") if self.current_risk_level == RiskLevel.WARNING else t("risk_low"))
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
ctk.CTkLabel(
|
|
698
|
+
risk_row,
|
|
699
|
+
text="●",
|
|
700
|
+
text_color=risk_color,
|
|
701
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=10),
|
|
702
|
+
).pack(side="left", padx=(0, 4))
|
|
703
|
+
|
|
704
|
+
ctk.CTkLabel(
|
|
705
|
+
risk_row,
|
|
706
|
+
text=risk_label,
|
|
707
|
+
text_color=risk_color,
|
|
708
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=self.FONT_SIZE_RISK_BADGE, weight="bold"),
|
|
709
|
+
).pack(side="left", padx=(0, 8))
|
|
710
|
+
|
|
711
|
+
risk_reason_lbl = ctk.CTkLabel(
|
|
712
|
+
risk_row,
|
|
713
|
+
text=self.current_risk_reason or t("risk_reason_default"),
|
|
714
|
+
text_color=self.COLOR_TEXT_MUTED,
|
|
715
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=self.FONT_SIZE_RISK_BADGE),
|
|
716
|
+
anchor="w",
|
|
717
|
+
justify="left",
|
|
718
|
+
)
|
|
719
|
+
risk_reason_lbl.pack(side="left", fill="x", expand=True)
|
|
720
|
+
self._autowrap(risk_reason_lbl, 4)
|
|
721
|
+
|
|
722
|
+
# 3. Section: EXPLANATION or CAUSE (Cleaned of raw markdown syntax)
|
|
723
|
+
is_error = any(w in self.current_query.lower() for w in ("diagnose", "error", "failed", "échec", "fail"))
|
|
724
|
+
section_title = t("section_cause") if is_error else t("section_explanation")
|
|
725
|
+
|
|
726
|
+
ctk.CTkLabel(
|
|
727
|
+
content_scroll,
|
|
728
|
+
text=section_title,
|
|
729
|
+
text_color=self.COLOR_TEXT_MUTED,
|
|
730
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=self.FONT_SIZE_SECTION_HEADER, weight="bold"),
|
|
731
|
+
anchor="w",
|
|
732
|
+
).pack(fill="x", pady=(0, 4))
|
|
733
|
+
|
|
734
|
+
cleaned_body = clean_markdown_explanation(self.current_cause)
|
|
735
|
+
|
|
736
|
+
expl_card = ctk.CTkFrame(
|
|
737
|
+
content_scroll,
|
|
738
|
+
fg_color=self.COLOR_BG_CARD,
|
|
739
|
+
border_color=self.COLOR_BORDER,
|
|
740
|
+
border_width=1,
|
|
741
|
+
corner_radius=8,
|
|
742
|
+
)
|
|
743
|
+
expl_card.pack(fill="x", pady=(0, 12))
|
|
744
|
+
|
|
745
|
+
self.cause_body = ctk.CTkLabel(
|
|
746
|
+
expl_card,
|
|
747
|
+
text=cleaned_body or t("thinking"),
|
|
748
|
+
text_color=self.COLOR_TEXT_PRIMARY if self.current_cause else self.COLOR_PRIMARY_BLUE,
|
|
749
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=self.FONT_SIZE_CAUSE_BODY),
|
|
750
|
+
anchor="w",
|
|
751
|
+
justify="left",
|
|
752
|
+
)
|
|
753
|
+
self.cause_body.pack(fill="x", padx=12, pady=10)
|
|
754
|
+
self._autowrap(self.cause_body, 24)
|
|
755
|
+
|
|
756
|
+
# Execution Result Box (appears when Enter/run is clicked)
|
|
757
|
+
self.exec_box = ctk.CTkFrame(content_scroll, fg_color=self.COLOR_BG_CODE, border_color=self.COLOR_BORDER, border_width=1, corner_radius=6)
|
|
758
|
+
self.exec_label = ctk.CTkLabel(
|
|
759
|
+
self.exec_box,
|
|
760
|
+
text="",
|
|
761
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_CODE, size=self.FONT_SIZE_EXEC_OUTPUT),
|
|
762
|
+
justify="left",
|
|
763
|
+
anchor="w",
|
|
764
|
+
)
|
|
765
|
+
self.exec_label.pack(fill="both", expand=True, padx=10, pady=8)
|
|
766
|
+
self._autowrap(self.exec_label, 20)
|
|
767
|
+
|
|
768
|
+
# 4. Bottom Controls: Follow-up Input Entry (packé côté bas avant la zone défilante)
|
|
769
|
+
|
|
770
|
+
bottom_input_box = ctk.CTkFrame(
|
|
771
|
+
self.body_container,
|
|
772
|
+
fg_color=self.COLOR_BG_INPUT,
|
|
773
|
+
border_color=self.COLOR_BORDER,
|
|
774
|
+
border_width=1,
|
|
775
|
+
corner_radius=8,
|
|
776
|
+
height=40,
|
|
777
|
+
)
|
|
778
|
+
bottom_input_box.pack(side="bottom", fill="x")
|
|
779
|
+
bottom_input_box.pack_propagate(False)
|
|
780
|
+
ctk.CTkFrame(self.body_container, fg_color=self.COLOR_BORDER, height=1).pack(
|
|
781
|
+
side="bottom", fill="x", pady=(0, 8)
|
|
782
|
+
)
|
|
783
|
+
content_scroll.pack(fill="both", expand=True, pady=(0, 8))
|
|
784
|
+
|
|
785
|
+
ctk.CTkLabel(
|
|
786
|
+
bottom_input_box,
|
|
787
|
+
text="›",
|
|
788
|
+
text_color=self.COLOR_PRIMARY_BLUE,
|
|
789
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_CODE, size=16, weight="bold"),
|
|
790
|
+
).pack(side="left", padx=(10, 6))
|
|
791
|
+
|
|
792
|
+
self.input_entry = ctk.CTkEntry(
|
|
793
|
+
bottom_input_box,
|
|
794
|
+
placeholder_text=t("input_placeholder"),
|
|
795
|
+
placeholder_text_color="#475569",
|
|
796
|
+
fg_color="transparent",
|
|
797
|
+
border_width=0,
|
|
798
|
+
text_color=self.COLOR_TEXT_PRIMARY,
|
|
799
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_CODE, size=self.FONT_SIZE_INPUT_ENTRY),
|
|
800
|
+
)
|
|
801
|
+
self.input_entry.pack(side="left", fill="both", expand=True, padx=(0, 6))
|
|
802
|
+
self.input_entry.bind("<Return>", self._on_input_return)
|
|
803
|
+
|
|
804
|
+
# Focus ring animation on bottom input
|
|
805
|
+
def _on_bottom_focus(e):
|
|
806
|
+
bottom_input_box.configure(border_color=self.COLOR_PRIMARY_BLUE)
|
|
807
|
+
|
|
808
|
+
def _on_bottom_blur(e):
|
|
809
|
+
bottom_input_box.configure(border_color=self.COLOR_BORDER)
|
|
810
|
+
|
|
811
|
+
self.input_entry.bind("<FocusIn>", _on_bottom_focus)
|
|
812
|
+
self.input_entry.bind("<FocusOut>", _on_bottom_blur)
|
|
813
|
+
|
|
814
|
+
# Back to launcher button
|
|
815
|
+
back_btn = ctk.CTkButton(
|
|
816
|
+
bottom_input_box,
|
|
817
|
+
text="← Retour",
|
|
818
|
+
fg_color="#1E293B",
|
|
819
|
+
hover_color=self.COLOR_PRIMARY_BLUE,
|
|
820
|
+
text_color="#94A3B8",
|
|
821
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=11),
|
|
822
|
+
width=70,
|
|
823
|
+
height=28,
|
|
824
|
+
corner_radius=5,
|
|
825
|
+
command=self._reset_to_launcher,
|
|
826
|
+
)
|
|
827
|
+
back_btn.pack(side="right", padx=(0, 6), pady=6)
|
|
828
|
+
|
|
829
|
+
def _reset_to_launcher(self) -> None:
|
|
830
|
+
"""Returns from result view back to launcher view."""
|
|
831
|
+
self.current_query = ""
|
|
832
|
+
self.current_cause = ""
|
|
833
|
+
self.current_cmd = ""
|
|
834
|
+
self._render_launcher_view()
|
|
835
|
+
|
|
836
|
+
def _create_action_pill(
|
|
837
|
+
self,
|
|
838
|
+
parent: ctk.CTkFrame,
|
|
839
|
+
key_text: str,
|
|
840
|
+
label_text: str,
|
|
841
|
+
command,
|
|
842
|
+
accent: bool = False,
|
|
843
|
+
) -> ctk.CTkFrame:
|
|
844
|
+
"""Creates a sleek key action pill."""
|
|
845
|
+
bg_col = self.COLOR_PRIMARY_BLUE if accent else self.COLOR_BG_CARD
|
|
846
|
+
border_col = self.COLOR_BLUE_HOVER if accent else self.COLOR_BORDER
|
|
847
|
+
|
|
848
|
+
pill = ctk.CTkFrame(
|
|
849
|
+
parent,
|
|
850
|
+
fg_color=bg_col,
|
|
851
|
+
border_color=border_col,
|
|
852
|
+
border_width=1,
|
|
853
|
+
corner_radius=6,
|
|
854
|
+
height=28,
|
|
855
|
+
)
|
|
856
|
+
# Pas de pack_propagate(False) : la pastille prend la largeur de son contenu (sinon 200 px fixes)
|
|
857
|
+
|
|
858
|
+
key_lbl = ctk.CTkLabel(
|
|
859
|
+
pill,
|
|
860
|
+
text=key_text,
|
|
861
|
+
text_color="#FFFFFF" if accent else "#CBD5E1",
|
|
862
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=self.FONT_SIZE_PILL_KEY, weight="bold"),
|
|
863
|
+
fg_color=self.COLOR_BLUE_HOVER if accent else "#1E293B",
|
|
864
|
+
corner_radius=4,
|
|
865
|
+
width=28 if len(key_text) <= 2 else 42,
|
|
866
|
+
height=20,
|
|
867
|
+
)
|
|
868
|
+
key_lbl.pack(side="left", padx=(3, 5), pady=3)
|
|
869
|
+
|
|
870
|
+
lbl = ctk.CTkLabel(
|
|
871
|
+
pill,
|
|
872
|
+
text=label_text,
|
|
873
|
+
text_color="#FFFFFF" if accent else self.COLOR_TEXT_MUTED,
|
|
874
|
+
font=ctk.CTkFont(family=self.FONT_FAMILY_UI, size=self.FONT_SIZE_PILL_LABEL, weight="bold" if accent else "normal"),
|
|
875
|
+
)
|
|
876
|
+
lbl.pack(side="left", padx=(0, 8), pady=3)
|
|
877
|
+
|
|
878
|
+
def _on_enter(e):
|
|
879
|
+
if not accent:
|
|
880
|
+
pill.configure(fg_color="#1E293B", border_color=self.COLOR_PRIMARY_BLUE)
|
|
881
|
+
|
|
882
|
+
def _on_leave(e):
|
|
883
|
+
if not accent:
|
|
884
|
+
pill.configure(fg_color=self.COLOR_BG_CARD, border_color=self.COLOR_BORDER)
|
|
885
|
+
|
|
886
|
+
for w in (pill, key_lbl, lbl):
|
|
887
|
+
w.bind("<Button-1>", lambda e: command())
|
|
888
|
+
w.bind("<Enter>", _on_enter)
|
|
889
|
+
w.bind("<Leave>", _on_leave)
|
|
890
|
+
|
|
891
|
+
return pill
|
|
892
|
+
|
|
893
|
+
def _handle_global_key(self, event) -> None:
|
|
894
|
+
"""Handles single-key shortcuts when the text entry is not focused."""
|
|
895
|
+
focused = self.focus_get()
|
|
896
|
+
if hasattr(self, "input_entry") and focused in (self.input_entry, self.input_entry._entry):
|
|
897
|
+
return
|
|
898
|
+
|
|
899
|
+
k = event.keysym.lower()
|
|
900
|
+
if k == "q":
|
|
901
|
+
self.destroy()
|
|
902
|
+
elif k == "c" and self.current_cmd:
|
|
903
|
+
self.copy_current_command()
|
|
904
|
+
elif k == "e" and self.current_cmd:
|
|
905
|
+
self.edit_current_command()
|
|
906
|
+
elif k == "return" and self.current_cmd:
|
|
907
|
+
self.run_current_command()
|
|
908
|
+
|
|
909
|
+
def run_current_command(self) -> None:
|
|
910
|
+
"""Executes the fix command via PTYExecutor and displays terminal output."""
|
|
911
|
+
cmd = self.current_cmd.strip()
|
|
912
|
+
if not cmd:
|
|
913
|
+
return
|
|
914
|
+
|
|
915
|
+
result = PTYExecutor.execute(cmd, cwd=os.getcwd(), forward_output=False)
|
|
916
|
+
|
|
917
|
+
is_ok = result.exit_code == 0
|
|
918
|
+
status_col = self.COLOR_SUCCESS if is_ok else self.COLOR_DANGER
|
|
919
|
+
status_text = (
|
|
920
|
+
f"› {t('cmd_success')}\n{result.clean_output.strip() or t('cmd_no_output')}"
|
|
921
|
+
if is_ok
|
|
922
|
+
else f"✘ Command exited with code {result.exit_code}\n{result.clean_output.strip() or t('cmd_no_output')}"
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
self.exec_label.configure(text=status_text, text_color=status_col)
|
|
926
|
+
self.exec_box.pack(fill="x", pady=(0, 10))
|
|
927
|
+
|
|
928
|
+
def copy_current_command(self) -> None:
|
|
929
|
+
"""Copies the fix command to clipboard."""
|
|
930
|
+
try:
|
|
931
|
+
self.clipboard_clear()
|
|
932
|
+
self.clipboard_append(self.current_cmd)
|
|
933
|
+
except Exception:
|
|
934
|
+
pass
|
|
935
|
+
|
|
936
|
+
if hasattr(self, "quick_copy_btn"):
|
|
937
|
+
self.quick_copy_btn.configure(text="✓ Copié !", fg_color=self.COLOR_SUCCESS)
|
|
938
|
+
self.after(1200, lambda: self.quick_copy_btn.configure(text="Copier", fg_color="#1E293B"))
|
|
939
|
+
|
|
940
|
+
if hasattr(self, "cmd_box"):
|
|
941
|
+
self.cmd_box.configure(border_color=self.COLOR_PRIMARY_BLUE)
|
|
942
|
+
self.after(500, lambda: self.cmd_box.configure(border_color=self.COLOR_BORDER))
|
|
943
|
+
|
|
944
|
+
def edit_current_command(self) -> None:
|
|
945
|
+
"""Populates the input entry with the current command."""
|
|
946
|
+
self.input_entry.delete(0, "end")
|
|
947
|
+
self.input_entry.insert(0, self.current_cmd)
|
|
948
|
+
self.input_entry.focus()
|
|
949
|
+
|
|
950
|
+
def _on_submit_input(self) -> None:
|
|
951
|
+
"""Queries AI with the text from input_entry."""
|
|
952
|
+
query = self.input_entry.get().strip()
|
|
953
|
+
if not query:
|
|
954
|
+
return
|
|
955
|
+
|
|
956
|
+
self.input_entry.delete(0, "end")
|
|
957
|
+
self._query_groq(query)
|
|
958
|
+
|
|
959
|
+
def _on_input_return(self, _event=None) -> str:
|
|
960
|
+
"""Submits the focused input and prevents the global Return shortcut."""
|
|
961
|
+
self._on_submit_input()
|
|
962
|
+
return "break"
|
|
963
|
+
|
|
964
|
+
def _query_groq(self, query: str, mode: str = "translate") -> None:
|
|
965
|
+
"""Sends query to active LLM in a background thread and updates the UI."""
|
|
966
|
+
self.current_query = query
|
|
967
|
+
self.current_cause = ""
|
|
968
|
+
self.current_cmd = ""
|
|
969
|
+
self.current_risk_reason = ""
|
|
970
|
+
self._render_result_view()
|
|
971
|
+
|
|
972
|
+
def _worker():
|
|
973
|
+
try:
|
|
974
|
+
result = chat_bridge.ask(
|
|
975
|
+
query if mode == "translate" else "",
|
|
976
|
+
mode=mode,
|
|
977
|
+
command=self.last_detected_cmd or "",
|
|
978
|
+
provider=self.provider,
|
|
979
|
+
)
|
|
980
|
+
self.after(0, lambda: self._apply_result(result))
|
|
981
|
+
except Exception as e:
|
|
982
|
+
msg = chat_bridge.describe_error(e)
|
|
983
|
+
self.after(0, lambda: self._apply_error(msg))
|
|
984
|
+
|
|
985
|
+
threading.Thread(target=_worker, daemon=True).start()
|
|
986
|
+
|
|
987
|
+
def _apply_result(self, result: ChatMessageResult) -> None:
|
|
988
|
+
"""Parses and renders AI diagnosis and suggested command."""
|
|
989
|
+
self.current_cause = result.content.strip()
|
|
990
|
+
|
|
991
|
+
if result.suggested_commands:
|
|
992
|
+
self.current_cmd = result.suggested_commands[0]
|
|
993
|
+
if result.safety_reports:
|
|
994
|
+
report = result.safety_reports[0]
|
|
995
|
+
self.current_risk_level = report.risk_level
|
|
996
|
+
self.current_risk_reason = report.reason or t("risk_reason_default")
|
|
997
|
+
else:
|
|
998
|
+
self.current_cmd = ""
|
|
999
|
+
self.current_risk_reason = ""
|
|
1000
|
+
|
|
1001
|
+
self._render_result_view()
|
|
1002
|
+
|
|
1003
|
+
def _apply_error(self, err_msg: str) -> None:
|
|
1004
|
+
"""Renders API error message."""
|
|
1005
|
+
self.current_cause = err_msg
|
|
1006
|
+
self.current_cmd = ""
|
|
1007
|
+
self._render_result_view()
|
|
1008
|
+
|
|
1009
|
+
def _start_drag(self, event) -> None:
|
|
1010
|
+
"""Records mouse offset for window dragging."""
|
|
1011
|
+
self._drag_x = event.x
|
|
1012
|
+
self._drag_y = event.y
|
|
1013
|
+
|
|
1014
|
+
def _on_drag(self, event) -> None:
|
|
1015
|
+
"""Updates window position as mouse is dragged."""
|
|
1016
|
+
x = self.winfo_x() + (event.x - self._drag_x)
|
|
1017
|
+
y = self.winfo_y() + (event.y - self._drag_y)
|
|
1018
|
+
self.geometry(f"+{x}+{y}")
|