pendingyou 0.38.0 → 0.40.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.40.0 (2026-10-28)
4
+
5
+ - Pending You for Herdr answers "Ask Maple again?": when one of your agents asks to send its card to another of your
6
+ assistants, one you handed one of its cards to before, the popup shows the card as Maple will see it, what Maple
7
+ helped with before and where its answer would go, and `y` says yes this time (the card goes to Maple, as handing it
8
+ over does, and `u` takes it back for 5 seconds) while `n` says no (the agent hears "Don’t send it to Maple"). Saying
9
+ "from now on" stays in Pending You. When Maple isn't one Herdr may hand the card to (it runs on another computer, or
10
+ it's gone), the card says to say yes in Pending You, and `n` still says no.
11
+ - A card your agent asked to send a teammate ("Send this to Doug?") says to answer it in Pending You, and `o` opens it:
12
+ no key answers it from Herdr. Before, the popup showed it as a plain card, and Pending You refused the answer.
13
+ - Both need a Pending You that says whom a card is to go to (2026-10-28); with an older one the popup is as before.
14
+
15
+ ## 0.39.0 (2026-10-25)
16
+
17
+ - In tmux, your agents' sessions now say which pane they run in, as they have in Herdr since 0.18.0: each presence
18
+ report carries the pane (`TMUX_PANE`, such as `%12`) and which tmux server it is (16 hex characters of a hash of the
19
+ computer's name and tmux's socket path, never the path itself). In a Herdr pane Herdr's is the one it says, even when
20
+ Herdr runs inside tmux. That's what lets an app you signed in on this computer bring the agent's tmux pane to the front
21
+ when you press Show on its card: the Pending You SDK's `show-in-tmux` example is one, to run on your own account.
22
+ Nothing else of tmux is read, and nothing is ever run in it. A Pending You from before tmux refuses the pane, and the
23
+ report goes again without it.
24
+
3
25
  ## 0.38.0 (2026-10-25)
4
26
 
5
27
  - Show in Herdr: press it on a card in Pending You, at your desk or on your phone, and Herdr moves to the pane of the
package/README.md CHANGED
@@ -365,7 +365,10 @@ has a Presence line for each agent.
365
365
  Since 0.18.0, a session in a [Herdr](https://herdr.dev) pane also says which pane (`HERDR_PANE_ID`, such as `w2:p1`)
366
366
  and which Herdr server (16 hex characters of a hash of the computer's name and Herdr's socket path, never the path
367
367
  itself), so Pending You can list where each of an agent's running sessions is and the Pending You plugin for Herdr can
368
- take you to it. Anywhere else nothing of the kind is sent.
368
+ take you to it. Since 0.39.0, a session in a tmux pane says which the same way (`TMUX_PANE`, such as `%12`, and 16 hex
369
+ characters of a hash of the computer's name and tmux's socket path), so an app you sign in on this computer can bring
370
+ it to the front when you press Show on its card; in a Herdr pane, even one inside tmux, Herdr's is the one it says.
371
+ Anywhere else nothing of the kind is sent.
369
372
 
370
373
  ## In Herdr
371
374
 
@@ -404,7 +407,8 @@ with a key: 1–9 for a choice (Enter sends several), y or n to approve or decli
404
407
  a step done or not, Tab between a grouped card's questions; r writes to the agent, l puts it in Later, d hands it to
405
408
  another of your agents, u undoes for 5 seconds, t goes to the agent's pane, o opens the card in Pending You. A
406
409
  high-stakes card (it spends money, can't be undone or goes public) is never answered from Herdr: o opens it where you
407
- hold to approve. Pending You says on the card and to the agent "Answered in Herdr on build-01". While you're active in
410
+ hold to approve. Since 0.40.0, when an agent asks to send its card to another of your assistants ("Ask Maple
411
+ again?"), y says yes this time and n says no; one it asks to send a teammate is answered in Pending You, and o opens it. Pending You says on the card and to the agent "Answered in Herdr on build-01". While you're active in
408
412
  Herdr (moving between panes, tabs and workspaces, or pressing keys in the popup) your phone stays quiet for those cards,
409
413
  as it does while Pending You is open on your desk.
410
414
 
@@ -637,7 +641,7 @@ whether the agent has a `pendingyou` MCP server and where it points. In a Herdr
637
641
  Herdr's own command line, on this computer, the badges above: a card's title (redacted, 80 characters at most) and its
638
642
  request id, never an answer or a note; `<app>-threads.json` remembers the pane each session runs in, and `herdr.json`
639
643
  whether the badges are off. Presence there (0.18.0) also sends Pending You the pane's id and the hash that names its
640
- Herdr server. With a sign-in that only hears, the hooks and
644
+ Herdr server, and in a tmux pane (0.39.0) the tmux pane's id and the hash that names its tmux server. With a sign-in that only hears, the hooks and
641
645
  listeners tell this computer's handed questions from another's by its names: its hostname, the names its sign-ins by
642
646
  code were given, a Mac's own name (`scutil --get ComputerName`) and, outside a hook, Tailscale's (`tailscale status`). For a permission prompt's card, the worker reads
643
647
  the end of the Claude Code session's transcript only for the name the session gave Pending You, and `git remote
@@ -22,6 +22,17 @@ export const askerOf = (card) => `${card.asker.agent?.name ?? card.asker.assista
22
22
  const askerName = (card) => card.asker.agent?.name ?? card.asker.assistant.name;
23
23
  export const HIGH_STAKES = 'High stakes: answer it in Pending You, where you hold to approve. o opens it.';
24
24
  export const NOT_HERE = 'Answer this one in Pending You: o opens it.';
25
+ export const SAY_YES_THERE = 'Say yes in Pending You: o opens it.';
26
+ const ASK_AGAIN_KEYS = 'y: yes, this time; n: no.';
27
+ export const sendToOf = (card) => card.sendTo && (card.status === 'pending' || card.status === 'snoozed') ? card.sendTo : null;
28
+ const asksAgain = (sendTo) => sendTo.kind === 'assistant';
29
+ export const toTeammate = (card) => `Send this to ${card.sendTo?.name ?? 'them'}? Answer it in Pending You: o opens it.`;
30
+ const dayOf = (at) => {
31
+ const when = new Date(at);
32
+ return Number.isNaN(when.getTime())
33
+ ? ''
34
+ : when.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
35
+ };
25
36
  const KINDS = [
26
37
  'choice',
27
38
  'multi',
@@ -38,7 +49,8 @@ const QUESTION_KINDS = [
38
49
  'approve',
39
50
  'text',
40
51
  ];
41
- const answersHere = (card) => card.actions.includes('answer') &&
52
+ const answersHere = (card) => !sendToOf(card) &&
53
+ card.actions.includes('answer') &&
42
54
  KINDS.includes(card.kind) &&
43
55
  (card.questions ?? []).every((question) => QUESTION_KINDS.includes(question.kind));
44
56
  const NO_WORDS = 'Pending You takes no words on this card from Herdr: o opens it.';
@@ -145,10 +157,51 @@ function listKey(state, key, now) {
145
157
  { kind: 'targets', card },
146
158
  ];
147
159
  }
160
+ const sendTo = sendToOf(card);
161
+ if (sendTo)
162
+ return sendToKey(state, card, sendTo, key);
148
163
  if (!answersHere(card))
149
164
  return [{ ...state, note: NOT_HERE }, NONE];
150
165
  return answerKey(state, card, key);
151
166
  }
167
+ function sendToKey(state, card, sendTo, key) {
168
+ if (!asksAgain(sendTo))
169
+ return [{ ...state, note: toTeammate(card) }, NONE];
170
+ if (key === 'y') {
171
+ const to = sendTo.to;
172
+ if (!to || !card.actions.includes('delegate'))
173
+ return [{ ...state, note: SAY_YES_THERE }, NONE];
174
+ return [
175
+ { ...state, note: '' },
176
+ {
177
+ kind: 'delegate',
178
+ card,
179
+ target: {
180
+ assistantId: to.assistantId,
181
+ agentId: to.agentId,
182
+ name: sendTo.name,
183
+ label: sendTo.name,
184
+ detail: '',
185
+ },
186
+ mode: sendTo.mode === 'freely' ? 'freely' : 'loop',
187
+ },
188
+ ];
189
+ }
190
+ if (key === 'n') {
191
+ if (!card.actions.includes('answer'))
192
+ return [{ ...state, note: NOT_HERE }, NONE];
193
+ return [
194
+ { ...state, note: '' },
195
+ {
196
+ kind: 'answer',
197
+ card,
198
+ answer: { version: card.version, notSent: true },
199
+ words: sendTo.name,
200
+ },
201
+ ];
202
+ }
203
+ return [{ ...state, note: ASK_AGAIN_KEYS }, NONE];
204
+ }
152
205
  function answerKey(state, card, key) {
153
206
  const send = (answer, words) => [
154
207
  { ...state, note: '' },
@@ -423,6 +476,7 @@ export function targetsFor(card, assistants) {
423
476
  continue;
424
477
  targets.push({
425
478
  assistantId: assistant.id,
479
+ name: assistant.name,
426
480
  label: `${assistant.name} (${assistant.appName})`,
427
481
  detail: [...where].join(' · ') || 'in the cloud',
428
482
  });
@@ -435,6 +489,7 @@ export function targetsFor(card, assistants) {
435
489
  targets.push({
436
490
  assistantId: assistant.id,
437
491
  agentId: agent.id,
492
+ name: agent.name,
438
493
  label: `${agent.name} (${assistant.appName})`,
439
494
  detail: [agent.state, ...where, ...(pane ? [`pane ${pane}`] : [])].join(' · '),
440
495
  });
@@ -516,10 +571,12 @@ export function headingOf(state) {
516
571
  function askLines(card, state, room, now) {
517
572
  const fit = (text) => cleanValue(text, room);
518
573
  const lines = [];
574
+ const sendTo = sendToOf(card);
575
+ const keyOf = (index) => (sendTo ? '·' : `${index + 1}`);
519
576
  const options = (list, recommended = [], chosen) => (list ?? []).slice(0, KEYS_MAX).map((option, index) => {
520
- const box = chosen ? (chosen.includes(option.id) ? '[x] ' : '[ ] ') : '';
577
+ const box = chosen && !sendTo ? (chosen.includes(option.id) ? '[x] ' : '[ ] ') : '';
521
578
  const star = recommended.includes(option.id) ? '★ ' : '';
522
- return fit(`${box}${index + 1} ${star}${option.label}${option.detail ? ` · ${option.detail}` : ''}`);
579
+ return fit(`${box}${keyOf(index)} ${star}${option.label}${option.detail ? ` · ${option.detail}` : ''}`);
523
580
  });
524
581
  const more = (list) => (list?.length ?? 0) > KEYS_MAX
525
582
  ? [fit(`… and ${(list?.length ?? 0) - KEYS_MAX} more: o opens the card`)]
@@ -544,7 +601,7 @@ function askLines(card, state, room, now) {
544
601
  if (card.text?.placeholder)
545
602
  lines.push(DIM(fit(card.text.placeholder)));
546
603
  for (const [index, suggestion] of (card.text?.suggestions ?? []).slice(0, KEYS_MAX).entries())
547
- lines.push(fit(`${index + 1} ${suggestion}`));
604
+ lines.push(fit(`${keyOf(index)} ${suggestion}`));
548
605
  break;
549
606
  case 'action':
550
607
  for (const [index, step] of steps(card).entries()) {
@@ -561,6 +618,11 @@ function askLines(card, state, room, now) {
561
618
  break;
562
619
  case 'group': {
563
620
  const questions = card.questions ?? [];
621
+ if (sendTo) {
622
+ for (const question of questions)
623
+ lines.push(fit(`· ${question.title}`));
624
+ break;
625
+ }
564
626
  for (const [index, question] of questions.entries()) {
565
627
  const answer = state.answers[question.id];
566
628
  const mark = index === state.question ? '▸' : answer ? '✓' : '·';
@@ -585,12 +647,44 @@ function askLines(card, state, room, now) {
585
647
  }
586
648
  if (card.fallback)
587
649
  lines.push(DIM(fit(`If you don’t answer ${fromNow(card.fallback.at, now)}: ${card.fallback.text}`)));
650
+ if (sendTo)
651
+ lines.push(...sendToLines(card, sendTo, room));
588
652
  if (card.highStakes)
589
653
  lines.push(...wrap(HIGH_STAKES, room, 2));
590
- else if (!answersHere(card))
654
+ else if (!sendTo && !answersHere(card))
591
655
  lines.push(...wrap(NOT_HERE, room, 2));
592
656
  return lines;
593
657
  }
658
+ function sendToLines(card, sendTo, room) {
659
+ if (!asksAgain(sendTo))
660
+ return wrap(toTeammate(card), room, 2);
661
+ const name = sendTo.name;
662
+ const helped = sendTo.helped;
663
+ const on = helped && dayOf(helped.at) ? ` on ${dayOf(helped.at)}` : '';
664
+ const before = helped
665
+ ? helped.answered
666
+ ? `${name} helped with “${helped.title}”${on}.`
667
+ : `You handed ${name} “${helped.title}”${on}.`
668
+ : null;
669
+ const goes = sendTo.mode === 'freely'
670
+ ? `${name}’s answer goes straight to ${askerName(card)}.`
671
+ : `${name}’s answer comes back to you first.`;
672
+ const yesHere = Boolean(sendTo.to) && card.actions.includes('delegate');
673
+ return [
674
+ BOLD(cleanValue(`Ask ${name} again?`, room)),
675
+ ...(before ? wrap(before, room, 2) : []),
676
+ ...wrap(goes, room, 2),
677
+ ...(yesHere || card.highStakes ? [] : wrap(SAY_YES_THERE, room, 2)),
678
+ ];
679
+ }
680
+ function sendToKeys(card, sendTo) {
681
+ if (!asksAgain(sendTo))
682
+ return [];
683
+ return [
684
+ ...(sendTo.to && card.actions.includes('delegate') ? ['y yes, this time'] : []),
685
+ ...(card.actions.includes('answer') ? ['n no'] : []),
686
+ ];
687
+ }
594
688
  function answerWords(question, answer) {
595
689
  if (answer.text)
596
690
  return answer.text;
@@ -657,7 +751,10 @@ export function keysLine(state, now) {
657
751
  if (card) {
658
752
  const busy = state.sent?.cardId === card.id && now < state.sent.until;
659
753
  if (!card.highStakes && !busy) {
660
- if (answersHere(card))
754
+ const sendTo = sendToOf(card);
755
+ if (sendTo)
756
+ keys.push(...sendToKeys(card, sendTo));
757
+ else if (answersHere(card))
661
758
  keys.push(...answerKeys(card, state));
662
759
  if (card.actions.includes('reply'))
663
760
  keys.push('r reply');
@@ -680,6 +777,8 @@ export function sentLine(state, now) {
680
777
  switch (sent.what) {
681
778
  case 'answer':
682
779
  return `Sent: ${sent.words} · ${undo}`;
780
+ case 'not_sent':
781
+ return `Not sent to ${sent.words} · ${undo}`;
683
782
  case 'later':
684
783
  return `In Later: ${sent.words.toLowerCase()} · ${undo}`;
685
784
  case 'delegate':
@@ -344,20 +344,22 @@ export async function queuePopup(io, place, options) {
344
344
  return false;
345
345
  }
346
346
  }
347
- case 'answer':
347
+ case 'answer': {
348
+ const what = effect.answer.notSent ? 'not_sent' : 'answer';
348
349
  if (!client) {
349
- state = sentState(state, { cardId: effect.card.id, what: 'answer', words: effect.words }, now);
350
+ state = sentState(state, { cardId: effect.card.id, what, words: effect.words }, now);
350
351
  return false;
351
352
  }
352
353
  try {
353
354
  const done = await client.cards.answer(effect.card.id, effect.answer);
354
- state = sentState(state, { cardId: effect.card.id, what: 'answer', words: effect.words, until: done.undoUntil }, io.now());
355
+ state = sentState(state, { cardId: effect.card.id, what, words: effect.words, until: done.undoUntil }, io.now());
355
356
  }
356
357
  catch (error) {
357
358
  fail(error);
358
359
  stale = true;
359
360
  }
360
361
  return false;
362
+ }
361
363
  case 'undo': {
362
364
  if (!client) {
363
365
  state = { ...state, sent: null, note: 'Undone: it’s waiting on you again.' };
@@ -365,7 +367,7 @@ export async function queuePopup(io, place, options) {
365
367
  }
366
368
  try {
367
369
  const { cardId, what } = effect.sent;
368
- if (what === 'answer')
370
+ if (what === 'answer' || what === 'not_sent')
369
371
  await client.cards.undo(cardId);
370
372
  else if (what === 'later')
371
373
  await client.cards.back(cardId);
@@ -412,7 +414,7 @@ export async function queuePopup(io, place, options) {
412
414
  return false;
413
415
  }
414
416
  case 'delegate': {
415
- const name = effect.target.label.replace(/ \(.*\)$/, '');
417
+ const name = effect.target.name ?? effect.target.label.replace(/ \(.*\)$/, '');
416
418
  if (!client) {
417
419
  state = sentState(state, { cardId: effect.card.id, what: 'delegate', words: name }, now);
418
420
  return false;
package/dist/presence.js CHANGED
@@ -6,8 +6,9 @@ import { reportThreads, threadNames } from "./apps/codex-wake.js";
6
6
  import { DEFAULT_ORIGIN } from "./args.js";
7
7
  import { readCredential } from "./credentials.js";
8
8
  import { configDir, readJson, withLock, writeWhole } from "./files.js";
9
- import { herdrTarget, herdrTerminal, reportCommand } from "./herdr.js";
9
+ import { herdrTarget, reportCommand } from "./herdr.js";
10
10
  import { endPrompts, sessionName } from "./permission.js";
11
+ import { terminalOf } from "./terminal.js";
11
12
  export const PRESENCE_PATH = '/mcp/cli/presence';
12
13
  export const PRESENCE_EVERY_MS = 5 * 60_000;
13
14
  export const LIVE_AGAIN_MS = 4 * 60_000;
@@ -67,7 +68,7 @@ export async function sendPresence(io, origin, presence, timeoutMs = SEND_MS, op
67
68
  cwd: presence.cwd && presence.cwd.length <= CWD_MAX ? presence.cwd : null,
68
69
  state: presence.state,
69
70
  };
70
- const terminal = herdrTerminal(io.env, io.host);
71
+ const terminal = terminalOf(io.env, io.host);
71
72
  let body = terminal ? { ...plain, terminal } : plain;
72
73
  const send = (force) => postJson(io, origin, PRESENCE_PATH, body, timeoutMs, {
73
74
  app: presence.app,
@@ -0,0 +1,20 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { isAbsolute } from 'node:path';
3
+ import { herdrTerminal } from "./herdr.js";
4
+ export const TMUX_PANE = /^%[0-9]{1,9}$/;
5
+ export function tmuxServer(env, host) {
6
+ const [socket = ''] = (env.TMUX ?? '').split(',');
7
+ if (!socket || !isAbsolute(socket) || /[\0\r\n]/.test(socket))
8
+ return null;
9
+ return createHash('sha256').update(`${host}${socket}`).digest('hex').slice(0, 16);
10
+ }
11
+ export function tmuxTerminal(env, host) {
12
+ const pane = env.TMUX_PANE ?? '';
13
+ if (!env.TMUX || !TMUX_PANE.test(pane))
14
+ return null;
15
+ const server = tmuxServer(env, host);
16
+ return { app: 'tmux', paneId: pane, ...(server ? { server } : {}) };
17
+ }
18
+ export function terminalOf(env, host) {
19
+ return herdrTerminal(env, host) ?? tmuxTerminal(env, host);
20
+ }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.38.0';
1
+ export const VERSION = '0.40.0';
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pendingyou-wake",
3
3
  "displayName": "Pending You",
4
- "version": "2.56.0",
4
+ "version": "2.58.0",
5
5
  "description": "Wakes Claude Code when your person answers a Pending You card it posted, even while it sits idle. Installed by `npx pendingyou init`.",
6
6
  "author": {
7
7
  "name": "RecordPlane",
package/mod/README.md CHANGED
@@ -4,4 +4,4 @@ The pendingyou command line’s copy of the Pending You plugin’s wake mod, for
4
4
 
5
5
  In each session it learns the cards the session posts to Pending You, checks them through the session’s own Pending You connection, and starts a turn when your person answers, writes back or a fallback comes due, or hands the session a question from another of their assistants (CLI 0.14.0), even while it sits idle. It answers `npx pendingyou hold` itself, so nothing waits in the background, and shows “2 waiting on you” under the prompt, after the plugin’s name. With the Pending You plugin installed too, only one copy acts.
6
6
 
7
- Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.56.0; send changes there.
7
+ Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.58.0; send changes there.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pendingyou",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "description": "Pending You for your coding agents: one command sets up Claude Code, Codex, OpenCode and Pi to ask you through Pending You and hear your answers, even while they sit idle.",
5
5
  "keywords": [
6
6
  "pendingyou",