aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/assistant/AssistantModule.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/assistant/AssistantModule.qml')
-rw-r--r--desktop/modules/assistant/AssistantModule.qml96
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 }
}
}