@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 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));
@@ -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 who = `${a.from}${a.role && a.role !== "interactive" ? ` (${a.role})` : ""}`;
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));
@@ -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({ title, acceptance, ...(invite ? { invite } : {}), ...(deadlineMs !== undefined ? { deadline: deadlineMs } : {}) });
440
- return text(`goal ${r.id} opened: "${title}"${deadlineMs !== undefined ? `\n${deadlineLabel(deadlineMs).trim()}` : ""}` +
441
- `\n\nNext: PROPOSE how to split it, then wait for peers to ACCEPT or OBJECT.`);
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
- return this.json(res, 200, { acts: space.ledger.since(Number(url.searchParams.get("since") ?? 0)) });
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
- if (goal.acts >= goal.budget) {
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
  ];
@@ -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
@@ -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 panelRawToggle: NSButton?
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.panelRawToggle?.isHidden = raw.isEmpty
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
- panelRawToggle?.title = panelShowingRaw ? "Show it laid out" : "Show it as sent"
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
- let status = label("", size: 11, weight: .medium, color: .secondaryLabelColor, width: inner)
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
- stack.addArrangedSubview(status)
1044
-
1045
- let row = NSStackView()
1046
- row.orientation = .horizontal
1047
- row.spacing = 10
1048
- row.translatesAutoresizingMaskIntoConstraints = false
1049
- let open = NSButton(title: "Open in \(state.surfaceName)", target: self, action: #selector(panelOpen))
1050
- open.bezelStyle = .rounded
1051
- open.keyEquivalent = "\r"
1052
- row.addArrangedSubview(open)
1053
- let copy = NSButton(title: "Copy the question", target: self, action: #selector(panelCopy))
1054
- copy.bezelStyle = .rounded
1055
- row.addArrangedSubview(copy)
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
- let done = NSButton(title: "Mark done", target: self, action: #selector(panelDone))
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
- let thread = NSButton(title: "Show the thread", target: self, action: #selector(panelThread))
1063
- thread.bezelStyle = .rounded
1064
- row.addArrangedSubview(thread)
1065
- let rawToggle = NSButton(title: "Show it as sent", target: self, action: #selector(panelToggleRaw))
1066
- rawToggle.bezelStyle = .rounded
1067
- rawToggle.toolTip = "The act exactly as your teammate's agent wrote it"
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
- panelRawToggle = rawToggle
1070
- row.addArrangedSubview(rawToggle)
1071
- let close = NSButton(title: "Close", target: self, action: #selector(panelClose))
1072
- close.bezelStyle = .rounded
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
- row.addArrangedSubview(close)
1075
- stack.addArrangedSubview(row)
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
- row.widthAnchor.constraint(lessThanOrEqualTo: stack.widthAnchor),
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
- panelRawToggle = nil
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
- copyPrompt(item)
1124
- setStatus("Copied. Paste it into your agent.")
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",
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": {