Updates
Rolling release / release (push) Successful in 7m7s

This commit is contained in:
2026-09-12 17:04:03 -04:00
parent 4383763cb5
commit 9ad09b593c
5 changed files with 414 additions and 175 deletions
+40 -63
View File
@@ -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();