Edit a preset's tiers from the bar panel
The gear on every preset row now opens an editor: the four tiers with the model each maps to, and for each one field that filters the provider's catalogue as you type (every word must match; arrows and Enter work), listed inline with context length and price, and that also takes any id typed by hand. LM Studio's server form moves one click inside the editor. The panel card moves from PopupCard to KeyboardPanel. PopupCard is an xdg-popup, which only receives keys after focus is routed through its parent surface, so no text field in it could ever be typed into - the existing server URL form included. KeyboardPanel primes layer-shell keyboard focus on open, which is why every shell panel with a text field uses it. Esc now closes the panel. The picker reads ~/.claude-mode/models-cache.json, which or_catalogue and lms_catalogue now write as a side effect, so models, doctor, setup and the menu's picker all keep it fresh and the panel never hits the network itself. One node per provider with its own fetchedAt/ok; a failed fetch keeps the old list, an LM Studio list is tied to its server, and no key or key name is ever stored. `models` gains --preset, --refresh and --json, and the Z.AI list now lives in one place. A tier edit to the active preset re-applies without the running-sessions prompt: that prompt guards against the endpoint or key moving, and a tier edit moves neither (preset url/auth still ask). A failed re-apply now says the edit was saved. `preset set` on an unknown name no longer creates it. Panel edits run through one chain that stops on the first failure and refreshes health.json at the end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -111,6 +111,21 @@ BarWidget {
|
||||
onLoadFailed: root.fallbackState = null
|
||||
}
|
||||
|
||||
// The model catalogue the panel's picker offers. claude-mode leaves it
|
||||
// behind whenever it fetches a provider's list anyway, so reading it here
|
||||
// costs no network - and nothing here polls a provider on a timer.
|
||||
property var modelsCache: null
|
||||
|
||||
FileView {
|
||||
id: modelsFile
|
||||
path: root.cmRoot + "/models-cache.json"
|
||||
watchChanges: true
|
||||
printErrors: false
|
||||
onFileChanged: reload()
|
||||
onLoaded: root.parseInto("modelsCache", text())
|
||||
onLoadFailed: root.modelsCache = null
|
||||
}
|
||||
|
||||
// claude-mode writes health.json itself, so a switch launched from the panel
|
||||
// lands back here through the FileView above. This only covers the case
|
||||
// where the file was never written at all.
|
||||
@@ -179,6 +194,7 @@ BarWidget {
|
||||
function refresh() {
|
||||
healthFile.reload()
|
||||
stateFile.reload()
|
||||
modelsFile.reload()
|
||||
if (!root.health) seedProc.running = true
|
||||
root.scanSessions()
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ Panel {
|
||||
// Both questions are answered by claude-mode rather than re-derived here, so
|
||||
// the widget and the CLI can never disagree about whether a switch is allowed
|
||||
// or about what is running.
|
||||
property string stage: "list" // list | blocked | confirm
|
||||
property string stage: "list" // list | blocked | confirm | server | repair | preset | presetTier
|
||||
property var blocker: null // the preflight verdict, when it refused
|
||||
property var sessionInfo: null // sessions --json, when any were found
|
||||
property string pendingMode: ""
|
||||
@@ -90,8 +90,13 @@ Panel {
|
||||
return null
|
||||
}
|
||||
|
||||
function openServerSettings(presetName) {
|
||||
// `from` is the stage to come back to - the preset editor, when opened from
|
||||
// there; otherwise the list, or the switch that was blocked.
|
||||
property string serverReturn: ""
|
||||
|
||||
function openServerSettings(presetName, from) {
|
||||
var e = root.presetEntry(presetName)
|
||||
root.serverReturn = from || ""
|
||||
root.serverPreset = presetName
|
||||
root.serverUrl = e && e.baseUrl ? String(e.baseUrl) : root.serverLocalDefault
|
||||
root.serverNeedsKey = !!(e && String(e.authMode) === "vault")
|
||||
@@ -139,7 +144,7 @@ Panel {
|
||||
// Straight back into the switch that was blocked, if there was one -
|
||||
// the whole point of coming here was to unblock it.
|
||||
if (root.pendingMode !== "") root.retryPending()
|
||||
else root.stage = "list"
|
||||
else root.stage = root.serverReturn !== "" ? root.serverReturn : "list"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,6 +230,190 @@ Panel {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Edits
|
||||
//
|
||||
// One chain for every edit the panel makes: each step runs only if the one
|
||||
// before it succeeded, the first failure stops it and its last stderr line
|
||||
// becomes the error, and health.json is rewritten at the end either way, so
|
||||
// the panel shows what the files now say rather than what was intended.
|
||||
property var opQueue: []
|
||||
property string opLabel: ""
|
||||
property string opLanding: ""
|
||||
|
||||
function runOps(label, ops, landing) {
|
||||
if (root.busy) return
|
||||
root.busy = true
|
||||
root.lastError = ""
|
||||
root.opLabel = label
|
||||
root.opLanding = landing
|
||||
root.opQueue = ops.slice()
|
||||
root.nextOp()
|
||||
}
|
||||
|
||||
function nextOp() {
|
||||
var q = root.opQueue.slice()
|
||||
var argv = q.length > 0 ? q.shift() : null
|
||||
root.opQueue = q
|
||||
opProc.failed = false
|
||||
opProc.lastLine = ""
|
||||
opProc.finalHealth = argv === null
|
||||
opProc.command = root.cli(argv === null ? ["health"] : argv)
|
||||
opProc.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: opProc
|
||||
running: false
|
||||
property bool failed: false
|
||||
property bool finalHealth: false
|
||||
property string lastLine: ""
|
||||
stderr: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var msg = String(text || "").replace(/\x1b\[[0-9;]*m/g, "").trim()
|
||||
var lines = msg.split("\n").filter(function (l) { return l.trim() !== "" })
|
||||
opProc.lastLine = lines.length > 0
|
||||
? lines[lines.length - 1].replace(/^\s*(FAIL|warn)\s*/, "").trim() : ""
|
||||
// The stream can finish after the exit it belongs to.
|
||||
if (opProc.failed && opProc.lastLine !== "") root.lastError = opProc.lastLine
|
||||
}
|
||||
}
|
||||
onExited: function (code) {
|
||||
if (opProc.finalHealth) {
|
||||
root.busy = false
|
||||
if (widget) widget.refresh()
|
||||
if (root.lastError === "") root.stage = root.opLanding
|
||||
return
|
||||
}
|
||||
if (code !== 0) {
|
||||
opProc.failed = true
|
||||
root.lastError = opProc.lastLine !== "" ? opProc.lastLine : root.opLabel + " failed"
|
||||
root.opQueue = []
|
||||
}
|
||||
Qt.callLater(root.nextOp)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Preset editor
|
||||
//
|
||||
// Stages rather than one form, because the card clamps to its content instead
|
||||
// of scrolling and a long form would simply be cut off:
|
||||
//
|
||||
// list -> preset -> presetTier -> preset
|
||||
//
|
||||
// The tier names live here rather than in Modes.js on purpose: that file is a
|
||||
// .pragma library, cached until the shell restarts, and a stale copy without
|
||||
// them would render an empty editor.
|
||||
readonly property var tierNames: ["opus", "sonnet", "haiku", "fable"]
|
||||
property string editPreset: ""
|
||||
property string editTier: ""
|
||||
property string editModel: ""
|
||||
|
||||
readonly property var editEntry: root.editPreset !== "" ? root.presetEntry(root.editPreset) : null
|
||||
readonly property string editProvider: editEntry ? String(editEntry.provider) : ""
|
||||
readonly property bool editIsActive: root.mode !== "anthropic" && root.editPreset !== "" && root.editPreset === root.preset
|
||||
|
||||
function editCurrent(tier) {
|
||||
var e = root.editEntry
|
||||
return e && e.models && e.models[tier] ? String(e.models[tier]) : ""
|
||||
}
|
||||
|
||||
function openPresetEditor(name) {
|
||||
root.lastError = ""
|
||||
root.editPreset = name
|
||||
root.editTier = ""
|
||||
root.stage = "preset"
|
||||
}
|
||||
|
||||
// The field starts empty rather than holding the current id: it doubles as
|
||||
// the list's filter, and a pre-filled id would narrow the list to itself.
|
||||
// The current mapping is shown on the line above instead.
|
||||
function openTierEditor(tier) {
|
||||
root.lastError = ""
|
||||
root.editTier = tier
|
||||
root.editModel = ""
|
||||
// Typing breaks the field's binding, so a value left from another tier
|
||||
// would otherwise survive into this one.
|
||||
modelField.text = ""
|
||||
modelList.currentIndex = -1
|
||||
root.stage = "presetTier"
|
||||
Qt.callLater(function () { modelField.forceActiveFocus() })
|
||||
}
|
||||
|
||||
function pickModel(id) {
|
||||
root.editModel = id
|
||||
modelField.text = id
|
||||
modelField.forceActiveFocus()
|
||||
}
|
||||
|
||||
function saveTier() {
|
||||
var m = root.editModel.trim()
|
||||
if (root.editPreset === "" || root.editTier === "") return
|
||||
if (m === "") { root.lastError = "Pick a model from the list, or type an id."; return }
|
||||
root.runOps("saving", [["preset", "set", root.editPreset, root.editTier, m]], "preset")
|
||||
}
|
||||
|
||||
// Every word has to appear somewhere in the id or its description, so
|
||||
// "deepseek flash" finds the flash variants without an exact substring.
|
||||
readonly property var modelMatches: {
|
||||
var words = root.editModel.trim().toLowerCase().split(/\s+/).filter(function (w) { return w !== "" })
|
||||
var all = root.modelOptions
|
||||
if (words.length === 0) return all
|
||||
var out = []
|
||||
for (var i = 0; i < all.length; i++) {
|
||||
var hay = (all[i].label + " " + all[i].description).toLowerCase()
|
||||
var hit = true
|
||||
for (var j = 0; j < words.length && hit; j++) hit = hay.indexOf(words[j]) !== -1
|
||||
if (hit) out.push(all[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function fetchModels() {
|
||||
root.runOps("fetching models", [["models", "--preset", root.editPreset, "--refresh"]], "presetTier")
|
||||
}
|
||||
|
||||
// The cached catalogue for the preset being edited. An LM Studio list is
|
||||
// only used when it came from the same server: two presets can point at two
|
||||
// machines with different models loaded.
|
||||
readonly property var catalogueNode: {
|
||||
var all = widget && widget.modelsCache && widget.modelsCache.providers ? widget.modelsCache.providers : null
|
||||
var n = all && root.editProvider !== "" ? all[root.editProvider] : null
|
||||
if (!n) return null
|
||||
if (root.editProvider === "lmstudio") {
|
||||
var want = root.editEntry && root.editEntry.baseUrl ? String(root.editEntry.baseUrl).replace(/\/+$/, "") : ""
|
||||
if (String(n.baseUrl || "") !== want) return null
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
readonly property var modelOptions: {
|
||||
var n = root.catalogueNode
|
||||
var ms = n && n.models ? n.models : []
|
||||
var out = []
|
||||
for (var i = 0; i < ms.length; i++) {
|
||||
var m = ms[i]
|
||||
var bits = []
|
||||
if (m.contextTokens) bits.push(Modes.contextLabel(m.contextTokens))
|
||||
if (m.priceIn !== undefined && m.priceIn !== null)
|
||||
bits.push("$" + m.priceIn + " in / $" + m.priceOut + " out per 1M")
|
||||
if (m.state) bits.push(String(m.state))
|
||||
if (m.note) bits.push(String(m.note))
|
||||
out.push({ value: String(m.id), label: String(m.id), description: bits.join(" · ") })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function ageLabel(iso) {
|
||||
var t = Date.parse(String(iso || ""))
|
||||
if (isNaN(t)) return ""
|
||||
var s = Math.max(0, (Date.now() - t) / 1000)
|
||||
if (s < 90) return "just now"
|
||||
if (s < 5400) return Math.round(s / 60) + " min ago"
|
||||
if (s < 129600) return Math.round(s / 3600) + " h ago"
|
||||
return Math.round(s / 86400) + " days ago"
|
||||
}
|
||||
|
||||
function storeServerKey() {
|
||||
remedyProc.command = root.cli(["set-key", root.serverKeyRef, "--terminal"])
|
||||
remedyProc.running = true
|
||||
@@ -237,7 +426,10 @@ Panel {
|
||||
root.pendingMode = ""
|
||||
root.pendingPreset = ""
|
||||
root.serverPreset = ""
|
||||
root.serverReturn = ""
|
||||
root.repairTarget = null
|
||||
root.editPreset = ""
|
||||
root.editTier = ""
|
||||
}
|
||||
|
||||
function switchTo(mode, presetName) {
|
||||
@@ -457,12 +649,22 @@ Panel {
|
||||
}
|
||||
}
|
||||
|
||||
PopupCard {
|
||||
// KeyboardPanel, not PopupCard. PopupCard is an xdg-popup, which only gets
|
||||
// keys after focus is routed through its parent surface - so no text field
|
||||
// in it could ever be typed into (the server URL form and the model search
|
||||
// both looked clickable and took nothing). KeyboardPanel is a layer-shell
|
||||
// surface that primes keyboard focus on open, which is why every shell panel
|
||||
// with a text field is built on it.
|
||||
KeyboardPanel {
|
||||
id: card
|
||||
anchorItem: root.anchorItem
|
||||
owner: root.barIdentity
|
||||
bar: root.bar
|
||||
open: root.opened
|
||||
// The panel now holds the keyboard while open, so keys need somewhere to
|
||||
// land when no field has focus - and Esc should close it, as it does
|
||||
// every other shell panel. Esc inside a text field bubbles up here too.
|
||||
focusTarget: column
|
||||
contentWidth: card.fittedContentWidth(Style.space(360))
|
||||
contentHeight: card.fittedContentHeight(column.implicitHeight)
|
||||
|
||||
@@ -470,6 +672,8 @@ Panel {
|
||||
id: column
|
||||
width: card.contentWidth - card.padding * 2
|
||||
spacing: Style.space(10)
|
||||
focus: true
|
||||
Keys.onEscapePressed: root.close()
|
||||
|
||||
// ---- Hero: what the next `claude` will actually use.
|
||||
Row {
|
||||
@@ -696,8 +900,9 @@ Panel {
|
||||
font.pixelSize: Style.space(10)
|
||||
}
|
||||
|
||||
// Clicking the row switches; the gear edits where that preset
|
||||
// points instead. Per row rather than per provider, because
|
||||
// Clicking the row switches; the gear opens that preset in the
|
||||
// editor instead. Per row rather than per provider, because
|
||||
// two presets of one provider can map tiers differently - and
|
||||
// two LM Studio presets can sit on two different machines.
|
||||
MouseArea {
|
||||
id: presetHover
|
||||
@@ -711,7 +916,7 @@ Panel {
|
||||
|
||||
Item {
|
||||
id: gearButton
|
||||
visible: modeEntry.thisMode === "lmstudio"
|
||||
visible: true
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Style.space(22)
|
||||
@@ -731,7 +936,7 @@ Panel {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: !root.busy
|
||||
onClicked: root.openServerSettings(String(presetRow.modelData.name))
|
||||
onClicked: root.openPresetEditor(String(presetRow.modelData.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -928,7 +1133,331 @@ Panel {
|
||||
spacing: Style.space(7)
|
||||
|
||||
PillButton { label: "Save"; primary: true; onTriggered: root.saveServerSettings() }
|
||||
PillButton { label: "Cancel"; onTriggered: root.resetFlow() }
|
||||
PillButton {
|
||||
label: root.serverReturn !== "" ? "Back" : "Cancel"
|
||||
onTriggered: {
|
||||
if (root.serverReturn !== "") root.stage = root.serverReturn
|
||||
else root.resetFlow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- One preset: its tier map, each row opening a picker.
|
||||
Column {
|
||||
width: parent.width
|
||||
visible: root.stage === "preset" && root.editPreset !== ""
|
||||
spacing: Style.space(6)
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: "'" + root.editPreset + "' · " + Modes.title(root.editProvider)
|
||||
color: Color.popups.text
|
||||
elide: Text.ElideRight
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(13)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: text !== ""
|
||||
text: root.editEntry && root.editEntry.description ? String(root.editEntry.description) : ""
|
||||
color: Color.muted
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(10)
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: root.editIsActive
|
||||
text: "In use now. A change is applied straight away and reaches sessions started after it."
|
||||
color: Color.accent
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(10)
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Style.space(1)
|
||||
|
||||
Repeater {
|
||||
model: root.stage === "preset" ? root.tierNames : []
|
||||
|
||||
Item {
|
||||
id: tierRow
|
||||
required property var modelData
|
||||
readonly property string tier: String(modelData)
|
||||
readonly property string current: root.editCurrent(tier)
|
||||
width: column.width
|
||||
height: Style.space(26)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: -Style.space(6)
|
||||
anchors.rightMargin: -Style.space(6)
|
||||
radius: Style.space(4)
|
||||
color: tierArea.containsMouse
|
||||
? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.14)
|
||||
: "transparent"
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: tierRow.tier
|
||||
color: Color.muted
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(11)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.space(54)
|
||||
anchors.right: tierChevron.left
|
||||
anchors.rightMargin: Style.space(6)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: tierRow.current !== "" ? tierRow.current : "not set"
|
||||
color: tierRow.current !== "" ? Color.popups.text : Color.muted
|
||||
elide: Text.ElideLeft
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(11)
|
||||
}
|
||||
|
||||
Text {
|
||||
id: tierChevron
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: String.fromCodePoint(0xF0142)
|
||||
color: tierArea.containsMouse ? Color.accent : Color.muted
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.font.body
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tierArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: !root.busy
|
||||
onClicked: root.openTierEditor(tierRow.tier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flow, not Row: these can total more than the card is wide, and a
|
||||
// Row lays them straight past its right edge instead of wrapping.
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Style.space(7)
|
||||
|
||||
PillButton {
|
||||
label: "Server settings…"
|
||||
visible: root.editProvider === "lmstudio"
|
||||
onTriggered: root.openServerSettings(root.editPreset, "preset")
|
||||
}
|
||||
PillButton { label: "Done"; primary: true; onTriggered: root.resetFlow() }
|
||||
}
|
||||
}
|
||||
|
||||
// ---- One tier: pick from the cached catalogue, or type any id.
|
||||
Column {
|
||||
width: parent.width
|
||||
visible: root.stage === "presetTier"
|
||||
spacing: Style.space(7)
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.editTier + " · '" + root.editPreset + "'"
|
||||
color: Color.popups.text
|
||||
elide: Text.ElideRight
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(13)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: "now: " + (root.editCurrent(root.editTier) || "not set")
|
||||
color: Color.muted
|
||||
elide: Text.ElideLeft
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
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: root.editModel
|
||||
placeholderText: root.modelOptions.length > 0
|
||||
? "Search " + root.modelOptions.length + " models, or type any id"
|
||||
: "Model id"
|
||||
foreground: Color.popups.text
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(11)
|
||||
onTextChanged: {
|
||||
root.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 < root.modelMatches.length)
|
||||
root.pickModel(String(root.modelMatches[modelList.currentIndex].value))
|
||||
else
|
||||
root.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: root.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: root.stage === "presetTier" ? root.modelMatches : []
|
||||
|
||||
delegate: Item {
|
||||
id: matchRow
|
||||
required property var modelData
|
||||
required property int index
|
||||
readonly property bool chosen: String(modelData.value) === root.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: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
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: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(9)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: matchArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.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: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(10)
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: root.modelOptions.length === 0
|
||||
text: root.catalogueNode && root.catalogueNode.ok === false
|
||||
? "The last fetch failed and no list is cached. Type an id, or try fetching again."
|
||||
: "No model list cached for " + Modes.title(root.editProvider) + " yet. Type an id, or fetch the list."
|
||||
color: Color.muted
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(10)
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: root.catalogueNode !== null && root.modelOptions.length > 0
|
||||
text: root.catalogueNode
|
||||
? root.modelOptions.length + " models cached, fetched " + root.ageLabel(root.catalogueNode.fetchedAt)
|
||||
+ (root.catalogueNode.ok === false ? " · the last refresh failed" : "")
|
||||
: ""
|
||||
color: Color.muted
|
||||
opacity: 0.85
|
||||
elide: Text.ElideRight
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(9)
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Style.space(7)
|
||||
|
||||
PillButton { label: "Save"; primary: true; onTriggered: root.saveTier() }
|
||||
PillButton {
|
||||
label: root.modelOptions.length > 0 ? "Refresh list" : "Fetch models"
|
||||
onTriggered: root.fetchModels()
|
||||
}
|
||||
PillButton {
|
||||
label: "Back"
|
||||
onTriggered: { root.lastError = ""; root.stage = "preset" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1378,7 +1907,8 @@ Panel {
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.busy
|
||||
? (ignoreProc.running ? "updating…" : (repairProc.running ? "repairing…" : "switching…"))
|
||||
? (opProc.running ? root.opLabel + "…"
|
||||
: (ignoreProc.running ? "updating…" : (repairProc.running ? "repairing…" : "switching…")))
|
||||
: (root.known && root.stage === "list" ? "Restart claude to pick this up." : "")
|
||||
visible: text !== ""
|
||||
color: Color.muted
|
||||
|
||||
Reference in New Issue
Block a user