No error is a dead end: crash, click, and your agent is already looking
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
@@ -66,6 +67,84 @@ SettingsPage {
|
||||
return detail + " · Repair runs: " + command;
|
||||
}
|
||||
|
||||
// ── The Health rung of the escalation ladder ────────────────────────────
|
||||
//
|
||||
// A red check with nothing to press is where this page used to end. It can
|
||||
// tell you the portals are down; it cannot tell you why, and the honest
|
||||
// next step -- read the journal, correlate against recent updates -- is
|
||||
// precisely the work an agent is good at. So the row grows one more button
|
||||
// carrying what the check knows.
|
||||
//
|
||||
// Offered only where it is the LAST resort. A check that offers a repair
|
||||
// has a better answer than a conversation, right up until that repair has
|
||||
// actually been run and failed.
|
||||
readonly property bool agentHandoffAvailable: {
|
||||
if (DesktopPreferences.get("healthAgentHandoff") !== true)
|
||||
return false;
|
||||
// No agent chosen is the shipped default, and it means what it says:
|
||||
// no button, no offer, the page exactly as it was.
|
||||
const agent = String(DesktopPreferences.get("preferredAgent") ?? "none");
|
||||
return agent !== "" && agent !== "none";
|
||||
}
|
||||
|
||||
// `repairFailed` is the row's own answer rather than a second computation
|
||||
// of it here: the status text beside the button already says "Repair
|
||||
// failed", and two independent readings of one fact is how a button starts
|
||||
// disagreeing with the words next to it.
|
||||
function canAskAgent(check: var, repairFailed: bool): bool {
|
||||
if (!root.agentHandoffAvailable || !check || check.status !== "error")
|
||||
return false;
|
||||
return check.action?.kind !== "repair" || repairFailed === true;
|
||||
}
|
||||
|
||||
// Built from the snapshot this page already holds rather than by shelling
|
||||
// the doctor a second time: these are the same fields `panama doctor check
|
||||
// <id>` returns, and asking twice would only create a way for the two to
|
||||
// disagree. The agent is handed that command anyway, so its first move is
|
||||
// a fresh reading rather than trust in ours.
|
||||
function agentPrompt(check: var, repairFailed: bool): string {
|
||||
const lines = [
|
||||
"A System Health check on this Panama machine is red and I want to know why.",
|
||||
"",
|
||||
"What panama doctor reported:",
|
||||
" check: " + String(check.id) + " (" + String(check.group) + ")",
|
||||
" title: " + String(check.title),
|
||||
" status: " + String(check.status),
|
||||
" detail: " + String(check.detail ?? "")
|
||||
];
|
||||
if (check.repairCommand)
|
||||
lines.push(" repair: " + String(check.repairCommand)
|
||||
+ (repairFailed ? " — run, and it failed" : " — offered, not yet run"));
|
||||
else
|
||||
lines.push(" repair: none offered");
|
||||
lines.push("");
|
||||
lines.push("Start with `panama doctor check " + String(check.id) + "` for the current");
|
||||
lines.push("snapshot, then find the cause: the journal first, then whether a recent");
|
||||
lines.push("package update or configuration change explains it.");
|
||||
lines.push("");
|
||||
lines.push("Diagnosis reads; it does not fix. Anything needing root goes through");
|
||||
lines.push("`panama-sudo --reason \"why\" -- <command>`, so the password prompt says why.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function askAgent(check: var, repairFailed: bool): void {
|
||||
if (!root.canAskAgent(check, repairFailed))
|
||||
return;
|
||||
// Reached by path rather than by name: the shell is started by systemd,
|
||||
// whose environment does not carry the repository's bin directory on
|
||||
// PATH. Same expansion the panama-crash-watch unit uses.
|
||||
Quickshell.execDetached(["sh", "-c",
|
||||
'"${PANAMA_PATH:-$HOME/.local/share/Panama}/bin/panama-agent" --prompt '
|
||||
+ root.shellQuote(root.agentPrompt(check, repairFailed))]);
|
||||
}
|
||||
|
||||
// POSIX single-quoting: everything between the quotes is literal, and the
|
||||
// only character needing care is the quote itself. A check's detail is
|
||||
// helper output, not a command, and this keeps it that way.
|
||||
function shellQuote(text: string): string {
|
||||
return "'" + String(text).replace(/'/g, "'\\''") + "'";
|
||||
}
|
||||
|
||||
function checksForGroup(group: string): var {
|
||||
return Health.checks.filter(check => check.group === group
|
||||
&& (check.status === "ok" || check.status === "unconfigured"));
|
||||
@@ -160,7 +239,12 @@ SettingsPage {
|
||||
const confirmationSheets = root.descendants(root, "health-confirmation-sheet:").filter(sheet => sheet.visible);
|
||||
const emptyGroups = root.descendants(root, "health-empty-group:").filter(label => label.visible);
|
||||
const fedoraHandoffs = root.descendants(root, "health-fedora-handoff:").filter(row => row.visible);
|
||||
const agentHandoffs = root.descendants(root, "health-ask-agent:").filter(button => button.visible);
|
||||
return {
|
||||
// Empty whenever no agent is chosen, which is the shipped default
|
||||
// and the state this page has to keep behaving exactly as it did.
|
||||
agentHandoffs: agentHandoffs.map(button =>
|
||||
String(button.objectName).slice("health-ask-agent:".length)),
|
||||
renderedRows: rows.map(row => {
|
||||
const objectName = String(row.objectName);
|
||||
const parts = objectName.split(":");
|
||||
@@ -295,16 +379,52 @@ SettingsPage {
|
||||
id: issueRepeater
|
||||
model: root.issueChecks
|
||||
|
||||
HealthCheckRow {
|
||||
// The row plus, where the check has run out of answers,
|
||||
// the handoff button beside it. The row keeps its own
|
||||
// layout and yields the width the button takes, so the
|
||||
// trailing controls never stack on top of each other.
|
||||
Item {
|
||||
id: issueEntry
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property bool offersAgent:
|
||||
root.canAskAgent(issueEntry.modelData, issueRow.repairFailed)
|
||||
|
||||
width: issueRows.width
|
||||
check: modelData
|
||||
detailText: root.repairDetail(modelData)
|
||||
issue: true
|
||||
divider: index < issueRepeater.count - 1
|
||||
onActionRequested: check => root.handleAction(check)
|
||||
implicitHeight: issueRow.implicitHeight
|
||||
|
||||
HealthCheckRow {
|
||||
id: issueRow
|
||||
|
||||
width: issueEntry.width
|
||||
- (issueEntry.offersAgent ? askAgentButton.width + 12 : 0)
|
||||
check: issueEntry.modelData
|
||||
detailText: root.repairDetail(issueEntry.modelData)
|
||||
issue: true
|
||||
divider: issueEntry.index < issueRepeater.count - 1
|
||||
onActionRequested: check => root.handleAction(check)
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
id: askAgentButton
|
||||
|
||||
objectName: `health-ask-agent:${issueEntry.modelData.id}`
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: issueEntry.offersAgent
|
||||
text: "Ask the agent"
|
||||
enabled: visible && !Health.busy
|
||||
activeFocusOnTab: enabled
|
||||
border.width: activeFocus ? 2 : 1
|
||||
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||
onClicked: root.askAgent(issueEntry.modelData, issueRow.repairFailed)
|
||||
Keys.onReturnPressed: if (enabled)
|
||||
root.askAgent(issueEntry.modelData, issueRow.repairFailed)
|
||||
Keys.onSpacePressed: if (enabled)
|
||||
root.askAgent(issueEntry.modelData, issueRow.repairFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user