pi-roundtable 0.10.0 → 0.11.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.
@@ -24,6 +24,7 @@ import {
24
24
  type CardAudienceOptions,
25
25
  CardAudiences,
26
26
  } from "./card-audience.ts";
27
+ import { CardCopies, copyReply, offeredCopy } from "./card-copies.ts";
27
28
  import { DiscordOwners, discordUser } from "./discord-owners.ts";
28
29
  import type { InteractionModule } from "./interaction-module.ts";
29
30
  import {
@@ -75,6 +76,13 @@ export interface OwnerCardsOptions {
75
76
  * answered. Without it no turn stops waiting on its cards.
76
77
  */
77
78
  resume?(turn: LateTurn): void;
79
+ /**
80
+ * Where and how long the text behind approval cards' copy buttons is kept. With a `path` it is
81
+ * a JSON file that survives a restart; without one the texts live in memory and a card's copy
82
+ * button stops working when the process restarts. `keep` (default 100) is how many cards'
83
+ * texts, the newest, and `keepDays` (default 30) how long.
84
+ */
85
+ copies?: { path?: string; keep?: number; keepDays?: number };
78
86
  }
79
87
 
80
88
  /**
@@ -91,11 +99,18 @@ export class OwnerCards implements InteractionModule {
91
99
  readonly #options: OwnerCardsOptions;
92
100
  readonly #audiences: CardAudiences;
93
101
  readonly #open = new Map<string, OpenCard>();
102
+ readonly #copies: CardCopies;
94
103
 
95
104
  constructor(options: OwnerCardsOptions) {
96
105
  this.#options = options;
106
+ this.#copies = new CardCopies({
107
+ ...options.copies,
108
+ logger: options.logger,
109
+ });
97
110
  this.#audiences = new CardAudiences(options, new DiscordOwners(options));
98
111
  }
112
+ /** Resolves once the remembered copy texts are on disk. */
113
+ flushCopies = (): Promise<void> => this.#copies.flush();
99
114
 
100
115
  commands() {
101
116
  return [];
@@ -114,9 +129,15 @@ export class OwnerCards implements InteractionModule {
114
129
  signal,
115
130
  minTier = "owner",
116
131
  wait,
117
- _details,
132
+ details,
118
133
  options,
119
134
  ) => {
135
+ const copy = offeredCopy(details?.copy, (chars) =>
136
+ this.#options.logger.warn(
137
+ { channelId, chars },
138
+ "card copy text too long; the card has no copy button",
139
+ ),
140
+ );
120
141
  const audience = await this.#audiences.approval(
121
142
  scope,
122
143
  minTier,
@@ -154,6 +175,7 @@ export class OwnerCards implements InteractionModule {
154
175
  : {}),
155
176
  title,
156
177
  sections: [message],
178
+ ...(copy ? { copy: copy.text } : {}),
157
179
  rows: (id, disabled) => [
158
180
  new ActionRowBuilder<ButtonBuilder>().addComponents(
159
181
  new ButtonBuilder()
@@ -168,6 +190,16 @@ export class OwnerCards implements InteractionModule {
168
190
  .setEmoji("❌")
169
191
  .setStyle(ButtonStyle.Secondary)
170
192
  .setDisabled(disabled),
193
+ // Copying answers nothing, so it stays usable once the card has ended.
194
+ ...(copy
195
+ ? [
196
+ new ButtonBuilder()
197
+ .setCustomId(`${CARD_PREFIX}${id}:copy`)
198
+ .setLabel(copy.label)
199
+ .setEmoji("📋")
200
+ .setStyle(ButtonStyle.Secondary),
201
+ ]
202
+ : []),
171
203
  ),
172
204
  ],
173
205
  footer: messages().cardApprovalFooter,
@@ -221,6 +253,8 @@ export class OwnerCards implements InteractionModule {
221
253
  audience: Audience;
222
254
  title: string;
223
255
  sections: string[];
256
+ /** The text behind the card's copy button, kept under the card's id. */
257
+ copy?: string;
224
258
  question?: OwnerQuestion;
225
259
  rows(id: string, disabled: boolean): Rows;
226
260
  footer: string;
@@ -236,6 +270,7 @@ export class OwnerCards implements InteractionModule {
236
270
  if (signal?.aborted) return Promise.resolve(cancelled);
237
271
  const wait = resume ? card.wait : undefined;
238
272
  const id = randomUUID();
273
+ if (card.copy !== undefined) this.#copies.remember(id, card.copy);
239
274
  const sections = [
240
275
  ...card.sections,
241
276
  ...(card.audience.note ? [card.audience.note] : []),
@@ -361,6 +396,17 @@ export class OwnerCards implements InteractionModule {
361
396
  const [id = "", action = ""] = interaction.customId
362
397
  .slice(CARD_PREFIX.length)
363
398
  .split(":");
399
+ // Copying answers nothing: anyone who sees the card may copy, as often as they like, also
400
+ // once the card has ended or the process restarted.
401
+ if (interaction.isButton() && action === "copy") {
402
+ const text = this.#copies.recall(id);
403
+ await interaction.reply(
404
+ text === undefined
405
+ ? { content: messages().cardCopyGone, flags: MessageFlags.Ephemeral }
406
+ : copyReply(text),
407
+ );
408
+ return true;
409
+ }
364
410
  const card = this.#open.get(id);
365
411
  if (!card) {
366
412
  await interaction.reply({
@@ -19,6 +19,9 @@ export interface AttachmentFailure {
19
19
  fromReference: boolean;
20
20
  }
21
21
 
22
+ /** The most images one turn carries to the model. */
23
+ export const MAX_TURN_IMAGES = 4;
24
+
22
25
  /** An image in the form the model accepts. */
23
26
  export interface ModelImage {
24
27
  data: string;
@@ -5,6 +5,10 @@ export function cardsEn(ctx: CatalogContext) {
5
5
  return {
6
6
  cardRunLabel: "Run",
7
7
  cardCancelLabel: "Cancel",
8
+ cardCopyLabel: "Copy",
9
+ cardCopyGone:
10
+ "The text of this card is no longer kept; ask the assistant to show it again.",
11
+ cardCopyFile: "The text is long, so it is attached as a file.",
8
12
  cardApprovalFooter:
9
13
  "Nothing runs until you approve; answer whenever you are ready.",
10
14
  cardQuestionFooter: "Answer whenever you are ready.",
@@ -39,6 +43,9 @@ export function cardsZhTW(ctx: CatalogContext): ReturnType<typeof cardsEn> {
39
43
  return {
40
44
  cardRunLabel: "執行",
41
45
  cardCancelLabel: "取消",
46
+ cardCopyLabel: "複製",
47
+ cardCopyGone: "這張卡片的文字已經不在了;請再請助理顯示一次。",
48
+ cardCopyFile: "文字比較長,所以用檔案附上。",
42
49
  cardApprovalFooter: "你核准之前不會執行;準備好再回覆就好。",
43
50
  cardQuestionFooter: "準備好再回答就好。",
44
51
  cardStopped: "⏹️ 這一輪已停止。",
@@ -35,6 +35,16 @@ export interface ApprovalDetails {
35
35
  input: Record<string, unknown>;
36
36
  /** The files the call sends by path; absent when it sends none. */
37
37
  files?: ApprovalFile[];
38
+ /**
39
+ * A text the card offers to copy, such as a command or a message the call would send. A surface
40
+ * that can shows a control, usable by anyone who sees the card, any number of times, that hands
41
+ * them `text` (`label` names the control, default the surface's own); using it never answers
42
+ * the prompt. It is informational: a surface that cannot show it ignores it. The text is
43
+ * visible to anyone who can see the card, so it holds nothing they may not read. A text over
44
+ * 20,000 characters is not offered (Discord logs a warning), and a surface with a size limit of
45
+ * its own leaves it out before it cuts the call's `input`.
46
+ */
47
+ copy?: { label?: string; text: string };
38
48
  }
39
49
 
40
50
  /**
@@ -6,10 +6,15 @@ export { DISCORD } from "../core/builtin/discord.ts";
6
6
  export type { DiscordAdapterConfig } from "../core/config/config.ts";
7
7
  export type {
8
8
  ChannelContext,
9
+ ChannelContextAttachment,
10
+ ChannelContextAttachmentOptions,
9
11
  ChannelContextMessage,
10
12
  ChannelContextOptions,
11
13
  } from "../core/contract/channel-context.ts";
12
- export { withChannelContext } from "../core/contract/channel-context.ts";
14
+ export {
15
+ withChannelContext,
16
+ withChannelContextImages,
17
+ } from "../core/contract/channel-context.ts";
13
18
  export type { AgentPanelMessage } from "../core/discord/agent-commands.ts";
14
19
  export type {
15
20
  AgentPanel,