diff options
Diffstat (limited to 'desktop/modules/assistant/AssistantModule.qml')
| -rw-r--r-- | desktop/modules/assistant/AssistantModule.qml | 96 |
1 files changed, 80 insertions, 16 deletions
diff --git a/desktop/modules/assistant/AssistantModule.qml b/desktop/modules/assistant/AssistantModule.qml index 8253cc6..f738ea6 100644 --- a/desktop/modules/assistant/AssistantModule.qml +++ b/desktop/modules/assistant/AssistantModule.qml @@ -42,10 +42,29 @@ Module { readonly property var labels: ({ llm: "Server", system: "System prompt", tool_rounds: "Tool rounds", temperature: "Temperature", thinking: "Thinking" }) - // config.toml as last read or written; "" when there is none. + // config.toml as last read or written; "" when there is none. The page + // edits draft, which save() writes; base is the file the draft began + // from, so a save over a file changed meanwhile is refused. property string text: "" - readonly property var values: C.scalars(am.text, am.keys) - readonly property var envList: C.env(am.text) + property string base: "" + property string draft: "" + readonly property var values: C.scalars(am.draft, am.keys) + readonly property var envList: C.env(am.draft) + // The [env] keys, assigned only when the set changes: a model bound to the + // draft would rebuild every row on each keystroke and drop the focus of + // the one being typed in. Rows read their values from envValue(). + property var envKeys: [] + onDraftChanged: { + const k = C.env(am.draft).map(e => e.key); + if (JSON.stringify(k) !== JSON.stringify(am.envKeys)) am.envKeys = k; + } + function envValue(key) { return (am.envList.find(e => e.key === key) ?? { value: "" }).value; } + // 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 + dirty: am.draft !== am.base || Object.keys(am.toolsWanted).length > 0 // The daemon's built-in values, from assistant.py --defaults. property var defaults: ({}) // [{name, on, description}] @@ -87,8 +106,12 @@ Module { watchChanges: true printErrors: false onFileChanged: reload() - onLoaded: am.text = text() - onLoadFailed: am.text = "" + // A clean draft follows the file; a dirty one is kept for save(). + onLoaded: { + am.text = text(); + if (am.draft === am.base) { am.base = am.text; am.draft = am.text; } + } + onLoadFailed: if (am.draft === am.base) { am.text = ""; am.base = ""; am.draft = ""; } } // One Process per call, so calls never share one (a running Process @@ -130,15 +153,40 @@ Module { + "with open(p + '.tmp', 'w') as f:\n f.write(t)\n" + "os.replace(p + '.tmp', p)\n" - function save(t) { + // The config first, then the tool renames, each only when there is + // something to do; any failure stops there and says why. + function save() { + 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(); }); + if (t === am.text) { tools(); return; } am.run([am.python, "-c", am.writer, am.cfgPath], t, (code, out, err) => { - if (code === 0) { am.text = t; am.pending = true; am.error = ""; } - else am.error = err.trim() || "config.toml could not be written"; + if (code !== 0) { am.error = err.trim() || "config.toml could not be written"; return; } + am.text = t; + am.base = t; + tools(); }); } + function discard() { + am.error = ""; + am.invalid = ({}); + am.toolsWanted = ({}); + am.base = am.text; + am.draft = am.text; + } + function markInvalid(id, bad) { + if (!!am.invalid[id] === bad) return; + const next = Object.assign({}, am.invalid); + if (bad) next[id] = true; else delete next[id]; + am.invalid = next; + } // toml is the value written as TOML, or null to fall back to the default. - function setKey(key, toml) { am.save(C.setScalar(am.text, key, toml)); } - function setEnv(key, value) { am.save(C.setEnv(am.text, key, value)); } + function setKey(key, toml) { am.draft = C.setScalar(am.draft, key, toml); } + function setEnv(key, value) { am.draft = C.setEnv(am.draft, key, value); } function quote(s) { return C.quote(s); } // Every tools/*.toml is loaded; *.toml.off is the same file switched off. @@ -158,14 +206,27 @@ Module { }); } + // Whether a tool is on in the draft: its wanted state, else the file's. + function toolOn(t) { return am.toolsWanted[t.name] ?? t.on; } function toggleTool(t) { - const on = am.toolsDir + "/" + t.name + ".toml"; + const next = Object.assign({}, am.toolsWanted); + if (!am.toolOn(t) === t.on) delete next[t.name]; else next[t.name] = !am.toolOn(t); + am.toolsWanted = next; + } + // One rename at a time, then done(). -n never overwrites a file that + // already holds the other name. + function applyTools(names, done) { + if (names.length === 0) { done(); return; } + const name = names[0]; + const on = am.toolsDir + "/" + name + ".toml"; const off = on + ".off"; - // -n: never overwrite a file that already holds the other name. - am.run(["mv", "-n", t.on ? on : off, t.on ? off : on], null, (code, out, err) => { - if (code === 0) { am.pending = true; am.error = ""; } - else am.error = err.trim() || "could not switch " + t.name; - am.listTools(); + const want = am.toolsWanted[name]; + am.run(["mv", "-n", want ? off : on, want ? on : off], null, (code, out, err) => { + if (code !== 0) { am.error = err.trim() || "could not switch " + name; am.listTools(); return; } + const next = Object.assign({}, am.toolsWanted); + delete next[name]; + am.toolsWanted = next; + am.applyTools(names.slice(1), done); }); } @@ -240,6 +301,9 @@ Module { page: Component { Page { title: "Assistant" + actions: Component { + SaveBar { dirty: am.dirty; valid: am.valid; onSave: am.save(); onDiscard: am.discard() } + } AssistantPage { width: parent.width; asst: am } } } |
