|
|
|
@@ -52,6 +52,7 @@ export const previewToolResult = (value) => {
|
|
|
|
|
}
|
|
|
|
|
if (parsed && typeof parsed === 'object') {
|
|
|
|
|
if (parsed.error) return safeText(parsed.error).slice(0, 160);
|
|
|
|
|
if (parsed.ok && parsed.action) return `${prettyToolName(parsed.action)}${parsed.name ? ` · ${parsed.name}` : ''}`.slice(0, 160);
|
|
|
|
|
if (parsed.status && parsed.url) return `${parsed.status} ${parsed.url}`.slice(0, 160);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
@@ -94,7 +95,7 @@ export class ConversationView {
|
|
|
|
|
this.settings.accessible_name = 'Open Jarvis settings';
|
|
|
|
|
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.statusLine = new St.Label({ text: 'armed · tap Listening to speak', style_class: 'jarvis-status-line' });
|
|
|
|
|
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 });
|
|
|
|
@@ -116,18 +117,44 @@ export class ConversationView {
|
|
|
|
|
this.confirmButtons.add_child(button);
|
|
|
|
|
}
|
|
|
|
|
this.confirm.add_child(this.confirmButtons);
|
|
|
|
|
this.scroll = new St.ScrollView({ style_class: compact ? 'jarvis-popup-scroll' : 'jarvis-session-scroll', overlay_scrollbars: true, x_expand: true });
|
|
|
|
|
this.scroll = new St.ScrollView({ style_class: compact ? 'jarvis-popup-scroll' : 'jarvis-session-scroll', overlay_scrollbars: false, x_expand: true });
|
|
|
|
|
try { this.scroll.hscrollbar_policy = St.PolicyType.NEVER; this.scroll.vscrollbar_policy = St.PolicyType.AUTOMATIC; } catch {}
|
|
|
|
|
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 }));
|
|
|
|
|
const thinkHeight = compact ? 160 : 200;
|
|
|
|
|
this.thinkingPane = new St.BoxLayout({
|
|
|
|
|
style_class: compact ? 'jarvis-thinking-pane' : 'jarvis-session-thinking-pane',
|
|
|
|
|
vertical: true,
|
|
|
|
|
x_expand: true,
|
|
|
|
|
y_expand: false,
|
|
|
|
|
visible: false,
|
|
|
|
|
reactive: true,
|
|
|
|
|
clip_to_allocation: true,
|
|
|
|
|
height: thinkHeight,
|
|
|
|
|
});
|
|
|
|
|
try { this.thinkingPane.set_height(thinkHeight); } catch {}
|
|
|
|
|
try { this.thinkingPane.set_style?.(`height: ${thinkHeight}px; max-height: ${thinkHeight}px;`); } catch {}
|
|
|
|
|
try { this.thinkingPane.clip_to_allocation = true; } catch {}
|
|
|
|
|
this.thinkingScroll = new St.ScrollView({
|
|
|
|
|
style_class: compact ? 'jarvis-thinking-scroll' : 'jarvis-session-thinking',
|
|
|
|
|
overlay_scrollbars: false,
|
|
|
|
|
x_expand: true,
|
|
|
|
|
y_expand: true,
|
|
|
|
|
visible: false,
|
|
|
|
|
reactive: true,
|
|
|
|
|
enable_mouse_scrolling: true,
|
|
|
|
|
clip_to_allocation: true,
|
|
|
|
|
});
|
|
|
|
|
try { this.thinkingScroll.hscrollbar_policy = St.PolicyType.NEVER; this.thinkingScroll.vscrollbar_policy = St.PolicyType.ALWAYS; } catch {}
|
|
|
|
|
try { this.thinkingScroll.overlay_scrollbars = false; } catch {}
|
|
|
|
|
try { this.thinkingScroll.clip_to_allocation = true; } catch {}
|
|
|
|
|
this.thinkingBox = new St.BoxLayout({ style_class: 'jarvis-thinking-box', vertical: true, x_expand: true, y_expand: false });
|
|
|
|
|
this.thinking = wrapLabel(new St.Label({ text: '', style_class: 'jarvis-thinking', x_expand: true, y_align: Clutter.ActorAlign.START, reactive: 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.thinkingPane.add_child(this.thinkingScroll);
|
|
|
|
|
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' });
|
|
|
|
@@ -136,14 +163,14 @@ export class ConversationView {
|
|
|
|
|
this.entry = new St.Entry({ hint_text: 'Ask Jarvis…', can_focus: true, x_expand: true });
|
|
|
|
|
this.entry.clutter_text.connect('activate', () => { const text = this.entry.get_text().trim(); if (text) { this.onAsk?.(text); this.entry.set_text(''); } });
|
|
|
|
|
this.controls = new St.BoxLayout({ style_class: 'jarvis-controls' });
|
|
|
|
|
this.talk = new St.Button({ label: 'Hold to talk', style_class: 'jarvis-chip jarvis-talk', reactive: true, can_focus: true });
|
|
|
|
|
this.talk.accessible_name = 'Hold to talk';
|
|
|
|
|
this.talk.connect('notify::pressed', () => this.onTalk?.(Boolean(this.talk.pressed)));
|
|
|
|
|
this.talk.connect('button-press-event', () => { this.onTalk?.(true); return Clutter.EVENT_PROPAGATE; });
|
|
|
|
|
this.talk.connect('button-release-event', () => { this.onTalk?.(false); return Clutter.EVENT_PROPAGATE; });
|
|
|
|
|
this.talk.connect('leave-event', () => { this.onTalk?.(false); return Clutter.EVENT_PROPAGATE; });
|
|
|
|
|
this.talk.connect('key-focus-out', () => this.onTalk?.(false));
|
|
|
|
|
this.talk = new St.Button({ label: 'Listening', style_class: 'jarvis-chip jarvis-talk jarvis-talk-off', reactive: true, can_focus: true });
|
|
|
|
|
this.talk.accessible_name = 'Start listening';
|
|
|
|
|
this._bindChip(this.talk, () => this.onListen?.());
|
|
|
|
|
this.mute = new St.Button({ label: 'Mute', style_class: 'jarvis-chip jarvis-mute', reactive: true, can_focus: true });
|
|
|
|
|
this.mute.accessible_name = 'Mute microphone';
|
|
|
|
|
this._bindChip(this.mute, () => this.onMute?.(!this._muted));
|
|
|
|
|
this.controls.add_child(this.talk);
|
|
|
|
|
this.controls.add_child(this.mute);
|
|
|
|
|
this.stop = new St.Button({ label: 'Stop', style_class: 'jarvis-chip jarvis-chip-quiet', reactive: true, can_focus: true });
|
|
|
|
|
this._bindChip(this.stop, () => this.onStop?.());
|
|
|
|
|
this.reset = new St.Button({ label: 'Reset', style_class: 'jarvis-chip jarvis-chip-quiet', reactive: true, can_focus: true });
|
|
|
|
@@ -166,7 +193,7 @@ export class ConversationView {
|
|
|
|
|
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.thinkingPane);
|
|
|
|
|
this.root.add_child(this.chipScroll);
|
|
|
|
|
this.root.add_child(this.entry);
|
|
|
|
|
this.root.add_child(this.controls);
|
|
|
|
@@ -174,12 +201,14 @@ export class ConversationView {
|
|
|
|
|
this.replyFinalized = false;
|
|
|
|
|
this._state = 'ARMED';
|
|
|
|
|
this._tab = 'chat';
|
|
|
|
|
this._muted = false;
|
|
|
|
|
this._activity = '';
|
|
|
|
|
this.reducedMotion = false;
|
|
|
|
|
this._chatLayoutTimers = [];
|
|
|
|
|
this._thinkLayoutTimers = [];
|
|
|
|
|
this._bindFollow(this.scroll);
|
|
|
|
|
this._bindFollow(this.thinkingScroll);
|
|
|
|
|
this._fitThinking();
|
|
|
|
|
}
|
|
|
|
|
_bindChip(button, action) {
|
|
|
|
|
button.connect('clicked', () => action?.());
|
|
|
|
@@ -208,12 +237,29 @@ export class ConversationView {
|
|
|
|
|
setNotice(text) { const body = shortError(text); this.notice.text = body; this.notice.visible = Boolean(body); }
|
|
|
|
|
_bindFollow(scroll) {
|
|
|
|
|
const adjustment = scroll?.get_vadjustment?.() || scroll?.vadjustment;
|
|
|
|
|
// Layout can change the range after the token handler has returned.
|
|
|
|
|
scroll._jarvisFollow = true;
|
|
|
|
|
adjustment?.connect?.('notify::value', () => {
|
|
|
|
|
if (this._pinning || this._destroyed) return;
|
|
|
|
|
scroll._jarvisFollow = this._nearBottom(adjustment);
|
|
|
|
|
});
|
|
|
|
|
for (const signal of ['notify::upper', 'notify::page-size'])
|
|
|
|
|
adjustment?.connect?.(signal, () => this._pinScroll(scroll));
|
|
|
|
|
adjustment?.connect?.(signal, () => {
|
|
|
|
|
if (scroll._jarvisFollow !== false) this._pinScroll(scroll);
|
|
|
|
|
});
|
|
|
|
|
scroll.connect('notify::mapped', () => {
|
|
|
|
|
if (scroll.mapped) this._followAfterLayout(scroll);
|
|
|
|
|
if (scroll === this.thinkingScroll && scroll.mapped) this._fitThinking();
|
|
|
|
|
});
|
|
|
|
|
if (scroll === this.thinkingScroll) {
|
|
|
|
|
scroll.connect('notify::allocation', () => this._fitThinking());
|
|
|
|
|
scroll.connect('notify::width', () => this._fitThinking());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_nearBottom(adjustment) {
|
|
|
|
|
const upper = Number(adjustment?.upper) || 0;
|
|
|
|
|
const page = Number(adjustment?.page_size) || 0;
|
|
|
|
|
const value = Number(adjustment?.value) || 0;
|
|
|
|
|
return value >= Math.max(0, upper - page) - 32;
|
|
|
|
|
}
|
|
|
|
|
_clearLayoutTimers(slot) {
|
|
|
|
|
for (const id of this[slot] || []) {
|
|
|
|
@@ -227,24 +273,58 @@ export class ConversationView {
|
|
|
|
|
try { child?.queue_relayout?.(); } catch {}
|
|
|
|
|
try { child?.get_first_child?.()?.queue_relayout?.(); } catch {}
|
|
|
|
|
}
|
|
|
|
|
_viewportWidth(scroll) {
|
|
|
|
|
const read = (box) => {
|
|
|
|
|
if (!box) return 0;
|
|
|
|
|
if (typeof box.get_width === 'function') return Number(box.get_width()) || 0;
|
|
|
|
|
const x1 = Number(box.x1) || 0;
|
|
|
|
|
const x2 = Number(box.x2) || 0;
|
|
|
|
|
return x2 > x1 ? x2 - x1 : Number(box.width) || 0;
|
|
|
|
|
};
|
|
|
|
|
try {
|
|
|
|
|
const width = read(scroll?.get_allocation_box?.() || scroll?.allocation);
|
|
|
|
|
if (width >= 200) return width;
|
|
|
|
|
} catch {}
|
|
|
|
|
return Number(scroll?.width) || Number(this.root?.width) || (this.compact ? 292 : 388);
|
|
|
|
|
}
|
|
|
|
|
_fitThinking() {
|
|
|
|
|
if (this._destroyed) return;
|
|
|
|
|
const label = this.thinking;
|
|
|
|
|
const text = label?.clutter_text;
|
|
|
|
|
if (!label || !text) return;
|
|
|
|
|
const width = Math.max(200, this._viewportWidth(this.thinkingScroll) - 28);
|
|
|
|
|
try { text.line_wrap = true; } catch {}
|
|
|
|
|
try { text.line_wrap_mode = Pango.WrapMode.WORD_CHAR; } catch {}
|
|
|
|
|
try { text.ellipsize = Pango.EllipsizeMode.NONE; } catch {}
|
|
|
|
|
try { text.width = width; } catch {}
|
|
|
|
|
try { text.set_line_wrap?.(true); } catch {}
|
|
|
|
|
try { label.queue_relayout?.(); } catch {}
|
|
|
|
|
try { this.thinkingBox?.queue_relayout?.(); } catch {}
|
|
|
|
|
}
|
|
|
|
|
_pinScroll(scroll) {
|
|
|
|
|
if (this._destroyed) return;
|
|
|
|
|
const adjustment = scroll?.get_vadjustment?.() || scroll?.vadjustment;
|
|
|
|
|
if (!adjustment) return;
|
|
|
|
|
if (scroll._jarvisFollow === false) return;
|
|
|
|
|
const upper = Number(adjustment.upper) || 0;
|
|
|
|
|
const page = Number(adjustment.page_size) || 0;
|
|
|
|
|
this._pinning = true;
|
|
|
|
|
try { adjustment.value = Math.max(0, upper - page); } catch {}
|
|
|
|
|
this._pinning = false;
|
|
|
|
|
}
|
|
|
|
|
_followAfterLayout(scroll) {
|
|
|
|
|
if (this._destroyed) return;
|
|
|
|
|
const slot = scroll === this.thinkingScroll ? '_thinkLayoutTimers' : '_chatLayoutTimers';
|
|
|
|
|
this._relayoutPane(scroll);
|
|
|
|
|
if (scroll === this.thinkingScroll) this._fitThinking();
|
|
|
|
|
scroll._jarvisFollow = true;
|
|
|
|
|
this._pinScroll(scroll);
|
|
|
|
|
// Coalesce bursts without postponing the follow on every token. Track the
|
|
|
|
|
// source until it runs so teardown never leaves callbacks on dead actors.
|
|
|
|
|
if (this[slot].length) return;
|
|
|
|
|
this[slot] = [GLib.timeout_add(GLib.PRIORITY_DEFAULT, 0, () => {
|
|
|
|
|
this[slot] = [];
|
|
|
|
|
if (scroll === this.thinkingScroll) this._fitThinking();
|
|
|
|
|
this._pinScroll(scroll);
|
|
|
|
|
return GLib.SOURCE_REMOVE;
|
|
|
|
|
})];
|
|
|
|
@@ -261,10 +341,12 @@ export class ConversationView {
|
|
|
|
|
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.thinkingPane.visible = thinking;
|
|
|
|
|
this.thinkingScroll.visible = thinking;
|
|
|
|
|
this.chipScroll.visible = !thinking && this.chips.get_n_children() > 0;
|
|
|
|
|
this.entry.visible = !thinking;
|
|
|
|
|
this.talk.visible = !thinking;
|
|
|
|
|
this._refreshListenButton();
|
|
|
|
|
if (thinking) this._fitThinking();
|
|
|
|
|
this.followActive();
|
|
|
|
|
}
|
|
|
|
|
addRow(who, text) {
|
|
|
|
@@ -298,8 +380,15 @@ export class ConversationView {
|
|
|
|
|
updateThinking(text) {
|
|
|
|
|
const chunk = safeText(text);
|
|
|
|
|
if (!chunk) return;
|
|
|
|
|
this.thinking.text = `${this.thinking.text || ''}${chunk}`;
|
|
|
|
|
// Map the pane before writing so the wrap width is the viewport, not 0.
|
|
|
|
|
this.showTab('thinking');
|
|
|
|
|
const next = `${this.thinking.text || ''}${chunk}`;
|
|
|
|
|
this.thinking.text = next;
|
|
|
|
|
try { this.thinking.set_text?.(next); } catch {}
|
|
|
|
|
try { if (this.thinking.clutter_text) this.thinking.clutter_text.text = next; } catch {}
|
|
|
|
|
this.thinkingScroll._jarvisFollow = true;
|
|
|
|
|
this._fitThinking();
|
|
|
|
|
this._followAfterLayout(this.thinkingScroll);
|
|
|
|
|
}
|
|
|
|
|
toggleThinking() { this.showTab(this._tab === 'thinking' ? 'chat' : 'thinking'); }
|
|
|
|
|
finishThinking() { this.showTab('chat'); }
|
|
|
|
@@ -401,16 +490,39 @@ export class ConversationView {
|
|
|
|
|
this.status.accessible_name = this.status.text;
|
|
|
|
|
this.status.style_class = 'jarvis-local';
|
|
|
|
|
}
|
|
|
|
|
setVoiceStatus({ tts, input, wake } = {}) {
|
|
|
|
|
this._voice = { tts: Boolean(tts), input: Boolean(input), wake: Boolean(wake) };
|
|
|
|
|
this.status.text = input ? 'Local' : 'Mic off';
|
|
|
|
|
setVoiceStatus({ tts, input, wake, muted } = {}) {
|
|
|
|
|
if (muted != null) this._muted = Boolean(muted);
|
|
|
|
|
this._voice = { tts: Boolean(tts), input: Boolean(input), wake: Boolean(wake), muted: this._muted };
|
|
|
|
|
this.status.text = this._muted ? 'Muted' : input ? 'Local' : 'Mic off';
|
|
|
|
|
this.status.accessible_name = this.status.text;
|
|
|
|
|
this.status.style_class = 'jarvis-local';
|
|
|
|
|
this.talk.reactive = Boolean(input);
|
|
|
|
|
this.talk.can_focus = Boolean(input);
|
|
|
|
|
this.talk.label = input ? 'Hold to talk' : 'Mic unavailable';
|
|
|
|
|
this._refreshMuteButton();
|
|
|
|
|
this._refreshListenButton();
|
|
|
|
|
this._refreshStatusLine();
|
|
|
|
|
}
|
|
|
|
|
setMuted(muted) {
|
|
|
|
|
this._muted = Boolean(muted);
|
|
|
|
|
if (this._voice) this._voice.muted = this._muted;
|
|
|
|
|
this._refreshMuteButton();
|
|
|
|
|
this._refreshListenButton();
|
|
|
|
|
this._refreshStatusLine();
|
|
|
|
|
}
|
|
|
|
|
_refreshMuteButton() {
|
|
|
|
|
if (!this.mute) return;
|
|
|
|
|
this.mute.label = this._muted ? 'Muted' : 'Mute';
|
|
|
|
|
this.mute.style_class = this._muted ? 'jarvis-chip jarvis-mute jarvis-mute-active' : 'jarvis-chip jarvis-mute';
|
|
|
|
|
this.mute.accessible_name = this._muted ? 'Unmute microphone' : 'Mute microphone';
|
|
|
|
|
}
|
|
|
|
|
_refreshListenButton() {
|
|
|
|
|
if (!this.talk) return;
|
|
|
|
|
const listening = !this._muted && this._state === 'LISTENING';
|
|
|
|
|
const available = !this._muted && (this._voice?.input !== false);
|
|
|
|
|
this.talk.label = 'Listening';
|
|
|
|
|
this.talk.reactive = available;
|
|
|
|
|
this.talk.can_focus = available;
|
|
|
|
|
this.talk.style_class = listening ? 'jarvis-chip jarvis-talk' : 'jarvis-chip jarvis-talk jarvis-talk-off';
|
|
|
|
|
this.talk.accessible_name = this._muted ? 'Listening disabled while muted' : listening ? 'Stop listening' : 'Start listening';
|
|
|
|
|
}
|
|
|
|
|
setState(state) {
|
|
|
|
|
const value = STATES.has(state) ? state : 'ARMED';
|
|
|
|
|
const changed = this._state !== value;
|
|
|
|
@@ -429,23 +541,28 @@ export class ConversationView {
|
|
|
|
|
}
|
|
|
|
|
this._refreshStatusLine();
|
|
|
|
|
const name = this._assistantName || 'Jarvis';
|
|
|
|
|
this.title.text = value === 'SLEEPING' ? `${name} · privacy` : name;
|
|
|
|
|
this.title.text = this._muted ? `${name} · muted` : value === 'SLEEPING' ? `${name} · privacy` : name;
|
|
|
|
|
this._refreshListenButton();
|
|
|
|
|
}
|
|
|
|
|
_refreshStatusLine() {
|
|
|
|
|
const value = this._state;
|
|
|
|
|
const voice = this._voice || {};
|
|
|
|
|
const bits = [];
|
|
|
|
|
if (voice.tts) bits.push('speech on');
|
|
|
|
|
else if (voice.tts === false) bits.push('speech off');
|
|
|
|
|
if (voice.input && voice.wake) bits.push('wake on');
|
|
|
|
|
else if (voice.input) bits.push('hold talk');
|
|
|
|
|
else if (voice.input === false) bits.push('mic off');
|
|
|
|
|
if (this._muted) bits.push('muted');
|
|
|
|
|
else {
|
|
|
|
|
if (voice.tts) bits.push('speech on');
|
|
|
|
|
else if (voice.tts === false) bits.push('speech off');
|
|
|
|
|
if (voice.input && voice.wake) bits.push('wake on');
|
|
|
|
|
else if (voice.input) bits.push('listening ready');
|
|
|
|
|
else if (voice.input === false) bits.push('mic off');
|
|
|
|
|
}
|
|
|
|
|
const extra = bits.length ? ` · ${bits.join(' · ')}` : '';
|
|
|
|
|
if (value === 'LISTENING') this.statusLine.text = `Listening${extra}`;
|
|
|
|
|
if (this._muted) this.statusLine.text = `muted · microphone off`;
|
|
|
|
|
else if (value === 'LISTENING') this.statusLine.text = `Listening${extra}`;
|
|
|
|
|
else if (value === 'SPEAKING') this.statusLine.text = `Speaking${extra}`;
|
|
|
|
|
else if (value === 'SLEEPING') this.statusLine.text = `privacy · microphone off${extra}`;
|
|
|
|
|
else if (value === 'THINKING') this.statusLine.text = `${this._activity || 'Thinking'}${extra}`;
|
|
|
|
|
else this.statusLine.text = `armed · Hold Talk to speak${extra}`;
|
|
|
|
|
else this.statusLine.text = `armed · tap Listening to speak${extra}`;
|
|
|
|
|
}
|
|
|
|
|
addChip(id, label, payload) {
|
|
|
|
|
const chip = new St.Button({ label: safeText(label), style_class: 'jarvis-chip jarvis-chip-suggested', reactive: true, can_focus: true });
|
|
|
|
@@ -462,7 +579,7 @@ export class ConversationView {
|
|
|
|
|
this.addRow('J', `${step.n ? `${step.n}. ` : ''}${action || 'computer step'}`);
|
|
|
|
|
} catch { this.addRow('J', json); }
|
|
|
|
|
}
|
|
|
|
|
endTalk() { this.onTalk?.(false); }
|
|
|
|
|
endTalk() {}
|
|
|
|
|
destroy() {
|
|
|
|
|
this._destroyed = true;
|
|
|
|
|
this._clearLayoutTimers('_chatLayoutTimers');
|
|
|
|
@@ -587,7 +704,10 @@ 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)));
|
|
|
|
|
const thinkHeight = Math.max(120, Math.min(240, monitor.height - 400));
|
|
|
|
|
this.view.thinkingPane?.set_height?.(thinkHeight);
|
|
|
|
|
this.view.thinkingPane?.set_style?.(`height: ${thinkHeight}px; max-height: ${thinkHeight}px;`);
|
|
|
|
|
this.view.thinkingScroll.set_height(thinkHeight);
|
|
|
|
|
}
|
|
|
|
|
this.root.visible = true;
|
|
|
|
|
this.view.followActive();
|
|
|
|
|