From 9b1ceda01f68d3e7cfdf9a92e51505344afb5555 Mon Sep 17 00:00:00 2001 From: Raven Scott Date: Fri, 11 Sep 2026 20:16:47 -0400 Subject: [PATCH] Updates --- .../__pycache__/main.cpython-314.pyc | Bin 27249 -> 28781 bytes .../jarvis@qvac.local/extension.js | 43 ++++++++++++++---- .../jarvis@qvac.local/stylesheet.css | 3 ++ daemon/dbus-service.js | 8 +++- daemon/harness-bridge.js | 2 +- daemon/index.js | 3 ++ dbus/io.qvac.Jarvis.Session.xml | 1 + skills/voice-prompt.js | 5 ++ test/gnome-extension.test.js | 3 ++ 9 files changed, 57 insertions(+), 11 deletions(-) diff --git a/apps/control-center/__pycache__/main.cpython-314.pyc b/apps/control-center/__pycache__/main.cpython-314.pyc index d9e7f307f8c0742521d366548d0439d36e39f7ee..63f8536d0bcac497755d291212d29d877c6cce8d 100644 GIT binary patch delta 5609 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 W-_j+XjLhk>AFn*SX;zo5asC(GV06L& diff --git a/apps/gnome-extension/jarvis@qvac.local/extension.js b/apps/gnome-extension/jarvis@qvac.local/extension.js index 512c449..02c04cc 100644 --- a/apps/gnome-extension/jarvis@qvac.local/extension.js +++ b/apps/gnome-extension/jarvis@qvac.local/extension.js @@ -66,6 +66,12 @@ class ArcOverlay { if (this.status.clutter_text) this.status.clutter_text.ellipsize = Pango.EllipsizeMode.END; this.header.add_child(this.title); this.header.add_child(new St.Widget({ x_expand: true })); this.header.add_child(this.status); this.statusLine = new St.Label({ text: 'Super+Shift+J · Hold Talk to speak', style_class: 'jarvis-status-line' }); + this.thinkingToggle = new St.Button({ label: 'Thinking', style_class: 'jarvis-thinking-toggle', visible: false, can_focus: true, reactive: true }); + this.thinkingToggle.accessible_name = 'Show or hide Jarvis thinking'; + this.thinkingToggle.connect('clicked', () => this.toggleThinking()); + this.thinking = new St.Label({ text: '', style_class: 'jarvis-thinking', visible: false, x_expand: true, can_focus: true }); + this.thinking.accessible_name = 'Jarvis thinking'; + if (this.thinking.clutter_text) { this.thinking.clutter_text.line_wrap = true; this.thinking.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; } this.job = new St.Label({ text: '', style_class: 'jarvis-job', can_focus: true, visible: false }); this.job.accessible_name = 'Jarvis job progress'; this.target = new St.Label({ text: '', style_class: 'jarvis-target', can_focus: true, visible: false }); this.target.accessible_name = 'Computer use target'; this.cursor = new St.Label({ text: '', style_class: 'jarvis-agent-cursor', can_focus: false, visible: false }); this.cursor.accessible_name = 'Visible computer use cursor'; @@ -80,7 +86,7 @@ class ArcOverlay { const chip = new St.Button({ label: mode, style_class: 'jarvis-chip', can_focus: true, reactive: true }); chip.accessible_name = `${mode} mode`; chip.connect('clicked', () => this.onMode?.(mode.toLowerCase())); this.chips.add_child(chip); } - this.root.add_child(this.header); this.root.add_child(this.statusLine); this.root.add_child(this.job); this.root.add_child(this.target); this.root.add_child(this.cursor); this.root.add_child(this.wave); this.root.add_child(this.scroll); this.root.add_child(this.chips); + this.root.add_child(this.header); this.root.add_child(this.statusLine); this.root.add_child(this.thinkingToggle); this.root.add_child(this.thinking); this.root.add_child(this.job); this.root.add_child(this.target); this.root.add_child(this.cursor); this.root.add_child(this.wave); this.root.add_child(this.scroll); this.root.add_child(this.chips); this.controls = new St.BoxLayout({ style_class: 'jarvis-chips' }); this.talk = new St.Button({ label: 'Talk', style_class: 'jarvis-chip jarvis-talk', reactive: true, can_focus: true }); this.talk.accessible_name = 'Hold Space or Enter to talk'; @@ -91,7 +97,7 @@ class ArcOverlay { this.talk.connect('button-press-event', () => { this.onTalk?.(true); return Clutter.EVENT_STOP; }); this.talk.connect('button-release-event', () => { this.onTalk?.(false); return Clutter.EVENT_STOP; }); this.controls.add_child(this.talk); - for (const [label, action] of [['Stop', () => this.onStop?.()], ['Close', () => this.hide()]]) { + for (const [label, action] of [['Stop', () => this.onStop?.()], ['Minimize', () => this.minimize()], ['Close', () => this.hide()]]) { const button = new St.Button({ label, style_class: 'jarvis-chip', reactive: true, can_focus: true }); button.connect('clicked', action); this.controls.add_child(button); } @@ -101,7 +107,8 @@ class ArcOverlay { this.reducedMotion = false; } attach() { Main.layoutManager.addChrome(this.root, { affectsStruts: false, trackFullscreen: false }); this.halo = new St.Widget({ style_class: 'jarvis-halo', reactive: false }); Main.layoutManager.addChrome(this.halo, { affectsStruts: false, trackFullscreen: false }); this.halo.hide(); this.hide(); } - show() { + show(force = false) { + if (this.minimized && !force) return; const monitor = Main.layoutManager.primaryMonitor; if (monitor) { const width = Math.min(OVERLAY_WIDTH, monitor.width - 80); @@ -113,12 +120,27 @@ class ArcOverlay { this.root.visible = true; if (!wasVisible) this.entry.grab_key_focus(); } - hide() { this.onTalk?.(false); this.root.visible = false; } - toggle() { this.root.visible ? this.hide() : this.show(); } - clear() { this.transcript.destroy_all_children(); } - addRow(who, text) { const body = safeText(text); const row = new St.Label({ text: `${who === 'J' ? 'J' : 'YOU'} ${body}`, style_class: `jarvis-row jarvis-row-${who === 'J' ? 'jarvis' : 'user'}`, can_focus: true }); row.accessible_name = `${who === 'J' ? 'Jarvis' : 'You'}: ${body}`; if (row.clutter_text) { row.clutter_text.line_wrap = true; row.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR; row.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; } this.transcript.add_child(row); if (this.transcript.get_n_children() > 100) this.transcript.get_first_child().destroy(); this.show(); return row; } - token(text) { const chunk = safeText(text); if (!chunk) return; let row = this.transcript.get_last_child?.(); if (!row || !String(row.style_class || '').includes('jarvis-row-jarvis')) { row = this.addRow('J', ''); } row.text = `${row.text}${chunk}`; row.accessible_name = `Jarvis: ${row.text}`; this.show(); } + hide() { this.onTalk?.(false); this.root.visible = false; this.minimized = false; } + minimize() { this.onTalk?.(false); this.root.visible = false; this.minimized = true; } + toggle() { this.root.visible ? this.hide() : this.show(true); } + clear() { this.transcript.destroy_all_children(); this.thinking.text = ''; this.thinking.visible = false; this.thinkingToggle.visible = false; } + _followConversation() { + const adjustment = this.scroll?.get_vadjustment?.() || this.scroll?.vadjustment; + if (!adjustment) return; + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE || GLib.PRIORITY_DEFAULT, () => { + try { adjustment.value = Math.max(0, adjustment.upper - adjustment.page_size); } catch {} + return GLib.SOURCE_REMOVE; + }); + } + addRow(who, text) { const body = safeText(text); const row = new St.Label({ text: `${who === 'J' ? 'J' : 'YOU'} ${body}`, style_class: `jarvis-row jarvis-row-${who === 'J' ? 'jarvis' : 'user'}`, can_focus: true }); row.accessible_name = `${who === 'J' ? 'Jarvis' : 'You'}: ${body}`; if (row.clutter_text) { row.clutter_text.line_wrap = true; row.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR; row.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; } this.transcript.add_child(row); if (this.transcript.get_n_children() > 100) this.transcript.get_first_child().destroy(); this.show(); this._followConversation(); return row; } + token(text) { const chunk = safeText(text); if (!chunk) return; let row = this.transcript.get_last_child?.(); if (!row || !String(row.style_class || '').includes('jarvis-row-jarvis')) { row = this.addRow('J', ''); } row.text = `${row.text}${chunk}`; row.accessible_name = `Jarvis: ${row.text}`; this.show(); this._followConversation(); } + updateThinking(text) { const chunk = safeText(text); if (!chunk) return; this.thinking.text = `${this.thinking.text || ''}${chunk}`; this.thinkingToggle.visible = true; this.thinking.visible = true; this.thinkingToggle.label = 'Thinking ▾'; this.show(); } + toggleThinking() { this.thinking.visible = !this.thinking.visible; this.thinkingToggle.label = this.thinking.visible ? 'Thinking ▾' : 'Thinking ▸'; } + finishThinking() { if (this.thinking.text) { this.thinkingToggle.visible = true; this.thinkingToggle.label = 'Thinking ▸'; this.thinking.visible = false; } } + addToolCall(json) { try { const call = JSON.parse(json); this.addRow('J', `Using ${safeText(call.name || 'tool')}…`); } catch { this.addRow('J', `Using ${safeText(json)}…`); } this.finishThinking(); } + addToolResult(json) { try { const result = JSON.parse(json); this.addRow('J', `${safeText(result.name || 'tool')} complete`); } catch { this.addRow('J', 'Tool complete'); } this.finishThinking(); } finalizeReply(text) { + this.finishThinking(); const spoken = safeText(text); let row = this.transcript.get_last_child?.(); if (row && String(row.style_class || '').includes('jarvis-row-jarvis')) { @@ -175,6 +197,9 @@ export default class JarvisExtension extends Extension { ['WakeHeard', () => this.overlay.show()], ['PartialTranscript', (text) => this.overlay.addRow('U', text)], ['Token', (text) => this.overlay.token(text)], + ['Thinking', (text) => this.overlay.updateThinking(text)], + ['ToolCall', (json) => this.overlay.addToolCall(json)], + ['ToolResult', (json) => this.overlay.addToolResult(json)], ['Reply', (text) => { this.overlay.finalizeReply(text); this.overlay.show(); }], ['SpeakingLevel', (rms) => this.overlay.level(rms)], ['ListeningLevel', (rms) => this.overlay.level(rms)], @@ -182,7 +207,7 @@ export default class JarvisExtension extends Extension { ['JobProgress', (id, pct, label) => this.overlay.addJob(id, pct, label)], ['ComputerStep', (json) => this.overlay.addStep(json)], ['ComputerHighlight', (json) => this.overlay.setTarget(json)], - ['Error', (code, message) => { this.overlay.addRow('J', message); if (coerceText(code) === 'VOICE_UNAVAILABLE') this.overlay.setConnectionStatus('voice-unavailable'); }], + ['Error', (code, message) => { this.overlay.addRow('J', message); this.overlay.finishThinking(); if (coerceText(code) === 'VOICE_UNAVAILABLE') this.overlay.setConnectionStatus('voice-unavailable'); }], ].map(([name, handler]) => proxy.on(name, handler)); proxy.onOwnerChanged = () => { if (this.proxy !== proxy) return; this._syncDaemon(); }; await this._syncDaemon(); diff --git a/apps/gnome-extension/jarvis@qvac.local/stylesheet.css b/apps/gnome-extension/jarvis@qvac.local/stylesheet.css index 9f802f3..c772302 100644 --- a/apps/gnome-extension/jarvis@qvac.local/stylesheet.css +++ b/apps/gnome-extension/jarvis@qvac.local/stylesheet.css @@ -4,6 +4,9 @@ .jarvis-title { font-weight: bold; letter-spacing: 1px; } .jarvis-local { color: var(--jarvis-accent, #F4B942); font-size: 11px; } .jarvis-status-line { color: #aeb6c8; font-size: 12px; } +.jarvis-thinking-toggle { color: #9aa8c0; font-size: 12px; padding: 4px 8px; border-radius: 8px; background-color: rgba(255, 255, 255, .05); } +.jarvis-thinking-toggle:hover, .jarvis-thinking-toggle:focus { color: #f6f7fb; background-color: rgba(79, 210, 255, .16); } +.jarvis-thinking { color: #aeb6c8; font-size: 12px; padding: 8px 12px; border-left: 2px solid #4FD2FF; background-color: rgba(79, 210, 255, .06); border-radius: 8px; } .jarvis-job, .jarvis-target { color: #4FD2FF; font-size: 12px; } .jarvis-agent-cursor { color: var(--jarvis-accent, #F4B942); font-size: 22px; } .jarvis-halo { border-top: 2px solid #F4B942; margin: 12px; } diff --git a/daemon/dbus-service.js b/daemon/dbus-service.js index 5d8c627..3b44a2b 100644 --- a/daemon/dbus-service.js +++ b/daemon/dbus-service.js @@ -54,6 +54,9 @@ export async function serveOnSessionBus(daemon) { JobProgress(id, pct, label) { return [String(id), Number(pct), String(label)]; } ComputerStep(json) { return String(json); } ComputerHighlight(json) { return String(json); } + Thinking(text) { return String(text); } + ToolCall(json) { return String(json); } + ToolResult(json) { return String(json); } } Session.configureMembers({ methods: { @@ -92,6 +95,9 @@ export async function serveOnSessionBus(daemon) { ComputerStep: { signature: 's' }, ComputerHighlight: { signature: 's' }, ConfirmationRequired: { signature: 'sss' }, + Thinking: { signature: 's' }, + ToolCall: { signature: 's' }, + ToolResult: { signature: 's' }, }, }); const bus = dbus.sessionBus(); @@ -133,7 +139,7 @@ export async function serveOnSessionBus(daemon) { daemon.on('Token', (token) => iface.Token(token)); daemon.on('Error', (code, message) => iface.Error(code, message)); daemon.on('ConfirmationRequired', (event) => iface.ConfirmationRequired(event?.tool || 'action', JSON.stringify(event?.args || {}), event?.pattern || 'explicit confirmation required')); - for (const signal of ['WakeHeard', 'PartialTranscript', 'FinalTranscript', 'SpeakingLevel', 'ListeningLevel', 'ChipOffered', 'JobProgress', 'ComputerStep', 'ComputerHighlight']) { + for (const signal of ['WakeHeard', 'PartialTranscript', 'FinalTranscript', 'SpeakingLevel', 'ListeningLevel', 'ChipOffered', 'JobProgress', 'ComputerStep', 'ComputerHighlight', 'Thinking', 'ToolCall', 'ToolResult']) { daemon.on(signal, (...args) => iface[signal](...args)); } return bus; diff --git a/daemon/harness-bridge.js b/daemon/harness-bridge.js index 3b4fc56..29cbe6e 100644 --- a/daemon/harness-bridge.js +++ b/daemon/harness-bridge.js @@ -28,7 +28,7 @@ export class HarnessBridge extends EventEmitter { await closeQvac(); throw error; } - for (const event of ['agent_message_chunk', 'tool_call', 'permission', 'ask_user', 'cap-chunk', 'error']) { + for (const event of ['agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_result', 'permission', 'ask_user', 'cap-chunk', 'error']) { this.session.on(event, (payload) => { if (event === 'agent_message_chunk' && payload?.text) { const parsed = parseHudSidecar(payload.text); diff --git a/daemon/index.js b/daemon/index.js index bc8a95e..4a46c03 100644 --- a/daemon/index.js +++ b/daemon/index.js @@ -40,6 +40,9 @@ export class JarvisDaemon extends EventEmitter { this.telemetry = new RuntimeTelemetry(); this._telemetryTimer = setInterval(() => this.telemetry.sample(), 60_000); this._telemetryTimer.unref?.(); this.harness.on('agent_message_chunk', (ev) => this.emit('Token', ev?.text || ev?.delta || '')); + this.harness.on('agent_thought_chunk', (ev) => this.emit('Thinking', String(ev?.text || ev?.delta || ''))); + this.harness.on('tool_call', (ev) => this.emit('ToolCall', JSON.stringify({ name: ev?.call?.name || 'tool', arguments: ev?.call?.arguments || {} }))); + this.harness.on('tool_result', (ev) => this.emit('ToolResult', JSON.stringify({ name: ev?.name || 'tool', result: ev?.result || '', toolCallId: ev?.toolCallId || '' }))); this.harness.on('permission', (ev) => this.emit('ConfirmationRequired', ev)); this.harness.on('hud_sidecar', (ev) => this.emit('ChipOffered', 'sidecar', ev?.title || 'Suggested action', JSON.stringify(ev || {}))); } diff --git a/dbus/io.qvac.Jarvis.Session.xml b/dbus/io.qvac.Jarvis.Session.xml index f696237..daf938c 100644 --- a/dbus/io.qvac.Jarvis.Session.xml +++ b/dbus/io.qvac.Jarvis.Session.xml @@ -24,6 +24,7 @@ + diff --git a/skills/voice-prompt.js b/skills/voice-prompt.js index 9a104c6..f2e4898 100644 --- a/skills/voice-prompt.js +++ b/skills/voice-prompt.js @@ -4,6 +4,11 @@ Ground every desktop, file, memory, and model claim in a tool result. Never clai Computer use requires an explicit user grant; never click or type while it is inactive, locked, or revoked. Destructive actions require confirmation in both the HUD and spoken conversation. Prefer structured tools and accessibility references over coordinates. +For questions about the computer, files, processes, or system state, call the +most relevant registered tool before answering. Never say that computer use or +terminal access is unavailable unless a tool result reports that limitation. +When the user asks to use the command line or terminal, call run_terminal_cmd +and base the answer on its result; do not substitute a refusal or guess. When a useful follow-up action exists, append a HUD sidecar exactly as {"title":"...","chips":[{"id":"...","label":"..."}],"confirmation":null}. The sidecar is for the HUD and must not be spoken.`; diff --git a/test/gnome-extension.test.js b/test/gnome-extension.test.js index 154929c..43bf891 100644 --- a/test/gnome-extension.test.js +++ b/test/gnome-extension.test.js @@ -111,6 +111,9 @@ test('GNOME GI imports expose default namespaces and panel has a real menu', () assert.match(source, /new PanelMenu.Button\(0.0, 'Jarvis QVAC', false\)/); assert.match(source, /PushToTalk/); assert.match(source, /finalizeReply/); + assert.match(source, /\['Thinking'/); + assert.match(source, /\['ToolCall'/); + assert.match(source, /Minimize/); }); test('D-Bus confirmation signal does not call missing Interface.emit', () => {