Skip to content

Commit 351be28

Browse files
raidos23cursoragent
andcommitted
fix(cli): dialogues Rich interactifs pour les plugins en mode terminal
En CLI, les plugins utilisent des panneaux Rich au lieu de QMessageBox, avec pause des spinners et saisie bloquante (sans acceptation automatique du oui). Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 68645a4 commit 351be28

5 files changed

Lines changed: 354 additions & 71 deletions

File tree

Plugins_SDK/GeneralContext/Dialog.py

Lines changed: 63 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
from Ui.Gui.WidgetsCreator import (
2727
InstallAuth,
2828
ProgressDialog,
29+
_is_cli_mode,
30+
_is_noninteractive,
2931
_redact_secrets,
3032
show_msgbox,
3133
sys_msgbox_for_installing,
@@ -41,22 +43,23 @@ def __init__(self):
4143
colorama.init()
4244
self.console = Console()
4345
self.plugin_id: Optional[str] = None
44-
self._ensure_qt_context()
46+
if not _is_cli_mode() and not _is_noninteractive():
47+
self._ensure_qt_context()
4548

4649
def _ensure_qt_context(self) -> None:
4750
"""Ensure a QApplication exists to allow showing Qt dialogs from plugins."""
4851
try:
4952
from PySide6.QtWidgets import QApplication
5053
import os
51-
54+
55+
# CLI and headless runs must stay on Rich, never bootstrap Qt here.
56+
if _is_cli_mode() or _is_noninteractive():
57+
return
58+
5259
# Skip if already initialized or in explicit headless mode
5360
if QApplication.instance() is not None:
5461
return
5562

56-
nonint = os.environ.get("PYCOMPILER_NONINTERACTIVE")
57-
if nonint and str(nonint).strip().lower() in ("1", "true", "yes"):
58-
return
59-
6063
# Initialize a minimal QApplication for the sandbox process
6164
# We use an empty list for argv and set offscreen if no display
6265
try:
@@ -178,48 +181,60 @@ def progress(
178181
) -> ProgressDialog:
179182
"""Create and return a ProgressDialog from Core.dialogs.
180183
181-
Uses Core.dialogs.ProgressDialog when GUI is available,
184+
Uses Core.dialogs.ProgressDialog when GUI is available,
182185
otherwise returns a Rich-based console fallback.
183186
"""
184-
from PySide6.QtWidgets import QApplication
185-
if QApplication.instance() is None:
186-
# Fallback for headless/sandbox mode using Rich
187-
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn
188-
189-
class ConsoleProgress:
190-
def __init__(self, title_str, console_obj):
191-
self.progress = Progress(
192-
SpinnerColumn(),
193-
TextColumn("[progress.description]{task.description}"),
194-
BarColumn(),
195-
TaskProgressColumn(),
196-
TimeElapsedColumn(),
197-
console=console_obj,
198-
transient=True # Disappear when finished
199-
)
200-
self.task_id = self.progress.add_task(description=title_str, total=None)
201-
202-
def show(self):
203-
self.progress.start()
204-
205-
def set_message(self, msg):
206-
self.progress.update(self.task_id, description=msg)
207-
208-
def set_status(self, msg):
209-
self.set_message(msg)
210-
211-
def set_progress(self, val, total=None):
212-
kwargs = {"completed": val}
213-
if total is not None:
214-
kwargs["total"] = total
215-
self.progress.update(self.task_id, **kwargs)
216-
217-
def close(self):
218-
self.progress.stop()
219-
220-
def is_canceled(self):
221-
return False
222-
223-
return ConsoleProgress(title, self.console) # type: ignore
187+
use_rich = _is_cli_mode() or _is_noninteractive()
188+
if not use_rich:
189+
try:
190+
from PySide6.QtWidgets import QApplication
191+
192+
if QApplication.instance() is not None:
193+
return ProgressDialog(title=title, cancelable=cancelable)
194+
except Exception:
195+
pass
196+
197+
from rich.progress import (
198+
BarColumn,
199+
Progress,
200+
SpinnerColumn,
201+
TaskProgressColumn,
202+
TextColumn,
203+
TimeElapsedColumn,
204+
)
224205

225-
return ProgressDialog(title=title, cancelable=cancelable)
206+
class ConsoleProgress:
207+
def __init__(self, title_str, console_obj):
208+
self.progress = Progress(
209+
SpinnerColumn(),
210+
TextColumn("[progress.description]{task.description}"),
211+
BarColumn(),
212+
TaskProgressColumn(),
213+
TimeElapsedColumn(),
214+
console=console_obj,
215+
transient=True,
216+
)
217+
self.task_id = self.progress.add_task(description=title_str, total=None)
218+
219+
def show(self):
220+
self.progress.start()
221+
222+
def set_message(self, msg):
223+
self.progress.update(self.task_id, description=msg)
224+
225+
def set_status(self, msg):
226+
self.set_message(msg)
227+
228+
def set_progress(self, val, total=None):
229+
kwargs = {"completed": val}
230+
if total is not None:
231+
kwargs["total"] = total
232+
self.progress.update(self.task_id, **kwargs)
233+
234+
def close(self):
235+
self.progress.stop()
236+
237+
def is_canceled(self):
238+
return False
239+
240+
return ConsoleProgress(title, self.console) # type: ignore

Ui/Cli/helpers.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,13 @@ def run_engine_compile(
7070
captured_stdout = []
7171
captured_stderr = []
7272

73+
from .interactive import register_cli_status, unregister_cli_status
74+
7375
console = get_console()
7476
status = None
7577
if not verbose and console:
7678
status = console.status("[cyan]Initialisation de la compilation...[/cyan]", spinner="dots")
79+
register_cli_status(status)
7780
status.start()
7881

7982
def _on_stdout(line: str):
@@ -124,6 +127,7 @@ def _on_stderr(line: str):
124127
signal.signal(signal.SIGINT, old_handler)
125128
if status:
126129
status.stop()
130+
unregister_cli_status(status)
127131

128132
if _CLI_CANCEL_EVENT.is_set():
129133
error("Compilation annulee par l'utilisateur (Ctrl+C).")
@@ -479,10 +483,13 @@ def run_bcasl_before_compile_sync(
479483

480484
from .output import error, info, log, success, get_console
481485

486+
from .interactive import register_cli_status, unregister_cli_status
487+
482488
console = get_console()
483489
status = None
484490
if not verbose and console:
485491
status = console.status("[cyan]Exécution de BCASL...[/cyan]", spinner="dots")
492+
register_cli_status(status)
486493
status.start()
487494

488495
class CliBcaslHost:
@@ -530,13 +537,15 @@ def append(self, msg: str):
530537
except Exception as exc:
531538
if status:
532539
status.stop()
540+
unregister_cli_status(status)
533541
error(f"BCASL execution failed: {exc}")
534542
return False
535543
finally:
536544
# Restore old handler
537545
signal.signal(signal.SIGINT, old_handler)
538546
if status:
539547
status.stop()
548+
unregister_cli_status(status)
540549

541550
if _CLI_CANCEL_EVENT.is_set():
542551
error("BCASL annule par l'utilisateur (Ctrl+C).")
@@ -579,10 +588,13 @@ def run_bcasl_headless(args: list[str], verbose: bool = False) -> int:
579588
workspace = candidate.resolve()
580589
break
581590

591+
from .interactive import register_cli_status, unregister_cli_status
592+
582593
console = get_console()
583594
status = None
584595
if not verbose and console:
585596
status = console.status("[cyan]Exécution de BCASL (headless)...[/cyan]", spinner="dots")
597+
register_cli_status(status)
586598
status.start()
587599

588600
class CliBcaslHost:
@@ -629,28 +641,36 @@ def append(self, msg: str):
629641
if _CLI_CANCEL_EVENT.is_set():
630642
if status:
631643
status.stop()
644+
unregister_cli_status(status)
632645
error("BCASL annule par l'utilisateur (Ctrl+C).")
633646
return 1
634647

635648
if report and hasattr(report, "ok") and not getattr(report, "ok"):
636649
if status:
637650
status.stop()
651+
unregister_cli_status(status)
638652
error("\nBCASL found issues.")
639653
return 1
640654

641655
if status:
642656
status.stop()
657+
unregister_cli_status(status)
643658
success("\nBCASL completed successfully.")
644659
return 0
645660
except Exception as exc:
646661
if status:
647662
status.stop()
663+
unregister_cli_status(status)
648664
error(f"BCASL failed: {exc}")
649665
return 1
650666
finally:
651667
signal.signal(signal.SIGINT, old_handler)
652668
if status:
653-
status.stop()
669+
try:
670+
status.stop()
671+
except Exception:
672+
pass
673+
unregister_cli_status(status)
654674

655675

656676
def launch_gui(*, legacy: bool = False) -> int:

0 commit comments

Comments
 (0)