import QtQuick import QtQuick.Controls import QtQuick.Layouts // Provider, model and key: what the engine needs before it can talk to a model // at all. Everything here goes through AgentCore, which moves the engine's own // config (its session stays where it is) and remembers the choice. A key is // verified against the provider before it is saved and is never read back into // this window -- the dialog only ever says where the key it will use comes // from. // // Three things live under "Advanced options", because they are for the odd // provider or the odd model: the provider's own entry (name, base URL, key // variable, models -- the engine keeps those in /models.json, which is // how a provider the presets do not know is added or an existing one is // pointed somewhere else) and the generation parameters (temperature, top-p, // max tokens), which are remembered per provider. Dialog { id: dlg objectName: "settingsDialog" parent: Overlay.overlay anchors.centerIn: parent modal: true closePolicy: Popup.CloseOnEscape padding: Theme.pad property var providers: [] // AgentCore::providers() property var choices: [] // AgentCore::modelChoices(picked) property string picked: "" property string model: "" property string note: "" // what the last action answered property string keyEnv: "" property string keyFrom: "" property bool hasKey: false property bool advanced: false property string modelFilter: "" // narrows a long model list /* The advanced form: the picked provider's entry, editable. */ property string advName: "" property string advBase: "" property string advKeyEnv: "" property string advModel: "" property string advModels: "" property string tempText: "" property string topPText: "" property string maxTokText: "" signal applyRequested(string provider, string model, string key, real temperature, real topP, int maxTokens) width: Math.min(660, parent ? parent.width - 32 : 660) height: Math.min(640, parent ? parent.height - 40 : 640) /* The Basic style paints a light panel; this window is dark, so the * dialog brings its own surface like every other one here. */ background: Rectangle { color: Theme.surface radius: Theme.radius border.width: 1 border.color: Theme.border } // A long provider list (a router lists hundreds) is a wall of chips: the // filter narrows it and the rest is one line ("+N more"). readonly property int chipCap: 40 readonly property var chips: { const f = modelFilter.trim().toLowerCase() const out = [] for (let i = 0; i < choices.length; i++) { const c = String(choices[i]) if (!f.length || c.toLowerCase().indexOf(f) >= 0) out.push(c) } return out } readonly property int chipsHidden: Math.max(0, chips.length - chipCap) /* "0.2" -> 0.2; anything empty or unreadable -> 0 (the field is not sent, * which is not the same as sending 0). */ function num(text) { const v = parseFloat(String(text).replace(",", ".")) return isFinite(v) && v > 0 ? v : 0 } function pickedEntry() { for (let i = 0; i < providers.length; i++) if (providers[i].name === dlg.picked) return providers[i] return null } /* Fill it in: the presets, the provider the engine is on now, and the * model that provider last used. */ function load(list, provider, currentModel) { providers = list picked = provider model = currentModel note = "" for (let i = 0; i < list.length; i++) if (list[i].current === true) picked = list[i].name choices = picked.length ? agent.modelChoices(picked) : [] modelFilter = "" syncPick() } /* Where the picked provider's key would come from and what it is set to. * From the entries -- a key itself never reaches the QML side. */ function syncPick() { hasKey = false keyFrom = "" keyEnv = "" const p = pickedEntry() if (p) { hasKey = p.hasKey === true keyFrom = p.keyFrom keyEnv = p.keyEnv /* The advanced form describes the picked provider: edit it, or * type another name to add one. */ advName = p.name advBase = p.base advKeyEnv = p.keyEnv advModel = p.model advModels = p.custom === true ? agent.modelChoices(p.name).join(", ") : "" } const g = picked.length ? agent.genParams(picked) : null tempText = g && g.temperature > 0 ? String(g.temperature) : "" topPText = g && g.top_p > 0 ? String(g.top_p) : "" maxTokText = g && g.max_tokens > 0 ? String(g.max_tokens) : "" } function keyState() { if (hasKey) return keyFrom === "saved" ? "a key is saved · paste another to replace it" : "key from the environment: " + keyFrom return keyEnv.length ? "no key yet — set " + keyEnv + ", or paste one here" : "no key yet — paste one here" } function usageLine() { const u = agent.usage() if (!u.requests) return "no tokens spent in this session yet" let s = u.requests + (u.requests === 1 ? " request" : " requests") + " · " + u.prompt + " in · " + u.out + " out" if (u.priced) s += " · " + u.cost.toFixed(4) + " " + u.currency return s } /* Write the form's entry into the engine's user file and list it again. */ function saveProvider() { /* The fields are the truth here, not the properties: a hand-typed * edit is what the user wants saved. */ const name = advNameField.text.trim() const err = agent.addProvider(name, advBaseField.text.trim(), advKeyEnvField.text.trim(), advModelField.text.trim(), advModelsField.text) if (err.length) { note = err return } providers = agent.providers() picked = name choices = agent.modelChoices(name) syncPick() note = "saved " + name + " to models.json — it is in the list above now" } function removeProvider() { const typed = advNameField.text.trim() const name = typed.length ? typed : picked const rc = agent.removeProvider(name) if (rc < 0) { note = "could not remove " + name return } providers = agent.providers() note = rc === 0 ? name + " is not in models.json (a preset of that name stays)" : "removed " + name + " from models.json" if (rc === 1 && providers.length > 0) picked = providers[0].name choices = picked.length ? agent.modelChoices(picked) : [] syncPick() } /* The form's fields follow the picked provider (and a user edit breaks * the binding, so the push is imperative). */ onAdvNameChanged: if (advNameField && advNameField.text !== advName) advNameField.text = advName onAdvBaseChanged: if (advBaseField && advBaseField.text !== advBase) advBaseField.text = advBase onAdvKeyEnvChanged: if (advKeyEnvField && advKeyEnvField.text !== advKeyEnv) advKeyEnvField.text = advKeyEnv onAdvModelChanged: if (advModelField && advModelField.text !== advModel) advModelField.text = advModel onAdvModelsChanged: if (advModelsField && advModelsField.text !== advModels) advModelsField.text = advModels onTempTextChanged: if (tempField && tempField.text !== tempText) tempField.text = tempText onTopPTextChanged: if (topPField && topPField.text !== topPText) topPField.text = topPText onMaxTokTextChanged: if (maxTokField && maxTokField.text !== maxTokText) maxTokField.text = maxTokText // One labelled field of the advanced form. component AdvField: ColumnLayout { property string label: "" property alias text: field.text property alias placeholder: field.placeholderText property int hintWidth: 0 Layout.fillWidth: true spacing: 2 Text { text: parent.label color: Theme.textDim font.pixelSize: Theme.smallSize } TextField { id: field Layout.fillWidth: true implicitHeight: Theme.touchTarget selectByMouse: true color: Theme.text placeholderTextColor: Theme.textFaint selectionColor: Theme.accentDim inputMethodHints: Qt.ImhNoPredictiveText background: Rectangle { radius: Theme.radiusSm color: Theme.bg border.width: 1 border.color: field.activeFocus ? Theme.accentDim : Theme.border } } } contentItem: ColumnLayout { objectName: "settingsPanel" spacing: Theme.gap Text { Layout.fillWidth: true text: "Provider, model, key" color: Theme.text font.pixelSize: Theme.bodySize font.bold: true } Text { Layout.fillWidth: true text: "Keys are stored under /cst-agent/credentials (mode 0600) and are never shown again." color: Theme.textFaint font.pixelSize: Theme.smallSize wrapMode: Text.Wrap } /* Everything scrolls, so the buttons stay where they are and a long * model list cannot push them out of the dialog. */ Flickable { id: body objectName: "settingsBody" Layout.fillWidth: true Layout.fillHeight: true Layout.minimumHeight: 120 clip: true contentWidth: width contentHeight: bodyCol.implicitHeight boundsBehavior: Flickable.StopAtBounds ScrollBar.vertical: ScrollBar {} ColumnLayout { id: bodyCol width: parent.width spacing: Theme.gap ColumnLayout { Layout.fillWidth: true spacing: 2 Repeater { model: dlg.providers delegate: Rectangle { id: prow required property var modelData readonly property bool selected: dlg.picked === modelData.name objectName: "providerRow:" + modelData.name Layout.fillWidth: true implicitHeight: prowCol.implicitHeight + 8 radius: Theme.radiusSm color: prow.selected ? Theme.accentDim : prowHover.hovered ? Theme.surfaceAlt : "transparent" border.width: 1 border.color: prow.selected ? Theme.accent : "transparent" HoverHandler { id: prowHover } MouseArea { anchors.fill: parent onClicked: { dlg.picked = prow.modelData.name modelField.text = prow.modelData.model dlg.choices = agent.modelChoices(prow.modelData.name) dlg.modelFilter = "" dlg.syncPick() } } ColumnLayout { id: prowCol anchors { left: parent.left right: parent.right top: parent.top margins: 4 } spacing: 0 RowLayout { Layout.fillWidth: true spacing: Theme.gap Text { text: prow.modelData.name color: prow.selected ? Theme.accentText : Theme.textDim font.pixelSize: Theme.smallSize font.bold: prow.selected } Text { visible: prow.modelData.current === true text: "in use" color: prow.selected ? Theme.okOnAccent : Theme.ok font.pixelSize: Theme.smallSize } Item { Layout.fillWidth: true } Text { text: prow.modelData.custom === true ? "custom" : prow.modelData.source color: prow.selected ? Theme.accentTextDim : Theme.textFaint font.pixelSize: Theme.smallSize } } Text { Layout.fillWidth: true elide: Text.ElideMiddle text: prow.modelData.base + " · " + (prow.modelData.hasKey === true ? (prow.modelData.keyFrom === "saved" ? "key saved" : "key from " + prow.modelData.keyFrom) : (prow.modelData.keyEnv.length ? "no key (" + prow.modelData.keyEnv + ")" : "no key")) color: prow.selected ? Theme.accentTextDim : Theme.textFaint font.pixelSize: Theme.smallSize } } } } } Text { Layout.fillWidth: true text: "Model" color: Theme.textDim font.pixelSize: Theme.smallSize font.bold: true } RowLayout { Layout.fillWidth: true spacing: Theme.gap TextField { id: modelField objectName: "settingsModel" Layout.fillWidth: true text: dlg.model implicitHeight: Theme.touchTarget selectByMouse: true color: Theme.text placeholderText: "model name" placeholderTextColor: Theme.textFaint selectionColor: Theme.accentDim inputMethodHints: Qt.ImhNoPredictiveText background: Rectangle { radius: Theme.radiusSm color: Theme.bg border.width: 1 border.color: modelField.activeFocus ? Theme.accentDim : Theme.border } } Text { Layout.preferredWidth: 90 elide: Text.ElideRight text: modelField.text.length ? agent.modelInfo(modelField.text) : "" color: Theme.textFaint font.pixelSize: Theme.smallSize } } /* A router can list hundreds of models: filter, cap, say how * many are left instead of filling the dialog. */ RowLayout { Layout.fillWidth: true visible: dlg.choices.length > 12 spacing: Theme.gap TextField { id: filterField objectName: "settingsFilter" Layout.fillWidth: true text: dlg.modelFilter implicitHeight: Theme.segmentHeight selectByMouse: true color: Theme.text placeholderText: "filter " + dlg.choices.length + " models" placeholderTextColor: Theme.textFaint selectionColor: Theme.accentDim inputMethodHints: Qt.ImhNoPredictiveText onTextChanged: dlg.modelFilter = text background: Rectangle { radius: Theme.radiusSm color: Theme.bg border.width: 1 border.color: filterField.activeFocus ? Theme.accentDim : Theme.border } } Text { Layout.preferredWidth: 110 elide: Text.ElideRight text: dlg.chips.length + " shown" + (dlg.chipsHidden ? " · +" + dlg.chipsHidden + " more" : "") color: Theme.textFaint font.pixelSize: Theme.smallSize } } Flow { Layout.fillWidth: true spacing: 6 visible: dlg.choices.length > 0 Repeater { model: dlg.chips.slice(0, dlg.chipCap) delegate: Rectangle { id: chip required property var modelData readonly property string name: String(chip.modelData) objectName: "modelChoice:" + chip.name implicitWidth: chipText.implicitWidth + 14 implicitHeight: 26 radius: Theme.radiusSm color: chipHover.hovered ? Theme.surfaceHi : "transparent" border.width: 1 border.color: modelField.text === chip.name ? Theme.accent : Theme.border HoverHandler { id: chipHover } Text { id: chipText anchors.centerIn: parent text: chip.name color: modelField.text === chip.name ? Theme.text : Theme.textDim font.pixelSize: Theme.smallSize } TapHandler { onTapped: modelField.text = chip.name } } } } TextField { id: keyField objectName: "settingsKey" Layout.fillWidth: true implicitHeight: Theme.touchTarget echoMode: TextInput.Password selectByMouse: true color: Theme.text placeholderText: "API key (leave empty to keep what is there)" placeholderTextColor: Theme.textFaint selectionColor: Theme.accentDim inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhSensitiveData onAccepted: dlg.apply() background: Rectangle { radius: Theme.radiusSm color: Theme.bg border.width: 1 border.color: keyField.activeFocus ? Theme.accentDim : Theme.border } } Text { Layout.fillWidth: true text: dlg.keyState() color: Theme.textFaint font.pixelSize: Theme.smallSize wrapMode: Text.Wrap } /* --- advanced ------------------------------------------- */ ToolButton { id: advToggle objectName: "settingsAdvanced" Layout.fillWidth: true implicitHeight: Theme.segmentHeight onClicked: dlg.advanced = !dlg.advanced contentItem: RowLayout { spacing: 6 ThemeIcon { name: dlg.advanced ? "arrow-down" : "arrow-right" glyph: dlg.advanced ? "▾" : "▸" size: 14 color: Theme.textDim } Text { text: "Advanced options" color: advToggle.hovered ? Theme.text : Theme.textDim font.pixelSize: Theme.smallSize font.bold: true } Item { Layout.fillWidth: true } Text { text: "custom provider · model parameters" color: Theme.textFaint font.pixelSize: Theme.smallSize } } background: Rectangle { radius: Theme.radiusSm color: advToggle.hovered ? Theme.surfaceAlt : "transparent" } } ColumnLayout { Layout.fillWidth: true visible: dlg.advanced spacing: 6 Text { Layout.fillWidth: true text: "Provider entry — saved in /cst-agent/models.json. " + "A name the presets know is overridden, a new one is added." color: Theme.textFaint font.pixelSize: Theme.smallSize wrapMode: Text.Wrap } GridLayout { Layout.fillWidth: true columns: 2 columnSpacing: Theme.gap rowSpacing: 4 AdvField { id: advNameField objectName: "advName" label: "Name" placeholder: "myllm" Layout.preferredWidth: 180 } AdvField { id: advBaseField objectName: "advBase" label: "Base URL" placeholder: "https://api.example.com/v1" } AdvField { id: advKeyEnvField objectName: "advKeyEnv" label: "Key variable" placeholder: "MYLLM_API_KEY" } AdvField { id: advModelField objectName: "advModel" label: "Default model" placeholder: "the model to start with" } AdvField { id: advModelsField objectName: "advModels" label: "Models it offers (comma separated, optional)" placeholder: "m1, m2" Layout.columnSpan: 2 } } RowLayout { Layout.fillWidth: true spacing: Theme.gap Button { id: saveProviderBtn objectName: "settingsSaveProvider" text: "Save provider" implicitHeight: Theme.touchTarget enabled: advNameField.text.trim().length > 0 && advBaseField.text.trim().length > 0 onClicked: dlg.saveProvider() background: Rectangle { radius: Theme.radiusSm color: saveProviderBtn.hovered ? Theme.surfaceHi : Theme.surfaceAlt border.width: 1 border.color: Theme.border } contentItem: Text { text: saveProviderBtn.text color: saveProviderBtn.enabled ? Theme.textDim : Theme.textFaint font.pixelSize: Theme.bodySize horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } Button { id: removeProviderBtn objectName: "settingsRemoveProvider" text: "Remove from models.json" implicitHeight: Theme.touchTarget readonly property var entry: dlg.pickedEntry() enabled: entry !== null && entry.custom === true ToolTip.visible: hovered ToolTip.text: "Only the user's own entry goes: a built-in preset of the same name stays" onClicked: dlg.removeProvider() background: Rectangle { radius: Theme.radiusSm color: removeProviderBtn.hovered && removeProviderBtn.enabled ? Theme.surfaceHi : Theme.surfaceAlt border.width: 1 border.color: Theme.border } contentItem: Text { text: removeProviderBtn.text color: removeProviderBtn.enabled ? Theme.textDim : Theme.textFaint font.pixelSize: Theme.bodySize horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } Item { Layout.fillWidth: true } } Text { Layout.fillWidth: true Layout.topMargin: 4 text: "Model parameters — remembered for " + (dlg.picked.length ? dlg.picked : "this provider") + ", sent with every request. Empty = the provider's own default." color: Theme.textFaint font.pixelSize: Theme.smallSize wrapMode: Text.Wrap } RowLayout { Layout.fillWidth: true spacing: Theme.gap AdvField { id: tempField objectName: "advTemperature" label: "Temperature" placeholder: "not sent" Layout.preferredWidth: 130 } AdvField { id: topPField objectName: "advTopP" label: "Top-p" placeholder: "not sent" Layout.preferredWidth: 130 } AdvField { id: maxTokField objectName: "advMaxTokens" label: "Max tokens" placeholder: "not sent" Layout.preferredWidth: 150 } Item { Layout.fillWidth: true } } } } } RowLayout { Layout.fillWidth: true spacing: Theme.gap Text { objectName: "settingsNote" Layout.fillWidth: true text: dlg.note.length ? dlg.note : dlg.usageLine() color: dlg.note.length ? Theme.warn : Theme.textFaint font.pixelSize: Theme.smallSize wrapMode: Text.Wrap maximumLineCount: 2 elide: Text.ElideRight } Button { id: closeBtn objectName: "settingsClose" text: "Close" implicitHeight: Theme.touchTarget onClicked: dlg.close() background: Rectangle { radius: Theme.radiusSm color: closeBtn.hovered ? Theme.surfaceHi : Theme.surfaceAlt border.width: 1 border.color: Theme.border } contentItem: Text { text: closeBtn.text color: Theme.textDim font.pixelSize: Theme.bodySize horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } Button { id: applyBtn objectName: "settingsApply" text: "Use this" implicitHeight: Theme.touchTarget onClicked: dlg.apply() background: Rectangle { radius: Theme.radiusSm color: applyBtn.hovered ? "#5cc0f0" : Theme.accent } contentItem: Text { text: applyBtn.text color: Theme.accentInk font.pixelSize: Theme.bodySize font.bold: true horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } } } /* The model parameters belong to the provider that is picked, so they are * applied first: useProvider() then takes over what was remembered for * whichever provider it ends on. */ function apply() { dlg.applyRequested(dlg.picked, modelField.text, keyField.text, dlg.num(tempField.text), dlg.num(topPField.text), Math.round(dlg.num(maxTokField.text))) } onOpened: { keyField.text = "" modelField.forceActiveFocus() } }