diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-17 11:34:07 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-17 11:34:07 +0200 |
| commit | 080d3d9aaffa8848d6de8546511f096d8e7c30f5 (patch) | |
| tree | 2663bd5c774d8db7fa024e82546f6cfa04c07e29 /desktop/modules/assistant/AssistantPage.qml | |
| parent | b8c0f01f058c87db5924067dbb475aa241c71b46 (diff) | |
| download | quickshell-080d3d9aaffa8848d6de8546511f096d8e7c30f5.tar.gz quickshell-080d3d9aaffa8848d6de8546511f096d8e7c30f5.zip | |
settings: pages edit a draft, and Save writes it
The qtmaildir, Assistant and Ask presets pages wrote a field on Enter,
and a switch or chip the moment it was clicked, so a half-finished
change reached the file and a slip could not be taken back. They now
edit a draft as fields are typed, and a Save and Discard pair in the
page header (SaveBar, through Page.actions) writes or drops it. Save
waits while a field holds a value that cannot be written, and a draft
over a file that changed on disk meanwhile is refused at Save rather
than written over it. A dot in the sidebar marks a page with unsaved
edits; drafts survive moving between pages. The four Mail pages share
one draft, since they are one file. Tool switches are staged too and
renamed on Save; model Load, Prefer and Restart stay immediate, being
actions rather than settings.
Live editing needed two rules. A field is no longer bound to its value,
which would reset the cursor and eat a typed trailing space; it follows
the value only when the draft moves under it. And a Repeater's model is
now the set of row keys (or a count), assigned only when that set
changes: an array rebuilt from the draft on each keystroke recreates
every row, including the one being typed in, which loses its focus.
Git, autostart's rows, HyprSunset, HyprIdle and User keep their own
buttons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules/assistant/AssistantPage.qml')
| -rw-r--r-- | desktop/modules/assistant/AssistantPage.qml | 94 |
1 files changed, 48 insertions, 46 deletions
diff --git a/desktop/modules/assistant/AssistantPage.qml b/desktop/modules/assistant/AssistantPage.qml index c6b63fc..832e9be 100644 --- a/desktop/modules/assistant/AssistantPage.qml +++ b/desktop/modules/assistant/AssistantPage.qml @@ -14,9 +14,9 @@ import QtQuick.Controls import "../.." // The view over AssistantModule: the model settings, the tools' environment, -// the tools themselves, and the restart that applies them. A field saves on -// Enter; emptied, it falls back to the daemon's default, which it shows as -// its placeholder. +// the tools themselves, and the restart that applies them. Every field and +// switch edits the module's draft; the header's Save writes it. Emptied, a +// field falls back to the daemon's default, which it shows as placeholder. Column { id: page spacing: 8 @@ -44,20 +44,24 @@ Column { color: Theme.text } - // A one-line input that saves on Enter and undoes on Escape; edited - // reports every keystroke, for the add row. + // A one-line input reporting every keystroke, which the page turns into + // a draft edit. It is not bound to value, which would reset the cursor + // and eat a trailing space; it follows the value only when the draft + // moves under it (Discard, an add). bad outlines a refused value. component Field: Rectangle { id: field property string value: "" property string placeholder: "" - signal commit(string text) + property bool bad: false signal edited(string text) - // Commit on Enter even when unchanged: the add row's fields track - // their value as they are typed, so they never differ from it. - property bool live: false + signal accepted height: 34 radius: 8 color: Theme.surface + border.width: field.bad ? 1 : 0 + border.color: Theme.red + onValueChanged: if (ti.text.trim() !== field.value.trim()) ti.text = field.value + onBadChanged: if (!field.bad) ti.text = field.value TextInput { id: ti anchors { fill: parent; leftMargin: 10; rightMargin: 10 } @@ -66,13 +70,9 @@ Column { selectByMouse: true color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } - text: field.value + Component.onCompleted: text = field.value onTextEdited: field.edited(text) - onAccepted: if (field.live || text !== field.value) field.commit(text) - Keys.onEscapePressed: event => { - if (text !== field.value) text = field.value; - else event.accepted = false; - } + onAccepted: field.accepted() Text { anchors.verticalCenter: parent.verticalCenter width: parent.width @@ -126,7 +126,7 @@ Column { anchors { left: parent.left; right: restartBtn.left; rightMargin: 8; verticalCenter: parent.verticalCenter } wrapMode: Text.Wrap text: page.asst.pending ? "Saved. The assistant reads these at startup: restart it to apply." - : "The assistant reads these at startup." + : "The assistant reads these at startup: Save, then restart it." color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } } @@ -245,7 +245,7 @@ Column { anchors.fill: parent value: page.v.llm ?? "" placeholder: page.d.llm ?? "" - onCommit: t => page.asst.setKey("llm", t.trim() ? page.asst.quote(t.trim()) : null) + onEdited: t => page.asst.setKey("llm", t.trim() ? page.asst.quote(t.trim()) : null) } } @@ -258,16 +258,18 @@ Column { width: 120 value: page.v.temperature !== undefined ? String(page.v.temperature) : "" placeholder: "the model's preset" - onCommit: t => { + bad: page.asst.invalid["key:temperature"] ?? false + onEdited: t => { const x = Number(t.trim()); - if (t.trim() === "") page.asst.setKey("temperature", null); - else if (!isNaN(x) && x >= 0 && x <= 2) page.asst.setKey("temperature", String(x)); - else page.asst.error = "Temperature is a number from 0 to 2."; + const ok = t.trim() === "" || (!isNaN(x) && x >= 0 && x <= 2); + page.asst.markInvalid("key:temperature", !ok); + if (ok) page.asst.setKey("temperature", t.trim() === "" ? null : String(x)); } } Small { anchors { left: temp.right; leftMargin: 10; verticalCenter: parent.verticalCenter } text: "0 to 2; empty uses the llama-server preset's" + color: temp.bad ? Theme.red : Theme.subtext } } @@ -280,16 +282,18 @@ Column { width: 120 value: page.v.tool_rounds !== undefined ? String(page.v.tool_rounds) : "" placeholder: page.d.tool_rounds !== undefined ? String(page.d.tool_rounds) : "" - onCommit: t => { + bad: page.asst.invalid["key:tool_rounds"] ?? false + onEdited: t => { const n = Number(t.trim()); - if (t.trim() === "") page.asst.setKey("tool_rounds", null); - else if (Number.isInteger(n) && n >= 0 && n <= 20) page.asst.setKey("tool_rounds", String(n)); - else page.asst.error = "Tool rounds is a whole number from 0 to 20."; + const ok = t.trim() === "" || (Number.isInteger(n) && n >= 0 && n <= 20); + page.asst.markInvalid("key:tool_rounds", !ok); + if (ok) page.asst.setKey("tool_rounds", t.trim() === "" ? null : String(n)); } } Small { anchors { left: rounds.right; leftMargin: 10; verticalCenter: parent.verticalCenter } - text: "tool round trips per question" + text: "a whole number, 0 to 20: tool round trips per question" + color: rounds.bad ? Theme.red : Theme.subtext } } @@ -338,6 +342,11 @@ Column { background: null padding: 0 text: page.shown("system") + // Into the draft as it is typed; the default, or nothing, + // unsets the key rather than copying the default in. + onTextChanged: if (prompt.text !== page.shown("system")) + page.asst.setKey("system", prompt.text.trim() === "" || prompt.text === page.d.system + ? null : page.asst.quote(prompt.text)) } } } @@ -345,12 +354,6 @@ Column { Row { spacing: 8 Button { - text: "Save prompt" - enabled: prompt.text.trim() !== "" && prompt.text !== page.shown("system") - onClicked: page.asst.setKey("system", prompt.text === page.d.system ? null - : page.asst.quote(prompt.text)) - } - Button { text: "Use the default" enabled: page.v.system !== undefined onClicked: page.asst.setKey("system", null) @@ -374,12 +377,12 @@ Column { property string confirming: "" Repeater { - model: page.asst.envList + model: page.asst.envKeys delegate: Item { id: envRow - required property var modelData - objectName: "env:" + modelData.key + required property string modelData + objectName: "env:" + modelData width: page.width height: 34 @@ -388,25 +391,25 @@ Column { anchors { left: parent.left; verticalCenter: parent.verticalCenter } width: 120 elide: Text.ElideRight - text: envRow.modelData.key + text: envRow.modelData color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } } Field { anchors { left: envKey.right; leftMargin: 8; right: envDel.left; rightMargin: 8; top: parent.top; bottom: parent.bottom } - value: envRow.modelData.value - onCommit: t => page.asst.setEnv(envRow.modelData.key, t) + value: page.asst.envValue(envRow.modelData) + onEdited: t => page.asst.setEnv(envRow.modelData, t) } Button { id: envDel anchors { right: parent.right; verticalCenter: parent.verticalCenter } danger: true - text: page.confirming === envRow.modelData.key ? "Confirm" : "Remove" + text: page.confirming === envRow.modelData ? "Confirm" : "Remove" onClicked: { - if (page.confirming === envRow.modelData.key) { + if (page.confirming === envRow.modelData) { page.confirming = ""; - page.asst.setEnv(envRow.modelData.key, null); - } else page.confirming = envRow.modelData.key; + page.asst.setEnv(envRow.modelData, null); + } else page.confirming = envRow.modelData; } } } @@ -439,9 +442,8 @@ Column { anchors { left: keyField.right; leftMargin: 8; right: addBtn.left; rightMargin: 8; top: parent.top; bottom: parent.bottom } value: page.newValue placeholder: "value" - live: true onEdited: t => page.newValue = t - onCommit: page.addEnv() + onAccepted: page.addEnv() } Button { id: addBtn @@ -474,7 +476,7 @@ Column { Switch { id: toolSw anchors { left: parent.left; verticalCenter: parent.verticalCenter } - checked: toolRow.modelData.on + checked: page.asst.toolOn(toolRow.modelData) onToggled: page.asst.toggleTool(toolRow.modelData) } Column { @@ -482,7 +484,7 @@ Column { anchors { left: toolSw.right; leftMargin: 10; right: parent.right; verticalCenter: parent.verticalCenter } Text { text: toolRow.modelData.name - color: toolRow.modelData.on ? Theme.text : Theme.subtext + color: page.asst.toolOn(toolRow.modelData) ? Theme.text : Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } } Small { |
