@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user