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).
159 lines
4.1 KiB
Python
159 lines
4.1 KiB
Python
"""Thin wrappers around the store modules + helpers the GUI pages share.
|
|
|
|
Putting these here means each page imports `from gui import services as svc`
|
|
rather than importing internal store modules directly. If we later swap an
|
|
implementation (e.g. switching memory backend), only this file changes.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
|
|
# Make sure project root is on sys.path when launched as `python -m gui`
|
|
# from inside C:\Jarvis\python\.
|
|
_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
if _PROJECT_ROOT not in sys.path:
|
|
sys.path.insert(0, _PROJECT_ROOT)
|
|
|
|
import memory_store
|
|
import profiles_store
|
|
import macros_store
|
|
import scheduler_store
|
|
import plugins_store
|
|
import llm_backend
|
|
|
|
|
|
# ---------- memory ----------
|
|
|
|
def memory_all() -> list[dict]:
|
|
return memory_store.all_facts()
|
|
|
|
|
|
def memory_remember(key: str, value: str) -> None:
|
|
memory_store.remember(key, value)
|
|
|
|
|
|
def memory_forget(key: str) -> bool:
|
|
return memory_store.forget(key)
|
|
|
|
|
|
def memory_search(query: str, limit: int = 5) -> list[dict]:
|
|
return memory_store.search(query, limit)
|
|
|
|
|
|
# ---------- profiles ----------
|
|
|
|
def profile_list() -> list[str]:
|
|
return profiles_store.list_names()
|
|
|
|
|
|
def profile_active() -> str:
|
|
return profiles_store.active_name()
|
|
|
|
|
|
def profile_set(name: str) -> dict:
|
|
return profiles_store.set_active(name)
|
|
|
|
|
|
# ---------- macros ----------
|
|
|
|
def macros_all() -> list[dict]:
|
|
out = []
|
|
for name in macros_store.list_names():
|
|
m = macros_store.get(name) or {}
|
|
out.append({
|
|
"name": name,
|
|
"steps_count": len(m.get("steps", [])),
|
|
"steps": m.get("steps", []),
|
|
"created_at": m.get("created_at", 0),
|
|
"last_run": m.get("last_run"),
|
|
})
|
|
return out
|
|
|
|
|
|
def macros_delete(name: str) -> bool:
|
|
return macros_store.delete(name)
|
|
|
|
|
|
def macros_recording() -> tuple[bool, str | None]:
|
|
return macros_store.is_recording(), macros_store.recording_name()
|
|
|
|
|
|
# ---------- scheduler ----------
|
|
|
|
def scheduler_all() -> list[dict]:
|
|
return scheduler_store.list_all()
|
|
|
|
|
|
def scheduler_remove(task_id: str) -> bool:
|
|
return scheduler_store.remove(task_id)
|
|
|
|
|
|
# ---------- plugins ----------
|
|
|
|
def plugins_all() -> list[dict]:
|
|
return plugins_store.list_packs()
|
|
|
|
|
|
def plugins_open_folder() -> str:
|
|
p = plugins_store.plugins_dir()
|
|
if os.name == "nt":
|
|
try:
|
|
subprocess.Popen(["explorer.exe", p])
|
|
except OSError:
|
|
pass
|
|
return p
|
|
|
|
|
|
def plugins_set_enabled(name: str, enabled: bool) -> tuple[bool, str]:
|
|
return plugins_store.set_enabled(name, enabled)
|
|
|
|
|
|
# ---------- LLM ----------
|
|
|
|
def llm_active() -> tuple[str, str]:
|
|
return llm_backend.current_backend(), llm_backend.current_model()
|
|
|
|
|
|
def llm_swap(target: str) -> str:
|
|
return llm_backend.swap_to(target)
|
|
|
|
|
|
# ---------- assistant launcher ----------
|
|
|
|
def assistant_run() -> bool:
|
|
"""Spawn main.py in a detached subprocess. Returns True on launch."""
|
|
try:
|
|
main_py = os.path.join(_PROJECT_ROOT, "main.py")
|
|
# CREATE_NEW_PROCESS_GROUP so it doesn't get killed with the GUI
|
|
creationflags = 0
|
|
if os.name == "nt":
|
|
creationflags = 0x00000200 # CREATE_NEW_PROCESS_GROUP
|
|
subprocess.Popen(
|
|
[sys.executable, main_py],
|
|
cwd=_PROJECT_ROOT,
|
|
creationflags=creationflags,
|
|
)
|
|
return True
|
|
except OSError as e:
|
|
print(f"[gui] failed to launch assistant: {e}")
|
|
return False
|
|
|
|
|
|
def assistant_is_running() -> bool:
|
|
"""Crude check: look for a python process running main.py from our dir.
|
|
|
|
Implementation note: we deliberately avoid pulling psutil — keeps the GUI
|
|
install footprint small. The Rust fork uses sysinfo; here we just scan
|
|
`tasklist` on Windows.
|
|
"""
|
|
if os.name != "nt":
|
|
return False
|
|
try:
|
|
out = subprocess.run(
|
|
["tasklist", "/FI", "IMAGENAME eq python.exe", "/V", "/FO", "CSV"],
|
|
capture_output=True, text=True, timeout=2,
|
|
)
|
|
except (OSError, subprocess.SubprocessError):
|
|
return False
|
|
return "main.py" in out.stdout
|