Check Point
Rolling release / release (push) Successful in 6m36s

This commit is contained in:
2026-09-12 09:07:09 -04:00
parent e9040d110a
commit a4073b9020
63 changed files with 2459 additions and 632 deletions
@@ -18,14 +18,23 @@ const GLYPHS = { ARMED: '◯', LISTENING: '◌', THINKING: '◉', SPEAKING: '◎
class JarvisProxy {
async connect() {
const proxy = await new Promise((resolve, reject) => Gio.DBusProxy.new(
Gio.DBus.session, Gio.DBusProxyFlags.DO_NOT_AUTO_START, null, BUS, PATH, IFACE, null,
if (this._closed) return this;
if (this.proxy) return this;
if (this._connecting) {
await this._connecting.catch(() => {});
return this;
}
this._connecting = new Promise((resolve, reject) => Gio.DBusProxy.new(
Gio.DBus.session, Gio.DBusProxyFlags.NONE, null, BUS, PATH, IFACE, null,
(source, result) => { try { resolve(Gio.DBusProxy.new_finish(result)); } catch (error) { reject(error); } },
));
if (this._closed) { proxy.run_dispose(); return this; }
this.proxy = proxy;
this._ownerSignal = proxy.connect('notify::g-name-owner', () => this.onOwnerChanged?.(this.owned()));
return this;
try {
const proxy = await this._connecting;
if (this._closed) { proxy.run_dispose(); return this; }
this.proxy = proxy;
this._ownerSignal = proxy.connect('notify::g-name-owner', () => this.onOwnerChanged?.(this.owned()));
return this;
} finally { this._connecting = null; }
}
owned() { try { return Boolean(this.proxy?.g_name_owner || this.proxy?.get_name_owner?.()); } catch { return false; } }
call(name, signature = null, value = null) {
@@ -47,7 +56,7 @@ export default class JarvisExtension extends Extension {
this.settings = this.getSettings();
this.proxy = new JarvisProxy();
this.popup = new ConversationView({ compact: true });
this.osd = new JarvisOsd(); this.osd.attach();
this.osd = new JarvisOsd();
this.cu = new ComputerUseChrome(); this.cu.attach();
this.session = new SessionPanel(); this.session.attach();
this._bindSurface(this.popup);
@@ -55,7 +64,11 @@ export default class JarvisExtension extends Extension {
this._applyAccessibility();
this._removeShellService = installShellService();
this._indicator = new PanelMenu.Button(0.0, 'Jarvis QVAC', false); this._indicator.accessible_name = 'Jarvis voice assistant';
this._glyph = new St.Label({ text: '◯ Jarvis', style_class: 'jarvis-panel-glyph', y_align: Clutter.ActorAlign.CENTER }); this._glyph.accessible_name = 'Jarvis idle'; this._indicator.add_child(this._glyph);
this._panelBox = new St.BoxLayout({ style_class: 'jarvis-panel-box', y_align: Clutter.ActorAlign.CENTER });
this._glyph = new St.Label({ text: '◯ Jarvis', style_class: 'jarvis-panel-glyph', y_align: Clutter.ActorAlign.CENTER }); this._glyph.accessible_name = 'Jarvis idle';
this._panelBox.add_child(this._glyph);
this.osd.attach(this._panelBox);
this._indicator.add_child(this._panelBox);
Main.panel.addToStatusArea('jarvis-qvac', this._indicator, 0, 'right');
this._mountPopup();
this._indicator.connect('button-press-event', (_actor, event) => {
@@ -173,28 +186,41 @@ export default class JarvisExtension extends Extension {
const proxy = this.proxy;
try {
await proxy.connect(); if (this.proxy !== proxy) return;
this._signals = [
['StateChanged', (state) => { this._setState(state); this._refreshVoiceStatus(); }],
['ContextReset', () => { this._eachView((view) => { view.clear(); view.setNotice('New conversation'); }); }],
['WakeHeard', () => this.osd.showWake()],
['PartialTranscript', (text) => this._eachView((view) => view.addRow('U', text))],
['Token', (text) => this._eachView((view) => view.token(text))],
['Thinking', (text) => this._eachView((view) => view.updateThinking(text))],
['ToolCall', (json) => this._eachView((view) => view.addToolCall(json))],
['ToolResult', (json) => this._eachView((view) => view.addToolResult(json))],
['Reply', (text) => this._eachView((view) => view.finalizeReply(text))],
['SpeakingLevel', () => {}],
['ListeningLevel', () => {}],
['ChipOffered', (id, label, payload) => this._eachView((view) => view.addChip(id, label, payload))],
['JobProgress', (id, pct, label) => this.cu.addJob(id, pct, label)],
['ComputerStep', (json) => { this.cu.addStep(json); this._eachView((view) => view.addStep(json)); }],
['ComputerHighlight', (json) => this.cu.setTarget(json)],
['ConfirmationRequired', (tool, args, pattern) => { this._eachView((view) => view.offerConfirm(tool, args, pattern)); this._openPopup(); }],
['Error', (code, message) => { this._eachView((view) => { view.setNotice(message); view.finishThinking(); }); if (coerceText(code) === 'VOICE_UNAVAILABLE') this._eachView((view) => view.setConnectionStatus('voice-unavailable')); }],
].map(([name, handler]) => proxy.on(name, handler));
proxy.onOwnerChanged = () => { if (this.proxy !== proxy) return; this._syncDaemon(); };
if (!this._signals) {
this._signals = [
['StateChanged', (state) => { this._setState(state); this._refreshVoiceStatus(); }],
['ContextReset', () => { this._eachView((view) => { view.clear(); view.setNotice('New conversation'); }); }],
['WakeHeard', () => this.osd.showWake()],
['PartialTranscript', (text) => this._eachView((view) => view.addRow('U', text))],
['Token', (text) => this._eachView((view) => view.token(text))],
['Thinking', (text) => this._eachView((view) => view.updateThinking(text))],
['ToolCall', (json) => this._eachView((view) => view.addToolCall(json))],
['ToolResult', (json) => this._eachView((view) => view.addToolResult(json))],
['Reply', (text) => this._eachView((view) => view.finalizeReply(text))],
['SpeakingLevel', () => {}],
['ListeningLevel', () => {}],
['ChipOffered', (id, label, payload) => this._eachView((view) => view.addChip(id, label, payload))],
['JobProgress', (id, pct, label) => this.cu.addJob(id, pct, label)],
['ComputerStep', (json) => { this.cu.addStep(json); this._eachView((view) => view.addStep(json)); }],
['ComputerHighlight', (json) => this.cu.setTarget(json)],
['ConfirmationRequired', (tool, args, pattern) => { this._eachView((view) => view.offerConfirm(tool, args, pattern)); this._openPopup(); }],
['Error', (code, message) => { this._eachView((view) => { view.setNotice(message); view.finishThinking(); }); if (coerceText(code) === 'VOICE_UNAVAILABLE') this._eachView((view) => view.setConnectionStatus('voice-unavailable')); }],
].map(([name, handler]) => proxy.on(name, handler));
proxy.onOwnerChanged = () => { if (this.proxy !== proxy) return; this._syncDaemon(); };
}
await this._syncDaemon();
} catch (error) { if (this.proxy !== proxy) return; this._eachView((view) => view.setConnectionStatus('offline')); log(`Jarvis daemon unavailable: ${error.message}`); }
if (this.proxy !== proxy) return;
this._keepSyncing();
}
_keepSyncing() {
if (this._syncTimer) return;
this._syncTimer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 2000, () => {
if (!this.proxy || this.proxy._closed) { this._syncTimer = 0; return GLib.SOURCE_REMOVE; }
if (!this.proxy.proxy) this._connectDaemon();
else this._syncDaemon();
return GLib.SOURCE_CONTINUE;
});
}
async _syncDaemon() {
if (!this.proxy) return;
@@ -203,6 +229,9 @@ export default class JarvisExtension extends Extension {
const result = await this.proxy.call('GetState'); if (!this.popup) return;
this._setState(result.deep_unpack()[0]);
await this._refreshVoiceStatus();
this._eachView((view) => {
if (/daemon is unavailable|Jarvis is starting/i.test(view.notice?.text || '')) view.setNotice('');
});
} catch (error) { this._eachView((view) => view.setConnectionStatus('offline')); log(`Jarvis daemon unavailable: ${error.message}`); }
}
async _refreshVoiceStatus() {
@@ -220,8 +249,14 @@ export default class JarvisExtension extends Extension {
} catch { this._eachView((view) => view.setConnectionStatus('voice-unavailable')); }
}
_call(name, signature, value) {
if (!this.proxy?.owned?.()) {
if (name !== 'PushToTalk') this._eachView((view) => view.setNotice('Jarvis is starting…'));
this._connectDaemon();
return;
}
this.proxy.call(name, signature, value).catch((error) => {
log(`Jarvis ${name}: ${error.message}`);
if (name === 'PushToTalk') return;
const fallback = name === 'ResetContext' ? 'Could not reset conversation' : `${name} failed: ${shortError(error.message)}`;
this._eachView((view) => view.setNotice(fallback));
});
@@ -258,6 +293,7 @@ export default class JarvisExtension extends Extension {
if (this._settingsChanged) this.settings.disconnect(this._settingsChanged);
if (this._styleChanged) this.settings.disconnect(this._styleChanged);
if (this._menuState) try { this._indicator.menu.disconnect(this._menuState); } catch {}
if (this._syncTimer) { try { GLib.Source.remove(this._syncTimer); } catch {} this._syncTimer = 0; }
this._signals?.forEach((id) => this.proxy?.proxy?.disconnect(id));
this.proxy?.close();
this.osd?.destroy(); this.cu?.destroy(); this.session?.destroy(); this.popup?.destroy();