diff options
| author | Igor Soarez <igor@soarez.org> | 2026-10-26 16:32:18 +0200 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-10-26 16:32:18 +0200 |
| commit | 185af91b4d525db6c192a050b53615772fb5fe11 (patch) | |
| tree | 6db92db4042dddcc5fee13d02943447ba9df5f2d /menubar | |
| parent | 1eb0ac3705276c6f81f44487c7e99c24a302d4d5 (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.swift | 17 | ||||
| -rw-r--r-- | menubar/model.swift | 53 | ||||
| -rw-r--r-- | menubar/panel.swift | 2 | ||||
| -rw-r--r-- | menubar/search.swift | 4 |
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 |
