@mercury-fw/channel-google-chat 0.8.0 → 0.8.2

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,21 @@
1
1
  # @mercury-fw/channel-google-chat
2
2
 
3
+ ## 0.8.2
4
+
5
+ ### Patch Changes
6
+
7
+ - edfebb5: - `@mercury-fw/plugin-web-search`: an answer no longer lists its sources twice. When the model writes its own list of sources at the end of the answer, the plugin's `Fonti:` list replaces it, and `web_fetch`'s description tells the model the pages it reads are listed for it.
8
+ - `@mercury-fw/channel-google-chat`: once a choice on a confirmation card is taken, the card shows it ("Scelto: …") instead of the buttons. A click that confirms nothing (expired, or someone else's in a shared space) leaves the card as it is.
9
+ - `@mercury-fw/channel-types`: `confirmReply(outcome)`, the reply to each confirmation outcome, for a channel that resolves a token through `resolveConfirmation`.
10
+ - `@mercury-fw/confirm-engine`: `tryConfirm` answers with `confirmReply`, same texts as before.
11
+
12
+ ## 0.8.1
13
+
14
+ ### Patch Changes
15
+
16
+ - 4431285: - plugin-web-search README: the minimum core (0.53), the Italian labels the person reads, where a person's list lives and how to edit it, and a config example matching the app `mfw create` writes.
17
+ - channel-google-chat README: the confirmation card has a button per choice, and a choice whose result goes back to the model runs the turn again.
18
+
3
19
  ## 0.8.0
4
20
 
5
21
  ### Minor Changes
package/README.md CHANGED
@@ -28,6 +28,8 @@ Each message reaches the core as `google-chat:users/<id>`, the sender's Chat use
28
28
 
29
29
  When a plugin that acts as the person (Jira, Bitbucket) needs them to log in, the channel sends a card with a button that opens the service's login, visible only to that person: in a direct message the space is already theirs, in a shared space the card is a private message to whoever asked, since anyone else opening it would log their own account in as them. The service sends the browser back to the HTTP channel's `/login/callback`, so the app needs [`@mercury-fw/channel-http`](https://www.npmjs.com/package/@mercury-fw/channel-http) with `HTTP_SURFACE_PUBLIC_URL` too; without it Mercury answers that nobody can log in on this instance.
30
30
 
31
+ When a tool stages an action that needs confirming, the channel sends a card with the action and a button per choice the tool offers: one `Conferma` for a plain staging (deleting a Jira issue, say), more when there's a choice to make (the web search plugin offers "Solo questa volta" and "Sempre per \<host\>" before reading a page on a host nobody allowed). Clicking a button confirms that choice and the others die with it, and the card shows the choice taken instead of the buttons; typing the token in the conversation does the same, leaving the card as it is. A click that confirms nothing (the request expired, or in a shared space it's someone else's) leaves the card for whoever has to choose. When the confirmed action's result goes back to the model (a page read after the person allowed it), the channel runs the conversation's turn again instead of sending the raw result, so the answer comes from the model, built on what came back.
32
+
31
33
  ## Table of contents
32
34
 
33
35
  - [Setting up the Chat app](#setting-up-the-chat-app)
@@ -21,7 +21,7 @@
21
21
  */
22
22
  import { sendMessage, sendCard, updateCard, getOrCreateDmSpace, type ServiceAccountCredentials, type TokenSource } from "./google-chat-app-client.ts";
23
23
  import { type PubSubSubscription } from "./google-chat-pubsub-stream.ts";
24
- import { type Provider, type Principal } from "@mercury-fw/channel-types";
24
+ import { type ConfirmOutcome, type Provider, type Principal } from "@mercury-fw/channel-types";
25
25
  /**
26
26
  * Composite session key: space + sender. Deliberately does NOT include
27
27
  * `thread` — confirmed live (two consecutive messages in the same DM
@@ -54,6 +54,8 @@ export type ParsedMessageEvent = {
54
54
  };
55
55
  export type ParsedCardClickEvent = {
56
56
  kind: "card-click";
57
+ /** The message holding the clicked card, when the event names it. */
58
+ messageName?: string;
57
59
  space: string;
58
60
  sender: string;
59
61
  senderEmail: string | undefined;
@@ -63,18 +65,25 @@ export type ParsedCardClickEvent = {
63
65
  };
64
66
  /** Parses one decoded Pub/Sub event, or `null` if it isn't a kind this provider acts on. */
65
67
  export declare function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardClickEvent | null;
66
- export type CardClickHandler = (params: Record<string, string>, space: string, sender: string, senderEmail?: string, isDirectMessage?: boolean) => Promise<void>;
68
+ export type CardClickHandler = (params: Record<string, string>, space: string, sender: string, senderEmail?: string, isDirectMessage?: boolean, messageName?: string) => Promise<void>;
67
69
  export type GoogleChatProviderDeps = {
68
70
  credentials: ServiceAccountCredentials;
69
71
  subscription: string;
70
72
  /**
71
73
  * Resolves a confirmation token against the core's shared store, injected by
72
74
  * the channel loader (`ChannelRuntimeContext.confirm`). Returns the reply to
73
- * send, or `null` when the input wasn't token-shaped. This is the whole of
74
- * the channel's contact with confirmation — the store, vault and note-writer
75
- * stay in the core.
75
+ * send, or `null` when the input wasn't token-shaped. With
76
+ * `resolveConfirmation` below, the channel's whole contact with confirmation:
77
+ * the store, vault and note-writer stay in the core.
76
78
  */
77
79
  confirm: (token: string, sessionKey: string, principal: Principal) => Promise<string | null>;
80
+ /**
81
+ * The structured sibling of `confirm` (`ChannelRuntimeContext.resolveConfirmation`).
82
+ * When it's there, a click resolves through it, and an accepted choice
83
+ * turns its card into the choice taken; without it, a click goes through
84
+ * `confirm` and the card stays as it was.
85
+ */
86
+ resolveConfirmation?: (token: string, sessionKey: string, principal: Principal) => Promise<ConfirmOutcome>;
78
87
  /**
79
88
  * Handles a `CARD_CLICKED` event's action parameters. Defaults to resolving
80
89
  * the confirm button's token through `deps.confirm` (the same path a bare
@@ -37,6 +37,8 @@ import {
37
37
  loginLinkText,
38
38
  PENDING_CONFIRMATION_NOTE,
39
39
  RESUME_AFTER_CONFIRM,
40
+ confirmReply,
41
+ type ConfirmOutcome,
40
42
  NO_REPLY,
41
43
  type LoginRequest,
42
44
  type PendingConfirmation,
@@ -53,7 +55,9 @@ import {
53
55
  * (a single `Conferma` for a plain staging). Each button's parameters carry
54
56
  * the token, so a click routes straight into the same `tryConfirm` path
55
57
  * a bare token typed on the terminal uses (see `onCardClick`'s `confirm`
56
- * case, below) — the user never has to see or type the token themselves.
58
+ * case, below) — the user never has to see or type the token themselves —
59
+ * plus its label and the summary, so the click can turn the card into
60
+ * `buildChosenCard` without the channel keeping any state.
57
61
  */
58
62
  function buildConfirmCard(pending: PendingConfirmation): ChatCard {
59
63
  return {
@@ -66,7 +70,16 @@ function buildConfirmCard(pending: PendingConfirmation): ChatCard {
66
70
  buttonList: {
67
71
  buttons: pending.choices.map((choice) => ({
68
72
  text: choice.label,
69
- onClick: { action: { function: "confirm", parameters: [{ key: "token", value: choice.token }] } },
73
+ onClick: {
74
+ action: {
75
+ function: "confirm",
76
+ parameters: [
77
+ { key: "token", value: choice.token },
78
+ { key: "label", value: choice.label },
79
+ { key: "summary", value: pending.summary },
80
+ ],
81
+ },
82
+ },
70
83
  })),
71
84
  },
72
85
  },
@@ -76,6 +89,21 @@ function buildConfirmCard(pending: PendingConfirmation): ChatCard {
76
89
  };
77
90
  }
78
91
 
92
+ /** The confirmation card once a choice is taken: the same request, the choice instead of the buttons. */
93
+ function buildChosenCard(summary: string, label: string | undefined): ChatCard {
94
+ return {
95
+ header: { title: "Conferma richiesta" },
96
+ sections: [
97
+ {
98
+ widgets: [
99
+ { textParagraph: { text: `\`${summary}\`` } },
100
+ { textParagraph: { text: label === undefined ? "Scelta registrata." : `Scelto: ${label}` } },
101
+ ],
102
+ },
103
+ ],
104
+ };
105
+ }
106
+
79
107
  /** The card that shows a person the link to log in to a service a plugin acts on as them: one button that opens it. */
80
108
  function buildLoginCard(login: LoginRequest): ChatCard {
81
109
  return {
@@ -221,6 +249,8 @@ export type ParsedMessageEvent = {
221
249
 
222
250
  export type ParsedCardClickEvent = {
223
251
  kind: "card-click";
252
+ /** The message holding the clicked card, when the event names it. */
253
+ messageName?: string;
224
254
  space: string;
225
255
  sender: string;
226
256
  senderEmail: string | undefined;
@@ -264,6 +294,7 @@ export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardCli
264
294
  }
265
295
  return {
266
296
  kind: "card-click",
297
+ ...(typeof event.message?.name === "string" ? { messageName: event.message.name } : {}),
267
298
  space: event.space.name,
268
299
  sender: event.user.name,
269
300
  senderEmail: emailOf(event.user),
@@ -280,6 +311,7 @@ export type CardClickHandler = (
280
311
  sender: string,
281
312
  senderEmail?: string,
282
313
  isDirectMessage?: boolean,
314
+ messageName?: string,
283
315
  ) => Promise<void>;
284
316
 
285
317
  export type GoogleChatProviderDeps = {
@@ -288,11 +320,18 @@ export type GoogleChatProviderDeps = {
288
320
  /**
289
321
  * Resolves a confirmation token against the core's shared store, injected by
290
322
  * the channel loader (`ChannelRuntimeContext.confirm`). Returns the reply to
291
- * send, or `null` when the input wasn't token-shaped. This is the whole of
292
- * the channel's contact with confirmation — the store, vault and note-writer
293
- * stay in the core.
323
+ * send, or `null` when the input wasn't token-shaped. With
324
+ * `resolveConfirmation` below, the channel's whole contact with confirmation:
325
+ * the store, vault and note-writer stay in the core.
294
326
  */
295
327
  confirm: (token: string, sessionKey: string, principal: Principal) => Promise<string | null>;
328
+ /**
329
+ * The structured sibling of `confirm` (`ChannelRuntimeContext.resolveConfirmation`).
330
+ * When it's there, a click resolves through it, and an accepted choice
331
+ * turns its card into the choice taken; without it, a click goes through
332
+ * `confirm` and the card stays as it was.
333
+ */
334
+ resolveConfirmation?: (token: string, sessionKey: string, principal: Principal) => Promise<ConfirmOutcome>;
296
335
  /**
297
336
  * Handles a `CARD_CLICKED` event's action parameters. Defaults to resolving
298
337
  * the confirm button's token through `deps.confirm` (the same path a bare
@@ -383,11 +422,14 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
383
422
  * Default `onCardClick`: the confirm button's token routes through the exact
384
423
  * same `deps.confirm` a bare token typed on the terminal uses — one execution
385
424
  * path, one set of valid/expired/wrong-session-token behaviors, regardless of
386
- * how the token got here.
425
+ * how the token got here. A click the core accepted (the action ran, failed,
426
+ * or the turn resumes) turns its card into the choice taken, before
427
+ * anything else; one it didn't (`not-found`: expired, used, or someone
428
+ * else's in a shared space) leaves the card for whoever has to choose.
387
429
  */
388
430
  const onCardClick: CardClickHandler =
389
431
  deps.onCardClick ??
390
- (async (params, space, sender, senderEmail, isDirectMessage = false) => {
432
+ (async (params, space, sender, senderEmail, isDirectMessage = false, messageName) => {
391
433
  const token = params.token;
392
434
  if (!token) {
393
435
  log(`[chat] card click with no token parameter`);
@@ -398,7 +440,26 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
398
440
  provider: "google-chat",
399
441
  ...(senderEmail === undefined ? {} : { claims: { email: senderEmail } }),
400
442
  };
401
- const reply = await deps.confirm(token, deriveSessionKey(space, sender), principal);
443
+ const sessionKey = deriveSessionKey(space, sender);
444
+ let reply: string | null;
445
+ if (deps.resolveConfirmation) {
446
+ const outcome = await deps.resolveConfirmation(token, sessionKey, principal);
447
+ const accepted = outcome.status === "ok" || outcome.status === "failed" || outcome.status === "resume";
448
+ if (accepted && messageName !== undefined && params.summary !== undefined) {
449
+ try {
450
+ await updateCardFn(messageName, buildChosenCard(params.summary, params.label), clientDeps);
451
+ log(`[chat:${space}] [out] confirm card ${messageName} patched to the choice taken`);
452
+ } catch (err) {
453
+ log(`[chat:${space}] confirm card patch failed: ${String(err)}`);
454
+ }
455
+ }
456
+ // On not-found nothing was taken, so `confirm` gives the reply as it
457
+ // always did: the core's own message for someone it won't talk to,
458
+ // which the outcome doesn't carry.
459
+ reply = outcome.status === "not-found" ? await deps.confirm(token, sessionKey, principal) : confirmReply(outcome);
460
+ } else {
461
+ reply = await deps.confirm(token, sessionKey, principal);
462
+ }
402
463
  if (reply === RESUME_AFTER_CONFIRM && activeHandleTurn) {
403
464
  await resumeTurn(activeHandleTurn, space, principal, isDirectMessage);
404
465
  return;
@@ -648,7 +709,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
648
709
  if (parsed.kind === "message") {
649
710
  await processMessageEvent(parsed, handleTurn);
650
711
  } else {
651
- await onCardClick(parsed.parameters, parsed.space, parsed.sender, parsed.senderEmail, parsed.isDirectMessage);
712
+ await onCardClick(parsed.parameters, parsed.space, parsed.sender, parsed.senderEmail, parsed.isDirectMessage, parsed.messageName);
652
713
  }
653
714
  } catch (err) {
654
715
  log(`[chat] event handling failed: ${String(err)}`);
package/index.ts CHANGED
@@ -39,6 +39,7 @@ export const googleChatChannel: ChannelPlugin = {
39
39
  },
40
40
  subscription,
41
41
  confirm: ctx.confirm,
42
+ ...(ctx.resolveConfirmation === undefined ? {} : { resolveConfirmation: ctx.resolveConfirmation }),
42
43
  log: ctx.log,
43
44
  });
44
45
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/channel-google-chat",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,10 +31,10 @@
31
31
  "@google-cloud/pubsub": "^5.3.1"
32
32
  },
33
33
  "peerDependencies": {
34
- "@mercury-fw/channel-types": ">=0.51.0 <1.0.0"
34
+ "@mercury-fw/channel-types": ">=0.53.1 <1.0.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@mercury-fw/channel-types": "0.51.0",
37
+ "@mercury-fw/channel-types": "0.53.1",
38
38
  "@mercury-fw/typescript-config": "*",
39
39
  "@types/bun": "^1.4.2",
40
40
  "typescript": "^6.0.3"