@@ -100,8 +100,8 @@ export class ConversationView {
|
||||
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._bindChip(this.chatTab, () => this.showTab('chat'));
|
||||
this._bindChip(this.thinkTab, () => this.showTab('thinking'));
|
||||
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 });
|
||||
@@ -174,7 +174,6 @@ export class ConversationView {
|
||||
this.replyFinalized = false;
|
||||
this._state = 'ARMED';
|
||||
this._tab = 'chat';
|
||||
this._userPickedTab = false;
|
||||
this._activity = '';
|
||||
this.reducedMotion = false;
|
||||
this._chatLayoutTimers = [];
|
||||
@@ -204,28 +203,17 @@ export class ConversationView {
|
||||
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); }
|
||||
_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);
|
||||
// Layout can change the range after the token handler has returned.
|
||||
for (const signal of ['notify::upper', 'notify::page-size'])
|
||||
adjustment?.connect?.(signal, () => this._pinScroll(scroll));
|
||||
scroll.connect('notify::mapped', () => {
|
||||
if (scroll.mapped) this._followAfterLayout(scroll);
|
||||
});
|
||||
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] || []) {
|
||||
@@ -239,42 +227,32 @@ export class ConversationView {
|
||||
try { child?.queue_relayout?.(); } catch {}
|
||||
try { child?.get_first_child?.()?.queue_relayout?.(); } catch {}
|
||||
}
|
||||
_pinScroll(scroll, { force = false } = {}) {
|
||||
_pinScroll(scroll) {
|
||||
if (this._destroyed) return;
|
||||
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, () => {
|
||||
this._pinScroll(scroll, { force });
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
}
|
||||
_followAfterLayout(scroll, { force = true } = {}) {
|
||||
_followAfterLayout(scroll) {
|
||||
if (this._destroyed) return;
|
||||
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 });
|
||||
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] = [];
|
||||
this._pinScroll(scroll);
|
||||
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 });
|
||||
this._followAfterLayout(this._tab === 'thinking' ? this.thinkingScroll : this.scroll);
|
||||
}
|
||||
showTab(name, { user = false } = {}) {
|
||||
if (user) this._userPickedTab = true;
|
||||
showTab(name) {
|
||||
this._tab = name === 'thinking' ? 'thinking' : 'chat';
|
||||
this._applyTab();
|
||||
}
|
||||
@@ -300,8 +278,7 @@ export class ConversationView {
|
||||
if (typeof this.transcript.remove_child === 'function') this.transcript.remove_child(first);
|
||||
else first.destroy();
|
||||
}
|
||||
if (this._tab === 'chat') this._followAfterLayout(this.scroll, { force: false });
|
||||
else this._followConversation();
|
||||
this.showTab('chat');
|
||||
return row;
|
||||
}
|
||||
token(text) {
|
||||
@@ -316,19 +293,16 @@ export class ConversationView {
|
||||
}
|
||||
row.text = `${row.text}${chunk}`;
|
||||
row.accessible_name = `Jarvis: ${row.text}`;
|
||||
if (this._tab === 'chat') this._followAfterLayout(this.scroll, { force: false });
|
||||
else this._followConversation();
|
||||
this.showTab('chat');
|
||||
}
|
||||
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');
|
||||
if (this._tab === 'thinking') this._followAfterLayout(this.thinkingScroll, { force: false });
|
||||
else this._followThinking();
|
||||
this.showTab('thinking');
|
||||
}
|
||||
toggleThinking() { this.showTab(this._tab === 'thinking' ? 'chat' : 'thinking', { user: true }); }
|
||||
finishThinking() { if (this._state !== 'THINKING') this.showTab('chat'); }
|
||||
toggleThinking() { this.showTab(this._tab === 'thinking' ? 'chat' : 'thinking'); }
|
||||
finishThinking() { this.showTab('chat'); }
|
||||
addToolCall(json) {
|
||||
this.streamingReply = false;
|
||||
this.replyFinalized = false;
|
||||
@@ -381,8 +355,7 @@ export class ConversationView {
|
||||
row.text = body;
|
||||
}
|
||||
row.accessible_name = `Activity: ${body}`;
|
||||
if (this._tab === 'chat') this._followAfterLayout(this.scroll, { force: false });
|
||||
else this._followConversation();
|
||||
this.showTab('chat');
|
||||
return row;
|
||||
}
|
||||
_addToolRow(text) { return this._setToolActivity(text); }
|
||||
@@ -441,16 +414,19 @@ export class ConversationView {
|
||||
}
|
||||
setState(state) {
|
||||
const value = STATES.has(state) ? state : 'ARMED';
|
||||
const changed = this._state !== value;
|
||||
this._state = value;
|
||||
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');
|
||||
// Polling repeats the coarse daemon state while thinking and answer tokens
|
||||
// alternate. Only a transition may select a pane; content selects it next.
|
||||
if (changed) {
|
||||
if (value === 'LISTENING') {
|
||||
this._activity = '';
|
||||
this.showTab('chat');
|
||||
} else if (value === 'THINKING') {
|
||||
this.showTab('thinking');
|
||||
} else if (value === 'SPEAKING' || value === 'ARMED') {
|
||||
this.showTab('chat');
|
||||
}
|
||||
}
|
||||
this._refreshStatusLine();
|
||||
const name = this._assistantName || 'Jarvis';
|
||||
@@ -489,6 +465,7 @@ export class ConversationView {
|
||||
}
|
||||
endTalk() { this.onTalk?.(false); }
|
||||
destroy() {
|
||||
this._destroyed = true;
|
||||
this._clearLayoutTimers('_chatLayoutTimers');
|
||||
this._clearLayoutTimers('_thinkLayoutTimers');
|
||||
this.root.destroy();
|
||||
|
||||
Reference in New Issue
Block a user