From d82f44188a91a6122c4d333a3df94e8ed8e6f289 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 23 Oct 2026 17:15:35 +0200 Subject: assistant settings: a cloud switch per tool; cloud Prefer rows show The Prefer rows for cloud models never built: the shell's JS engine has no Array.prototype.flatMap, and the Repeater's model threw a TypeError (seen in qs log). The ids are now joined with reduce in the module. Each tool row gains a cloud switch, applied on Save after the renames: it writes or drops a top-level cloud = true, validated with tomllib and replaced atomically, on name.toml or name.toml.off. Co-Authored-By: Claude Opus 5.5 --- desktop/modules/assistant/AssistantModule.qml | 70 +++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 10 deletions(-) (limited to 'desktop/modules/assistant/AssistantModule.qml') diff --git a/desktop/modules/assistant/AssistantModule.qml b/desktop/modules/assistant/AssistantModule.qml index 5e7673c..a05556d 100644 --- a/desktop/modules/assistant/AssistantModule.qml +++ b/desktop/modules/assistant/AssistantModule.qml @@ -67,17 +67,24 @@ Module { // not rebuilt on each keystroke (as envKeys). readonly property var clouds: C.clouds(am.draft) readonly property var savedClouds: C.clouds(am.text) ?? [] + // Their models as the canvas names them, "provider:model". reduce, not + // flatMap: the shell's JS engine has no flatMap. + readonly property var cloudIds: am.savedClouds.reduce( + (a, c) => a.concat(c.models.map(m => c.name + ":" + m)), []) property int cloudCount: 0 readonly property string cloudError: am.clouds ? C.cloudError(am.clouds) : "" // name -> the provider's ids, or why they could not be fetched. property var fetched: ({}) property string fetching: "" - // Tool switches flipped but not saved: name -> wanted on. + // Tool switches flipped but not saved: name -> wanted on; and the same + // for each tool's cloud = true, which offers it to cloud chats. property var toolsWanted: ({}) + property var cloudWanted: ({}) // Fields holding a value that cannot be written; Save waits for none. property var invalid: ({}) readonly property bool valid: Object.keys(am.invalid).length === 0 && am.cloudError === "" dirty: am.draft !== am.base || Object.keys(am.toolsWanted).length > 0 + || Object.keys(am.cloudWanted).length > 0 // The daemon's built-in values, from assistant.py --defaults. property var defaults: ({}) // [{name, on, description}] @@ -183,12 +190,14 @@ Module { } am.error = ""; const t = am.draft; - const changed = t !== am.text || Object.keys(am.toolsWanted).length > 0; - const tools = () => am.applyTools(Object.keys(am.toolsWanted), () => { - if (changed) am.pending = true; - am.listTools(); - if (done) done(); - }); + const changed = t !== am.text || Object.keys(am.toolsWanted).length > 0 + || Object.keys(am.cloudWanted).length > 0; + const tools = () => am.applyTools(Object.keys(am.toolsWanted), () => + am.applyCloud(Object.keys(am.cloudWanted), () => { + if (changed) am.pending = true; + am.listTools(); + if (done) done(); + })); if (t === am.text) { tools(); return; } am.run([am.python, "-c", am.writer, am.cfgPath], t, (code, out, err) => { if (code !== 0) { am.error = err.trim() || "config.toml could not be written"; return; } @@ -201,6 +210,7 @@ Module { am.error = ""; am.invalid = ({}); am.toolsWanted = ({}); + am.cloudWanted = ({}); am.base = am.text; am.draft = am.text; } @@ -246,9 +256,10 @@ Module { + "for p in glob.glob(sys.argv[1] + '/*.toml') + glob.glob(sys.argv[1] + '/*.toml.off'):\n" + " on = p.endswith('.toml')\n" + " name = os.path.basename(p).removesuffix('.off').removesuffix('.toml')\n" - + " try:\n d = tomllib.load(open(p, 'rb')).get('description', '')\n" - + " except Exception as e:\n d = f'unreadable: {e}'\n" - + " out.append({'name': name, 'on': on, 'description': d})\n" + + " try:\n t = tomllib.load(open(p, 'rb'))\n" + + " d, cloud = t.get('description', ''), t.get('cloud') is True\n" + + " except Exception as e:\n d, cloud = f'unreadable: {e}', False\n" + + " out.append({'name': name, 'on': on, 'cloud': cloud, 'description': d})\n" + "print(json.dumps(sorted(out, key=lambda t: t['name'])))\n" function listTools() { @@ -264,6 +275,45 @@ Module { if (!am.toolOn(t) === t.on) delete next[t.name]; else next[t.name] = !am.toolOn(t); am.toolsWanted = next; } + // Whether a tool goes to cloud chats in the draft, as toolOn. + function toolCloud(t) { return am.cloudWanted[t.name] ?? t.cloud; } + function toggleCloud(t) { + const next = Object.assign({}, am.cloudWanted); + if (!am.toolCloud(t) === t.cloud) delete next[t.name]; else next[t.name] = !am.toolCloud(t); + am.cloudWanted = next; + } + + // Sets or drops a tool file's top-level cloud line, validated with + // tomllib and replaced atomically, like config.toml. The file is + // name.toml or, switched off, name.toml.off. Added as the first line: + // a top-level key is valid there whatever the file holds below. + readonly property string clouder: "import os, re, sys, tomllib\n" + + "p, on = sys.argv[1], sys.argv[2] == '1'\n" + + "if not os.path.exists(p):\n p += '.off'\n" + + "lines = open(p).read().split('\\n')\n" + + "head = next((i for i, l in enumerate(lines) if l.lstrip().startswith('[')), len(lines))\n" + + "lines = [l for i, l in enumerate(lines) if i >= head or not re.match(r'\\s*cloud\\s*=', l)]\n" + + "if on:\n lines.insert(0, 'cloud = true')\n" + + "t = '\\n'.join(lines)\n" + + "try:\n tomllib.loads(t)\n" + + "except tomllib.TOMLDecodeError as e:\n sys.exit(f'{os.path.basename(p)}: not valid TOML: {e}')\n" + + "with open(p + '.tmp', 'w') as f:\n f.write(t)\n" + + "os.replace(p + '.tmp', p)\n" + + // One file at a time, after the renames, then done(). + function applyCloud(names, done) { + if (names.length === 0) { done(); return; } + const name = names[0]; + am.run([am.python, "-c", am.clouder, am.toolsDir + "/" + name + ".toml", + am.cloudWanted[name] ? "1" : "0"], null, (code, out, err) => { + if (code !== 0) { am.error = err.trim() || "could not change " + name; am.listTools(); return; } + const next = Object.assign({}, am.cloudWanted); + delete next[name]; + am.cloudWanted = next; + am.applyCloud(names.slice(1), done); + }); + } + // One rename at a time, then done(). -n never overwrites a file that // already holds the other name. function applyTools(names, done) { -- cgit v1.2.3