aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/assistant/AssistantModule.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-23 17:15:35 +0200
committerDanilo M. <danix@danix.xyz>2026-10-23 17:15:35 +0200
commitd82f44188a91a6122c4d333a3df94e8ed8e6f289 (patch)
treede15a1518c2941f01a5345581d66f0bddfac8bd0 /desktop/modules/assistant/AssistantModule.qml
parent566b1a73ad1caf7ed399f96f5ec45153f7bd980f (diff)
downloadquickshell-d82f44188a91a6122c4d333a3df94e8ed8e6f289.tar.gz
quickshell-d82f44188a91a6122c4d333a3df94e8ed8e6f289.zip
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 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules/assistant/AssistantModule.qml')
-rw-r--r--desktop/modules/assistant/AssistantModule.qml70
1 files changed, 60 insertions, 10 deletions
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) {