aboutsummaryrefslogtreecommitdiff
path: root/menubar
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-10-26 16:32:18 +0200
committerIgor Soarez <igor@soarez.org>2026-10-26 16:32:18 +0200
commit185af91b4d525db6c192a050b53615772fb5fe11 (patch)
tree6db92db4042dddcc5fee13d02943447ba9df5f2d /menubar
parent1eb0ac3705276c6f81f44487c7e99c24a302d4d5 (diff)
importance: mark an agent high, normal, or low
within a status, more important agents are listed first, and so jumped to first; status stays first. a high agent shows a bold ! before its name, a low agent's name is lightly dimmed with "low" on its second line, and it makes no sound (it still notifies and waits in line). in pi, /importance high|normal|low (completes the levels; none once one is typed out, so Enter runs it). the extension saves it in the session as a custom entry (dingger-importance), so /reload, restarts, /resume and restore bring it back; /new starts at normal. it shows a level other than normal in pi's footer, emits dingger:importance { level } for other extensions, and sends it with every report. for other agents, `dingger importance [level] [--pane]`; it refuses a pi that reports itself, whose saved level would win. the daemon stays the one writer of pane state: it writes @dingger_importance (unset for normal), which goes with the agent and survives a daemon restart. `dingger status` marks high and low. tests: e2e step 21 (order, the option, no sound for low, the menu bar, the command, the extension through a harness that now matches pi's extension API), a DINGGER_SOUND seam so sounds are observable, and a status-line check that read the counts once (flaky) now polls. pi-live: /importance in real pi 1.4, its footer and session entry, and the level after a restore; every pi there now uses the test's agent directory (a restored pi read the user's). negative controls fail each part.
Diffstat (limited to 'menubar')
-rw-r--r--menubar/main.swift17
-rw-r--r--menubar/model.swift53
-rw-r--r--menubar/panel.swift2
-rw-r--r--menubar/search.swift4
4 files changed, 63 insertions, 13 deletions
diff --git a/menubar/main.swift b/menubar/main.swift
index 17b9fcc..c37386e 100644
--- a/menubar/main.swift
+++ b/menubar/main.swift
@@ -397,6 +397,21 @@ if arguments.contains("--selftest") {
since: $0.2, path: "~") }.sorted(by: listedBefore).map(\.name)
let expected = ["blocked", "done old", "done new", "working new", "working old", "idle new", "idle old", "idle unknown"]
if listed != expected { failures.append("order: waiting oldest first, the rest most recent first: \(listed)") }
+ let byImportance: [(String, Status, Importance, Int)] = [
+ ("done old", .done, .normal, 10), ("done new high", .done, .high, 90), ("done older low", .done, .low, 5),
+ ("blocked low", .blocked, .low, 99), ("working high", .working, .high, 1), ("working new", .working, .normal, 80),
+ ]
+ let importanceListed = byImportance.map { Row(paneId: $0.0, agent: "pi", name: $0.0, status: $0.1, session: "s",
+ window: "w", since: $0.3, path: "~", importance: $0.2) }
+ .sorted(by: listedBefore).map(\.name)
+ let importanceExpected = ["blocked low", "done new high", "done old", "done older low", "working high", "working new"]
+ if importanceListed != importanceExpected {
+ failures.append("order: status first, then importance, then time: \(importanceListed)")
+ }
+ let high = Row(paneId: "%1", agent: "pi", name: "Fix it", status: .done, session: "s", window: "w", since: 1, importance: .high)
+ if rowTitle(high).string != "✓ ! Fix it" || rowTitlePrefix(high) != "✓ ! " {
+ failures.append("title: a high agent is marked: \(rowTitle(high).string)")
+ }
failures.forEach { print("FAIL \($0)") }
print(failures.isEmpty ? "selftest ok" : "selftest: \(failures.count) failed")
exit(failures.isEmpty ? 0 : 1)
@@ -405,7 +420,7 @@ if arguments.contains("--print") {
let rows = fetchRows()
print("title: \(plainTitle(for: rows))")
for row in rows {
- print("\(row.status.symbol) \(row.status.rawValue)\t\(row.label)\t\(row.subtitle)")
+ print("\(row.status.symbol) \(row.status.rawValue)\t\(row.markedLabel)\t\(row.subtitle)")
}
exit(0)
}
diff --git a/menubar/model.swift b/menubar/model.swift
index 8bed858..760ff31 100644
--- a/menubar/model.swift
+++ b/menubar/model.swift
@@ -65,6 +65,20 @@ enum Status: String {
}
}
+/// The agent's importance (`@dingger_importance`, unset for normal): it
+/// orders agents within a status, and a low agent makes no sound.
+enum Importance: Int {
+ case low = 0, normal = 1, high = 2
+
+ init(option: String) {
+ switch option {
+ case "high": self = .high
+ case "low": self = .low
+ default: self = .normal
+ }
+ }
+}
+
struct Row: Equatable {
let paneId: String
let agent: String
@@ -76,6 +90,7 @@ struct Row: Equatable {
let since: Int
/// The pane's working directory, home as "~".
var path: String = ""
+ var importance: Importance = .normal
var location: String { "\(session) · \(window)" }
var waiting: Bool { status == .blocked || status == .done }
@@ -86,9 +101,14 @@ struct Row: Equatable {
return name.count > 80 ? String(name.prefix(79)) + "…" : name
}
- /// Dim second line: where it runs, which harness, and how long it waits.
+ /// The title's text as plain text: `! ` marks a high agent.
+ var markedLabel: String { (importance == .high ? "! " : "") + label }
+
+ /// Dim second line: where it runs, which harness, whether it is low,
+ /// and how long it waits.
var subtitle: String {
var parts = [location, agent]
+ if importance == .low { parts.append("low") }
if waiting, since != Int.max {
parts.append("\(status.rawValue) \(elapsed(sinceMs: since))")
}
@@ -219,13 +239,15 @@ func runDinggerInLoginShell(_ args: [String]) -> (ok: Bool, output: String) {
private let home = NSHomeDirectory()
-/// Every agent pane: herdr's status priority; within a status, done and
-/// blocked oldest first (the queue), working and idle most recent first
-/// (`@dingger_since`: when the pane entered its status); then by place.
+/// Every agent pane: herdr's status priority; within a status, the more
+/// important first, then done and blocked oldest first (the queue),
+/// working and idle most recent first (`@dingger_since`: when the pane
+/// entered its status); then by place.
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}",
].joined(separator: "\t")
guard let out = tmux(["list-panes", "-a", "-F", format]) else { return [] }
// A pane in several sessions (a session group, a linked window) is
@@ -233,13 +255,14 @@ func fetchRows() -> [Row] {
var listed = Set<String>()
let rows: [Row] = out.split(separator: "\n").compactMap { line in
let f = line.split(separator: "\t", omittingEmptySubsequences: false).map(String.init)
- guard f.count >= 10, !f[1].isEmpty, listed.insert(f[0]).inserted else { return nil }
+ guard f.count >= 11, !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]
return Row(paneId: f[0], agent: f[1], name: f[4], status: status,
session: f[5], window: window, since: Int(f[3]) ?? Int.max,
- path: f[9].hasPrefix(home) ? "~" + f[9].dropFirst(home.count) : f[9])
+ path: f[9].hasPrefix(home) ? "~" + f[9].dropFirst(home.count) : f[9],
+ importance: Importance(option: f[10]))
}
return rows.sorted(by: listedBefore)
}
@@ -247,6 +270,7 @@ func fetchRows() -> [Row] {
/// The list order (see `fetchRows`).
func listedBefore(_ a: Row, _ b: Row) -> Bool {
if a.status.priority != b.status.priority { return a.status.priority > b.status.priority }
+ if a.importance != b.importance { return a.importance.rawValue > b.importance.rawValue }
if a.since != b.since {
// Int.max: never stamped; last either way.
if a.since == Int.max || b.since == Int.max { return a.since < b.since }
@@ -291,8 +315,15 @@ func plainTitle(for rows: [Row]) -> String {
title(for: rows).string
}
-/// Row title: the colored status symbol, then the agent's name. Unnamed
-/// agents show their harness, dimmed, so it never competes with real names.
+/// The title's characters before the name: the status symbol, and `!` for a
+/// high agent.
+func rowTitlePrefix(_ row: Row) -> String {
+ "\(row.status.symbol) " + (row.importance == .high ? "! " : "")
+}
+
+/// Row title: the colored status symbol, a bold `!` for a high agent, then
+/// the agent's name. Unnamed agents show their harness, dimmed, so it never
+/// competes with real names; a low agent's name is lightly dimmed.
func rowTitle(_ row: Row, primary: NSColor = .labelColor,
secondary: NSColor = .secondaryLabelColor) -> NSAttributedString {
let font = NSFont.menuFont(ofSize: 0)
@@ -300,7 +331,11 @@ func rowTitle(_ row: Row, primary: NSColor = .labelColor,
let text = NSMutableAttributedString()
text.append(NSAttributedString(
string: "\(row.status.symbol) ", attributes: [.font: mono, .foregroundColor: row.status.color]))
- let labelColor = row.name.isEmpty ? secondary : primary
+ if row.importance == .high {
+ text.append(NSAttributedString(
+ string: "! ", attributes: [.font: NSFont.boldSystemFont(ofSize: font.pointSize), .foregroundColor: primary]))
+ }
+ let labelColor = row.name.isEmpty ? secondary : row.importance == .low ? primary.withAlphaComponent(0.7) : primary
text.append(NSAttributedString(
string: row.label, attributes: [.font: font, .foregroundColor: labelColor]))
return text
diff --git a/menubar/panel.swift b/menubar/panel.swift
index 6ab85f2..8edab70 100644
--- a/menubar/panel.swift
+++ b/menubar/panel.swift
@@ -153,7 +153,7 @@ struct PanelContent {
}
for (index, row) in rows.enumerated() {
if index == separatorBefore { lines.append("---") }
- lines.append("\(row.status.symbol) \(row.status.rawValue)\t\(row.label)\t\(row.subtitle)")
+ lines.append("\(row.status.symbol) \(row.status.rawValue)\t\(row.markedLabel)\t\(row.subtitle)")
}
if overflow > 0 { lines.append("+\(overflow) more waiting") }
if showsSummary {
diff --git a/menubar/search.swift b/menubar/search.swift
index 21db063..2bc044a 100644
--- a/menubar/search.swift
+++ b/menubar/search.swift
@@ -171,7 +171,7 @@ struct SearchState: Equatable {
for match in visible {
let row = match.row
let marker = row.paneId == selected ? "›" : " "
- lines.append("\(marker) \(row.status.symbol) \(row.status.rawValue)\t\(row.label)\t\(row.subtitle)")
+ lines.append("\(marker) \(row.status.symbol) \(row.status.rawValue)\t\(row.markedLabel)\t\(row.subtitle)")
}
lines.append(footer)
return lines.joined(separator: "\n")
@@ -192,7 +192,7 @@ private func highlightedTitle(_ match: SearchMatch) -> NSAttributedString {
let text = NSMutableAttributedString(
attributedString: rowTitle(match.row, primary: mochaText, secondary: mochaSubtext))
guard !match.labelPositions.isEmpty else { return text }
- let prefix = "\(match.row.status.symbol) ".utf16.count
+ let prefix = rowTitlePrefix(match.row).utf16.count
let font = NSFont.boldSystemFont(ofSize: NSFont.menuFont(ofSize: 0).pointSize)
let label = Array(match.row.label)
var utf16Offset = prefix