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