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::useProvider(), 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. 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 "Use this" answered property string keyEnv: "" property string keyFrom: "" property bool hasKey: false signal applyRequested(string provider, string model, string key) width: Math.min(580, parent ? parent.width - 32 : 580) height: Math.min(560, parent ? parent.height - 60 : 560) /* 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 } /* 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) : [] syncPick() } /* Where the picked provider's key would come from. From the entries -- * a key itself never reaches the QML side. */ function syncPick() { hasKey = false keyFrom = "" keyEnv = "" for (let i = 0; i < providers.length; i++) { const p = providers[i] if (p.name !== picked) continue hasKey = p.hasKey === true keyFrom = p.keyFrom keyEnv = p.keyEnv return } } 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 } 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 } Flickable { Layout.fillWidth: true Layout.fillHeight: true Layout.minimumHeight: 96 clip: true contentWidth: width contentHeight: plist.implicitHeight boundsBehavior: Flickable.StopAtBounds ScrollBar.vertical: ScrollBar {} ColumnLayout { id: plist width: parent.width 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.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.text : Theme.textDim font.pixelSize: Theme.smallSize font.bold: prow.selected } Text { visible: prow.modelData.current === true text: "in use" color: Theme.ok font.pixelSize: Theme.smallSize } Item { Layout.fillWidth: true } Text { text: prow.modelData.source color: 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: 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 placeholderText: "model name" inputMethodHints: Qt.ImhNoPredictiveText } Text { Layout.preferredWidth: 90 elide: Text.ElideRight text: modelField.text.length ? agent.modelInfo(modelField.text) : "" color: Theme.textFaint font.pixelSize: Theme.smallSize } } Flow { Layout.fillWidth: true spacing: 6 visible: dlg.choices.length > 0 Repeater { model: dlg.choices 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 placeholderText: "API key (leave empty to keep what is there)" inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhSensitiveData onAccepted: dlg.applyRequested(dlg.picked, modelField.text, keyField.text) } Text { Layout.fillWidth: true text: dlg.keyState() color: Theme.textFaint font.pixelSize: Theme.smallSize wrapMode: Text.Wrap } 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 { objectName: "settingsClose" text: "Close" implicitHeight: Theme.touchTarget onClicked: dlg.close() } Button { objectName: "settingsApply" text: "Use this" highlighted: true implicitHeight: Theme.touchTarget onClicked: dlg.applyRequested(dlg.picked, modelField.text, keyField.text) } } } }