@mercury-fw/channel-google-chat 0.6.0 → 0.7.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,13 @@
1
1
  # @mercury-fw/channel-google-chat
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4184ea6: - `loginLinkText` words a login link the same way on every channel, and the contract states that a login link is shown only to the person it's for.
8
+ - Google Chat shows the login to a service a plugin acts on as the person (Jira, Bitbucket) as a button that opens it, visible only to them: in a shared space it's a private message to whoever asked.
9
+ - `@mercury-fw/channel-google-chat` needs `@mercury-fw/channel-types` 0.46.0 or later.
10
+
3
11
  ## 0.6.0
4
12
 
5
13
  ### Minor Changes
package/README.md CHANGED
@@ -26,6 +26,8 @@ channels: [googleChatChannel],
26
26
 
27
27
  Each message reaches the core as `google-chat:users/<id>`, the sender's Chat user, with their email when Google vouches for it (a person's, never a bot's), which a user directory can join on: the ZITADEL directory of `@mercury-fw/plugin-zitadel` does.
28
28
 
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
+
29
31
  ## Table of contents
30
32
 
31
33
  - [Setting up the Chat app](#setting-up-the-chat-app)
@@ -55,10 +55,16 @@ export type ChatCard = {
55
55
  };
56
56
  sections: ChatCardSection[];
57
57
  };
58
- /** Posts `card` as a Cards v2 message to `space`. Returns the created message's `name`, same shape as `sendMessage`. */
58
+ /**
59
+ * Posts `card` as a Cards v2 message to `space`. Returns the created message's
60
+ * `name`, same shape as `sendMessage`. With `privateViewer` (`users/<id>`) only
61
+ * that user and the app see it, in a space anyone else is in too.
62
+ */
59
63
  export declare function sendCard(space: string, card: ChatCard, deps: {
60
64
  tokenSource: TokenSource;
61
65
  fetchFn?: FetchFn;
66
+ }, options?: {
67
+ privateViewer?: string;
62
68
  }): Promise<{
63
69
  name: string;
64
70
  }>;
@@ -171,17 +171,22 @@ export type ChatCardSection = {
171
171
  };
172
172
  export type ChatCard = { header?: { title: string }; sections: ChatCardSection[] };
173
173
 
174
- /** Posts `card` as a Cards v2 message to `space`. Returns the created message's `name`, same shape as `sendMessage`. */
174
+ /**
175
+ * Posts `card` as a Cards v2 message to `space`. Returns the created message's
176
+ * `name`, same shape as `sendMessage`. With `privateViewer` (`users/<id>`) only
177
+ * that user and the app see it, in a space anyone else is in too.
178
+ */
175
179
  export async function sendCard(
176
180
  space: string,
177
181
  card: ChatCard,
178
182
  deps: { tokenSource: TokenSource; fetchFn?: FetchFn },
183
+ options: { privateViewer?: string } = {},
179
184
  ): Promise<{ name: string }> {
180
- const data = (await callChatApi(
181
- `${space}/messages`,
182
- { method: "POST", body: { cardsV2: [{ cardId: "card", card }] } },
183
- deps,
184
- )) as { name: string };
185
+ const body = {
186
+ cardsV2: [{ cardId: "card", card }],
187
+ ...(options.privateViewer === undefined ? {} : { privateMessageViewer: { name: options.privateViewer } }),
188
+ };
189
+ const data = (await callChatApi(`${space}/messages`, { method: "POST", body }, deps)) as { name: string };
185
190
  return { name: data.name };
186
191
  }
187
192
 
@@ -32,9 +32,12 @@ import {
32
32
  import { openSubscription, type PubSubSubscription, type StreamMessage } from "./google-chat-pubsub-stream.ts";
33
33
  import { splitForSendLimit } from "./google-chat-message-buffer.ts";
34
34
  import {
35
+ detectLoginRequired,
35
36
  detectPendingConfirmation,
37
+ loginLinkText,
36
38
  PENDING_CONFIRMATION_NOTE,
37
39
  NO_REPLY,
40
+ type LoginRequest,
38
41
  type PendingConfirmation,
39
42
  type ToolOutcome,
40
43
  type Provider,
@@ -73,6 +76,13 @@ function buildConfirmCard(pending: PendingConfirmation): ChatCard {
73
76
  };
74
77
  }
75
78
 
79
+ /** 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
+ function buildLoginCard(login: LoginRequest): ChatCard {
81
+ return {
82
+ sections: [{ widgets: [{ buttonList: { buttons: [{ text: loginLinkText(login), onClick: { openLink: { url: login.url } } }] } }] }],
83
+ };
84
+ }
85
+
76
86
  const TOOL_STATUS_LINES: Record<"loading" | ToolOutcome, string> = {
77
87
  loading: "In corso…",
78
88
  success: "Fatto.",
@@ -338,8 +348,12 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
338
348
  });
339
349
  let activeSubscription: PubSubSubscription | undefined;
340
350
 
341
- /** Per-turn output sink — same responsibilities as the retired `ChatStreamer`, rebuilt against the new client. */
342
- function createSink(space: string): TurnSink {
351
+ /**
352
+ * Per-turn output sink — same responsibilities as the retired `ChatStreamer`,
353
+ * rebuilt against the new client. `viewer` is the person the turn is for;
354
+ * outside a direct message, what only they may see is sent to them alone.
355
+ */
356
+ function createSink(space: string, viewer: { user: string; isDirectMessage: boolean }): TurnSink {
343
357
  let chain: Promise<void> = Promise.resolve();
344
358
 
345
359
  function enqueue(fn: () => Promise<void>): void {
@@ -376,6 +390,9 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
376
390
  // the same turn (e.g. after a tool call) always gets its own new card,
377
391
  // never reuses this one.
378
392
  let receivingCardName: string | undefined;
393
+
394
+ // A tool asking again in the same turn hands back the same pending login.
395
+ const shownLogins = new Set<string>();
379
396
  enqueue(async () => {
380
397
  log(`[chat:${space}] [out] receiving card`);
381
398
  const sent = await sendCardFn(space, buildReceivingCard(), clientDeps);
@@ -470,6 +487,16 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
470
487
  sentMessageNames.add(sent.name);
471
488
  });
472
489
  }
490
+ const login = detectLoginRequired(step);
491
+ if (login && !shownLogins.has(login.url)) {
492
+ shownLogins.add(login.url);
493
+ enqueue(async () => {
494
+ log(`[chat:${space}] [out] login card: ${login.service}`);
495
+ const options = viewer.isDirectMessage ? undefined : { privateViewer: viewer.user };
496
+ const sent = await sendCardFn(space, buildLoginCard(login), clientDeps, options);
497
+ sentMessageNames.add(sent.name);
498
+ });
499
+ }
473
500
  },
474
501
  finalize: async (finalText: string) => {
475
502
  const trimmed = finalText.trim();
@@ -520,7 +547,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
520
547
 
521
548
  busySessions.add(sessionKey);
522
549
  try {
523
- const sink = createSink(event.space);
550
+ const sink = createSink(event.space, { user: event.sender, isDirectMessage: event.isDirectMessage });
524
551
  await handleTurn(
525
552
  {
526
553
  channel: "google-chat",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/channel-google-chat",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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.36.0 <1.0.0"
34
+ "@mercury-fw/channel-types": ">=0.46.0 <1.0.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@mercury-fw/channel-types": "0.44.1",
37
+ "@mercury-fw/channel-types": "0.46.0",
38
38
  "@mercury-fw/typescript-config": "*",
39
39
  "@types/bun": "^1.4.2",
40
40
  "typescript": "^6.0.3"