@join-pantheon/cli 0.30.0 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -35
- package/dist/agents.js +41 -17
- package/dist/claude-confine.js +37 -21
- package/dist/cli.js +414 -38
- package/dist/client.js +258 -9
- package/dist/command-state.js +25 -2
- package/dist/config.js +79 -7
- package/dist/continued.js +42 -10
- package/dist/doctor.js +93 -15
- package/dist/file-lock.js +197 -0
- package/dist/home-guard.js +372 -17
- package/dist/identity.js +77 -1
- package/dist/mcp-supervisor.js +5 -2
- package/dist/mcp.js +78 -30
- package/dist/nonces.js +130 -0
- package/dist/notifier.js +35 -0
- package/dist/notify.js +9 -3
- package/dist/pending.js +113 -8
- package/dist/pins.js +161 -0
- package/dist/profile.js +19 -1
- package/dist/protocol.js +40 -1
- package/dist/quickstart.js +94 -13
- package/dist/reads.js +8 -1
- package/dist/relay.js +208 -35
- package/dist/repo-find.js +127 -0
- package/dist/repo-route.js +70 -0
- package/dist/roster-file.js +21 -2
- package/dist/run-failure.js +16 -0
- package/dist/service.js +7 -1
- package/dist/setup-guide.js +70 -29
- package/dist/tray.js +55 -4
- package/dist/untrusted.js +24 -5
- package/dist/upgrade.js +9 -5
- package/dist/verify-act.js +102 -0
- package/dist/worker-fleet.js +30 -9
- package/dist/worker-policy.js +96 -6
- package/dist/worker.js +1059 -93
- package/helper/macos/menubar.swift +245 -105
- package/package.json +1 -1
|
@@ -26,7 +26,11 @@ guard argv.count >= 2 else {
|
|
|
26
26
|
fputs("usage: pantheon-tray <path-to-pantheon> [poll-seconds]\n", stderr)
|
|
27
27
|
exit(64)
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
/// `pantheon-tray render <path-to-pantheon> <out.png>`: draw the board from tray-status into
|
|
30
|
+
/// a PNG and exit — the layout can be checked without anyone clicking the icon.
|
|
31
|
+
/// PANTHEON_TRAY_OPEN="handling,waiting,…" opens those counted lines in the picture.
|
|
32
|
+
let renderTo: String? = argv.count >= 4 && argv[1] == "render" ? argv[3] : nil
|
|
33
|
+
let pantheonBin = renderTo != nil ? argv[2] : argv[1]
|
|
30
34
|
let pollSeconds = argv.count >= 3 ? (Double(argv[2]) ?? 15) : 15
|
|
31
35
|
|
|
32
36
|
struct TrayState {
|
|
@@ -70,8 +74,21 @@ struct Item {
|
|
|
70
74
|
let space: String
|
|
71
75
|
/// whose human must decide this: this member for their own decision, the teammate for theirs, "" when it is not a decision
|
|
72
76
|
let decider: String
|
|
77
|
+
/// one-click answers for a question this machine asked its own person (may a teammate wake it, may runs use commands)
|
|
78
|
+
var choices: [(label: String, answer: String)] = []
|
|
79
|
+
/// where the board draws it (tray.ts arrangeBoard): now (a card), handling, waiting, older
|
|
80
|
+
var lane: String = ""
|
|
81
|
+
/// what else this card stands for — the teammate's request a decision is holding up
|
|
82
|
+
var context: String = ""
|
|
83
|
+
/// a session of this person's has claimed it (handling: an agent is on it, not just queued)
|
|
84
|
+
var claimed: Bool = false
|
|
73
85
|
|
|
74
86
|
var isDecision: Bool { kind == "decision" }
|
|
87
|
+
/// A tray-status from before lanes — or with a lane this helper does not know —
|
|
88
|
+
/// draws as it did before lanes: a decision or owed row is a card, a wait is not.
|
|
89
|
+
/// An unknown lane must never hide a row.
|
|
90
|
+
var effectiveLane: String { Item.lanes.contains(lane) ? lane : (kind == "waiting" ? "waiting" : "now") }
|
|
91
|
+
static let lanes: Set<String> = ["now", "handling", "waiting", "older"]
|
|
75
92
|
var heading: String {
|
|
76
93
|
switch kind {
|
|
77
94
|
case "decision": return "Needs your decision"
|
|
@@ -85,10 +102,17 @@ struct Item {
|
|
|
85
102
|
var parts: [String] = []
|
|
86
103
|
// Who is asking, on every kind. A decision row used to say only "11h ago",
|
|
87
104
|
// which is the one fact a reader could already guess.
|
|
88
|
-
|
|
105
|
+
// Who has it, by where the board put it: a card is on this person; a handled row is an agent's.
|
|
106
|
+
if kind == "owed" {
|
|
107
|
+
parts.append(effectiveLane == "handling" ? (claimed ? "your agent is on it" : "your worker picks it up") : "waiting on you")
|
|
108
|
+
} else if !who.isEmpty { parts.append(who) }
|
|
89
109
|
if when > 0 { parts.append(Item.age(when)) }
|
|
90
110
|
if showSpace && !space.isEmpty { parts.append(space) }
|
|
91
|
-
|
|
111
|
+
let head = parts.joined(separator: " · ")
|
|
112
|
+
// The request a decision is holding up, said once, after who and when.
|
|
113
|
+
guard !context.isEmpty else { return head }
|
|
114
|
+
let c = context.count > 70 ? String(context.prefix(69)) + "…" : context
|
|
115
|
+
return head + " · for: " + c
|
|
92
116
|
}
|
|
93
117
|
static func age(_ ms: Double) -> String {
|
|
94
118
|
let secs = max(0, Date().timeIntervalSince1970 - ms / 1000)
|
|
@@ -155,7 +179,14 @@ func fetchState() -> TrayState {
|
|
|
155
179
|
goal: i["goal"] as? String ?? "",
|
|
156
180
|
ref: i["ref"] as? String ?? "",
|
|
157
181
|
space: i["space"] as? String ?? "",
|
|
158
|
-
decider: i["decider"] as? String ?? ""
|
|
182
|
+
decider: i["decider"] as? String ?? "",
|
|
183
|
+
choices: (i["choices"] as? [[String: Any]] ?? []).compactMap { c in
|
|
184
|
+
guard let l = c["label"] as? String, let a = c["answer"] as? String else { return nil }
|
|
185
|
+
return (label: l, answer: a)
|
|
186
|
+
},
|
|
187
|
+
lane: i["lane"] as? String ?? "",
|
|
188
|
+
context: i["context"] as? String ?? "",
|
|
189
|
+
claimed: i["claimed"] as? Bool ?? false
|
|
159
190
|
))
|
|
160
191
|
}
|
|
161
192
|
for d in (j["decisions"] as? [[String: Any]] ?? []) {
|
|
@@ -180,6 +211,29 @@ func fetchState() -> TrayState {
|
|
|
180
211
|
* loop, and hit-testing two zones in one view is far more predictable there than
|
|
181
212
|
* nesting buttons inside an NSMenuItem.
|
|
182
213
|
*/
|
|
214
|
+
/// "Later": a card put away until tomorrow morning, on this machine only — a
|
|
215
|
+
/// view preference, never an answer. Kept in the menu bar's own defaults.
|
|
216
|
+
enum Later {
|
|
217
|
+
private static let key = "pantheon.later"
|
|
218
|
+
/// The ids put away and not yet due, with when they come back (seconds since 1970).
|
|
219
|
+
static func active() -> [String: Double] {
|
|
220
|
+
let now = Date().timeIntervalSince1970
|
|
221
|
+
let all = (UserDefaults.standard.dictionary(forKey: key) as? [String: Double]) ?? [:]
|
|
222
|
+
let live = all.filter { $0.value > now }
|
|
223
|
+
if live.count != all.count { UserDefaults.standard.set(live, forKey: key) }
|
|
224
|
+
return live
|
|
225
|
+
}
|
|
226
|
+
/// Back at 9:00 tomorrow, local time.
|
|
227
|
+
static func snooze(_ id: String) {
|
|
228
|
+
let cal = Calendar.current
|
|
229
|
+
let tomorrow = cal.date(byAdding: .day, value: 1, to: Date()) ?? Date().addingTimeInterval(86_400)
|
|
230
|
+
let at = cal.date(bySettingHour: 9, minute: 0, second: 0, of: tomorrow) ?? tomorrow
|
|
231
|
+
var all = active(); all[id] = at.timeIntervalSince1970
|
|
232
|
+
UserDefaults.standard.set(all, forKey: key)
|
|
233
|
+
}
|
|
234
|
+
static func bring(_ id: String) { var all = active(); all.removeValue(forKey: id); UserDefaults.standard.set(all, forKey: key) }
|
|
235
|
+
}
|
|
236
|
+
|
|
183
237
|
final class RowView: NSView {
|
|
184
238
|
private let item: Item
|
|
185
239
|
private let onOpen: (Item) -> Void
|
|
@@ -226,13 +280,13 @@ final class RowView: NSView {
|
|
|
226
280
|
return max(1, min(3, Int(ceil(h / lineHeight))))
|
|
227
281
|
}
|
|
228
282
|
|
|
229
|
-
init(item: Item, width: CGFloat, inTeamSection: Bool = false, onOpen: @escaping (Item) -> Void, onCopy: @escaping (Item) -> Void) {
|
|
283
|
+
init(item: Item, width: CGFloat, inTeamSection: Bool = false, showSpace: Bool? = nil, onOpen: @escaping (Item) -> Void, onCopy: @escaping (Item) -> Void) {
|
|
230
284
|
self.item = item
|
|
231
285
|
self.onOpen = onOpen
|
|
232
286
|
self.onCopy = onCopy
|
|
233
287
|
self.leading = inTeamSection ? 26 : 12
|
|
234
288
|
self.dot = inTeamSection && item.isDecision
|
|
235
|
-
self.sub = item.subtitle(showSpace: !inTeamSection)
|
|
289
|
+
self.sub = item.subtitle(showSpace: showSpace ?? !inTeamSection)
|
|
236
290
|
self.titleLines = RowView.lines(for: item.title, width: (RowView.knownWidth ?? width) - RowView.textInsets - (leading - 12))
|
|
237
291
|
let textHeight = CGFloat(titleLines) * RowView.lineHeight
|
|
238
292
|
super.init(frame: NSRect(x: 0, y: 0, width: width, height: textHeight + (sub.isEmpty ? 13 : 29)))
|
|
@@ -463,6 +517,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
463
517
|
func applicationDidFinishLaunching(_ note: Notification) {
|
|
464
518
|
NSApp.setActivationPolicy(.accessory)
|
|
465
519
|
item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
520
|
+
// (After the item exists: the board reads its button for placement.)
|
|
521
|
+
if let out = renderTo { renderBoard(to: out); exit(0) }
|
|
466
522
|
let menu = NSMenu()
|
|
467
523
|
menu.delegate = self
|
|
468
524
|
classicMenu = menu
|
|
@@ -513,12 +569,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
513
569
|
|
|
514
570
|
private var classicMenu: NSMenu?
|
|
515
571
|
private var boardPanel: BoardPanel?
|
|
572
|
+
/// which of the board's counted lines are open ("handling", "waiting", "older", "later", "more")
|
|
573
|
+
private var boardOpen: Set<String> = []
|
|
516
574
|
private var boardSignature = ""
|
|
517
575
|
private var boardMonitor: Any?
|
|
518
576
|
static let boardWidth: CGFloat = 470
|
|
519
577
|
|
|
578
|
+
/// The cards: what needs this person now, less what they put away for later.
|
|
579
|
+
/// The badge, the tooltip and both boards' headers all count these, so they agree.
|
|
580
|
+
private var boardCards: [Item] {
|
|
581
|
+
let later = Later.active()
|
|
582
|
+
return state.items.filter { $0.effectiveLane == "now" && later[$0.id] == nil }
|
|
583
|
+
}
|
|
584
|
+
|
|
520
585
|
private func signature() -> String {
|
|
521
|
-
|
|
586
|
+
// Everything the board draws from, not just which ids: a row moving lane (out
|
|
587
|
+
// of the worker's window), a new repeat, a claim, or a Later coming due all
|
|
588
|
+
// change what an open board must show.
|
|
589
|
+
"\(state.items.map { "\($0.id):\($0.effectiveLane):\($0.claimed):\($0.context)" }.joined(separator: ","))|\(Later.active().keys.sorted().joined(separator: ","))|\(state.decisions.count)|\(state.online)|\(state.members)|\(state.workerRunning)|\(state.relayOk)|\(actionError ?? "")|\(workerAction ?? "")"
|
|
522
590
|
}
|
|
523
591
|
|
|
524
592
|
@objc private func iconClicked(_ sender: Any?) {
|
|
@@ -551,6 +619,35 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
551
619
|
item.button?.highlight(false)
|
|
552
620
|
}
|
|
553
621
|
|
|
622
|
+
/// One card as a row of the classic menu.
|
|
623
|
+
private func menuRow(_ it: Item, showSpace: Bool) -> NSMenuItem {
|
|
624
|
+
let mi = NSMenuItem(title: it.title, action: nil, keyEquivalent: "")
|
|
625
|
+
mi.toolTip = it.detail
|
|
626
|
+
mi.view = RowView(item: it, width: RowView.rowWidth, showSpace: showSpace,
|
|
627
|
+
onOpen: { [weak self] x in self?.showDetail(x) },
|
|
628
|
+
onCopy: { [weak self] x in self?.copyPrompt(x) })
|
|
629
|
+
return mi
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/// Open or close one of the board's counted lines, and redraw.
|
|
633
|
+
private func toggleBoardLine(_ key: String) {
|
|
634
|
+
if boardOpen.contains(key) { boardOpen.remove(key) } else { boardOpen.insert(key) }
|
|
635
|
+
rebuildBoard()
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/// The board as it would open now, drawn into a PNG (render mode, above).
|
|
639
|
+
func renderBoard(to path: String) {
|
|
640
|
+
state = fetchState()
|
|
641
|
+
hasLoaded = true
|
|
642
|
+
boardOpen = Set((ProcessInfo.processInfo.environment["PANTHEON_TRAY_OPEN"] ?? "").split(separator: ",").map(String.init))
|
|
643
|
+
let panel = makeBoardPanel()
|
|
644
|
+
boardPanel = panel
|
|
645
|
+
rebuildBoard()
|
|
646
|
+
guard let view = panel.contentView, let rep = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { return }
|
|
647
|
+
view.cacheDisplay(in: view.bounds, to: rep)
|
|
648
|
+
try? rep.representation(using: .png, properties: [:])?.write(to: URL(fileURLWithPath: path))
|
|
649
|
+
}
|
|
650
|
+
|
|
554
651
|
private func makeBoardPanel() -> BoardPanel {
|
|
555
652
|
let panel = BoardPanel(contentRect: NSRect(x: 0, y: 0, width: AppDelegate.boardWidth, height: 200),
|
|
556
653
|
styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false)
|
|
@@ -589,52 +686,55 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
589
686
|
let sep = { rows.append(BoardSeparator(width: w)) }
|
|
590
687
|
let openRow: (Item) -> Void = { [weak self] it in self?.closeBoard(); self?.showDetail(it) }
|
|
591
688
|
let copyRow: (Item) -> Void = { [weak self] it in self?.copyPrompt(it) }
|
|
592
|
-
let section = { (title: String, detail: String, items: [Item], team: Bool) in
|
|
593
|
-
guard !items.isEmpty else { return }
|
|
594
|
-
rows.append(HeaderView(lead: title, rest: detail, width: w, bright: team))
|
|
595
|
-
for it in items.prefix(team ? 8 : 6) { rows.append(RowView(item: it, width: w, inTeamSection: team, onOpen: openRow, onCopy: copyRow)) }
|
|
596
|
-
if items.count > (team ? 8 : 6) { rows.append(ActionRow("…and \(items.count - (team ? 8 : 6)) more", width: w)) }
|
|
597
|
-
}
|
|
598
|
-
|
|
599
689
|
if !hasLoaded {
|
|
600
690
|
note("Checking Pantheon…")
|
|
601
691
|
} else if !state.reachable {
|
|
602
|
-
note("Unable to check Pantheon status"); note("
|
|
692
|
+
note("Unable to check Pantheon status"); note("Open More below: Refresh now, or Check connection")
|
|
603
693
|
} else if !state.configured {
|
|
604
694
|
note("Connect with your team"); note("Run pantheon start, or join with an invite link")
|
|
605
695
|
} else {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
let
|
|
610
|
-
let
|
|
611
|
-
let
|
|
696
|
+
// One board, no noise (tray.ts arrangeBoard): every card is something this
|
|
697
|
+
// person has to do, each once, across every team; the rest is one line each,
|
|
698
|
+
// opened on demand. No cap — nothing that needs them is hidden.
|
|
699
|
+
let multi = state.spaces.count > 1
|
|
700
|
+
let later = Later.active()
|
|
701
|
+
let cards = boardCards
|
|
702
|
+
let putAway = state.items.filter { $0.effectiveLane == "now" && later[$0.id] != nil }
|
|
703
|
+
let handling = state.items.filter { $0.effectiveLane == "handling" }
|
|
704
|
+
let waiting = state.items.filter { $0.effectiveLane == "waiting" }
|
|
705
|
+
let older = state.items.filter { $0.effectiveLane == "older" }
|
|
706
|
+
let need = cards.isEmpty ? "Nothing needs you" : "\(cards.count) need\(cards.count == 1 ? "s" : "") you"
|
|
707
|
+
let where_ = multi ? "\(state.spaces.count) teams" : state.space
|
|
612
708
|
rows.append(HeaderView(lead: need, rest: "\(state.member) · \(where_) · " + (state.relayOk ? "\(state.online) of \(state.members) online" : "connection unavailable"), width: w))
|
|
613
709
|
if !state.relayOk { note("Could not reach: \(state.unavailableSpaces.joined(separator: ", "))") }
|
|
614
710
|
if !state.decisionsOk { note("Some of this could not be checked — the list may be incomplete") }
|
|
615
711
|
sep()
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
712
|
+
for it in cards { rows.append(RowView(item: it, width: w, showSpace: multi, onOpen: openRow, onCopy: copyRow)) }
|
|
713
|
+
// The heading already says nothing needs you: said again only what it does not.
|
|
714
|
+
if cards.isEmpty && state.decisionsOk && handling.isEmpty && waiting.isEmpty && older.isEmpty && putAway.isEmpty {
|
|
715
|
+
note("All clear — nothing open for your agents either")
|
|
716
|
+
}
|
|
717
|
+
// The rest, counted: a line each, opened in place. (A plain loop: a local
|
|
718
|
+
// closure capturing both `rows` and a weak self crashed the optimised build.)
|
|
719
|
+
let lanes: [(key: String, title: String, items: [Item])] = [
|
|
720
|
+
("handling", "Your agents are handling · \(handling.count)", handling),
|
|
721
|
+
("waiting", "Waiting on teammates · \(waiting.count)", waiting),
|
|
722
|
+
("older", "Older than a week · \(older.count)", older),
|
|
723
|
+
("later", "Later · \(putAway.count)", putAway),
|
|
724
|
+
]
|
|
725
|
+
if lanes.contains(where: { !$0.items.isEmpty }) { sep() }
|
|
726
|
+
for lane in lanes where !lane.items.isEmpty {
|
|
727
|
+
let open = boardOpen.contains(lane.key)
|
|
728
|
+
let key = lane.key
|
|
729
|
+
rows.append(ActionRow(lane.title, hint: open ? "hide" : "show", width: w, onClick: { [weak self] in self?.toggleBoardLine(key) }))
|
|
730
|
+
// Indented under their line, so an opened lane never reads as more cards.
|
|
731
|
+
if open { for it in lane.items { rows.append(RowView(item: it, width: w, inTeamSection: true, showSpace: multi, onOpen: openRow, onCopy: copyRow)) } }
|
|
634
732
|
}
|
|
635
|
-
|
|
733
|
+
// The prompt it hands over asks about every question waiting on this person,
|
|
734
|
+
// put away or not, so the count is of all of them.
|
|
735
|
+
if state.decisions.count > 1 {
|
|
636
736
|
sep()
|
|
637
|
-
act(
|
|
737
|
+
act("Review all \(state.decisions.count) questions in \(state.surfaceName)", "") { [weak self] in self?.openAllDecisions() }
|
|
638
738
|
}
|
|
639
739
|
sep()
|
|
640
740
|
if let action = workerAction { note(action) }
|
|
@@ -644,12 +744,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
644
744
|
}
|
|
645
745
|
if let error = actionError { sep(); note(error) }
|
|
646
746
|
sep()
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
747
|
+
// The terminal tools, out of the way until asked for.
|
|
748
|
+
let moreOpen = boardOpen.contains("more")
|
|
749
|
+
rows.append(ActionRow("More", hint: moreOpen ? "hide" : "version \(state.version)", width: w, onClick: { [weak self] in self?.toggleBoardLine("more") }))
|
|
750
|
+
if moreOpen {
|
|
751
|
+
act("Open status in Terminal", "") { [weak self] in self?.openStatus() }
|
|
752
|
+
act("Check connection in Terminal", "version \(state.version)") { [weak self] in self?.openDoctor() }
|
|
753
|
+
act("View decision details in Terminal", "") { [weak self] in self?.openApprovals() }
|
|
754
|
+
act("Refresh now", "") { [weak self] in self?.refresh() }
|
|
755
|
+
act("Quit pantheon menu bar", "") { [weak self] in self?.quit() }
|
|
756
|
+
}
|
|
653
757
|
|
|
654
758
|
// Lay the rows out top to bottom at their own heights, in a card no taller than the screen.
|
|
655
759
|
let total = rows.reduce(CGFloat(16)) { $0 + $1.frame.height }
|
|
@@ -722,9 +826,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
722
826
|
let mode: MarkMode = !state.configured || !state.relayOk ? .off : state.workerRunning ? .full : .idle
|
|
723
827
|
item.button?.image = markImage(mode)
|
|
724
828
|
let unknown = hasLoaded && (!state.reachable || (state.configured && !state.decisionsOk))
|
|
725
|
-
|
|
829
|
+
let need = hasLoaded ? boardCards.count : 0
|
|
830
|
+
item.button?.title = actionError != nil ? " !" : need == 0 ? (unknown ? " !" : "") : " \(need)\(unknown ? "+" : "")"
|
|
726
831
|
item.button?.imagePosition = .imageLeading
|
|
727
|
-
let summary = !hasLoaded ? "Checking status…" : !state.reachable ? "Unable to check status" : !state.configured ? "Not joined to a team yet" : !state.decisionsOk ? "Unable to check every decision" :
|
|
832
|
+
let summary = !hasLoaded ? "Checking status…" : !state.reachable ? "Unable to check status" : !state.configured ? "Not joined to a team yet" : !state.decisionsOk ? "Unable to check every decision" : need == 0 ? "Nothing needs you" : "\(need) need\(need == 1 ? "s" : "") you"
|
|
728
833
|
item.button?.toolTip = "Pantheon — " + (actionError ?? summary)
|
|
729
834
|
item.button?.setAccessibilityLabel("Pantheon — " + (actionError ?? summary))
|
|
730
835
|
}
|
|
@@ -749,65 +854,46 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
749
854
|
info("Connect with your team")
|
|
750
855
|
info("Run pantheon start, or join with an invite link")
|
|
751
856
|
} else {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
let
|
|
755
|
-
let
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
if index > 0 { menu.addItem(.separator()) }
|
|
770
|
-
let rows = state.items.filter { $0.space == team }
|
|
771
|
-
let ordered = rows.filter { $0.kind == "decision" } + rows.filter { $0.kind == "owed" } + rows.filter { $0.kind == "waiting" }
|
|
772
|
-
if state.unavailableSpaces.contains(team) {
|
|
773
|
-
let h = NSMenuItem(title: team, action: nil, keyEquivalent: ""); h.isEnabled = false
|
|
774
|
-
h.view = HeaderView(lead: team, rest: "could not be checked", width: RowView.rowWidth, bright: false); menu.addItem(h)
|
|
775
|
-
} else if ordered.isEmpty {
|
|
776
|
-
let h = NSMenuItem(title: team, action: nil, keyEquivalent: ""); h.isEnabled = false
|
|
777
|
-
h.view = HeaderView(lead: team, rest: "nothing open", width: RowView.rowWidth, bright: false); menu.addItem(h)
|
|
778
|
-
} else {
|
|
779
|
-
addRows(menu, team, ordered, team: true, detail: counts(ordered))
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
if !decisions.isEmpty {
|
|
783
|
-
menu.addItem(.separator())
|
|
784
|
-
menu.addItem(makeItem(decisions.count > 1 ? "Review all \(decisions.count) in \(state.surfaceName)" : "Review it in \(state.surfaceName)", #selector(openAllDecisions)))
|
|
785
|
-
}
|
|
786
|
-
} else {
|
|
787
|
-
info("\(state.member) · \(state.space)")
|
|
788
|
-
info(state.relayOk
|
|
789
|
-
? "Connected · \(state.online) of \(state.members) members online"
|
|
790
|
-
: "Connection unavailable · \(state.unavailableSpaces.joined(separator: ", "))")
|
|
857
|
+
// The same board as the panel (rebuildBoard), as a menu: the cards, then the
|
|
858
|
+
// rest as a submenu each.
|
|
859
|
+
let multi = state.spaces.count > 1
|
|
860
|
+
let later = Later.active()
|
|
861
|
+
let cards = boardCards
|
|
862
|
+
let putAway = state.items.filter { $0.effectiveLane == "now" && later[$0.id] != nil }
|
|
863
|
+
let handling = state.items.filter { $0.effectiveLane == "handling" }
|
|
864
|
+
let waiting = state.items.filter { $0.effectiveLane == "waiting" }
|
|
865
|
+
let older = state.items.filter { $0.effectiveLane == "older" }
|
|
866
|
+
let summary = NSMenuItem(title: state.member, action: nil, keyEquivalent: "")
|
|
867
|
+
summary.isEnabled = false
|
|
868
|
+
summary.view = HeaderView(lead: cards.isEmpty ? "Nothing needs you" : "\(cards.count) need\(cards.count == 1 ? "s" : "") you",
|
|
869
|
+
rest: "\(state.member) · " + (multi ? "\(state.spaces.count) teams" : state.space) + " · " + (state.relayOk ? "\(state.online) of \(state.members) online" : "connection unavailable"),
|
|
870
|
+
width: RowView.rowWidth)
|
|
871
|
+
menu.addItem(summary)
|
|
872
|
+
if !state.relayOk { info("Could not reach: \(state.unavailableSpaces.joined(separator: ", "))") }
|
|
873
|
+
if !state.decisionsOk { info("Some of this could not be checked — the list may be incomplete") }
|
|
791
874
|
menu.addItem(.separator())
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
875
|
+
let row = { (it: Item) -> NSMenuItem in self.menuRow(it, showSpace: multi) }
|
|
876
|
+
for it in cards { menu.addItem(row(it)) }
|
|
877
|
+
if cards.isEmpty && state.decisionsOk && handling.isEmpty && waiting.isEmpty && older.isEmpty && putAway.isEmpty {
|
|
878
|
+
info("All clear — nothing open for your agents either")
|
|
795
879
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
}
|
|
880
|
+
let lanes: [(String, [Item])] = [
|
|
881
|
+
("Your agents are handling · \(handling.count)", handling),
|
|
882
|
+
("Waiting on teammates · \(waiting.count)", waiting),
|
|
883
|
+
("Older than a week · \(older.count)", older),
|
|
884
|
+
("Later · \(putAway.count)", putAway),
|
|
885
|
+
].filter { !$0.1.isEmpty }
|
|
886
|
+
if !lanes.isEmpty { menu.addItem(.separator()) }
|
|
887
|
+
for (title, items) in lanes {
|
|
888
|
+
let mi = NSMenuItem(title: title, action: nil, keyEquivalent: "")
|
|
889
|
+
let sub = NSMenu()
|
|
890
|
+
for it in items { sub.addItem(row(it)) }
|
|
891
|
+
mi.submenu = sub
|
|
892
|
+
menu.addItem(mi)
|
|
810
893
|
}
|
|
894
|
+
if state.decisions.count > 1 {
|
|
895
|
+
menu.addItem(.separator())
|
|
896
|
+
menu.addItem(makeItem("Review all \(state.decisions.count) questions in \(state.surfaceName)", #selector(openAllDecisions)))
|
|
811
897
|
}
|
|
812
898
|
menu.addItem(.separator())
|
|
813
899
|
info(state.workerRunning ? "Unattended work is on" : state.workerInstalled ? "Unattended work is paused" : "Unattended work is not set up")
|
|
@@ -1189,6 +1275,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
1189
1275
|
rows.spacing = 8
|
|
1190
1276
|
rows.translatesAutoresizingMaskIntoConstraints = false
|
|
1191
1277
|
|
|
1278
|
+
// A question this machine asked its own person: answered in one click, here.
|
|
1279
|
+
if item.isDecision && !item.choices.isEmpty {
|
|
1280
|
+
let row0 = NSStackView()
|
|
1281
|
+
row0.orientation = .horizontal
|
|
1282
|
+
row0.spacing = 10
|
|
1283
|
+
for (n, c) in item.choices.enumerated() {
|
|
1284
|
+
let b = button(c.label, #selector(panelChoose(_:)), "Answers it on this machine: \(c.answer)")
|
|
1285
|
+
b.tag = n
|
|
1286
|
+
// No Return key here: a habitual Enter must never grant "Always". A
|
|
1287
|
+
// permission is a click on the words that give it.
|
|
1288
|
+
b.keyEquivalent = ""
|
|
1289
|
+
row0.addArrangedSubview(b)
|
|
1290
|
+
}
|
|
1291
|
+
rows.addArrangedSubview(row0)
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1192
1294
|
let row1 = NSStackView()
|
|
1193
1295
|
row1.orientation = .horizontal
|
|
1194
1296
|
row1.spacing = 10
|
|
@@ -1197,6 +1299,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
1197
1299
|
if item.isDecision {
|
|
1198
1300
|
row1.addArrangedSubview(button("Mark done", #selector(panelDone), "Tells the agent you settled this in person, so it stops asking."))
|
|
1199
1301
|
}
|
|
1302
|
+
// Put away until tomorrow morning, on this machine — nothing is sent.
|
|
1303
|
+
if item.effectiveLane == "now" {
|
|
1304
|
+
let away = Later.active()[item.id] != nil
|
|
1305
|
+
row1.addArrangedSubview(button(away ? "Bring back" : "Later", #selector(panelLater),
|
|
1306
|
+
away ? "Puts it back on the board now." : "Puts it away until 9:00 tomorrow, on this machine only. Nothing is sent; it is still owed."))
|
|
1307
|
+
}
|
|
1200
1308
|
row1.addArrangedSubview(button("Show the thread", #selector(panelThread), "Every act in this goal, in Terminal, from the team's folder."))
|
|
1201
1309
|
rows.addArrangedSubview(row1)
|
|
1202
1310
|
|
|
@@ -1319,6 +1427,31 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
1319
1427
|
switch title { case "Answer": return "ANSWER"; case "Ask back": return "ASK"; default: return "NOTE" }
|
|
1320
1428
|
}
|
|
1321
1429
|
|
|
1430
|
+
/// One of the question's own answers, sent as its ANSWER — the same local path
|
|
1431
|
+
/// a typed reply takes, so a grant it carries takes effect on this machine.
|
|
1432
|
+
@objc private func panelChoose(_ sender: NSButton) {
|
|
1433
|
+
guard let item = panelItem, sender.tag >= 0, sender.tag < item.choices.count else { return }
|
|
1434
|
+
let c = item.choices[sender.tag]
|
|
1435
|
+
var payload: [String: Any] = ["action": "reply", "kind": "ANSWER", "body": c.answer, "goal": item.goal, "space": item.space, "decision": item.id]
|
|
1436
|
+
if !item.ref.isEmpty { payload["act"] = item.ref }
|
|
1437
|
+
guard writeAction(payload) else { setStatus("Could not write the action. Use Show the thread instead."); return }
|
|
1438
|
+
setStatus("Answering…")
|
|
1439
|
+
sender.isEnabled = false
|
|
1440
|
+
DispatchQueue.global().async {
|
|
1441
|
+
let r = run(pantheonBin, ["explain", "--queued"], timeout: 25)
|
|
1442
|
+
DispatchQueue.main.async {
|
|
1443
|
+
sender.isEnabled = true
|
|
1444
|
+
let out = (r?.out ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1445
|
+
if r?.code == 0 {
|
|
1446
|
+
self.setStatus("\(c.label) — done.")
|
|
1447
|
+
self.refresh()
|
|
1448
|
+
} else {
|
|
1449
|
+
self.setStatus(out.hasPrefix("not sent: ") ? String(out.dropFirst("not sent: ".count)) : "the relay could not be reached")
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1322
1455
|
/// Send the words in the box as the chosen act. The CLI does the sending and
|
|
1323
1456
|
/// prints "sent KIND id=… seq=…" or "not sent: why" on stdout; both are shown.
|
|
1324
1457
|
@objc private func panelSend(_ sender: Any?) {
|
|
@@ -1394,6 +1527,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
1394
1527
|
showThread(item)
|
|
1395
1528
|
}
|
|
1396
1529
|
/// Answer the decision as handled, with no terminal — the same action the alert's Done takes.
|
|
1530
|
+
@objc private func panelLater() {
|
|
1531
|
+
guard let item = panelItem else { return }
|
|
1532
|
+
if Later.active()[item.id] != nil { Later.bring(item.id) } else { Later.snooze(item.id) }
|
|
1533
|
+
detailPanel?.close()
|
|
1534
|
+
if boardPanel?.isVisible == true { rebuildBoard() }
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1397
1537
|
@objc private func panelDone() {
|
|
1398
1538
|
guard let item = panelItem, item.isDecision else { return }
|
|
1399
1539
|
setStatus("Marking it done…")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@join-pantheon/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.1",
|
|
4
4
|
"description": "Let your coding agent collaborate directly with your teammates' agents — ask, agree an interface, and deliver, without a human relaying messages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|