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

This commit is contained in:
2026-09-12 10:43:01 -04:00
parent f26204505e
commit b8c60e4326
14 changed files with 701 additions and 89 deletions
@@ -10,15 +10,20 @@
.jarvis-title { font-weight: bold; letter-spacing: 0.4px; font-size: 14px; }
.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: 11px; padding: 2px 8px; border-radius: 8px; background-color: transparent; }
.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: 6px 10px; border-left: 2px solid #4FD2FF; background-color: rgba(79, 210, 255, .06); border-radius: 8px; }
.jarvis-tabs { spacing: 6px; }
.jarvis-tab { padding: 4px 10px; border-radius: 999px; background-color: transparent; color: #9aa8c0; font-size: 12px; }
.jarvis-tab:hover, .jarvis-tab:focus { color: #f6f7fb; background-color: rgba(255, 255, 255, .08); }
.jarvis-tab-active { background-color: var(--jarvis-accent, #F4B942); color: #16191f; font-weight: bold; }
.jarvis-thinking { color: #aeb6c8; font-size: 12px; padding: 8px 10px; border-left: 2px solid #4FD2FF; background-color: rgba(79, 210, 255, .06); border-radius: 8px; }
.jarvis-notice { color: #f4b942; font-size: 12px; }
.jarvis-confirm { spacing: 8px; padding: 4px 0; }
.jarvis-confirm-actions { spacing: 6px; }
.jarvis-confirm-label { color: #f6f7fb; font-size: 13px; }
.jarvis-popup-scroll { height: 140px; }
.jarvis-popup-scroll { height: 180px; }
.jarvis-thinking-scroll { height: 160px; }
.jarvis-thinking-box { spacing: 0; }
.jarvis-session-scroll { height: 220px; }
.jarvis-session-thinking { height: 200px; }
.jarvis-transcript { spacing: 6px; }
.jarvis-row { padding: 6px 10px; border-radius: 8px; font-size: 13px; }
.jarvis-row-user { border-left: 2px solid var(--jarvis-accent, #F4B942); background-color: rgba(244, 185, 66, .08); }
+157 -24
View File
@@ -23,10 +23,28 @@ export const coerceText = (value) => {
export const safeText = (value) => coerceText(value).replace(/[<>]/g, '');
export const shortError = (value) => safeText(value).split('\n')[0].replace(/\s+/g, ' ').slice(0, 140);
export const previewToolResult = (value) => {
const text = safeText(value).replace(/\s+/g, ' ').trim();
const raw = typeof value === 'string' ? value : coerceText(value);
const text = safeText(raw).replace(/\s+/g, ' ').trim();
if (!text || text === '(no output)') return '';
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
const first = parsed[0] && (parsed[0].title || parsed[0].url || parsed[0].name);
if (first) return parsed.length === 1 ? String(first).slice(0, 80) : `${parsed.length} results · ${String(first).slice(0, 60)}`;
return parsed.length ? `${parsed.length} results` : '';
}
if (parsed && typeof parsed === 'object') {
if (parsed.error) return safeText(parsed.error).slice(0, 160);
if (parsed.status && parsed.url) return `${parsed.status} ${parsed.url}`.slice(0, 160);
}
} catch {
const status = text.match(/"status"\s*:\s*(\d+)/);
const href = text.match(/"url"\s*:\s*"([^"]+)"/);
if (status) return `${status[1]}${href ? ' ' + href[1] : ''}`.slice(0, 160);
}
return text.slice(0, 160);
};
export const prettyToolName = (value) => safeText(value || 'tool').replace(/_/g, ' ').replace(/\s+/g, ' ').trim() || 'tool';
function wrapLabel(label) {
if (label.clutter_text) {
@@ -57,11 +75,15 @@ export class ConversationView {
this._bindChip(this.settings, () => this.onSettings?.());
this.header.add_child(this.settings);
this.statusLine = new St.Label({ text: 'armed · 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, x_align: Clutter.ActorAlign.START });
this.thinkingToggle.accessible_name = 'Show or hide Jarvis thinking';
this.thinkingToggle.connect('clicked', () => this.toggleThinking());
this.thinking = wrapLabel(new St.Label({ text: '', style_class: 'jarvis-thinking', visible: false, x_expand: true, can_focus: true }));
this.thinking.accessible_name = 'Jarvis thinking';
this.tabs = new St.BoxLayout({ style_class: 'jarvis-tabs', x_expand: true });
this.chatTab = new St.Button({ label: 'Chat', style_class: 'jarvis-tab jarvis-tab-active', reactive: true, can_focus: true });
this.thinkTab = new St.Button({ label: 'Thinking', style_class: 'jarvis-tab', reactive: true, can_focus: true });
this.chatTab.accessible_name = 'Chat tab';
this.thinkTab.accessible_name = 'Thinking tab';
this._bindChip(this.chatTab, () => this.showTab('chat', { user: true }));
this._bindChip(this.thinkTab, () => this.showTab('thinking', { user: true }));
this.tabs.add_child(this.chatTab);
this.tabs.add_child(this.thinkTab);
this.notice = new St.Label({ text: '', style_class: 'jarvis-notice', visible: false, x_expand: true });
this.notice.accessible_name = 'Jarvis notice';
this.confirm = new St.BoxLayout({ style_class: 'jarvis-confirm', visible: false, x_expand: true, vertical: compact });
@@ -79,6 +101,13 @@ export class ConversationView {
this.transcript = new St.BoxLayout({ style_class: 'jarvis-transcript', vertical: true, x_expand: true });
this.transcript.accessible_name = 'Conversation transcript';
if (typeof this.scroll.set_child === 'function') this.scroll.set_child(this.transcript); else this.scroll.add_child(this.transcript);
this.thinkingScroll = new St.ScrollView({ style_class: compact ? 'jarvis-thinking-scroll' : 'jarvis-session-thinking', overlay_scrollbars: true, x_expand: true, visible: false });
try { this.thinkingScroll.hscrollbar_policy = St.PolicyType.NEVER; this.thinkingScroll.vscrollbar_policy = St.PolicyType.AUTOMATIC; } catch {}
this.thinkingBox = new St.BoxLayout({ style_class: 'jarvis-thinking-box', vertical: true, x_expand: true });
this.thinking = wrapLabel(new St.Label({ text: '', style_class: 'jarvis-thinking', x_expand: true, can_focus: true }));
this.thinking.accessible_name = 'Jarvis thinking';
this.thinkingBox.add_child(this.thinking);
if (typeof this.thinkingScroll.set_child === 'function') this.thinkingScroll.set_child(this.thinkingBox); else this.thinkingScroll.add_child(this.thinkingBox);
this.chipScroll = new St.ScrollView({ style_class: 'jarvis-chip-scroll', overlay_scrollbars: true, x_expand: true, visible: false });
try { this.chipScroll.vscrollbar_policy = St.PolicyType.NEVER; this.chipScroll.hscrollbar_policy = St.PolicyType.AUTOMATIC; } catch {}
this.chips = new St.BoxLayout({ style_class: 'jarvis-chips' });
@@ -113,17 +142,20 @@ export class ConversationView {
}
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.tabs);
this.root.add_child(this.notice);
this.root.add_child(this.confirm);
this.root.add_child(this.scroll);
this.root.add_child(this.thinkingScroll);
this.root.add_child(this.chipScroll);
this.root.add_child(this.entry);
this.root.add_child(this.controls);
this.streamingReply = false;
this.replyFinalized = false;
this._state = 'ARMED';
this._tab = 'chat';
this._userPickedTab = false;
this._activity = '';
this.reducedMotion = false;
}
_bindChip(button, action) {
@@ -132,24 +164,42 @@ export class ConversationView {
clear() {
this.transcript.destroy_all_children();
this.thinking.text = '';
this.thinking.visible = false;
this.thinkingToggle.visible = false;
this.notice.visible = false;
this.confirm.visible = false;
this.chipScroll.visible = false;
this.chips.destroy_all_children();
this.streamingReply = false;
this.replyFinalized = false;
this._activity = '';
this._userPickedTab = false;
this.showTab('chat');
}
setNotice(text) { const body = shortError(text); this.notice.text = body; this.notice.visible = Boolean(body); }
_followConversation() {
const adjustment = this.scroll?.get_vadjustment?.() || this.scroll?.vadjustment;
_followConversation() { this._followScroll(this.scroll); }
_followThinking() { this._followScroll(this.thinkingScroll); }
_followScroll(scroll) {
const adjustment = scroll?.get_vadjustment?.() || 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;
});
}
showTab(name, { user = false } = {}) {
if (user) this._userPickedTab = true;
this._tab = name === 'thinking' ? 'thinking' : 'chat';
this._applyTab();
}
_applyTab() {
const thinking = this._tab === 'thinking';
this.chatTab.style_class = thinking ? 'jarvis-tab' : 'jarvis-tab jarvis-tab-active';
this.thinkTab.style_class = thinking ? 'jarvis-tab jarvis-tab-active' : 'jarvis-tab';
this.scroll.visible = !thinking;
this.thinkingScroll.visible = thinking;
this.chipScroll.visible = !thinking && this.chips.get_n_children() > 0;
this.entry.visible = !thinking;
this.talk.visible = !thinking;
}
addRow(who, text) {
const body = safeText(text);
const row = wrapLabel(new St.Label({ text: `${who === 'J' ? 'J' : 'YOU'} ${body}`, style_class: `jarvis-row jarvis-row-${who === 'J' ? 'jarvis' : 'user'}`, can_focus: true }));
@@ -182,14 +232,67 @@ export class ConversationView {
const chunk = safeText(text);
if (!chunk) return;
this.thinking.text = `${this.thinking.text || ''}${chunk}`;
this.thinkingToggle.visible = true;
this.thinkingToggle.label = 'Thinking ▾';
if (this._state === 'THINKING' && !this._userPickedTab) this.showTab('thinking');
this._followThinking();
}
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) { this.streamingReply = false; this.replyFinalized = false; try { const call = JSON.parse(json); this._addToolRow(`Using ${safeText(call.name || 'tool')}`); } catch { this._addToolRow(`Using ${safeText(json)}`); } }
addToolResult(json) { this.streamingReply = false; try { const result = JSON.parse(json); const name = safeText(result.name || 'tool'); const preview = previewToolResult(result.result); this._addToolRow(preview ? `${name}: ${preview}` : `${name} complete`); } catch { this._addToolRow('Tool complete'); } }
_addToolRow(text) { const row = this.addRow('J', text); row.style_class = `${row.style_class} jarvis-row-tool`; }
toggleThinking() { this.showTab(this._tab === 'thinking' ? 'chat' : 'thinking', { user: true }); }
finishThinking() { if (this._state !== 'THINKING') this.showTab('chat'); }
addToolCall(json) {
this.streamingReply = false;
this.replyFinalized = false;
let name = 'tool';
try { name = JSON.parse(json).name || 'tool'; } catch { name = json; }
const pretty = prettyToolName(name);
this._activity = `Using ${pretty}`;
this._refreshStatusLine();
this._setToolActivity(pretty);
}
addToolResult(json) {
this.streamingReply = false;
let name = 'tool';
let preview = '';
let failed = false;
try {
const result = JSON.parse(json);
name = result.name || 'tool';
const payload = result.result;
if (payload && typeof payload === 'object' && payload.error) {
failed = true;
preview = previewToolResult(payload.error);
} else {
preview = previewToolResult(payload);
if (/error|timed out|HTTP \d+/i.test(preview)) failed = true;
}
} catch {
preview = previewToolResult(json);
}
const pretty = prettyToolName(name);
this._setToolActivity(failed && preview ? `${pretty} · ${preview}` : preview ? `${pretty} · ${preview}` : `${pretty} · done`);
if (this._state === 'THINKING') {
this._activity = failed ? `${pretty} failed` : `${pretty} · done`;
this._refreshStatusLine();
}
}
_setToolActivity(text) {
const body = safeText(text);
let row = this.transcript.get_last_child?.();
if (!row || !String(row.style_class || '').includes('jarvis-row-tool')) {
row = wrapLabel(new St.Label({ text: body, style_class: 'jarvis-row jarvis-row-tool', can_focus: true }));
this.transcript.add_child(row);
while (this.transcript.get_n_children() > this.maxRows) {
const first = this.transcript.get_first_child();
if (!first) break;
if (typeof this.transcript.remove_child === 'function') this.transcript.remove_child(first);
else first.destroy();
}
} else {
row.text = body;
}
row.accessible_name = `Activity: ${body}`;
this._followConversation();
return row;
}
_addToolRow(text) { return this._setToolActivity(text); }
offerConfirm(tool, argsJson, pattern) {
let jobId = ''; let toolCallId = ''; let detail = safeText(pattern);
try {
@@ -234,18 +337,43 @@ export class ConversationView {
setState(state) {
const value = STATES.has(state) ? state : 'ARMED';
this._state = value;
this.statusLine.text = `${value.toLowerCase()} · Hold Talk to speak`;
if (value === 'LISTENING') {
this._userPickedTab = false;
this._activity = '';
this.showTab('chat');
} else if (value === 'THINKING') {
if (!this._userPickedTab) this.showTab('thinking');
} else if (value === 'SPEAKING' || value === 'ARMED') {
this._userPickedTab = false;
this.showTab('chat');
}
this._refreshStatusLine();
this.title.text = value === 'SLEEPING' ? 'Jarvis · privacy' : 'Jarvis';
this.status.style_class = `jarvis-local jarvis-state-${value.toLowerCase()}`;
}
_refreshStatusLine() {
const value = this._state;
if (value === 'LISTENING') this.statusLine.text = 'Listening';
else if (value === 'SPEAKING') this.statusLine.text = 'Speaking';
else if (value === 'SLEEPING') this.statusLine.text = 'privacy · microphone off';
else if (value === 'THINKING') this.statusLine.text = this._activity || 'Thinking';
else this.statusLine.text = 'armed · Hold Talk to speak';
}
addChip(id, label, payload) {
const chip = new St.Button({ label: safeText(label), style_class: 'jarvis-chip jarvis-chip-suggested', reactive: true, can_focus: true });
chip.accessible_name = `Suggested action: ${safeText(label)}`;
chip.connect('clicked', () => this.onSuggestion?.(id, payload));
this.chips.add_child(chip);
this.chipScroll.visible = true;
this.chipScroll.visible = this._tab !== 'thinking';
}
addStep(json) {
try {
const step = JSON.parse(json);
const action = String(step.action || '');
if (action === 'grant' || action === 'revoke' || action === 'backend') return;
this.addRow('J', `${step.n ? `${step.n}. ` : ''}${action || 'computer step'}`);
} catch { this.addRow('J', json); }
}
addStep(json) { try { const step = JSON.parse(json); this.addRow('J', `${step.n ? `${step.n}. ` : ''}${step.action || 'computer step'}`); } catch { this.addRow('J', json); } }
endTalk() { this.onTalk?.(false); }
destroy() { this.root.destroy(); }
}
@@ -321,8 +449,12 @@ export class ComputerUseChrome {
addJob(id, pct, label) { this.job.text = `${safeText(label)} · ${Math.round(Number(pct) * 100)}%`; this.job.visible = true; this._reveal(); }
setTarget(json) { this.target.text = `${safeText(json)}`; this.cursor.text = '◎'; this.cursor.visible = true; this.target.visible = true; this._reveal(); }
addStep(json) {
try { const step = JSON.parse(json); this.step.text = `${step.n ? `${step.n}. ` : ''}${step.action || 'computer step'}`; }
catch { this.step.text = safeText(json); }
let step = {};
try { step = JSON.parse(json); } catch { return; }
const action = String(step.action || '');
if (action === 'revoke') { this.hide(); return; }
if (action === 'grant' || action === 'backend') return;
this.step.text = `${step.n ? `${step.n}. ` : ''}${action || 'computer step'}`;
this.step.visible = true;
this._reveal();
}
@@ -346,6 +478,7 @@ export class SessionPanel {
this.root.set_width(width);
this.root.set_position(monitor.x + Math.max(24, monitor.width - width - 24), monitor.y + 40);
this.view.scroll.set_height(Math.max(120, Math.min(280, monitor.height - 360)));
this.view.thinkingScroll.set_height(Math.max(120, Math.min(240, monitor.height - 400)));
}
this.root.visible = true;
}