Personality controls in settings
Rolling release / release (push) Successful in 7m29s

This commit is contained in:
2026-09-12 15:56:35 -04:00
parent 5317576087
commit 2f6e4b3af9
19 changed files with 104 additions and 13 deletions
@@ -38,3 +38,8 @@ button.jarvis-swatch:checked,
button.jarvis-swatch:active {
box-shadow: 0 0 0 2px @jarvis_ink, 0 0 0 4px @jarvis_gold;
}
textview.jarvis-prompt {
background: @jarvis_ink;
color: #F6F7FB;
}
@@ -10,6 +10,15 @@
"maxLength": 32,
"description": "What to call the assistant in the tray, speech, and workspace IDENTITY.md."
},
{
"key": "assistantPrompt",
"title": "How they should act",
"group": "Identity",
"default": "",
"type": "text",
"maxLength": 4000,
"description": "Optional extra instructions: tone, habits, jokes, things to always do or avoid. Spoken safety rules still win."
},
{
"key": "ttsEnabled",
"title": "Spoken replies",
@@ -123,6 +123,38 @@ export class SettingsEditor {
clear.connect('clicked', () => { changed(''); row.subtitle = field.description; });
row.add_suffix(choose); row.add_suffix(clear);
row._setValue = value => { row.subtitle = value || field.description; };
} else if (field.type === 'text') {
const box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 6, margin_start: 12, margin_end: 12, margin_top: 6, margin_bottom: 8 });
const title = new Gtk.Label({ label: field.title, xalign: 0, wrap: true });
title.add_css_class('heading');
const help = new Gtk.Label({ label: field.description, wrap: true, xalign: 0 });
help.add_css_class('dim-label');
const view = new Gtk.TextView({ wrap_mode: Gtk.WrapMode.WORD_CHAR, accepts_tab: false, left_margin: 8, right_margin: 8, top_margin: 8, bottom_margin: 8 });
view.add_css_class('jarvis-prompt');
const text = String(this.values[field.key] || '');
view.buffer.set_text(text, -1);
view.buffer.connect('changed', () => {
const buffer = view.buffer;
changed(buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), false));
});
const scroll = new Gtk.ScrolledWindow({
min_content_height: 140,
max_content_height: 220,
hexpand: true,
vexpand: false,
has_frame: true,
propagate_natural_height: true,
});
scroll.set_child(view);
box.append(title);
box.append(help);
box.append(scroll);
box._setValue = value => {
const next = String(value || '');
const buffer = view.buffer;
if (buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), false) !== next) buffer.set_text(next, -1);
};
row = box;
} else {
row = new Adw.EntryRow({ title: field.title, tooltip_text: field.description, text: field.type === 'list' ? this.values[field.key].join(', ') : this.values[field.key] });
row.connect('changed', () => changed(field.type === 'list' ? row.text.split(',').map(v => v.trim()).filter(Boolean) : row.text));
@@ -21,10 +21,14 @@ export function normalizeSettings(source, fields, { strict = false } = {}) {
value = value.replace(/[\r\n\t]/g, ' ').replace(/\s+/g, ' ').slice(0, field.maxLength || 32);
if (!value || /[<>&]/.test(value)) value = field.default;
}
if (field.type === 'text' && typeof value === 'string') {
value = value.replace(/\0/g, '').replace(/\s+$/g, '').slice(0, field.maxLength || 4000);
}
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')
: field.type === 'text' ? typeof value === 'string' && value.length <= (field.maxLength || 4000)
: 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;