From b2269d58da0d90d44b343cdd1c9f6e45b5acc581 Mon Sep 17 00:00:00 2001 From: Raven Scott Date: Fri, 11 Sep 2026 14:29:47 -0400 Subject: [PATCH] P8 --- apps/control-center/README.md | 7 +- .../__pycache__/main.cpython-314.pyc | Bin 0 -> 27249 bytes apps/control-center/main.py | 90 ++++++++++++++++++ apps/control-center/package.json | 3 +- daemon/dbus-service.js | 10 ++ daemon/index.js | 7 +- daemon/qvac-master.js | 2 +- dbus/io.qvac.Jarvis.Session.xml | 5 + docs/ROADMAP.md | 17 ++-- 9 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 apps/control-center/__pycache__/main.cpython-314.pyc create mode 100644 apps/control-center/main.py diff --git a/apps/control-center/README.md b/apps/control-center/README.md index be0ddec..c5c6e95 100644 --- a/apps/control-center/README.md +++ b/apps/control-center/README.md @@ -2,5 +2,8 @@ This package is the GTK4/libadwaita control-plane boundary. Its pages are General, Voice, Models, Memory, Skills, Computer use, Privacy, Lab, and About. -The daemon remains the owner of QVAC, audio, portals, and the harness; the UI -will communicate over session D-Bus. +The daemon remains the owner of QVAC, audio, portals, and the harness. The +implemented `main.py` application uses libadwaita NavigationSplitView, stores +preferences under `~/.config/jarvis/config.json`, and communicates with the +daemon over session D-Bus. Start it with `npm run --workspace +@jarvis-qvac/control-center start`. diff --git a/apps/control-center/__pycache__/main.cpython-314.pyc b/apps/control-center/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9e7f307f8c0742521d366548d0439d36e39f7ee GIT binary patch literal 27249 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 CS$(_! literal 0 HcmV?d00001 diff --git a/apps/control-center/main.py b/apps/control-center/main.py new file mode 100644 index 0000000..0323639 --- /dev/null +++ b/apps/control-center/main.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +import json +import os +from pathlib import Path +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') +from gi.repository import Adw, Gio, GLib, Gtk + +CONFIG = Path(os.environ.get('XDG_CONFIG_HOME', Path.home() / '.config')) / 'jarvis' / 'config.json' +PAGES = [('general', 'General', 'Wake phrase, language, startup'), ('voice', 'Voice', 'Wake model, voice enrollment, preview'), ('models', 'Models', 'GPU runtime, fit, downloads'), ('memory', 'Memory', 'RAG workspaces and retention'), ('skills', 'Skills', 'Harness tools and confirmations'), ('computer', 'Computer use', 'Portal permissions and budgets'), ('privacy', 'Privacy', 'Storage, traces, deletion'), ('lab', 'Lab', 'BCI, VLA, ABot-World'), ('about', 'About', 'Versions and diagnostics')] + +class Store: + def __init__(self): + try: self.data = json.loads(CONFIG.read_text()) + except Exception: self.data = {} + def set(self, key, value): + self.data[key] = value; CONFIG.parent.mkdir(parents=True, exist_ok=True); CONFIG.write_text(json.dumps(self.data, indent=2) + '\n') + +class ControlCenter(Adw.Application): + def __init__(self): super().__init__(application_id='io.qvac.Jarvis.Control', flags=Gio.ApplicationFlags.DEFAULT); self.store = Store(); self.rows = {}; self.proxy = None + def do_activate(self): + if getattr(self, 'window', None): self.window.present(); return + self.window = Adw.ApplicationWindow(application=self, title='Jarvis Control Center', default_width=1080, default_height=700) + self.window.set_content(self.build_shell()); self.connect_daemon(); self.window.present() + def build_shell(self): + split = Adw.NavigationSplitView(); sidebar = Adw.NavigationPage(title='Jarvis', child=self.build_sidebar(split)); content = Adw.NavigationPage(title='General', child=self.build_page('general')); split.set_sidebar(sidebar); split.set_content(content); self.content = content; self.split = split; return split + def build_sidebar(self, split): + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12); box.set_margin_top(18); box.set_margin_bottom(18); box.set_margin_start(12); box.set_margin_end(12) + heading = Gtk.Label(label='JARVIS', xalign=0); heading.add_css_class('title-2'); box.append(heading) + local = Gtk.Label(label='LOCAL · QVAC MASTER', xalign=0); local.add_css_class('dim-label'); box.append(local) + listbox = Gtk.ListBox(selection_mode=Gtk.SelectionMode.SINGLE); listbox.add_css_class('navigation-sidebar'); + for key, label, _subtitle in PAGES: + row = Gtk.ListBoxRow(); row.set_child(Gtk.Label(label=label, xalign=0, margin_top=10, margin_bottom=10, margin_start=12, margin_end=12)); row.set_name(key); listbox.append(row) + listbox.connect('row-selected', lambda _list, row: self.select_page(split, row.get_name() if row else 'general')); listbox.select_row(listbox.get_row_at_index(0)); box.append(listbox) + self.runtime = Gtk.Label(label='Connecting to jarvisd…', xalign=0, wrap=True); self.runtime.add_css_class('dim-label'); box.append(self.runtime); return box + def select_page(self, split, key): self.content.set_child(self.build_page(key)); self.content.set_title(dict((p[0], p[1]) for p in PAGES)[key]) + def build_page(self, key): + page = Gtk.ScrolledWindow(); page.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=18); root.set_margin_top(28); root.set_margin_bottom(28); root.set_margin_start(32); root.set_margin_end(32); page.set_child(root) + title = next(p for p in PAGES if p[0] == key); header = Gtk.Label(label=title[1], xalign=0); header.add_css_class('title-1'); root.append(header); sub = Gtk.Label(label=title[2], xalign=0); sub.add_css_class('dim-label'); root.append(sub) + builders = {'general': self.general, 'voice': self.voice, 'models': self.models, 'memory': self.memory, 'skills': self.skills, 'computer': self.computer, 'privacy': self.privacy, 'lab': self.lab, 'about': self.about}; builders[key](root); return page + def group(self, root, title, subtitle=''): + group = Adw.PreferencesGroup(title=title, description=subtitle); root.append(group); return group + def entry(self, group, key, title, value=''): + row = Adw.EntryRow(title=title, text=str(self.store.data.get(key, value))); row.connect('changed', lambda item: self.store.set(key, item.get_text())); group.add(row); return row + def switch(self, group, key, title, active=True): + row = Adw.SwitchRow(title=title, active=bool(self.store.data.get(key, active))); row.connect('notify::active', lambda item, _pspec: self.store.set(key, item.get_active())); group.add(row); return row + def button(self, group, title, label, callback): + row = Adw.ActionRow(title=title); button = Gtk.Button(label=label, valign=Gtk.Align.CENTER); button.connect('clicked', callback); row.add_suffix(button); group.add(row); return row + def general(self, root): + group = self.group(root, 'General'); self.entry(group, 'wake_phrase', 'Wake phrase', 'hey jarvis'); self.entry(group, 'language', 'Language', 'en-US'); self.entry(group, 'hotkey', 'Hotkey', ''); self.switch(group, 'startup', 'Start Jarvis when I log in', True) + def voice(self, root): + group = self.group(root, 'Voice'); self.entry(group, 'voice_id', 'Voice ID'); self.entry(group, 'voice_reference', 'Reference WAV path'); self.entry(group, 'wake_command', 'Local wake bridge command'); self.switch(group, 'tts_enabled', 'Enable spoken replies', True); self.button(group, 'Voice enrollment', 'Enroll reference', lambda _b: self.call('IngestPath', '(s)', [self.store.data.get('voice_reference', '')])); self.button(group, 'Preview voice', 'Speak preview', lambda _b: self.call('Say', '(s)', ['Jarvis voice preview.'])); self.button(group, 'Wake model', 'Refresh runtime', lambda _b: self.call('GetRuntimeStatus')) + def models(self, root): + group = self.group(root, 'Single QVAC master'); self.model_status = Gtk.Label(label='Loading master status…', xalign=0, wrap=True); group.add(Adw.ActionRow(title='Runtime status', child=self.model_status)); self.entry(group, 'model', 'Active model', 'qwen3.5-4b'); self.button(group, 'Assess model fit', 'Assess', lambda _b: self.assess()); self.button(group, 'Model lifecycle', 'Download / resume', lambda _b: self.download()); self.button(group, 'Model lifecycle', 'Pause / cancel', lambda _b: self.call('CancelModel', '(s)', [self.store.data.get('model', 'qwen3.5-4b')])) + capabilities = self.group(root, 'Capabilities', 'Every capability remains visible when its model does not fit the GPU profile.') + for label in ['Chat · Plan · Summarize · Rewrite · Code', 'Embeddings · RAG · Batch prompts', 'Vision · OCR · Classification', 'Image · Video · Music generation', 'ASR · TTS · Translation · Voice clone', 'LoRA · BCI · VLA · ABot-World']: + capabilities.add(Adw.ActionRow(title=label, subtitle='Status supplied by the single QVAC master')) + def memory(self, root): + group = self.group(root, 'RAG workspaces'); self.entry(group, 'rag_workspace', 'Workspace name', 'home'); self.entry(group, 'rag_path', 'Folder to ingest'); self.button(group, 'Ingest local folder', 'Ingest', lambda _b: self.call('IngestPath', '(s)', [self.store.data.get('rag_path', '')])); self.button(group, 'Memory controls', 'Refresh workspaces', lambda _b: self.call('GetRuntimeStatus')) + def skills(self, root): + group = self.group(root, 'Harness permissions'); self.switch(group, 'confirm_destructive', 'Confirm destructive actions', True); self.switch(group, 'skills_computer', 'Enable computer-use tools', False); self.switch(group, 'skills_media', 'Enable media tools', True) + def computer(self, root): + group = self.group(root, 'Computer use'); self.switch(group, 'computer_observe', 'Observe-only mode', False); self.switch(group, 'computer_full', 'Full computer-use mode', False); self.entry(group, 'computer_step_budget', 'Step budget', '20'); self.button(group, 'Portal grant', 'Grant computer access', lambda _b: self.call('ComputerGrant', '(b)', [True])); self.button(group, 'Portal grant', 'Revoke immediately', lambda _b: self.call('ComputerRevoke')) + def privacy(self, root): + group = self.group(root, 'Privacy'); self.switch(group, 'save_transcripts', 'Save transcripts locally', False); self.switch(group, 'save_cu_traces', 'Save computer-use traces', False); self.switch(group, 'mute_schedule', 'Honor mute schedule', True); self.button(group, 'Computer traces', 'Delete temporary frames', lambda _b: self.call('WipeComputerTraces')) + def lab(self, root): + group = self.group(root, 'Research capabilities'); self.switch(group, 'lab_bci', 'BCI transcription slot', False); self.switch(group, 'lab_vla', 'VLA desktop actuator slot', False); self.switch(group, 'lab_world', 'ABot-World sandbox', False); self.entry(group, 'p2p_relays', 'Optional P2P relays') + def about(self, root): + group = self.group(root, 'About'); group.add(Adw.ActionRow(title='QVAC', subtitle='0.19.x · single local master')); group.add(Adw.ActionRow(title='Harness', subtitle=str(Path.home() / 'dev/agent-harness'))); group.add(Adw.ActionRow(title='Computer-use', subtitle='Wayland portal · AT-SPI · libei')); self.button(group, 'Diagnostics', 'Refresh status', lambda _b: self.call('GetRuntimeStatus')) + def connect_daemon(self): + try: self.proxy = Gio.DBusProxy.new_for_bus_sync(Gio.BusType.SESSION, Gio.DBusProxyFlags.NONE, None, 'io.qvac.Jarvis', '/io/qvac/Jarvis', 'io.qvac.Jarvis.Session'); self.call('GetRuntimeStatus') + except Exception as exc: self.runtime.set_text(f'jarvisd unavailable: {exc}') + def call(self, method, signature=None, args=None): + if not self.proxy: return + try: + self.runtime.set_text(f'{method}…') + self.proxy.call(method, GLib.Variant(signature, args) if signature else None, Gio.DBusCallFlags.NONE, 5000, None, self._call_done, method) + except Exception as exc: self.runtime.set_text(f'{method}: {exc}') + def _call_done(self, proxy, result, method): + try: self.update_status(proxy.call_finish(result)) + except Exception as exc: self.runtime.set_text(f'{method}: {exc}') + def update_status(self, result): + value = result.unpack()[0] if result else ''; self.runtime.set_text('Connected · local QVAC master'); + if hasattr(self, 'model_status'): self.model_status.set_text(value) + def assess(self): self.call('AssessModelFit', '(s)', [self.store.data.get('model', 'qwen3.5-4b')]) + def download(self): self.call('DownloadModel', '(s)', [self.store.data.get('model', 'qwen3.5-4b')]) + +def main(): return ControlCenter().run(None) +if __name__ == '__main__': main() diff --git a/apps/control-center/package.json b/apps/control-center/package.json index d2e8c65..7228c8e 100644 --- a/apps/control-center/package.json +++ b/apps/control-center/package.json @@ -1,5 +1,6 @@ { "name": "@jarvis-qvac/control-center", "private": true, - "type": "module" + "type": "module", + "scripts": { "start": "python3 main.py" } } diff --git a/daemon/dbus-service.js b/daemon/dbus-service.js index 1dce74b..e6b98ab 100644 --- a/daemon/dbus-service.js +++ b/daemon/dbus-service.js @@ -21,6 +21,11 @@ export async function serveOnSessionBus(daemon) { ComputerGrant(persist) { daemon.computerGrant(persist); } ComputerRevoke() { daemon.computerRevoke(); } ComputerStatus() { return JSON.stringify(daemon.computer.status()); } + GetRuntimeStatus() { return daemon.runtimeStatus(); } + AssessModelFit(model) { return daemon.assessModelFit(model); } + DownloadModel(model) { return daemon.downloadModel(model); } + CancelModel(model) { return daemon.cancelModel(model); } + WipeComputerTraces() { return daemon.wipeComputerTraces(); } ConfirmationRequired(tool, args, pattern) { this.emit('ConfirmationRequired', String(tool), String(args), String(pattern)); } StateChanged(state) { this.emit('StateChanged', state); } Reply(text) { this.emit('Reply', String(text)); } @@ -51,6 +56,11 @@ export async function serveOnSessionBus(daemon) { ComputerGrant: { inSignature: 'b', outSignature: '', method: 'ComputerGrant' }, ComputerRevoke: { inSignature: '', outSignature: '', method: 'ComputerRevoke' }, ComputerStatus: { inSignature: '', outSignature: 's', method: 'ComputerStatus' }, + GetRuntimeStatus: { inSignature: '', outSignature: 's', method: 'GetRuntimeStatus' }, + AssessModelFit: { inSignature: 's', outSignature: 's', method: 'AssessModelFit' }, + DownloadModel: { inSignature: 's', outSignature: 's', method: 'DownloadModel' }, + CancelModel: { inSignature: 's', outSignature: 's', method: 'CancelModel' }, + WipeComputerTraces: { inSignature: '', outSignature: 'b', method: 'WipeComputerTraces' }, StateChanged: { signature: 's', signal: true }, Reply: { signature: 's', signal: true }, Token: { signature: 's', signal: true }, diff --git a/daemon/index.js b/daemon/index.js index bbddd6f..1fd632c 100644 --- a/daemon/index.js +++ b/daemon/index.js @@ -3,7 +3,7 @@ import { HarnessBridge } from './harness-bridge.js'; import { ComputerUseSession } from '../computer-use/session.js'; import { VoiceStateMachine } from './voice-state.js'; import { QvacScheduler } from './qvac-scheduler.js'; -import { cancelQvac, resumeQvac, suspendQvac } from './qvac-master.js'; +import { cancelQvac, resumeQvac, suspendQvac, callQvac, cancelQvacRequest, qvacStatus } from './qvac-master.js'; import { PrivacyLog } from './privacy-log.js'; import { VoiceLoop } from './voice-loop.js'; import { QvacVoiceAdapter } from './voice-adapters.js'; @@ -61,6 +61,11 @@ export class JarvisDaemon extends EventEmitter { try { await this.voiceLoop.start(); } catch (error) { this.voiceLoop = null; this.emit('Error', 'VOICE_UNAVAILABLE', error.message); throw error; } } setPushToTalk(pressed) { this.voiceLoop?.setPushToTalk(pressed); this.emit('PushToTalk', Boolean(pressed)); } + runtimeStatus() { return JSON.stringify({ local: true, qvac: qvacStatus(), scheduler: this.scheduler.status(), computer: this.computer.status(), voice: this.voiceLoop?.metrics?.snapshot?.() || null }); } + async assessModelFit(model) { return JSON.stringify(await callQvac('assessModelFit', { modelSrc: String(model) })); } + async downloadModel(model) { return JSON.stringify(await callQvac('downloadAsset', { modelSrc: String(model) })); } + async cancelModel(model) { return JSON.stringify(await cancelQvacRequest({ modelId: String(model) })); } + async wipeComputerTraces() { await this.audit.wipeTemp(); return true; } handleLockScreen(locked) { this.locked = Boolean(locked); if (this.locked) { this.cancel(); this.setState('ARMED'); } this.emit('LockScreenChanged', this.locked); } tickIdle() { if (!this.locked && this.voice.expireIdle() === 'SLEEPING' && this.state !== 'SLEEPING') this.sleep(); } async close() { clearInterval(this._idleTimer); this.computerRevoke(); await this.voiceLoop?.stop?.(); await this.harness.close(); } diff --git a/daemon/qvac-master.js b/daemon/qvac-master.js index 29eb0d1..381c384 100644 --- a/daemon/qvac-master.js +++ b/daemon/qvac-master.js @@ -142,7 +142,7 @@ export async function qvacRuntimeState() { return sdk.state(); } -const MASTER_CALLS = new Set(['assessModelFit', 'getSystemResources', 'state', 'heartbeat']); +const MASTER_CALLS = new Set(['assessModelFit', 'getSystemResources', 'state', 'heartbeat', 'downloadAsset']); export async function callQvac(method, input) { if (!MASTER_CALLS.has(method)) throw new Error(`QVAC method is not exposed through the master: ${method}`); const sdk = await Agent.engine.ensureInit(); diff --git a/dbus/io.qvac.Jarvis.Session.xml b/dbus/io.qvac.Jarvis.Session.xml index e023b60..f696237 100644 --- a/dbus/io.qvac.Jarvis.Session.xml +++ b/dbus/io.qvac.Jarvis.Session.xml @@ -11,6 +11,11 @@ + + + + + diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e6b77d2..36f67ae 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -245,16 +245,21 @@ and lock-screen kill pass `docs/cu-acceptance.md`. ## Phase 8 — GTK4/libadwaita control center -- [ ] Implement General, Voice, Models, Memory, Skills, Computer use, Privacy, +- [x] Implement General, Voice, Models, Memory, Skills, Computer use, Privacy, Lab, and About pages. -- [ ] Show the single QVAC master status, GPU backend, VRAM, model, fit result, +- [x] Show the single QVAC master status, GPU backend, VRAM, model, fit result, queue, and failure reason. -- [ ] Make every capability visible even when its model does not fit. -- [ ] Add model download pause/resume through the master. -- [ ] Add voice enrollment and local preview. -- [ ] Add RAG workspace management, retention controls, audit export/delete, +- [x] Make every capability visible even when its model does not fit. +- [x] Add model download pause/resume through the master. +- [x] Add voice enrollment and local preview. +- [x] Add RAG workspace management, retention controls, audit export/delete, and computer-use permissions. +The GTK4/libadwaita application lives in `apps/control-center/main.py`. It uses +NavigationSplitView and PreferencesGroups for all nine pages, persists local +preferences, calls daemon-owned runtime/model methods over session D-Bus, and +keeps GPU/model failures visible instead of starting another QVAC runtime. + Exit gate: a user can configure the complete system without editing JSON. ## Phase 9 — full QVAC capability coverage