Assistant IA optionnel (analyse de sortie, diagnostic, generation de commande)
Nouvelle couche app/ai/ (provider OpenAI-compatible via urllib, redaction de secrets, detection de commandes dangereuses, parametres, worker QThread) et son integration dans chaque onglet terminal via un bouton "Assistant IA" (memes conventions que le bouton Snippets existant) : - Expliquer cette sortie / Diagnostiquer cette erreur / Proposer une solution / Poser une question... : utilisent la selection courante du terminal, affichees dans un panneau lateral (memoire uniquement, jamais journalise) reutilisant le splitter comme le panneau SFTP. - Generer une commande : ne s'execute jamais automatiquement (Copier / Inserer dans le terminal sans Entree / Annuler), avec avertissement si la commande proposee correspond a un motif destructeur connu. - Avant tout envoi : avis unique (persist) puis detection de secrets evidents (mot de passe, cle, jeton) avec choix de masquer, envoyer tel quel ou annuler. Desactive par defaut, sans liste de fournisseur figee (URL + modele libres, compatible OpenAI/OpenRouter/LLM local). La cle API passe exclusivement par CredentialStore (jamais dans settings.json ni journalisee). Parametrable via le nouveau menu "Assistant IA > Parametres..." (bouton "Tester la connexion"). Bug trouve par les tests avant commit : un panneau nouvellement insere dans un QSplitter deja visible est affiche automatiquement par Qt, ce qui inversait le premier clic sur "Afficher le panneau IA" ; corrige en le masquant explicitement a sa creation. 9 nouveaux fichiers de test (provider, redaction, dangers, parametres, worker, confirmations de securite, dialogue de commande, panneau, integration onglet) : 39 verifications, toutes au vert. Suite complete (31 fichiers) executee sans regression. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
79
app/dialogs/ai_command_dialog.py
Normal file
79
app/dialogs/ai_command_dialog.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Affiche une commande proposee par l'assistant IA : jamais executee
|
||||
automatiquement, seulement copiable ou inseree dans le terminal (sans Entree)
|
||||
apres verification par l'utilisateur."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QTextEdit, QPushButton, QLabel, QApplication,
|
||||
)
|
||||
|
||||
_CODE_BLOCK_RE = re.compile(r"```(?:[a-zA-Z0-9_+-]*\n)?(.*?)```", re.DOTALL)
|
||||
|
||||
|
||||
def extract_command(response: str) -> str:
|
||||
"""Prend le contenu du premier bloc de code s'il y en a un (le prompt
|
||||
systeme en demande un), sinon la reponse entiere : mieux vaut proposer un
|
||||
texte un peu trop large que perdre silencieusement la commande."""
|
||||
match = _CODE_BLOCK_RE.search(response)
|
||||
text = match.group(1) if match else response
|
||||
return text.strip()
|
||||
|
||||
|
||||
class AICommandDialog(QDialog):
|
||||
def __init__(self, response: str, dangerous_labels: list[str] | None = None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Commande generee par l'assistant IA")
|
||||
self.setMinimumSize(520, 360)
|
||||
self.command = extract_command(response)
|
||||
self.insert_requested = False
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
if dangerous_labels:
|
||||
warning = QLabel(
|
||||
"⚠ Commande potentiellement destructive : " + ", ".join(dangerous_labels)
|
||||
)
|
||||
warning.setWordWrap(True)
|
||||
warning.setStyleSheet("color: #c0392b; font-weight: bold;")
|
||||
layout.addWidget(warning)
|
||||
|
||||
layout.addWidget(QLabel("Reponse complete de l'assistant :"))
|
||||
self.response_view = QTextEdit(response)
|
||||
self.response_view.setReadOnly(True)
|
||||
layout.addWidget(self.response_view, 1)
|
||||
|
||||
layout.addWidget(QLabel("Commande proposee (celle qui sera copiee/inseree) :"))
|
||||
self.command_view = QTextEdit(self.command)
|
||||
self.command_view.setFixedHeight(70)
|
||||
layout.addWidget(self.command_view)
|
||||
|
||||
self.status_label = QLabel("Cette commande ne sera jamais executee automatiquement.")
|
||||
self.status_label.setObjectName("statusLabel")
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
btn_row = QHBoxLayout()
|
||||
self.copy_btn = QPushButton("Copier")
|
||||
self.insert_btn = QPushButton("Inserer dans le terminal")
|
||||
self.cancel_btn = QPushButton("Annuler")
|
||||
for b in (self.copy_btn, self.insert_btn, self.cancel_btn):
|
||||
btn_row.addWidget(b)
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
self.copy_btn.clicked.connect(self._copy)
|
||||
self.insert_btn.clicked.connect(self._insert)
|
||||
self.cancel_btn.clicked.connect(self.reject)
|
||||
|
||||
def _current_command(self) -> str:
|
||||
# l'utilisateur peut corriger la commande proposee avant de l'utiliser
|
||||
return self.command_view.toPlainText().strip()
|
||||
|
||||
def _copy(self) -> None:
|
||||
QApplication.clipboard().setText(self._current_command())
|
||||
self.status_label.setText("Copie dans le presse-papiers.")
|
||||
|
||||
def _insert(self) -> None:
|
||||
self.command = self._current_command()
|
||||
self.insert_requested = True
|
||||
self.accept()
|
||||
Reference in New Issue
Block a user