pendingyou 0.39.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,17 @@
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
+
3
15
  ## 0.39.0 (2026-10-25)
4
16
 
5
17
  - In tmux, your agents' sessions now say which pane they run in, as they have in Herdr since 0.18.0: each presence
package/README.md CHANGED
@@ -407,7 +407,8 @@ with a key: 1–9 for a choice (Enter sends several), y or n to approve or decli
407
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
408
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
409
409
  high-stakes card (it spends money, can't be undone or goes public) is never answered from Herdr: o opens it where you
410
- 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
411
412
  Herdr (moving between panes, tabs and workspaces, or pressing keys in the popup) your phone stays quiet for those cards,
412
413
  as it does while Pending You is open on your desk.
413
414
 
@@ -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/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.39.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.39.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",