feat: Wave 3 — plugin loader + Flet GUI (parity with Rust fork)

Plugin loader (mirrors Rust #29):
- plugins_store.py walks %APPDATA%\com.priler.jarvis\plugins\<name>\
  for commands.yaml fragments and merges them into VA_CMD_LIST at
  startup. Same schema as the project root commands.yaml. Per-pack
  `disabled` flag file skips a pack without deletion. Built-in ids
  win conflicts so a malicious pack can't silently override
  open_browser. 9 unit tests (parity with the Rust tests).
- main.py: plugin merge runs after the root yaml load; logs the
  number of extra commands picked up.

Flet GUI (#27 — first parity with the Rust Tauri GUI):
- New gui/ package: __main__.py launches via `python -m gui` (or
  gui.bat). app.py routes between 7 pages (Главная / Команды /
  Макросы / Расписание / Память / Плагины / Настройки) via
  NavigationRail; dark theme matching the Tauri look.
- gui/services.py centralises every store-module call the pages
  share — single place to swap implementations later.
- One file per page in gui/pages/, all expose `build(page)` and use
  the same card layout idiom. Pages mirror the Tauri ones feature-
  for-feature: home (status + counters + launcher), commands
  (filterable list of all VA_CMD_LIST entries), macros (list +
  delete + recording banner), scheduler (list + remove), memory
  (add/forget/search), plugins (toggle/open folder), settings
  (profile + LLM hot-swap).
- requirements.txt: flet>=0.85 (optional; assistant runs without).
- 3 smoke tests verify imports + build callables; skip cleanly when
  Flet isn't installed.

README: documents both the GUI and the plugin layout in user-facing
terms. Tests: 42 → 54 (+9 plugins +3 GUI smoke).
This commit is contained in:
Bossiara13 2026-05-16 13:27:04 +03:00
parent 2bef1d5ea7
commit 59a1258dcc
19 changed files with 1233 additions and 0 deletions

97
gui/pages/plugins.py Normal file
View file

@ -0,0 +1,97 @@
"""Plugins page — list user packs, toggle enabled, open folder."""
import flet as ft
from gui import services as svc
def build(page: ft.Page) -> ft.Control:
list_view = ft.Column(spacing=8, expand=True, scroll=ft.ScrollMode.AUTO)
def render():
list_view.controls.clear()
packs = svc.plugins_all()
if not packs:
list_view.controls.append(
ft.Column([
ft.Text(
"Плагинов пока нет. Создай папку с commands.yaml в "
"%APPDATA%\\com.priler.jarvis\\plugins\\, нажми «Обновить».",
color="#8aa0a8",
),
])
)
for p in packs:
list_view.controls.append(_pack_row(p, on_toggle=lambda n=p["name"], e=p["enabled"]: do_toggle(n, e)))
page.update()
def do_toggle(name: str, was_enabled: bool):
ok, msg = svc.plugins_set_enabled(name, not was_enabled)
page.open(ft.SnackBar(
content=ft.Text(
"Перезапусти main.py, чтобы применить." if ok else f"Ошибка: {msg}",
color="white",
),
bgcolor="#33d3a4" if ok else "#d8584f",
))
render()
def do_open(_):
path = svc.plugins_open_folder()
page.open(ft.SnackBar(content=ft.Text(f"Папка: {path}", color="white"), bgcolor="#33d3a4"))
render()
return ft.Column(
[
ft.Row([
ft.Text("Плагины", size=24, weight=ft.FontWeight.W_700),
ft.Row([
ft.OutlinedButton(text="Открыть папку", icon=ft.Icons.FOLDER_OPEN, on_click=do_open),
ft.FilledButton(text="Обновить", icon=ft.Icons.REFRESH, on_click=lambda e: render()),
]),
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
ft.Text(
"Папки в %APPDATA%\\com.priler.jarvis\\plugins\\, "
"каждая содержит свой commands.yaml. Изменения подхватываются после рестарта main.py.",
color="#8aa0a8", size=12,
),
list_view,
],
spacing=10,
expand=True,
)
def _pack_row(p: dict, on_toggle) -> ft.Control:
err = p.get("error")
return ft.Container(
content=ft.Column([
ft.Row([
ft.Text(p["name"], weight=ft.FontWeight.W_600, size=14),
ft.Container(
content=ft.Text(
"ошибка" if err else f"{p['command_count']} команд",
size=10, color="white",
),
bgcolor="#7a2c2c" if err else ("#1e6b5a" if p["enabled"] else "#404040"),
padding=ft.padding.symmetric(horizontal=8, vertical=2),
border_radius=10,
),
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
ft.Text(p["path"], size=10, color="#5a6a72", font_family="Cascadia Mono"),
ft.Text(err, size=11, color="#ff8888") if err else ft.Container(height=0),
ft.Row([
ft.Switch(
value=p["enabled"],
on_change=lambda e: on_toggle(),
disabled=bool(err),
),
ft.Text("Включён" if p["enabled"] else "Выключен",
size=11, color="#33d3a4" if p["enabled"] else "#8aa0a8"),
]),
], spacing=6),
bgcolor="#15252b",
border_radius=8,
padding=12,
)