Updates
Rolling release / release (push) Successful in 6m25s

This commit is contained in:
2026-09-11 20:16:47 -04:00
parent d8d4f18e10
commit 9b1ceda01f
9 changed files with 57 additions and 11 deletions
Binary file not shown.
@@ -66,6 +66,12 @@ class ArcOverlay {
if (this.status.clutter_text) this.status.clutter_text.ellipsize = Pango.EllipsizeMode.END; 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.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.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.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.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'; 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`; 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); 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.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 = 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'; 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-press-event', () => { this.onTalk?.(true); return Clutter.EVENT_STOP; });
this.talk.connect('button-release-event', () => { this.onTalk?.(false); return Clutter.EVENT_STOP; }); this.talk.connect('button-release-event', () => { this.onTalk?.(false); return Clutter.EVENT_STOP; });
this.controls.add_child(this.talk); 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 }); const button = new St.Button({ label, style_class: 'jarvis-chip', reactive: true, can_focus: true });
button.connect('clicked', action); this.controls.add_child(button); button.connect('clicked', action); this.controls.add_child(button);
} }
@@ -101,7 +107,8 @@ class ArcOverlay {
this.reducedMotion = false; 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(); } 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; const monitor = Main.layoutManager.primaryMonitor;
if (monitor) { if (monitor) {
const width = Math.min(OVERLAY_WIDTH, monitor.width - 80); const width = Math.min(OVERLAY_WIDTH, monitor.width - 80);
@@ -113,12 +120,27 @@ class ArcOverlay {
this.root.visible = true; this.root.visible = true;
if (!wasVisible) this.entry.grab_key_focus(); if (!wasVisible) this.entry.grab_key_focus();
} }
hide() { this.onTalk?.(false); this.root.visible = false; } hide() { this.onTalk?.(false); this.root.visible = false; this.minimized = false; }
toggle() { this.root.visible ? this.hide() : this.show(); } minimize() { this.onTalk?.(false); this.root.visible = false; this.minimized = true; }
clear() { this.transcript.destroy_all_children(); } toggle() { this.root.visible ? this.hide() : this.show(true); }
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; } clear() { this.transcript.destroy_all_children(); this.thinking.text = ''; this.thinking.visible = false; this.thinkingToggle.visible = false; }
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(); } _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) { finalizeReply(text) {
this.finishThinking();
const spoken = safeText(text); const spoken = safeText(text);
let row = this.transcript.get_last_child?.(); let row = this.transcript.get_last_child?.();
if (row && String(row.style_class || '').includes('jarvis-row-jarvis')) { if (row && String(row.style_class || '').includes('jarvis-row-jarvis')) {
@@ -175,6 +197,9 @@ export default class JarvisExtension extends Extension {
['WakeHeard', () => this.overlay.show()], ['WakeHeard', () => this.overlay.show()],
['PartialTranscript', (text) => this.overlay.addRow('U', text)], ['PartialTranscript', (text) => this.overlay.addRow('U', text)],
['Token', (text) => this.overlay.token(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(); }], ['Reply', (text) => { this.overlay.finalizeReply(text); this.overlay.show(); }],
['SpeakingLevel', (rms) => this.overlay.level(rms)], ['SpeakingLevel', (rms) => this.overlay.level(rms)],
['ListeningLevel', (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)], ['JobProgress', (id, pct, label) => this.overlay.addJob(id, pct, label)],
['ComputerStep', (json) => this.overlay.addStep(json)], ['ComputerStep', (json) => this.overlay.addStep(json)],
['ComputerHighlight', (json) => this.overlay.setTarget(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)); ].map(([name, handler]) => proxy.on(name, handler));
proxy.onOwnerChanged = () => { if (this.proxy !== proxy) return; this._syncDaemon(); }; proxy.onOwnerChanged = () => { if (this.proxy !== proxy) return; this._syncDaemon(); };
await this._syncDaemon(); await this._syncDaemon();
@@ -4,6 +4,9 @@
.jarvis-title { font-weight: bold; letter-spacing: 1px; } .jarvis-title { font-weight: bold; letter-spacing: 1px; }
.jarvis-local { color: var(--jarvis-accent, #F4B942); font-size: 11px; } .jarvis-local { color: var(--jarvis-accent, #F4B942); font-size: 11px; }
.jarvis-status-line { color: #aeb6c8; font-size: 12px; } .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-job, .jarvis-target { color: #4FD2FF; font-size: 12px; }
.jarvis-agent-cursor { color: var(--jarvis-accent, #F4B942); font-size: 22px; } .jarvis-agent-cursor { color: var(--jarvis-accent, #F4B942); font-size: 22px; }
.jarvis-halo { border-top: 2px solid #F4B942; margin: 12px; } .jarvis-halo { border-top: 2px solid #F4B942; margin: 12px; }
+7 -1
View File
@@ -54,6 +54,9 @@ export async function serveOnSessionBus(daemon) {
JobProgress(id, pct, label) { return [String(id), Number(pct), String(label)]; } JobProgress(id, pct, label) { return [String(id), Number(pct), String(label)]; }
ComputerStep(json) { return String(json); } ComputerStep(json) { return String(json); }
ComputerHighlight(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({ Session.configureMembers({
methods: { methods: {
@@ -92,6 +95,9 @@ export async function serveOnSessionBus(daemon) {
ComputerStep: { signature: 's' }, ComputerStep: { signature: 's' },
ComputerHighlight: { signature: 's' }, ComputerHighlight: { signature: 's' },
ConfirmationRequired: { signature: 'sss' }, ConfirmationRequired: { signature: 'sss' },
Thinking: { signature: 's' },
ToolCall: { signature: 's' },
ToolResult: { signature: 's' },
}, },
}); });
const bus = dbus.sessionBus(); const bus = dbus.sessionBus();
@@ -133,7 +139,7 @@ export async function serveOnSessionBus(daemon) {
daemon.on('Token', (token) => iface.Token(token)); daemon.on('Token', (token) => iface.Token(token));
daemon.on('Error', (code, message) => iface.Error(code, message)); 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')); 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)); daemon.on(signal, (...args) => iface[signal](...args));
} }
return bus; return bus;
+1 -1
View File
@@ -28,7 +28,7 @@ export class HarnessBridge extends EventEmitter {
await closeQvac(); await closeQvac();
throw error; 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) => { this.session.on(event, (payload) => {
if (event === 'agent_message_chunk' && payload?.text) { if (event === 'agent_message_chunk' && payload?.text) {
const parsed = parseHudSidecar(payload.text); const parsed = parseHudSidecar(payload.text);
+3
View File
@@ -40,6 +40,9 @@ export class JarvisDaemon extends EventEmitter {
this.telemetry = new RuntimeTelemetry(); this.telemetry = new RuntimeTelemetry();
this._telemetryTimer = setInterval(() => this.telemetry.sample(), 60_000); this._telemetryTimer.unref?.(); 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_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('permission', (ev) => this.emit('ConfirmationRequired', ev));
this.harness.on('hud_sidecar', (ev) => this.emit('ChipOffered', 'sidecar', ev?.title || 'Suggested action', JSON.stringify(ev || {}))); this.harness.on('hud_sidecar', (ev) => this.emit('ChipOffered', 'sidecar', ev?.title || 'Suggested action', JSON.stringify(ev || {})));
} }
+1
View File
@@ -24,6 +24,7 @@
<signal name="JobProgress"><arg type="s"/><arg type="d"/><arg type="s"/></signal> <signal name="JobProgress"><arg type="s"/><arg type="d"/><arg type="s"/></signal>
<signal name="ComputerStep"><arg type="s"/></signal><signal name="ComputerHighlight"><arg type="s"/></signal> <signal name="ComputerStep"><arg type="s"/></signal><signal name="ComputerHighlight"><arg type="s"/></signal>
<signal name="ConfirmationRequired"><arg type="s"/><arg type="s"/><arg type="s"/></signal> <signal name="ConfirmationRequired"><arg type="s"/><arg type="s"/><arg type="s"/></signal>
<signal name="Thinking"><arg type="s"/></signal><signal name="ToolCall"><arg type="s"/></signal><signal name="ToolResult"><arg type="s"/></signal>
<signal name="Error"><arg type="s"/><arg type="s"/></signal> <signal name="Error"><arg type="s"/><arg type="s"/></signal>
</interface> </interface>
</node> </node>
+5
View File
@@ -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. 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. Destructive actions require confirmation in both the HUD and spoken conversation.
Prefer structured tools and accessibility references over coordinates. 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 <jarvis_hud>{"title":"...","chips":[{"id":"...","label":"..."}],"confirmation":null}</jarvis_hud>. When a useful follow-up action exists, append a HUD sidecar exactly as <jarvis_hud>{"title":"...","chips":[{"id":"...","label":"..."}],"confirmation":null}</jarvis_hud>.
The sidecar is for the HUD and must not be spoken.`; The sidecar is for the HUD and must not be spoken.`;
+3
View File
@@ -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, /new PanelMenu.Button\(0.0, 'Jarvis QVAC', false\)/);
assert.match(source, /PushToTalk/); assert.match(source, /PushToTalk/);
assert.match(source, /finalizeReply/); 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', () => { test('D-Bus confirmation signal does not call missing Interface.emit', () => {