import QtQuick import qs.Commons import qs.Ui // TierStage.qml - One tier: pick from the cached catalogue, or type any id. // // One stage of the claude-mode panel. Panel.qml hands itself in as `panel` // and owns all the state and actions; this file only draws them. Column { id: stage required property var panel width: parent.width visible: stage.panel.stage === "presetTier" spacing: Style.space(7) Text { width: parent.width text: stage.panel.editTier + " · '" + stage.panel.editPreset + "'" color: Color.popups.text elide: Text.ElideRight font.family: stage.panel.uiFont font.pixelSize: Style.space(13) font.bold: true } Text { width: parent.width text: "now: " + (stage.panel.editCurrent(stage.panel.editTier) || "not set") color: Color.muted elide: Text.ElideLeft font.family: stage.panel.uiFont font.pixelSize: Style.space(10) } // One field that both filters the catalogue and takes any id typed by // hand, with the matches listed inline beneath it - rather than the // shell's SearchableDropdown, whose list is a second popup layered on // this card. Inline, the card's height is the only thing to manage, // and the list can never be clipped by the card's edge. TextField { id: modelField width: parent.width text: stage.panel.editModel placeholderText: stage.panel.modelOptions.length > 0 ? "Search " + stage.panel.modelOptions.length + " models, or type any id" : "Model id" foreground: Color.popups.text font.family: stage.panel.uiFont font.pixelSize: Style.space(11) onTextChanged: { stage.panel.editModel = text modelList.currentIndex = -1 } // Enter takes the highlighted match if the arrows chose one, and // saves what is in the field otherwise. onAccepted: { if (modelList.currentIndex >= 0 && modelList.currentIndex < stage.panel.modelMatches.length) stage.panel.pickModel(String(stage.panel.modelMatches[modelList.currentIndex].value)) else stage.panel.saveTier() } Keys.onDownPressed: function (event) { if (modelList.count > 0) { modelList.currentIndex = Math.min(modelList.currentIndex + 1, modelList.count - 1) modelList.positionViewAtIndex(modelList.currentIndex, ListView.Contain) } event.accepted = true } Keys.onUpPressed: function (event) { if (modelList.currentIndex >= 0) { modelList.currentIndex = modelList.currentIndex - 1 if (modelList.currentIndex >= 0) modelList.positionViewAtIndex(modelList.currentIndex, ListView.Contain) } event.accepted = true } } // Scrolls inside a box of at most six rows: the card clamps to its // content rather than scrolling, so an unbounded list would push the // buttons off the bottom edge. Rectangle { id: modelListFrame readonly property int rowHeight: Style.space(30) width: parent.width visible: stage.panel.modelOptions.length > 0 height: rowHeight * Math.max(1, Math.min(modelList.count, 6)) + 2 radius: Style.space(4) color: "transparent" border.width: 1 border.color: Qt.rgba(Color.popups.text.r, Color.popups.text.g, Color.popups.text.b, 0.14) ListView { id: modelList anchors.fill: parent anchors.margins: 1 clip: true boundsBehavior: Flickable.StopAtBounds currentIndex: -1 model: stage.panel.stage === "presetTier" ? stage.panel.modelMatches : [] delegate: Item { id: matchRow required property var modelData required property int index readonly property bool chosen: String(modelData.value) === stage.panel.editModel.trim() width: modelList.width height: modelListFrame.rowHeight Rectangle { anchors.fill: parent color: matchRow.index === modelList.currentIndex || matchArea.containsMouse ? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.16) : (matchRow.chosen ? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.10) : "transparent") } Column { anchors.left: parent.left anchors.right: parent.right anchors.leftMargin: Style.space(8) anchors.rightMargin: Style.space(8) anchors.verticalCenter: parent.verticalCenter spacing: 0 Text { width: parent.width text: String(matchRow.modelData.label) color: matchRow.chosen ? Color.accent : Color.popups.text elide: Text.ElideMiddle font.family: stage.panel.uiFont font.pixelSize: Style.space(11) font.bold: matchRow.chosen } Text { width: parent.width visible: text !== "" text: String(matchRow.modelData.description || "") color: Color.muted elide: Text.ElideRight font.family: stage.panel.uiFont font.pixelSize: Style.space(9) } } MouseArea { id: matchArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: stage.panel.pickModel(String(matchRow.modelData.value)) } } } Text { anchors.centerIn: parent visible: modelList.count === 0 text: "No match. Save uses the id as typed." color: Color.muted font.family: stage.panel.uiFont font.pixelSize: Style.space(10) } } Text { width: parent.width visible: stage.panel.modelOptions.length === 0 text: stage.panel.catalogueNode && stage.panel.catalogueNode.ok === false ? "The last fetch failed and no list is cached. Type an id, or try fetching again." : "No model list cached for " + stage.panel.pTitle(stage.panel.editProvider) + " yet. Type an id, or fetch the list." color: Color.muted wrapMode: Text.WordWrap font.family: stage.panel.uiFont font.pixelSize: Style.space(10) } Text { width: parent.width visible: stage.panel.catalogueNode !== null && stage.panel.modelOptions.length > 0 text: stage.panel.catalogueNode ? stage.panel.modelOptions.length + " models cached, fetched " + stage.panel.ageLabel(stage.panel.catalogueNode.fetchedAt) + (stage.panel.catalogueNode.ok === false ? " · the last refresh failed" : "") : "" color: Color.muted opacity: 0.85 elide: Text.ElideRight font.family: stage.panel.uiFont font.pixelSize: Style.space(9) } Flow { width: parent.width spacing: Style.space(7) PillButton { fontFamily: stage.panel.uiFont; label: "Save"; primary: true; onTriggered: stage.panel.saveTier() } PillButton { fontFamily: stage.panel.uiFont; label: stage.panel.modelOptions.length > 0 ? "Refresh list" : "Fetch models" onTriggered: stage.panel.fetchModels() } PillButton { fontFamily: stage.panel.uiFont; label: "Back" onTriggered: { stage.panel.lastError = ""; stage.panel.stage = "preset" } } } // The panel cannot reach this field by id, so when a tier is opened or a // model picked it says so, and the field is reset or filled here. Typing // breaks the field's binding, so without the reset a value left from another // tier would survive into this one. Connections { target: stage.panel function onTierEditorOpened() { modelField.text = "" modelList.currentIndex = -1 Qt.callLater(function () { modelField.forceActiveFocus() }) } function onModelPicked(modelId) { modelField.text = modelId modelField.forceActiveFocus() } } }