// Agents. // // Which AI tool answers when the desktop offers to investigate something, what // the desktop is allowed to hand it, and how much of your subscription is left. // // The preferred agent ships as "none" and that is not a placeholder: until one // is chosen, every rung of the escalation ladder stays silent -- a crash // notification carries no action, System Health grows no button, a failed // reload says only that it failed. A desktop that volunteered a tool nobody // installed would be worse than one that says nothing. import QtQuick import Quickshell import Quickshell.Io import qs.config import qs.services import qs.widgets SettingsPage { id: root objectName: "agents-page" title: "Agents" lede: "Your AI tools, and what the desktop is allowed to hand them." // The options come from the schema rather than from a list here, so this // page cannot offer an agent the preference would refuse. readonly property var agentOptions: PreferenceSchema.spec("preferredAgent")?.options ?? [] readonly property string preferred: String(DesktopPreferences.get("preferredAgent") ?? "none") // ── Install state, or no claim at all ─────────────────────────────────── // // A tile says "Not installed" only once something has actually looked. Any // other order gets it wrong in the direction that matters: a page telling // somebody their agent is missing, when it is sitting right there, teaches // them not to believe the page. // // Probed through a LOGIN shell rather than this one. Quickshell is started // by systemd, whose PATH does not include ~/.local/bin -- where both of // these usually land -- and a login shell is the environment the launcher // hands the agent when it spawns a terminal. Asking with the shell's own // PATH would report "not installed" for an agent that starts perfectly. property var installed: ({}) property bool probed: false function absorbProbe(text: string): void { const found = {}; for (const line of String(text).split("\n")) { const name = line.trim(); if (name !== "") found[name] = true; } root.installed = found; root.probed = true; } Process { id: agentProbe running: true command: ["bash", "-lc", "for agent in claude codex; do command -v \"$agent\" >/dev/null 2>&1 && printf '%s\\n' \"$agent\"; done"] stdout: StdioCollector { onStreamFinished: root.absorbProbe(this.text) } } function iconFor(value: string): string { switch (value) { case "claude": return "starred-symbolic"; case "codex": return "utilities-terminal-symbolic"; default: return "notifications-disabled-symbolic"; } } function tileDetail(value: string): string { if (value === "none") return "Stay quiet"; if (!root.probed) return ""; return root.installed[value] === true ? "Installed" : "Not installed"; } SettingsCard { title: "Preferred agent" subtitle: "Who answers when the desktop offers to investigate something. Until one is chosen, crash notifications carry no action — the desktop stays quiet rather than volunteering a tool you do not use." // The same tile shape the power profiles use: three rows with the word // "Active" in one of them is a list you have to read to find out what // is set; three tiles with one lit answers that without reading. Flow { id: tiles width: parent.width spacing: 10 bottomPadding: 12 readonly property int columns: tiles.width >= 460 ? 3 : 1 readonly property real tileWidth: (tiles.width - tiles.spacing * (tiles.columns - 1)) / tiles.columns Repeater { model: root.agentOptions Rectangle { id: tile required property var modelData readonly property string value: String(tile.modelData.value) readonly property bool selected: tile.value === root.preferred readonly property string detail: root.tileDetail(tile.value) objectName: `agent-tile:${tile.value}` width: tiles.tileWidth implicitHeight: tileBody.implicitHeight + 24 radius: Theme.cardRadius color: tile.selected ? Theme.alpha(Theme.accent, 0.09) : Theme.alpha(Theme.fg, tileHover.hovered ? 0.08 : 0.04) border.width: tile.selected || tile.activeFocus ? 2 : 1 border.color: tile.activeFocus ? Theme.accentSecondary : (tile.selected ? Theme.alpha(Theme.accent, 0.6) : Theme.alpha(Theme.fg, 0.08)) activeFocusOnTab: true Accessible.role: Accessible.RadioButton Accessible.name: String(tile.modelData.label) Accessible.description: tile.detail Accessible.checked: tile.selected // An agent this machine cannot start is still selectable: // the probe answers for THIS session's login shell, and // being wrong about that must not lock somebody out of a // choice they are entitled to make. The tile says what it // found; the person decides. function choose(): void { if (!tile.selected) SystemSettings.commitPreference("preferredAgent", tile.value); } Keys.onReturnPressed: tile.choose() Keys.onSpacePressed: tile.choose() Column { id: tileBody anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: 12 spacing: 6 ThemedIcon { icon: root.iconFor(tile.value) iconFallback: "system-run-symbolic" size: 20 tint: tile.selected ? Theme.accent : Theme.fgDim } Text { width: parent.width text: String(tile.modelData.label) color: Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall font.weight: Font.DemiBold elide: Text.ElideRight } Text { width: parent.width visible: tile.detail !== "" text: tile.detail color: Theme.fgMuted font.family: Theme.fontFamily font.pixelSize: Math.max(9, Theme.fontSizeSmall - 1) wrapMode: Text.WordWrap } } HoverHandler { id: tileHover cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: { tile.choose(); tile.forceActiveFocus(); } } } } } } SettingsCard { title: "When something breaks" subtitle: "Each of these is a failure that used to be a dead end. Every one still needs an agent chosen above before it offers anything." ToggleRow { setting: "crashDiagnoseOffer" } ToggleRow { setting: "reloadFailureOffer" } ToggleRow { setting: "healthAgentHandoff" } ToggleRow { setting: "agentAutoApprove"; divider: false } } SettingsCard { title: "Usage in the bar" subtitle: "The bar shows the fullest limit — the one that stops your next prompt. Clicking it opens the whole picture." // The same switch the Bar page carries, deliberately: this is the page // somebody is on when they wonder where the number went, and the Bar // page is the page they are on when they are choosing what the bar // contains. Declared as an intentional mirror in // tests/quickshell/settings-ownership-contract. ToggleRow { setting: "showAgentUsage" } ToggleRow { setting: "agentUsageClaude" } ToggleRow { setting: "agentUsageCodex" } SliderRow { setting: "agentUsageRefreshMinutes"; divider: false } } }