Allow to change the agents name + Workspace files
Rolling release / release (push) Successful in 8m2s

This commit is contained in:
2026-09-12 15:30:11 -04:00
parent 874154f39a
commit 5317576087
30 changed files with 338 additions and 174 deletions
@@ -65,6 +65,7 @@ export default class JarvisExtension extends Extension {
this._bindSurface(this.session.view);
this._applyAccessibility();
this._removeShellService = installShellService();
this._assistantName = 'Jarvis';
this._indicator = new PanelMenu.Button(0.0, 'Jarvis QVAC', false); this._indicator.accessible_name = 'Jarvis voice assistant';
this._panelBox = new St.BoxLayout({ style_class: 'jarvis-panel-box', y_align: Clutter.ActorAlign.CENTER });
this._mark = new St.Icon({ icon_name: 'audio-input-microphone-symbolic', icon_size: 16, style_class: 'jarvis-panel-mark', y_align: Clutter.ActorAlign.CENTER });
@@ -257,11 +258,13 @@ export default class JarvisExtension extends Extension {
const tts = Boolean(voice.tts || voice.speech);
this._eachView((view) => view.setVoiceStatus({ tts, input, wake: voice.wake }));
}
const name = status.settings?.assistantName;
if (name) this._applyAssistantName(name);
} catch { this._eachView((view) => view.setConnectionStatus('voice-unavailable')); }
}
_call(name, signature, value) {
if (!this.proxy?.owned?.()) {
if (name !== 'PushToTalk') this._eachView((view) => view.setNotice('Jarvis is starting…'));
if (name !== 'PushToTalk') this._eachView((view) => view.setNotice(`${this._assistantName || 'Jarvis'} is starting…`));
this._connectDaemon();
return;
}
@@ -272,11 +275,20 @@ export default class JarvisExtension extends Extension {
this._eachView((view) => view.setNotice(fallback));
});
}
_applyAssistantName(name) {
const text = safeText(name).slice(0, 32) || 'Jarvis';
this._assistantName = text;
this._eachView((view) => view.setAssistantName?.(text));
this._indicator.accessible_name = `${text} voice assistant`;
this._glyph.accessible_name = `${text} idle`;
if (this._glyph.visible !== false) this._glyph.text = text;
}
_setState(state) {
const value = safeText(state);
this._glyph.text = 'Jarvis';
this._glyph.accessible_name = `Jarvis ${value.toLowerCase()}`;
this._indicator.accessible_name = `Jarvis ${value.toLowerCase()}`;
const name = this._assistantName || 'Jarvis';
this._glyph.text = name;
this._glyph.accessible_name = `${name} ${value.toLowerCase()}`;
this._indicator.accessible_name = `${name} ${value.toLowerCase()}`;
if (this._panelBox) {
for (const name of ['armed', 'listening', 'speaking', 'thinking', 'sleeping']) {
this._panelBox.remove_style_class_name(`jarvis-state-${name}`);
@@ -1,6 +1,15 @@
{
"version": 1,
"fields": [
{
"key": "assistantName",
"title": "Assistant name",
"group": "Identity",
"default": "Jarvis",
"type": "string",
"maxLength": 32,
"description": "What to call the assistant in the tray, speech, and workspace IDENTITY.md."
},
{
"key": "ttsEnabled",
"title": "Spoken replies",
@@ -17,11 +17,15 @@ export function normalizeSettings(source, fields, { strict = false } = {}) {
if (field.type === 'list' && typeof value === 'string') value = value.split(',').map(v => v.trim()).filter(Boolean);
if (field.key === 'asrLanguage' && typeof value === 'string') value = value.toLowerCase().split(/[-_]/)[0];
if (['string', 'file'].includes(field.type) && typeof value === 'string') value = value.trim();
if (field.key === 'assistantName' && typeof value === 'string') {
value = value.replace(/[\r\n\t]/g, ' ').replace(/\s+/g, ' ').slice(0, field.maxLength || 32);
if (!value || /[<>&]/.test(value)) value = field.default;
}
const valid = field.type === 'boolean' ? typeof value === 'boolean'
: field.type === 'number' ? Number.isFinite(value) && value >= field.min && value <= field.max && (field.step < 1 || Number.isInteger(value))
: field.type === 'choice' ? field.options.some(option => option.value === value)
: field.type === 'list' ? Array.isArray(value) && value.every(item => typeof item === 'string')
: typeof value === 'string' && value.length <= 4096;
: typeof value === 'string' && value.length <= (field.maxLength || 4096);
if (!valid && strict) throw new Error(`Invalid value for ${field.title}`);
result[field.key] = valid ? value : field.default;
}
@@ -159,7 +159,7 @@ export function fillSettingsWindow(window, settings, directory) {
const editor = new SettingsEditor(directory, settings);
const intro = new Adw.PreferencesGroup();
intro.add(brandBanner(directory));
const voice = editor.page(window, 'Voice', ['Speech', 'Voice design'], 'audio-speakers-symbolic', true, intro);
const voice = editor.page(window, 'Voice', ['Identity', 'Speech', 'Voice design'], 'audio-speakers-symbolic', true, intro);
window.add(voice);
const listening = editor.page(window, 'Listening', ['Listening', 'Wake and privacy', 'Detection tuning', 'Audio routing'], 'audio-input-microphone-symbolic');
window.add(listening);
@@ -181,6 +181,15 @@ export class ConversationView {
_bindChip(button, action) {
button.connect('clicked', () => action?.());
}
setAssistantName(name) {
const text = safeText(name).slice(0, 32) || 'Jarvis';
this._assistantName = text;
this.title.text = text;
this.title.accessible_name = `${text} status`;
this.root.accessible_name = this.compact ? `${text} voice assistant` : `${text} conversation`;
if (this.settings) this.settings.accessible_name = `Open ${text} settings`;
if (this.notice) this.notice.accessible_name = `${text} notice`;
}
clear() {
this.transcript.destroy_all_children();
this.thinking.text = '';