diff options
Diffstat (limited to 'menubar/panel.swift')
| -rw-r--r-- | menubar/panel.swift | 93 |
1 files changed, 84 insertions, 9 deletions
diff --git a/menubar/panel.swift b/menubar/panel.swift index 21b4644..5347ceb 100644 --- a/menubar/panel.swift +++ b/menubar/panel.swift @@ -163,7 +163,7 @@ struct PanelContent { } for (index, row) in rows.enumerated() { if index == separatorBefore { lines.append("---") } - lines.append("\(row.status.symbol) \(row.status.rawValue)\t\(row.markedLabel)\t\(row.subtitle)") + lines.append(row.panelText) } if overflow > 0 { lines.append("+\(overflow) more waiting") } if showsSummary { @@ -291,8 +291,10 @@ final class ActionMenuItem: NSMenuItem { @objc private func run() { handler() } } -/// One agent row: colored symbol and name, then a dim subtitle. Right-click -/// offers "Mark as unread" for an idle agent. +/// One agent row: colored symbol and name, then a dim line with where it +/// runs, and at its right edge today's cost and a context meter, each in a +/// fixed column so they line up down the list. Right-click offers "Mark as +/// unread" for an idle agent. final class PanelRowView: ClickView { private let selected: Bool private let contextMenu: NSMenu? @@ -311,10 +313,35 @@ final class PanelRowView: ClickView { self.onClick = { onClick(row.paneId) } let title = singleLine(NSTextField( labelWithAttributedString: titleText ?? rowTitle(row, primary: mochaText, secondary: mochaSubtext))) - let subtitleLabel = NSTextField(labelWithString: row.subtitle) - subtitleLabel.font = .systemFont(ofSize: 11) - subtitleLabel.textColor = mochaSubtext - let subtitle = singleLine(subtitleLabel) + let placeLabel = NSTextField(labelWithString: row.place) + placeLabel.font = .systemFont(ofSize: 11) + placeLabel.textColor = mochaSubtext + let place = singleLine(placeLabel) + let cost = NSTextField(labelWithString: row.cost >= 0.005 ? dollars(row.cost) : "") + cost.font = .monospacedDigitSystemFont(ofSize: 11, weight: .regular) + cost.textColor = mochaSubtext + cost.alignment = .right + let meter = ContextMeter(percent: row.context) + // The meter at the right edge, the cost before it; the place takes + // what is left and truncates. + let subtitle = NSView() + for view in [place, cost, meter] { + view.translatesAutoresizingMaskIntoConstraints = false + subtitle.addSubview(view) + } + NSLayoutConstraint.activate([ + meter.trailingAnchor.constraint(equalTo: subtitle.trailingAnchor), + meter.centerYAnchor.constraint(equalTo: subtitle.centerYAnchor), + meter.widthAnchor.constraint(equalToConstant: ContextMeter.size.width), + meter.heightAnchor.constraint(equalToConstant: ContextMeter.size.height), + cost.trailingAnchor.constraint(equalTo: meter.leadingAnchor, constant: -6), + cost.widthAnchor.constraint(equalToConstant: 52), + cost.firstBaselineAnchor.constraint(equalTo: place.firstBaselineAnchor), + place.leadingAnchor.constraint(equalTo: subtitle.leadingAnchor), + place.trailingAnchor.constraint(lessThanOrEqualTo: cost.leadingAnchor, constant: -6), + place.topAnchor.constraint(equalTo: subtitle.topAnchor), + place.bottomAnchor.constraint(equalTo: subtitle.bottomAnchor), + ]) let stack = NSStackView(views: [title, subtitle]) stack.orientation = .vertical stack.alignment = .leading @@ -327,7 +354,7 @@ final class PanelRowView: ClickView { stack.topAnchor.constraint(equalTo: topAnchor, constant: 5), stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -5), title.widthAnchor.constraint(lessThanOrEqualTo: stack.widthAnchor), - subtitle.widthAnchor.constraint(lessThanOrEqualTo: stack.widthAnchor), + subtitle.widthAnchor.constraint(equalTo: stack.widthAnchor), ]) toolTip = row.name.isEmpty ? nil : row.name } @@ -343,6 +370,38 @@ final class PanelRowView: ClickView { } } +/// How full an agent's context window is: a small bar, gray, then yellow +/// above 70% and red above 90% (as pi's footer); nothing when unknown. +final class ContextMeter: NSView { + static let size = NSSize(width: 44, height: 5) + let percent: Double? + + init(percent: Double?) { + self.percent = percent + super.init(frame: NSRect(origin: .zero, size: Self.size)) + translatesAutoresizingMaskIntoConstraints = false + toolTip = percent.map { "\(contextText($0)) of context" } + } + + required init?(coder: NSCoder) { fatalError("not used") } + + /// The filled part: at least a dot, at most the whole bar. + static func fill(_ percent: Double, width: CGFloat) -> CGFloat { + max(size.height, min(width, width * CGFloat(percent) / 100)) + } + + override func draw(_ dirtyRect: NSRect) { + guard let percent else { return } + let radius = bounds.height / 2 + mochaSurface.setFill() + NSBezierPath(roundedRect: bounds, xRadius: radius, yRadius: radius).fill() + (contextWarning(percent) ?? mochaOverlay.blended(withFraction: 0.35, of: mochaSubtext) ?? mochaOverlay).setFill() + var filled = bounds + filled.size.width = Self.fill(percent, width: bounds.width) + NSBezierPath(roundedRect: filled, xRadius: radius, yRadius: radius).fill() + } +} + /// The restore offer: "↺ Restore 9 sessions · 22 agents", what it was. private final class OfferView: ClickView { init(title: String, detail: String, onClick: @escaping () -> Void) { @@ -718,12 +777,28 @@ func panelSelftest() -> [String] { check(full.plainText.hasSuffix("[Show less ▴]") && !compact.plainText.contains("Show"), "toggle offered only when allowed, labelled by layout") + // Context use. + var filling = row("f", .idle) + filling.cost = 3.12 + filling.context = 78.4 + check(filling.subtitle == "s · f · pi · $3.12 · 78%", "context: the menu's text, after the cost") + check(filling.place == "s · f · pi" && filling.panelText.hasSuffix("\ts · f · pi\t$3.12 78%"), + "context: the panel's columns") + check(contextWarning(70) == nil && contextWarning(70.5) == mochaYellow && contextWarning(90.5) == mochaRed, + "context: pi's colors, above 70% and 90%") + check(ContextMeter.fill(0, width: 44) == 5 && ContextMeter.fill(50, width: 44) == 22 + && ContextMeter.fill(150, width: 44) == 44, "context: the bar's fill") + filling.context = nil + filling.cost = 0 + check(filling.subtitle == "s · f · pi" && !filling.panelText.contains("%"), "context: unknown, nothing") + // Spend. check(dollars(0.42) == "$0.42" && dollars(19.849) == "$19.85" && dollars(99.994) == "$99.99" && dollars(99.996) == "$100" && dollars(1967.14) == "$1,967", "spend: cents below $100, then whole dollars") var spender = row("s", .done) spender.cost = 19.85 - check(spender.subtitle.hasPrefix("s · s · pi · $19.85 · done "), "spend: today's in the subtitle, before the wait") + check(spender.subtitle.hasPrefix("s · s · pi · done ") && spender.subtitle.hasSuffix(" · $19.85"), + "spend: today's in the menu's subtitle, after the wait") spender.cost = 0.004 check(!spender.subtitle.contains("$"), "spend: nothing spent, nothing shown") check(Spend(today: 8.86, week: 456.87, month: 8.86).line == "$8.86 today · $457 this week · $8.86 this month" |
