@join-pantheon/cli 0.25.0 → 0.25.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 +4 -0
- package/dist/admin-stats.js +11 -2
- package/dist/cli.js +18 -4
- package/dist/config.js +28 -0
- package/dist/doctor.js +19 -4
- package/dist/machine.js +12 -5
- package/dist/relay.js +43 -0
- package/dist/service.js +9 -1
- package/dist/upgrade.js +11 -1
- package/helper/macos/menubar.swift +15 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,6 +96,10 @@ any other server. The install output prints that exact line, the defaults screen
|
|
|
96
96
|
your agent sends in your name, `pantheon setup --ask-before-sending` (or the same flag on start/join)
|
|
97
97
|
makes reads silent and prompts for send, open goal and claim; `--approve-all` switches back.
|
|
98
98
|
|
|
99
|
+
If all you were sent is the code — `pantheon join iIF-uHY8…` with no team or link — that works
|
|
100
|
+
too: the relay names the team the code opens, and the join proceeds. A spent or expired code says
|
|
101
|
+
so and asks for a fresh invite; a relay that cannot be reached asks you for the whole line instead.
|
|
102
|
+
|
|
99
103
|
Already a member — new laptop, or just pulled a newer build? The same full setup is one command:
|
|
100
104
|
`pantheon install --user` (from your project's repo if you have one).
|
|
101
105
|
|
package/dist/admin-stats.js
CHANGED
|
@@ -125,6 +125,15 @@ export function versionBehind(pkg, relay) {
|
|
|
125
125
|
}
|
|
126
126
|
return false;
|
|
127
127
|
}
|
|
128
|
+
export const SELF_UPGRADE_SINCE = "0.24.0";
|
|
129
|
+
export function stuckBehind(pkg, relay) {
|
|
130
|
+
if (!versionBehind(pkg, relay))
|
|
131
|
+
return false;
|
|
132
|
+
if (versionBehind(pkg, SELF_UPGRADE_SINCE))
|
|
133
|
+
return true;
|
|
134
|
+
const [aMaj = 0, aMin = 0] = pkg.split(".").map(Number), [bMaj = 0, bMin = 0] = relay.split(".").map(Number);
|
|
135
|
+
return aMaj < bMaj || bMin - aMin >= 2;
|
|
136
|
+
}
|
|
128
137
|
const daysAgo = (ts, now) => Math.max(0, Math.floor((now - ts) / DAY));
|
|
129
138
|
export function spaceHealth(input) {
|
|
130
139
|
const now = input.now ?? Date.now();
|
|
@@ -153,10 +162,10 @@ export function spaceHealth(input) {
|
|
|
153
162
|
if (!m.last_pkg)
|
|
154
163
|
continue;
|
|
155
164
|
versions[m.last_pkg] = (versions[m.last_pkg] ?? 0) + 1;
|
|
156
|
-
if (
|
|
165
|
+
if (stuckBehind(m.last_pkg, input.relayPkg))
|
|
157
166
|
behind.push({ member: m.member, pkg: m.last_pkg, days: daysAgo(seenAt(m), now) });
|
|
158
167
|
}
|
|
159
|
-
behind.sort((a, b) => a.days - b.days);
|
|
168
|
+
behind.sort((a, b) => (versionBehind(a.pkg, b.pkg) ? -1 : versionBehind(b.pkg, a.pkg) ? 1 : a.days - b.days));
|
|
160
169
|
const newest = admitted.filter((m) => m.last_pkg).sort((a, b) => seenAt(b) - seenAt(a))[0];
|
|
161
170
|
const seenSays = last_seen === null ? "" : `; last seen ${daysAgo(last_seen, now)}d ago${newest ? ` on ${newest.last_pkg}` : ""}`;
|
|
162
171
|
let verdict, says;
|
package/dist/cli.js
CHANGED
|
@@ -10,10 +10,10 @@ import { randomBytes } from "node:crypto";
|
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { basename, dirname, join, resolve } from "node:path";
|
|
12
12
|
import { PantheonClient } from "./client.js";
|
|
13
|
-
import { fromInvite, load, save, toInvite, toJoinLink, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
|
|
13
|
+
import { fromInvite, looksLikeInviteCode, resolveInviteCode, load, save, toInvite, toJoinLink, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
|
|
14
14
|
import { fmtLimit } from "./plans.js";
|
|
15
15
|
import { validEmail, validUsername } from "./accounts.js";
|
|
16
|
-
import { install, launcher, removeCodexUserScope } from "./install.js";
|
|
16
|
+
import { install, launcher, installForUser, removeCodexUserScope } from "./install.js";
|
|
17
17
|
import { Relay } from "./relay.js";
|
|
18
18
|
import { runMcp } from "./mcp.js";
|
|
19
19
|
import { Worker } from "./worker.js";
|
|
@@ -728,7 +728,7 @@ async function main() {
|
|
|
728
728
|
return;
|
|
729
729
|
}
|
|
730
730
|
case "join": {
|
|
731
|
-
const invite = argv[1] ?? die("usage: pantheon join <invite> (pantheons://host/space#token,
|
|
731
|
+
const invite = argv[1] ?? die("usage: pantheon join <invite> (pantheons://host/space#token, https://www.joinpantheon.network/join/<team>#<token>, or just the code) [setup answers as flags — pantheon setup --questions]");
|
|
732
732
|
const setupFlags = parseSetupFlags(argv);
|
|
733
733
|
if (setupFlags.errors.length)
|
|
734
734
|
die(setupFlags.errors.join("\n") + "\n pantheon setup --questions lists every answer");
|
|
@@ -737,7 +737,11 @@ async function main() {
|
|
|
737
737
|
parsed = fromInvite(invite);
|
|
738
738
|
}
|
|
739
739
|
catch (e) {
|
|
740
|
-
|
|
740
|
+
if (!looksLikeInviteCode(invite))
|
|
741
|
+
die(e.message);
|
|
742
|
+
const candidates = [flag("relay"), process.env.PANTHEON_RELAY, load()?.relay, DEFAULT_RELAY].filter((r) => !!r);
|
|
743
|
+
parsed = await resolveInviteCode(invite, candidates).catch((err) => die(err.message.replace(/^INVITE_CODE_[A-Z]+:\s*/, "")));
|
|
744
|
+
console.error(D(`That was just the code — it opens "${parsed.space}" on ${new URL(parsed.relay).host}. Joining that team.\n`));
|
|
741
745
|
}
|
|
742
746
|
if (!load()?.account && sameHost(parsed.relay, DEFAULT_RELAY)) {
|
|
743
747
|
die(`sign in first — this relay verifies email.\n New here: pantheon signup --email you@work.com --username you\n Have an account: pantheon login --email you@work.com\n Then run this same \`pantheon join\` command again — the invite is still good.`);
|
|
@@ -811,6 +815,16 @@ async function main() {
|
|
|
811
815
|
console.error(`\n${B("Restart your agent")}, then ask it: "check pantheon_status and introduce yourself to the team".`);
|
|
812
816
|
return;
|
|
813
817
|
}
|
|
818
|
+
case "rewire": {
|
|
819
|
+
const rows = installForUser().map((t) => ({ tool: t.tool, status: t.status }));
|
|
820
|
+
if (argv.includes("--json")) {
|
|
821
|
+
console.log(JSON.stringify(rows));
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
for (const r of rows)
|
|
825
|
+
console.error(` ${r.tool.padEnd(14)}${r.status}`);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
814
828
|
case "upgrade": {
|
|
815
829
|
const { upgrade } = await import("./upgrade.js");
|
|
816
830
|
console.error(`${B("pantheon upgrade")}\n`);
|
package/dist/config.js
CHANGED
|
@@ -305,4 +305,32 @@ export function fromInvite(invite) {
|
|
|
305
305
|
token,
|
|
306
306
|
};
|
|
307
307
|
}
|
|
308
|
+
export const INVITE_CODE = /^[A-Za-z0-9_-]{16,64}$/;
|
|
309
|
+
export const looksLikeInviteCode = (s) => INVITE_CODE.test(s.trim().replace(/^['"`]|['"`]$/g, ""));
|
|
310
|
+
export async function resolveInviteCode(code, relays, fetchImpl = fetch) {
|
|
311
|
+
const token = code.trim().replace(/^['"`]|['"`]$/g, "");
|
|
312
|
+
const tried = [...new Set(relays.map((r) => r.replace(/\/+$/, "")).filter(Boolean))];
|
|
313
|
+
let answered = false;
|
|
314
|
+
for (const relay of tried) {
|
|
315
|
+
const http = relay.replace(/^ws/, "http");
|
|
316
|
+
try {
|
|
317
|
+
const res = await fetchImpl(`${http}/join?token=${encodeURIComponent(token)}`, { signal: AbortSignal.timeout(6_000) });
|
|
318
|
+
if (res.status === 404) {
|
|
319
|
+
answered = true;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (!res.ok)
|
|
323
|
+
continue;
|
|
324
|
+
const body = (await res.json());
|
|
325
|
+
if (typeof body.team === "string" && body.team)
|
|
326
|
+
return { relay, space: body.team, token };
|
|
327
|
+
}
|
|
328
|
+
catch { }
|
|
329
|
+
}
|
|
330
|
+
const hosts = tried.map((r) => new URL(r).host).join(", ");
|
|
331
|
+
if (answered) {
|
|
332
|
+
throw new Error(`INVITE_CODE_UNKNOWN: that is an invite code, but no open invite on ${hosts} matches it — it may have been used already or expired (they last 24 hours).\n Ask your teammate for a fresh one: pantheon invite`);
|
|
333
|
+
}
|
|
334
|
+
throw new Error(`INVITE_CODE_UNREACHABLE: that looks like an invite code, but ${hosts} could not be asked which team it opens.\n Paste the whole invite line instead: pantheon join https://www.joinpantheon.network/join/<team>#${token}`);
|
|
335
|
+
}
|
|
308
336
|
//# sourceMappingURL=config.js.map
|
package/dist/doctor.js
CHANGED
|
@@ -12,7 +12,7 @@ import { fingerprint, signChallenge, verifyChallenge } from "./identity.js";
|
|
|
12
12
|
import { approvalSummary, chosenApproval, onPath } from "./install.js";
|
|
13
13
|
import { hookState, readIntent, agentHookStates } from "./notify.js";
|
|
14
14
|
import { loadPersonas } from "./persona.js";
|
|
15
|
-
import { workerProc } from "./machine.js";
|
|
15
|
+
import { workerProc, menubarProc, notifierProc } from "./machine.js";
|
|
16
16
|
import { VERSION } from "./version.js";
|
|
17
17
|
import { serviceInstalled } from "./service.js";
|
|
18
18
|
import { hostEnv } from "./host-env.js";
|
|
@@ -213,9 +213,24 @@ export async function doctor(projectDir) {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
if (host.desktop) {
|
|
216
|
-
serviceInstalled("menubar")
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
if (!serviceInstalled("menubar")) {
|
|
217
|
+
add("warn", "menu bar", "not installed on a Mac that can run it", "install it with: pantheon service install menubar (needs the Xcode Command Line Tools — xcode-select --install — and a setup that ran on this computer, not in a cloud agent). --no-menubar at setup also leaves it off; that is fine if it was your choice", "not installed");
|
|
218
|
+
}
|
|
219
|
+
else if (!menubarProc().running) {
|
|
220
|
+
add("warn", "menu bar", "installed, but not running — launchd is not holding it, so there is no icon right now", "pantheon service restart menubar (loads it back; something unloaded it — a stop, a crash loop, or a test run on this machine)", "installed, not running");
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
add("pass", "menu bar", "installed and running — status, pause/resume, and a badge when a decision needs you", undefined, "installed and running");
|
|
224
|
+
}
|
|
225
|
+
if (!serviceInstalled("notifier")) {
|
|
226
|
+
add("warn", "alerts", "not installed — no desktop notification when a teammate needs you", "pantheon service install notifier (--no-notifications at setup also leaves it off; that is fine if it was your choice)", "not installed");
|
|
227
|
+
}
|
|
228
|
+
else if (!notifierProc().running) {
|
|
229
|
+
add("warn", "alerts", "installed, but not running — nothing on this Mac will show you a teammate's message", "pantheon service restart notifier (loads it back; something unloaded it — a stop, a crash loop, or a test run on this machine)", "installed, not running");
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
add("pass", "alerts", "installed and running — a desktop notification when a teammate needs you", undefined, "installed and running");
|
|
233
|
+
}
|
|
219
234
|
}
|
|
220
235
|
if (!hookStates.length && !hook.installed) {
|
|
221
236
|
add("warn", "wake-up hook", "not installed — your agent will not notice a peer's message until you ask it to", "pantheon hook install --user (auto-detects claude / codex / cursor on PATH)", "not installed");
|
package/dist/machine.js
CHANGED
|
@@ -134,22 +134,29 @@ export function dataSummary(dir) {
|
|
|
134
134
|
const spaces = files.split("\n").length;
|
|
135
135
|
return `${acts} acts across ${spaces} space${spaces > 1 ? "s" : ""}`;
|
|
136
136
|
}
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
const defaultStopIo = () => ({
|
|
138
|
+
proc: (kind) => kind === "relay" ? relayProc(8787) : kind === "worker" ? workerProc() : kind === "menubar" ? menubarProc() : notifierProc(),
|
|
139
|
+
installed: (kind) => existsSync(plistPath(kind)),
|
|
140
|
+
sh,
|
|
141
|
+
});
|
|
142
|
+
export function stopProc(kind, io = defaultStopIo()) {
|
|
143
|
+
const p = io.proc(kind);
|
|
139
144
|
if (!p.running)
|
|
140
145
|
return [`${kind}: already stopped`];
|
|
141
146
|
if (p.managedBy === "launchd") {
|
|
147
|
+
if (!io.installed(kind))
|
|
148
|
+
return [`${kind}: running under launchd, but not installed from this home — left alone`];
|
|
142
149
|
const plist = plistPath(kind);
|
|
143
150
|
const uid = process.getuid?.() ?? 0;
|
|
144
|
-
if (sh(`launchctl bootout gui/${uid}/dev.pantheon.${kind} 2>&1`) !== null ||
|
|
145
|
-
sh(`launchctl unload -w "${plist}" 2>&1`) !== null) {
|
|
151
|
+
if (io.sh(`launchctl bootout gui/${uid}/dev.pantheon.${kind} 2>&1`) !== null ||
|
|
152
|
+
io.sh(`launchctl unload -w "${plist}" 2>&1`) !== null) {
|
|
146
153
|
return [`${kind}: stopped, and will not restart at login`,
|
|
147
154
|
` start it again with: pantheon service install ${kind}`];
|
|
148
155
|
}
|
|
149
156
|
return [`${kind}: could not stop the service — try: launchctl bootout gui/${uid}/dev.pantheon.${kind}`];
|
|
150
157
|
}
|
|
151
158
|
if (p.pid) {
|
|
152
|
-
sh(`kill ${p.pid}`);
|
|
159
|
+
io.sh(`kill ${p.pid}`);
|
|
153
160
|
return [`${kind}: stopped pid ${p.pid} (it was started by hand, not as a service)`];
|
|
154
161
|
}
|
|
155
162
|
return [`${kind}: running but no pid found — nothing stopped`];
|
package/dist/relay.js
CHANGED
|
@@ -263,6 +263,15 @@ export class Relay {
|
|
|
263
263
|
: {}),
|
|
264
264
|
});
|
|
265
265
|
}
|
|
266
|
+
if (url.pathname === "/join") {
|
|
267
|
+
const token = (url.searchParams.get("token") ?? "").trim();
|
|
268
|
+
if (!/^[A-Za-z0-9_-]{16,64}$/.test(token))
|
|
269
|
+
return this.json(res, 400, { error: "an invite code is required: /join?token=<code>" });
|
|
270
|
+
const team = this.teamOfInvite(token);
|
|
271
|
+
return team
|
|
272
|
+
? this.json(res, 200, { team })
|
|
273
|
+
: this.json(res, 404, { error: "no open invite matches that code — it may have been used already or expired" });
|
|
274
|
+
}
|
|
266
275
|
if (url.pathname === "/act") {
|
|
267
276
|
const space = this.spaces.get(url.searchParams.get("space") ?? "");
|
|
268
277
|
const token = url.searchParams.get("token") ?? "";
|
|
@@ -713,6 +722,40 @@ button:hover{background:#1d4ed8}.err{color:var(--bad);margin-top:12px}
|
|
|
713
722
|
adminCsrf() {
|
|
714
723
|
return createHash("sha256").update("pantheon-admin-csrf " + (this.adminToken ?? "")).digest("hex").slice(0, 32);
|
|
715
724
|
}
|
|
725
|
+
teamOfInvite(token) {
|
|
726
|
+
const exact = (al) => { const inv = al.findInvite(token); return !!inv && inv.token === token; };
|
|
727
|
+
for (const sp of this.spaces.values())
|
|
728
|
+
if (exact(sp.allow))
|
|
729
|
+
return sp.name;
|
|
730
|
+
const safeOf = (name) => name.replace(/[^a-z0-9._-]/gi, "_");
|
|
731
|
+
const live = new Set([...this.spaces.keys()].map(safeOf));
|
|
732
|
+
let files = [];
|
|
733
|
+
try {
|
|
734
|
+
files = readdirSync(this.dataDir);
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
for (const f of files) {
|
|
740
|
+
if (!f.endsWith(".members.json"))
|
|
741
|
+
continue;
|
|
742
|
+
const safe = f.slice(0, -".members.json".length);
|
|
743
|
+
if (live.has(safe))
|
|
744
|
+
continue;
|
|
745
|
+
try {
|
|
746
|
+
if (!exact(new Allowlist(this.dataDir, safe)))
|
|
747
|
+
continue;
|
|
748
|
+
const named = this.sleepingRecords(safe).find((r) => r.space)?.space;
|
|
749
|
+
if (named)
|
|
750
|
+
return named;
|
|
751
|
+
if (safeOf(safe) === safe && !/_/.test(safe))
|
|
752
|
+
return safe;
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
catch { }
|
|
756
|
+
}
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
716
759
|
fleetHealth(now = Date.now()) {
|
|
717
760
|
const out = [];
|
|
718
761
|
const seen = new Set();
|
package/dist/service.js
CHANGED
|
@@ -164,14 +164,22 @@ export function uninstallService(kind) {
|
|
|
164
164
|
export function restartService(kind, exec = (c, a) => execFileSync(c, a, { stdio: "ignore" })) {
|
|
165
165
|
if (!serviceInstalled(kind))
|
|
166
166
|
return [` ${kind}: not installed here`];
|
|
167
|
+
const uid = process.getuid?.() ?? 501;
|
|
167
168
|
try {
|
|
168
169
|
if (platform() === "darwin")
|
|
169
|
-
exec("launchctl", ["kickstart", "-k", `gui/${
|
|
170
|
+
exec("launchctl", ["kickstart", "-k", `gui/${uid}/${LABEL(kind)}`]);
|
|
170
171
|
else
|
|
171
172
|
exec("systemctl", ["--user", "restart", `pantheon-${kind}`]);
|
|
172
173
|
return [` ${kind}: restarted on the new code`];
|
|
173
174
|
}
|
|
174
175
|
catch (e) {
|
|
176
|
+
if (platform() === "darwin") {
|
|
177
|
+
try {
|
|
178
|
+
exec("launchctl", ["bootstrap", `gui/${uid}`, plistPath(kind)]);
|
|
179
|
+
return [` ${kind}: was not loaded in launchd — loaded and started on the new code`];
|
|
180
|
+
}
|
|
181
|
+
catch { }
|
|
182
|
+
}
|
|
175
183
|
return [` ${kind}: could not restart (${e.message.split("\n")[0]}) — pantheon service restart ${kind}`];
|
|
176
184
|
}
|
|
177
185
|
}
|
package/dist/upgrade.js
CHANGED
|
@@ -16,6 +16,16 @@ function pkgVersion(root) {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
export const PKG_NAME = "@join-pantheon/cli";
|
|
19
|
+
export function rewireVia(root, fallback, run = (file, args) => execFileSync(process.execPath, [file, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 120_000 })) {
|
|
20
|
+
try {
|
|
21
|
+
const out = JSON.parse(run(join(root, "dist", "cli.js"), ["rewire", "--json"]));
|
|
22
|
+
if (Array.isArray(out) && out.every((o) => o && typeof o.tool === "string" && typeof o.status === "string")) {
|
|
23
|
+
return out.map((o) => ({ tool: o.tool, status: o.status }));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
return fallback();
|
|
28
|
+
}
|
|
19
29
|
export function defaultDeps() {
|
|
20
30
|
const git = (root, ...args) => execFileSync("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
21
31
|
return {
|
|
@@ -29,7 +39,7 @@ export function defaultDeps() {
|
|
|
29
39
|
pull: (root) => git(root, "pull", "--ff-only"),
|
|
30
40
|
build: (root) => execFileSync("npm", ["install", "--no-audit", "--no-fund"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }),
|
|
31
41
|
version: pkgVersion,
|
|
32
|
-
rewire: () => installForUser().map((t) => ({ tool: t.tool, status: t.status })),
|
|
42
|
+
rewire: () => rewireVia(repoRoot(), () => installForUser().map((t) => ({ tool: t.tool, status: t.status }))),
|
|
33
43
|
running: () => {
|
|
34
44
|
const up = [];
|
|
35
45
|
try {
|
|
@@ -182,12 +182,18 @@ final class RowView: NSView {
|
|
|
182
182
|
private var copied = false
|
|
183
183
|
|
|
184
184
|
private let titleLines: Int
|
|
185
|
+
/// The menu is the board people read every day; 430 wrapped a plain sentence
|
|
186
|
+
/// onto two lines and truncated a decision. 520 carries ~40% more per line.
|
|
187
|
+
static let rowWidth: CGFloat = 520
|
|
185
188
|
private static let titleFont = NSFont.systemFont(ofSize: 13, weight: .medium)
|
|
186
189
|
private static let lineHeight: CGFloat = 17
|
|
187
190
|
|
|
188
|
-
/// How many lines this title needs, at most
|
|
191
|
+
/// How many lines this title needs, at most three. A one-line row truncated
|
|
189
192
|
/// mid-word ("Sid asked: Sid is asking: is this work in producti…") hid the
|
|
190
|
-
/// only part worth reading; two lines
|
|
193
|
+
/// only part worth reading; two lines carried most titles, and a decision
|
|
194
|
+
/// ("Praneet wants the unattended worker able to do everything overlapping
|
|
195
|
+
/// Pantheo…") still lost its point at the end of the second. Three lines,
|
|
196
|
+
/// taken only when needed, carry every real title whole.
|
|
191
197
|
static func lines(for text: String, width: CGFloat) -> Int {
|
|
192
198
|
let para = NSMutableParagraphStyle()
|
|
193
199
|
para.lineBreakMode = .byWordWrapping
|
|
@@ -195,14 +201,14 @@ final class RowView: NSView {
|
|
|
195
201
|
with: NSSize(width: width, height: .greatestFiniteMagnitude),
|
|
196
202
|
options: [.usesLineFragmentOrigin, .usesFontLeading],
|
|
197
203
|
attributes: [.font: titleFont, .paragraphStyle: para]).height
|
|
198
|
-
return max(1, min(
|
|
204
|
+
return max(1, min(3, Int(ceil(h / lineHeight))))
|
|
199
205
|
}
|
|
200
206
|
|
|
201
207
|
init(item: Item, width: CGFloat, onOpen: @escaping (Item) -> Void, onCopy: @escaping (Item) -> Void) {
|
|
202
208
|
self.item = item
|
|
203
209
|
self.onOpen = onOpen
|
|
204
210
|
self.onCopy = onCopy
|
|
205
|
-
self.titleLines = RowView.lines(for: item.title, width: width -
|
|
211
|
+
self.titleLines = RowView.lines(for: item.title, width: width - RowView.textInsets)
|
|
206
212
|
let textHeight = CGFloat(titleLines) * RowView.lineHeight
|
|
207
213
|
super.init(frame: NSRect(x: 0, y: 0, width: width, height: textHeight + (item.subtitle.isEmpty ? 13 : 29)))
|
|
208
214
|
setAccessibilityRole(.button)
|
|
@@ -210,7 +216,9 @@ final class RowView: NSView {
|
|
|
210
216
|
}
|
|
211
217
|
required init?(coder: NSCoder) { return nil }
|
|
212
218
|
|
|
213
|
-
|
|
219
|
+
/// Left inset plus what the copy control needs on the right: 28 for the control, 8 clear of the text, 8 to the edge.
|
|
220
|
+
static let textInsets: CGFloat = 12 + 44
|
|
221
|
+
private var copyRect: NSRect { NSRect(x: bounds.maxX - 36, y: bounds.midY - 12, width: 28, height: 24) }
|
|
214
222
|
|
|
215
223
|
|
|
216
224
|
override func updateTrackingAreas() {
|
|
@@ -253,7 +261,7 @@ final class RowView: NSView {
|
|
|
253
261
|
para.lineSpacing = 0
|
|
254
262
|
let flat = NSMutableParagraphStyle()
|
|
255
263
|
flat.lineBreakMode = .byTruncatingTail
|
|
256
|
-
let textWidth = bounds.width -
|
|
264
|
+
let textWidth = bounds.width - RowView.textInsets
|
|
257
265
|
let hasSub = !item.subtitle.isEmpty
|
|
258
266
|
let titleHeight = CGFloat(titleLines) * RowView.lineHeight
|
|
259
267
|
let titleY = bounds.maxY - 7 - titleHeight
|
|
@@ -856,7 +864,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
856
864
|
for item in items.prefix(6) {
|
|
857
865
|
let mi = NSMenuItem(title: item.title, action: nil, keyEquivalent: "")
|
|
858
866
|
mi.toolTip = item.detail
|
|
859
|
-
mi.view = RowView(item: item, width:
|
|
867
|
+
mi.view = RowView(item: item, width: RowView.rowWidth,
|
|
860
868
|
onOpen: { [weak self] it in self?.showDetail(it) },
|
|
861
869
|
onCopy: { [weak self] it in self?.copyPrompt(it) })
|
|
862
870
|
menu.addItem(mi)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@join-pantheon/cli",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.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": {
|