@join-pantheon/cli 0.26.3 → 0.26.5
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 +1 -1
- package/dist/cli.js +76 -3
- package/dist/history-view.js +2 -1
- package/dist/ledger.js +3 -0
- package/dist/mcp-instructions.js +3 -1
- package/dist/mcp.js +15 -6
- package/dist/protocol.js +1 -0
- package/dist/relay.js +11 -4
- package/dist/tray.js +2 -2
- package/dist/workdir.js +30 -0
- package/dist/worklist.js +17 -0
- package/helper/macos/menubar.swift +286 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -499,7 +499,7 @@ All enforced in the relay, not in a prompt — a prompt can be argued out of its
|
|
|
499
499
|
|
|
500
500
|
| Guard | Default | Why |
|
|
501
501
|
|---|---|---|
|
|
502
|
-
| Loop guard | 6 acts in a row | one delegate cannot monologue; only a peer act resets the run |
|
|
502
|
+
| Loop guard | 6 acts in a row | one delegate cannot monologue; only a peer act resets the run. Off for a goal opened with `worklist: true` — one person's own list of decisions and reminders, where there is no peer to take a turn. The act budget still applies there. |
|
|
503
503
|
| Goal budget | 80 acts | a runaway goal stalls and raises a human obligation instead of burning tokens |
|
|
504
504
|
| Rate limit | 40 acts / 10s | per member |
|
|
505
505
|
| Body cap | 32000 chars | over 2000 needs a one-line summary — a peer reading six thousand words misses paragraph nine |
|
package/dist/cli.js
CHANGED
|
@@ -326,6 +326,20 @@ async function guidedSetup(cfg, project, opts = {}) {
|
|
|
326
326
|
rl?.close();
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
|
+
async function workdirFor(cfg, hud) {
|
|
330
|
+
const { loadAll, findMembership } = await import("./config.js");
|
|
331
|
+
const { pickWorkdir } = await import("./workdir.js");
|
|
332
|
+
const all = loadAll();
|
|
333
|
+
const m = all ? findMembership(all, hud.space || cfg.space) : undefined;
|
|
334
|
+
const team = m?.name ?? cfg.space;
|
|
335
|
+
const bindings = all && m ? Object.values(all.bindings ?? {}).filter((b) => b.space === m.id) : [];
|
|
336
|
+
let hints = [];
|
|
337
|
+
if (bindings.length > 1 && hud.goal) {
|
|
338
|
+
const acts = await fetchHistory(cfg).catch(() => []);
|
|
339
|
+
hints = acts.filter((x) => x.goal === hud.goal).flatMap((x) => [x.body, JSON.stringify(x.meta ?? {})]);
|
|
340
|
+
}
|
|
341
|
+
return pickWorkdir(bindings, hints, team);
|
|
342
|
+
}
|
|
329
343
|
async function fetchHistory(cfg, since = 0) {
|
|
330
344
|
const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
331
345
|
const res = await fetch(`${base}/history?space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}&since=${since}`, { signal: AbortSignal.timeout(8000) });
|
|
@@ -963,7 +977,7 @@ async function main() {
|
|
|
963
977
|
if (!goals.length)
|
|
964
978
|
console.log(` ${D("(none)")}`);
|
|
965
979
|
for (const g of goals) {
|
|
966
|
-
console.log(` ${g.state.padEnd(8)} ${String(g.acts).padStart(3)}/${g.budget} ${g.title} ${D(g.id)}`);
|
|
980
|
+
console.log(` ${g.state.padEnd(8)} ${String(g.acts).padStart(3)}/${g.budget} ${g.title}${g.kind === "worklist" ? D(" [worklist]") : ""} ${D(g.id)}`);
|
|
967
981
|
}
|
|
968
982
|
const owed = client.obligations;
|
|
969
983
|
const ageOf = (t) => {
|
|
@@ -2262,6 +2276,46 @@ async function main() {
|
|
|
2262
2276
|
}
|
|
2263
2277
|
return;
|
|
2264
2278
|
}
|
|
2279
|
+
if (hud?.action === "reply") {
|
|
2280
|
+
const notSent = (why) => { console.log(`not sent: ${why.split("\n")[0]}`); process.exit(1); };
|
|
2281
|
+
const kind = hud.kind ?? "NOTE";
|
|
2282
|
+
const body = (hud.body ?? "").trim();
|
|
2283
|
+
if (!body)
|
|
2284
|
+
notSent("nothing to send");
|
|
2285
|
+
const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
2286
|
+
await c2.connect().catch((e) => notSent(e.message));
|
|
2287
|
+
const o = hud.decision ? c2.obligations.find((x) => x.id === hud.decision) : undefined;
|
|
2288
|
+
const acts = await fetchHistory(cfg).catch(() => []);
|
|
2289
|
+
const target = acts.find((x) => x.id === (o?.ref ?? hud.act));
|
|
2290
|
+
const goal = o?.goal ?? target?.goal ?? hud.goal ?? null;
|
|
2291
|
+
if (kind === "ANSWER" && !o && !target) {
|
|
2292
|
+
c2.close();
|
|
2293
|
+
notSent("that decision is no longer waiting — nothing to answer");
|
|
2294
|
+
}
|
|
2295
|
+
const ref = kind === "ANSWER" ? (o?.ref ?? target?.id ?? null) : kind === "ASK" ? (target?.id ?? null) : null;
|
|
2296
|
+
const to = kind === "ASK" ? (target && target.from !== cfg.member ? [target.from] : null) : null;
|
|
2297
|
+
if (kind === "ASK" && !to) {
|
|
2298
|
+
c2.close();
|
|
2299
|
+
notSent("cannot tell who raised this — use Show the thread and reply from there");
|
|
2300
|
+
}
|
|
2301
|
+
const sent = await c2.send({ act: kind, body, goal, ref, to, after_seq: c2.readSeq, meta: { surface: "menubar" } })
|
|
2302
|
+
.catch((e) => notSent(e.message));
|
|
2303
|
+
c2.close();
|
|
2304
|
+
console.log(`sent ${kind} id=${sent.id} seq=${sent.seq}`);
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
if (hud?.action === "retract") {
|
|
2308
|
+
const notSent = (why) => { console.log(`not sent: ${why.split("\n")[0]}`); process.exit(1); };
|
|
2309
|
+
if (!hud.act)
|
|
2310
|
+
notSent("nothing to retract");
|
|
2311
|
+
const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
2312
|
+
await c2.connect().catch((e) => notSent(e.message));
|
|
2313
|
+
const sent = await c2.send({ act: "RETRACT", body: "Retracted from the menu bar.", goal: null, ref: hud.act, to: null, meta: { surface: "menubar" } })
|
|
2314
|
+
.catch((e) => notSent(e.message));
|
|
2315
|
+
c2.close();
|
|
2316
|
+
console.log(`sent RETRACT id=${sent.id} seq=${sent.seq}`);
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2265
2319
|
if (hud?.action === "done") {
|
|
2266
2320
|
const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
2267
2321
|
await c2.connect().catch((e) => die(e.message));
|
|
@@ -2278,6 +2332,15 @@ async function main() {
|
|
|
2278
2332
|
return;
|
|
2279
2333
|
}
|
|
2280
2334
|
if (hud?.action === "details") {
|
|
2335
|
+
const wd = await workdirFor(cfg, hud).catch(() => null);
|
|
2336
|
+
if (wd?.path) {
|
|
2337
|
+
try {
|
|
2338
|
+
process.chdir(wd.path);
|
|
2339
|
+
}
|
|
2340
|
+
catch { }
|
|
2341
|
+
}
|
|
2342
|
+
if (wd)
|
|
2343
|
+
console.error(D(` ${wd.note}`));
|
|
2281
2344
|
const acts = await fetchHistory(cfg).catch(() => []);
|
|
2282
2345
|
const a = hud.act ? acts.find((x) => x.id === hud.act) : undefined;
|
|
2283
2346
|
let o;
|
|
@@ -2377,6 +2440,10 @@ async function main() {
|
|
|
2377
2440
|
}
|
|
2378
2441
|
if (hud?.action === "open") {
|
|
2379
2442
|
const p = loadProfile(cfg);
|
|
2443
|
+
const wd = await workdirFor(cfg, hud).catch(() => null);
|
|
2444
|
+
const cwd = wd?.path ?? undefined;
|
|
2445
|
+
if (wd)
|
|
2446
|
+
console.error(D(` ${wd.note}`));
|
|
2380
2447
|
if (hud.tool && isAgentSurface(hud.tool) && p.agent.primary_surface !== hud.tool) {
|
|
2381
2448
|
saveProfile({ ...p, agent: { ...p.agent, primary_surface: hud.tool }, updated_at: Date.now() });
|
|
2382
2449
|
console.error(D(`${hud.tool} is now where your decisions open`));
|
|
@@ -2388,7 +2455,7 @@ async function main() {
|
|
|
2388
2455
|
const { spawnSync } = await import("node:child_process");
|
|
2389
2456
|
{
|
|
2390
2457
|
const sp = agentSpawn(bin, [hud.prompt]);
|
|
2391
|
-
spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
|
|
2458
|
+
spawnSync(sp.command, sp.args, { stdio: "inherit", ...(cwd ? { cwd } : {}), ...sp.options });
|
|
2392
2459
|
}
|
|
2393
2460
|
return;
|
|
2394
2461
|
}
|
|
@@ -2400,12 +2467,18 @@ async function main() {
|
|
|
2400
2467
|
const { spawnSync } = await import("node:child_process");
|
|
2401
2468
|
{
|
|
2402
2469
|
const sp = agentSpawn(agent.binary, launchArgs(agent.id, hud.prompt, sessionName(hud.prompt)));
|
|
2403
|
-
spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
|
|
2470
|
+
spawnSync(sp.command, sp.args, { stdio: "inherit", ...(cwd ? { cwd } : {}), ...sp.options });
|
|
2404
2471
|
}
|
|
2405
2472
|
return;
|
|
2406
2473
|
}
|
|
2407
2474
|
if (hud.decision)
|
|
2408
2475
|
id = hud.decision;
|
|
2476
|
+
if (cwd) {
|
|
2477
|
+
try {
|
|
2478
|
+
process.chdir(cwd);
|
|
2479
|
+
}
|
|
2480
|
+
catch { }
|
|
2481
|
+
}
|
|
2409
2482
|
}
|
|
2410
2483
|
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
2411
2484
|
await client.connect().catch((e) => die(e.message));
|
package/dist/history-view.js
CHANGED
|
@@ -8,7 +8,8 @@ export function selectActs(acts, opts = {}) {
|
|
|
8
8
|
}
|
|
9
9
|
const when = (ts) => new Date(ts).toISOString().slice(0, 19).replace("T", " ") + "Z";
|
|
10
10
|
export function formatAct(a, opts = {}) {
|
|
11
|
-
const
|
|
11
|
+
const surface = a.meta?.surface;
|
|
12
|
+
const who = `${a.from}${surface === "menubar" ? " (from the menu bar)" : a.role && a.role !== "interactive" ? ` (${a.role})` : ""}`;
|
|
12
13
|
const head = `[${a.seq}] ${a.act} from ${who}${a.to?.length ? ` to ${a.to.join(",")}` : ""} ${when(a.ts)} id=${a.id}${a.ref ? ` ref=${a.ref}` : ""}${a.goal ? ` goal=${a.goal}${opts.goalTitle ? ` "${opts.goalTitle}"` : ""}` : ""}`;
|
|
13
14
|
if (opts.short)
|
|
14
15
|
return `${head}\n ${(a.summary ?? a.body).split("\n")[0].slice(0, 160)}`;
|
package/dist/ledger.js
CHANGED
|
@@ -35,6 +35,9 @@ export class Ledger {
|
|
|
35
35
|
since(seq, limit = 500) {
|
|
36
36
|
return this.records.filter((r) => r.seq > seq).slice(0, limit);
|
|
37
37
|
}
|
|
38
|
+
latest(limit = 500) {
|
|
39
|
+
return this.records.slice(Math.max(0, this.records.length - limit));
|
|
40
|
+
}
|
|
38
41
|
byGoal(goal, limit = 200) {
|
|
39
42
|
const hits = this.records.filter((r) => r.goal === goal);
|
|
40
43
|
return hits.slice(Math.max(0, hits.length - limit));
|
package/dist/mcp-instructions.js
CHANGED
|
@@ -8,7 +8,8 @@ export function protocol() {
|
|
|
8
8
|
4. back to 1
|
|
9
9
|
|
|
10
10
|
Keep looping while any goal you are in is OPEN. Stop only when every goal you are in is
|
|
11
|
-
CLOSED, or when you have ESCALATED and are genuinely waiting on your human.
|
|
11
|
+
CLOSED, or when you have ESCALATED and are genuinely waiting on your human. A goal marked
|
|
12
|
+
[worklist] is one person's own list: it does not keep you in the loop.
|
|
12
13
|
|
|
13
14
|
RULES
|
|
14
15
|
- Talk to peers, not to your human. If a peer could answer it, ASK the peer instead of asking your human.
|
|
@@ -33,6 +34,7 @@ RULES
|
|
|
33
34
|
- Verdict first, evidence after, and short: a peer reads your act in one screen. Say when you edited
|
|
34
35
|
your own human's working tree as a side effect, and leave those edits uncommitted for them.
|
|
35
36
|
- If pantheon_send returns PEER_TURN you have been talking to yourself — go back to pantheon_wait.
|
|
37
|
+
Filing reminders and decisions for your OWN human is not that: open the goal with worklist: true.
|
|
36
38
|
|
|
37
39
|
The relay only moves messages. It has no access to your files, your shell, or your credentials.
|
|
38
40
|
|
package/dist/mcp.js
CHANGED
|
@@ -5,6 +5,7 @@ import { PantheonClient } from "./client.js";
|
|
|
5
5
|
import { fenceBlock, fenceRubric, assess, fenceReminder } from "./untrusted.js";
|
|
6
6
|
import { resolveFor } from "./config.js";
|
|
7
7
|
import { LIMITS, SENDABLE } from "./protocol.js";
|
|
8
|
+
import { goalKindLabel, openedText } from "./worklist.js";
|
|
8
9
|
import { VERSION } from "./version.js";
|
|
9
10
|
import { installedVersion } from "./mcp-supervisor.js";
|
|
10
11
|
import { backendFromLabel } from "./agents.js";
|
|
@@ -246,7 +247,7 @@ export async function runMcp() {
|
|
|
246
247
|
? `peers: ${peers.map((p) => `${p.id} [${p.online ? "online" : "offline"}, ${p.agent}${reach(p)}]`).join(" ")}`
|
|
247
248
|
: `peers: none yet — you are the only delegate here. Your human's colleague needs to run 'pantheon join'.`,
|
|
248
249
|
open.length
|
|
249
|
-
? `open goals:\n${open.map((g) => ` ${g.id} ${g.state} ${g.acts}/${g.budget} acts "${g.title}" members: ${g.members.join(",")}${deadlineLabel(g.deadline)}`).join("\n")}`
|
|
250
|
+
? `open goals:\n${open.map((g) => ` ${g.id} ${g.state} ${g.acts}/${g.budget} acts "${g.title}" members: ${g.members.join(",")}${deadlineLabel(g.deadline)}${goalKindLabel(g)}`).join("\n")}`
|
|
250
251
|
: `open goals: none`,
|
|
251
252
|
mine.length
|
|
252
253
|
? `YOURS TO HANDLE (claim before working):\n${mine.map((o) => ` ${o.id} ${o.kind}${o.claimed_by ? " [yours]" : ""} (ref ${o.ref}) — ${o.body}`).join("\n")}`
|
|
@@ -415,8 +416,11 @@ export async function runMcp() {
|
|
|
415
416
|
deadline: z.string().optional().describe("when this goal should be closed by, ISO 8601 with timezone (e.g. 2026-08-28T22:00:00+04:00). " +
|
|
416
417
|
"Shown on every status line, and shown OVERDUE when passed — a deadline that lives only in prose is not tracked by anything."),
|
|
417
418
|
distinct_from_open_goals: z.boolean().optional().describe("confirm this goal is NOT the same work as any currently open goal. Required when an open goal already involves the same members."),
|
|
419
|
+
worklist: z.boolean().optional().describe("this goal is your human's OWN list — decisions with deadlines and reminders you file for them, which peers may read but " +
|
|
420
|
+
"nobody owes anything in. Turns off the six-acts-in-a-row guard for this goal (the act budget still applies) and it does not " +
|
|
421
|
+
"keep you in the loop. Use it when your human asks you to set up reminders or a pipeline for themselves; never for shared work."),
|
|
418
422
|
},
|
|
419
|
-
}, async ({ title, acceptance, invite, deadline, distinct_from_open_goals }) => guard(async () => {
|
|
423
|
+
}, async ({ title, acceptance, invite, deadline, distinct_from_open_goals, worklist }) => guard(async () => {
|
|
420
424
|
let deadlineMs;
|
|
421
425
|
if (deadline !== undefined) {
|
|
422
426
|
deadlineMs = Date.parse(deadline);
|
|
@@ -427,7 +431,7 @@ export async function runMcp() {
|
|
|
427
431
|
return fail(`deadline ${new Date(deadlineMs).toISOString()} is already in the past — check the timezone.`);
|
|
428
432
|
}
|
|
429
433
|
}
|
|
430
|
-
if (!distinct_from_open_goals) {
|
|
434
|
+
if (!distinct_from_open_goals && !worklist) {
|
|
431
435
|
const overlapping = overlappingOpenGoals(client.goals.values(), cfg.member, invite, client.roster.map((m) => m.id));
|
|
432
436
|
if (overlapping.length) {
|
|
433
437
|
return fail(`not opened — ${overlapping.length} open goal(s) already involve the same members:\n` +
|
|
@@ -436,9 +440,13 @@ export async function runMcp() {
|
|
|
436
440
|
`If yours is genuinely distinct work, call pantheon_open_goal again with distinct_from_open_goals: true.`);
|
|
437
441
|
}
|
|
438
442
|
}
|
|
439
|
-
const r = await client.openGoal({
|
|
440
|
-
|
|
441
|
-
|
|
443
|
+
const r = await client.openGoal({
|
|
444
|
+
title, acceptance,
|
|
445
|
+
...(invite ? { invite } : {}),
|
|
446
|
+
...(deadlineMs !== undefined ? { deadline: deadlineMs } : {}),
|
|
447
|
+
...(worklist ? { kind: "worklist" } : {}),
|
|
448
|
+
});
|
|
449
|
+
return text(openedText(r.id, title, !!worklist, client.goals.get(r.id), deadlineMs !== undefined ? `\n${deadlineLabel(deadlineMs).trim()}` : ""));
|
|
442
450
|
}));
|
|
443
451
|
server.registerTool("pantheon_claim", {
|
|
444
452
|
title: "Claim a piece of work",
|
|
@@ -465,6 +473,7 @@ export async function runMcp() {
|
|
|
465
473
|
url.pathname = "/history";
|
|
466
474
|
url.searchParams.set("space", cfg.space);
|
|
467
475
|
url.searchParams.set("token", cfg.token);
|
|
476
|
+
url.searchParams.set("goal", goal);
|
|
468
477
|
return fetch(url).then(async (res) => {
|
|
469
478
|
const { acts } = (await res.json());
|
|
470
479
|
const hits = acts.filter((a) => a.goal === goal).slice(-(limit ?? 60));
|
package/dist/protocol.js
CHANGED
|
@@ -179,6 +179,7 @@ export const OpenGoal = z.object({
|
|
|
179
179
|
invite: z.array(z.string().min(1).max(48)).max(16).optional(),
|
|
180
180
|
budget: z.number().int().min(4).max(1000).optional(),
|
|
181
181
|
deadline: z.number().int().positive().optional(),
|
|
182
|
+
kind: z.enum(["worklist"]).optional(),
|
|
182
183
|
});
|
|
183
184
|
export const Ping = z.object({ t: z.literal("ping") });
|
|
184
185
|
export const DeleteSpace = z.object({
|
package/dist/relay.js
CHANGED
|
@@ -288,7 +288,10 @@ export class Relay {
|
|
|
288
288
|
const token = url.searchParams.get("token") ?? "";
|
|
289
289
|
if (!space || space.token !== token)
|
|
290
290
|
return this.json(res, 404, { error: "no such space" });
|
|
291
|
-
|
|
291
|
+
const goal = url.searchParams.get("goal");
|
|
292
|
+
const since = Number(url.searchParams.get("since") ?? 0);
|
|
293
|
+
const acts = goal ? space.ledger.byGoal(goal) : since > 0 ? space.ledger.since(since) : space.ledger.latest();
|
|
294
|
+
return this.json(res, 200, { acts, head: space.ledger.head });
|
|
292
295
|
}
|
|
293
296
|
if (url.pathname === "/obligations") {
|
|
294
297
|
const space = this.spaces.get(url.searchParams.get("space") ?? "");
|
|
@@ -1964,6 +1967,7 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
1964
1967
|
opened_by: me.id,
|
|
1965
1968
|
opened_at: Date.now(),
|
|
1966
1969
|
...(m.deadline !== undefined ? { deadline: m.deadline } : {}),
|
|
1970
|
+
...(m.kind !== undefined ? { kind: m.kind } : {}),
|
|
1967
1971
|
};
|
|
1968
1972
|
space.goals.set(goal.id, goal);
|
|
1969
1973
|
space.store.save(space.goals, space.obligations, space.profiles);
|
|
@@ -1971,7 +1975,8 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
1971
1975
|
const rec = this.commit(space, {
|
|
1972
1976
|
from: me.id, act: "NOTE", to: null, goal: goal.id, ref: null,
|
|
1973
1977
|
body: `goal opened: ${goal.title}\nacceptance: ${goal.acceptance || "(none stated)"}` +
|
|
1974
|
-
(goal.deadline ? `\ndeadline: ${new Date(goal.deadline).toISOString()}` : "")
|
|
1978
|
+
(goal.deadline ? `\ndeadline: ${new Date(goal.deadline).toISOString()}` : "") +
|
|
1979
|
+
(goal.kind === "worklist" ? `\nkind: worklist — ${me.id}'s own list; peers may read it, nobody owes anything in it` : ""),
|
|
1975
1980
|
});
|
|
1976
1981
|
this.send(ws, { t: "ack", ...(m.cid ? { cid: m.cid } : {}), id: goal.id, seq: rec.seq });
|
|
1977
1982
|
return;
|
|
@@ -2229,11 +2234,13 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2229
2234
|
return { code: "GOAL_CLOSED", message: `goal "${goal.title}" is closed` };
|
|
2230
2235
|
if (!goal.members.includes(me.id))
|
|
2231
2236
|
return { code: "NOT_IN_GOAL", message: `you are not a member of "${goal.title}"` };
|
|
2232
|
-
|
|
2237
|
+
const settlesOwnHuman = act === "ANSWER" && !!ref
|
|
2238
|
+
&& [...sp.obligations.values()].some((o) => o.kind === "HUMAN" && o.ref === ref && o.owner === `human:${me.id}`);
|
|
2239
|
+
if (goal.acts >= goal.budget && !settlesOwnHuman) {
|
|
2233
2240
|
return { code: "BUDGET_SPENT", message: `goal "${goal.title}" spent its ${goal.budget}-act budget and needs a human` };
|
|
2234
2241
|
}
|
|
2235
2242
|
const run = sp.lastActor.get(goalId);
|
|
2236
|
-
if (run && run.member === me.id && run.run >= LIMITS.consecutive && act !== "ESCALATE" && act !== "CLOSE") {
|
|
2243
|
+
if (!settlesOwnHuman && goal.kind !== "worklist" && run && run.member === me.id && run.run >= LIMITS.consecutive && act !== "ESCALATE" && act !== "CLOSE") {
|
|
2237
2244
|
return {
|
|
2238
2245
|
code: "PEER_TURN",
|
|
2239
2246
|
message: `you have sent ${run.run} acts in a row in "${goal.title}". Wait for a peer (pantheon_wait), or ESCALATE if nobody can move it.`,
|
package/dist/tray.js
CHANGED
|
@@ -131,14 +131,14 @@ async function trayStatusOne(cfg) {
|
|
|
131
131
|
const items = [
|
|
132
132
|
...mineHuman.map((o) => {
|
|
133
133
|
const from = senderOf(o);
|
|
134
|
-
return row(o, "decision", !from || from === cfg.member ? "your agent" : from, decisionPrompt(o.id, decisionTitle(o.body)));
|
|
134
|
+
return { ...row(o, "decision", !from || from === cfg.member ? "your agent" : from, decisionPrompt(o.id, decisionTitle(o.body))), decider: cfg.member };
|
|
135
135
|
}),
|
|
136
136
|
...owed.map((o) => row(o, "owed", "you", owedPrompt(decisionTitle(o.body), o.goal))),
|
|
137
137
|
...waiting.map((o) => {
|
|
138
138
|
const who = o.owner.replace(/^human:/, "");
|
|
139
139
|
const r = row(o, "waiting", who, waitingPrompt(who, decisionTitle(o.body), o.goal));
|
|
140
140
|
return o.owner.startsWith("human:")
|
|
141
|
-
? { ...r, title: `${who} decides: ${r.title}`, headline: `${who} decides: ${r.headline}
|
|
141
|
+
? { ...r, title: `${who} decides: ${r.title}`, headline: `${who} decides: ${r.headline}`, decider: who }
|
|
142
142
|
: r;
|
|
143
143
|
}),
|
|
144
144
|
];
|
package/dist/workdir.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
export function gitActivity(path) {
|
|
4
|
+
for (const f of ["index", "FETCH_HEAD", "HEAD"]) {
|
|
5
|
+
try {
|
|
6
|
+
return statSync(join(path, ".git", f)).mtimeMs;
|
|
7
|
+
}
|
|
8
|
+
catch { }
|
|
9
|
+
}
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
export function pickWorkdir(bindings, hints, team, activity = gitActivity, exists = existsSync) {
|
|
13
|
+
const live = bindings.filter((b) => exists(b.path));
|
|
14
|
+
if (!live.length) {
|
|
15
|
+
return {
|
|
16
|
+
path: null,
|
|
17
|
+
note: `no folder on this machine is bound to "${team}", so this opens in your home folder. To fix it, run pantheon use ${team} --here inside the repo.`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const named = live.filter((b) => hints.some((h) => (b.origin ? h.includes(b.origin) : false) || h.includes(b.path)));
|
|
21
|
+
const pool = named.length ? named : live;
|
|
22
|
+
const chosen = [...pool].sort((x, y) => (activity(y.path) - activity(x.path)) || (y.bound_at - x.bound_at))[0];
|
|
23
|
+
const others = live.filter((b) => b.path !== chosen.path).map((b) => basename(b.path));
|
|
24
|
+
const why = named.length ? "named in this goal" : live.length > 1 ? "your most recent git activity" : "";
|
|
25
|
+
return {
|
|
26
|
+
path: chosen.path,
|
|
27
|
+
note: `in ${basename(chosen.path)}${why ? ` (${why})` : ""}${others.length ? ` — "${team}" is also bound to ${others.join(", ")}` : ""}`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=workdir.js.map
|
package/dist/worklist.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function goalKindLabel(g) {
|
|
2
|
+
return g.kind === "worklist" ? " [worklist — one person's own list; nobody owes anything here, and it does not keep you in the loop]" : "";
|
|
3
|
+
}
|
|
4
|
+
export function openedText(id, title, askedWorklist, seen, deadlineLine = "") {
|
|
5
|
+
const head = `goal ${id} opened: "${title}"${deadlineLine}`;
|
|
6
|
+
if (!askedWorklist)
|
|
7
|
+
return `${head}\n\nNext: PROPOSE how to split it, then wait for peers to ACCEPT or OBJECT.`;
|
|
8
|
+
if (seen?.kind !== "worklist") {
|
|
9
|
+
return `${head}\n\nOpened as an ORDINARY goal: this relay is older than the worklist feature and ignored the flag. ` +
|
|
10
|
+
`The six-acts-in-a-row guard still applies here, so file items in batches of six and expect PEER_TURN between them ` +
|
|
11
|
+
`until the relay is upgraded.`;
|
|
12
|
+
}
|
|
13
|
+
return `${head} [worklist]\n\nThis is your human's own list. File each item as one act: ESCALATE for a decision they must make ` +
|
|
14
|
+
`(with its deadline in the body), NOTE for a reminder. Nobody owes anything here, PEER_TURN never fires, ` +
|
|
15
|
+
`and this goal does not keep you in the loop — the act budget still applies.`;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=worklist.js.map
|
|
@@ -68,6 +68,8 @@ struct Item {
|
|
|
68
68
|
let goal: String
|
|
69
69
|
let ref: String
|
|
70
70
|
let space: String
|
|
71
|
+
/// whose human must decide this: this member for their own decision, the teammate for theirs, "" when it is not a decision
|
|
72
|
+
let decider: String
|
|
71
73
|
|
|
72
74
|
var isDecision: Bool { kind == "decision" }
|
|
73
75
|
var heading: String {
|
|
@@ -152,7 +154,8 @@ func fetchState() -> TrayState {
|
|
|
152
154
|
when: (i["when"] as? Double) ?? Double(i["when"] as? Int ?? 0),
|
|
153
155
|
goal: i["goal"] as? String ?? "",
|
|
154
156
|
ref: i["ref"] as? String ?? "",
|
|
155
|
-
space: i["space"] as? String ?? ""
|
|
157
|
+
space: i["space"] as? String ?? "",
|
|
158
|
+
decider: i["decider"] as? String ?? ""
|
|
156
159
|
))
|
|
157
160
|
}
|
|
158
161
|
for d in (j["decisions"] as? [[String: Any]] ?? []) {
|
|
@@ -840,7 +843,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
840
843
|
private var panelRendered: NSAttributedString? // the act laid out for a person
|
|
841
844
|
private var panelRaw: String = "" // the act exactly as it was sent
|
|
842
845
|
private var panelShowingRaw = false
|
|
843
|
-
private var
|
|
846
|
+
private var panelRawButton: NSButton? // "Original text" / "Laid out", shown only when the two differ
|
|
847
|
+
private var panelButtons: NSStackView? // the action row; hidden while replying
|
|
848
|
+
private var panelReplyRow: NSStackView? // the reply box; shown only in reply mode
|
|
849
|
+
private var panelCancel: NSButton?
|
|
850
|
+
private var panelReplyButton: NSButton? // the primary action, hidden while replying
|
|
851
|
+
private var panelStatusRow: NSStackView? // collapses when there is nothing to say
|
|
852
|
+
private var panelReply: NSTextView? // the person's own words
|
|
853
|
+
private var panelReplyKind: NSPopUpButton? // Answer / Note / Ask back
|
|
854
|
+
private var panelSendButton: NSButton?
|
|
855
|
+
private var panelRetract: NSButton? // the undo, for a minute after a send
|
|
856
|
+
private var panelLastSent: String = "" // act id of what was just sent
|
|
844
857
|
|
|
845
858
|
private func label(_ text: String, size: CGFloat, weight: NSFont.Weight, color: NSColor, width: CGFloat) -> NSTextField {
|
|
846
859
|
let l = NSTextField(labelWithString: text)
|
|
@@ -951,7 +964,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
951
964
|
guard self.panelItem?.id == item.id else { return }
|
|
952
965
|
self.panelRendered = rendered
|
|
953
966
|
self.panelRaw = raw
|
|
954
|
-
self.
|
|
967
|
+
self.panelRawButton?.isHidden = raw.isEmpty
|
|
955
968
|
if !headline.isEmpty { self.panelHeadline?.stringValue = headline }
|
|
956
969
|
if !self.panelShowingRaw && rendered.length > 0 { self.showBody(rendered, width: width) }
|
|
957
970
|
}
|
|
@@ -962,7 +975,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
962
975
|
@objc private func panelToggleRaw() {
|
|
963
976
|
guard let item = panelItem else { return }
|
|
964
977
|
panelShowingRaw.toggle()
|
|
965
|
-
|
|
978
|
+
panelRawButton?.title = panelShowingRaw ? "Laid out" : "Original text"
|
|
966
979
|
let width: CGFloat = 620 - 52
|
|
967
980
|
if panelShowingRaw { showBody(plain(panelRaw.isEmpty ? item.detail : panelRaw), width: width) }
|
|
968
981
|
else if let r = panelRendered { showBody(r, width: width) }
|
|
@@ -1037,42 +1050,161 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
1037
1050
|
if !item.goal.isEmpty { meta += " · goal \(item.goal)" }
|
|
1038
1051
|
stack.addArrangedSubview(label(meta, size: 11, weight: .regular, color: .tertiaryLabelColor, width: inner))
|
|
1039
1052
|
|
|
1040
|
-
|
|
1053
|
+
// Whose decision this is, said before the box that answers it. Your own
|
|
1054
|
+
// answer to a teammate's decision lands as an act and settles nothing, so the
|
|
1055
|
+
// panel offers Answer only to the person it is waiting on.
|
|
1056
|
+
let owner: String = item.isDecision ? "Yours to decide."
|
|
1057
|
+
: !item.decider.isEmpty ? "\(item.decider)'s to decide. You can add a note or ask back."
|
|
1058
|
+
: item.kind == "owed" ? "Your agent owes this. You can add a note or ask back."
|
|
1059
|
+
: "Waiting on \(item.who). You can add a note or ask back."
|
|
1060
|
+
let ownerRow = NSStackView()
|
|
1061
|
+
ownerRow.orientation = .horizontal
|
|
1062
|
+
ownerRow.alignment = .centerY
|
|
1063
|
+
ownerRow.spacing = 10
|
|
1064
|
+
ownerRow.translatesAutoresizingMaskIntoConstraints = false
|
|
1065
|
+
let ownerLabel = label(owner, size: 12, weight: .medium, color: .secondaryLabelColor, width: inner - 170)
|
|
1066
|
+
ownerLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
1067
|
+
ownerRow.addArrangedSubview(ownerLabel)
|
|
1068
|
+
let ownerSpacer = NSView()
|
|
1069
|
+
ownerSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
1070
|
+
ownerSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
1071
|
+
ownerRow.addArrangedSubview(ownerSpacer)
|
|
1072
|
+
// The one action that is the person's own voice, set apart from the rest and
|
|
1073
|
+
// reachable with Return. Pressing it only opens the box; nothing is sent.
|
|
1074
|
+
let replyBtn = NSButton(title: "Reply manually", target: self, action: #selector(panelReplyManually))
|
|
1075
|
+
replyBtn.bezelStyle = .rounded
|
|
1076
|
+
replyBtn.keyEquivalent = "\r"
|
|
1077
|
+
replyBtn.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
1078
|
+
replyBtn.toolTip = "Type your own answer, note or question. Nothing is drafted for you, and nothing is sent until you press Send."
|
|
1079
|
+
panelReplyButton = replyBtn
|
|
1080
|
+
ownerRow.addArrangedSubview(replyBtn)
|
|
1081
|
+
ownerRow.widthAnchor.constraint(equalToConstant: inner).isActive = true
|
|
1082
|
+
if let idLine = stack.arrangedSubviews.last { stack.setCustomSpacing(6, after: idLine) }
|
|
1083
|
+
stack.addArrangedSubview(ownerRow)
|
|
1084
|
+
// Clear air between the person's own action and the rest: a rule, with room
|
|
1085
|
+
// on both sides, so the blue button is not read as part of the row below.
|
|
1086
|
+
stack.setCustomSpacing(22, after: ownerRow)
|
|
1087
|
+
let rule = NSBox()
|
|
1088
|
+
rule.boxType = .separator
|
|
1089
|
+
rule.translatesAutoresizingMaskIntoConstraints = false
|
|
1090
|
+
rule.widthAnchor.constraint(equalToConstant: inner).isActive = true
|
|
1091
|
+
stack.addArrangedSubview(rule)
|
|
1092
|
+
stack.setCustomSpacing(22, after: rule)
|
|
1093
|
+
|
|
1094
|
+
// The reply, in the person's own words: one popup for what it is, the box,
|
|
1095
|
+
// one Send. Return sends; Shift-Return makes a new line.
|
|
1096
|
+
let replyRow = NSStackView()
|
|
1097
|
+
replyRow.orientation = .horizontal
|
|
1098
|
+
replyRow.alignment = .top
|
|
1099
|
+
replyRow.spacing = 8
|
|
1100
|
+
replyRow.translatesAutoresizingMaskIntoConstraints = false
|
|
1101
|
+
let kinds = NSPopUpButton(frame: .zero, pullsDown: false)
|
|
1102
|
+
for k in replyKinds(item) { kinds.addItem(withTitle: k) }
|
|
1103
|
+
kinds.toolTip = "Answer settles it and the agent continues. Note goes on the record and settles nothing. Ask back puts a question to the agent that raised it."
|
|
1104
|
+
kinds.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
1105
|
+
panelReplyKind = kinds
|
|
1106
|
+
replyRow.addArrangedSubview(kinds)
|
|
1107
|
+
let replyScroll = NSScrollView()
|
|
1108
|
+
replyScroll.hasVerticalScroller = false
|
|
1109
|
+
replyScroll.borderType = .bezelBorder
|
|
1110
|
+
replyScroll.translatesAutoresizingMaskIntoConstraints = false
|
|
1111
|
+
let reply = NSTextView()
|
|
1112
|
+
reply.isEditable = true
|
|
1113
|
+
reply.isRichText = false
|
|
1114
|
+
reply.font = .systemFont(ofSize: 13)
|
|
1115
|
+
reply.textContainerInset = NSSize(width: 4, height: 6)
|
|
1116
|
+
reply.isVerticallyResizable = true
|
|
1117
|
+
reply.isHorizontallyResizable = false
|
|
1118
|
+
reply.autoresizingMask = [.width]
|
|
1119
|
+
reply.textContainer?.widthTracksTextView = true
|
|
1120
|
+
reply.delegate = self
|
|
1121
|
+
replyScroll.documentView = reply
|
|
1122
|
+
replyScroll.heightAnchor.constraint(equalToConstant: 58).isActive = true
|
|
1123
|
+
replyScroll.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
1124
|
+
panelReply = reply
|
|
1125
|
+
replyRow.addArrangedSubview(replyScroll)
|
|
1126
|
+
let send = NSButton(title: "Send", target: self, action: #selector(panelSend(_:)))
|
|
1127
|
+
send.bezelStyle = .rounded
|
|
1128
|
+
send.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
1129
|
+
panelSendButton = send
|
|
1130
|
+
replyRow.addArrangedSubview(send)
|
|
1131
|
+
let cancel = NSButton(title: "Cancel", target: self, action: #selector(panelCancelReply))
|
|
1132
|
+
cancel.bezelStyle = .rounded
|
|
1133
|
+
cancel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
1134
|
+
cancel.toolTip = "Back to the buttons. Your words stay in the box."
|
|
1135
|
+
panelCancel = cancel
|
|
1136
|
+
replyRow.addArrangedSubview(cancel)
|
|
1137
|
+
replyRow.widthAnchor.constraint(equalToConstant: inner).isActive = true
|
|
1138
|
+
panelReplyRow = replyRow
|
|
1139
|
+
stack.addArrangedSubview(replyRow)
|
|
1140
|
+
replyRow.isHidden = true
|
|
1141
|
+
|
|
1142
|
+
// What happened, and the undo for a slip: Retract shows for a minute after a send.
|
|
1143
|
+
let statusRow = NSStackView()
|
|
1144
|
+
statusRow.orientation = .horizontal
|
|
1145
|
+
statusRow.spacing = 10
|
|
1146
|
+
statusRow.translatesAutoresizingMaskIntoConstraints = false
|
|
1147
|
+
let status = label("", size: 11, weight: .medium, color: .secondaryLabelColor, width: inner - 90)
|
|
1041
1148
|
status.isHidden = true
|
|
1042
1149
|
panelStatus = status
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1150
|
+
statusRow.addArrangedSubview(status)
|
|
1151
|
+
let retract = NSButton(title: "Retract", target: self, action: #selector(panelRetractAction))
|
|
1152
|
+
retract.bezelStyle = .rounded
|
|
1153
|
+
retract.controlSize = .small
|
|
1154
|
+
retract.toolTip = "Withdraws what you just sent. Your words stay in the record as retracted."
|
|
1155
|
+
retract.isHidden = true
|
|
1156
|
+
panelRetract = retract
|
|
1157
|
+
statusRow.addArrangedSubview(retract)
|
|
1158
|
+
panelStatusRow = statusRow
|
|
1159
|
+
stack.addArrangedSubview(statusRow)
|
|
1160
|
+
statusRow.isHidden = true
|
|
1161
|
+
|
|
1162
|
+
// Two rows: what to do about it, then what to do with the text. Every
|
|
1163
|
+
// button refuses to compress, so the labels are always whole.
|
|
1164
|
+
func button(_ title: String, _ sel: Selector, _ tip: String) -> NSButton {
|
|
1165
|
+
let b = NSButton(title: title, target: self, action: sel)
|
|
1166
|
+
b.bezelStyle = .rounded
|
|
1167
|
+
b.toolTip = tip
|
|
1168
|
+
b.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
1169
|
+
return b
|
|
1170
|
+
}
|
|
1171
|
+
let rows = NSStackView()
|
|
1172
|
+
rows.orientation = .vertical
|
|
1173
|
+
rows.alignment = .leading
|
|
1174
|
+
rows.spacing = 8
|
|
1175
|
+
rows.translatesAutoresizingMaskIntoConstraints = false
|
|
1176
|
+
|
|
1177
|
+
let row1 = NSStackView()
|
|
1178
|
+
row1.orientation = .horizontal
|
|
1179
|
+
row1.spacing = 10
|
|
1180
|
+
row1.addArrangedSubview(button("Explain in \(state.surfaceName)", #selector(panelOpen),
|
|
1181
|
+
"Opens \(state.surfaceName) in the team's folder and asks it to lay out your options. It will not decide or reply for you."))
|
|
1056
1182
|
if item.isDecision {
|
|
1057
|
-
|
|
1058
|
-
done.bezelStyle = .rounded
|
|
1059
|
-
done.toolTip = "Answer it as handled, so it stops asking"
|
|
1060
|
-
row.addArrangedSubview(done)
|
|
1183
|
+
row1.addArrangedSubview(button("Mark done", #selector(panelDone), "Tells the agent you settled this in person, so it stops asking."))
|
|
1061
1184
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
let
|
|
1066
|
-
|
|
1067
|
-
|
|
1185
|
+
row1.addArrangedSubview(button("Show the thread", #selector(panelThread), "Every act in this goal, in Terminal, from the team's folder."))
|
|
1186
|
+
rows.addArrangedSubview(row1)
|
|
1187
|
+
|
|
1188
|
+
let row2 = NSStackView()
|
|
1189
|
+
row2.orientation = .horizontal
|
|
1190
|
+
row2.spacing = 10
|
|
1191
|
+
row2.addArrangedSubview(button("Copy the question", #selector(panelCopy), "Copies the question and its context as text, for an email or a message."))
|
|
1192
|
+
let rawToggle = button("Original text", #selector(panelToggleRaw), "Exactly as the agent wrote it, before Pantheon laid it out.")
|
|
1068
1193
|
rawToggle.isHidden = true
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
let
|
|
1072
|
-
|
|
1194
|
+
panelRawButton = rawToggle
|
|
1195
|
+
row2.addArrangedSubview(rawToggle)
|
|
1196
|
+
let spacer = NSView()
|
|
1197
|
+
spacer.translatesAutoresizingMaskIntoConstraints = false
|
|
1198
|
+
spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
1199
|
+
row2.addArrangedSubview(spacer)
|
|
1200
|
+
let close = button("Close", #selector(panelClose), "Nothing changes.")
|
|
1073
1201
|
close.keyEquivalent = "\u{1b}"
|
|
1074
|
-
|
|
1075
|
-
|
|
1202
|
+
row2.addArrangedSubview(close)
|
|
1203
|
+
row2.widthAnchor.constraint(equalToConstant: inner).isActive = true
|
|
1204
|
+
rows.addArrangedSubview(row2)
|
|
1205
|
+
|
|
1206
|
+
panelButtons = rows
|
|
1207
|
+
stack.addArrangedSubview(rows)
|
|
1076
1208
|
|
|
1077
1209
|
NSLayoutConstraint.activate([
|
|
1078
1210
|
stack.leadingAnchor.constraint(equalTo: effect.leadingAnchor, constant: 26),
|
|
@@ -1080,7 +1212,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
1080
1212
|
stack.topAnchor.constraint(equalTo: effect.topAnchor, constant: 36),
|
|
1081
1213
|
stack.bottomAnchor.constraint(equalTo: effect.bottomAnchor, constant: -22),
|
|
1082
1214
|
scroll.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
1083
|
-
|
|
1215
|
+
rows.widthAnchor.constraint(lessThanOrEqualTo: stack.widthAnchor),
|
|
1084
1216
|
])
|
|
1085
1217
|
let bh = scroll.heightAnchor.constraint(equalToConstant: bodyHeight(plain(item.detail), width: inner))
|
|
1086
1218
|
bh.isActive = true
|
|
@@ -1104,6 +1236,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
1104
1236
|
private func setStatus(_ text: String) {
|
|
1105
1237
|
panelStatus?.stringValue = text
|
|
1106
1238
|
panelStatus?.isHidden = text.isEmpty
|
|
1239
|
+
panelStatusRow?.isHidden = text.isEmpty && (panelRetract?.isHidden ?? true)
|
|
1107
1240
|
}
|
|
1108
1241
|
|
|
1109
1242
|
@objc private func panelClose() {
|
|
@@ -1114,14 +1247,120 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
1114
1247
|
panelItem = nil
|
|
1115
1248
|
panelHeadline = nil
|
|
1116
1249
|
panelRendered = nil
|
|
1117
|
-
|
|
1250
|
+
panelRawButton = nil
|
|
1251
|
+
panelButtons = nil
|
|
1252
|
+
panelReplyRow = nil
|
|
1253
|
+
panelCancel = nil
|
|
1254
|
+
panelReplyButton = nil
|
|
1255
|
+
panelStatusRow = nil
|
|
1256
|
+
panelReply = nil
|
|
1257
|
+
panelReplyKind = nil
|
|
1258
|
+
panelSendButton = nil
|
|
1259
|
+
panelRetract = nil
|
|
1260
|
+
panelLastSent = ""
|
|
1118
1261
|
panelRaw = ""
|
|
1119
1262
|
panelShowingRaw = false
|
|
1120
1263
|
}
|
|
1264
|
+
/// The question as text — what a person pasting into an email or a message
|
|
1265
|
+
/// wants. The agent prompt still rides with Explain, which copies it as a
|
|
1266
|
+
/// fallback when the agent cannot be opened.
|
|
1121
1267
|
@objc private func panelCopy() {
|
|
1122
1268
|
guard let item = panelItem else { return }
|
|
1123
|
-
|
|
1124
|
-
|
|
1269
|
+
let pb = NSPasteboard.general
|
|
1270
|
+
pb.clearContents()
|
|
1271
|
+
pb.setString(item.headline + "\n\n" + plain(panelRaw.isEmpty ? item.detail : panelRaw).string, forType: .string)
|
|
1272
|
+
setStatus("Copied the question.")
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/// Reply mode: the buttons step aside and only the box remains, with Cancel to
|
|
1276
|
+
/// bring them back. The body gives up a little height so the panel stays put.
|
|
1277
|
+
@objc private func panelReplyManually() {
|
|
1278
|
+
panelButtons?.isHidden = true
|
|
1279
|
+
panelReplyButton?.isHidden = true
|
|
1280
|
+
panelReplyButton?.keyEquivalent = ""
|
|
1281
|
+
panelSendButton?.keyEquivalent = "\r"
|
|
1282
|
+
panelReplyRow?.isHidden = false
|
|
1283
|
+
if let c = panelBodyHeight, c.constant > 140 { c.constant -= 20 }
|
|
1284
|
+
panelRetract?.isHidden = true
|
|
1285
|
+
setStatus("")
|
|
1286
|
+
if let tv = panelReply { detailPanel?.makeFirstResponder(tv) }
|
|
1287
|
+
}
|
|
1288
|
+
@objc private func panelCancelReply() { exitReplyMode() }
|
|
1289
|
+
private func exitReplyMode() {
|
|
1290
|
+
panelReplyRow?.isHidden = true
|
|
1291
|
+
panelSendButton?.keyEquivalent = ""
|
|
1292
|
+
panelButtons?.isHidden = false
|
|
1293
|
+
panelReplyButton?.isHidden = false
|
|
1294
|
+
panelReplyButton?.keyEquivalent = "\r"
|
|
1295
|
+
if let c = panelBodyHeight, c.constant > 0 { c.constant += 20 }
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/// What a person may send on this item. Answer only on their own decision:
|
|
1299
|
+
/// anyone else's answer would land as an act and settle nothing.
|
|
1300
|
+
private func replyKinds(_ item: Item) -> [String] {
|
|
1301
|
+
item.isDecision ? ["Answer", "Note", "Ask back"] : ["Note", "Ask back"]
|
|
1302
|
+
}
|
|
1303
|
+
private func kindCode(_ title: String?) -> String {
|
|
1304
|
+
switch title { case "Answer": return "ANSWER"; case "Ask back": return "ASK"; default: return "NOTE" }
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/// Send the words in the box as the chosen act. The CLI does the sending and
|
|
1308
|
+
/// prints "sent KIND id=… seq=…" or "not sent: why" on stdout; both are shown.
|
|
1309
|
+
@objc private func panelSend(_ sender: Any?) {
|
|
1310
|
+
guard let item = panelItem, let tv = panelReply else { return }
|
|
1311
|
+
let text = tv.string.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1312
|
+
if text.isEmpty { setStatus("Type your reply first."); return }
|
|
1313
|
+
let kind = kindCode(panelReplyKind?.titleOfSelectedItem)
|
|
1314
|
+
var payload: [String: Any] = ["action": "reply", "kind": kind, "body": text, "goal": item.goal, "space": item.space]
|
|
1315
|
+
if item.isDecision { payload["decision"] = item.id }
|
|
1316
|
+
if !item.ref.isEmpty { payload["act"] = item.ref }
|
|
1317
|
+
guard writeAction(payload) else { setStatus("Could not write the action. Use Show the thread instead."); return }
|
|
1318
|
+
setStatus("Sending…")
|
|
1319
|
+
panelSendButton?.isEnabled = false
|
|
1320
|
+
DispatchQueue.global().async {
|
|
1321
|
+
let r = run(pantheonBin, ["explain", "--queued"], timeout: 25)
|
|
1322
|
+
DispatchQueue.main.async {
|
|
1323
|
+
self.panelSendButton?.isEnabled = true
|
|
1324
|
+
let out = (r?.out ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1325
|
+
if r?.code == 0, let idTok = out.split(separator: " ").first(where: { $0.hasPrefix("id=") }) {
|
|
1326
|
+
self.panelLastSent = String(idTok.dropFirst(3))
|
|
1327
|
+
tv.string = ""
|
|
1328
|
+
let what = kind == "ANSWER" ? "Answered — it is no longer waiting on you." : kind == "ASK" ? "Asked — the agent that raised this owes you a reply." : "Noted — it stays waiting."
|
|
1329
|
+
self.setStatus(what)
|
|
1330
|
+
self.exitReplyMode()
|
|
1331
|
+
self.panelRetract?.isHidden = false
|
|
1332
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 60) { [weak self] in self?.panelRetract?.isHidden = true }
|
|
1333
|
+
self.refresh()
|
|
1334
|
+
} else {
|
|
1335
|
+
let why = out.hasPrefix("not sent: ") ? String(out.dropFirst("not sent: ".count)) : "the relay could not be reached"
|
|
1336
|
+
self.setStatus("Not sent — \(why) Your words are still in the box.")
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/// The undo. RETRACT withdraws the act; the record keeps it as retracted.
|
|
1343
|
+
@objc private func panelRetractAction() {
|
|
1344
|
+
guard !panelLastSent.isEmpty else { return }
|
|
1345
|
+
let id = panelLastSent
|
|
1346
|
+
guard writeAction(["action": "retract", "act": id]) else { setStatus("Could not write the action."); return }
|
|
1347
|
+
setStatus("Retracting…")
|
|
1348
|
+
panelRetract?.isEnabled = false
|
|
1349
|
+
DispatchQueue.global().async {
|
|
1350
|
+
let r = run(pantheonBin, ["explain", "--queued"], timeout: 25)
|
|
1351
|
+
DispatchQueue.main.async {
|
|
1352
|
+
self.panelRetract?.isEnabled = true
|
|
1353
|
+
if r?.code == 0 {
|
|
1354
|
+
self.panelLastSent = ""
|
|
1355
|
+
self.panelRetract?.isHidden = true
|
|
1356
|
+
self.setStatus("Retracted.")
|
|
1357
|
+
self.refresh()
|
|
1358
|
+
} else {
|
|
1359
|
+
let out = (r?.out ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1360
|
+
self.setStatus("Could not retract — \(out.hasPrefix("not sent: ") ? String(out.dropFirst("not sent: ".count)) : "the relay could not be reached")")
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1125
1364
|
}
|
|
1126
1365
|
@objc private func panelOpen() {
|
|
1127
1366
|
guard let item = panelItem else { return }
|
|
@@ -1328,3 +1567,12 @@ let app = NSApplication.shared
|
|
|
1328
1567
|
let delegate = AppDelegate()
|
|
1329
1568
|
app.delegate = delegate
|
|
1330
1569
|
app.run()
|
|
1570
|
+
|
|
1571
|
+
extension AppDelegate: NSTextViewDelegate {
|
|
1572
|
+
func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
|
1573
|
+
guard textView === panelReply, commandSelector == #selector(NSResponder.insertNewline(_:)) else { return false }
|
|
1574
|
+
if NSApp.currentEvent?.modifierFlags.contains(.shift) == true { return false }
|
|
1575
|
+
panelSend(nil)
|
|
1576
|
+
return true
|
|
1577
|
+
}
|
|
1578
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@join-pantheon/cli",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.5",
|
|
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": {
|