@@ -137,7 +137,10 @@ export default class JarvisExtension extends Extension {
|
||||
settingsItem.connect('activate', () => this._openSettings());
|
||||
menu.addMenuItem(settingsItem);
|
||||
} catch {}
|
||||
this._menuState = menu.connect('open-state-changed', (_menu, open) => { if (!open) this.popup.endTalk(); });
|
||||
this._menuState = menu.connect('open-state-changed', (_menu, open) => {
|
||||
if (!open) this.popup.endTalk();
|
||||
else this.popup.followActive();
|
||||
});
|
||||
}
|
||||
_openPopup() { try { this._indicator.menu.open(); } catch {} }
|
||||
_openSettings() {
|
||||
|
||||
@@ -177,6 +177,10 @@ export class ConversationView {
|
||||
this._userPickedTab = false;
|
||||
this._activity = '';
|
||||
this.reducedMotion = false;
|
||||
this._chatLayoutTimers = [];
|
||||
this._thinkLayoutTimers = [];
|
||||
this._bindFollow(this.scroll);
|
||||
this._bindFollow(this.thinkingScroll);
|
||||
}
|
||||
_bindChip(button, action) {
|
||||
button.connect('clicked', () => action?.());
|
||||
@@ -204,16 +208,71 @@ export class ConversationView {
|
||||
this.showTab('chat');
|
||||
}
|
||||
setNotice(text) { const body = shortError(text); this.notice.text = body; this.notice.visible = Boolean(body); }
|
||||
_followConversation() { this._followScroll(this.scroll); }
|
||||
_followThinking() { this._followScroll(this.thinkingScroll); }
|
||||
_followScroll(scroll) {
|
||||
_bindFollow(scroll) {
|
||||
const adjustment = scroll?.get_vadjustment?.() || scroll?.vadjustment;
|
||||
if (!adjustment || typeof adjustment.connect !== 'function') return;
|
||||
scroll._jarvisFollow = true;
|
||||
adjustment.connect('notify::value', () => {
|
||||
if (this._pinning) return;
|
||||
scroll._jarvisFollow = this._nearBottom(adjustment);
|
||||
});
|
||||
adjustment.connect('notify::upper', () => {
|
||||
if (scroll.visible === false) return;
|
||||
if (scroll._jarvisFollow !== false) this._pinScroll(scroll, { force: true });
|
||||
});
|
||||
}
|
||||
_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] || []) {
|
||||
try { GLib.Source.remove(id); } catch {}
|
||||
}
|
||||
this[slot] = [];
|
||||
}
|
||||
_relayoutPane(scroll) {
|
||||
try { scroll?.queue_relayout?.(); } catch {}
|
||||
const child = scroll?.get_child?.() || scroll?.get_first_child?.();
|
||||
try { child?.queue_relayout?.(); } catch {}
|
||||
try { child?.get_first_child?.()?.queue_relayout?.(); } catch {}
|
||||
}
|
||||
_pinScroll(scroll, { force = false } = {}) {
|
||||
const adjustment = scroll?.get_vadjustment?.() || scroll?.vadjustment;
|
||||
if (!adjustment) return;
|
||||
if (force) scroll._jarvisFollow = true;
|
||||
if (!force && 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;
|
||||
}
|
||||
_followScroll(scroll, { force = false } = {}) {
|
||||
this._pinScroll(scroll, { force });
|
||||
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE || GLib.PRIORITY_DEFAULT, () => {
|
||||
try { adjustment.value = Math.max(0, adjustment.upper - adjustment.page_size); } catch {}
|
||||
this._pinScroll(scroll, { force });
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
}
|
||||
_followAfterLayout(scroll, { force = true } = {}) {
|
||||
const slot = scroll === this.thinkingScroll ? '_thinkLayoutTimers' : '_chatLayoutTimers';
|
||||
this._relayoutPane(scroll);
|
||||
this._followScroll(scroll, { force });
|
||||
this._clearLayoutTimers(slot);
|
||||
this[slot] = [0, 50].map((delay) => GLib.timeout_add(GLib.PRIORITY_DEFAULT, delay, () => {
|
||||
this._pinScroll(scroll, { force });
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}));
|
||||
}
|
||||
_followConversation() { this._followScroll(this.scroll); }
|
||||
_followThinking() { this._followScroll(this.thinkingScroll); }
|
||||
followActive() {
|
||||
const scroll = this._tab === 'thinking' ? this.thinkingScroll : this.scroll;
|
||||
this._followAfterLayout(scroll, { force: true });
|
||||
}
|
||||
showTab(name, { user = false } = {}) {
|
||||
if (user) this._userPickedTab = true;
|
||||
this._tab = name === 'thinking' ? 'thinking' : 'chat';
|
||||
@@ -228,6 +287,7 @@ export class ConversationView {
|
||||
this.chipScroll.visible = !thinking && this.chips.get_n_children() > 0;
|
||||
this.entry.visible = !thinking;
|
||||
this.talk.visible = !thinking;
|
||||
this.followActive();
|
||||
}
|
||||
addRow(who, text) {
|
||||
const body = safeText(text);
|
||||
@@ -240,7 +300,8 @@ export class ConversationView {
|
||||
if (typeof this.transcript.remove_child === 'function') this.transcript.remove_child(first);
|
||||
else first.destroy();
|
||||
}
|
||||
this._followConversation();
|
||||
if (this._tab === 'chat') this._followAfterLayout(this.scroll, { force: false });
|
||||
else this._followConversation();
|
||||
return row;
|
||||
}
|
||||
token(text) {
|
||||
@@ -255,14 +316,16 @@ export class ConversationView {
|
||||
}
|
||||
row.text = `${row.text}${chunk}`;
|
||||
row.accessible_name = `Jarvis: ${row.text}`;
|
||||
this._followConversation();
|
||||
if (this._tab === 'chat') this._followAfterLayout(this.scroll, { force: false });
|
||||
else this._followConversation();
|
||||
}
|
||||
updateThinking(text) {
|
||||
const chunk = safeText(text);
|
||||
if (!chunk) return;
|
||||
this.thinking.text = `${this.thinking.text || ''}${chunk}`;
|
||||
if (this._state === 'THINKING' && !this._userPickedTab) this.showTab('thinking');
|
||||
this._followThinking();
|
||||
if (this._tab === 'thinking') this._followAfterLayout(this.thinkingScroll, { force: false });
|
||||
else this._followThinking();
|
||||
}
|
||||
toggleThinking() { this.showTab(this._tab === 'thinking' ? 'chat' : 'thinking', { user: true }); }
|
||||
finishThinking() { if (this._state !== 'THINKING') this.showTab('chat'); }
|
||||
@@ -318,7 +381,8 @@ export class ConversationView {
|
||||
row.text = body;
|
||||
}
|
||||
row.accessible_name = `Activity: ${body}`;
|
||||
this._followConversation();
|
||||
if (this._tab === 'chat') this._followAfterLayout(this.scroll, { force: false });
|
||||
else this._followConversation();
|
||||
return row;
|
||||
}
|
||||
_addToolRow(text) { return this._setToolActivity(text); }
|
||||
@@ -341,15 +405,22 @@ export class ConversationView {
|
||||
finalizeReply(text) {
|
||||
this.finishThinking();
|
||||
const spoken = safeText(text);
|
||||
if (this.replyFinalized) return;
|
||||
if (this.replyFinalized) {
|
||||
this.followActive();
|
||||
return;
|
||||
}
|
||||
let row = this.transcript.get_last_child?.();
|
||||
if (this.streamingReply && row && String(row.style_class || '').includes('jarvis-row-jarvis') && !String(row.style_class || '').includes('jarvis-row-tool')) {
|
||||
const body = String(row.text || '').replace(/^J\s+/, '');
|
||||
if (spoken && !body.trim()) { row.text = `J ${spoken}`; row.accessible_name = `Jarvis: ${spoken}`; }
|
||||
this.streamingReply = false; this.replyFinalized = true; return;
|
||||
this.streamingReply = false;
|
||||
this.replyFinalized = true;
|
||||
this.followActive();
|
||||
return;
|
||||
}
|
||||
if (spoken) this.addRow('J', spoken);
|
||||
this.replyFinalized = true;
|
||||
this.followActive();
|
||||
}
|
||||
setConnectionStatus(kind) {
|
||||
this._connection = kind;
|
||||
@@ -382,7 +453,8 @@ export class ConversationView {
|
||||
this.showTab('chat');
|
||||
}
|
||||
this._refreshStatusLine();
|
||||
this.title.text = value === 'SLEEPING' ? 'Jarvis · privacy' : 'Jarvis';
|
||||
const name = this._assistantName || 'Jarvis';
|
||||
this.title.text = value === 'SLEEPING' ? `${name} · privacy` : name;
|
||||
}
|
||||
_refreshStatusLine() {
|
||||
const value = this._state;
|
||||
@@ -416,7 +488,11 @@ export class ConversationView {
|
||||
} catch { this.addRow('J', json); }
|
||||
}
|
||||
endTalk() { this.onTalk?.(false); }
|
||||
destroy() { this.root.destroy(); }
|
||||
destroy() {
|
||||
this._clearLayoutTimers('_chatLayoutTimers');
|
||||
this._clearLayoutTimers('_thinkLayoutTimers');
|
||||
this.root.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const PANEL_STATES = {
|
||||
@@ -538,6 +614,7 @@ export class SessionPanel {
|
||||
this.view.thinkingScroll.set_height(Math.max(120, Math.min(240, monitor.height - 400)));
|
||||
}
|
||||
this.root.visible = true;
|
||||
this.view.followActive();
|
||||
}
|
||||
hide() { this.view.endTalk(); this.root.visible = false; this.minimized = true; }
|
||||
toggle() { this.root.visible ? this.hide() : this.show(true); }
|
||||
|
||||
Reference in New Issue
Block a user