import QtQuick import Quickshell.Io import qs.Commons import qs.Ui import "Modes.js" as Modes Panel { id: root moduleName: "smoido.claude-mode" ipcTarget: "smoido.claude-mode" property var anchorItem: null property var hostWidget: null property var widget: null // The bar identifies a panel by the widget mounted in its slot, not by this // nested panel, so the popout coordinator has to be handed that widget. readonly property var barIdentity: hostWidget || root readonly property var health: widget ? widget.health : null readonly property string mode: widget ? widget.mode : "" readonly property string preset: widget ? widget.preset : "" readonly property bool known: widget ? widget.known : false readonly property string cmRoot: widget ? widget.cmRoot : Modes.defaultRoot() // Which gateway row has its preset list unfolded. Only one at a time, and // never anthropic, which takes no preset. property string expandedMode: "" property bool busy: false property string lastError: "" function open() { root.expandedMode = "" root.lastError = "" root.resetFlow() if (widget) widget.refresh() root.controller.show() } function toggle() { root.opened ? root.close() : root.open() } // A gateway mode needs a preset chosen before anything can be applied, so // clicking one unfolds its list instead of switching blind. anthropic has // nothing to choose and applies on the spot. function activate(mode) { if (!Modes.needsPreset(mode)) { switchTo(mode, ""); return } root.expandedMode = (root.expandedMode === mode) ? "" : mode } // ---- Switch flow // // A switch is not one action. It rewrites settings.json, which every future // session reads and no running session re-reads, so two things have to be // settled before the write: whether the target can actually serve requests, // and what should happen to the sessions still talking to the old one. // // list -> preflight -> blocked // -> sessions -> confirm -> switch -> list // // 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 var blocker: null // the preflight verdict, when it refused property var sessionInfo: null // sessions --json, when any were found property string pendingMode: "" property string pendingPreset: "" function cli(args) { return [root.cmRoot + "/bin/claude-mode"].concat(args) } function parseJson(t) { try { return JSON.parse(String(t || "")) } catch (e) { return null } } // ---- Server settings (LM Studio) // // LM Studio ships listening on loopback, but it does not have to be there: it // can be another machine on the LAN, or something reached through a tunnel or // a reverse proxy, and it can have authentication switched on. All three are // just a baseUrl and an auth block in the preset, so this edits those two // rather than pretending the local default is the only shape. property string serverPreset: "" property string serverUrl: "" property bool serverNeedsKey: false property string serverKeyRef: "lmstudio" readonly property string serverLocalDefault: "http://127.0.0.1:1234" function presetEntry(name) { var all = (health && health.presets) ? health.presets : [] for (var i = 0; i < all.length; i++) if (String(all[i].name) === name) return all[i] return null } function openServerSettings(presetName) { var e = root.presetEntry(presetName) root.serverPreset = presetName root.serverUrl = e && e.baseUrl ? String(e.baseUrl) : root.serverLocalDefault root.serverNeedsKey = !!(e && String(e.authMode) === "vault") root.serverKeyRef = e && e.keyRef ? String(e.keyRef) : "lmstudio" root.blocker = null root.stage = "server" } // Two writes, sequenced: the URL first, then the auth block, then health.json // is refreshed so the form and the rest of the panel agree about what the // preset now says. function saveServerSettings() { if (root.busy || root.serverPreset === "") return root.busy = true urlProc.command = root.cli(["preset", "url", root.serverPreset, root.serverUrl.trim()]) urlProc.running = true } Process { id: urlProc running: false onExited: function (code) { authProc.command = root.cli(root.serverNeedsKey ? ["preset", "auth", root.serverPreset, "key", root.serverKeyRef] : ["preset", "auth", root.serverPreset, "none"]) authProc.running = true } } Process { id: authProc running: false onExited: function (code) { healthProc.command = root.cli(["health"]) healthProc.running = true } } Process { id: healthProc running: false onExited: function (code) { root.busy = false if (widget) widget.refresh() // 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" } } function storeServerKey() { remedyProc.command = root.cli(["set-key", root.serverKeyRef, "--terminal"]) remedyProc.running = true } function resetFlow() { root.stage = "list" root.blocker = null root.sessionInfo = null root.pendingMode = "" root.pendingPreset = "" root.serverPreset = "" } function switchTo(mode, presetName) { if (root.busy) return root.lastError = "" root.pendingMode = mode root.pendingPreset = presetName root.busy = true preflightProc.command = root.cli(presetName === "" ? ["preflight", mode] : ["preflight", mode, presetName]) preflightProc.running = true } // Nothing is written until this says so. A mode with no key stored, or a // local server that is not running, would otherwise switch cleanly and leave // every session started afterwards broken in a way that looks like Claude // Code's fault rather than a missing key. Process { id: preflightProc running: false property var verdict: null stdout: StdioCollector { waitForEnd: true onStreamFinished: preflightProc.verdict = root.parseJson(text) } onExited: function (code) { var v = preflightProc.verdict preflightProc.verdict = null if (v && v.ok === false) { root.busy = false root.blocker = v root.stage = "blocked" return } sessionsProc.command = root.cli(["sessions", "--json"]) sessionsProc.running = true } } Process { id: sessionsProc running: false property var info: null stdout: StdioCollector { waitForEnd: true onStreamFinished: sessionsProc.info = root.parseJson(text) } onExited: function (code) { var info = sessionsProc.info sessionsProc.info = null root.busy = false if (info && info.count > 0) { root.sessionInfo = info root.stage = "confirm" return } root.applySwitch("none") } } // sessionAction: none | stop | restart, applied to the sessions left on the // old provider once the new one is written. function applySwitch(sessionAction) { root.busy = true root.stage = "list" switchProc.sessionAction = sessionAction switchProc.command = root.cli(root.pendingPreset === "" ? [root.pendingMode] : [root.pendingMode, root.pendingPreset]) switchProc.running = true } // claude-mode rewrites health.json as the last step of a switch, so the // widget's FileView is what actually updates the display. This only has to // report failures and then deal with the sessions. Process { id: switchProc running: false property string sessionAction: "none" 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() !== "" }) if (lines.length > 0) root.lastError = lines[lines.length - 1].replace(/^\s*(FAIL|warn)\s*/, "").trim() } } onExited: function (code) { root.busy = false root.expandedMode = "" root.blocker = null root.sessionInfo = null if (code === 0) { root.lastError = "" // Strictly after the write. A session reopened before it would come // straight back up on the provider we just left. if (switchProc.sessionAction !== "none") { sessionActProc.command = root.cli(["sessions", switchProc.sessionAction === "restart" ? "--restart" : "--stop", "--yes"]) sessionActProc.running = true } } switchProc.sessionAction = "none" root.pendingMode = "" root.pendingPreset = "" if (widget) widget.refresh() } } // --yes because the confirmation already happened, in the card below. Without // it the CLI would refuse rather than act on a decision nobody typed. Process { id: sessionActProc; running: false } // The remedy for a missing key is a hidden password prompt, which a bar popup // has nowhere to host - so the CLI is asked to open a terminal for it. Process { id: remedyProc; running: false } function runRemedy() { var b = root.blocker if (!b) return if (String(b.remedyKind) === "set-key") { remedyProc.command = root.cli(["set-key", String(b.keyRef), "--terminal"]) remedyProc.running = true } } function retryPending() { if (root.pendingMode === "") { root.resetFlow(); return } var m = root.pendingMode, p = root.pendingPreset root.blocker = null root.stage = "list" root.switchTo(m, p) } readonly property var modelRows: { var models = health && health.models ? health.models : null if (!models) return [] var tiers = ["opus", "sonnet", "haiku", "fable"] var out = [] for (var i = 0; i < tiers.length; i++) { if (models[tiers[i]]) out.push({ tier: tiers[i], id: String(models[tiers[i]]) }) } return out } // Color.urgent derives from the theme's `red`, and monochrome themes define // that as a desaturated slate - Solitude's is #565d60, dimmer than the panel // text it is supposed to outrank, so an error would read as the quietest // line on the card. Use the theme's colour only when it carries real hue. readonly property color urgentColor: { var c = Color.urgent var mx = Math.max(c.r, c.g, c.b) var mn = Math.min(c.r, c.g, c.b) var lightness = (mx + mn) / 2 var denom = 1 - Math.abs(2 * lightness - 1) var saturation = denom > 0.0001 ? (mx - mn) / denom : 0 return saturation >= 0.15 ? c : "#d2685f" } readonly property string footerText: { if (!known) return "claude-mode not found in " + cmRoot var bits = [] if (health && health.version) bits.push("claude-mode " + health.version) if (health && health.keyBackend) bits.push("key: " + health.keyBackend) var ctx = health && health.contextTokens ? Modes.contextLabel(health.contextTokens) : "" if (ctx !== "") bits.push(ctx) return bits.join(" · ") } component PillButton: Rectangle { id: pb property string label: "" property bool primary: false signal triggered() implicitWidth: pbText.implicitWidth + Style.space(20) implicitHeight: Style.space(25) radius: Style.space(4) readonly property color tint: primary ? Color.accent : Color.popups.text color: pbArea.containsMouse ? Qt.rgba(tint.r, tint.g, tint.b, primary ? 0.30 : 0.14) : Qt.rgba(tint.r, tint.g, tint.b, primary ? 0.16 : 0.00) border.width: 1 border.color: Qt.rgba(tint.r, tint.g, tint.b, primary ? 0.75 : 0.30) Text { id: pbText anchors.centerIn: parent text: pb.label color: Color.popups.text font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(11) font.bold: pb.primary } MouseArea { id: pbArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: pb.triggered() } } PopupCard { id: card anchorItem: root.anchorItem owner: root.barIdentity bar: root.bar open: root.opened contentWidth: card.fittedContentWidth(Style.space(360)) contentHeight: card.fittedContentHeight(column.implicitHeight) Column { id: column width: card.contentWidth - card.padding * 2 spacing: Style.space(10) // ---- Hero: what the next `claude` will actually use. Row { id: heroRow width: parent.width spacing: Style.space(10) Item { id: heroGlyph anchors.verticalCenter: parent.verticalCenter width: Style.font.display height: Style.font.display readonly property color tint: root.known && root.mode !== "anthropic" ? Color.accent : Color.popups.text BrandIcon { anchors.centerIn: parent visible: Modes.logo(root.mode) !== "" pathData: Modes.logo(root.mode) opticalScale: Modes.logoScale(root.mode) color: heroGlyph.tint iconSize: Style.font.display } Text { anchors.centerIn: parent visible: Modes.logo(root.mode) === "" text: Modes.glyph(root.mode) color: heroGlyph.tint font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.font.display } } Column { anchors.verticalCenter: parent.verticalCenter width: heroRow.width - heroGlyph.width - heroRow.spacing spacing: Style.space(2) Text { width: parent.width elide: Text.ElideRight text: Modes.title(root.mode) color: Color.popups.text font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(17) } Text { width: parent.width elide: Text.ElideRight text: root.preset !== "" ? "preset · " + root.preset : Modes.blurb(root.mode) color: Color.muted font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(11) } } } PanelSeparator { width: parent.width } PanelSectionHeader { text: "SWITCH TO" visible: root.stage === "list" foreground: Color.popups.text fontFamily: root.bar ? root.bar.fontFamily : Style.font.family } // ---- The modes, current one marked rather than hidden. The CLI omits // the active mode because a list you arrow through should not offer a // no-op; here the list is also the status display, so it stays. Column { width: parent.width visible: root.stage === "list" spacing: Style.space(1) Repeater { model: root.stage === "list" ? Modes.ORDER : [] Column { id: modeEntry required property var modelData width: column.width readonly property string thisMode: String(modelData) readonly property bool isCurrent: thisMode === root.mode readonly property bool isExpanded: root.expandedMode === thisMode readonly property var presetList: Modes.presetsFor(root.health, thisMode, Modes.DEFAULT_PRESET) Item { width: parent.width height: Style.space(30) Rectangle { anchors.fill: parent anchors.leftMargin: -Style.space(6) anchors.rightMargin: -Style.space(6) radius: Style.space(4) color: modeEntry.isCurrent ? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.12) : (modeHover.containsMouse ? Qt.rgba(Color.popups.text.r, Color.popups.text.g, Color.popups.text.b, 0.07) : "transparent") } BrandIcon { id: modeGlyph anchors.left: parent.left anchors.leftMargin: Style.space(3) anchors.verticalCenter: parent.verticalCenter pathData: Modes.logo(modeEntry.thisMode) opticalScale: Modes.logoScale(modeEntry.thisMode) color: modeEntry.isCurrent ? Color.accent : Color.popups.text iconSize: Style.space(16) } Column { anchors.left: modeGlyph.right anchors.leftMargin: Style.space(8) anchors.right: modeMark.left anchors.rightMargin: Style.space(6) anchors.verticalCenter: parent.verticalCenter spacing: 0 Text { text: Modes.title(modeEntry.thisMode) color: Color.popups.text font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.font.body font.bold: modeEntry.isCurrent } Text { width: parent.width text: Modes.blurb(modeEntry.thisMode) color: Color.muted elide: Text.ElideRight font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(10) } } // Checkmark on the active mode; a chevron on a gateway row that // has a preset list waiting behind it. Text { id: modeMark anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: modeEntry.isCurrent ? String.fromCodePoint(0xF012C) : (Modes.needsPreset(modeEntry.thisMode) ? String.fromCodePoint(modeEntry.isExpanded ? 0xF0140 : 0xF0142) : "") color: modeEntry.isCurrent ? Color.accent : Color.muted font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.font.body } MouseArea { id: modeHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor enabled: !root.busy onClicked: root.activate(modeEntry.thisMode) } } // ---- Presets for this provider, unfolded in place. Column { width: parent.width visible: modeEntry.isExpanded && modeEntry.presetList.length > 0 spacing: Style.space(1) Repeater { model: modeEntry.isExpanded ? modeEntry.presetList : [] Item { id: presetRow required property var modelData width: modeEntry.width height: Style.space(26) readonly property bool isActive: modeEntry.isCurrent && String(modelData.name) === root.preset Rectangle { anchors.fill: parent anchors.leftMargin: Style.space(20) anchors.rightMargin: -Style.space(6) radius: Style.space(4) color: presetHover.containsMouse ? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.14) : "transparent" } Text { id: presetName anchors.left: parent.left anchors.leftMargin: Style.space(30) anchors.verticalCenter: parent.verticalCenter text: presetRow.modelData.name color: presetRow.isActive ? Color.accent : Color.popups.text font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(12) font.bold: presetRow.isActive } // The opus mapping is the one that tells you what you are // about to be talking to; the rest is in the tooltip. Text { anchors.left: presetName.right anchors.leftMargin: Style.space(8) anchors.right: gearButton.visible ? gearButton.left : parent.right anchors.rightMargin: Style.space(4) anchors.verticalCenter: parent.verticalCenter horizontalAlignment: Text.AlignRight text: presetRow.modelData.models && presetRow.modelData.models.opus ? String(presetRow.modelData.models.opus) : "" color: Color.muted elide: Text.ElideLeft font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(10) } // Clicking the row switches; the gear edits where that preset // points instead. Per row rather than per provider, because // two LM Studio presets can sit on two different machines. MouseArea { id: presetHover anchors.fill: parent anchors.rightMargin: gearButton.visible ? gearButton.width : 0 hoverEnabled: true cursorShape: Qt.PointingHandCursor enabled: !root.busy onClicked: root.switchTo(modeEntry.thisMode, String(presetRow.modelData.name)) } Item { id: gearButton visible: modeEntry.thisMode === "lmstudio" anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter width: Style.space(22) height: parent.height Text { anchors.centerIn: parent text: String.fromCodePoint(0xF0493) color: gearArea.containsMouse ? Color.accent : Color.muted font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(12) } MouseArea { id: gearArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor enabled: !root.busy onClicked: root.openServerSettings(String(presetRow.modelData.name)) } } } } } } } } // ---- Active model map, only meaningful behind a gateway. // ---- Preflight refused. Name the missing thing and offer the one action // that fixes it, rather than failing the click silently. Column { width: parent.width visible: root.stage === "blocked" && root.blocker !== null spacing: Style.space(7) Text { width: parent.width text: root.blocker ? String(root.blocker.title) : "" color: root.urgentColor wrapMode: Text.WordWrap font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(13) font.bold: true } Text { width: parent.width text: root.blocker ? String(root.blocker.detail) : "" color: Color.popups.text wrapMode: Text.WordWrap lineHeight: 1.2 font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(11) } Text { width: parent.width visible: root.blocker && String(root.blocker.remedy) !== "" text: root.blocker ? "$ " + String(root.blocker.remedy) : "" color: Color.muted wrapMode: Text.WrapAnywhere font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(10) } Row { spacing: Style.space(7) PillButton { label: "Store the key…" primary: true visible: root.blocker && String(root.blocker.remedyKind) === "set-key" onTriggered: root.runRemedy() } PillButton { label: "Server settings…" primary: root.blocker && ["start-server", "set-url", "needs-key"].indexOf(String(root.blocker.remedyKind)) >= 0 visible: root.pendingMode === "lmstudio" onTriggered: root.openServerSettings(root.pendingPreset) } PillButton { label: root.blocker && String(root.blocker.remedyKind) === "start-server" ? "Check again" : "Try again" primary: root.blocker && String(root.blocker.remedyKind) === "start-server" onTriggered: root.retryPending() } PillButton { label: "Cancel"; onTriggered: root.resetFlow() } } } // ---- Where the LM Studio server is, and whether it needs a key. Column { width: parent.width visible: root.stage === "server" spacing: Style.space(8) Text { width: parent.width text: "Server for '" + root.serverPreset + "'" 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: "LM Studio does not have to be on this machine. Point this at a " + "LAN address, or anything reachable through a tunnel or proxy." color: Color.muted wrapMode: Text.WordWrap lineHeight: 1.2 font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(10) } TextField { id: urlField width: parent.width text: root.serverUrl placeholderText: root.serverLocalDefault foreground: Color.popups.text font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(11) onTextChanged: root.serverUrl = text onAccepted: root.saveServerSettings() } Row { spacing: Style.space(7) PillButton { label: "Use local default" onTriggered: { root.serverUrl = root.serverLocalDefault; urlField.text = root.serverLocalDefault } } } PanelSeparator { width: parent.width } Row { width: parent.width spacing: Style.space(9) ToggleSwitch { id: authToggle anchors.verticalCenter: parent.verticalCenter checked: root.serverNeedsKey foreground: Color.popups.text accent: Color.accent onToggled: root.serverNeedsKey = !root.serverNeedsKey } Column { anchors.verticalCenter: parent.verticalCenter width: parent.width - authToggle.width - parent.spacing spacing: Style.space(1) Text { width: parent.width text: "Server requires an API key" color: Color.popups.text elide: Text.ElideRight font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(11) } Text { width: parent.width text: root.serverNeedsKey ? "Kept in the vault as '" + root.serverKeyRef + "', never in settings.json." : "Sends LM Studio's placeholder token, which is not a secret." color: Color.muted wrapMode: Text.WordWrap font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(10) } } } Row { spacing: Style.space(7) visible: root.serverNeedsKey PillButton { label: "Store the key…"; onTriggered: root.storeServerKey() } } PanelSeparator { width: parent.width } Row { spacing: Style.space(7) PillButton { label: "Save"; primary: true; onTriggered: root.saveServerSettings() } PillButton { label: "Cancel"; onTriggered: root.resetFlow() } } } // ---- Sessions are running. They will keep the old provider until they // are restarted, and one mid-request can lose that turn outright, so this // is a decision rather than a notification. Column { width: parent.width visible: root.stage === "confirm" && root.sessionInfo !== null spacing: Style.space(7) readonly property int total: root.sessionInfo ? Number(root.sessionInfo.count) : 0 readonly property int busyCount: root.sessionInfo ? Number(root.sessionInfo.busy) : 0 Text { width: parent.width text: parent.total === 1 ? "1 Claude session is running" : parent.total + " Claude sessions are running" color: Color.popups.text wrapMode: Text.WordWrap font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(13) font.bold: true } Text { width: parent.width text: "Claude Code reads its settings once, at startup, so these keep " + Modes.title(root.mode) + " until they are restarted." color: Color.muted wrapMode: Text.WordWrap lineHeight: 1.2 font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(10) } Text { width: parent.width visible: parent.busyCount > 0 text: parent.busyCount === 1 ? "One is working right now and will lose that turn." : parent.busyCount + " are working right now and will lose those turns." color: root.urgentColor 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.sessionInfo ? root.sessionInfo.sessions : [] Item { required property var modelData width: column.width height: Style.space(17) Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter text: parent.modelData.tty color: Color.muted font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(10) } Text { anchors.left: parent.left anchors.leftMargin: Style.space(52) anchors.right: busyTag.left anchors.rightMargin: Style.space(6) anchors.verticalCenter: parent.verticalCenter text: parent.modelData.cwd color: Color.popups.text elide: Text.ElideLeft font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(10) } Text { id: busyTag anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter visible: parent.modelData.busy === true text: "working" color: root.urgentColor font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(10) } } } } Row { spacing: Style.space(7) PillButton { label: "Switch and restart" primary: true onTriggered: root.applySwitch("restart") } PillButton { label: "Switch and close"; onTriggered: root.applySwitch("stop") } PillButton { label: "Switch only"; onTriggered: root.applySwitch("none") } PillButton { label: "Cancel"; onTriggered: root.resetFlow() } } } PanelSeparator { width: parent.width; visible: root.modelRows.length > 0 && root.stage === "list" } Column { width: parent.width spacing: Style.space(1) visible: root.modelRows.length > 0 && root.stage === "list" PanelSectionHeader { text: "MODEL MAP" foreground: Color.popups.text fontFamily: root.bar ? root.bar.fontFamily : Style.font.family } Repeater { model: root.modelRows Item { required property var modelData width: column.width height: Style.space(19) Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter text: parent.modelData.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: parent.right anchors.verticalCenter: parent.verticalCenter horizontalAlignment: Text.AlignRight text: parent.modelData.id color: Color.popups.text elide: Text.ElideLeft font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(11) } } } } PanelSeparator { width: parent.width } // ---- Failures worth surfacing: a preset with no key stored exits // non-zero, and without this the click would just look inert. Text { width: parent.width visible: root.lastError !== "" text: root.lastError color: root.urgentColor wrapMode: Text.WordWrap font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(10) } Text { width: parent.width text: root.busy ? "switching…" : (root.known && root.stage === "list" ? "Restart claude to pick this up." : "") visible: text !== "" 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 text: root.footerText visible: text !== "" color: Color.muted opacity: 0.8 wrapMode: Text.WordWrap font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.space(10) } } } }