@join-pantheon/cli 0.23.0 → 0.23.2
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/dist/cli.js +62 -14
- package/dist/client.js +8 -0
- package/dist/notice.js +157 -0
- package/dist/notifier.js +25 -14
- package/dist/quickstart.js +1 -0
- package/dist/relay.js +281 -186
- package/dist/tray.js +2 -2
- package/dist/worker.js +9 -11
- package/helper/macos/main.swift +231 -65
- package/package.json +1 -1
package/dist/tray.js
CHANGED
|
@@ -41,6 +41,7 @@ export function writeTrayCommands(home = CONFIG_DIR) {
|
|
|
41
41
|
["pantheon-doctor.command", `${JSON.stringify(bin)} doctor`],
|
|
42
42
|
["pantheon-approvals.command", `${JSON.stringify(bin)} approvals`],
|
|
43
43
|
["pantheon-explain.command", `${JSON.stringify(bin)} explain --queued`],
|
|
44
|
+
["pantheon-details.command", `${JSON.stringify(bin)} explain --queued`],
|
|
44
45
|
]) {
|
|
45
46
|
const file = join(dir, name);
|
|
46
47
|
const keep = name === "pantheon-explain.command" ? "" : `\necho\nread -s -k '?press any key to close '`;
|
|
@@ -74,8 +75,7 @@ async function getJson(url) {
|
|
|
74
75
|
clearTimeout(t);
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
|
-
export async function trayStatus(cfg = load()) {
|
|
78
|
-
const f = loadAll();
|
|
78
|
+
export async function trayStatus(cfg = load(), f = loadAll()) {
|
|
79
79
|
const ms = Object.values(f?.spaces ?? {});
|
|
80
80
|
if (f && ms.length > 1) {
|
|
81
81
|
const parts = await Promise.all(ms.map((m) => trayStatusOne(configFor(f, m, null))));
|
package/dist/worker.js
CHANGED
|
@@ -11,7 +11,9 @@ import { agentHookStates, repairHook } from "./notify.js";
|
|
|
11
11
|
import { fence, assess } from "./untrusted.js";
|
|
12
12
|
import { allowed, recordRun } from "./spend.js";
|
|
13
13
|
import { loadProfile, reservedDecisionsNote } from "./profile.js";
|
|
14
|
-
import { clip
|
|
14
|
+
import { clip } from "./brief.js";
|
|
15
|
+
import { composeWorker } from "./notice.js";
|
|
16
|
+
import { installedTools } from "./notifier.js";
|
|
15
17
|
import { loadPersonas, matchPersona, personaByName, personaMatches, effectivePolicy, personaBrief, describePersona, } from "./persona.js";
|
|
16
18
|
import { isContinuationFor, LIMITS, shouldDeferToHuman } from "./protocol.js";
|
|
17
19
|
const git = (cwd, ...args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
@@ -538,6 +540,7 @@ export class Worker {
|
|
|
538
540
|
}
|
|
539
541
|
const reserved = reservedDecisionsNote(loadProfile(this.cfg));
|
|
540
542
|
const brief = makeBrief(branch) + baseNote(baseRef, baseSha) + (reserved ? `\n\n${reserved}` : "");
|
|
543
|
+
const startedAt = Date.now();
|
|
541
544
|
writeFileSync(join(worktree, ".pantheon-task.md"), brief);
|
|
542
545
|
let command;
|
|
543
546
|
let args;
|
|
@@ -587,17 +590,10 @@ export class Worker {
|
|
|
587
590
|
at: Date.now(), goal: o.goal, obligation: o.id, ok,
|
|
588
591
|
triggeredBy, summary,
|
|
589
592
|
}, this.opts.home);
|
|
590
|
-
if (!ok
|
|
593
|
+
if (this.opts.notify && (!ok || loadProfile(this.cfg).notifications?.worker_done)) {
|
|
591
594
|
try {
|
|
592
|
-
const
|
|
593
|
-
this.opts.notify({
|
|
594
|
-
}
|
|
595
|
-
catch { }
|
|
596
|
-
}
|
|
597
|
-
else if (ok && this.opts.notify && loadProfile(this.cfg).notifications?.worker_done) {
|
|
598
|
-
try {
|
|
599
|
-
const n = workerDoneNotice({ ok, triggeredBy, summary });
|
|
600
|
-
this.opts.notify({ title: n.title, subtitle: n.subtitle, body: n.body, eyebrow: "Pantheon · worker finished" });
|
|
595
|
+
const reply = client.mineSince(startedAt, o.goal).pop() ?? null;
|
|
596
|
+
this.opts.notify(composeWorker({ ok, triggeredBy, obligationBody: o.body, goal: o.goal ? client.goals.get(o.goal) : undefined, reply, obligationRef: o.ref }, { tools: installedTools(), preferred: loadProfile(this.cfg).agent.primary_surface }));
|
|
601
597
|
}
|
|
602
598
|
catch { }
|
|
603
599
|
}
|
|
@@ -694,6 +690,8 @@ export class Worker {
|
|
|
694
690
|
this.opts.log(`! self-upgrade failed: ${e.message}`);
|
|
695
691
|
}
|
|
696
692
|
if (result.ok && !result.alreadyCurrent && result.to === result.from) {
|
|
693
|
+
delete policy.attempts.self_upgrade;
|
|
694
|
+
savePolicy(policy, home);
|
|
697
695
|
this.opts.log(`* self-upgrade: the registry does not serve ${relayPkg} yet (installed ${result.to}); will check again`);
|
|
698
696
|
return;
|
|
699
697
|
}
|
package/helper/macos/main.swift
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Pantheon
|
|
2
|
+
* Pantheon notification HUD.
|
|
3
3
|
*
|
|
4
4
|
* A small, unsigned AppKit panel instead of a Notification Center or
|
|
5
|
-
* AppleScript notification.
|
|
6
|
-
*
|
|
5
|
+
* AppleScript notification. It answers the four things a person needs in a
|
|
6
|
+
* glance — who, what happened, what to do, where — and offers three actions:
|
|
7
|
+
*
|
|
8
|
+
* Open in <tool> ▾ launches the chosen agent CLI with the question pre-filled
|
|
9
|
+
* (the choice becomes the default), through a .command file
|
|
10
|
+
* Terminal runs — no automation permission needed
|
|
11
|
+
* Show details prints the act and the decision in Terminal
|
|
12
|
+
* Dismiss / × closes
|
|
13
|
+
*
|
|
14
|
+
* It owns no state. Everything it needs arrives as one JSON document:
|
|
15
|
+
*
|
|
16
|
+
* pantheon-hud post --json '<notice json>'
|
|
17
|
+
*
|
|
18
|
+
* The older positional form is still accepted so a freshly upgraded CLI and a
|
|
19
|
+
* not-yet-rebuilt helper (or the reverse) keep working during an upgrade:
|
|
7
20
|
*
|
|
8
21
|
* pantheon-hud post <title> <body> <activate-bundle-id> [ready-prompt]
|
|
9
22
|
*/
|
|
@@ -12,73 +25,138 @@ import AppKit
|
|
|
12
25
|
let args = CommandLine.arguments
|
|
13
26
|
|
|
14
27
|
if args.count == 2, args[1] == "version" {
|
|
15
|
-
print("
|
|
28
|
+
print("2")
|
|
16
29
|
exit(0)
|
|
17
30
|
}
|
|
18
31
|
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
struct Tool: Decodable { let id: String; let label: String }
|
|
33
|
+
struct Primary: Decodable { let label: String; let `default`: String; let tools: [Tool]; let prompt: String }
|
|
34
|
+
struct Details: Decodable { let act: String?; let decision: String? }
|
|
35
|
+
struct Notice: Decodable {
|
|
36
|
+
let kind: String
|
|
37
|
+
let who: String
|
|
38
|
+
let avatar: String?
|
|
39
|
+
let heading: String
|
|
40
|
+
let context: String
|
|
41
|
+
let body: String
|
|
42
|
+
let ttl: Double
|
|
43
|
+
let sound: Bool
|
|
44
|
+
let primary: Primary?
|
|
45
|
+
let details: Details?
|
|
46
|
+
/** where to write the chosen action and which .command files to open — supplied by the CLI */
|
|
47
|
+
let bin_dir: String
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
var notice: Notice
|
|
51
|
+
var legacyBundle: String? = nil
|
|
52
|
+
var legacyPrompt = ""
|
|
53
|
+
|
|
54
|
+
if args.count >= 4, args[1] == "post", args[2] == "--json", let data = args[3].data(using: .utf8), let n = try? JSONDecoder().decode(Notice.self, from: data) {
|
|
55
|
+
notice = n
|
|
56
|
+
} else if args.count >= 5, args[1] == "post" {
|
|
57
|
+
// Legacy: <title> <body> <bundle> [prompt]. Shown in the new layout with a single "Open" action that activates the bundle.
|
|
58
|
+
let eyebrow = ProcessInfo.processInfo.environment["PANTHEON_HUD_EYEBROW"] ?? "Pantheon"
|
|
59
|
+
legacyBundle = args[4]
|
|
60
|
+
legacyPrompt = args.count >= 6 ? args[5] : ""
|
|
61
|
+
let json = """
|
|
62
|
+
{"kind":"legacy","who":\(jsonString(eyebrow)),"heading":\(jsonString(args[2])),"context":"","body":\(jsonString(args[3])),"ttl":20,"sound":false,"primary":null,"details":null,"bin_dir":""}
|
|
63
|
+
"""
|
|
64
|
+
notice = try! JSONDecoder().decode(Notice.self, from: json.data(using: .utf8)!)
|
|
65
|
+
} else {
|
|
66
|
+
fputs("usage: pantheon-hud post --json '<notice>' | pantheon-hud post <title> <body> <bundle-id> [prompt]\n", stderr)
|
|
21
67
|
exit(64)
|
|
22
68
|
}
|
|
23
69
|
|
|
24
|
-
|
|
25
|
-
let
|
|
26
|
-
let
|
|
27
|
-
|
|
70
|
+
func jsonString(_ s: String) -> String {
|
|
71
|
+
let d = try! JSONSerialization.data(withJSONObject: [s])
|
|
72
|
+
let arr = String(data: d, encoding: .utf8)!
|
|
73
|
+
return String(arr.dropFirst().dropLast())
|
|
74
|
+
}
|
|
28
75
|
|
|
29
76
|
private func label(_ text: String, font: NSFont, colour: NSColor, lines: Int) -> NSTextField {
|
|
30
77
|
let field = NSTextField(wrappingLabelWithString: text)
|
|
31
78
|
field.font = font
|
|
32
79
|
field.textColor = colour
|
|
33
80
|
field.maximumNumberOfLines = lines
|
|
34
|
-
field.lineBreakMode = .byTruncatingTail
|
|
81
|
+
field.lineBreakMode = lines > 1 ? .byWordWrapping : .byTruncatingTail
|
|
82
|
+
field.cell?.truncatesLastVisibleLine = true
|
|
35
83
|
field.translatesAutoresizingMaskIntoConstraints = false
|
|
84
|
+
field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
85
|
+
// Without this a wrapping label under Auto Layout lays out as one truncated line.
|
|
86
|
+
field.preferredMaxLayoutWidth = 420 - 32
|
|
36
87
|
return field
|
|
37
88
|
}
|
|
38
89
|
|
|
39
|
-
final class
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
90
|
+
final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
91
|
+
private var panel: NSPanel?
|
|
92
|
+
private var timer: DispatchWorkItem?
|
|
93
|
+
private var hovering = false
|
|
43
94
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if let
|
|
51
|
-
|
|
52
|
-
config.activates = true
|
|
53
|
-
NSWorkspace.shared.openApplication(at: url, configuration: config) { _, _ in
|
|
54
|
-
NSApp.terminate(nil)
|
|
55
|
-
}
|
|
56
|
-
} else {
|
|
57
|
-
NSApp.terminate(nil)
|
|
95
|
+
// MARK: actions
|
|
96
|
+
|
|
97
|
+
/// Write what was chosen for `pantheon explain --queued`, then open the .command that runs it in Terminal.
|
|
98
|
+
private func act(_ payload: [String: Any], command: String) {
|
|
99
|
+
guard !notice.bin_dir.isEmpty else { return }
|
|
100
|
+
let dir = URL(fileURLWithPath: notice.bin_dir)
|
|
101
|
+
if let data = try? JSONSerialization.data(withJSONObject: payload) {
|
|
102
|
+
try? data.write(to: dir.appendingPathComponent(".hud-action.json"), options: .atomic)
|
|
58
103
|
}
|
|
104
|
+
NSWorkspace.shared.open(dir.appendingPathComponent(command))
|
|
105
|
+
NSApp.terminate(nil)
|
|
59
106
|
}
|
|
60
|
-
}
|
|
61
107
|
|
|
62
|
-
|
|
63
|
-
|
|
108
|
+
@objc func openTool(_ sender: NSMenuItem) {
|
|
109
|
+
guard let p = notice.primary else { return }
|
|
110
|
+
let tool = (sender.representedObject as? String) ?? p.default
|
|
111
|
+
act(["action": "open", "tool": tool, "prompt": p.prompt, "decision": notice.details?.decision as Any, "act": notice.details?.act as Any].compactMapValues { $0 is NSNull ? nil : $0 }, command: "pantheon-explain.command")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@objc func openDefault(_ sender: Any?) {
|
|
115
|
+
guard let p = notice.primary else { return }
|
|
116
|
+
act(["action": "open", "tool": p.default, "prompt": p.prompt, "decision": notice.details?.decision as Any, "act": notice.details?.act as Any].compactMapValues { $0 is NSNull ? nil : $0 }, command: "pantheon-explain.command")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@objc func showDetails(_ sender: Any?) {
|
|
120
|
+
act(["action": "details", "act": notice.details?.act as Any, "decision": notice.details?.decision as Any].compactMapValues { $0 is NSNull ? nil : $0 }, command: "pantheon-details.command")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@objc func legacyOpen(_ sender: Any?) {
|
|
124
|
+
if !legacyPrompt.isEmpty { NSPasteboard.general.clearContents(); NSPasteboard.general.setString(legacyPrompt, forType: .string) }
|
|
125
|
+
if let b = legacyBundle, let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: b) {
|
|
126
|
+
let config = NSWorkspace.OpenConfiguration(); config.activates = true
|
|
127
|
+
NSWorkspace.shared.openApplication(at: url, configuration: config) { _, _ in NSApp.terminate(nil) }
|
|
128
|
+
} else { NSApp.terminate(nil) }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@objc func dismiss(_ sender: Any?) { NSApp.terminate(nil) }
|
|
132
|
+
|
|
133
|
+
// MARK: timing — a hover pauses the fade; ttl 0 stays until acted on
|
|
134
|
+
|
|
135
|
+
private func schedule() {
|
|
136
|
+
timer?.cancel()
|
|
137
|
+
guard notice.ttl > 0 else { return }
|
|
138
|
+
let item = DispatchWorkItem { [weak self] in if self?.hovering == false { NSApp.terminate(nil) } else { self?.schedule() } }
|
|
139
|
+
timer = item
|
|
140
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + notice.ttl, execute: item)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// MARK: layout
|
|
64
144
|
|
|
65
145
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
66
146
|
NSApp.setActivationPolicy(.accessory)
|
|
67
147
|
|
|
68
|
-
let width: CGFloat =
|
|
69
|
-
let
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
backing: .buffered,
|
|
74
|
-
defer: false
|
|
75
|
-
)
|
|
148
|
+
let width: CGFloat = 420
|
|
149
|
+
let hasContext = !notice.context.isEmpty
|
|
150
|
+
// Height follows the content: measured after the constraints are in place, so a
|
|
151
|
+
// five-line body gets five lines and a one-line body leaves no empty band.
|
|
152
|
+
let panel = NSPanel(contentRect: NSRect(x: 0, y: 0, width: width, height: 200), styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false)
|
|
76
153
|
panel.isOpaque = false
|
|
77
154
|
panel.backgroundColor = .clear
|
|
78
155
|
panel.hasShadow = true
|
|
79
156
|
panel.level = .floating
|
|
80
157
|
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .transient]
|
|
81
158
|
panel.hidesOnDeactivate = false
|
|
159
|
+
panel.isMovableByWindowBackground = true
|
|
82
160
|
|
|
83
161
|
let effect = NSVisualEffectView(frame: panel.contentView?.bounds ?? .zero)
|
|
84
162
|
effect.autoresizingMask = [.width, .height]
|
|
@@ -89,32 +167,109 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
89
167
|
effect.layer?.cornerRadius = 16
|
|
90
168
|
effect.layer?.masksToBounds = true
|
|
91
169
|
|
|
92
|
-
let root =
|
|
93
|
-
root.
|
|
170
|
+
let root = HoverView(frame: effect.bounds)
|
|
171
|
+
root.translatesAutoresizingMaskIntoConstraints = false
|
|
172
|
+
root.onHover = { [weak self] inside in self?.hovering = inside }
|
|
94
173
|
effect.addSubview(root)
|
|
95
174
|
|
|
96
|
-
let
|
|
97
|
-
let
|
|
98
|
-
let
|
|
99
|
-
let body = label(
|
|
100
|
-
|
|
101
|
-
let
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
175
|
+
let who = label(notice.who, font: .systemFont(ofSize: 12, weight: .medium), colour: .secondaryLabelColor, lines: 1)
|
|
176
|
+
let heading = label(notice.heading, font: .systemFont(ofSize: 16, weight: .semibold), colour: .labelColor, lines: 1)
|
|
177
|
+
let context = label(notice.context, font: .systemFont(ofSize: 12, weight: .medium), colour: .tertiaryLabelColor, lines: 1)
|
|
178
|
+
let body = label(notice.body, font: .systemFont(ofSize: 13), colour: .secondaryLabelColor, lines: 3)
|
|
179
|
+
|
|
180
|
+
let close = NSButton(title: "×", target: self, action: #selector(dismiss(_:)))
|
|
181
|
+
close.isBordered = false
|
|
182
|
+
close.font = .systemFont(ofSize: 15, weight: .medium)
|
|
183
|
+
close.contentTintColor = .tertiaryLabelColor
|
|
184
|
+
close.translatesAutoresizingMaskIntoConstraints = false
|
|
185
|
+
close.setAccessibilityLabel("Dismiss")
|
|
186
|
+
|
|
187
|
+
for view in [who, heading, body, close] { root.addSubview(view) }
|
|
188
|
+
if hasContext { root.addSubview(context) }
|
|
189
|
+
|
|
190
|
+
var constraints: [NSLayoutConstraint] = [
|
|
191
|
+
root.widthAnchor.constraint(equalToConstant: width),
|
|
192
|
+
root.leadingAnchor.constraint(equalTo: effect.leadingAnchor), root.trailingAnchor.constraint(equalTo: effect.trailingAnchor),
|
|
193
|
+
root.topAnchor.constraint(equalTo: effect.topAnchor), root.bottomAnchor.constraint(equalTo: effect.bottomAnchor),
|
|
194
|
+
who.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: 16),
|
|
195
|
+
who.topAnchor.constraint(equalTo: root.topAnchor, constant: 16),
|
|
196
|
+
who.trailingAnchor.constraint(lessThanOrEqualTo: close.leadingAnchor, constant: -8),
|
|
197
|
+
close.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -12),
|
|
198
|
+
close.topAnchor.constraint(equalTo: root.topAnchor, constant: 10),
|
|
199
|
+
heading.leadingAnchor.constraint(equalTo: who.leadingAnchor),
|
|
200
|
+
heading.topAnchor.constraint(equalTo: who.bottomAnchor, constant: 10),
|
|
201
|
+
heading.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -16),
|
|
202
|
+
body.leadingAnchor.constraint(equalTo: who.leadingAnchor),
|
|
203
|
+
body.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -16),
|
|
204
|
+
]
|
|
205
|
+
if hasContext {
|
|
206
|
+
constraints += [
|
|
207
|
+
context.leadingAnchor.constraint(equalTo: who.leadingAnchor),
|
|
208
|
+
context.topAnchor.constraint(equalTo: heading.bottomAnchor, constant: 2),
|
|
209
|
+
context.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -16),
|
|
210
|
+
body.topAnchor.constraint(equalTo: context.bottomAnchor, constant: 6),
|
|
211
|
+
]
|
|
212
|
+
} else {
|
|
213
|
+
constraints.append(body.topAnchor.constraint(equalTo: heading.bottomAnchor, constant: 6))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// actions
|
|
217
|
+
if let p = notice.primary {
|
|
218
|
+
let pull = NSPopUpButton(frame: .zero, pullsDown: true)
|
|
219
|
+
pull.translatesAutoresizingMaskIntoConstraints = false
|
|
220
|
+
pull.bezelStyle = .rounded
|
|
221
|
+
pull.font = .systemFont(ofSize: 12, weight: .semibold)
|
|
222
|
+
pull.addItem(withTitle: p.label) // the visible title of a pull-down is its first item
|
|
223
|
+
for t in p.tools {
|
|
224
|
+
let mi = NSMenuItem(title: "Open in \(t.label)\(t.id == p.default ? " (default)" : "")", action: #selector(openTool(_:)), keyEquivalent: "")
|
|
225
|
+
mi.target = self
|
|
226
|
+
mi.representedObject = t.id
|
|
227
|
+
pull.menu?.addItem(mi)
|
|
228
|
+
}
|
|
229
|
+
root.addSubview(pull)
|
|
230
|
+
constraints += [
|
|
231
|
+
pull.leadingAnchor.constraint(equalTo: who.leadingAnchor),
|
|
232
|
+
pull.topAnchor.constraint(equalTo: body.bottomAnchor, constant: 16),
|
|
233
|
+
pull.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -14),
|
|
234
|
+
pull.widthAnchor.constraint(greaterThanOrEqualToConstant: 170),
|
|
235
|
+
]
|
|
236
|
+
var trailing: NSView = pull
|
|
237
|
+
if notice.details != nil {
|
|
238
|
+
let d = NSButton(title: "Show details", target: self, action: #selector(showDetails(_:)))
|
|
239
|
+
d.isBordered = false; d.font = .systemFont(ofSize: 12, weight: .medium); d.contentTintColor = .controlAccentColor
|
|
240
|
+
d.translatesAutoresizingMaskIntoConstraints = false
|
|
241
|
+
root.addSubview(d)
|
|
242
|
+
constraints += [d.leadingAnchor.constraint(equalTo: pull.trailingAnchor, constant: 14), d.centerYAnchor.constraint(equalTo: pull.centerYAnchor)]
|
|
243
|
+
trailing = d
|
|
244
|
+
}
|
|
245
|
+
let dis = NSButton(title: "Dismiss", target: self, action: #selector(dismiss(_:)))
|
|
246
|
+
dis.isBordered = false; dis.font = .systemFont(ofSize: 12); dis.contentTintColor = .secondaryLabelColor
|
|
247
|
+
dis.translatesAutoresizingMaskIntoConstraints = false
|
|
248
|
+
root.addSubview(dis)
|
|
249
|
+
constraints += [dis.leadingAnchor.constraint(equalTo: trailing.trailingAnchor, constant: 14), dis.centerYAnchor.constraint(equalTo: pull.centerYAnchor)]
|
|
250
|
+
} else if notice.details != nil || legacyBundle != nil {
|
|
251
|
+
let main = NSButton(title: legacyBundle != nil ? "Open your agent" : "Show details", target: self, action: legacyBundle != nil ? #selector(legacyOpen(_:)) : #selector(showDetails(_:)))
|
|
252
|
+
main.bezelStyle = .rounded; main.font = .systemFont(ofSize: 12, weight: .semibold)
|
|
253
|
+
main.translatesAutoresizingMaskIntoConstraints = false
|
|
254
|
+
let dis = NSButton(title: "Dismiss", target: self, action: #selector(dismiss(_:)))
|
|
255
|
+
dis.isBordered = false; dis.font = .systemFont(ofSize: 12); dis.contentTintColor = .secondaryLabelColor
|
|
256
|
+
dis.translatesAutoresizingMaskIntoConstraints = false
|
|
257
|
+
root.addSubview(main); root.addSubview(dis)
|
|
258
|
+
constraints += [
|
|
259
|
+
main.leadingAnchor.constraint(equalTo: who.leadingAnchor), main.topAnchor.constraint(equalTo: body.bottomAnchor, constant: 16), main.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -14),
|
|
260
|
+
dis.leadingAnchor.constraint(equalTo: main.trailingAnchor, constant: 14), dis.centerYAnchor.constraint(equalTo: main.centerYAnchor),
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
if notice.primary == nil && notice.details == nil && legacyBundle == nil {
|
|
264
|
+
constraints.append(body.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -16))
|
|
265
|
+
}
|
|
266
|
+
NSLayoutConstraint.activate(constraints)
|
|
116
267
|
|
|
117
268
|
panel.contentView = effect
|
|
269
|
+
effect.layoutSubtreeIfNeeded()
|
|
270
|
+
let height = max(120, root.fittingSize.height)
|
|
271
|
+
panel.setContentSize(NSSize(width: width, height: height))
|
|
272
|
+
effect.frame = NSRect(x: 0, y: 0, width: width, height: height)
|
|
118
273
|
guard let screen = NSScreen.main ?? NSScreen.screens.first else { NSApp.terminate(nil); return }
|
|
119
274
|
let visible = screen.visibleFrame
|
|
120
275
|
let target = NSPoint(x: visible.maxX - width - 18, y: visible.maxY - height - 18)
|
|
@@ -126,11 +281,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
126
281
|
panel.animator().alphaValue = 1
|
|
127
282
|
panel.animator().setFrameOrigin(target)
|
|
128
283
|
}
|
|
129
|
-
NSSound(named: "Glass")?.play()
|
|
130
|
-
|
|
284
|
+
if notice.sound { NSSound(named: "Glass")?.play() }
|
|
131
285
|
self.panel = panel
|
|
132
|
-
|
|
286
|
+
schedule()
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/// Tracks the pointer so a notification being read does not fade under it.
|
|
291
|
+
final class HoverView: NSView {
|
|
292
|
+
var onHover: ((Bool) -> Void)?
|
|
293
|
+
override func updateTrackingAreas() {
|
|
294
|
+
super.updateTrackingAreas()
|
|
295
|
+
for a in trackingAreas { removeTrackingArea(a) }
|
|
296
|
+
addTrackingArea(NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], owner: self, userInfo: nil))
|
|
133
297
|
}
|
|
298
|
+
override func mouseEntered(with event: NSEvent) { onHover?(true) }
|
|
299
|
+
override func mouseExited(with event: NSEvent) { onHover?(false) }
|
|
134
300
|
}
|
|
135
301
|
|
|
136
302
|
let app = NSApplication.shared
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@join-pantheon/cli",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.2",
|
|
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": {
|