diff options
Diffstat (limited to 'desktop/modules')
| -rw-r--r-- | desktop/modules/assistant/AssistantModule.qml | 67 |
1 files changed, 55 insertions, 12 deletions
diff --git a/desktop/modules/assistant/AssistantModule.qml b/desktop/modules/assistant/AssistantModule.qml index 1e3c1e0..5e7673c 100644 --- a/desktop/modules/assistant/AssistantModule.qml +++ b/desktop/modules/assistant/AssistantModule.qml @@ -27,7 +27,7 @@ Module { settings: true group: "Assistant" keywords: ["llm", "model", "llama", "server", "prompt", "temperature", "thinking", - "rounds", "tools", "environment", "daemon", "restart"] + "rounds", "tools", "environment", "daemon", "restart", "cloud", "provider"] readonly property string home: Quickshell.env("HOME") readonly property string daemonDir: home + "/Programming/GIT/desktop-assistant" @@ -57,13 +57,26 @@ Module { onDraftChanged: { const k = C.env(am.draft).map(e => e.key); if (JSON.stringify(k) !== JSON.stringify(am.envKeys)) am.envKeys = k; + const n = (C.clouds(am.draft) ?? []).length; + if (n !== am.cloudCount) am.cloudCount = n; } function envValue(key) { return (am.envList.find(e => e.key === key) ?? { value: "" }).value; } + // The [[cloud]] tables in the draft, null when one cannot be read (the + // page then leaves them alone), and the saved ones, which the canvas and + // Prefer offer. cloudCount follows the draft's count only, so rows are + // not rebuilt on each keystroke (as envKeys). + readonly property var clouds: C.clouds(am.draft) + readonly property var savedClouds: C.clouds(am.text) ?? [] + 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. property var toolsWanted: ({}) // 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 + readonly property bool valid: Object.keys(am.invalid).length === 0 && am.cloudError === "" dirty: am.draft !== am.base || Object.keys(am.toolsWanted).length > 0 // The daemon's built-in values, from assistant.py --defaults. property var defaults: ({}) @@ -163,14 +176,19 @@ Module { // The config first, then the tool renames, each only when there is // something to do; any failure stops there and says why. - function save() { + function save(done) { if (am.text !== am.base) { am.error = "config.toml changed on disk since these edits began. Discard to load it, then redo them."; return; } am.error = ""; const t = am.draft; - const tools = () => am.applyTools(Object.keys(am.toolsWanted), () => { am.pending = true; am.listTools(); }); + 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(); + }); 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; } @@ -197,6 +215,31 @@ Module { function setEnv(key, value) { am.draft = C.setEnv(am.draft, key, value); } function quote(s) { return C.quote(s); } + // patch is {name}, {url}, {key_cmd} or {models}; i === cloudCount adds one. + function editCloud(i, patch) { + const list = (am.clouds ?? []).map(c => Object.assign({}, c)); + if (i === list.length) list.push({ name: "", url: "", key_cmd: "", models: [] }); + Object.assign(list[i], patch); + am.draft = C.setClouds(am.draft, list); + } + function removeCloud(i) { am.draft = C.setClouds(am.draft, am.clouds.filter((c, j) => j !== i)); } + + // The daemon reads the saved config, so Fetch saves the draft first. + // The key command runs in that process; the key never reaches here. + function fetchModels(name) { + am.save(() => { + am.fetching = name; + am.run([am.python, am.daemonDir + "/assistant.py", "--cloud-models", name], null, + (code, out, err) => { + am.fetching = ""; + let got; + try { got = code === 0 ? JSON.parse(out) : (err.trim().split("\n").pop() || `exit ${code}`); } + catch (e) { got = "the assistant sent an unreadable list"; } + am.fetched = Object.assign({}, am.fetched, { [name]: got }); + }); + }); + } + // Every tools/*.toml is loaded; *.toml.off is the same file switched off. readonly property string lister: "import glob, json, os, sys, tomllib\n" + "out = []\n" @@ -269,17 +312,17 @@ Module { else { poll.left = 300; poll.start(); } }); } - // Called when the assistant canvas opens: the preferred model, if one is - // set, still listed, and neither loaded nor loading, is loaded. One model - // and one client at a time is how this desktop runs, so opening the - // assistant is the moment to bring its model back. The list is read on - // every open, preferred model or not, so hears is current. - function ensurePreferred() { + // Called when the canvas opens and when its chat's model changes, with + // the chat's model, else the preferred one: loaded if it is a router + // model neither loaded nor loading. A cloud id is not in the router's + // list, so nothing is swapped for it. One model and one client at a time + // is how this desktop runs, so this is the moment to bring the model + // back. The list is read every time, so hears is current. + function ensureModel(id) { if (am.loading !== "") return; am.listModels(() => { - const id = Prefs.assistantModel; const m = am.models.find(x => x.id === id); - if (id !== "" && m && m.status !== "loaded" && m.status !== "loading") am.loadModel(id); + if (m && m.status !== "loaded" && m.status !== "loading") am.loadModel(id); }); } |
