aboutsummaryrefslogtreecommitdiff
path: root/menubar
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-11-05 19:21:43 +0200
committerIgor Soarez <igor@soarez.org>2026-11-05 19:21:43 +0200
commit5842b093cc502b46076423e4f8efdbbc568f86aa (patch)
tree35b5a2ab971a2b9abb3188e56e0416033b4f13ea /menubar
parent3b7b7ed091bae3b0609c38b2d445201cc4b7a026 (diff)
pi: a failed run says so: a 'failed' alert with the error, and the error in red until seen
Diffstat (limited to 'menubar')
-rw-r--r--menubar/model.swift27
-rw-r--r--menubar/panel.swift25
2 files changed, 43 insertions, 9 deletions
diff --git a/menubar/model.swift b/menubar/model.swift
index 4117ba5..fb83105 100644
--- a/menubar/model.swift
+++ b/menubar/model.swift
@@ -105,6 +105,11 @@ struct Row: Equatable {
/// How full its context window is, in percent (`@dingger_context`), as
/// pi's footer shows it; nil when unknown.
var context: Double? = nil
+ /// Why its run failed (`@dingger_error`), while it is done; "" if not.
+ var error: String = ""
+
+ /// Done, but its run failed.
+ var failed: Bool { status == .done && !error.isEmpty }
var location: String { "\(session) · \(window)" }
/// A draft left behind: where the user looks, they may be typing it.
@@ -134,15 +139,25 @@ struct Row: Equatable {
}
/// The panel's second line, before its cost and context columns.
- var place: String {
+ var place: String { [placeBase, failure].compactMap { $0 }.joined(separator: " · ") }
+
+ /// Where it runs, which harness, whether it is low, and how long it
+ /// waits, unless its run failed (see `failure`).
+ var placeBase: String {
var parts = [location, agent]
if importance == .low { parts.append("low") }
- if waiting, since != Int.max {
+ if waiting, since != Int.max, !failed {
parts.append("\(status.rawValue) \(elapsed(sinceMs: since))")
}
return parts.joined(separator: " · ")
}
+ /// "failed 3m · 401: invalid x-api-key", for a failed run.
+ var failure: String? {
+ guard failed else { return nil }
+ return (since != Int.max ? "failed \(elapsed(sinceMs: since))" : "failed") + " · " + error
+ }
+
/// The panel's row as text: its columns, then cost and context.
var panelText: String {
let right = [cost >= 0.005 ? dollars(cost) : nil, context.map(contextText)].compactMap { $0 }
@@ -356,19 +371,19 @@ func fetchRows() -> [Row] {
let format = [
"#{pane_id}", "#{@agent}", "#{@agent_state}", "#{@dingger_since}", "#{@agent_name}",
"#{session_name}", "#{window_name}", "#{window_panes}", "#{pane_index}", "#{pane_current_path}",
- "#{@dingger_importance}", "#{window_active}", "#{session_attached}", "#{@dingger_draft}", "#{@dingger_cost}", "#{@dingger_context}",
+ "#{@dingger_importance}", "#{window_active}", "#{session_attached}", "#{@dingger_draft}", "#{@dingger_cost}", "#{@dingger_context}", "#{@dingger_error}",
].joined(separator: "\t")
guard let out = tmux(["list-panes", "-a", "-F", format]) else { return [] }
let lines = out.split(separator: "\n").map {
$0.split(separator: "\t", omittingEmptySubsequences: false).map(String.init)
}
// Looked at in any of its sessions, as the daemon counts it.
- let attended = Set(lines.filter { $0.count >= 16 && $0[11] == "1" && $0[12] != "0" }.map { $0[0] })
+ let attended = Set(lines.filter { $0.count >= 17 && $0[11] == "1" && $0[12] != "0" }.map { $0[0] })
// A pane in several sessions (a session group, a linked window) is
// listed once, under its first session, as the daemon counts it.
var listed = Set<String>()
let rows: [Row] = lines.compactMap { f in
- guard f.count >= 16, !f[1].isEmpty, listed.insert(f[0]).inserted else { return nil }
+ guard f.count >= 17, !f[1].isEmpty, listed.insert(f[0]).inserted else { return nil }
let status = Status(rawValue: f[2]) ?? .unknown
guard status != .unknown else { return nil }
let window = (Int(f[7]) ?? 1) > 1 ? "\(f[6]).\(f[8])" : f[6]
@@ -376,7 +391,7 @@ func fetchRows() -> [Row] {
session: f[5], window: window, since: Int(f[3]) ?? Int.max,
path: f[9].hasPrefix(home) ? "~" + f[9].dropFirst(home.count) : f[9],
importance: Importance(option: f[10]), draft: f[13], attended: attended.contains(f[0]),
- cost: Double(f[14]) ?? 0, context: Double(f[15]))
+ cost: Double(f[14]) ?? 0, context: Double(f[15]), error: f[16])
}
return rows.sorted(by: listedBefore)
}
diff --git a/menubar/panel.swift b/menubar/panel.swift
index 5347ceb..174d30e 100644
--- a/menubar/panel.swift
+++ b/menubar/panel.swift
@@ -313,9 +313,17 @@ final class PanelRowView: ClickView {
self.onClick = { onClick(row.paneId) }
let title = singleLine(NSTextField(
labelWithAttributedString: titleText ?? rowTitle(row, primary: mochaText, secondary: mochaSubtext)))
- let placeLabel = NSTextField(labelWithString: row.place)
- placeLabel.font = .systemFont(ofSize: 11)
- placeLabel.textColor = mochaSubtext
+ // A failed run's "failed 3m · why" in red.
+ let small = NSFont.systemFont(ofSize: 11)
+ let placeText = NSMutableAttributedString(
+ string: row.placeBase, attributes: [.font: small, .foregroundColor: mochaSubtext])
+ if let failure = row.failure {
+ placeText.append(NSAttributedString(string: " · ", attributes: [.font: small, .foregroundColor: mochaSubtext]))
+ placeText.append(NSAttributedString(string: failure, attributes: [.font: small, .foregroundColor: mochaRed]))
+ }
+ let placeLabel = NSTextField(labelWithAttributedString: placeText)
+ // Its own font too, or it lays out as 13 pt (the colors stay).
+ placeLabel.font = small
let place = singleLine(placeLabel)
let cost = NSTextField(labelWithString: row.cost >= 0.005 ? dollars(row.cost) : "")
cost.font = .monospacedDigitSystemFont(ofSize: 11, weight: .regular)
@@ -777,6 +785,17 @@ func panelSelftest() -> [String] {
check(full.plainText.hasSuffix("[Show less ▴]") && !compact.plainText.contains("Show"),
"toggle offered only when allowed, labelled by layout")
+ // A failed run.
+ var broken = row("b", .done)
+ broken.error = "401: invalid x-api-key"
+ broken.cost = 1.5
+ check(broken.failed && broken.place.hasPrefix("s · b · pi · failed ") && broken.place.hasSuffix(" · 401: invalid x-api-key"),
+ "failed: in place of the wait, with why")
+ check(broken.subtitle.hasSuffix("401: invalid x-api-key · $1.50"), "failed: the menu's text, then the cost")
+ var seen = row("b", .idle)
+ seen.error = broken.error
+ check(!seen.failed && seen.place == "s · b · pi", "failed: once seen, plain idle")
+
// Context use.
var filling = row("f", .idle)
filling.cost = 3.12